Skip to content

refactor(harness): declare tool layout in one adapter table - #51

Merged
replygirl merged 34 commits into
mainfrom
worktree-harness-adapter-table
Oct 5, 2026
Merged

replygirl merged 34 commits into
mainfrom
worktree-harness-adapter-table

Conversation

@replygirl

@replygirl replygirl commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

The harness layer's tool shape was spread across five places that had to agree by hand: harness.yaml's harnesses: block, name branches in render.ts, DETECT_PATHS/RESTART_LINES in init.ts, and separate copies of the roots in update.ts/doctor.ts. None of them could express the per-tool shapes the pinned OpenSpec dist declares (a split commands root, per-extension filename templates, TOML serialization, IDE-restart flags, setup notes). This change puts all of it in one typed HARNESS_TABLE in apps/cli/src/harness/adapters.ts. render.ts, init, update and doctor all read the table, and none of them keeps its own copy. HarnessName/HARNESS_NAMES/isHarnessName are still exported, now derived from the table, and keep the same name, shape and order.

What each consumer reads from the table

  • render.ts: skills and command paths, frontmatter builder, injectArguments, rulesPath, and the markdown/toml serializer. The table is injectable through RenderOptions.adapters.
  • init.ts: builds the --harness value list and error text from HARNESS_NAMES and detects from each row's detectionPaths. The opsx leftover sweep and doctor's harness scan both read a row's shape through isHarnessDocument/findOpsxFiles. The receipt prints each selected row's setupNote, then upstream's single requiresIdeRestart line.
  • update.ts: gets the skills root, legacy roots and the rulesPath marker from the row, and MANAGED_REMOVAL_ROOTS is removalRoots(). Manifest routing is keyed on frontmatter === null, which is how TOML commands get tracked. A scope: 'home' file is refused before any write.
  • doctor.ts: attributes a file to the row with the longest-prefix surface (skills root, legacy skills root, commands dir or rules dir), falling back to primary root, and resolves references through the owning row's dialect-aware pattern. Both walks use scanRoots().

None of the four shipped rows sets requiresIdeRestart, so no receipt gains a line.

Byte-identical output

  • Render goldens: git diff 70b32f9e HEAD -- apps/cli/test/unit/__golden__/harness-render/ is empty.
  • Wiring characterization (init receipts for every harness selection, the invalid --harness error, both detection systems, removal containment, doctor human and --json): re-taken on the rebased, unmodified tree at 2f5a9de7. git diff 2f5a9de7 HEAD -- apps/cli/test/integration/__golden__/harness-wiring/ is empty.
  • A sandboxed built-binary cospec init --harness all --yes in a fresh repo, run once from this branch and once from main: both write 127 files to the identical file-tree digest (sha256:599c19a0…0902) and identical normalized stdout (sha256:aa758dca…eb62) — full byte identity, including the version stamp, since the branch rebased onto main's v0.8.3 release before the comparison.
  • generate:check shows no drift, test:pack is green, and apps/cli/test/contract/ and parity-pending.yaml have no changes against main.

Review rounds (table-fitness gaps, no behavior change today)

Three post-implementation review rounds found table-fitness gaps — "no behavior change today, breaks when a row is added" — each fixed on this branch before merge:

  1. Round 1: update.ts's orphan sweep matched commands by a literal .md filter instead of each row's extension; doctor.ts attributed a file by primary root first instead of the longest matching surface, and matched references only on / instead of the owning row's invocation prefix.
  2. Round 2: doctor.ts's harness-file scan and init's opsx leftover scan both read a literal .md filter instead of each row's shape (isHarnessDocument); the init receipt's shared-skills-root line was keyed on the literal ids codex/agents instead of equal resolved skills roots.
  3. Round 3: doctor.ts attributed a file by primary root on a tie instead of the longest row surface, missing legacy skills roots as surfaces.

Each fix added a synthetic fixture row to the relevant test file proving the gap, rather than relying on the four shipped rows (which can't exercise it).

Routed by ruling, not fixed here

Two further findings are genuine, user-visible defects on main today — not table-fitness gaps this change introduces — and fixing them here would break the byte-identical scope above. Per ruling, they are routed to a new follow-on PR, harness-receipt-and-doctor-scope, sequenced directly after this one merges:

  1. The init receipt's closing Try: /cospec:propose … hint always prints in Claude's spelling, regardless of which row was selected.
  2. Doctor's (and init's leftover sweep's) scan reads every .md file under a row's skills or legacy skills root, rather than narrowing to SKILL.md and the table's command paths.

design.md's Non-Goals no longer frame either one as a deliberate choice this change preserves — both are now named as known defects, out of scope here by ruling.

Gates

Rebased onto main twice: first onto d25c5c0 once unknown-option-contract, upstream-spellings and passthrough-json-and-doctor had merged (all three now recorded archived under ## Blocked by), then a second time onto 67f20c5d (the v0.8.3 release plus reset-yes-pipe-flake) — 0 conflicts both times. cospec validate harness-adapter-table --strict passes and cospec apply exits 0. mise run check is green on the final tree: lint, format, typecheck, unit 1891, contract 2403, integration 184, bench 339, release-test 14, all 0 fail; generate:check no drift; agents:check in sync; cospec-validate-all 0 errors.

Archive

cospec archive harness-adapter-table, no flags — validated, moved the change to openspec/changes/archive/2026-10-05-harness-adapter-table/, blocking-changes fully synced. The archive commit touches only openspec/ paths:

 .../2026-10-05-harness-adapter-table}/.openspec.yaml       | 0
 .../2026-10-05-harness-adapter-table}/blocking-changes.md  | 0
 .../2026-10-05-harness-adapter-table}/design.md            | 0
 .../2026-10-05-harness-adapter-table}/proposal.md          | 0
 .../2026-10-05-harness-adapter-table}/tasks.md             | 0
 .../2026-10-05-harness-adapter-table}/verification.md      | 0
 6 files changed, 0 insertions(+), 0 deletions(-)

🤖 Generated with Claude Code

@replygirl
replygirl force-pushed the worktree-harness-adapter-table branch from d8179be to 112fd4e Compare September 29, 2026 08:26
replygirl and others added 29 commits October 5, 2026 00:44
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
First implementation commit for harness-adapter-table (R8): committed raw
golden files (Buffer-compared, regenerated only under COSPEC_GOLDEN_WRITE=1)
for renderHarnessFiles across claude/codex/opencode/agents alone and together,
plus a wiring characterization of init/update/doctor's current behavior
(receipts, --harness validation, both detection systems, removal containment,
doctor findings). No src file is touched, so later commits in this branch have
a provable byte-identity baseline to diff against.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add HarnessAdapter and the four-row HARNESS_TABLE beside the harness.yaml
harnesses block (additive; render still reads the yaml). HarnessName,
HARNESS_NAMES and isHarnessName now derive from the table with the same
ids in the same order. Adds the flat body dialect, path helpers, and the
two-pass scan-root and removal-root derivations, with table invariants,
yaml path parity and pinned AI_TOOLS field parity tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
renderHarnessFiles now takes every path, frontmatter builder, argument
injection and rules file from the tool rows, with a RenderOptions.adapters
test seam and a scope field on RenderedFile. The harnesses: block leaves
canon/workflows/harness.yaml, which now carries workflow identity only,
and the opencode body dialect becomes flat. Rendered bytes are unchanged:
the render goldens and existing snapshots show no diff.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
render.ts dispatches on the row's commands.serializer: markdown as before,
toml as upstream Gemini's description/prompt layout with both escapers
ported, emitted with no frontmatter and no content hash. Fixture rows
through RenderOptions.adapters cover toml, .prompt, .prompt.md, a split
commands root, namespaced and flat filenames, the @ prefix and home scope.
Goldens and generated output are byte-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Build the compiled binary and run init --harness all in a fresh temp
repo before T3 touches init.ts, per tasks.md's timing requirement.
Records the file count and a sha256 digest over the sorted per-file
sha256sum output plus the stdout digest, for task 5.2's post-T3
re-take to compare against in verification 3.8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Run the full unit/integration/contract suites and generate:check
against the table-based render (groups 2 and 3), confirm the render
and snapshot goldens are untouched, and record observed evidence for
verification rows 1.1-1.4, 2.1-2.9 and 4.1-4.3. Row 1.4 defers its
after-task-5.5 half until T3 lands; every other row in this checkpoint
passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
harness-integration.md now names HARNESS_TABLE in harness/adapters.ts
as the single place a tool's skills/commands/rules layout is declared,
notes that harness.yaml carries workflow identity only, and rewrites
the shared-root, legacy-migration and restart-line bullets around the
table's actual fields (skillsDir, bodyDialect, legacySkillsDirs,
rulesPath, setupNote, requiresIdeRestart). No apps/docs change: no
user-facing behavior changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The invocation-prefix test compared the real opencode row (already `/`)
against its own live render, so a regression in the flat respelling
moved both sides. Relocate the fixture row to `.x/` and require every
file to be Buffer-equal to the committed pre-change opencode golden;
repoint verification 2.6 and task 3.2 at it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rebased onto main d25c5c0 (unknown-option-contract, upstream-spellings,
passthrough-json-and-doctor merged). Blocked by now lists all three as
archived; sync-blockers reports the change fully unblocked. Verification
4.4 and 5.1 observed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
COSPEC_GOLDEN_WRITE=1 over harness-wiring.test.ts on main d25c5c0 with
apps/cli/src/commands/ unmodified rewrote every golden byte-identically,
so no golden file changes here. Records the built-binary init --harness
all baseline (127 files, file-list digest equal to task 1.3's; the
normalized stdout digest task 5.6 compares against).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
init.ts builds the --harness value list from HARNESS_NAMES, detects from
each row's detectionPaths, walks the opsx sweep over scanRoots(), and
prints each selected row's setupNote plus upstream's single IDE restart
line (ideRestartLine; none of the four rows sets requiresIdeRestart).
DETECT_PATHS and RESTART_LINES are gone. adapters.ts exports primaryRoot
and ideRestartLine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…E (5.4)

update.ts reads each harness's skills root, legacy skills roots and
rulesPath marker from its row, takes MANAGED_REMOVAL_ROOTS from
removalRoots(), routes manifest tracking on frontmatter === null, and
refuses a home-scoped rendered file before any write. GenerateOptions
gains the adapters test seam; the write-mode receipt prints the
requiresIdeRestart line after a write (never, for the four rows).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
doctor.ts resolves a dangling reference through the owning row's skills
root and commandPath, walks scanRoots() for harness markdown and
sidecars, and attributes a file to the row whose primaryRoot prefixes
it. The SKILL_BASE and COMMAND_LOC copies are gone; WORKFLOW_SKILL's
comment now says it mirrors workflow identity, not tool layout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
.agents/shared.md (synced to CLAUDE.md and AGENTS.md) now says managed
harness files compose from canon plus HARNESS_TABLE in
harness/adapters.ts, which render, init, update and doctor all read.
docs/harness-integration.md names what each command reads from the
table, the manifest tracking of TOML commands, the home-scope refusal
and update's restart line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every verification row observed after T3: wiring goldens unchanged since
the task 5.2 re-take (4625056), render goldens unchanged since task 1.1,
generate:check no drift, test:pack green, and the built binary's init
--harness all file list and normalized stdout identical to task 5.2's.
Tasks 5.3 to 5.6 and 7.1 ticked; mise run check green at 77db34a.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
update's orphan sweep filtered command dirs on a literal `.md`, so a row
with a `.prompt` extension never had a retired command removed and
`update --check` reported no drift. Match each command dir against the
extensions of the markdown rows that render into it; a TOML row's dir is
left to the manifest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
doctor matched references only as `/cospec[:-]`, so an `@`-prefix row's
dangling `@cospec-<id>` passed silently, and it attributed a file only by
its row's primary root, so a split-root row's skills tree was skipped.
Fall back to the row whose skills root, commands dir or rules dir covers
the file, and match the owning row's invocation prefix. Record the
review fixes in the design, tasks and verification ledger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/harness-integration.md and .agents/shared.md claimed no command
branches on a tool name and that a later tool needs only a new row.
init's settings merge and claude default, its codex/agents receipt line
and doctor's `.md`-only scan still sit outside the table; say so, and
list what update's sweep and doctor's attribution now read from it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Doctor's stale-harness, mixed-versions and dangling-ref checks collected
only `*.md`, so a `.prompt` command row got none of them. The scan now
reads each markdown row's command extension under its commands dir and
the skill file's extension under its skills roots, from HARNESS_TABLE;
TOML commands stay with the manifest. The four rows scan the same files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The init receipt's "skills ... share the .agents/skills root" line was
keyed on the ids codex and agents, so a third row on that root was left
out. It is now one line per skills root that two or more rows resolve
to, naming every row on it in table order. The four rows print the same
bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Init's opsx leftover scan still filtered on a literal `.md`, so a
`.prompt` leftover in a row's commands dir was never found; it now
reads files through isHarnessDocument like doctor. The skill filename
comes from SKILL_FILE in adapters.ts instead of four `SKILL.md`
literals. The four rows render and scan the same files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Design records init's Claude-only settings merge, `claude` default and
`/cospec:propose` hint, and openspec's shared opsx skills root, as
deliberate. docs/harness-integration.md and shared.md stop listing the
receipt line and doctor's `.md`-only scan as gaps now the table drives
both; CLAUDE.md and AGENTS.md re-synced.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A commands dir under another row's primary root (Antigravity's
.agents/workflows) went to the earlier row, and a legacy skills root no
primary root covers went to no row, so its references were never checked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The scan reads every .md under a skills root's top-level dir, not only
skill files, and the legacy-skills migration covers only .codex/skills.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
replygirl and others added 2 commits October 5, 2026 00:44
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
design.md framed the receipt's always-Claude /cospec:propose hint and
doctor's every-.md scan breadth as deliberate Non-Goals; both are known
defects on main, routed by ruling to harness-receipt-and-doctor-scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@replygirl
replygirl force-pushed the worktree-harness-adapter-table branch from 815ca40 to 5d9a842 Compare October 5, 2026 05:45
replygirl and others added 3 commits October 5, 2026 00:49
Second rebase onto main (v0.8.3 + reset-yes-pipe-flake, 67f20c5): 0
conflicts, render/wiring goldens untouched, and a sandboxed built-binary
init --harness all run is now byte-identical to main including the
version stamp (both trees are cospec@0.8.3).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mise run check green on the rebased tree (HEAD 9ce9a8e): unit 1891,
contract 2403, integration 184, bench 339, release-test 14, all 0 fail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cospec archive, no flags: validates, moves the change to
openspec/changes/archive/2026-10-05-harness-adapter-table/, and reports
blocking-changes fully synced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@replygirl
replygirl marked this pull request as ready for review October 5, 2026 06:11
Copilot AI balanced review requested due to automatic review settings October 5, 2026 06:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@replygirl
replygirl merged commit 16598da into main Oct 5, 2026
12 checks passed
@replygirl
replygirl deleted the worktree-harness-adapter-table branch October 5, 2026 06:41
replygirl added a commit that referenced this pull request Oct 5, 2026
… files

* fix(harness): plan harness-receipt-and-doctor-scope change

Proposal, blocking-changes, design, harness-workflows delta, tasks and
verification for the two defects #51 surfaced: the init receipt hint
always spelled for Claude, and doctor's harness checks reading every
.md under a harness dir.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(harness): confirm baseline for harness-receipt-and-doctor-scope

