I have been a software developer, architect, and technical leader for my entire career. But I can honestly say that I have never been more excited about designing, building and deploying software systems than I am right now. You see, I have never really been a fan of the minutiae of writing code. The semicolons; the curly braces; finding which obscure SDK function does the one thing I’m trying to do. Ugh. I just love building things that solve real problems.
The advent of AI-assisted coding tools has triggered a re-awakening of my love for software development. Whether it be VSCode/Copilot, Windsurf (my personal favorite), Cursor, Claude Code or any of the myriad of solutions available in the marketplace today, they all simply make writing code more fun.
While these tools have been available in some form for a couple of years now, I personally saw real productivity growth only when I started to employ some of the advanced techniques and standards that I will share with you in this article. Before I begin, I want to clarify my stance on some new terminology that has entered our vernacular in the last year — Vibe Coding. Here is I how define and separate the two forms of work:
- Vibe Coding: The act of producing a functional prototype for an envisioned software system. May be executed by almost anyone: from expert software developers all the way down to those with little-to-no development experience. Much of the code is not reviewed or even understood by the developer. The system is likely produced with less attention to things like security, scalability, maintainability, cost-efficiency, etc.
- AI-Assisted Software Development: The act of building software by a trained software engineer/architect with the assistance of AI agents. While a majority of the code is produced by the agent, the engineer was directly responsible for the design, review, and acceptance of all underlying. Explicit attention is given to coding and security best-practices. Scalability (based on the desired audience/scale for the system) and maintainability are a high priority when directing the agent in its execution of engineering tasks.
I have definitely done my share of the former while learning about these systems and playing around with interesting ideas, but in my day-job (and the subject of this article), I mostly focus on the latter.
I have mostly used Windsurf and VSCode/Copilot, so my examples in the following sections will be focused on these two IDEs. However, most of the features that I will describe in this article are available in many different IDEs, but possibly using different names.
Level 1: Use the tools!
You’ll hear many people out there talking about how the position of software engineering is changing. I disagree…
It has changed.
If you are writing software today without the basic assistance of AI, you are simply not as valuable to companies that might be hiring (and definitely not as productive) as those who are using the tools. At a bare minimum, you are behind the curve if you are not using these IDEs for:
- Tab Completions — AI is constantly watching what you type and is making predictions/recommendations on what you should type next. Small, repetitive edits area a great candidate for completions.
- AI Chat Window (Agent, Ask/Chat) — This is the new feature that these IDEs are providing. You can select a LLM to use and then either chat with it (similar to ChatGPT) or allow it it use its tools to edit your code and interact with your local environment.
- Terminal/Tool Use — The agent can execute commands in your terminal to perform various actions on your local system. Other tools are also available (e.g. tools from MCP servers), but I consider that more of a Level 2 skill.
I also want to mention one important feature of the VSCode-based IDEs (this includes Windsurf and Cursor, which are forks of the VSCode open source project. Other IDEs may support a similar concept). When you open your project in VSCode, you typically open a single folder. But you do not have to limit your agent’s access to only a single folder/repository. You can create a “multi-root workspace” in VSCode by right-clicking in the empty space in the file explorer and adding/removing folders from your workspace. Don’t forget to save your workspace file so you can come back to this multi-folder project later.
Level 2: Persistent Agent Context
Reaching this next level requires a litte more understanding of LLMs, Agents and their strengths/limitations.
LLMs are stateless. LLMs were (likely) never trained on your code, your previous chat conversations, or even the last response that it gave you. Every time you execute a call to a LLM (“inference”), you must pass in EVERYTHING that it needs to understand the current situation — this is called “context”. All LLMs have limits on the amount of context that they can accept. All of these items would be considered part of context:
- Static prompts, rules, instructions
- Memories of past conversations
- Documentation/code fragments that have been specifically retrieved as relevant to the current conversation
- Tool Descriptions. Every tool that you want to LLM to have at its disposal must be described to the LLM in detail so that it recognizes when and how to request execution of those tools.
- Conversation history. This includes all user messages, agent messages, tool use requests and responses that are part of the current conversation. When conversations become too long for the model context window, most IDEs will replace parts of the conversation history with LLM-generated summaries. You lose some detail here, so be mindful of your conversation length.
All of these IDEs have smart ways of allowing the agent to search through your project directories to find relevant code/documentation files. But these searches are limited in sophistication. The trick is to capture the knowledge of you and your team (in natural language) into persistent context assets that can be automatically included by the agent or manually requested by the user. Markdown has become the universal format of these persistent context assets. This is due to the fact that it provides some human-readability, but also because LLMs have been trained on lots of markdown from the internet so it is readily understood by models.
The basic idea is that you are capturing permanent aspects of your project, team proceses, and desired agent behaviors as instructions to the agent. These are persisted with the source code for your project and therefore benefit all who clone your repo and participate in development. Think of it as a mechanism for providing each developer (including you!) with an agent specifically trained in the ways of your project. You build natural consistency in the artifacts committed by each developer by laying out all the rules ahead of time and making them available to every agent that will operation on your codebase
Rules/Instructions
These files contain information about your project that would be generally valuable to most agent interactions. Examples include:
- Directory structure
- Tech stack
- Testing conventions
- Documentation reference
- Agent behavioral constraints
- Code generation guidelines (naming conventions, etc)
## Code Style
- Use TypeScript strict mode
- Prefer functional components
- Use async/await over .then()
## Architecture
- Services in /src/services/
- Follow repository pattern
- No direct database calls from handlers
## Testing
- Jest for unit tests
- 80% coverage minimum
Everytime you run your agent and you find yourself entering something like “ALWAYS…” or “You MUST….”, think about putting that into a rule or instruction. Most IDEs support some form of targeting for these instructions. For example, you can provide a file Glob pattern that indicates that the instruction should only be included in the context when working with particular file types or paths.
GitHub Copilot calls these Instructions. Windsurf calls them Rules.
AGENTS.md
Another form of rule/instructions that most IDEs support is AGENTS.md. You would put the same type of information into this file, but the method for automatic inclusion is slightly different. AGENTS.md files are included based on their location in the project directory structure. Consider each AGENTS.md file as including a ./** pattern of inclusion — a particular AGENTS.md file in a specific project directory is included in the context when operating on files in that directory and any subdirectories.
It’s important to note that AGENTS.md and IDE-specific rules files have a bit of overlap in the purposes they can serve. It really doesn’t matter which ones you choose as long as you don’t duplicate your context information — just use whichever file best serves the file/directory-scoping needs of your project.
Prompts/Workflows
These types of context are typically included on-demand, by request of the user. IDEs like VSCode and Windsurf support these through “slash” commands in the chat entry field. You create a prompt/workflow, give it a name, and the IDE will make it available in the auto-complete list when / is typed.
I like to use the “workflow” term when thinking about these types of permanent context assets. I typically document specific procedures that I want the agent to follow at certain times. For example, for running tests and troubleshooting them in a particular project of mine, I have found a set of instructions that allows the agent to perform the task with relative autonomy. Document the terminal commands/scripts that should be run along with other small details to guide the agent through multi-step processes and you have now automated a particular time-consuming task.
In Windsurf, these are called Workflows. Copilot has Prompts.
Agent Skills
When a basic text file with natural language instructions is not enough to guide the agent through a complex task, a new standard called Skills has emerged. With skills, you package a typical prompt/instruction markdown file (SKILL.md) along with supporting materials that could include additional reference documentation, scripts, or other static assets.
As an example, I have built a couple of skills that allow agents to securely interact with deployed Postgres and Redis instances that the system depends on. When I ask my agent to troubleshoot an issue I am seeing with my deployed system, the agent can perform queries against these dependency apps to pull in valuable information to aid in solving the problem. It is considered good practice to NEVER send cleartext secret values in agent interactions — including passwords, API tokens, encryption keys and others. Since all of the developers on my team have command-line access to our enterprise Vault system, I wrote scripts that the agent can use to securely retrieve passwords from Vault and use them (hidden in environment variables) to authenticate to the remote Postgres/Redis deployments. I package these scripts along with the accompanying instructions for how to access our various deployment environments (dev/stage/prod) in an Agent Skill which can be part of a project repo or committed to a separate “utility” repo that all developers use.
Copilot adds skills automatically based on your SKILL.md name and description. Windsurf can do the same, or you can manually insert the skill reference into your chat using the @ symbol.
Model Context Protocol (MCP)
MCP is an emerging standard from Anthropic (recently donated to the Linux Foundation). I could devote an entire article to MCP, so I’m going to give it an extremely lightweight treatment in this article.
Basically, MCP is wireline protocol for standardizing how agents can utilize externally-hosted tools (APIs/functions), prompts, and other resources.
Most IDEs allow you to configure MCP servers and make their various forms of context availble to the coding agent. One publicly-available server I highly recommend is Context7. But there are numerous opensource servers out there and you can even consider building your own servers to access your personal or company-proprietary tools to make your coding agents even more powerful.
Level 3: Spec-Driven Development
The last level I’ll discuss in this article involves the fact that you will eventually run into a “complexity” wall with AI-assisted coding. Imagine you are implementing a large new feature in your complex software system. This new feature may require thousands of lines of new implementation and test code across new or existing services/components. Can AI help you write something so complicated? Yes. Yes it can.
Here’s what you don’t want to do… start typing in the agent chat window. If your request is really that complicated, how can you possibly expect to convey all of the various requirements and design constraints of your new feature in a tiny little chat pane? You just can’t. Sure, your agent knows all about your existing code, but you’re going to need some extensive documentation to feed into the agent so that it can implement things just the way you want (and the way you yourself would do it)!
Introducing Spec-Driven Development. In this process, you design your system in cooperaton with the coding agent before it writes a single line of code. The outputs of this process is a set of requirements, specifications, design documents, and implementation steps that give your agent a much higher probability of creating the thing that you actually have in your head.
There are several tools and methodologies that have come out over the last year or so, but I’m going to talk about the one that I’ve taken a liking to: OpenSpec. The system consists of a collection of workflows/prompts for any IDE that guides your agent through the steps of the process. It also defines some constraints for the documents generated by the process and a command-line utility to help bring it all together.
The workflow and repo files look something like this:
Before you can run the workflow, you must install the openspec command line utility and run openspec init from the root of your project folder. openspec init will ask you for information about which IDE you are using so it can install its skills in the IDE-specific location within your project. The openspec init command also generates the following primary agent skills:
openspec-new-change: Scaffolds a new change proposalopenspec-continue-change: Proceed to generate the next change artifact. Useful for more complex changes that require careful review at every stage to avoid having the agent take an incorrect approachopenspec-ff-change: Proceed to generate all remaining change artifacts. Useful for simpler changes that require less review of every artifactopenspec-apply-change: Implement/execute the proposed changeopenspec-verify-change: Instructs the agent to compare the implementation against the proposed change to verify alignmentopenspec-archive-change: Merge delta specs into the top-level source-of-truth specs for the project after implementation. Also archives the proposal itself.openspec-explore: While not an actual part of the openspec process, this is a very helpful skill for learning about the code and possible implications of any proposed change.
There are a couple of supplementary skills that accompany these core skills, but I’ll leave that as an exercise the reader to explore their usefulness.
openspec init creates this top-level directory in your project:
openspec/
changes/
specs/
.openspec.yaml
changes/ contains all outstanding proposals (not yet implemented. When proposals are completed and archived, they go in the changes/archive/ folder. The specs/ folder contains the source-of-truth specifications for your project — a set of files that have all project requirements. When creating new proposals, “delta” specs will be created that indicate added/modified/removed requirements that will be merged into the top-level specs upon archiving the proposal.
Proposals
After you have initialized your project, it is time to generate a “proposal”. A proposal is basically a directory (under changes/) containing documentation about a modification that you want to make to your system. Run the either the continue or the ff prompt and give the agent as much information as you can about the thing you want to build. Don’t worry about catching every little detail in this first prompt. The agent will build the first versions of the following files using information from project.md and by scanning your repository to look for various code touch-points:
proposal-name/
specs/
spec1.md
spec2.md
proposal.md
design.md (optional)
tasks.md
proposal.md: This file contains a slightly more detailed write-up of your proposed change.specs/: This folder contains requirements documents for each major area of functionality covered by your proposal. As I’ve previously mentioned, specs are the one file that live outside of the proposal process. The proposal process will consider how your changes affect existing specifications (adding/removing/modifying existing requirements) and how they introduce entirely new categories of requirements (new spec documents). After you complete the implementation of a proposal, the specs from the proposal are merged/removed/added to your top-level requirements.design.md: This is a detailed design document that describes all the various aspects of your change, how they impact your codebase, design decisions and tradeoffs, etc.tasks.md: A step-by-step procedure for implementing the proposal. The agent will follow this procedure when “applying” the proposal.
Now, you have a starting design! It might be very good, it might be very bad. It is your responsibility to review ALL of the proposal artifacts and work with your agent to modify the proposal to your exact liking. The end result should be a task list that closely mirrors how you would have implemented this change yourself.
Implementation
Once you are happy with the proposal, you run the openspec-apply-change skill and reference your proposal name. This will instruct the agent to begin following the steps in the proposal task list, consulting the design and requirements documents along the way, when necessary. I have built some extremely large proposals with well over 200 individual coding, testing, migration tasks. In this case, the task list is typically broken up into sections (1.1, 1.2, 2.1, 2.2, etc). For larger proposals, I will typically work with the agent to implement one section at a time according to my way of working/testing. This allows me plenty of room to correct the agent’s trajectory or even modify the proposal and task list as I discover things that I hadn’t previously though of. I usually commit to my repo after each major task list section is complete.
Archive
When the proposal is fully implemented and you are happy with it (typically, as part of the branch that will be merged to your main branch), you “archive” the proposal. The archive process is quite simple:
- Move your proposal folder to the openspec/changes/archive/ directory.
- Merge your proposal specs with the main project specs.
That’s it. A much more sophisticated way to do large, complicated projects using your new AI-assisted coding skills!
These levels I’ve described above are, by no means, the end of advancement as a modern-day software engineer. You’ll find people that are running several agents in parallel (but I think there is a limit to the amount of context-switching that an engineer can do and still properly review the outputs produced by the coding agent. You’ll also find numerous specialized features provided by each IDE that tackles a different aspect of the AI-assisted engineering.
I encourage all of you to ascend these lavels and beyond to reach levels of productivity (and recognition) that you’ve never seen before. Good luck, and happy coding!
