Skip to content

Ship the agent guidance the repo was missing - #29

Merged
somebox merged 6 commits into
mainfrom
feat/agent-guidance
Aug 30, 2026
Merged

Ship the agent guidance the repo was missing#29
somebox merged 6 commits into
mainfrom
feat/agent-guidance

Conversation

@somebox

@somebox somebox commented Aug 29, 2026

Copy link
Copy Markdown
Owner

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 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. serverInfo.version answered "poc" on tagged releases. And the guidance contradicted itself: the skill told a worker to record on its own card while pi-cards told its subagent never to.

What changed

internal/agentguide 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.

  • MCPinitialize serves the instructions and a real version via a variadic WithVersion option.
  • CLIcards 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, 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-skill opts out; --global follows $HOME, never $CARDS_HOME, which relocates the board but not the harness directory.
  • Skill — carries a project-practices reference 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.
  • Docs.claude/skills is 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.sh is 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 init ships zero link types, no default_board, a welcome board), so changing the starter surfaces here instead of silently invalidating the playbook.

internal/smoke/adoption_test.go is the agentic half — build-tagged, skipping without CARDS_AGENT_CMD, following the idiom pi-cards already 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 init ships 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 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.

Found by writing this

Two hard load failures the playbook now warns about, both the same class: settings.default_board naming a board whose file does not exist yet, and deleting a card type while a board still lists it in card_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 (MkdirTemp creates 0700, Rename preserves it), unreadable to a CI runner or container on another uid; and the oracle read link edges as type when the wire name is type_id, which would have made the hierarchy canary fail on every run including correct ones.

Verification

go build, go vet, full go test ./..., and -race on the touched packages all pass. scripts/smoke-adoption.sh is 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: 524c06f7 and 086bb35f, both in review with evidence.

🤖 Generated with Claude Code

somebox and others added 6 commits August 28, 2026 18:20
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.
@somebox
somebox merged commit 2a7f854 into main Aug 30, 2026
4 checks passed
@somebox
somebox deleted the feat/agent-guidance branch August 30, 2026 04:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant