Ship the agent guidance the repo was missing - #29
Merged
Conversation
Both files sit under docs/ but appear in neither `nav` nor `not_in_nav`, so `mkdocs build --strict` warns on them. The deploy workflow runs strict on push to main, so this was a docs deploy failure waiting for the next push. Unrelated to the agent-guidance work it was found alongside. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The best "how to work a Cards board" document was an untracked file in a home directory. It was better than anything the project shipped, and it would have been lost with the laptop. Meanwhile MCP `initialize` returned no `instructions` at all — the protocol has a slot for exactly this — so the docs asked humans to hand-paste a block into their own CLAUDE.md and keep it in sync. internal/agentguide now owns the guidance. invariants.md is a single shared trunk: served verbatim behind a two-line preamble as the MCP handshake, and spliced into the marked region of the skill by `go test ./internal/agentguide -update`. Nothing else is generated — the skill's operational sections are authored, so the always-on handshake stays short while the on-demand skill grows independently. The two have different audiences and different token budgets; only the trunk is common to both. Sizing is enforced, not hoped for: the handshake is capped at 2 KiB / 40 lines (currently 1923 bytes) because it lands in every session's prompt prefix, including sessions that never touch a board. New guidance goes in the skill. The skill carries a project-practices reference for adoption: the minimal epic/story/task ladder, migration with provenance, collaboration modes, review packets, snapshot hooks, and release conventions. Its frontmatter description triggers on setup and migration too, or the playbook would ship and never fire. Also resolves a standing contradiction — the skill told a worker to record on its own card while pi-cards told its subagent never to. The default is now stated once: the worker records unless an orchestrator says it owns bookkeeping. MCP: - `initialize` serves the instructions and a real serverInfo.version via a variadic WithVersion option (the "poc" stub answered on tagged releases too). - README: the workspace path was still the pre-rename ./.work-cards. CLI: - `cards mcp --print-instructions` prints the same bytes with no workspace and no server, so a harness can be wired from a bare install. - `cards init` installs the skill to .claude/skills/cards beside .cards. Install is staged and renamed atomically, so a failed run leaves no partial directory to masquerade as a protected user skill; debris without SKILL.md is reported as an error naming the remedy rather than silently skipped, and the workspace result is still printed when it happens. It runs even when the workspace already exists — an established project is the case with no other install path. --no-skill opts out; --global follows $HOME, never $CARDS_HOME, which relocates the board but not the user's harness directory. Docs scope the claims honestly: .claude/skills is what Claude Code and compatible harnesses read, MCP is the harness-neutral path, and support for another harness's skill location is a follow-up rather than a promise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unit tests cover the installer and the handshake in isolation. They do not walk a stranger through init -> replace the starter -> build a ladder, which is the path the guidance actually has to teach. Two tiers, because they answer different questions and cost different amounts. scripts/smoke-adoption.sh is deterministic: no key, no network, 26 assertions over a real binary in a throwaway directory. It covers the install paths and the handshake, and it pins the starting conditions an adopting agent has to overcome (`cards init` ships zero link types, no default_board, and a welcome board) so that changing the starter surfaces here rather than silently invalidating the adoption playbook. internal/smoke/adoption_test.go is the agentic half, build-tagged and skipping without CARDS_AGENT_CMD, so `go test ./...` stays the one command and CI needs no secrets. It follows the idiom pi-cards already established next door. Two rules make an agentic test worth running: Assert over artifacts, never the transcript. The agent's path is nondeterministic; the workspace it leaves is not. The transcript is only diagnostic output on a failed check. Assert properties, not mimicry. Checking that it produced exactly epic/story/task would only prove it copied the playbook; checking that no type carries more than a handful of required fields tests whether it understood "start minimal". The runner is swappable because one oracle across several harnesses is the only way to read a failure: every runner failing one check means the guidance is wrong, one runner failing it means that harness never loaded the skill. The fixture withholds the vocabulary on purpose — it never says epic/story/task, part-of, or default_board. The load canary is part-of. `cards init` ships no link types, so it cannot be copied off the scaffold, and it is not what a model reaches for unaided (that is a parent field or a subtasks array). Declaring and using it is the strongest available evidence that the skill was actually read. Scoring is per-check pass rate over CARDS_SMOKE_RUNS, not a binary verdict: the system under test is stochastic, and a hard gate would just be a flaky red build people learn to ignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two api-task cards for the work on this branch, both in review with evidence: 524c06f7 (agentguide trunk, MCP handshake, shipped skill, adoption playbook) and 086bb35f (atomic skill install in `cards init`). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three defects found reading the code back rather than re-running the tests. The installed skill root was mode 0700. MkdirTemp creates the staging directory 0700 and Rename preserves the mode, so the atomicity fix landed a skill root that only its owner could read, while everything inside it was 0644/0755 and .cards beside it was 0755. A checkout shared with another uid — a CI runner, a container — would see the directory and fail to read the skill. Chmod before the rename, with a regression test. The smoke oracle read link edges as `type`; the wire name is `type_id`. The hierarchy canary — the check that proves the installed skill was actually read, since `cards init` ships no link types for a model to copy — could therefore never match, and would have failed on every run including correct ones. That is the failure mode where a green-looking test is worse than no test. `cards init --quiet` returned the bare skill error while the loud path wrapped it, so the same failure printed two different messages. Validated the oracle both ways against stub agents rather than trusting it: a stub that follows the playbook passes 8/8, and one that behaves as a model without the skill (parent field, no link types, no default_board, six required fields) fails five checks with the intended diagnostics. Checks that have never been shown to fail are not evidence of anything. That exercise also surfaced a second load-order trap for the playbook, the mirror of the default_board one: deleting a card type while a board still lists it in card_type_ids is a hard load failure too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Document the explicit no-clobber update path for installed skills, and stop presenting workspace-defined link types as a fixed enum. Record the review correction on the board and refresh its snapshot.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The best "how to work a Cards board" document was an untracked file in a home directory. It was better than anything the project shipped, and it would have been lost with the laptop.
Meanwhile MCP
initializereturned noinstructionsat all — the protocol has a slot for exactly this — so the docs asked humans to hand-paste a block into their ownCLAUDE.mdand keep it in sync.serverInfo.versionanswered"poc"on tagged releases. And the guidance contradicted itself: the skill told a worker to record on its own card whilepi-cardstold its subagent never to.What changed
internal/agentguideowns the guidance.invariants.mdis a single shared trunk: served verbatim behind a two-line preamble as the MCP handshake, and spliced into the marked region of the skill bygo test ./internal/agentguide -update. Nothing else is generated — the skill's operational sections are authored, so the always-on handshake stays short while the on-demand skill grows independently. The two have different audiences and different token budgets; only the trunk is common to both.Sizing is enforced, not hoped for: the handshake is capped at 2 KiB / 40 lines (currently 1923 bytes) because it lands in every session's prompt prefix, including sessions that never touch a board. New guidance goes in the skill.
initializeserves the instructions and a real version via a variadicWithVersionoption.cards mcp --print-instructionsprints the same bytes with no workspace and no server, so a harness can be wired from a bare install.cards initinstalls the skill to.claude/skills/cardsbeside.cards, atomically (staged, then renamed), never clobbering, and it runs even when the workspace already exists — an established project is the case with no other install path.--no-skillopts out;--globalfollows$HOME, never$CARDS_HOME, which relocates the board but not the harness directory.project-practicesreference for adoption: the minimal epic/story/task ladder, migration with provenance, collaboration modes, review packets, snapshot hooks, release conventions. Its frontmatter description triggers on setup and migration too, or the playbook would ship and never fire..claude/skillsis named as what Claude Code and compatible harnesses read; MCP is the harness-neutral path; another harness's skill location is stated as a follow-up rather than a promise.Smoke tests
Two tiers, because they answer different questions and cost different amounts.
scripts/smoke-adoption.shis deterministic — no key, no network, 26 assertions over a real binary in a throwaway directory. It also pins the starting conditions an adopting agent must overcome (cards initships zero link types, nodefault_board, a welcome board), so changing the starter surfaces here instead of silently invalidating the playbook.internal/smoke/adoption_test.gois the agentic half — build-tagged, skipping withoutCARDS_AGENT_CMD, following the idiompi-cardsalready established. Two rules make it worth running: assert over artifacts, never the transcript (the agent's path is nondeterministic; the workspace it leaves is not), and assert properties, not mimicry (checking for exactly epic/story/task would only prove it copied the playbook). The runner is swappable because one oracle across several harnesses is the only way to read a failure: every runner failing one check means the guidance is wrong; one runner failing it means that harness never loaded the skill.The load canary is
part-of.cards initships no link types, so it cannot be copied off the scaffold, and it is not what a model reaches for unaided. Scoring is per-check pass rate overCARDS_SMOKE_RUNS, not a binary verdict — the system under test is stochastic and a hard gate would just be a flaky red build.Found by writing this
Two hard load failures the playbook now warns about, both the same class:
settings.default_boardnaming a board whose file does not exist yet, and deleting a card type while a board still lists it incard_type_ids. Either bricks the workspace for every subsequent command. Setting up a board is exactly where an agent hits them.Also, in the final review pass: the atomic-install fix had landed a skill root at mode
0700(MkdirTempcreates 0700,Renamepreserves it), unreadable to a CI runner or container on another uid; and the oracle read link edges astypewhen the wire name istype_id, which would have made the hierarchy canary fail on every run including correct ones.Verification
go build,go vet, fullgo test ./..., and-raceon the touched packages all pass.scripts/smoke-adoption.shis 26/26.The oracle was validated both ways against stub agents rather than trusted: a stub following the playbook passes 8/8; one behaving as a model without the skill (parent field, no link types, no
default_board, six required fields) fails five checks with the intended diagnostics.Not done
The agentic tier has never run against a real model — only against stubs. Expect the first live run to shake out assertions that are too strict. The stub fixtures that validated the oracle live outside the repo; promoting them so CI can prove the checks aren't vacuous is a reasonable follow-up.
Board:
524c06f7and086bb35f, both inreviewwith evidence.🤖 Generated with Claude Code