Task 1.1: the branch sits on main at 16598da (#51 merged) and
cospec apply exits 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(harness): pin the receipt hint per selected harness

Lifts the receipt's two closing hint lines into receiptHintLines with
today's output, and adds verification 1.6's rows: claude, all and none
keep /cospec:propose; opencode, codex, agents, an explicit
opencode,claude list and a synthetic @ row land as test.failing until
the hint follows the first selected row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(harness): spell the init receipt hint for the selected harness

cospec init ended every receipt with /cospec:propose, a command only
Claude Code registers. The two closing hint lines now go through the
first selected row's body dialect and invocation prefix with
transformBody, the respelling its generated bodies get; init and
render.ts read the canon workflow manifest through one shared loader.

Release note: the `cospec init` receipt for `--harness opencode`,
`--harness codex` and `--harness agents` (and for any list or detection
whose first row is one of them) now ends with the corrected hint:
`/cospec-propose` for OpenCode, and
`$cospec-propose (Codex) or /cospec-propose (other agents)` for Codex
and the shared .agents skills. The claude, all, default and none
receipts are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(harness): pin doctor's harness checks to files cospec writes

Adds verification 2.1-2.4 and 3.2's rows. A user's .claude/notes.md, a
nested worktree's copy under .claude/worktrees/, a SKILL.md deeper than
one directory, and other markdown under a scan root land as
test.failing until isHarnessDocument narrows. A dangling ref in a
cospec-written command, the legacy .codex/skills root, the table's
command paths, and upstream's legacy opsx command paths in the leftover
scan pass before and after.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(harness): check only harness files cospec writes in doctor

isHarnessDocument accepted every .md under a top-level harness dir, so
doctor's stale-harness, mixed-versions and dangling-ref checks read a
user's own .claude/notes.md and a nested worktree's checkout under
.claude/worktrees/. It now accepts exactly <skills-root>/<skill>/SKILL.md
(project and legacy skills roots) and each markdown row's command paths,
matched on the full root. The opsx leftover scan keeps today's breadth:
it moves into init.ts (isLeftoverCandidate, leftoverScanFiles), and
findOpsxFiles and doctor's checkOpsx both read it.

Release note:
- The `cospec init` receipt for `--harness opencode`, `--harness codex`
  and `--harness agents` (and for any list or detection whose first row
  is one of them) ends with the corrected hint lines. The claude, all,
  default and none receipts do not change.
- `cospec doctor` (text and `--json`) no longer reports stale-harness,
  mixed-versions or dangling-ref findings for markdown outside
  <skills-root>/<skill>/SKILL.md and the table's command paths. A repo
  that failed doctor only because of such a file, such as a user's own
  notes or a nested worktree's copy, now passes, and its findings and
  summary counts shrink. Findings on files cospec writes are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(harness): document the receipt hint and doctor's scan boundary

harness-setup.md states that init's Try: hint uses the first selected
harness's spelling and what --harness none prints. The commands.md
doctor row names the files the harness checks read and that user
markdown and nested worktree copies are no longer checked.
docs/harness-integration.md drops its two known-defect passages and
describes the corrected behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(agents): describe the corrected receipt hint and doctor scan

Replaces shared.md's known-defects sentence with the behaviour this
change ships, then syncs CLAUDE.md and AGENTS.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(harness): tick the release-note task for the doctor fix

Task 6.1: e21c226's body relays the proposal's BREAKING list (receipt
hint for opencode/codex/agents; doctor's narrowed harness checks) with
no ! and no BREAKING CHANGE footer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(harness): record the full check in the verification ledger

Task 7.1 and rows 5.1-5.2: mise run check exits 0 and validate --strict
is clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(harness): record PR #63 and close out the verification ledger

Tasks 1.2, 6.2 and 7.2: the PR number is recorded in proposal.md and the
ledger header; row 4.4 observed the BREAKING list in both fix-commit
bodies and the PR body; validate --strict is clean and every
verification row is [x]. Task 7.3 (archive) stays open for the merge
stage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(harness): tick the archive task row

Task 7.3 (cospec archive) is performed by the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(harness): archive harness-receipt-and-doctor-scope

cospec archive: moved the change to
openspec/changes/archive/2026-10-05-harness-receipt-and-doctor-scope/ and
merged both new requirements (init receipt hint follows the selected
harness; doctor checks only the harness files cospec writes) into
openspec/specs/harness-workflows/spec.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

2 participants