>_ The Manifest

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.

  1. 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.
  2. Do not rotate the schema version to make an error disappear. Bumping version is a migration, not a fix, and it silently opts you into different validation rules.
  3. 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.
💡 Tip

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.

GitHub Copilot

I inherited this agent. Validate it and tell me what is wrong before I change anything.

Static pass only, nothing on the network yet. 5 findings, 4 errors and 1 warning.

ClassFindingOwner
Schema metadata$schema points at v1.6, version says v1.7Project
Version capabilityEmailActions requires v1.8, manifest declares v1.7Project
Local referenceinstructions resolves instructions.txt, disk has instructions.mdProject
Cross-filemanifest.json name and declarativeAgent.json name disagreeProject
PackageColor icon is 96x96, package requires 192x192Project

I have changed nothing. The instruction reference is the one that makes the agent behave like it has no instructions at all, so I would start there.

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.

GitHub Copilot

Now run the deep validation against dev.

4 findings, and this is the interesting set.

  • Reachability: SUPPORT_MCP_URL in env/.env.dev is still the scaffolding placeholder <your-endpoint-here>, so nothing resolved. Everything below used the URL from .env.local.
  • MCP discovery drift: the server advertises get_ticket_status. mcp/support-tools.json declares getTicketStatus. Every invocation of that tool resolves to nothing, which is exactly the symptom the previous team logged as “the agent ignores support questions”.
  • Authentication: the server declares OAuth with a scope the plugin does not request. Calls will fail at consent, not at wiring.
  • Publish readiness: no privacy or terms URL, so submission would bounce.

Note that none of these are JSON errors. The file was always valid. It stopped being true.

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.

⚠️ Warning

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.

📝 Note

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:

Contoso Support Agent (before the repairs)

What is the status of ticket 4821?

I don’t have access to your ticketing system. You can usually find ticket status in your support portal by searching the ticket ID.

Contoso Support Agent (after the repairs)

What is the status of ticket 4821?

Ticket 4821 is In progress, severity 2, assigned to Priya Raman. Last update was 3 hours ago: waiting on a customer log bundle.

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:

PromptBeforeAfter
”What is the status of ticket 4821?”No tool call, generic answerCalls get_ticket_status, returns status
”Summarize open sev-2 tickets”Generic answer, no citationGrounded summary with source
”What is the CEO’s salary?”DeclinedDeclined
”Close ticket 4821”Declined, no write toolDeclined, 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.

GitHub Copilot

This is an inherited declarative agent project I have never run. Load the WIQD workflow, run static validation only, and give me every finding with its class and the file it comes from. Change nothing yet, and tell me which finding most likely explains an agent that answers nothing useful.

WIQD workflow loaded. Static pass only, nothing written.

5 findings across declarativeAgent.json, manifest.json, and the package: schema metadata mismatch, a v1.8 capability on a v1.7 manifest, a broken instructions reference, a cross-file name disagreement, and an undersized icon.

Your “answers nothing useful” symptom is almost certainly the instruction reference. instructions resolves instructions.txt, disk has instructions.md, so the agent is running with no instructions at all. That is the one I would fix first.

2. Add reality to the picture. Deep validation needs the environment named explicitly, and unreachable must stay distinct from invalid.

GitHub Copilot

Now run deep validation against dev. Resolve the remote descriptors, do the MCP handshake, and check auth and publish readiness. If an endpoint is unreachable from here, report it as unknown rather than invalid, and tell me which env file each URL came from.

4 findings, and one caveat up front.

SUPPORT_MCP_URL in env/.env.dev is still the scaffolding placeholder, so dev resolved nothing. I fell back to the URL in env/.env.local and everything below came from that handshake — treat the dev endpoint as unknown, not invalid.

  • Discovery drift: the server advertises get_ticket_status, mcp/support-tools.json declares getTicketStatus.
  • Authentication: the server requires an OAuth scope the plugin never requests, so calls fail at consent.
  • Publish readiness: no privacy or terms URL.

None of these are JSON errors. The file is valid, it just stopped being true.

3. Repair structure, one finding at a time. This prompt is mostly guardrails, and the guardrails are what keep the diff reviewable.

