A passing JSON parse does not mean your declarative agent works. It means your braces match. The agent you inherited last week parses perfectly and answers roughly nothing, so the job is not “read the code”, it is “find out how much of what this project claims is still true”. Work IQ Developer Tools validates in two passes with two very different definitions of correct.
The Project You Just Inherited
Here is the repository, annotated with what turns out to be wrong. You do not know any of the right column yet.
appPackage/
declarativeAgent.json # version: v1.7, $schema: v1.6, declares a v1.8 capability
manifest.json # app name disagrees with the agent name
instructions.md # the manifest points at instructions.txt
ai-plugin.json # action description reads "Gets ticket data."
mcp/support-tools.json # declares getTicketStatus; the server renamed it
env/
.env.dev # SUPPORT_MCP_URL=<your-endpoint-here>
Before touching a single file, three rules. They sound like process, and they are the difference between a repair and an incident.
- Preserve the original. Branch, commit the untouched state, and keep every repair as its own diff. If the agent behaves differently on Friday, you want to know which of five fixes did it.
- Do not rotate the schema version to make an error disappear. Bumping
versionis a migration, not a fix, and it silently opts you into different validation rules. - Separate structural repairs from behavior changes. Fixing a broken file path restores intent. Rewriting a description changes what the model does. Never ship them in the same commit.
Run environment diagnostics first, so you can tell workstation failures from project failures. A missing Agents Toolkit or an unauthenticated CLI produces errors that look exactly like a broken project, and you will spend an hour blaming code that is fine.
Static Validation Checks the Project Against Itself
Static validation reads the project on disk and nothing else. No network, no tenant, no credentials. It answers exactly one question, whether the project describes itself consistently.
Five classes, every one of them inside the repository. That is the useful property of a static pass: deterministic, offline, and never blaming the network for something you can fix on your laptop.
Deep Validation Checks the Project Against Reality
Deep validation resolves the project against the world. It fetches remote descriptors, performs an MCP handshake, checks that authentication metadata is coherent, and evaluates publish readiness. It needs network, sometimes credentials, and it finds the failures a schema check never sees.
That distinction is the whole argument. Static validation protects you from yourself. Deep validation protects you from everyone else: the team that renamed a tool, the identity owner who tightened a scope, the colleague who never filled in a placeholder.
Deep validation fails differently when it cannot reach a private MCP endpoint. “Unreachable” is not “invalid”. Treat a network-blocked finding as unknown, re-run it from a host that can see the endpoint, and never repair a manifest based on a connection timeout.
Repair One Finding at a Time
The temptation with nine findings is to fix nine things and validate once. Do not. The repair loop is one finding at a time: identify the authoritative source, make the narrowest possible edit, revalidate.
Finding 1, the missing instruction reference. The authoritative source is disk, and disk says instructions.md. The narrow fix is the manifest pointer, not renaming a file that git history may reference:
{
"instructions": "$[file('instructions.txt')]",
"instructions": "$[file('instructions.md')]"
}
Revalidate. Four findings left.
Finding 2, the capability that outruns the version. This one is a migration, and it is the only place where changing version is correct. The manifest declares EmailActions, which the v1.8 manifest schema introduced and v1.7 does not know about. You have two honest options: drop the capability, or migrate the manifest. If the capability is load-bearing, migrate both fields together:
{
"$schema": "https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.8/schema.json",
"version": "v1.8"
}
Those two fields do different jobs, and conflating them is the most common confusion I see on inherited projects. version selects the validation rules that the validator and the runtime apply. $schema drives editor completion and hover in VS Code. A project with version: v1.7 and a v1.6 $schema validates against v1.7 while your editor quietly offers a v1.6 vocabulary, which is how a wrong field gets typed with full autocomplete confidence.
Finding 3, the renamed MCP tool. The authoritative source here is never the docs and never the old manifest. It is the live handshake. Re-sync from discovery, take the exact name the server advertises, and revalidate:
{
"name": "getTicketStatus",
"name": "get_ticket_status"
}
Findings 4 and 5, the placeholder URL and the package icon, are mechanical. Fill in the environment variable, replace the icon, revalidate after each.
Sometimes a finding is intentional. An internal agent may deliberately have no public terms URL. Document that decision in the repository next to the suppression rather than in a pull request comment nobody will find in March, and re-review it before publishing externally.
Structural Correctness Is Not Semantic Quality
At this point validation is clean, and the agent is still mediocre. Clean means everything declared exists and resolves, not that the model will make good decisions.
The ai-plugin.json action description still reads "Gets ticket data." Four words, nothing about when the tool applies. A schema validator has no opinion about that sentence. The model has a very strong one: it will not call the tool.
{
"description_for_model": "Gets ticket data.",
"description_for_model": "Retrieves the current status, assigned engineer, severity, and last update for a customer support ticket by ticket ID. Use when a user asks about the progress, owner, or resolution of a specific ticket."
}
This is a behavior change, so it gets its own commit and its own justification. Structural repairs restore what the project meant. Semantic repairs change what the agent does. Keep them apart and your bisect takes a minute when tool selection shifts next week.
Prove You Did Not Change Behavior
Repairing five things and declaring victory is how you find out in production that the agent now answers questions it used to decline. You need a before and after, even a small one.
Run a handful of prompts before the repairs, capture the responses, and run the same set afterward. Here is the same question, asked of the same agent, on either side of the tool rename:
The instructions never changed between those two answers. The second one happened because the tool name in the manifest finally matched the one on the server. The full regression set:
| Prompt | Before | After |
|---|---|---|
| ”What is the status of ticket 4821?” | No tool call, generic answer | Calls get_ticket_status, returns status |
| ”Summarize open sev-2 tickets” | Generic answer, no citation | Grounded summary with source |
| ”What is the CEO’s salary?” | Declined | Declined |
| ”Close ticket 4821” | Declined, no write tool | Declined, no write tool |
The two “declined” rows are the point: they prove you fixed plumbing without loosening guardrails. If a refusal turns into an answer, you did not repair the agent, you changed it. The same logic scales into a generated eval suite, which is where agents without evals are just vibes picks up the thread.
Turn Clean Validation Into a Gate
The last step is making sure nobody inherits this project in the same condition you did. Every workflow you just ran by hand has a non-interactive equivalent with machine-readable output and stable exit codes:
wiqd agent validate --json --deep || exit 1
It exits non-zero on findings, so it drops into a pull request check unchanged, and the JSON body carries the finding class and location to annotate the diff with. Split the passes: static validation on every pull request, deep validation on a schedule or on the release branch where credentials exist. When deep validation cannot authenticate in CI, fail the job loudly, because a silently skipped gate is worse than no gate.
Do the Whole Migration in Natural Language
Everything above is a workflow, not a command list, and that is exactly why the best way to run it is a conversation in GitHub Copilot CLI with the WIQD plugin loaded. The plugin carries the lifecycle knowledge, so you describe the outcome and the constraints, and the tooling picks the passes, the order, and the exact edits. You are not memorizing flags, you are stating intent.
The trick is to keep one prompt per phase, because each phase has a different review bar. Here is the series I use on an inherited project, start to finish.
1. Triage before touching anything. The constraint is the point: no edits, just a findings list you can reason about.
2. Add reality to the picture. Deep validation needs the environment named explicitly, and unreachable must stay distinct from invalid.
3. Repair structure, one finding at a time. This prompt is mostly guardrails, and the guardrails are what keep the diff reviewable.
Notice what the constraints bought. It stopped at the placeholder instead of guessing a URL, and it refused to fold a version migration into a repair commit. Those are not lucky outputs, they are the direct consequence of saying “one at a time”, “authoritative source”, and “do not change descriptions” in the prompt. Vague prompts get you a heroic single commit that fixes nine things and explains none of them.
4. Migrate the manifest version deliberately. A version bump is its own decision, so it gets its own prompt and its own commit.
5. Change behavior on purpose. Semantic work is where the model’s judgment helps most, and where you should read every word it writes.
6. Prove nothing loosened. The refusals matter more than the answers.
7. Make it someone else’s known-good state. The last prompt turns the whole session into a gate.
Keep the prompt series in the repository, as a MIGRATION.md or a set of saved prompts. The next person to inherit a project gets your workflow, not just your result, and the prompts double as documentation of why each phase is separate.
The Value You Just Unlocked
- A triage vocabulary: Findings sort into schema, required field, local reference, cross-file, remote reachability, discovery drift, authentication, and publish readiness, each with an obvious owner.
- Two passes with two jobs: Static validation says the project is internally consistent, deep validation says it is still true about the outside world.
- Version-aware repairs:
versionselects the rules and$schemadrives editor completion, so a v1.7 project stops being validated with v1.8 assumptions. - Narrow, reversible diffs: One finding, one authoritative source, one edit, one revalidation, one commit.
- Structural and semantic work kept apart: Restoring intent and changing behavior land in different commits with different review bars.
- A reproducible baseline: Clean validation becomes an exit-code gate, so the next person to inherit the project inherits a known-good state.
- A repeatable prompt series: The whole migration runs as natural language in GitHub Copilot CLI, one prompt per phase, with the constraints that keep each diff reviewable written into the prompt itself.
Inheriting an agent stops being archaeology once the tooling can tell you which claims in the repository are still backed by reality. You are not reading a manifest and hoping. You are reading a findings list and closing it.
Resources
- Work IQ Developer Tools documentation
- Work IQ Developer Tools on GitHub
- Declarative agent manifest schema 1.8
- Declarative agent manifest schema 1.7
- Build MCP plugins for declarative agents
- GitHub Copilot CLI
- Microsoft 365 Agents Toolkit CLI
- Microsoft 365 Agents Toolkit overview
- Debugging declarative agents in Visual Studio Code
- Debugging declarative agent failures
- The declarative agent lifecycle, end to end with WIQD
Have questions or want to share what you're building? Connect with me on LinkedIn or check out more on The Manifest.