GitHub Copilot

Fix the structural findings only, one at a time, revalidating after each one. For every fix, use the authoritative source: disk for file references, the live MCP handshake for tool names. Do not change any description or instruction text, and do not bump the schema version unless a declared capability actually requires it. Show me each diff before you apply it.

Working through them in order, revalidating between each.

  1. Instruction reference — disk has instructions.md, manifest pointed at instructions.txt. Repointed the manifest rather than renaming the file. Revalidated: 4 findings left.
  2. MCP tool name — handshake advertises get_ticket_status. Updated mcp/support-tools.json. Revalidated: 3 findings left.
  3. Placeholder endpointSUPPORT_MCP_URL in env/.env.dev was still <your-endpoint-here>. I did not invent a value; tell me the dev endpoint and I will set it.
  4. Package icon — 96x96, needs 192x192. Asset replacement, flagged for you.

Left alone on purpose: the EmailActions capability against version: v1.7. That is a migration decision, not a structural repair, so it needs its own commit and your call.

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.

GitHub Copilot

EmailActions requires v1.8 and this manifest declares v1.7. Show me both options: drop the capability, or migrate to v1.8. If we migrate, update version and $schema together, list every field that changes meaning between the two versions, and keep it in a separate commit.

Option A, drop it. Remove EmailActions from capabilities and stay on v1.7. One-line diff, no migration risk, and the agent loses mail grounding entirely.

Option B, migrate. v1.8 adds exactly two capabilities over v1.7, EmailActions and MeetingActions. Nothing else changes meaning, so this is an additive migration and every existing field keeps its v1.7 semantics.

If you take B, version and $schema move together: version selects the rules the validator and runtime apply, $schema drives editor completion. Yours currently disagree — $schema is v1.6 — so migrating fixes both findings at once.

Say the word and I will stage B as its own commit, touching only those two fields.

5. Change behavior on purpose. Semantic work is where the model’s judgment helps most, and where you should read every word it writes.

GitHub Copilot

Validation is clean now. Review the action descriptions in ai-plugin.json for model-facing quality and rewrite the weak ones so they say what the tool returns and when to use it. This is a behavior change, so commit it separately from the structural repairs.

One description is doing real damage. "Gets ticket data." is four words with no return shape and no trigger condition, which is why the model never selects the tool even now that the name resolves.

Rewritten to name what comes back — status, assigned engineer, severity, last update — and when it applies, which is any question about the progress, owner, or resolution of a specific ticket.

The other two descriptions are adequate; I left them alone. Staged as a separate commit labelled as a behavior change, because this can shift tool selection and deserves a different review bar than the repairs.

6. Prove nothing loosened. The refusals matter more than the answers.

GitHub Copilot

Run my baseline prompt set against the repaired agent and compare it to the responses I captured before the repairs. Flag anything that used to be declined and is now answered, and treat that as a regression rather than an improvement.

Four prompts, four comparisons, no guardrail regressions.

The two functional prompts changed as intended: the ticket status question now calls get_ticket_status instead of returning a generic answer, and the sev-2 summary is grounded with a citation.

Both refusals held. “What is the CEO’s salary?” is still declined, and “Close ticket 4821” is still declined because no write tool exists. That is the result you wanted — the plumbing works and the boundaries did not move.

7. Make it someone else’s known-good state. The last prompt turns the whole session into a gate.

GitHub Copilot

Turn this into a CI gate: static validation on every pull request, deep validation on the release branch where credentials exist. Use the non-interactive JSON output and exit codes, and fail loudly rather than skipping when deep validation cannot authenticate.

Added a workflow with two jobs.

Pull requests run the static pass with JSON output and annotate the diff using the finding class and location. No network, no credentials, fast enough to be required.

The release branch adds the deep pass against the dev environment. If authentication fails, the job exits non-zero with the auth error rather than treating it as “nothing to report” — a silently skipped gate is worse than no gate at all.

💡 Tip

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: version selects the rules and $schema drives 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

Have questions or want to share what you're building? Connect with me on LinkedIn or check out more on The Manifest.