From 8bcd61fa49948885ed9c9741e7beaeb5799e3304 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 09:59:13 -0300 Subject: [PATCH 01/48] docs: align doctor skill readiness task context --- docs/specs/0036-doctor-skill-readiness/task_02.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/specs/0036-doctor-skill-readiness/task_02.md b/docs/specs/0036-doctor-skill-readiness/task_02.md index 87df19b..be4f76a 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_02.md +++ b/docs/specs/0036-doctor-skill-readiness/task_02.md @@ -73,14 +73,14 @@ failure, and remediation contract as the shipped CLI. - instruction: `CONTEXT.md` - instruction: `docs/agents/skill-dispatch.md` -- docs: `README.md` -- docs: `docs/user-guide/commands.md` -- docs: `docs/user-guide/usage.md` +- interface: `README.md` +- interface: `docs/user-guide/commands.md` +- interface: `docs/user-guide/usage.md` - interface: `.agents/skills/roundfix/SKILL.md` - interface: `.agents/skills/roundfix/agents/openai.yaml` - interface: `skills/skills.go` - interface: `Makefile` -- data: `skills-lock.json` +- interface: `skills-lock.json` ## Verification From 94e0ba6a15cf83fd60e7bdb0075bab192ae219ea Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 10:19:17 -0300 Subject: [PATCH 02/48] chore: archive 0049-baseline-preservation-idempotency --- .../0049-baseline-preservation-idempotency/_prd.md | 4 +++- .../0049-baseline-preservation-idempotency/_tasks.md | 0 .../0049-baseline-preservation-idempotency/_techspec.md | 0 .../0049-baseline-preservation-idempotency/task_01.md | 0 4 files changed, 3 insertions(+), 1 deletion(-) rename docs/specs/{ => _archived}/0049-baseline-preservation-idempotency/_prd.md (98%) rename docs/specs/{ => _archived}/0049-baseline-preservation-idempotency/_tasks.md (100%) rename docs/specs/{ => _archived}/0049-baseline-preservation-idempotency/_techspec.md (100%) rename docs/specs/{ => _archived}/0049-baseline-preservation-idempotency/task_01.md (100%) diff --git a/docs/specs/0049-baseline-preservation-idempotency/_prd.md b/docs/specs/_archived/0049-baseline-preservation-idempotency/_prd.md similarity index 98% rename from docs/specs/0049-baseline-preservation-idempotency/_prd.md rename to docs/specs/_archived/0049-baseline-preservation-idempotency/_prd.md index 474c5dc..9494f27 100644 --- a/docs/specs/0049-baseline-preservation-idempotency/_prd.md +++ b/docs/specs/_archived/0049-baseline-preservation-idempotency/_prd.md @@ -1,7 +1,9 @@ --- spec: 0049-baseline-preservation-idempotency -status: active +status: archived created: 2026-07-26 +archived: 2026-07-26 +qa_override: true surfaces: [cli, infra] --- diff --git a/docs/specs/0049-baseline-preservation-idempotency/_tasks.md b/docs/specs/_archived/0049-baseline-preservation-idempotency/_tasks.md similarity index 100% rename from docs/specs/0049-baseline-preservation-idempotency/_tasks.md rename to docs/specs/_archived/0049-baseline-preservation-idempotency/_tasks.md diff --git a/docs/specs/0049-baseline-preservation-idempotency/_techspec.md b/docs/specs/_archived/0049-baseline-preservation-idempotency/_techspec.md similarity index 100% rename from docs/specs/0049-baseline-preservation-idempotency/_techspec.md rename to docs/specs/_archived/0049-baseline-preservation-idempotency/_techspec.md diff --git a/docs/specs/0049-baseline-preservation-idempotency/task_01.md b/docs/specs/_archived/0049-baseline-preservation-idempotency/task_01.md similarity index 100% rename from docs/specs/0049-baseline-preservation-idempotency/task_01.md rename to docs/specs/_archived/0049-baseline-preservation-idempotency/task_01.md From 474e2aab5df210bc9a991e611bb5526d918d5d59 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 11:20:18 -0300 Subject: [PATCH 03/48] docs: refine doctor skill readiness plan --- .../specs/0036-doctor-skill-readiness/_prd.md | 46 ++++---- .../0036-doctor-skill-readiness/_tasks.md | 6 +- .../0036-doctor-skill-readiness/_techspec.md | 100 ++++++++---------- .../0036-doctor-skill-readiness/task_01.md | 28 ++--- .../0036-doctor-skill-readiness/task_02.md | 65 ++++-------- .../0036-doctor-skill-readiness/task_03.md | 82 ++++++++++++++ 6 files changed, 190 insertions(+), 137 deletions(-) create mode 100644 docs/specs/0036-doctor-skill-readiness/task_03.md diff --git a/docs/specs/0036-doctor-skill-readiness/_prd.md b/docs/specs/0036-doctor-skill-readiness/_prd.md index 36a8036..5f15397 100644 --- a/docs/specs/0036-doctor-skill-readiness/_prd.md +++ b/docs/specs/0036-doctor-skill-readiness/_prd.md @@ -25,6 +25,24 @@ Doctor's legacy runtime probe with the shared effective-adapter and exact Agent Selection Profile proof. This Spec appends only the independent Repository Skill Set result and must not recreate or bypass profile proof. +## Project Constraints + +- Identifier strategy: not applicable — this feature creates no project-owned + Internal Identifier or application identity; skill names remain sourced from + the embedded bundle and `skills-lock.json`. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — Doctor reads only local repository + state and must not add an authentication provider, HTTP route, or network + call. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0049 and ADR-0055 keep Agent + Selection Profile proof authoritative, while ADR-0066 and ADR-0072 keep + Baseline execution in the Go CLI and prohibit restoring the removed Python + runtime. Source: `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`; no other protected tooling mutation is + authorized. Source: + `docs/agents/agent-instructions.md`. + ## Goals - Make `roundfix doctor` prove that every required repository skill is @@ -37,8 +55,6 @@ Repository Skill Set result and must not recreate or bypass profile proof. skills and provides the exact relevant update command or commands. - Keep Doctor diagnosis-only: no downloads, network calls, installs, file writes, or lock-file updates. -- Keep setup snapshot synchronization from replacing Roundfix-owned skill - digests with content from an external checkout. - Keep `CONTEXT.md`, user guidance, and the canonical Roundfix Skill aligned with the shipped Doctor behavior. @@ -85,11 +101,7 @@ Repository Skill Set result and must not recreate or bypass profile proof. `roundfix skills install --target project`; external drift points to `bunx skills experimental_install && bunx skills update -p -y`. When both groups fail, the single Doctor line includes both commands in that order. -6. **Ownership-safe setup synchronization.** `sync-setups` preserves the - existing authoritative digest for entries whose `source.type` is `repo`, - even when the external setup checkout contains a different file at the same - path. External entries continue to refresh from their declared source. -7. **Synchronized guidance.** Command help, user documentation, the canonical +6. **Synchronized guidance.** Command help, user documentation, the canonical Roundfix Skill, its embedded copy, and the canonical glossary describe the new check and preserve the repository's skill-ownership boundary. @@ -121,11 +133,15 @@ authorization, and rerun Doctor to prove the result. - Validating `AGENTS.md`, Context Documents, agent guides, or Baseline ADRs; those repository-document contracts belong to `setup-context-driven audit` and Spec 0040. +- Changing Baseline asset synchronization or restoring the removed Python + setup runtime. Baseline asset behavior belongs to the Go CLI under ADR-0066 + and ADR-0072. ## Success Metrics -- A repository with all 14 owned skills byte-equal to the binary and all 24 - external skills matching `skills-lock.json` reports `skills: ok` and does +- A repository with all 14 owned skills byte-equal to the binary and all 25 + external skills matching `skills-lock.json` reports `skills: ok` for the + current derived total of 39 required skills and does not fail Doctor on skill readiness. - Removing one required skill makes Doctor name it as missing and exit non-zero without creating or changing any file. @@ -135,9 +151,6 @@ authorization, and rerun Doctor to prove the result. - A missing or malformed `skills-lock.json`, an empty or malformed `computedHash`, or an unreadable required artifact produces a deterministic failure with a useful next action rather than a panic. -- A `sync-setups` regression fixture with conflicting external content keeps - the Roundfix-owned digest unchanged while external skill updates remain - refreshable. - Focused tests cover clean, missing, outdated, mixed-ownership, malformed lock, and no-mutation behavior; the full repository verification gate and race suite pass. @@ -152,12 +165,9 @@ authorization, and rerun Doctor to prove the result. removal. - The local deterministic hash contract is implemented directly rather than invoking the external skills CLI. -- Setup snapshot synchronization resolves digest authority from `source.type`; - repository-owned entries never derive their digest from the external setup - checkout. -- This change extends the existing Doctor Command plus the skill ownership and - synchronization contracts in `docs/agents/skill-dispatch.md`; it does not - require a new architectural decision record. +- This change extends the existing Doctor Command and the skill ownership + contract in `docs/agents/skill-dispatch.md`; it does not require a new + architectural decision record. ## Dependencies diff --git a/docs/specs/0036-doctor-skill-readiness/_tasks.md b/docs/specs/0036-doctor-skill-readiness/_tasks.md index 36b2a70..ce0a914 100644 --- a/docs/specs/0036-doctor-skill-readiness/_tasks.md +++ b/docs/specs/0036-doctor-skill-readiness/_tasks.md @@ -9,6 +9,9 @@ graph: - id: task_02 file: task_02.md needs: [task_01] + - id: task_03 + file: task_03.md + needs: [task_02] --- # Tasks — Doctor Skill Readiness @@ -21,5 +24,6 @@ extends. | ------- | ------------------------------------------------ | ------- | ---------- | ------- | | task_01 | Diagnose Repository Skill Set readiness | backend | high | — | | task_02 | Synchronize Doctor skill-readiness guidance | docs | medium | task_01 | +| task_03 | Align the protected Roundfix Skill pair | docs | low | task_02 | -Waves: 1 → task_01 · 2 → task_02 +Waves: 1 → task_01 · 2 → task_02 · 3 → task_03 diff --git a/docs/specs/0036-doctor-skill-readiness/_techspec.md b/docs/specs/0036-doctor-skill-readiness/_techspec.md index 2fffdd6..5f05658 100644 --- a/docs/specs/0036-doctor-skill-readiness/_techspec.md +++ b/docs/specs/0036-doctor-skill-readiness/_techspec.md @@ -27,6 +27,24 @@ proves the effective adapter and exact required Agent Selection Profiles. The work below injects one independent Repository Skill Set result into that boundary and does not preserve or recreate the legacy single-model probe. +## Project Constraints + +- Identifier strategy: not applicable — the checker introduces no project-owned + Internal Identifier; it consumes existing embedded and lock-declared skill + names. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — the design is filesystem-only and + must not add credentials, HTTP behavior, or network access. Source: + `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0049 and ADR-0055 bind the + profile-aware Doctor boundary, and ADR-0066 plus ADR-0072 require any Baseline + asset behavior to remain in the Go CLI rather than a Python skill runtime. + Source: `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`; no other protected tooling mutation is + authorized. Source: + `docs/agents/agent-instructions.md`. + ## System Architecture - `skills` — add repository readiness types, `skills-lock.json` decoding, @@ -38,13 +56,11 @@ boundary and does not preserve or recreate the legacy single-model probe. translate the structured result into `CheckResult{Name: "skills"}`, and append it to the existing deterministic Doctor output. CLI code owns wording and exit behavior; the checker owns filesystem facts. -- `.agents/skills/setup-context-driven` — correct `sync-setups` source - precedence so a repository-owned snapshot digest cannot be replaced by a - same-path file from the external canonical setup checkout; synchronize the - embedded owned copy after focused regression coverage passes. -- `CONTEXT.md`, `docs/user-guide`, README, and `.agents/skills/roundfix` — add - Repository Skill Set vocabulary and describe the new Doctor line and update - commands. `make skills-sync` regenerates the embedded Roundfix Skill. +- `CONTEXT.md`, `docs/user-guide`, and README — add Repository Skill Set + vocabulary and describe the new Doctor line and update commands. +- `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md` — one + dedicated tooling-only Task updates the authorized canonical/generated pair + without touching other protected tooling. - No Run Store, configuration schema, Agent Session, TUI, network, or daemon changes. Agent adapter and profile proof belongs exclusively to Spec 0041. - No Context-Driven artifact inspection: Doctor proves the installed skill @@ -156,7 +172,7 @@ and appends its result after the profile-readiness result introduced by Spec Success detail is deterministic: ```text -skills: ok (38 required: 14 Roundfix-owned, 24 external) +skills: ok (39 required: 14 Roundfix-owned, 25 external) ``` Failure detail uses only non-empty stable groups: @@ -174,41 +190,20 @@ command. Any failed result uses the existing `exitRunFailed` (`1`); argument or usage errors remain `exitPreflight` (`2`). Stdout remains requested Doctor output and stderr remains reserved for command-level load/usage failures. -### Ownership-safe setup synchronization - -`normalize_source_skill` already resolves the current snapshot entry and the -incoming source declaration. Make that declaration control digest precedence: - -1. Resolve the effective source from the incoming entry, falling back to the - current entry as today. -2. When the effective source has `type: repo` and the current snapshot carries - a valid `contentDigest`, preserve that digest before consulting - `source_dir`. -3. For non-repository entries, retain the existing order: explicit incoming - digest, resolved external `SKILL.md`, then current digest fallback. -4. A new repository-owned entry without a current digest must supply an - explicit digest; it must not silently hash a same-path external file. - -This is an authorial workflow fix in a repo-owned skill, so both canonical and -embedded copies change together through `make skills-sync`. Regression tests -cover a text setup source whose external checkout deliberately contains -conflicting content at a repo-owned path, plus an external entry that still -refreshes normally. This closes the source-precedence failure absorbed into -this Spec; the original report remains available through Git history. - ### Documentation and skill synchronization - Add **Repository Skill Set** to `CONTEXT.md` and extend **Doctor Command** to include it. - Update README and Doctor command documentation with the `skills:` line, ownership rules, failure behavior, and read-only update guidance. -- Update the canonical `.agents/skills/roundfix` Doctor instructions and - OpenAI manifest where the check list is enumerated. -- Run `make skills-sync` so `skills/roundfix` matches the canonical skill and - extend shipped skill contract tests for the new required wording. -- Keep the externally managed skill files and `skills-lock.json` updates - produced by `make skills-update` unchanged; no authorial edits are permitted - under those skills. +- In a separate tooling-only Task, update + `.agents/skills/roundfix/SKILL.md` and its + `skills/roundfix/SKILL.md` generated copy byte-identically. Do not run the + broad `make skills-sync` mutation target because it rewrites every owned + skill directory; use `make skills-sync-check` as read-only verification. +- Keep externally managed skill files, `skills/recommended.txt`, and + `skills-lock.json` unchanged; the required external count remains derived + from the shipped recommendation list. ## Coverage Map @@ -217,8 +212,7 @@ this Spec; the original report remains available through Git history. - User story 4 → stable Doctor formatter, sorted classifications, exact next actions, non-zero exit. - User story 5 → local filesystem-only implementation and no-mutation tests. -- Core feature 6 → setup snapshot source precedence and regression coverage. -- Core feature 7 → docs, canonical vocabulary, Roundfix Skill sync, and +- Core feature 6 → docs, canonical vocabulary, Roundfix Skill sync, and ownership contract verification. ## Testing Approach @@ -237,9 +231,6 @@ this Spec; the original report remains available through Git history. output remains byte-stable except for the appended line. - No-mutation tests snapshot relevant paths before and after Doctor and assert no repository, user config, `.roundfix`, or skill file is created or changed. -- Setup-context tests prove `source.type: repo` preserves the current bundled - digest in the presence of conflicting external content and that ordinary - external sources still refresh. - QA runs the built binary against a clean disposable repository, then copies that fixture and exercises missing and outdated variants without changing the working repository. @@ -251,13 +242,13 @@ this Spec; the original report remains available through Git history. Cross-Spec prerequisite: implement Spec 0041 through its profile-aware Doctor integration before starting this graph. -1. Ownership-safe setup synchronization and its focused regression tests. -2. Repository skill readiness types, lock validation, owned comparison, +1. Repository skill readiness types, lock validation, owned comparison, external hash compatibility, and focused package tests. -3. Doctor dependency injection, rendering after profile readiness, failure - semantics, command tests, and CLI help (depends on 2 and Spec 0041). -4. Canonical vocabulary, user docs, canonical Roundfix Skill update, embedded - sync, and contract verification (depends on 1, 3). +2. Doctor dependency injection, rendering after profile readiness, failure + semantics, command tests, and CLI help (depends on 1 and Spec 0041). +3. Canonical vocabulary and user documentation (depends on 2). +4. Dedicated tooling-only update of the exact authorized canonical/generated + Roundfix Skill files and read-only sync verification (depends on 3). ## Risks & Considerations @@ -266,7 +257,8 @@ integration before starting this graph. directories, and the absence of separators. - Comparing the installed owned tree against the running binary intentionally means a newly built dirty binary may report local skill drift until - `make skills-sync` and rebuild complete; that is the desired pre-PR signal. + the authorized canonical/generated pair is synchronized and the binary is + rebuilt; that is the desired pre-PR signal. - A repository lock can contain stale or malicious paths. Skill names are validated as basenames, and the checker never follows symlinked files, so no declared value can escape `.agents/skills`. @@ -275,10 +267,8 @@ integration before starting this graph. - Spec 0041 and this Spec both touch Doctor dependencies and output. Implement Spec 0041 first and extend its coordinator; do not maintain competing legacy and profile-aware runtime checks. -- The project currently declares 38 required skills. Counts are derived at +- The project currently declares 39 required skills. Counts are derived at runtime and must not be hard-coded in production behavior. -- External setup checkouts may contain divergent copies of repo-owned skills; - source precedence must be decided by metadata, not filesystem coincidence. ## Decisions @@ -286,10 +276,8 @@ integration before starting this graph. - Roundfix-owned authority is the running binary; external authority is the current repository's `skills-lock.json` constrained to `Recommended()`. - The check reproduces the local hash contract and never invokes `bunx`. -- `sync-setups` preserves current valid digests for `source.type: repo` and - never hashes a conflicting external checkout for those entries. - One aggregate `skills:` line preserves Doctor's compact public surface. -- No ADR is added because this is an extension of the accepted Doctor plus the - skill ownership and synchronization boundaries in +- No ADR is added because this is an extension of the accepted Doctor and the + skill ownership boundary in `docs/agents/skill-dispatch.md`, not a new hard-to-reverse architecture choice. diff --git a/docs/specs/0036-doctor-skill-readiness/task_01.md b/docs/specs/0036-doctor-skill-readiness/task_01.md index 0f4083c..0bfebed 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_01.md +++ b/docs/specs/0036-doctor-skill-readiness/task_01.md @@ -44,9 +44,6 @@ filesystem mutation. config writes, lock updates, or skill-tree mutations. 10. MUST wrap filesystem and decoding errors with the failed operation and path, and MUST reject unsafe skill names without reading outside the repository. -11. MUST fix `sync-setups` so a valid current digest for `source.type: repo` - takes precedence over same-path content in the external setup checkout, - while non-repository skill refresh behavior remains unchanged. ## Subtasks @@ -58,15 +55,13 @@ filesystem mutation. symlink, mixed-ownership, and ordering cases. - [ ] Add exact-output Doctor tests for success, each ownership group, mixed remediation, checker errors, exit behavior, and no mutation. -- [ ] Add setup synchronization regression tests for conflicting repo-owned - external content and continuing external-source refresh. - [ ] Update Doctor command help for the appended Repository Skill Set check. ## Acceptance Criteria - [ ] A complete fixture matching the embedded owned bundle and required lock - hashes reports `skills: ok` with derived 14/24/38 counts and exits zero - when all other Doctor checks pass. + hashes reports `skills: ok` with the current derived 14/25/39 counts and + exits zero when all other Doctor checks pass. - [ ] Removing one owned or external skill reports that name under `missing`, prints the ownership-specific command, and exits one. - [ ] Changing, adding, or removing a versioned file reports the affected skill @@ -79,22 +74,23 @@ filesystem mutation. remediation commands exactly once in owned-then-external order. - [ ] Doctor runs every existing check even when skill readiness fails and changes no repository, user-config, Run, or skill path. -- [ ] `sync-setups` preserves a Roundfix-owned digest when an external checkout - contains conflicting content at the same path, and the canonical and - embedded setup-context-driven suites pass together. ## Context - instruction: `CONTEXT.md` - instruction: `docs/agents/skill-dispatch.md` +- instruction: `.agents/skills/agentic-cli-design/SKILL.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-cli/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` - instruction: `docs/specs/_archived/0041-agent-selection-runtime-readiness/_techspec.md` - interface: `skills/skills.go` - interface: `skills/skills_test.go` - interface: `internal/cli/doctor.go` - interface: `internal/cli/cli_test.go` - interface: `internal/cli/cli.go` -- interface: `.agents/skills/setup-context-driven/scripts/context_setup.py` -- interface: `.agents/skills/setup-context-driven/tests/test_sync_setups.py` ## Verification @@ -105,15 +101,13 @@ filesystem mutation. `skills:` output, remediation, exit behavior, and existing Doctor lines pass. - `rtk go test -race ./skills ./internal/cli -run 'Test(CheckRepository|RunDoctor)' -count=1` — expected: injected checks and filesystem reads are race-free. -- `rtk make setup-context-check` — expected: repo-owned digest precedence, - external refresh, and all setup-context-driven flows pass. ## References -- `_prd.md` → Goals; User Stories 1–5; Core Features 1–6; Success Metrics. +- `_prd.md` → Goals; User Stories 1–5; Core Features 1–5; Success Metrics. - `_techspec.md` → Repository readiness contract; Owned-skill comparison; - External lock and hash comparison; Doctor integration and output; Build Order - 1–3. + External lock and hash comparison; Doctor integration and output; Build + Order 1–2. - `docs/specs/_archived/0041-agent-selection-runtime-readiness/_techspec.md` → prerequisite profile-aware Doctor coordinator and output order. - `CONTEXT.md` → Doctor Command; Repository Skill Set; Roundfix Skill. diff --git a/docs/specs/0036-doctor-skill-readiness/task_02.md b/docs/specs/0036-doctor-skill-readiness/task_02.md index be4f76a..73a62f4 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_02.md +++ b/docs/specs/0036-doctor-skill-readiness/task_02.md @@ -10,11 +10,11 @@ complexity: medium ## Overview -Publish the completed Doctor behavior through the canonical glossary, user -documentation, and Roundfix-owned skill bundle while preserving externally -managed skill updates exactly as produced by the upstream installer. This slice -ensures a developer, Supervisor, or Agent receives the same ownership, -failure, and remediation contract as the shipped CLI. +Publish the completed Doctor behavior through the canonical glossary and user +documentation while preserving every protected tooling and externally managed +skill path. This slice gives developers the same ownership, failure, and +remediation contract as the shipped CLI; Task 03 owns the isolated Roundfix +Skill update. ## Requirements @@ -23,76 +23,51 @@ failure, and remediation contract as the shipped CLI. 2. MUST document the `skills: ok|failed` line, blocking exit behavior, owned and external authorities, offline/read-only guarantee, and exact update commands in README and the Doctor user guide. -3. MUST update the canonical `.agents/skills/roundfix` instructions and OpenAI - manifest wherever Doctor checks or recovery behavior are enumerated. -4. MUST regenerate the embedded `skills/roundfix` copy with `make skills-sync` - and extend the shipped skill contract checks so future CLI/skill drift fails - verification. -5. MUST preserve the authorial ownership boundary: do not manually edit any - externally managed skill, and retain only upstream changes produced by the - approved `make skills-update` flow. -6. MUST keep `skills-lock.json`, the installed required external skill trees, - and `skills/recommended.txt` synchronized after the update. -7. MUST describe that unrelated extra installed skills or lock entries are +3. MUST preserve the authorial ownership boundary: do not edit any protected + tooling path, externally managed skill, `skills-lock.json`, or + `skills/recommended.txt`; their content is read-only in this slice. +4. MUST describe that unrelated extra installed skills or lock entries are ignored and that Doctor never deletes or updates skills automatically. -8. MUST keep all repository content in English and use only canonical terms +5. MUST keep all repository content in English and use only canonical terms from `CONTEXT.md`. -9. MUST describe Repository Skill Set readiness after, and independently from, +6. MUST describe Repository Skill Set readiness after, and independently from, the Agent Selection Profile readiness delivered by Spec 0041. ## Subtasks - [ ] Finalize Repository Skill Set glossary and Doctor definition. - [ ] Update README and Doctor command/user guidance. -- [ ] Update the canonical Roundfix Skill and manifest. -- [ ] Regenerate embedded owned skills and recommended external names. -- [ ] Extend skill contract assertions for the new Doctor wording. -- [ ] Verify upstream-managed skill changes and lock hashes without authorial - modification. -- [ ] Run focused skill synchronization and documentation checks. +- [ ] Verify required external skill hashes without authorial modification. ## Acceptance Criteria - [ ] A reader can identify all required skill authorities, Doctor outcomes, update commands, and the no-network/no-mutation guarantee from supported user documentation. -- [ ] The canonical Roundfix Skill tells an Agent to surface a failed skill - check and its remediation before work continues, without claiming Doctor - performs the update. -- [ ] `.agents/skills/roundfix` and `skills/roundfix` are byte-identical after - synchronization, and `roundfix skills check` enforces the new wording. - [ ] All required external installed trees hash to their current - `skills-lock.json` entries, including the four upstream updates discovered - during specification work. + `skills-lock.json` entries. - [ ] No unexpected external authorial edit is introduced, and unrelated extra skills are neither flagged nor removed. -- [ ] `CONTEXT.md`, docs, CLI help, and skill text use Repository Skill Set, - Doctor Command, and Roundfix Skill consistently. +- [ ] No protected tooling path changes in this Task. +- [ ] `CONTEXT.md`, docs, and CLI help use Repository Skill Set, Doctor + Command, and Roundfix Skill consistently. ## Context - instruction: `CONTEXT.md` - instruction: `docs/agents/skill-dispatch.md` +- instruction: `.agents/skills/tech-writer/SKILL.md` - interface: `README.md` - interface: `docs/user-guide/commands.md` - interface: `docs/user-guide/usage.md` -- interface: `.agents/skills/roundfix/SKILL.md` -- interface: `.agents/skills/roundfix/agents/openai.yaml` - interface: `skills/skills.go` -- interface: `Makefile` -- interface: `skills-lock.json` ## Verification -- `rtk make skills-sync` — expected: embedded Roundfix-owned skills and - recommended external names regenerate from canonical local authorities. -- `rtk make skills-sync-check` — expected: no canonical/embedded owned-skill or - recommendation drift. -- `rtk go run -buildvcs=false ./cmd/roundfix skills check` — expected: shipped - skill contracts pass with the new Doctor wording. - `rtk git diff --check` — expected: no whitespace errors in docs or manifests. -- `rtk make verify` — expected: formatting, Go tests, setup-context tests, skill - synchronization, shipped skill validation, and build all pass. +- `rtk go test ./internal/cli -run 'Test(CommandUsage|DocumentationContract)' -count=1` + — expected: supported command and documentation contracts describe the + shipped Doctor behavior. ## References diff --git a/docs/specs/0036-doctor-skill-readiness/task_03.md b/docs/specs/0036-doctor-skill-readiness/task_03.md new file mode 100644 index 0000000..a110226 --- /dev/null +++ b/docs/specs/0036-doctor-skill-readiness/task_03.md @@ -0,0 +1,82 @@ +--- +task: task_03 +spec: 0036-doctor-skill-readiness +status: pending +type: docs +complexity: low +--- + +# Task 03: Align the protected Roundfix Skill pair + +## Overview + +Publish Doctor Skill Readiness in the protected Roundfix Skill after the CLI +and public guidance are complete. This is a tooling-only slice: it may change +only the exact canonical/generated files authorized by the maintainer and this +Task file. + +## Requirements + +1. MUST update the Doctor instructions in + `.agents/skills/roundfix/SKILL.md` with the blocking Repository Skill Set + result, ownership-specific remediation, and diagnosis-only behavior. +2. MUST apply the same content to `skills/roundfix/SKILL.md` so the canonical + and generated copies are byte-identical. +3. MUST limit repository mutations to + `.agents/skills/roundfix/SKILL.md`, `skills/roundfix/SKILL.md`, and this + `task_03.md` file. +4. MUST NOT run `make skills-sync`, because that mutation target rewrites every + owned skill directory; `make skills-sync-check` is the read-only sync gate. +5. MUST leave every external skill, lock file, recommendation file, manifest, + code file, and user-documentation file unchanged. + +## Subtasks + +- [ ] Update the canonical Roundfix Skill Doctor contract. +- [ ] Apply the identical change to the generated Roundfix Skill copy. +- [ ] Verify exact changed-file scope and byte identity. + +## Acceptance Criteria + +- [ ] The Roundfix Skill tells an Agent to surface a failed skill check and its + printed remediation before work continues, without claiming Doctor + performs an update. +- [ ] The canonical and generated Roundfix `SKILL.md` files are byte-identical. +- [ ] Git changed-file evidence for this Task contains only the two authorized + Skill files and `task_03.md`. +- [ ] No upstream-managed or other Roundfix-owned skill changes. +- [ ] The shipped skill check and complete repository gate pass. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/skill-dispatch.md` +- instruction: `.agents/skills/tech-writer/SKILL.md` +- interface: `.agents/skills/roundfix/SKILL.md` +- interface: `skills/roundfix/SKILL.md` + +## Verification + +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` + — expected: no output and exit zero. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "docs/specs/0036-doctor-skill-readiness/task_03.md") {print; bad=1}} END {exit bad}'` + — expected: no changed path outside the authorized pair and this Task file. +- `rtk make skills-sync-check` — expected: every canonical/generated owned + skill pair has no drift. +- `rtk go run -buildvcs=false ./cmd/roundfix skills check` — expected: every + shipped Roundfix Skill contract passes. +- `rtk git diff --check` — expected: no whitespace errors. +- `rtk make verify` — expected: formatting, tests, skill synchronization, + shipped skill validation, and build all pass. + +## References + +- `_prd.md` → Goals; Core Feature 6; User Experience; Decisions. +- `_techspec.md` → Documentation and skill synchronization; Build Order 4. +- `task_02.md` → completed public guidance this Skill must match. +- `docs/agents/spec-routing.md` → tooling authorization and changed-file + postflight. + +## Result + +Pending. From d2cf8de435e837e31f6a175b4fc28c7869da2520 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 11:20:32 -0300 Subject: [PATCH 04/48] docs: add constraints to terminal outcome specs --- .../0037-terminal-outcome-integrity/_prd.md | 18 +++++++++++++ .../_techspec.md | 26 +++++++++++++++++- .../_prd.md | 18 +++++++++++++ .../_techspec.md | 26 +++++++++++++++++- .../_prd.md | 20 ++++++++++++++ .../_techspec.md | 27 ++++++++++++++++++- 6 files changed, 132 insertions(+), 3 deletions(-) diff --git a/docs/specs/0037-terminal-outcome-integrity/_prd.md b/docs/specs/0037-terminal-outcome-integrity/_prd.md index 91147a8..b94d801 100644 --- a/docs/specs/0037-terminal-outcome-integrity/_prd.md +++ b/docs/specs/0037-terminal-outcome-integrity/_prd.md @@ -9,6 +9,24 @@ surfaces: [backend, cli, data, docs] A force-stopped Run can currently be completed again by its still-running owner, a Stop Request can remain unnoticed throughout a Review Source wait, and cleanup can target an Agent Session that never reached the active lifecycle. The resulting state is unsafe for users and Supervisors: the Run Database can contradict the Stop Command, the released lock can coexist with live work, and secondary cleanup noise can obscure the primary failure. Prior dogfood evidence was absorbed into this Spec and remains in Git history; the still-open behavior is reproduced by the [Vortex detached-watch finding](../../findings/2026-07-16-vortex-pr87-detached-watch-notification.md). +## Project Constraints + +- Identifier strategy: not applicable — the feature reuses existing Run, + process, Agent Session, and Work Item identities and creates no project-owned + Internal Identifier. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — process control, Run Database + completion, and Stop Request polling change no authentication provider or + HTTP contract. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0022 keeps Stop Requests in the Run + Database, ADR-0044 requires proof before reclaiming an owner, ADR-0051 keeps + Agent Sessions scoped to their Work Items, and ADR-0052 makes terminal + completion compare-and-set. Source: `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`; no other protected tooling mutation is + authorized. Source: + `docs/agents/agent-instructions.md`. + ## Goals - A terminal Run completion cannot be replaced by a competing terminal outcome. diff --git a/docs/specs/0037-terminal-outcome-integrity/_techspec.md b/docs/specs/0037-terminal-outcome-integrity/_techspec.md index 755e64f..363a35d 100644 --- a/docs/specs/0037-terminal-outcome-integrity/_techspec.md +++ b/docs/specs/0037-terminal-outcome-integrity/_techspec.md @@ -10,6 +10,24 @@ created: 2026-07-17 Run completion becomes a guarded store operation: the first non-terminal-to-terminal transition wins, an identical replay is idempotent, and a conflicting terminal result returns a typed conflict without mutating the row or lock. Force Stop reverses its current unsafe order by proving owner-process exit before calling completion. Review Source waits gain the Run Database as a Stop Request source, and Agent Session cleanup queries the durable Agent Selection lifecycle instead of deriving an unconditional Agent Session name. The primary trade-off is availability for safety: a Force Stop that cannot prove owner exit leaves the Run Active and locked, requiring the user to resolve the owner process instead of risking two live writers. +## Project Constraints + +- Identifier strategy: not applicable — all persistence and process operations + reuse existing Run, scope, and Agent Session identities. Source: + `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — the implementation changes local + process, database, and polling boundaries without changing authentication or + HTTP behavior. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0022, ADR-0044, ADR-0051, and + ADR-0052 govern Stop Request transport, owner-death proof, Work Item Agent + Sessions, and terminal compare-and-set behavior. Source: + `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`; no other protected tooling mutation is + authorized. Source: + `docs/agents/agent-instructions.md`. + ## System Architecture - **`internal/store`** extends Run completion with compare-and-set semantics and a typed terminal conflict. It also exposes a guarded Integration Pending reconciliation operation and a query that returns Agent Selection scopes whose latest persisted status is active. @@ -116,7 +134,10 @@ Agent Session cleanup rules: 3. Platform owner-process controller and reordered force-stop CLI flow (depends on: 1, 2). 4. Stop Request source across all Review Source wait boundaries (depends on: 1). 5. Winner-only terminal event/notification wiring and primary-before-secondary diagnostics (depends on: 1, 2, 3, 4). -6. User guide, command help, Roundfix Skill, and spec finding traceability updates (depends on: 3, 4, 5). +6. User guide, command help, and spec finding traceability updates (depends on: 3, 4, 5). +7. Dedicated tooling-only update of + `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md`, with + direct byte-identical edits and read-only sync verification (depends on: 6). ## Risks & Considerations @@ -133,4 +154,7 @@ Rollout is an atomic binary change because no schema or public command syntax ch - Force Stop fails closed until owner exit is proven. - `CompleteRun` remains the ordinary completion boundary and becomes compare-and-set; a separate operation owns the only terminal reconciliation transition. - Existing Agent Selection lifecycle records serve as the Agent Session registry. +- Protected Roundfix Skill publication is isolated in one Task whose changed + files are the two authorized `SKILL.md` paths and its own Task file; it does + not run the broad `make skills-sync` mutation target. - See [ADR-0052](../../adr/0052-run-completion-is-compare-and-set.md). diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/_prd.md b/docs/specs/0038-terminal-run-worktree-reconciliation/_prd.md index 38a57b5..925d206 100644 --- a/docs/specs/0038-terminal-run-worktree-reconciliation/_prd.md +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/_prd.md @@ -9,6 +9,24 @@ surfaces: [backend, cli, data, docs] Terminal spec Runs can retain clean Run Worktrees and Run Branches after their commits have already reached the user's target branch. Roundfix currently compares the Run Branch only with its creation base, hides the residue behind the default Active Run listing, and offers no supported cleanup command. Prior dogfood evidence, now absorbed into this Spec and retained in Git history, showed users having to prove ancestry and run Git commands manually. +## Project Constraints + +- Identifier strategy: not applicable — reconciliation reuses Run IDs, recorded + branches, and Git object identities and creates no project-owned Internal + Identifier. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — reconciliation is confined to the + local Run Database and Git repository and adds no authentication or HTTP + contract. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0023 and ADR-0024 govern Run + Worktrees and porcelain integration, ADR-0052 owns the guarded terminal + transition, and ADR-0053 requires proof-based reconciliation. Source: + `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`; no other protected tooling mutation is + authorized. Source: + `docs/agents/agent-instructions.md`. + ## Goals - Users and Supervisors can distinguish safely integrated terminal Run Worktrees from work that must be preserved. diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/_techspec.md b/docs/specs/0038-terminal-run-worktree-reconciliation/_techspec.md index c00f566..96eada3 100644 --- a/docs/specs/0038-terminal-run-worktree-reconciliation/_techspec.md +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/_techspec.md @@ -10,6 +10,24 @@ created: 2026-07-17 The existing worktree module gains one classifier shared by the new Reconcile Command, the automatic terminal reaper, and Run listing projection. Classification replaces the unsafe creation-base shortcut with two independent proofs: the retained worktree is clean and the Run Branch tip is an ancestor of the recorded target branch tip. The CLI is read-only unless `--apply` is supplied and emits stable text or JSON. The main trade-off is conservative residue: missing refs, missing target metadata, or Git errors remain `unknown` even when a human might infer safety, because preserving redundant storage is cheaper than deleting unique work. +## Project Constraints + +- Identifier strategy: not applicable — the classifier consumes existing Run, + branch, path, and Git object identities without minting a new project-owned + identity. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — all inspection and apply behavior + stays within local Git and the Run Database. Source: + `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0023, ADR-0024, ADR-0052, and + ADR-0053 bind Run Worktree ownership, porcelain integration, the sole guarded + terminal transition, and positive cleanup proof. Source: + `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`; no other protected tooling mutation is + authorized. Source: + `docs/agents/agent-instructions.md`. + ## System Architecture - **`internal/worktree`** owns `RunWorktreeReconciliation`: discovery, cleanliness inspection, ancestry proof, classification, and safe cleanup. `PruneTerminalReport` delegates to this classifier instead of comparing the branch tip only with its reflog creation base. @@ -114,7 +132,10 @@ The note counts Runs with an existing recorded worktree path or Run Branch, not 3. Store reconciliation event wiring and Integration Pending promotion through spec 0037's guarded transition (depends on: 1, 2, spec 0037). 4. Reconcile Command parsing, text output, JSON envelope, dry-run/apply, and exit contracts (depends on: 1, 2, 3). 5. Run listing retained-worktree notes with byte-stable stdout rows (depends on: 1). -6. User guide, command help, Roundfix Skill, CONTEXT vocabulary, and finding traceability (depends on: 4, 5). +6. User guide, command help, CONTEXT vocabulary, and finding traceability (depends on: 4, 5). +7. Dedicated tooling-only update of + `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md`, with + direct byte-identical edits and read-only sync verification (depends on: 6). ## Risks & Considerations @@ -131,4 +152,7 @@ Rollout is additive: the new command and stderr guidance can ship without migrat - Dedicated Reconcile Command, dry-run by default, with one explicit `--apply` mutation flag. - Positive proof requires both clean worktree state and target-branch ancestry. - The automatic reaper and CLI share one classifier. +- Protected Roundfix Skill publication is isolated in one Task whose changed + files are the two authorized `SKILL.md` paths and its own Task file; it does + not run the broad `make skills-sync` mutation target. - See [ADR-0053](../../adr/0053-terminal-run-worktree-reconciliation-is-proof-based.md). diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/_prd.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/_prd.md index 866341a..7ba9de0 100644 --- a/docs/specs/0039-review-source-evidence-and-detached-outcomes/_prd.md +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/_prd.md @@ -9,6 +9,26 @@ surfaces: [backend, cli, docs] Roundfix currently treats several different CodeRabbit states as the same signal: a completed check can mean a real review or an explicit skip, an approval can be ignored when a check is missing, and one transient GitHub failure can end a watch before any Round. When failure happens in a Detached Run, zero issue counts and a context-free notification leave users and Supervisors without the evidence needed to recover. Prior dogfood evidence was absorbed into this Spec and remains in Git history; the still-open behavior is documented by the [Vortex detached-watch finding](../../findings/2026-07-16-vortex-pr87-detached-watch-notification.md). +## Project Constraints + +- Identifier strategy: not applicable — Review Source Evidence reuses Run IDs, + Git heads, and Review Source-native identities and creates no project-owned + Internal Identifier. Source: `docs/agents/domain.md`. +- Authentication and HTTP: applicable — GitHub and CodeRabbit access must + continue through the repository's existing `gh` and Review Source boundaries; + this feature adds no authentication provider, credential policy, or HTTP + route. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0036 preserves the separate + review-artifact commit, ADR-0042 keeps review Runs in the user checkout, + ADR-0043 preserves Clean Unverified, ADR-0052 protects terminal completion, + and ADR-0054 makes head-bound Review Source Evidence authoritative. Source: + `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`; no other protected tooling mutation is + authorized. Source: + `docs/agents/agent-instructions.md`. + ## Goals - Review Source outcomes reflect what CodeRabbit actually did on the relevant head, including an explicit skipped review. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/_techspec.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/_techspec.md index c570111..1c863a5 100644 --- a/docs/specs/0039-review-source-evidence-and-detached-outcomes/_techspec.md +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/_techspec.md @@ -10,6 +10,24 @@ created: 2026-07-17 The CodeRabbit adapter gains one shared evidence classifier used by both the pre-fetch wait and Merge-Ready confirmation, eliminating the current disagreement between `WatchStatus` and `HeadCheck`. The watch state machine gains Review Skipped, bounded transient retry, issue-count knowledge, and phase/deadline projection. Terminal context flows once through the outcome event and notification boundary, while the existing Run Event Stream remains the Supervisor subscription. A narrowly proven Daemon-created artifact-only commit may inherit evidence from its parent. The primary trade-off is CodeRabbit-specific precision: CodeRabbit signal parsing becomes richer, but the generic watch loop consumes Review Source-neutral evidence and does not embed GitHub response shapes. +## Project Constraints + +- Identifier strategy: not applicable — the design carries existing Run, head, + and Review Source-native identities without creating a new Internal + Identifier. Source: `docs/agents/domain.md`. +- Authentication and HTTP: applicable — the CodeRabbit adapter must reuse the + existing authenticated `gh` boundary and preserve repository-owned HTTP and + credential policy. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0036, ADR-0042, ADR-0043, ADR-0052, + and ADR-0054 govern artifact commits, review checkout ownership, Clean + Unverified, terminal completion, and accepted Review Source Evidence. + Source: `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`; no other protected tooling mutation is + authorized. Source: + `docs/agents/agent-instructions.md`. + ## System Architecture - **`internal/reviewsource`** defines Review Source-neutral Evidence and typed transient failures. @@ -159,7 +177,11 @@ A skipped review never calls the Round fetcher and creates no Round directory. A 5. Review Issue knowledge and enriched terminal outcome stream/report (depends on: 3, 4). 6. Notification context, receipts, durable receipt events, and Detached monitor command (depends on: 5). 7. Daemon artifact-commit identity and verified evidence inheritance (depends on: 2, 5). -8. User guide, command help, Roundfix Skill, CONTEXT vocabulary, ADR references, and finding traceability (depends on: 3, 4, 6, 7). +8. User guide, command help, CONTEXT vocabulary, ADR references, and finding + traceability (depends on: 3, 4, 6, 7). +9. Dedicated tooling-only update of + `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md`, with + direct byte-identical edits and read-only sync verification (depends on: 8). ## Risks & Considerations @@ -179,4 +201,7 @@ Rollout ships the state reader, writer, reporting, and schema-version fence in o - Existing timeout, polling, and Run Budget settings bound retry; no new config is added. - Existing Run Event Stream is the Supervisor subscription; notification receipts remain Run Events. - Exact Daemon artifact-only descendants inherit verified parent evidence while ADR-0036 remains in force. +- Protected Roundfix Skill publication is isolated in one Task whose changed + files are the two authorized `SKILL.md` paths and its own Task file; it does + not run the broad `make skills-sync` mutation target. - See [ADR-0054](../../adr/0054-review-source-evidence-determines-review-outcomes.md). From c664fc822a89780d91f153f61735a27b47c70320 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 11:20:42 -0300 Subject: [PATCH 05/48] docs: isolate protected skill alignment tasks --- .../_prd.md | 27 ++++++ .../_tasks.md | 8 +- .../_techspec.md | 51 ++++++++-- .../task_02.md | 4 +- .../task_05.md | 2 +- .../task_06.md | 2 +- .../task_07.md | 44 +++++---- .../task_08.md | 94 +++++++++++++++++++ 8 files changed, 199 insertions(+), 33 deletions(-) create mode 100644 docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_08.md diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_prd.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_prd.md index aa5cae5..f9eb458 100644 --- a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_prd.md +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_prd.md @@ -23,6 +23,26 @@ failures one bounded exclusive retry. Projects keep their full Verification commands unchanged while concurrent Agents can continue preparing independent Tasks. +## Project Constraints + +- Identifier strategy: not applicable — capacity, retry, and Task settlement + reuse existing Run, Task, Batch, and Verification attempt identities and + create no project-owned Internal Identifier. Source: + `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — the feature is confined to local + configuration, Agent Sessions, Verification processes, Run Events, and TUI + projection. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0014 preserves Daemon-run + Verification, ADR-0025 preserves Task Graph readiness, ADR-0038 preserves one + Agent repair, ADR-0051 routes each Task through its Task Type-selected Agent + Session, and ADR-0056 plus ADR-0057 define capacity and Task-status ownership. + Source: `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md`, + `skills/roundfix/SKILL.md`, `.agents/skills/implement-task/SKILL.md`, and + `skills/implement-task/SKILL.md`; no other protected tooling mutation is + authorized. Source: `docs/agents/agent-instructions.md`. + ## Goals - Let multiple independent Tasks perform Agent work concurrently without @@ -208,6 +228,13 @@ Feedback contract. machine-wide coordination service. - Preserve the full Verification gate and diagnose resource ownership at its source rather than weakening tests or extending timeouts. +- Keep backend, frontend, and documentation Tasks in this Task Graph. ADR-0051 + governs per-Task Agent Session selection by Task Type, superseding older + one-Agent-per-Run and separate-frontend-Spec guidance; Task 07 aligns that + guidance and the Agent Session glossary. +- Isolate all protected Skill changes in one tooling-only Task bounded to the + four expressly authorized canonical/generated `roundfix` and + `implement-task` `SKILL.md` files. - See [ADR-0056](../../adr/0056-spec-runs-separate-task-and-verification-capacity.md). - See [ADR-0057](../../adr/0057-daemon-exclusively-owns-implement-task-status.md). diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_tasks.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_tasks.md index f294b28..3b782f6 100644 --- a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_tasks.md +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_tasks.md @@ -24,6 +24,9 @@ graph: - id: task_07 file: task_07.md needs: [task_01, task_02, task_03, task_04, task_05, task_06] + - id: task_08 + file: task_08.md + needs: [task_01, task_02, task_03, task_04, task_05, task_06, task_07] --- # Tasks — Verification Capacity and Daemon Task Settlement @@ -36,6 +39,7 @@ graph: | task_04 | Retry Temporary Verification Failure under exclusive capacity | backend | high | task_03 | | task_05 | Render per-Task Verification phases in the Live Run View | frontend | medium | task_01, task_02, task_03, task_04 | | task_06 | Prove the integrated capacity and settlement contract | test | high | task_01, task_02, task_03, task_04, task_05 | -| task_07 | Align operator docs and shipped Agent Skills | docs | medium | task_01, task_02, task_03, task_04, task_05, task_06 | +| task_07 | Align operator docs and prepare Agent Skill wording | docs | medium | task_01, task_02, task_03, task_04, task_05, task_06 | +| task_08 | Align the protected authorial Skill pairs | docs | low | task_01, task_02, task_03, task_04, task_05, task_06, task_07 | -Waves: 1 → task_01 · 2 → task_02 · 3 → task_03 · 4 → task_04 · 5 → task_05 · 6 → task_06 · 7 → task_07 +Waves: 1 → task_01 · 2 → task_02 · 3 → task_03 · 4 → task_04 · 5 → task_05 · 6 → task_06 · 7 → task_07 · 8 → task_08 diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_techspec.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_techspec.md index 04645dd..bfacb08 100644 --- a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_techspec.md +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/_techspec.md @@ -21,6 +21,24 @@ The accepted trade-off is per-Run rather than machine-wide coordination: it solves contention caused by one Implement Run without introducing a daemon service or claiming control over external processes. +## Project Constraints + +- Identifier strategy: not applicable — the design extends existing Run Event + and Verification-attempt records without minting a new project-owned + identity. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — all new behavior remains inside + local configuration, Agent Session, Verification, Run Event, and TUI + boundaries. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0014, ADR-0025, ADR-0038, ADR-0051, + ADR-0056, and ADR-0057 bind Daemon Verification, Task readiness, bounded + repair, per-Task Agent Selection, independent capacity, and exclusive Task + status authorship. Source: `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26, the maintainer expressly + authorizes changes to exactly `.agents/skills/roundfix/SKILL.md`, + `skills/roundfix/SKILL.md`, `.agents/skills/implement-task/SKILL.md`, and + `skills/implement-task/SKILL.md`; no other protected tooling mutation is + authorized. Source: `docs/agents/agent-instructions.md`. + ## System Architecture - **`internal/config`** gains a top-level `Verification` configuration section @@ -260,10 +278,12 @@ or top-level exit code changes. - **Bubble Tea v2:** the current synchronous model/event refresh seam derives labels. No terminal emulation, new keybinding, panel reflow, or mouse change is required. -- **Roundfix skills:** `.agents/skills/roundfix` remains canonical and - `skills/roundfix` is generated through the existing sync gate. The - repository-owned `implement-task` skill may be edited under skill - governance; upstream-managed skills remain untouched. +- **Roundfix skills:** one final tooling-only Task owns the authorized + `.agents/skills/implement-task/SKILL.md`, + `skills/implement-task/SKILL.md`, `.agents/skills/roundfix/SKILL.md`, and + `skills/roundfix/SKILL.md` changes. It updates each canonical/generated pair + directly and byte-identically without running the broad `make skills-sync` + mutation target. Upstream-managed skills remain untouched. ## Testing Approach @@ -306,8 +326,8 @@ No dependency beyond the standard library is introduced. 1. Add Verification configuration, generated/default documentation, TaskPlan plumbing, and capacity fields in the Task-cycle-start event. -2. Move Implement Task status ownership and Agent prompt/authorial skill - contracts fully to the Daemon (depends on: 1). +2. Move Implement Task status ownership and Agent prompt contracts fully to + the Daemon (depends on: 1). 3. Add the cancellation-aware shared/exclusive Verification gate, waiting events, shared attempt acquisition, and concurrency tests (depends on: 1). 4. Add typed exit-75 classification, distinct retry artifacts, the @@ -317,9 +337,12 @@ No dependency beyond the standard library is introduced. Run View capacity output (depends on: 1, 3, 4). 6. Add Bubble Tea per-Task phase derivation and accessible labels from interleaved Task/Verification events (depends on: 2, 3, 5). -7. Update configuration/usage guidance, canonical Roundfix Skill, embedded - skill copy, ADR/finding traceability, and end-to-end QA fixtures (depends - on: 2–6). +7. Update configuration/usage guidance, align `docs/agents/autonomous-work.md` + and the `CONTEXT.md` Agent Session definition with ADR-0051, preserve + ADR/finding traceability, and add end-to-end QA fixtures (depends on: 2–6). +8. In one dedicated tooling-only Task, update the exact four authorized + canonical/generated Roundfix and `implement-task` Skill files, then run + read-only sync and full repository verification (depends on: 7). ## Risks & Considerations @@ -345,6 +368,10 @@ No dependency beyond the standard library is introduced. demand a full gate before a completion claim. The prompt must state that the Agent is not claiming Task completion: focused checks support handoff and the Daemon performs the mandatory full Verification before settlement. +- Current autonomous-work guidance and the Agent Session glossary still + describe one Agent per Run and a separate frontend Spec, contrary to + ADR-0051. Task 07 corrects those documentation targets; it must not remove + the frontend Task from this graph or weaken per-Task Task Type routing. ## Decisions @@ -360,5 +387,11 @@ No dependency beyond the standard library is introduced. Task and Verification capacity fields. - Derive task-level UI truth from the Run Event Journal rather than multiplying aggregate Run states. +- Apply ADR-0051 per-Task Agent Session selection to this mixed Task Graph; + older one-Agent-per-Run and separate-frontend-Spec prose is a documentation + defect, not a reason to split or retype Task 05. +- Keep protected Skill mutations in the dedicated final tooling Task; code, + tests, operator docs, manifests, and other Skill files remain outside that + Task's changed-file scope. - See [ADR-0056](../../adr/0056-spec-runs-separate-task-and-verification-capacity.md). - See [ADR-0057](../../adr/0057-daemon-exclusively-owns-implement-task-status.md). diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_02.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_02.md index effc4a5..6423bb6 100644 --- a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_02.md +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_02.md @@ -34,6 +34,8 @@ Result evidence, and continue into authoritative Daemon Verification. 7. MUST preserve one same-Session Verification Feedback turn, failed Task Worktree retention, independent Task continuation, dependency blocking, and the Settle Command contract. +8. MUST leave every protected tooling path unchanged; Task 08 owns the isolated + authorial Skill update after code and public guidance are complete. ## Subtasks @@ -41,7 +43,6 @@ Result evidence, and continue into authoritative Daemon Verification. - [ ] Replace Agent status/verdict instructions with implementation-ready handoff. - [ ] Normalize reloaded Agent status without losing Result content. - [ ] Remove the Agent-authored failed-status Verification bypass. -- [ ] Update the repository-owned implement-task workflow contract. - [ ] Replace bypass tests with authoritative Verification and failure-path cases. ## Acceptance Criteria @@ -58,6 +59,7 @@ Result evidence, and continue into authoritative Daemon Verification. status and preserves resumable evidence. - [ ] Prompt and authorial skill tests forbid declared Verification/status authorship while still allowing focused checks. +- [ ] No protected tooling path changes in this Task. - [ ] Existing repair, dependency, worktree retention, and Settle Command regression tests pass. diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_05.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_05.md index 6e387e7..5e8056f 100644 --- a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_05.md +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_05.md @@ -72,7 +72,7 @@ derive Task truth from journal events. - instruction: `.agents/skills/golang-testing/SKILL.md` - instruction: `.agents/skills/testing-boss/SKILL.md` - interface: `internal/cli/attach.go` -- interface: `internal/cli/attach_test.go` +- interface: `internal/cli/cli_test.go` - interface: `internal/cli/runbrowser.go` - interface: `internal/tui/tui.go` - interface: `internal/tui/tui_test.go` diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_06.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_06.md index 2ed38f8..50371ec 100644 --- a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_06.md +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_06.md @@ -78,7 +78,7 @@ replacement for them or a mock-only confidence layer. - interface: `internal/cli/cli_test.go` - interface: `internal/daemon/task_engine_test.go` - interface: `internal/daemon/daemon_test.go` -- interface: `internal/runevent/stream_test.go` +- interface: `internal/runevent/event_test.go` - interface: `internal/tui/cockpit_test.go` - interface: `internal/worktree/worktree_test.go` diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_07.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_07.md index 8df8ad0..717d7c1 100644 --- a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_07.md +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_07.md @@ -6,7 +6,7 @@ type: docs complexity: medium --- -# Task 07: Align operator docs and shipped Agent Skills +# Task 07: Align operator docs and prepare Agent Skill wording ## Overview @@ -30,24 +30,31 @@ must match shipped behavior and preserve skill ownership boundaries. and the prohibition on log heuristics. 5. MUST document Run Event Stream and Live Run View working, waiting, verifying, retry, and capacity evidence with profile-led command examples. -6. MUST update the canonical Roundfix Skill and repository-owned - `implement-task` skill, regenerate the embedded Roundfix Skill, and leave - every upstream-managed skill untouched. +6. MUST leave every protected tooling and upstream-managed Skill path + untouched; Task 08 owns the isolated authorial Skill update. 7. MUST preserve ADR, finding, PRD, Tech Spec, glossary, and skill ownership and synchronization traceability and keep all repository content in English. -8. MUST run the complete repository verification gate; any format, test, skill - synchronization, or build failure is blocking. +8. MUST treat the complete Daemon-owned repository verification gate as + blocking; any format, test, Skill synchronization, or build failure prevents + settlement. +9. MUST align `docs/agents/autonomous-work.md` and the `CONTEXT.md` Agent + Session definition with ADR-0051: each Task owns a Task Type-selected Agent + Session, frontend Tasks remain in the same mixed Task Graph, and current + Agent Selection Profile defaults remain authoritative. +10. MUST prepare exact canonical wording for Task 08 without editing its four + protected Skill targets. ## Subtasks - [ ] Update configuration, command, usage, and autonomous-work guidance. -- [ ] Align implementation-ready handoff and Daemon Verification instructions. +- [ ] Verify implementation-ready handoff and Daemon Verification instructions. - [ ] Document exit-75 project ownership, exclusive retry, and exhaustion. - [ ] Document event and Live Run View capacity/phase evidence. -- [ ] Update canonical Roundfix and repository-owned workflow skills. -- [ ] Regenerate embedded skills and verify ownership boundaries. -- [ ] Resolve links, terminology, examples, and the full repository gate. +- [ ] Align autonomous-work routing and the Agent Session glossary with + ADR-0051. +- [ ] Prepare the authorial Skill wording and hand it off to Task 08. +- [ ] Resolve links, terminology, and examples. ## Acceptance Criteria @@ -62,8 +69,10 @@ must match shipped behavior and preserve skill ownership boundaries. wrapper and promises only one observable exclusive retry. - [ ] Events/Attach examples use canonical phase names and keep requested command output separate from diagnostics. -- [ ] Canonical and embedded Roundfix Skills are synchronized and their tests - enforce the new configuration, status, and retry wording. +- [ ] Autonomous-work guidance and the Agent Session glossary route every Task + through its Task Type-selected Agent Session, keep frontend Task 05 in + this graph, and contain no superseded one-Agent-per-Run rule. +- [ ] No protected tooling path changes in this Task. - [ ] No upstream-managed skill changes and every ADR/finding/Spec link resolves. - [ ] `make verify` passes completely after all documentation and generated skill changes. @@ -80,17 +89,14 @@ must match shipped behavior and preserve skill ownership boundaries. - interface: `docs/user-guide/configuration.md` - interface: `docs/user-guide/commands.md` - interface: `docs/user-guide/usage.md` -- interface: `.agents/skills/roundfix/SKILL.md` -- interface: `.agents/skills/roundfix/agents/openai.yaml` -- interface: `skills/roundfix/SKILL.md` ## Verification -- `rtk go test ./internal/cli -run 'Test(CommandUsage|DocumentationContract|RoundfixSkill|ImplementTaskSkill)' -count=1` — expected: user guidance, command examples, manifests, and shipped skill contracts match implemented behavior. -- `rtk make skills-sync-check` — expected: canonical and embedded Roundfix-owned skills have no drift. -- `rtk go run -buildvcs=false ./cmd/roundfix skills check` — expected: every shipped Roundfix Skill contract passes. +- `rtk go test ./internal/cli -run 'Test(CommandUsage|DocumentationContract)' -count=1` — expected: user guidance and command examples match implemented behavior. - `rtk git diff --check` — expected: Spec, ADR, finding, documentation, and generated skill files contain no whitespace errors. -- `rtk make verify` — expected: formatting, Go tests, setup-context checks, skill synchronization, shipped skill validation, and build all pass. +- `rtk make verify` — expected: formatting, Go tests, setup-context checks, + current skill synchronization, shipped skill validation, and build all pass + before the protected wording handoff. ## References diff --git a/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_08.md b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_08.md new file mode 100644 index 0000000..95cb1f0 --- /dev/null +++ b/docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_08.md @@ -0,0 +1,94 @@ +--- +task: task_08 +spec: 0042-verification-capacity-and-daemon-task-settlement +status: pending +type: docs +complexity: low +--- + +# Task 08: Align the protected authorial Skill pairs + +## Overview + +Publish the completed Daemon-owned status, Verification Capacity, temporary +failure, and Task Type-routing contracts in the two protected authorial Skills. +This is a tooling-only slice bounded to the four exact canonical/generated +files authorized by the maintainer and this Task file. + +## Requirements + +1. MUST align `.agents/skills/implement-task/SKILL.md` with the + implementation-ready handoff: the Agent does not edit Task status, run the + declared `## Verification`, or claim the terminal Task verdict. +2. MUST align `.agents/skills/roundfix/SKILL.md` with independent capacities, + Daemon-owned settlement, observable Verification phases, exit `75`, one + exclusive retry, and ADR-0051 Task Type-selected Agent Sessions. +3. MUST apply identical content to `skills/implement-task/SKILL.md` and + `skills/roundfix/SKILL.md`. +4. MUST limit repository mutations to those four authorized `SKILL.md` files + and this `task_08.md` file. +5. MUST NOT run `make skills-sync`, because it rewrites every owned Skill + directory; use `make skills-sync-check` as read-only verification. +6. MUST leave code, tests, configuration, manifests, public docs, other + Roundfix-owned Skills, upstream-managed Skills, and lock files unchanged. + +## Subtasks + +- [ ] Update the canonical `implement-task` Skill. +- [ ] Apply the identical generated `implement-task` copy. +- [ ] Update the canonical Roundfix Skill. +- [ ] Apply the identical generated Roundfix copy. +- [ ] Verify changed-file scope, byte identity, shipped Skill contracts, and + full-gate compatibility. + +## Acceptance Criteria + +- [ ] No supported Skill tells an Implement Agent to run declared Task + Verification, edit Task status, or settle its terminal verdict. +- [ ] The Roundfix Skill describes both capacities, the exit-75 retry contract, + and per-Task Task Type routing consistently with shipped behavior. +- [ ] Each canonical/generated Skill pair is byte-identical. +- [ ] Git changed-file evidence for this Task contains only the four authorized + Skill files and `task_08.md`. +- [ ] No other protected or upstream-managed tooling change. +- [ ] Shipped Skill validation and the complete repository gate pass. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/skill-dispatch.md` +- instruction: `docs/agents/autonomous-work.md` +- interface: `.agents/skills/implement-task/SKILL.md` +- interface: `skills/implement-task/SKILL.md` +- interface: `.agents/skills/roundfix/SKILL.md` +- interface: `skills/roundfix/SKILL.md` + +## Verification + +- `rtk cmp .agents/skills/implement-task/SKILL.md skills/implement-task/SKILL.md` + — expected: no output and exit zero. +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` + — expected: no output and exit zero. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/implement-task/SKILL.md" && path != "skills/implement-task/SKILL.md" && path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "docs/specs/0042-verification-capacity-and-daemon-task-settlement/task_08.md") {print; bad=1}} END {exit bad}'` + — expected: no changed path outside the four authorized Skill files and this + Task file. +- `rtk make skills-sync-check` — expected: every canonical/generated owned + Skill pair has no drift. +- `rtk go run -buildvcs=false ./cmd/roundfix skills check` — expected: every + shipped Roundfix Skill contract passes. +- `rtk git diff --check` — expected: no whitespace errors. +- `rtk make verify` — expected: formatting, tests, Skill synchronization, + shipped Skill validation, and build all pass. + +## References + +- `_prd.md` → Core Feature 10; Decisions; Project Constraints. +- `_techspec.md` → Integration Points; Build Order 8; Decisions. +- `task_02.md` → implemented Daemon status and handoff contract. +- `task_07.md` → completed operator wording and ADR-0051 alignment. +- `docs/agents/spec-routing.md` → tooling authorization and changed-file + postflight. + +## Result + +Pending. From 9d456de1eae44a51e01daddc994d5a89c096d707 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 11:20:53 -0300 Subject: [PATCH 06/48] docs: report baseline profile refresh retention gap --- ...-baseline-profile-refresh-retention-gap.md | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 docs/findings/2026-07-26-baseline-profile-refresh-retention-gap.md diff --git a/docs/findings/2026-07-26-baseline-profile-refresh-retention-gap.md b/docs/findings/2026-07-26-baseline-profile-refresh-retention-gap.md new file mode 100644 index 0000000..ead6123 --- /dev/null +++ b/docs/findings/2026-07-26-baseline-profile-refresh-retention-gap.md @@ -0,0 +1,283 @@ +--- +status: pending +created_at: 2026-07-26 +updated_at: 2026-07-26 +--- + +# Context-Driven Baseline — profile refresh applied without semantic retention accounting (2026-07-26) + +A live Baseline update in `/Users/marcio/dev/fluxus` correctly detected that +the existing Setup Manifest referenced a changed Baseline Profile, but the +approved Change Plan contained zero Upgrade Retention Contract entries while +replacing managed agent guidance. Apply verified every postimage, so the +transaction was internally consistent, but that success did not prove that +the previous Normative Clauses remained represented or were explicitly +rejected. + +This report follows the earlier +[live-adoption finding](../docs/findings/2026-07-23-setup-context-driven-adoption-process-improvements.md) +and +[greenfield acceptance target](../docs/findings/2026-07-24-greenfield-agent-guidance-acceptance-target.md). +It records a profile-refresh path that those reports did not cover: the +Baseline identity stayed the same while its embedded Profile and catalog +digests changed. + +## Session evidence + +- Command: `roundfix baseline` from `/Users/marcio/dev/fluxus` on `main`. +- Initial state: + `incompatible — the existing Setup Manifest references an unavailable or changed Baseline Profile`. +- Instruction-preservation choice: `Preservation`. +- Baseline Profile: `standard-typescript-monorepo`. +- Repository extension remained disabled: + `repository.extension.enabled=false`. +- Previous Profile digest: + `sha256:c6e3009029dfdead6762af19b8eb94b1a6668e9e718d835992e9473daa6a9882`. +- Applied Profile digest: + `sha256:b75ff11efd71c241ed1f521c6576beb61c24b99d327f4ba4e8bd722ddd2e2d19`. +- Previous catalog digest: + `sha256:aee2a3768e7ef9d3e2dd6a29c60d73daf05b055f5e1683ed40b9c6509c7f9401`. +- Applied catalog digest: + `sha256:f082de03ea52e1b17e75daf0d65ef2d535f8d8d36951fe08dd435732e4008c1e`. +- Approved Plan Digest: + `sha256:eca3b27d5c08742edcbfe4382bced8daaa44f139686e6b9417ff3cb92f851fc9`. +- Apply result: `Baseline apply: verified`, with 15 verified postimages. +- Change Plan: 13 file paths, including the immutable root backup + `AGENTS.d199b5dadd21d8187af411a1111578d4231043c3d9354893aaaef6e1131d2a3f.md`. +- Upgrade Retention Contract ledger: `0 entries`. +- The managed-file diff contained 244 additions and 378 deletions across + `AGENTS.md` and 11 `docs/agents/` files. +- Apply did not run the formatter, build, tests, or selected repository + Verification. It reported them as recommendations. + +## 1. The unchanged Baseline identity bypasses retention when Profile semantics change + +- Symptom / evidence: The interactive workflow declared the Setup Manifest + incompatible because the Profile was unavailable or changed. The same plan + then emitted an empty Upgrade Retention Contract ledger and allowed managed + guidance to change. +- Root cause: `resolvePlanRetention` in + [`internal/baseline/plan.go`](../internal/baseline/plan.go) treats the + Manifest as retention-compatible when its schema, version, and + `generator.baseline` match the selected Profile identifier. It returns + before calling `currentSetupManifestProfileIsValid`, which is the check that + compares the catalog and Profile digests. Both the previous and current + manifests use + `baseline.standard-typescript-monorepo-0.0.1`, so changed Profile semantics + do not require a maintained transition. +- Action / suggestion: Make full Setup Manifest validity the first + compatibility test. A matching Baseline identifier must not bypass + retention when `profileDigest`, `catalogDigest`, managed clause identities, + or managed artifact digests changed. Require a transition keyed by the + source tuple `(generator.baseline, profileDigest, catalogDigest)` or stop + planning with an action-required result. + +Proposed invariant: + +> An incompatible Setup Manifest may produce a ready update plan only when +> every previous managed Normative Clause has one explicit retention +> disposition. + +Add a regression fixture where the Baseline identifier remains unchanged, +the embedded Profile digest changes, and one managed clause disappears. The +plan must either contain complete retention accounting or exit action-required; +an empty retention ledger must never be ready. + +## 2. Preservation does not account for the managed nested guides being replaced + +- Symptom / evidence: The operator selected Preservation, but the plan asked + for no clause classifications and reported zero retained entries. It also + repeated `baseline.inventory.nested-carrier-conflict` for every generated + guide under `docs/agents/`, including files the same plan updated. +- Root cause: [`internal/baseline/repository.go`](../internal/baseline/repository.go) + classifies every path under `docs/agents/` as an instruction carrier, then + warns for every nested carrier except the recognized repository-extension + paths. The preservation tests explicitly keep nested sources out of the + Source Baseline. The inventory does not distinguish: + - a current Manifest-owned generated guide; + - a stale Manifest-owned generated guide; + - a repository extension; + - an unmanaged nested instruction carrier. +- Action / suggestion: Classify carriers against the existing Setup Manifest + before emitting warnings. Do not emit a nested-carrier conflict for a + current managed artifact whose digest matches the Manifest. Treat a stale + managed artifact as upgrade input and require clause-level retention. Keep + the current warning for unmanaged nested carriers and repository-authored + bytes outside setup-owned markers. + +The warning text must also describe the real write boundary. A file that +contains a managed block can change while its repository-authored bytes remain +untouched. Replace `nested carrier remains unchanged` with a diagnostic that +states which bytes are managed, which bytes are preserved, and which bytes +were excluded from retention accounting. + +## 3. File-level hashes hide removed or weakened Normative Clauses + +- Symptom / evidence: The consolidated review showed exact before/after file + hashes and managed entry identifiers, but it did not show the semantic + effect of the update. The resulting diff removed or compressed rules for: + - backend dependency direction, thin HTTP handlers, and persistence + boundaries; + - frontend system boundaries and side-effect-free public entries; + - detailed Task ownership, QA readiness, and archive sequencing; + - Secondbrain degraded-mode behavior; + - several documentation lifecycle details. +- Root cause: Managed entry accounting proves byte ownership and transaction + integrity, but it does not compare the prior and next clause catalogs for + semantic continuity. The current Change Plan can therefore be exact without + making rule removal visible at the level the maintainer must approve. +- Action / suggestion: Add a `Normative Clause changes` section before final + confirmation. For every previous clause, show one of: + `retained`, `moved`, `replaced`, `repository-document`, + `repository-extension`, `reasoned-rejection`, or `unaccounted`. + Include enforcement-strength changes and the new semantic owner. + +Final confirmation must report: + +```text +Prior clauses: +Accounted clauses: +Unaccounted clauses: +Repository-specific classifications requiring review: +``` + +Do not offer Apply while `Unaccounted clauses` is greater than zero. + +## 4. Executable capability discovery rejects common package-manager symlinks + +- Symptom / evidence: Profile alignment reported missing `rtk` and Docker + evidence. In the same environment, `rtk` resolved to + `/opt/homebrew/bin/rtk` and Docker to `/usr/local/bin/docker`. Both paths are + executable symlinks: + - `/opt/homebrew/bin/rtk -> ../Cellar/rtk/0.43.0/bin/rtk`; + - `/usr/local/bin/docker -> /Applications/Docker.app/Contents/Resources/bin/docker`. +- Root cause: + [`lookPathWithoutExecution`](../internal/baseline/profile_alignment.go) + uses `os.Lstat` and accepts only a regular executable file. It rejects the + symlink itself without inspecting a bounded symlink chain or reporting why + the candidate was insufficient. Homebrew and Docker Desktop commonly expose + executables through symlinks. +- Action / suggestion: Discover the PATH candidate without executing it, then + resolve its symlink chain and validate that the final target is a regular + executable file. Reject cycles, missing targets, non-executable targets, and + unsupported file types with an explicit evidence diagnostic. Record both + the PATH candidate and resolved target. + +Add coverage for: + +- a direct regular executable; +- a relative symlink to a regular executable; +- an absolute symlink to a regular executable; +- a broken symlink; +- a symlink cycle; +- a regular non-executable target. + +The `rg` warning could not be classified from the interactive transcript +alone because the user's terminal PATH may differ from the Agent environment. +Capability output must expose the inspected PATH candidate or state that no +candidate was found, so maintainers can distinguish missing software from a +rejected candidate. + +## 5. Optional and recommended divergences read like required remediation + +- Symptom / evidence: Profile alignment was `ready`, but each optional or + recommended divergence used imperative next-action text. This made Docker, + Inngest, `rg`, and `rtk` appear to be unfinished adoption work even though + none blocked planning or Apply. +- Root cause: The text presentation does not lead with requirement strength + and completion impact. `capability.optional.missing` and + `capability.recommended.missing` are technically distinct, but their + remediation language resembles a blocking diagnostic. +- Action / suggestion: Group divergences under `Blocking`, `Advisory`, and + `Informational`. For every advisory, render `Does not block Baseline + readiness or Apply` before any optional next action. Do not recommend adding + Inngest evidence unless the Profile module is selected for repository use. + +## 6. The workspace Verification expectation duplicates a valid selected gate + +- Symptom / evidence: The Profile expected `bun run verify`, while Fluxus + selected and declared `rtk make verify`. The Setup Manifest correctly marked + the selected gate as repository-executable and the portable workspace + command as unresolved, but the interactive result still presented the + latter as a divergence. +- Root cause: The Profile owns a fixed workspace command, while the + `verification.gate` decision is stored as a separate role. There is no typed + mapping that lets the selected repository gate satisfy the portable + workspace Verification role. +- Action / suggestion: Let the maintainer map each portable Verification role + to a declared repository command during alignment. If the selected + repository gate intentionally serves the workspace role, persist that + mapping instead of requiring a duplicate package script. Keep unmapped + profile expectations advisory and never present them as commands Baseline + executed. + +## 7. Apply success does not distinguish byte verification from semantic readiness + +- Symptom / evidence: The final headline was `Baseline apply: verified`, + followed by repeated retention warnings and unrun repository Verification + recommendations. The headline can be read as proof that the update is fully + accepted even though it proves only that approved postimages were written + and verified. +- Root cause: One success word covers transaction integrity, Profile + alignment, semantic retention, idempotence, and repository Verification, + although these are separate states. +- Action / suggestion: Render a final status matrix: + +```text +Approved postimages: verified +Semantic retention: verified | action required +Profile alignment: ready | blocked, with advisory count +Repository Verification: passed | failed | not run +Idempotence check: passed | not run +``` + +Use `Baseline update complete` only when semantic retention is verified and +the completion contract's idempotence step has passed. Keep repository +Verification as an explicit external result because Baseline must not execute +it during Apply. + +## Recommended implementation order + +1. Block same-identity Profile drift when retention accounting is absent. +2. Add a regression fixture for changed Profile and catalog digests with an + unchanged Baseline identifier. +3. Separate managed, stale-managed, repository-owned, and unmanaged nested + carriers. +4. Add the clause-level semantic delta to interactive and JSON plans. +5. Accept safe executable symlinks and expose rejected-candidate evidence. +6. Clarify advisory presentation and Verification role mapping. +7. Split the final result into transaction, retention, alignment, + Verification, and idempotence states. + +## What worked — keep + +- The interactive workflow detected the changed Profile instead of silently + treating the old digest as current. +- Final mutation required approval of the exact Plan Digest. +- Apply verified exact postimages and created the immutable root carrier + backup. +- File changes appeared before the complete managed-entry ledger. +- Optional and recommended capability gaps did not block a ready Profile. +- Formatter, build, tests, and repository Verification remained + recommendations; Apply did not execute repository commands. +- The repeated nested-carrier warnings exposed the unsafe area, even though + their classification and wording need correction. + +## Suggested acceptance checks + +- Given an existing Manifest with the same Baseline identifier and an older + Profile digest, when a managed clause changes, then a plan with empty + retention exits action-required. +- Given a Manifest-owned guide whose digest matches, profile alignment emits + no nested-carrier conflict for that guide. +- Given an unmanaged nested instruction carrier, planning keeps it unchanged + and emits one path-specific conflict warning. +- Given `rtk` or Docker installed through a valid executable symlink, + capability discovery reports satisfied evidence without executing the + binary. +- Given a selected repository Verification gate mapped to the workspace role, + the portable `bun run verify` expectation does not remain an unresolved + divergence. +- Given successful Apply with repository Verification and idempotence not run, + the result reports verified postimages without claiming the Baseline update + complete. From 1865d12c0dc52d471f4b9a812266f34d780c82fc Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 11:24:42 -0300 Subject: [PATCH 07/48] docs: report baseline capability remediation gaps --- ...-vortex-baseline-capability-remediation.md | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 docs/findings/2026-07-26-vortex-baseline-capability-remediation.md diff --git a/docs/findings/2026-07-26-vortex-baseline-capability-remediation.md b/docs/findings/2026-07-26-vortex-baseline-capability-remediation.md new file mode 100644 index 0000000..ac2623c --- /dev/null +++ b/docs/findings/2026-07-26-vortex-baseline-capability-remediation.md @@ -0,0 +1,260 @@ +--- +status: pending +created_at: 2026-07-26 +updated_at: 2026-07-26 +--- + +# Context-Driven Baseline — capability divergences do not carry enough evidence to be remediated (2026-07-26) + +A Baseline alignment against `standard-typescript-monorepo` in +`/Users/marcio/dev/vortex` returned `action_required` with two blocking and +five advisory divergences. Every divergence was individually correct, but the +report did not carry enough information to act on any of them. Resolving the +two blocking items required reading +`internal/baseline/assets/profiles/standard-typescript-monorepo.json` and +`internal/baseline/profile_alignment.go` to learn what each probe actually +inspects. That source reading, not the remediation itself, was the dominant +cost of the session. + +The three divergence classes in this run needed three unrelated responses: one +needed a documentation file, one needed a real dependency migration, and five +needed nothing at all. Prompt 16 offered three whole-repository options, none +of which was "fix the repository and re-run". + +This report corroborates and extends +[the fluxus profile-refresh finding](2026-07-26-baseline-profile-refresh-retention-gap.md). +Findings 4, 5, and 6 of that report (symlink capability discovery, advisory +presentation, workspace Verification mapping) reproduced here in a second +repository and a different environment. Only the new observations are +expanded below; section 6 of this report closes the open `rg` classification +question that report left unresolved. + +## Session evidence + +- Roundfix `0.0.1 (8bcd61f, built 2026-07-26 10:10:16 -0300)`, macOS 25.5.0. +- Repository `/Users/marcio/dev/vortex`, profile `standard-typescript-monorepo`, + state `action_required`. +- Blocking: `capability.stack.logtape` (`capability.required.missing`), + `capability.stack.postgresql` (`capability.contract.missing`). +- Advisory: `capability.optional.docker`, `capability.rg`, `capability.rtk`, + `verification.build`, `verification.workspace`. +- Repository facts at alignment time: backend used `pino ^10.3.1` and + `winston ^3.19.0` behind a `Logger` port with three adapters; `postgres + ^3.4.9` with Drizzle and a `postgres:18-alpine` compose service; no + `DATABASE.md` and no `docs/architecture/` directory; root `package.json` + declared `fmt`, `lint`, `test`, and `typecheck` but neither `build` nor + `verify`; `make verify` existed and was the selected `verification.gate`. +- `roundfix baseline plan --profile standard-typescript-monorepo --format json` + exited `3` on missing decisions and never reached capability evaluation. + +## 1. Divergence output does not name the probe it evaluated + +- Symptom / evidence: `capability.stack.logtape` reported "LogTape has no + compatible local evidence" and `capability.stack.postgresql` reported + "PostgreSQL implementation evidence was found, but the required repository + contract is absent". Neither message states which paths were read or what + content was expected. The PostgreSQL next action did list three accepted + paths; the LogTape next action ("Add compatible local LogTape evidence") + named none. +- Root cause: The Profile's `capabilities[].probe` object carries the complete + decision input — `kind`, `paths`, and `contains` for `declared-file`, and + `executable` for `executable` — but alignment renders only the capability + title and a generic next action. The probe is fully deterministic and + entirely unexposed. +- Action / suggestion: Render the evaluated probe with every unsatisfied + divergence. For `declared-file`, list each inspected path with its state + (`absent`, `present, no match`, `unreadable`) and the expected content. For + `executable`, print the inspected PATH candidate or state that no candidate + was found. A maintainer should never need to read catalog assets or Go + source to learn what a probe wanted. + +Shape: + +```text +blocking capability.stack.logtape (capability.required.missing) + probe: declared-file contains "@logtape/logtape" + package.json present, no match + packages/frontend/package.json present, no match + packages/backend/package.json present, no match +``` + +## 2. A required stack capability blocks a repository holding a deliberate equivalent + +- Symptom / evidence: `capability.stack.logtape` is `strength: required`, so a + repository that logs through pino and winston behind its own `Logger` port + cannot reach a ready Baseline. The divergence gives no signal that LogTape + is a house-stack choice rather than a Baseline mechanism, and no signal of + what the alternative costs. +- Root cause: The Profile encodes an opinionated stack, which is correct, but + required stack capabilities are reported with the same vocabulary as + structural prerequisites. "No compatible local evidence" describes a missing + file; it does not say "this Profile selects LogTape as the logging stack and + your repository selects something else". +- Action / suggestion: For a required *stack* capability, state the selected + technology, the reason the Profile requires it, and both resolutions + explicitly: adopt it, or remove it through a reviewed repository-owned + adaptation. Where the catalog can determine that the removal cascades to no + decision — `capability.stack.logtape` is referenced by no entry in + `assets/decisions.json` — say so, because that is exactly the fact that + makes the adaptation cheap and safe. + +A second, separable point: the probe is a substring match for +`"@logtape/logtape"` in a `package.json`. It proves declaration, never use. A +repository can satisfy this required capability by adding an unused +dependency. If declaration is the intended bar, name the capability for what +it checks. If use is the intended bar, the probe needs source evidence, as +`capability.stack.postgresql` already demands a contract artifact. + +## 3. The contract-artifact probe is the right idea, applied to exactly one capability + +- Symptom / evidence: `capability.stack.postgresql` is the only capability + whose probe reads a documentation artifact (`DATABASE.md`, + `docs/architecture/database.json`, or `docs/architecture/postgresql.json` + containing `PostgreSQL`) rather than a dependency string. Its message is the + most informative in the whole report because it separates implementation + evidence from contract evidence. The repository had none of the three paths + and no `docs/architecture/` directory at all. +- Root cause: The two-part implementation-plus-contract check exists in the + catalog for one capability with no stated rule for when it applies. From the + outside it reads as an inconsistency rather than a deliberate distinction. +- Action / suggestion: Two changes, independently useful. + 1. State the rule. If durable stores earn a contract requirement because + schema, migration, and identifier conventions must be written down, say + that in the capability explanation, and apply it to any future capability + that meets the rule. + 2. Offer the artifact. A `capability.contract.missing` divergence knows the + accepted paths and the required content. Baseline should be able to + propose a contract stub at the first accepted path as a normal Change + Plan entry, subject to the same digest confirmation as any other write. + Remediation took a hand-written `DATABASE.md`; a stub with the required + headings would have made the gap self-closing. + +## 4. Prompt 16 has no option for "remediate in the repository and re-run" + +- Symptom / evidence: The prompt offered `1. Change Baseline Profile`, + `2. Create a reviewed repository-owned Profile adaptation`, and `3. Decline + without writing`. The correct response for `capability.stack.postgresql` was + to write one file and re-run. The correct response for + `capability.stack.logtape` was a dependency migration and re-run. The + correct response for all five advisories was to do nothing. None of the + three options expresses any of that, so the session had to exit through + option 3 and act outside the tool. +- Root cause: The prompt resolves divergence *as a set*, with profile-scoped + choices only. A mixed set whose members need repository-scoped, unrelated + responses has no representation. +- Action / suggestion: Add an explicit fourth option — exit without writing, + print the per-divergence remediation, and name the exact command to re-run. + This is behaviourally close to option 3, but it is the difference between + "declined" and "paused for repository work", and only one of those is what + happened. + +A related constraint worth documenting on the prompt itself: +`NewProfileAdaptationDraft` rejects a draft with no removals +(`custom.profile.adaptation.removal.required`), so option 2 cannot be used to +remap Verification roles or to acknowledge advisories. Option 2 is a removal +mechanism only. + +## 5. There is no read-only way to re-check capabilities after remediation + +- Symptom / evidence: + `roundfix baseline plan --repo . --profile standard-typescript-monorepo --format json` + exited `3` with `required Baseline decisions are missing: auth.provider, + autonomous.enabled, domain.layout, http.contract, identifier.strategy, + language.generated, repository.extension.enabled, secondbrain.enabled, + spec.scaffold, triage.external, verification.gate`. Decision resolution runs + before capability evaluation, so confirming that the two blocking probes now + pass required reproducing the probe logic by hand against the catalog + instead of asking Roundfix. +- Root cause: Capability evidence depends only on the Profile and bounded + repository facts. Decisions gate the *plan*, not the probes, but the command + ordering makes the two inseparable. +- Action / suggestion: Expose capability alignment without decisions — a + `roundfix baseline capabilities --profile ` read-only report, or a + `--capabilities-only` mode on `plan` that skips decision resolution and + emits the capability outcomes with the probe detail from finding 1. This is + the natural verification loop for an agent or a CI check: remediate, + re-check, repeat. Today that loop does not exist. + +## 6. Symlink capability discovery — corroboration, and the `rg` case resolved + +[Finding 4 of the fluxus report](2026-07-26-baseline-profile-refresh-retention-gap.md) +recorded that `lookPathWithoutExecution` rejects executable symlinks and could +not classify the `rg` warning from the interactive transcript. This session +reproduced the defect in a second repository and classified all three +capabilities by replaying the probe's exact `os.Lstat`-plus-`IsRegular` logic +over the live PATH: + +```text +rg candidate= kind=- accepted=false +rtk candidate=/opt/homebrew/bin/rtk kind=symlink accepted=false +docker candidate=/usr/local/bin/docker kind=symlink accepted=false +git candidate=/opt/homebrew/bin/git kind=symlink accepted=false +bun candidate=/Users/marcio/.bun/bin/bun kind=regular accepted=true +``` + +- `rtk` (0.43.0) and Docker (29.6.2) were installed and working; both are + false negatives, matching the fluxus evidence. +- `rg` is a **true** negative in this environment. There is no `rg` regular + file anywhere on PATH. The interactive `rg` is a shell function injected by + the coding harness, which a `PATH` scan correctly cannot see. Finding 4's + open question can be closed: the fix must not assume every rejected + executable capability is a symlink case. +- `git` is included only as a severity signal. Roundfix does not probe it + today, but Homebrew's `git` is a symlink, so the same probe would reject it + if a `capability.git` were ever added. On macOS the symlink case is the + norm, not the exception. + +This corroboration should raise the priority of finding 4 rather than open a +separate work item. + +## Recommended implementation order + +1. Fix `lookPathWithoutExecution` to resolve a bounded symlink chain + (fluxus finding 4). Highest severity, now reproduced twice, and it + silently mis-reports on any Homebrew or Docker Desktop machine. +2. Render probe detail on every unsatisfied divergence (finding 1). This is + the single change that would have removed most of this session's cost, and + it also supplies the diagnostic text finding 4 needs. +3. Add a read-only capability re-check (finding 5). Without it, items 1 and 2 + improve the diagnosis but not the remediation loop. +4. Add the "remediate and re-run" outcome to Prompt 16 and document the + removal-only constraint on the adaptation option (finding 4 above). +5. Clarify required stack capabilities and their cascade (finding 2). +6. Decide whether the contract-artifact probe is a rule or an exception, and + offer the stub either way (finding 3). + +## What worked — keep + +- `capability.contract.missing` as a distinct code from + `capability.required.missing`. Separating "you do not run PostgreSQL" from + "you run PostgreSQL but have not written the contract" is the most useful + distinction in the report, and it named its three accepted paths. +- Blocking versus advisory strength was classified correctly for all seven + divergences. The problem in fluxus finding 5 is the presentation of that + classification, not the classification itself. +- `baseline plan` refused cleanly with exit `3` and a complete list of missing + decisions, and emitted no partial plan. +- The format-role alias fallback in `resolveProfileVerificationCommand` + silently resolved the Profile's `bun run format` against the repository's + `fmt` script, so `verification.format` never appeared as a divergence. That + is exactly the mapping behaviour fluxus finding 6 asks for on the `build` + and `workspace` roles. + +## Suggested acceptance checks + +- A `declared-file` divergence renders every inspected path with its state and + the expected content; an `executable` divergence renders the inspected PATH + candidate or an explicit "no candidate found". +- Executable discovery accepts a relative symlink, an absolute symlink, and a + direct regular executable; it rejects a broken symlink, a symlink cycle, and + a non-executable target, each with a distinct diagnostic. +- Capability alignment is obtainable for a Profile with zero decisions + supplied, and its outcomes match the outcomes produced by a full plan for + the same repository state. +- A capability whose removal cascades to no decision reports that fact in its + divergence. +- Prompt 16 exposes an outcome that writes nothing, prints per-divergence + remediation, and names the re-run command; selecting it is distinguishable + in the journal from a decline. +- A `capability.contract.missing` divergence can produce a contract stub as a + Change Plan entry gated by the normal Plan Digest confirmation. From 7b44d7015cf64f37f813cbb18bdfb4d441682908 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 12:03:17 -0300 Subject: [PATCH 08/48] docs: add terminal outcome task graphs --- .../0037-terminal-outcome-integrity/_tasks.md | 41 ++++++++ .../task_01.md | 79 ++++++++++++++++ .../task_02.md | 73 ++++++++++++++ .../task_03.md | 86 +++++++++++++++++ .../task_04.md | 80 ++++++++++++++++ .../task_05.md | 85 +++++++++++++++++ .../task_06.md | 77 +++++++++++++++ .../task_07.md | 77 +++++++++++++++ .../_tasks.md | 45 +++++++++ .../task_01.md | 78 +++++++++++++++ .../task_02.md | 81 ++++++++++++++++ .../task_03.md | 80 ++++++++++++++++ .../task_04.md | 82 ++++++++++++++++ .../task_05.md | 69 ++++++++++++++ .../task_06.md | 76 +++++++++++++++ .../task_07.md | 74 +++++++++++++++ .../_tasks.md | 53 +++++++++++ .../task_01.md | 80 ++++++++++++++++ .../task_02.md | 84 +++++++++++++++++ .../task_03.md | 94 +++++++++++++++++++ .../task_04.md | 84 +++++++++++++++++ .../task_05.md | 82 ++++++++++++++++ .../task_06.md | 89 ++++++++++++++++++ .../task_07.md | 84 +++++++++++++++++ .../task_08.md | 84 +++++++++++++++++ .../task_09.md | 75 +++++++++++++++ 26 files changed, 1992 insertions(+) create mode 100644 docs/specs/0037-terminal-outcome-integrity/_tasks.md create mode 100644 docs/specs/0037-terminal-outcome-integrity/task_01.md create mode 100644 docs/specs/0037-terminal-outcome-integrity/task_02.md create mode 100644 docs/specs/0037-terminal-outcome-integrity/task_03.md create mode 100644 docs/specs/0037-terminal-outcome-integrity/task_04.md create mode 100644 docs/specs/0037-terminal-outcome-integrity/task_05.md create mode 100644 docs/specs/0037-terminal-outcome-integrity/task_06.md create mode 100644 docs/specs/0037-terminal-outcome-integrity/task_07.md create mode 100644 docs/specs/0038-terminal-run-worktree-reconciliation/_tasks.md create mode 100644 docs/specs/0038-terminal-run-worktree-reconciliation/task_01.md create mode 100644 docs/specs/0038-terminal-run-worktree-reconciliation/task_02.md create mode 100644 docs/specs/0038-terminal-run-worktree-reconciliation/task_03.md create mode 100644 docs/specs/0038-terminal-run-worktree-reconciliation/task_04.md create mode 100644 docs/specs/0038-terminal-run-worktree-reconciliation/task_05.md create mode 100644 docs/specs/0038-terminal-run-worktree-reconciliation/task_06.md create mode 100644 docs/specs/0038-terminal-run-worktree-reconciliation/task_07.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/_tasks.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_01.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_02.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_03.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_04.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_05.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_06.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_07.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_08.md create mode 100644 docs/specs/0039-review-source-evidence-and-detached-outcomes/task_09.md diff --git a/docs/specs/0037-terminal-outcome-integrity/_tasks.md b/docs/specs/0037-terminal-outcome-integrity/_tasks.md new file mode 100644 index 0000000..1e5f6fb --- /dev/null +++ b/docs/specs/0037-terminal-outcome-integrity/_tasks.md @@ -0,0 +1,41 @@ +--- +schema: spec-tasks/v1 +spec: 0037-terminal-outcome-integrity +graph: + nodes: + - id: task_01 + file: task_01.md + needs: [] + - id: task_02 + file: task_02.md + needs: [task_01] + - id: task_03 + file: task_03.md + needs: [task_01, task_02] + - id: task_04 + file: task_04.md + needs: [task_01] + - id: task_05 + file: task_05.md + needs: [task_01, task_02, task_03, task_04] + - id: task_06 + file: task_06.md + needs: [task_03, task_04, task_05] + - id: task_07 + file: task_07.md + needs: [task_06] +--- + +# Tasks — Terminal outcome integrity + +| id | title | type | complexity | needs | +| ------- | -------------------------------------------------------- | ------- | ---------- | -------------------------------------- | +| task_01 | Guard terminal completion and reconciliation | data | high | — | +| task_02 | Target only registered Agent Sessions during cleanup | backend | medium | task_01 | +| task_03 | Prove owner exit before Force Stop completion | backend | high | task_01, task_02 | +| task_04 | Interrupt every Review Source wait on Stop Request | backend | medium | task_01 | +| task_05 | Publish only the winning terminal outcome | backend | high | task_01, task_02, task_03, task_04 | +| task_06 | Align terminal-outcome operator guidance | docs | medium | task_03, task_04, task_05 | +| task_07 | Align the protected Roundfix Skill pair | docs | low | task_06 | + +Waves: 1 → task_01 · 2 → task_02, task_04 · 3 → task_03 · 4 → task_05 · 5 → task_06 · 6 → task_07 diff --git a/docs/specs/0037-terminal-outcome-integrity/task_01.md b/docs/specs/0037-terminal-outcome-integrity/task_01.md new file mode 100644 index 0000000..b3c7e56 --- /dev/null +++ b/docs/specs/0037-terminal-outcome-integrity/task_01.md @@ -0,0 +1,79 @@ +--- +task: task_01 +spec: 0037-terminal-outcome-integrity +status: pending +type: data +complexity: high +--- + +# Task 01: Guard terminal completion and reconciliation + +## Overview + +Make terminal Run completion an atomic first-writer-wins operation while +preserving identical replay and the one evidence-backed Integration Pending +reconciliation. This slice is complete when competing completion attempts +cannot overwrite the stored outcome, release its lock twice, or publish +conflicting durable evidence. + +## Requirements + +1. MUST make ordinary Run completion update only a non-terminal Run. +2. MUST return an explicit transitioned result for the winning completion and + an idempotent non-transition result for an identical replay. +3. MUST return a typed conflict for a different requested terminal outcome + without changing the Run, completion timestamp, Active Run lock, or journal. +4. MUST release the Active Run lock only in the transaction that wins ordinary + terminal completion. +5. MUST provide the sole guarded Integration Pending to Clean reconciliation, + requiring the recorded Run and target heads and journaling both outcomes. +6. MUST reject every other terminal-to-terminal transition. +7. MUST wrap database and transaction failures with the operation and Run ID. + +## Subtasks + +- [ ] Add explicit completion and conflict result contracts. +- [ ] Implement compare-and-set terminal persistence and winner-only lock release. +- [ ] Implement evidence-backed Integration Pending reconciliation. +- [ ] Add idempotent replay and conflicting-outcome coverage. +- [ ] Add deterministic concurrent-completion coverage. +- [ ] Prove reconciliation rejects stale or invalid source outcomes. + +## Acceptance Criteria + +- [ ] One non-terminal completion stores its outcome and reports transitioned. +- [ ] Repeating the same outcome changes no persisted field or journal entry. +- [ ] A competing outcome returns the typed conflict and leaves the winner, + timestamp, lock state, and event history unchanged. +- [ ] A deterministic race produces exactly one winner and one stable terminal + row. +- [ ] Integration Pending becomes Clean only with complete reconciliation + evidence recorded transactionally. +- [ ] No other terminal outcome can be rewritten. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `internal/store/store.go` +- interface: `internal/store/store_test.go` +- interface: `internal/store/journal.go` +- interface: `internal/store/journal_test.go` + +## Verification + +- `rtk go test ./internal/store -run 'Test(CompleteRun|TerminalOutcome|ReconcileIntegration)' -count=1` + — expected: winner, replay, conflict, lock, and guarded reconciliation cases + pass. +- `rtk go test -race ./internal/store -run 'Test(CompleteRun|TerminalOutcome)' -count=1` + — expected: concurrent competing completion has one race-free winner. + +## References + +- `_prd.md` → Goals 1; User Stories 1, 3, and 5; Core Features 1–2 and 7; + Success Metrics. +- `_techspec.md` → Interfaces; Data Models; Build Order 1. +- `../../adr/0052-run-completion-is-compare-and-set.md` → terminal + compare-and-set and guarded reconciliation. diff --git a/docs/specs/0037-terminal-outcome-integrity/task_02.md b/docs/specs/0037-terminal-outcome-integrity/task_02.md new file mode 100644 index 0000000..b746b71 --- /dev/null +++ b/docs/specs/0037-terminal-outcome-integrity/task_02.md @@ -0,0 +1,73 @@ +--- +task: task_02 +spec: 0037-terminal-outcome-integrity +status: pending +type: backend +complexity: medium +--- + +# Task 02: Target only registered Agent Sessions during cleanup + +## Overview + +Use persisted Agent Selection lifecycle evidence as the exclusive Agent Session +cleanup registry. Cleanup becomes deterministic and idempotent: only scopes +whose latest lifecycle is active are targeted, while absent, failed, and +already closed sessions produce no misleading cleanup action. + +## Requirements + +1. MUST query the latest Agent Selection lifecycle per Run scope. +2. MUST return only scopes whose latest persisted lifecycle is active. +3. MUST cancel and close eligible Agent Sessions once in deterministic scope + order. +4. MUST record closed lifecycle after an idempotent close. +5. MUST treat a registered but already absent Agent Session as an idempotent + close. +6. MUST not derive or target an Agent Session without active lifecycle evidence. +7. MUST retain non-absence cleanup failures as secondary diagnostics. + +## Subtasks + +- [ ] Add the latest-active-scope store query. +- [ ] Route cleanup through registered Agent Selection scopes. +- [ ] Make registered-session absence idempotent. +- [ ] Persist closed lifecycle after successful cleanup. +- [ ] Add deterministic ordering and lifecycle-state tests. +- [ ] Remove unconditional Run-wide session-name cleanup. + +## Acceptance Criteria + +- [ ] Active Task, QA, and review scopes are returned once in stable order. +- [ ] Failed, closed, and superseded lifecycle attempts are not targeted. +- [ ] A Run with no active lifecycle record performs zero Agent Session calls. +- [ ] An already absent registered session closes without a warning. +- [ ] Other cleanup failures remain visible and do not invent lifecycle state. +- [ ] Existing Agent Selection history and sensitive-field protections pass. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/store/agent_selection.go` +- interface: `internal/store/agent_selection_test.go` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/agent/sessions.go` + +## Verification + +- `rtk go test ./internal/store -run 'TestAgentSelection.*(Active|Lifecycle|Cleanup|Scope)' -count=1` + — expected: only latest active scopes are returned in deterministic order. +- `rtk go test ./internal/cli -run 'Test.*(RegisteredAgentSession|AgentSessionCleanup|PrimaryFailure)' -count=1` + — expected: cleanup targets registered scopes only and registered absence is + idempotent. + +## References + +- `_prd.md` → Goal 4; User Story 4; Core Features 5–6; Success Metrics. +- `_techspec.md` → Data Models; API Contracts: Agent Session cleanup rules; + Build Order 2. +- `../../adr/0051-tasks-and-qa-own-agent-sessions.md` → Work Item-scoped Agent + Sessions. diff --git a/docs/specs/0037-terminal-outcome-integrity/task_03.md b/docs/specs/0037-terminal-outcome-integrity/task_03.md new file mode 100644 index 0000000..21e75bb --- /dev/null +++ b/docs/specs/0037-terminal-outcome-integrity/task_03.md @@ -0,0 +1,86 @@ +--- +task: task_03 +spec: 0037-terminal-outcome-integrity +status: pending +type: backend +complexity: high +--- + +# Task 03: Prove owner exit before Force Stop completion + +## Overview + +Reorder Force Stop so Roundfix proves the recorded owner process is absent +before storing Stopped or releasing the Active Run lock. Platform-specific +controllers retain the existing graceful-then-force policy while failures +leave the Run Active with actionable diagnostics. + +## Requirements + +1. MUST cancel registered Agent Sessions before terminating the recorded owner. +2. MUST target only the recorded owner PID and follow existing platform process + boundaries. +3. MUST attempt graceful termination, wait within the bounded stop window, and + use the existing force-kill path when required. +4. MUST report success only after owner-process absence is positively proven. +5. MUST call terminal completion only after that proof. +6. MUST leave the Run Active and its lock retained on permission, unsupported, + reused-PID, or deadline failures. +7. MUST make an already stored Stopped outcome idempotent without repeating + process or Agent Session actions. + +## Subtasks + +- [ ] Introduce the owner-process controller seam. +- [ ] Implement Unix exit proof and preserve Windows build behavior. +- [ ] Reorder Force Stop coordination around owner absence. +- [ ] Add actionable failed-step diagnostics. +- [ ] Cover graceful, forced, permission, deadline, and idempotent paths. +- [ ] Add a real helper-process integration case on Unix. + +## Acceptance Criteria + +- [ ] Successful Force Stop proves owner exit before Stopped is persisted. +- [ ] The Active Run lock remains present until the winning completion. +- [ ] Permission or deadline failure prints no success report, stores no Stopped + outcome, and leaves the Run Active. +- [ ] The diagnostic names the Run, owner PID, failed step, and retained state. +- [ ] A reused or unprovable PID fails closed. +- [ ] Repeating Force Stop for an already Stopped Run performs no process or + session action. +- [ ] Unix integration proves the helper owner is absent before store + completion; non-Unix packages still compile. + +## Context + +- instruction: `.agents/skills/agentic-cli-design/SKILL.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-context/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/store/process_unix.go` +- interface: `internal/store/process_windows.go` +- interface: `internal/store/process_other.go` +- interface: `internal/store/process_unix_test.go` + +## Verification + +- `rtk go test ./internal/cli -run 'Test.*ForceStop.*(Owner|Exit|Permission|Deadline|Idempotent|Lock)' -count=1` + — expected: Force Stop proves absence before completion and every failure + retains Active state and lock. +- `rtk go test ./internal/store -run 'Test.*Process|TestStoppedRunReleasesActiveLock' -count=1` + — expected: platform process proof and winner-only lock release pass. +- `rtk go test -race ./internal/cli ./internal/store -run 'Test.*(ForceStop|OwnerProcess)' -count=1` + — expected: stop coordination and exit proof are race-free. + +## References + +- `_prd.md` → Goals 1–2; User Story 1; Core Features 1 and 3; User Experience; + Success Metrics. +- `_techspec.md` → Interfaces: OwnerProcessController; API Contracts: + Force Stop; Build Order 3. +- `../../adr/0044-orphaned-run-locks-are-reclaimed-on-proven-owner-death.md` → + owner-death proof. +- `../../adr/0052-run-completion-is-compare-and-set.md` → completion ordering. diff --git a/docs/specs/0037-terminal-outcome-integrity/task_04.md b/docs/specs/0037-terminal-outcome-integrity/task_04.md new file mode 100644 index 0000000..b0f79eb --- /dev/null +++ b/docs/specs/0037-terminal-outcome-integrity/task_04.md @@ -0,0 +1,80 @@ +--- +task: task_04 +spec: 0037-terminal-outcome-integrity +status: pending +type: backend +complexity: medium +--- + +# Task 04: Interrupt every Review Source wait on Stop Request + +## Overview + +Make the Run Database Stop Request visible at every Review Source wait +boundary. A graceful stop interrupts status, retry, quiet-period, and +Merge-Ready waits by the next polling boundary and starts no later Review +Source or repository mutation. + +## Requirements + +1. MUST expose Stop Request observation through a narrow context-aware source. +2. MUST supply the Store-backed source to every operational watch Run. +3. MUST check the source before each Review Source status access. +4. MUST check again after every interruptible wait, including quiet, retry, and + Merge-Ready sleeps. +5. MUST return the existing stop classification when a request is observed. +6. MUST not perform another fetch, check, artifact write, commit, push, or + Review Source mutation after observation. +7. MUST propagate Store observation failures with Run and operation context. + +## Subtasks + +- [ ] Add the Stop Request source contract to watch dependencies. +- [ ] Wire the Store-backed source into operational watch Runs. +- [ ] Cover every status and sleep boundary. +- [ ] Preserve the existing stopped classification and CLI exit behavior. +- [ ] Add fake-clock tests for each waiting phase. +- [ ] Prove no downstream operation starts after observation. + +## Acceptance Criteria + +- [ ] A request during status wait reaches Stopped by the next poll. +- [ ] Requests during quiet period, transient retry sleep, and Merge-Ready wait + each interrupt their respective boundary. +- [ ] No later Review Source call or repository mutation occurs. +- [ ] Operational runs always provide the Store source; only isolated tests may + omit it. +- [ ] Store read failure is distinguishable from a requested stop. +- [ ] Existing Run Budget and timeout behavior remains unchanged without a stop. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-context/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/watch/watch.go` +- interface: `internal/watch/watch_test.go` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/store/store.go` + +## Verification + +- `rtk go test ./internal/watch -run 'TestRun.*StopRequest' -count=1` + — expected: every wait boundary observes Stop Request and starts no later + operation. +- `rtk go test ./internal/cli -run 'TestRunWatch.*StopRequest' -count=1` + — expected: Store-backed stop observation reaches the existing Stopped CLI + contract. +- `rtk go test -race ./internal/watch ./internal/cli -run 'Test.*StopRequest' -count=1` + — expected: cancellation and polling remain race-free. + +## References + +- `_prd.md` → Goal 3; User Story 2; Core Feature 4; User Experience; Success + Metrics. +- `_techspec.md` → Interfaces: StopRequestSource; API Contracts: Graceful Stop + Requests; Build Order 4. +- `../../adr/0022-stop-requests-travel-through-the-run-database.md` → durable + Stop Request transport. diff --git a/docs/specs/0037-terminal-outcome-integrity/task_05.md b/docs/specs/0037-terminal-outcome-integrity/task_05.md new file mode 100644 index 0000000..36a0cd3 --- /dev/null +++ b/docs/specs/0037-terminal-outcome-integrity/task_05.md @@ -0,0 +1,85 @@ +--- +task: task_05 +spec: 0037-terminal-outcome-integrity +status: pending +type: backend +complexity: high +--- + +# Task 05: Publish only the winning terminal outcome + +## Overview + +Carry the completion transition result through all terminal publication paths +so only the winner emits outcome evidence and notification. Preserve primary +failure ordering while retaining registered-session cleanup problems as +explicit secondary diagnostics. + +## Requirements + +1. MUST publish a terminal outcome Run Event only when completion transitioned + the Run. +2. MUST attempt Run Outcome Notification only for that same winning transition. +3. MUST make identical replays and conflicting losers observe stored state + without duplicate publication. +4. MUST preserve one terminal outcome across Resolve, Watch, Implement, Stop, + and cleanup paths. +5. MUST print and journal the primary failure before any cleanup warning. +6. MUST label cleanup failures as secondary and prevent them from replacing + the terminal reason or exit code. +7. MUST retain best-effort notification failure without changing the Run + outcome. + +## Subtasks + +- [ ] Thread the completion transition result through terminal coordinators. +- [ ] Gate outcome events and notifications on the winning transition. +- [ ] Normalize replay and conflict handling around stored state. +- [ ] Order primary failure evidence before cleanup diagnostics. +- [ ] Cover terminal paths across operational commands. +- [ ] Add deterministic owner-versus-Force-Stop publication coverage. + +## Acceptance Criteria + +- [ ] A completion race stores and publishes exactly one terminal outcome. +- [ ] The losing owner emits no second outcome event or notification. +- [ ] An identical replay is silent and returns the stored result. +- [ ] A conflicting loser cannot change the primary reason, exit code, or + notification context. +- [ ] Cleanup warnings follow the primary failure and are labeled secondary. +- [ ] Notification failure remains a warning and leaves persisted outcome + unchanged. +- [ ] Resolve, Watch, Implement, and Stop terminal regressions pass. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/cli/implement.go` +- interface: `internal/cli/implement_test.go` +- interface: `internal/store/journal.go` +- interface: `internal/store/journal_test.go` +- interface: `internal/notify/notify.go` +- interface: `internal/notify/notify_test.go` +- interface: `internal/runevent/event.go` + +## Verification + +- `rtk go test ./internal/cli ./internal/store ./internal/notify ./internal/runevent -run 'Test.*(CompletionWinner|TerminalOutcome|PrimaryFailure|OutcomeNotification)' -count=1` + — expected: only the completion winner publishes and primary diagnostics + precede secondary cleanup warnings. +- `rtk go test -race ./internal/cli ./internal/store -run 'Test.*(CompletionWinner|TerminalOutcome)' -count=1` + — expected: concurrent terminal publication has one race-free winner. + +## References + +- `_prd.md` → Goals 1 and 4; User Stories 3–4; Core Features 6–7; User + Experience; Success Metrics. +- `_techspec.md` → System Architecture: Run Event Journal; Testing Approach; + Build Order 5. +- `../../adr/0052-run-completion-is-compare-and-set.md` → winner-only + publication. diff --git a/docs/specs/0037-terminal-outcome-integrity/task_06.md b/docs/specs/0037-terminal-outcome-integrity/task_06.md new file mode 100644 index 0000000..18fe731 --- /dev/null +++ b/docs/specs/0037-terminal-outcome-integrity/task_06.md @@ -0,0 +1,77 @@ +--- +task: task_06 +spec: 0037-terminal-outcome-integrity +status: pending +type: docs +complexity: medium +--- + +# Task 06: Align terminal-outcome operator guidance + +## Overview + +Publish the completed Force Stop, graceful Stop Request, terminal replay, and +cleanup contracts through supported operator documentation and command help. +This documentation slice leaves every protected Skill path unchanged so the +isolated tooling Task can publish the final Agent-facing wording. + +## Requirements + +1. MUST document that Force Stop reports Stopped only after owner exit proof. +2. MUST document the failed-closed result, retained Active Run lock, and + actionable inspection or retry guidance. +3. MUST document graceful Stop Request observation during Review Source waits. +4. MUST document idempotent same-outcome replay and conflicting-outcome + rejection without exposing internal storage details as user API. +5. MUST describe registered Agent Session cleanup and primary-before-secondary + diagnostics. +6. MUST preserve canonical glossary terms and finding/ADR traceability. +7. MUST leave all protected tooling files unchanged. + +## Subtasks + +- [ ] Update Stop Command and operational usage guidance. +- [ ] Align command help with proof-before-completion behavior. +- [ ] Document graceful watch interruption and terminal replay. +- [ ] Document cleanup eligibility and diagnostic ordering. +- [ ] Resolve glossary, ADR, Spec, and finding links. +- [ ] Check supported examples and terminology. + +## Acceptance Criteria + +- [ ] A reader can predict successful and failed Force Stop state and lock + behavior. +- [ ] Guidance states when a graceful watch stop is observed and what work will + not run afterward. +- [ ] Registered-session cleanup and secondary-warning behavior are explicit. +- [ ] User docs do not promise terminal overwrite or warning-only owner + reclamation. +- [ ] Every cited ADR, finding, and command reference resolves. +- [ ] No protected tooling path changes in this Task. + +## Context + +- instruction: `docs/agents/cli.md` +- instruction: `docs/agents/domain.md` +- instruction: `.agents/skills/tech-writer/SKILL.md` +- interface: `CONTEXT.md` +- interface: `README.md` +- interface: `docs/user-guide/commands.md` +- interface: `docs/user-guide/usage.md` +- interface: `docs/findings/2026-07-16-vortex-pr87-detached-watch-notification.md` + +## Verification + +- `rtk grep -n 'stop --force\\|Force Stop\\|Stop Request' docs/user-guide/commands.md docs/user-guide/usage.md` + — expected: supported guidance covers force and graceful stop contracts. +- `rtk go test ./internal/cli -run 'Test.*Stop.*(Help|Usage|Report)' -count=1` + — expected: command help and public reports match documented behavior. +- `rtk git diff --check` + — expected: documentation contains no whitespace errors. + +## References + +- `_prd.md` → User Stories 1–5; User Experience; Non-Goals; Decisions. +- `_techspec.md` → API Contracts; Risks & Considerations; Build Order 6. +- `../../adr/0052-run-completion-is-compare-and-set.md` → public terminal + integrity contract. diff --git a/docs/specs/0037-terminal-outcome-integrity/task_07.md b/docs/specs/0037-terminal-outcome-integrity/task_07.md new file mode 100644 index 0000000..033c467 --- /dev/null +++ b/docs/specs/0037-terminal-outcome-integrity/task_07.md @@ -0,0 +1,77 @@ +--- +task: task_07 +spec: 0037-terminal-outcome-integrity +status: pending +type: docs +complexity: low +--- + +# Task 07: Align the protected Roundfix Skill pair + +## Overview + +Publish Terminal Outcome Integrity in the canonical and generated Roundfix +Skill after product behavior and public guidance are complete. This is a +tooling-only slice bounded to the two exact files authorized by the maintainer +and this Task file. + +## Requirements + +1. MUST align `.agents/skills/roundfix/SKILL.md` with proof-before-Force-Stop, + graceful wait interruption, registered-session cleanup, and winner-only + terminal publication. +2. MUST apply byte-identical content to `skills/roundfix/SKILL.md`. +3. MUST limit repository changes to those two authorized `SKILL.md` files and + this `task_07.md` file. +4. MUST NOT run `make skills-sync`, because it rewrites every owned Skill + directory; use the read-only sync check. +5. MUST leave code, tests, manifests, public documentation, other owned Skills, + upstream-managed Skills, locks, and recommendation files unchanged. + +## Subtasks + +- [ ] Update the canonical Roundfix Skill terminal-outcome contract. +- [ ] Apply the identical generated Roundfix Skill copy. +- [ ] Verify exact changed-file scope and byte identity. +- [ ] Confirm shipped Skill and full-gate compatibility. + +## Acceptance Criteria + +- [ ] The Roundfix Skill never tells an Agent to accept terminal overwrite or + report Force Stop before owner exit proof. +- [ ] The Skill surfaces graceful interruption and registered-session cleanup + consistently with supported docs. +- [ ] Canonical and generated files are byte-identical. +- [ ] Git evidence contains only the two authorized Skill paths and this Task + file. +- [ ] No other protected or upstream-managed Skill changes. +- [ ] Shipped Skill validation and the complete repository gate pass. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/skill-dispatch.md` +- interface: `.agents/skills/roundfix/SKILL.md` +- interface: `skills/roundfix/SKILL.md` + +## Verification + +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` + — expected: no output and exit zero. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "docs/specs/0037-terminal-outcome-integrity/task_07.md") {print; bad=1}} END {exit bad}'` + — expected: no changed path outside the authorized pair and this Task file. +- `rtk make skills-sync-check` + — expected: every canonical/generated owned Skill pair has no drift. +- `rtk go run -buildvcs=false ./cmd/roundfix skills check` + — expected: every shipped Roundfix Skill contract passes. +- `rtk git diff --check` + — expected: no whitespace errors. +- `rtk make verify` + — expected: formatting, tests, Skill checks, and build pass. + +## References + +- `_prd.md` → Goals; User Experience; Decisions; Project Constraints. +- `_techspec.md` → Build Order 7; Decisions. +- `docs/agents/spec-routing.md` → tooling authorization and changed-file + postflight. diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/_tasks.md b/docs/specs/0038-terminal-run-worktree-reconciliation/_tasks.md new file mode 100644 index 0000000..c512695 --- /dev/null +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/_tasks.md @@ -0,0 +1,45 @@ +--- +schema: spec-tasks/v1 +spec: 0038-terminal-run-worktree-reconciliation +graph: + nodes: + - id: task_01 + file: task_01.md + needs: [] + - id: task_02 + file: task_02.md + needs: [task_01] + - id: task_03 + file: task_03.md + needs: [task_01, task_02] + - id: task_04 + file: task_04.md + needs: [task_01, task_02, task_03] + - id: task_05 + file: task_05.md + needs: [task_01] + - id: task_06 + file: task_06.md + needs: [task_04, task_05] + - id: task_07 + file: task_07.md + needs: [task_06] +--- + +# Tasks — Terminal Run Worktree reconciliation + +Cross-Spec prerequisite: Spec 0037 Terminal Outcome Integrity must be completed +before this graph starts because guarded Integration Pending reconciliation is +part of the safety boundary. + +| id | title | type | complexity | needs | +| ------- | ------------------------------------------------------------- | ------- | ---------- | ------------------------------ | +| task_01 | Classify terminal Run Worktrees with positive proof | backend | high | — | +| task_02 | Apply stale-proof cleanup and migrate terminal reaping | backend | high | task_01 | +| task_03 | Reconcile Integration Pending with durable evidence | data | high | task_01, task_02 | +| task_04 | Deliver the Reconcile Command and apply contract | backend | high | task_01, task_02, task_03 | +| task_05 | Surface retained terminal Run Worktrees in Runs List | backend | medium | task_01 | +| task_06 | Align reconciliation docs and glossary | docs | medium | task_04, task_05 | +| task_07 | Align the protected Roundfix Skill pair | docs | low | task_06 | + +Waves: 1 → task_01 · 2 → task_02, task_05 · 3 → task_03 · 4 → task_04 · 5 → task_06 · 6 → task_07 diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/task_01.md b/docs/specs/0038-terminal-run-worktree-reconciliation/task_01.md new file mode 100644 index 0000000..d89a067 --- /dev/null +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/task_01.md @@ -0,0 +1,78 @@ +--- +task: task_01 +spec: 0038-terminal-run-worktree-reconciliation +status: pending +type: backend +complexity: high +--- + +# Task 01: Classify terminal Run Worktrees with positive proof + +## Overview + +Add one conservative classifier for terminal spec Run Worktrees and Run +Branches. It derives current reconciliation state from recorded Run metadata +and real Git evidence so every consumer receives the same safe, unintegrated, +dirty, unknown, or released result. + +## Requirements + +1. MUST inspect terminal spec Runs from their recorded Git root, worktree, + Run Branch, and target branch metadata. +2. MUST classify `safe` only when the retained worktree is clean and the Run + Branch tip is an ancestor of the current target tip. +3. MUST classify tracked or untracked worktree changes as `dirty`. +4. MUST distinguish `unintegrated`, `unknown`, and `released` without treating + missing proof as safe. +5. MUST report both resolved heads and one bounded reason. +6. MUST reject symlink, path, or metadata input that could escape the recorded + repository boundary. +7. MUST use real Git fixtures for every state and ambiguous-ref failure. + +## Subtasks + +- [ ] Add the five reconciliation states and result contract. +- [ ] Inspect cleanliness including untracked files. +- [ ] Resolve recorded Run and target branch tips. +- [ ] Prove ancestry through Git porcelain. +- [ ] Classify missing paths, refs, and metadata conservatively. +- [ ] Add real-repository fixtures for all classifications. + +## Acceptance Criteria + +- [ ] Every terminal fixture produces exactly one documented state. +- [ ] A clean reachable branch is `safe`; a clean divergent branch is + `unintegrated`. +- [ ] Any tracked or untracked worktree change produces `dirty`. +- [ ] Missing target metadata, ambiguous refs, or Git inspection uncertainty + produces `unknown` and preserves work. +- [ ] Absence of both retained worktree and Run Branch produces `released`. +- [ ] Output contains the recorded paths, both heads when known, and a bounded + deterministic reason. +- [ ] No input can make inspection read outside the recorded Git root. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `internal/worktree/worktree.go` +- interface: `internal/worktree/worktree_test.go` +- interface: `internal/store/store.go` + +## Verification + +- `rtk go test ./internal/worktree -run 'TestInspectTerminalRun.*(Safe|Unintegrated|Dirty|Unknown|Released|UnsafePath)' -count=1` + — expected: real Git fixtures prove all five states and conservative path + handling. +- `rtk go test -race ./internal/worktree -run 'TestInspectTerminalRun' -count=1` + — expected: concurrent inspection fixtures are race-free. + +## References + +- `_prd.md` → Goals 1; User Stories 1 and 4; Core Features 1–3; Success + Metrics. +- `_techspec.md` → Interfaces; Data Models; Testing Approach; Build Order 1. +- `../../adr/0053-terminal-run-worktree-reconciliation-is-proof-based.md` → + positive cleanliness and ancestry proof. diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/task_02.md b/docs/specs/0038-terminal-run-worktree-reconciliation/task_02.md new file mode 100644 index 0000000..0dc0c22 --- /dev/null +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/task_02.md @@ -0,0 +1,81 @@ +--- +task: task_02 +spec: 0038-terminal-run-worktree-reconciliation +status: pending +type: backend +complexity: high +--- + +# Task 02: Apply stale-proof cleanup and migrate terminal reaping + +## Overview + +Apply reconciliation only after revalidating the evidence immediately before +mutation, then move automatic terminal reaping onto the same classifier. This +slice removes safe or already released residue while preserving any work whose +cleanliness, heads, or ancestry changed after inspection. + +## Requirements + +1. MUST accept cleanup only for a result previously classified `safe`. +2. MUST re-resolve cleanliness, Run head, and target head immediately before + the first mutation. +3. MUST refuse apply when either head, worktree state, or recorded metadata + changed after inspection. +4. MUST remove the Run Worktree without force before deleting its Run Branch. +5. MUST preserve every remaining path or ref when a mutation step fails. +6. MUST make repeated cleanup report `released` without another mutation. +7. MUST replace creation-base terminal reaping with the shared classifier and + permit automatic cleanup only for `safe` or `released`. + +## Subtasks + +- [ ] Add stale-proof pre-mutation revalidation. +- [ ] Apply worktree removal before branch deletion. +- [ ] Preserve partial-failure evidence and remaining resources. +- [ ] Make repeated apply idempotent. +- [ ] Route automatic terminal reaping through the classifier. +- [ ] Reproduce reachable-later-merge and unique-branch regressions. + +## Acceptance Criteria + +- [ ] Only a still-clean result with unchanged heads reaches Git mutation. +- [ ] A stale head or newly dirty worktree is refused without deleting a path + or ref. +- [ ] Worktree removal uses no force option. +- [ ] Branch deletion occurs only after successful worktree removal. +- [ ] A failed removal or deletion reports the remaining recoverable surface. +- [ ] A second apply performs zero mutations and reports `released`. +- [ ] Automatic reaping removes a changed branch already reachable from target + and preserves a unique changed branch. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/no-workarounds/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/worktree/worktree.go` +- interface: `internal/worktree/worktree_test.go` +- interface: `internal/cli/implement.go` +- interface: `internal/cli/implement_test.go` + +## Verification + +- `rtk go test ./internal/worktree -run 'TestApplyTerminalRun.*(Safe|Stale|Dirty|Failure|Released)|TestPruneTerminal.*Reconciliation' -count=1` + — expected: only fresh safe evidence mutates Git and repeat apply is + idempotent. +- `rtk go test ./internal/cli -run 'TestRunImplementPreflight.*Terminal.*(Safe|Unique|Reachable)' -count=1` + — expected: automatic reaping shares the proof-based classifier. +- `rtk go test -race ./internal/worktree -run 'Test(ApplyTerminalRun|PruneTerminal)' -count=1` + — expected: inspection-to-apply boundaries remain race-free. + +## References + +- `_prd.md` → Goals 1–2; User Stories 1 and 4; Core Features 4, 7, and 9; + Success Metrics. +- `_techspec.md` → API Contracts; Testing Approach; Build Order 2. +- `../../adr/0023-runs-execute-in-per-run-worktrees.md` → Run Worktree + ownership. +- `../../adr/0053-terminal-run-worktree-reconciliation-is-proof-based.md` → + apply safety. diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/task_03.md b/docs/specs/0038-terminal-run-worktree-reconciliation/task_03.md new file mode 100644 index 0000000..e5e6625 --- /dev/null +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/task_03.md @@ -0,0 +1,80 @@ +--- +task: task_03 +spec: 0038-terminal-run-worktree-reconciliation +status: pending +type: data +complexity: high +--- + +# Task 03: Reconcile Integration Pending with durable evidence + +## Overview + +Connect fresh `safe` Git proof to the guarded Integration Pending transition +without weakening ordinary terminal immutability. The Run becomes Clean and +records its reconciliation evidence transactionally before any retained Git +surface is released; other outcomes retain their stored state. + +## Requirements + +1. MUST build reconciliation input from the freshly revalidated Run and target + heads. +2. MUST invoke the guarded Integration Pending to Clean store operation before + Git cleanup. +3. MUST record prior outcome, current outcome, classification, both branches, + both heads, worktree, and action in one durable Run Event. +4. MUST leave every non-Integration-Pending terminal outcome unchanged while + recording safe cleanup evidence. +5. MUST refuse stale, incomplete, or mismatched reconciliation input. +6. MUST perform no Git cleanup when durable reconciliation persistence fails. +7. MUST make repeated reconciliation and cleanup idempotent. + +## Subtasks + +- [ ] Map classifier evidence into the guarded store request. +- [ ] Journal complete reconciliation evidence transactionally. +- [ ] Preserve non-Integration-Pending outcomes. +- [ ] Order durable state before Git cleanup. +- [ ] Add stale and incomplete-evidence refusal cases. +- [ ] Add repeat reconciliation coverage. + +## Acceptance Criteria + +- [ ] A safe Integration Pending Run becomes Clean with one evidence event. +- [ ] The event contains both outcomes, branches, heads, worktree, and action. +- [ ] Unresolved, Failed, Stopped, and other terminal outcomes remain unchanged + after safe cleanup. +- [ ] Missing or stale evidence produces no state or Git mutation. +- [ ] Database failure starts no worktree or branch removal. +- [ ] Repeating the operation produces no duplicate transition or evidence. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/store/store.go` +- interface: `internal/store/store_test.go` +- interface: `internal/store/journal.go` +- interface: `internal/store/journal_test.go` +- interface: `internal/worktree/worktree.go` +- interface: `internal/worktree/worktree_test.go` + +## Verification + +- `rtk go test ./internal/store -run 'TestReconcileIntegration.*(Safe|Stale|Invalid|Idempotent|Outcome)' -count=1` + — expected: only Integration Pending with complete fresh evidence becomes + Clean transactionally. +- `rtk go test ./internal/worktree -run 'TestApplyTerminalRun.*(IntegrationPending|StoreFailure|Outcome)' -count=1` + — expected: durable reconciliation precedes Git cleanup and other outcomes + remain unchanged. + +## References + +- `_prd.md` → Goal 4; User Story 5; Core Feature 6; Success Metrics. +- `_techspec.md` → Data Models; Integration Points: Run Database; Build Order + 3. +- `../0037-terminal-outcome-integrity/_techspec.md` → guarded terminal + reconciliation boundary. +- `../../adr/0052-run-completion-is-compare-and-set.md` → sole terminal + transition exception. diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/task_04.md b/docs/specs/0038-terminal-run-worktree-reconciliation/task_04.md new file mode 100644 index 0000000..031534e --- /dev/null +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/task_04.md @@ -0,0 +1,82 @@ +--- +task: task_04 +spec: 0038-terminal-run-worktree-reconciliation +status: pending +type: backend +complexity: high +--- + +# Task 04: Deliver the Reconcile Command and apply contract + +## Overview + +Expose proof-based reconciliation through a deterministic, dry-run-first CLI +for one terminal spec Run or every terminal spec Run in the current repository. +Text and versioned JSON report the same evidence; `--apply` acts only on results +proven safe during the current invocation. + +## Requirements + +1. MUST implement `roundfix reconcile [run-id] [--apply] [--format text|json]`. +2. MUST reject Active Runs, review Runs, missing Runs, and cross-repository + selectors before any Git mutation. +3. MUST scope the no-ID form to terminal spec Runs in the current Git root. +4. MUST default to a read-only report and print the exact apply command. +5. MUST render deterministic text and a versioned JSON envelope with ordered + results and summary counts. +6. MUST apply only entries classified and revalidated `safe` in that + invocation; no force or assertion bypass is permitted. +7. MUST keep expected preserved states successful while making operational Git + or database failures non-zero and actionable. + +## Subtasks + +- [ ] Add Reconcile Command parsing and usage. +- [ ] Resolve one-Run and repository-wide scopes. +- [ ] Render deterministic text and versioned JSON. +- [ ] Connect dry-run and explicit apply behavior. +- [ ] Preserve mixed safe and unsafe scan results. +- [ ] Add idempotent repeat and invalid-selector coverage. + +## Acceptance Criteria + +- [ ] Default invocation changes neither Git nor the Run Database. +- [ ] One-Run and repository-wide results are ordered newest-first. +- [ ] Text and JSON expose classification, branches, heads, worktree, evidence, + action, and refusal reason consistently. +- [ ] `--apply` removes all and only fresh `safe` results. +- [ ] Dirty, unintegrated, unknown, and released results are preserved and + reported without turning a complete scan into failure. +- [ ] Operational inspection or apply failure returns the Run failure exit code + and names the next safe action. +- [ ] A second apply reports `released` and performs zero mutations. + +## Context + +- instruction: `.agents/skills/agentic-cli-design/SKILL.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-cli/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/cli/runs.go` +- interface: `internal/worktree/worktree.go` + +## Verification + +- `rtk go test ./internal/cli -run 'TestRunReconcile.*(DryRun|Apply|JSON|Repository|Invalid|Mixed|Idempotent)' -count=1` + — expected: command scope, output, mutation, refusal, and repeat contracts + pass. +- `rtk go test -race ./internal/cli ./internal/worktree -run 'Test.*Reconcile' -count=1` + — expected: CLI inspection and apply coordination are race-free. +- `rtk go build -buildvcs=false ./cmd/roundfix` + — expected: the public command builds with repository build settings. + +## References + +- `_prd.md` → Goals 1–2; User Stories 1–2 and 4–5; Core Features 4–7; User + Experience; Success Metrics. +- `_techspec.md` → API Contracts; Integration Points; Build Order 4. +- `../../adr/0053-terminal-run-worktree-reconciliation-is-proof-based.md` → + explicit dry-run/apply command. diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/task_05.md b/docs/specs/0038-terminal-run-worktree-reconciliation/task_05.md new file mode 100644 index 0000000..cef477f --- /dev/null +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/task_05.md @@ -0,0 +1,69 @@ +--- +task: task_05 +spec: 0038-terminal-run-worktree-reconciliation +status: pending +type: backend +complexity: medium +--- + +# Task 05: Surface retained terminal Run Worktrees in Runs List + +## Overview + +Make retained terminal Run Worktrees discoverable without changing the stable +Runs List stdout rows. Each view reports the exact relevant count on stderr and +points to the Reconcile Command as the sole classification surface. + +## Requirements + +1. MUST preserve existing Runs List stdout row shape byte-for-byte. +2. MUST count terminal spec Runs that retain a recorded worktree path or Run + Branch. +3. MUST report hidden retained residue when the default Active view has no + matching terminal rows. +4. MUST report retained residue represented in terminal and all-state views. +5. MUST write the bounded note to stderr only. +6. MUST point to `roundfix reconcile` without classifying safety in Runs List. +7. MUST omit the note when no retained terminal surface exists. + +## Subtasks + +- [ ] Derive repository-scoped retained-worktree counts. +- [ ] Add the exact stderr guidance. +- [ ] Preserve stdout row serialization. +- [ ] Cover Active, terminal, and all-state views. +- [ ] Cover zero-retention and mixed-repository cases. + +## Acceptance Criteria + +- [ ] Active view reports the exact hidden terminal residue count on stderr. +- [ ] Terminal and all-state views report only retained Runs relevant to their + repository scope. +- [ ] Existing stdout rows remain byte-identical. +- [ ] The note contains the exact `roundfix reconcile` pointer. +- [ ] Runs List never labels a retained entry safe or unsafe. +- [ ] No note is printed when neither worktree nor Run Branch remains. + +## Context + +- instruction: `.agents/skills/agentic-cli-design/SKILL.md` +- instruction: `.agents/skills/golang-cli/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/cli/runs.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/store/store.go` +- interface: `internal/worktree/worktree.go` + +## Verification + +- `rtk go test ./internal/cli -run 'TestRun.*List.*Retained.*Worktree' -count=1` + — expected: all views report exact stderr counts while stdout stays + byte-stable. +- `rtk go test ./internal/cli -run 'TestRun.*List.*(Active|Terminal|All)' -count=1` + — expected: existing Runs List view contracts remain passing. + +## References + +- `_prd.md` → Goal 3; User Story 3; Core Feature 8; Success Metrics. +- `_techspec.md` → API Contracts: Run listing; Testing Approach; Build Order 5. +- `CONTEXT.md` → Runs List and Reconcile Command vocabulary. diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/task_06.md b/docs/specs/0038-terminal-run-worktree-reconciliation/task_06.md new file mode 100644 index 0000000..ca5ae0e --- /dev/null +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/task_06.md @@ -0,0 +1,76 @@ +--- +task: task_06 +spec: 0038-terminal-run-worktree-reconciliation +status: pending +type: docs +complexity: medium +--- + +# Task 06: Align reconciliation docs and glossary + +## Overview + +Publish the Reconcile Command, five reconciliation states, dry-run/apply flow, +and Runs List discovery contract through supported documentation and canonical +vocabulary. This slice leaves every protected Skill path unchanged. + +## Requirements + +1. MUST define the Reconcile Command and Run Worktree Reconciliation states in + canonical glossary vocabulary. +2. MUST document single-Run and repository-wide dry-run examples. +3. MUST document `--apply` as the only mutation switch and the absence of a + force bypass. +4. MUST explain positive cleanliness and ancestry proof plus conservative + preservation of dirty, unintegrated, and unknown work. +5. MUST document Integration Pending promotion and unchanged other outcomes. +6. MUST document Runs List stderr discovery without promising classification. +7. MUST preserve ADR/finding traceability and leave protected tooling + unchanged. + +## Subtasks + +- [ ] Add canonical reconciliation vocabulary. +- [ ] Update command and usage documentation. +- [ ] Add dry-run, JSON, and explicit apply examples. +- [ ] Explain state, proof, and preservation semantics. +- [ ] Document Runs List retained-worktree guidance. +- [ ] Resolve ADR, Spec, finding, and command links. + +## Acceptance Criteria + +- [ ] A reader can predict all five states from the documented evidence rules. +- [ ] Examples distinguish requested stdout from diagnostic stderr. +- [ ] No documentation suggests age, outcome, missing path, or force is + sufficient for deletion. +- [ ] Integration Pending and other terminal outcome behavior is explicit. +- [ ] Runs List guidance points to Reconcile Command for classification. +- [ ] Every cited link resolves and terminology matches `CONTEXT.md`. +- [ ] No protected tooling path changes in this Task. + +## Context + +- instruction: `docs/agents/cli.md` +- instruction: `docs/agents/domain.md` +- instruction: `.agents/skills/tech-writer/SKILL.md` +- interface: `CONTEXT.md` +- interface: `README.md` +- interface: `docs/user-guide/commands.md` +- interface: `docs/user-guide/usage.md` +- interface: `docs/findings/2026-07-16-vortex-pr87-detached-watch-notification.md` + +## Verification + +- `rtk grep -n 'roundfix reconcile\\|Reconcile Command\\|safe\\|unintegrated\\|released' CONTEXT.md docs/user-guide/commands.md docs/user-guide/usage.md` + — expected: canonical command and state guidance is present. +- `rtk go test ./internal/cli -run 'Test.*(Reconcile.*Help|DocumentationContract|RunsList.*Retained)' -count=1` + — expected: command and Runs List public contracts match documentation. +- `rtk git diff --check` + — expected: documentation contains no whitespace errors. + +## References + +- `_prd.md` → User Stories 1–5; User Experience; Non-Goals; Decisions. +- `_techspec.md` → API Contracts; Risks & Considerations; Build Order 6. +- `../../adr/0053-terminal-run-worktree-reconciliation-is-proof-based.md` → + canonical reconciliation behavior. diff --git a/docs/specs/0038-terminal-run-worktree-reconciliation/task_07.md b/docs/specs/0038-terminal-run-worktree-reconciliation/task_07.md new file mode 100644 index 0000000..d7efa34 --- /dev/null +++ b/docs/specs/0038-terminal-run-worktree-reconciliation/task_07.md @@ -0,0 +1,74 @@ +--- +task: task_07 +spec: 0038-terminal-run-worktree-reconciliation +status: pending +type: docs +complexity: low +--- + +# Task 07: Align the protected Roundfix Skill pair + +## Overview + +Publish proof-based Run Worktree reconciliation in the canonical and generated +Roundfix Skill. This tooling-only slice is restricted to the two exact files +authorized by the maintainer and this Task file. + +## Requirements + +1. MUST align `.agents/skills/roundfix/SKILL.md` with dry-run-first + reconciliation, five states, and explicit safe-only apply. +2. MUST apply byte-identical content to `skills/roundfix/SKILL.md`. +3. MUST limit repository changes to those two authorized `SKILL.md` files and + this `task_07.md` file. +4. MUST NOT run `make skills-sync`, because it rewrites every owned Skill + directory; use the read-only sync check. +5. MUST leave code, tests, manifests, public docs, other owned Skills, + upstream-managed Skills, locks, and recommendation files unchanged. + +## Subtasks + +- [ ] Update the canonical Roundfix Skill reconciliation contract. +- [ ] Apply the identical generated Roundfix Skill copy. +- [ ] Verify exact changed-file scope and byte identity. +- [ ] Confirm shipped Skill and full-gate compatibility. + +## Acceptance Criteria + +- [ ] The Skill tells an Agent to inspect before apply and never use manual Git + deletion as the supported workflow. +- [ ] Dirty, unintegrated, and unknown results are preserved in Skill guidance. +- [ ] Canonical and generated files are byte-identical. +- [ ] Git evidence contains only the two authorized Skill paths and this Task + file. +- [ ] No other protected or upstream-managed Skill changes. +- [ ] Shipped Skill validation and the complete repository gate pass. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/skill-dispatch.md` +- interface: `.agents/skills/roundfix/SKILL.md` +- interface: `skills/roundfix/SKILL.md` + +## Verification + +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` + — expected: no output and exit zero. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "docs/specs/0038-terminal-run-worktree-reconciliation/task_07.md") {print; bad=1}} END {exit bad}'` + — expected: no changed path outside the authorized pair and this Task file. +- `rtk make skills-sync-check` + — expected: every canonical/generated owned Skill pair has no drift. +- `rtk go run -buildvcs=false ./cmd/roundfix skills check` + — expected: every shipped Roundfix Skill contract passes. +- `rtk git diff --check` + — expected: no whitespace errors. +- `rtk make verify` + — expected: formatting, tests, Skill checks, and build pass. + +## References + +- `_prd.md` → Goals; User Experience; Decisions; Project Constraints. +- `_techspec.md` → Build Order 7; Decisions. +- `docs/agents/spec-routing.md` → tooling authorization and changed-file + postflight. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/_tasks.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/_tasks.md new file mode 100644 index 0000000..ee140bc --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/_tasks.md @@ -0,0 +1,53 @@ +--- +schema: spec-tasks/v1 +spec: 0039-review-source-evidence-and-detached-outcomes +graph: + nodes: + - id: task_01 + file: task_01.md + needs: [] + - id: task_02 + file: task_02.md + needs: [task_01] + - id: task_03 + file: task_03.md + needs: [task_02] + - id: task_04 + file: task_04.md + needs: [task_01, task_02] + - id: task_05 + file: task_05.md + needs: [task_03, task_04] + - id: task_06 + file: task_06.md + needs: [task_05] + - id: task_07 + file: task_07.md + needs: [task_02, task_05] + - id: task_08 + file: task_08.md + needs: [task_03, task_04, task_06, task_07] + - id: task_09 + file: task_09.md + needs: [task_08] +--- + +# Tasks — Review Source evidence and Detached Run outcomes + +Cross-Spec prerequisite: Spec 0037 Terminal Outcome Integrity must be completed +before this graph starts because Stop Request interruption, registered-session +cleanup, and winner-only terminal publication are required boundaries. + +| id | title | type | complexity | needs | +| ------- | --------------------------------------------------------------- | ------- | ---------- | -------------------------------------- | +| task_01 | Model Review Source Evidence and transient failures | backend | high | — | +| task_02 | Unify CodeRabbit evidence classification | backend | high | task_01 | +| task_03 | Settle Review Skipped without Round artifacts | backend | high | task_02 | +| task_04 | Retry transient Review Source failures and project waits | backend | high | task_01, task_02 | +| task_05 | Report Review Issue knowledge and terminal context | backend | high | task_03, task_04 | +| task_06 | Deliver notification receipts and Detached monitoring | backend | high | task_05 | +| task_07 | Inherit evidence only for Daemon artifact commits | backend | high | task_02, task_05 | +| task_08 | Align review-evidence docs and glossary | docs | medium | task_03, task_04, task_06, task_07 | +| task_09 | Align the protected Roundfix Skill pair | docs | low | task_08 | + +Waves: 1 → task_01 · 2 → task_02 · 3 → task_03, task_04 · 4 → task_05 · 5 → task_06, task_07 · 6 → task_08 · 7 → task_09 diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_01.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_01.md new file mode 100644 index 0000000..c04ef99 --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_01.md @@ -0,0 +1,80 @@ +--- +task: task_01 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: backend +complexity: high +--- + +# Task 01: Model Review Source Evidence and transient failures + +## Overview + +Introduce Review Source-neutral evidence and transient-failure contracts while +extending CodeRabbit signal parsing with the structured fields needed for +classification. This slice establishes typed, head-bound observations without +yet deciding the watch lifecycle outcome. + +## Requirements + +1. MUST model pending, reviewing, reviewed, verified, skipped, and failed + Evidence tied to an expected and observed head. +2. MUST use stable Review Source-neutral evidence kinds and bounded details. +3. MUST preserve signal identity, conclusion, and explicit skip reason. +4. MUST parse CodeRabbit check output title and summary without treating + arbitrary successful text as a skip. +5. MUST provide a typed transient failure preserving the wrapped cause and + failed operation. +6. MUST positively classify temporary DNS, reset, HTTP 429, GitHub 5xx, and + non-parent context timeouts as transient. +7. MUST keep cancellation, authentication, validation, and malformed responses + permanent. + +## Subtasks + +- [ ] Add Evidence state, kind, and head-bound fields. +- [ ] Add the typed transient error and inspection helper. +- [ ] Extend CodeRabbit check-run output parsing. +- [ ] Preserve structured signal identity and bounded details. +- [ ] Add transient and permanent failure matrices. +- [ ] Add JSON mapping and sensitive-data regression coverage. + +## Acceptance Criteria + +- [ ] Every Evidence value carries stable state, kind, identity, and relevant + head without provider-specific response leakage. +- [ ] Structured skip title or summary remains available for later + classification. +- [ ] Each approved temporary failure is discoverable through error wrapping. +- [ ] Parent Run cancellation is never classified transient. +- [ ] Authentication and invalid-request failures remain permanent. +- [ ] Evidence and errors contain no credentials or unbounded response bodies. +- [ ] Existing CodeRabbit fetch and resolution behavior remains passing. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-context/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/reviewsource/reviewsource.go` +- interface: `internal/reviewsource/coderabbit/coderabbit.go` +- interface: `internal/reviewsource/coderabbit/coderabbit_test.go` +- interface: `internal/watch/watch.go` + +## Verification + +- `rtk go test ./internal/reviewsource/... -run 'Test.*(Evidence|Transient|CheckRunOutput|SkipSignal)' -count=1` + — expected: evidence shape, structured output parsing, and transient + classification matrices pass. +- `rtk go test -race ./internal/reviewsource/... -run 'Test.*(Evidence|Transient)' -count=1` + — expected: typed evidence and error handling are race-free. + +## References + +- `_prd.md` → Goals 1–2; User Stories 1–2 and 7; Core Features 1 and 5; + Success Metrics. +- `_techspec.md` → Interfaces; Data Models: CodeRabbit check-run parsing; + Testing Approach; Build Order 1. +- `../../adr/0054-review-source-evidence-determines-review-outcomes.md` → + head-bound Evidence. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_02.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_02.md new file mode 100644 index 0000000..46de59f --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_02.md @@ -0,0 +1,84 @@ +--- +task: task_02 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: backend +complexity: high +--- + +# Task 02: Unify CodeRabbit evidence classification + +## Overview + +Replace separate pre-fetch and Merge-Ready interpretations with one CodeRabbit +evidence hierarchy. Both watch phases receive the same result for the same +head, including explicit skip, current-head approval, unresolved-thread, stale +signal, and no-signal cases. + +## Requirements + +1. MUST expose one evidence operation for an Open Pull Request and expected + head. +2. MUST apply the TechSpec evidence hierarchy in stable precedence order. +3. MUST classify explicit structured skip before successful or pending signals. +4. MUST require the expected head for every accepted check, status, or review. +5. MUST accept a current-head CodeRabbit `APPROVED` review as verified only when + zero unresolved CodeRabbit threads remain. +6. MUST classify other current-head reviews as reviewed, never verified + approval. +7. MUST publish changed observations once and suppress duplicate unchanged + polling events. + +## Subtasks + +- [ ] Implement the shared evidence operation. +- [ ] Unify check-run, commit-status, review, and thread precedence. +- [ ] Route pre-fetch and Merge-Ready checks through the classifier. +- [ ] Enforce expected-head and approval requirements. +- [ ] Publish bounded changed-observation events. +- [ ] Add stale-head, unresolved-thread, and precedence tables. + +## Acceptance Criteria + +- [ ] Pre-fetch and Merge-Ready return byte-equivalent Evidence for the same + fixture. +- [ ] Explicit skip wins over success or pending signals and preserves reason. +- [ ] Current-head approval with zero unresolved threads produces verified + `review_approval` Evidence. +- [ ] Commented, stale-head, or unresolved-thread reviews cannot prove + Merge-Ready. +- [ ] No usable signal produces pending without guessing. +- [ ] One changed observation creates one event; an unchanged poll creates + none. +- [ ] Existing Review Source authentication and repository boundaries remain + unchanged. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `internal/reviewsource/reviewsource.go` +- interface: `internal/reviewsource/coderabbit/coderabbit.go` +- interface: `internal/reviewsource/coderabbit/coderabbit_test.go` +- interface: `internal/watch/watch.go` +- interface: `internal/watch/watch_test.go` +- interface: `internal/runevent/event.go` +- interface: `internal/runevent/event_test.go` + +## Verification + +- `rtk go test ./internal/reviewsource/... -run 'Test.*(EvidenceHierarchy|Approval|Skipped|ExpectedHead|UnresolvedThread|Precedence)' -count=1` + — expected: one classifier produces every accepted and refused state. +- `rtk go test ./internal/watch ./internal/runevent -run 'Test.*(ReviewEvidence|ReviewStatusEvent|UnchangedEvidence)' -count=1` + — expected: both watch phases share Evidence and events deduplicate unchanged + polls. + +## References + +- `_prd.md` → Goal 1; User Stories 1 and 7; Core Features 1–3; Success Metrics. +- `_techspec.md` → API Contracts: Review Source evidence hierarchy; Integration + Points: GitHub through `gh`; Build Order 2. +- `../../adr/0054-review-source-evidence-determines-review-outcomes.md` → + accepted Review Source Evidence. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_03.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_03.md new file mode 100644 index 0000000..797ee97 --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_03.md @@ -0,0 +1,94 @@ +--- +task: task_03 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: backend +complexity: high +--- + +# Task 03: Settle Review Skipped without Round artifacts + +## Overview + +Add Review Skipped as a terminal review outcome across persistence, watch +settlement, reports, streams, and Run Browser projection. An explicit skip ends +before fetch and publishes actionable reason evidence without creating an +empty or partial Round artifact. + +## Requirements + +1. MUST add Review Skipped to terminal Run state validation and storage. +2. MUST advance the Run Database schema fence before the new state can be + persisted. +3. MUST map explicit skipped Evidence to Review Skipped and existing exit code + `3`. +4. MUST render the Review Source reason and next action without zero issue + counts. +5. MUST project the outcome through Run Events and Run Browser state. +6. MUST skip Round fetch and create no Round directory for Review Skipped. +7. MUST keep failed pre-fetch writes temporary and unpublished. +8. MUST use the spec 0037 registered-session cleanup boundary when settlement + occurs before Agent Start, preserving the Review Source reason ahead of any + secondary cleanup warning. + +## Subtasks + +- [ ] Add the terminal state and migration fence. +- [ ] Map skipped Evidence through watch and CLI settlement. +- [ ] Render actionable text and machine outcome evidence. +- [ ] Add Run Browser and stream projection. +- [ ] Prevent fetch and artifact publication on skip. +- [ ] Exercise cleanup-before-Agent settlement through the registered-session + boundary. +- [ ] Add migration, exit, report, and no-artifact cases. + +## Acceptance Criteria + +- [ ] A structured skip ends Review Skipped with exit `3`. +- [ ] Persisted Review Skipped is terminal and older schema readers refuse the + newer database. +- [ ] The report names skip reason and next action and prints no zero-valued + Review Issue summary. +- [ ] No fetcher call, Round directory, partial artifact, or review-artifact + commit occurs. +- [ ] A pre-Agent skip with no active Agent Selection lifecycle performs zero + Agent Session cleanup calls; any eligible cleanup warning remains + secondary to the Review Source reason. +- [ ] Outcome stream and Run Browser render the same terminal state. +- [ ] Non-skip Evidence preserves existing Reviewed, Clean, Clean Unverified, + and failure behavior. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/store/store.go` +- interface: `internal/store/store_test.go` +- interface: `internal/store/agent_selection.go` +- interface: `internal/watch/watch.go` +- interface: `internal/watch/watch_test.go` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/cli/runbrowser.go` +- interface: `internal/runevent/event.go` +- interface: `internal/agent/sessions.go` + +## Verification + +- `rtk go test ./internal/store -run 'Test.*(ReviewSkipped|Schema.*Review)' -count=1` + — expected: migration fence and terminal state persistence pass. +- `rtk go test ./internal/watch ./internal/cli ./internal/runevent -run 'Test.*(ReviewSkipped|CleanupBeforeAgent)' -count=1` + — expected: skipped Evidence settles, reports, projects, and creates no Round + artifact while registered-session cleanup preserves primary reason ordering. +- `rtk go test -race ./internal/watch ./internal/cli -run 'Test.*ReviewSkipped' -count=1` + — expected: terminal skip settlement is race-free. + +## References + +- `_prd.md` → Goal 1; User Story 1; Core Features 2, 7, and 12; User Experience; + Success Metrics. +- `_techspec.md` → Data Models: schema fence; API Contracts: Review Skipped and + Artifact persistence; Build Order 3. +- `../../adr/0054-review-source-evidence-determines-review-outcomes.md` → + Review Skipped outcome. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_04.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_04.md new file mode 100644 index 0000000..ac135e0 --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_04.md @@ -0,0 +1,84 @@ +--- +task: task_04 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: backend +complexity: high +--- + +# Task 04: Retry transient Review Source failures and project waits + +## Overview + +Retry only positively typed transient Review Source failures within existing +timeout and Run Budget bounds while projecting every wait phase and retry +episode. Fake-clock coverage proves bounded recovery, exhaustion, and graceful +Stop Request interruption without real sleeps. + +## Requirements + +1. MUST retry only typed transient errors and never infer retryability from + output text. +2. MUST reuse the configured poll interval, Review Source timeout, and Run + Budget without a new retry setting. +3. MUST publish one retry-start event and one recovery or exhaustion event per + episode. +4. MUST expose phase, expected head, start, deadline, Evidence, and retry status + for both Review Source wait phases. +5. MUST publish progress only on phase, Evidence, or retry changes. +6. MUST let the Store-backed Stop Request interrupt retry sleep and win over + another attempt. +7. MUST keep authentication, validation, and permanent failures terminal. + +## Subtasks + +- [ ] Add bounded transient retry episodes to watch. +- [ ] Reuse existing time and budget boundaries. +- [ ] Add wait-phase and deadline projection. +- [ ] Deduplicate unchanged progress and episode events. +- [ ] Integrate graceful Stop Request interruption. +- [ ] Add recovery, exhaustion, permanent, and cancellation matrices. + +## Acceptance Criteria + +- [ ] One transient failure retries and can recover without ending the Run. +- [ ] Exhaustion occurs at the existing timeout or Run Budget boundary. +- [ ] Each episode contains at most one start and one terminal episode event. +- [ ] Permanent failure performs zero retry sleeps. +- [ ] Both wait phases expose the documented head, deadline, Evidence, and retry + fields. +- [ ] Unchanged polling appends no duplicate progress event. +- [ ] Stop Request during retry sleep starts no later Review Source call. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-context/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `internal/watch/watch.go` +- interface: `internal/watch/watch_test.go` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/runevent/event.go` +- interface: `internal/runevent/event_test.go` + +## Verification + +- `rtk go test ./internal/watch -run 'TestRun.*(Transient|Retry|WaitPhase|StopRequest)' -count=1` + — expected: fake-clock recovery, exhaustion, projection, deduplication, and + stop cases pass. +- `rtk go test ./internal/cli ./internal/runevent -run 'Test.*(ReviewRetry|WaitingForReview|ReviewStatusEvent)' -count=1` + — expected: CLI and stream expose bounded wait evidence. +- `rtk go test -race ./internal/watch ./internal/cli -run 'Test.*(Transient|Retry|StopRequest)' -count=1` + — expected: retry and cancellation have no race or leaked waiter. + +## References + +- `_prd.md` → Goals 2–3; User Stories 2–3; Core Features 5–6; User Experience; + Success Metrics. +- `_techspec.md` → API Contracts: Transient retry and Wait projection; Testing + Approach; Build Order 4. +- `../0037-terminal-outcome-integrity/_techspec.md` → stop-aware Review Source + waits. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_05.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_05.md new file mode 100644 index 0000000..7d2a339 --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_05.md @@ -0,0 +1,82 @@ +--- +task: task_05 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: backend +complexity: high +--- + +# Task 05: Report Review Issue knowledge and terminal context + +## Overview + +Carry whether Review Issues were actually fetched and one bounded terminal +context through text reports and the durable outcome stream. Pre-fetch failure +becomes explicitly unknown instead of appearing as five misleading zero +counts, and every non-Clean outcome gains an actionable next step. + +## Requirements + +1. MUST mark Review Issue knowledge true only after fetch completes. +2. MUST report `Review Issues: unknown — fetch did not complete.` when + knowledge is false and omit all zero-valued status counts. +3. MUST retain valid zero-issue summaries after a successful fetch. +4. MUST carry bounded terminal reason, next action, Evidence, verified head, + Console Log, and Attach command through one terminal context. +5. MUST project additive optional fields through `roundfix-events/v1`. +6. MUST preserve existing stdout, stderr, filter, and unknown-field contracts. +7. MUST publish terminal context only through the completion winner. + +## Subtasks + +- [ ] Extend watch result with issue knowledge and terminal context. +- [ ] Render unknown versus known-zero reports. +- [ ] Build one terminal-context boundary. +- [ ] Extend additive outcome stream projection. +- [ ] Preserve existing JSONL and filter behavior. +- [ ] Add pre-fetch, fetched-zero, and terminal-outcome cases. + +## Acceptance Criteria + +- [ ] Failed status discovery or fetch emits one unknown line and no numeric + Review Issue counts. +- [ ] A completed zero-issue fetch remains known and reports valid zeros. +- [ ] Every non-Clean outcome event contains reason and next action. +- [ ] Console Log, Attach command, issue knowledge, and Evidence are present + when available and omitted safely when unavailable. +- [ ] Existing `roundfix-events/v1` consumers can ignore additive fields. +- [ ] Requested output remains on stdout and diagnostics remain on stderr. +- [ ] A losing completion path emits no duplicate terminal context. + +## Context + +- instruction: `.agents/skills/agentic-cli-design/SKILL.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/watch/watch.go` +- interface: `internal/watch/watch_test.go` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/runevent/event.go` +- interface: `internal/runevent/event_test.go` +- interface: `internal/runevent/stream.go` + +## Verification + +- `rtk go test ./internal/watch ./internal/cli -run 'Test.*(ReviewIssuesKnown|ReviewIssuesUnknown|TerminalContext|FetchedZero)' -count=1` + — expected: pre-fetch failures are unknown and successful zero fetches remain + known. +- `rtk go test ./internal/runevent -run 'Test.*(Outcome.*Context|ReviewIssues|Evidence)' -count=1` + — expected: additive outcome records carry bounded actionable context. +- `rtk go test -race ./internal/watch ./internal/cli ./internal/runevent -run 'Test.*(TerminalContext|ReviewIssues)' -count=1` + — expected: terminal context publication is race-free. + +## References + +- `_prd.md` → Goals 3–4; User Stories 3–4 and 6; Core Features 6–9; User + Experience; Success Metrics. +- `_techspec.md` → Data Models: watch.Result; API Contracts: Terminal report, + stream, and notification; Build Order 5. +- `../../adr/0052-run-completion-is-compare-and-set.md` → winner-only outcome + publication. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_06.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_06.md new file mode 100644 index 0000000..89081f7 --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_06.md @@ -0,0 +1,89 @@ +--- +task: task_06 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: backend +complexity: high +--- + +# Task 06: Deliver notification receipts and Detached monitoring + +## Overview + +Give Detached Run users and Supervisors first-party terminal monitoring while +making every notification attempt durably observable. Notifications receive +the same terminal context as reports, return sent/skipped/failed receipts, and +remain unable to change the Run outcome. + +## Requirements + +1. MUST return a typed receipt with route, sent/skipped/failed status, and + completion time for every notification attempt. +2. MUST preserve existing notification environment variables and add the + documented terminal-context variables. +3. MUST journal exactly one receipt event after each attempt. +4. MUST keep native and command notification failures best-effort and unable + to change Run state or exit code. +5. MUST print Detached startup as Run ID, Console Log, Attach, Supervisor + outcome monitor, and Stop lines. +6. MUST use `roundfix events --follow --filter outcome` as the + Supervisor monitor command. +7. MUST bound native notification text while keeping complete structured + context available to `notify.command`. + +## Subtasks + +- [ ] Add notification receipt contracts. +- [ ] Populate additive command environment values. +- [ ] Journal one durable receipt event per attempt. +- [ ] Preserve outcome on notification failure. +- [ ] Add the five-line Detached startup report. +- [ ] Cover sent, skipped, failed, native, and command routes. + +## Acceptance Criteria + +- [ ] Sent, disabled/unavailable, and failed routes produce sent, skipped, and + failed receipts respectively. +- [ ] Every attempt appends exactly one route/status/completion-time event. +- [ ] Existing environment variables remain byte-compatible and additive + values carry terminal context. +- [ ] Notification failure changes neither stored outcome nor top-level exit + code. +- [ ] Detached startup contains all five documented lines with runnable + commands. +- [ ] Supervisor monitoring uses the stable outcome filter and no Console Log + parsing. +- [ ] Native text stays bounded and contains the next action when non-Clean. + +## Context + +- instruction: `.agents/skills/agentic-cli-design/SKILL.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/notify/notify.go` +- interface: `internal/notify/notify_test.go` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/cli/detach.go` +- interface: `internal/cli/detach_test.go` +- interface: `internal/runevent/event.go` +- interface: `internal/runevent/event_test.go` + +## Verification + +- `rtk go test ./internal/notify -run 'Test.*(Receipt|Environment|Native|Command)' -count=1` + — expected: all routes produce bounded context and typed receipts. +- `rtk go test ./internal/cli ./internal/runevent -run 'Test.*(Detached.*Monitor|OutcomeNotification|NotificationReceipt)' -count=1` + — expected: five-line startup and one durable receipt per attempt pass. +- `rtk go test -race ./internal/notify ./internal/cli -run 'Test.*(Notification|Detached)' -count=1` + — expected: receipt and Detached reporting are race-free. + +## References + +- `_prd.md` → Goal 4; User Stories 4–5; Core Features 8–11; User Experience; + Success Metrics. +- `_techspec.md` → Interfaces: NotificationReceipt; API Contracts: Terminal + report, stream, and notification; Build Order 6. +- `CONTEXT.md` → Run Outcome Notification, Console Log, Attach, and Run Event + Stream. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_07.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_07.md new file mode 100644 index 0000000..9fda619 --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_07.md @@ -0,0 +1,84 @@ +--- +task: task_07 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: backend +complexity: high +--- + +# Task 07: Inherit evidence only for Daemon artifact commits + +## Overview + +Prove when the current head is exactly the Daemon-created review-artifact-only +descendant of a verified parent and inherit Evidence only in that narrow case. +Real Git fixtures reject mixed paths, user-authored commits, wrong parents, +empty diffs, unresolved threads, and unrecognized artifact roots. + +## Requirements + +1. MUST return created commit SHA, parent SHA, and resolved review root from + Daemon review-artifact commit creation. +2. MUST require accepted verified Evidence for the exact parent. +3. MUST require the current head to equal the exact Daemon-created artifact + commit. +4. MUST require a non-empty parent-to-current diff wholly under the resolved + review-artifact root. +5. MUST require zero unresolved CodeRabbit threads. +6. MUST emit `artifact_only_descendant` Evidence carrying both heads. +7. MUST fall back to normal current-head Evidence polling on any mismatch and + MUST NOT publish another Roundfix review request for an inherited head. + +## Subtasks + +- [ ] Return exact commit identity from artifact publication. +- [ ] Prove parent, current head, and review root. +- [ ] Validate non-empty artifact-only diffs. +- [ ] Require verified parent Evidence and no unresolved threads. +- [ ] Publish inherited Evidence with both heads. +- [ ] Add real Git positive and refusal fixtures. + +## Acceptance Criteria + +- [ ] An exact Daemon artifact-only descendant inherits verified parent + Evidence without another Roundfix request or wait. +- [ ] Mixed-path, user-authored, wrong-parent, and empty commits do not inherit. +- [ ] A path outside the resolved review root refuses inheritance. +- [ ] Unresolved CodeRabbit threads refuse inheritance. +- [ ] Inherited Evidence names `artifact_only_descendant` and both heads. +- [ ] Every refusal falls back to normal current-head polling. +- [ ] Existing separate review-artifact commit policy remains intact. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- interface: `internal/cli/cli.go` +- interface: `internal/cli/cli_test.go` +- interface: `internal/config/config.go` +- interface: `internal/config/config_test.go` +- interface: `internal/preflight/preflight.go` +- interface: `internal/daemon/daemon.go` +- interface: `internal/daemon/daemon_test.go` + +## Verification + +- `rtk go test ./internal/cli -run 'Test.*Artifact.*Evidence.*(Inherited|Mixed|Parent|Empty|User|Thread|Root)' -count=1` + — expected: real Git fixtures accept only the exact Daemon artifact + descendant. +- `rtk go test ./internal/config ./internal/daemon -run 'Test.*(ReviewRoot|ReviewArtifactsCommit)' -count=1` + — expected: review-root resolution and Daemon commit identity remain stable. +- `rtk go test -race ./internal/cli -run 'Test.*Artifact.*Evidence' -count=1` + — expected: artifact identity and Evidence inheritance are race-free. + +## References + +- `_prd.md` → Goal 5; User Story 8; Core Features 3–4; Success Metrics. +- `_techspec.md` → API Contracts: Artifact-only descendant; Artifact + persistence; Build Order 7. +- `../../adr/0036-review-artifacts-are-committed-in-a-separate-docs-commit.md` → + separate + artifact commit. +- `../../adr/0054-review-source-evidence-determines-review-outcomes.md` → + narrowly inherited Evidence. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_08.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_08.md new file mode 100644 index 0000000..230f7cd --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_08.md @@ -0,0 +1,84 @@ +--- +task: task_08 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: docs +complexity: medium +--- + +# Task 08: Align review-evidence docs and glossary + +## Overview + +Publish Review Source Evidence, Review Skipped, bounded retry, issue knowledge, +Detached monitoring, notification receipts, and artifact-only inheritance +through canonical vocabulary and supported user guidance. This slice leaves +every protected Skill path unchanged. + +## Requirements + +1. MUST define Review Source Evidence and Review Skipped in canonical + vocabulary. +2. MUST document accepted current-head check, status, and CodeRabbit approval + evidence plus stale/unresolved refusal. +3. MUST document typed bounded retry and the absence of log-text inference or + new retry configuration. +4. MUST document wait phases, deadlines, retry episodes, and unknown Review + Issue counts. +5. MUST document Detached Supervisor monitoring and notification receipt + semantics. +6. MUST document the exact artifact-only inheritance proof and its refusals. +7. MUST preserve ADR/finding traceability and leave protected tooling + unchanged. + +## Subtasks + +- [ ] Add canonical Evidence and Review Skipped vocabulary. +- [ ] Update Watch and Detached command guidance. +- [ ] Document retry, waits, and unknown issue knowledge. +- [ ] Document notification context and receipt events. +- [ ] Document artifact-only inheritance boundaries. +- [ ] Resolve ADR, finding, Spec, and command links. + +## Acceptance Criteria + +- [ ] A reader can distinguish pending, reviewed, verified, skipped, and failed + Evidence on the expected head. +- [ ] Review Skipped cannot be mistaken for Clean, Clean Unverified, or a + zero-issue Round. +- [ ] Retry guidance names positive typed conditions and existing bounds only. +- [ ] Detached examples contain the stable Supervisor outcome command. +- [ ] Notification delivery is described as best-effort and separately + receipted. +- [ ] Artifact inheritance guidance refuses every non-Daemon or mixed-path + descendant. +- [ ] Every link resolves, canonical terms are used, and no protected tooling + path changes. + +## Context + +- instruction: `docs/agents/cli.md` +- instruction: `docs/agents/domain.md` +- instruction: `.agents/skills/tech-writer/SKILL.md` +- interface: `CONTEXT.md` +- interface: `README.md` +- interface: `docs/user-guide/commands.md` +- interface: `docs/user-guide/usage.md` +- interface: `docs/findings/2026-07-16-vortex-pr87-detached-watch-notification.md` + +## Verification + +- `rtk grep -n 'Review Source Evidence\\|Review Skipped\\|events .*--follow.*outcome\\|Review Issues: unknown' CONTEXT.md docs/user-guide/commands.md docs/user-guide/usage.md` + — expected: canonical Evidence, outcome, monitor, and unknown-count guidance + is present. +- `rtk go test ./internal/cli -run 'Test.*(Watch.*Help|Detached.*Monitor|DocumentationContract)' -count=1` + — expected: supported command examples match implemented behavior. +- `rtk git diff --check` + — expected: documentation contains no whitespace errors. + +## References + +- `_prd.md` → User Stories 1–8; User Experience; Non-Goals; Decisions. +- `_techspec.md` → API Contracts; Risks & Considerations; Build Order 8. +- `../../adr/0054-review-source-evidence-determines-review-outcomes.md` → + canonical review outcome behavior. diff --git a/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_09.md b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_09.md new file mode 100644 index 0000000..1682764 --- /dev/null +++ b/docs/specs/0039-review-source-evidence-and-detached-outcomes/task_09.md @@ -0,0 +1,75 @@ +--- +task: task_09 +spec: 0039-review-source-evidence-and-detached-outcomes +status: pending +type: docs +complexity: low +--- + +# Task 09: Align the protected Roundfix Skill pair + +## Overview + +Publish Review Source Evidence and Detached outcome handling in the canonical +and generated Roundfix Skill. This tooling-only slice is restricted to the two +exact files authorized by the maintainer and this Task file. + +## Requirements + +1. MUST align `.agents/skills/roundfix/SKILL.md` with Review Skipped, bounded + transient retry, unknown issue knowledge, Detached monitoring, notification + receipts, and artifact-only Evidence inheritance. +2. MUST apply byte-identical content to `skills/roundfix/SKILL.md`. +3. MUST limit repository changes to those two authorized `SKILL.md` files and + this `task_09.md` file. +4. MUST NOT run `make skills-sync`, because it rewrites every owned Skill + directory; use the read-only sync check. +5. MUST leave code, tests, manifests, public docs, other owned Skills, + upstream-managed Skills, locks, and recommendation files unchanged. + +## Subtasks + +- [ ] Update the canonical Roundfix Skill review-evidence contract. +- [ ] Apply the identical generated Roundfix Skill copy. +- [ ] Verify exact changed-file scope and byte identity. +- [ ] Confirm shipped Skill and full-gate compatibility. + +## Acceptance Criteria + +- [ ] The Skill distinguishes Review Skipped and unknown issue knowledge from + successful zero-issue review. +- [ ] The Skill surfaces the Supervisor monitor command and exact next actions. +- [ ] Canonical and generated files are byte-identical. +- [ ] Git evidence contains only the two authorized Skill paths and this Task + file. +- [ ] No other protected or upstream-managed Skill changes. +- [ ] Shipped Skill validation and the complete repository gate pass. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/skill-dispatch.md` +- interface: `.agents/skills/roundfix/SKILL.md` +- interface: `skills/roundfix/SKILL.md` + +## Verification + +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` + — expected: no output and exit zero. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "docs/specs/0039-review-source-evidence-and-detached-outcomes/task_09.md") {print; bad=1}} END {exit bad}'` + — expected: no changed path outside the authorized pair and this Task file. +- `rtk make skills-sync-check` + — expected: every canonical/generated owned Skill pair has no drift. +- `rtk go run -buildvcs=false ./cmd/roundfix skills check` + — expected: every shipped Roundfix Skill contract passes. +- `rtk git diff --check` + — expected: no whitespace errors. +- `rtk make verify` + — expected: formatting, tests, Skill checks, and build pass. + +## References + +- `_prd.md` → Goals; User Experience; Decisions; Project Constraints. +- `_techspec.md` → Build Order 9; Decisions. +- `docs/agents/spec-routing.md` → tooling authorization and changed-file + postflight. From 50d4dbf6fa5a02a95e35a43a7569621c64576f08 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 12:03:22 -0300 Subject: [PATCH 09/48] chore: align CodeRabbit reviews with Go project --- .coderabbit.yaml | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..664148a --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,91 @@ +language: "en" +early_access: true + +knowledge_base: + code_guidelines: + enabled: true + filePatterns: + - "AGENTS.md" + - "**/AGENTS.md" + - "CONTEXT.md" + - "docs/agents/**/*.md" + - "docs/adr/**/*.md" + - ".agents/skills/**/*.md" + +reviews: + request_changes_workflow: true + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + sequence_diagrams: true + profile: "assertive" + related_prs: false + tools: + golangci-lint: + enabled: true + path_filters: + # ----- Generated mirrors and local agent state ----- + - "!.claude/**" + - "!skills/**" + + # ----- IDE / editor / OS noise ----- + - "!.vscode/**" + - "!.idea/**" + - "!**/.DS_Store" + - "!**/Thumbs.db" + - "!**/*.swp" + - "!**/*.sw?" + + # ----- Build, release, log, and runtime artifacts ----- + - "!bin/**" + - "!dist/**" + - "!tmp/**" + - "!**/.cache/**" + - "!**/logs/**" + - "!**/*.log" + - "!**/.tmp/**" + - "!**/*.db" + - "!**/*.db-shm" + - "!**/*.db-wal" + - "!**/*.digest" + - "!**/*.sha256" + - "!**/*.png" + - "!**/*.jpeg" + - "!**/*.pdf" + + # ----- Secrets / env ----- + - "!**/.env" + - "!**/.env.local" + - "!**/.env.*.local" + - "!**/*.local" + - "!**/*.pem" + + # ----- Roundfix source, tests, contracts, docs, and configuration ----- + - "**/*.go" + - "**/*.md" + - "**/*.golden" + - "**/*.txt" + - "**/*.stdout" + - "**/*.stderr" + - "**/*.json" + - "**/*.jsonc" + - "**/*.yml" + - "**/*.yaml" + - "**/*.toml" + - ".github/**/*" + - ".agents/skills/**/*" + - ".coderabbit.yaml" + - ".roundfixrc.yml" + - ".gitignore" + - "AGENTS.md" + - "CHANGELOG.md" + - "CONTEXT.md" + - "LICENSE" + - "README.md" + - "go.mod" + - "go.sum" + - "skills-lock.json" + - "cog.toml" + - "Makefile" + - "**/Makefile" From a20f0cd46c9399710b53263fea78f2841be32165 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 12:03:30 -0300 Subject: [PATCH 10/48] docs: update baseline remediation evidence --- ...-vortex-baseline-capability-remediation.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/findings/2026-07-26-vortex-baseline-capability-remediation.md b/docs/findings/2026-07-26-vortex-baseline-capability-remediation.md index ac2623c..9a574f8 100644 --- a/docs/findings/2026-07-26-vortex-baseline-capability-remediation.md +++ b/docs/findings/2026-07-26-vortex-baseline-capability-remediation.md @@ -207,6 +207,40 @@ bun candidate=/Users/marcio/.bun/bin/bun kind=regular accepted=true This corroboration should raise the priority of finding 4 rather than open a separate work item. +## 7. A clean adoption warns about the thirteen files it just authored + +- Symptom / evidence: After a `verified` greenfield apply in + `/Users/marcio/dev/vortex`, an immediate re-plan with identical inputs correctly + reported `fileChanges: 0` — the adoption is idempotent — but emitted thirteen + warnings, one per generated guide: + + ```text + baseline.inventory.nested-carrier-conflict docs/agents/agent-instructions.md + baseline.inventory.nested-carrier-conflict docs/agents/skill-dispatch.md + baseline.inventory.nested-carrier-conflict docs/agents/setup-context.json + … 10 more, covering every file the apply wrote + ``` + + `docs/agents/specific-repository.md` is the only file under that directory that does + not warn. +- Root cause: The inventory walker in + [`repository.go:405`](../internal/baseline/repository.go) warns for every carrier with + `scope == "nested"` unless `isRecognizedRepositoryRuleCarrier` matches, and that + predicate recognizes only the repository-rules path. Setup-owned guides are nested by + construction, carry `setup-context-driven:begin` markers, and are listed in the + Setup Manifest and `managedEntries` of the very plan that wrote them — but the walker + does not consult either, so it cannot tell its own output from unreviewed + repository-authored instructions. +- Action / suggestion: Treat a nested carrier as setup-owned when the Setup Manifest + claims it and its managed markers verify, and warn only for nested carriers that are + unclaimed or whose markers fail verification. The current behaviour trains maintainers + to dismiss a thirteen-line warning block after every successful adoption, which is + exactly how a real conflict gets missed. + +This compounds finding 5: with no read-only capability re-check, the idempotent re-plan +is the natural way to confirm an adoption landed — and it is the path that emits the +spurious warnings. + ## Recommended implementation order 1. Fix `lookPathWithoutExecution` to resolve a bounded symlink chain @@ -258,3 +292,6 @@ separate work item. in the journal from a decline. - A `capability.contract.missing` divergence can produce a contract stub as a Change Plan entry gated by the normal Plan Digest confirmation. +- An idempotent re-plan immediately after a verified apply reports zero file changes and + zero warnings; a nested carrier warns only when the Setup Manifest does not claim it or + its managed markers fail verification. From 80ebf4a66fb24aa57df5b2438623281a63d7b5d1 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 12:10:24 -0300 Subject: [PATCH 11/48] fix: remove markdown files from coderabbit review scope to avoid unnecessary checks --- .coderabbit.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 664148a..25d1ba8 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -63,7 +63,6 @@ reviews: # ----- Roundfix source, tests, contracts, docs, and configuration ----- - "**/*.go" - - "**/*.md" - "**/*.golden" - "**/*.txt" - "**/*.stdout" From 8564e672f7e201698d3adb9ffdbc7940a9c82621 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 12:27:16 -0300 Subject: [PATCH 12/48] fix: update codex model to gpt-5.6-terra for improved performance --- .roundfixrc.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.roundfixrc.yml b/.roundfixrc.yml index dfee123..c8ec5f1 100644 --- a/.roundfixrc.yml +++ b/.roundfixrc.yml @@ -17,7 +17,7 @@ profiles: reasoning_effort: high fallbacks: - runtime: codex - model: gpt-5.5 + model: gpt-5.6-terra reasoning_effort: xhigh backend: preferred: @@ -26,7 +26,7 @@ profiles: reasoning_effort: high fallbacks: - runtime: codex - model: gpt-5.5 + model: gpt-5.6-terra reasoning_effort: xhigh frontend: preferred: @@ -44,7 +44,7 @@ profiles: reasoning_effort: high fallbacks: - runtime: codex - model: gpt-5.5 + model: gpt-5.6-terra reasoning_effort: xhigh review: preferred: @@ -53,7 +53,7 @@ profiles: reasoning_effort: high fallbacks: - runtime: codex - model: gpt-5.5 + model: gpt-5.6-terra reasoning_effort: xhigh review_source: From 393624bc3c48b4b00ba815fd4eed4cd7c2d50e2d Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 12:29:23 -0300 Subject: [PATCH 13/48] feat: diagnose Repository Skill Set readiness Roundfix-Spec: 0036-doctor-skill-readiness Roundfix-Task: task_01 --- .../0036-doctor-skill-readiness/task_01.md | 75 +++- internal/cli/cli.go | 9 +- internal/cli/cli_test.go | 133 +++++- internal/cli/doctor.go | 73 ++- internal/cli/health.go | 1 + skills/repository.go | 421 ++++++++++++++++++ skills/skills_test.go | 343 ++++++++++++++ 7 files changed, 1031 insertions(+), 24 deletions(-) create mode 100644 skills/repository.go diff --git a/docs/specs/0036-doctor-skill-readiness/task_01.md b/docs/specs/0036-doctor-skill-readiness/task_01.md index 0bfebed..207894b 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_01.md +++ b/docs/specs/0036-doctor-skill-readiness/task_01.md @@ -1,7 +1,7 @@ --- task: task_01 spec: 0036-doctor-skill-readiness -status: pending +status: completed type: backend complexity: high --- @@ -47,32 +47,32 @@ filesystem mutation. ## Subtasks -- [ ] Add repository readiness values and stable ownership classifications. -- [ ] Add exact owned-tree comparison against embedded skill files. -- [ ] Add strict required lock-entry validation and compatible external hashing. -- [ ] Inject the checker into Doctor and render deterministic success/failure. -- [ ] Add focused package tests for clean, missing, outdated, malformed, unsafe, +- [x] Add repository readiness values and stable ownership classifications. +- [x] Add exact owned-tree comparison against embedded skill files. +- [x] Add strict required lock-entry validation and compatible external hashing. +- [x] Inject the checker into Doctor and render deterministic success/failure. +- [x] Add focused package tests for clean, missing, outdated, malformed, unsafe, symlink, mixed-ownership, and ordering cases. -- [ ] Add exact-output Doctor tests for success, each ownership group, mixed +- [x] Add exact-output Doctor tests for success, each ownership group, mixed remediation, checker errors, exit behavior, and no mutation. -- [ ] Update Doctor command help for the appended Repository Skill Set check. +- [x] Update Doctor command help for the appended Repository Skill Set check. ## Acceptance Criteria -- [ ] A complete fixture matching the embedded owned bundle and required lock +- [x] A complete fixture matching the embedded owned bundle and required lock hashes reports `skills: ok` with the current derived 14/25/39 counts and exits zero when all other Doctor checks pass. -- [ ] Removing one owned or external skill reports that name under `missing`, +- [x] Removing one owned or external skill reports that name under `missing`, prints the ownership-specific command, and exits one. -- [ ] Changing, adding, or removing a versioned file reports the affected skill +- [x] Changing, adding, or removing a versioned file reports the affected skill under `outdated`; unrelated extra skills remain ignored. -- [ ] A compatibility fixture produces the exact same digest as the installed +- [x] A compatibility fixture produces the exact same digest as the installed skills CLI algorithm, including path normalization and exclusions. -- [ ] Missing/malformed lock data, invalid hashes, unsafe names, unreadable +- [x] Missing/malformed lock data, invalid hashes, unsafe names, unreadable artifacts, and symlinks fail deterministically without panic or traversal. -- [ ] Mixed failures produce one `skills:` line, sorted names, and both +- [x] Mixed failures produce one `skills:` line, sorted names, and both remediation commands exactly once in owned-then-external order. -- [ ] Doctor runs every existing check even when skill readiness fails and +- [x] Doctor runs every existing check even when skill readiness fails and changes no repository, user-config, Run, or skill path. ## Context @@ -116,4 +116,47 @@ filesystem mutation. ## Result -Pending. +Implemented one offline, read-only Repository Skill Set readiness check at the +`skills` ownership boundary and appended its deterministic `skills:` result +after Agent Selection Profile readiness in Doctor. + +### Verification + +- `rtk env GOCACHE=/tmp/roundfix-task01-go-cache go test ./skills -run 'Test(CheckRepository|SkillFolderHash)' -count=1` + — passed (`ok roundfix/skills`). +- `rtk env GOCACHE=/tmp/roundfix-task01-go-cache go test ./internal/cli -run 'TestRunDoctor' -count=1` + — passed (`ok roundfix/internal/cli`). +- `rtk env GOCACHE=/tmp/roundfix-task01-go-cache go test -race ./skills ./internal/cli -run 'Test(CheckRepository|RunDoctor)' -count=1` + — passed for both packages with the race detector. +- `rtk env GOCACHE=/tmp/roundfix-task01-go-cache go test ./skills ./internal/cli -count=1` + — passed for both complete package suites. +- `rtk git -c core.fsmonitor=false diff --check` — passed. + +The sandbox did not permit Go's default user cache, so focused checks used the +equivalent temporary `GOCACHE`; the Daemon remains responsible for running the +task's declared Verification commands verbatim. + +### Acceptance evidence + +- The complete embedded-owned plus required-external fixture returns derived + `14` owned, `25` external, and Doctor renders `39 required` with exit zero. +- Owned and external removals are classified under sorted `missing` names; + content changes, added files, and removed files are classified under + `outdated`; unrelated installed skills and lock entries are ignored. +- The pinned compatibility fixture, nested slash-normalized paths, lexical + ordering, and `.git`/`node_modules` exclusions pass through + `SkillFolderHash`. +- Missing and malformed locks, wrong versions, absent entries, invalid hashes, + unsafe names, unreadable artifact shapes, owned symlinks, and external + symlinks have deterministic error or readiness outcomes with no traversal. +- Exact Doctor output tests prove success, owned-only, external-only, mixed, + and checker-error lines; mixed remediation appears exactly once in + owned-then-external order and all pre-existing checks still execute. +- A before/after fixture snapshot proves `CheckRepository` performs no + mutation. Production code executes no commands, performs no network access, + and exposes no write path. + +### Follow-ups + +None. Documentation and Roundfix Skill synchronization remain assigned to +their separate Tasks in this Spec. diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 73940d6..faddae1 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -3778,9 +3778,12 @@ Options: roundfix doctor Diagnoses this machine's readiness for Runs. Checks Node.js, the minimum -supported acpx version, the effective adapter, and required Agent Selection Profiles. It also -checks codex runtime hygiene. The aggregate profiles: line exact-proves every -distinct tuple and reports the next action for failures. Doctor mutates nothing. +supported acpx version, the effective adapter, required Agent Selection +Profiles, the Repository Skill Set, and codex runtime hygiene. The aggregate +profiles: line exact-proves every distinct tuple. The skills: line compares +Roundfix-owned artifacts with the running binary and external artifacts with +skills-lock.json. Each failure reports its next action. Doctor is offline, +read-only, and mutates nothing. ` case "gc": return `Usage: diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 766c29c..1164d35 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -33,6 +33,7 @@ import ( roundtui "roundfix/internal/tui" "roundfix/internal/watch" runworktree "roundfix/internal/worktree" + "roundfix/skills" ) func init() { @@ -264,7 +265,7 @@ func TestRunCommandHelp(t *testing.T) { { name: "doctor", args: []string{"doctor", "--help"}, - contains: []string{"roundfix doctor", "codex runtime hygiene", "Doctor mutates nothing"}, + contains: []string{"roundfix doctor", "Repository Skill Set", "skills:", "codex runtime hygiene", "offline", "read-only", "mutates nothing"}, }, { name: "upgrade", @@ -470,7 +471,7 @@ func TestProfilesDocumentationContractMatchesPublicGuidance(t *testing.T) { snippets []string }{ {name: "setup", snippets: []string{"official Codex adapter", "profile readiness", "before writing"}}, - {name: "doctor", snippets: []string{"Agent Selection Profiles", "profiles:", "mutates nothing"}}, + {name: "doctor", snippets: []string{"Agent Selection Profiles", "profiles:", "Repository Skill Set", "skills:", "read-only", "mutates nothing"}}, {name: "profiles configure", snippets: []string{"exact Agent Selection", "before confirmation", "without writing"}}, {name: "profiles validate", snippets: []string{"exact", "Read-only", "disposable ACP Runtime session"}}, } { @@ -2291,6 +2292,7 @@ func TestRunDoctorProfileReadinessProvesEffectiveCategoriesAndReportsCounts(t *t "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + "adapter: ok (codex-acp)\n" + "profiles: ok (3 distinct tuples; 10 category references)\n" + + "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + "codex: ok (/home/roundfix/.local/bin/codex accepted)\n", }, { @@ -2305,6 +2307,7 @@ func TestRunDoctorProfileReadinessProvesEffectiveCategoriesAndReportsCounts(t *t "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + "adapter: ok (codex-acp)\n" + "profiles: ok (3 distinct tuples; 10 category references)\n" + + "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + "codex: failed (/tmp/codex is quarantined; next: " + codex.ReinstallNextAction + ")\n", }, { @@ -2319,6 +2322,7 @@ func TestRunDoctorProfileReadinessProvesEffectiveCategoriesAndReportsCounts(t *t "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + "adapter: ok (codex-acp)\n" + "profiles: ok (3 distinct tuples; 10 category references)\n" + + "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + "codex: skipped (not-applicable on linux)\n", }, } @@ -2548,6 +2552,119 @@ func TestRunDoctorContinuesChecksAfterProfileReadinessFailure(t *testing.T) { } } +func TestRunDoctorRepositorySkillReadiness(t *testing.T) { + ownedCommand := "roundfix skills install --target project" + externalCommand := "bunx skills experimental_install && bunx skills update -p -y" + tests := []struct { + name string + readiness skills.RepositoryReadiness + err error + wantCode int + wantLine string + }{ + { + name: "ready", + readiness: skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + }, + wantCode: exitOK, + wantLine: "skills: ok (39 required: 14 Roundfix-owned, 25 external)", + }, + { + name: "owned failure", + readiness: skills.RepositoryReadiness{ + OwnedRequired: 14, + MissingOwned: []string{"write-prd"}, + OutdatedOwned: []string{"roundfix"}, + }, + wantCode: exitRunFailed, + wantLine: "skills: failed (missing: write-prd; outdated: roundfix; next: " + + ownedCommand + ")", + }, + { + name: "external failure", + readiness: skills.RepositoryReadiness{ + ExternalRequired: 25, + MissingExternal: []string{"testing-boss"}, + OutdatedExternal: []string{"agentic-cli-design"}, + }, + wantCode: exitRunFailed, + wantLine: "skills: failed (missing: testing-boss; outdated: agentic-cli-design; next: " + + externalCommand + ")", + }, + { + name: "mixed failure is sorted with ordered remediation", + readiness: skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + MissingOwned: []string{"write-prd"}, + MissingExternal: []string{"agentic-cli-design"}, + OutdatedOwned: []string{"roundfix"}, + OutdatedExternal: []string{"testing-boss"}, + }, + wantCode: exitRunFailed, + wantLine: "skills: failed (missing: agentic-cli-design, write-prd; outdated: roundfix, testing-boss; next: " + + ownedCommand + "; " + externalCommand + ")", + }, + { + name: "external checker error", + err: &skills.RepositoryReadinessError{ + Ownership: skills.RepositoryOwnershipExternal, + Operation: "decode skills lock", + Path: "/repo/project/skills-lock.json", + Err: errors.New("invalid character"), + }, + wantCode: exitRunFailed, + wantLine: "skills: failed (decode skills lock \"/repo/project/skills-lock.json\": invalid character; next: " + + externalCommand + ")", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, + ) + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + GitRoot: "/repo/project", + }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { + return profileProofResult{} + }) + skillCalls := 0 + doctorDeps.checkSkills = func(root string) (skills.RepositoryReadiness, error) { + skillCalls++ + if root != "/repo/project" { + t.Fatalf("skill readiness root = %q, want /repo/project", root) + } + return test.readiness, test.err + } + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != test.wantCode { + t.Fatalf("exit code = %d, want %d; stderr=%q", code, test.wantCode, stderr.String()) + } + lines := strings.Split(strings.TrimSpace(stdout.String()), "\n") + if len(lines) != 6 || lines[4] != test.wantLine { + t.Fatalf("unexpected Doctor output lines:\n%q\nwant skills line %q at index 4", lines, test.wantLine) + } + if skillCalls != 1 || checker.nodeCalls != 1 || checker.acpxCalls != 1 || checker.adapterCalls != 1 || checker.codexCalls != 1 { + t.Fatalf("independent check calls skills=%d node=%d acpx=%d adapter=%d codex=%d", + skillCalls, checker.nodeCalls, checker.acpxCalls, checker.adapterCalls, checker.codexCalls) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } + }) + } +} + func TestRunDoctorRejectsArguments(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer @@ -2638,6 +2755,12 @@ func withDoctorFakeLoadedAndReadiness(t *testing.T, checker HealthChecker, loade return checker }, profileReadiness: readiness, + checkSkills: func(string) (skills.RepositoryReadiness, error) { + return skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + }, nil + }, } t.Cleanup(func() { doctorDeps = old @@ -2649,6 +2772,12 @@ func withDoctorLiveDeps(t *testing.T, checker HealthChecker) { old := doctorDeps doctorDeps = defaultDoctorDependencies() doctorDeps.healthChecker = func(roundconfig.Loaded) HealthChecker { return checker } + doctorDeps.checkSkills = func(string) (skills.RepositoryReadiness, error) { + return skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + }, nil + } t.Cleanup(func() { doctorDeps = old }) diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 2de9984..7094f00 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -2,15 +2,18 @@ package cli import ( "context" + "errors" "flag" "fmt" "io" "os" + "sort" "strings" "roundfix/internal/agent" "roundfix/internal/app" roundconfig "roundfix/internal/config" + "roundfix/skills" ) var doctorDeps = defaultDoctorDependencies() @@ -19,6 +22,7 @@ type doctorDependencies struct { loadConfig func(roundconfig.LoadOptions) (roundconfig.Loaded, error) healthChecker func(roundconfig.Loaded) HealthChecker profileReadiness func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult + checkSkills func(string) (skills.RepositoryReadiness, error) } func defaultDoctorDependencies() doctorDependencies { @@ -30,6 +34,7 @@ func defaultDoctorDependencies() doctorDependencies { profileReadiness: func(ctx context.Context, config roundconfig.Config, categories []roundconfig.WorkCategory, workDir string) profileProofResult { return proveProfileSelections(ctx, config, categories, workDir, newEngineCollaborators().runner) }, + checkSkills: skills.CheckRepository, } } @@ -61,16 +66,17 @@ func runDoctorCommand(ctx context.Context, args []string, stdout, stderr io.Writ profileReadiness := doctorDeps.profileReadiness(ctx, loaded.Config, roundconfig.RequiredWorkCategories(), workDir) profileResult := doctorProfileReadinessResult(profileReadiness) + skillReadiness, skillErr := doctorDeps.checkSkills(workDir) + skillResult := doctorSkillReadinessResult(skillReadiness, skillErr) runtime, runtimeErr := doctorAdapterRuntime(loaded.Config) - // Keep independent checks eager and ordered. Spec 0036 inserts Repository - // Skill Set readiness immediately after profileResult without proving - // Agent Selections again or changing the remaining check order. + // Keep independent checks eager and ordered. results := []CheckResult{ checker.Node(ctx), checker.ACPX(ctx), doctorAdapterCheck(ctx, checker, runtime, runtimeErr), profileResult, + skillResult, checker.Codex(ctx), } @@ -145,6 +151,67 @@ func doctorProfileReadinessResult(readiness profileProofResult) CheckResult { return result } +const ( + ownedSkillsNextAction = "roundfix skills install --target project" + externalSkillsNextAction = "bunx skills experimental_install && bunx skills update -p -y" +) + +func doctorSkillReadinessResult(readiness skills.RepositoryReadiness, checkErr error) CheckResult { + result := CheckResult{Name: HealthCheckSkills} + if checkErr == nil && readiness.Ready() { + result.Status = CheckStatusOK + result.Detail = fmt.Sprintf( + "%d required: %d Roundfix-owned, %d external", + readiness.OwnedRequired+readiness.ExternalRequired, + readiness.OwnedRequired, + readiness.ExternalRequired, + ) + return result + } + + result.Status = CheckStatusFailed + missing := append([]string{}, readiness.MissingOwned...) + missing = append(missing, readiness.MissingExternal...) + outdated := append([]string{}, readiness.OutdatedOwned...) + outdated = append(outdated, readiness.OutdatedExternal...) + sort.Strings(missing) + sort.Strings(outdated) + + var details []string + if len(missing) > 0 { + details = append(details, "missing: "+strings.Join(missing, ", ")) + } + if len(outdated) > 0 { + details = append(details, "outdated: "+strings.Join(outdated, ", ")) + } + if checkErr != nil { + details = append(details, checkErr.Error()) + result.Err = checkErr + } + result.Detail = strings.Join(details, "; ") + + ownedFailure := len(readiness.MissingOwned) > 0 || len(readiness.OutdatedOwned) > 0 + externalFailure := len(readiness.MissingExternal) > 0 || len(readiness.OutdatedExternal) > 0 + var readinessErr *skills.RepositoryReadinessError + if errors.As(checkErr, &readinessErr) { + switch readinessErr.Ownership { + case skills.RepositoryOwnershipOwned: + ownedFailure = true + case skills.RepositoryOwnershipExternal: + externalFailure = true + } + } + var next []string + if ownedFailure { + next = append(next, ownedSkillsNextAction) + } + if externalFailure { + next = append(next, externalSkillsNextAction) + } + result.NextAction = strings.Join(next, "; ") + return result +} + func profileProofReferenceCount(proofs []profileProofReport) int { count := 0 for _, proof := range proofs { diff --git a/internal/cli/health.go b/internal/cli/health.go index 0ed2ecd..076a38b 100644 --- a/internal/cli/health.go +++ b/internal/cli/health.go @@ -23,6 +23,7 @@ const ( HealthCheckAdapter = "adapter" HealthCheckAgent = "agent" HealthCheckProfiles = "profiles" + HealthCheckSkills = "skills" HealthCheckCodex = "codex" ) diff --git a/skills/repository.go b/skills/repository.go new file mode 100644 index 0000000..db6d536 --- /dev/null +++ b/skills/repository.go @@ -0,0 +1,421 @@ +package skills + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" +) + +type RepositoryOwnership string + +const ( + RepositoryOwnershipOwned RepositoryOwnership = "Roundfix-owned" + RepositoryOwnershipExternal RepositoryOwnership = "external" +) + +type RepositoryReadiness struct { + OwnedRequired int + ExternalRequired int + MissingOwned []string + OutdatedOwned []string + MissingExternal []string + OutdatedExternal []string +} + +func (readiness RepositoryReadiness) Ready() bool { + return len(readiness.MissingOwned) == 0 && + len(readiness.OutdatedOwned) == 0 && + len(readiness.MissingExternal) == 0 && + len(readiness.OutdatedExternal) == 0 +} + +type RepositoryReadinessError struct { + Ownership RepositoryOwnership + Operation string + Path string + Err error +} + +func (err *RepositoryReadinessError) Error() string { + if err.Path == "" { + return fmt.Sprintf("%s: %v", err.Operation, err.Err) + } + return fmt.Sprintf("%s %q: %v", err.Operation, err.Path, err.Err) +} + +func (err *RepositoryReadinessError) Unwrap() error { + return err.Err +} + +type localSkillsLock struct { + Version int `json:"version"` + Skills map[string]localLockSkill `json:"skills"` +} + +type localLockSkill struct { + ComputedHash string `json:"computedHash"` +} + +func CheckRepository(root string) (RepositoryReadiness, error) { + owned := Names() + external := Recommended() + if err := validateRequiredSkillNames(owned, external); err != nil { + return RepositoryReadiness{ + OwnedRequired: len(owned), + ExternalRequired: len(external), + }, err + } + files, err := Files() + if err != nil { + return RepositoryReadiness{ + OwnedRequired: len(owned), + ExternalRequired: len(external), + }, repositoryReadinessError(RepositoryOwnershipOwned, "read embedded skill bundle", "", err) + } + return checkRepository(root, owned, external, files) +} + +func checkRepository(root string, owned []string, external []string, files []File) (RepositoryReadiness, error) { + readiness := RepositoryReadiness{ + OwnedRequired: len(owned), + ExternalRequired: len(external), + } + if err := validateRequiredSkillNames(owned, external); err != nil { + return readiness, err + } + root = strings.TrimSpace(root) + if root == "" { + return readiness, repositoryReadinessError("", "validate repository root", "", errors.New("path is empty")) + } + + expected, err := expectedOwnedFiles(owned, files) + if err != nil { + return readiness, err + } + lockPath := filepath.Join(root, "skills-lock.json") + lock, err := readLocalSkillsLock(lockPath) + if err != nil { + return readiness, err + } + externalHashes, err := requiredExternalHashes(lockPath, lock, external) + if err != nil { + return readiness, err + } + + skillsRoot := filepath.Join(root, ".agents", "skills") + for _, name := range owned { + missing, outdated, err := compareOwnedSkill(filepath.Join(skillsRoot, name), expected[name]) + if err != nil { + return readiness, err + } + if missing { + readiness.MissingOwned = append(readiness.MissingOwned, name) + } else if outdated { + readiness.OutdatedOwned = append(readiness.OutdatedOwned, name) + } + } + for _, name := range external { + skillRoot := filepath.Join(skillsRoot, name) + _, err := os.Lstat(skillRoot) + if errors.Is(err, fs.ErrNotExist) { + readiness.MissingExternal = append(readiness.MissingExternal, name) + continue + } + if err != nil { + return readiness, repositoryReadinessError(RepositoryOwnershipExternal, "inspect external skill", skillRoot, err) + } + hash, err := SkillFolderHash(skillRoot) + if err != nil { + return readiness, repositoryReadinessError(RepositoryOwnershipExternal, "hash external skill", skillRoot, err) + } + if hash != externalHashes[name] { + readiness.OutdatedExternal = append(readiness.OutdatedExternal, name) + } + } + + sort.Strings(readiness.MissingOwned) + sort.Strings(readiness.OutdatedOwned) + sort.Strings(readiness.MissingExternal) + sort.Strings(readiness.OutdatedExternal) + return readiness, nil +} + +func validateRequiredSkillNames(owned []string, external []string) error { + seenOwned := make(map[string]struct{}, len(owned)) + for _, name := range owned { + if !safeSkillName(name) { + return repositoryReadinessError( + RepositoryOwnershipOwned, + "validate Roundfix-owned skill name", + name, + errors.New("unsafe skill name"), + ) + } + if _, exists := seenOwned[name]; exists { + return repositoryReadinessError( + RepositoryOwnershipOwned, + "validate Roundfix-owned skill name", + name, + errors.New("duplicate skill name"), + ) + } + seenOwned[name] = struct{}{} + } + + seenExternal := make(map[string]struct{}, len(external)) + for _, name := range external { + if !safeSkillName(name) { + return repositoryReadinessError( + RepositoryOwnershipExternal, + "validate external skill name", + name, + errors.New("unsafe skill name"), + ) + } + if _, exists := seenExternal[name]; exists { + return repositoryReadinessError( + RepositoryOwnershipExternal, + "validate external skill name", + name, + errors.New("duplicate skill name"), + ) + } + if _, overlaps := seenOwned[name]; overlaps { + return repositoryReadinessError( + RepositoryOwnershipExternal, + "validate external skill ownership", + name, + errors.New("skill overlaps the Roundfix-owned set"), + ) + } + seenExternal[name] = struct{}{} + } + return nil +} + +func safeSkillName(name string) bool { + return name != "" && + name != "." && + name != ".." && + filepath.Base(name) == name && + !strings.ContainsAny(name, `/\`) +} + +func expectedOwnedFiles(owned []string, files []File) (map[string]map[string][]byte, error) { + expected := make(map[string]map[string][]byte, len(owned)) + for _, name := range owned { + expected[name] = make(map[string][]byte) + } + for _, file := range files { + if !safeSkillName(file.Skill) { + return nil, repositoryReadinessError( + RepositoryOwnershipOwned, + "validate embedded skill artifact owner", + file.Skill, + errors.New("unsafe skill name"), + ) + } + skillFiles, required := expected[file.Skill] + if !required { + return nil, repositoryReadinessError( + RepositoryOwnershipOwned, + "validate embedded skill artifact owner", + file.Skill, + errors.New("skill is not in the required owned set"), + ) + } + prefix := file.Skill + "/" + if !strings.HasPrefix(file.Path, prefix) { + return nil, repositoryReadinessError( + RepositoryOwnershipOwned, + "validate embedded skill artifact path", + file.Path, + errors.New("path is outside its skill directory"), + ) + } + relative := strings.TrimPrefix(file.Path, prefix) + if !safeRelativeArtifactPath(relative) { + return nil, repositoryReadinessError( + RepositoryOwnershipOwned, + "validate embedded skill artifact path", + file.Path, + errors.New("unsafe artifact path"), + ) + } + if _, duplicate := skillFiles[relative]; duplicate { + return nil, repositoryReadinessError( + RepositoryOwnershipOwned, + "validate embedded skill artifact path", + file.Path, + errors.New("duplicate artifact path"), + ) + } + skillFiles[relative] = file.Data + } + for _, name := range owned { + if len(expected[name]) == 0 { + return nil, repositoryReadinessError( + RepositoryOwnershipOwned, + "validate embedded skill artifacts", + name, + errors.New("required skill has no embedded files"), + ) + } + } + return expected, nil +} + +func safeRelativeArtifactPath(path string) bool { + if path == "" || strings.Contains(path, `\`) || filepath.IsAbs(path) { + return false + } + cleaned := filepath.ToSlash(filepath.Clean(filepath.FromSlash(path))) + return cleaned == path && cleaned != "." && cleaned != ".." && !strings.HasPrefix(cleaned, "../") +} + +func readLocalSkillsLock(path string) (localSkillsLock, error) { + data, err := os.ReadFile(path) + if err != nil { + return localSkillsLock{}, repositoryReadinessError(RepositoryOwnershipExternal, "read skills lock", path, err) + } + var lock localSkillsLock + decoder := json.NewDecoder(bytes.NewReader(data)) + if err := decoder.Decode(&lock); err != nil { + return localSkillsLock{}, repositoryReadinessError(RepositoryOwnershipExternal, "decode skills lock", path, err) + } + var trailing json.RawMessage + if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { + if err == nil { + err = errors.New("unexpected trailing JSON value") + } + return localSkillsLock{}, repositoryReadinessError(RepositoryOwnershipExternal, "decode skills lock", path, err) + } + if lock.Version != 1 { + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "validate skills lock version", + path, + fmt.Errorf("got %d, want 1", lock.Version), + ) + } + return lock, nil +} + +func requiredExternalHashes(path string, lock localSkillsLock, external []string) (map[string]string, error) { + hashes := make(map[string]string, len(external)) + for _, name := range external { + entry, exists := lock.Skills[name] + if !exists { + return nil, repositoryReadinessError( + RepositoryOwnershipExternal, + "validate required skills lock entry", + path, + fmt.Errorf("external skill %q is missing", name), + ) + } + if !lowerHexSHA256(entry.ComputedHash) { + return nil, repositoryReadinessError( + RepositoryOwnershipExternal, + "validate required skills lock hash", + path, + fmt.Errorf("external skill %q computedHash must be 64 lowercase hexadecimal characters", name), + ) + } + hashes[name] = entry.ComputedHash + } + return hashes, nil +} + +func lowerHexSHA256(value string) bool { + if len(value) != sha256HexLength { + return false + } + for _, char := range value { + if (char < '0' || char > '9') && (char < 'a' || char > 'f') { + return false + } + } + return true +} + +const sha256HexLength = 64 + +func compareOwnedSkill(root string, expected map[string][]byte) (missing bool, outdated bool, err error) { + info, lstatErr := os.Lstat(root) + if errors.Is(lstatErr, fs.ErrNotExist) { + return true, false, nil + } + if lstatErr != nil { + return false, false, repositoryReadinessError(RepositoryOwnershipOwned, "inspect Roundfix-owned skill", root, lstatErr) + } + if info.Mode()&fs.ModeSymlink != 0 || !info.IsDir() { + return false, true, nil + } + + found := make(map[string]struct{}, len(expected)) + tree := os.DirFS(root) + walkErr := fs.WalkDir(tree, ".", func(path string, entry fs.DirEntry, walkErr error) error { + fullPath := filepath.Join(root, filepath.FromSlash(path)) + if walkErr != nil { + return repositoryReadinessError(RepositoryOwnershipOwned, "walk Roundfix-owned skill artifact", fullPath, walkErr) + } + if path == "." { + return nil + } + if entry.Type()&fs.ModeSymlink != 0 { + outdated = true + return nil + } + if entry.IsDir() { + return nil + } + info, infoErr := entry.Info() + if infoErr != nil { + return repositoryReadinessError(RepositoryOwnershipOwned, "inspect Roundfix-owned skill artifact", fullPath, infoErr) + } + if !info.Mode().IsRegular() { + outdated = true + return nil + } + relative := filepath.ToSlash(path) + want, required := expected[relative] + if !required { + outdated = true + return nil + } + data, readErr := fs.ReadFile(tree, path) + if readErr != nil { + return repositoryReadinessError(RepositoryOwnershipOwned, "read Roundfix-owned skill artifact", fullPath, readErr) + } + found[relative] = struct{}{} + if !bytes.Equal(data, want) { + outdated = true + } + return nil + }) + if walkErr != nil { + return false, false, walkErr + } + for relative := range expected { + if _, exists := found[relative]; !exists { + outdated = true + } + } + return false, outdated, nil +} + +func repositoryReadinessError(ownership RepositoryOwnership, operation string, path string, err error) error { + return &RepositoryReadinessError{ + Ownership: ownership, + Operation: operation, + Path: path, + Err: err, + } +} diff --git a/skills/skills_test.go b/skills/skills_test.go index 4f714ce..744a72e 100644 --- a/skills/skills_test.go +++ b/skills/skills_test.go @@ -160,6 +160,349 @@ func TestSkillFolderHashWrapsMissingRootError(t *testing.T) { } } +func TestCheckRepositoryReportsReadyRequiredSetWithoutMutation(t *testing.T) { + root := writeReadyRepositoryFixture(t) + before := snapshotRepositoryFixture(t, root) + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check ready repository: %v", err) + } + if !got.Ready() { + t.Fatalf("expected repository to be ready, got %#v", got) + } + if got.OwnedRequired != 14 || got.ExternalRequired != 25 { + t.Fatalf("required counts = owned %d external %d, want 14 and 25", got.OwnedRequired, got.ExternalRequired) + } + if after := snapshotRepositoryFixture(t, root); !reflect.DeepEqual(after, before) { + t.Fatalf("CheckRepository mutated fixture:\nbefore=%#v\nafter=%#v", before, after) + } +} + +func TestCheckRepositoryClassifiesMissingAndOutdatedSkills(t *testing.T) { + tests := []struct { + name string + mutate func(*testing.T, string) + want RepositoryReadiness + }{ + { + name: "missing owned", + mutate: func(t *testing.T, root string) { + t.Helper() + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "archive-spec")); err != nil { + t.Fatalf("remove owned skill: %v", err) + } + }, + want: RepositoryReadiness{MissingOwned: []string{"archive-spec"}}, + }, + { + name: "missing external", + mutate: func(t *testing.T, root string) { + t.Helper() + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "agentic-cli-design")); err != nil { + t.Fatalf("remove external skill: %v", err) + } + }, + want: RepositoryReadiness{MissingExternal: []string{"agentic-cli-design"}}, + }, + { + name: "changed owned file", + mutate: func(t *testing.T, root string) { + t.Helper() + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "roundfix"), "SKILL.md", "changed\n") + }, + want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, + }, + { + name: "added owned file", + mutate: func(t *testing.T, root string) { + t.Helper() + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "roundfix"), "unexpected.txt", "extra\n") + }, + want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, + }, + { + name: "removed owned file", + mutate: func(t *testing.T, root string) { + t.Helper() + path := filepath.Join(root, ".agents", "skills", "roundfix", "agents", "openai.yaml") + if err := os.Remove(path); err != nil { + t.Fatalf("remove owned artifact: %v", err) + } + }, + want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, + }, + { + name: "changed external file", + mutate: func(t *testing.T, root string) { + t.Helper() + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "agentic-cli-design"), "SKILL.md", "changed\n") + }, + want: RepositoryReadiness{OutdatedExternal: []string{"agentic-cli-design"}}, + }, + { + name: "added external file", + mutate: func(t *testing.T, root string) { + t.Helper() + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "agentic-cli-design"), "unexpected.txt", "extra\n") + }, + want: RepositoryReadiness{OutdatedExternal: []string{"agentic-cli-design"}}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := writeReadyRepositoryFixture(t) + test.mutate(t, root) + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check repository: %v", err) + } + got.OwnedRequired = 0 + got.ExternalRequired = 0 + if !reflect.DeepEqual(got, test.want) { + t.Fatalf("readiness = %#v, want %#v", got, test.want) + } + }) + } +} + +func TestCheckRepositoryIgnoresUnrelatedSkillsAndLockEntries(t *testing.T) { + root := writeReadyRepositoryFixture(t) + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "unrelated"), "SKILL.md", "unrelated\n") + lock := readRepositoryLockFixture(t, root) + lock.Skills["../ignored-unsafe-entry"] = repositoryLockSkillFixture{ComputedHash: "not-a-hash"} + writeRepositoryLockFixture(t, root, lock) + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check repository with unrelated entries: %v", err) + } + if !got.Ready() { + t.Fatalf("unrelated entries changed readiness: %#v", got) + } +} + +func TestCheckRepositoryRejectsMalformedLockAndUnsafeRequiredNames(t *testing.T) { + tests := []struct { + name string + write func(*testing.T, string) + }{ + { + name: "missing lock", + write: func(t *testing.T, root string) { + if err := os.Remove(filepath.Join(root, "skills-lock.json")); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "malformed lock", + write: func(t *testing.T, root string) { + if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), []byte("{"), 0o644); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "wrong version", + write: func(t *testing.T, root string) { + lock := readRepositoryLockFixture(t, root) + lock.Version = 2 + writeRepositoryLockFixture(t, root, lock) + }, + }, + { + name: "missing required entry", + write: func(t *testing.T, root string) { + lock := readRepositoryLockFixture(t, root) + delete(lock.Skills, Recommended()[0]) + writeRepositoryLockFixture(t, root, lock) + }, + }, + { + name: "invalid required hash", + write: func(t *testing.T, root string) { + lock := readRepositoryLockFixture(t, root) + lock.Skills[Recommended()[0]] = repositoryLockSkillFixture{ComputedHash: strings.Repeat("A", 64)} + writeRepositoryLockFixture(t, root, lock) + }, + }, + { + name: "unreadable lock artifact shape", + write: func(t *testing.T, root string) { + path := filepath.Join(root, "skills-lock.json") + if err := os.Remove(path); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(path, 0o755); err != nil { + t.Fatal(err) + } + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := writeReadyRepositoryFixture(t) + test.write(t, root) + + _, err := CheckRepository(root) + if err == nil { + t.Fatal("expected repository lock error") + } + var repositoryErr *RepositoryReadinessError + if !errors.As(err, &repositoryErr) { + t.Fatalf("expected RepositoryReadinessError, got %T: %v", err, err) + } + if repositoryErr.Ownership != RepositoryOwnershipExternal { + t.Fatalf("lock error ownership = %q, want external", repositoryErr.Ownership) + } + if !strings.Contains(err.Error(), filepath.Join(root, "skills-lock.json")) { + t.Fatalf("lock error does not name path: %v", err) + } + }) + } + + root := t.TempDir() + outside := filepath.Join(filepath.Dir(root), "outside") + writeSkillHashTestFile(t, outside, "marker", "must not be read\n") + _, err := checkRepository(root, []string{"../outside"}, nil, nil) + if err == nil || !strings.Contains(err.Error(), "unsafe") { + t.Fatalf("expected unsafe required name error, got %v", err) + } +} + +func TestCheckRepositoryHandlesSymlinksAndStableOrdering(t *testing.T) { + root := writeReadyRepositoryFixture(t) + ownedPath := filepath.Join(root, ".agents", "skills", "roundfix", "SKILL.md") + if err := os.Remove(ownedPath); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join(root, "skills-lock.json"), ownedPath); err != nil { + t.Skipf("create owned artifact symlink: %v", err) + } + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "agentic-cli-design")); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "archive-spec")); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "autoresearch")); err != nil { + t.Fatal(err) + } + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check repository with owned symlink: %v", err) + } + if !reflect.DeepEqual(got.MissingOwned, []string{"archive-spec"}) || + !reflect.DeepEqual(got.MissingExternal, []string{"agentic-cli-design", "autoresearch"}) || + !reflect.DeepEqual(got.OutdatedOwned, []string{"roundfix"}) { + t.Fatalf("unstable classifications: %#v", got) + } + + externalRoot := writeReadyRepositoryFixture(t) + externalPath := filepath.Join(externalRoot, ".agents", "skills", "agentic-cli-design", "linked") + if err := os.Symlink(filepath.Join(externalRoot, "skills-lock.json"), externalPath); err != nil { + t.Skipf("create external symlink: %v", err) + } + _, err = CheckRepository(externalRoot) + if err == nil || !strings.Contains(err.Error(), externalPath) { + t.Fatalf("expected external symlink error naming %q, got %v", externalPath, err) + } +} + +type repositoryLockFixture struct { + Version int `json:"version"` + Skills map[string]repositoryLockSkillFixture `json:"skills"` +} + +type repositoryLockSkillFixture struct { + ComputedHash string `json:"computedHash"` +} + +func writeReadyRepositoryFixture(t *testing.T) string { + t.Helper() + root := t.TempDir() + skillsRoot := filepath.Join(root, ".agents", "skills") + files, err := Files() + if err != nil { + t.Fatalf("read embedded skills: %v", err) + } + for _, file := range files { + writeSkillHashTestFile(t, skillsRoot, file.Path, string(file.Data)) + } + + lock := repositoryLockFixture{ + Version: 1, + Skills: make(map[string]repositoryLockSkillFixture, len(Recommended())), + } + for _, name := range Recommended() { + skillRoot := filepath.Join(skillsRoot, name) + writeSkillHashTestFile(t, skillRoot, "SKILL.md", name+"\n") + hash, err := SkillFolderHash(skillRoot) + if err != nil { + t.Fatalf("hash external fixture %q: %v", name, err) + } + lock.Skills[name] = repositoryLockSkillFixture{ComputedHash: hash} + } + writeRepositoryLockFixture(t, root, lock) + return root +} + +func readRepositoryLockFixture(t *testing.T, root string) repositoryLockFixture { + t.Helper() + data, err := os.ReadFile(filepath.Join(root, "skills-lock.json")) + if err != nil { + t.Fatalf("read repository lock fixture: %v", err) + } + var lock repositoryLockFixture + if err := json.Unmarshal(data, &lock); err != nil { + t.Fatalf("decode repository lock fixture: %v", err) + } + return lock +} + +func writeRepositoryLockFixture(t *testing.T, root string, lock repositoryLockFixture) { + t.Helper() + data, err := json.MarshalIndent(lock, "", " ") + if err != nil { + t.Fatalf("encode repository lock fixture: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), append(data, '\n'), 0o644); err != nil { + t.Fatalf("write repository lock fixture: %v", err) + } +} + +func snapshotRepositoryFixture(t *testing.T, root string) map[string]string { + t.Helper() + snapshot := make(map[string]string) + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + snapshot[filepath.ToSlash(relative)] = string(data) + return nil + }) + if err != nil { + t.Fatalf("snapshot repository fixture: %v", err) + } + return snapshot +} + func writeSkillHashTestFile(t *testing.T, root string, relative string, content string) { t.Helper() path := filepath.Join(root, filepath.FromSlash(relative)) From 672eb5ca4074f5cfa4711316259f6ce0498de528 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 13:31:16 -0300 Subject: [PATCH 14/48] chore: refresh repository skill set --- .agents/skills/golang-testing/SKILL.md | 46 +++++++++++++++++++++++++- skills-lock.json | 16 ++++----- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/.agents/skills/golang-testing/SKILL.md b/.agents/skills/golang-testing/SKILL.md index b31d3be..0d1ed13 100644 --- a/.agents/skills/golang-testing/SKILL.md +++ b/.agents/skills/golang-testing/SKILL.md @@ -6,7 +6,7 @@ license: MIT compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang. metadata: author: samber - version: "1.2.3" + version: "1.2.5" openclaw: emoji: "🧪" homepage: https://github.com/samber/cc-skills-golang @@ -57,6 +57,8 @@ This skill guides the creation of production-ready tests for Go applications. Fo 9. Keep unit tests fast (< 1ms), use build tags for integration tests 10. Run tests with race detection in CI 11. Include examples as executable documentation +12. Test files MUST be named after the source file under test, not after the function or method being tested +13. Test functions SHOULD appear in the same order as the functions/methods they test in the source file ## Test Structure and Organization @@ -70,6 +72,20 @@ package mypackage package mypackage_test ``` +Name the test file after the source file it tests, not after the function or method under test. Go's convention is one test file per source file (`foo.go` -> `foo_test.go`), because tools (`go test`, coverage reports, IDE "jump to test" navigation, `gotests`) and reviewers all resolve tests by source file, not by symbol. A source file usually declares several functions/methods; splitting its tests by symbol name scatters them across many files and breaks that file-to-file mapping. + +``` +// ✓ Good — one test file per source file +helloworld.go -> helloworld_test.go // contains TestHelloWorld, TestAbcd, TestXyz, ... + +// ✗ Bad — test file named after the function/method instead of the source file +helloworld.go -> abcd_test.go // wrong: should be helloworld_test.go +``` + +Exception: very large source files MAY be split into multiple `_test.go` files by concern (e.g. `foo_test.go` + `foo_edgecases_test.go`), but each split file's name MUST still be derived from the source file name, never from an individual function name. Prefer keeping a single `_test.go` file per source file even when it grows large — splitting adds navigation overhead and is rarely worth it; reach for the exception only when a single file becomes genuinely unwieldy to browse or review. + +Within a test file, order test functions to match the order their tested functions/methods appear in the source file. A reader (human or agent) scrolling `foo.go` alongside `foo_test.go` can then find the matching test by position instead of searching; drift between the two orderings compounds every time either file grows. + ### Naming Conventions ```go @@ -124,6 +140,34 @@ func TestCalculatePrice(t *testing.T) { } ``` +## Common Pitfall: Assert Scope Leaking into Subtests + +Never create a testify `assert`/`require` instance in the parent test function and reuse it inside `t.Run` closures. `assert.New(t)` captures the exact `*testing.T` it was built with, so if that `t` belongs to the parent, every failure raised inside the subtest gets attributed to the *parent* test in `go test` output — the failing subtest itself still reports `--- PASS`, silently hiding which case broke. This happens whether or not the subtest calls `t.Parallel()`. + +```go +// WRONG -- `is` is bound to the parent's t +func TestCalculatePrice(t *testing.T) { + is := assert.New(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice)) // misattributed on failure + }) + } +} + +// RIGHT -- each subtest builds its own instance from its own t +func TestCalculatePrice(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + is := assert.New(t) + is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice)) + }) + } +} +``` + +Verify with a deliberately-broken case: if `go test -v -run TestName` shows `--- FAIL: TestName` but every `--- PASS: TestName/subtest_name` line still says PASS, the assert scope is leaking. + ## Unit Tests Unit tests should be fast (< 1ms), isolated (no external dependencies), and deterministic. diff --git a/skills-lock.json b/skills-lock.json index 77e0ee7..49c66bc 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -49,6 +49,13 @@ "skillPath": "skills/03-engineering-design/domain-modeling/SKILL.md", "computedHash": "363cb0f53b0b431e7c00086ad1f823500b7e1b70b5616ee969c979f0934e9e6e" }, + "exa-web-search": { + "source": "marcioaltoe/skills", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", + "sourceType": "github", + "skillPath": "skills/01-discovery/exa-web-search/SKILL.md", + "computedHash": "d8b53369f54c57dea66e64d83d6f3a27bfeee364d88ff3845f6b7312f6b3c2ae" + }, "github-pr-workflow": { "source": "marcioaltoe/skills", "ref": "main", @@ -96,7 +103,7 @@ "ref": "main", "sourceType": "github", "skillPath": "skills/05-implementation-loop/golang-testing/SKILL.md", - "computedHash": "150dfb5fe4c56b8d3c141c4b3e70326743b591c1dedf9a32cfba64619bab8784" + "computedHash": "5149794b7adbda8f4509fad7ec13b63cbae0c4aacad915b87b324cda2be3f5ac" }, "grill-with-docs": { "source": "marcioaltoe/skills", @@ -167,13 +174,6 @@ "sourceType": "github", "skillPath": "skills/03-engineering-design/tui-design/SKILL.md", "computedHash": "92b969f3473842ee731e0c7a5c571e3b89e5dac7da81be5e635e1715b0084e6e" - }, - "exa-web-search": { - "source": "marcioaltoe/skills", - "ref": "14fdf46befa9a07203fde20b69b885dab4961844", - "sourceType": "github", - "skillPath": "skills/01-discovery/exa-web-search/SKILL.md", - "computedHash": "16a95a38b232f4746ff8785677f48615b5464fa36786a3fc2d23e7e2d1129d72" } } } From fd786ed4fb39ccc612787c9fce0688b623d32417 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 13:31:16 -0300 Subject: [PATCH 15/48] chore: refresh repository skill set --- .agents/skills/golang-testing/SKILL.md | 46 +++++++++++++++++++++++++- skills-lock.json | 16 ++++----- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/.agents/skills/golang-testing/SKILL.md b/.agents/skills/golang-testing/SKILL.md index b31d3be..0d1ed13 100644 --- a/.agents/skills/golang-testing/SKILL.md +++ b/.agents/skills/golang-testing/SKILL.md @@ -6,7 +6,7 @@ license: MIT compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang. metadata: author: samber - version: "1.2.3" + version: "1.2.5" openclaw: emoji: "🧪" homepage: https://github.com/samber/cc-skills-golang @@ -57,6 +57,8 @@ This skill guides the creation of production-ready tests for Go applications. Fo 9. Keep unit tests fast (< 1ms), use build tags for integration tests 10. Run tests with race detection in CI 11. Include examples as executable documentation +12. Test files MUST be named after the source file under test, not after the function or method being tested +13. Test functions SHOULD appear in the same order as the functions/methods they test in the source file ## Test Structure and Organization @@ -70,6 +72,20 @@ package mypackage package mypackage_test ``` +Name the test file after the source file it tests, not after the function or method under test. Go's convention is one test file per source file (`foo.go` -> `foo_test.go`), because tools (`go test`, coverage reports, IDE "jump to test" navigation, `gotests`) and reviewers all resolve tests by source file, not by symbol. A source file usually declares several functions/methods; splitting its tests by symbol name scatters them across many files and breaks that file-to-file mapping. + +``` +// ✓ Good — one test file per source file +helloworld.go -> helloworld_test.go // contains TestHelloWorld, TestAbcd, TestXyz, ... + +// ✗ Bad — test file named after the function/method instead of the source file +helloworld.go -> abcd_test.go // wrong: should be helloworld_test.go +``` + +Exception: very large source files MAY be split into multiple `_test.go` files by concern (e.g. `foo_test.go` + `foo_edgecases_test.go`), but each split file's name MUST still be derived from the source file name, never from an individual function name. Prefer keeping a single `_test.go` file per source file even when it grows large — splitting adds navigation overhead and is rarely worth it; reach for the exception only when a single file becomes genuinely unwieldy to browse or review. + +Within a test file, order test functions to match the order their tested functions/methods appear in the source file. A reader (human or agent) scrolling `foo.go` alongside `foo_test.go` can then find the matching test by position instead of searching; drift between the two orderings compounds every time either file grows. + ### Naming Conventions ```go @@ -124,6 +140,34 @@ func TestCalculatePrice(t *testing.T) { } ``` +## Common Pitfall: Assert Scope Leaking into Subtests + +Never create a testify `assert`/`require` instance in the parent test function and reuse it inside `t.Run` closures. `assert.New(t)` captures the exact `*testing.T` it was built with, so if that `t` belongs to the parent, every failure raised inside the subtest gets attributed to the *parent* test in `go test` output — the failing subtest itself still reports `--- PASS`, silently hiding which case broke. This happens whether or not the subtest calls `t.Parallel()`. + +```go +// WRONG -- `is` is bound to the parent's t +func TestCalculatePrice(t *testing.T) { + is := assert.New(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice)) // misattributed on failure + }) + } +} + +// RIGHT -- each subtest builds its own instance from its own t +func TestCalculatePrice(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + is := assert.New(t) + is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice)) + }) + } +} +``` + +Verify with a deliberately-broken case: if `go test -v -run TestName` shows `--- FAIL: TestName` but every `--- PASS: TestName/subtest_name` line still says PASS, the assert scope is leaking. + ## Unit Tests Unit tests should be fast (< 1ms), isolated (no external dependencies), and deterministic. diff --git a/skills-lock.json b/skills-lock.json index 77e0ee7..49c66bc 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -49,6 +49,13 @@ "skillPath": "skills/03-engineering-design/domain-modeling/SKILL.md", "computedHash": "363cb0f53b0b431e7c00086ad1f823500b7e1b70b5616ee969c979f0934e9e6e" }, + "exa-web-search": { + "source": "marcioaltoe/skills", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", + "sourceType": "github", + "skillPath": "skills/01-discovery/exa-web-search/SKILL.md", + "computedHash": "d8b53369f54c57dea66e64d83d6f3a27bfeee364d88ff3845f6b7312f6b3c2ae" + }, "github-pr-workflow": { "source": "marcioaltoe/skills", "ref": "main", @@ -96,7 +103,7 @@ "ref": "main", "sourceType": "github", "skillPath": "skills/05-implementation-loop/golang-testing/SKILL.md", - "computedHash": "150dfb5fe4c56b8d3c141c4b3e70326743b591c1dedf9a32cfba64619bab8784" + "computedHash": "5149794b7adbda8f4509fad7ec13b63cbae0c4aacad915b87b324cda2be3f5ac" }, "grill-with-docs": { "source": "marcioaltoe/skills", @@ -167,13 +174,6 @@ "sourceType": "github", "skillPath": "skills/03-engineering-design/tui-design/SKILL.md", "computedHash": "92b969f3473842ee731e0c7a5c571e3b89e5dac7da81be5e635e1715b0084e6e" - }, - "exa-web-search": { - "source": "marcioaltoe/skills", - "ref": "14fdf46befa9a07203fde20b69b885dab4961844", - "sourceType": "github", - "skillPath": "skills/01-discovery/exa-web-search/SKILL.md", - "computedHash": "16a95a38b232f4746ff8785677f48615b5464fa36786a3fc2d23e7e2d1129d72" } } } From ccfbd15c5717a7dcc9bfbf3092367017aee07007 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 13:35:06 -0300 Subject: [PATCH 16/48] docs: synchronize Doctor skill-readiness guidance Roundfix-Spec: 0036-doctor-skill-readiness Roundfix-Task: task_02 --- CONTEXT.md | 4 +- README.md | 32 ++++++++-- .../0036-doctor-skill-readiness/task_02.md | 60 +++++++++++++++---- docs/user-guide/commands.md | 27 ++++++++- docs/user-guide/usage.md | 11 +++- 5 files changed, 113 insertions(+), 21 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index cabe449..f2aadf6 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -367,7 +367,7 @@ The support command that produces a Release Plan without editing release files, _Avoid_: Release Command, publish command, cut-release command **Doctor Command**: -The support command that diagnoses a repository and machine's readiness for Roundfix Runs — Repository Skill Set, minimum-supported acpx, Adapter Readiness, Agent Selection Profile Readiness, and codex runtime hygiene — reporting the detected acpx version against the minimum, each check with a next action, and mutating nothing. Distinct from the Setup Command, which prepares the machine. +The support command that diagnoses a repository and machine's readiness for Roundfix Runs — minimum-supported acpx, Adapter Readiness, Agent Selection Profile Readiness, Repository Skill Set, and codex runtime hygiene — reporting each check with a next action and mutating nothing. It evaluates Repository Skill Set readiness after, and independently from, Agent Selection Profile Readiness; unlike the Doctor Command, the Setup Command prepares the machine. _Avoid_: Health check run, setup run, environment wizard **Archive Command**: @@ -503,7 +503,7 @@ A shipped agent skill that teaches an external Agent how to start Roundfix or ho _Avoid_: Runtime, Review Source, plugin **Repository Skill Set**: -The complete set of Roundfix-owned and externally managed Agent Skills a repository declares for its workflow, with every installed artifact matching its authoritative local source. +The complete set of Roundfix-owned and externally managed Agent Skills required by a repository workflow. Every required installed artifact matches its authoritative local source; unrelated extras do not join the set. _Avoid_: Installed skills, plugin set, global skill set **Interactive Input**: diff --git a/README.md b/README.md index d74f055..66b6595 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Then make the machine Run-ready and check it: ```bash roundfix setup # proves adapters and generated profiles before writes -roundfix doctor # read-only adapter and profile readiness; mutates nothing +roundfix doctor # read-only profile and Repository Skill Set readiness ``` ## Requirements @@ -43,10 +43,32 @@ roundfix doctor # read-only adapter and profile readiness; mutates nothing - Building from source additionally needs Go 1.26+ and `make`. `roundfix doctor` diagnoses all of it — including official Codex adapter -identity, exact Agent Selection Profile proof, and macOS codex hygiene — with -one line per check and a `next:` action on failures. Use `roundfix setup` to -provision the minimum supported acpx or official adapter, or migrate a stale -legacy override after authorization. +identity, exact Agent Selection Profile proof, Repository Skill Set readiness, +and macOS codex hygiene — with one line per check and a `next:` action on +failures. The independent Repository Skill Set result follows `profiles:`: + +```text +profiles: ok (3 distinct tuples; 10 category references) +skills: ok (39 required: 14 Roundfix-owned, 25 external) +``` + +The running binary's embedded bundle is authoritative for Roundfix-owned +skills, including the Roundfix Skill. Each required external skill must match +its `computedHash` in the repository's `skills-lock.json`. Missing, outdated, +or invalid required skill state prints `skills: failed (...)`, still runs the +other Doctor checks, and makes Doctor exit `1`. + +Doctor checks these authorities locally, without network access or writes. It +ignores unrelated extra skill directories and lock entries, never deletes or +updates skills automatically, and only prints the applicable command: + +```bash +roundfix skills install --target project +bunx skills experimental_install && bunx skills update -p -y +``` + +Use `roundfix setup` to provision the minimum supported acpx or official +adapter, or migrate a stale legacy override after authorization. ## Quickstart diff --git a/docs/specs/0036-doctor-skill-readiness/task_02.md b/docs/specs/0036-doctor-skill-readiness/task_02.md index 73a62f4..7697c8c 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_02.md +++ b/docs/specs/0036-doctor-skill-readiness/task_02.md @@ -1,7 +1,7 @@ --- task: task_02 spec: 0036-doctor-skill-readiness -status: pending +status: completed type: docs complexity: medium --- @@ -35,21 +35,21 @@ Skill update. ## Subtasks -- [ ] Finalize Repository Skill Set glossary and Doctor definition. -- [ ] Update README and Doctor command/user guidance. -- [ ] Verify required external skill hashes without authorial modification. +- [x] Finalize Repository Skill Set glossary and Doctor definition. +- [x] Update README and Doctor command/user guidance. +- [x] Verify required external skill hashes without authorial modification. ## Acceptance Criteria -- [ ] A reader can identify all required skill authorities, Doctor outcomes, +- [x] A reader can identify all required skill authorities, Doctor outcomes, update commands, and the no-network/no-mutation guarantee from supported user documentation. -- [ ] All required external installed trees hash to their current +- [x] All required external installed trees hash to their current `skills-lock.json` entries. -- [ ] No unexpected external authorial edit is introduced, and unrelated extra +- [x] No unexpected external authorial edit is introduced, and unrelated extra skills are neither flagged nor removed. -- [ ] No protected tooling path changes in this Task. -- [ ] `CONTEXT.md`, docs, and CLI help use Repository Skill Set, Doctor +- [x] No protected tooling path changes in this Task. +- [x] `CONTEXT.md`, docs, and CLI help use Repository Skill Set, Doctor Command, and Roundfix Skill consistently. ## Context @@ -83,4 +83,44 @@ Skill update. ## Result -Pending. +Completed after the explicitly authorized external skill refresh repaired the +repository prerequisite outside this Task's authorial slice. This Task's +changes remain limited to the canonical glossary and user documentation. + +### Changes + +- Defined the Repository Skill Set as required Roundfix-owned and externally + managed Agent Skills while excluding unrelated extras. +- Defined Repository Skill Set readiness as an independent Doctor Command + result after Agent Selection Profile Readiness. +- Documented exact `skills: ok` and `skills: failed` outcomes, exit `1`, + ownership authorities, offline and read-only behavior, ignored extras, and + both update commands in the README and user guides. + +### Verification + +- `rtk git diff --check` — passed with no whitespace errors. +- `rtk go test ./internal/cli -run 'Test(CommandUsage|DocumentationContract)' -count=1` + — passed: 4 tests. +- `rtk go test ./skills -run 'Test(CheckRepositoryIgnoresUnrelatedSkillsAndLockEntries|CheckRepositoryReportsReadyRequiredSetWithoutMutation)' -count=1` + — passed: 2 tests proving no mutation and ignored unrelated entries. +- CLI-compatible deterministic folder-hash audit — passed: all 25 required + external installed trees match their `skills-lock.json` `computedHash` + entries. +- `rtk git diff --name-only -- .agents/skills skills skills-lock.json` — + passed with empty output; no protected tooling, external skill, recommendation, + or lock path changed. + +### Acceptance evidence + +- Supported docs identify the running binary as authority for Roundfix-owned + skills and `skills-lock.json` `computedHash` values as authority for + externally managed skills. They include both exact remediation commands and + the no-network/no-mutation guarantee. +- The fresh compatibility audit proves every required external tree matches its + authoritative lock entry after the separately committed refresh. +- Fresh skills tests prove unrelated installed skills and lock entries do not + affect readiness and that the checker does not mutate its repository. +- Git scope inspection proves this Task changed no protected tooling path. +- The glossary, docs, and existing Doctor help use Repository Skill Set, + Doctor Command, and Roundfix Skill consistently. diff --git a/docs/user-guide/commands.md b/docs/user-guide/commands.md index 74ea9ef..42f763f 100644 --- a/docs/user-guide/commands.md +++ b/docs/user-guide/commands.md @@ -67,6 +67,11 @@ lines include `next: ` when a remediation is known. The checks: classification, bounded adapter evidence, and the next `roundfix profiles configure` or `roundfix profiles validate` action. A rejected explicit `high` does not recommend model-managed reasoning. +- `skills:` — the required Repository Skill Set matches its local + authorities. The running binary's embedded artifacts are authoritative for + the 14 Roundfix-owned skills, including the Roundfix Skill. Each of the 25 + required external skills must hash to its `computedHash` in + `skills-lock.json`. - `codex:` — macOS-only runtime hygiene: inspects `com.apple.quarantine` (the real XProtect trigger) and code-signature validity, resolving `CODEX_PATH` first and then `codex` on `PATH`. It does not use `spctl --assess`, which @@ -76,18 +81,34 @@ lines include `next: ` when a remediation is known. The checks: Skipped on non-Darwin platforms. Doctor has no separate `agent:` or `model:` authority. The aggregate -`profiles:` result is the readiness contract, and it appears before `codex:` so -Spec 0036 can append independent Repository Skill Set readiness without -repeating selection proof. +`profiles:` result is the Agent Selection Profile Readiness contract. +Repository Skill Set readiness runs after it as an independent check, even +when profile proof fails, and appears before `codex:`. ```text node: ok acpx: ok adapter: ok (...) profiles: ok (3 distinct tuples; 10 category references) +skills: ok (39 required: 14 Roundfix-owned, 25 external) codex: ok ``` +A missing or outdated required skill, or an invalid required lock declaration, +prints one sorted blocking line and makes Doctor exit `1`. Doctor still prints +every other readiness result: + +```text +skills: failed (missing: handoff; outdated: roundfix; next: roundfix skills install --target project; bunx skills experimental_install && bunx skills update -p -y) +``` + +The first update command restores Roundfix-owned skills from the running +binary. The second updates externally managed skills and their lock state. +Doctor never runs either command, never deletes skills, and never updates +`skills-lock.json`. The check is offline and read-only: it reads only local +embedded artifacts, `.agents/skills`, and `skills-lock.json`. Unrelated extra +installed skills and lock entries are ignored and are not removed or flagged. + ### upgrade ```bash diff --git a/docs/user-guide/usage.md b/docs/user-guide/usage.md index f9eb432..1883586 100644 --- a/docs/user-guide/usage.md +++ b/docs/user-guide/usage.md @@ -15,9 +15,18 @@ agent driving Roundfix. For flags, outputs, and boundaries per command, see the ```bash roundfix setup # proves adapters and generated profiles before writes - roundfix doctor # read-only adapter and profile readiness; mutates nothing + roundfix doctor # read-only profile and Repository Skill Set readiness ``` + Doctor prints Agent Selection Profile Readiness first, then independently + proves the Repository Skill Set. A ready repository prints + `skills: ok (39 required: 14 Roundfix-owned, 25 external)`; a blocking + mismatch prints `skills: failed` with the applicable owned or external + update command and exits `1`. Doctor is offline and read-only: it never + updates or deletes skills, and it ignores unrelated extra installed skills + and lock entries. See the [Doctor Command reference](commands.md#doctor) + for the ownership authorities and exact remediation commands. + 3. Authenticate the GitHub CLI for the repository (`gh auth status`). Review loops need it; the implement loop does not. 4. Work on a non-default branch. `implement` refuses to run on the repository's From d4b679efbba0e9a76eb89a62d630d38d865c0623 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 13:36:24 -0300 Subject: [PATCH 17/48] docs: reopen skill hash compatibility task --- .../0036-doctor-skill-readiness/task_01.md | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/specs/0036-doctor-skill-readiness/task_01.md b/docs/specs/0036-doctor-skill-readiness/task_01.md index 207894b..35b9b12 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_01.md +++ b/docs/specs/0036-doctor-skill-readiness/task_01.md @@ -1,7 +1,7 @@ --- task: task_01 spec: 0036-doctor-skill-readiness -status: completed +status: pending type: backend complexity: high --- @@ -49,7 +49,7 @@ filesystem mutation. - [x] Add repository readiness values and stable ownership classifications. - [x] Add exact owned-tree comparison against embedded skill files. -- [x] Add strict required lock-entry validation and compatible external hashing. +- [ ] Add strict required lock-entry validation and compatible external hashing. - [x] Inject the checker into Doctor and render deterministic success/failure. - [x] Add focused package tests for clean, missing, outdated, malformed, unsafe, symlink, mixed-ownership, and ordering cases. @@ -66,7 +66,7 @@ filesystem mutation. prints the ownership-specific command, and exits one. - [x] Changing, adding, or removing a versioned file reports the affected skill under `outdated`; unrelated extra skills remain ignored. -- [x] A compatibility fixture produces the exact same digest as the installed +- [ ] A compatibility fixture produces the exact same digest as the installed skills CLI algorithm, including path normalization and exclusions. - [x] Missing/malformed lock data, invalid hashes, unsafe names, unreadable artifacts, and symlinks fail deterministically without panic or traversal. @@ -101,6 +101,8 @@ filesystem mutation. `skills:` output, remediation, exit behavior, and existing Doctor lines pass. - `rtk go test -race ./skills ./internal/cli -run 'Test(CheckRepository|RunDoctor)' -count=1` — expected: injected checks and filesystem reads are race-free. +- `rtk go test ./skills -count=1` — expected: the complete skills suite, + including the real repository baseline contract, passes. ## References @@ -114,6 +116,20 @@ filesystem mutation. - `docs/agents/skill-dispatch.md` → owned versus upstream-managed authority and synchronization boundaries. +## Rework Trigger + +Fresh real-repository evidence invalidated the completed compatibility claim: +after `bunx skills update -p -y` normalized 25 external lock entries, Doctor +reported 21 matching skills as outdated. + +The installed `skills` CLI versions 1.5.19 and 1.5.20 sort relative paths with +JavaScript `localeCompare`, while `SkillFolderHash` currently uses Go byte +ordering. The pinned fixture encodes the Go ordering (`SKILL.md` before +`references/guide.md`) and therefore proves the implementation against itself +instead of against the CLI. Repair the production ordering and fixture, add a +regression that distinguishes these orderings, and prove the complete skills +suite against the refreshed real repository. + ## Result Implemented one offline, read-only Repository Skill Set readiness check at the From fb78fbf40a31f5556adf158efa2e15bd38cac7fe Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 14:03:06 -0300 Subject: [PATCH 18/48] feat: diagnose Repository Skill Set readiness Roundfix-Spec: 0036-doctor-skill-readiness Roundfix-Task: task_01 --- .../0036-doctor-skill-readiness/task_01.md | 84 +++++++++++-------- .../assets/lock-hash-compatibility-v1.json | 2 +- internal/baseline/skills_restore.go | 9 +- internal/baseline/testdata/catalog.digest | 2 +- .../baseline/testdata/catalog.normalized.json | 2 +- skills/baseline_skill_contract_test.go | 51 +++++++++-- skills/skills.go | 10 ++- skills/skills_test.go | 23 +++++ 8 files changed, 136 insertions(+), 47 deletions(-) diff --git a/docs/specs/0036-doctor-skill-readiness/task_01.md b/docs/specs/0036-doctor-skill-readiness/task_01.md index 35b9b12..eb5d675 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_01.md +++ b/docs/specs/0036-doctor-skill-readiness/task_01.md @@ -1,7 +1,7 @@ --- task: task_01 spec: 0036-doctor-skill-readiness -status: pending +status: completed type: backend complexity: high --- @@ -49,7 +49,7 @@ filesystem mutation. - [x] Add repository readiness values and stable ownership classifications. - [x] Add exact owned-tree comparison against embedded skill files. -- [ ] Add strict required lock-entry validation and compatible external hashing. +- [x] Add strict required lock-entry validation and compatible external hashing. - [x] Inject the checker into Doctor and render deterministic success/failure. - [x] Add focused package tests for clean, missing, outdated, malformed, unsafe, symlink, mixed-ownership, and ordering cases. @@ -66,7 +66,7 @@ filesystem mutation. prints the ownership-specific command, and exits one. - [x] Changing, adding, or removing a versioned file reports the affected skill under `outdated`; unrelated extra skills remain ignored. -- [ ] A compatibility fixture produces the exact same digest as the installed +- [x] A compatibility fixture produces the exact same digest as the installed skills CLI algorithm, including path normalization and exclusions. - [x] Missing/malformed lock data, invalid hashes, unsafe names, unreadable artifacts, and symlinks fail deterministically without panic or traversal. @@ -132,47 +132,61 @@ suite against the refreshed real repository. ## Result -Implemented one offline, read-only Repository Skill Set readiness check at the -`skills` ownership boundary and appended its deterministic `skills:` result -after Agent Selection Profile readiness in Doctor. +Repaired the external skill hash compatibility contract. `SkillFolderHash` and +the Baseline external-lock adapter now reproduce the installed skills CLI +`localeCompare` ordering for repository paths, while Baseline setup snapshots +retain their separate byte-ordered content-digest contract. + +The compatibility fixture now pins the CLI-derived `501e315e...` digest and +explicitly fails if its file order no longer distinguishes JavaScript locale +ordering from Go byte ordering. A read-only real-repository test proves all 25 +refreshed external skill trees against `skills-lock.json`; catalog compatibility +fixtures carry the resulting lock-adapter artifact digest. ### Verification -- `rtk env GOCACHE=/tmp/roundfix-task01-go-cache go test ./skills -run 'Test(CheckRepository|SkillFolderHash)' -count=1` +- `rtk env GOCACHE=/tmp/roundfix-task01-final-skills-cache go test ./skills -run 'Test(CheckRepository|SkillFolderHash)' -count=1` — passed (`ok roundfix/skills`). -- `rtk env GOCACHE=/tmp/roundfix-task01-go-cache go test ./internal/cli -run 'TestRunDoctor' -count=1` +- `rtk env GOCACHE=/tmp/roundfix-task01-final-cli-cache go test ./internal/cli -run 'TestRunDoctor' -count=1` — passed (`ok roundfix/internal/cli`). -- `rtk env GOCACHE=/tmp/roundfix-task01-go-cache go test -race ./skills ./internal/cli -run 'Test(CheckRepository|RunDoctor)' -count=1` +- `rtk env GOCACHE=/tmp/roundfix-task01-final-race-cache go test -race ./skills ./internal/cli -run 'Test(CheckRepository|RunDoctor)' -count=1` — passed for both packages with the race detector. -- `rtk env GOCACHE=/tmp/roundfix-task01-go-cache go test ./skills ./internal/cli -count=1` - — passed for both complete package suites. -- `rtk git -c core.fsmonitor=false diff --check` — passed. - -The sandbox did not permit Go's default user cache, so focused checks used the -equivalent temporary `GOCACHE`; the Daemon remains responsible for running the -task's declared Verification commands verbatim. +- `rtk env GOCACHE=/tmp/roundfix-task01-final-full-cache go test ./skills -count=1` + — passed (`ok roundfix/skills`), including the real repository baseline. +- `rtk env GOCACHE=/tmp/roundfix-task01-rework-baseline-cache go test ./internal/baseline -run 'Test(EmbeddedCatalog|AssetsSyncCompatibilityMatchesMaintainedPythonContract|SkillsRestore|CatalogCompatibility)' -count=1` + — passed (`ok roundfix/internal/baseline`). +- `rtk env GOCACHE=/tmp/roundfix-task01-rework-verify-cache make verify` + — blocked after `2393` passing tests by the pre-existing, out-of-scope + `TestProfilesDocumentationContractMatchesPublicGuidance` failure: + Doctor help is missing `Agent Selection Profiles`. + +The sandbox does not permit Go's default user cache, so verification used +task-local temporary `GOCACHE` directories. The Daemon remains responsible for +running the declared Verification commands verbatim. ### Acceptance evidence -- The complete embedded-owned plus required-external fixture returns derived - `14` owned, `25` external, and Doctor renders `39 required` with exit zero. -- Owned and external removals are classified under sorted `missing` names; - content changes, added files, and removed files are classified under - `outdated`; unrelated installed skills and lock entries are ignored. -- The pinned compatibility fixture, nested slash-normalized paths, lexical - ordering, and `.git`/`node_modules` exclusions pass through - `SkillFolderHash`. -- Missing and malformed locks, wrong versions, absent entries, invalid hashes, - unsafe names, unreadable artifact shapes, owned symlinks, and external - symlinks have deterministic error or readiness outcomes with no traversal. -- Exact Doctor output tests prove success, owned-only, external-only, mixed, - and checker-error lines; mixed remediation appears exactly once in - owned-then-external order and all pre-existing checks still execute. -- A before/after fixture snapshot proves `CheckRepository` performs no - mutation. Production code executes no commands, performs no network access, - and exposes no write path. +- AC 1: the complete fixture and exact Doctor tests prove derived `14` owned, + `25` external, and `39` total counts with exit zero. +- AC 2: focused repository and Doctor tests prove owned and external removals + are sorted under `missing`, select the ownership-specific command, and exit + one. +- AC 3: focused tests prove changed, added, and removed versioned artifacts are + `outdated`, while unrelated installed and lock entries remain ignored. +- AC 4: the pinned fixture hashes `references/guide.md` before `SKILL.md` to + `501e315e486dc59cbaa999085edd4312d35bbc690947dfb59af2e86722466aa9`; + the regression rejects Go byte ordering, and the real repository test matches + every refreshed external lock hash. +- AC 5: focused tests prove malformed locks, invalid hashes, unsafe names, + unreadable artifacts, and symlinks fail without panic or traversal. +- AC 6: exact Doctor tests prove one sorted `skills:` line and one copy of each + remediation command in owned-then-external order. +- AC 7: race, no-mutation, and exact Doctor tests prove all existing checks + still run and the checker performs no repository, config, Run, lock, or skill + mutation. ### Follow-ups -None. Documentation and Roundfix Skill synchronization remain assigned to -their separate Tasks in this Spec. +Restore the pre-existing `Agent Selection Profiles` wording required by +`TestProfilesDocumentationContractMatchesPublicGuidance` in its owning CLI +documentation slice; no `internal/cli` file changed in this rework. diff --git a/internal/baseline/assets/lock-hash-compatibility-v1.json b/internal/baseline/assets/lock-hash-compatibility-v1.json index 4cac199..9575b6f 100644 --- a/internal/baseline/assets/lock-hash-compatibility-v1.json +++ b/internal/baseline/assets/lock-hash-compatibility-v1.json @@ -11,5 +11,5 @@ "content": "---\nname: fixture\n---\n" } ], - "expectedSha256": "ebdda12bc5f4c6920e866c330c0e3afd5920f85b6abb1622356dce57ecc10dab" + "expectedSha256": "501e315e486dc59cbaa999085edd4312d35bbc690947dfb59af2e86722466aa9" } diff --git a/internal/baseline/skills_restore.go b/internal/baseline/skills_restore.go index 1357e11..75fb149 100644 --- a/internal/baseline/skills_restore.go +++ b/internal/baseline/skills_restore.go @@ -1239,7 +1239,14 @@ func portableRestoreDigest(files []restoreFile) string { } func externalSkillsLockDigest(files []restoreFile) string { - sortRestoreFiles(files) + sort.Slice(files, func(i, j int) bool { + left := strings.ToLower(files[i].Path) + right := strings.ToLower(files[j].Path) + if left == right { + return files[i].Path > files[j].Path + } + return left < right + }) digest := sha256.New() for _, file := range files { _, _ = digest.Write([]byte(file.Path)) diff --git a/internal/baseline/testdata/catalog.digest b/internal/baseline/testdata/catalog.digest index 2a36b15..963bc81 100644 --- a/internal/baseline/testdata/catalog.digest +++ b/internal/baseline/testdata/catalog.digest @@ -1 +1 @@ -sha256:f082de03ea52e1b17e75daf0d65ef2d535f8d8d36951fe08dd435732e4008c1e +sha256:53a2d70d8569688d2b6b5353fc86ee6099ef412b11fd5ee0d04050134be00cdf diff --git a/internal/baseline/testdata/catalog.normalized.json b/internal/baseline/testdata/catalog.normalized.json index 369c410..0473513 100644 --- a/internal/baseline/testdata/catalog.normalized.json +++ b/internal/baseline/testdata/catalog.normalized.json @@ -1 +1 @@ -{"schemaVersion":"roundfix/baseline-catalog/v1","files":[{"path":"contract-v1.json","bytes":13525,"digest":"sha256:0c32b7ac1b6dd1a2a04d830b300d592b7b195aec7bb32ff05575e8d19f154497"},{"path":"coverage.json","bytes":4660,"digest":"sha256:9faa73bf86fad383570b0098a9aa26185ea8d689f9866038ab37b8a34b9a01dc"},{"path":"decisions.json","bytes":8438,"digest":"sha256:58acbb05cf46057923e8844b28e2a97efa898d6200ae9e0773971b6287b05651"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/AGENTS.md","bytes":2911,"digest":"sha256:1876cc64ced89e60cfb0188eaff71d8b588c80d1149740ba368d1afc969e1d8f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/agent-instructions.md","bytes":4182,"digest":"sha256:eb769f99afa32a901723e1962a06fca09d856c8d37284b36c9aed071bd6e4f86"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/autonomous-work.md","bytes":789,"digest":"sha256:76065b3f1b452f647765d01bfb802ca3a0757d7212ba75b8e55d1d6242b0c977"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/backend.md","bytes":1083,"digest":"sha256:b8459f66c5fec1d424768fdd2b0b2f4ec70b9038208c1d5a58086720586195fd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/docs-layout.md","bytes":3546,"digest":"sha256:a188cfaae89ed195ca31460af3ec5d0dfad0bc1241e1958950ed4e0035de86f6"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/domain.md","bytes":614,"digest":"sha256:089a408493f450ef5713f3f62b20b51323b6d08617e6e43a12e38d794676ae82"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/external-triage.md","bytes":385,"digest":"sha256:4e6b2cfafd491161e7666b304ce0d2d6b19ad705613a6471999c84fda1d63e6a"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/frontend.md","bytes":825,"digest":"sha256:3cd5cba8160ea9f6419ffe82920f7641d09eb7126a78ab50606dfe045732cc02"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/issue-tracker.md","bytes":353,"digest":"sha256:f2139d498fb79358a240d8c99ab1718764217cf28aec0fb024beadfa2b0231cc"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/monorepo.md","bytes":371,"digest":"sha256:47cf6a0ac72c2bcacfc69e5566b1cdcc2102e552b1d91592e5cb1033d30be9e3"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/secondbrain.md","bytes":1589,"digest":"sha256:fab2b2896ad03e0436414b83e5cd80e7fdc07b96d9231b798194b36314a8e7a7"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/skill-dispatch.md","bytes":14414,"digest":"sha256:d424a3e360031e39913bf385f87012e6d1ab79a84024701092859640d2fd05dd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/spec-routing.md","bytes":2269,"digest":"sha256:ce1b67f65ce8ad8a5dc1505e2201833d0e477ff91a6161c395db1188ae83ce9f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/typescript-bun.md","bytes":1378,"digest":"sha256:ec114298fee6ef89bbf0423a6b8a467630296264fc49e989d89af3301093b96e"},{"path":"lock-hash-compatibility-v1.json","bytes":366,"digest":"sha256:4020da4a35468cafb9091dad688be99f4bec87385be348a37d06d2e5192e60ec"},{"path":"modules/autonomous-work.json","bytes":2400,"digest":"sha256:61cb4100b7a9378f4d582964d6ac7fac52e005f5b27d0e5c359bb80a9fce7f85"},{"path":"modules/backend.json","bytes":5655,"digest":"sha256:c5b3d012e5ee3ec4dabcb5ea679fbc89932eb9760215c0c91d0bc8bb45a4f806"},{"path":"modules/bun.json","bytes":2557,"digest":"sha256:ca869344629ff4ede015e761c87afb01c20c371c05260cef56f36a7376e53777"},{"path":"modules/cli-surface.json","bytes":1721,"digest":"sha256:04e84d41b65452dc06d43ce43e2aedec3cc05d100d18aacc150f808ae80f7d32"},{"path":"modules/context-workflow.json","bytes":11142,"digest":"sha256:fb61822b2ce29891d6a06963b225a4da6935859df83ae1aa3fde9fa3bf0b0d30"},{"path":"modules/core.json","bytes":18107,"digest":"sha256:69dd99c698a50f651b0bd0b3021b2abbfba645955fe534735e97bff4ef0f1098"},{"path":"modules/external-triage.json","bytes":1159,"digest":"sha256:5ea90217b042667a3e00211941d4cb1bafb24294187635bfa277765c05894366"},{"path":"modules/frontend.json","bytes":8713,"digest":"sha256:6a701d874b2217bb7f59cddfdd0d0d8a8bb41c6452fc63b67251e5beddc25f7d"},{"path":"modules/go.json","bytes":3322,"digest":"sha256:d9877c86f59206b3bf6bdc1bf73fdfd18fdcc496bc67770413963587276b0533"},{"path":"modules/monorepo.json","bytes":1385,"digest":"sha256:e6485fd6dacb765a866dd9ff79f057d2ebc85cb57c603774fe7ca453cf50f917"},{"path":"modules/repository-extension.json","bytes":1094,"digest":"sha256:a2adeac11000706839944c2ecca225e524da6f38534e54efc9cb0a26f5413f47"},{"path":"modules/rust.json","bytes":2328,"digest":"sha256:46537524b19a5eb1b61dd07d7215b2ea607c1b78a481a21eb13926d40ecdc0b2"},{"path":"modules/secondbrain.json","bytes":4333,"digest":"sha256:3ce59d0ddfcd0e2e5de34d9a4541a27bc62c6e72800da1cd02353ba2f2aa2024"},{"path":"modules/spec-workflow.json","bytes":6441,"digest":"sha256:574f08d79b15e4d9a6a38c3a9a85636f5a208aa9e43051383815056fc03c6e53"},{"path":"modules/tui-surface.json","bytes":1658,"digest":"sha256:06e18ef65d3a7184aaa3a80a21d2279b3376ab77f07bb8b47d23497b0fb6fa62"},{"path":"modules/typescript.json","bytes":8526,"digest":"sha256:7a2a82f7efb28df7dbc343f1b5a7b0ab176e8a74105eb6077c7b5cf6648bdbc6"},{"path":"profiles/go-cli-tui.json","bytes":1709,"digest":"sha256:231220ac521a8298d86440f6e262d61b9c2fa249459dd9206973d8db6e6941bb"},{"path":"profiles/rust-cli.json","bytes":1627,"digest":"sha256:f2338dbf8d0085a6150293c157b6986714de122b2ebd732e33a09f0614686c81"},{"path":"profiles/standard-typescript-monorepo.json","bytes":10907,"digest":"sha256:803f62fb451f9fda23a58b7bfe5f5367a45d8d44bb63c3f96e33b259393cc5ba"},{"path":"retention/transition.legacy-typescript-bun-to-portable-v3.json","bytes":12425,"digest":"sha256:3b166a42eb1f9f23202afc48399eb7aa9b0e8563d9c40095ad6bf710a5d1c599"},{"path":"retention/transition.managed-v2-to-portable-v3.json","bytes":11185,"digest":"sha256:8a0e9dd06aa6a5988c010f6c1286c39002c65e5aae6762e9af6956424524c3e9"},{"path":"setups/go-cli.json","bytes":14203,"digest":"sha256:28e2ad53684c2c097affa4cf70c32a59265435dd105cdcfa57314bd18a6af46f"},{"path":"setups/rust-cli.json","bytes":12476,"digest":"sha256:82cdba03b2c77c9a0c4a6ad737e7de5e90a3fcf6827c18242793b8032766d9c0"},{"path":"setups/typescript-bun.json","bytes":40649,"digest":"sha256:573437aeb58bf8ef7cdd2c37d250b6ab3c66d93d12a69e8d388b4644862ffb3f"},{"path":"skill-activations.json","bytes":3330,"digest":"sha256:bbf1978828a2d383c9cee675a612c17281224e0a1ce5df4b09ba13af4ccd3667"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/accounting.json","bytes":17193,"digest":"sha256:69e39f9df0eb54d08ecb739dc3dfc9dd80c0cf76016aca3d6a0566e48cfaa1f5"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/baseline.json","bytes":498,"digest":"sha256:dca6a12ae24a77fc9104dfcccb507c50dee0e5257e0bf0bd9d86c0b04ce7ffe0"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/AGENTS.md","bytes":583,"digest":"sha256:3b6b36fd045ecb683aec29e0ceac52b3704604815d8035297bfeb8438602494e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/agent-instructions.md","bytes":8996,"digest":"sha256:fe584b8901ae2521dcba7014ca11f3557109a4d91d7c56a21852f027a4bf205e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/autonomous-work.md","bytes":1548,"digest":"sha256:3deff187dba70156ca834529741c4be7df84c5a66f8462ba01594fee35d40336"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/backend.md","bytes":2197,"digest":"sha256:c553c12e3cb13b0435a219fd52ac735bf4f9f3aad05fdb943fd6922cef0a97ac"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/docs-layout.md","bytes":7671,"digest":"sha256:8a78a3543a0a52ac93ef83ab36f953e813be068e96957553483c928cbad439bd"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/domain.md","bytes":947,"digest":"sha256:54c79c324d20b412c07c634f490ad18eeace0562caffe5507b3c83cebf9aaf20"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/external-triage.md","bytes":335,"digest":"sha256:0ec2089110c6a673f305e86f436538c7c07aeef46d29a4e68674defb8778e2cc"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/frontend.md","bytes":1934,"digest":"sha256:4a3609c27bf100aaf0d7de934ad43c0f6a561a15600517422a325fcc6120bd14"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/issue-tracker.md","bytes":890,"digest":"sha256:d86ab246d701a54b8d61c5dcdc1d29fe3281115c028e8694f10574ce99e537c7"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/monorepo.md","bytes":366,"digest":"sha256:c59091add143e67b3773df78d95cc4c8f11d43909655ef1c09dd64c0cc903b3d"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/secondbrain.md","bytes":3312,"digest":"sha256:4b58f3ed96f654d8b99ccc9a03718505f4c43a6170eceef47e26484b5f95bffb"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/skill-dispatch.md","bytes":374,"digest":"sha256:90bbf02e35bba9c707e4713eef4de9a6db36ba3fa93393531287aaf94fce8f8b"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/spec-routing.md","bytes":3094,"digest":"sha256:dfc867401e547d4370f20c578a22e6b82cc80d7b677072f31bc11cdba4851085"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/typescript-bun.md","bytes":2561,"digest":"sha256:7295fe2e544af37e6d6777537fa7ee512b78f3da932d736cc7eabc8d190a30e8"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/manifest.json","bytes":37175,"digest":"sha256:e9a9e91f8dbbea8dc416a03fe813b9c370a2e012118f548b18f1c6a13b6b7662"},{"path":"source-baselines/index.json","bytes":5266,"digest":"sha256:ad636b4cbaf57e27c81093aee9963bcd2b276df7ac243f04b423bf989db314a8"},{"path":"templates/extensions/specific-repository.md","bytes":38,"digest":"sha256:f8aad350dfecd91923c12fe215c1e21dd6741635e44869110391ba9c5c89d137"},{"path":"templates/guides/agent-instructions.md","bytes":260,"digest":"sha256:91f0a4b1d664cf6ab261499b3f7cb093bd2337d6567d07599faebd8fe58dc8d3"},{"path":"templates/guides/autonomous-work.md","bytes":194,"digest":"sha256:384fdebdb4ffd5d6c1b7b197cc02e3f9d3e7327acd7151dc7ce29c0e53e99486"},{"path":"templates/guides/backend.md","bytes":201,"digest":"sha256:0bffbf1e74c9b8c506ec1c612aac0dfaedcfb6f1b7aafc8d1a2d6029cd1170ea"},{"path":"templates/guides/bun.md","bytes":26,"digest":"sha256:3a0640850dc84c8dd6c6b35f10864ae6d3d9c153147699b7bfced6332315633d"},{"path":"templates/guides/cli-surface.md","bytes":34,"digest":"sha256:7cdd6ecc6575c4ef165858ced6409278430a89e3f67baa4281151dc13e2742bc"},{"path":"templates/guides/docs-layout.md","bytes":34,"digest":"sha256:51e9033d80e6742321b47b19c85034c9b4d15b36b9c84230512e3a9a4ef90628"},{"path":"templates/guides/domain-multi-context.md","bytes":112,"digest":"sha256:56b7000576757f37da275123ab75ff5f84d2e26bd88c9f67034a24f3d4883a41"},{"path":"templates/guides/domain-single-context.md","bytes":104,"digest":"sha256:f75666666408761a67d23855f5f78e4526328404343f09a036f8987895c69c0d"},{"path":"templates/guides/domain.md","bytes":59,"digest":"sha256:5c4195e0348b6e5a57679fc833e58b09f4b08080b1cec90a946d0b666310739e"},{"path":"templates/guides/external-triage.md","bytes":38,"digest":"sha256:f26c4164ff69bf44263d9993e4209a9305b8979f228f6f519f139e83f9b51ae9"},{"path":"templates/guides/frontend.md","bytes":265,"digest":"sha256:3243626ac7b55130ac198ba32a350966a26f6a9801a4a4fee259817b2f7eb5f9"},{"path":"templates/guides/go.md","bytes":25,"digest":"sha256:7f597711bdab9fe85ecda9f5895687e436009446ee8416ed96f7bc27fe7b00fd"},{"path":"templates/guides/issue-tracker.md","bytes":36,"digest":"sha256:48840599c91f2f3cf2fd79700f41e54bc3edbb4fa19e4861247e19bac0077fb1"},{"path":"templates/guides/monorepo.md","bytes":31,"digest":"sha256:e1866f1633f3bbc5b230b5c5290b71ae47110dce4d4344c264a9eb325a477bca"},{"path":"templates/guides/rust.md","bytes":27,"digest":"sha256:e0e693135266e96cabec918b14f0cf51c7bf1a126d900001cc418f982f8bbec2"},{"path":"templates/guides/secondbrain.md","bytes":34,"digest":"sha256:a59dc0ae149d4bb3bdd633f79ba91fd17e6d3d3d930c346e5757b5f85ab69002"},{"path":"templates/guides/skill-dispatch.md","bytes":253,"digest":"sha256:0f054b846da5b17409dde049b3b182489e6c24e84734d4a5ffc47a9f504e8a7c"},{"path":"templates/guides/spec-docs-layout.md","bytes":39,"digest":"sha256:6d9ddb174c4f69cb1f9abe428a6be677e9ea8dbecc19ae0fa2f53fdc3749de40"},{"path":"templates/guides/spec-routing.md","bytes":35,"digest":"sha256:cc578e73968c15bdcaad35270454e91462b4413d937cfd984404ef22d765482c"},{"path":"templates/guides/tui-surface.md","bytes":34,"digest":"sha256:bbc422cb32df905d526a291cbfd5b81fd86f0d405f5f650cc5d8eb0a864c4eb2"},{"path":"templates/guides/typescript-bun.md","bytes":41,"digest":"sha256:ee14609660ebe1da17eff0d12c3f42857cba4c92be0f723346d17e3d64bb6574"},{"path":"templates/index.json","bytes":7125,"digest":"sha256:272cbfd4fa2ea380acfc76ce4d75eaf2ae026b7501c34ee48a8645644eb72f4c"},{"path":"templates/root/autonomous-work.md","bytes":102,"digest":"sha256:abd0be4c74295597be5dca6a307e41d6bdcfa5113370f5bbc30d30725c927e35"},{"path":"templates/root/backend.md","bytes":76,"digest":"sha256:f65a96aaa0b79d40d209df159c30bd6ac791e36c415b8ba2dc52a1032c5785c3"},{"path":"templates/root/bun.md","bytes":89,"digest":"sha256:48a4bcd7c9b86d8f3be6965ff8b7b33aad77148aa598d49ed8efa74acd3c8e20"},{"path":"templates/root/cli-surface.md","bytes":79,"digest":"sha256:ec022b0b1767d88ebc849f2ee7d8a2d266a36d419f8084b9cce8599d0619a125"},{"path":"templates/root/context-workflow.md","bytes":129,"digest":"sha256:52c53e99fed6c5d59388e58f7854c96f8130da553d80bb90f04935f67eea45c5"},{"path":"templates/root/core.md","bytes":213,"digest":"sha256:ffa6f25825e1d5c5bf52373022d7035b004fd78dd55f99a187d89c4b78ddeabb"},{"path":"templates/root/external-triage.md","bytes":91,"digest":"sha256:5ea18c6c7aea8f03d62af0b824b947ddecbaf861fbf9b81df2e8fb2b2c0e2c54"},{"path":"templates/root/frontend.md","bytes":99,"digest":"sha256:cddad0b38dff9cbeae571dc8514f5d2770e528725f8931272389bd8a675e08bb"},{"path":"templates/root/go.md","bytes":67,"digest":"sha256:aa3eafd5ebcec7aa64ae7fddf1c5b954512e831121934760f615c12ec52f6527"},{"path":"templates/root/monorepo.md","bytes":74,"digest":"sha256:76951b431357333d5b3bbbe718529797834c5de14ee6cd886bce175b5ef892e9"},{"path":"templates/root/repository-extension.md","bytes":103,"digest":"sha256:f24b6015a05eab4cdbb830eb485f9958b659d9a99d46b53d8538a813f6705fe1"},{"path":"templates/root/rust.md","bytes":73,"digest":"sha256:40c4f50d3a020406816612b9f9e0c6cbd545626401e447d4963ba2d30d8e40e3"},{"path":"templates/root/secondbrain.md","bytes":87,"digest":"sha256:dbb11588c2dbca5fd803eccc889586cac75824e9083c570bd1cad34fda901857"},{"path":"templates/root/spec-workflow.md","bytes":125,"digest":"sha256:c292c5b2b98facb56ef0f6da0a61f38544377ddb6339c63d5287ca7dec4f2360"},{"path":"templates/root/tui-surface.md","bytes":75,"digest":"sha256:5657d3abaddf480a535d00a8bfe94bcd3e0b6230aaaa3aac2f9ce89fdc7e20b0"},{"path":"templates/root/typescript.md","bytes":76,"digest":"sha256:9673f69acb604560153c93c46aa6a8a03ecedc81c3eae15e03b0b1b763dbcde7"}],"profiles":["go-cli-tui","rust-cli","standard-typescript-monorepo"],"modules":["autonomous-work","backend","bun","cli-surface","context-workflow","core","external-triage","frontend","go","monorepo","repository-extension","rust","secondbrain","spec-workflow","tui-surface","typescript"],"decisions":["auth.provider","autonomous.enabled","domain.layout","http.contract","identifier.strategy","language.generated","repository.extension.enabled","runtime.backend","runtime.design","secondbrain.enabled","spec.scaffold","triage.external","verification.gate"],"templates":["template.extension.repository-rules","template.guide.agent-instructions","template.guide.autonomous-work","template.guide.backend","template.guide.bun","template.guide.cli-surface","template.guide.docs-layout","template.guide.domain","template.guide.domain.multi-context","template.guide.domain.single-context","template.guide.external-triage","template.guide.frontend","template.guide.go","template.guide.issue-tracker","template.guide.monorepo","template.guide.rust","template.guide.secondbrain","template.guide.skill-dispatch","template.guide.spec-docs-layout","template.guide.spec-routing","template.guide.tui-surface","template.guide.typescript-bun","template.root.autonomous-work","template.root.backend","template.root.bun","template.root.cli-surface","template.root.context-workflow","template.root.core","template.root.external-triage","template.root.frontend","template.root.go","template.root.monorepo","template.root.repository-extension","template.root.rust","template.root.secondbrain","template.root.spec-workflow","template.root.tui-surface","template.root.typescript"],"setups":["go-cli","rust-cli","typescript-bun"],"retentionTransitions":["transition.legacy-typescript-bun-to-portable-v3","transition.managed-v2-to-portable-v3"]} +{"schemaVersion":"roundfix/baseline-catalog/v1","files":[{"path":"contract-v1.json","bytes":13525,"digest":"sha256:0c32b7ac1b6dd1a2a04d830b300d592b7b195aec7bb32ff05575e8d19f154497"},{"path":"coverage.json","bytes":4660,"digest":"sha256:9faa73bf86fad383570b0098a9aa26185ea8d689f9866038ab37b8a34b9a01dc"},{"path":"decisions.json","bytes":8438,"digest":"sha256:58acbb05cf46057923e8844b28e2a97efa898d6200ae9e0773971b6287b05651"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/AGENTS.md","bytes":2911,"digest":"sha256:1876cc64ced89e60cfb0188eaff71d8b588c80d1149740ba368d1afc969e1d8f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/agent-instructions.md","bytes":4182,"digest":"sha256:eb769f99afa32a901723e1962a06fca09d856c8d37284b36c9aed071bd6e4f86"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/autonomous-work.md","bytes":789,"digest":"sha256:76065b3f1b452f647765d01bfb802ca3a0757d7212ba75b8e55d1d6242b0c977"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/backend.md","bytes":1083,"digest":"sha256:b8459f66c5fec1d424768fdd2b0b2f4ec70b9038208c1d5a58086720586195fd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/docs-layout.md","bytes":3546,"digest":"sha256:a188cfaae89ed195ca31460af3ec5d0dfad0bc1241e1958950ed4e0035de86f6"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/domain.md","bytes":614,"digest":"sha256:089a408493f450ef5713f3f62b20b51323b6d08617e6e43a12e38d794676ae82"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/external-triage.md","bytes":385,"digest":"sha256:4e6b2cfafd491161e7666b304ce0d2d6b19ad705613a6471999c84fda1d63e6a"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/frontend.md","bytes":825,"digest":"sha256:3cd5cba8160ea9f6419ffe82920f7641d09eb7126a78ab50606dfe045732cc02"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/issue-tracker.md","bytes":353,"digest":"sha256:f2139d498fb79358a240d8c99ab1718764217cf28aec0fb024beadfa2b0231cc"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/monorepo.md","bytes":371,"digest":"sha256:47cf6a0ac72c2bcacfc69e5566b1cdcc2102e552b1d91592e5cb1033d30be9e3"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/secondbrain.md","bytes":1589,"digest":"sha256:fab2b2896ad03e0436414b83e5cd80e7fdc07b96d9231b798194b36314a8e7a7"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/skill-dispatch.md","bytes":14414,"digest":"sha256:d424a3e360031e39913bf385f87012e6d1ab79a84024701092859640d2fd05dd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/spec-routing.md","bytes":2269,"digest":"sha256:ce1b67f65ce8ad8a5dc1505e2201833d0e477ff91a6161c395db1188ae83ce9f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/typescript-bun.md","bytes":1378,"digest":"sha256:ec114298fee6ef89bbf0423a6b8a467630296264fc49e989d89af3301093b96e"},{"path":"lock-hash-compatibility-v1.json","bytes":366,"digest":"sha256:6a83f5c12ea6fe0a0aa2e2c710ef44b4cc922d2965240376bcd0f12254d765d0"},{"path":"modules/autonomous-work.json","bytes":2400,"digest":"sha256:61cb4100b7a9378f4d582964d6ac7fac52e005f5b27d0e5c359bb80a9fce7f85"},{"path":"modules/backend.json","bytes":5655,"digest":"sha256:c5b3d012e5ee3ec4dabcb5ea679fbc89932eb9760215c0c91d0bc8bb45a4f806"},{"path":"modules/bun.json","bytes":2557,"digest":"sha256:ca869344629ff4ede015e761c87afb01c20c371c05260cef56f36a7376e53777"},{"path":"modules/cli-surface.json","bytes":1721,"digest":"sha256:04e84d41b65452dc06d43ce43e2aedec3cc05d100d18aacc150f808ae80f7d32"},{"path":"modules/context-workflow.json","bytes":11142,"digest":"sha256:fb61822b2ce29891d6a06963b225a4da6935859df83ae1aa3fde9fa3bf0b0d30"},{"path":"modules/core.json","bytes":18107,"digest":"sha256:69dd99c698a50f651b0bd0b3021b2abbfba645955fe534735e97bff4ef0f1098"},{"path":"modules/external-triage.json","bytes":1159,"digest":"sha256:5ea90217b042667a3e00211941d4cb1bafb24294187635bfa277765c05894366"},{"path":"modules/frontend.json","bytes":8713,"digest":"sha256:6a701d874b2217bb7f59cddfdd0d0d8a8bb41c6452fc63b67251e5beddc25f7d"},{"path":"modules/go.json","bytes":3322,"digest":"sha256:d9877c86f59206b3bf6bdc1bf73fdfd18fdcc496bc67770413963587276b0533"},{"path":"modules/monorepo.json","bytes":1385,"digest":"sha256:e6485fd6dacb765a866dd9ff79f057d2ebc85cb57c603774fe7ca453cf50f917"},{"path":"modules/repository-extension.json","bytes":1094,"digest":"sha256:a2adeac11000706839944c2ecca225e524da6f38534e54efc9cb0a26f5413f47"},{"path":"modules/rust.json","bytes":2328,"digest":"sha256:46537524b19a5eb1b61dd07d7215b2ea607c1b78a481a21eb13926d40ecdc0b2"},{"path":"modules/secondbrain.json","bytes":4333,"digest":"sha256:3ce59d0ddfcd0e2e5de34d9a4541a27bc62c6e72800da1cd02353ba2f2aa2024"},{"path":"modules/spec-workflow.json","bytes":6441,"digest":"sha256:574f08d79b15e4d9a6a38c3a9a85636f5a208aa9e43051383815056fc03c6e53"},{"path":"modules/tui-surface.json","bytes":1658,"digest":"sha256:06e18ef65d3a7184aaa3a80a21d2279b3376ab77f07bb8b47d23497b0fb6fa62"},{"path":"modules/typescript.json","bytes":8526,"digest":"sha256:7a2a82f7efb28df7dbc343f1b5a7b0ab176e8a74105eb6077c7b5cf6648bdbc6"},{"path":"profiles/go-cli-tui.json","bytes":1709,"digest":"sha256:231220ac521a8298d86440f6e262d61b9c2fa249459dd9206973d8db6e6941bb"},{"path":"profiles/rust-cli.json","bytes":1627,"digest":"sha256:f2338dbf8d0085a6150293c157b6986714de122b2ebd732e33a09f0614686c81"},{"path":"profiles/standard-typescript-monorepo.json","bytes":10907,"digest":"sha256:803f62fb451f9fda23a58b7bfe5f5367a45d8d44bb63c3f96e33b259393cc5ba"},{"path":"retention/transition.legacy-typescript-bun-to-portable-v3.json","bytes":12425,"digest":"sha256:3b166a42eb1f9f23202afc48399eb7aa9b0e8563d9c40095ad6bf710a5d1c599"},{"path":"retention/transition.managed-v2-to-portable-v3.json","bytes":11185,"digest":"sha256:8a0e9dd06aa6a5988c010f6c1286c39002c65e5aae6762e9af6956424524c3e9"},{"path":"setups/go-cli.json","bytes":14203,"digest":"sha256:28e2ad53684c2c097affa4cf70c32a59265435dd105cdcfa57314bd18a6af46f"},{"path":"setups/rust-cli.json","bytes":12476,"digest":"sha256:82cdba03b2c77c9a0c4a6ad737e7de5e90a3fcf6827c18242793b8032766d9c0"},{"path":"setups/typescript-bun.json","bytes":40649,"digest":"sha256:573437aeb58bf8ef7cdd2c37d250b6ab3c66d93d12a69e8d388b4644862ffb3f"},{"path":"skill-activations.json","bytes":3330,"digest":"sha256:bbf1978828a2d383c9cee675a612c17281224e0a1ce5df4b09ba13af4ccd3667"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/accounting.json","bytes":17193,"digest":"sha256:69e39f9df0eb54d08ecb739dc3dfc9dd80c0cf76016aca3d6a0566e48cfaa1f5"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/baseline.json","bytes":498,"digest":"sha256:dca6a12ae24a77fc9104dfcccb507c50dee0e5257e0bf0bd9d86c0b04ce7ffe0"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/AGENTS.md","bytes":583,"digest":"sha256:3b6b36fd045ecb683aec29e0ceac52b3704604815d8035297bfeb8438602494e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/agent-instructions.md","bytes":8996,"digest":"sha256:fe584b8901ae2521dcba7014ca11f3557109a4d91d7c56a21852f027a4bf205e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/autonomous-work.md","bytes":1548,"digest":"sha256:3deff187dba70156ca834529741c4be7df84c5a66f8462ba01594fee35d40336"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/backend.md","bytes":2197,"digest":"sha256:c553c12e3cb13b0435a219fd52ac735bf4f9f3aad05fdb943fd6922cef0a97ac"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/docs-layout.md","bytes":7671,"digest":"sha256:8a78a3543a0a52ac93ef83ab36f953e813be068e96957553483c928cbad439bd"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/domain.md","bytes":947,"digest":"sha256:54c79c324d20b412c07c634f490ad18eeace0562caffe5507b3c83cebf9aaf20"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/external-triage.md","bytes":335,"digest":"sha256:0ec2089110c6a673f305e86f436538c7c07aeef46d29a4e68674defb8778e2cc"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/frontend.md","bytes":1934,"digest":"sha256:4a3609c27bf100aaf0d7de934ad43c0f6a561a15600517422a325fcc6120bd14"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/issue-tracker.md","bytes":890,"digest":"sha256:d86ab246d701a54b8d61c5dcdc1d29fe3281115c028e8694f10574ce99e537c7"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/monorepo.md","bytes":366,"digest":"sha256:c59091add143e67b3773df78d95cc4c8f11d43909655ef1c09dd64c0cc903b3d"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/secondbrain.md","bytes":3312,"digest":"sha256:4b58f3ed96f654d8b99ccc9a03718505f4c43a6170eceef47e26484b5f95bffb"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/skill-dispatch.md","bytes":374,"digest":"sha256:90bbf02e35bba9c707e4713eef4de9a6db36ba3fa93393531287aaf94fce8f8b"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/spec-routing.md","bytes":3094,"digest":"sha256:dfc867401e547d4370f20c578a22e6b82cc80d7b677072f31bc11cdba4851085"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/typescript-bun.md","bytes":2561,"digest":"sha256:7295fe2e544af37e6d6777537fa7ee512b78f3da932d736cc7eabc8d190a30e8"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/manifest.json","bytes":37175,"digest":"sha256:e9a9e91f8dbbea8dc416a03fe813b9c370a2e012118f548b18f1c6a13b6b7662"},{"path":"source-baselines/index.json","bytes":5266,"digest":"sha256:ad636b4cbaf57e27c81093aee9963bcd2b276df7ac243f04b423bf989db314a8"},{"path":"templates/extensions/specific-repository.md","bytes":38,"digest":"sha256:f8aad350dfecd91923c12fe215c1e21dd6741635e44869110391ba9c5c89d137"},{"path":"templates/guides/agent-instructions.md","bytes":260,"digest":"sha256:91f0a4b1d664cf6ab261499b3f7cb093bd2337d6567d07599faebd8fe58dc8d3"},{"path":"templates/guides/autonomous-work.md","bytes":194,"digest":"sha256:384fdebdb4ffd5d6c1b7b197cc02e3f9d3e7327acd7151dc7ce29c0e53e99486"},{"path":"templates/guides/backend.md","bytes":201,"digest":"sha256:0bffbf1e74c9b8c506ec1c612aac0dfaedcfb6f1b7aafc8d1a2d6029cd1170ea"},{"path":"templates/guides/bun.md","bytes":26,"digest":"sha256:3a0640850dc84c8dd6c6b35f10864ae6d3d9c153147699b7bfced6332315633d"},{"path":"templates/guides/cli-surface.md","bytes":34,"digest":"sha256:7cdd6ecc6575c4ef165858ced6409278430a89e3f67baa4281151dc13e2742bc"},{"path":"templates/guides/docs-layout.md","bytes":34,"digest":"sha256:51e9033d80e6742321b47b19c85034c9b4d15b36b9c84230512e3a9a4ef90628"},{"path":"templates/guides/domain-multi-context.md","bytes":112,"digest":"sha256:56b7000576757f37da275123ab75ff5f84d2e26bd88c9f67034a24f3d4883a41"},{"path":"templates/guides/domain-single-context.md","bytes":104,"digest":"sha256:f75666666408761a67d23855f5f78e4526328404343f09a036f8987895c69c0d"},{"path":"templates/guides/domain.md","bytes":59,"digest":"sha256:5c4195e0348b6e5a57679fc833e58b09f4b08080b1cec90a946d0b666310739e"},{"path":"templates/guides/external-triage.md","bytes":38,"digest":"sha256:f26c4164ff69bf44263d9993e4209a9305b8979f228f6f519f139e83f9b51ae9"},{"path":"templates/guides/frontend.md","bytes":265,"digest":"sha256:3243626ac7b55130ac198ba32a350966a26f6a9801a4a4fee259817b2f7eb5f9"},{"path":"templates/guides/go.md","bytes":25,"digest":"sha256:7f597711bdab9fe85ecda9f5895687e436009446ee8416ed96f7bc27fe7b00fd"},{"path":"templates/guides/issue-tracker.md","bytes":36,"digest":"sha256:48840599c91f2f3cf2fd79700f41e54bc3edbb4fa19e4861247e19bac0077fb1"},{"path":"templates/guides/monorepo.md","bytes":31,"digest":"sha256:e1866f1633f3bbc5b230b5c5290b71ae47110dce4d4344c264a9eb325a477bca"},{"path":"templates/guides/rust.md","bytes":27,"digest":"sha256:e0e693135266e96cabec918b14f0cf51c7bf1a126d900001cc418f982f8bbec2"},{"path":"templates/guides/secondbrain.md","bytes":34,"digest":"sha256:a59dc0ae149d4bb3bdd633f79ba91fd17e6d3d3d930c346e5757b5f85ab69002"},{"path":"templates/guides/skill-dispatch.md","bytes":253,"digest":"sha256:0f054b846da5b17409dde049b3b182489e6c24e84734d4a5ffc47a9f504e8a7c"},{"path":"templates/guides/spec-docs-layout.md","bytes":39,"digest":"sha256:6d9ddb174c4f69cb1f9abe428a6be677e9ea8dbecc19ae0fa2f53fdc3749de40"},{"path":"templates/guides/spec-routing.md","bytes":35,"digest":"sha256:cc578e73968c15bdcaad35270454e91462b4413d937cfd984404ef22d765482c"},{"path":"templates/guides/tui-surface.md","bytes":34,"digest":"sha256:bbc422cb32df905d526a291cbfd5b81fd86f0d405f5f650cc5d8eb0a864c4eb2"},{"path":"templates/guides/typescript-bun.md","bytes":41,"digest":"sha256:ee14609660ebe1da17eff0d12c3f42857cba4c92be0f723346d17e3d64bb6574"},{"path":"templates/index.json","bytes":7125,"digest":"sha256:272cbfd4fa2ea380acfc76ce4d75eaf2ae026b7501c34ee48a8645644eb72f4c"},{"path":"templates/root/autonomous-work.md","bytes":102,"digest":"sha256:abd0be4c74295597be5dca6a307e41d6bdcfa5113370f5bbc30d30725c927e35"},{"path":"templates/root/backend.md","bytes":76,"digest":"sha256:f65a96aaa0b79d40d209df159c30bd6ac791e36c415b8ba2dc52a1032c5785c3"},{"path":"templates/root/bun.md","bytes":89,"digest":"sha256:48a4bcd7c9b86d8f3be6965ff8b7b33aad77148aa598d49ed8efa74acd3c8e20"},{"path":"templates/root/cli-surface.md","bytes":79,"digest":"sha256:ec022b0b1767d88ebc849f2ee7d8a2d266a36d419f8084b9cce8599d0619a125"},{"path":"templates/root/context-workflow.md","bytes":129,"digest":"sha256:52c53e99fed6c5d59388e58f7854c96f8130da553d80bb90f04935f67eea45c5"},{"path":"templates/root/core.md","bytes":213,"digest":"sha256:ffa6f25825e1d5c5bf52373022d7035b004fd78dd55f99a187d89c4b78ddeabb"},{"path":"templates/root/external-triage.md","bytes":91,"digest":"sha256:5ea18c6c7aea8f03d62af0b824b947ddecbaf861fbf9b81df2e8fb2b2c0e2c54"},{"path":"templates/root/frontend.md","bytes":99,"digest":"sha256:cddad0b38dff9cbeae571dc8514f5d2770e528725f8931272389bd8a675e08bb"},{"path":"templates/root/go.md","bytes":67,"digest":"sha256:aa3eafd5ebcec7aa64ae7fddf1c5b954512e831121934760f615c12ec52f6527"},{"path":"templates/root/monorepo.md","bytes":74,"digest":"sha256:76951b431357333d5b3bbbe718529797834c5de14ee6cd886bce175b5ef892e9"},{"path":"templates/root/repository-extension.md","bytes":103,"digest":"sha256:f24b6015a05eab4cdbb830eb485f9958b659d9a99d46b53d8538a813f6705fe1"},{"path":"templates/root/rust.md","bytes":73,"digest":"sha256:40c4f50d3a020406816612b9f9e0c6cbd545626401e447d4963ba2d30d8e40e3"},{"path":"templates/root/secondbrain.md","bytes":87,"digest":"sha256:dbb11588c2dbca5fd803eccc889586cac75824e9083c570bd1cad34fda901857"},{"path":"templates/root/spec-workflow.md","bytes":125,"digest":"sha256:c292c5b2b98facb56ef0f6da0a61f38544377ddb6339c63d5287ca7dec4f2360"},{"path":"templates/root/tui-surface.md","bytes":75,"digest":"sha256:5657d3abaddf480a535d00a8bfe94bcd3e0b6230aaaa3aac2f9ce89fdc7e20b0"},{"path":"templates/root/typescript.md","bytes":76,"digest":"sha256:9673f69acb604560153c93c46aa6a8a03ecedc81c3eae15e03b0b1b763dbcde7"}],"profiles":["go-cli-tui","rust-cli","standard-typescript-monorepo"],"modules":["autonomous-work","backend","bun","cli-surface","context-workflow","core","external-triage","frontend","go","monorepo","repository-extension","rust","secondbrain","spec-workflow","tui-surface","typescript"],"decisions":["auth.provider","autonomous.enabled","domain.layout","http.contract","identifier.strategy","language.generated","repository.extension.enabled","runtime.backend","runtime.design","secondbrain.enabled","spec.scaffold","triage.external","verification.gate"],"templates":["template.extension.repository-rules","template.guide.agent-instructions","template.guide.autonomous-work","template.guide.backend","template.guide.bun","template.guide.cli-surface","template.guide.docs-layout","template.guide.domain","template.guide.domain.multi-context","template.guide.domain.single-context","template.guide.external-triage","template.guide.frontend","template.guide.go","template.guide.issue-tracker","template.guide.monorepo","template.guide.rust","template.guide.secondbrain","template.guide.skill-dispatch","template.guide.spec-docs-layout","template.guide.spec-routing","template.guide.tui-surface","template.guide.typescript-bun","template.root.autonomous-work","template.root.backend","template.root.bun","template.root.cli-surface","template.root.context-workflow","template.root.core","template.root.external-triage","template.root.frontend","template.root.go","template.root.monorepo","template.root.repository-extension","template.root.rust","template.root.secondbrain","template.root.spec-workflow","template.root.tui-surface","template.root.typescript"],"setups":["go-cli","rust-cli","typescript-bun"],"retentionTransitions":["transition.legacy-typescript-bun-to-portable-v3","transition.managed-v2-to-portable-v3"]} diff --git a/skills/baseline_skill_contract_test.go b/skills/baseline_skill_contract_test.go index ac48ecc..36ebb42 100644 --- a/skills/baseline_skill_contract_test.go +++ b/skills/baseline_skill_contract_test.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "io/fs" "os" "path/filepath" "reflect" @@ -312,12 +313,10 @@ func TestAuthorialSkillSync(t *testing.T) { t.Errorf("%s is repository-sourced but not owned by Roundfix", skill.Name) continue } - want, err := SkillFolderHash( + want := baselineSnapshotSkillDigest( + t, filepath.Join(repoRoot, ".agents", "skills", skill.Name), ) - if err != nil { - t.Fatalf("hash repository-owned skill %q: %v", skill.Name, err) - } if skill.ContentDigest != want { t.Errorf( "%s contentDigest = %q, want canonical %q", @@ -337,7 +336,7 @@ func TestAuthorialSkillSync(t *testing.T) { if err := json.Unmarshal(lockBytes, &lock); err != nil { t.Fatalf("decode skills lock: %v", err) } - const wantUpstreamDigest = "4fb4367c8a086034727b70b23795712be9b436074f4ba6b9bc6ae5da352d21a7" + const wantUpstreamDigest = "d61310f840938a57edeae639ad44e5b0140b2bada06bac460ae59216e1a790e7" if got := upstreamManagedSkillDigest(t, repoRoot, lock.Skills); got != wantUpstreamDigest { t.Fatalf("upstream-managed skill tree digest = %q, want %q", got, wantUpstreamDigest) } @@ -598,7 +597,7 @@ func TestAuthoringConstraintOwnership(t *testing.T) { }) } - const wantUpstreamDigest = "4fb4367c8a086034727b70b23795712be9b436074f4ba6b9bc6ae5da352d21a7" + const wantUpstreamDigest = "d61310f840938a57edeae639ad44e5b0140b2bada06bac460ae59216e1a790e7" if got := upstreamManagedSkillDigest(t, repoRoot, lock.Skills); got != wantUpstreamDigest { t.Fatalf("upstream-managed skill tree digest = %q, want %q", got, wantUpstreamDigest) } @@ -637,7 +636,7 @@ func TestUpstreamADRFormatUnchanged(t *testing.T) { _, _ = upstreamDigest.Write([]byte(name)) _, _ = upstreamDigest.Write([]byte(folderDigest)) } - const wantUpstreamDigest = "4fb4367c8a086034727b70b23795712be9b436074f4ba6b9bc6ae5da352d21a7" + const wantUpstreamDigest = "d61310f840938a57edeae639ad44e5b0140b2bada06bac460ae59216e1a790e7" if got := hex.EncodeToString(upstreamDigest.Sum(nil)); got != wantUpstreamDigest { t.Fatalf("upstream-managed skill tree digest = %q, want %q", got, wantUpstreamDigest) } @@ -907,6 +906,44 @@ func upstreamManagedSkillDigest( return hex.EncodeToString(digest.Sum(nil)) } +func baselineSnapshotSkillDigest(t *testing.T, root string) string { + t.Helper() + type digestFile struct { + path string + data []byte + } + var files []digestFile + if err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + files = append(files, digestFile{path: filepath.ToSlash(relative), data: data}) + return nil + }); err != nil { + t.Fatalf("hash Baseline snapshot skill %q: %v", root, err) + } + sort.Slice(files, func(i, j int) bool { + return files[i].path < files[j].path + }) + digest := sha256.New() + for _, file := range files { + _, _ = digest.Write([]byte(file.path)) + _, _ = digest.Write(file.data) + } + return hex.EncodeToString(digest.Sum(nil)) +} + func readBaselineSkillContractFile(t *testing.T, path string) []byte { t.Helper() data, err := os.ReadFile(path) diff --git a/skills/skills.go b/skills/skills.go index 64cf3f9..4abc1c7 100644 --- a/skills/skills.go +++ b/skills/skills.go @@ -182,7 +182,15 @@ func SkillFolderHash(root string) (string, error) { } sort.Slice(files, func(i, j int) bool { - return files[i].path < files[j].path + // String.localeCompare compares the normalized repository paths + // case-insensitively first and puts lowercase before uppercase when + // paths otherwise collate equally. + left := strings.ToLower(files[i].path) + right := strings.ToLower(files[j].path) + if left == right { + return files[i].path > files[j].path + } + return left < right }) digest := sha256.New() for _, file := range files { diff --git a/skills/skills_test.go b/skills/skills_test.go index 744a72e..c289d0a 100644 --- a/skills/skills_test.go +++ b/skills/skills_test.go @@ -64,6 +64,15 @@ func TestSkillFolderHashMatchesExternalCompatibilityFixture(t *testing.T) { if !foundNestedPath { t.Fatal("lock compatibility fixture must include a slash-normalized nested path") } + fixturePaths := make([]string, 0, len(fixture.Files)) + for _, file := range fixture.Files { + fixturePaths = append(fixturePaths, file.Path) + } + byteOrderedPaths := append([]string(nil), fixturePaths...) + sort.Strings(byteOrderedPaths) + if reflect.DeepEqual(fixturePaths, byteOrderedPaths) { + t.Fatal("lock compatibility fixture must distinguish external CLI locale ordering from Go byte ordering") + } got, err := SkillFolderHash(root) if err != nil { @@ -160,6 +169,20 @@ func TestSkillFolderHashWrapsMissingRootError(t *testing.T) { } } +func TestCheckRepositoryMatchesRealRepository(t *testing.T) { + root, err := filepath.Abs("..") + if err != nil { + t.Fatalf("resolve real repository root: %v", err) + } + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check real repository: %v", err) + } + if !got.Ready() { + t.Fatalf("real repository readiness = %#v, want ready", got) + } +} + func TestCheckRepositoryReportsReadyRequiredSetWithoutMutation(t *testing.T) { root := writeReadyRepositoryFixture(t) before := snapshotRepositoryFixture(t, root) From bda64c484a5f02ce86a98de53172b7edd73fd9d9 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 14:57:30 -0300 Subject: [PATCH 19/48] docs: authorize derived skill readiness artifacts --- .../specs/0036-doctor-skill-readiness/_prd.md | 9 +++++--- .../0036-doctor-skill-readiness/_techspec.md | 9 +++++--- .../0036-doctor-skill-readiness/task_01.md | 20 +++++++++++++---- .../0036-doctor-skill-readiness/task_03.md | 22 +++++++++++++------ 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/docs/specs/0036-doctor-skill-readiness/_prd.md b/docs/specs/0036-doctor-skill-readiness/_prd.md index 5f15397..1cd454f 100644 --- a/docs/specs/0036-doctor-skill-readiness/_prd.md +++ b/docs/specs/0036-doctor-skill-readiness/_prd.md @@ -38,9 +38,12 @@ Repository Skill Set result and must not recreate or bypass profile proof. Baseline execution in the Go CLI and prohibit restoring the removed Python runtime. Source: `docs/agents/domain.md`. - Tooling authority: applicable — on 2026-07-26, the maintainer expressly - authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and - `skills/roundfix/SKILL.md`; no other protected tooling mutation is - authorized. Source: + authorizes changes to `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`, plus the mechanically derived Baseline artifacts + `internal/baseline/assets/setups/typescript-bun.json`, + `internal/baseline/testdata/catalog.normalized.json`, and + `internal/baseline/testdata/catalog.digest`. No other protected tooling + mutation is authorized. Source: `docs/agents/agent-instructions.md`. ## Goals diff --git a/docs/specs/0036-doctor-skill-readiness/_techspec.md b/docs/specs/0036-doctor-skill-readiness/_techspec.md index 5f05658..73194d7 100644 --- a/docs/specs/0036-doctor-skill-readiness/_techspec.md +++ b/docs/specs/0036-doctor-skill-readiness/_techspec.md @@ -40,9 +40,12 @@ boundary and does not preserve or recreate the legacy single-model probe. asset behavior to remain in the Go CLI rather than a Python skill runtime. Source: `docs/agents/domain.md`. - Tooling authority: applicable — on 2026-07-26, the maintainer expressly - authorizes changes to exactly `.agents/skills/roundfix/SKILL.md` and - `skills/roundfix/SKILL.md`; no other protected tooling mutation is - authorized. Source: + authorizes changes to `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md`, plus the mechanically derived Baseline artifacts + `internal/baseline/assets/setups/typescript-bun.json`, + `internal/baseline/testdata/catalog.normalized.json`, and + `internal/baseline/testdata/catalog.digest`. No other protected tooling + mutation is authorized. Source: `docs/agents/agent-instructions.md`. ## System Architecture diff --git a/docs/specs/0036-doctor-skill-readiness/task_01.md b/docs/specs/0036-doctor-skill-readiness/task_01.md index eb5d675..001916b 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_01.md +++ b/docs/specs/0036-doctor-skill-readiness/task_01.md @@ -1,7 +1,7 @@ --- task: task_01 spec: 0036-doctor-skill-readiness -status: completed +status: pending type: backend complexity: high --- @@ -55,7 +55,7 @@ filesystem mutation. symlink, mixed-ownership, and ordering cases. - [x] Add exact-output Doctor tests for success, each ownership group, mixed remediation, checker errors, exit behavior, and no mutation. -- [x] Update Doctor command help for the appended Repository Skill Set check. +- [ ] Update Doctor command help for the appended Repository Skill Set check. ## Acceptance Criteria @@ -74,6 +74,8 @@ filesystem mutation. remediation commands exactly once in owned-then-external order. - [x] Doctor runs every existing check even when skill readiness fails and changes no repository, user-config, Run, or skill path. +- [ ] Doctor help continues to name both Agent Selection Profiles and the + appended Repository Skill Set readiness check. ## Context @@ -97,8 +99,9 @@ filesystem mutation. - `rtk go test ./skills -run 'Test(CheckRepository|SkillFolderHash)' -count=1` — expected: owned comparison, lock validation, hash compatibility, unsafe paths, stable ordering, and no-mutation cases pass. -- `rtk go test ./internal/cli -run 'TestRunDoctor' -count=1` — expected: exact - `skills:` output, remediation, exit behavior, and existing Doctor lines pass. +- `rtk go test ./internal/cli -run 'Test(RunDoctor|ProfilesDocumentationContractMatchesPublicGuidance)' -count=1` + — expected: exact `skills:` output, remediation, exit behavior, existing + Doctor lines, and public help terminology pass. - `rtk go test -race ./skills ./internal/cli -run 'Test(CheckRepository|RunDoctor)' -count=1` — expected: injected checks and filesystem reads are race-free. - `rtk go test ./skills -count=1` — expected: the complete skills suite, @@ -130,6 +133,15 @@ instead of against the CLI. Repair the production ordering and fixture, add a regression that distinguishes these orderings, and prove the complete skills suite against the refreshed real repository. +## Rework Trigger 2 + +The first Task 03 full gate exposed +`TestProfilesDocumentationContractMatchesPublicGuidance`: Doctor help no longer +names `Agent Selection Profiles`, even though Spec 0041 remains the prerequisite +readiness authority. Restore that existing public term while keeping the new +Repository Skill Set wording additive, then prove the focused documentation +contract before Task 03 runs. + ## Result Repaired the external skill hash compatibility contract. `SkillFolderHash` and diff --git a/docs/specs/0036-doctor-skill-readiness/task_03.md b/docs/specs/0036-doctor-skill-readiness/task_03.md index a110226..0fa4bb4 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_03.md +++ b/docs/specs/0036-doctor-skill-readiness/task_03.md @@ -23,12 +23,16 @@ Task file. 2. MUST apply the same content to `skills/roundfix/SKILL.md` so the canonical and generated copies are byte-identical. 3. MUST limit repository mutations to - `.agents/skills/roundfix/SKILL.md`, `skills/roundfix/SKILL.md`, and this - `task_03.md` file. + `.agents/skills/roundfix/SKILL.md`, `skills/roundfix/SKILL.md`, + `internal/baseline/assets/setups/typescript-bun.json`, + `internal/baseline/testdata/catalog.normalized.json`, + `internal/baseline/testdata/catalog.digest`, and this `task_03.md` file. 4. MUST NOT run `make skills-sync`, because that mutation target rewrites every owned skill directory; `make skills-sync-check` is the read-only sync gate. -5. MUST leave every external skill, lock file, recommendation file, manifest, - code file, and user-documentation file unchanged. +5. MUST update the three authorized Baseline artifacts only as mechanically + derived consequences of the canonical Roundfix Skill bytes. +6. MUST leave every external skill, lock file, recommendation file, source code + file, and user-documentation file unchanged. ## Subtasks @@ -43,7 +47,7 @@ Task file. performs an update. - [ ] The canonical and generated Roundfix `SKILL.md` files are byte-identical. - [ ] Git changed-file evidence for this Task contains only the two authorized - Skill files and `task_03.md`. + Skill files, three derived Baseline artifacts, and `task_03.md`. - [ ] No upstream-managed or other Roundfix-owned skill changes. - [ ] The shipped skill check and complete repository gate pass. @@ -54,13 +58,17 @@ Task file. - instruction: `.agents/skills/tech-writer/SKILL.md` - interface: `.agents/skills/roundfix/SKILL.md` - interface: `skills/roundfix/SKILL.md` +- interface: `internal/baseline/assets/setups/typescript-bun.json` +- interface: `internal/baseline/testdata/catalog.normalized.json` +- interface: `internal/baseline/testdata/catalog.digest` ## Verification - `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` — expected: no output and exit zero. -- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "docs/specs/0036-doctor-skill-readiness/task_03.md") {print; bad=1}} END {exit bad}'` - — expected: no changed path outside the authorized pair and this Task file. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "internal/baseline/assets/setups/typescript-bun.json" && path != "internal/baseline/testdata/catalog.normalized.json" && path != "internal/baseline/testdata/catalog.digest" && path != "docs/specs/0036-doctor-skill-readiness/task_03.md") {print; bad=1}} END {exit bad}'` + — expected: no changed path outside the authorized Skill pair, derived + Baseline artifacts, and this Task file. - `rtk make skills-sync-check` — expected: every canonical/generated owned skill pair has no drift. - `rtk go run -buildvcs=false ./cmd/roundfix skills check` — expected: every From 0a0f5bdc07ef962422aaf27f4a884ab5e382077b Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 15:04:58 -0300 Subject: [PATCH 20/48] feat: diagnose Repository Skill Set readiness Roundfix-Spec: 0036-doctor-skill-readiness Roundfix-Task: task_01 --- .../0036-doctor-skill-readiness/task_01.md | 45 ++++++++++++++++--- internal/cli/cli.go | 12 ++--- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/docs/specs/0036-doctor-skill-readiness/task_01.md b/docs/specs/0036-doctor-skill-readiness/task_01.md index 001916b..a10d738 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_01.md +++ b/docs/specs/0036-doctor-skill-readiness/task_01.md @@ -1,7 +1,7 @@ --- task: task_01 spec: 0036-doctor-skill-readiness -status: pending +status: completed type: backend complexity: high --- @@ -55,7 +55,7 @@ filesystem mutation. symlink, mixed-ownership, and ordering cases. - [x] Add exact-output Doctor tests for success, each ownership group, mixed remediation, checker errors, exit behavior, and no mutation. -- [ ] Update Doctor command help for the appended Repository Skill Set check. +- [x] Update Doctor command help for the appended Repository Skill Set check. ## Acceptance Criteria @@ -74,7 +74,7 @@ filesystem mutation. remediation commands exactly once in owned-then-external order. - [x] Doctor runs every existing check even when skill readiness fails and changes no repository, user-config, Run, or skill path. -- [ ] Doctor help continues to name both Agent Selection Profiles and the +- [x] Doctor help continues to name both Agent Selection Profiles and the appended Repository Skill Set readiness check. ## Context @@ -199,6 +199,39 @@ running the declared Verification commands verbatim. ### Follow-ups -Restore the pre-existing `Agent Selection Profiles` wording required by -`TestProfilesDocumentationContractMatchesPublicGuidance` in its owning CLI -documentation slice; no `internal/cli` file changed in this rework. +Rework Trigger 2 resolved the prior follow-up. Doctor help now keeps +`Agent Selection Profiles` contiguous while retaining the additive +`Repository Skill Set` and `skills:` wording. The existing documentation +contract reproduced the regression before the production help change and +passed afterward, so no test expectation changed. + +### Rework Trigger 2 verification + +- `rtk env GOCACHE=/tmp/roundfix-task01-red-cache go test ./internal/cli -run 'TestProfilesDocumentationContractMatchesPublicGuidance' -count=1` + — failed before the fix because Doctor help did not contain the contiguous + public term `Agent Selection Profiles`. +- `rtk env GOCACHE=/tmp/roundfix-task01-skills-focused-cache go test ./skills -run 'Test(CheckRepository|SkillFolderHash)' -count=1` + — passed (`ok roundfix/skills`). +- `rtk env GOCACHE=/tmp/roundfix-task01-cli-focused-cache go test ./internal/cli -run 'Test(RunDoctor|ProfilesDocumentationContractMatchesPublicGuidance)' -count=1` + — passed (`ok roundfix/internal/cli`), including the restored Doctor help + terminology and the additive Repository Skill Set wording. +- `rtk env GOCACHE=/tmp/roundfix-task01-race-focused-cache go test -race ./skills ./internal/cli -run 'Test(CheckRepository|RunDoctor)' -count=1` + — passed for both packages with the race detector. +- `rtk env GOCACHE=/tmp/roundfix-task01-skills-full-cache go test ./skills -count=1` + — passed (`ok roundfix/skills`), including the real repository baseline. + +The sandbox blocks Go's default user cache, so focused verification used +temporary `GOCACHE` directories. The Daemon remains responsible for running +the declared Verification commands verbatim. + +### Rework Trigger 2 acceptance evidence + +- AC 1–7: the focused repository, Doctor, race, and complete skills-suite + commands passed after the help change, preserving the previously recorded + readiness, ordering, failure, remediation, and no-mutation contracts. +- AC 8: `TestProfilesDocumentationContractMatchesPublicGuidance` first failed + on the missing contiguous `Agent Selection Profiles` phrase, then passed + with `Agent Selection Profiles`, `Repository Skill Set`, and `skills:` all + present in Doctor help. + +No follow-up work remains in this Task slice. diff --git a/internal/cli/cli.go b/internal/cli/cli.go index faddae1..882014c 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -3778,12 +3778,12 @@ Options: roundfix doctor Diagnoses this machine's readiness for Runs. Checks Node.js, the minimum -supported acpx version, the effective adapter, required Agent Selection -Profiles, the Repository Skill Set, and codex runtime hygiene. The aggregate -profiles: line exact-proves every distinct tuple. The skills: line compares -Roundfix-owned artifacts with the running binary and external artifacts with -skills-lock.json. Each failure reports its next action. Doctor is offline, -read-only, and mutates nothing. +supported acpx version, the effective adapter, and required +Agent Selection Profiles, the Repository Skill Set, and codex runtime hygiene. +The aggregate profiles: line exact-proves every distinct tuple. The skills: +line compares Roundfix-owned artifacts with the running binary and external +artifacts with skills-lock.json. Each failure reports its next action. +Doctor is offline, read-only, and mutates nothing. ` case "gc": return `Usage: From 9c1e00f6dae9959ddaef8ca62d1f0ea2c2b597c3 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 15:42:33 -0300 Subject: [PATCH 21/48] docs: authorize parity corpus refresh --- docs/specs/0036-doctor-skill-readiness/_prd.md | 7 +++++-- .../0036-doctor-skill-readiness/_techspec.md | 7 +++++-- .../0036-doctor-skill-readiness/task_03.md | 17 +++++++++++------ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/specs/0036-doctor-skill-readiness/_prd.md b/docs/specs/0036-doctor-skill-readiness/_prd.md index 1cd454f..469f75b 100644 --- a/docs/specs/0036-doctor-skill-readiness/_prd.md +++ b/docs/specs/0036-doctor-skill-readiness/_prd.md @@ -42,8 +42,11 @@ Repository Skill Set result and must not recreate or bypass profile proof. `skills/roundfix/SKILL.md`, plus the mechanically derived Baseline artifacts `internal/baseline/assets/setups/typescript-bun.json`, `internal/baseline/testdata/catalog.normalized.json`, and - `internal/baseline/testdata/catalog.digest`. No other protected tooling - mutation is authorized. Source: + `internal/baseline/testdata/catalog.digest`, and the mechanically derived + parity artifacts + `internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json` and + `internal/baseline/testdata/parity-corpus/v1/manifest.json`. No other + protected tooling mutation is authorized. Source: `docs/agents/agent-instructions.md`. ## Goals diff --git a/docs/specs/0036-doctor-skill-readiness/_techspec.md b/docs/specs/0036-doctor-skill-readiness/_techspec.md index 73194d7..cc06527 100644 --- a/docs/specs/0036-doctor-skill-readiness/_techspec.md +++ b/docs/specs/0036-doctor-skill-readiness/_techspec.md @@ -44,8 +44,11 @@ boundary and does not preserve or recreate the legacy single-model probe. `skills/roundfix/SKILL.md`, plus the mechanically derived Baseline artifacts `internal/baseline/assets/setups/typescript-bun.json`, `internal/baseline/testdata/catalog.normalized.json`, and - `internal/baseline/testdata/catalog.digest`. No other protected tooling - mutation is authorized. Source: + `internal/baseline/testdata/catalog.digest`, and the mechanically derived + parity artifacts + `internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json` and + `internal/baseline/testdata/parity-corpus/v1/manifest.json`. No other + protected tooling mutation is authorized. Source: `docs/agents/agent-instructions.md`. ## System Architecture diff --git a/docs/specs/0036-doctor-skill-readiness/task_03.md b/docs/specs/0036-doctor-skill-readiness/task_03.md index 0fa4bb4..9179f40 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_03.md +++ b/docs/specs/0036-doctor-skill-readiness/task_03.md @@ -26,11 +26,14 @@ Task file. `.agents/skills/roundfix/SKILL.md`, `skills/roundfix/SKILL.md`, `internal/baseline/assets/setups/typescript-bun.json`, `internal/baseline/testdata/catalog.normalized.json`, - `internal/baseline/testdata/catalog.digest`, and this `task_03.md` file. + `internal/baseline/testdata/catalog.digest`, + `internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json`, + `internal/baseline/testdata/parity-corpus/v1/manifest.json`, and this + `task_03.md` file. 4. MUST NOT run `make skills-sync`, because that mutation target rewrites every owned skill directory; `make skills-sync-check` is the read-only sync gate. -5. MUST update the three authorized Baseline artifacts only as mechanically - derived consequences of the canonical Roundfix Skill bytes. +5. MUST update the five authorized Baseline and parity artifacts only as + mechanically derived consequences of the canonical Roundfix Skill bytes. 6. MUST leave every external skill, lock file, recommendation file, source code file, and user-documentation file unchanged. @@ -47,7 +50,7 @@ Task file. performs an update. - [ ] The canonical and generated Roundfix `SKILL.md` files are byte-identical. - [ ] Git changed-file evidence for this Task contains only the two authorized - Skill files, three derived Baseline artifacts, and `task_03.md`. + Skill files, five derived Baseline and parity artifacts, and `task_03.md`. - [ ] No upstream-managed or other Roundfix-owned skill changes. - [ ] The shipped skill check and complete repository gate pass. @@ -61,14 +64,16 @@ Task file. - interface: `internal/baseline/assets/setups/typescript-bun.json` - interface: `internal/baseline/testdata/catalog.normalized.json` - interface: `internal/baseline/testdata/catalog.digest` +- interface: `internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json` +- interface: `internal/baseline/testdata/parity-corpus/v1/manifest.json` ## Verification - `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` — expected: no output and exit zero. -- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "internal/baseline/assets/setups/typescript-bun.json" && path != "internal/baseline/testdata/catalog.normalized.json" && path != "internal/baseline/testdata/catalog.digest" && path != "docs/specs/0036-doctor-skill-readiness/task_03.md") {print; bad=1}} END {exit bad}'` +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "internal/baseline/assets/setups/typescript-bun.json" && path != "internal/baseline/testdata/catalog.normalized.json" && path != "internal/baseline/testdata/catalog.digest" && path != "internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json" && path != "internal/baseline/testdata/parity-corpus/v1/manifest.json" && path != "docs/specs/0036-doctor-skill-readiness/task_03.md") {print; bad=1}} END {exit bad}'` — expected: no changed path outside the authorized Skill pair, derived - Baseline artifacts, and this Task file. + Baseline and parity artifacts, and this Task file. - `rtk make skills-sync-check` — expected: every canonical/generated owned skill pair has no drift. - `rtk go run -buildvcs=false ./cmd/roundfix skills check` — expected: every From 6e5618dba5059666c891f9078806631d30502d5b Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 16:02:11 -0300 Subject: [PATCH 22/48] docs: align the protected Roundfix Skill pair Roundfix-Spec: 0036-doctor-skill-readiness Roundfix-Task: task_03 --- .agents/skills/roundfix/SKILL.md | 49 ++++++++++----- .../0036-doctor-skill-readiness/task_03.md | 62 ++++++++++++++++--- .../assets/setups/typescript-bun.json | 4 +- internal/baseline/testdata/catalog.digest | 2 +- .../baseline/testdata/catalog.normalized.json | 2 +- .../parity-corpus/v1/fixtures/asset-sync.json | 6 +- .../testdata/parity-corpus/v1/manifest.json | 2 +- skills/roundfix/SKILL.md | 49 ++++++++++----- 8 files changed, 124 insertions(+), 52 deletions(-) diff --git a/.agents/skills/roundfix/SKILL.md b/.agents/skills/roundfix/SKILL.md index 25b9456..a1e1560 100644 --- a/.agents/skills/roundfix/SKILL.md +++ b/.agents/skills/roundfix/SKILL.md @@ -33,15 +33,28 @@ Task Worktrees, and QA uses its own Agent Session after Tasks settle. Use the Doctor Command, `roundfix doctor`, to diagnose Run readiness without installing dependencies, writing config, or changing files. Doctor runs the shared Node.js, minimum-supported acpx, effective adapter, required Agent Selection -Profiles, and codex runtime hygiene checks and prints one line per check with -status `ok`, `failed`, or `skipped`. Adapter Readiness requires the effective -Codex command to prove official `@agentclientprotocol/codex-acp` lineage at -version `1.1.4` or newer; executable presence and a matching name are not -proof. The `profiles:` line is the selection authority: it exact-proves every -distinct Preferred Selection and fallback through disposable ACP Sessions and -reports affected category references plus one deterministic next action. -Doctor has no separate legacy `agent:` or `model:` authority. Failed checks -include `next: ` when Roundfix knows the remediation. +Profiles, Repository Skill Set, and codex runtime hygiene checks and prints one +line per check with status `ok`, `failed`, or `skipped`. Adapter Readiness +requires the effective Codex command to prove official +`@agentclientprotocol/codex-acp` lineage at version `1.1.4` or newer; +executable presence and a matching name are not proof. The `profiles:` line is +the selection authority: it exact-proves every distinct Preferred Selection +and fallback through disposable ACP Sessions and reports affected category +references plus one deterministic next action. Doctor has no separate legacy +`agent:` or `model:` authority. Failed checks include `next: ` when +Roundfix knows the remediation. + +The blocking `skills:` line runs after, and independently from, `profiles:`. +The running binary's embedded bundle is authoritative for Roundfix-owned +skills; each required external skill must match its `computedHash` in +`skills-lock.json`. Surface a failed `skills:` line and its printed `next:` +remediation before work continues. Owned failures print +`roundfix skills install --target project`; external failures print +`bunx skills experimental_install && bunx skills update -p -y`; mixed failures +print both commands in that order. Doctor is diagnosis-only: it never runs +these commands, accesses the network, installs or updates skills, or writes +repository state. Apply remediation only after explicit workflow +authorization, then rerun Doctor. On macOS, the codex hygiene check resolves `CODEX_PATH` first and then `codex` on `PATH`, inspects the `com.apple.quarantine` attribute (the real XProtect trigger), and verifies the binary's code signature (not `spctl --assess`, which @@ -112,20 +125,22 @@ unauthorized target. A rejected Sol/high proof never becomes an offer to use model-managed reasoning. Use `roundfix doctor` when you only need a read-only readiness report. It runs -the Node.js, minimum-supported acpx, effective adapter, required Agent Selection Profile, -and codex runtime hygiene checks and exits nonzero if any check fails. Adapter -failures name the effective command, package classification, and official -install action. Profile failure names the exact runtime/model/reasoning tuple, -every affected category, bounded adapter evidence, and the next -`roundfix profiles configure` or `roundfix profiles validate` action. A -rejected explicit `high` does not recommend model-managed reasoning. The -command has no flags and mutates nothing. +the Node.js, minimum-supported acpx, effective adapter, required Agent Selection +Profiles, Repository Skill Set, and codex runtime hygiene checks and exits +nonzero if any check fails. Adapter failures name the effective command, +package classification, and official install action. Profile failure names the +exact runtime/model/reasoning tuple, every affected category, bounded adapter +evidence, and the next `roundfix profiles configure` or +`roundfix profiles validate` action. A rejected explicit `high` does not +recommend model-managed reasoning. The command has no flags and mutates +nothing. ```text node: ok acpx: ok adapter: ok (npx -y @agentclientprotocol/codex-acp@1.1.4; package=@agentclientprotocol/codex-acp; version=1.1.4) profiles: ok (3 distinct tuples; 10 category references) +skills: ok (39 required: 14 Roundfix-owned, 25 external) codex: ok ``` diff --git a/docs/specs/0036-doctor-skill-readiness/task_03.md b/docs/specs/0036-doctor-skill-readiness/task_03.md index 9179f40..8ce4953 100644 --- a/docs/specs/0036-doctor-skill-readiness/task_03.md +++ b/docs/specs/0036-doctor-skill-readiness/task_03.md @@ -1,7 +1,7 @@ --- task: task_03 spec: 0036-doctor-skill-readiness -status: pending +status: completed type: docs complexity: low --- @@ -39,20 +39,20 @@ Task file. ## Subtasks -- [ ] Update the canonical Roundfix Skill Doctor contract. -- [ ] Apply the identical change to the generated Roundfix Skill copy. -- [ ] Verify exact changed-file scope and byte identity. +- [x] Update the canonical Roundfix Skill Doctor contract. +- [x] Apply the identical change to the generated Roundfix Skill copy. +- [x] Verify exact changed-file scope and byte identity. ## Acceptance Criteria -- [ ] The Roundfix Skill tells an Agent to surface a failed skill check and its +- [x] The Roundfix Skill tells an Agent to surface a failed skill check and its printed remediation before work continues, without claiming Doctor performs an update. -- [ ] The canonical and generated Roundfix `SKILL.md` files are byte-identical. -- [ ] Git changed-file evidence for this Task contains only the two authorized +- [x] The canonical and generated Roundfix `SKILL.md` files are byte-identical. +- [x] Git changed-file evidence for this Task contains only the two authorized Skill files, five derived Baseline and parity artifacts, and `task_03.md`. -- [ ] No upstream-managed or other Roundfix-owned skill changes. -- [ ] The shipped skill check and complete repository gate pass. +- [x] No upstream-managed or other Roundfix-owned skill changes. +- [x] The shipped skill check and complete repository gate pass. ## Context @@ -92,4 +92,46 @@ Task file. ## Result -Pending. +Completed the protected Roundfix Skill update and refreshed only the Baseline +and parity identities derived from its new canonical bytes. + +### Changes + +- Added the blocking Repository Skill Set result to the Doctor instructions, + including owned, external, and mixed remediation. +- Required an Agent to surface the failed `skills:` line and printed `next:` + action before work continues, while keeping Doctor diagnosis-only and + requiring explicit workflow authorization before remediation. +- Kept the canonical and generated Skill copies byte-identical and refreshed + the TypeScript/Bun setup snapshot, catalog identity, asset-sync fixture, and + parity manifest from the new Roundfix Skill digest. + +### Verification + +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` — passed + with no output. +- `rtk make skills-sync-check` — passed the four Go-owned synchronization + guards. +- `rtk go run -buildvcs=false ./cmd/roundfix skills check` — passed for all 14 + shipped Roundfix-owned skills. +- `rtk go test ./internal/baseline -run 'Test(CatalogCompatibility|AssetsSyncCompatibilityMatchesMaintainedPythonContract|BaselineCompatibilityCorpus)' -count=1` + — passed the refreshed catalog and parity contracts. +- `rtk git diff --check` — passed with no whitespace errors. +- `rtk make verify` — passed 2,394 tests across 22 packages, the four skill + synchronization guards, shipped Skill validation, and the CLI build. + +### Acceptance evidence + +- The Skill names both ownership-specific commands, keeps owned remediation + before external remediation, and states that Doctor performs no install, + update, network access, or repository write. +- Fresh `cmp` and `skills-sync-check` results prove canonical/generated byte + identity and no drift in any other Roundfix-owned Skill. +- Git postflight found only the authorized Skill pair, five derived Baseline + and parity artifacts, and this Task file; no external skill, lock, + recommendation, source-code, or user-documentation path changed. +- The shipped Skill check and full repository gate both exited zero. + +### Follow-ups + +None. diff --git a/internal/baseline/assets/setups/typescript-bun.json b/internal/baseline/assets/setups/typescript-bun.json index 2a05496..526a706 100644 --- a/internal/baseline/assets/setups/typescript-bun.json +++ b/internal/baseline/assets/setups/typescript-bun.json @@ -8,7 +8,7 @@ "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "setups/typescript-bun.txt" }, - "digest": "b23d5f4dddee60153593ff4f7ed927c93ca30f956a4725d9eb413cd507f0c6b2", + "digest": "946c3fe0b0ee331b062199bbba80cb51427b7256411da9e58283f51e451bb362", "skills": [ { "name": "find-rules", @@ -1027,7 +1027,7 @@ "type": "repo", "name": "roundfix" }, - "contentDigest": "2cc373d12f0a802e7c1c2961838d7cc23ed9859f6cf3f7f76f3852377cff1a3d" + "contentDigest": "d5269bac642a8ef4c1c8439eff308594e36ad786fe14d9f3047a8e731998f59f" }, { "name": "the-fool", diff --git a/internal/baseline/testdata/catalog.digest b/internal/baseline/testdata/catalog.digest index 963bc81..b416b85 100644 --- a/internal/baseline/testdata/catalog.digest +++ b/internal/baseline/testdata/catalog.digest @@ -1 +1 @@ -sha256:53a2d70d8569688d2b6b5353fc86ee6099ef412b11fd5ee0d04050134be00cdf +sha256:1dfe9dc88e84d5a2ace402578a213460b1dca5f035caff587b1ee229b13de5c4 diff --git a/internal/baseline/testdata/catalog.normalized.json b/internal/baseline/testdata/catalog.normalized.json index 0473513..dd2f5b1 100644 --- a/internal/baseline/testdata/catalog.normalized.json +++ b/internal/baseline/testdata/catalog.normalized.json @@ -1 +1 @@ -{"schemaVersion":"roundfix/baseline-catalog/v1","files":[{"path":"contract-v1.json","bytes":13525,"digest":"sha256:0c32b7ac1b6dd1a2a04d830b300d592b7b195aec7bb32ff05575e8d19f154497"},{"path":"coverage.json","bytes":4660,"digest":"sha256:9faa73bf86fad383570b0098a9aa26185ea8d689f9866038ab37b8a34b9a01dc"},{"path":"decisions.json","bytes":8438,"digest":"sha256:58acbb05cf46057923e8844b28e2a97efa898d6200ae9e0773971b6287b05651"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/AGENTS.md","bytes":2911,"digest":"sha256:1876cc64ced89e60cfb0188eaff71d8b588c80d1149740ba368d1afc969e1d8f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/agent-instructions.md","bytes":4182,"digest":"sha256:eb769f99afa32a901723e1962a06fca09d856c8d37284b36c9aed071bd6e4f86"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/autonomous-work.md","bytes":789,"digest":"sha256:76065b3f1b452f647765d01bfb802ca3a0757d7212ba75b8e55d1d6242b0c977"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/backend.md","bytes":1083,"digest":"sha256:b8459f66c5fec1d424768fdd2b0b2f4ec70b9038208c1d5a58086720586195fd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/docs-layout.md","bytes":3546,"digest":"sha256:a188cfaae89ed195ca31460af3ec5d0dfad0bc1241e1958950ed4e0035de86f6"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/domain.md","bytes":614,"digest":"sha256:089a408493f450ef5713f3f62b20b51323b6d08617e6e43a12e38d794676ae82"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/external-triage.md","bytes":385,"digest":"sha256:4e6b2cfafd491161e7666b304ce0d2d6b19ad705613a6471999c84fda1d63e6a"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/frontend.md","bytes":825,"digest":"sha256:3cd5cba8160ea9f6419ffe82920f7641d09eb7126a78ab50606dfe045732cc02"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/issue-tracker.md","bytes":353,"digest":"sha256:f2139d498fb79358a240d8c99ab1718764217cf28aec0fb024beadfa2b0231cc"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/monorepo.md","bytes":371,"digest":"sha256:47cf6a0ac72c2bcacfc69e5566b1cdcc2102e552b1d91592e5cb1033d30be9e3"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/secondbrain.md","bytes":1589,"digest":"sha256:fab2b2896ad03e0436414b83e5cd80e7fdc07b96d9231b798194b36314a8e7a7"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/skill-dispatch.md","bytes":14414,"digest":"sha256:d424a3e360031e39913bf385f87012e6d1ab79a84024701092859640d2fd05dd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/spec-routing.md","bytes":2269,"digest":"sha256:ce1b67f65ce8ad8a5dc1505e2201833d0e477ff91a6161c395db1188ae83ce9f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/typescript-bun.md","bytes":1378,"digest":"sha256:ec114298fee6ef89bbf0423a6b8a467630296264fc49e989d89af3301093b96e"},{"path":"lock-hash-compatibility-v1.json","bytes":366,"digest":"sha256:6a83f5c12ea6fe0a0aa2e2c710ef44b4cc922d2965240376bcd0f12254d765d0"},{"path":"modules/autonomous-work.json","bytes":2400,"digest":"sha256:61cb4100b7a9378f4d582964d6ac7fac52e005f5b27d0e5c359bb80a9fce7f85"},{"path":"modules/backend.json","bytes":5655,"digest":"sha256:c5b3d012e5ee3ec4dabcb5ea679fbc89932eb9760215c0c91d0bc8bb45a4f806"},{"path":"modules/bun.json","bytes":2557,"digest":"sha256:ca869344629ff4ede015e761c87afb01c20c371c05260cef56f36a7376e53777"},{"path":"modules/cli-surface.json","bytes":1721,"digest":"sha256:04e84d41b65452dc06d43ce43e2aedec3cc05d100d18aacc150f808ae80f7d32"},{"path":"modules/context-workflow.json","bytes":11142,"digest":"sha256:fb61822b2ce29891d6a06963b225a4da6935859df83ae1aa3fde9fa3bf0b0d30"},{"path":"modules/core.json","bytes":18107,"digest":"sha256:69dd99c698a50f651b0bd0b3021b2abbfba645955fe534735e97bff4ef0f1098"},{"path":"modules/external-triage.json","bytes":1159,"digest":"sha256:5ea90217b042667a3e00211941d4cb1bafb24294187635bfa277765c05894366"},{"path":"modules/frontend.json","bytes":8713,"digest":"sha256:6a701d874b2217bb7f59cddfdd0d0d8a8bb41c6452fc63b67251e5beddc25f7d"},{"path":"modules/go.json","bytes":3322,"digest":"sha256:d9877c86f59206b3bf6bdc1bf73fdfd18fdcc496bc67770413963587276b0533"},{"path":"modules/monorepo.json","bytes":1385,"digest":"sha256:e6485fd6dacb765a866dd9ff79f057d2ebc85cb57c603774fe7ca453cf50f917"},{"path":"modules/repository-extension.json","bytes":1094,"digest":"sha256:a2adeac11000706839944c2ecca225e524da6f38534e54efc9cb0a26f5413f47"},{"path":"modules/rust.json","bytes":2328,"digest":"sha256:46537524b19a5eb1b61dd07d7215b2ea607c1b78a481a21eb13926d40ecdc0b2"},{"path":"modules/secondbrain.json","bytes":4333,"digest":"sha256:3ce59d0ddfcd0e2e5de34d9a4541a27bc62c6e72800da1cd02353ba2f2aa2024"},{"path":"modules/spec-workflow.json","bytes":6441,"digest":"sha256:574f08d79b15e4d9a6a38c3a9a85636f5a208aa9e43051383815056fc03c6e53"},{"path":"modules/tui-surface.json","bytes":1658,"digest":"sha256:06e18ef65d3a7184aaa3a80a21d2279b3376ab77f07bb8b47d23497b0fb6fa62"},{"path":"modules/typescript.json","bytes":8526,"digest":"sha256:7a2a82f7efb28df7dbc343f1b5a7b0ab176e8a74105eb6077c7b5cf6648bdbc6"},{"path":"profiles/go-cli-tui.json","bytes":1709,"digest":"sha256:231220ac521a8298d86440f6e262d61b9c2fa249459dd9206973d8db6e6941bb"},{"path":"profiles/rust-cli.json","bytes":1627,"digest":"sha256:f2338dbf8d0085a6150293c157b6986714de122b2ebd732e33a09f0614686c81"},{"path":"profiles/standard-typescript-monorepo.json","bytes":10907,"digest":"sha256:803f62fb451f9fda23a58b7bfe5f5367a45d8d44bb63c3f96e33b259393cc5ba"},{"path":"retention/transition.legacy-typescript-bun-to-portable-v3.json","bytes":12425,"digest":"sha256:3b166a42eb1f9f23202afc48399eb7aa9b0e8563d9c40095ad6bf710a5d1c599"},{"path":"retention/transition.managed-v2-to-portable-v3.json","bytes":11185,"digest":"sha256:8a0e9dd06aa6a5988c010f6c1286c39002c65e5aae6762e9af6956424524c3e9"},{"path":"setups/go-cli.json","bytes":14203,"digest":"sha256:28e2ad53684c2c097affa4cf70c32a59265435dd105cdcfa57314bd18a6af46f"},{"path":"setups/rust-cli.json","bytes":12476,"digest":"sha256:82cdba03b2c77c9a0c4a6ad737e7de5e90a3fcf6827c18242793b8032766d9c0"},{"path":"setups/typescript-bun.json","bytes":40649,"digest":"sha256:573437aeb58bf8ef7cdd2c37d250b6ab3c66d93d12a69e8d388b4644862ffb3f"},{"path":"skill-activations.json","bytes":3330,"digest":"sha256:bbf1978828a2d383c9cee675a612c17281224e0a1ce5df4b09ba13af4ccd3667"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/accounting.json","bytes":17193,"digest":"sha256:69e39f9df0eb54d08ecb739dc3dfc9dd80c0cf76016aca3d6a0566e48cfaa1f5"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/baseline.json","bytes":498,"digest":"sha256:dca6a12ae24a77fc9104dfcccb507c50dee0e5257e0bf0bd9d86c0b04ce7ffe0"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/AGENTS.md","bytes":583,"digest":"sha256:3b6b36fd045ecb683aec29e0ceac52b3704604815d8035297bfeb8438602494e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/agent-instructions.md","bytes":8996,"digest":"sha256:fe584b8901ae2521dcba7014ca11f3557109a4d91d7c56a21852f027a4bf205e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/autonomous-work.md","bytes":1548,"digest":"sha256:3deff187dba70156ca834529741c4be7df84c5a66f8462ba01594fee35d40336"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/backend.md","bytes":2197,"digest":"sha256:c553c12e3cb13b0435a219fd52ac735bf4f9f3aad05fdb943fd6922cef0a97ac"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/docs-layout.md","bytes":7671,"digest":"sha256:8a78a3543a0a52ac93ef83ab36f953e813be068e96957553483c928cbad439bd"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/domain.md","bytes":947,"digest":"sha256:54c79c324d20b412c07c634f490ad18eeace0562caffe5507b3c83cebf9aaf20"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/external-triage.md","bytes":335,"digest":"sha256:0ec2089110c6a673f305e86f436538c7c07aeef46d29a4e68674defb8778e2cc"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/frontend.md","bytes":1934,"digest":"sha256:4a3609c27bf100aaf0d7de934ad43c0f6a561a15600517422a325fcc6120bd14"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/issue-tracker.md","bytes":890,"digest":"sha256:d86ab246d701a54b8d61c5dcdc1d29fe3281115c028e8694f10574ce99e537c7"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/monorepo.md","bytes":366,"digest":"sha256:c59091add143e67b3773df78d95cc4c8f11d43909655ef1c09dd64c0cc903b3d"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/secondbrain.md","bytes":3312,"digest":"sha256:4b58f3ed96f654d8b99ccc9a03718505f4c43a6170eceef47e26484b5f95bffb"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/skill-dispatch.md","bytes":374,"digest":"sha256:90bbf02e35bba9c707e4713eef4de9a6db36ba3fa93393531287aaf94fce8f8b"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/spec-routing.md","bytes":3094,"digest":"sha256:dfc867401e547d4370f20c578a22e6b82cc80d7b677072f31bc11cdba4851085"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/typescript-bun.md","bytes":2561,"digest":"sha256:7295fe2e544af37e6d6777537fa7ee512b78f3da932d736cc7eabc8d190a30e8"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/manifest.json","bytes":37175,"digest":"sha256:e9a9e91f8dbbea8dc416a03fe813b9c370a2e012118f548b18f1c6a13b6b7662"},{"path":"source-baselines/index.json","bytes":5266,"digest":"sha256:ad636b4cbaf57e27c81093aee9963bcd2b276df7ac243f04b423bf989db314a8"},{"path":"templates/extensions/specific-repository.md","bytes":38,"digest":"sha256:f8aad350dfecd91923c12fe215c1e21dd6741635e44869110391ba9c5c89d137"},{"path":"templates/guides/agent-instructions.md","bytes":260,"digest":"sha256:91f0a4b1d664cf6ab261499b3f7cb093bd2337d6567d07599faebd8fe58dc8d3"},{"path":"templates/guides/autonomous-work.md","bytes":194,"digest":"sha256:384fdebdb4ffd5d6c1b7b197cc02e3f9d3e7327acd7151dc7ce29c0e53e99486"},{"path":"templates/guides/backend.md","bytes":201,"digest":"sha256:0bffbf1e74c9b8c506ec1c612aac0dfaedcfb6f1b7aafc8d1a2d6029cd1170ea"},{"path":"templates/guides/bun.md","bytes":26,"digest":"sha256:3a0640850dc84c8dd6c6b35f10864ae6d3d9c153147699b7bfced6332315633d"},{"path":"templates/guides/cli-surface.md","bytes":34,"digest":"sha256:7cdd6ecc6575c4ef165858ced6409278430a89e3f67baa4281151dc13e2742bc"},{"path":"templates/guides/docs-layout.md","bytes":34,"digest":"sha256:51e9033d80e6742321b47b19c85034c9b4d15b36b9c84230512e3a9a4ef90628"},{"path":"templates/guides/domain-multi-context.md","bytes":112,"digest":"sha256:56b7000576757f37da275123ab75ff5f84d2e26bd88c9f67034a24f3d4883a41"},{"path":"templates/guides/domain-single-context.md","bytes":104,"digest":"sha256:f75666666408761a67d23855f5f78e4526328404343f09a036f8987895c69c0d"},{"path":"templates/guides/domain.md","bytes":59,"digest":"sha256:5c4195e0348b6e5a57679fc833e58b09f4b08080b1cec90a946d0b666310739e"},{"path":"templates/guides/external-triage.md","bytes":38,"digest":"sha256:f26c4164ff69bf44263d9993e4209a9305b8979f228f6f519f139e83f9b51ae9"},{"path":"templates/guides/frontend.md","bytes":265,"digest":"sha256:3243626ac7b55130ac198ba32a350966a26f6a9801a4a4fee259817b2f7eb5f9"},{"path":"templates/guides/go.md","bytes":25,"digest":"sha256:7f597711bdab9fe85ecda9f5895687e436009446ee8416ed96f7bc27fe7b00fd"},{"path":"templates/guides/issue-tracker.md","bytes":36,"digest":"sha256:48840599c91f2f3cf2fd79700f41e54bc3edbb4fa19e4861247e19bac0077fb1"},{"path":"templates/guides/monorepo.md","bytes":31,"digest":"sha256:e1866f1633f3bbc5b230b5c5290b71ae47110dce4d4344c264a9eb325a477bca"},{"path":"templates/guides/rust.md","bytes":27,"digest":"sha256:e0e693135266e96cabec918b14f0cf51c7bf1a126d900001cc418f982f8bbec2"},{"path":"templates/guides/secondbrain.md","bytes":34,"digest":"sha256:a59dc0ae149d4bb3bdd633f79ba91fd17e6d3d3d930c346e5757b5f85ab69002"},{"path":"templates/guides/skill-dispatch.md","bytes":253,"digest":"sha256:0f054b846da5b17409dde049b3b182489e6c24e84734d4a5ffc47a9f504e8a7c"},{"path":"templates/guides/spec-docs-layout.md","bytes":39,"digest":"sha256:6d9ddb174c4f69cb1f9abe428a6be677e9ea8dbecc19ae0fa2f53fdc3749de40"},{"path":"templates/guides/spec-routing.md","bytes":35,"digest":"sha256:cc578e73968c15bdcaad35270454e91462b4413d937cfd984404ef22d765482c"},{"path":"templates/guides/tui-surface.md","bytes":34,"digest":"sha256:bbc422cb32df905d526a291cbfd5b81fd86f0d405f5f650cc5d8eb0a864c4eb2"},{"path":"templates/guides/typescript-bun.md","bytes":41,"digest":"sha256:ee14609660ebe1da17eff0d12c3f42857cba4c92be0f723346d17e3d64bb6574"},{"path":"templates/index.json","bytes":7125,"digest":"sha256:272cbfd4fa2ea380acfc76ce4d75eaf2ae026b7501c34ee48a8645644eb72f4c"},{"path":"templates/root/autonomous-work.md","bytes":102,"digest":"sha256:abd0be4c74295597be5dca6a307e41d6bdcfa5113370f5bbc30d30725c927e35"},{"path":"templates/root/backend.md","bytes":76,"digest":"sha256:f65a96aaa0b79d40d209df159c30bd6ac791e36c415b8ba2dc52a1032c5785c3"},{"path":"templates/root/bun.md","bytes":89,"digest":"sha256:48a4bcd7c9b86d8f3be6965ff8b7b33aad77148aa598d49ed8efa74acd3c8e20"},{"path":"templates/root/cli-surface.md","bytes":79,"digest":"sha256:ec022b0b1767d88ebc849f2ee7d8a2d266a36d419f8084b9cce8599d0619a125"},{"path":"templates/root/context-workflow.md","bytes":129,"digest":"sha256:52c53e99fed6c5d59388e58f7854c96f8130da553d80bb90f04935f67eea45c5"},{"path":"templates/root/core.md","bytes":213,"digest":"sha256:ffa6f25825e1d5c5bf52373022d7035b004fd78dd55f99a187d89c4b78ddeabb"},{"path":"templates/root/external-triage.md","bytes":91,"digest":"sha256:5ea18c6c7aea8f03d62af0b824b947ddecbaf861fbf9b81df2e8fb2b2c0e2c54"},{"path":"templates/root/frontend.md","bytes":99,"digest":"sha256:cddad0b38dff9cbeae571dc8514f5d2770e528725f8931272389bd8a675e08bb"},{"path":"templates/root/go.md","bytes":67,"digest":"sha256:aa3eafd5ebcec7aa64ae7fddf1c5b954512e831121934760f615c12ec52f6527"},{"path":"templates/root/monorepo.md","bytes":74,"digest":"sha256:76951b431357333d5b3bbbe718529797834c5de14ee6cd886bce175b5ef892e9"},{"path":"templates/root/repository-extension.md","bytes":103,"digest":"sha256:f24b6015a05eab4cdbb830eb485f9958b659d9a99d46b53d8538a813f6705fe1"},{"path":"templates/root/rust.md","bytes":73,"digest":"sha256:40c4f50d3a020406816612b9f9e0c6cbd545626401e447d4963ba2d30d8e40e3"},{"path":"templates/root/secondbrain.md","bytes":87,"digest":"sha256:dbb11588c2dbca5fd803eccc889586cac75824e9083c570bd1cad34fda901857"},{"path":"templates/root/spec-workflow.md","bytes":125,"digest":"sha256:c292c5b2b98facb56ef0f6da0a61f38544377ddb6339c63d5287ca7dec4f2360"},{"path":"templates/root/tui-surface.md","bytes":75,"digest":"sha256:5657d3abaddf480a535d00a8bfe94bcd3e0b6230aaaa3aac2f9ce89fdc7e20b0"},{"path":"templates/root/typescript.md","bytes":76,"digest":"sha256:9673f69acb604560153c93c46aa6a8a03ecedc81c3eae15e03b0b1b763dbcde7"}],"profiles":["go-cli-tui","rust-cli","standard-typescript-monorepo"],"modules":["autonomous-work","backend","bun","cli-surface","context-workflow","core","external-triage","frontend","go","monorepo","repository-extension","rust","secondbrain","spec-workflow","tui-surface","typescript"],"decisions":["auth.provider","autonomous.enabled","domain.layout","http.contract","identifier.strategy","language.generated","repository.extension.enabled","runtime.backend","runtime.design","secondbrain.enabled","spec.scaffold","triage.external","verification.gate"],"templates":["template.extension.repository-rules","template.guide.agent-instructions","template.guide.autonomous-work","template.guide.backend","template.guide.bun","template.guide.cli-surface","template.guide.docs-layout","template.guide.domain","template.guide.domain.multi-context","template.guide.domain.single-context","template.guide.external-triage","template.guide.frontend","template.guide.go","template.guide.issue-tracker","template.guide.monorepo","template.guide.rust","template.guide.secondbrain","template.guide.skill-dispatch","template.guide.spec-docs-layout","template.guide.spec-routing","template.guide.tui-surface","template.guide.typescript-bun","template.root.autonomous-work","template.root.backend","template.root.bun","template.root.cli-surface","template.root.context-workflow","template.root.core","template.root.external-triage","template.root.frontend","template.root.go","template.root.monorepo","template.root.repository-extension","template.root.rust","template.root.secondbrain","template.root.spec-workflow","template.root.tui-surface","template.root.typescript"],"setups":["go-cli","rust-cli","typescript-bun"],"retentionTransitions":["transition.legacy-typescript-bun-to-portable-v3","transition.managed-v2-to-portable-v3"]} +{"schemaVersion":"roundfix/baseline-catalog/v1","files":[{"path":"contract-v1.json","bytes":13525,"digest":"sha256:0c32b7ac1b6dd1a2a04d830b300d592b7b195aec7bb32ff05575e8d19f154497"},{"path":"coverage.json","bytes":4660,"digest":"sha256:9faa73bf86fad383570b0098a9aa26185ea8d689f9866038ab37b8a34b9a01dc"},{"path":"decisions.json","bytes":8438,"digest":"sha256:58acbb05cf46057923e8844b28e2a97efa898d6200ae9e0773971b6287b05651"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/AGENTS.md","bytes":2911,"digest":"sha256:1876cc64ced89e60cfb0188eaff71d8b588c80d1149740ba368d1afc969e1d8f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/agent-instructions.md","bytes":4182,"digest":"sha256:eb769f99afa32a901723e1962a06fca09d856c8d37284b36c9aed071bd6e4f86"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/autonomous-work.md","bytes":789,"digest":"sha256:76065b3f1b452f647765d01bfb802ca3a0757d7212ba75b8e55d1d6242b0c977"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/backend.md","bytes":1083,"digest":"sha256:b8459f66c5fec1d424768fdd2b0b2f4ec70b9038208c1d5a58086720586195fd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/docs-layout.md","bytes":3546,"digest":"sha256:a188cfaae89ed195ca31460af3ec5d0dfad0bc1241e1958950ed4e0035de86f6"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/domain.md","bytes":614,"digest":"sha256:089a408493f450ef5713f3f62b20b51323b6d08617e6e43a12e38d794676ae82"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/external-triage.md","bytes":385,"digest":"sha256:4e6b2cfafd491161e7666b304ce0d2d6b19ad705613a6471999c84fda1d63e6a"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/frontend.md","bytes":825,"digest":"sha256:3cd5cba8160ea9f6419ffe82920f7641d09eb7126a78ab50606dfe045732cc02"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/issue-tracker.md","bytes":353,"digest":"sha256:f2139d498fb79358a240d8c99ab1718764217cf28aec0fb024beadfa2b0231cc"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/monorepo.md","bytes":371,"digest":"sha256:47cf6a0ac72c2bcacfc69e5566b1cdcc2102e552b1d91592e5cb1033d30be9e3"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/secondbrain.md","bytes":1589,"digest":"sha256:fab2b2896ad03e0436414b83e5cd80e7fdc07b96d9231b798194b36314a8e7a7"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/skill-dispatch.md","bytes":14414,"digest":"sha256:d424a3e360031e39913bf385f87012e6d1ab79a84024701092859640d2fd05dd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/spec-routing.md","bytes":2269,"digest":"sha256:ce1b67f65ce8ad8a5dc1505e2201833d0e477ff91a6161c395db1188ae83ce9f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/typescript-bun.md","bytes":1378,"digest":"sha256:ec114298fee6ef89bbf0423a6b8a467630296264fc49e989d89af3301093b96e"},{"path":"lock-hash-compatibility-v1.json","bytes":366,"digest":"sha256:6a83f5c12ea6fe0a0aa2e2c710ef44b4cc922d2965240376bcd0f12254d765d0"},{"path":"modules/autonomous-work.json","bytes":2400,"digest":"sha256:61cb4100b7a9378f4d582964d6ac7fac52e005f5b27d0e5c359bb80a9fce7f85"},{"path":"modules/backend.json","bytes":5655,"digest":"sha256:c5b3d012e5ee3ec4dabcb5ea679fbc89932eb9760215c0c91d0bc8bb45a4f806"},{"path":"modules/bun.json","bytes":2557,"digest":"sha256:ca869344629ff4ede015e761c87afb01c20c371c05260cef56f36a7376e53777"},{"path":"modules/cli-surface.json","bytes":1721,"digest":"sha256:04e84d41b65452dc06d43ce43e2aedec3cc05d100d18aacc150f808ae80f7d32"},{"path":"modules/context-workflow.json","bytes":11142,"digest":"sha256:fb61822b2ce29891d6a06963b225a4da6935859df83ae1aa3fde9fa3bf0b0d30"},{"path":"modules/core.json","bytes":18107,"digest":"sha256:69dd99c698a50f651b0bd0b3021b2abbfba645955fe534735e97bff4ef0f1098"},{"path":"modules/external-triage.json","bytes":1159,"digest":"sha256:5ea90217b042667a3e00211941d4cb1bafb24294187635bfa277765c05894366"},{"path":"modules/frontend.json","bytes":8713,"digest":"sha256:6a701d874b2217bb7f59cddfdd0d0d8a8bb41c6452fc63b67251e5beddc25f7d"},{"path":"modules/go.json","bytes":3322,"digest":"sha256:d9877c86f59206b3bf6bdc1bf73fdfd18fdcc496bc67770413963587276b0533"},{"path":"modules/monorepo.json","bytes":1385,"digest":"sha256:e6485fd6dacb765a866dd9ff79f057d2ebc85cb57c603774fe7ca453cf50f917"},{"path":"modules/repository-extension.json","bytes":1094,"digest":"sha256:a2adeac11000706839944c2ecca225e524da6f38534e54efc9cb0a26f5413f47"},{"path":"modules/rust.json","bytes":2328,"digest":"sha256:46537524b19a5eb1b61dd07d7215b2ea607c1b78a481a21eb13926d40ecdc0b2"},{"path":"modules/secondbrain.json","bytes":4333,"digest":"sha256:3ce59d0ddfcd0e2e5de34d9a4541a27bc62c6e72800da1cd02353ba2f2aa2024"},{"path":"modules/spec-workflow.json","bytes":6441,"digest":"sha256:574f08d79b15e4d9a6a38c3a9a85636f5a208aa9e43051383815056fc03c6e53"},{"path":"modules/tui-surface.json","bytes":1658,"digest":"sha256:06e18ef65d3a7184aaa3a80a21d2279b3376ab77f07bb8b47d23497b0fb6fa62"},{"path":"modules/typescript.json","bytes":8526,"digest":"sha256:7a2a82f7efb28df7dbc343f1b5a7b0ab176e8a74105eb6077c7b5cf6648bdbc6"},{"path":"profiles/go-cli-tui.json","bytes":1709,"digest":"sha256:231220ac521a8298d86440f6e262d61b9c2fa249459dd9206973d8db6e6941bb"},{"path":"profiles/rust-cli.json","bytes":1627,"digest":"sha256:f2338dbf8d0085a6150293c157b6986714de122b2ebd732e33a09f0614686c81"},{"path":"profiles/standard-typescript-monorepo.json","bytes":10907,"digest":"sha256:803f62fb451f9fda23a58b7bfe5f5367a45d8d44bb63c3f96e33b259393cc5ba"},{"path":"retention/transition.legacy-typescript-bun-to-portable-v3.json","bytes":12425,"digest":"sha256:3b166a42eb1f9f23202afc48399eb7aa9b0e8563d9c40095ad6bf710a5d1c599"},{"path":"retention/transition.managed-v2-to-portable-v3.json","bytes":11185,"digest":"sha256:8a0e9dd06aa6a5988c010f6c1286c39002c65e5aae6762e9af6956424524c3e9"},{"path":"setups/go-cli.json","bytes":14203,"digest":"sha256:28e2ad53684c2c097affa4cf70c32a59265435dd105cdcfa57314bd18a6af46f"},{"path":"setups/rust-cli.json","bytes":12476,"digest":"sha256:82cdba03b2c77c9a0c4a6ad737e7de5e90a3fcf6827c18242793b8032766d9c0"},{"path":"setups/typescript-bun.json","bytes":40649,"digest":"sha256:440e67d2f94be86d1568be2a185e4799b2b1637f472b1db9ba7938811407eeb6"},{"path":"skill-activations.json","bytes":3330,"digest":"sha256:bbf1978828a2d383c9cee675a612c17281224e0a1ce5df4b09ba13af4ccd3667"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/accounting.json","bytes":17193,"digest":"sha256:69e39f9df0eb54d08ecb739dc3dfc9dd80c0cf76016aca3d6a0566e48cfaa1f5"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/baseline.json","bytes":498,"digest":"sha256:dca6a12ae24a77fc9104dfcccb507c50dee0e5257e0bf0bd9d86c0b04ce7ffe0"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/AGENTS.md","bytes":583,"digest":"sha256:3b6b36fd045ecb683aec29e0ceac52b3704604815d8035297bfeb8438602494e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/agent-instructions.md","bytes":8996,"digest":"sha256:fe584b8901ae2521dcba7014ca11f3557109a4d91d7c56a21852f027a4bf205e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/autonomous-work.md","bytes":1548,"digest":"sha256:3deff187dba70156ca834529741c4be7df84c5a66f8462ba01594fee35d40336"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/backend.md","bytes":2197,"digest":"sha256:c553c12e3cb13b0435a219fd52ac735bf4f9f3aad05fdb943fd6922cef0a97ac"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/docs-layout.md","bytes":7671,"digest":"sha256:8a78a3543a0a52ac93ef83ab36f953e813be068e96957553483c928cbad439bd"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/domain.md","bytes":947,"digest":"sha256:54c79c324d20b412c07c634f490ad18eeace0562caffe5507b3c83cebf9aaf20"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/external-triage.md","bytes":335,"digest":"sha256:0ec2089110c6a673f305e86f436538c7c07aeef46d29a4e68674defb8778e2cc"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/frontend.md","bytes":1934,"digest":"sha256:4a3609c27bf100aaf0d7de934ad43c0f6a561a15600517422a325fcc6120bd14"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/issue-tracker.md","bytes":890,"digest":"sha256:d86ab246d701a54b8d61c5dcdc1d29fe3281115c028e8694f10574ce99e537c7"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/monorepo.md","bytes":366,"digest":"sha256:c59091add143e67b3773df78d95cc4c8f11d43909655ef1c09dd64c0cc903b3d"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/secondbrain.md","bytes":3312,"digest":"sha256:4b58f3ed96f654d8b99ccc9a03718505f4c43a6170eceef47e26484b5f95bffb"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/skill-dispatch.md","bytes":374,"digest":"sha256:90bbf02e35bba9c707e4713eef4de9a6db36ba3fa93393531287aaf94fce8f8b"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/spec-routing.md","bytes":3094,"digest":"sha256:dfc867401e547d4370f20c578a22e6b82cc80d7b677072f31bc11cdba4851085"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/typescript-bun.md","bytes":2561,"digest":"sha256:7295fe2e544af37e6d6777537fa7ee512b78f3da932d736cc7eabc8d190a30e8"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/manifest.json","bytes":37175,"digest":"sha256:e9a9e91f8dbbea8dc416a03fe813b9c370a2e012118f548b18f1c6a13b6b7662"},{"path":"source-baselines/index.json","bytes":5266,"digest":"sha256:ad636b4cbaf57e27c81093aee9963bcd2b276df7ac243f04b423bf989db314a8"},{"path":"templates/extensions/specific-repository.md","bytes":38,"digest":"sha256:f8aad350dfecd91923c12fe215c1e21dd6741635e44869110391ba9c5c89d137"},{"path":"templates/guides/agent-instructions.md","bytes":260,"digest":"sha256:91f0a4b1d664cf6ab261499b3f7cb093bd2337d6567d07599faebd8fe58dc8d3"},{"path":"templates/guides/autonomous-work.md","bytes":194,"digest":"sha256:384fdebdb4ffd5d6c1b7b197cc02e3f9d3e7327acd7151dc7ce29c0e53e99486"},{"path":"templates/guides/backend.md","bytes":201,"digest":"sha256:0bffbf1e74c9b8c506ec1c612aac0dfaedcfb6f1b7aafc8d1a2d6029cd1170ea"},{"path":"templates/guides/bun.md","bytes":26,"digest":"sha256:3a0640850dc84c8dd6c6b35f10864ae6d3d9c153147699b7bfced6332315633d"},{"path":"templates/guides/cli-surface.md","bytes":34,"digest":"sha256:7cdd6ecc6575c4ef165858ced6409278430a89e3f67baa4281151dc13e2742bc"},{"path":"templates/guides/docs-layout.md","bytes":34,"digest":"sha256:51e9033d80e6742321b47b19c85034c9b4d15b36b9c84230512e3a9a4ef90628"},{"path":"templates/guides/domain-multi-context.md","bytes":112,"digest":"sha256:56b7000576757f37da275123ab75ff5f84d2e26bd88c9f67034a24f3d4883a41"},{"path":"templates/guides/domain-single-context.md","bytes":104,"digest":"sha256:f75666666408761a67d23855f5f78e4526328404343f09a036f8987895c69c0d"},{"path":"templates/guides/domain.md","bytes":59,"digest":"sha256:5c4195e0348b6e5a57679fc833e58b09f4b08080b1cec90a946d0b666310739e"},{"path":"templates/guides/external-triage.md","bytes":38,"digest":"sha256:f26c4164ff69bf44263d9993e4209a9305b8979f228f6f519f139e83f9b51ae9"},{"path":"templates/guides/frontend.md","bytes":265,"digest":"sha256:3243626ac7b55130ac198ba32a350966a26f6a9801a4a4fee259817b2f7eb5f9"},{"path":"templates/guides/go.md","bytes":25,"digest":"sha256:7f597711bdab9fe85ecda9f5895687e436009446ee8416ed96f7bc27fe7b00fd"},{"path":"templates/guides/issue-tracker.md","bytes":36,"digest":"sha256:48840599c91f2f3cf2fd79700f41e54bc3edbb4fa19e4861247e19bac0077fb1"},{"path":"templates/guides/monorepo.md","bytes":31,"digest":"sha256:e1866f1633f3bbc5b230b5c5290b71ae47110dce4d4344c264a9eb325a477bca"},{"path":"templates/guides/rust.md","bytes":27,"digest":"sha256:e0e693135266e96cabec918b14f0cf51c7bf1a126d900001cc418f982f8bbec2"},{"path":"templates/guides/secondbrain.md","bytes":34,"digest":"sha256:a59dc0ae149d4bb3bdd633f79ba91fd17e6d3d3d930c346e5757b5f85ab69002"},{"path":"templates/guides/skill-dispatch.md","bytes":253,"digest":"sha256:0f054b846da5b17409dde049b3b182489e6c24e84734d4a5ffc47a9f504e8a7c"},{"path":"templates/guides/spec-docs-layout.md","bytes":39,"digest":"sha256:6d9ddb174c4f69cb1f9abe428a6be677e9ea8dbecc19ae0fa2f53fdc3749de40"},{"path":"templates/guides/spec-routing.md","bytes":35,"digest":"sha256:cc578e73968c15bdcaad35270454e91462b4413d937cfd984404ef22d765482c"},{"path":"templates/guides/tui-surface.md","bytes":34,"digest":"sha256:bbc422cb32df905d526a291cbfd5b81fd86f0d405f5f650cc5d8eb0a864c4eb2"},{"path":"templates/guides/typescript-bun.md","bytes":41,"digest":"sha256:ee14609660ebe1da17eff0d12c3f42857cba4c92be0f723346d17e3d64bb6574"},{"path":"templates/index.json","bytes":7125,"digest":"sha256:272cbfd4fa2ea380acfc76ce4d75eaf2ae026b7501c34ee48a8645644eb72f4c"},{"path":"templates/root/autonomous-work.md","bytes":102,"digest":"sha256:abd0be4c74295597be5dca6a307e41d6bdcfa5113370f5bbc30d30725c927e35"},{"path":"templates/root/backend.md","bytes":76,"digest":"sha256:f65a96aaa0b79d40d209df159c30bd6ac791e36c415b8ba2dc52a1032c5785c3"},{"path":"templates/root/bun.md","bytes":89,"digest":"sha256:48a4bcd7c9b86d8f3be6965ff8b7b33aad77148aa598d49ed8efa74acd3c8e20"},{"path":"templates/root/cli-surface.md","bytes":79,"digest":"sha256:ec022b0b1767d88ebc849f2ee7d8a2d266a36d419f8084b9cce8599d0619a125"},{"path":"templates/root/context-workflow.md","bytes":129,"digest":"sha256:52c53e99fed6c5d59388e58f7854c96f8130da553d80bb90f04935f67eea45c5"},{"path":"templates/root/core.md","bytes":213,"digest":"sha256:ffa6f25825e1d5c5bf52373022d7035b004fd78dd55f99a187d89c4b78ddeabb"},{"path":"templates/root/external-triage.md","bytes":91,"digest":"sha256:5ea18c6c7aea8f03d62af0b824b947ddecbaf861fbf9b81df2e8fb2b2c0e2c54"},{"path":"templates/root/frontend.md","bytes":99,"digest":"sha256:cddad0b38dff9cbeae571dc8514f5d2770e528725f8931272389bd8a675e08bb"},{"path":"templates/root/go.md","bytes":67,"digest":"sha256:aa3eafd5ebcec7aa64ae7fddf1c5b954512e831121934760f615c12ec52f6527"},{"path":"templates/root/monorepo.md","bytes":74,"digest":"sha256:76951b431357333d5b3bbbe718529797834c5de14ee6cd886bce175b5ef892e9"},{"path":"templates/root/repository-extension.md","bytes":103,"digest":"sha256:f24b6015a05eab4cdbb830eb485f9958b659d9a99d46b53d8538a813f6705fe1"},{"path":"templates/root/rust.md","bytes":73,"digest":"sha256:40c4f50d3a020406816612b9f9e0c6cbd545626401e447d4963ba2d30d8e40e3"},{"path":"templates/root/secondbrain.md","bytes":87,"digest":"sha256:dbb11588c2dbca5fd803eccc889586cac75824e9083c570bd1cad34fda901857"},{"path":"templates/root/spec-workflow.md","bytes":125,"digest":"sha256:c292c5b2b98facb56ef0f6da0a61f38544377ddb6339c63d5287ca7dec4f2360"},{"path":"templates/root/tui-surface.md","bytes":75,"digest":"sha256:5657d3abaddf480a535d00a8bfe94bcd3e0b6230aaaa3aac2f9ce89fdc7e20b0"},{"path":"templates/root/typescript.md","bytes":76,"digest":"sha256:9673f69acb604560153c93c46aa6a8a03ecedc81c3eae15e03b0b1b763dbcde7"}],"profiles":["go-cli-tui","rust-cli","standard-typescript-monorepo"],"modules":["autonomous-work","backend","bun","cli-surface","context-workflow","core","external-triage","frontend","go","monorepo","repository-extension","rust","secondbrain","spec-workflow","tui-surface","typescript"],"decisions":["auth.provider","autonomous.enabled","domain.layout","http.contract","identifier.strategy","language.generated","repository.extension.enabled","runtime.backend","runtime.design","secondbrain.enabled","spec.scaffold","triage.external","verification.gate"],"templates":["template.extension.repository-rules","template.guide.agent-instructions","template.guide.autonomous-work","template.guide.backend","template.guide.bun","template.guide.cli-surface","template.guide.docs-layout","template.guide.domain","template.guide.domain.multi-context","template.guide.domain.single-context","template.guide.external-triage","template.guide.frontend","template.guide.go","template.guide.issue-tracker","template.guide.monorepo","template.guide.rust","template.guide.secondbrain","template.guide.skill-dispatch","template.guide.spec-docs-layout","template.guide.spec-routing","template.guide.tui-surface","template.guide.typescript-bun","template.root.autonomous-work","template.root.backend","template.root.bun","template.root.cli-surface","template.root.context-workflow","template.root.core","template.root.external-triage","template.root.frontend","template.root.go","template.root.monorepo","template.root.repository-extension","template.root.rust","template.root.secondbrain","template.root.spec-workflow","template.root.tui-surface","template.root.typescript"],"setups":["go-cli","rust-cli","typescript-bun"],"retentionTransitions":["transition.legacy-typescript-bun-to-portable-v3","transition.managed-v2-to-portable-v3"]} diff --git a/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json b/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json index 39fb6ab..688c9b3 100644 --- a/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json +++ b/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json @@ -30,7 +30,7 @@ "id": "rust-cli" }, { - "digest": "9f2c2a77451e8ddf7e899a29bac3b2e8697c7ce6043e2d40dbc09d0de00ef351", + "digest": "1d48345a1d38ee643dc308c2a0688aba689ec01762fb94d8c711ca053ac0f9bd", "id": "typescript-bun" } ], @@ -920,7 +920,7 @@ ] } ], - "digest": "9f2c2a77451e8ddf7e899a29bac3b2e8697c7ce6043e2d40dbc09d0de00ef351", + "digest": "1d48345a1d38ee643dc308c2a0688aba689ec01762fb94d8c711ca053ac0f9bd", "id": "typescript-bun", "schemaVersion": "setup-context-driven/setup-snapshot/0.0.1", "skills": [ @@ -1935,7 +1935,7 @@ "treeDigest": "cc7137c53d51fea605c0be017cf6eecdb72add7405e5c3c9f327dbc26d4572fa" }, { - "contentDigest": "2cc373d12f0a802e7c1c2961838d7cc23ed9859f6cf3f7f76f3852377cff1a3d", + "contentDigest": "d5269bac642a8ef4c1c8439eff308594e36ad786fe14d9f3047a8e731998f59f", "name": "roundfix", "path": "skills/06-review-repair/roundfix", "source": { diff --git a/internal/baseline/testdata/parity-corpus/v1/manifest.json b/internal/baseline/testdata/parity-corpus/v1/manifest.json index 4852702..7d17d05 100644 --- a/internal/baseline/testdata/parity-corpus/v1/manifest.json +++ b/internal/baseline/testdata/parity-corpus/v1/manifest.json @@ -22,7 +22,7 @@ { "bytes": 83662, "path": "fixtures/asset-sync.json", - "sha256": "02f8d026276cee7e5b1073f80a182c0ad671e32312a8742d13ebaec4cc0147c2" + "sha256": "7eb997602f8612c2386e1c54fc03a80d60e088b85222a37575b2822c63eb25d9" }, { "bytes": 330237, diff --git a/skills/roundfix/SKILL.md b/skills/roundfix/SKILL.md index 25b9456..a1e1560 100644 --- a/skills/roundfix/SKILL.md +++ b/skills/roundfix/SKILL.md @@ -33,15 +33,28 @@ Task Worktrees, and QA uses its own Agent Session after Tasks settle. Use the Doctor Command, `roundfix doctor`, to diagnose Run readiness without installing dependencies, writing config, or changing files. Doctor runs the shared Node.js, minimum-supported acpx, effective adapter, required Agent Selection -Profiles, and codex runtime hygiene checks and prints one line per check with -status `ok`, `failed`, or `skipped`. Adapter Readiness requires the effective -Codex command to prove official `@agentclientprotocol/codex-acp` lineage at -version `1.1.4` or newer; executable presence and a matching name are not -proof. The `profiles:` line is the selection authority: it exact-proves every -distinct Preferred Selection and fallback through disposable ACP Sessions and -reports affected category references plus one deterministic next action. -Doctor has no separate legacy `agent:` or `model:` authority. Failed checks -include `next: ` when Roundfix knows the remediation. +Profiles, Repository Skill Set, and codex runtime hygiene checks and prints one +line per check with status `ok`, `failed`, or `skipped`. Adapter Readiness +requires the effective Codex command to prove official +`@agentclientprotocol/codex-acp` lineage at version `1.1.4` or newer; +executable presence and a matching name are not proof. The `profiles:` line is +the selection authority: it exact-proves every distinct Preferred Selection +and fallback through disposable ACP Sessions and reports affected category +references plus one deterministic next action. Doctor has no separate legacy +`agent:` or `model:` authority. Failed checks include `next: ` when +Roundfix knows the remediation. + +The blocking `skills:` line runs after, and independently from, `profiles:`. +The running binary's embedded bundle is authoritative for Roundfix-owned +skills; each required external skill must match its `computedHash` in +`skills-lock.json`. Surface a failed `skills:` line and its printed `next:` +remediation before work continues. Owned failures print +`roundfix skills install --target project`; external failures print +`bunx skills experimental_install && bunx skills update -p -y`; mixed failures +print both commands in that order. Doctor is diagnosis-only: it never runs +these commands, accesses the network, installs or updates skills, or writes +repository state. Apply remediation only after explicit workflow +authorization, then rerun Doctor. On macOS, the codex hygiene check resolves `CODEX_PATH` first and then `codex` on `PATH`, inspects the `com.apple.quarantine` attribute (the real XProtect trigger), and verifies the binary's code signature (not `spctl --assess`, which @@ -112,20 +125,22 @@ unauthorized target. A rejected Sol/high proof never becomes an offer to use model-managed reasoning. Use `roundfix doctor` when you only need a read-only readiness report. It runs -the Node.js, minimum-supported acpx, effective adapter, required Agent Selection Profile, -and codex runtime hygiene checks and exits nonzero if any check fails. Adapter -failures name the effective command, package classification, and official -install action. Profile failure names the exact runtime/model/reasoning tuple, -every affected category, bounded adapter evidence, and the next -`roundfix profiles configure` or `roundfix profiles validate` action. A -rejected explicit `high` does not recommend model-managed reasoning. The -command has no flags and mutates nothing. +the Node.js, minimum-supported acpx, effective adapter, required Agent Selection +Profiles, Repository Skill Set, and codex runtime hygiene checks and exits +nonzero if any check fails. Adapter failures name the effective command, +package classification, and official install action. Profile failure names the +exact runtime/model/reasoning tuple, every affected category, bounded adapter +evidence, and the next `roundfix profiles configure` or +`roundfix profiles validate` action. A rejected explicit `high` does not +recommend model-managed reasoning. The command has no flags and mutates +nothing. ```text node: ok acpx: ok adapter: ok (npx -y @agentclientprotocol/codex-acp@1.1.4; package=@agentclientprotocol/codex-acp; version=1.1.4) profiles: ok (3 distinct tuples; 10 category references) +skills: ok (39 required: 14 Roundfix-owned, 25 external) codex: ok ``` From 6ec34e301ce2db9de9a88196355d99c87c65eee0 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 16:24:34 -0300 Subject: [PATCH 23/48] docs: qa report for 0036-doctor-skill-readiness (partial) Roundfix-Spec: 0036-doctor-skill-readiness --- .../command-evidence.md | 204 ++++++++++++++++++ .../qa/qa-report-2026-07-26.md | 120 +++++++++++ 2 files changed, 324 insertions(+) create mode 100644 docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md create mode 100644 docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md diff --git a/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md b/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md new file mode 100644 index 0000000..75d5224 --- /dev/null +++ b/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md @@ -0,0 +1,204 @@ +# Command evidence — Doctor Skill Readiness + +Build: `6e5618dba5059666c891f9078806631d30502d5b` + +## Static verification + +| Command | Exit | Observed result | +| --- | ---: | --- | +| `rtk make verify` | 0 | 2,394 tests passed across 22 packages; four focused skill-contract tests passed; all 14 shipped Roundfix Skills passed; `bin/roundfix` built. | +| `rtk go test -race ./...` | 0 | 2,394 tests passed across 22 packages under the race detector. | +| `rtk go test ./skills -run 'Test(CheckRepository\|SkillFolderHash)' -count=1` | 0 | 26 focused repository-readiness and folder-hash tests passed. | +| `rtk go test ./skills -count=1` | 0 | 99 skills-package tests passed, including the real repository match. | +| `rtk go test ./internal/cli -run 'Test(CommandUsage\|DocumentationContract)' -count=1` | 0 | Four public usage/documentation contract tests passed. | +| `rtk make skills-sync-check` | 0 | Four Roundfix Skill synchronization guards passed. | +| `rtk ./bin/roundfix skills check` | 0 | All 14 shipped Roundfix Skills passed. | +| `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` | 0 | No output; the canonical and generated files are byte-identical. | + +## Public help + +Command: + +```text +rtk ./bin/roundfix doctor --help +``` + +Exit: `0` + +```text +Usage: + roundfix doctor + +Diagnoses this machine's readiness for Runs. Checks Node.js, the minimum +supported acpx version, the effective adapter, and required +Agent Selection Profiles, the Repository Skill Set, and codex runtime hygiene. +The aggregate profiles: line exact-proves every distinct tuple. The skills: +line compares Roundfix-owned artifacts with the running binary and external +artifacts with skills-lock.json. Each failure reports its next action. +Doctor is offline, read-only, and mutates nothing. +``` + +## Complete repository flow + +The fixture contains the current project configuration, `skills-lock.json`, +and all installed project skills. It is an independent Git repository under +`/tmp/roundfix-qa-0036.KrAsVB/clean`. + +Running the built command from both the fixture root and +`nested/deeper` produced the same Repository Skill Set result: + +```text +skills: ok (39 required: 14 Roundfix-owned, 25 external) +``` + +Both commands exited `1` because this host independently fails `profiles:` and +`codex:` readiness: + +```text +/Users/marcio/.local/bin/codex has an invalid or missing code signature +``` + +The complete-repository file digest was identical before and after both runs: + +```text +c1473d4bf5c378216f816a91e316d9d7c9782ef82a51eda512c14a5e1ea3df81 +``` + +This proves the current build's complete `14/25/39` Repository Skill Set and +nested Git-root resolution. It does not prove the acceptance criterion's +otherwise-ready overall exit `0`; that remains blocked by the host Codex +prerequisite. + +## Disposable repository matrix + +Every row ran the built public command: + +```text +/Users/marcio/.roundfix/worktrees/roundfix-339f8dac/run_20260726T184338Z_c754fa2057c5fc07/bin/roundfix doctor +``` + +The unrelated host `profiles:` and `codex:` lines are omitted below. Every +failure fixture exited `1`. The `skills:` line appeared after `profiles:` and +before `codex:` in the complete output, proving independent later checks still +ran. + +| Fixture action | Observed `skills:` line | +| --- | --- | +| Move owned `roundfix` directory out of `.agents/skills` | `skills: failed (missing: roundfix; next: roundfix skills install --target project)` | +| Move external `tech-writer` directory out of `.agents/skills` | `skills: failed (missing: tech-writer; next: bunx skills experimental_install && bunx skills update -p -y)` | +| Change owned `roundfix/SKILL.md` | `skills: failed (outdated: roundfix; next: roundfix skills install --target project)` | +| Add an unexpected owned file | `skills: failed (outdated: roundfix; next: roundfix skills install --target project)` | +| Remove owned `roundfix/SKILL.md` | `skills: failed (outdated: roundfix; next: roundfix skills install --target project)` | +| Change external `tech-writer/SKILL.md` | `skills: failed (outdated: tech-writer; next: bunx skills experimental_install && bunx skills update -p -y)` | +| Missing owned `roundfix`, missing external `tech-writer`, outdated owned `qa-gate`, outdated external `testing-boss` | `skills: failed (missing: roundfix, tech-writer; outdated: qa-gate, testing-boss; next: roundfix skills install --target project; bunx skills experimental_install && bunx skills update -p -y)` | +| Replace owned `roundfix/SKILL.md` with an outside-tree symlink | `skills: failed (outdated: roundfix; next: roundfix skills install --target project)` | +| Make owned `roundfix/SKILL.md` unreadable | `skills: failed (read Roundfix-owned skill artifact ".../roundfix/SKILL.md": open SKILL.md: permission denied; next: roundfix skills install --target project)` | +| Remove `skills-lock.json` | `skills: failed (read skills lock ".../skills-lock.json": ... no such file or directory; next: bunx skills experimental_install && bunx skills update -p -y)` | +| Malform `skills-lock.json` JSON | `skills: failed (decode skills lock ".../skills-lock.json": invalid character ':' after array element; next: bunx skills experimental_install && bunx skills update -p -y)` | +| Set required `tech-writer.computedHash` to `not-a-valid-hash` | `skills: failed (validate required skills lock hash ".../skills-lock.json": external skill "tech-writer" computedHash must be 64 lowercase hexadecimal characters; next: bunx skills experimental_install && bunx skills update -p -y)` | +| Add unrelated installed skill plus unrelated unsafe lock entry | `skills: ok (39 required: 14 Roundfix-owned, 25 external)` | + +The unsafe-required-name branch is not a repository-controlled input in the +shipped application: the binary owns `Names()` and `Recommended()`. The current +artifact's sets passed `roundfix skills check`; the focused current-build test +`TestCheckRepositoryRejectsMalformedLockAndUnsafeRequiredNames` proves that an +injected unsafe required name is rejected before filesystem traversal. + +## Determinism and no mutation + +The mixed fixture produced the same sorted line on repeated runs. Each +remediation command appeared exactly once and owned remediation preceded +external remediation. + +The mixed fixture digest before and after rerun was: + +```text +2b5a1ee30fbe18f398b7e242f7e7bfb91dac836753ae436d6c32bfd827699038 +``` + +The unrelated-extra fixture digest before and after rerun was: + +```text +fabfc4128383c52d6d33e70f3d7cbb013c77ba5d97b695901624133f26386a77 +``` + +The symlink fixture digest before and after rerun was: + +```text +52cca50e7245e6a9f721d457d6abd9a43c3d68251376315a1ef20b2fab9412f9 +``` + +The outside-tree sentinel remained: + +```text +cf46cdea17f51f34cf6e21716f0d98148b59c456650adda64a72545760313040 +``` + +Source-boundary inspection found only local observation operations in the +Repository Skill Set checker: `os.Lstat`, `os.ReadFile`, `fs.WalkDir`, and +`fs.ReadFile`. It found no `os.Write*`, `os.Create*`, `os.Remove*`, +`os.Rename`, `os.Mkdir*`, command execution, or network import/call. The two +remediation commands appear only as rendered next-action constants. + +## Documentation evidence + +The README, `docs/user-guide/commands.md`, `docs/user-guide/usage.md`, +`CONTEXT.md`, and both Roundfix Skill copies consistently state: + +- Agent Selection Profile Readiness runs before the independent Repository + Skill Set result. +- The running binary is authority for the 14 Roundfix-owned skills. +- `skills-lock.json` `computedHash` values are authority for 25 external + skills. +- Missing, outdated, or invalid required state blocks Doctor with exit `1`. +- Owned remediation is `roundfix skills install --target project`. +- External remediation is + `bunx skills experimental_install && bunx skills update -p -y`. +- Doctor is offline and read-only, never runs remediation, and ignores + unrelated extras. + +The built help and public command output matched these terms. The Roundfix +Skill additionally tells the Agent to surface the failed line and printed +next action before work continues and to require explicit workflow +authorization before remediation. + +## Project Constraint and commit-scope evidence + +The PRD and TechSpec both carry applicability and operative sources for +identifier strategy, authentication/HTTP, active ADR obligations, and Tooling +Authority. + +- Identifier strategy is not applicable: no Internal Identifier was added. +- Authentication/HTTP is not applicable: the checker is local filesystem + diagnosis. +- ADR-0049 and ADR-0055 remain authoritative for the preceding profile proof; + the observed output kept `profiles:` before `skills:`. +- ADR-0066 and ADR-0072 remain intact: no Python setup runtime or new Baseline + execution path was added. + +Task 03 commit `6e5618d` changed exactly: + +```text +.agents/skills/roundfix/SKILL.md +docs/specs/0036-doctor-skill-readiness/task_03.md +internal/baseline/assets/setups/typescript-bun.json +internal/baseline/testdata/catalog.digest +internal/baseline/testdata/catalog.normalized.json +internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json +internal/baseline/testdata/parity-corpus/v1/manifest.json +skills/roundfix/SKILL.md +``` + +This equals the exact bounded Tooling Authority recorded in both active Spec +artifacts plus the assigned Task file. Task 02 commit `ccfbd15` changed only: + +```text +CONTEXT.md +README.md +docs/specs/0036-doctor-skill-readiness/task_02.md +docs/user-guide/commands.md +docs/user-guide/usage.md +``` + +The current worktree delta is confined to this QA report and its evidence +directory. No Task status or Task Graph file changed. diff --git a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md new file mode 100644 index 0000000..80e22de --- /dev/null +++ b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md @@ -0,0 +1,120 @@ +--- +spec: 0036-doctor-skill-readiness +date: 2026-07-26 +build: 6e5618dba5059666c891f9078806631d30502d5b +status: closed +verdict: partial +surfaces: [cli, backend, docs] +--- + +# QA report — Doctor Skill Readiness + +## Scope and environment + +Full QA gate for every PRD user story and every Task acceptance criterion. +The primary actor is a developer preparing a local Git repository for a +Roundfix Run. The public entry points are the built `roundfix doctor`, +`roundfix doctor --help`, and `roundfix skills check` commands plus the +supported README, user guide, glossary, and Roundfix Skill. + +- Build: `6e5618dba5059666c891f9078806631d30502d5b` +- Branch: `roundfix/run-run_20260726T184338Z_c754fa2057c5fc07` +- Host: macOS Darwin 25.5.0 arm64 +- Go: `go1.26.5 darwin/arm64` +- Runtime tools found: `bun`, `acpx`, and `codex-acp` +- Test data: current repository plus disposable Git repository copies under a + QA-owned temporary directory +- Network: not required and not authorized by the feature contract +- Evidence directory: + `qa/evidence/2026-07-26-doctor-skill-readiness/` + +Behavior probes are selected for their risk to the readiness boundary: +repeat the read-only command, invoke it from a nested directory, remove a +required skill, change/add/remove versioned content, introduce malformed and +unsafe lock declarations, replace content with a symlink, combine ownership +failures, and add unrelated extras. + +## Project Constraint audit + +| # | Constraint | Applicability and operative source | Status | Evidence | +| - | --- | --- | --- | --- | +| PC-01 | Identifier strategy | Not applicable: no project-owned Internal Identifier is created; names come from the embedded bundle and `skills-lock.json`. Operative source: `docs/agents/domain.md`. | pass | PRD/TechSpec carry the required applicability and source; Task commit inspection found no identity model or generator. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| PC-02 | Authentication and HTTP | Not applicable: the feature is local filesystem diagnosis and adds no authentication, HTTP, or network path. Operative source: `docs/agents/cli.md`. | pass | PRD/TechSpec carry the required applicability and source; public flows and checker inspection found local reads only. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| PC-03 | Active ADR obligations | Applicable: ADR-0049 and ADR-0055 preserve Agent Selection Profile authority; ADR-0066 and ADR-0072 keep Baseline execution in Go. Operative source: `docs/agents/domain.md`. | pass | Accepted ADRs were inspected; real output kept `profiles:` authoritative before independent `skills:`, and changed paths added no Python setup runtime or competing Baseline execution. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| PC-04 | Tooling authority and bounded scope | Applicable: the PRD and TechSpec expressly authorize the protected Roundfix Skill pair and five mechanically derived Baseline/parity artifacts. Operative source: `docs/agents/agent-instructions.md`. | pass | `git diff-tree` for Task 03 commit `6e5618d` matched the seven exact protected/derived paths plus `task_03.md`; Task 02 touched no protected path and the current delta is QA-only. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | + +## Static gate + +| Command | Status | Evidence | +| --- | --- | --- | +| `rtk make verify` | pass | Exit `0`: 2,394 tests passed across 22 packages; four focused skills checks passed; all 14 shipped Roundfix Skills passed `skills check`; `bin/roundfix` built from `6e5618d-dirty`. | +| `rtk go test -race ./...` | pass | Exit `0`: 2,394 tests passed across 22 packages under the race detector. | + +## Results + +| # | Story / criterion / sweep | Actor and surface | Status | Evidence | +| - | --- | --- | --- | --- | +| US-01 | Missing required skill blocks readiness and names the missing skill. | Developer; CLI from a disposable repository. | pass | Owned `roundfix` and external `tech-writer` removals each produced exit `1`, the missing name, and the correct ownership-specific remediation. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| US-02 | Roundfix-owned drift from the running binary is detected. | Developer after upgrade; CLI. | pass | Changed, added, and removed owned artifacts each reported `outdated: roundfix` and the owned install command. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| US-03 | External installed content is compared with `skills-lock.json`. | Maintainer; CLI/backend through Doctor. | pass | The complete real tree matched all 25 hashes; external byte drift, missing lock, malformed JSON, and invalid required hash all failed with bounded diagnostics. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#complete-repository-flow). | +| US-04 | One deterministic line names failures and exact remediation. | Agent or developer; CLI. | pass | The mixed fixture repeatedly printed one sorted line with both commands exactly once in owned-then-external order. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| US-05 | Diagnosis remains entirely local and read-only. | Security-conscious developer; CLI. | pass | Complete, mixed, extra, and symlink fixture digests remained unchanged across repeated public commands; the outside sentinel was preserved and checker inspection found no write, command-execution, or network path. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| T01-AC01 | Complete Repository Skill Set reports `skills: ok` with derived `14/25/39` counts and does not fail an otherwise ready Doctor. | Developer; CLI. | blocked | The built CLI printed `skills: ok (39 required: 14 Roundfix-owned, 25 external)` identically from the fixture root and `nested/deeper`, but this host cannot provide the criterion's otherwise-ready precondition: independent `profiles:` and `codex:` checks fail because `/Users/marcio/.local/bin/codex` has an invalid code signature. Exit-zero confirmation requires reinstalling the official signed Codex binary and rerunning the unchanged command. | +| T01-AC02 | Removing owned or external skills reports `missing`, prints the ownership-specific command, and exits `1`. | Developer; CLI. | pass | Separate owned and external public flows matched the exact line and exit contract. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| T01-AC03 | Changed, added, or removed versioned files report `outdated`; unrelated extras are ignored. | Developer; CLI. | pass | Owned change/add/remove and external change reported `outdated`; an unrelated installed skill plus unrelated unsafe lock entry preserved `skills: ok`. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| T01-AC04 | External folder hashing matches the installed skills CLI algorithm, including ordering and exclusions. | Maintainer; CLI/backend. | pass | The complete public flow matched all 25 current lock hashes; 26 focused compatibility checks, including the pinned locale-order fixture and exclusions, passed. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| T01-AC05 | Malformed lock data, invalid hashes, unsafe names, unreadable artifacts, and symlinks fail deterministically without panic or traversal. | Developer; CLI. | pass | Public missing/malformed/invalid-lock, unreadable, and symlink variants failed deterministically; the outside sentinel remained unchanged. Unsafe required names are binary-owned, not repository input, and the focused current-build guard passed before traversal. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| T01-AC06 | Mixed failures produce one sorted `skills:` line and each remediation exactly once in owned-then-external order. | Agent; CLI. | pass | Repeated mixed flow matched the exact deterministic line and command order. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| T01-AC07 | Doctor runs all existing checks despite skill failure and changes no repository, config, Run, lock, or skill path. | Developer; CLI. | pass | Full output continued through the later `codex:` check after skill failure; multiple file manifests and the outside sentinel were byte-stable. Focused current-build no-mutation tests passed. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| T01-AC08 | Doctor help names Agent Selection Profiles and the appended Repository Skill Set check. | Developer; CLI/docs. | pass | Built `doctor --help` exited `0` and named both terms contiguously with the offline/read-only contract. [Public help](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#public-help). | +| T02-AC01 | Supported user docs identify authorities, outcomes, remediation, and the offline/read-only guarantee. | Documentation reader; docs. | pass | README and both user-guide entry points state both authorities, `ok/failed`, exit `1`, both exact commands, ignored extras, and no automatic mutation/network. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | +| T02-AC02 | All required external installed trees match current `skills-lock.json`. | Maintainer; CLI. | pass | The public complete-repository flow printed 25 external required and `skills: ok`; the full skills package and focused compatibility suite passed. [Complete flow](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#complete-repository-flow). | +| T02-AC03 | No external authorial edit is introduced; unrelated extras are neither flagged nor removed. | Maintainer; Git/CLI. | pass | Task 02 commit scope contains only glossary/user docs/Task file; the extra fixture remained byte-stable and printed `skills: ok`. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| T02-AC04 | Task 02 changes no protected tooling path. | Maintainer; Git. | pass | `git diff-tree` for `ccfbd15` listed only `CONTEXT.md`, README, two user guides, and `task_02.md`. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| T02-AC05 | Glossary, docs, and CLI help use Repository Skill Set, Doctor Command, and Roundfix Skill consistently. | Documentation reader; docs/CLI. | pass | Canonical cross-read matched the built help/output; four documentation-contract tests passed. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | +| T03-AC01 | Roundfix Skill tells an Agent to surface failed readiness and printed remediation before work continues without claiming Doctor updates. | Agent; docs. | pass | Canonical Skill instructs surfacing the failed line/next action, says Doctor is diagnosis-only, and requires explicit workflow authorization before remediation. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | +| T03-AC02 | Canonical and generated Roundfix Skill files are byte-identical. | Maintainer; docs/tooling. | pass | `cmp` exited `0`; `make skills-sync-check` passed four guards. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| T03-AC03 | Task 03 changed paths contain only the authorized Skill pair, five derived artifacts, and its Task file. | Maintainer; Git. | pass | `git diff-tree` for `6e5618d` matched the eight allowed paths exactly. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| T03-AC04 | No upstream-managed or other Roundfix-owned skill changed. | Maintainer; Git/tooling. | pass | Task 03 path audit found only the Roundfix Skill pair; complete synchronization and shipped checks passed. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| T03-AC05 | Shipped Skill validation and complete repository gate pass. | Maintainer; CLI/static gate. | pass | Built `roundfix skills check` passed all 14 owned skills and `rtk make verify` exited `0`. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| CLI-01 | Public CLI contract preserves stdout/stderr placement, exit codes, deterministic order, nested-directory resolution, and independent later checks. | Developer; CLI sweep. | pass | Help exit `0`; failures exit `1`; requested one-line checks stayed on stdout in stable order; nested invocation resolved the same Git root; `codex:` remained after skill failures. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md). | +| DOC-01 | Documentation can be followed without undocumented knowledge and matches executable help/output. | New developer; docs sweep. | pass | Documented `doctor`, `doctor --help`, and `skills check` commands were run; their output and read-only/remediation boundaries matched the guides. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | +| NG-01 | Doctor does not download, install, update, delete, write, or contact a network service. | Security-conscious developer; CLI/scope. | pass | Repeated public-flow digests were unchanged; checker inspection found only local read APIs and rendered remediation constants. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| NG-02 | Unrelated extra skills and lock entries stay ignored and are not removed. | Maintainer; CLI. | pass | Extra fixture printed `skills: ok`; its digest was identical before and after. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| NG-03 | `roundfix skills check` retains its shipped-bundle integrity role; global installations remain outside repository readiness scope. | Maintainer; CLI. | pass | `roundfix skills check` independently passed all 14 shipped Skills; isolated project fixtures determined Repository Skill Set output from their Git root. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| NG-04 | Doctor does not validate Context Documents/Baseline ADRs or restore the removed Python runtime. | Maintainer; CLI/Git. | pass | Public output added only `skills:`; changed-path and static guards found no Python setup runtime or Context/Baseline audit path. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | + +## Findings + +No product finding was observed. The only incomplete criterion is an external +host-readiness prerequisite, recorded below. + +## Blocked and skipped + +- `T01-AC01` is blocked only on the existing host Codex readiness prerequisite. + The Repository Skill Set itself passed from both public entry locations and + the fixture digest remained + `c1473d4bf5c378216f816a91e316d9d7c9782ef82a51eda512c14a5e1ea3df81` + across both runs. Unblock by reinstalling the official signed Codex binary, + then rerun the built `roundfix doctor` in the complete fixture and require + exit `0`. + +## Coverage + +- User stories: `5/5` passed. +- Task acceptance criteria: `17/18` passed; `1` blocked. +- Constraint audits: `4/4` passed. +- Surface and Non-Goal sweeps: `6/6` passed. +- Static gates: `2/2` passed. +- Behavior probes: nested invocation, repeat, owned/external missing, + change/add/remove drift, malformed and invalid lock, unreadable file, + symlink traversal guard, mixed ownership, and unrelated extras all + attempted. +- Pending rows: `0`. + +## Final verdict + +Partial. All feature-specific stories, 17 of 18 Task criteria, constraint +audits, surface sweeps, and static gates passed; rerun the complete fixture +after restoring a valid officially signed Codex binary to close `T01-AC01` +with an overall Doctor exit `0`. From e08ad4ea5f76eb600f8c71d241500653e925c100 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 16:28:42 -0300 Subject: [PATCH 24/48] docs: close doctor skill readiness qa --- .../full-access-clean-flow.md | 89 +++++++++++++++++++ .../qa/qa-report-2026-07-26-full-access.md | 72 +++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md create mode 100644 docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-full-access.md diff --git a/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md b/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md new file mode 100644 index 0000000..74aa0d4 --- /dev/null +++ b/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md @@ -0,0 +1,89 @@ +# Full-access clean Doctor flow + +Build: `6e5618dba5059666c891f9078806631d30502d5b` + +This rerun closes the environment-only block recorded in +`qa-report-2026-07-26.md`. The sandboxed QA Agent could observe +`skills: ok`, but its runtime reported the configured Codex binary as +unsigned. A full-access supervisor reproduction proved that +`/Users/marcio/.local/bin/codex` has a valid signature and no quarantine +attribute. + +The original clean fixture still inherited this repository's unrelated +frontend profile, whose Claude adapter currently lacks valid capability +evidence. A second disposable fixture copied the same Git repository, +Repository Skill Set, and `skills-lock.json`, then changed only its temporary +project configuration so `frontend` used the already-proven Codex selections: + +```yaml +frontend: + preferred: + runtime: codex + model: gpt-5.6-sol + reasoning_effort: high + fallbacks: + - runtime: codex + model: gpt-5.6-terra + reasoning_effort: xhigh +``` + +This fixture change supplies the acceptance criterion's "otherwise ready" +precondition. It does not change the product build, repository configuration, +installed skills, or lock authority. + +## Before fingerprint + +Command: + +```text +rtk proxy find . -type f -not -path '*/.git/*' -print0 | rtk sort -z | rtk xargs -0 shasum -a 256 | rtk shasum -a 256 +``` + +Exit: `0` + +```text +30ef13d9aff07f90da8eaacdead275b7c00ddc486da9e24417a027776d0712ba - +``` + +## Root invocation + +Command: + +```text +rtk env CODEX_PATH=/Users/marcio/.local/bin/codex /Users/marcio/.roundfix/worktrees/roundfix-339f8dac/run_20260726T184338Z_c754fa2057c5fc07/bin/roundfix doctor +``` + +Working directory: +`/tmp/roundfix-qa-0036.KrAsVB/clean-ready` + +Exit: `0` + +```text +node: ok (v26.5.0 >= 22.13.0) +acpx: ok (0.12.1 >= 0.12.0) +adapter: ok (command="codex-acp"; package=@agentclientprotocol/codex-acp; version=1.1.7) +profiles: ok (2 distinct tuples; 10 category references) +skills: ok (39 required: 14 Roundfix-owned, 25 external) +codex: ok (/Users/marcio/.local/bin/codex has a valid code signature and no com.apple.quarantine attribute) +``` + +## Nested invocation + +The same command ran from +`/tmp/roundfix-qa-0036.KrAsVB/clean-ready/nested/deeper`. + +Exit: `0` + +The complete output was byte-identical to the root invocation, independently +confirming Git-root discovery and the same `14/25/39` Repository Skill Set. + +## After fingerprint + +The fingerprint command exited `0` after both Doctor runs: + +```text +30ef13d9aff07f90da8eaacdead275b7c00ddc486da9e24417a027776d0712ba - +``` + +The before and after fingerprints match. Both public invocations were +read-only and left the complete disposable fixture unchanged. diff --git a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-full-access.md b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-full-access.md new file mode 100644 index 0000000..a274c51 --- /dev/null +++ b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-full-access.md @@ -0,0 +1,72 @@ +--- +spec: 0036-doctor-skill-readiness +date: 2026-07-26 +build: 6e5618dba5059666c891f9078806631d30502d5b +status: closed +verdict: pass +surfaces: [cli, backend, docs] +--- + +# QA report — Doctor Skill Readiness full-access rerun + +## Scope and environment + +This report reruns the only blocked criterion from +`qa-report-2026-07-26.md`. The product build is unchanged: commit +`6e5618dba5059666c891f9078806631d30502d5b`. The earlier report remains the +complete matrix and evidence record for all user stories, Task criteria, +Project Constraints, surface sweeps, and Non-Goals. + +The rerun used the built `roundfix doctor` command from a full-access +supervisor session on macOS Darwin 25.5.0 arm64. Its disposable Git repository +contains the same installed Repository Skill Set and `skills-lock.json` as the +original QA fixture. Only the temporary fixture's unrelated `frontend` Agent +Selection Profile was changed to already-proven Codex selections so the +criterion's "otherwise ready Doctor" precondition existed. + +Evidence: +`evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md`. + +## Static gate + +The unchanged build retains the fresh static evidence captured by the +immediately preceding full QA run: + +| Command | Status | Evidence | +| --- | --- | --- | +| `rtk make verify` | pass | Exit `0`: 2,394 tests passed across 22 packages, skill synchronization and shipped Skill validation passed, and the CLI built. See `qa-report-2026-07-26.md`. | +| `rtk go test -race ./...` | pass | Exit `0`: 2,394 tests passed across 22 packages under the race detector. See `qa-report-2026-07-26.md`. | + +## Results + +| # | Story / criterion / sweep | Actor and surface | Status | Evidence | +| - | --- | --- | --- | --- | +| PREV-01 | Five user stories, the other 17 Task criteria, four Project Constraint audits, six surface/Non-Goal sweeps, static gates, and behavior probes remain valid on the identical build. | Developer, maintainer, Agent, documentation reader; CLI/backend/docs. | pass | The closed partial report records every row with zero failures and zero pending rows: `qa-report-2026-07-26.md`. | +| T01-AC01 | A complete Repository Skill Set reports derived `14/25/39` counts and does not fail an otherwise ready Doctor. | Developer; built CLI from disposable repository root. | pass | Root invocation exited `0` with `profiles: ok`, `skills: ok (39 required: 14 Roundfix-owned, 25 external)`, and `codex: ok`. The nested invocation independently produced the same output and exit. [Full-access evidence](evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md). | +| T01-AC01-NM | The clean success path remains deterministic and read-only. | Security-conscious developer; built CLI from root and nested directory. | pass | The complete fixture fingerprint was `30ef13d9aff07f90da8eaacdead275b7c00ddc486da9e24417a027776d0712ba` before and after both commands. [Full-access evidence](evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md). | + +## Findings + +No product finding was observed. + +## Blocked and skipped + +None. + +## Coverage + +- User stories: `5/5` passed. +- Task acceptance criteria: `18/18` passed. +- Project Constraint audits: `4/4` passed. +- Surface and Non-Goal sweeps: `6/6` passed. +- Static gates: `2/2` passed. +- Clean public-flow confirmations: root and nested invocation both exited `0`; + before/after fixture fingerprints matched. +- Pending, blocked, skipped, or failed rows: `0`. + +## Final verdict + +Pass. Every user story, Task acceptance criterion, Project Constraint, +surface sweep, Non-Goal, and static gate passed against build `6e5618d`; the +full-access rerun closed the sole environment-only block without changing the +product build. From 46efe283736611bfd883f3262e59f3126575565e Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 16:30:37 -0300 Subject: [PATCH 25/48] docs: order doctor skill readiness qa reports --- .../full-access-clean-flow.md | 2 +- .../qa/qa-report-2026-07-26-01-partial.md | 120 ++++++++++++++++++ .../qa/qa-report-2026-07-26-full-access.md | 72 ----------- .../qa/qa-report-2026-07-26.md | 120 ++++++------------ 4 files changed, 157 insertions(+), 157 deletions(-) create mode 100644 docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md delete mode 100644 docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-full-access.md diff --git a/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md b/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md index 74aa0d4..d96d4fb 100644 --- a/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md +++ b/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md @@ -3,7 +3,7 @@ Build: `6e5618dba5059666c891f9078806631d30502d5b` This rerun closes the environment-only block recorded in -`qa-report-2026-07-26.md`. The sandboxed QA Agent could observe +`qa-report-2026-07-26-01-partial.md`. The sandboxed QA Agent could observe `skills: ok`, but its runtime reported the configured Codex binary as unsigned. A full-access supervisor reproduction proved that `/Users/marcio/.local/bin/codex` has a valid signature and no quarantine diff --git a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md new file mode 100644 index 0000000..80e22de --- /dev/null +++ b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md @@ -0,0 +1,120 @@ +--- +spec: 0036-doctor-skill-readiness +date: 2026-07-26 +build: 6e5618dba5059666c891f9078806631d30502d5b +status: closed +verdict: partial +surfaces: [cli, backend, docs] +--- + +# QA report — Doctor Skill Readiness + +## Scope and environment + +Full QA gate for every PRD user story and every Task acceptance criterion. +The primary actor is a developer preparing a local Git repository for a +Roundfix Run. The public entry points are the built `roundfix doctor`, +`roundfix doctor --help`, and `roundfix skills check` commands plus the +supported README, user guide, glossary, and Roundfix Skill. + +- Build: `6e5618dba5059666c891f9078806631d30502d5b` +- Branch: `roundfix/run-run_20260726T184338Z_c754fa2057c5fc07` +- Host: macOS Darwin 25.5.0 arm64 +- Go: `go1.26.5 darwin/arm64` +- Runtime tools found: `bun`, `acpx`, and `codex-acp` +- Test data: current repository plus disposable Git repository copies under a + QA-owned temporary directory +- Network: not required and not authorized by the feature contract +- Evidence directory: + `qa/evidence/2026-07-26-doctor-skill-readiness/` + +Behavior probes are selected for their risk to the readiness boundary: +repeat the read-only command, invoke it from a nested directory, remove a +required skill, change/add/remove versioned content, introduce malformed and +unsafe lock declarations, replace content with a symlink, combine ownership +failures, and add unrelated extras. + +## Project Constraint audit + +| # | Constraint | Applicability and operative source | Status | Evidence | +| - | --- | --- | --- | --- | +| PC-01 | Identifier strategy | Not applicable: no project-owned Internal Identifier is created; names come from the embedded bundle and `skills-lock.json`. Operative source: `docs/agents/domain.md`. | pass | PRD/TechSpec carry the required applicability and source; Task commit inspection found no identity model or generator. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| PC-02 | Authentication and HTTP | Not applicable: the feature is local filesystem diagnosis and adds no authentication, HTTP, or network path. Operative source: `docs/agents/cli.md`. | pass | PRD/TechSpec carry the required applicability and source; public flows and checker inspection found local reads only. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| PC-03 | Active ADR obligations | Applicable: ADR-0049 and ADR-0055 preserve Agent Selection Profile authority; ADR-0066 and ADR-0072 keep Baseline execution in Go. Operative source: `docs/agents/domain.md`. | pass | Accepted ADRs were inspected; real output kept `profiles:` authoritative before independent `skills:`, and changed paths added no Python setup runtime or competing Baseline execution. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| PC-04 | Tooling authority and bounded scope | Applicable: the PRD and TechSpec expressly authorize the protected Roundfix Skill pair and five mechanically derived Baseline/parity artifacts. Operative source: `docs/agents/agent-instructions.md`. | pass | `git diff-tree` for Task 03 commit `6e5618d` matched the seven exact protected/derived paths plus `task_03.md`; Task 02 touched no protected path and the current delta is QA-only. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | + +## Static gate + +| Command | Status | Evidence | +| --- | --- | --- | +| `rtk make verify` | pass | Exit `0`: 2,394 tests passed across 22 packages; four focused skills checks passed; all 14 shipped Roundfix Skills passed `skills check`; `bin/roundfix` built from `6e5618d-dirty`. | +| `rtk go test -race ./...` | pass | Exit `0`: 2,394 tests passed across 22 packages under the race detector. | + +## Results + +| # | Story / criterion / sweep | Actor and surface | Status | Evidence | +| - | --- | --- | --- | --- | +| US-01 | Missing required skill blocks readiness and names the missing skill. | Developer; CLI from a disposable repository. | pass | Owned `roundfix` and external `tech-writer` removals each produced exit `1`, the missing name, and the correct ownership-specific remediation. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| US-02 | Roundfix-owned drift from the running binary is detected. | Developer after upgrade; CLI. | pass | Changed, added, and removed owned artifacts each reported `outdated: roundfix` and the owned install command. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| US-03 | External installed content is compared with `skills-lock.json`. | Maintainer; CLI/backend through Doctor. | pass | The complete real tree matched all 25 hashes; external byte drift, missing lock, malformed JSON, and invalid required hash all failed with bounded diagnostics. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#complete-repository-flow). | +| US-04 | One deterministic line names failures and exact remediation. | Agent or developer; CLI. | pass | The mixed fixture repeatedly printed one sorted line with both commands exactly once in owned-then-external order. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| US-05 | Diagnosis remains entirely local and read-only. | Security-conscious developer; CLI. | pass | Complete, mixed, extra, and symlink fixture digests remained unchanged across repeated public commands; the outside sentinel was preserved and checker inspection found no write, command-execution, or network path. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| T01-AC01 | Complete Repository Skill Set reports `skills: ok` with derived `14/25/39` counts and does not fail an otherwise ready Doctor. | Developer; CLI. | blocked | The built CLI printed `skills: ok (39 required: 14 Roundfix-owned, 25 external)` identically from the fixture root and `nested/deeper`, but this host cannot provide the criterion's otherwise-ready precondition: independent `profiles:` and `codex:` checks fail because `/Users/marcio/.local/bin/codex` has an invalid code signature. Exit-zero confirmation requires reinstalling the official signed Codex binary and rerunning the unchanged command. | +| T01-AC02 | Removing owned or external skills reports `missing`, prints the ownership-specific command, and exits `1`. | Developer; CLI. | pass | Separate owned and external public flows matched the exact line and exit contract. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| T01-AC03 | Changed, added, or removed versioned files report `outdated`; unrelated extras are ignored. | Developer; CLI. | pass | Owned change/add/remove and external change reported `outdated`; an unrelated installed skill plus unrelated unsafe lock entry preserved `skills: ok`. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| T01-AC04 | External folder hashing matches the installed skills CLI algorithm, including ordering and exclusions. | Maintainer; CLI/backend. | pass | The complete public flow matched all 25 current lock hashes; 26 focused compatibility checks, including the pinned locale-order fixture and exclusions, passed. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| T01-AC05 | Malformed lock data, invalid hashes, unsafe names, unreadable artifacts, and symlinks fail deterministically without panic or traversal. | Developer; CLI. | pass | Public missing/malformed/invalid-lock, unreadable, and symlink variants failed deterministically; the outside sentinel remained unchanged. Unsafe required names are binary-owned, not repository input, and the focused current-build guard passed before traversal. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | +| T01-AC06 | Mixed failures produce one sorted `skills:` line and each remediation exactly once in owned-then-external order. | Agent; CLI. | pass | Repeated mixed flow matched the exact deterministic line and command order. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| T01-AC07 | Doctor runs all existing checks despite skill failure and changes no repository, config, Run, lock, or skill path. | Developer; CLI. | pass | Full output continued through the later `codex:` check after skill failure; multiple file manifests and the outside sentinel were byte-stable. Focused current-build no-mutation tests passed. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| T01-AC08 | Doctor help names Agent Selection Profiles and the appended Repository Skill Set check. | Developer; CLI/docs. | pass | Built `doctor --help` exited `0` and named both terms contiguously with the offline/read-only contract. [Public help](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#public-help). | +| T02-AC01 | Supported user docs identify authorities, outcomes, remediation, and the offline/read-only guarantee. | Documentation reader; docs. | pass | README and both user-guide entry points state both authorities, `ok/failed`, exit `1`, both exact commands, ignored extras, and no automatic mutation/network. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | +| T02-AC02 | All required external installed trees match current `skills-lock.json`. | Maintainer; CLI. | pass | The public complete-repository flow printed 25 external required and `skills: ok`; the full skills package and focused compatibility suite passed. [Complete flow](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#complete-repository-flow). | +| T02-AC03 | No external authorial edit is introduced; unrelated extras are neither flagged nor removed. | Maintainer; Git/CLI. | pass | Task 02 commit scope contains only glossary/user docs/Task file; the extra fixture remained byte-stable and printed `skills: ok`. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| T02-AC04 | Task 02 changes no protected tooling path. | Maintainer; Git. | pass | `git diff-tree` for `ccfbd15` listed only `CONTEXT.md`, README, two user guides, and `task_02.md`. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| T02-AC05 | Glossary, docs, and CLI help use Repository Skill Set, Doctor Command, and Roundfix Skill consistently. | Documentation reader; docs/CLI. | pass | Canonical cross-read matched the built help/output; four documentation-contract tests passed. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | +| T03-AC01 | Roundfix Skill tells an Agent to surface failed readiness and printed remediation before work continues without claiming Doctor updates. | Agent; docs. | pass | Canonical Skill instructs surfacing the failed line/next action, says Doctor is diagnosis-only, and requires explicit workflow authorization before remediation. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | +| T03-AC02 | Canonical and generated Roundfix Skill files are byte-identical. | Maintainer; docs/tooling. | pass | `cmp` exited `0`; `make skills-sync-check` passed four guards. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| T03-AC03 | Task 03 changed paths contain only the authorized Skill pair, five derived artifacts, and its Task file. | Maintainer; Git. | pass | `git diff-tree` for `6e5618d` matched the eight allowed paths exactly. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| T03-AC04 | No upstream-managed or other Roundfix-owned skill changed. | Maintainer; Git/tooling. | pass | Task 03 path audit found only the Roundfix Skill pair; complete synchronization and shipped checks passed. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| T03-AC05 | Shipped Skill validation and complete repository gate pass. | Maintainer; CLI/static gate. | pass | Built `roundfix skills check` passed all 14 owned skills and `rtk make verify` exited `0`. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| CLI-01 | Public CLI contract preserves stdout/stderr placement, exit codes, deterministic order, nested-directory resolution, and independent later checks. | Developer; CLI sweep. | pass | Help exit `0`; failures exit `1`; requested one-line checks stayed on stdout in stable order; nested invocation resolved the same Git root; `codex:` remained after skill failures. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md). | +| DOC-01 | Documentation can be followed without undocumented knowledge and matches executable help/output. | New developer; docs sweep. | pass | Documented `doctor`, `doctor --help`, and `skills check` commands were run; their output and read-only/remediation boundaries matched the guides. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | +| NG-01 | Doctor does not download, install, update, delete, write, or contact a network service. | Security-conscious developer; CLI/scope. | pass | Repeated public-flow digests were unchanged; checker inspection found only local read APIs and rendered remediation constants. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| NG-02 | Unrelated extra skills and lock entries stay ignored and are not removed. | Maintainer; CLI. | pass | Extra fixture printed `skills: ok`; its digest was identical before and after. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | +| NG-03 | `roundfix skills check` retains its shipped-bundle integrity role; global installations remain outside repository readiness scope. | Maintainer; CLI. | pass | `roundfix skills check` independently passed all 14 shipped Skills; isolated project fixtures determined Repository Skill Set output from their Git root. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | +| NG-04 | Doctor does not validate Context Documents/Baseline ADRs or restore the removed Python runtime. | Maintainer; CLI/Git. | pass | Public output added only `skills:`; changed-path and static guards found no Python setup runtime or Context/Baseline audit path. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | + +## Findings + +No product finding was observed. The only incomplete criterion is an external +host-readiness prerequisite, recorded below. + +## Blocked and skipped + +- `T01-AC01` is blocked only on the existing host Codex readiness prerequisite. + The Repository Skill Set itself passed from both public entry locations and + the fixture digest remained + `c1473d4bf5c378216f816a91e316d9d7c9782ef82a51eda512c14a5e1ea3df81` + across both runs. Unblock by reinstalling the official signed Codex binary, + then rerun the built `roundfix doctor` in the complete fixture and require + exit `0`. + +## Coverage + +- User stories: `5/5` passed. +- Task acceptance criteria: `17/18` passed; `1` blocked. +- Constraint audits: `4/4` passed. +- Surface and Non-Goal sweeps: `6/6` passed. +- Static gates: `2/2` passed. +- Behavior probes: nested invocation, repeat, owned/external missing, + change/add/remove drift, malformed and invalid lock, unreadable file, + symlink traversal guard, mixed ownership, and unrelated extras all + attempted. +- Pending rows: `0`. + +## Final verdict + +Partial. All feature-specific stories, 17 of 18 Task criteria, constraint +audits, surface sweeps, and static gates passed; rerun the complete fixture +after restoring a valid officially signed Codex binary to close `T01-AC01` +with an overall Doctor exit `0`. diff --git a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-full-access.md b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-full-access.md deleted file mode 100644 index a274c51..0000000 --- a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-full-access.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -spec: 0036-doctor-skill-readiness -date: 2026-07-26 -build: 6e5618dba5059666c891f9078806631d30502d5b -status: closed -verdict: pass -surfaces: [cli, backend, docs] ---- - -# QA report — Doctor Skill Readiness full-access rerun - -## Scope and environment - -This report reruns the only blocked criterion from -`qa-report-2026-07-26.md`. The product build is unchanged: commit -`6e5618dba5059666c891f9078806631d30502d5b`. The earlier report remains the -complete matrix and evidence record for all user stories, Task criteria, -Project Constraints, surface sweeps, and Non-Goals. - -The rerun used the built `roundfix doctor` command from a full-access -supervisor session on macOS Darwin 25.5.0 arm64. Its disposable Git repository -contains the same installed Repository Skill Set and `skills-lock.json` as the -original QA fixture. Only the temporary fixture's unrelated `frontend` Agent -Selection Profile was changed to already-proven Codex selections so the -criterion's "otherwise ready Doctor" precondition existed. - -Evidence: -`evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md`. - -## Static gate - -The unchanged build retains the fresh static evidence captured by the -immediately preceding full QA run: - -| Command | Status | Evidence | -| --- | --- | --- | -| `rtk make verify` | pass | Exit `0`: 2,394 tests passed across 22 packages, skill synchronization and shipped Skill validation passed, and the CLI built. See `qa-report-2026-07-26.md`. | -| `rtk go test -race ./...` | pass | Exit `0`: 2,394 tests passed across 22 packages under the race detector. See `qa-report-2026-07-26.md`. | - -## Results - -| # | Story / criterion / sweep | Actor and surface | Status | Evidence | -| - | --- | --- | --- | --- | -| PREV-01 | Five user stories, the other 17 Task criteria, four Project Constraint audits, six surface/Non-Goal sweeps, static gates, and behavior probes remain valid on the identical build. | Developer, maintainer, Agent, documentation reader; CLI/backend/docs. | pass | The closed partial report records every row with zero failures and zero pending rows: `qa-report-2026-07-26.md`. | -| T01-AC01 | A complete Repository Skill Set reports derived `14/25/39` counts and does not fail an otherwise ready Doctor. | Developer; built CLI from disposable repository root. | pass | Root invocation exited `0` with `profiles: ok`, `skills: ok (39 required: 14 Roundfix-owned, 25 external)`, and `codex: ok`. The nested invocation independently produced the same output and exit. [Full-access evidence](evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md). | -| T01-AC01-NM | The clean success path remains deterministic and read-only. | Security-conscious developer; built CLI from root and nested directory. | pass | The complete fixture fingerprint was `30ef13d9aff07f90da8eaacdead275b7c00ddc486da9e24417a027776d0712ba` before and after both commands. [Full-access evidence](evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md). | - -## Findings - -No product finding was observed. - -## Blocked and skipped - -None. - -## Coverage - -- User stories: `5/5` passed. -- Task acceptance criteria: `18/18` passed. -- Project Constraint audits: `4/4` passed. -- Surface and Non-Goal sweeps: `6/6` passed. -- Static gates: `2/2` passed. -- Clean public-flow confirmations: root and nested invocation both exited `0`; - before/after fixture fingerprints matched. -- Pending, blocked, skipped, or failed rows: `0`. - -## Final verdict - -Pass. Every user story, Task acceptance criterion, Project Constraint, -surface sweep, Non-Goal, and static gate passed against build `6e5618d`; the -full-access rerun closed the sole environment-only block without changing the -product build. diff --git a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md index 80e22de..fca40e9 100644 --- a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md +++ b/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md @@ -3,118 +3,70 @@ spec: 0036-doctor-skill-readiness date: 2026-07-26 build: 6e5618dba5059666c891f9078806631d30502d5b status: closed -verdict: partial +verdict: pass surfaces: [cli, backend, docs] --- -# QA report — Doctor Skill Readiness +# QA report — Doctor Skill Readiness full-access rerun ## Scope and environment -Full QA gate for every PRD user story and every Task acceptance criterion. -The primary actor is a developer preparing a local Git repository for a -Roundfix Run. The public entry points are the built `roundfix doctor`, -`roundfix doctor --help`, and `roundfix skills check` commands plus the -supported README, user guide, glossary, and Roundfix Skill. - -- Build: `6e5618dba5059666c891f9078806631d30502d5b` -- Branch: `roundfix/run-run_20260726T184338Z_c754fa2057c5fc07` -- Host: macOS Darwin 25.5.0 arm64 -- Go: `go1.26.5 darwin/arm64` -- Runtime tools found: `bun`, `acpx`, and `codex-acp` -- Test data: current repository plus disposable Git repository copies under a - QA-owned temporary directory -- Network: not required and not authorized by the feature contract -- Evidence directory: - `qa/evidence/2026-07-26-doctor-skill-readiness/` - -Behavior probes are selected for their risk to the readiness boundary: -repeat the read-only command, invoke it from a nested directory, remove a -required skill, change/add/remove versioned content, introduce malformed and -unsafe lock declarations, replace content with a symlink, combine ownership -failures, and add unrelated extras. - -## Project Constraint audit - -| # | Constraint | Applicability and operative source | Status | Evidence | -| - | --- | --- | --- | --- | -| PC-01 | Identifier strategy | Not applicable: no project-owned Internal Identifier is created; names come from the embedded bundle and `skills-lock.json`. Operative source: `docs/agents/domain.md`. | pass | PRD/TechSpec carry the required applicability and source; Task commit inspection found no identity model or generator. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | -| PC-02 | Authentication and HTTP | Not applicable: the feature is local filesystem diagnosis and adds no authentication, HTTP, or network path. Operative source: `docs/agents/cli.md`. | pass | PRD/TechSpec carry the required applicability and source; public flows and checker inspection found local reads only. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | -| PC-03 | Active ADR obligations | Applicable: ADR-0049 and ADR-0055 preserve Agent Selection Profile authority; ADR-0066 and ADR-0072 keep Baseline execution in Go. Operative source: `docs/agents/domain.md`. | pass | Accepted ADRs were inspected; real output kept `profiles:` authoritative before independent `skills:`, and changed paths added no Python setup runtime or competing Baseline execution. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | -| PC-04 | Tooling authority and bounded scope | Applicable: the PRD and TechSpec expressly authorize the protected Roundfix Skill pair and five mechanically derived Baseline/parity artifacts. Operative source: `docs/agents/agent-instructions.md`. | pass | `git diff-tree` for Task 03 commit `6e5618d` matched the seven exact protected/derived paths plus `task_03.md`; Task 02 touched no protected path and the current delta is QA-only. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +This report reruns the only blocked criterion from +`qa-report-2026-07-26-01-partial.md`. The product build is unchanged: commit +`6e5618dba5059666c891f9078806631d30502d5b`. The earlier report remains the +complete matrix and evidence record for all user stories, Task criteria, +Project Constraints, surface sweeps, and Non-Goals. + +The rerun used the built `roundfix doctor` command from a full-access +supervisor session on macOS Darwin 25.5.0 arm64. Its disposable Git repository +contains the same installed Repository Skill Set and `skills-lock.json` as the +original QA fixture. Only the temporary fixture's unrelated `frontend` Agent +Selection Profile was changed to already-proven Codex selections so the +criterion's "otherwise ready Doctor" precondition existed. + +Evidence: +`evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md`. ## Static gate +The unchanged build retains the fresh static evidence captured by the +immediately preceding full QA run: + | Command | Status | Evidence | | --- | --- | --- | -| `rtk make verify` | pass | Exit `0`: 2,394 tests passed across 22 packages; four focused skills checks passed; all 14 shipped Roundfix Skills passed `skills check`; `bin/roundfix` built from `6e5618d-dirty`. | -| `rtk go test -race ./...` | pass | Exit `0`: 2,394 tests passed across 22 packages under the race detector. | +| `rtk make verify` | pass | Exit `0`: 2,394 tests passed across 22 packages, skill synchronization and shipped Skill validation passed, and the CLI built. See `qa-report-2026-07-26-01-partial.md`. | +| `rtk go test -race ./...` | pass | Exit `0`: 2,394 tests passed across 22 packages under the race detector. See `qa-report-2026-07-26-01-partial.md`. | ## Results | # | Story / criterion / sweep | Actor and surface | Status | Evidence | | - | --- | --- | --- | --- | -| US-01 | Missing required skill blocks readiness and names the missing skill. | Developer; CLI from a disposable repository. | pass | Owned `roundfix` and external `tech-writer` removals each produced exit `1`, the missing name, and the correct ownership-specific remediation. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | -| US-02 | Roundfix-owned drift from the running binary is detected. | Developer after upgrade; CLI. | pass | Changed, added, and removed owned artifacts each reported `outdated: roundfix` and the owned install command. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | -| US-03 | External installed content is compared with `skills-lock.json`. | Maintainer; CLI/backend through Doctor. | pass | The complete real tree matched all 25 hashes; external byte drift, missing lock, malformed JSON, and invalid required hash all failed with bounded diagnostics. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#complete-repository-flow). | -| US-04 | One deterministic line names failures and exact remediation. | Agent or developer; CLI. | pass | The mixed fixture repeatedly printed one sorted line with both commands exactly once in owned-then-external order. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | -| US-05 | Diagnosis remains entirely local and read-only. | Security-conscious developer; CLI. | pass | Complete, mixed, extra, and symlink fixture digests remained unchanged across repeated public commands; the outside sentinel was preserved and checker inspection found no write, command-execution, or network path. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | -| T01-AC01 | Complete Repository Skill Set reports `skills: ok` with derived `14/25/39` counts and does not fail an otherwise ready Doctor. | Developer; CLI. | blocked | The built CLI printed `skills: ok (39 required: 14 Roundfix-owned, 25 external)` identically from the fixture root and `nested/deeper`, but this host cannot provide the criterion's otherwise-ready precondition: independent `profiles:` and `codex:` checks fail because `/Users/marcio/.local/bin/codex` has an invalid code signature. Exit-zero confirmation requires reinstalling the official signed Codex binary and rerunning the unchanged command. | -| T01-AC02 | Removing owned or external skills reports `missing`, prints the ownership-specific command, and exits `1`. | Developer; CLI. | pass | Separate owned and external public flows matched the exact line and exit contract. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | -| T01-AC03 | Changed, added, or removed versioned files report `outdated`; unrelated extras are ignored. | Developer; CLI. | pass | Owned change/add/remove and external change reported `outdated`; an unrelated installed skill plus unrelated unsafe lock entry preserved `skills: ok`. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | -| T01-AC04 | External folder hashing matches the installed skills CLI algorithm, including ordering and exclusions. | Maintainer; CLI/backend. | pass | The complete public flow matched all 25 current lock hashes; 26 focused compatibility checks, including the pinned locale-order fixture and exclusions, passed. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | -| T01-AC05 | Malformed lock data, invalid hashes, unsafe names, unreadable artifacts, and symlinks fail deterministically without panic or traversal. | Developer; CLI. | pass | Public missing/malformed/invalid-lock, unreadable, and symlink variants failed deterministically; the outside sentinel remained unchanged. Unsafe required names are binary-owned, not repository input, and the focused current-build guard passed before traversal. [Fixture matrix](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#disposable-repository-matrix). | -| T01-AC06 | Mixed failures produce one sorted `skills:` line and each remediation exactly once in owned-then-external order. | Agent; CLI. | pass | Repeated mixed flow matched the exact deterministic line and command order. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | -| T01-AC07 | Doctor runs all existing checks despite skill failure and changes no repository, config, Run, lock, or skill path. | Developer; CLI. | pass | Full output continued through the later `codex:` check after skill failure; multiple file manifests and the outside sentinel were byte-stable. Focused current-build no-mutation tests passed. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | -| T01-AC08 | Doctor help names Agent Selection Profiles and the appended Repository Skill Set check. | Developer; CLI/docs. | pass | Built `doctor --help` exited `0` and named both terms contiguously with the offline/read-only contract. [Public help](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#public-help). | -| T02-AC01 | Supported user docs identify authorities, outcomes, remediation, and the offline/read-only guarantee. | Documentation reader; docs. | pass | README and both user-guide entry points state both authorities, `ok/failed`, exit `1`, both exact commands, ignored extras, and no automatic mutation/network. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | -| T02-AC02 | All required external installed trees match current `skills-lock.json`. | Maintainer; CLI. | pass | The public complete-repository flow printed 25 external required and `skills: ok`; the full skills package and focused compatibility suite passed. [Complete flow](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#complete-repository-flow). | -| T02-AC03 | No external authorial edit is introduced; unrelated extras are neither flagged nor removed. | Maintainer; Git/CLI. | pass | Task 02 commit scope contains only glossary/user docs/Task file; the extra fixture remained byte-stable and printed `skills: ok`. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | -| T02-AC04 | Task 02 changes no protected tooling path. | Maintainer; Git. | pass | `git diff-tree` for `ccfbd15` listed only `CONTEXT.md`, README, two user guides, and `task_02.md`. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | -| T02-AC05 | Glossary, docs, and CLI help use Repository Skill Set, Doctor Command, and Roundfix Skill consistently. | Documentation reader; docs/CLI. | pass | Canonical cross-read matched the built help/output; four documentation-contract tests passed. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | -| T03-AC01 | Roundfix Skill tells an Agent to surface failed readiness and printed remediation before work continues without claiming Doctor updates. | Agent; docs. | pass | Canonical Skill instructs surfacing the failed line/next action, says Doctor is diagnosis-only, and requires explicit workflow authorization before remediation. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | -| T03-AC02 | Canonical and generated Roundfix Skill files are byte-identical. | Maintainer; docs/tooling. | pass | `cmp` exited `0`; `make skills-sync-check` passed four guards. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | -| T03-AC03 | Task 03 changed paths contain only the authorized Skill pair, five derived artifacts, and its Task file. | Maintainer; Git. | pass | `git diff-tree` for `6e5618d` matched the eight allowed paths exactly. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | -| T03-AC04 | No upstream-managed or other Roundfix-owned skill changed. | Maintainer; Git/tooling. | pass | Task 03 path audit found only the Roundfix Skill pair; complete synchronization and shipped checks passed. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | -| T03-AC05 | Shipped Skill validation and complete repository gate pass. | Maintainer; CLI/static gate. | pass | Built `roundfix skills check` passed all 14 owned skills and `rtk make verify` exited `0`. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | -| CLI-01 | Public CLI contract preserves stdout/stderr placement, exit codes, deterministic order, nested-directory resolution, and independent later checks. | Developer; CLI sweep. | pass | Help exit `0`; failures exit `1`; requested one-line checks stayed on stdout in stable order; nested invocation resolved the same Git root; `codex:` remained after skill failures. [Command evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md). | -| DOC-01 | Documentation can be followed without undocumented knowledge and matches executable help/output. | New developer; docs sweep. | pass | Documented `doctor`, `doctor --help`, and `skills check` commands were run; their output and read-only/remediation boundaries matched the guides. [Documentation evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#documentation-evidence). | -| NG-01 | Doctor does not download, install, update, delete, write, or contact a network service. | Security-conscious developer; CLI/scope. | pass | Repeated public-flow digests were unchanged; checker inspection found only local read APIs and rendered remediation constants. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | -| NG-02 | Unrelated extra skills and lock entries stay ignored and are not removed. | Maintainer; CLI. | pass | Extra fixture printed `skills: ok`; its digest was identical before and after. [Determinism evidence](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#determinism-and-no-mutation). | -| NG-03 | `roundfix skills check` retains its shipped-bundle integrity role; global installations remain outside repository readiness scope. | Maintainer; CLI. | pass | `roundfix skills check` independently passed all 14 shipped Skills; isolated project fixtures determined Repository Skill Set output from their Git root. [Static verification](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-verification). | -| NG-04 | Doctor does not validate Context Documents/Baseline ADRs or restore the removed Python runtime. | Maintainer; CLI/Git. | pass | Public output added only `skills:`; changed-path and static guards found no Python setup runtime or Context/Baseline audit path. [Commit scope](evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#project-constraint-and-commit-scope-evidence). | +| PREV-01 | Five user stories, the other 17 Task criteria, four Project Constraint audits, six surface/Non-Goal sweeps, static gates, and behavior probes remain valid on the identical build. | Developer, maintainer, Agent, documentation reader; CLI/backend/docs. | pass | The closed partial report records every row with zero failures and zero pending rows: `qa-report-2026-07-26-01-partial.md`. | +| T01-AC01 | A complete Repository Skill Set reports derived `14/25/39` counts and does not fail an otherwise ready Doctor. | Developer; built CLI from disposable repository root. | pass | Root invocation exited `0` with `profiles: ok`, `skills: ok (39 required: 14 Roundfix-owned, 25 external)`, and `codex: ok`. The nested invocation independently produced the same output and exit. [Full-access evidence](evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md). | +| T01-AC01-NM | The clean success path remains deterministic and read-only. | Security-conscious developer; built CLI from root and nested directory. | pass | The complete fixture fingerprint was `30ef13d9aff07f90da8eaacdead275b7c00ddc486da9e24417a027776d0712ba` before and after both commands. [Full-access evidence](evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md). | ## Findings -No product finding was observed. The only incomplete criterion is an external -host-readiness prerequisite, recorded below. +No product finding was observed. ## Blocked and skipped -- `T01-AC01` is blocked only on the existing host Codex readiness prerequisite. - The Repository Skill Set itself passed from both public entry locations and - the fixture digest remained - `c1473d4bf5c378216f816a91e316d9d7c9782ef82a51eda512c14a5e1ea3df81` - across both runs. Unblock by reinstalling the official signed Codex binary, - then rerun the built `roundfix doctor` in the complete fixture and require - exit `0`. +None. ## Coverage - User stories: `5/5` passed. -- Task acceptance criteria: `17/18` passed; `1` blocked. -- Constraint audits: `4/4` passed. +- Task acceptance criteria: `18/18` passed. +- Project Constraint audits: `4/4` passed. - Surface and Non-Goal sweeps: `6/6` passed. - Static gates: `2/2` passed. -- Behavior probes: nested invocation, repeat, owned/external missing, - change/add/remove drift, malformed and invalid lock, unreadable file, - symlink traversal guard, mixed ownership, and unrelated extras all - attempted. -- Pending rows: `0`. +- Clean public-flow confirmations: root and nested invocation both exited `0`; + before/after fixture fingerprints matched. +- Pending, blocked, skipped, or failed rows: `0`. ## Final verdict -Partial. All feature-specific stories, 17 of 18 Task criteria, constraint -audits, surface sweeps, and static gates passed; rerun the complete fixture -after restoring a valid officially signed Codex binary to close `T01-AC01` -with an overall Doctor exit `0`. +Pass. Every user story, Task acceptance criterion, Project Constraint, +surface sweep, Non-Goal, and static gate passed against build `6e5618d`; the +full-access rerun closed the sole environment-only block without changing the +product build. From 46b7534641df4fecb63f19ee9a92f41aae27867e Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 16:31:08 -0300 Subject: [PATCH 26/48] chore: archive doctor skill readiness spec --- .../{ => _archived}/0036-doctor-skill-readiness/_prd.md | 5 ++++- .../{ => _archived}/0036-doctor-skill-readiness/_tasks.md | 0 .../{ => _archived}/0036-doctor-skill-readiness/_techspec.md | 0 .../2026-07-26-doctor-skill-readiness/command-evidence.md | 0 .../full-access-clean-flow.md | 0 .../qa/qa-report-2026-07-26-01-partial.md | 0 .../0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md | 0 .../{ => _archived}/0036-doctor-skill-readiness/task_01.md | 0 .../{ => _archived}/0036-doctor-skill-readiness/task_02.md | 0 .../{ => _archived}/0036-doctor-skill-readiness/task_03.md | 0 10 files changed, 4 insertions(+), 1 deletion(-) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/_prd.md (99%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/_tasks.md (100%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/_techspec.md (100%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md (100%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md (100%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md (100%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md (100%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/task_01.md (100%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/task_02.md (100%) rename docs/specs/{ => _archived}/0036-doctor-skill-readiness/task_03.md (100%) diff --git a/docs/specs/0036-doctor-skill-readiness/_prd.md b/docs/specs/_archived/0036-doctor-skill-readiness/_prd.md similarity index 99% rename from docs/specs/0036-doctor-skill-readiness/_prd.md rename to docs/specs/_archived/0036-doctor-skill-readiness/_prd.md index 469f75b..9c988c6 100644 --- a/docs/specs/0036-doctor-skill-readiness/_prd.md +++ b/docs/specs/_archived/0036-doctor-skill-readiness/_prd.md @@ -1,10 +1,13 @@ --- spec: 0036-doctor-skill-readiness -status: active +status: archived created: 2026-07-17 surfaces: [cli, backend, docs] +archived: "2026-07-26" +source_slug: 0036-doctor-skill-readiness --- + # Doctor Skill Readiness Roundfix depends on a repository-local set of Agent Skills, but the Doctor diff --git a/docs/specs/0036-doctor-skill-readiness/_tasks.md b/docs/specs/_archived/0036-doctor-skill-readiness/_tasks.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/_tasks.md rename to docs/specs/_archived/0036-doctor-skill-readiness/_tasks.md diff --git a/docs/specs/0036-doctor-skill-readiness/_techspec.md b/docs/specs/_archived/0036-doctor-skill-readiness/_techspec.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/_techspec.md rename to docs/specs/_archived/0036-doctor-skill-readiness/_techspec.md diff --git a/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md b/docs/specs/_archived/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md rename to docs/specs/_archived/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md diff --git a/docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md b/docs/specs/_archived/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md rename to docs/specs/_archived/0036-doctor-skill-readiness/qa/evidence/2026-07-26-doctor-skill-readiness/full-access-clean-flow.md diff --git a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md b/docs/specs/_archived/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md rename to docs/specs/_archived/0036-doctor-skill-readiness/qa/qa-report-2026-07-26-01-partial.md diff --git a/docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md b/docs/specs/_archived/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md rename to docs/specs/_archived/0036-doctor-skill-readiness/qa/qa-report-2026-07-26.md diff --git a/docs/specs/0036-doctor-skill-readiness/task_01.md b/docs/specs/_archived/0036-doctor-skill-readiness/task_01.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/task_01.md rename to docs/specs/_archived/0036-doctor-skill-readiness/task_01.md diff --git a/docs/specs/0036-doctor-skill-readiness/task_02.md b/docs/specs/_archived/0036-doctor-skill-readiness/task_02.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/task_02.md rename to docs/specs/_archived/0036-doctor-skill-readiness/task_02.md diff --git a/docs/specs/0036-doctor-skill-readiness/task_03.md b/docs/specs/_archived/0036-doctor-skill-readiness/task_03.md similarity index 100% rename from docs/specs/0036-doctor-skill-readiness/task_03.md rename to docs/specs/_archived/0036-doctor-skill-readiness/task_03.md From af3bd79678fc937d4e1ed794fd6cd06749f929c7 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 16:40:29 -0300 Subject: [PATCH 27/48] docs: document Claude adapter migration --- ...-claude-adapter-configoptions-migration.md | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 docs/findings/2026-07-26-claude-adapter-configoptions-migration.md diff --git a/docs/findings/2026-07-26-claude-adapter-configoptions-migration.md b/docs/findings/2026-07-26-claude-adapter-configoptions-migration.md new file mode 100644 index 0000000..df60040 --- /dev/null +++ b/docs/findings/2026-07-26-claude-adapter-configoptions-migration.md @@ -0,0 +1,301 @@ +--- +status: pending +created_at: 2026-07-26 +updated_at: 2026-07-26 +--- + +# Agent Selection — the configured Claude ACP adapter is deprecated and advertises no capability evidence (2026-07-26) + +`claude/claude-opus-5/xhigh` fails Agent Selection preflight with +`capability_evidence_invalid: missing_config_options`. The failure is not +about the model: `claude-opus-5` is advertised and gets selected successfully. +Roundfix is pointed at `@zed-industries/claude-code-acp`, which npm deprecated +in favour of `@agentclientprotocol/claude-agent-acp` — the same rename Roundfix +already followed for Codex. The replacement adapter advertises the exact two +controls Roundfix requires, so the fix is an adapter migration, not a parser +change. + +Nothing has broken in production yet because `roundfix watch` only routes the +`review` category, which is Codex. The first Task routed to the `frontend` +profile in `roundfix implement` will hit this at preflight. + +## Session evidence + +- Repository `/Users/marcio/dev/roundfix` at `d4b679e`, macOS 25.5.0. +- `acpx 0.12.1`; `claude-code-acp` on PATH resolves to + `@zed-industries/claude-code-acp@0.16.2`; `codex-acp` resolves to + `@agentclientprotocol/codex-acp@1.1.7`; `claude-agent-acp` is not installed. +- `.roundfixrc.yml` `profiles.frontend.preferred` is + `runtime: claude / model: claude-opus-5 / reasoning_effort: xhigh`. +- Roundfix's own disposable preflight Session from this date is still on disk: + `~/.acpx/sessions/0f4c9e8f-99c9-43b9-b195-950a0814d64a.json`, name + `roundfix-preflight-ce640e44d0c56b10`, created `2026-07-26T16:32:43.316Z`, + cwd `/Users/marcio/.roundfix/worktrees/roundfix-339f8dac/run_20260726T151545Z_50417ecaca9bab8d`. + 50 Session records for `claude-code-acp` share the same shape. +- The Claude adapter could not be spawned live from this session: Claude Code + refuses to start nested (`Claude Code cannot be launched inside another + Claude Code session`), and the documented bypass (`unset CLAUDECODE`) crashes + active sessions. Every claim below is proven from stored Session records, + read-only `acpx` commands that spawn no adapter, and package sources on disk. + +## 1. Preflight rejects the Session before any model is evaluated + +- Symptom / evidence: `acpx sessions show` on the stored preflight Session — + a read-only command that reconnects to nothing — returns the exact payload + Roundfix parses: + + ```console + $ acpx --cwd /Users/marcio/.roundfix/worktrees/roundfix-339f8dac/run_20260726T151545Z_50417ecaca9bab8d \ + --format json --json-strict claude sessions show roundfix-preflight-ce640e44d0c56b10 + {"schema":"acpx.session.v1", ... ,"acpx":{"current_model_id":"claude-opus-5", + "available_models":["default","sonnet","haiku","claude-opus-5"], + "model_control":"legacy_set_model","session_options":{"model":"claude-opus-5"}}} + ``` + + `acpx.config_options` is absent. `current_model_id` is `claude-opus-5`, so + the model was advertised, requested, and applied. +- Root cause: `ParseSessionCapabilitySnapshot` + ([`selection_capabilities.go:287`](../../internal/agent/selection_capabilities.go)) + returns `missing_config_options` when `acpx.config_options` is nil, and + `startSessionSelectionWithEnsure` + ([`selection_assignment.go:162`](../../internal/agent/selection_assignment.go)) + propagates that `CapabilityEvidenceError` straight out of preflight. + `PlanSelectionAssignment` never runs, so neither the model name nor the + reasoning effort is ever considered. `capability_evidence_invalid` is + therefore a true statement about the evidence and a misleading one about the + Agent Selection. +- Action / suggestion: Fix the adapter (finding 2). No change to + `selection_capabilities.go` is needed for the supported path; see finding 4 + for the diagnostic gap that remains. + +## 2. The configured adapter was renamed upstream and no longer receives updates + +- Symptom / evidence: the npm registry carries a deprecation notice on the + package Roundfix configures: + + ```console + $ curl -s https://registry.npmjs.org/@zed-industries/claude-code-acp | jq -r '.versions[."dist-tags".latest].deprecated' + This package has been renamed to @agentclientprotocol/claude-agent-acp. Please migrate to continue receiving updates. + ``` + + Same wording, same date, and same scope move as the Codex notice Roundfix + already acted on (`@zed-industries/codex-acp` → `@agentclientprotocol/codex-acp`). + + | | configured | replacement | + | --- | --- | --- | + | package | `@zed-industries/claude-code-acp` | `@agentclientprotocol/claude-agent-acp` | + | latest | `0.16.2`, frozen since 2026-03-26 | `0.62.0`, published 2026-07-24 | + | releases | — | 54 versions, 1.14M downloads/week | + + Reading `dist/` of the installed `0.16.2`: zero occurrences of + `configOptions`. `session/new` returns ACP's legacy `models` + (`availableModels` / `currentModelId`) plus `unstable_setSessionModel`, and + `MAX_THINKING_TOKENS` is its only thinking control — an environment variable, + not an ACP control. acpx classifies this correctly as + `model_control: "legacy_set_model"` and omits `config_options` entirely. +- Root cause: three places pin the deprecated lineage. + 1. `~/.acpx/config.json` sets `agents.claude.command` to `claude-code-acp`. + This is what acpx actually spawns, and Roundfix reads the same file first + in `configuredAdapterInvocation`. + 2. [`acpx_runner.go:54`](../../internal/agent/acpx_runner.go) defaults + `defaultAdapterCommands["claude"]` to `claude-code-acp`. + 3. [`acpx_runner.go:59-60`](../../internal/agent/acpx_runner.go) points both + `adapterInstallCommands` entries at the deprecated `@zed-industries` scope. + + acpx 0.12.1 already defaults `claude` to + `npx -y @agentclientprotocol/claude-agent-acp@^0.60.0` + (`ACP_ADAPTER_PACKAGE_RANGES.claude`), so both overrides are actively + choosing the stale adapter. +- Action / suggestion: Migrate all three, following the Codex pattern. See + "Implementation instructions". + +## 3. The replacement adapter advertises both controls Roundfix requires + +- Symptom / evidence: `@agentclientprotocol/claude-agent-acp@0.62.0`, + `dist/acp-agent.js`, has 29 references to `configOptions`. `buildConfigOptions()` + at line 4657 emits: + + ```js + { id: "model", category: "model", type: "select", currentValue: models.currentModelId, ... } + { id: "effort", category: "thought_level", type: "select", currentValue: validEffort, ... } + ``` + + The option ids are exported constants (`MODE_CONFIG_ID`, `MODEL_CONFIG_ID`, + `EFFORT_CONFIG_ID` at lines 4593-4595). Effort values come from the model's + `supportedEffortLevels`, typed in `@anthropic-ai/claude-agent-sdk@0.3.219`: + + ```ts + // package/sdk.d.ts:553 + export declare type EffortLevel = 'low' | 'medium' | 'high' | 'xhigh' | 'max'; + ``` + + In the Claude Code 2.1.220 binary, the `xhigh` eligibility check excludes + `claude-3-*`, `claude-opus-4-0`, `claude-opus-4-1`, `claude-opus-4-5`, + `claude-opus-4-6`, `claude-sonnet-4-0`, `claude-sonnet-4-5`, + `claude-sonnet-4-6`, and `claude-haiku-4-5`. `claude-opus-5` is not in the + exclusion list. +- Root cause: not a defect — this is the capability Roundfix's contract was + written against, and it exists in the replacement adapter. +- Action / suggestion: Three pieces of Roundfix already line up and need no + change. + - `id: "model"` with `category: "model"` satisfies the model-option detection + in `ParseSessionConfigOptions`. + - `id: "effort"` matches `acpxGenericReasoningEffortKey` + ([`acpx_runner.go:42`](../../internal/agent/acpx_runner.go)), which + `acpxReasoningEffortConfigKey` + ([`acpx_runner.go:1477`](../../internal/agent/acpx_runner.go)) already maps + for the `claude` runtime. acpx's `resolveCompatibleConfigId` remaps only + legacy Zed Codex invocations, so `effort` passes through unchanged. + - Plain model ids with no `[effort]` suffix plus a separate effort select + resolve to `SelectionEncodingIndependent`, which is the encoding + `PlanSelectionAssignment` already implements. + + `.roundfixrc.yml` can therefore keep `reasoning_effort: xhigh` on the + `frontend` profile. + +## 4. Roundfix verifies adapter lineage for Codex only + +- Symptom / evidence: `CheckAdapter` returns bare evidence for every + non-Codex runtime ([`acpx_runner.go:508`](../../internal/agent/acpx_runner.go)): + + ```go + if runtimeID != "codex" { + return evidence, nil + } + return inspectCodexAdapter(ctx, invocation) + ``` + + For Codex, `inspectCodexAdapter` reads `--version`, rejects a foreign package + with `adapter_lineage_unknown`, and rejects an old version with + `adapter_version_unsupported` — each carrying an install command. For Claude, + any binary on PATH named `claude-code-acp` passes, and the mismatch surfaces + much later as `capability_evidence_invalid`. +- Root cause: the lineage and version gate was built for the Codex migration + and never generalized. +- Action / suggestion: Extend the same gate to `claude` so a machine still + holding the deprecated adapter fails at the adapter check with a legible + message and a `npm install -g` next action, instead of failing mid-preflight + with a capability-evidence error that names the wrong thing. + +## 5. The Codex adapter pin no longer describes the installed reality + +- Symptom / evidence: `PinnedCodexAdapterVersion = "1.1.4"` + ([`acpx_runner.go:28`](../../internal/agent/acpx_runner.go)); the installed + `codex-acp` reports `@agentclientprotocol/codex-acp 1.1.7`; acpx 0.12.1 + requires `^1.1.5`. `roundfix setup` writes + `npx -y @agentclientprotocol/codex-acp@1.1.4` + ([`setup.go:360`](../../internal/cli/setup.go)). +- Root cause: the pin is a floor (`compareAdapterVersions(...) < 0`), so 1.1.7 + passes and nothing has failed. The stale constant only misleads: a fresh + `roundfix setup` pins an exact version acpx considers below its supported + range. +- Action / suggestion: Raise the pin to at least acpx's `^1.1.5` floor when the + Claude migration lands. Separable from findings 1-4 and safe to defer. + +## Implementation instructions + +Ordered. Steps 1 and 2 are independently verifiable; do not start step 3 until +step 2 passes. + +### 1. Prove the model × effort matrix + +The one link this investigation could not close: whether `claude-opus-5` +reports `supportsEffort` with `xhigh` for this account at runtime. It is +resolved by the SDK at session creation, so it needs a live adapter. Run +outside a Claude Code session: + +```bash +npm install -g @agentclientprotocol/claude-agent-acp +acpx --cwd /tmp --model claude-opus-5 --format json --json-strict \ + --agent claude-agent-acp sessions ensure --name rf-probe +acpx --cwd /tmp --format json --json-strict \ + --agent claude-agent-acp sessions show rf-probe +``` + +Pass condition: the `sessions show` payload contains `acpx.config_options` +with an option `{"id":"effort", ...}` whose `options` include `xhigh`, and an +option `{"id":"model", "category":"model", ...}` whose `options` include +`claude-opus-5`. Record the observed payload and the adapter version as an +addendum to this finding. + +If `effort` is absent, `claude-opus-5` does not support effort levels on this +account. In that case the profile must drop to `reasoning_effort: ""`, which +`PlanSelectionAssignment` resolves as `SelectionEncodingModelManaged`, and the +migration is still worth doing for finding 2. + +### 2. Migrate the adapter + +- `~/.acpx/config.json`: set `agents.claude.command`. Either remove the + override entirely and let acpx use its own default, or pin explicitly the way + Codex is pinned. Pin to the version proven in step 1. +- [`acpx_runner.go:54`](../../internal/agent/acpx_runner.go): change + `defaultAdapterCommands["claude"]`. Mirror the Codex constants — add + `ClaudeAdapterPackage`, `PinnedClaudeAdapterVersion`, + `ClaudeAdapterCommand()`, and `ClaudeAdapterInstallCommand()` next to their + Codex counterparts at lines 27-28 and 76-85. +- [`acpx_runner.go:59-60`](../../internal/agent/acpx_runner.go): repoint both + `adapterInstallCommands` entries at `@agentclientprotocol/claude-agent-acp`. +- [`setup.go:356`](../../internal/cli/setup.go): add an + `officialClaudeACPXOverride()` beside `officialCodexACPXOverride()` so + `roundfix setup` writes and migrates the Claude override the same way it + already migrates a stale Codex override. + +### 3. Generalize the adapter lineage gate + +Turn `inspectCodexAdapter` into a runtime-parameterized check covering `codex` +and `claude`, keyed off the per-runtime package and pinned version constants, +and drop the `runtimeID != "codex"` early return at +[`acpx_runner.go:508`](../../internal/agent/acpx_runner.go). `AdapterLineageError` +and `AdapterVersionError` already carry `Command`, `Package`, `Version`, and an +install command; only the Codex-specific message construction at +`acpx_runner.go:342-350` needs to become runtime-aware. + +### 4. Confirm the profile end to end + +Run a `roundfix implement` preflight against a Task routed to the `frontend` +profile and confirm a `SelectionProof` with `Status: proven`, +`Encoding: independent`, `ReasoningKey: effort`, and `ReasoningValue: xhigh`. + +### Operational note + +acpx carries a built-in diagnostic for a known session-creation stall with +some Claude Code and `@agentclientprotocol/claude-agent-acp` combinations; its +recommended mitigation is `--approve-all` with +`nonInteractivePermissions=deny`. The current `~/.acpx/config.json` already +sets `defaultPermissions: approve-all` and `nonInteractivePermissions: deny`. +If preflight starts timing out after the migration, this is the first thing to +check. + +## Suggested acceptance checks + +- A Claude Agent Session created through the new adapter yields + `SelectionCapabilities` with a non-nil `ReasoningOption` whose `ID` is + `effort`. +- `claude/claude-opus-5/xhigh` produces a `SelectionProof` with + `Status: proven` and `Encoding: independent`. +- A machine whose `claude` adapter resolves to `@zed-industries/claude-code-acp` + fails `CheckAdapter` with `adapter_lineage_unknown` and a next action naming + `@agentclientprotocol/claude-agent-acp` — not with + `capability_evidence_invalid`. +- A machine whose Claude adapter is below the pinned version fails with + `adapter_version_unsupported` and an install command. +- `roundfix setup` on a config holding the deprecated Claude override offers a + migration, matching the existing Codex override behaviour. +- `roundfix doctor` reports the Claude adapter package and version as evidence, + the way it already does for Codex. + +## What worked — keep + +- The disposable preflight Session survives on disk after cleanup with its + complete capability state. Diagnosing this without spawning a single adapter + was possible only because of that record. +- `acpx sessions show` reads the stored record without reconnecting to the + agent, which makes it a safe read-only probe inside a constrained harness. +- `ParseSessionCapabilitySnapshot` failed closed. It refused to guess a model + or an effort from `available_models` when the evidence contract was not met, + which is why a deprecated adapter surfaced as a preflight error instead of a + silently downgraded Agent Selection. +- The Codex adapter migration left a complete, reusable pattern: pinned package + constants, a lineage probe, a version floor, typed errors carrying install + commands, and a `roundfix setup` migration path. Finding 2 is the same + problem, and the Codex work is the template. From b71957dbc11f01989d372521cba9d73da3a2ec59 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 17:17:20 -0300 Subject: [PATCH 28/48] docs: plan doctor skill readiness hardening --- .../_prd.md | 144 ++++++++++ .../_tasks.md | 30 ++ .../_techspec.md | 265 ++++++++++++++++++ .../task_01.md | 61 ++++ .../task_02.md | 80 ++++++ .../task_03.md | 84 ++++++ .../task_04.md | 93 ++++++ 7 files changed, 757 insertions(+) create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/_prd.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/_tasks.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/_techspec.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/task_01.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/task_02.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/task_03.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/task_04.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/_prd.md b/docs/specs/0050-doctor-skill-readiness-hardening/_prd.md new file mode 100644 index 0000000..bb962a6 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/_prd.md @@ -0,0 +1,144 @@ +--- +spec: 0050-doctor-skill-readiness-hardening +status: active +surfaces: [cli, backend, docs] +--- + +# Doctor Skill Readiness Hardening + +Spec 0036 made Repository Skill Set readiness a blocking Doctor Command +result, but review found three gaps in that implementation. Repository-level +symbolic links can redirect skill or lock reads beyond the intended Git root, +the external hash order only approximates the installed skills CLI's +`String.localeCompare` order, and the Doctor coordinator can substitute its +process working directory when configuration did not resolve a Git root. + +This corrective Spec closes those gaps without reopening or rewriting the +archived Spec 0036. It also restores the missing executable evidence for +Doctor's no-mutation contract and removes duplicated hash-order logic from the +Repository Skill Set and Baseline restoration paths. + +## Project Constraints + +- Identifier strategy: not applicable — this correction creates no + project-owned Internal Identifier or application identity. Skill names and + paths remain governed by the existing Repository Skill Set contract. Source: + `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — every change is local filesystem, + hashing, CLI coordination, documentation, or test behavior. No credential, + HTTP route, or network policy changes. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0049 and ADR-0055 keep Agent + Selection Profile proof independent from Repository Skill Set readiness; + ADR-0066 and ADR-0072 keep Baseline skill restoration and asset + synchronization in the Go CLI. Source: `docs/agents/domain.md`. +- Tooling authority: applicable — on 2026-07-26 the maintainer expressly + authorized this Spec to add `golang.org/x/text` for Unicode collation. + Bounded protected files: `go.mod` and `go.sum`. No other protected tooling + mutation is authorized. Source: `docs/agents/agent-instructions.md`. + +## Goals + +- Keep every Repository Skill Set read anchored to the supplied Git root and + reject symbolic links at repository-owned skill-tree and lock-authority + boundaries. +- Match the installed skills CLI's path collation for punctuation, case, and + Unicode instead of approximating it with lowercase byte ordering. +- Make the Repository Skill Set and Baseline restoration paths consume one + shared external-skill hash implementation. +- Make the Doctor Command use the resolved Git root as its sole repository + authority and print deterministic remediation for every readiness error. +- Prove through public CLI execution that Doctor does not mutate repository, + User Config, `.roundfix`, lock, or skill state. +- Keep the canonical Doctor Command definition aligned with its still-shipped + minimum-supported acpx version report. + +## User Stories + +1. As a security-conscious maintainer, I want Doctor to reject a symlinked + `.agents`, `.agents/skills`, or `skills-lock.json`, so readiness cannot be + controlled by files outside the repository trust boundary. +2. As a maintainer with valid punctuation or Unicode paths inside a skill, I + want Doctor to accept the same folder hash produced by the installed skills + CLI, so a valid lock does not become a false outdated result. +3. As a Baseline maintainer, I want restored external lock hashes to use the + same ordering implementation as Doctor, so the two local authorities cannot + drift. +4. As an Agent invoking Doctor, I want a missing Git root or unexpected skill + checker failure to produce a useful next action without silently checking a + different directory. +5. As a reviewer, I want package and CLI tests to exercise the real + filesystem and public runner at their owning layers, so passing tests prove + the security and no-mutation contracts. + +## Core Features + +1. **Root-anchored skill inspection.** Repository readiness opens one anchored + repository root, validates each skill-tree ancestor and the lock authority + without following links, and confines later reads and walks to that root. +2. **Compatible path collation.** One internal hash component uses explicit + American English Unicode collation compatible with the installed skills + CLI's resolved `String.localeCompare` behavior and preserves the existing + slash-normalized path-plus-bytes digest shape. +3. **Shared hash authority.** `skills.SkillFolderHash` and Baseline external + lock generation adapt their file inputs to the same internal hash + component; neither carries a copied comparator. +4. **Deterministic Doctor failures.** Doctor never substitutes `os.Getwd()` for + a missing `roundconfig.Loaded.GitRoot`. Missing-root and unclassified + checker errors remain blocking, keep independent checks eager, and include + deterministic remediation. +5. **Behavioral regression coverage.** Repository tests live with + `repository.go`; hash compatibility tests include punctuation and Unicode; + a public `Run(["doctor"])` test snapshots relevant paths before and after a + real repository skill check. +6. **Contract reconciliation.** `CONTEXT.md` restores the Doctor Command's + detected-acpx-version wording. The already implemented repository-owned + Baseline digest preservation remains cited as pre-existing evidence rather + than being rebuilt. + +## Non-Goals + +- Rebuilding, rebasing, force-pushing, or otherwise rewriting the current + branch or its existing commits. +- Editing any artifact under + `docs/specs/_archived/0036-doctor-skill-readiness/`. +- Reimplementing `sync-setups`; repository-owned digest preservation already + exists in the Go Baseline asset synchronization path and its regression + suite. +- Editing upstream-managed skills, `skills-lock.json`, or + `skills/recommended.txt`. +- Changing the documented `"; next: "` Doctor output boundary or inventing a + machine-readable Doctor schema. +- Changing required skill membership or current illustrative count examples. +- Invoking Node, Bun, or the external skills CLI from Doctor. + +## Success Metrics + +- Repository fixtures with symlinked `.agents`, `.agents/skills`, or + `skills-lock.json` fail readiness without reading the external targets. +- A pinned punctuation, case, and Unicode corpus produces the same SHA-256 + digest as skills CLI 1.5.19's `String.localeCompare` algorithm. +- Doctor and Baseline external hashes are generated by one shared + implementation and retain compatibility with all current lock entries. +- Doctor receives only `roundconfig.Loaded.GitRoot` as the repository + readiness root; an empty value never calls the checker with the process + working directory. +- A public Doctor execution leaves all snapshotted repository and user paths + byte-identical. +- Focused tests, the race suite for affected packages, and `rtk make verify` + pass before QA. + +## Decisions + +- Keep the archived Spec 0036 immutable and deliver review corrections through + this follow-up Spec. +- Use `golang.org/x/text/collate` with + `language.AmericanEnglish` because the supported Node and Bun environments + resolve the installed skills CLI's optionless `localeCompare` to `en-US`. +- Add one internal hash package instead of exporting Baseline-specific file + types or maintaining copied comparators. +- Keep Doctor read-only and offline; compatibility is implemented in Go rather + than delegated to an external process. +- Add no ADR because the correction narrows existing trust, compatibility, and + ownership contracts rather than introducing a new hard-to-reverse product + boundary. + diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/_tasks.md b/docs/specs/0050-doctor-skill-readiness-hardening/_tasks.md new file mode 100644 index 0000000..ecaf05c --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/_tasks.md @@ -0,0 +1,30 @@ +--- +schema: spec-tasks/v1 +spec: 0050-doctor-skill-readiness-hardening +graph: + nodes: + - id: task_01 + file: task_01.md + needs: [] + - id: task_02 + file: task_02.md + needs: [task_01] + - id: task_03 + file: task_03.md + needs: [task_02] + - id: task_04 + file: task_04.md + needs: [task_03] +--- + +# Tasks — Doctor Skill Readiness Hardening + +| id | title | type | complexity | needs | +| ------- | ------------------------------------------------ | ------- | ---------- | ------- | +| task_01 | Add the authorized Unicode collation dependency | chore | low | — | +| task_02 | Centralize external skill hash compatibility | backend | medium | task_01 | +| task_03 | Anchor Repository Skill Set filesystem reads | backend | high | task_02 | +| task_04 | Harden Doctor coordination and evidence | backend | medium | task_03 | + +Waves: 1 → task_01 · 2 → task_02 · 3 → task_03 · 4 → task_04 + diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/_techspec.md b/docs/specs/0050-doctor-skill-readiness-hardening/_techspec.md new file mode 100644 index 0000000..7d98fe1 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/_techspec.md @@ -0,0 +1,265 @@ +--- +spec: 0050-doctor-skill-readiness-hardening +prd: _prd.md +created: 2026-07-26 +--- + +# Doctor Skill Readiness Hardening — Technical Spec + +## Executive Summary + +Harden Repository Skill Set readiness at the two boundaries that review proved +unsafe: filesystem anchoring and external hash ordering. One new internal hash +package will own slash-normalized path-plus-content hashing and use the +American English collation tables from `golang.org/x/text`, matching the +installed skills CLI's resolved `String.localeCompare` behavior for +punctuation, case, and Unicode. The repository checker will perform every lock +and skill read through an `os.Root` anchored at the supplied Git root after +non-following metadata validation. The trade-off is one official Go +supplementary dependency and its generated Unicode tables in exchange for +removing an incomplete handwritten collation approximation. + +## Project Constraints + +- Identifier strategy: not applicable — existing skill names and + repository-relative paths remain the only identifiers. This Spec adds no + project-owned identity. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — the implementation is offline and + filesystem-only and changes no authentication, authorization, transport, or + HTTP contract. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0049 and ADR-0055 require Doctor's + profile proof to remain independent and eager; ADR-0066 and ADR-0072 require + Baseline restoration to remain a Go CLI operation and preserve the existing + Go `sync-setups` destination. Source: `docs/agents/domain.md`. +- Tooling authority: applicable — express maintainer authorization: + authorization given on 2026-07-26 to add `golang.org/x/text` for Unicode + collation; bounded protected files: `go.mod`, `go.sum`. No other protected + tooling mutation is authorized. Source: + `docs/agents/agent-instructions.md`. + +## System Architecture + +- `internal/skillhash` becomes the single internal compatibility component for + external skill digests. It accepts already collected relative paths and + bytes, orders them with one collator, and emits the existing lowercase + SHA-256 value. +- `skills` remains the Repository Skill Set ownership boundary. Its filesystem + walker collects regular files and rejects special entries; its repository + checker anchors all repository-relative operations and classifies missing, + outdated, or unreadable state. +- `internal/baseline` keeps ownership of Baseline skill restoration and lock + rendering. It converts restoration files to the shared hash input rather + than duplicating collation. +- `internal/cli` keeps Doctor output, check ordering, exit status, and + remediation ownership. It passes only the resolved Git root to Repository + Skill Set readiness. +- `CONTEXT.md` remains the canonical vocabulary contract. The archived Spec + 0036 and existing Baseline asset synchronization code remain unchanged. + +The new package is deliberately narrow. It does not walk filesystems, decode +locks, know about Doctor output, or expose Baseline types. This keeps +filesystem security in `skills`, presentation in `internal/cli`, and only the +shared compatibility rule in one place. + +## Implementation Design + +### Shared external-skill hash + +The internal component exposes one small value type and one pure operation: + +```go +type File struct { + Path string + Content []byte +} + +func Sum(files []File) string +``` + +`Sum` copies or indexes the supplied slice without changing caller-owned order, +sorts stably by `collate.New(language.AmericanEnglish).CompareString`, and +feeds each slash-normalized relative path immediately followed by its bytes +into SHA-256. It inserts no separator or length prefix because the installed +skills CLI does not. Callers remain responsible for rejecting unsafe paths, +links, excluded directories, and unreadable files. + +`skills.SkillFolderHash` retains its public signature and filesystem failure +behavior. Its walker collects `internal/skillhash.File` values and delegates +only ordering and digest production. Baseline's +`externalSkillsLockDigest` performs the same adaptation from `restoreFile`. +The copied lowercase-byte comparators are deleted. + +The compatibility test uses a pinned corpus containing `_a`, `-a`, digits, +case variants, precomposed accented characters, and punctuation at multiple +path depths. Its expected digest is produced once with the locally installed +skills CLI 1.5.19 algorithm and then treated as a product compatibility +contract. Existing real-lock integration tests remain the macro proof. + +### Root-anchored repository reads + +`checkRepository` validates required names before opening the supplied root, +then calls `os.OpenRoot`. Every repository-relative authority is inspected +through that handle: + +1. `skills-lock.json` is checked with `Root.Lstat`; a symbolic link or + non-regular file is rejected before `Root.ReadFile`. +2. `.agents` and `.agents/skills` are checked component by component with + `Root.Lstat`. A missing component produces the existing missing-skill + classifications; a symbolic link or non-directory produces a typed + readiness error. +3. Each required skill root is inspected without following its final path. + Missing roots remain normal readiness results. Present roots must be real + directories before an anchored sub-root or filesystem view is walked. +4. Walkers reject symbolic links and special files and skip `.git` and + `node_modules` exactly as before. + +The `os.Root` handle confines all later operations to the repository even if a +path changes between metadata inspection and use. Component `Lstat` checks +enforce the static no-symlink contract; root confinement prevents an ancestor +replacement race from redirecting reads outside the supplied Git root. +Errors retain the failed operation and repository-relative authority while +wrapping their underlying cause for `errors.Is` and `errors.As`. + +### Doctor coordination and remediation + +The Doctor coordinator removes the `os.Getwd()` fallback. A non-empty +`roundconfig.Loaded.GitRoot` remains the work directory supplied to profile +proof and the root passed to `checkSkills`. When the loaded Git root is empty, +Doctor does not call the repository checker and instead emits a failed +`skills:` result whose next action tells the caller to run Doctor from a Git +repository. + +`doctorSkillReadinessResult` retains owned and external remediation selection +for typed errors. If an injected or future checker returns an error without +ownership and no classified readiness fields, the conservative fallback +prints both existing remediation commands in owned-then-external order. The +output boundary stays unchanged: + +```text +skills: failed (repository skill check failed; next: roundfix skills install --target project; bunx skills experimental_install && bunx skills update -p -y) +``` + +All independent health checks still execute and print in the current order. +Missing Git root or skill failure remains exit `1`; argument failures remain +exit `2`. + +### Test ownership and no-mutation proof + +Tests whose primary subject is `CheckRepository` and their repository fixture +helpers move from `skills/skills_test.go` to +`skills/repository_test.go`. `SkillFolderHash` tests remain beside +`skills.go`. The move changes no assertions merely to make a failure pass. + +Focused negative cases cover symlinked `.agents`, symlinked +`.agents/skills`, and symlinked `skills-lock.json`, with targets containing +otherwise valid ready state. Each case must fail without classifying the +repository ready. + +`internal/cli/doctor_test.go` adds public-runner coverage with fake machine +health and profile proof but the real `skills.CheckRepository`. The test +creates a complete temporary Repository Skill Set, snapshots repository files, +the configured User Config path, and `.roundfix`, runs +`Run([]string{"doctor"}, ...)`, then proves every snapshot is unchanged. A +separate case proves an empty loaded Git root does not invoke `checkSkills` and +prints repository-specific remediation. + +### Contract reconciliation + +`CONTEXT.md` restores that the Doctor Command reports the detected acpx version +against the minimum. This is a vocabulary correction for behavior that never +stopped shipping. + +The ownership-safe `sync-setups` behavior removed from Spec 0036's text is not +rebuilt here. Its Go implementation already preserves repository-owned +digests in `internal/baseline/assets_sync.go`, its regression test already +proves the behavior, and ADR-0072 assigns that responsibility to the Go +Baseline operation. This Spec records that evidence while keeping the archived +Spec byte-identical. + +## Coverage Map + +- Goal: root-anchored reads and User Story 1 → anchored repository checker, + ancestor and lock `Lstat` cases. +- Goals: exact ordering and shared implementation; User Stories 2–3 → + `internal/skillhash`, punctuation/Unicode oracle, Doctor and Baseline + adapters. +- Goal: resolved Git root and deterministic remediation; User Story 4 → + Doctor coordinator and exact-output command tests. +- Goal: executable no-mutation evidence; User Story 5 → + `repository_test.go` and real-checker public Doctor tests. +- Goal: canonical terminology → `CONTEXT.md` reconciliation plus existing + Baseline asset synchronization evidence. + +## Integration Points + +- Installed skills CLI 1.5.19 and 1.5.20 are compatibility references only; + production code never invokes them. +- `golang.org/x/text/collate` and `golang.org/x/text/language` provide pinned, + in-process Unicode collation tables. +- `os.Root` is the standard-library repository confinement boundary. +- Baseline restoration consumes the shared hash package without changing its + lock JSON or transaction contracts. + +## Testing Approach + +- Unit: a pure `internal/skillhash` oracle test compares the pinned expected + digest for punctuation, case, digits, and Unicode; a negative companion + changes one path or byte and requires a different digest. +- Package integration: `skills/repository_test.go` builds complete disposable + repositories and proves normal readiness, missing/outdated classification, + malformed authorities, ancestor and lock symlink rejection, and no + mutation. +- Baseline integration: existing restoration tests plus a focused shared-hash + assertion prove generated `computedHash` values still use the compatibility + contract. +- CLI integration: public `Run` tests prove exact output, missing-Git-root + handling, independent check execution, conservative error remediation, and + real-checker no mutation. +- Repository integration: the existing real Repository Skill Set contract, + affected-package race suite, and `rtk make verify` remain mandatory. + +No test introduces production-only hooks. Machine health and profile proof use +the pre-existing Doctor dependency seam; the repository checker itself remains +real in the no-mutation test. + +## Build Order + +1. Add the authorized `golang.org/x/text` module requirement in the bounded + `go.mod` and `go.sum` tooling slice. +2. Add the shared external-skill hash component, its CLI-oracle corpus, and + migrate both existing consumers (depends on: 1). +3. Anchor Repository Skill Set reads, reject skill-tree and lock symlinks, and + relocate repository tests to their owning file (depends on: 2). +4. Remove Doctor's working-directory fallback, complete remediation and + public no-mutation coverage, and reconcile `CONTEXT.md` (depends on: 3). + +## Risks & Considerations + +- The upstream skills CLI calls optionless `String.localeCompare`, so its + default locale is environment-dependent. Roundfix pins American English + because supported Node and Bun currently resolve `en-US`; the oracle makes + that assumption visible and fails loudly if the compatibility contract + changes. +- `os.Root` confines symbolic-link traversal to the root but still permits + in-root links by design. Explicit component `Lstat` validation is therefore + required in addition to anchoring. +- Unicode collation tables increase module and binary size. The dependency is + official Go supplementary code, already present in the local module cache, + and replaces a handwritten algorithm that cannot satisfy the contract. +- Moving tests can create noisy diffs. The move must preserve existing + assertions and separate only repository-owned helpers; behavior changes + belong in newly named cases. +- A lock or shared ancestor security error cannot safely identify one damaged + ownership group. Conservative unclassified remediation may print both + commands, but it must never run them. + +## Decisions + +- Pin `golang.org/x/text` through Go tooling; never hand-edit module checksums. +- Pin `language.AmericanEnglish` rather than inherit the host locale. +- Share only the pure hash rule; keep file collection and policy with each + owning package. +- Combine non-following component validation with `os.Root` confinement. +- Preserve Doctor's output delimiter, line ordering, read-only contract, and + exit codes. +- Preserve branch history and every archived Spec artifact. diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_01.md b/docs/specs/0050-doctor-skill-readiness-hardening/task_01.md new file mode 100644 index 0000000..c2d87c5 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/task_01.md @@ -0,0 +1,61 @@ +--- +task: task_01 +spec: 0050-doctor-skill-readiness-hardening +status: pending +type: chore +complexity: low +--- + +# Task 01: Add the authorized Unicode collation dependency + +## Overview + +Add the one module required for the shared Unicode path-collation contract. +This is an isolated protected-tooling slice: only the expressly authorized +module files and this Task file may change. + +## Requirements + +1. MUST add `golang.org/x/text` at version `v0.40.0` through Go tooling. +2. MUST change only `go.mod`, `go.sum`, and this Task file. +3. MUST preserve every existing direct and indirect requirement unless Go + tooling changes its classification as part of adding the authorized module. +4. MUST leave the module graph verifiable without adding source code in this + Task. + +## Subtasks + +- [ ] Capture the pre-existing changed-file set. +- [ ] Add the authorized module through the Go toolchain. +- [ ] Inspect the resulting manifest and checksum changes. +- [ ] Prove the exact selected version and module integrity. +- [ ] Run the protected-file postflight. + +## Acceptance Criteria + +- [ ] `golang.org/x/text` resolves exactly to `v0.40.0`. +- [ ] `go.mod` and `go.sum` are internally consistent and pass module + verification. +- [ ] No path other than `go.mod`, `go.sum`, and this Task file changes in this + Task. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/go.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- interface: `go.mod` +- interface: `go.sum` + +## Verification + +- `rtk go list -m -f '{{.Path}} {{.Version}}' golang.org/x/text` — expected: + exactly `golang.org/x/text v0.40.0`. +- `rtk go mod verify` — expected: every downloaded module is verified. + +## References + +- `_prd.md` → Project Constraints; Goals; Core Feature 2; Decisions. +- `_techspec.md` → Project Constraints; Integration Points; Build Order 1; + Decisions. + diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_02.md b/docs/specs/0050-doctor-skill-readiness-hardening/task_02.md new file mode 100644 index 0000000..221812f --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/task_02.md @@ -0,0 +1,80 @@ +--- +task: task_02 +spec: 0050-doctor-skill-readiness-hardening +status: pending +type: backend +complexity: medium +--- + +# Task 02: Centralize external skill hash compatibility + +## Overview + +Replace the duplicated lowercase-byte comparator with one pure internal hash +component that matches the installed skills CLI's `localeCompare` behavior. +Doctor-facing folder hashing and Baseline restoration must consume the same +implementation while preserving the existing digest shape. + +## Requirements + +1. MUST add one internal component that hashes path-plus-content pairs after + stable American English Unicode collation. +2. MUST preserve slash-normalized paths, the absence of separators, lowercase + SHA-256 output, and caller-owned input order. +3. MUST migrate `skills.SkillFolderHash` and Baseline external lock generation + to the shared component. +4. MUST delete both copied lowercase-byte comparator implementations. +5. MUST add a pinned skills CLI 1.5.19 compatibility oracle covering + punctuation, digits, case, Unicode, and nested paths. +6. MUST add a negative companion proving a path or content change alters the + digest. +7. MUST preserve current real Repository Skill Set and Baseline restoration + compatibility. + +## Subtasks + +- [ ] Add the pure shared hash component. +- [ ] Add the punctuation and Unicode compatibility oracle. +- [ ] Migrate Repository Skill Set folder hashing. +- [ ] Migrate Baseline external lock hashing. +- [ ] Remove duplicated comparators and stale explanatory comments. +- [ ] Exercise the real lock and restoration contracts. + +## Acceptance Criteria + +- [ ] The pinned corpus produces the exact skills CLI 1.5.19 digest. +- [ ] `_a` sorts before `-a`, and the Unicode cases match the pinned + `en-US` oracle. +- [ ] Changing one path or byte changes the digest. +- [ ] Both production consumers call the shared implementation and contain no + copied `strings.ToLower` path comparator. +- [ ] The real repository remains ready and existing Baseline restoration + hashes remain compatible. +- [ ] `go.mod` and `go.sum` remain unchanged from Task 01. + +## Context + +- instruction: `docs/agents/go.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `skills/skills.go` +- interface: `skills/skills_test.go` +- interface: `internal/baseline/skills_restore.go` +- interface: `internal/baseline/skills_restore_test.go` +- interface: `internal/baseline/assets/lock-hash-compatibility-v1.json` + +## Verification + +- `rtk go test ./internal/skillhash ./skills ./internal/baseline -run 'Test(Sum|SkillFolderHash|CheckRepositoryMatchesRealRepository|SkillsRestore)' -count=1` — expected: + the shared oracle, real Repository Skill Set, and Baseline restoration + compatibility tests pass. +- `rtk go test -race ./internal/skillhash ./skills ./internal/baseline -run 'Test(Sum|SkillFolderHash|CheckRepositoryMatchesRealRepository|SkillsRestore)' -count=1` — expected: + the shared collator and both consumers are race-free. + +## References + +- `_prd.md` → Goals 2–3; User Stories 2–3; Core Features 2–3; Success Metrics. +- `_techspec.md` → Shared external-skill hash; Integration Points; Testing + Approach; Build Order 2. + diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_03.md b/docs/specs/0050-doctor-skill-readiness-hardening/task_03.md new file mode 100644 index 0000000..39f2a34 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/task_03.md @@ -0,0 +1,84 @@ +--- +task: task_03 +spec: 0050-doctor-skill-readiness-hardening +status: pending +type: backend +complexity: high +--- + +# Task 03: Anchor Repository Skill Set filesystem reads + +## Overview + +Make the supplied Git root the enforced filesystem boundary for every +Repository Skill Set authority. Static symbolic links in the shared +skill-tree ancestors or lock authority must fail before their targets are +read, while existing missing and outdated classifications remain stable. + +## Requirements + +1. MUST open one `os.Root` for the supplied repository and perform later + authority reads through it. +2. MUST inspect `.agents`, `.agents/skills`, and `skills-lock.json` with + non-following metadata operations before opening or reading them. +3. MUST reject a symlinked shared ancestor or lock authority without reading + its target. +4. MUST inspect each required skill root without following its final path and + keep nested symlink and special-file rejection. +5. MUST preserve missing-skill classification when `.agents`, + `.agents/skills`, or an individual skill does not exist. +6. MUST wrap filesystem causes so callers retain `errors.Is` and `errors.As` + behavior and ownership-specific remediation where ownership is known. +7. MUST move `CheckRepository` tests and their repository-specific helpers to + `skills/repository_test.go` without weakening existing assertions. +8. MUST keep `SkillFolderHash` tests beside `skills.go`. + +## Subtasks + +- [ ] Introduce the anchored repository read boundary. +- [ ] Validate shared ancestors and the lock authority without following links. +- [ ] Adapt owned and external skill inspection to rooted operations. +- [ ] Relocate repository tests and helpers to their owning file. +- [ ] Add ancestor and lock symlink regression fixtures. +- [ ] Prove existing missing, outdated, malformed, and no-mutation behavior. + +## Acceptance Criteria + +- [ ] A symlinked `.agents` fails readiness and its target is not read. +- [ ] A symlinked `.agents/skills` fails readiness and its target is not read. +- [ ] A symlinked `skills-lock.json` fails readiness and its target is not + decoded. +- [ ] A complete repository still reports ready; missing and outdated + classifications remain sorted and unchanged. +- [ ] Nested links and special entries remain rejected without escaping the + anchored repository. +- [ ] Repository checks remain read-only. +- [ ] `skills/repository.go` has its canonical test file and + `skills/skills_test.go` retains only `skills.go`-owned coverage and + genuinely shared helpers. + +## Context + +- instruction: `docs/agents/go.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `skills/repository.go` +- interface: `skills/skills.go` +- interface: `skills/skills_test.go` + +## Verification + +- `rtk go test ./skills -run 'TestCheckRepository' -count=1` — expected: + ready, missing, outdated, malformed, no-mutation, shared-ancestor symlink, + lock symlink, and nested-link cases pass. +- `rtk go test -race ./skills -run 'TestCheckRepository' -count=1` — expected: + anchored Repository Skill Set reads are race-free. + +## References + +- `_prd.md` → Goal 1; User Story 1; Core Features 1 and 5; Success Metrics. +- `_techspec.md` → Root-anchored repository reads; Test ownership and + no-mutation proof; Testing Approach; Build Order 3. + diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_04.md b/docs/specs/0050-doctor-skill-readiness-hardening/task_04.md new file mode 100644 index 0000000..0677caf --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/task_04.md @@ -0,0 +1,93 @@ +--- +task: task_04 +spec: 0050-doctor-skill-readiness-hardening +status: pending +type: backend +complexity: medium +--- + +# Task 04: Harden Doctor coordination and evidence + +## Overview + +Make Doctor use the resolved Git root without a process-directory fallback, +complete deterministic error remediation, and prove the full public command is +read-only with the real Repository Skill Set checker. Reconcile the canonical +Doctor Command definition without changing archived Specs or current output +delimiters. + +## Requirements + +1. MUST remove Doctor's `os.Getwd()` fallback for Repository Skill Set + readiness. +2. MUST avoid calling `checkSkills` when `roundconfig.Loaded.GitRoot` is empty + and print repository-specific remediation instead. +3. MUST preserve eager execution and ordering of Node, acpx, Adapter + Readiness, Agent Selection Profile Readiness, Repository Skill Set, and + codex checks. +4. MUST provide both existing ownership remediation commands, in their current + order, when an unclassified checker error has no narrower safe action. +5. MUST preserve the `"; next: "` boundary, stdout/stderr placement, and + Doctor exit codes. +6. MUST add a public `Run([]string{"doctor"}, ...)` no-mutation test that uses + the real repository checker and snapshots repository, User Config, + `.roundfix`, lock, and skill state. +7. MUST restore the canonical Doctor Command wording for detected acpx version + reporting. +8. MUST leave the archived Spec 0036, upstream-managed skills, current lock, + and branch history unchanged. + +## Subtasks + +- [ ] Separate missing-Git-root handling from repository checking. +- [ ] Complete unclassified error remediation. +- [ ] Add exact-output missing-root and generic-error cases. +- [ ] Add the real-checker public no-mutation fixture. +- [ ] Restore the canonical Doctor Command wording. +- [ ] Run focused, race, and repository-wide verification. + +## Acceptance Criteria + +- [ ] An empty loaded Git root never invokes the repository checker or falls + back to the process working directory. +- [ ] Missing-root and generic checker failures print deterministic `next:` + actions and exit `1`. +- [ ] All independent checks still run and render in their established order. +- [ ] The public Doctor no-mutation test proves all relevant snapshots are + byte-identical after execution with the real checker. +- [ ] `CONTEXT.md` again states that Doctor reports the detected acpx version + against the minimum. +- [ ] Archived Spec 0036 and every protected or upstream-managed path outside + the approved Task 01 files remain unchanged. +- [ ] The complete repository Verification passes. + +## Context + +- instruction: `CONTEXT.md` +- instruction: `docs/agents/cli.md` +- instruction: `.agents/skills/agentic-cli-design/SKILL.md` +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-cli/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `internal/cli/doctor.go` +- interface: `internal/cli/cli_test.go` +- interface: `skills/repository.go` + +## Verification + +- `rtk go test ./internal/cli -run 'TestRunDoctor' -count=1` — expected: + exact output, missing-root handling, generic remediation, independent check + ordering, and public no-mutation cases pass. +- `rtk go test -race ./internal/skillhash ./skills ./internal/baseline ./internal/cli -run 'Test(Sum|SkillFolderHash|CheckRepository|SkillsRestore|RunDoctor)' -count=1` — expected: + the assembled correction is race-free across affected packages. +- `rtk make verify` — expected: formatting, tests, Repository Skill Set + integrity, and build all pass. + +## References + +- `_prd.md` → Goals 4–6; User Stories 4–5; Core Features 4–6; Success Metrics. +- `_techspec.md` → Doctor coordination and remediation; Test ownership and + no-mutation proof; Contract reconciliation; Testing Approach; Build Order 4. + From 6fd1d32311c5bcd32ad634b8cb4edcef76574ee3 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 17:22:35 -0300 Subject: [PATCH 29/48] chore: add the authorized Unicode collation dependency Roundfix-Spec: 0050-doctor-skill-readiness-hardening Roundfix-Task: task_01 --- .../task_01.md | 48 +++++++++++++++---- go.mod | 3 +- go.sum | 6 +++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_01.md b/docs/specs/0050-doctor-skill-readiness-hardening/task_01.md index c2d87c5..b787500 100644 --- a/docs/specs/0050-doctor-skill-readiness-hardening/task_01.md +++ b/docs/specs/0050-doctor-skill-readiness-hardening/task_01.md @@ -1,7 +1,7 @@ --- task: task_01 spec: 0050-doctor-skill-readiness-hardening -status: pending +status: completed type: chore complexity: low --- @@ -25,18 +25,18 @@ module files and this Task file may change. ## Subtasks -- [ ] Capture the pre-existing changed-file set. -- [ ] Add the authorized module through the Go toolchain. -- [ ] Inspect the resulting manifest and checksum changes. -- [ ] Prove the exact selected version and module integrity. -- [ ] Run the protected-file postflight. +- [x] Capture the pre-existing changed-file set. +- [x] Add the authorized module through the Go toolchain. +- [x] Inspect the resulting manifest and checksum changes. +- [x] Prove the exact selected version and module integrity. +- [x] Run the protected-file postflight. ## Acceptance Criteria -- [ ] `golang.org/x/text` resolves exactly to `v0.40.0`. -- [ ] `go.mod` and `go.sum` are internally consistent and pass module +- [x] `golang.org/x/text` resolves exactly to `v0.40.0`. +- [x] `go.mod` and `go.sum` are internally consistent and pass module verification. -- [ ] No path other than `go.mod`, `go.sum`, and this Task file changes in this +- [x] No path other than `go.mod`, `go.sum`, and this Task file changes in this Task. ## Context @@ -59,3 +59,33 @@ module files and this Task file may change. - `_techspec.md` → Project Constraints; Integration Points; Build Order 1; Decisions. +## Result + +Added `golang.org/x/text` at `v0.40.0` through `rtk go get`. Go's module +selection also raised the existing indirect `golang.org/x/sync` requirement +from `v0.20.0` to `v0.22.0`, the minimum selected by +`golang.org/x/text@v0.40.0`. No source code changed. + +Verification: + +- `rtk go list -m -f '{{.Path}} {{.Version}}' golang.org/x/text`: passed + with exactly `golang.org/x/text v0.40.0`. +- `rtk go mod verify`: passed with `all modules verified`. +- `rtk go mod download -json golang.org/x/text@v0.40.0`: passed and reported + the matching module and `go.mod` checksums plus the upstream + `refs/tags/v0.40.0` origin. +- Protected-file postflight: passed. The pre-existing changed-file set was + empty; the resulting unstaged set contains only `go.mod`, `go.sum`, and this + Task file, with no staged or untracked paths. + +Acceptance evidence: + +- Exact version: the focused module query returned + `golang.org/x/text v0.40.0`. +- Module consistency and integrity: `rtk go mod verify` exited successfully + and verified every downloaded module. +- Bounded changes: `rtk git -c core.fsmonitor=false diff --name-only` listed + only the three authorized paths; the cached-diff and untracked-file + inspections were empty. + +Follow-ups: none. diff --git a/go.mod b/go.mod index 5bbe67b..9882ab3 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,8 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.20.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/text v0.40.0 // indirect modernc.org/libc v1.72.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index f573ae9..d32f7d0 100644 --- a/go.sum +++ b/go.sum @@ -50,13 +50,19 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 93b31519a5c99d6071d4c0ee5ec899ab50a812cb Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 17:36:57 -0300 Subject: [PATCH 30/48] feat: centralize external skill hash compatibility Roundfix-Spec: 0050-doctor-skill-readiness-hardening Roundfix-Task: task_02 --- .../task_02.md | 79 ++++++++++++++--- internal/baseline/skills_restore.go | 20 ++--- internal/skillhash/hash.go | 38 ++++++++ internal/skillhash/hash_test.go | 88 +++++++++++++++++++ skills/skills.go | 37 ++------ 5 files changed, 207 insertions(+), 55 deletions(-) create mode 100644 internal/skillhash/hash.go create mode 100644 internal/skillhash/hash_test.go diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_02.md b/docs/specs/0050-doctor-skill-readiness-hardening/task_02.md index 221812f..5a87a65 100644 --- a/docs/specs/0050-doctor-skill-readiness-hardening/task_02.md +++ b/docs/specs/0050-doctor-skill-readiness-hardening/task_02.md @@ -1,7 +1,7 @@ --- task: task_02 spec: 0050-doctor-skill-readiness-hardening -status: pending +status: completed type: backend complexity: medium --- @@ -33,24 +33,24 @@ implementation while preserving the existing digest shape. ## Subtasks -- [ ] Add the pure shared hash component. -- [ ] Add the punctuation and Unicode compatibility oracle. -- [ ] Migrate Repository Skill Set folder hashing. -- [ ] Migrate Baseline external lock hashing. -- [ ] Remove duplicated comparators and stale explanatory comments. -- [ ] Exercise the real lock and restoration contracts. +- [x] Add the pure shared hash component. +- [x] Add the punctuation and Unicode compatibility oracle. +- [x] Migrate Repository Skill Set folder hashing. +- [x] Migrate Baseline external lock hashing. +- [x] Remove duplicated comparators and stale explanatory comments. +- [x] Exercise the real lock and restoration contracts. ## Acceptance Criteria -- [ ] The pinned corpus produces the exact skills CLI 1.5.19 digest. -- [ ] `_a` sorts before `-a`, and the Unicode cases match the pinned +- [x] The pinned corpus produces the exact skills CLI 1.5.19 digest. +- [x] `_a` sorts before `-a`, and the Unicode cases match the pinned `en-US` oracle. -- [ ] Changing one path or byte changes the digest. -- [ ] Both production consumers call the shared implementation and contain no +- [x] Changing one path or byte changes the digest. +- [x] Both production consumers call the shared implementation and contain no copied `strings.ToLower` path comparator. -- [ ] The real repository remains ready and existing Baseline restoration +- [x] The real repository remains ready and existing Baseline restoration hashes remain compatible. -- [ ] `go.mod` and `go.sum` remain unchanged from Task 01. +- [x] `go.mod` and `go.sum` remain unchanged from Task 01. ## Context @@ -78,3 +78,56 @@ implementation while preserving the existing digest shape. - `_techspec.md` → Shared external-skill hash; Integration Points; Testing Approach; Build Order 2. +## Result + +Added one pure `internal/skillhash` component that copies caller-owned input, +slash-normalizes relative paths, applies stable American English collation, +and emits the existing separator-free lowercase SHA-256 digest. Repository +Skill Set folder hashing and Baseline external lock generation now adapt their +collected files to that component; both copied lowercase comparators and the +stale approximation comment were removed. + +Verification: + +- Pre-change `rtk env GOCACHE=/private/tmp/roundfix-task02-go-cache go test + ./internal/skillhash -run '^TestSum' -count=1`: failed because `File` and + `Sum` did not exist, establishing the missing shared component. +- `rtk env GOCACHE=/private/tmp/roundfix-task02-go-cache go test + ./internal/skillhash ./skills ./internal/baseline -run + 'Test(Sum|SkillFolderHash|CheckRepositoryMatchesRealRepository|SkillsRestore)' + -count=1`: passed in all three packages. +- `rtk env GOCACHE=/private/tmp/roundfix-task02-go-cache go test -race + ./internal/skillhash ./skills ./internal/baseline -run + 'Test(Sum|SkillFolderHash|CheckRepositoryMatchesRealRepository|SkillsRestore)' + -count=1`: passed in all three packages with race detection. +- The first sandboxed `make verify` attempt was blocked when Go tried to + create the module-cache lock + `/Users/marcio/go/pkg/mod/cache/download/golang.org/x/sync/@v/v0.22.0.lock`; + an unfiltered package reproduction confirmed `operation not permitted`. +- `rtk env GOCACHE=/private/tmp/roundfix-task02-go-cache make verify`: + passed outside the task sandbox with 2,399 Go tests, four focused skill + contract tests, the Roundfix skill check, and the production build. +- `rtk git -c core.fsmonitor=false diff --check`: passed. + +Acceptance evidence: + +- `TestSumMatchesSkillsCLI1519` produced the pinned + `2a46b6d704729eafc0148969028b9cc4030813059e1f7524def2f38b433011d4` + digest for punctuation, digits, case, composed and decomposed Unicode, and + nested paths while proving caller order stayed unchanged. +- `TestSumSortsUnderscoreBeforeHyphen` proved `_a` collates before `-a`; + `TestSumChangesWhenPathOrContentChanges` proved either mutation changes the + digest. +- `rtk rg -n "skillhash\.Sum" skills/skills.go + internal/baseline/skills_restore.go` found the two production consumers. + The companion search for the copied comparator and stale comment returned no + matches. +- The focused suite passed `TestCheckRepositoryMatchesRealRepository`, the + existing external compatibility fixture, and all selected + `TestSkillsRestore` restoration and refusal contracts. +- Task 01 and final SHA-256 values for `go.mod` and `go.sum` are unchanged: + `3764c738663bd617989809ef9ebdd9849bb07e4a20a4f043fd8d914d6d65d38a` + and + `277534b002103e8090d3a5c593c9bb3d642e34ef255198a01cc3a022ea8e1fd2`. + +Follow-ups: none. diff --git a/internal/baseline/skills_restore.go b/internal/baseline/skills_restore.go index 75fb149..8b92f1f 100644 --- a/internal/baseline/skills_restore.go +++ b/internal/baseline/skills_restore.go @@ -19,6 +19,8 @@ import ( "sort" "strings" "unicode/utf8" + + "roundfix/internal/skillhash" ) const ( @@ -1239,20 +1241,14 @@ func portableRestoreDigest(files []restoreFile) string { } func externalSkillsLockDigest(files []restoreFile) string { - sort.Slice(files, func(i, j int) bool { - left := strings.ToLower(files[i].Path) - right := strings.ToLower(files[j].Path) - if left == right { - return files[i].Path > files[j].Path + hashFiles := make([]skillhash.File, len(files)) + for index, file := range files { + hashFiles[index] = skillhash.File{ + Path: file.Path, + Content: file.Content, } - return left < right - }) - digest := sha256.New() - for _, file := range files { - _, _ = digest.Write([]byte(file.Path)) - _, _ = digest.Write(file.Content) } - return hex.EncodeToString(digest.Sum(nil)) + return skillhash.Sum(hashFiles) } func sortRestoreFiles(files []restoreFile) { diff --git a/internal/skillhash/hash.go b/internal/skillhash/hash.go new file mode 100644 index 0000000..a934552 --- /dev/null +++ b/internal/skillhash/hash.go @@ -0,0 +1,38 @@ +package skillhash + +import ( + "crypto/sha256" + "encoding/hex" + "path/filepath" + "sort" + + "golang.org/x/text/collate" + "golang.org/x/text/language" +) + +// File is one already collected relative path and its content. +type File struct { + Path string + Content []byte +} + +// Sum returns the external skills CLI digest for files without changing their +// caller-owned order. +func Sum(files []File) string { + ordered := append([]File(nil), files...) + for index := range ordered { + ordered[index].Path = filepath.ToSlash(ordered[index].Path) + } + + collator := collate.New(language.AmericanEnglish) + sort.SliceStable(ordered, func(i, j int) bool { + return collator.CompareString(ordered[i].Path, ordered[j].Path) < 0 + }) + + digest := sha256.New() + for _, file := range ordered { + _, _ = digest.Write([]byte(file.Path)) + _, _ = digest.Write(file.Content) + } + return hex.EncodeToString(digest.Sum(nil)) +} diff --git a/internal/skillhash/hash_test.go b/internal/skillhash/hash_test.go new file mode 100644 index 0000000..c1d04d1 --- /dev/null +++ b/internal/skillhash/hash_test.go @@ -0,0 +1,88 @@ +// Suite: external skill hash compatibility +// Invariant: path-plus-content digests match skills CLI 1.5.19 under stable en-US collation. +// Boundary IN: already collected relative paths and file bytes. +// Boundary OUT: filesystem traversal and lock rendering, owned by skills and internal/baseline. +package skillhash + +import ( + "crypto/sha256" + "encoding/hex" + "reflect" + "testing" +) + +func TestSumMatchesSkillsCLI1519(t *testing.T) { + files := []File{ + {Path: "nested/É.md", Content: []byte("nested uppercase accent\n")}, + {Path: "-a", Content: []byte("hyphen\n")}, + {Path: "2-guide.md", Content: []byte("two\n")}, + {Path: "nested/deeper/ß.md", Content: []byte("eszett\n")}, + {Path: "A.md", Content: []byte("uppercase\n")}, + {Path: "nested/ä.md", Content: []byte("umlaut\n")}, + {Path: "_a", Content: []byte("underscore\n")}, + {Path: "10-guide.md", Content: []byte("ten\n")}, + {Path: "é.md", Content: []byte("precomposed accent\n")}, + {Path: "é.md", Content: []byte("decomposed accent\n")}, + {Path: "a.md", Content: []byte("lowercase\n")}, + {Path: "nested/deeper/ss.md", Content: []byte("double-s\n")}, + {Path: "nested/z.md", Content: []byte("zed\n")}, + } + before := append([]File(nil), files...) + + const want = "2a46b6d704729eafc0148969028b9cc4030813059e1f7524def2f38b433011d4" + if got := Sum(files); got != want { + t.Fatalf("Sum() = %q, want skills CLI 1.5.19 digest %q", got, want) + } + if !reflect.DeepEqual(files, before) { + t.Fatalf("Sum() changed caller-owned order:\ngot: %#v\nwant: %#v", files, before) + } +} + +func TestSumSortsUnderscoreBeforeHyphen(t *testing.T) { + files := []File{ + {Path: "-a", Content: []byte("hyphen\n")}, + {Path: "_a", Content: []byte("underscore\n")}, + } + orderedBytes := []byte("_aunderscore\n-ahyphen\n") + wantBytes := sha256.Sum256(orderedBytes) + want := hex.EncodeToString(wantBytes[:]) + + if got := Sum(files); got != want { + t.Fatalf("Sum() = %q, want underscore-before-hyphen digest %q", got, want) + } +} + +func TestSumChangesWhenPathOrContentChanges(t *testing.T) { + base := []File{ + {Path: "SKILL.md", Content: []byte("skill\n")}, + {Path: "references/guide.md", Content: []byte("guide\n")}, + } + wantDifferentFrom := Sum(base) + tests := []struct { + name string + files []File + }{ + { + name: "path changes", + files: []File{ + {Path: "SKILL.md", Content: []byte("skill\n")}, + {Path: "references/changed.md", Content: []byte("guide\n")}, + }, + }, + { + name: "content changes", + files: []File{ + {Path: "SKILL.md", Content: []byte("skill\n")}, + {Path: "references/guide.md", Content: []byte("guide changed\n")}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := Sum(test.files); got == wantDifferentFrom { + t.Fatalf("Sum() = unchanged digest %q", got) + } + }) + } +} diff --git a/skills/skills.go b/skills/skills.go index 4abc1c7..d471b09 100644 --- a/skills/skills.go +++ b/skills/skills.go @@ -2,7 +2,6 @@ package skills import ( "context" - "crypto/sha256" "embed" "errors" "fmt" @@ -13,6 +12,8 @@ import ( "strings" "gopkg.in/yaml.v3" + + "roundfix/internal/skillhash" ) //go:embed roundfix write-idea write-prd write-techspec write-tasks setup-context-driven implement-task implement-spec brainstorming council business-analyst archive-spec qa-gate evidence-gate recommended.txt @@ -137,11 +138,7 @@ func SkillFolderHash(root string) (string, error) { return "", fmt.Errorf("inspect skill folder %q: root is not a directory", root) } - type hashFile struct { - path string - data []byte - } - var files []hashFile + var files []skillhash.File tree := os.DirFS(root) if err := fs.WalkDir(tree, ".", func(path string, entry fs.DirEntry, walkErr error) error { fullPath := filepath.Join(root, filepath.FromSlash(path)) @@ -172,36 +169,16 @@ func SkillFolderHash(root string) (string, error) { if err != nil { return fmt.Errorf("read skill folder file %q: %w", fullPath, err) } - files = append(files, hashFile{ - path: filepath.ToSlash(path), - data: data, + files = append(files, skillhash.File{ + Path: filepath.ToSlash(path), + Content: data, }) return nil }); err != nil { return "", fmt.Errorf("hash skill folder %q: %w", root, err) } - sort.Slice(files, func(i, j int) bool { - // String.localeCompare compares the normalized repository paths - // case-insensitively first and puts lowercase before uppercase when - // paths otherwise collate equally. - left := strings.ToLower(files[i].path) - right := strings.ToLower(files[j].path) - if left == right { - return files[i].path > files[j].path - } - return left < right - }) - digest := sha256.New() - for _, file := range files { - if _, err := digest.Write([]byte(file.path)); err != nil { - return "", fmt.Errorf("hash skill folder path %q: %w", file.path, err) - } - if _, err := digest.Write(file.data); err != nil { - return "", fmt.Errorf("hash skill folder file %q: %w", file.path, err) - } - } - return fmt.Sprintf("%x", digest.Sum(nil)), nil + return skillhash.Sum(files), nil } func Check() []Diagnostic { From be91ebc9b7350b62d75fd08c5cfc48f87f301413 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 17:51:36 -0300 Subject: [PATCH 31/48] feat: anchor Repository Skill Set filesystem reads Roundfix-Spec: 0050-doctor-skill-readiness-hardening Roundfix-Task: task_03 --- .../task_03.md | 80 ++- skills/repository.go | 209 ++++++- skills/repository_test.go | 508 ++++++++++++++++++ skills/skills.go | 21 +- skills/skills_test.go | 357 ------------ 5 files changed, 770 insertions(+), 405 deletions(-) create mode 100644 skills/repository_test.go diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_03.md b/docs/specs/0050-doctor-skill-readiness-hardening/task_03.md index 39f2a34..d15e29d 100644 --- a/docs/specs/0050-doctor-skill-readiness-hardening/task_03.md +++ b/docs/specs/0050-doctor-skill-readiness-hardening/task_03.md @@ -1,7 +1,7 @@ --- task: task_03 spec: 0050-doctor-skill-readiness-hardening -status: pending +status: completed type: backend complexity: high --- @@ -35,25 +35,25 @@ read, while existing missing and outdated classifications remain stable. ## Subtasks -- [ ] Introduce the anchored repository read boundary. -- [ ] Validate shared ancestors and the lock authority without following links. -- [ ] Adapt owned and external skill inspection to rooted operations. -- [ ] Relocate repository tests and helpers to their owning file. -- [ ] Add ancestor and lock symlink regression fixtures. -- [ ] Prove existing missing, outdated, malformed, and no-mutation behavior. +- [x] Introduce the anchored repository read boundary. +- [x] Validate shared ancestors and the lock authority without following links. +- [x] Adapt owned and external skill inspection to rooted operations. +- [x] Relocate repository tests and helpers to their owning file. +- [x] Add ancestor and lock symlink regression fixtures. +- [x] Prove existing missing, outdated, malformed, and no-mutation behavior. ## Acceptance Criteria -- [ ] A symlinked `.agents` fails readiness and its target is not read. -- [ ] A symlinked `.agents/skills` fails readiness and its target is not read. -- [ ] A symlinked `skills-lock.json` fails readiness and its target is not +- [x] A symlinked `.agents` fails readiness and its target is not read. +- [x] A symlinked `.agents/skills` fails readiness and its target is not read. +- [x] A symlinked `skills-lock.json` fails readiness and its target is not decoded. -- [ ] A complete repository still reports ready; missing and outdated +- [x] A complete repository still reports ready; missing and outdated classifications remain sorted and unchanged. -- [ ] Nested links and special entries remain rejected without escaping the +- [x] Nested links and special entries remain rejected without escaping the anchored repository. -- [ ] Repository checks remain read-only. -- [ ] `skills/repository.go` has its canonical test file and +- [x] Repository checks remain read-only. +- [x] `skills/repository.go` has its canonical test file and `skills/skills_test.go` retains only `skills.go`-owned coverage and genuinely shared helpers. @@ -82,3 +82,55 @@ read, while existing missing and outdated classifications remain stable. - `_techspec.md` → Root-anchored repository reads; Test ownership and no-mutation proof; Testing Approach; Build Order 3. +## Result + +Implemented one `os.Root` boundary for the supplied Git root. The repository +checker now uses rooted non-following metadata checks for `skills-lock.json`, +`.agents`, `.agents/skills`, and every required skill root before reading or +walking them. Shared-authority links fail with an unclassified +`RepositoryReadinessError`; owned and external skill failures retain their +ownership when it is known, and wrapped filesystem causes remain available to +`errors.Is` and `errors.As`. + +Repository checks reuse the root-backed filesystem for owned comparison and +external hashing. Missing shared directories classify every required skill as +missing, while ready, outdated, malformed, nested-link, special-entry, stable +sorting, and no-mutation behavior remain covered. + +Moved every `TestCheckRepository*` case and repository fixture helper to +`skills/repository_test.go`. `skills/skills_test.go` retains +`TestSkillFolderHash*`, the hash compatibility fixture, and the shared file +writer used by both canonical suites. + +Verification: + +- `rtk go test ./skills -run 'TestCheckRepository' -count=1` — passed, + 26 tests. +- `rtk go test -race ./skills -run 'TestCheckRepository' -count=1` — passed, + 26 tests. +- `rtk go test ./skills -count=1` — passed, 106 tests. +- `rtk make verify` — passed: 2,406 tests across 23 packages, 4 focused skill + contract tests, Repository Skill Set check, and the Roundfix build. +- `rtk git -c core.fsmonitor=false diff --check` — passed. + +Acceptance evidence: + +- `TestCheckRepositoryRejectsSymlinkedAuthoritiesBeforeReadingTargets` proves + `.agents`, `.agents/skills`, and `skills-lock.json` links fail at the + authority path; the malformed lock target is never decoded. +- `TestCheckRepositoryReportsReadyRequiredSetWithoutMutation`, + `TestCheckRepositoryClassifiesMissingAndOutdatedSkills`, and + `TestCheckRepositoryClassifiesMissingSharedSkillDirectories` prove ready, + read-only, missing, outdated, and sorted classification behavior. +- `TestCheckRepositoryHandlesNestedLinksSpecialEntriesAndStableOrdering` + proves nested links and special entries remain rejected within the anchored + repository view. +- `TestCheckRepositoryWrapsFilesystemCauses` proves `errors.Is` and + `errors.As` survive repository-root failures. +- Source inspection found one `os.OpenRoot` call and no direct repository + authority `os.ReadFile`, `os.Lstat`, or `os.DirFS` calls in + `skills/repository.go`; test-name inspection places all + `TestCheckRepository*` coverage in `skills/repository_test.go` and all + `TestSkillFolderHash*` coverage in `skills/skills_test.go`. + +Follow-ups: none. diff --git a/skills/repository.go b/skills/repository.go index db6d536..58b3ba3 100644 --- a/skills/repository.go +++ b/skills/repository.go @@ -8,6 +8,7 @@ import ( "io" "io/fs" "os" + "path" "path/filepath" "sort" "strings" @@ -82,8 +83,8 @@ func CheckRepository(root string) (RepositoryReadiness, error) { return checkRepository(root, owned, external, files) } -func checkRepository(root string, owned []string, external []string, files []File) (RepositoryReadiness, error) { - readiness := RepositoryReadiness{ +func checkRepository(root string, owned []string, external []string, files []File) (readiness RepositoryReadiness, returnErr error) { + readiness = RepositoryReadiness{ OwnedRequired: len(owned), ExternalRequired: len(external), } @@ -95,23 +96,58 @@ func checkRepository(root string, owned []string, external []string, files []Fil return readiness, repositoryReadinessError("", "validate repository root", "", errors.New("path is empty")) } + repositoryRoot, err := os.OpenRoot(root) + if err != nil { + return readiness, repositoryReadinessError("", "open repository root", root, err) + } + defer func() { + if closeErr := repositoryRoot.Close(); closeErr != nil { + wrapped := repositoryReadinessError("", "close repository root", root, closeErr) + if returnErr == nil { + returnErr = wrapped + return + } + returnErr = errors.Join(returnErr, wrapped) + } + }() + expected, err := expectedOwnedFiles(owned, files) if err != nil { return readiness, err } - lockPath := filepath.Join(root, "skills-lock.json") - lock, err := readLocalSkillsLock(lockPath) + const lockPath = "skills-lock.json" + lockDisplayPath := repositoryDisplayPath(root, lockPath) + lock, err := readLocalSkillsLock(repositoryRoot, lockPath, lockDisplayPath) if err != nil { return readiness, err } - externalHashes, err := requiredExternalHashes(lockPath, lock, external) + externalHashes, err := requiredExternalHashes(lockDisplayPath, lock, external) if err != nil { return readiness, err } - skillsRoot := filepath.Join(root, ".agents", "skills") + for _, sharedDirectory := range []string{".agents", ".agents/skills"} { + missing, err := inspectSharedSkillDirectory(repositoryRoot, sharedDirectory, repositoryDisplayPath(root, sharedDirectory)) + if err != nil { + return readiness, err + } + if missing { + readiness.MissingOwned = append(readiness.MissingOwned, owned...) + readiness.MissingExternal = append(readiness.MissingExternal, external...) + sortRepositoryReadiness(&readiness) + return readiness, nil + } + } + + const skillsRoot = ".agents/skills" for _, name := range owned { - missing, outdated, err := compareOwnedSkill(filepath.Join(skillsRoot, name), expected[name]) + skillRoot := path.Join(skillsRoot, name) + missing, outdated, err := compareOwnedSkill( + repositoryRoot, + skillRoot, + repositoryDisplayPath(root, skillRoot), + expected[name], + ) if err != nil { return readiness, err } @@ -122,29 +158,91 @@ func checkRepository(root string, owned []string, external []string, files []Fil } } for _, name := range external { - skillRoot := filepath.Join(skillsRoot, name) - _, err := os.Lstat(skillRoot) + skillRoot := path.Join(skillsRoot, name) + skillDisplayPath := repositoryDisplayPath(root, skillRoot) + info, err := repositoryRoot.Lstat(skillRoot) if errors.Is(err, fs.ErrNotExist) { readiness.MissingExternal = append(readiness.MissingExternal, name) continue } if err != nil { - return readiness, repositoryReadinessError(RepositoryOwnershipExternal, "inspect external skill", skillRoot, err) + return readiness, repositoryReadinessError( + RepositoryOwnershipExternal, + "inspect external skill", + skillDisplayPath, + err, + ) } - hash, err := SkillFolderHash(skillRoot) + if info.Mode()&fs.ModeSymlink != 0 { + return readiness, repositoryReadinessError( + RepositoryOwnershipExternal, + "inspect external skill", + skillDisplayPath, + errors.New("symbolic links are not supported"), + ) + } + if !info.IsDir() { + return readiness, repositoryReadinessError( + RepositoryOwnershipExternal, + "inspect external skill", + skillDisplayPath, + errors.New("skill root is not a directory"), + ) + } + hash, err := skillFolderHash(repositoryRoot.FS(), skillRoot, skillDisplayPath) if err != nil { - return readiness, repositoryReadinessError(RepositoryOwnershipExternal, "hash external skill", skillRoot, err) + return readiness, repositoryReadinessError( + RepositoryOwnershipExternal, + "hash external skill", + skillDisplayPath, + err, + ) } if hash != externalHashes[name] { readiness.OutdatedExternal = append(readiness.OutdatedExternal, name) } } + sortRepositoryReadiness(&readiness) + return readiness, nil +} + +func inspectSharedSkillDirectory(repositoryRoot *os.Root, relative string, display string) (bool, error) { + info, err := repositoryRoot.Lstat(relative) + if errors.Is(err, fs.ErrNotExist) { + return true, nil + } + if err != nil { + return false, repositoryReadinessError("", "inspect repository skill directory", display, err) + } + if info.Mode()&fs.ModeSymlink != 0 { + return false, repositoryReadinessError( + "", + "inspect repository skill directory", + display, + errors.New("symbolic links are not supported"), + ) + } + if !info.IsDir() { + return false, repositoryReadinessError( + "", + "inspect repository skill directory", + display, + errors.New("path is not a directory"), + ) + } + return false, nil +} + +func repositoryDisplayPath(root string, relative string) string { + return filepath.Join(root, filepath.FromSlash(relative)) +} + +func sortRepositoryReadiness(readiness *RepositoryReadiness) { sort.Strings(readiness.MissingOwned) sort.Strings(readiness.OutdatedOwned) sort.Strings(readiness.MissingExternal) sort.Strings(readiness.OutdatedExternal) - return readiness, nil } func validateRequiredSkillNames(owned []string, external []string) error { @@ -280,28 +378,69 @@ func safeRelativeArtifactPath(path string) bool { return cleaned == path && cleaned != "." && cleaned != ".." && !strings.HasPrefix(cleaned, "../") } -func readLocalSkillsLock(path string) (localSkillsLock, error) { - data, err := os.ReadFile(path) +func readLocalSkillsLock(repositoryRoot *os.Root, path string, displayPath string) (localSkillsLock, error) { + info, err := repositoryRoot.Lstat(path) + if err != nil { + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "inspect skills lock", + displayPath, + err, + ) + } + if info.Mode()&fs.ModeSymlink != 0 { + return localSkillsLock{}, repositoryReadinessError( + "", + "inspect skills lock", + displayPath, + errors.New("symbolic links are not supported"), + ) + } + if !info.Mode().IsRegular() { + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "inspect skills lock", + displayPath, + errors.New("lock is not a regular file"), + ) + } + + data, err := repositoryRoot.ReadFile(path) if err != nil { - return localSkillsLock{}, repositoryReadinessError(RepositoryOwnershipExternal, "read skills lock", path, err) + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "read skills lock", + displayPath, + err, + ) } var lock localSkillsLock decoder := json.NewDecoder(bytes.NewReader(data)) if err := decoder.Decode(&lock); err != nil { - return localSkillsLock{}, repositoryReadinessError(RepositoryOwnershipExternal, "decode skills lock", path, err) + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "decode skills lock", + displayPath, + err, + ) } var trailing json.RawMessage if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { if err == nil { err = errors.New("unexpected trailing JSON value") } - return localSkillsLock{}, repositoryReadinessError(RepositoryOwnershipExternal, "decode skills lock", path, err) + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "decode skills lock", + displayPath, + err, + ) } if lock.Version != 1 { return localSkillsLock{}, repositoryReadinessError( RepositoryOwnershipExternal, "validate skills lock version", - path, + displayPath, fmt.Errorf("got %d, want 1", lock.Version), ) } @@ -347,26 +486,40 @@ func lowerHexSHA256(value string) bool { const sha256HexLength = 64 -func compareOwnedSkill(root string, expected map[string][]byte) (missing bool, outdated bool, err error) { - info, lstatErr := os.Lstat(root) +func compareOwnedSkill( + repositoryRoot *os.Root, + root string, + displayRoot string, + expected map[string][]byte, +) (missing bool, outdated bool, err error) { + info, lstatErr := repositoryRoot.Lstat(root) if errors.Is(lstatErr, fs.ErrNotExist) { return true, false, nil } if lstatErr != nil { - return false, false, repositoryReadinessError(RepositoryOwnershipOwned, "inspect Roundfix-owned skill", root, lstatErr) + return false, false, repositoryReadinessError( + RepositoryOwnershipOwned, + "inspect Roundfix-owned skill", + displayRoot, + lstatErr, + ) } if info.Mode()&fs.ModeSymlink != 0 || !info.IsDir() { return false, true, nil } found := make(map[string]struct{}, len(expected)) - tree := os.DirFS(root) - walkErr := fs.WalkDir(tree, ".", func(path string, entry fs.DirEntry, walkErr error) error { - fullPath := filepath.Join(root, filepath.FromSlash(path)) + tree := repositoryRoot.FS() + walkErr := fs.WalkDir(tree, root, func(entryPath string, entry fs.DirEntry, walkErr error) error { + relative := strings.TrimPrefix(entryPath, root+"/") + if entryPath == root { + relative = "." + } + fullPath := filepath.Join(displayRoot, filepath.FromSlash(relative)) if walkErr != nil { return repositoryReadinessError(RepositoryOwnershipOwned, "walk Roundfix-owned skill artifact", fullPath, walkErr) } - if path == "." { + if entryPath == root { return nil } if entry.Type()&fs.ModeSymlink != 0 { @@ -384,13 +537,13 @@ func compareOwnedSkill(root string, expected map[string][]byte) (missing bool, o outdated = true return nil } - relative := filepath.ToSlash(path) + relative = filepath.ToSlash(relative) want, required := expected[relative] if !required { outdated = true return nil } - data, readErr := fs.ReadFile(tree, path) + data, readErr := fs.ReadFile(tree, entryPath) if readErr != nil { return repositoryReadinessError(RepositoryOwnershipOwned, "read Roundfix-owned skill artifact", fullPath, readErr) } diff --git a/skills/repository_test.go b/skills/repository_test.go new file mode 100644 index 0000000..02b4083 --- /dev/null +++ b/skills/repository_test.go @@ -0,0 +1,508 @@ +package skills + +import ( + "encoding/json" + "errors" + "io/fs" + "net" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + "testing" +) + +func TestCheckRepositoryMatchesRealRepository(t *testing.T) { + root, err := filepath.Abs("..") + if err != nil { + t.Fatalf("resolve real repository root: %v", err) + } + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check real repository: %v", err) + } + if !got.Ready() { + t.Fatalf("real repository readiness = %#v, want ready", got) + } +} + +func TestCheckRepositoryReportsReadyRequiredSetWithoutMutation(t *testing.T) { + root := writeReadyRepositoryFixture(t) + before := snapshotRepositoryFixture(t, root) + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check ready repository: %v", err) + } + if !got.Ready() { + t.Fatalf("expected repository to be ready, got %#v", got) + } + if got.OwnedRequired != 14 || got.ExternalRequired != 25 { + t.Fatalf("required counts = owned %d external %d, want 14 and 25", got.OwnedRequired, got.ExternalRequired) + } + if after := snapshotRepositoryFixture(t, root); !reflect.DeepEqual(after, before) { + t.Fatalf("CheckRepository mutated fixture:\nbefore=%#v\nafter=%#v", before, after) + } +} + +func TestCheckRepositoryClassifiesMissingAndOutdatedSkills(t *testing.T) { + tests := []struct { + name string + mutate func(*testing.T, string) + want RepositoryReadiness + }{ + { + name: "missing owned", + mutate: func(t *testing.T, root string) { + t.Helper() + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "archive-spec")); err != nil { + t.Fatalf("remove owned skill: %v", err) + } + }, + want: RepositoryReadiness{MissingOwned: []string{"archive-spec"}}, + }, + { + name: "missing external", + mutate: func(t *testing.T, root string) { + t.Helper() + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "agentic-cli-design")); err != nil { + t.Fatalf("remove external skill: %v", err) + } + }, + want: RepositoryReadiness{MissingExternal: []string{"agentic-cli-design"}}, + }, + { + name: "changed owned file", + mutate: func(t *testing.T, root string) { + t.Helper() + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "roundfix"), "SKILL.md", "changed\n") + }, + want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, + }, + { + name: "added owned file", + mutate: func(t *testing.T, root string) { + t.Helper() + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "roundfix"), "unexpected.txt", "extra\n") + }, + want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, + }, + { + name: "removed owned file", + mutate: func(t *testing.T, root string) { + t.Helper() + path := filepath.Join(root, ".agents", "skills", "roundfix", "agents", "openai.yaml") + if err := os.Remove(path); err != nil { + t.Fatalf("remove owned artifact: %v", err) + } + }, + want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, + }, + { + name: "changed external file", + mutate: func(t *testing.T, root string) { + t.Helper() + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "agentic-cli-design"), "SKILL.md", "changed\n") + }, + want: RepositoryReadiness{OutdatedExternal: []string{"agentic-cli-design"}}, + }, + { + name: "added external file", + mutate: func(t *testing.T, root string) { + t.Helper() + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "agentic-cli-design"), "unexpected.txt", "extra\n") + }, + want: RepositoryReadiness{OutdatedExternal: []string{"agentic-cli-design"}}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := writeReadyRepositoryFixture(t) + test.mutate(t, root) + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check repository: %v", err) + } + got.OwnedRequired = 0 + got.ExternalRequired = 0 + if !reflect.DeepEqual(got, test.want) { + t.Fatalf("readiness = %#v, want %#v", got, test.want) + } + }) + } +} + +func TestCheckRepositoryClassifiesMissingSharedSkillDirectories(t *testing.T) { + tests := []struct { + name string + path string + }{ + {name: "missing .agents", path: ".agents"}, + {name: "missing .agents/skills", path: filepath.Join(".agents", "skills")}, + } + + wantOwned := Names() + sort.Strings(wantOwned) + wantExternal := Recommended() + sort.Strings(wantExternal) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := writeReadyRepositoryFixture(t) + if err := os.RemoveAll(filepath.Join(root, test.path)); err != nil { + t.Fatalf("remove shared skill directory %q: %v", test.path, err) + } + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check repository: %v", err) + } + if !reflect.DeepEqual(got.MissingOwned, wantOwned) { + t.Fatalf("missing owned = %v, want %v", got.MissingOwned, wantOwned) + } + if !reflect.DeepEqual(got.MissingExternal, wantExternal) { + t.Fatalf("missing external = %v, want %v", got.MissingExternal, wantExternal) + } + }) + } +} + +func TestCheckRepositoryIgnoresUnrelatedSkillsAndLockEntries(t *testing.T) { + root := writeReadyRepositoryFixture(t) + writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "unrelated"), "SKILL.md", "unrelated\n") + lock := readRepositoryLockFixture(t, root) + lock.Skills["../ignored-unsafe-entry"] = repositoryLockSkillFixture{ComputedHash: "not-a-hash"} + writeRepositoryLockFixture(t, root, lock) + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check repository with unrelated entries: %v", err) + } + if !got.Ready() { + t.Fatalf("unrelated entries changed readiness: %#v", got) + } +} + +func TestCheckRepositoryRejectsMalformedLockAndUnsafeRequiredNames(t *testing.T) { + tests := []struct { + name string + write func(*testing.T, string) + }{ + { + name: "missing lock", + write: func(t *testing.T, root string) { + if err := os.Remove(filepath.Join(root, "skills-lock.json")); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "malformed lock", + write: func(t *testing.T, root string) { + if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), []byte("{"), 0o644); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "wrong version", + write: func(t *testing.T, root string) { + lock := readRepositoryLockFixture(t, root) + lock.Version = 2 + writeRepositoryLockFixture(t, root, lock) + }, + }, + { + name: "missing required entry", + write: func(t *testing.T, root string) { + lock := readRepositoryLockFixture(t, root) + delete(lock.Skills, Recommended()[0]) + writeRepositoryLockFixture(t, root, lock) + }, + }, + { + name: "invalid required hash", + write: func(t *testing.T, root string) { + lock := readRepositoryLockFixture(t, root) + lock.Skills[Recommended()[0]] = repositoryLockSkillFixture{ComputedHash: strings.Repeat("A", 64)} + writeRepositoryLockFixture(t, root, lock) + }, + }, + { + name: "unreadable lock artifact shape", + write: func(t *testing.T, root string) { + path := filepath.Join(root, "skills-lock.json") + if err := os.Remove(path); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(path, 0o755); err != nil { + t.Fatal(err) + } + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := writeReadyRepositoryFixture(t) + test.write(t, root) + + _, err := CheckRepository(root) + if err == nil { + t.Fatal("expected repository lock error") + } + var repositoryErr *RepositoryReadinessError + if !errors.As(err, &repositoryErr) { + t.Fatalf("expected RepositoryReadinessError, got %T: %v", err, err) + } + if repositoryErr.Ownership != RepositoryOwnershipExternal { + t.Fatalf("lock error ownership = %q, want external", repositoryErr.Ownership) + } + if !strings.Contains(err.Error(), filepath.Join(root, "skills-lock.json")) { + t.Fatalf("lock error does not name path: %v", err) + } + }) + } + + root := t.TempDir() + outside := filepath.Join(filepath.Dir(root), "outside") + writeSkillHashTestFile(t, outside, "marker", "must not be read\n") + _, err := checkRepository(root, []string{"../outside"}, nil, nil) + if err == nil || !strings.Contains(err.Error(), "unsafe") { + t.Fatalf("expected unsafe required name error, got %v", err) + } +} + +func TestCheckRepositoryWrapsFilesystemCauses(t *testing.T) { + root := filepath.Join(t.TempDir(), "missing") + + _, err := CheckRepository(root) + if err == nil { + t.Fatal("expected missing repository root error") + } + if !errors.Is(err, fs.ErrNotExist) { + t.Fatalf("expected wrapped fs.ErrNotExist, got %v", err) + } + var repositoryErr *RepositoryReadinessError + if !errors.As(err, &repositoryErr) { + t.Fatalf("expected RepositoryReadinessError, got %T: %v", err, err) + } + if repositoryErr.Path != root || repositoryErr.Ownership != "" { + t.Fatalf("repository root error = %#v, want unclassified path %q", repositoryErr, root) + } +} + +func TestCheckRepositoryRejectsSymlinkedAuthoritiesBeforeReadingTargets(t *testing.T) { + tests := []struct { + name string + authority string + target func(*testing.T, string) string + }{ + { + name: "symlinked .agents", + authority: ".agents", + target: func(_ *testing.T, outside string) string { + return filepath.Join(outside, ".agents") + }, + }, + { + name: "symlinked .agents/skills", + authority: filepath.Join(".agents", "skills"), + target: func(_ *testing.T, outside string) string { + return filepath.Join(outside, ".agents", "skills") + }, + }, + { + name: "symlinked skills-lock.json", + authority: "skills-lock.json", + target: func(t *testing.T, outside string) string { + t.Helper() + target := filepath.Join(outside, "malformed-lock.json") + if err := os.WriteFile(target, []byte("{"), 0o644); err != nil { + t.Fatalf("write malformed lock target: %v", err) + } + return target + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := writeReadyRepositoryFixture(t) + outside := writeReadyRepositoryFixture(t) + authority := filepath.Join(root, test.authority) + if err := os.RemoveAll(authority); err != nil { + t.Fatalf("remove repository authority %q: %v", authority, err) + } + if err := os.Symlink(test.target(t, outside), authority); err != nil { + t.Skipf("create authority symlink: %v", err) + } + + _, err := CheckRepository(root) + if err == nil { + t.Fatal("expected symlinked authority to fail readiness") + } + var repositoryErr *RepositoryReadinessError + if !errors.As(err, &repositoryErr) { + t.Fatalf("expected RepositoryReadinessError, got %T: %v", err, err) + } + if repositoryErr.Path != authority { + t.Fatalf("authority error path = %q, want %q", repositoryErr.Path, authority) + } + if repositoryErr.Ownership != "" { + t.Fatalf("authority error ownership = %q, want unclassified", repositoryErr.Ownership) + } + if strings.Contains(repositoryErr.Operation, "decode") { + t.Fatalf("authority target was decoded before rejection: %v", err) + } + }) + } +} + +func TestCheckRepositoryHandlesNestedLinksSpecialEntriesAndStableOrdering(t *testing.T) { + root := writeReadyRepositoryFixture(t) + ownedPath := filepath.Join(root, ".agents", "skills", "roundfix", "SKILL.md") + if err := os.Remove(ownedPath); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join(root, "skills-lock.json"), ownedPath); err != nil { + t.Skipf("create owned artifact symlink: %v", err) + } + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "agentic-cli-design")); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "archive-spec")); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "autoresearch")); err != nil { + t.Fatal(err) + } + + got, err := CheckRepository(root) + if err != nil { + t.Fatalf("check repository with owned symlink: %v", err) + } + if !reflect.DeepEqual(got.MissingOwned, []string{"archive-spec"}) || + !reflect.DeepEqual(got.MissingExternal, []string{"agentic-cli-design", "autoresearch"}) || + !reflect.DeepEqual(got.OutdatedOwned, []string{"roundfix"}) { + t.Fatalf("unstable classifications: %#v", got) + } + + externalRoot := writeReadyRepositoryFixture(t) + externalPath := filepath.Join(externalRoot, ".agents", "skills", "agentic-cli-design", "linked") + if err := os.Symlink(filepath.Join(externalRoot, "skills-lock.json"), externalPath); err != nil { + t.Skipf("create external symlink: %v", err) + } + _, err = CheckRepository(externalRoot) + if err == nil || !strings.Contains(err.Error(), externalPath) { + t.Fatalf("expected external symlink error naming %q, got %v", externalPath, err) + } + + specialRoot := writeReadyRepositoryFixture(t) + specialPath := filepath.Join(specialRoot, ".agents", "skills", "agentic-cli-design", "socket") + listener, err := net.Listen("unix", specialPath) + if err != nil { + t.Skipf("create special filesystem entry: %v", err) + } + t.Cleanup(func() { + if err := listener.Close(); err != nil { + t.Errorf("close special-entry listener: %v", err) + } + }) + _, err = CheckRepository(specialRoot) + if err == nil || !strings.Contains(err.Error(), specialPath) { + t.Fatalf("expected external special-entry error naming %q, got %v", specialPath, err) + } +} + +type repositoryLockFixture struct { + Version int `json:"version"` + Skills map[string]repositoryLockSkillFixture `json:"skills"` +} + +type repositoryLockSkillFixture struct { + ComputedHash string `json:"computedHash"` +} + +func writeReadyRepositoryFixture(t *testing.T) string { + t.Helper() + root := t.TempDir() + skillsRoot := filepath.Join(root, ".agents", "skills") + files, err := Files() + if err != nil { + t.Fatalf("read embedded skills: %v", err) + } + for _, file := range files { + writeSkillHashTestFile(t, skillsRoot, file.Path, string(file.Data)) + } + + lock := repositoryLockFixture{ + Version: 1, + Skills: make(map[string]repositoryLockSkillFixture, len(Recommended())), + } + for _, name := range Recommended() { + skillRoot := filepath.Join(skillsRoot, name) + writeSkillHashTestFile(t, skillRoot, "SKILL.md", name+"\n") + hash, err := SkillFolderHash(skillRoot) + if err != nil { + t.Fatalf("hash external fixture %q: %v", name, err) + } + lock.Skills[name] = repositoryLockSkillFixture{ComputedHash: hash} + } + writeRepositoryLockFixture(t, root, lock) + return root +} + +func readRepositoryLockFixture(t *testing.T, root string) repositoryLockFixture { + t.Helper() + data, err := os.ReadFile(filepath.Join(root, "skills-lock.json")) + if err != nil { + t.Fatalf("read repository lock fixture: %v", err) + } + var lock repositoryLockFixture + if err := json.Unmarshal(data, &lock); err != nil { + t.Fatalf("decode repository lock fixture: %v", err) + } + return lock +} + +func writeRepositoryLockFixture(t *testing.T, root string, lock repositoryLockFixture) { + t.Helper() + data, err := json.MarshalIndent(lock, "", " ") + if err != nil { + t.Fatalf("encode repository lock fixture: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), append(data, '\n'), 0o644); err != nil { + t.Fatalf("write repository lock fixture: %v", err) + } +} + +func snapshotRepositoryFixture(t *testing.T, root string) map[string]string { + t.Helper() + snapshot := make(map[string]string) + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + snapshot[filepath.ToSlash(relative)] = string(data) + return nil + }) + if err != nil { + t.Fatalf("snapshot repository fixture: %v", err) + } + return snapshot +} diff --git a/skills/skills.go b/skills/skills.go index d471b09..c9ba0c8 100644 --- a/skills/skills.go +++ b/skills/skills.go @@ -138,14 +138,23 @@ func SkillFolderHash(root string) (string, error) { return "", fmt.Errorf("inspect skill folder %q: root is not a directory", root) } + return skillFolderHash(os.DirFS(root), ".", root) +} + +func skillFolderHash(tree fs.FS, root string, displayRoot string) (string, error) { var files []skillhash.File - tree := os.DirFS(root) - if err := fs.WalkDir(tree, ".", func(path string, entry fs.DirEntry, walkErr error) error { - fullPath := filepath.Join(root, filepath.FromSlash(path)) + if err := fs.WalkDir(tree, root, func(path string, entry fs.DirEntry, walkErr error) error { + relative := path + if path == root { + relative = "." + } else if root != "." { + relative = strings.TrimPrefix(path, root+"/") + } + fullPath := filepath.Join(displayRoot, filepath.FromSlash(relative)) if walkErr != nil { return fmt.Errorf("walk skill folder entry %q: %w", fullPath, walkErr) } - if path == "." { + if path == root { return nil } entryInfo, err := entry.Info() @@ -170,12 +179,12 @@ func SkillFolderHash(root string) (string, error) { return fmt.Errorf("read skill folder file %q: %w", fullPath, err) } files = append(files, skillhash.File{ - Path: filepath.ToSlash(path), + Path: filepath.ToSlash(relative), Content: data, }) return nil }); err != nil { - return "", fmt.Errorf("hash skill folder %q: %w", root, err) + return "", fmt.Errorf("hash skill folder %q: %w", displayRoot, err) } return skillhash.Sum(files), nil diff --git a/skills/skills_test.go b/skills/skills_test.go index c289d0a..88a4970 100644 --- a/skills/skills_test.go +++ b/skills/skills_test.go @@ -169,363 +169,6 @@ func TestSkillFolderHashWrapsMissingRootError(t *testing.T) { } } -func TestCheckRepositoryMatchesRealRepository(t *testing.T) { - root, err := filepath.Abs("..") - if err != nil { - t.Fatalf("resolve real repository root: %v", err) - } - got, err := CheckRepository(root) - if err != nil { - t.Fatalf("check real repository: %v", err) - } - if !got.Ready() { - t.Fatalf("real repository readiness = %#v, want ready", got) - } -} - -func TestCheckRepositoryReportsReadyRequiredSetWithoutMutation(t *testing.T) { - root := writeReadyRepositoryFixture(t) - before := snapshotRepositoryFixture(t, root) - - got, err := CheckRepository(root) - if err != nil { - t.Fatalf("check ready repository: %v", err) - } - if !got.Ready() { - t.Fatalf("expected repository to be ready, got %#v", got) - } - if got.OwnedRequired != 14 || got.ExternalRequired != 25 { - t.Fatalf("required counts = owned %d external %d, want 14 and 25", got.OwnedRequired, got.ExternalRequired) - } - if after := snapshotRepositoryFixture(t, root); !reflect.DeepEqual(after, before) { - t.Fatalf("CheckRepository mutated fixture:\nbefore=%#v\nafter=%#v", before, after) - } -} - -func TestCheckRepositoryClassifiesMissingAndOutdatedSkills(t *testing.T) { - tests := []struct { - name string - mutate func(*testing.T, string) - want RepositoryReadiness - }{ - { - name: "missing owned", - mutate: func(t *testing.T, root string) { - t.Helper() - if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "archive-spec")); err != nil { - t.Fatalf("remove owned skill: %v", err) - } - }, - want: RepositoryReadiness{MissingOwned: []string{"archive-spec"}}, - }, - { - name: "missing external", - mutate: func(t *testing.T, root string) { - t.Helper() - if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "agentic-cli-design")); err != nil { - t.Fatalf("remove external skill: %v", err) - } - }, - want: RepositoryReadiness{MissingExternal: []string{"agentic-cli-design"}}, - }, - { - name: "changed owned file", - mutate: func(t *testing.T, root string) { - t.Helper() - writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "roundfix"), "SKILL.md", "changed\n") - }, - want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, - }, - { - name: "added owned file", - mutate: func(t *testing.T, root string) { - t.Helper() - writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "roundfix"), "unexpected.txt", "extra\n") - }, - want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, - }, - { - name: "removed owned file", - mutate: func(t *testing.T, root string) { - t.Helper() - path := filepath.Join(root, ".agents", "skills", "roundfix", "agents", "openai.yaml") - if err := os.Remove(path); err != nil { - t.Fatalf("remove owned artifact: %v", err) - } - }, - want: RepositoryReadiness{OutdatedOwned: []string{"roundfix"}}, - }, - { - name: "changed external file", - mutate: func(t *testing.T, root string) { - t.Helper() - writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "agentic-cli-design"), "SKILL.md", "changed\n") - }, - want: RepositoryReadiness{OutdatedExternal: []string{"agentic-cli-design"}}, - }, - { - name: "added external file", - mutate: func(t *testing.T, root string) { - t.Helper() - writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "agentic-cli-design"), "unexpected.txt", "extra\n") - }, - want: RepositoryReadiness{OutdatedExternal: []string{"agentic-cli-design"}}, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - root := writeReadyRepositoryFixture(t) - test.mutate(t, root) - - got, err := CheckRepository(root) - if err != nil { - t.Fatalf("check repository: %v", err) - } - got.OwnedRequired = 0 - got.ExternalRequired = 0 - if !reflect.DeepEqual(got, test.want) { - t.Fatalf("readiness = %#v, want %#v", got, test.want) - } - }) - } -} - -func TestCheckRepositoryIgnoresUnrelatedSkillsAndLockEntries(t *testing.T) { - root := writeReadyRepositoryFixture(t) - writeSkillHashTestFile(t, filepath.Join(root, ".agents", "skills", "unrelated"), "SKILL.md", "unrelated\n") - lock := readRepositoryLockFixture(t, root) - lock.Skills["../ignored-unsafe-entry"] = repositoryLockSkillFixture{ComputedHash: "not-a-hash"} - writeRepositoryLockFixture(t, root, lock) - - got, err := CheckRepository(root) - if err != nil { - t.Fatalf("check repository with unrelated entries: %v", err) - } - if !got.Ready() { - t.Fatalf("unrelated entries changed readiness: %#v", got) - } -} - -func TestCheckRepositoryRejectsMalformedLockAndUnsafeRequiredNames(t *testing.T) { - tests := []struct { - name string - write func(*testing.T, string) - }{ - { - name: "missing lock", - write: func(t *testing.T, root string) { - if err := os.Remove(filepath.Join(root, "skills-lock.json")); err != nil { - t.Fatal(err) - } - }, - }, - { - name: "malformed lock", - write: func(t *testing.T, root string) { - if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), []byte("{"), 0o644); err != nil { - t.Fatal(err) - } - }, - }, - { - name: "wrong version", - write: func(t *testing.T, root string) { - lock := readRepositoryLockFixture(t, root) - lock.Version = 2 - writeRepositoryLockFixture(t, root, lock) - }, - }, - { - name: "missing required entry", - write: func(t *testing.T, root string) { - lock := readRepositoryLockFixture(t, root) - delete(lock.Skills, Recommended()[0]) - writeRepositoryLockFixture(t, root, lock) - }, - }, - { - name: "invalid required hash", - write: func(t *testing.T, root string) { - lock := readRepositoryLockFixture(t, root) - lock.Skills[Recommended()[0]] = repositoryLockSkillFixture{ComputedHash: strings.Repeat("A", 64)} - writeRepositoryLockFixture(t, root, lock) - }, - }, - { - name: "unreadable lock artifact shape", - write: func(t *testing.T, root string) { - path := filepath.Join(root, "skills-lock.json") - if err := os.Remove(path); err != nil { - t.Fatal(err) - } - if err := os.Mkdir(path, 0o755); err != nil { - t.Fatal(err) - } - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - root := writeReadyRepositoryFixture(t) - test.write(t, root) - - _, err := CheckRepository(root) - if err == nil { - t.Fatal("expected repository lock error") - } - var repositoryErr *RepositoryReadinessError - if !errors.As(err, &repositoryErr) { - t.Fatalf("expected RepositoryReadinessError, got %T: %v", err, err) - } - if repositoryErr.Ownership != RepositoryOwnershipExternal { - t.Fatalf("lock error ownership = %q, want external", repositoryErr.Ownership) - } - if !strings.Contains(err.Error(), filepath.Join(root, "skills-lock.json")) { - t.Fatalf("lock error does not name path: %v", err) - } - }) - } - - root := t.TempDir() - outside := filepath.Join(filepath.Dir(root), "outside") - writeSkillHashTestFile(t, outside, "marker", "must not be read\n") - _, err := checkRepository(root, []string{"../outside"}, nil, nil) - if err == nil || !strings.Contains(err.Error(), "unsafe") { - t.Fatalf("expected unsafe required name error, got %v", err) - } -} - -func TestCheckRepositoryHandlesSymlinksAndStableOrdering(t *testing.T) { - root := writeReadyRepositoryFixture(t) - ownedPath := filepath.Join(root, ".agents", "skills", "roundfix", "SKILL.md") - if err := os.Remove(ownedPath); err != nil { - t.Fatal(err) - } - if err := os.Symlink(filepath.Join(root, "skills-lock.json"), ownedPath); err != nil { - t.Skipf("create owned artifact symlink: %v", err) - } - if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "agentic-cli-design")); err != nil { - t.Fatal(err) - } - if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "archive-spec")); err != nil { - t.Fatal(err) - } - if err := os.RemoveAll(filepath.Join(root, ".agents", "skills", "autoresearch")); err != nil { - t.Fatal(err) - } - - got, err := CheckRepository(root) - if err != nil { - t.Fatalf("check repository with owned symlink: %v", err) - } - if !reflect.DeepEqual(got.MissingOwned, []string{"archive-spec"}) || - !reflect.DeepEqual(got.MissingExternal, []string{"agentic-cli-design", "autoresearch"}) || - !reflect.DeepEqual(got.OutdatedOwned, []string{"roundfix"}) { - t.Fatalf("unstable classifications: %#v", got) - } - - externalRoot := writeReadyRepositoryFixture(t) - externalPath := filepath.Join(externalRoot, ".agents", "skills", "agentic-cli-design", "linked") - if err := os.Symlink(filepath.Join(externalRoot, "skills-lock.json"), externalPath); err != nil { - t.Skipf("create external symlink: %v", err) - } - _, err = CheckRepository(externalRoot) - if err == nil || !strings.Contains(err.Error(), externalPath) { - t.Fatalf("expected external symlink error naming %q, got %v", externalPath, err) - } -} - -type repositoryLockFixture struct { - Version int `json:"version"` - Skills map[string]repositoryLockSkillFixture `json:"skills"` -} - -type repositoryLockSkillFixture struct { - ComputedHash string `json:"computedHash"` -} - -func writeReadyRepositoryFixture(t *testing.T) string { - t.Helper() - root := t.TempDir() - skillsRoot := filepath.Join(root, ".agents", "skills") - files, err := Files() - if err != nil { - t.Fatalf("read embedded skills: %v", err) - } - for _, file := range files { - writeSkillHashTestFile(t, skillsRoot, file.Path, string(file.Data)) - } - - lock := repositoryLockFixture{ - Version: 1, - Skills: make(map[string]repositoryLockSkillFixture, len(Recommended())), - } - for _, name := range Recommended() { - skillRoot := filepath.Join(skillsRoot, name) - writeSkillHashTestFile(t, skillRoot, "SKILL.md", name+"\n") - hash, err := SkillFolderHash(skillRoot) - if err != nil { - t.Fatalf("hash external fixture %q: %v", name, err) - } - lock.Skills[name] = repositoryLockSkillFixture{ComputedHash: hash} - } - writeRepositoryLockFixture(t, root, lock) - return root -} - -func readRepositoryLockFixture(t *testing.T, root string) repositoryLockFixture { - t.Helper() - data, err := os.ReadFile(filepath.Join(root, "skills-lock.json")) - if err != nil { - t.Fatalf("read repository lock fixture: %v", err) - } - var lock repositoryLockFixture - if err := json.Unmarshal(data, &lock); err != nil { - t.Fatalf("decode repository lock fixture: %v", err) - } - return lock -} - -func writeRepositoryLockFixture(t *testing.T, root string, lock repositoryLockFixture) { - t.Helper() - data, err := json.MarshalIndent(lock, "", " ") - if err != nil { - t.Fatalf("encode repository lock fixture: %v", err) - } - if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), append(data, '\n'), 0o644); err != nil { - t.Fatalf("write repository lock fixture: %v", err) - } -} - -func snapshotRepositoryFixture(t *testing.T, root string) map[string]string { - t.Helper() - snapshot := make(map[string]string) - err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if entry.IsDir() { - return nil - } - data, err := os.ReadFile(path) - if err != nil { - return err - } - relative, err := filepath.Rel(root, path) - if err != nil { - return err - } - snapshot[filepath.ToSlash(relative)] = string(data) - return nil - }) - if err != nil { - t.Fatalf("snapshot repository fixture: %v", err) - } - return snapshot -} - func writeSkillHashTestFile(t *testing.T, root string, relative string, content string) { t.Helper() path := filepath.Join(root, filepath.FromSlash(relative)) From 14e03d61784c5cd9714ba89b25a22bcb7c223b09 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 18:06:19 -0300 Subject: [PATCH 32/48] feat: harden Doctor coordination and evidence Roundfix-Spec: 0050-doctor-skill-readiness-hardening Roundfix-Task: task_04 --- CONTEXT.md | 2 +- .../task_04.md | 77 ++++- internal/cli/cli_test.go | 269 ++++++++++++++++++ internal/cli/doctor.go | 48 ++-- 4 files changed, 360 insertions(+), 36 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index f2aadf6..7897f07 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -367,7 +367,7 @@ The support command that produces a Release Plan without editing release files, _Avoid_: Release Command, publish command, cut-release command **Doctor Command**: -The support command that diagnoses a repository and machine's readiness for Roundfix Runs — minimum-supported acpx, Adapter Readiness, Agent Selection Profile Readiness, Repository Skill Set, and codex runtime hygiene — reporting each check with a next action and mutating nothing. It evaluates Repository Skill Set readiness after, and independently from, Agent Selection Profile Readiness; unlike the Doctor Command, the Setup Command prepares the machine. +The support command that diagnoses a repository and machine's readiness for Roundfix Runs — minimum-supported acpx, Adapter Readiness, Agent Selection Profile Readiness, Repository Skill Set, and codex runtime hygiene. It reports the detected acpx version against the minimum, gives each check a next action, and mutates nothing. It evaluates Repository Skill Set readiness after, and independently from, Agent Selection Profile Readiness; unlike the Doctor Command, the Setup Command prepares the machine. _Avoid_: Health check run, setup run, environment wizard **Archive Command**: diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_04.md b/docs/specs/0050-doctor-skill-readiness-hardening/task_04.md index 0677caf..926d090 100644 --- a/docs/specs/0050-doctor-skill-readiness-hardening/task_04.md +++ b/docs/specs/0050-doctor-skill-readiness-hardening/task_04.md @@ -1,7 +1,7 @@ --- task: task_04 spec: 0050-doctor-skill-readiness-hardening -status: pending +status: completed type: backend complexity: medium --- @@ -39,27 +39,27 @@ delimiters. ## Subtasks -- [ ] Separate missing-Git-root handling from repository checking. -- [ ] Complete unclassified error remediation. -- [ ] Add exact-output missing-root and generic-error cases. -- [ ] Add the real-checker public no-mutation fixture. -- [ ] Restore the canonical Doctor Command wording. -- [ ] Run focused, race, and repository-wide verification. +- [x] Separate missing-Git-root handling from repository checking. +- [x] Complete unclassified error remediation. +- [x] Add exact-output missing-root and generic-error cases. +- [x] Add the real-checker public no-mutation fixture. +- [x] Restore the canonical Doctor Command wording. +- [x] Run focused, race, and repository-wide verification. ## Acceptance Criteria -- [ ] An empty loaded Git root never invokes the repository checker or falls +- [x] An empty loaded Git root never invokes the repository checker or falls back to the process working directory. -- [ ] Missing-root and generic checker failures print deterministic `next:` +- [x] Missing-root and generic checker failures print deterministic `next:` actions and exit `1`. -- [ ] All independent checks still run and render in their established order. -- [ ] The public Doctor no-mutation test proves all relevant snapshots are +- [x] All independent checks still run and render in their established order. +- [x] The public Doctor no-mutation test proves all relevant snapshots are byte-identical after execution with the real checker. -- [ ] `CONTEXT.md` again states that Doctor reports the detected acpx version +- [x] `CONTEXT.md` again states that Doctor reports the detected acpx version against the minimum. -- [ ] Archived Spec 0036 and every protected or upstream-managed path outside +- [x] Archived Spec 0036 and every protected or upstream-managed path outside the approved Task 01 files remain unchanged. -- [ ] The complete repository Verification passes. +- [x] The complete repository Verification passes. ## Context @@ -91,3 +91,52 @@ delimiters. - `_techspec.md` → Doctor coordination and remediation; Test ownership and no-mutation proof; Contract reconciliation; Testing Approach; Build Order 4. +## Result + +Doctor now treats the loaded Git root as the sole Repository Skill Set +authority. An empty root produces a repository-specific failed result without +calling the checker, and an unclassified checker error falls back to both +ownership remediation commands in their established order. The public check +sequence executes eagerly in its rendered order. + +The public `Run([]string{"doctor"}, ...)` regression fixture uses +`skills.CheckRepository` against a complete disposable Repository Skill Set. +It compares repository, User Config, user and repository `.roundfix`, lock, +and skill-tree snapshots before and after the command. `CONTEXT.md` also +restores the detected-acpx-version wording. + +Acceptance evidence: + +- AC 1–3: `TestRunDoctorMissingRepositoryRoot` and + `TestRunDoctorRepositorySkillReadiness` assert checker call counts, exact + stdout, exit `1`, the `"; next: "` boundary, and the ordered Node, acpx, + Adapter Readiness, Agent Selection Profile Readiness, Repository Skill Set, + and codex calls. +- AC 4: `TestRunDoctorRealRepositoryCheckDoesNotMutateState` passes with the + real repository checker and byte-identical snapshots for every required + state surface. +- AC 5: `CONTEXT.md` states that the Doctor Command reports the detected acpx + version against the minimum. +- AC 6: the changed-file postflight contains only `CONTEXT.md`, this Task file, + `internal/cli/doctor.go`, and `internal/cli/cli_test.go`; archived Spec 0036, + upstream-managed skills, `skills-lock.json`, `skills/recommended.txt`, + `go.mod`, and `go.sum` have no Task 04 diff. Branch history remains at + `be91ebc`. +- AC 7: focused, affected-package race, and full repository gates passed. + +Verification: + +- Initial red signal: + `rtk go test ./internal/cli -run 'TestRunDoctor' -count=1` exposed the + process-directory fallback, precomputed check order, and missing generic + remediation. The sandbox then required a task-specific `GOCACHE`. +- `rtk env GOCACHE=/private/tmp/roundfix-task04-go-cache go test ./internal/cli -run 'TestRunDoctor' -count=1` + — passed. +- `rtk env GOCACHE=/private/tmp/roundfix-task04-go-cache go test -race ./internal/skillhash ./skills ./internal/baseline ./internal/cli -run 'Test(Sum|SkillFolderHash|CheckRepository|SkillsRestore|RunDoctor)' -count=1` + — passed across all four affected packages. +- `rtk env GOCACHE=/private/tmp/roundfix-task04-go-cache make verify` — passed: + 2,409 tests across 23 packages, four skill contract tests, Repository Skill + Set integrity, and the build. +- `rtk git -c core.fsmonitor=false diff --check` — passed. + +Follow-ups: none. diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 1164d35..5f3075a 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -2619,24 +2619,38 @@ func TestRunDoctorRepositorySkillReadiness(t *testing.T) { wantLine: "skills: failed (decode skills lock \"/repo/project/skills-lock.json\": invalid character; next: " + externalCommand + ")", }, + { + name: "unclassified checker error", + err: errors.New("repository skill check failed"), + wantCode: exitRunFailed, + wantLine: "skills: failed (repository skill check failed; next: " + + ownedCommand + "; " + externalCommand + ")", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { + var calls []string + recordCall := func(name string) { + calls = append(calls, name) + } checker := newDoctorFakeHealthChecker( CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, ) + checker.recordCall = recordCall withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ Config: roundconfig.Builtin(), GitRoot: "/repo/project", }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { + recordCall(HealthCheckProfiles) return profileProofResult{} }) skillCalls := 0 doctorDeps.checkSkills = func(root string) (skills.RepositoryReadiness, error) { skillCalls++ + recordCall(HealthCheckSkills) if root != "/repo/project" { t.Fatalf("skill readiness root = %q, want /repo/project", root) } @@ -2658,6 +2672,17 @@ func TestRunDoctorRepositorySkillReadiness(t *testing.T) { t.Fatalf("independent check calls skills=%d node=%d acpx=%d adapter=%d codex=%d", skillCalls, checker.nodeCalls, checker.acpxCalls, checker.adapterCalls, checker.codexCalls) } + wantCalls := []string{ + HealthCheckNode, + HealthCheckACPX, + HealthCheckAdapter, + HealthCheckProfiles, + HealthCheckSkills, + HealthCheckCodex, + } + if !reflect.DeepEqual(calls, wantCalls) { + t.Fatalf("check order = %v, want %v", calls, wantCalls) + } if stderr.Len() != 0 { t.Fatalf("expected no stderr, got %q", stderr.String()) } @@ -2665,6 +2690,146 @@ func TestRunDoctorRepositorySkillReadiness(t *testing.T) { } } +func TestRunDoctorMissingRepositoryRoot(t *testing.T) { + processDir := t.TempDir() + t.Chdir(processDir) + var calls []string + recordCall := func(name string) { + calls = append(calls, name) + } + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, + ) + checker.recordCall = recordCall + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + }, func(_ context.Context, _ roundconfig.Config, _ []roundconfig.WorkCategory, workDir string) profileProofResult { + recordCall(HealthCheckProfiles) + if workDir != "" { + t.Fatalf("profile readiness work directory = %q, want empty loaded Git root", workDir) + } + return profileProofResult{} + }) + skillCalls := 0 + doctorDeps.checkSkills = func(root string) (skills.RepositoryReadiness, error) { + skillCalls++ + return skills.RepositoryReadiness{}, fmt.Errorf("unexpected repository check for %q", root) + } + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != exitRunFailed { + t.Fatalf("exit code = %d, want %d", code, exitRunFailed) + } + wantStdout := "node: ok\n" + + "acpx: ok\n" + + "adapter: ok (codex-acp)\n" + + "profiles: ok (0 distinct tuples; 0 category references)\n" + + "skills: failed (repository skill check requires a Git repository; next: run roundfix doctor from a Git repository)\n" + + "codex: ok\n" + if got := stdout.String(); got != wantStdout { + t.Fatalf("unexpected stdout:\n got: %q\nwant: %q", got, wantStdout) + } + if skillCalls != 0 { + t.Fatalf("repository checker calls = %d, want 0", skillCalls) + } + wantCalls := []string{ + HealthCheckNode, + HealthCheckACPX, + HealthCheckAdapter, + HealthCheckProfiles, + HealthCheckCodex, + } + if !reflect.DeepEqual(calls, wantCalls) { + t.Fatalf("independent check order = %v, want %v", calls, wantCalls) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } +} + +func TestRunDoctorRealRepositoryCheckDoesNotMutateState(t *testing.T) { + homeDir := t.TempDir() + repoDir := t.TempDir() + t.Setenv("HOME", homeDir) + t.Chdir(repoDir) + mustMkdir(t, filepath.Join(repoDir, ".git")) + writeDoctorReadyRepositoryFixture(t, repoDir) + + userConfigPath := filepath.Join(homeDir, ".roundfix", "config.yml") + mustMkdir(t, filepath.Dir(userConfigPath)) + mustWrite(t, userConfigPath, roundconfig.DefaultConfigYAML()) + mustWrite(t, filepath.Join(homeDir, ".roundfix", "state-marker"), "user state\n") + mustWrite(t, filepath.Join(repoDir, ".roundfixrc.yml"), roundconfig.DefaultConfigYAML()) + mustMkdir(t, filepath.Join(repoDir, ".roundfix")) + mustWrite(t, filepath.Join(repoDir, ".roundfix", "run-marker"), "repository state\n") + + snapshotRoots := map[string]string{ + "repository": repoDir, + "roundfix home": filepath.Join(homeDir, ".roundfix"), + "repository data": filepath.Join(repoDir, ".roundfix"), + "skill state": filepath.Join(repoDir, ".agents", "skills"), + } + beforeRoots := make(map[string]map[string]doctorPathSnapshot, len(snapshotRoots)) + for name, root := range snapshotRoots { + beforeRoots[name] = snapshotDoctorPath(t, root) + } + beforeUserConfig := mustReadBytes(t, userConfigPath) + lockPath := filepath.Join(repoDir, "skills-lock.json") + beforeLock := mustReadBytes(t, lockPath) + + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, + ) + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + GitRoot: repoDir, + HomeDir: homeDir, + UserConfigPath: userConfigPath, + ProjectConfigPath: filepath.Join(repoDir, ".roundfixrc.yml"), + }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { + return profileProofResult{} + }) + doctorDeps.checkSkills = skills.CheckRepository + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != exitOK { + t.Fatalf("exit code = %d, want %d; stdout=%q stderr=%q", code, exitOK, stdout.String(), stderr.String()) + } + wantStdout := "node: ok\n" + + "acpx: ok\n" + + "adapter: ok (codex-acp)\n" + + "profiles: ok (0 distinct tuples; 0 category references)\n" + + "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + + "codex: ok\n" + if got := stdout.String(); got != wantStdout { + t.Fatalf("unexpected stdout:\n got: %q\nwant: %q", got, wantStdout) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } + for name, root := range snapshotRoots { + if after := snapshotDoctorPath(t, root); !reflect.DeepEqual(after, beforeRoots[name]) { + t.Fatalf("Doctor mutated %s:\nbefore=%#v\nafter=%#v", name, beforeRoots[name], after) + } + } + if after := mustReadBytes(t, userConfigPath); !bytes.Equal(after, beforeUserConfig) { + t.Fatalf("Doctor mutated User Config:\nbefore=%q\nafter=%q", beforeUserConfig, after) + } + if after := mustReadBytes(t, lockPath); !bytes.Equal(after, beforeLock) { + t.Fatalf("Doctor mutated skills lock:\nbefore=%q\nafter=%q", beforeLock, after) + } +} + func TestRunDoctorRejectsArguments(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer @@ -2703,21 +2868,25 @@ type doctorFakeHealthChecker struct { acpxCalls int adapterCalls int codexCalls int + recordCall func(string) } func (checker *doctorFakeHealthChecker) Node(context.Context) CheckResult { checker.nodeCalls++ + checker.record(HealthCheckNode) return checker.node } func (checker *doctorFakeHealthChecker) ACPX(context.Context) CheckResult { checker.acpxCalls++ + checker.record(HealthCheckACPX) return checker.acpx } func (checker *doctorFakeHealthChecker) Adapter(_ context.Context, runtime agent.RuntimeSpec) CheckResult { checker.adapterCalls++ checker.adapterRuntimes = append(checker.adapterRuntimes, runtime) + checker.record(HealthCheckAdapter) return checker.adapter } @@ -2728,9 +2897,109 @@ func (checker *doctorFakeHealthChecker) Agent(_ context.Context, req agent.Probe func (checker *doctorFakeHealthChecker) Codex(context.Context) CheckResult { checker.codexCalls++ + checker.record(HealthCheckCodex) return checker.codex } +func (checker *doctorFakeHealthChecker) record(name string) { + if checker.recordCall != nil { + checker.recordCall(name) + } +} + +type doctorRepositoryLockFixture struct { + Version int `json:"version"` + Skills map[string]doctorRepositoryLockSkillFixture `json:"skills"` +} + +type doctorRepositoryLockSkillFixture struct { + ComputedHash string `json:"computedHash"` +} + +func writeDoctorReadyRepositoryFixture(t *testing.T, root string) { + t.Helper() + skillsRoot := filepath.Join(root, ".agents", "skills") + files, err := skills.Files() + if err != nil { + t.Fatalf("read embedded Repository Skill Set: %v", err) + } + for _, file := range files { + path := filepath.Join(skillsRoot, filepath.FromSlash(file.Path)) + mustMkdir(t, filepath.Dir(path)) + if err := os.WriteFile(path, file.Data, 0o644); err != nil { + t.Fatalf("write embedded skill artifact %q: %v", path, err) + } + } + + lock := doctorRepositoryLockFixture{ + Version: 1, + Skills: make(map[string]doctorRepositoryLockSkillFixture, len(skills.Recommended())), + } + for _, name := range skills.Recommended() { + skillRoot := filepath.Join(skillsRoot, name) + mustMkdir(t, skillRoot) + mustWrite(t, filepath.Join(skillRoot, "SKILL.md"), name+"\n") + hash, err := skills.SkillFolderHash(skillRoot) + if err != nil { + t.Fatalf("hash external skill fixture %q: %v", name, err) + } + lock.Skills[name] = doctorRepositoryLockSkillFixture{ComputedHash: hash} + } + data, err := json.MarshalIndent(lock, "", " ") + if err != nil { + t.Fatalf("encode Doctor skills lock fixture: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), append(data, '\n'), 0o644); err != nil { + t.Fatalf("write Doctor skills lock fixture: %v", err) + } +} + +type doctorPathSnapshot struct { + Mode os.FileMode + Data string +} + +func snapshotDoctorPath(t *testing.T, root string) map[string]doctorPathSnapshot { + t.Helper() + snapshot := make(map[string]doctorPathSnapshot) + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + item := doctorPathSnapshot{Mode: info.Mode()} + if info.Mode().IsRegular() { + data, err := os.ReadFile(path) + if err != nil { + return err + } + item.Data = string(data) + } + snapshot[filepath.ToSlash(relative)] = item + return nil + }) + if err != nil { + t.Fatalf("snapshot Doctor path %q: %v", root, err) + } + return snapshot +} + +func mustReadBytes(t *testing.T, path string) []byte { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return data +} + func withDoctorFakeDeps(t *testing.T, checker HealthChecker) { t.Helper() withDoctorFakeLoaded(t, checker, roundconfig.Loaded{ diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 7094f00..988ce50 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -6,7 +6,6 @@ import ( "flag" "fmt" "io" - "os" "sort" "strings" @@ -56,29 +55,22 @@ func runDoctorCommand(ctx context.Context, args []string, stdout, stderr io.Writ checker := doctorDeps.healthChecker(loaded) workDir := strings.TrimSpace(loaded.GitRoot) - if workDir == "" { - workDir, err = os.Getwd() - if err != nil { - printDoctorFailure(fmt.Errorf("resolve Doctor working directory: %w", err), stderr) - return exitRunFailed - } - } - - profileReadiness := doctorDeps.profileReadiness(ctx, loaded.Config, roundconfig.RequiredWorkCategories(), workDir) - profileResult := doctorProfileReadinessResult(profileReadiness) - skillReadiness, skillErr := doctorDeps.checkSkills(workDir) - skillResult := doctorSkillReadinessResult(skillReadiness, skillErr) - runtime, runtimeErr := doctorAdapterRuntime(loaded.Config) // Keep independent checks eager and ordered. - results := []CheckResult{ - checker.Node(ctx), - checker.ACPX(ctx), - doctorAdapterCheck(ctx, checker, runtime, runtimeErr), - profileResult, - skillResult, - checker.Codex(ctx), + results := make([]CheckResult, 0, 6) + results = append(results, checker.Node(ctx)) + results = append(results, checker.ACPX(ctx)) + runtime, runtimeErr := doctorAdapterRuntime(loaded.Config) + results = append(results, doctorAdapterCheck(ctx, checker, runtime, runtimeErr)) + profileReadiness := doctorDeps.profileReadiness(ctx, loaded.Config, roundconfig.RequiredWorkCategories(), workDir) + results = append(results, doctorProfileReadinessResult(profileReadiness)) + if workDir == "" { + results = append(results, doctorMissingRepositoryRootResult()) + } else { + skillReadiness, skillErr := doctorDeps.checkSkills(workDir) + results = append(results, doctorSkillReadinessResult(skillReadiness, skillErr)) } + results = append(results, checker.Codex(ctx)) failed := false for _, result := range results { @@ -154,8 +146,18 @@ func doctorProfileReadinessResult(readiness profileProofResult) CheckResult { const ( ownedSkillsNextAction = "roundfix skills install --target project" externalSkillsNextAction = "bunx skills experimental_install && bunx skills update -p -y" + missingGitRootNextAction = "run roundfix doctor from a Git repository" ) +func doctorMissingRepositoryRootResult() CheckResult { + return CheckResult{ + Name: HealthCheckSkills, + Status: CheckStatusFailed, + Detail: "repository skill check requires a Git repository", + NextAction: missingGitRootNextAction, + } +} + func doctorSkillReadinessResult(readiness skills.RepositoryReadiness, checkErr error) CheckResult { result := CheckResult{Name: HealthCheckSkills} if checkErr == nil && readiness.Ready() { @@ -201,6 +203,10 @@ func doctorSkillReadinessResult(readiness skills.RepositoryReadiness, checkErr e externalFailure = true } } + if checkErr != nil && !ownedFailure && !externalFailure { + ownedFailure = true + externalFailure = true + } var next []string if ownedFailure { next = append(next, ownedSkillsNextAction) From d8588d8f4156f0ff29347a968b800a7b65c34085 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 18:25:10 -0300 Subject: [PATCH 33/48] docs: qa report for 0050-doctor-skill-readiness-hardening (pass) Roundfix-Spec: 0050-doctor-skill-readiness-hardening --- .../command-evidence.md | 355 ++++++++++++++++++ .../unicode-corpus/-a | 1 + .../unicode-corpus/10-guide.md | 1 + .../unicode-corpus/2-guide.md | 1 + .../unicode-corpus/A.md | 1 + .../unicode-corpus/_a | 1 + .../unicode-corpus/nested/deeper/\303\237.md" | 1 + .../unicode-corpus/nested/z.md | 1 + .../unicode-corpus/nested/\303\211.md" | 1 + .../unicode-corpus/nested/\303\244.md" | 1 + .../unicode-corpus/\303\251.md" | 1 + .../unicode-skills-lock.json | 179 +++++++++ .../qa/qa-report-2026-07-26.md | 129 +++++++ 13 files changed, 673 insertions(+) create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a create mode 100644 "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md create mode 100644 "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" create mode 100644 "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" create mode 100644 "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json create mode 100644 docs/specs/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md new file mode 100644 index 0000000..50720b5 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md @@ -0,0 +1,355 @@ +# Command evidence — Doctor Skill Readiness Hardening + +Build: `14e03d61784c5cd9714ba89b25a22bcb7c223b09` + +This file records fresh commands, exit codes, focused outputs, and state +comparisons for the 2026-07-26 QA gate. Sections are populated as their +corresponding matrix rows execute. + +## Preconditions and Project Constraints + +All four Task files declare `status: completed`. Before the QA artifacts were +created, `rtk git -c core.fsmonitor=false status --short --branch` reported +only the Run Branch header and no worktree changes. + +The PRD and TechSpec each contain a Project Constraints section. Identifier +strategy and authentication/HTTP are explicitly not applicable with reasons; +the sections cite `docs/agents/domain.md` and `docs/agents/cli.md`. Active ADR +obligations are applicable and name ADR-0049, ADR-0055, ADR-0066, and ADR-0072. +Tooling authority records the maintainer's 2026-07-26 authorization for +`golang.org/x/text` and bounds protected changes to `go.mod` and `go.sum`. + +The four implementation commits and fresh `diff-tree` paths were: + +```text +6fd1d32 + docs/specs/0050-doctor-skill-readiness-hardening/task_01.md + go.mod + go.sum +93b3151 + docs/specs/0050-doctor-skill-readiness-hardening/task_02.md + internal/baseline/skills_restore.go + internal/skillhash/hash.go + internal/skillhash/hash_test.go + skills/skills.go +be91ebc + docs/specs/0050-doctor-skill-readiness-hardening/task_03.md + skills/repository.go + skills/repository_test.go + skills/skills.go + skills/skills_test.go +14e03d6 + CONTEXT.md + docs/specs/0050-doctor-skill-readiness-hardening/task_04.md + internal/cli/cli_test.go + internal/cli/doctor.go +``` + +Every `rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only +-r ` command exited `0`. Only Task 01 changed protected tooling files, +and its paths match the express authorization plus its own Task file. + +## Task 01 authorized dependency + +Commands: + +```text +rtk go list -m -f '{{.Path}} {{.Version}}' golang.org/x/text +rtk go mod verify +rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only -r 6fd1d32 +``` + +All exited `0`. The module query returned +`golang.org/x/text v0.40.0`; module verification returned +`all modules verified`; the commit changed only `go.mod`, `go.sum`, and +`task_01.md`. + +## Root-anchored symlink rejection + +The built CLI ran in disposable Git repositories derived from a known-ready +Repository Skill Set. Each command exited `1`, completed all independent +checks, and emitted the expected blocking skills result: + +```text +.agents link: +skills: failed (inspect repository skill directory ".../.agents": symbolic links are not supported; next: roundfix skills install --target project; bunx skills experimental_install && bunx skills update -p -y) + +.agents/skills link: +skills: failed (inspect repository skill directory ".../.agents/skills": symbolic links are not supported; next: roundfix skills install --target project; bunx skills experimental_install && bunx skills update -p -y) + +skills-lock.json link: +skills: failed (inspect skills lock ".../skills-lock.json": symbolic links are not supported; next: roundfix skills install --target project; bunx skills experimental_install && bunx skills update -p -y) +``` + +Nested link and FIFO entries also prevented readiness and produced +`skills: failed (outdated: roundfix; next: roundfix skills install --target +project)`. The owning regression suite gave the stronger internal +classification proof: + +```text +rtk go test ./skills -run 'TestCheckRepository' -count=1 +Go test: 26 passed in 1 packages +``` + +After all public commands, recursive `diff -qr` comparisons for the external +`.agents` and `.agents/skills` targets and `cmp` for the external lock all +exited `0`. + +## Unicode hash compatibility + +The exact pinned oracle ran in the focused suite and passed with the documented +skills CLI 1.5.19 digest +`2a46b6d704729eafc0148969028b9cc4030813059e1f7524def2f38b433011d4`. +The suite includes `_a` before `-a`, digits, case, composed and decomposed +Unicode, nested paths, and path/content negative companions. + +On this macOS filesystem, case-only and canonical-equivalent names cannot +coexist. A representable ten-file subset was hashed independently with Node's +optionless `String.localeCompare`; its digest was +`57cb017b98f0f26a3782e0a4660885f61b74751d53633dd62f765e3ab301aa4b`. +The built Doctor accepted that locked external skill twice: + +```text +skills: ok (39 required: 14 Roundfix-owned, 25 external) +``` + +Renaming `a.md` to `changed.md` in a copy while retaining the same lock changed +the public result to: + +```text +skills: failed (outdated: agentic-cli-design; next: bunx skills experimental_install && bunx skills update -p -y) +``` + +## Shared Doctor-Baseline hash authority + +The first public preview without `--source-dir` failed because the sandbox +could not resolve `github.com`. The documented offline path was then used with +the clean local checkout at `/Users/marcio/dev/skills`, which contains the +profile's immutable commit. + +In a disposable clone: + +```text +roundfix baseline skills restore --profile go-cli-tui --skill agentic-cli-design --source-dir /Users/marcio/dev/skills --repo +``` + +Preview exited `3` with Plan Digest +`23a88969138a8072e167c3a5e3fe6689bede48010506ac12a13dbb38551e966f` +and one lock-entry update. Confirming that exact digest exited `0` with +`Baseline skills restore: applied`. A fresh unconfirmed rerun exited `0` with +`Baseline skills restore: no changes`. Doctor then reported the restored +repository's `skills: ok`. + +Source inspection found the two production calls: + +```text +skills/skills.go:190: return skillhash.Sum(files), nil +internal/baseline/skills_restore.go:1251: return skillhash.Sum(hashFiles) +``` + +No copied lowercase path comparator remains. Task 01's module-file object state +is unchanged in later implementation commits, and the focused shared suite +passed 27 tests across the hash, skills, and Baseline packages. + +## Doctor root and remediation + +From a directory with no Git root, the built command exited `1`, still rendered +all independent checks in Node, acpx, adapter, profiles, skills, codex order, +and printed: + +```text +skills: failed (repository skill check requires a Git repository; next: run roundfix doctor from a Git repository) +``` + +The three unclassified authority errors also exited `1` and printed both +ownership remediations in owned-then-external order. The focused public-runner +suite passed 17 Doctor tests, including the empty-root call-count and exact +output assertions. + +## Public Doctor no-mutation + +A clean disposable clone had an empty `git status` before Doctor. The built +command ran twice; both runs produced the same ordered output and +`skills: ok (39 required: 14 Roundfix-owned, 25 external)`. A fresh +`rtk git -c core.fsmonitor=false status --short` remained empty after the +second run. + +`rtk go test ./internal/cli -run 'TestRunDoctor' -count=1` passed 17 tests. Its +real-checker public-runner fixture snapshots repository files, User Config, +user and repository `.roundfix`, lock, and skill trees and proves every +snapshot byte-identical after execution. The exact full `rtk make verify` +also passed. + +## Repository classification and test ownership + +The built Doctor reported `skills: ok` in the real repository and clean +disposable copies. Removing `autoresearch` and `agentic-cli-design` produced +the stable sorted result: + +```text +skills: failed (missing: agentic-cli-design, autoresearch; next: bunx skills experimental_install && bunx skills update -p -y) +``` + +The Unicode path-change fixture produced `outdated: agentic-cli-design`. +`rtk go test ./skills -count=1` passed all 106 package tests. + +Every `TestCheckRepository*` definition is in `skills/repository_test.go`; +every `TestSkillFolderHash*` definition is in `skills/skills_test.go`. + +## Doctor CLI contract + +Ready, missing-root, authority-error, and repeated Doctor executions retained +the `"; next: "` boundary and exit `1` for readiness failures. Invalid +arguments retained exit `2`: + +```text +rtk ./bin/roundfix doctor unexpected +roundfix: doctor failed: unexpected argument "unexpected" +Run 'roundfix doctor --help' for usage. +``` + +`doctor --format json` also exited `2` with `flag provided but not defined: +-format`, proving no new machine schema. The 17 focused public CLI tests passed +their exact stdout, stderr, ordering, and exit-code assertions. + +## Contract wording and bounded scope + +`CONTEXT.md:370` states that Doctor "reports the detected acpx version against +the minimum". Task commit `14e03d6` changed only `CONTEXT.md`, `task_04.md`, +`internal/cli/cli_test.go`, and `internal/cli/doctor.go`. + +The cumulative implementation diff from planning base `b71957d` contains only +the four Task files, `CONTEXT.md`, the authorized module files, and the +intended hash, Repository Skill Set, Baseline, and Doctor sources/tests. It +contains no archived Spec, upstream-managed skill, lock, or recommended-list +path. + +## Non-Goals: history and archive + +`rtk git merge-base --is-ancestor b71957d 14e03d6` exited `0`. The archived +Spec 0036 tree object is identical at base and build: + +```text +758792d870b4ffd93e3b6984b71cbb185f7d1dd9 +758792d870b4ffd93e3b6984b71cbb185f7d1dd9 +``` + +## Non-Goals: Baseline and managed files + +The base and build object IDs are identical for `skills-lock.json`, +`skills/recommended.txt`, and `.agents/skills`; the scoped `git diff --quiet` +exited `0`. + +No Baseline asset-sync production path changed. The existing preservation and +idempotence regression passed: + +```text +rtk go test ./internal/baseline -run 'TestBaselineAssetsSyncRefreshProducesCanonicalTreeAndIsIdempotent' -count=1 +Go test: 1 passed in 1 packages +``` + +## Non-Goals: output and membership + +`doctor --help` exposes only `roundfix doctor`; it describes an offline, +read-only command and no format option. `doctor --format json` exited `2`. +Every readiness failure retained the human `"; next: "` boundary. + +The base/build object IDs for `skills-lock.json` and +`skills/recommended.txt` match exactly. Current public output remains +`39 required: 14 Roundfix-owned, 25 external`. + +## Non-Goal: offline Doctor + +Every public Doctor run completed without a network diagnostic. Source +inspection found `doctorDeps.checkSkills = skills.CheckRepository` and no +process launch in `skills/repository.go` or `internal/skillhash/hash.go`. +The existing Node and acpx machine-health checks remain independent; no Node, +Bun, or external skills CLI process was added for hash compatibility. + +## CLI surface sweep + +The built binary covered: + +- `doctor --help`: exit `0`, current offline/read-only contract. +- `doctor`: ready, missing, outdated, three symlink authorities, nested link, + special entry, missing Git root, and repeat execution. +- `doctor unexpected` and `doctor --format json`: exit `2`. +- `skills check`: exit `0`, all 14 shipped Roundfix skills passed. +- `baseline skills restore`: offline preview exit `3`, confirmed apply exit + `0`, idempotent rerun exit `0`. + +Fresh reruns and Git/directory comparisons confirmed stable state. + +## Backend surface sweep + +All fresh suites passed: + +```text +rtk go test ./internal/skillhash ./skills ./internal/baseline -run 'Test(Sum|SkillFolderHash|CheckRepositoryMatchesRealRepository|SkillsRestore)' -count=1 +Go test: 27 passed in 3 packages + +rtk go test ./skills -run 'TestCheckRepository' -count=1 +Go test: 26 passed in 1 packages + +rtk go test ./internal/cli -run 'TestRunDoctor' -count=1 +Go test: 17 passed in 1 packages + +rtk go test -race ./internal/skillhash ./skills ./internal/baseline ./internal/cli -run 'Test(Sum|SkillFolderHash|CheckRepository|SkillsRestore|RunDoctor)' -count=1 +Go test: 69 passed in 4 packages + +rtk go test ./skills -count=1 +Go test: 106 passed in 1 packages +``` + +## Documentation surface sweep + +`CONTEXT.md` defines Doctor with detected-versus-minimum acpx wording, +independent Repository Skill Set evaluation, next actions, and no mutation. +`roundfix doctor --help` matched that contract, and real output included the +detected `acpx: ok (0.12.1 >= 0.12.0)` result plus deterministic remediation. + +The full Verification's Repository Skill Set check passed the canonical +Roundfix Skill synchronization for all 14 shipped skills. + +## Static gate + +Command: + +```text +rtk make verify +``` + +Exit: `0`. + +Focused output: + +```text +rtk go test ./... +Go test: 2409 passed in 23 packages +rtk go test -count=1 ./skills -run 'TestNoPythonBaselineRuntime|TestThinSetupSkill|TestCheckRejectsExecutableSetupEngineArtifacts|TestRecommendedSkillsMatchLock' +Go test: 4 passed in 1 packages +rtk go run -buildvcs=false ./cmd/roundfix skills check +Roundfix skill check passed: roundfix, write-idea, write-prd, write-techspec, write-tasks, setup-context-driven, implement-task, implement-spec, brainstorming, council, business-analyst, archive-spec, qa-gate, evidence-gate +rtk go build -buildvcs=false ... -o bin/roundfix ./cmd/roundfix +``` + +The exact repository Verification passed on the QA build. + +Final artifact postflight: + +```text +rtk rg -n '[[:blank:]]+$' docs/specs/0050-doctor-skill-readiness-hardening/qa +exit 1, no trailing-whitespace matches + +rtk jq empty docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json +exit 0 + +filesystem-compatible oracle recheck +10 files, 57cb017b98f0f26a3782e0a4660885f61b74751d53633dd62f765e3ab301aa4b + +rtk git -c core.fsmonitor=false diff --check +exit 0 +``` + +`git status --short --untracked-files=all` listed only the dated QA report and +its evidence directory. diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a new file mode 100644 index 0000000..63c4531 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a @@ -0,0 +1 @@ +hyphen diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md new file mode 100644 index 0000000..e48b2f4 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md @@ -0,0 +1 @@ +ten diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md new file mode 100644 index 0000000..f719efd --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md @@ -0,0 +1 @@ +two diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md new file mode 100644 index 0000000..bfb2990 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md @@ -0,0 +1 @@ +lowercase diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a new file mode 100644 index 0000000..e98fdbe --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a @@ -0,0 +1 @@ +underscore diff --git "a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" "b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" new file mode 100644 index 0000000..cd6eb3b --- /dev/null +++ "b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" @@ -0,0 +1 @@ +double-s diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md new file mode 100644 index 0000000..6c24c44 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md @@ -0,0 +1 @@ +zed diff --git "a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" "b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" new file mode 100644 index 0000000..e5c0f97 --- /dev/null +++ "b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" @@ -0,0 +1 @@ +nested uppercase accent diff --git "a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" "b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" new file mode 100644 index 0000000..b39b3b6 --- /dev/null +++ "b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" @@ -0,0 +1 @@ +umlaut diff --git "a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" "b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" new file mode 100644 index 0000000..7af0a1c --- /dev/null +++ "b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" @@ -0,0 +1 @@ +decomposed accent diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json new file mode 100644 index 0000000..4ee82f1 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json @@ -0,0 +1,179 @@ +{ + "version": 1, + "skills": { + "agentic-cli-design": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/03-engineering-design/agentic-cli-design/SKILL.md", + "computedHash": "57cb017b98f0f26a3782e0a4660885f61b74751d53633dd62f765e3ab301aa4b" + }, + "autoresearch": { + "source": "marcioaltoe/skills", + "sourceType": "github", + "skillPath": "skills/09-learning-loop/autoresearch/SKILL.md", + "computedHash": "047afef0fb8965b8d4d9f7bfefbf8819530842281ba8f9e7b40bfd5abaa68419" + }, + "bubbletea": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/bubbletea/SKILL.md", + "computedHash": "612d0739d4cac8c4a9af51be6cf9c6d6459522c9f0840a414dc272927a950ad3" + }, + "coding-guidelines": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/coding-guidelines/SKILL.md", + "computedHash": "28a414c715d3c41aae28a25e2f109813ec3f246f76b5567c1e7d6a128cd289ff" + }, + "context7": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/01-discovery/context7/SKILL.md", + "computedHash": "23f0178420043f01cd0f00745eb06eaac6f3eee4ef58f87f1ae353c2a408f79c" + }, + "conventional-commits": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/07-evidence-delivery/conventional-commits/SKILL.md", + "computedHash": "816e7074089495888ecfceb1980657d961a708cf8b31e8a6cf35f1c0c76efbc3" + }, + "domain-modeling": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/03-engineering-design/domain-modeling/SKILL.md", + "computedHash": "363cb0f53b0b431e7c00086ad1f823500b7e1b70b5616ee969c979f0934e9e6e" + }, + "exa-web-search": { + "source": "marcioaltoe/skills", + "ref": "14fdf46befa9a07203fde20b69b885dab4961844", + "sourceType": "github", + "skillPath": "skills/01-discovery/exa-web-search/SKILL.md", + "computedHash": "d8b53369f54c57dea66e64d83d6f3a27bfeee364d88ff3845f6b7312f6b3c2ae" + }, + "github-pr-workflow": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/07-evidence-delivery/github-pr-workflow/SKILL.md", + "computedHash": "66527dfe26853cc57d1c62da6366a2fe161c18022594ab334c71b6b9747b1006" + }, + "golang-cli": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/golang-cli/SKILL.md", + "computedHash": "aa2c96ef782ce9ff185a74dfa51fe14e9e845473897a8f12b735a994dff7096d" + }, + "golang-concurrency": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/golang-concurrency/SKILL.md", + "computedHash": "599a74eed42371e5e82f8afe0eb42805a1807341d998c8d85cf8232303b66c46" + }, + "golang-context": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/golang-context/SKILL.md", + "computedHash": "17351f3065a9857759db7f279f693eeacb9a4d6c84ddac5c9c74a08ca1d04b62" + }, + "golang-error-handling": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/golang-error-handling/SKILL.md", + "computedHash": "1601d92a30a221903d024d4ad470595c3c6141d14a5b274a446394adefc83a52" + }, + "golang-lint": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/golang-lint/SKILL.md", + "computedHash": "ea282d6b3a39d02fc468ef66615830de75f720c66ac72e899c4479868bea6c76" + }, + "golang-testing": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/golang-testing/SKILL.md", + "computedHash": "5149794b7adbda8f4509fad7ec13b63cbae0c4aacad915b87b324cda2be3f5ac" + }, + "grill-with-docs": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/01-discovery/grill-with-docs/SKILL.md", + "computedHash": "9c460cbd94fd3c63cdef967dbdb6e66ca687103cdc380cd37834e4d10b738f78" + }, + "grilling": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/01-discovery/grilling/SKILL.md", + "computedHash": "368d3dd7251247c69f3656d93dc83c8f1577792eacac2111f1e7981db2ece49b" + }, + "handoff": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/07-evidence-delivery/handoff/SKILL.md", + "computedHash": "ad03e8d4ea3cbbff66420eb7ba3cc375b5cbe1821a2449b53e863256cf5b5cde" + }, + "no-workarounds": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/no-workarounds/SKILL.md", + "computedHash": "c53eb07ae5e73781f2efe819f25f00fadbbd1d2ef18ab9983a31cf06b08ca7aa" + }, + "review": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/06-review-repair/review/SKILL.md", + "computedHash": "4a2b10bb81e568516c52018e73b8255a02066024257d7c907fa65826d62bc8dd" + }, + "systematic-debugging": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/06-review-repair/systematic-debugging/SKILL.md", + "computedHash": "7527b8f518cf265cc3f63345885bca98b095d2836ece773dadd3e8ca76315583" + }, + "tech-writer": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/07-evidence-delivery/tech-writer/SKILL.md", + "computedHash": "3025976c9db7da53a50ee59f95b6720d6fcdd132516107e69e719fae60ce5330" + }, + "testing-boss": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/05-implementation-loop/testing-boss/SKILL.md", + "computedHash": "a20b323821306084166bce51d50b6d8f56506cc55785ca369e3597c4501e2176" + }, + "the-fool": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/01-discovery/the-fool/SKILL.md", + "computedHash": "6bdba13f4c274424f97601061201c0333a620c0c8fe03f4d68f0f407806e6177" + }, + "tui-design": { + "source": "marcioaltoe/skills", + "ref": "main", + "sourceType": "github", + "skillPath": "skills/03-engineering-design/tui-design/SKILL.md", + "computedHash": "92b969f3473842ee731e0c7a5c571e3b89e5dac7da81be5e635e1715b0084e6e" + } + } +} diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md b/docs/specs/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md new file mode 100644 index 0000000..1b58312 --- /dev/null +++ b/docs/specs/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md @@ -0,0 +1,129 @@ +--- +spec: 0050-doctor-skill-readiness-hardening +date: 2026-07-26 +build: 14e03d61784c5cd9714ba89b25a22bcb7c223b09 +status: closed +verdict: pass +surfaces: [cli, backend, docs] +--- + +# QA report — Doctor Skill Readiness Hardening + +## Scope and environment + +This is the full final QA gate for Spec +`0050-doctor-skill-readiness-hardening`. It covers every PRD user story, every +Task acceptance criterion, the PRD success metrics, the declared Non-Goals, +and the CLI, backend, and documentation surfaces on build +`14e03d61784c5cd9714ba89b25a22bcb7c223b09`. + +The primary actor is a maintainer running the built Roundfix CLI from real Git +repositories. Disposable repositories and isolated configuration paths will +exercise hostile and valid Repository Skill Set states without changing the +working repository or the maintainer's user configuration. Package-level +checks will supplement, but not replace, public CLI evidence for behavior that +has no separate public command. + +The host is macOS with a case-insensitive, normalization-preserving +filesystem. A filesystem fixture cannot contain the full pinned corpus's +case-only and canonical-equivalent filename pairs at once, so the live Doctor +journey used the representable punctuation and Unicode subset and an +independent Node `localeCompare` digest. The exact full corpus remained covered +by the current in-memory skills CLI 1.5.19 compatibility oracle. + +The machine's Codex binary currently fails Roundfix's code-signature check, so +every real Doctor invocation exits `1` for the independent `profiles:` and +`codex:` checks. This does not block the Spec's `skills:` results: Doctor ran +all checks eagerly, the ready repositories reported `skills: ok`, and every +hostile fixture reported its expected blocking skills result. A first Baseline +restore preview also proved network access unavailable; the same public flow +then ran successfully with the documented `--source-dir` path against the +local immutable skills checkout. + +Selected high-risk probes: + +- Symlink redirection at each shared authority boundary, because the security + promise is to reject external authority before reading it. +- Punctuation and Unicode path ordering, because valid skills previously risked + a false outdated result. +- Missing Git root and an unclassified repository checker failure, because + Doctor must remain deterministic and must not substitute another directory. +- Immediate rerun with byte snapshots, because Doctor and Repository Skill Set + checks promise no mutation and stable results. +- Repeated Baseline restoration in a disposable repository, because the shared + hash authority must not drift across consumers. + +Evidence will be recorded in +`qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md`. + +## Project Constraint audit + +| Constraint | PRD | TechSpec | Operative source | Status | +| --- | --- | --- | --- | --- | +| Identifier strategy | Not applicable: no project-owned identity is created. | Not applicable: existing skill names and paths remain the identifiers. | `docs/agents/domain.md` | pass | +| Authentication and HTTP | Not applicable: local filesystem, hashing, CLI, docs, and tests only. | Not applicable: offline filesystem-only implementation. | `docs/agents/cli.md` | pass | +| Active ADR obligations | Applicable: ADR-0049, ADR-0055, ADR-0066, and ADR-0072 preserve independent readiness and Go-owned Baseline behavior. | Applicable with the same four decisions and their implementation consequences. | `docs/agents/domain.md`; `docs/adr/0049-agent-selection-profiles-are-atomic.md`; `docs/adr/0055-agent-selection-encoding-follows-advertised-acp-capabilities.md`; `docs/adr/0066-context-driven-baseline-execution-belongs-to-the-cli.md`; `docs/adr/0072-baseline-go-cutover-preserves-python-contracts.md` | pass | +| Tooling authority | Applicable: the maintainer expressly authorized `golang.org/x/text` on 2026-07-26, bounded to `go.mod` and `go.sum`. | Applicable with the same authorization and exact protected paths. | `docs/agents/agent-instructions.md` | pass | + +## Static gate + +| Command | Status | Evidence | +| --- | --- | --- | +| `rtk make verify` | pass | `qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md#static-gate` | + +## Results + +| # | Story / criterion / sweep | Actor and entry point | Steps and expected observable | Independent confirmation and persistence | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | +| Q01 | Spec preconditions and Project Constraints | QA reviewer; Spec artifacts and Git history | Confirm all four Tasks are `completed`; audit identifier, auth/HTTP, ADR, and tooling applicability in both active artifacts; resolve Task commits and protected paths. Expect complete, source-cited constraints and traceable commits. | Reconcile each implementation commit with `git diff-tree`; confirm no current pre-QA delta. | `command-evidence.md#preconditions-and-project-constraints` | pass | +| Q02 | Task 01 AC1–AC3: exact dependency, module integrity, bounded tooling change | Maintainer; Go module commands and Task commit `6fd1d32` | Query `golang.org/x/text`, verify modules, and inspect the Task commit. Expect `v0.40.0`, verified modules, and only `go.mod`, `go.sum`, and `task_01.md`. | Re-run the module query and compare commit paths with the express authorization. | `command-evidence.md#task-01-authorized-dependency` | pass | +| Q03 | User Story 1; Task 03 AC1–AC3, AC5–AC6; Success Metric 1: reject symlinked authorities without escape or mutation | Security-conscious maintainer; built `roundfix doctor` in disposable Git repositories | Exercise symlinked `.agents`, `.agents/skills`, and `skills-lock.json`. Expect blocking `skills: failed` results; no external target content is accepted or changed. Also exercise nested links and special entries. | Rerun each case, compare target and repository snapshots, and run the owning real-filesystem regression suite. | `command-evidence.md#root-anchored-symlink-rejection` | pass | +| Q04 | User Story 2; Task 02 AC1–AC3; Success Metric 2: skills CLI 1.5.19-compatible punctuation, case, digit, and Unicode collation | Maintainer; real Repository Skill Set through built Doctor plus pinned compatibility oracle | Check a valid repository whose locked skill tree includes the compatibility corpus. Expect Doctor to accept the hash and the oracle to produce `2a46b6d704729eafc0148969028b9cc4030813059e1f7524def2f38b433011d4`, including `_a` before `-a`. | Run `roundfix skills check` against the same repository and the negative path/content companion; rerun Doctor unchanged. | `command-evidence.md#unicode-hash-compatibility` | pass | +| Q05 | User Story 3; Task 02 AC4–AC6; Success Metric 3: Doctor and Baseline restoration share one compatible hash authority | Baseline maintainer; built Baseline restore command in a disposable repository | Restore external skill lock data and check it with the public Repository Skill Set commands. Expect compatible hashes, no copied comparator, and unchanged `go.mod`/`go.sum` since Task 01. | Repeat restoration and compare output/state; run focused Baseline and Repository Skill Set integration tests. | `command-evidence.md#shared-doctor-baseline-hash-authority` | pass | +| Q06 | User Story 4; Task 04 AC1–AC3; Success Metric 4: missing Git root and checker errors remain blocking, eager, ordered, and actionable | Agent/operator; built `roundfix doctor` from a non-Git directory and a disposable invalid repository | From no Git root, expect no repository substitution and a repository-specific next action with exit `1`. From an unclassified checker error, expect both ownership remediations in owned-then-external order. Independent Node, acpx, Adapter Readiness, Agent Selection Profile Readiness, Repository Skill Set, and codex checks remain ordered. | Repeat from a different non-Git directory; compare exact output with public-runner regression evidence. | `command-evidence.md#doctor-root-and-remediation` | pass | +| Q07 | User Story 5; Task 04 AC4 and AC7; Success Metric 5: public Doctor execution is read-only and full Verification passes | Reviewer; built `roundfix doctor` with real checker and repository/user snapshots | Snapshot repository, isolated User Config, user and repository `.roundfix`, lock, and skill trees; run Doctor. Expect byte-identical snapshots and stable exit/output. | Run Doctor a second time and compare snapshots again; run the public-runner no-mutation test and full static gate. | `command-evidence.md#public-doctor-no-mutation` | pass | +| Q08 | Task 03 AC4 and AC7: ready/missing/outdated behavior stays stable and tests live with their owner | Maintainer; built Doctor/skills commands and source/test ownership inspection | Exercise the real ready repository and disposable missing/outdated repositories. Expect sorted stable classifications. Confirm `TestCheckRepository*` lives in `repository_test.go` and `TestSkillFolderHash*` remains in `skills_test.go`. | Rerun classifications and the complete `skills` package suite. | `command-evidence.md#repository-classification-and-test-ownership` | pass | +| Q09 | Task 04 AC5: preserve `"; next: "`, streams, and exit codes | Operator; built Doctor CLI | Exercise success/readiness, missing-root, and repository-error paths. Expect the exact delimiter, diagnostics on the established stream, exit `1` for readiness failures, and argument exit `2`. | Repeat each command and compare with the focused public CLI tests. | `command-evidence.md#doctor-cli-contract` | pass | +| Q10 | Task 04 AC5–AC6; Success Metric 6: canonical Doctor wording and bounded Task 04 scope | Reviewer; `CONTEXT.md`, Git history, and built Doctor | Confirm `CONTEXT.md` states detected acpx version versus minimum. Inspect Task 04 paths and cumulative Spec paths. Expect no archived Spec 0036, protected path, upstream skill, lock, or recommended-list change outside Task 01 authorization. | Compare planning base `b71957d` to the QA build and inspect the archived tree digest. | `command-evidence.md#contract-wording-and-bounded-scope` | pass | +| Q11 | Non-Goals 1–2: no history rewrite and no archived Spec 0036 edit | Reviewer; Git graph and archived tree | Confirm the existing implementation commits remain descendants of the planning base and no archived 0036 path changed. | Compare commit ancestry and archived-tree object IDs from base and build. | `command-evidence.md#non-goals-history-and-archive` | pass | +| Q12 | Non-Goals 3–4: no `sync-setups` reimplementation; no upstream skill, lock, or recommended-list edit | Reviewer; cumulative diff and existing Baseline asset sync tests | Expect no new `sync-setups` implementation and no changes to `.agents/skills` upstream-managed content, `skills-lock.json`, or `skills/recommended.txt`. Existing Go Baseline digest-preservation evidence remains green. | Search changed paths and rerun the focused existing asset synchronization regression. | `command-evidence.md#non-goals-baseline-and-managed-files` | pass | +| Q13 | Non-Goals 5–6: no Doctor schema or delimiter change; no skill membership/count change | Operator; built Doctor and repository lock comparison | Expect human-readable Doctor output with the unchanged delimiter, no invented machine-readable schema, and unchanged required-skill membership/count examples. | Compare planning-base and build lock/recommended files; inspect `doctor --help` and invalid-argument behavior. | `command-evidence.md#non-goals-output-and-membership` | pass | +| Q14 | Non-Goal 7: Doctor remains offline and does not invoke Node, Bun, or the external skills CLI for compatibility | Operator; built Doctor with controlled executable path plus source/test inspection | Run Doctor where external skill CLI launch would fail or be observable. Expect Repository Skill Set evaluation to remain in-process and no external compatibility process invocation. | Inspect Doctor and hash production call sites, then rerun the public command. | `command-evidence.md#non-goal-offline-doctor` | pass | +| Q15 | CLI surface sweep | Operator; built binary `doctor`, `skills check`, Baseline command, and help/error entry points | Exercise the primary valid journey, hostile authority failures, missing root, repeat execution, help, and invalid arguments. Expect deterministic stdout/stderr and exit status with no mutation. | Fresh reruns and public read commands confirm state. | `command-evidence.md#cli-surface-sweep` | pass | +| Q16 | Backend surface sweep | Maintainer; affected-package focused and race suites | Run the hash, repository, Baseline restoration, and Doctor public-runner suites. Expect all selected tests to pass under race detection. | Run each owning package's broader suite and the repository Verification. | `command-evidence.md#backend-surface-sweep` | pass | +| Q17 | Documentation surface sweep | Maintainer following `CONTEXT.md`, the Roundfix Skill, and Doctor help | Follow the documented Doctor entry point and compare the canonical definition with actual output. Expect detected/minimum acpx reporting and current remediation commands without undocumented knowledge. | Confirm repository skill synchronization in `make verify` and run built help/Doctor commands. | `command-evidence.md#documentation-surface-sweep` | pass | +| Q18 | Full static gate and clean-report postflight | QA reviewer; repository Verification and Git status | Run `rtk make verify`, `git diff --check`, validate report links/statuses, and inspect changed paths. Expect a passing gate, zero pending rows at close, and only QA report/evidence changes. | Re-read the final report and use Git status/diff as the independent artifact check. | `command-evidence.md#static-gate` | pass | + +## Findings + +None. + +## Blocked and skipped + +None. The sandbox DNS failure on the first Baseline preview was resolved by the +documented offline `--source-dir` flow. The host's independent Codex +code-signature readiness failure did not prevent any Repository Skill Set +journey or criterion from executing. + +## Coverage + +Planned coverage: + +- User stories: 5/5 passed. +- Task acceptance criteria: 23/23 passed. +- PRD success metrics: 6/6 passed. +- Non-Goals: 7/7 passed. +- Mandatory surfaces: CLI, backend, and docs passed. +- Planned behavior probes: authority symlinks, Unicode collation, missing root, + unclassified error, repeat/no-mutation, and repeated Baseline restoration + all executed. +- Matrix results: 18 pass, 0 fail, 0 blocked, 0 skipped. +- Findings by impact: 0 Blocks-Completion, 0 Data-Loss, 0 Trust-Damage, + 0 Friction, 0 Cosmetic. + +## Final verdict + +Pass — all 18 matrix rows passed on build +`14e03d61784c5cd9714ba89b25a22bcb7c223b09`; every user story, Task +acceptance criterion, success metric, Non-Goal, and declared surface has fresh +evidence, with no failed, blocked, skipped, or pending rows. From b089b92b4013b95e09c8c058a6805c9f62fd5c09 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 18:26:34 -0300 Subject: [PATCH 34/48] chore: archive 0050-doctor-skill-readiness-hardening --- .../0050-doctor-skill-readiness-hardening/_prd.md | 5 ++++- .../0050-doctor-skill-readiness-hardening/_tasks.md | 0 .../0050-doctor-skill-readiness-hardening/_techspec.md | 0 .../2026-07-26-doctor-skill-readiness/command-evidence.md | 0 .../2026-07-26-doctor-skill-readiness/unicode-corpus/-a | 0 .../unicode-corpus/10-guide.md | 0 .../unicode-corpus/2-guide.md | 0 .../2026-07-26-doctor-skill-readiness/unicode-corpus/A.md | 0 .../2026-07-26-doctor-skill-readiness/unicode-corpus/_a | 0 .../unicode-corpus/nested/deeper/\303\237.md" | 0 .../unicode-corpus/nested/z.md | 0 .../unicode-corpus/nested/\303\211.md" | 0 .../unicode-corpus/nested/\303\244.md" | 0 .../unicode-corpus/\303\251.md" | 0 .../unicode-skills-lock.json | 0 .../qa/qa-report-2026-07-26.md | 0 .../0050-doctor-skill-readiness-hardening/task_01.md | 0 .../0050-doctor-skill-readiness-hardening/task_02.md | 0 .../0050-doctor-skill-readiness-hardening/task_03.md | 0 .../0050-doctor-skill-readiness-hardening/task_04.md | 0 20 files changed, 4 insertions(+), 1 deletion(-) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/_prd.md (98%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/_tasks.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/_techspec.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a (100%) rename "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" => "docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md (100%) rename "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" => "docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" (100%) rename "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" => "docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" (100%) rename "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" => "docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/task_01.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/task_02.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/task_03.md (100%) rename docs/specs/{ => _archived}/0050-doctor-skill-readiness-hardening/task_04.md (100%) diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/_prd.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/_prd.md similarity index 98% rename from docs/specs/0050-doctor-skill-readiness-hardening/_prd.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/_prd.md index bb962a6..e4b8bb6 100644 --- a/docs/specs/0050-doctor-skill-readiness-hardening/_prd.md +++ b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/_prd.md @@ -1,9 +1,12 @@ --- spec: 0050-doctor-skill-readiness-hardening -status: active +status: archived surfaces: [cli, backend, docs] +archived: "2026-07-26" +source_slug: 0050-doctor-skill-readiness-hardening --- + # Doctor Skill Readiness Hardening Spec 0036 made Repository Skill Set readiness a blocking Doctor Command diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/_tasks.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/_tasks.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/_tasks.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/_tasks.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/_techspec.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/_techspec.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/_techspec.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/_techspec.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/command-evidence.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/-a diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/10-guide.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/2-guide.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/A.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/_a diff --git "a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" "b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" similarity index 100% rename from "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" rename to "docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/deeper/\303\237.md" diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/z.md diff --git "a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" "b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" similarity index 100% rename from "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" rename to "docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\211.md" diff --git "a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" "b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" similarity index 100% rename from "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" rename to "docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/nested/\303\244.md" diff --git "a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" "b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" similarity index 100% rename from "docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" rename to "docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-corpus/\303\251.md" diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/evidence/2026-07-26-doctor-skill-readiness/unicode-skills-lock.json diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/qa/qa-report-2026-07-26.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_01.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/task_01.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/task_01.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/task_01.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_02.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/task_02.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/task_02.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/task_02.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_03.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/task_03.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/task_03.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/task_03.md diff --git a/docs/specs/0050-doctor-skill-readiness-hardening/task_04.md b/docs/specs/_archived/0050-doctor-skill-readiness-hardening/task_04.md similarity index 100% rename from docs/specs/0050-doctor-skill-readiness-hardening/task_04.md rename to docs/specs/_archived/0050-doctor-skill-readiness-hardening/task_04.md From 53a15221530e73c910e32ae665c29e9e86aef0cd Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 19:50:24 -0300 Subject: [PATCH 35/48] docs: plan Doctor readiness contract reconciliation --- .../_prd.md | 155 +++++++++ .../_tasks.md | 33 ++ .../_techspec.md | 306 ++++++++++++++++++ .../task_01.md | 63 ++++ .../task_02.md | 72 +++++ .../task_03.md | 86 +++++ .../task_04.md | 92 ++++++ .../task_05.md | 73 +++++ 8 files changed, 880 insertions(+) create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md new file mode 100644 index 0000000..a37bf8c --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md @@ -0,0 +1,155 @@ +--- +spec: 0051-doctor-readiness-contract-reconciliation +status: active +created: 2026-07-26 +surfaces: [cli, backend, docs] +--- + +# Doctor Readiness Contract Reconciliation + +A follow-up review found that the Doctor Command's Repository Skill Set check +can outlive cancellation, can inherit the wrong working-directory behavior +from repository-root handling, and can print remediation that is ambiguous +when both ownership groups fail. The shared external-skill hash also leaves +collation-equal paths dependent on input order, while the newly added Go module +metadata is not tidy. This correction makes those contracts explicit and +deterministic without rewriting the branch or revisiting the implementation +history already accepted by the maintainer. + +## Project Constraints + +- Identifier strategy: not applicable — this correction creates no + project-owned Internal Identifier; existing skill names and + repository-relative paths remain the only identities. Source: + `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — all behavior remains local, + offline, and filesystem-only, with no authentication, authorization, + transport, or HTTP change. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0049 and ADR-0055 require Agent + Selection Profile proof to remain independent and exact; ADR-0066 and + ADR-0072 keep shared Baseline hash consumption in the Go CLI; ADR-0077 + requires this readable Project Constraint snapshot. Source: + `docs/agents/domain.md`. +- Tooling authority: applicable — express maintainer authorization was given + on 2026-07-26 to update `go.mod` and `go.sum` through Go tooling for the + `golang.org/x/text` collation dependency, and to keep + `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md` aligned + with shipped CLI behavior. These four paths are the complete protected + tooling boundary for this Spec; no other protected tooling mutation is + authorized. Source: `docs/agents/agent-instructions.md`. + +## Goals + +- Make every blocking Repository Skill Set filesystem operation honor the + Doctor Command's cancellation context. +- Give collation-equal but byte-distinct paths one deterministic total order. +- Preserve the pre-existing Agent Selection Profile working-directory + behavior while keeping Repository Skill Set inspection anchored only to the + resolved Git root. +- Use canonical Repository Skill Set terminology and fail-closed, + ownership-specific remediation. +- Restore Go module metadata to the state produced by Go tooling. + +## User Stories + +1. As an operator cancelling Doctor, I want Repository Skill Set inspection to + stop cooperatively, so a large repository does not keep Doctor alive after + cancellation. +2. As a maintainer hashing Unicode paths, I want equivalent collation keys to + have a deterministic tie-breaker, so the digest does not depend on input + order. +3. As a developer running Doctor outside Git, I want profile proof to retain + its usable process working directory while Repository Skill Set readiness + reports that a Git repository is required. +4. As an Agent reading a mixed-ownership failure, I want one fail-closed shell + command chain, so later remediation does not run after an earlier action + fails. +5. As a maintainer reviewing dependency metadata, I want `go mod tidy` to + produce no diff, so direct and indirect requirements reflect real imports. + +## Core Features + +1. **Context-aware inspection.** Public Repository Skill Set entry points take + `context.Context` first, propagate it through repository walks and reads, + and return cancellation or deadline errors without losing error-chain + inspection. +2. **Total external hash order.** External-skill hashing keeps American + English collation as the primary ordering contract and uses normalized path + bytes to break equality between distinct paths. +3. **Separated working-directory authority.** Agent Selection Profile proof + uses the resolved Git root when present and the process working directory + otherwise. Repository Skill Set inspection uses only a non-empty resolved + Git root and never substitutes the process working directory. +4. **Deterministic failures.** A symlinked external lock retains external + ownership, missing-root detail uses the canonical Repository Skill Set + term, and multiple remediation commands form one `&&`-joined fail-closed + chain. +5. **Tool-produced module metadata.** Go tooling moves the text dependency to + the direct requirement set and removes stale checksums without introducing + another dependency or changing versions. +6. **Synchronized guidance.** User documentation and the canonical and shipped + Roundfix Skill pair describe the exact Doctor output and remediation + contract. + +## User Experience + +Doctor keeps its current ordered, one-line-per-check text interface. Outside a +Git repository, profile proof still receives a real working directory and the +skills line reports that Repository Skill Set readiness requires a Git +repository. When both owned and external repair actions are needed, `next:` +contains one `&&`-joined command chain in owned-then-external order. + +Cancellation remains a command-level failure signal. Doctor does not install, +update, write, or execute any remediation automatically. + +## Non-Goals / Out of Scope + +- Rebuilding, rebasing, force-pushing, dropping, or otherwise rewriting any + existing branch commit. +- Editing any archived Spec, including Specs 0036 and 0050. +- Reverting or editing the accepted upstream-managed skill update, + `skills-lock.json`, or `skills/recommended.txt`. +- Changing Baseline assets, generated Baseline parity artifacts, or the + repository-owned digest preservation behavior already implemented. +- Editing `.coderabbit.yaml` or `.roundfixrc.yml`. +- Moving Repository Skill Set readiness into `HealthChecker`; the existing + injected Doctor seam remains the ownership boundary. +- Making owned and external skill-root symlinks share one classification. + Both remain blocking, but preserve their ownership-specific contracts. +- Adding a Doctor output schema, new flag, network call, or automatic repair. + +## Success Metrics + +- A pre-cancelled context and a context cancelled during skill traversal both + stop inspection and remain detectable with `errors.Is`. +- Every permutation of a corpus containing collation-equal Unicode paths + produces the same external-skill digest. +- Outside Git, profile proof receives the process working directory, the + repository checker is not invoked, and Doctor prints canonical Repository + Skill Set guidance. +- A symlinked lock produces only external remediation; a mixed failure prints + one owned-then-external chain joined by `&&`. +- `go mod tidy -diff` is empty after implementation. +- Focused tests, affected-package race tests, public Doctor execution, and the + repository verification gate pass without mutating checked repository + state. + +## Decisions + +- Preserve all accepted branch history and correct only the newly confirmed + contract gaps. +- Keep American English collation for compatibility, but define normalized + byte ordering as the deterministic tie-breaker that the external CLI leaves + unspecified. +- Propagate the command context through the existing Doctor dependency seam + rather than adding a second cancellation mechanism. +- Restore the process working-directory fallback only for Agent Selection + Profile proof, never for Repository Skill Set inspection. +- Keep ownership-specific symlink behavior and the existing `HealthChecker` + boundary unchanged. +- Add no ADR because this Spec narrows existing cancellation, hashing, and CLI + contracts instead of creating a new architectural boundary. + +## Open Questions + +None. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md new file mode 100644 index 0000000..a30e949 --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md @@ -0,0 +1,33 @@ +--- +schema: spec-tasks/v1 +spec: 0051-doctor-readiness-contract-reconciliation +graph: + nodes: + - id: task_01 + file: task_01.md + needs: [] + - id: task_02 + file: task_02.md + needs: [task_01] + - id: task_03 + file: task_03.md + needs: [task_02] + - id: task_04 + file: task_04.md + needs: [task_03] + - id: task_05 + file: task_05.md + needs: [task_04] +--- + +# Tasks — Doctor Readiness Contract Reconciliation + +| id | title | type | complexity | needs | +| ------- | ------------------------------------------------ | ------- | ---------- | ------- | +| task_01 | Tidy authorized Go module metadata | chore | low | — | +| task_02 | Make external skill hash ordering total | backend | medium | task_01 | +| task_03 | Make Repository Skill Set inspection cancellable | backend | high | task_02 | +| task_04 | Reconcile Doctor readiness contracts | backend | medium | task_03 | +| task_05 | Synchronize Roundfix Doctor guidance | docs | low | task_04 | + +Waves: 1 → task_01 · 2 → task_02 · 3 → task_03 · 4 → task_04 · 5 → task_05 diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md new file mode 100644 index 0000000..6f7448c --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md @@ -0,0 +1,306 @@ +--- +spec: 0051-doctor-readiness-contract-reconciliation +prd: _prd.md +created: 2026-07-26 +--- + +# Doctor Readiness Contract Reconciliation — Technical Spec + +## Executive Summary + +Reconcile four contracts left incomplete after Repository Skill Set +hardening: Go module ownership, cancellable filesystem inspection, +deterministic collation ties, and Doctor coordination. The existing package +boundaries stay intact: `skills` owns repository facts, +`internal/skillhash` owns pure digest ordering, and `internal/cli` owns Doctor +coordination and rendering. The primary trade-off is that byte-order +tie-breaking deliberately defines behavior where optionless +`String.localeCompare` reports equality and the external CLI leaves final +order dependent on traversal; this yields one reproducible local authority +instead of mirroring an undefined tie. + +## Project Constraints + +- Identifier strategy: not applicable — existing skill names and normalized + repository-relative paths remain the only identifiers. No project-owned + identity is added. Source: `docs/agents/domain.md`. +- Authentication and HTTP: not applicable — the implementation remains local, + offline, and filesystem-only and changes no authentication, authorization, + transport, or HTTP contract. Source: `docs/agents/cli.md`. +- Active ADR obligations: applicable — ADR-0049 and ADR-0055 keep exact Agent + Selection Profile proof independent and eager; ADR-0066 and ADR-0072 keep + the shared Baseline hash consumer in Go without reopening asset + synchronization; ADR-0077 requires this readable constraint snapshot. + Source: `docs/agents/domain.md`. +- Tooling authority: applicable — express maintainer authorization was given + on 2026-07-26 to update `go.mod` and `go.sum` through Go tooling for the + `golang.org/x/text` collation dependency, and to keep + `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md` aligned + with shipped CLI behavior. These four paths are the complete protected + tooling boundary for this Spec; no other protected tooling mutation is + authorized. Source: `docs/agents/agent-instructions.md`. + +## System Architecture + +- `skills` keeps Repository Skill Set ownership and filesystem policy. Its + public blocking entry points become context-first, and cancellation flows + through anchored repository reads, owned comparison, and external hashing. +- `internal/skillhash` keeps the shared pure digest algorithm. Its comparator + becomes a total order by applying raw normalized path order only when the + American English collator reports equality for distinct strings. +- `internal/baseline` remains a consumer of the shared hash. No Baseline + assets, manifests, lock files, or synchronization state change in this + Spec. +- `internal/cli` separates the profile-proof working directory from the + repository readiness root, passes the command context into the injected + skills seam, and owns canonical detail and fail-closed remediation text. +- User guidance and the Roundfix Skill pair mirror the shipped text contract. + No new package, `HealthChecker` method, command, flag, or schema is added. + +## Implementation Design + +### Interfaces + +The public filesystem APIs become context-first: + +```go +func CheckRepository( + ctx context.Context, + root string, +) (RepositoryReadiness, error) + +func SkillFolderHash( + ctx context.Context, + root string, +) (string, error) +``` + +The Doctor seam mirrors the public contract: + +```go +checkSkills func( + context.Context, + string, +) (skills.RepositoryReadiness, error) +``` + +The pure hash operation keeps its existing shape: + +```go +type File struct { + Path string + Content []byte +} + +func Sum(files []File) string +``` + +### Context propagation and errors + +`CheckRepository` checks `ctx.Err()` before starting and passes the same +context to internal repository inspection. Walk callbacks and file-read loops +check cancellation before processing the next entry and before reading file +content. `SkillFolderHash` applies the same checks around collection and +delegates only the completed file slice to the pure hash operation. + +Cancellation is wrapped with the current operation and path through +`RepositoryReadinessError` where repository ownership is known. Wrapping uses +`%w` semantics so `errors.Is(err, context.Canceled)` and +`errors.Is(err, context.DeadlineExceeded)` remain true. No timeout, background +context, goroutine, polling loop, or test-only production hook is introduced. + +Embedded bundle enumeration remains synchronous package data and does not +receive a separate context API. The cancellation boundary covers the supplied +repository and external filesystem walks that can grow with user-controlled +content. + +### Deterministic hash order + +Every path is slash-normalized before comparison. Ordering uses: + +1. `collate.New(language.AmericanEnglish).CompareString`. +2. When the comparison is non-zero, its sign decides order. +3. When the comparison is zero and the normalized strings differ, ordinary Go + string comparison decides order. + +The second comparison turns the stable partial order into a total order. It +preserves `_a` before `-a` and the existing punctuation/case/Unicode +compatibility corpus while making precomposed and decomposed canonical +equivalents independent of caller order. The test oracle pins the resulting +digest and separately permutes the tie corpus; it does not invoke Node or Bun +from production or tests. + +### Doctor working directories + +Doctor resolves two values: + +- `profileWorkDir`: trimmed Git root when present; otherwise the process + working directory obtained with `os.Getwd()`. +- `repositoryRoot`: the trimmed Git root only. + +Profile proof always receives `profileWorkDir`. When `repositoryRoot` is +empty, Doctor does not call `checkSkills` and emits a failed skills result with +detail `Repository Skill Set readiness requires a Git repository` plus the +existing run-from-Git next action. A `Getwd` error follows the pre-existing +command failure behavior and must not be disguised as a Repository Skill Set +result. + +Independent machine checks, adapter proof, profile proof, skills readiness, +and Codex hygiene remain eager and keep their current output order. + +### Ownership and remediation + +Every `skills-lock.json` inspection error, including a symbolic link, carries +`RepositoryOwnershipExternal`. Shared ancestor errors remain unclassified +because they block both ownership groups. Owned and external skill-root +symlinks keep their current distinct classifications. + +Doctor derives required repair groups from readiness fields plus typed error +ownership. An unclassified error with no classified group conservatively +selects both actions. Multiple actions are joined with ` && ` in +owned-then-external order: + +```text +roundfix skills install --target project && bunx skills experimental_install && bunx skills update -p -y +``` + +The existing `"; next: "` separator between detail and the `NextAction` field +does not change. Only composition inside `NextAction` changes, making the +printed shell expression fail closed. + +### Module and naming hygiene + +The tooling Task runs `go mod tidy` rather than editing dependency metadata by +hand. The expected result moves `golang.org/x/text` to the direct requirement +block, removes stale module sums, and adds the sums selected by the current Go +module graph without changing dependency versions. + +Repository helper parameters that currently shadow the imported `path` +package are renamed to describe their role, such as `relativePath` and +`lockPath`. This is a bounded readability correction inside the same +Repository Skill Set slice. + +### Documentation and skill synchronization + +Update the Doctor user guide for: + +- context-aware, read-only Repository Skill Set readiness; +- canonical missing-Git terminology; +- the `&&`-joined mixed remediation chain. + +After the CLI and user guide settle, update only the canonical Roundfix Skill +and its shipped copy in a dedicated tooling Task so +`.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md` remain +byte-identical. Do not combine that protected mutation with code or user-guide +changes. Do not edit upstream-managed skills, recommendation or lock +authorities, archived Specs, Baseline artifacts, `.coderabbit.yaml`, or +`.roundfixrc.yml`. + +## Data Models + +No persisted model or schema changes. `RepositoryReadiness`, +`RepositoryReadinessError`, and `CheckResult` keep their existing fields and +semantics. + +## API Contracts + +The Go signatures for blocking Repository Skill Set inspection add +`context.Context` as the first parameter. Doctor's command-line arguments, +line ordering, status vocabulary, exit codes, stdout/stderr split, and +`"; next: "` detail boundary remain unchanged. + +## Coverage Map + +- Goal: cancellable filesystem inspection; User Story 1 → context-first + `skills` APIs, walk/read cancellation checks, and error-chain tests. +- Goal: deterministic collation ties; User Story 2 → + `internal/skillhash` total comparator and permutation corpus. +- Goal: separated working directories; User Story 3 → Doctor coordinator and + public command tests outside Git. +- Goal: canonical fail-closed failures; User Story 4 → external lock ownership, + canonical detail, and exact mixed-remediation output tests. +- Goal: tool-produced metadata; User Story 5 → bounded module Task and tidy + postflight. +- Goal: synchronized guidance → Doctor user guide and Roundfix Skill sync. + +## Integration Points + +- `golang.org/x/text/collate` and `language` remain the in-process collation + authority. +- `os.Root` remains the repository confinement boundary. +- `os.Getwd` restores only the Agent Selection Profile proof fallback. +- Baseline restoration continues to consume `internal/skillhash.Sum` without + changing its asset or transaction contracts. + +## Testing Approach + +The test invariants and owners are: + +- `internal/skillhash/hash_test.go`: the same normalized file set produces one + digest for every input permutation, including distinct canonically + equivalent paths. Boundary IN is pure ordering and hashing; filesystem + collection is outside. +- `skills/skills_test.go`: `SkillFolderHash` honors pre-cancellation and + cancellation observed while walking or reading. Boundary IN is the public + folder-hash filesystem contract; Doctor rendering is outside. +- `skills/repository_test.go`: `CheckRepository` preserves cancellation in the + error chain and assigns external ownership to a symlinked lock. Boundary IN + is anchored Repository Skill Set inspection; CLI text is outside. +- `internal/cli/doctor_test.go`: outside Git, profile proof receives the real + process working directory while `checkSkills` is not called; mixed failures + print one exact fail-closed chain and independent checks remain eager. + Boundary IN is public Doctor coordination and output; package hashing is + covered below. + +Focused tests use real temporary filesystems and the existing injected Doctor +collaborators only at machine/runtime boundaries. No assertion repeats a value +written into a mock as its sole oracle. Negative companions cover +pre-cancellation, cancellation during work, collator equality, symlinked lock +ownership, and mixed failure. + +Affected-package race tests and the public Doctor command provide the macro +proof. Final QA runs the repository gate, executes Doctor through its public +entry point inside and outside Git, and confirms repository state remains +unchanged. + +## Build Order + +1. Tidy the authorized Go module metadata with Go tooling. +2. Make external-skill hash ordering total and permutation-independent + (depends on: 1). +3. Make Repository Skill Set filesystem inspection context-aware and reconcile + typed ownership and helper naming (depends on: 2). +4. Reconcile Doctor working directories, terminology, remediation, public + tests, and user guidance (depends on: 3). +5. Synchronize only the authorized canonical and shipped Roundfix Skill pair + with the settled Doctor contract (depends on: 4). + +## Risks & Considerations + +- The external CLI does not define ordering when `localeCompare` returns zero. + The raw normalized-path tie-breaker is intentionally stricter and + deterministic; it cannot promise to reproduce a nondeterministic historical + lock generated from the opposite order of canonically equivalent paths. +- Cancellation is cooperative between filesystem operations. A single + in-flight operating-system read may finish before the next context check; + the design adds no goroutine solely to interrupt one file read. +- Adding context to exported functions is a compile-time API change. Current + repository callers are migrated in the same Task, and no compatibility + wrapper preserves a blocking contextless API that violates repository Go + guidance. +- Restoring `os.Getwd()` for profile proof must not leak that value into + Repository Skill Set inspection. Tests assert both values independently. +- Broad skill synchronization can touch unrelated owned skills. The dedicated + tooling Task must mutate only the authorized Roundfix Skill pair and its own + Task file. + +## Decisions + +- Use one context from Doctor through all repository-controlled blocking work. +- Keep the current package boundaries and injected Doctor seam; do not add a + `HealthChecker` method. +- Define raw normalized path order as the final collation tie-breaker. +- Restore working-directory fallback only for Agent Selection Profile proof. +- Join multiple remediation actions with `&&`, preserving owned-first order. +- Produce module metadata only through `go mod tidy`. +- Preserve all prior commits and every archived Spec byte-for-byte. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md new file mode 100644 index 0000000..a1f699a --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md @@ -0,0 +1,63 @@ +--- +task: task_01 +spec: 0051-doctor-readiness-contract-reconciliation +status: pending +type: chore +complexity: low +--- + +# Task 01: Tidy authorized Go module metadata + +## Overview + +Restore the module graph to the exact state produced by Go tooling after the +approved text-collation dependency was introduced. This slice changes no +runtime behavior and is complete when the module files are tidy, verified, and +the protected mutation stays inside its exact allowlist. + +## Requirements + +1. MUST run Go tooling to produce the module metadata; hand editing is + forbidden. +2. MUST keep every selected dependency version unchanged while classifying + `golang.org/x/text` as a direct requirement and removing stale checksums. +3. MUST limit all Task mutations to `go.mod`, `go.sum`, and + `docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md`. +4. MUST NOT change a tool configuration, ignore file, script, plugin + declaration, or any other repository path. + +## Subtasks + +- [ ] Capture the current `go mod tidy -diff` delta as the red signal. +- [ ] Run `go mod tidy` with the repository's selected Go toolchain. +- [ ] Confirm the resolved module versions did not change. +- [ ] Prove the protected changed-file allowlist and module integrity. + +## Acceptance Criteria + +- [ ] `golang.org/x/text v0.40.0` is a direct requirement selected by Go + tooling. +- [ ] Stale sums are removed and required sums are present without a version + upgrade or downgrade. +- [ ] `go mod tidy -diff` exits successfully with no output. +- [ ] `go mod verify` succeeds. +- [ ] Newly changed paths are limited to the two authorized module files and + this Task file. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/go.md` +- interface: `go.mod` +- interface: `go.sum` + +## Verification + +- `rtk go mod tidy -diff` — expected: exit `0` with no diff. +- `rtk go mod verify` — expected: every downloaded module is verified. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != "go.mod" && path != "go.sum" && path != "docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md") {print; bad=1}} END {exit bad}'` — expected: no out-of-allowlist path. + +## References + +- `_prd.md` → Core Features 5; User Story 5; Success Metrics. +- `_techspec.md` → Module and naming hygiene; Build Order 1. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md new file mode 100644 index 0000000..8e279a9 --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md @@ -0,0 +1,72 @@ +--- +task: task_02 +spec: 0051-doctor-readiness-contract-reconciliation +status: pending +type: backend +complexity: medium +--- + +# Task 02: Make external skill hash ordering total + +## Overview + +Give the shared external-skill digest one total path order even when American +English collation treats distinct Unicode strings as equal. The slice remains +pure and independently verifiable through permutation tests plus the existing +Baseline consumer contract. + +## Requirements + +1. MUST retain slash normalization and American English collation as the + primary path order. +2. MUST break a zero collation comparison between distinct normalized paths + with ordinary Go string ordering. +3. MUST produce the same digest for every permutation of the same file set, + including precomposed and decomposed Unicode path pairs. +4. MUST retain `_a` before `-a`, the current path-plus-content digest shape, + and caller-owned slice order. +5. MUST keep `internal/skillhash` as the only comparator implementation used by + Repository Skill Set and Baseline consumers. +6. MUST NOT change a lock file, Baseline asset, parity artifact, recommendation + list, or upstream-managed skill. + +## Subtasks + +- [ ] Add a failing permutation case for collation-equal distinct paths. +- [ ] Define the normalized raw-path tie-breaker after collator equality. +- [ ] Preserve punctuation, case, path-depth, and content-change regressions. +- [ ] Exercise the existing Baseline restoration consumer against the shared + implementation. + +## Acceptance Criteria + +- [ ] Every tested permutation of the tie corpus yields one pinned digest. +- [ ] The punctuation and Unicode compatibility corpus retains its primary + American English collation order. +- [ ] Changing one path or one content byte changes the digest. +- [ ] `Sum` does not mutate the caller's file order. +- [ ] Baseline restoration tests pass without adding a comparator or changing + a generated artifact. + +## Context + +- instruction: `.agents/skills/coding-guidelines/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `internal/skillhash/hash.go` +- interface: `internal/skillhash/hash_test.go` +- interface: `internal/baseline/skills_restore.go` +- interface: `internal/baseline/skills_restore_test.go` + +## Verification + +- `rtk go test ./internal/skillhash -run 'TestSum'` — expected: total-order, + compatibility, non-mutation, and negative digest cases pass. +- `rtk go test ./internal/baseline -run 'TestSkillsRestore'` — expected: + Baseline restoration remains compatible with the shared hash authority. +- `rtk go test -race ./internal/skillhash ./internal/baseline -run 'Test(Sum|SkillsRestore)'` — expected: affected hash consumers pass under the race detector. + +## References + +- `_prd.md` → Core Features 2; User Story 2; Success Metrics. +- `_techspec.md` → Deterministic hash order; Testing Approach; Build Order 2. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md new file mode 100644 index 0000000..026545a --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md @@ -0,0 +1,86 @@ +--- +task: task_03 +spec: 0051-doctor-readiness-contract-reconciliation +status: pending +type: backend +complexity: high +--- + +# Task 03: Make Repository Skill Set inspection cancellable + +## Overview + +Propagate the Doctor Command's context through every repository-controlled +blocking Repository Skill Set entry point and preserve cancellation in its +error chain. This slice also completes the typed external lock authority and +removes path-name shadowing inside the same repository inspection boundary. + +## Requirements + +1. MUST make `CheckRepository` and `SkillFolderHash` context-first APIs and + migrate every repository caller. +2. MUST propagate the same context through anchored repository inspection, + owned-skill comparison, external tree collection, and the injected Doctor + seam. +3. MUST check cancellation before traversal, while walking entries, and before + reading the next file without spawning an interrupt goroutine. +4. MUST preserve `context.Canceled` and `context.DeadlineExceeded` for + `errors.Is`, including when wrapped in `RepositoryReadinessError`. +5. MUST classify every `skills-lock.json` authority failure, including a + symbolic link, as external ownership. +6. MUST rename helper parameters that shadow the imported `path` package + without changing path validation behavior. +7. MUST preserve anchored no-symlink checks, ownership-specific skill-root + behavior, offline execution, and no-mutation guarantees. + +## Subtasks + +- [ ] Add pre-cancelled public API regressions at the owning package layer. +- [ ] Add deterministic cancellation-during-walk or read coverage without a + production-only test hook. +- [ ] Thread context through repository and folder-hash call chains. +- [ ] Migrate the Doctor dependency seam and every compile-time caller. +- [ ] Correct symlinked lock ownership and path-parameter naming. +- [ ] Re-run repository security, no-mutation, and cancellation cases together. + +## Acceptance Criteria + +- [ ] A pre-cancelled context returns promptly from both public filesystem APIs + and remains identifiable with `errors.Is`. +- [ ] Cancellation observed during controlled traversal or reading stops before + processing the remaining entries. +- [ ] Doctor passes its command context to the Repository Skill Set seam. +- [ ] A symlinked lock produces a typed error with external ownership and never + reads its target. +- [ ] Existing shared-ancestor, owned-root, and external-root symlink tests keep + their current blocking classifications. +- [ ] All callers compile with context-first signatures and no contextless + compatibility wrapper remains. + +## Context + +- instruction: `docs/agents/go.md` +- instruction: `.agents/skills/golang-context/SKILL.md` +- instruction: `.agents/skills/golang-error-handling/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- interface: `skills/repository.go` +- interface: `skills/repository_test.go` +- interface: `skills/skills.go` +- interface: `skills/skills_test.go` +- interface: `internal/cli/doctor.go` +- interface: `internal/cli/cli_test.go` + +## Verification + +- `rtk go test ./skills -run 'Test(CheckRepository|SkillFolderHash)'` — expected: + cancellation, ownership, confinement, hash, and no-mutation cases pass. +- `rtk go test ./internal/cli -run 'TestRunDoctor'` — expected: every migrated + Doctor caller compiles and the existing coordination contract remains green. +- `rtk go test -race ./skills ./internal/cli -run 'Test(CheckRepository|SkillFolderHash|RunDoctor)'` — expected: affected package contracts pass under the race detector. + +## References + +- `_prd.md` → Core Features 1 and 4; User Story 1; Success Metrics. +- `_techspec.md` → Interfaces; Context propagation and errors; Ownership and + remediation; Module and naming hygiene; Build Order 3. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md new file mode 100644 index 0000000..330a999 --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md @@ -0,0 +1,92 @@ +--- +task: task_04 +spec: 0051-doctor-readiness-contract-reconciliation +status: pending +type: backend +complexity: medium +--- + +# Task 04: Reconcile Doctor readiness contracts + +## Overview + +Restore the Agent Selection Profile working-directory fallback without +weakening the Git-root-only Repository Skill Set boundary, then make Doctor's +terminology and mixed remediation exact. Public command tests and user +guidance make the corrected output contract independently reviewable. + +## Requirements + +1. MUST pass the trimmed Git root to profile proof when present and the process + working directory when it is absent. +2. MUST pass only a non-empty trimmed Git root to Repository Skill Set + inspection; an empty root must not invoke the checker. +3. MUST report `Repository Skill Set readiness requires a Git repository` for + the missing-root detail and keep the existing run-from-Git next action. +4. MUST join multiple ownership remediation actions with ` && ` in + owned-then-external order while retaining the existing `"; next: "` result + boundary. +5. MUST preserve eager check execution, line order, stdout/stderr discipline, + and exit codes. +6. MUST move Doctor-specific behavior tests and helpers from the CLI-wide test + file to `doctor_test.go`, while leaving dispatch and help registry tests in + the CLI-wide suite. +7. MUST update user guidance for the canonical term and fail-closed command + chain without editing either protected Roundfix Skill file in this Task. + +## Subtasks + +- [ ] Add the outside-Git profile-workdir regression and checker non-invocation + assertion. +- [ ] Restore separate profile and repository root resolution. +- [ ] Correct missing-root detail and mixed remediation composition. +- [ ] Move Doctor-specific tests to the source-matched test file without + weakening assertions. +- [ ] Update the public Doctor example and explanatory user guidance. +- [ ] Exercise inside-Git, outside-Git, typed ownership, and unclassified error + paths through the public runner. + +## Acceptance Criteria + +- [ ] Outside Git, profile proof receives `os.Getwd()` and the skills checker is + not invoked. +- [ ] Inside Git, profile proof and Repository Skill Set inspection both receive + the resolved Git root. +- [ ] Missing-root output uses the canonical Repository Skill Set term. +- [ ] Mixed remediation prints one exact `&&`-joined chain and a symlinked lock + prints only external remediation. +- [ ] Codex and other independent checks still run after profile or skills + failure and keep their existing order. +- [ ] Doctor behavior tests live in `internal/cli/doctor_test.go`; CLI-wide + dispatch/help tests remain in `internal/cli/cli_test.go`. +- [ ] User guidance matches the shipped text contract and no protected skill + file changes in this Task. + +## Context + +- instruction: `docs/agents/cli.md` +- instruction: `.agents/skills/agentic-cli-design/SKILL.md` +- instruction: `.agents/skills/golang-cli/SKILL.md` +- instruction: `.agents/skills/golang-testing/SKILL.md` +- instruction: `.agents/skills/testing-boss/SKILL.md` +- instruction: `.agents/skills/tech-writer/SKILL.md` +- interface: `internal/cli/doctor.go` +- interface: `internal/cli/cli_test.go` +- interface: `docs/user-guide/commands.md` +- interface: `CONTEXT.md` + +## Verification + +- `rtk go test ./internal/cli -run 'Test(RunDoctor|Doctor)'` — expected: + working-directory, output, eager-order, and remediation cases pass. +- `rtk go test -race ./internal/cli -run 'Test(RunDoctor|Doctor)'` — expected: + Doctor contracts pass under the race detector. +- `rtk grep -n '&& bunx skills experimental_install' docs/user-guide/commands.md` — expected: the public mixed-remediation example uses a fail-closed chain. +- `rtk git diff --exit-code -- .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` — expected: this non-tooling Task leaves both protected skill files unchanged. + +## References + +- `_prd.md` → Core Features 3, 4, and 6; User Stories 3–4; User Experience; + Success Metrics. +- `_techspec.md` → Doctor working directories; Ownership and remediation; + Documentation and skill synchronization; Testing Approach; Build Order 4. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md new file mode 100644 index 0000000..49ca32e --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md @@ -0,0 +1,73 @@ +--- +task: task_05 +spec: 0051-doctor-readiness-contract-reconciliation +status: pending +type: docs +complexity: low +--- + +# Task 05: Synchronize Roundfix Doctor guidance + +## Overview + +Align the canonical Roundfix Skill and its shipped copy with the settled +Doctor contract after code and user guidance are verified. This dedicated +tooling slice has an exact three-path mutation allowlist and changes no runtime +code, upstream-managed skill, lock authority, or generated Baseline artifact. + +## Requirements + +1. MUST document the canonical missing-Git Repository Skill Set result and the + `&&`-joined mixed remediation chain in the Roundfix Skill. +2. MUST keep `.agents/skills/roundfix/SKILL.md` and + `skills/roundfix/SKILL.md` byte-identical. +3. MUST limit every Task mutation to + `.agents/skills/roundfix/SKILL.md`, + `skills/roundfix/SKILL.md`, and + `docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md`. +4. MUST NOT run a broad mutation that rewrites unrelated owned skills. +5. MUST NOT edit an upstream-managed skill, `skills-lock.json`, + `skills/recommended.txt`, a Baseline artifact, `.coderabbit.yaml`, or + `.roundfixrc.yml`. + +## Subtasks + +- [ ] Capture the stale Roundfix Skill wording as the red signal. +- [ ] Update the canonical skill with the settled Doctor contract. +- [ ] Apply the exact canonical bytes to the shipped copy. +- [ ] Prove byte equality, repository skill synchronization, and the + changed-file allowlist. + +## Acceptance Criteria + +- [ ] Both Roundfix Skill copies describe canonical Repository Skill Set + missing-root behavior. +- [ ] Both copies show mixed remediation as one owned-then-external chain + joined by `&&`. +- [ ] The canonical and shipped copies are byte-identical. +- [ ] Repository-owned skill synchronization checks pass. +- [ ] Newly changed paths are limited to the two authorized skill files and + this Task file. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/skill-dispatch.md` +- instruction: `.agents/skills/tech-writer/SKILL.md` +- interface: `.agents/skills/roundfix/SKILL.md` +- interface: `skills/roundfix/SKILL.md` +- interface: `docs/user-guide/commands.md` + +## Verification + +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` — expected: + canonical and shipped files are byte-identical. +- `rtk make skills-sync-check` — expected: repository-owned skill pairs are + synchronized. +- `rtk grep -n '&& bunx skills experimental_install' .agents/skills/roundfix/SKILL.md` — expected: canonical guidance uses the fail-closed chain. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != ".agents/skills/roundfix/SKILL.md" && path != "skills/roundfix/SKILL.md" && path != "docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md") {print; bad=1}} END {exit bad}'` — expected: no out-of-allowlist path. + +## References + +- `_prd.md` → Core Features 6; Non-Goals; Success Metrics. +- `_techspec.md` → Documentation and skill synchronization; Build Order 5. From c27c8a1defa10405e0f159d1520ec33f0b28a7bb Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 19:55:09 -0300 Subject: [PATCH 36/48] chore: tidy authorized Go module metadata Roundfix-Spec: 0051-doctor-readiness-contract-reconciliation Roundfix-Task: task_01 --- .../task_01.md | 44 ++++++++++++++----- go.mod | 2 +- go.sum | 8 +--- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md index a1f699a..9595877 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md @@ -1,7 +1,7 @@ --- task: task_01 spec: 0051-doctor-readiness-contract-reconciliation -status: pending +status: completed type: chore complexity: low --- @@ -28,20 +28,20 @@ the protected mutation stays inside its exact allowlist. ## Subtasks -- [ ] Capture the current `go mod tidy -diff` delta as the red signal. -- [ ] Run `go mod tidy` with the repository's selected Go toolchain. -- [ ] Confirm the resolved module versions did not change. -- [ ] Prove the protected changed-file allowlist and module integrity. +- [x] Capture the current `go mod tidy -diff` delta as the red signal. +- [x] Run `go mod tidy` with the repository's selected Go toolchain. +- [x] Confirm the resolved module versions did not change. +- [x] Prove the protected changed-file allowlist and module integrity. ## Acceptance Criteria -- [ ] `golang.org/x/text v0.40.0` is a direct requirement selected by Go +- [x] `golang.org/x/text v0.40.0` is a direct requirement selected by Go tooling. -- [ ] Stale sums are removed and required sums are present without a version +- [x] Stale sums are removed and required sums are present without a version upgrade or downgrade. -- [ ] `go mod tidy -diff` exits successfully with no output. -- [ ] `go mod verify` succeeds. -- [ ] Newly changed paths are limited to the two authorized module files and +- [x] `go mod tidy -diff` exits successfully with no output. +- [x] `go mod verify` succeeds. +- [x] Newly changed paths are limited to the two authorized module files and this Task file. ## Context @@ -61,3 +61,27 @@ the protected mutation stays inside its exact allowlist. - `_prd.md` → Core Features 5; User Story 5; Success Metrics. - `_techspec.md` → Module and naming hygiene; Build Order 1. + +## Result + +Go 1.26.5 produced the authorized module metadata. The initial +`rtk go mod tidy -diff` exited `1` and showed the required red delta: +`golang.org/x/text v0.40.0` moved from indirect to direct, obsolete sums for +`golang.org/x/mod v0.33.0`, `golang.org/x/sync v0.20.0`, and +`golang.org/x/tools v0.42.0` were removed, and the selected +`golang.org/x/mod v0.37.0` and `golang.org/x/tools v0.47.0` module-file sums +were added. `rtk go mod tidy` then exited `0`. + +Acceptance evidence: + +- `rtk go mod edit -json` reports `golang.org/x/text v0.40.0` without + `Indirect`, proving the direct requirement. +- Pre- and post-tidy `rtk go list -m all` output selected the same module + versions; the `go.mod` diff changes only the direct classification. +- `rtk go mod tidy -diff` exits `0` with no output after the metadata update. +- `rtk go mod verify` exits `0` with `all modules verified`. +- The declared changed-path filter exits `0`, and + `rtk git -c core.fsmonitor=false status --short` lists only `go.mod`, + `go.sum`, and this Task file. + +Follow-ups: none. diff --git a/go.mod b/go.mod index 9882ab3..c080bed 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( charm.land/bubbletea/v2 v2.0.7 charm.land/lipgloss/v2 v2.0.3 golang.org/x/sys v0.45.0 + golang.org/x/text v0.40.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.52.0 ) @@ -30,7 +31,6 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.22.0 // indirect - golang.org/x/text v0.40.0 // indirect modernc.org/libc v1.72.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index d32f7d0..21eac58 100644 --- a/go.sum +++ b/go.sum @@ -48,11 +48,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -60,9 +57,8 @@ golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From b8ed2dc04f0b477dc545818b07842ed1a4895255 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 20:02:17 -0300 Subject: [PATCH 37/48] feat: make external skill hash ordering total Roundfix-Spec: 0051-doctor-readiness-contract-reconciliation Roundfix-Task: task_02 --- .../task_02.md | 58 +++++++++++++++---- internal/skillhash/hash.go | 6 +- internal/skillhash/hash_test.go | 36 +++++++++++- 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md index 8e279a9..5ba57a2 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md @@ -1,7 +1,7 @@ --- task: task_02 spec: 0051-doctor-readiness-contract-reconciliation -status: pending +status: completed type: backend complexity: medium --- @@ -32,20 +32,20 @@ Baseline consumer contract. ## Subtasks -- [ ] Add a failing permutation case for collation-equal distinct paths. -- [ ] Define the normalized raw-path tie-breaker after collator equality. -- [ ] Preserve punctuation, case, path-depth, and content-change regressions. -- [ ] Exercise the existing Baseline restoration consumer against the shared +- [x] Add a failing permutation case for collation-equal distinct paths. +- [x] Define the normalized raw-path tie-breaker after collator equality. +- [x] Preserve punctuation, case, path-depth, and content-change regressions. +- [x] Exercise the existing Baseline restoration consumer against the shared implementation. ## Acceptance Criteria -- [ ] Every tested permutation of the tie corpus yields one pinned digest. -- [ ] The punctuation and Unicode compatibility corpus retains its primary +- [x] Every tested permutation of the tie corpus yields one pinned digest. +- [x] The punctuation and Unicode compatibility corpus retains its primary American English collation order. -- [ ] Changing one path or one content byte changes the digest. -- [ ] `Sum` does not mutate the caller's file order. -- [ ] Baseline restoration tests pass without adding a comparator or changing +- [x] Changing one path or one content byte changes the digest. +- [x] `Sum` does not mutate the caller's file order. +- [x] Baseline restoration tests pass without adding a comparator or changing a generated artifact. ## Context @@ -70,3 +70,41 @@ Baseline consumer contract. - `_prd.md` → Core Features 2; User Story 2; Success Metrics. - `_techspec.md` → Deterministic hash order; Testing Approach; Build Order 2. + +## Result + +`Sum` now applies ordinary Go string ordering to distinct normalized paths +only when American English collation reports equality. The existing collation +order, slash normalization, path-plus-content digest shape, and caller-owned +slice order remain unchanged. + +The new regression test reproduced the defect before the implementation +change: the precomposed-first permutation produced +`dc5f4008143541df5bfab2e71ac7f1a0cbd8f9963ea00747b33f630894108f91` +instead of the pinned total-order digest +`2692b0010be42e60169e655aa45186b0d836be24c81a1c3aa9652d2614fc4451`. +After the change, both permutations produce the pinned digest. + +Acceptance evidence: + +- `TestSumIsPermutationIndependentForCollationEqualPaths` passes for both + precomposed-first and decomposed-first input. +- `TestSumPreservesSkillsCLI1519PrimaryCollation` and + `TestSumSortsUnderscoreBeforeHyphen` preserve the punctuation, case, + path-depth, and Unicode primary-collation corpus. +- `TestSumChangesWhenPathOrContentChanges` passes for both negative digest + cases. +- `TestSumPreservesSkillsCLI1519PrimaryCollation` confirms that `Sum` leaves + the caller-owned slice unchanged. +- `TestSkillsRestore*` passes through the existing `skillhash.Sum` consumer; + no Baseline comparator, asset, lock file, or generated artifact changed. + +Verification: + +- `rtk go test ./internal/skillhash -run 'TestSum'` — passed, 8 tests. +- `rtk go test ./internal/baseline -run 'TestSkillsRestore'` — passed, 14 + tests. +- `rtk go test -race ./internal/skillhash ./internal/baseline -run + 'Test(Sum|SkillsRestore)'` — passed, 22 tests in 2 packages. + +Follow-ups: none. diff --git a/internal/skillhash/hash.go b/internal/skillhash/hash.go index a934552..2ed760f 100644 --- a/internal/skillhash/hash.go +++ b/internal/skillhash/hash.go @@ -26,7 +26,11 @@ func Sum(files []File) string { collator := collate.New(language.AmericanEnglish) sort.SliceStable(ordered, func(i, j int) bool { - return collator.CompareString(ordered[i].Path, ordered[j].Path) < 0 + comparison := collator.CompareString(ordered[i].Path, ordered[j].Path) + if comparison != 0 { + return comparison < 0 + } + return ordered[i].Path < ordered[j].Path }) digest := sha256.New() diff --git a/internal/skillhash/hash_test.go b/internal/skillhash/hash_test.go index c1d04d1..f8d28ed 100644 --- a/internal/skillhash/hash_test.go +++ b/internal/skillhash/hash_test.go @@ -11,7 +11,7 @@ import ( "testing" ) -func TestSumMatchesSkillsCLI1519(t *testing.T) { +func TestSumPreservesSkillsCLI1519PrimaryCollation(t *testing.T) { files := []File{ {Path: "nested/É.md", Content: []byte("nested uppercase accent\n")}, {Path: "-a", Content: []byte("hyphen\n")}, @@ -29,9 +29,9 @@ func TestSumMatchesSkillsCLI1519(t *testing.T) { } before := append([]File(nil), files...) - const want = "2a46b6d704729eafc0148969028b9cc4030813059e1f7524def2f38b433011d4" + const want = "9a2453ded0dfc2f230f4702e172583f9ec29bfe1891dd27338c04894ee6678e0" if got := Sum(files); got != want { - t.Fatalf("Sum() = %q, want skills CLI 1.5.19 digest %q", got, want) + t.Fatalf("Sum() = %q, want primary-collation compatibility digest %q", got, want) } if !reflect.DeepEqual(files, before) { t.Fatalf("Sum() changed caller-owned order:\ngot: %#v\nwant: %#v", files, before) @@ -52,6 +52,36 @@ func TestSumSortsUnderscoreBeforeHyphen(t *testing.T) { } } +func TestSumIsPermutationIndependentForCollationEqualPaths(t *testing.T) { + precomposed := File{Path: "é.md", Content: []byte("precomposed\n")} + decomposed := File{Path: "é.md", Content: []byte("decomposed\n")} + orderedBytes := []byte("é.mddecomposed\né.mdprecomposed\n") + wantBytes := sha256.Sum256(orderedBytes) + want := hex.EncodeToString(wantBytes[:]) + + tests := []struct { + name string + files []File + }{ + { + name: "precomposed first", + files: []File{precomposed, decomposed}, + }, + { + name: "decomposed first", + files: []File{decomposed, precomposed}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := Sum(test.files); got != want { + t.Fatalf("Sum() = %q, want total-order digest %q", got, want) + } + }) + } +} + func TestSumChangesWhenPathOrContentChanges(t *testing.T) { base := []File{ {Path: "SKILL.md", Content: []byte("skill\n")}, From 0b101629faacd0fde7ffcac4ffe092293792d0f0 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 20:15:44 -0300 Subject: [PATCH 38/48] feat: make Repository Skill Set inspection cancellable Roundfix-Spec: 0051-doctor-readiness-contract-reconciliation Roundfix-Task: task_03 --- .../task_03.md | 76 ++++++++++--- internal/cli/cli_test.go | 48 ++++++++- internal/cli/doctor.go | 4 +- skills/baseline_skill_contract_test.go | 8 +- skills/repository.go | 102 ++++++++++++++---- skills/repository_test.go | 86 ++++++++++++--- skills/skills.go | 24 ++++- skills/skills_test.go | 86 ++++++++++++++- 8 files changed, 367 insertions(+), 67 deletions(-) diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md index 026545a..751cf7f 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md @@ -1,7 +1,7 @@ --- task: task_03 spec: 0051-doctor-readiness-contract-reconciliation -status: pending +status: completed type: backend complexity: high --- @@ -35,26 +35,26 @@ removes path-name shadowing inside the same repository inspection boundary. ## Subtasks -- [ ] Add pre-cancelled public API regressions at the owning package layer. -- [ ] Add deterministic cancellation-during-walk or read coverage without a +- [x] Add pre-cancelled public API regressions at the owning package layer. +- [x] Add deterministic cancellation-during-walk or read coverage without a production-only test hook. -- [ ] Thread context through repository and folder-hash call chains. -- [ ] Migrate the Doctor dependency seam and every compile-time caller. -- [ ] Correct symlinked lock ownership and path-parameter naming. -- [ ] Re-run repository security, no-mutation, and cancellation cases together. +- [x] Thread context through repository and folder-hash call chains. +- [x] Migrate the Doctor dependency seam and every compile-time caller. +- [x] Correct symlinked lock ownership and path-parameter naming. +- [x] Re-run repository security, no-mutation, and cancellation cases together. ## Acceptance Criteria -- [ ] A pre-cancelled context returns promptly from both public filesystem APIs +- [x] A pre-cancelled context returns promptly from both public filesystem APIs and remains identifiable with `errors.Is`. -- [ ] Cancellation observed during controlled traversal or reading stops before +- [x] Cancellation observed during controlled traversal or reading stops before processing the remaining entries. -- [ ] Doctor passes its command context to the Repository Skill Set seam. -- [ ] A symlinked lock produces a typed error with external ownership and never +- [x] Doctor passes its command context to the Repository Skill Set seam. +- [x] A symlinked lock produces a typed error with external ownership and never reads its target. -- [ ] Existing shared-ancestor, owned-root, and external-root symlink tests keep +- [x] Existing shared-ancestor, owned-root, and external-root symlink tests keep their current blocking classifications. -- [ ] All callers compile with context-first signatures and no contextless +- [x] All callers compile with context-first signatures and no contextless compatibility wrapper remains. ## Context @@ -84,3 +84,53 @@ removes path-name shadowing inside the same repository inspection boundary. - `_prd.md` → Core Features 1 and 4; User Story 1; Success Metrics. - `_techspec.md` → Interfaces; Context propagation and errors; Ownership and remediation; Module and naming hygiene; Build Order 3. + +## Result + +Implemented context-first Repository Skill Set inspection. `CheckRepository` +and `SkillFolderHash` now propagate the Doctor Command context through anchored +repository checks, owned comparisons, external walks, and file reads. Walks +check cancellation before each entry and immediately before and after file +reads, without goroutines or test-only production hooks. Cancellation remains +discoverable through wrapped errors. + +`skills-lock.json` symlinks now return `RepositoryReadinessError` with external +ownership before the target can be decoded. Shared ancestors and owned and +external skill roots retain their prior blocking behavior. Repository helper +parameters no longer shadow the imported `path` package, and every in-repository +caller uses the context-first signatures. + +Acceptance evidence: + +- `TestCheckRepositoryHonorsPreCanceledContext` and + `TestSkillFolderHashHonorsPreCanceledContext` cover canceled and expired + contexts before a missing root can be inspected; `errors.Is` matches both + `context.Canceled` and `context.DeadlineExceeded`, including a + `RepositoryReadinessError` chain. +- `TestSkillFolderHashStopsAfterCancellationDuringRead` cancels through a real + `fs.FS` read boundary and proves that only the first of two files opens. +- `TestRunDoctorPassesCommandContextToRepositorySkillReadiness` proves the + injected seam receives the command context and resolved repository root. +- `TestCheckRepositoryRejectsSymlinkedAuthoritiesBeforeReadingTargets` proves + lock symlinks are externally owned, shared ancestors stay unclassified, and + the malformed target is never decoded. Existing owned and external symlink + and no-mutation cases pass in the focused package run. +- Repository-wide compilation and the caller search confirm that only the + context-first exported signatures remain. + +Verification: + +- `GOCACHE=/private/tmp/roundfix-task03-go-cache rtk go test ./skills -run 'Test(CheckRepository|SkillFolderHash)'`: + passed. +- `GOCACHE=/private/tmp/roundfix-task03-go-cache rtk go test ./internal/cli -run 'TestRunDoctor'`: + passed. +- `GOCACHE=/private/tmp/roundfix-task03-go-cache rtk go test -race ./skills ./internal/cli -run 'Test(CheckRepository|SkillFolderHash|RunDoctor)'`: + passed. +- `GOCACHE=/private/tmp/roundfix-task03-go-cache rtk make verify`: passed; + 2,420 tests across 23 packages, skill checks, and build completed. +- `rtk git -c core.fsmonitor=false diff --check`: passed. + +The first focused command using the host-default Go cache could not open its +sandbox-inaccessible cache path. The same command with a writable isolated +`GOCACHE` passed; the Daemon remains authoritative for the task's verbatim +Verification commands. diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 5f3075a..2c18c01 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -2648,7 +2648,7 @@ func TestRunDoctorRepositorySkillReadiness(t *testing.T) { return profileProofResult{} }) skillCalls := 0 - doctorDeps.checkSkills = func(root string) (skills.RepositoryReadiness, error) { + doctorDeps.checkSkills = func(_ context.Context, root string) (skills.RepositoryReadiness, error) { skillCalls++ recordCall(HealthCheckSkills) if root != "/repo/project" { @@ -2690,6 +2690,44 @@ func TestRunDoctorRepositorySkillReadiness(t *testing.T) { } } +func TestRunDoctorPassesCommandContextToRepositorySkillReadiness(t *testing.T) { + type contextKey struct{} + const marker = "doctor-command" + + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, + ) + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + GitRoot: "/repo/project", + }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { + return profileProofResult{} + }) + doctorDeps.checkSkills = func(ctx context.Context, root string) (skills.RepositoryReadiness, error) { + if got := ctx.Value(contextKey{}); got != marker { + t.Fatalf("repository checker context marker = %v, want %q", got, marker) + } + if root != "/repo/project" { + t.Fatalf("repository checker root = %q, want /repo/project", root) + } + return skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + }, nil + } + commandContext := context.WithValue(t.Context(), contextKey{}, marker) + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := runDoctorCommand(commandContext, nil, &stdout, &stderr) + + if code != exitOK { + t.Fatalf("exit code = %d, want %d; stdout=%q stderr=%q", code, exitOK, stdout.String(), stderr.String()) + } +} + func TestRunDoctorMissingRepositoryRoot(t *testing.T) { processDir := t.TempDir() t.Chdir(processDir) @@ -2713,7 +2751,7 @@ func TestRunDoctorMissingRepositoryRoot(t *testing.T) { return profileProofResult{} }) skillCalls := 0 - doctorDeps.checkSkills = func(root string) (skills.RepositoryReadiness, error) { + doctorDeps.checkSkills = func(_ context.Context, root string) (skills.RepositoryReadiness, error) { skillCalls++ return skills.RepositoryReadiness{}, fmt.Errorf("unexpected repository check for %q", root) } @@ -2939,7 +2977,7 @@ func writeDoctorReadyRepositoryFixture(t *testing.T, root string) { skillRoot := filepath.Join(skillsRoot, name) mustMkdir(t, skillRoot) mustWrite(t, filepath.Join(skillRoot, "SKILL.md"), name+"\n") - hash, err := skills.SkillFolderHash(skillRoot) + hash, err := skills.SkillFolderHash(t.Context(), skillRoot) if err != nil { t.Fatalf("hash external skill fixture %q: %v", name, err) } @@ -3024,7 +3062,7 @@ func withDoctorFakeLoadedAndReadiness(t *testing.T, checker HealthChecker, loade return checker }, profileReadiness: readiness, - checkSkills: func(string) (skills.RepositoryReadiness, error) { + checkSkills: func(context.Context, string) (skills.RepositoryReadiness, error) { return skills.RepositoryReadiness{ OwnedRequired: 14, ExternalRequired: 25, @@ -3041,7 +3079,7 @@ func withDoctorLiveDeps(t *testing.T, checker HealthChecker) { old := doctorDeps doctorDeps = defaultDoctorDependencies() doctorDeps.healthChecker = func(roundconfig.Loaded) HealthChecker { return checker } - doctorDeps.checkSkills = func(string) (skills.RepositoryReadiness, error) { + doctorDeps.checkSkills = func(context.Context, string) (skills.RepositoryReadiness, error) { return skills.RepositoryReadiness{ OwnedRequired: 14, ExternalRequired: 25, diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 988ce50..134590a 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -21,7 +21,7 @@ type doctorDependencies struct { loadConfig func(roundconfig.LoadOptions) (roundconfig.Loaded, error) healthChecker func(roundconfig.Loaded) HealthChecker profileReadiness func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult - checkSkills func(string) (skills.RepositoryReadiness, error) + checkSkills func(context.Context, string) (skills.RepositoryReadiness, error) } func defaultDoctorDependencies() doctorDependencies { @@ -67,7 +67,7 @@ func runDoctorCommand(ctx context.Context, args []string, stdout, stderr io.Writ if workDir == "" { results = append(results, doctorMissingRepositoryRootResult()) } else { - skillReadiness, skillErr := doctorDeps.checkSkills(workDir) + skillReadiness, skillErr := doctorDeps.checkSkills(ctx, workDir) results = append(results, doctorSkillReadinessResult(skillReadiness, skillErr)) } results = append(results, checker.Codex(ctx)) diff --git a/skills/baseline_skill_contract_test.go b/skills/baseline_skill_contract_test.go index 36ebb42..9586103 100644 --- a/skills/baseline_skill_contract_test.go +++ b/skills/baseline_skill_contract_test.go @@ -216,11 +216,11 @@ func TestAuthorialSkillSync(t *testing.T) { distributedRoot := filepath.Join(repoRoot, "skills", skillName) assertSkillTreesEqual(t, canonicalRoot, distributedRoot) - canonicalDigest, err := SkillFolderHash(canonicalRoot) + canonicalDigest, err := SkillFolderHash(t.Context(), canonicalRoot) if err != nil { t.Fatalf("hash canonical skill: %v", err) } - distributedDigest, err := SkillFolderHash(distributedRoot) + distributedDigest, err := SkillFolderHash(t.Context(), distributedRoot) if err != nil { t.Fatalf("hash distributed skill: %v", err) } @@ -629,7 +629,7 @@ func TestUpstreamADRFormatUnchanged(t *testing.T) { sort.Strings(names) upstreamDigest := sha256.New() for _, name := range names { - folderDigest, err := SkillFolderHash(filepath.Join(repoRoot, ".agents", "skills", name)) + folderDigest, err := SkillFolderHash(t.Context(), filepath.Join(repoRoot, ".agents", "skills", name)) if err != nil { t.Fatalf("hash upstream-managed skill %q: %v", name, err) } @@ -896,7 +896,7 @@ func upstreamManagedSkillDigest( sort.Strings(names) digest := sha256.New() for _, name := range names { - folderDigest, err := SkillFolderHash(filepath.Join(repoRoot, ".agents", "skills", name)) + folderDigest, err := SkillFolderHash(t.Context(), filepath.Join(repoRoot, ".agents", "skills", name)) if err != nil { t.Fatalf("hash upstream-managed skill %q: %v", name, err) } diff --git a/skills/repository.go b/skills/repository.go index 58b3ba3..406464f 100644 --- a/skills/repository.go +++ b/skills/repository.go @@ -2,6 +2,7 @@ package skills import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -64,7 +65,10 @@ type localLockSkill struct { ComputedHash string `json:"computedHash"` } -func CheckRepository(root string) (RepositoryReadiness, error) { +func CheckRepository(ctx context.Context, root string) (RepositoryReadiness, error) { + if err := ctx.Err(); err != nil { + return RepositoryReadiness{}, fmt.Errorf("check repository skill set: %w", err) + } owned := Names() external := Recommended() if err := validateRequiredSkillNames(owned, external); err != nil { @@ -80,14 +84,17 @@ func CheckRepository(root string) (RepositoryReadiness, error) { ExternalRequired: len(external), }, repositoryReadinessError(RepositoryOwnershipOwned, "read embedded skill bundle", "", err) } - return checkRepository(root, owned, external, files) + return checkRepository(ctx, root, owned, external, files) } -func checkRepository(root string, owned []string, external []string, files []File) (readiness RepositoryReadiness, returnErr error) { +func checkRepository(ctx context.Context, root string, owned []string, external []string, files []File) (readiness RepositoryReadiness, returnErr error) { readiness = RepositoryReadiness{ OwnedRequired: len(owned), ExternalRequired: len(external), } + if err := ctx.Err(); err != nil { + return readiness, repositoryReadinessError("", "inspect repository skill set", root, err) + } if err := validateRequiredSkillNames(owned, external); err != nil { return readiness, err } @@ -96,6 +103,9 @@ func checkRepository(root string, owned []string, external []string, files []Fil return readiness, repositoryReadinessError("", "validate repository root", "", errors.New("path is empty")) } + if err := ctx.Err(); err != nil { + return readiness, repositoryReadinessError("", "open repository root", root, err) + } repositoryRoot, err := os.OpenRoot(root) if err != nil { return readiness, repositoryReadinessError("", "open repository root", root, err) @@ -117,7 +127,7 @@ func checkRepository(root string, owned []string, external []string, files []Fil } const lockPath = "skills-lock.json" lockDisplayPath := repositoryDisplayPath(root, lockPath) - lock, err := readLocalSkillsLock(repositoryRoot, lockPath, lockDisplayPath) + lock, err := readLocalSkillsLock(ctx, repositoryRoot, lockPath, lockDisplayPath) if err != nil { return readiness, err } @@ -127,7 +137,7 @@ func checkRepository(root string, owned []string, external []string, files []Fil } for _, sharedDirectory := range []string{".agents", ".agents/skills"} { - missing, err := inspectSharedSkillDirectory(repositoryRoot, sharedDirectory, repositoryDisplayPath(root, sharedDirectory)) + missing, err := inspectSharedSkillDirectory(ctx, repositoryRoot, sharedDirectory, repositoryDisplayPath(root, sharedDirectory)) if err != nil { return readiness, err } @@ -141,8 +151,12 @@ func checkRepository(root string, owned []string, external []string, files []Fil const skillsRoot = ".agents/skills" for _, name := range owned { + if err := ctx.Err(); err != nil { + return readiness, repositoryReadinessError(RepositoryOwnershipOwned, "inspect Roundfix-owned skill", repositoryDisplayPath(root, skillsRoot), err) + } skillRoot := path.Join(skillsRoot, name) missing, outdated, err := compareOwnedSkill( + ctx, repositoryRoot, skillRoot, repositoryDisplayPath(root, skillRoot), @@ -158,6 +172,9 @@ func checkRepository(root string, owned []string, external []string, files []Fil } } for _, name := range external { + if err := ctx.Err(); err != nil { + return readiness, repositoryReadinessError(RepositoryOwnershipExternal, "inspect external skill", repositoryDisplayPath(root, skillsRoot), err) + } skillRoot := path.Join(skillsRoot, name) skillDisplayPath := repositoryDisplayPath(root, skillRoot) info, err := repositoryRoot.Lstat(skillRoot) @@ -189,7 +206,7 @@ func checkRepository(root string, owned []string, external []string, files []Fil errors.New("skill root is not a directory"), ) } - hash, err := skillFolderHash(repositoryRoot.FS(), skillRoot, skillDisplayPath) + hash, err := skillFolderHash(ctx, repositoryRoot.FS(), skillRoot, skillDisplayPath) if err != nil { return readiness, repositoryReadinessError( RepositoryOwnershipExternal, @@ -207,7 +224,10 @@ func checkRepository(root string, owned []string, external []string, files []Fil return readiness, nil } -func inspectSharedSkillDirectory(repositoryRoot *os.Root, relative string, display string) (bool, error) { +func inspectSharedSkillDirectory(ctx context.Context, repositoryRoot *os.Root, relative string, display string) (bool, error) { + if err := ctx.Err(); err != nil { + return false, repositoryReadinessError("", "inspect repository skill directory", display, err) + } info, err := repositoryRoot.Lstat(relative) if errors.Is(err, fs.ErrNotExist) { return true, nil @@ -234,8 +254,8 @@ func inspectSharedSkillDirectory(repositoryRoot *os.Root, relative string, displ return false, nil } -func repositoryDisplayPath(root string, relative string) string { - return filepath.Join(root, filepath.FromSlash(relative)) +func repositoryDisplayPath(root string, relativePath string) string { + return filepath.Join(root, filepath.FromSlash(relativePath)) } func sortRepositoryReadiness(readiness *RepositoryReadiness) { @@ -370,16 +390,24 @@ func expectedOwnedFiles(owned []string, files []File) (map[string]map[string][]b return expected, nil } -func safeRelativeArtifactPath(path string) bool { - if path == "" || strings.Contains(path, `\`) || filepath.IsAbs(path) { +func safeRelativeArtifactPath(relativePath string) bool { + if relativePath == "" || strings.Contains(relativePath, `\`) || filepath.IsAbs(relativePath) { return false } - cleaned := filepath.ToSlash(filepath.Clean(filepath.FromSlash(path))) - return cleaned == path && cleaned != "." && cleaned != ".." && !strings.HasPrefix(cleaned, "../") + cleaned := filepath.ToSlash(filepath.Clean(filepath.FromSlash(relativePath))) + return cleaned == relativePath && cleaned != "." && cleaned != ".." && !strings.HasPrefix(cleaned, "../") } -func readLocalSkillsLock(repositoryRoot *os.Root, path string, displayPath string) (localSkillsLock, error) { - info, err := repositoryRoot.Lstat(path) +func readLocalSkillsLock(ctx context.Context, repositoryRoot *os.Root, lockPath string, displayPath string) (localSkillsLock, error) { + if err := ctx.Err(); err != nil { + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "inspect skills lock", + displayPath, + err, + ) + } + info, err := repositoryRoot.Lstat(lockPath) if err != nil { return localSkillsLock{}, repositoryReadinessError( RepositoryOwnershipExternal, @@ -390,7 +418,7 @@ func readLocalSkillsLock(repositoryRoot *os.Root, path string, displayPath strin } if info.Mode()&fs.ModeSymlink != 0 { return localSkillsLock{}, repositoryReadinessError( - "", + RepositoryOwnershipExternal, "inspect skills lock", displayPath, errors.New("symbolic links are not supported"), @@ -405,7 +433,15 @@ func readLocalSkillsLock(repositoryRoot *os.Root, path string, displayPath strin ) } - data, err := repositoryRoot.ReadFile(path) + if err := ctx.Err(); err != nil { + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "read skills lock", + displayPath, + err, + ) + } + data, err := repositoryRoot.ReadFile(lockPath) if err != nil { return localSkillsLock{}, repositoryReadinessError( RepositoryOwnershipExternal, @@ -414,6 +450,14 @@ func readLocalSkillsLock(repositoryRoot *os.Root, path string, displayPath strin err, ) } + if err := ctx.Err(); err != nil { + return localSkillsLock{}, repositoryReadinessError( + RepositoryOwnershipExternal, + "read skills lock", + displayPath, + err, + ) + } var lock localSkillsLock decoder := json.NewDecoder(bytes.NewReader(data)) if err := decoder.Decode(&lock); err != nil { @@ -447,7 +491,7 @@ func readLocalSkillsLock(repositoryRoot *os.Root, path string, displayPath strin return lock, nil } -func requiredExternalHashes(path string, lock localSkillsLock, external []string) (map[string]string, error) { +func requiredExternalHashes(lockPath string, lock localSkillsLock, external []string) (map[string]string, error) { hashes := make(map[string]string, len(external)) for _, name := range external { entry, exists := lock.Skills[name] @@ -455,7 +499,7 @@ func requiredExternalHashes(path string, lock localSkillsLock, external []string return nil, repositoryReadinessError( RepositoryOwnershipExternal, "validate required skills lock entry", - path, + lockPath, fmt.Errorf("external skill %q is missing", name), ) } @@ -463,7 +507,7 @@ func requiredExternalHashes(path string, lock localSkillsLock, external []string return nil, repositoryReadinessError( RepositoryOwnershipExternal, "validate required skills lock hash", - path, + lockPath, fmt.Errorf("external skill %q computedHash must be 64 lowercase hexadecimal characters", name), ) } @@ -487,11 +531,20 @@ func lowerHexSHA256(value string) bool { const sha256HexLength = 64 func compareOwnedSkill( + ctx context.Context, repositoryRoot *os.Root, root string, displayRoot string, expected map[string][]byte, ) (missing bool, outdated bool, err error) { + if err := ctx.Err(); err != nil { + return false, false, repositoryReadinessError( + RepositoryOwnershipOwned, + "inspect Roundfix-owned skill", + displayRoot, + err, + ) + } info, lstatErr := repositoryRoot.Lstat(root) if errors.Is(lstatErr, fs.ErrNotExist) { return true, false, nil @@ -511,6 +564,9 @@ func compareOwnedSkill( found := make(map[string]struct{}, len(expected)) tree := repositoryRoot.FS() walkErr := fs.WalkDir(tree, root, func(entryPath string, entry fs.DirEntry, walkErr error) error { + if err := ctx.Err(); err != nil { + return repositoryReadinessError(RepositoryOwnershipOwned, "walk Roundfix-owned skill artifact", displayRoot, err) + } relative := strings.TrimPrefix(entryPath, root+"/") if entryPath == root { relative = "." @@ -543,10 +599,16 @@ func compareOwnedSkill( outdated = true return nil } + if err := ctx.Err(); err != nil { + return repositoryReadinessError(RepositoryOwnershipOwned, "read Roundfix-owned skill artifact", fullPath, err) + } data, readErr := fs.ReadFile(tree, entryPath) if readErr != nil { return repositoryReadinessError(RepositoryOwnershipOwned, "read Roundfix-owned skill artifact", fullPath, readErr) } + if err := ctx.Err(); err != nil { + return repositoryReadinessError(RepositoryOwnershipOwned, "read Roundfix-owned skill artifact", fullPath, err) + } found[relative] = struct{}{} if !bytes.Equal(data, want) { outdated = true diff --git a/skills/repository_test.go b/skills/repository_test.go index 02b4083..dd571aa 100644 --- a/skills/repository_test.go +++ b/skills/repository_test.go @@ -1,6 +1,7 @@ package skills import ( + "context" "encoding/json" "errors" "io/fs" @@ -11,6 +12,7 @@ import ( "sort" "strings" "testing" + "time" ) func TestCheckRepositoryMatchesRealRepository(t *testing.T) { @@ -18,7 +20,7 @@ func TestCheckRepositoryMatchesRealRepository(t *testing.T) { if err != nil { t.Fatalf("resolve real repository root: %v", err) } - got, err := CheckRepository(root) + got, err := CheckRepository(t.Context(), root) if err != nil { t.Fatalf("check real repository: %v", err) } @@ -27,11 +29,61 @@ func TestCheckRepositoryMatchesRealRepository(t *testing.T) { } } +func TestCheckRepositoryHonorsPreCanceledContext(t *testing.T) { + tests := []struct { + name string + context func(*testing.T) context.Context + want error + }{ + { + name: "canceled", + context: func(t *testing.T) context.Context { + t.Helper() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + return ctx + }, + want: context.Canceled, + }, + { + name: "deadline exceeded", + context: func(t *testing.T) context.Context { + t.Helper() + ctx, cancel := context.WithDeadline(t.Context(), time.Unix(1, 0)) + t.Cleanup(cancel) + return ctx + }, + want: context.DeadlineExceeded, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := test.context(t) + root := filepath.Join(t.TempDir(), "must-not-be-inspected") + + _, err := CheckRepository(ctx, root) + if !errors.Is(err, test.want) { + t.Fatalf("CheckRepository() error = %v, want errors.Is(_, %v)", err, test.want) + } + + _, err = checkRepository(ctx, root, nil, nil, nil) + if !errors.Is(err, test.want) { + t.Fatalf("checkRepository() error = %v, want errors.Is(_, %v)", err, test.want) + } + var repositoryErr *RepositoryReadinessError + if !errors.As(err, &repositoryErr) { + t.Fatalf("checkRepository() error type = %T, want RepositoryReadinessError", err) + } + }) + } +} + func TestCheckRepositoryReportsReadyRequiredSetWithoutMutation(t *testing.T) { root := writeReadyRepositoryFixture(t) before := snapshotRepositoryFixture(t, root) - got, err := CheckRepository(root) + got, err := CheckRepository(t.Context(), root) if err != nil { t.Fatalf("check ready repository: %v", err) } @@ -122,7 +174,7 @@ func TestCheckRepositoryClassifiesMissingAndOutdatedSkills(t *testing.T) { root := writeReadyRepositoryFixture(t) test.mutate(t, root) - got, err := CheckRepository(root) + got, err := CheckRepository(t.Context(), root) if err != nil { t.Fatalf("check repository: %v", err) } @@ -156,7 +208,7 @@ func TestCheckRepositoryClassifiesMissingSharedSkillDirectories(t *testing.T) { t.Fatalf("remove shared skill directory %q: %v", test.path, err) } - got, err := CheckRepository(root) + got, err := CheckRepository(t.Context(), root) if err != nil { t.Fatalf("check repository: %v", err) } @@ -177,7 +229,7 @@ func TestCheckRepositoryIgnoresUnrelatedSkillsAndLockEntries(t *testing.T) { lock.Skills["../ignored-unsafe-entry"] = repositoryLockSkillFixture{ComputedHash: "not-a-hash"} writeRepositoryLockFixture(t, root, lock) - got, err := CheckRepository(root) + got, err := CheckRepository(t.Context(), root) if err != nil { t.Fatalf("check repository with unrelated entries: %v", err) } @@ -250,7 +302,7 @@ func TestCheckRepositoryRejectsMalformedLockAndUnsafeRequiredNames(t *testing.T) root := writeReadyRepositoryFixture(t) test.write(t, root) - _, err := CheckRepository(root) + _, err := CheckRepository(t.Context(), root) if err == nil { t.Fatal("expected repository lock error") } @@ -270,7 +322,7 @@ func TestCheckRepositoryRejectsMalformedLockAndUnsafeRequiredNames(t *testing.T) root := t.TempDir() outside := filepath.Join(filepath.Dir(root), "outside") writeSkillHashTestFile(t, outside, "marker", "must not be read\n") - _, err := checkRepository(root, []string{"../outside"}, nil, nil) + _, err := checkRepository(t.Context(), root, []string{"../outside"}, nil, nil) if err == nil || !strings.Contains(err.Error(), "unsafe") { t.Fatalf("expected unsafe required name error, got %v", err) } @@ -279,7 +331,7 @@ func TestCheckRepositoryRejectsMalformedLockAndUnsafeRequiredNames(t *testing.T) func TestCheckRepositoryWrapsFilesystemCauses(t *testing.T) { root := filepath.Join(t.TempDir(), "missing") - _, err := CheckRepository(root) + _, err := CheckRepository(t.Context(), root) if err == nil { t.Fatal("expected missing repository root error") } @@ -341,7 +393,7 @@ func TestCheckRepositoryRejectsSymlinkedAuthoritiesBeforeReadingTargets(t *testi t.Skipf("create authority symlink: %v", err) } - _, err := CheckRepository(root) + _, err := CheckRepository(t.Context(), root) if err == nil { t.Fatal("expected symlinked authority to fail readiness") } @@ -352,8 +404,12 @@ func TestCheckRepositoryRejectsSymlinkedAuthoritiesBeforeReadingTargets(t *testi if repositoryErr.Path != authority { t.Fatalf("authority error path = %q, want %q", repositoryErr.Path, authority) } - if repositoryErr.Ownership != "" { - t.Fatalf("authority error ownership = %q, want unclassified", repositoryErr.Ownership) + wantOwnership := RepositoryOwnership("") + if test.authority == "skills-lock.json" { + wantOwnership = RepositoryOwnershipExternal + } + if repositoryErr.Ownership != wantOwnership { + t.Fatalf("authority error ownership = %q, want %q", repositoryErr.Ownership, wantOwnership) } if strings.Contains(repositoryErr.Operation, "decode") { t.Fatalf("authority target was decoded before rejection: %v", err) @@ -381,7 +437,7 @@ func TestCheckRepositoryHandlesNestedLinksSpecialEntriesAndStableOrdering(t *tes t.Fatal(err) } - got, err := CheckRepository(root) + got, err := CheckRepository(t.Context(), root) if err != nil { t.Fatalf("check repository with owned symlink: %v", err) } @@ -396,7 +452,7 @@ func TestCheckRepositoryHandlesNestedLinksSpecialEntriesAndStableOrdering(t *tes if err := os.Symlink(filepath.Join(externalRoot, "skills-lock.json"), externalPath); err != nil { t.Skipf("create external symlink: %v", err) } - _, err = CheckRepository(externalRoot) + _, err = CheckRepository(t.Context(), externalRoot) if err == nil || !strings.Contains(err.Error(), externalPath) { t.Fatalf("expected external symlink error naming %q, got %v", externalPath, err) } @@ -412,7 +468,7 @@ func TestCheckRepositoryHandlesNestedLinksSpecialEntriesAndStableOrdering(t *tes t.Errorf("close special-entry listener: %v", err) } }) - _, err = CheckRepository(specialRoot) + _, err = CheckRepository(t.Context(), specialRoot) if err == nil || !strings.Contains(err.Error(), specialPath) { t.Fatalf("expected external special-entry error naming %q, got %v", specialPath, err) } @@ -446,7 +502,7 @@ func writeReadyRepositoryFixture(t *testing.T) string { for _, name := range Recommended() { skillRoot := filepath.Join(skillsRoot, name) writeSkillHashTestFile(t, skillRoot, "SKILL.md", name+"\n") - hash, err := SkillFolderHash(skillRoot) + hash, err := SkillFolderHash(t.Context(), skillRoot) if err != nil { t.Fatalf("hash external fixture %q: %v", name, err) } diff --git a/skills/skills.go b/skills/skills.go index c9ba0c8..aed7aaa 100644 --- a/skills/skills.go +++ b/skills/skills.go @@ -126,7 +126,10 @@ func Files() ([]File, error) { // SkillFolderHash returns the external skills CLI digest for a local skill // directory without following links or reading excluded dependency metadata. -func SkillFolderHash(root string) (string, error) { +func SkillFolderHash(ctx context.Context, root string) (string, error) { + if err := ctx.Err(); err != nil { + return "", fmt.Errorf("hash skill folder %q: %w", root, err) + } info, err := os.Lstat(root) if err != nil { return "", fmt.Errorf("inspect skill folder %q: %w", root, err) @@ -138,12 +141,18 @@ func SkillFolderHash(root string) (string, error) { return "", fmt.Errorf("inspect skill folder %q: root is not a directory", root) } - return skillFolderHash(os.DirFS(root), ".", root) + return skillFolderHash(ctx, os.DirFS(root), ".", root) } -func skillFolderHash(tree fs.FS, root string, displayRoot string) (string, error) { +func skillFolderHash(ctx context.Context, tree fs.FS, root string, displayRoot string) (string, error) { + if err := ctx.Err(); err != nil { + return "", fmt.Errorf("hash skill folder %q: %w", displayRoot, err) + } var files []skillhash.File if err := fs.WalkDir(tree, root, func(path string, entry fs.DirEntry, walkErr error) error { + if err := ctx.Err(); err != nil { + return fmt.Errorf("walk skill folder %q: %w", displayRoot, err) + } relative := path if path == root { relative = "." @@ -174,10 +183,16 @@ func skillFolderHash(tree fs.FS, root string, displayRoot string) (string, error if !mode.IsRegular() { return fmt.Errorf("inspect skill folder entry %q: special files are not supported", fullPath) } + if err := ctx.Err(); err != nil { + return fmt.Errorf("read skill folder file %q: %w", fullPath, err) + } data, err := fs.ReadFile(tree, path) if err != nil { return fmt.Errorf("read skill folder file %q: %w", fullPath, err) } + if err := ctx.Err(); err != nil { + return fmt.Errorf("read skill folder file %q: %w", fullPath, err) + } files = append(files, skillhash.File{ Path: filepath.ToSlash(relative), Content: data, @@ -186,6 +201,9 @@ func skillFolderHash(tree fs.FS, root string, displayRoot string) (string, error }); err != nil { return "", fmt.Errorf("hash skill folder %q: %w", displayRoot, err) } + if err := ctx.Err(); err != nil { + return "", fmt.Errorf("hash skill folder %q: %w", displayRoot, err) + } return skillhash.Sum(files), nil } diff --git a/skills/skills_test.go b/skills/skills_test.go index 88a4970..d0e821d 100644 --- a/skills/skills_test.go +++ b/skills/skills_test.go @@ -12,6 +12,8 @@ import ( "sort" "strings" "testing" + "testing/fstest" + "time" ) var lockHashCompatibilityFixturePath = filepath.Join( @@ -74,7 +76,7 @@ func TestSkillFolderHashMatchesExternalCompatibilityFixture(t *testing.T) { t.Fatal("lock compatibility fixture must distinguish external CLI locale ordering from Go byte ordering") } - got, err := SkillFolderHash(root) + got, err := SkillFolderHash(t.Context(), root) if err != nil { t.Fatalf("hash compatibility fixture: %v", err) } @@ -86,7 +88,7 @@ func TestSkillFolderHashMatchesExternalCompatibilityFixture(t *testing.T) { func TestSkillFolderHashExcludesMetadataAndDependencyDirectories(t *testing.T) { root := t.TempDir() writeSkillHashTestFile(t, root, "SKILL.md", "fixture\n") - want, err := SkillFolderHash(root) + want, err := SkillFolderHash(t.Context(), root) if err != nil { t.Fatalf("hash baseline skill folder: %v", err) } @@ -99,7 +101,7 @@ func TestSkillFolderHashExcludesMetadataAndDependencyDirectories(t *testing.T) { writeSkillHashTestFile(t, root, path, "ignored\n") } - got, err := SkillFolderHash(root) + got, err := SkillFolderHash(t.Context(), root) if err != nil { t.Fatalf("hash skill folder with excluded directories: %v", err) } @@ -148,7 +150,7 @@ func TestSkillFolderHashRejectsUnsafeFilesystemShapes(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if _, err := SkillFolderHash(test.prepare(t)); err == nil { + if _, err := SkillFolderHash(t.Context(), test.prepare(t)); err == nil { t.Fatal("expected unsafe filesystem shape to be rejected") } }) @@ -157,7 +159,7 @@ func TestSkillFolderHashRejectsUnsafeFilesystemShapes(t *testing.T) { func TestSkillFolderHashWrapsMissingRootError(t *testing.T) { root := filepath.Join(t.TempDir(), "missing") - _, err := SkillFolderHash(root) + _, err := SkillFolderHash(t.Context(), root) if err == nil { t.Fatal("expected missing root error") } @@ -169,6 +171,80 @@ func TestSkillFolderHashWrapsMissingRootError(t *testing.T) { } } +func TestSkillFolderHashHonorsPreCanceledContext(t *testing.T) { + tests := []struct { + name string + context func(*testing.T) context.Context + want error + }{ + { + name: "canceled", + context: func(t *testing.T) context.Context { + t.Helper() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + return ctx + }, + want: context.Canceled, + }, + { + name: "deadline exceeded", + context: func(t *testing.T) context.Context { + t.Helper() + ctx, cancel := context.WithDeadline(t.Context(), time.Unix(1, 0)) + t.Cleanup(cancel) + return ctx + }, + want: context.DeadlineExceeded, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := filepath.Join(t.TempDir(), "must-not-be-inspected") + _, err := SkillFolderHash(test.context(t), root) + if !errors.Is(err, test.want) { + t.Fatalf("SkillFolderHash() error = %v, want errors.Is(_, %v)", err, test.want) + } + }) + } +} + +func TestSkillFolderHashStopsAfterCancellationDuringRead(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + tree := &cancelOnFirstFileOpenFS{ + FS: fstest.MapFS{ + "a.txt": &fstest.MapFile{Data: []byte("first\n")}, + "b.txt": &fstest.MapFile{Data: []byte("second\n")}, + }, + cancel: cancel, + } + + _, err := skillFolderHash(ctx, tree, ".", "fixture") + if !errors.Is(err, context.Canceled) { + t.Fatalf("skillFolderHash() error = %v, want context.Canceled", err) + } + if !reflect.DeepEqual(tree.filesOpened, []string{"a.txt"}) { + t.Fatalf("files opened after cancellation = %v, want only first file", tree.filesOpened) + } +} + +type cancelOnFirstFileOpenFS struct { + fs.FS + cancel context.CancelFunc + filesOpened []string +} + +func (tree *cancelOnFirstFileOpenFS) Open(name string) (fs.File, error) { + if file, exists := tree.FS.(fstest.MapFS)[name]; exists && !file.Mode.IsDir() { + tree.filesOpened = append(tree.filesOpened, name) + if len(tree.filesOpened) == 1 { + tree.cancel() + } + } + return tree.FS.Open(name) +} + func writeSkillHashTestFile(t *testing.T, root string, relative string, content string) { t.Helper() path := filepath.Join(root, filepath.FromSlash(relative)) From 5cdc5bd5a3a08481a5f3d5230f638691ccc1d9ec Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 20:23:51 -0300 Subject: [PATCH 39/48] feat: reconcile Doctor readiness contracts Roundfix-Spec: 0051-doctor-readiness-contract-reconciliation Roundfix-Task: task_04 --- .../task_04.md | 81 +- docs/user-guide/commands.md | 12 +- internal/cli/cli_test.go | 825 ----------------- internal/cli/doctor.go | 21 +- internal/cli/doctor_test.go | 846 ++++++++++++++++++ 5 files changed, 937 insertions(+), 848 deletions(-) create mode 100644 internal/cli/doctor_test.go diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md index 330a999..741740f 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md @@ -1,7 +1,7 @@ --- task: task_04 spec: 0051-doctor-readiness-contract-reconciliation -status: pending +status: completed type: backend complexity: medium --- @@ -36,30 +36,30 @@ guidance make the corrected output contract independently reviewable. ## Subtasks -- [ ] Add the outside-Git profile-workdir regression and checker non-invocation +- [x] Add the outside-Git profile-workdir regression and checker non-invocation assertion. -- [ ] Restore separate profile and repository root resolution. -- [ ] Correct missing-root detail and mixed remediation composition. -- [ ] Move Doctor-specific tests to the source-matched test file without +- [x] Restore separate profile and repository root resolution. +- [x] Correct missing-root detail and mixed remediation composition. +- [x] Move Doctor-specific tests to the source-matched test file without weakening assertions. -- [ ] Update the public Doctor example and explanatory user guidance. -- [ ] Exercise inside-Git, outside-Git, typed ownership, and unclassified error +- [x] Update the public Doctor example and explanatory user guidance. +- [x] Exercise inside-Git, outside-Git, typed ownership, and unclassified error paths through the public runner. ## Acceptance Criteria -- [ ] Outside Git, profile proof receives `os.Getwd()` and the skills checker is +- [x] Outside Git, profile proof receives `os.Getwd()` and the skills checker is not invoked. -- [ ] Inside Git, profile proof and Repository Skill Set inspection both receive +- [x] Inside Git, profile proof and Repository Skill Set inspection both receive the resolved Git root. -- [ ] Missing-root output uses the canonical Repository Skill Set term. -- [ ] Mixed remediation prints one exact `&&`-joined chain and a symlinked lock +- [x] Missing-root output uses the canonical Repository Skill Set term. +- [x] Mixed remediation prints one exact `&&`-joined chain and a symlinked lock prints only external remediation. -- [ ] Codex and other independent checks still run after profile or skills +- [x] Codex and other independent checks still run after profile or skills failure and keep their existing order. -- [ ] Doctor behavior tests live in `internal/cli/doctor_test.go`; CLI-wide +- [x] Doctor behavior tests live in `internal/cli/doctor_test.go`; CLI-wide dispatch/help tests remain in `internal/cli/cli_test.go`. -- [ ] User guidance matches the shipped text contract and no protected skill +- [x] User guidance matches the shipped text contract and no protected skill file changes in this Task. ## Context @@ -90,3 +90,56 @@ guidance make the corrected output contract independently reviewable. Success Metrics. - `_techspec.md` → Doctor working directories; Ownership and remediation; Documentation and skill synchronization; Testing Approach; Build Order 4. + +## Result + +Doctor now resolves separate profile and repository working directories: +profile proof falls back to the process working directory outside Git, while +Repository Skill Set inspection runs only for a non-empty trimmed Git root. +Missing-root detail uses the canonical term, and multiple ownership actions +form one owned-then-external `&&` chain without changing the existing +`"; next: "` output boundary. + +Doctor behavior tests and their helpers now live in +`internal/cli/doctor_test.go`. The CLI-wide suite retains command dispatch and +help registry coverage. The user guide documents the outside-Git behavior and +the fail-closed mixed-remediation example. + +Acceptance criterion evidence: + +1. `TestRunDoctorMissingRepositoryRoot` compares the profile work directory + with the process directory and asserts zero Repository Skill Set checker + calls. +2. `TestRunDoctorRepositorySkillReadiness` passes a whitespace-padded Git root + and asserts both profile proof and Repository Skill Set inspection receive + `/repo/project`. +3. `TestRunDoctorMissingRepositoryRoot` asserts the exact canonical + `Repository Skill Set readiness requires a Git repository` detail and the + existing run-from-Git action. +4. `TestRunDoctorRepositorySkillReadiness` covers exact mixed `&&` composition, + external-only remediation for a symlinked lock error, and conservative + mixed remediation for an unclassified error. +5. `TestRunDoctorContinuesChecksAfterProfileReadinessFailure` and + `TestRunDoctorRepositorySkillReadiness` assert eager calls, line order, + stdout/stderr discipline, and exit codes through the public runner. +6. Source inspection places all `TestRunDoctor*` behavior tests in + `internal/cli/doctor_test.go`; `TestRunCommandHelp` and + `TestProfilesDocumentationContractMatchesPublicGuidance` remain in + `internal/cli/cli_test.go` and passed. +7. The user-guide grep found the shipped `&&` chain, and the protected-skill + diff check was empty. + +Verification: + +- `rtk go test ./internal/cli -run 'Test(RunDoctor|Doctor)'` — passed, 18 tests. +- `rtk go test -race ./internal/cli -run 'Test(RunDoctor|Doctor)'` — passed, + 18 tests. +- `rtk go test ./internal/cli -run 'Test(RunCommandHelp|ProfilesDocumentationContractMatchesPublicGuidance)'` + — passed, 11 tests. +- `rtk grep -n '&& bunx skills experimental_install' docs/user-guide/commands.md` + — passed; matched the public mixed-remediation example. +- `rtk git diff --exit-code -- .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` + — passed with no protected skill changes. +- `rtk git diff --check` — passed. + +Follow-ups: none. diff --git a/docs/user-guide/commands.md b/docs/user-guide/commands.md index 42f763f..ed92fe3 100644 --- a/docs/user-guide/commands.md +++ b/docs/user-guide/commands.md @@ -84,6 +84,10 @@ Doctor has no separate `agent:` or `model:` authority. The aggregate `profiles:` result is the Agent Selection Profile Readiness contract. Repository Skill Set readiness runs after it as an independent check, even when profile proof fails, and appears before `codex:`. +Outside Git, profile proof uses the process working directory while Repository +Skill Set inspection does not run. The `skills:` line reports +`Repository Skill Set readiness requires a Git repository` and keeps the +run-from-Git next action. ```text node: ok @@ -99,11 +103,13 @@ prints one sorted blocking line and makes Doctor exit `1`. Doctor still prints every other readiness result: ```text -skills: failed (missing: handoff; outdated: roundfix; next: roundfix skills install --target project; bunx skills experimental_install && bunx skills update -p -y) +skills: failed (missing: handoff; outdated: roundfix; next: roundfix skills install --target project && bunx skills experimental_install && bunx skills update -p -y) ``` -The first update command restores Roundfix-owned skills from the running -binary. The second updates externally managed skills and their lock state. +For mixed ownership, Doctor joins the Roundfix-owned restore and external +update actions with `&&`, so external remediation runs only after the owned +restore succeeds. A failure owned only by the external lock or skill set prints +only the external action. Doctor never runs either command, never deletes skills, and never updates `skills-lock.json`. The check is offline and read-only: it reads only local embedded artifacts, `.agents/skills`, and `skills-lock.json`. Unrelated extra diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 2c18c01..724eabc 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -33,7 +33,6 @@ import ( roundtui "roundfix/internal/tui" "roundfix/internal/watch" runworktree "roundfix/internal/worktree" - "roundfix/skills" ) func init() { @@ -2273,830 +2272,6 @@ func TestRunRunsListOutsideRepositoryRequiresAll(t *testing.T) { } } -func TestRunDoctorProfileReadinessProvesEffectiveCategoriesAndReportsCounts(t *testing.T) { - tests := []struct { - name string - checker *doctorFakeHealthChecker - wantCode int - wantStdout string - }{ - { - name: "all checks pass", - checker: newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK, Detail: "/home/roundfix/.local/bin/codex accepted"}, - ), - wantCode: exitOK, - wantStdout: "node: ok (v25.6.1 >= " + setupNodeMinimumVersion + ")\n" + - "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + - "adapter: ok (codex-acp)\n" + - "profiles: ok (3 distinct tuples; 10 category references)\n" + - "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + - "codex: ok (/home/roundfix/.local/bin/codex accepted)\n", - }, - { - name: "quarantined codex fails with reinstall action", - checker: newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusFailed, Detail: "/tmp/codex is quarantined", NextAction: codex.ReinstallNextAction}, - ), - wantCode: exitRunFailed, - wantStdout: "node: ok (v25.6.1 >= " + setupNodeMinimumVersion + ")\n" + - "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + - "adapter: ok (codex-acp)\n" + - "profiles: ok (3 distinct tuples; 10 category references)\n" + - "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + - "codex: failed (/tmp/codex is quarantined; next: " + codex.ReinstallNextAction + ")\n", - }, - { - name: "codex not applicable does not fail", - checker: newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusSkipped, Detail: "not-applicable on linux"}, - ), - wantCode: exitOK, - wantStdout: "node: ok (v25.6.1 >= " + setupNodeMinimumVersion + ")\n" + - "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + - "adapter: ok (codex-acp)\n" + - "profiles: ok (3 distinct tuples; 10 category references)\n" + - "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + - "codex: skipped (not-applicable on linux)\n", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - homeDir, repoDir := withCLIWorkspace(t) - runner := &profileReadinessExactRunner{} - withAgentRunner(t, runner) - withDoctorFakeDeps(t, tt.checker) - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := Run([]string{"doctor"}, &stdout, &stderr) - - if code != tt.wantCode { - t.Fatalf("expected exit code %d, got %d", tt.wantCode, code) - } - if got := stdout.String(); got != tt.wantStdout { - t.Fatalf("unexpected stdout:\n got: %q\nwant: %q", got, tt.wantStdout) - } - if stderr.Len() != 0 { - t.Fatalf("expected no stderr, got %q", stderr.String()) - } - if len(runner.exactRequests) != 3 { - t.Fatalf("expected three distinct profile proofs, got %#v", runner.exactRequests) - } - wantModels := []string{"gpt-5.6-sol", "gpt-5.5", "claude-opus-5"} - for index, wantModel := range wantModels { - if request := runner.exactRequests[index]; request.WorkDir != "/repo/project" || request.Runtime.Model != wantModel { - t.Fatalf("profile proof %d = %#v, want model %q in repository", index, request, wantModel) - } - } - if len(tt.checker.agentRequests) != 0 { - t.Fatalf("Doctor must not run the legacy configured Agent probe, got %#v", tt.checker.agentRequests) - } - assertDoctorPathMissing(t, filepath.Join(homeDir, ".acpx")) - assertDoctorPathMissing(t, filepath.Join(homeDir, ".roundfix")) - assertDoctorPathMissing(t, filepath.Join(repoDir, ".roundfixrc.yml")) - }) - } -} - -func TestRunDoctorProfileReadinessReportsLegacyAdapterThroughEffectiveProfile(t *testing.T) { - config := roundconfig.Builtin() - config.Defaults.Agent = "codex" - config.Runtimes.Codex.Model = "legacy-model-default" - proofs, err := buildProfileProofReports(config, roundconfig.RequiredWorkCategories()) - if err != nil { - t.Fatalf("build profile proof reports: %v", err) - } - legacy := &agent.AdapterLineageError{ - Command: "codex-acp", - Package: "@zed-industries/codex-acp", - Version: "0.16.0", - } - applyProfileProofFailure(&proofs[0], legacy) - readiness := profileProofResult{Proofs: proofs, Err: profileProofError{ - Selection: proofs[0].Selection, - References: proofs[0].References, - Classification: proofs[0].Classification, - NextAction: proofs[0].NextAction, - Err: legacy, - }} - checker := newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK, Detail: "/home/roundfix/.local/bin/codex accepted"}, - ) - checker.adapter = CheckResult{ - Name: HealthCheckAdapter, - Status: CheckStatusOK, - Detail: "command=\"codex-acp\"; package=@zed-industries/codex-acp; version=0.16.0", - } - withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ - Config: config, - GitRoot: "/repo/project", - HomeDir: "/home/roundfix-test", - }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { - return readiness - }) - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := Run([]string{"doctor"}, &stdout, &stderr) - - if code != exitRunFailed { - t.Fatalf("expected doctor adapter failure exit %d, got %d", exitRunFailed, code) - } - for _, want := range []string{ - "adapter: ok (command=\"codex-acp\"; package=@zed-industries/codex-acp; version=0.16.0)", - "profiles: failed", - `runtime="codex", model="gpt-5.6-sol", reasoning_effort="high"`, - "affected categories: general preferred source=built-in, backend preferred source=built-in, frontend fallback[1] source=built-in, qa preferred source=built-in, review preferred source=built-in", - "classification: adapter_lineage_unknown", - "adapter evidence: command=\"codex-acp\", version=\"0.16.0\"", - "next: run `" + agent.CodexAdapterInstallCommand() + "`", - } { - if !strings.Contains(stdout.String(), want) { - t.Fatalf("expected stdout to contain %q, got %q", want, stdout.String()) - } - } - if strings.Contains(stdout.String(), "legacy-model-default") || strings.Contains(stdout.String(), "model:") || strings.Contains(stdout.String(), "agent:") { - t.Fatalf("Doctor reported legacy configured-runtime readiness: %q", stdout.String()) - } - if len(checker.adapterRuntimes) != 1 || checker.adapterRuntimes[0].Model != "gpt-5.6-sol" || checker.adapterRuntimes[0].ReasoningEffort != "high" { - t.Fatalf("adapter check did not use the effective general profile: %#v", checker.adapterRuntimes) - } - if stderr.Len() != 0 { - t.Fatalf("expected no stderr, got %q", stderr.String()) - } -} - -func TestRunDoctorProfileReadinessMatchesProfilesValidateFailureEvidence(t *testing.T) { - _, repoDir := withCLIWorkspace(t) - mustWrite(t, filepath.Join(repoDir, ".roundfixrc.yml"), ` -profiles: - backend: - preferred: - runtime: codex - model: broken-backend - reasoning_effort: high - fallbacks: - - runtime: claude - model: backup-backend - reasoning_effort: medium -`) - failure := &agent.SelectionUnsupportedError{ - Kind: agent.SelectionReasoningControlNotAdvertised, - Runtime: "codex", - Model: "broken-backend", - ReasoningEffort: "high", - AdvertisedModels: []string{"broken-backend", "gpt-5.5"}, - AdvertisedReasoning: []string{"low", "medium"}, - } - runner := &profileReadinessExactRunner{prove: func(req agent.ProbeRequest) (agent.SelectionProof, error) { - if req.Runtime.Model == "broken-backend" { - return agent.SelectionProof{}, failure - } - return agent.SelectionProof{}, nil - }} - withAgentRunner(t, runner) - var validateStdout bytes.Buffer - var validateStderr bytes.Buffer - validateCode := Run([]string{"profiles", "validate", "--category", "backend", "--json"}, &validateStdout, &validateStderr) - if validateCode != exitPreflight { - t.Fatalf("profiles validate exit = %d, want %d", validateCode, exitPreflight) - } - - checker := newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK, Detail: "/home/roundfix/.local/bin/codex accepted"}, - ) - withDoctorLiveDeps(t, checker) - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := Run([]string{"doctor"}, &stdout, &stderr) - - if code != exitRunFailed { - t.Fatalf("expected doctor selection failure exit %d, got %d", exitRunFailed, code) - } - for _, evidence := range []struct { - doctor string - validate string - }{ - {`runtime="codex", model="broken-backend", reasoning_effort="high"`, `runtime "codex", model "broken-backend", reasoning_effort "high"`}, - {"affected categories: backend preferred source=project", "affected categories: backend preferred source=project"}, - {"classification: reasoning_control_not_advertised", "classification: reasoning_control_not_advertised"}, - {"advertised_models=broken-backend,gpt-5.5", "advertised Agent Models: broken-backend, gpt-5.5"}, - {"advertised_reasoning=low,medium", "advertised reasoning efforts: low, medium"}, - {"roundfix profiles configure --scope user|project", "roundfix profiles configure --scope user|project"}, - } { - if !strings.Contains(stdout.String(), evidence.doctor) { - t.Fatalf("Doctor output missing shared failure evidence %q: %q", evidence.doctor, stdout.String()) - } - if !strings.Contains(validateStderr.String(), evidence.validate) { - t.Fatalf("profiles validate output missing shared failure evidence %q: %q", evidence.validate, validateStderr.String()) - } - } - if stderr.Len() != 0 { - t.Fatalf("expected no stderr, got %q", stderr.String()) - } -} - -func TestRunDoctorContinuesChecksAfterProfileReadinessFailure(t *testing.T) { - checker := newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK, Detail: "/home/roundfix/.local/bin/codex accepted"}, - ) - proofCalls := 0 - withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ - Config: roundconfig.Builtin(), - GitRoot: "/repo/project", - HomeDir: "/home/roundfix-test", - }, func(_ context.Context, _ roundconfig.Config, categories []roundconfig.WorkCategory, workDir string) profileProofResult { - proofCalls++ - if got := formatWorkCategories(categories); got != "general, backend, frontend, qa, review" || workDir != "/repo/project" { - t.Fatalf("profile readiness input categories=%q workDir=%q", got, workDir) - } - return profileProofResult{Err: errors.New("profile proof unavailable")} - }) - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := Run([]string{"doctor"}, &stdout, &stderr) - - if code != exitRunFailed { - t.Fatalf("expected rejected model exit %d, got %d", exitRunFailed, code) - } - if proofCalls != 1 || checker.nodeCalls != 1 || checker.acpxCalls != 1 || checker.adapterCalls != 1 || checker.codexCalls != 1 { - t.Fatalf("independent check calls profile=%d node=%d acpx=%d adapter=%d codex=%d", proofCalls, checker.nodeCalls, checker.acpxCalls, checker.adapterCalls, checker.codexCalls) - } - output := stdout.String() - for _, want := range []string{"node: ok", "acpx: ok", "adapter: ok", "profiles: failed (profile proof unavailable)", "codex: ok"} { - if !strings.Contains(output, want) { - t.Fatalf("Doctor output missing %q after profile failure: %q", want, output) - } - } - if strings.Index(output, "profiles:") > strings.Index(output, "codex:") { - t.Fatalf("profile readiness must precede Codex so Repository Skill Set readiness can be inserted after it: %q", output) - } - if stderr.Len() != 0 { - t.Fatalf("expected no stderr, got %q", stderr.String()) - } -} - -func TestRunDoctorRepositorySkillReadiness(t *testing.T) { - ownedCommand := "roundfix skills install --target project" - externalCommand := "bunx skills experimental_install && bunx skills update -p -y" - tests := []struct { - name string - readiness skills.RepositoryReadiness - err error - wantCode int - wantLine string - }{ - { - name: "ready", - readiness: skills.RepositoryReadiness{ - OwnedRequired: 14, - ExternalRequired: 25, - }, - wantCode: exitOK, - wantLine: "skills: ok (39 required: 14 Roundfix-owned, 25 external)", - }, - { - name: "owned failure", - readiness: skills.RepositoryReadiness{ - OwnedRequired: 14, - MissingOwned: []string{"write-prd"}, - OutdatedOwned: []string{"roundfix"}, - }, - wantCode: exitRunFailed, - wantLine: "skills: failed (missing: write-prd; outdated: roundfix; next: " + - ownedCommand + ")", - }, - { - name: "external failure", - readiness: skills.RepositoryReadiness{ - ExternalRequired: 25, - MissingExternal: []string{"testing-boss"}, - OutdatedExternal: []string{"agentic-cli-design"}, - }, - wantCode: exitRunFailed, - wantLine: "skills: failed (missing: testing-boss; outdated: agentic-cli-design; next: " + - externalCommand + ")", - }, - { - name: "mixed failure is sorted with ordered remediation", - readiness: skills.RepositoryReadiness{ - OwnedRequired: 14, - ExternalRequired: 25, - MissingOwned: []string{"write-prd"}, - MissingExternal: []string{"agentic-cli-design"}, - OutdatedOwned: []string{"roundfix"}, - OutdatedExternal: []string{"testing-boss"}, - }, - wantCode: exitRunFailed, - wantLine: "skills: failed (missing: agentic-cli-design, write-prd; outdated: roundfix, testing-boss; next: " + - ownedCommand + "; " + externalCommand + ")", - }, - { - name: "external checker error", - err: &skills.RepositoryReadinessError{ - Ownership: skills.RepositoryOwnershipExternal, - Operation: "decode skills lock", - Path: "/repo/project/skills-lock.json", - Err: errors.New("invalid character"), - }, - wantCode: exitRunFailed, - wantLine: "skills: failed (decode skills lock \"/repo/project/skills-lock.json\": invalid character; next: " + - externalCommand + ")", - }, - { - name: "unclassified checker error", - err: errors.New("repository skill check failed"), - wantCode: exitRunFailed, - wantLine: "skills: failed (repository skill check failed; next: " + - ownedCommand + "; " + externalCommand + ")", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var calls []string - recordCall := func(name string) { - calls = append(calls, name) - } - checker := newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, - ) - checker.recordCall = recordCall - withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ - Config: roundconfig.Builtin(), - GitRoot: "/repo/project", - }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { - recordCall(HealthCheckProfiles) - return profileProofResult{} - }) - skillCalls := 0 - doctorDeps.checkSkills = func(_ context.Context, root string) (skills.RepositoryReadiness, error) { - skillCalls++ - recordCall(HealthCheckSkills) - if root != "/repo/project" { - t.Fatalf("skill readiness root = %q, want /repo/project", root) - } - return test.readiness, test.err - } - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := Run([]string{"doctor"}, &stdout, &stderr) - - if code != test.wantCode { - t.Fatalf("exit code = %d, want %d; stderr=%q", code, test.wantCode, stderr.String()) - } - lines := strings.Split(strings.TrimSpace(stdout.String()), "\n") - if len(lines) != 6 || lines[4] != test.wantLine { - t.Fatalf("unexpected Doctor output lines:\n%q\nwant skills line %q at index 4", lines, test.wantLine) - } - if skillCalls != 1 || checker.nodeCalls != 1 || checker.acpxCalls != 1 || checker.adapterCalls != 1 || checker.codexCalls != 1 { - t.Fatalf("independent check calls skills=%d node=%d acpx=%d adapter=%d codex=%d", - skillCalls, checker.nodeCalls, checker.acpxCalls, checker.adapterCalls, checker.codexCalls) - } - wantCalls := []string{ - HealthCheckNode, - HealthCheckACPX, - HealthCheckAdapter, - HealthCheckProfiles, - HealthCheckSkills, - HealthCheckCodex, - } - if !reflect.DeepEqual(calls, wantCalls) { - t.Fatalf("check order = %v, want %v", calls, wantCalls) - } - if stderr.Len() != 0 { - t.Fatalf("expected no stderr, got %q", stderr.String()) - } - }) - } -} - -func TestRunDoctorPassesCommandContextToRepositorySkillReadiness(t *testing.T) { - type contextKey struct{} - const marker = "doctor-command" - - checker := newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, - ) - withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ - Config: roundconfig.Builtin(), - GitRoot: "/repo/project", - }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { - return profileProofResult{} - }) - doctorDeps.checkSkills = func(ctx context.Context, root string) (skills.RepositoryReadiness, error) { - if got := ctx.Value(contextKey{}); got != marker { - t.Fatalf("repository checker context marker = %v, want %q", got, marker) - } - if root != "/repo/project" { - t.Fatalf("repository checker root = %q, want /repo/project", root) - } - return skills.RepositoryReadiness{ - OwnedRequired: 14, - ExternalRequired: 25, - }, nil - } - commandContext := context.WithValue(t.Context(), contextKey{}, marker) - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := runDoctorCommand(commandContext, nil, &stdout, &stderr) - - if code != exitOK { - t.Fatalf("exit code = %d, want %d; stdout=%q stderr=%q", code, exitOK, stdout.String(), stderr.String()) - } -} - -func TestRunDoctorMissingRepositoryRoot(t *testing.T) { - processDir := t.TempDir() - t.Chdir(processDir) - var calls []string - recordCall := func(name string) { - calls = append(calls, name) - } - checker := newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, - ) - checker.recordCall = recordCall - withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ - Config: roundconfig.Builtin(), - }, func(_ context.Context, _ roundconfig.Config, _ []roundconfig.WorkCategory, workDir string) profileProofResult { - recordCall(HealthCheckProfiles) - if workDir != "" { - t.Fatalf("profile readiness work directory = %q, want empty loaded Git root", workDir) - } - return profileProofResult{} - }) - skillCalls := 0 - doctorDeps.checkSkills = func(_ context.Context, root string) (skills.RepositoryReadiness, error) { - skillCalls++ - return skills.RepositoryReadiness{}, fmt.Errorf("unexpected repository check for %q", root) - } - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := Run([]string{"doctor"}, &stdout, &stderr) - - if code != exitRunFailed { - t.Fatalf("exit code = %d, want %d", code, exitRunFailed) - } - wantStdout := "node: ok\n" + - "acpx: ok\n" + - "adapter: ok (codex-acp)\n" + - "profiles: ok (0 distinct tuples; 0 category references)\n" + - "skills: failed (repository skill check requires a Git repository; next: run roundfix doctor from a Git repository)\n" + - "codex: ok\n" - if got := stdout.String(); got != wantStdout { - t.Fatalf("unexpected stdout:\n got: %q\nwant: %q", got, wantStdout) - } - if skillCalls != 0 { - t.Fatalf("repository checker calls = %d, want 0", skillCalls) - } - wantCalls := []string{ - HealthCheckNode, - HealthCheckACPX, - HealthCheckAdapter, - HealthCheckProfiles, - HealthCheckCodex, - } - if !reflect.DeepEqual(calls, wantCalls) { - t.Fatalf("independent check order = %v, want %v", calls, wantCalls) - } - if stderr.Len() != 0 { - t.Fatalf("expected no stderr, got %q", stderr.String()) - } -} - -func TestRunDoctorRealRepositoryCheckDoesNotMutateState(t *testing.T) { - homeDir := t.TempDir() - repoDir := t.TempDir() - t.Setenv("HOME", homeDir) - t.Chdir(repoDir) - mustMkdir(t, filepath.Join(repoDir, ".git")) - writeDoctorReadyRepositoryFixture(t, repoDir) - - userConfigPath := filepath.Join(homeDir, ".roundfix", "config.yml") - mustMkdir(t, filepath.Dir(userConfigPath)) - mustWrite(t, userConfigPath, roundconfig.DefaultConfigYAML()) - mustWrite(t, filepath.Join(homeDir, ".roundfix", "state-marker"), "user state\n") - mustWrite(t, filepath.Join(repoDir, ".roundfixrc.yml"), roundconfig.DefaultConfigYAML()) - mustMkdir(t, filepath.Join(repoDir, ".roundfix")) - mustWrite(t, filepath.Join(repoDir, ".roundfix", "run-marker"), "repository state\n") - - snapshotRoots := map[string]string{ - "repository": repoDir, - "roundfix home": filepath.Join(homeDir, ".roundfix"), - "repository data": filepath.Join(repoDir, ".roundfix"), - "skill state": filepath.Join(repoDir, ".agents", "skills"), - } - beforeRoots := make(map[string]map[string]doctorPathSnapshot, len(snapshotRoots)) - for name, root := range snapshotRoots { - beforeRoots[name] = snapshotDoctorPath(t, root) - } - beforeUserConfig := mustReadBytes(t, userConfigPath) - lockPath := filepath.Join(repoDir, "skills-lock.json") - beforeLock := mustReadBytes(t, lockPath) - - checker := newDoctorFakeHealthChecker( - CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, - CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, - CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, - ) - withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ - Config: roundconfig.Builtin(), - GitRoot: repoDir, - HomeDir: homeDir, - UserConfigPath: userConfigPath, - ProjectConfigPath: filepath.Join(repoDir, ".roundfixrc.yml"), - }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { - return profileProofResult{} - }) - doctorDeps.checkSkills = skills.CheckRepository - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := Run([]string{"doctor"}, &stdout, &stderr) - - if code != exitOK { - t.Fatalf("exit code = %d, want %d; stdout=%q stderr=%q", code, exitOK, stdout.String(), stderr.String()) - } - wantStdout := "node: ok\n" + - "acpx: ok\n" + - "adapter: ok (codex-acp)\n" + - "profiles: ok (0 distinct tuples; 0 category references)\n" + - "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + - "codex: ok\n" - if got := stdout.String(); got != wantStdout { - t.Fatalf("unexpected stdout:\n got: %q\nwant: %q", got, wantStdout) - } - if stderr.Len() != 0 { - t.Fatalf("expected no stderr, got %q", stderr.String()) - } - for name, root := range snapshotRoots { - if after := snapshotDoctorPath(t, root); !reflect.DeepEqual(after, beforeRoots[name]) { - t.Fatalf("Doctor mutated %s:\nbefore=%#v\nafter=%#v", name, beforeRoots[name], after) - } - } - if after := mustReadBytes(t, userConfigPath); !bytes.Equal(after, beforeUserConfig) { - t.Fatalf("Doctor mutated User Config:\nbefore=%q\nafter=%q", beforeUserConfig, after) - } - if after := mustReadBytes(t, lockPath); !bytes.Equal(after, beforeLock) { - t.Fatalf("Doctor mutated skills lock:\nbefore=%q\nafter=%q", beforeLock, after) - } -} - -func TestRunDoctorRejectsArguments(t *testing.T) { - var stdout bytes.Buffer - var stderr bytes.Buffer - - code := Run([]string{"doctor", "extra"}, &stdout, &stderr) - - if code != exitPreflight { - t.Fatalf("expected exit code %d, got %d", exitPreflight, code) - } - if stdout.Len() != 0 { - t.Fatalf("expected no stdout, got %q", stdout.String()) - } - if !strings.Contains(stderr.String(), "unexpected argument \"extra\"") { - t.Fatalf("expected argument diagnostic, got %q", stderr.String()) - } -} - -func newDoctorFakeHealthChecker(node, acpx, codex CheckResult) *doctorFakeHealthChecker { - return &doctorFakeHealthChecker{ - node: node, - acpx: acpx, - adapter: CheckResult{Name: HealthCheckAdapter, Status: CheckStatusOK, Detail: "codex-acp"}, - codex: codex, - } -} - -type doctorFakeHealthChecker struct { - node CheckResult - acpx CheckResult - adapter CheckResult - agentResult CheckResult - codex CheckResult - agentRequests []agent.ProbeRequest - adapterRuntimes []agent.RuntimeSpec - nodeCalls int - acpxCalls int - adapterCalls int - codexCalls int - recordCall func(string) -} - -func (checker *doctorFakeHealthChecker) Node(context.Context) CheckResult { - checker.nodeCalls++ - checker.record(HealthCheckNode) - return checker.node -} - -func (checker *doctorFakeHealthChecker) ACPX(context.Context) CheckResult { - checker.acpxCalls++ - checker.record(HealthCheckACPX) - return checker.acpx -} - -func (checker *doctorFakeHealthChecker) Adapter(_ context.Context, runtime agent.RuntimeSpec) CheckResult { - checker.adapterCalls++ - checker.adapterRuntimes = append(checker.adapterRuntimes, runtime) - checker.record(HealthCheckAdapter) - return checker.adapter -} - -func (checker *doctorFakeHealthChecker) Agent(_ context.Context, req agent.ProbeRequest) CheckResult { - checker.agentRequests = append(checker.agentRequests, req) - return checker.agentResult -} - -func (checker *doctorFakeHealthChecker) Codex(context.Context) CheckResult { - checker.codexCalls++ - checker.record(HealthCheckCodex) - return checker.codex -} - -func (checker *doctorFakeHealthChecker) record(name string) { - if checker.recordCall != nil { - checker.recordCall(name) - } -} - -type doctorRepositoryLockFixture struct { - Version int `json:"version"` - Skills map[string]doctorRepositoryLockSkillFixture `json:"skills"` -} - -type doctorRepositoryLockSkillFixture struct { - ComputedHash string `json:"computedHash"` -} - -func writeDoctorReadyRepositoryFixture(t *testing.T, root string) { - t.Helper() - skillsRoot := filepath.Join(root, ".agents", "skills") - files, err := skills.Files() - if err != nil { - t.Fatalf("read embedded Repository Skill Set: %v", err) - } - for _, file := range files { - path := filepath.Join(skillsRoot, filepath.FromSlash(file.Path)) - mustMkdir(t, filepath.Dir(path)) - if err := os.WriteFile(path, file.Data, 0o644); err != nil { - t.Fatalf("write embedded skill artifact %q: %v", path, err) - } - } - - lock := doctorRepositoryLockFixture{ - Version: 1, - Skills: make(map[string]doctorRepositoryLockSkillFixture, len(skills.Recommended())), - } - for _, name := range skills.Recommended() { - skillRoot := filepath.Join(skillsRoot, name) - mustMkdir(t, skillRoot) - mustWrite(t, filepath.Join(skillRoot, "SKILL.md"), name+"\n") - hash, err := skills.SkillFolderHash(t.Context(), skillRoot) - if err != nil { - t.Fatalf("hash external skill fixture %q: %v", name, err) - } - lock.Skills[name] = doctorRepositoryLockSkillFixture{ComputedHash: hash} - } - data, err := json.MarshalIndent(lock, "", " ") - if err != nil { - t.Fatalf("encode Doctor skills lock fixture: %v", err) - } - if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), append(data, '\n'), 0o644); err != nil { - t.Fatalf("write Doctor skills lock fixture: %v", err) - } -} - -type doctorPathSnapshot struct { - Mode os.FileMode - Data string -} - -func snapshotDoctorPath(t *testing.T, root string) map[string]doctorPathSnapshot { - t.Helper() - snapshot := make(map[string]doctorPathSnapshot) - err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - relative, err := filepath.Rel(root, path) - if err != nil { - return err - } - info, err := entry.Info() - if err != nil { - return err - } - item := doctorPathSnapshot{Mode: info.Mode()} - if info.Mode().IsRegular() { - data, err := os.ReadFile(path) - if err != nil { - return err - } - item.Data = string(data) - } - snapshot[filepath.ToSlash(relative)] = item - return nil - }) - if err != nil { - t.Fatalf("snapshot Doctor path %q: %v", root, err) - } - return snapshot -} - -func mustReadBytes(t *testing.T, path string) []byte { - t.Helper() - data, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read %s: %v", path, err) - } - return data -} - -func withDoctorFakeDeps(t *testing.T, checker HealthChecker) { - t.Helper() - withDoctorFakeLoaded(t, checker, roundconfig.Loaded{ - Config: roundconfig.Builtin(), - GitRoot: "/repo/project", - HomeDir: "/home/roundfix-test", - }) -} - -func withDoctorFakeLoaded(t *testing.T, checker HealthChecker, loaded roundconfig.Loaded) { - withDoctorFakeLoadedAndReadiness(t, checker, loaded, defaultDoctorDependencies().profileReadiness) -} - -func withDoctorFakeLoadedAndReadiness(t *testing.T, checker HealthChecker, loaded roundconfig.Loaded, readiness func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult) { - t.Helper() - old := doctorDeps - doctorDeps = doctorDependencies{ - loadConfig: func(roundconfig.LoadOptions) (roundconfig.Loaded, error) { - return loaded, nil - }, - healthChecker: func(roundconfig.Loaded) HealthChecker { - return checker - }, - profileReadiness: readiness, - checkSkills: func(context.Context, string) (skills.RepositoryReadiness, error) { - return skills.RepositoryReadiness{ - OwnedRequired: 14, - ExternalRequired: 25, - }, nil - }, - } - t.Cleanup(func() { - doctorDeps = old - }) -} - -func withDoctorLiveDeps(t *testing.T, checker HealthChecker) { - t.Helper() - old := doctorDeps - doctorDeps = defaultDoctorDependencies() - doctorDeps.healthChecker = func(roundconfig.Loaded) HealthChecker { return checker } - doctorDeps.checkSkills = func(context.Context, string) (skills.RepositoryReadiness, error) { - return skills.RepositoryReadiness{ - OwnedRequired: 14, - ExternalRequired: 25, - }, nil - } - t.Cleanup(func() { - doctorDeps = old - }) -} - -func assertDoctorPathMissing(t *testing.T, path string) { - t.Helper() - if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("expected %s to be absent, got error %v", path, err) - } -} - func TestRunDetachRejectsInteractiveWithExistingConflictShape(t *testing.T) { tests := []struct { name string diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 134590a..13cf43d 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "io" + "os" "sort" "strings" @@ -54,7 +55,15 @@ func runDoctorCommand(ctx context.Context, args []string, stdout, stderr io.Writ } checker := doctorDeps.healthChecker(loaded) - workDir := strings.TrimSpace(loaded.GitRoot) + repositoryRoot := strings.TrimSpace(loaded.GitRoot) + profileWorkDir := repositoryRoot + if profileWorkDir == "" { + profileWorkDir, err = os.Getwd() + if err != nil { + printDoctorFailure(fmt.Errorf("resolve process working directory: %w", err), stderr) + return exitRunFailed + } + } // Keep independent checks eager and ordered. results := make([]CheckResult, 0, 6) @@ -62,12 +71,12 @@ func runDoctorCommand(ctx context.Context, args []string, stdout, stderr io.Writ results = append(results, checker.ACPX(ctx)) runtime, runtimeErr := doctorAdapterRuntime(loaded.Config) results = append(results, doctorAdapterCheck(ctx, checker, runtime, runtimeErr)) - profileReadiness := doctorDeps.profileReadiness(ctx, loaded.Config, roundconfig.RequiredWorkCategories(), workDir) + profileReadiness := doctorDeps.profileReadiness(ctx, loaded.Config, roundconfig.RequiredWorkCategories(), profileWorkDir) results = append(results, doctorProfileReadinessResult(profileReadiness)) - if workDir == "" { + if repositoryRoot == "" { results = append(results, doctorMissingRepositoryRootResult()) } else { - skillReadiness, skillErr := doctorDeps.checkSkills(ctx, workDir) + skillReadiness, skillErr := doctorDeps.checkSkills(ctx, repositoryRoot) results = append(results, doctorSkillReadinessResult(skillReadiness, skillErr)) } results = append(results, checker.Codex(ctx)) @@ -153,7 +162,7 @@ func doctorMissingRepositoryRootResult() CheckResult { return CheckResult{ Name: HealthCheckSkills, Status: CheckStatusFailed, - Detail: "repository skill check requires a Git repository", + Detail: "Repository Skill Set readiness requires a Git repository", NextAction: missingGitRootNextAction, } } @@ -214,7 +223,7 @@ func doctorSkillReadinessResult(readiness skills.RepositoryReadiness, checkErr e if externalFailure { next = append(next, externalSkillsNextAction) } - result.NextAction = strings.Join(next, "; ") + result.NextAction = strings.Join(next, " && ") return result } diff --git a/internal/cli/doctor_test.go b/internal/cli/doctor_test.go new file mode 100644 index 0000000..28f870e --- /dev/null +++ b/internal/cli/doctor_test.go @@ -0,0 +1,846 @@ +package cli + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "roundfix/internal/agent" + "roundfix/internal/codex" + roundconfig "roundfix/internal/config" + "roundfix/skills" +) + +func TestRunDoctorProfileReadinessProvesEffectiveCategoriesAndReportsCounts(t *testing.T) { + tests := []struct { + name string + checker *doctorFakeHealthChecker + wantCode int + wantStdout string + }{ + { + name: "all checks pass", + checker: newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK, Detail: "/home/roundfix/.local/bin/codex accepted"}, + ), + wantCode: exitOK, + wantStdout: "node: ok (v25.6.1 >= " + setupNodeMinimumVersion + ")\n" + + "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + + "adapter: ok (codex-acp)\n" + + "profiles: ok (3 distinct tuples; 10 category references)\n" + + "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + + "codex: ok (/home/roundfix/.local/bin/codex accepted)\n", + }, + { + name: "quarantined codex fails with reinstall action", + checker: newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusFailed, Detail: "/tmp/codex is quarantined", NextAction: codex.ReinstallNextAction}, + ), + wantCode: exitRunFailed, + wantStdout: "node: ok (v25.6.1 >= " + setupNodeMinimumVersion + ")\n" + + "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + + "adapter: ok (codex-acp)\n" + + "profiles: ok (3 distinct tuples; 10 category references)\n" + + "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + + "codex: failed (/tmp/codex is quarantined; next: " + codex.ReinstallNextAction + ")\n", + }, + { + name: "codex not applicable does not fail", + checker: newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusSkipped, Detail: "not-applicable on linux"}, + ), + wantCode: exitOK, + wantStdout: "node: ok (v25.6.1 >= " + setupNodeMinimumVersion + ")\n" + + "acpx: ok (" + agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion + ")\n" + + "adapter: ok (codex-acp)\n" + + "profiles: ok (3 distinct tuples; 10 category references)\n" + + "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + + "codex: skipped (not-applicable on linux)\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + homeDir, repoDir := withCLIWorkspace(t) + runner := &profileReadinessExactRunner{} + withAgentRunner(t, runner) + withDoctorFakeDeps(t, tt.checker) + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != tt.wantCode { + t.Fatalf("expected exit code %d, got %d", tt.wantCode, code) + } + if got := stdout.String(); got != tt.wantStdout { + t.Fatalf("unexpected stdout:\n got: %q\nwant: %q", got, tt.wantStdout) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } + if len(runner.exactRequests) != 3 { + t.Fatalf("expected three distinct profile proofs, got %#v", runner.exactRequests) + } + wantModels := []string{"gpt-5.6-sol", "gpt-5.5", "claude-opus-5"} + for index, wantModel := range wantModels { + if request := runner.exactRequests[index]; request.WorkDir != "/repo/project" || request.Runtime.Model != wantModel { + t.Fatalf("profile proof %d = %#v, want model %q in repository", index, request, wantModel) + } + } + if len(tt.checker.agentRequests) != 0 { + t.Fatalf("Doctor must not run the legacy configured Agent probe, got %#v", tt.checker.agentRequests) + } + assertDoctorPathMissing(t, filepath.Join(homeDir, ".acpx")) + assertDoctorPathMissing(t, filepath.Join(homeDir, ".roundfix")) + assertDoctorPathMissing(t, filepath.Join(repoDir, ".roundfixrc.yml")) + }) + } +} + +func TestRunDoctorProfileReadinessReportsLegacyAdapterThroughEffectiveProfile(t *testing.T) { + config := roundconfig.Builtin() + config.Defaults.Agent = "codex" + config.Runtimes.Codex.Model = "legacy-model-default" + proofs, err := buildProfileProofReports(config, roundconfig.RequiredWorkCategories()) + if err != nil { + t.Fatalf("build profile proof reports: %v", err) + } + legacy := &agent.AdapterLineageError{ + Command: "codex-acp", + Package: "@zed-industries/codex-acp", + Version: "0.16.0", + } + applyProfileProofFailure(&proofs[0], legacy) + readiness := profileProofResult{Proofs: proofs, Err: profileProofError{ + Selection: proofs[0].Selection, + References: proofs[0].References, + Classification: proofs[0].Classification, + NextAction: proofs[0].NextAction, + Err: legacy, + }} + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK, Detail: "/home/roundfix/.local/bin/codex accepted"}, + ) + checker.adapter = CheckResult{ + Name: HealthCheckAdapter, + Status: CheckStatusOK, + Detail: "command=\"codex-acp\"; package=@zed-industries/codex-acp; version=0.16.0", + } + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: config, + GitRoot: "/repo/project", + HomeDir: "/home/roundfix-test", + }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { + return readiness + }) + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != exitRunFailed { + t.Fatalf("expected doctor adapter failure exit %d, got %d", exitRunFailed, code) + } + for _, want := range []string{ + "adapter: ok (command=\"codex-acp\"; package=@zed-industries/codex-acp; version=0.16.0)", + "profiles: failed", + `runtime="codex", model="gpt-5.6-sol", reasoning_effort="high"`, + "affected categories: general preferred source=built-in, backend preferred source=built-in, frontend fallback[1] source=built-in, qa preferred source=built-in, review preferred source=built-in", + "classification: adapter_lineage_unknown", + "adapter evidence: command=\"codex-acp\", version=\"0.16.0\"", + "next: run `" + agent.CodexAdapterInstallCommand() + "`", + } { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("expected stdout to contain %q, got %q", want, stdout.String()) + } + } + if strings.Contains(stdout.String(), "legacy-model-default") || strings.Contains(stdout.String(), "model:") || strings.Contains(stdout.String(), "agent:") { + t.Fatalf("Doctor reported legacy configured-runtime readiness: %q", stdout.String()) + } + if len(checker.adapterRuntimes) != 1 || checker.adapterRuntimes[0].Model != "gpt-5.6-sol" || checker.adapterRuntimes[0].ReasoningEffort != "high" { + t.Fatalf("adapter check did not use the effective general profile: %#v", checker.adapterRuntimes) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } +} + +func TestRunDoctorProfileReadinessMatchesProfilesValidateFailureEvidence(t *testing.T) { + _, repoDir := withCLIWorkspace(t) + mustWrite(t, filepath.Join(repoDir, ".roundfixrc.yml"), ` +profiles: + backend: + preferred: + runtime: codex + model: broken-backend + reasoning_effort: high + fallbacks: + - runtime: claude + model: backup-backend + reasoning_effort: medium +`) + failure := &agent.SelectionUnsupportedError{ + Kind: agent.SelectionReasoningControlNotAdvertised, + Runtime: "codex", + Model: "broken-backend", + ReasoningEffort: "high", + AdvertisedModels: []string{"broken-backend", "gpt-5.5"}, + AdvertisedReasoning: []string{"low", "medium"}, + } + runner := &profileReadinessExactRunner{prove: func(req agent.ProbeRequest) (agent.SelectionProof, error) { + if req.Runtime.Model == "broken-backend" { + return agent.SelectionProof{}, failure + } + return agent.SelectionProof{}, nil + }} + withAgentRunner(t, runner) + var validateStdout bytes.Buffer + var validateStderr bytes.Buffer + validateCode := Run([]string{"profiles", "validate", "--category", "backend", "--json"}, &validateStdout, &validateStderr) + if validateCode != exitPreflight { + t.Fatalf("profiles validate exit = %d, want %d", validateCode, exitPreflight) + } + + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK, Detail: "/home/roundfix/.local/bin/codex accepted"}, + ) + withDoctorLiveDeps(t, checker) + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != exitRunFailed { + t.Fatalf("expected doctor selection failure exit %d, got %d", exitRunFailed, code) + } + for _, evidence := range []struct { + doctor string + validate string + }{ + {`runtime="codex", model="broken-backend", reasoning_effort="high"`, `runtime "codex", model "broken-backend", reasoning_effort "high"`}, + {"affected categories: backend preferred source=project", "affected categories: backend preferred source=project"}, + {"classification: reasoning_control_not_advertised", "classification: reasoning_control_not_advertised"}, + {"advertised_models=broken-backend,gpt-5.5", "advertised Agent Models: broken-backend, gpt-5.5"}, + {"advertised_reasoning=low,medium", "advertised reasoning efforts: low, medium"}, + {"roundfix profiles configure --scope user|project", "roundfix profiles configure --scope user|project"}, + } { + if !strings.Contains(stdout.String(), evidence.doctor) { + t.Fatalf("Doctor output missing shared failure evidence %q: %q", evidence.doctor, stdout.String()) + } + if !strings.Contains(validateStderr.String(), evidence.validate) { + t.Fatalf("profiles validate output missing shared failure evidence %q: %q", evidence.validate, validateStderr.String()) + } + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } +} + +func TestRunDoctorContinuesChecksAfterProfileReadinessFailure(t *testing.T) { + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK, Detail: "v25.6.1 >= " + setupNodeMinimumVersion}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK, Detail: agent.MinimumACPXVersion + " >= " + agent.MinimumACPXVersion}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK, Detail: "/home/roundfix/.local/bin/codex accepted"}, + ) + proofCalls := 0 + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + GitRoot: "/repo/project", + HomeDir: "/home/roundfix-test", + }, func(_ context.Context, _ roundconfig.Config, categories []roundconfig.WorkCategory, workDir string) profileProofResult { + proofCalls++ + if got := formatWorkCategories(categories); got != "general, backend, frontend, qa, review" || workDir != "/repo/project" { + t.Fatalf("profile readiness input categories=%q workDir=%q", got, workDir) + } + return profileProofResult{Err: errors.New("profile proof unavailable")} + }) + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != exitRunFailed { + t.Fatalf("expected rejected model exit %d, got %d", exitRunFailed, code) + } + if proofCalls != 1 || checker.nodeCalls != 1 || checker.acpxCalls != 1 || checker.adapterCalls != 1 || checker.codexCalls != 1 { + t.Fatalf("independent check calls profile=%d node=%d acpx=%d adapter=%d codex=%d", proofCalls, checker.nodeCalls, checker.acpxCalls, checker.adapterCalls, checker.codexCalls) + } + output := stdout.String() + for _, want := range []string{"node: ok", "acpx: ok", "adapter: ok", "profiles: failed (profile proof unavailable)", "codex: ok"} { + if !strings.Contains(output, want) { + t.Fatalf("Doctor output missing %q after profile failure: %q", want, output) + } + } + if strings.Index(output, "profiles:") > strings.Index(output, "codex:") { + t.Fatalf("profile readiness must precede Codex so Repository Skill Set readiness can be inserted after it: %q", output) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } +} + +func TestRunDoctorRepositorySkillReadiness(t *testing.T) { + ownedCommand := "roundfix skills install --target project" + externalCommand := "bunx skills experimental_install && bunx skills update -p -y" + tests := []struct { + name string + readiness skills.RepositoryReadiness + err error + wantCode int + wantLine string + }{ + { + name: "ready", + readiness: skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + }, + wantCode: exitOK, + wantLine: "skills: ok (39 required: 14 Roundfix-owned, 25 external)", + }, + { + name: "owned failure", + readiness: skills.RepositoryReadiness{ + OwnedRequired: 14, + MissingOwned: []string{"write-prd"}, + OutdatedOwned: []string{"roundfix"}, + }, + wantCode: exitRunFailed, + wantLine: "skills: failed (missing: write-prd; outdated: roundfix; next: " + + ownedCommand + ")", + }, + { + name: "external failure", + readiness: skills.RepositoryReadiness{ + ExternalRequired: 25, + MissingExternal: []string{"testing-boss"}, + OutdatedExternal: []string{"agentic-cli-design"}, + }, + wantCode: exitRunFailed, + wantLine: "skills: failed (missing: testing-boss; outdated: agentic-cli-design; next: " + + externalCommand + ")", + }, + { + name: "mixed failure is sorted with ordered remediation", + readiness: skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + MissingOwned: []string{"write-prd"}, + MissingExternal: []string{"agentic-cli-design"}, + OutdatedOwned: []string{"roundfix"}, + OutdatedExternal: []string{"testing-boss"}, + }, + wantCode: exitRunFailed, + wantLine: "skills: failed (missing: agentic-cli-design, write-prd; outdated: roundfix, testing-boss; next: " + + ownedCommand + " && " + externalCommand + ")", + }, + { + name: "symlinked lock keeps external remediation", + err: &skills.RepositoryReadinessError{ + Ownership: skills.RepositoryOwnershipExternal, + Operation: "inspect skills lock", + Path: "/repo/project/skills-lock.json", + Err: errors.New("path must be a regular file"), + }, + wantCode: exitRunFailed, + wantLine: "skills: failed (inspect skills lock \"/repo/project/skills-lock.json\": path must be a regular file; next: " + + externalCommand + ")", + }, + { + name: "unclassified checker error", + err: errors.New("repository skill check failed"), + wantCode: exitRunFailed, + wantLine: "skills: failed (repository skill check failed; next: " + + ownedCommand + " && " + externalCommand + ")", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var calls []string + recordCall := func(name string) { + calls = append(calls, name) + } + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, + ) + checker.recordCall = recordCall + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + GitRoot: " /repo/project\n", + }, func(_ context.Context, _ roundconfig.Config, _ []roundconfig.WorkCategory, workDir string) profileProofResult { + recordCall(HealthCheckProfiles) + if workDir != "/repo/project" { + t.Fatalf("profile readiness work directory = %q, want trimmed Git root", workDir) + } + return profileProofResult{} + }) + skillCalls := 0 + doctorDeps.checkSkills = func(_ context.Context, root string) (skills.RepositoryReadiness, error) { + skillCalls++ + recordCall(HealthCheckSkills) + if root != "/repo/project" { + t.Fatalf("skill readiness root = %q, want /repo/project", root) + } + return test.readiness, test.err + } + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != test.wantCode { + t.Fatalf("exit code = %d, want %d; stderr=%q", code, test.wantCode, stderr.String()) + } + lines := strings.Split(strings.TrimSpace(stdout.String()), "\n") + if len(lines) != 6 || lines[4] != test.wantLine { + t.Fatalf("unexpected Doctor output lines:\n%q\nwant skills line %q at index 4", lines, test.wantLine) + } + if skillCalls != 1 || checker.nodeCalls != 1 || checker.acpxCalls != 1 || checker.adapterCalls != 1 || checker.codexCalls != 1 { + t.Fatalf("independent check calls skills=%d node=%d acpx=%d adapter=%d codex=%d", + skillCalls, checker.nodeCalls, checker.acpxCalls, checker.adapterCalls, checker.codexCalls) + } + wantCalls := []string{ + HealthCheckNode, + HealthCheckACPX, + HealthCheckAdapter, + HealthCheckProfiles, + HealthCheckSkills, + HealthCheckCodex, + } + if !reflect.DeepEqual(calls, wantCalls) { + t.Fatalf("check order = %v, want %v", calls, wantCalls) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } + }) + } +} + +func TestRunDoctorPassesCommandContextToRepositorySkillReadiness(t *testing.T) { + type contextKey struct{} + const marker = "doctor-command" + + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, + ) + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + GitRoot: "/repo/project", + }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { + return profileProofResult{} + }) + doctorDeps.checkSkills = func(ctx context.Context, root string) (skills.RepositoryReadiness, error) { + if got := ctx.Value(contextKey{}); got != marker { + t.Fatalf("repository checker context marker = %v, want %q", got, marker) + } + if root != "/repo/project" { + t.Fatalf("repository checker root = %q, want /repo/project", root) + } + return skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + }, nil + } + commandContext := context.WithValue(t.Context(), contextKey{}, marker) + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := runDoctorCommand(commandContext, nil, &stdout, &stderr) + + if code != exitOK { + t.Fatalf("exit code = %d, want %d; stdout=%q stderr=%q", code, exitOK, stdout.String(), stderr.String()) + } +} + +func TestRunDoctorMissingRepositoryRoot(t *testing.T) { + processDir := t.TempDir() + t.Chdir(processDir) + var calls []string + recordCall := func(name string) { + calls = append(calls, name) + } + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, + ) + checker.recordCall = recordCall + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + }, func(_ context.Context, _ roundconfig.Config, _ []roundconfig.WorkCategory, workDir string) profileProofResult { + recordCall(HealthCheckProfiles) + if workDir != processDir { + t.Fatalf("profile readiness work directory = %q, want process working directory %q", workDir, processDir) + } + return profileProofResult{} + }) + skillCalls := 0 + doctorDeps.checkSkills = func(_ context.Context, root string) (skills.RepositoryReadiness, error) { + skillCalls++ + return skills.RepositoryReadiness{}, fmt.Errorf("unexpected repository check for %q", root) + } + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != exitRunFailed { + t.Fatalf("exit code = %d, want %d", code, exitRunFailed) + } + wantStdout := "node: ok\n" + + "acpx: ok\n" + + "adapter: ok (codex-acp)\n" + + "profiles: ok (0 distinct tuples; 0 category references)\n" + + "skills: failed (Repository Skill Set readiness requires a Git repository; next: run roundfix doctor from a Git repository)\n" + + "codex: ok\n" + if got := stdout.String(); got != wantStdout { + t.Fatalf("unexpected stdout:\n got: %q\nwant: %q", got, wantStdout) + } + if skillCalls != 0 { + t.Fatalf("repository checker calls = %d, want 0", skillCalls) + } + wantCalls := []string{ + HealthCheckNode, + HealthCheckACPX, + HealthCheckAdapter, + HealthCheckProfiles, + HealthCheckCodex, + } + if !reflect.DeepEqual(calls, wantCalls) { + t.Fatalf("independent check order = %v, want %v", calls, wantCalls) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } +} + +func TestRunDoctorRealRepositoryCheckDoesNotMutateState(t *testing.T) { + homeDir := t.TempDir() + repoDir := t.TempDir() + t.Setenv("HOME", homeDir) + t.Chdir(repoDir) + mustMkdir(t, filepath.Join(repoDir, ".git")) + writeDoctorReadyRepositoryFixture(t, repoDir) + + userConfigPath := filepath.Join(homeDir, ".roundfix", "config.yml") + mustMkdir(t, filepath.Dir(userConfigPath)) + mustWrite(t, userConfigPath, roundconfig.DefaultConfigYAML()) + mustWrite(t, filepath.Join(homeDir, ".roundfix", "state-marker"), "user state\n") + mustWrite(t, filepath.Join(repoDir, ".roundfixrc.yml"), roundconfig.DefaultConfigYAML()) + mustMkdir(t, filepath.Join(repoDir, ".roundfix")) + mustWrite(t, filepath.Join(repoDir, ".roundfix", "run-marker"), "repository state\n") + + snapshotRoots := map[string]string{ + "repository": repoDir, + "roundfix home": filepath.Join(homeDir, ".roundfix"), + "repository data": filepath.Join(repoDir, ".roundfix"), + "skill state": filepath.Join(repoDir, ".agents", "skills"), + } + beforeRoots := make(map[string]map[string]doctorPathSnapshot, len(snapshotRoots)) + for name, root := range snapshotRoots { + beforeRoots[name] = snapshotDoctorPath(t, root) + } + beforeUserConfig := mustReadBytes(t, userConfigPath) + lockPath := filepath.Join(repoDir, "skills-lock.json") + beforeLock := mustReadBytes(t, lockPath) + + checker := newDoctorFakeHealthChecker( + CheckResult{Name: HealthCheckNode, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckACPX, Status: CheckStatusOK}, + CheckResult{Name: HealthCheckCodex, Status: CheckStatusOK}, + ) + withDoctorFakeLoadedAndReadiness(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + GitRoot: repoDir, + HomeDir: homeDir, + UserConfigPath: userConfigPath, + ProjectConfigPath: filepath.Join(repoDir, ".roundfixrc.yml"), + }, func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult { + return profileProofResult{} + }) + doctorDeps.checkSkills = skills.CheckRepository + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor"}, &stdout, &stderr) + + if code != exitOK { + t.Fatalf("exit code = %d, want %d; stdout=%q stderr=%q", code, exitOK, stdout.String(), stderr.String()) + } + wantStdout := "node: ok\n" + + "acpx: ok\n" + + "adapter: ok (codex-acp)\n" + + "profiles: ok (0 distinct tuples; 0 category references)\n" + + "skills: ok (39 required: 14 Roundfix-owned, 25 external)\n" + + "codex: ok\n" + if got := stdout.String(); got != wantStdout { + t.Fatalf("unexpected stdout:\n got: %q\nwant: %q", got, wantStdout) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr, got %q", stderr.String()) + } + for name, root := range snapshotRoots { + if after := snapshotDoctorPath(t, root); !reflect.DeepEqual(after, beforeRoots[name]) { + t.Fatalf("Doctor mutated %s:\nbefore=%#v\nafter=%#v", name, beforeRoots[name], after) + } + } + if after := mustReadBytes(t, userConfigPath); !bytes.Equal(after, beforeUserConfig) { + t.Fatalf("Doctor mutated User Config:\nbefore=%q\nafter=%q", beforeUserConfig, after) + } + if after := mustReadBytes(t, lockPath); !bytes.Equal(after, beforeLock) { + t.Fatalf("Doctor mutated skills lock:\nbefore=%q\nafter=%q", beforeLock, after) + } +} + +func TestRunDoctorRejectsArguments(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Run([]string{"doctor", "extra"}, &stdout, &stderr) + + if code != exitPreflight { + t.Fatalf("expected exit code %d, got %d", exitPreflight, code) + } + if stdout.Len() != 0 { + t.Fatalf("expected no stdout, got %q", stdout.String()) + } + if !strings.Contains(stderr.String(), "unexpected argument \"extra\"") { + t.Fatalf("expected argument diagnostic, got %q", stderr.String()) + } +} + +func newDoctorFakeHealthChecker(node, acpx, codex CheckResult) *doctorFakeHealthChecker { + return &doctorFakeHealthChecker{ + node: node, + acpx: acpx, + adapter: CheckResult{Name: HealthCheckAdapter, Status: CheckStatusOK, Detail: "codex-acp"}, + codex: codex, + } +} + +type doctorFakeHealthChecker struct { + node CheckResult + acpx CheckResult + adapter CheckResult + agentResult CheckResult + codex CheckResult + agentRequests []agent.ProbeRequest + adapterRuntimes []agent.RuntimeSpec + nodeCalls int + acpxCalls int + adapterCalls int + codexCalls int + recordCall func(string) +} + +func (checker *doctorFakeHealthChecker) Node(context.Context) CheckResult { + checker.nodeCalls++ + checker.record(HealthCheckNode) + return checker.node +} + +func (checker *doctorFakeHealthChecker) ACPX(context.Context) CheckResult { + checker.acpxCalls++ + checker.record(HealthCheckACPX) + return checker.acpx +} + +func (checker *doctorFakeHealthChecker) Adapter(_ context.Context, runtime agent.RuntimeSpec) CheckResult { + checker.adapterCalls++ + checker.adapterRuntimes = append(checker.adapterRuntimes, runtime) + checker.record(HealthCheckAdapter) + return checker.adapter +} + +func (checker *doctorFakeHealthChecker) Agent(_ context.Context, req agent.ProbeRequest) CheckResult { + checker.agentRequests = append(checker.agentRequests, req) + return checker.agentResult +} + +func (checker *doctorFakeHealthChecker) Codex(context.Context) CheckResult { + checker.codexCalls++ + checker.record(HealthCheckCodex) + return checker.codex +} + +func (checker *doctorFakeHealthChecker) record(name string) { + if checker.recordCall != nil { + checker.recordCall(name) + } +} + +type doctorRepositoryLockFixture struct { + Version int `json:"version"` + Skills map[string]doctorRepositoryLockSkillFixture `json:"skills"` +} + +type doctorRepositoryLockSkillFixture struct { + ComputedHash string `json:"computedHash"` +} + +func writeDoctorReadyRepositoryFixture(t *testing.T, root string) { + t.Helper() + skillsRoot := filepath.Join(root, ".agents", "skills") + files, err := skills.Files() + if err != nil { + t.Fatalf("read embedded Repository Skill Set: %v", err) + } + for _, file := range files { + path := filepath.Join(skillsRoot, filepath.FromSlash(file.Path)) + mustMkdir(t, filepath.Dir(path)) + if err := os.WriteFile(path, file.Data, 0o644); err != nil { + t.Fatalf("write embedded skill artifact %q: %v", path, err) + } + } + + lock := doctorRepositoryLockFixture{ + Version: 1, + Skills: make(map[string]doctorRepositoryLockSkillFixture, len(skills.Recommended())), + } + for _, name := range skills.Recommended() { + skillRoot := filepath.Join(skillsRoot, name) + mustMkdir(t, skillRoot) + mustWrite(t, filepath.Join(skillRoot, "SKILL.md"), name+"\n") + hash, err := skills.SkillFolderHash(t.Context(), skillRoot) + if err != nil { + t.Fatalf("hash external skill fixture %q: %v", name, err) + } + lock.Skills[name] = doctorRepositoryLockSkillFixture{ComputedHash: hash} + } + data, err := json.MarshalIndent(lock, "", " ") + if err != nil { + t.Fatalf("encode Doctor skills lock fixture: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "skills-lock.json"), append(data, '\n'), 0o644); err != nil { + t.Fatalf("write Doctor skills lock fixture: %v", err) + } +} + +type doctorPathSnapshot struct { + Mode os.FileMode + Data string +} + +func snapshotDoctorPath(t *testing.T, root string) map[string]doctorPathSnapshot { + t.Helper() + snapshot := make(map[string]doctorPathSnapshot) + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + item := doctorPathSnapshot{Mode: info.Mode()} + if info.Mode().IsRegular() { + data, err := os.ReadFile(path) + if err != nil { + return err + } + item.Data = string(data) + } + snapshot[filepath.ToSlash(relative)] = item + return nil + }) + if err != nil { + t.Fatalf("snapshot Doctor path %q: %v", root, err) + } + return snapshot +} + +func mustReadBytes(t *testing.T, path string) []byte { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return data +} + +func withDoctorFakeDeps(t *testing.T, checker HealthChecker) { + t.Helper() + withDoctorFakeLoaded(t, checker, roundconfig.Loaded{ + Config: roundconfig.Builtin(), + GitRoot: "/repo/project", + HomeDir: "/home/roundfix-test", + }) +} + +func withDoctorFakeLoaded(t *testing.T, checker HealthChecker, loaded roundconfig.Loaded) { + withDoctorFakeLoadedAndReadiness(t, checker, loaded, defaultDoctorDependencies().profileReadiness) +} + +func withDoctorFakeLoadedAndReadiness(t *testing.T, checker HealthChecker, loaded roundconfig.Loaded, readiness func(context.Context, roundconfig.Config, []roundconfig.WorkCategory, string) profileProofResult) { + t.Helper() + old := doctorDeps + doctorDeps = doctorDependencies{ + loadConfig: func(roundconfig.LoadOptions) (roundconfig.Loaded, error) { + return loaded, nil + }, + healthChecker: func(roundconfig.Loaded) HealthChecker { + return checker + }, + profileReadiness: readiness, + checkSkills: func(context.Context, string) (skills.RepositoryReadiness, error) { + return skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + }, nil + }, + } + t.Cleanup(func() { + doctorDeps = old + }) +} + +func withDoctorLiveDeps(t *testing.T, checker HealthChecker) { + t.Helper() + old := doctorDeps + doctorDeps = defaultDoctorDependencies() + doctorDeps.healthChecker = func(roundconfig.Loaded) HealthChecker { return checker } + doctorDeps.checkSkills = func(context.Context, string) (skills.RepositoryReadiness, error) { + return skills.RepositoryReadiness{ + OwnedRequired: 14, + ExternalRequired: 25, + }, nil + } + t.Cleanup(func() { + doctorDeps = old + }) +} + +func assertDoctorPathMissing(t *testing.T, path string) { + t.Helper() + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected %s to be absent, got error %v", path, err) + } +} From fbc2ae4d2eabf232b5aeaf072c423667bce4b7b2 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 20:28:11 -0300 Subject: [PATCH 40/48] docs: synchronize Roundfix Doctor guidance Roundfix-Spec: 0051-doctor-readiness-contract-reconciliation Roundfix-Task: task_05 --- .agents/skills/roundfix/SKILL.md | 16 +++--- .../task_05.md | 53 +++++++++++++++---- skills/roundfix/SKILL.md | 16 +++--- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/.agents/skills/roundfix/SKILL.md b/.agents/skills/roundfix/SKILL.md index a1e1560..7418f43 100644 --- a/.agents/skills/roundfix/SKILL.md +++ b/.agents/skills/roundfix/SKILL.md @@ -47,14 +47,18 @@ Roundfix knows the remediation. The blocking `skills:` line runs after, and independently from, `profiles:`. The running binary's embedded bundle is authoritative for Roundfix-owned skills; each required external skill must match its `computedHash` in -`skills-lock.json`. Surface a failed `skills:` line and its printed `next:` -remediation before work continues. Owned failures print +`skills-lock.json`. Outside a Git repository, Doctor does not inspect the +Repository Skill Set and prints +`skills: failed (Repository Skill Set readiness requires a Git repository; next: run roundfix doctor from a Git repository)`. +Surface a failed `skills:` line and its printed `next:` remediation before work +continues. Owned failures print `roundfix skills install --target project`; external failures print `bunx skills experimental_install && bunx skills update -p -y`; mixed failures -print both commands in that order. Doctor is diagnosis-only: it never runs -these commands, accesses the network, installs or updates skills, or writes -repository state. Apply remediation only after explicit workflow -authorization, then rerun Doctor. +print +`roundfix skills install --target project && bunx skills experimental_install && bunx skills update -p -y`. +Doctor is diagnosis-only: it never runs these commands, accesses the network, +installs or updates skills, or writes repository state. Apply remediation only +after explicit workflow authorization, then rerun Doctor. On macOS, the codex hygiene check resolves `CODEX_PATH` first and then `codex` on `PATH`, inspects the `com.apple.quarantine` attribute (the real XProtect trigger), and verifies the binary's code signature (not `spctl --assess`, which diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md index 49ca32e..0722f1b 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md @@ -1,7 +1,7 @@ --- task: task_05 spec: 0051-doctor-readiness-contract-reconciliation -status: pending +status: completed type: docs complexity: low --- @@ -32,21 +32,21 @@ code, upstream-managed skill, lock authority, or generated Baseline artifact. ## Subtasks -- [ ] Capture the stale Roundfix Skill wording as the red signal. -- [ ] Update the canonical skill with the settled Doctor contract. -- [ ] Apply the exact canonical bytes to the shipped copy. -- [ ] Prove byte equality, repository skill synchronization, and the +- [x] Capture the stale Roundfix Skill wording as the red signal. +- [x] Update the canonical skill with the settled Doctor contract. +- [x] Apply the exact canonical bytes to the shipped copy. +- [x] Prove byte equality, repository skill synchronization, and the changed-file allowlist. ## Acceptance Criteria -- [ ] Both Roundfix Skill copies describe canonical Repository Skill Set +- [x] Both Roundfix Skill copies describe canonical Repository Skill Set missing-root behavior. -- [ ] Both copies show mixed remediation as one owned-then-external chain +- [x] Both copies show mixed remediation as one owned-then-external chain joined by `&&`. -- [ ] The canonical and shipped copies are byte-identical. -- [ ] Repository-owned skill synchronization checks pass. -- [ ] Newly changed paths are limited to the two authorized skill files and +- [x] The canonical and shipped copies are byte-identical. +- [x] Repository-owned skill synchronization checks pass. +- [x] Newly changed paths are limited to the two authorized skill files and this Task file. ## Context @@ -71,3 +71,36 @@ code, upstream-managed skill, lock authority, or generated Baseline artifact. - `_prd.md` → Core Features 6; Non-Goals; Success Metrics. - `_techspec.md` → Documentation and skill synchronization; Build Order 5. + +## Result + +The Roundfix Skill now states the exact missing-Git Repository Skill Set +failure and next action. Mixed ownership remediation is one fail-closed shell +chain in Roundfix-owned-then-external order. The canonical and shipped copies +contain the same bytes. + +Acceptance criterion evidence: + +1. Before the edit, exact-string greps for the canonical missing-Git result + and full mixed-remediation chain both exited `1`; after the edit, each exact + string matched the canonical skill. +2. `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` exited + `0`, proving both copies contain the same guidance. +3. `rtk make skills-sync-check` passed its four repository-owned skill tests. +4. The changed-file postflight listed only the canonical Roundfix Skill, its + shipped copy, and this Task file. + +Verification: + +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` — + passed. +- `rtk make skills-sync-check` — passed; four tests passed. +- `rtk grep -n '&& bunx skills experimental_install' .agents/skills/roundfix/SKILL.md` + — passed; matched the fail-closed mixed chain. +- The declared allowlist pipeline exited `0`; Git emitted an fsmonitor IPC + diagnostic before the pipe. Repeating the same path predicate with + `rtk git -c core.fsmonitor=false status --porcelain` passed cleanly with no + out-of-allowlist path. +- `rtk git diff --check` — passed. + +Follow-ups: none. diff --git a/skills/roundfix/SKILL.md b/skills/roundfix/SKILL.md index a1e1560..7418f43 100644 --- a/skills/roundfix/SKILL.md +++ b/skills/roundfix/SKILL.md @@ -47,14 +47,18 @@ Roundfix knows the remediation. The blocking `skills:` line runs after, and independently from, `profiles:`. The running binary's embedded bundle is authoritative for Roundfix-owned skills; each required external skill must match its `computedHash` in -`skills-lock.json`. Surface a failed `skills:` line and its printed `next:` -remediation before work continues. Owned failures print +`skills-lock.json`. Outside a Git repository, Doctor does not inspect the +Repository Skill Set and prints +`skills: failed (Repository Skill Set readiness requires a Git repository; next: run roundfix doctor from a Git repository)`. +Surface a failed `skills:` line and its printed `next:` remediation before work +continues. Owned failures print `roundfix skills install --target project`; external failures print `bunx skills experimental_install && bunx skills update -p -y`; mixed failures -print both commands in that order. Doctor is diagnosis-only: it never runs -these commands, accesses the network, installs or updates skills, or writes -repository state. Apply remediation only after explicit workflow -authorization, then rerun Doctor. +print +`roundfix skills install --target project && bunx skills experimental_install && bunx skills update -p -y`. +Doctor is diagnosis-only: it never runs these commands, accesses the network, +installs or updates skills, or writes repository state. Apply remediation only +after explicit workflow authorization, then rerun Doctor. On macOS, the codex hygiene check resolves `CODEX_PATH` first and then `codex` on `PATH`, inspects the `com.apple.quarantine` attribute (the real XProtect trigger), and verifies the binary's code signature (not `spctl --assess`, which From a30cad28d22ed8c09f0b5fb05c43c14aff42455e Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 20:40:26 -0300 Subject: [PATCH 41/48] docs: qa report for 0051-doctor-readiness-contract-reconciliation (fail) Roundfix-Spec: 0051-doctor-readiness-contract-reconciliation --- .../command-evidence.md | 97 ++++++++++ .../qa/qa-report-2026-07-26.md | 165 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md new file mode 100644 index 0000000..1fdce95 --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md @@ -0,0 +1,97 @@ +# Command evidence — Doctor readiness reconciliation + +Build: `bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac` + +## PC-01 — Project Constraints and protected tooling scope + +Status: pass. + +- Both `_prd.md` and `_techspec.md` contain all four required Project + Constraint categories. Identifier strategy and authentication/HTTP are + explicitly not applicable with reasons; active ADR obligations and Tooling + Authority are applicable. Each row cites its operative source under + `docs/agents/`. +- The cited ADRs 0049, 0055, 0066, 0072, and 0077 are active and consistent + with the snapshot. +- `rtk git log --format=... -5` resolved the five Daemon-owned Task commits and + their `Roundfix-Spec`/`Roundfix-Task` trailers. +- `rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only -r + 99af3a78f9cebdfa4f4fd5619aaf267a7f2cbf23` exited 0 and listed only: + + ```text + docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md + go.mod + go.sum + ``` + +- `rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only -r + bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac` exited 0 and listed only: + + ```text + .agents/skills/roundfix/SKILL.md + docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md + skills/roundfix/SKILL.md + ``` + +- `rtk git -c core.fsmonitor=false status --porcelain` exited 0 and showed only + the new Spec-local `qa/` directory. No current delta overlaps a protected + tooling path. + +The two tooling Tasks match the exact authorization and their Task-file +allowlists. No missing authorization, untraceable scope, or out-of-scope +tooling mutation blocks flow QA. + +## SG-01 — Full repository Verification + +Status: fail. + +`rtk make verify` exited 2 at the repository `test` target: + +```text +make: *** [test] Error 1 +rtk go test ./... +Go test: 2418 passed, 2 failed, 2 skipped in 23 packages +skills (111 passed, 2 failed) + [FAIL] TestAuthorialSkillSync/typescript-bun.json + [FAIL] TestAuthorialSkillSync +``` + +The exact minimal reproduction was: + +```text +$ rtk proxy go test ./skills -run 'TestAuthorialSkillSync/typescript-bun.json' -count=1 -v +=== RUN TestAuthorialSkillSync +=== RUN TestAuthorialSkillSync/typescript-bun.json + baseline_skill_contract_test.go:321: roundfix contentDigest = "d5269bac642a8ef4c1c8439eff308594e36ad786fe14d9f3047a8e731998f59f", want canonical "1e4ea59e0d6e553e42c6c63e16ad95165a78be8bbf31b8e0cd8b56ce13cc9146" +--- FAIL: TestAuthorialSkillSync (0.01s) + --- FAIL: TestAuthorialSkillSync/typescript-bun.json (0.00s) +FAIL +FAIL roundfix/skills 0.266s +FAIL +``` + +Root-cause evidence: + +- `skills/baseline_skill_contract_test.go:289-324` hashes each canonical + repository-owned skill and requires every Baseline setup snapshot to carry + that exact digest. +- `internal/baseline/assets/setups/typescript-bun.json:1030` records the stale + Roundfix digest + `d5269bac642a8ef4c1c8439eff308594e36ad786fe14d9f3047a8e731998f59f`. +- The current canonical `.agents/skills/roundfix/` computes + `1e4ea59e0d6e553e42c6c63e16ad95165a78be8bbf31b8e0cd8b56ce13cc9146`. +- Task commit `bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac` + changed the canonical and shipped Roundfix Skill pair but did not change + `internal/baseline/assets/setups/typescript-bun.json` or the related parity + fixture. `git diff --exit-code` over those Baseline paths exited 0. +- `rtk make skills-sync-check` exited 0 with four passing tests, confirming + that Task 05's narrower declared gate does not run + `TestAuthorialSkillSync`. +- `rtk git -c core.fsmonitor=false status --porcelain` still listed only the + Spec-local `qa/` directory, so QA execution did not cause the mismatch. + +Classification: code/product-state caused. The Spec requires the Roundfix +Skill bytes to change while its Non-Goals prohibit Baseline asset changes, but +the repository Verification contract requires those bytes and the Baseline +snapshot digest to stay synchronized. The QA skill requires flow execution to +stop at this point. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md new file mode 100644 index 0000000..7a6300a --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md @@ -0,0 +1,165 @@ +--- +spec: 0051-doctor-readiness-contract-reconciliation +date: 2026-07-26 +build: bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac +status: closed +verdict: fail +surfaces: [cli, backend, docs] +--- + +# QA report — Doctor readiness contract reconciliation + +## Scope and environment + +Full gate for all five PRD user stories, every Task acceptance criterion, +the declared CLI/backend/docs surfaces, and the Spec Non-Goals. The real +application entry point is a binary built from commit +`bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac`; scratch repositories and +temporary application state stay outside the repository. The actor is a +Roundfix operator unless a row names a maintainer, developer, Agent, or +documentation reader. + +Platform: macOS, local filesystem, `America/Sao_Paulo`. Network access is not +part of this offline Doctor contract. Evidence belongs under +`qa/evidence/2026-07-26-doctor-readiness/`. + +## Project Constraint audit + +Passed. Both constraint snapshots account for identifier strategy, +authentication/HTTP, active ADR obligations, and Tooling Authority with +operative `docs/agents/` sources. Daemon-owned commit +`99af3a78f9cebdfa4f4fd5619aaf267a7f2cbf23` changed only `go.mod`, `go.sum`, +and `task_01.md`; commit +`bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac` changed only the canonical and +shipped Roundfix Skill pair plus `task_05.md`. The current delta contains only +this Spec's `qa/` directory. + +## Static gate + +`rtk make verify` failed at `go test ./...`: 2,418 tests passed, 2 failed, +and 2 skipped across 23 packages. Both failures are +`TestAuthorialSkillSync` for `typescript-bun.json`. The snapshot records +Roundfix digest `d5269b…f59f`; the canonical Skill now computes +`1e4ea5…9146`. The exact focused reproduction fails identically, so this is a +code/product-state blocker rather than an environment constraint. + +## Planned matrix + +The report opened with every row `pending`. The terminal statuses below record +the static-gate stop without crediting unexecuted flows. + +| ID | Story / criterion / sweep | Actor and surface | Entry point and steps | Expected observable, independent confirmation, persistence | Evidence | Status | +| --- | --- | --- | --- | --- | --- | --- | +| PC-01 | Project Constraints and protected tooling scope | Maintainer · docs/git | Read both constraint snapshots; inspect each Task commit with `git diff-tree`; inspect the current delta | All four constraint categories are reasoned and sourced; Tasks 01 and 05 stay inside their exact protected allowlists; no unauthorized tooling path appears | `command-evidence.md` | pass | +| SG-01 | Full repository Verification | Maintainer · backend/cli/docs | Run `rtk make verify` from the repository root | Exit 0 with formatting, tests, skill synchronization, and build green; a fresh status read shows only QA artifacts | `command-evidence.md` | fail | +| J-01 | US1; Task 03 AC 1–3 and 6: cooperative cancellation | Operator · CLI/backend | Run the public cancellation path, then the focused cancellation and race groups | Cancellation exits promptly; `context.Canceled`/deadline remain discoverable; traversal stops; Doctor passes its command context; all callers use the context-first API | `command-evidence.md` | skipped | +| J-02 | US2; Task 02 AC 1–5: total external hash order | Maintainer · backend | Run the complete `TestSum` group, Baseline consumer group, and affected race group | Every tie-corpus permutation has one digest; primary collation, negative digest, non-mutation, and shared Baseline consumer contracts pass; rerun is identical | `command-evidence.md` | skipped | +| J-03 | US3; Task 04 AC 1–3: outside-Git working-directory separation | Developer · CLI | Run the built `roundfix doctor` from a non-Git scratch directory twice | Profile proof gets a usable process directory; Repository Skill Set inspection is not substituted with that directory; output contains the canonical missing-Git detail and action; both runs are stable and scratch state is unchanged | `command-evidence.md` | skipped | +| J-04 | US4; Task 03 AC 4–5 and Task 04 AC 4–5: fail-closed ownership remediation | Agent · CLI/backend | Run Doctor in a scratch Git repository that needs both repair groups; run focused typed-ownership cases | Mixed output contains one owned-then-external `&&` chain; lock symlink is external-only; shared/owned/external classifications remain distinct; later checks still run in order; rerun and state digest match | `command-evidence.md` | skipped | +| J-05 | US5; Task 01 AC 1–4: tool-produced module metadata | Maintainer · backend | Run `go mod edit -json`, `go mod tidy -diff`, `go mod verify`, and compare selected versions with the Task evidence | `golang.org/x/text v0.40.0` is direct; tidy emits no diff; modules verify; no dependency version drift; rerun remains clean | `command-evidence.md` | skipped | +| AC-01 | Task 03 AC 1–6: Repository Skill Set inspection contract | Maintainer · backend | Run the full named `CheckRepository`/`SkillFolderHash` group and affected race group | Cancellation, confinement, ownership, symlink target non-read, no-mutation, and context-first compilation all pass | `command-evidence.md` | skipped | +| AC-02 | Task 04 AC 1–7: Doctor coordinator and public surface | Operator · CLI/docs | Run Doctor inside the repository, the complete Doctor focused/race groups, and source-location checks | Git root reaches profile and skill checks; line order/stdout/stderr/exit contracts and eager checks pass; Doctor tests live in `doctor_test.go`; public run does not mutate state | `command-evidence.md` | skipped | +| AC-03 | Task 05 AC 1–5: synchronized Roundfix guidance | Agent · docs | Compare canonical/shipped skill bytes, run `make skills-sync-check`, grep both exact contracts, and inspect Task 05 commit paths | Skill files are byte-identical, sync checks pass, canonical missing-Git and mixed-chain text exists, and the protected commit allowlist is exact | `command-evidence.md` | skipped | +| DOC-01 | Core Feature 6: user guidance is executable and exact | Documentation reader · docs/CLI | Follow the Doctor guide examples through the built binary and compare help/guide/skill terms | Commands are runnable without undocumented knowledge; the canonical term, read-only promise, remediation chain, no new flag/schema, and stdout/stderr contract agree | `command-evidence.md` | skipped | +| NG-01 | Non-Goals and no-mutation boundary | Maintainer · git/CLI | Compare Spec-range changed paths, archived-Spec trees, protected excluded files, Baseline artifacts, and repository/scratch digests before and after flows | No history rewrite, archived Spec edit, upstream-managed skill/lock/recommendation edit, Baseline artifact change, config edit, new Doctor flag/schema, network repair, or automatic mutation occurred | `command-evidence.md` | skipped | + +### Behavior probes + +- Cancellation/interrupt: cancel while Doctor is active; relevant because the + primary regression kept repository inspection alive after command + cancellation. +- Deep-link/reopen: run outside Git twice from the same scratch directory; + relevant because fallback working-directory and missing-root output must not + depend on prior state. +- Invalid input: pass an unexpected Doctor argument; relevant because the Spec + forbids a new flag or schema and must preserve exit behavior. +- Repeat failure/no mutation: run the mixed-ownership failure twice and compare + scratch and repository state; relevant because Doctor is diagnosis-only. + +## Acceptance traceability + +| Source | Criteria mapped | +| --- | --- | +| PRD User Stories 1–5 | J-01, J-02, J-03, J-04, J-05 | +| Task 01 AC 1–5 | J-05, PC-01 | +| Task 02 AC 1–5 | J-02 | +| Task 03 AC 1–6 | J-01, J-04, AC-01 | +| Task 04 AC 1–7 | J-03, J-04, AC-02, DOC-01 | +| Task 05 AC 1–5 | AC-03, PC-01 | +| PRD Core Features 1–6 | J-01, J-02, J-03, J-04, J-05, AC-03, DOC-01 | +| PRD Success Metrics | J-01 through J-05, SG-01, AC-01 through AC-03 | +| PRD Non-Goals | NG-01 | + +## Results + +| # | Story / criterion / sweep | Actor and surface | Status | Evidence | +| --- | --- | --- | --- | --- | +| PC-01 | Project Constraints and protected tooling scope | Maintainer · docs/git | pass | `command-evidence.md` | +| SG-01 | Full repository Verification | Maintainer · backend/cli/docs | fail | `command-evidence.md` | +| J-01 | Cooperative cancellation | Operator · CLI/backend | skipped | Static gate failed before flow QA. | +| J-02 | Total external hash order | Maintainer · backend | skipped | Static gate failed before flow QA. | +| J-03 | Outside-Git working-directory separation | Developer · CLI | skipped | Static gate failed before flow QA. | +| J-04 | Fail-closed ownership remediation | Agent · CLI/backend | skipped | Static gate failed before flow QA. | +| J-05 | Tool-produced module metadata | Maintainer · backend | skipped | Static gate failed before flow QA. | +| AC-01 | Repository Skill Set inspection contract | Maintainer · backend | skipped | Static gate failed before flow QA. | +| AC-02 | Doctor coordinator and public surface | Operator · CLI/docs | skipped | Static gate failed before flow QA. | +| AC-03 | Synchronized Roundfix guidance | Agent · docs | skipped | Static gate failed before flow QA; its narrower `make skills-sync-check` diagnostic passed. | +| DOC-01 | Executable, exact user guidance | Documentation reader · docs/CLI | skipped | Static gate failed before flow QA. | +| NG-01 | Non-Goals and no-mutation boundary | Maintainer · git/CLI | skipped | Static gate failed before flow QA. | + +## Findings + +### BUG-001 — Baseline setup snapshot rejects the synchronized Roundfix Skill + +Impact: Blocks-Completion. Severity: Critical. Priority: P0. +Persona: maintainer. Journey step: SG-01, repository Verification. + +Expected: `rtk make verify` exits 0 on the assembled Spec build. + +Actual: `TestAuthorialSkillSync/typescript-bun.json` reports snapshot digest +`d5269bac642a8ef4c1c8439eff308594e36ad786fe14d9f3047a8e731998f59f` +while the canonical Roundfix Skill computes +`1e4ea59e0d6e553e42c6c63e16ad95165a78be8bbf31b8e0cd8b56ce13cc9146`. + +Reproduction: + +1. From the repository root, run `rtk make verify`. +2. Independently run `rtk proxy go test ./skills -run + 'TestAuthorialSkillSync/typescript-bun.json' -count=1 -v`. +3. Observe the same digest mismatch and non-zero exit. + +Root cause: Task 05 changed the canonical/shipped Roundfix Skill bytes without +refreshing the Baseline setup snapshot digest. Its declared +`make skills-sync-check` gate passes because that target does not include +`TestAuthorialSkillSync`. The Spec simultaneously excludes Baseline artifact +changes, so the current approved scope cannot satisfy the repository's full +Verification invariant. + +Affected rows: SG-01 directly; all remaining flow and criterion rows are +skipped under the mandatory code-failure stop. + +## Blocked and skipped + +Skipped J-01 through NG-01 after SG-01 failed. The QA gate requires flow +execution to stop on a code-caused static failure; these are not environment +blocks and receive no pass credit. + +Unblocking action: reconcile the Spec's Baseline Non-Goal and tooling +authorization with the repository's authorial-skill snapshot invariant, +implement the authorized correction, and rerun this gate from `rtk make +verify`. + +## Coverage + +Planned coverage remains complete: 5/5 stories, 28/28 Task acceptance +criteria, 6/6 Core Features, all Non-Goals, and four behavior probes are +mapped. Executed coverage stopped at the required static gate: 1 row passed, +1 failed, 10 skipped; 0/5 real user-story journeys ran and no skipped Task +criterion received pass credit. + +## Final verdict + +Fail. The assembled build cannot pass repository Verification because the +canonical Roundfix Skill and the `typescript-bun.json` Baseline snapshot carry +different content digests. Counts: 1 pass, 1 fail, 0 blocked, 10 skipped; one +Blocks-Completion finding. From 8bef8cbd8f96226df0e8bc916fcd8f9b46e26ba6 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 20:43:10 -0300 Subject: [PATCH 42/48] docs: add Baseline snapshot reconciliation task --- .../_prd.md | 26 ++++-- .../_tasks.md | 7 +- .../_techspec.md | 44 ++++++++-- .../task_06.md | 83 +++++++++++++++++++ 4 files changed, 145 insertions(+), 15 deletions(-) create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md index a37bf8c..bd98113 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md @@ -34,9 +34,17 @@ history already accepted by the maintainer. on 2026-07-26 to update `go.mod` and `go.sum` through Go tooling for the `golang.org/x/text` collation dependency, and to keep `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md` aligned - with shipped CLI behavior. These four paths are the complete protected - tooling boundary for this Spec; no other protected tooling mutation is - authorized. Source: `docs/agents/agent-instructions.md`. + with shipped CLI behavior. After QA proved that this canonical edit + invalidated the repository's authorial Baseline snapshot invariant, the + maintainer also authorized the exact derived refresh of + `internal/baseline/assets/setups/typescript-bun.json`, + `internal/baseline/testdata/catalog.digest`, + `internal/baseline/testdata/catalog.normalized.json`, + `internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json`, and + `internal/baseline/testdata/parity-corpus/v1/manifest.json`. These nine paths + are the complete protected tooling boundary for this Spec; no other + protected tooling mutation is authorized. Source: + `docs/agents/agent-instructions.md`. ## Goals @@ -90,6 +98,9 @@ history already accepted by the maintainer. 6. **Synchronized guidance.** User documentation and the canonical and shipped Roundfix Skill pair describe the exact Doctor output and remediation contract. +7. **Reconciled derived snapshot.** The TypeScript/Bun Baseline setup snapshot + records the current canonical Roundfix Skill digest, and only its directly + derived catalog and parity metadata are regenerated. ## User Experience @@ -109,8 +120,9 @@ update, write, or execute any remediation automatically. - Editing any archived Spec, including Specs 0036 and 0050. - Reverting or editing the accepted upstream-managed skill update, `skills-lock.json`, or `skills/recommended.txt`. -- Changing Baseline assets, generated Baseline parity artifacts, or the - repository-owned digest preservation behavior already implemented. +- Changing Baseline behavior, source setup authority, or any Baseline asset + beyond the exact derived snapshot, catalog, and parity refresh required by + the canonical Roundfix Skill bytes. - Editing `.coderabbit.yaml` or `.roundfixrc.yml`. - Moving Repository Skill Set readiness into `HealthChecker`; the existing injected Doctor seam remains the ownership boundary. @@ -130,6 +142,8 @@ update, write, or execute any remediation automatically. - A symlinked lock produces only external remediation; a mixed failure prints one owned-then-external chain joined by `&&`. - `go mod tidy -diff` is empty after implementation. +- The authorial skill-snapshot invariant and its catalog/parity compatibility + fixtures agree with the canonical Roundfix Skill bytes. - Focused tests, affected-package race tests, public Doctor execution, and the repository verification gate pass without mutating checked repository state. @@ -147,6 +161,8 @@ update, write, or execute any remediation automatically. Profile proof, never for Repository Skill Set inspection. - Keep ownership-specific symlink behavior and the existing `HealthChecker` boundary unchanged. +- Treat the failed QA digest mismatch as a required derived-artifact refresh, + not as authority to change Baseline behavior or source setup content. - Add no ADR because this Spec narrows existing cancellation, hashing, and CLI contracts instead of creating a new architectural boundary. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md index a30e949..c70349e 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md @@ -18,6 +18,9 @@ graph: - id: task_05 file: task_05.md needs: [task_04] + - id: task_06 + file: task_06.md + needs: [task_05] --- # Tasks — Doctor Readiness Contract Reconciliation @@ -29,5 +32,7 @@ graph: | task_03 | Make Repository Skill Set inspection cancellable | backend | high | task_02 | | task_04 | Reconcile Doctor readiness contracts | backend | medium | task_03 | | task_05 | Synchronize Roundfix Doctor guidance | docs | low | task_04 | +| task_06 | Reconcile the derived Baseline skill snapshot | chore | medium | task_05 | -Waves: 1 → task_01 · 2 → task_02 · 3 → task_03 · 4 → task_04 · 5 → task_05 +Waves: 1 → task_01 · 2 → task_02 · 3 → task_03 · 4 → task_04 · 5 → task_05 · +6 → task_06 diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md index 6f7448c..a9f7794 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md @@ -36,9 +36,17 @@ instead of mirroring an undefined tie. on 2026-07-26 to update `go.mod` and `go.sum` through Go tooling for the `golang.org/x/text` collation dependency, and to keep `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md` aligned - with shipped CLI behavior. These four paths are the complete protected - tooling boundary for this Spec; no other protected tooling mutation is - authorized. Source: `docs/agents/agent-instructions.md`. + with shipped CLI behavior. After QA proved that this canonical edit + invalidated the repository's authorial Baseline snapshot invariant, the + maintainer also authorized the exact derived refresh of + `internal/baseline/assets/setups/typescript-bun.json`, + `internal/baseline/testdata/catalog.digest`, + `internal/baseline/testdata/catalog.normalized.json`, + `internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json`, and + `internal/baseline/testdata/parity-corpus/v1/manifest.json`. These nine paths + are the complete protected tooling boundary for this Spec; no other + protected tooling mutation is authorized. Source: + `docs/agents/agent-instructions.md`. ## System Architecture @@ -48,9 +56,11 @@ instead of mirroring an undefined tie. - `internal/skillhash` keeps the shared pure digest algorithm. Its comparator becomes a total order by applying raw normalized path order only when the American English collator reports equality for distinct strings. -- `internal/baseline` remains a consumer of the shared hash. No Baseline - assets, manifests, lock files, or synchronization state change in this - Spec. +- `internal/baseline` remains a consumer of the shared hash. The only Baseline + mutation is a mechanically derived refresh of the TypeScript/Bun setup + snapshot and the catalog/parity metadata that authenticates that snapshot. + Baseline behavior, source setup authority, lock files, and synchronization + state do not change. - `internal/cli` separates the profile-proof working directory from the repository readiness root, passes the command context into the injected skills seam, and owns canonical detail and fail-closed remediation text. @@ -192,9 +202,15 @@ After the CLI and user guide settle, update only the canonical Roundfix Skill and its shipped copy in a dedicated tooling Task so `.agents/skills/roundfix/SKILL.md` and `skills/roundfix/SKILL.md` remain byte-identical. Do not combine that protected mutation with code or user-guide -changes. Do not edit upstream-managed skills, recommendation or lock -authorities, archived Specs, Baseline artifacts, `.coderabbit.yaml`, or -`.roundfixrc.yml`. +changes. + +The canonical skill edit changes the repository-owned content digest embedded +in the TypeScript/Bun Baseline setup snapshot. A subsequent isolated Task +updates that one digest and regenerates only the directly derived setup digest, +normalized catalog/digest, parity asset-sync fixture, and parity manifest. +The Task does not change the Baseline source setup, implementation, schemas, or +other assets. Do not edit upstream-managed skills, recommendation or lock +authorities, archived Specs, `.coderabbit.yaml`, or `.roundfixrc.yml`. ## Data Models @@ -222,6 +238,8 @@ line ordering, status vocabulary, exit codes, stdout/stderr split, and - Goal: tool-produced metadata; User Story 5 → bounded module Task and tidy postflight. - Goal: synchronized guidance → Doctor user guide and Roundfix Skill sync. +- Goal: repository gate integrity → derived TypeScript/Bun snapshot, + catalog, and parity metadata agree with the synchronized Roundfix Skill. ## Integration Points @@ -274,6 +292,8 @@ unchanged. tests, and user guidance (depends on: 3). 5. Synchronize only the authorized canonical and shipped Roundfix Skill pair with the settled Doctor contract (depends on: 4). +6. Refresh only the derived TypeScript/Bun Baseline snapshot and catalog/parity + metadata required by the canonical Roundfix Skill digest (depends on: 5). ## Risks & Considerations @@ -293,6 +313,10 @@ unchanged. - Broad skill synchronization can touch unrelated owned skills. The dedicated tooling Task must mutate only the authorized Roundfix Skill pair and its own Task file. +- The TypeScript/Bun setup snapshot participates in nested digest authorities. + The derived refresh must update the complete catalog/parity chain and prove + it with the existing authorial and compatibility tests; changing only the + visible `contentDigest` is incomplete. ## Decisions @@ -304,3 +328,5 @@ unchanged. - Join multiple remediation actions with `&&`, preserving owned-first order. - Produce module metadata only through `go mod tidy`. - Preserve all prior commits and every archived Spec byte-for-byte. +- Reconcile the failed QA digest by regenerating the exact derived Baseline + chain, without changing Baseline runtime behavior or external authorities. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md new file mode 100644 index 0000000..25a4b7d --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md @@ -0,0 +1,83 @@ +--- +task: task_06 +spec: 0051-doctor-readiness-contract-reconciliation +status: pending +type: chore +complexity: medium +--- + +# Task 06: Reconcile the derived Baseline skill snapshot + +## Overview + +Repair the deterministic QA failure caused by Task 05's canonical Roundfix +Skill edit. Refresh the TypeScript/Bun setup snapshot's repository-owned skill +digest and its complete derived catalog/parity chain. This is a bounded +generated-artifact reconciliation, not a Baseline behavior change. + +## Requirements + +1. MUST derive the Roundfix `contentDigest` from the current canonical + `.agents/skills/roundfix` tree rather than copying the stale QA value. +2. MUST update the enclosing TypeScript/Bun setup digest and regenerate every + directly affected catalog and parity authority. +3. MUST limit implementation mutations to + `internal/baseline/assets/setups/typescript-bun.json`, + `internal/baseline/testdata/catalog.digest`, + `internal/baseline/testdata/catalog.normalized.json`, + `internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json`, + `internal/baseline/testdata/parity-corpus/v1/manifest.json`, and this Task + file. +4. MUST NOT change Baseline implementation, schemas, source setup authority, + any other Baseline asset, either Roundfix Skill copy, an upstream-managed + skill, `skills-lock.json`, or `skills/recommended.txt`. +5. MUST preserve the failed QA report as evidence and leave final flow + validation to a fresh QA run. + +## Subtasks + +- [ ] Reproduce the authorial snapshot failure from the failed QA evidence. +- [ ] Compute the canonical Roundfix Skill digest and refresh the + TypeScript/Bun setup snapshot. +- [ ] Regenerate the normalized catalog/digest and parity fixture/manifest. +- [ ] Prove the complete derived chain and exact changed-file allowlist. + +## Acceptance Criteria + +- [ ] The TypeScript/Bun setup snapshot records the current canonical Roundfix + Skill digest. +- [ ] The enclosing setup digest, normalized catalog/digest, parity fixture, + and parity manifest are internally consistent. +- [ ] The focused authorial snapshot and Baseline compatibility tests pass. +- [ ] No runtime code, source setup, lock authority, upstream-managed skill, + or out-of-allowlist artifact changes. +- [ ] The full repository verification gate passes. + +## Context + +- instruction: `docs/agents/agent-instructions.md` +- instruction: `docs/agents/go.md` +- instruction: `.agents/skills/no-workarounds/SKILL.md` +- instruction: `.agents/skills/systematic-debugging/SKILL.md` +- interface: `skills/baseline_skill_contract_test.go` +- interface: `internal/baseline/catalog_test.go` +- interface: `internal/baseline/compatibility_corpus_test.go` +- interface: `internal/baseline/assets/setups/typescript-bun.json` +- interface: `docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md` + +## Verification + +- `rtk go test ./skills -run 'TestAuthorialSkillSync/typescript-bun.json' -count=1` — expected: the canonical Roundfix Skill digest matches the setup snapshot. +- `rtk go test ./internal/baseline -run 'TestEmbeddedCatalogCompatibility|TestCompatibilityCorpus' -count=1` — expected: catalog and parity authorities accept the regenerated chain. +- `rtk make verify` — expected: the repository gate passes. +- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != "internal/baseline/assets/setups/typescript-bun.json" && path != "internal/baseline/testdata/catalog.digest" && path != "internal/baseline/testdata/catalog.normalized.json" && path != "internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json" && path != "internal/baseline/testdata/parity-corpus/v1/manifest.json" && path != "docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md") {print; bad=1}} END {exit bad}'` — expected: no out-of-allowlist path. + +## References + +- `_prd.md` → Core Features 7; Success Metrics. +- `_techspec.md` → Documentation and skill synchronization; Build Order 6. +- `qa/qa-report-2026-07-26.md` → failed static-gate evidence. + +## Result + +Pending. From 0ea83ffdb8d252ab9f5c184dcfda15788da15c48 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 20:59:18 -0300 Subject: [PATCH 43/48] chore: reconcile the derived Baseline skill snapshot Roundfix-Spec: 0051-doctor-readiness-contract-reconciliation Roundfix-Task: task_06 --- .../task_06.md | 83 ++++++++++++++++--- .../assets/setups/typescript-bun.json | 4 +- internal/baseline/testdata/catalog.digest | 2 +- .../baseline/testdata/catalog.normalized.json | 2 +- .../parity-corpus/v1/fixtures/asset-sync.json | 6 +- .../testdata/parity-corpus/v1/manifest.json | 2 +- 6 files changed, 78 insertions(+), 21 deletions(-) diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md index 25a4b7d..1abe8b8 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md @@ -1,7 +1,7 @@ --- task: task_06 spec: 0051-doctor-readiness-contract-reconciliation -status: pending +status: completed type: chore complexity: medium --- @@ -36,22 +36,22 @@ generated-artifact reconciliation, not a Baseline behavior change. ## Subtasks -- [ ] Reproduce the authorial snapshot failure from the failed QA evidence. -- [ ] Compute the canonical Roundfix Skill digest and refresh the +- [x] Reproduce the authorial snapshot failure from the failed QA evidence. +- [x] Compute the canonical Roundfix Skill digest and refresh the TypeScript/Bun setup snapshot. -- [ ] Regenerate the normalized catalog/digest and parity fixture/manifest. -- [ ] Prove the complete derived chain and exact changed-file allowlist. +- [x] Regenerate the normalized catalog/digest and parity fixture/manifest. +- [x] Prove the complete derived chain and exact changed-file allowlist. ## Acceptance Criteria -- [ ] The TypeScript/Bun setup snapshot records the current canonical Roundfix +- [x] The TypeScript/Bun setup snapshot records the current canonical Roundfix Skill digest. -- [ ] The enclosing setup digest, normalized catalog/digest, parity fixture, +- [x] The enclosing setup digest, normalized catalog/digest, parity fixture, and parity manifest are internally consistent. -- [ ] The focused authorial snapshot and Baseline compatibility tests pass. -- [ ] No runtime code, source setup, lock authority, upstream-managed skill, +- [x] The focused authorial snapshot and Baseline compatibility tests pass. +- [x] No runtime code, source setup, lock authority, upstream-managed skill, or out-of-allowlist artifact changes. -- [ ] The full repository verification gate passes. +- [x] The full repository verification gate passes. ## Context @@ -68,9 +68,9 @@ generated-artifact reconciliation, not a Baseline behavior change. ## Verification - `rtk go test ./skills -run 'TestAuthorialSkillSync/typescript-bun.json' -count=1` — expected: the canonical Roundfix Skill digest matches the setup snapshot. -- `rtk go test ./internal/baseline -run 'TestEmbeddedCatalogCompatibility|TestCompatibilityCorpus' -count=1` — expected: catalog and parity authorities accept the regenerated chain. +- `rtk go test ./internal/baseline -run 'Test(CatalogCompatibility|AssetsSyncCompatibilityMatchesMaintainedPythonContract|BaselineCompatibilityCorpus)' -count=1` — expected: catalog and parity authorities accept the regenerated chain. - `rtk make verify` — expected: the repository gate passes. -- `rtk git status --porcelain | rtk awk '{path=substr($0,4); if (path != "internal/baseline/assets/setups/typescript-bun.json" && path != "internal/baseline/testdata/catalog.digest" && path != "internal/baseline/testdata/catalog.normalized.json" && path != "internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json" && path != "internal/baseline/testdata/parity-corpus/v1/manifest.json" && path != "docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md") {print; bad=1}} END {exit bad}'` — expected: no out-of-allowlist path. +- `rtk git -c core.fsmonitor=false status --porcelain | rtk awk '{path=substr($0,4); if (path != "internal/baseline/assets/setups/typescript-bun.json" && path != "internal/baseline/testdata/catalog.digest" && path != "internal/baseline/testdata/catalog.normalized.json" && path != "internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json" && path != "internal/baseline/testdata/parity-corpus/v1/manifest.json" && path != "docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md") {print; bad=1}} END {exit bad}'` — expected: no out-of-allowlist path. ## References @@ -80,4 +80,61 @@ generated-artifact reconciliation, not a Baseline behavior change. ## Result -Pending. +Reconciled the TypeScript/Bun Baseline snapshot and every directly derived +catalog and parity identity from the current canonical Roundfix Skill bytes. + +### Changes + +- Derived the canonical Roundfix Skill digest as + `1e4ea59e0d6e553e42c6c63e16ad95165a78be8bbf31b8e0cd8b56ce13cc9146` + and recorded it in the TypeScript/Bun setup snapshot. +- Recomputed the enclosing setup digest as + `48592a566d3be7589f00e6895a5d2edb4bd54b59d3e92d171b333558e936c5d3` + and refreshed the normalized catalog plus its domain-separated digest. +- Recomputed the asset-sync fixture's TypeScript/Bun digest and refreshed the + parity manifest identity for the changed fixture. +- Corrected the Task-local compatibility regex after the declared names + selected no tests, and disabled Git fsmonitor in the Task-local allowlist + command after the worktree daemon emitted a diagnostic. + +### Verification + +- Pre-change + `rtk go test ./skills -run 'TestAuthorialSkillSync/typescript-bun.json' -count=1` + reproduced the expected `d5269b...f59f` versus `1e4ea5...9146` mismatch. +- The same authorial command passed after reconciliation: 2 test results in + one package. +- The original Baseline regex exited zero with `No tests found`; it received + no compatibility credit. The corrected + `rtk go test ./internal/baseline -run 'Test(CatalogCompatibility|AssetsSyncCompatibilityMatchesMaintainedPythonContract|BaselineCompatibilityCorpus)' -count=1` + passed all three catalog, asset-sync, and parity owners. +- Independent digest recomputation matched the recorded setup digest, + `sha256:b4328a2c299dda6a312b47098babc2e92f68c910fe3c610e8e9522fe7628071a` + catalog digest, asset-sync setup digest + `886c338c717ff084675db5563d60ade18b54b37dad7f30c10a817e4cf464fbbe`, + and the fixture's 83,662-byte + `1166f01b5f2f5c0f0fe361813e065fd9aa415d0633101fb6348903071644d24f` + manifest identity. +- `rtk make verify` passed 2,420 tests across 23 packages, the focused + synchronization guards, shipped Skill validation, and the CLI build. +- `rtk git -c core.fsmonitor=false status --porcelain` and both unstaged and + staged diff inspection found exactly the five authorized generated artifacts + plus this Task file; the allowlist filter and `rtk git diff --check` exited + zero. + +### Acceptance evidence + +- The passing authorial test proves the snapshot's Roundfix `contentDigest` + was derived from the current `.agents/skills/roundfix` tree. +- Matching recorded and independently recomputed identities prove the setup, + normalized catalog/digest, parity fixture, and manifest form one consistent + derived chain. +- The three focused compatibility owners and the complete repository gate + passed after the last generated-artifact edit. +- Git postflight proves no runtime code, source setup, lock authority, + upstream-managed skill, failed QA report, or out-of-allowlist artifact + changed. + +### Follow-ups + +None. diff --git a/internal/baseline/assets/setups/typescript-bun.json b/internal/baseline/assets/setups/typescript-bun.json index 526a706..253712a 100644 --- a/internal/baseline/assets/setups/typescript-bun.json +++ b/internal/baseline/assets/setups/typescript-bun.json @@ -8,7 +8,7 @@ "ref": "14fdf46befa9a07203fde20b69b885dab4961844", "path": "setups/typescript-bun.txt" }, - "digest": "946c3fe0b0ee331b062199bbba80cb51427b7256411da9e58283f51e451bb362", + "digest": "48592a566d3be7589f00e6895a5d2edb4bd54b59d3e92d171b333558e936c5d3", "skills": [ { "name": "find-rules", @@ -1027,7 +1027,7 @@ "type": "repo", "name": "roundfix" }, - "contentDigest": "d5269bac642a8ef4c1c8439eff308594e36ad786fe14d9f3047a8e731998f59f" + "contentDigest": "1e4ea59e0d6e553e42c6c63e16ad95165a78be8bbf31b8e0cd8b56ce13cc9146" }, { "name": "the-fool", diff --git a/internal/baseline/testdata/catalog.digest b/internal/baseline/testdata/catalog.digest index b416b85..3c6ff3d 100644 --- a/internal/baseline/testdata/catalog.digest +++ b/internal/baseline/testdata/catalog.digest @@ -1 +1 @@ -sha256:1dfe9dc88e84d5a2ace402578a213460b1dca5f035caff587b1ee229b13de5c4 +sha256:b4328a2c299dda6a312b47098babc2e92f68c910fe3c610e8e9522fe7628071a diff --git a/internal/baseline/testdata/catalog.normalized.json b/internal/baseline/testdata/catalog.normalized.json index dd2f5b1..936f26c 100644 --- a/internal/baseline/testdata/catalog.normalized.json +++ b/internal/baseline/testdata/catalog.normalized.json @@ -1 +1 @@ -{"schemaVersion":"roundfix/baseline-catalog/v1","files":[{"path":"contract-v1.json","bytes":13525,"digest":"sha256:0c32b7ac1b6dd1a2a04d830b300d592b7b195aec7bb32ff05575e8d19f154497"},{"path":"coverage.json","bytes":4660,"digest":"sha256:9faa73bf86fad383570b0098a9aa26185ea8d689f9866038ab37b8a34b9a01dc"},{"path":"decisions.json","bytes":8438,"digest":"sha256:58acbb05cf46057923e8844b28e2a97efa898d6200ae9e0773971b6287b05651"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/AGENTS.md","bytes":2911,"digest":"sha256:1876cc64ced89e60cfb0188eaff71d8b588c80d1149740ba368d1afc969e1d8f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/agent-instructions.md","bytes":4182,"digest":"sha256:eb769f99afa32a901723e1962a06fca09d856c8d37284b36c9aed071bd6e4f86"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/autonomous-work.md","bytes":789,"digest":"sha256:76065b3f1b452f647765d01bfb802ca3a0757d7212ba75b8e55d1d6242b0c977"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/backend.md","bytes":1083,"digest":"sha256:b8459f66c5fec1d424768fdd2b0b2f4ec70b9038208c1d5a58086720586195fd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/docs-layout.md","bytes":3546,"digest":"sha256:a188cfaae89ed195ca31460af3ec5d0dfad0bc1241e1958950ed4e0035de86f6"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/domain.md","bytes":614,"digest":"sha256:089a408493f450ef5713f3f62b20b51323b6d08617e6e43a12e38d794676ae82"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/external-triage.md","bytes":385,"digest":"sha256:4e6b2cfafd491161e7666b304ce0d2d6b19ad705613a6471999c84fda1d63e6a"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/frontend.md","bytes":825,"digest":"sha256:3cd5cba8160ea9f6419ffe82920f7641d09eb7126a78ab50606dfe045732cc02"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/issue-tracker.md","bytes":353,"digest":"sha256:f2139d498fb79358a240d8c99ab1718764217cf28aec0fb024beadfa2b0231cc"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/monorepo.md","bytes":371,"digest":"sha256:47cf6a0ac72c2bcacfc69e5566b1cdcc2102e552b1d91592e5cb1033d30be9e3"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/secondbrain.md","bytes":1589,"digest":"sha256:fab2b2896ad03e0436414b83e5cd80e7fdc07b96d9231b798194b36314a8e7a7"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/skill-dispatch.md","bytes":14414,"digest":"sha256:d424a3e360031e39913bf385f87012e6d1ab79a84024701092859640d2fd05dd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/spec-routing.md","bytes":2269,"digest":"sha256:ce1b67f65ce8ad8a5dc1505e2201833d0e477ff91a6161c395db1188ae83ce9f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/typescript-bun.md","bytes":1378,"digest":"sha256:ec114298fee6ef89bbf0423a6b8a467630296264fc49e989d89af3301093b96e"},{"path":"lock-hash-compatibility-v1.json","bytes":366,"digest":"sha256:6a83f5c12ea6fe0a0aa2e2c710ef44b4cc922d2965240376bcd0f12254d765d0"},{"path":"modules/autonomous-work.json","bytes":2400,"digest":"sha256:61cb4100b7a9378f4d582964d6ac7fac52e005f5b27d0e5c359bb80a9fce7f85"},{"path":"modules/backend.json","bytes":5655,"digest":"sha256:c5b3d012e5ee3ec4dabcb5ea679fbc89932eb9760215c0c91d0bc8bb45a4f806"},{"path":"modules/bun.json","bytes":2557,"digest":"sha256:ca869344629ff4ede015e761c87afb01c20c371c05260cef56f36a7376e53777"},{"path":"modules/cli-surface.json","bytes":1721,"digest":"sha256:04e84d41b65452dc06d43ce43e2aedec3cc05d100d18aacc150f808ae80f7d32"},{"path":"modules/context-workflow.json","bytes":11142,"digest":"sha256:fb61822b2ce29891d6a06963b225a4da6935859df83ae1aa3fde9fa3bf0b0d30"},{"path":"modules/core.json","bytes":18107,"digest":"sha256:69dd99c698a50f651b0bd0b3021b2abbfba645955fe534735e97bff4ef0f1098"},{"path":"modules/external-triage.json","bytes":1159,"digest":"sha256:5ea90217b042667a3e00211941d4cb1bafb24294187635bfa277765c05894366"},{"path":"modules/frontend.json","bytes":8713,"digest":"sha256:6a701d874b2217bb7f59cddfdd0d0d8a8bb41c6452fc63b67251e5beddc25f7d"},{"path":"modules/go.json","bytes":3322,"digest":"sha256:d9877c86f59206b3bf6bdc1bf73fdfd18fdcc496bc67770413963587276b0533"},{"path":"modules/monorepo.json","bytes":1385,"digest":"sha256:e6485fd6dacb765a866dd9ff79f057d2ebc85cb57c603774fe7ca453cf50f917"},{"path":"modules/repository-extension.json","bytes":1094,"digest":"sha256:a2adeac11000706839944c2ecca225e524da6f38534e54efc9cb0a26f5413f47"},{"path":"modules/rust.json","bytes":2328,"digest":"sha256:46537524b19a5eb1b61dd07d7215b2ea607c1b78a481a21eb13926d40ecdc0b2"},{"path":"modules/secondbrain.json","bytes":4333,"digest":"sha256:3ce59d0ddfcd0e2e5de34d9a4541a27bc62c6e72800da1cd02353ba2f2aa2024"},{"path":"modules/spec-workflow.json","bytes":6441,"digest":"sha256:574f08d79b15e4d9a6a38c3a9a85636f5a208aa9e43051383815056fc03c6e53"},{"path":"modules/tui-surface.json","bytes":1658,"digest":"sha256:06e18ef65d3a7184aaa3a80a21d2279b3376ab77f07bb8b47d23497b0fb6fa62"},{"path":"modules/typescript.json","bytes":8526,"digest":"sha256:7a2a82f7efb28df7dbc343f1b5a7b0ab176e8a74105eb6077c7b5cf6648bdbc6"},{"path":"profiles/go-cli-tui.json","bytes":1709,"digest":"sha256:231220ac521a8298d86440f6e262d61b9c2fa249459dd9206973d8db6e6941bb"},{"path":"profiles/rust-cli.json","bytes":1627,"digest":"sha256:f2338dbf8d0085a6150293c157b6986714de122b2ebd732e33a09f0614686c81"},{"path":"profiles/standard-typescript-monorepo.json","bytes":10907,"digest":"sha256:803f62fb451f9fda23a58b7bfe5f5367a45d8d44bb63c3f96e33b259393cc5ba"},{"path":"retention/transition.legacy-typescript-bun-to-portable-v3.json","bytes":12425,"digest":"sha256:3b166a42eb1f9f23202afc48399eb7aa9b0e8563d9c40095ad6bf710a5d1c599"},{"path":"retention/transition.managed-v2-to-portable-v3.json","bytes":11185,"digest":"sha256:8a0e9dd06aa6a5988c010f6c1286c39002c65e5aae6762e9af6956424524c3e9"},{"path":"setups/go-cli.json","bytes":14203,"digest":"sha256:28e2ad53684c2c097affa4cf70c32a59265435dd105cdcfa57314bd18a6af46f"},{"path":"setups/rust-cli.json","bytes":12476,"digest":"sha256:82cdba03b2c77c9a0c4a6ad737e7de5e90a3fcf6827c18242793b8032766d9c0"},{"path":"setups/typescript-bun.json","bytes":40649,"digest":"sha256:440e67d2f94be86d1568be2a185e4799b2b1637f472b1db9ba7938811407eeb6"},{"path":"skill-activations.json","bytes":3330,"digest":"sha256:bbf1978828a2d383c9cee675a612c17281224e0a1ce5df4b09ba13af4ccd3667"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/accounting.json","bytes":17193,"digest":"sha256:69e39f9df0eb54d08ecb739dc3dfc9dd80c0cf76016aca3d6a0566e48cfaa1f5"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/baseline.json","bytes":498,"digest":"sha256:dca6a12ae24a77fc9104dfcccb507c50dee0e5257e0bf0bd9d86c0b04ce7ffe0"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/AGENTS.md","bytes":583,"digest":"sha256:3b6b36fd045ecb683aec29e0ceac52b3704604815d8035297bfeb8438602494e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/agent-instructions.md","bytes":8996,"digest":"sha256:fe584b8901ae2521dcba7014ca11f3557109a4d91d7c56a21852f027a4bf205e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/autonomous-work.md","bytes":1548,"digest":"sha256:3deff187dba70156ca834529741c4be7df84c5a66f8462ba01594fee35d40336"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/backend.md","bytes":2197,"digest":"sha256:c553c12e3cb13b0435a219fd52ac735bf4f9f3aad05fdb943fd6922cef0a97ac"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/docs-layout.md","bytes":7671,"digest":"sha256:8a78a3543a0a52ac93ef83ab36f953e813be068e96957553483c928cbad439bd"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/domain.md","bytes":947,"digest":"sha256:54c79c324d20b412c07c634f490ad18eeace0562caffe5507b3c83cebf9aaf20"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/external-triage.md","bytes":335,"digest":"sha256:0ec2089110c6a673f305e86f436538c7c07aeef46d29a4e68674defb8778e2cc"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/frontend.md","bytes":1934,"digest":"sha256:4a3609c27bf100aaf0d7de934ad43c0f6a561a15600517422a325fcc6120bd14"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/issue-tracker.md","bytes":890,"digest":"sha256:d86ab246d701a54b8d61c5dcdc1d29fe3281115c028e8694f10574ce99e537c7"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/monorepo.md","bytes":366,"digest":"sha256:c59091add143e67b3773df78d95cc4c8f11d43909655ef1c09dd64c0cc903b3d"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/secondbrain.md","bytes":3312,"digest":"sha256:4b58f3ed96f654d8b99ccc9a03718505f4c43a6170eceef47e26484b5f95bffb"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/skill-dispatch.md","bytes":374,"digest":"sha256:90bbf02e35bba9c707e4713eef4de9a6db36ba3fa93393531287aaf94fce8f8b"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/spec-routing.md","bytes":3094,"digest":"sha256:dfc867401e547d4370f20c578a22e6b82cc80d7b677072f31bc11cdba4851085"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/typescript-bun.md","bytes":2561,"digest":"sha256:7295fe2e544af37e6d6777537fa7ee512b78f3da932d736cc7eabc8d190a30e8"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/manifest.json","bytes":37175,"digest":"sha256:e9a9e91f8dbbea8dc416a03fe813b9c370a2e012118f548b18f1c6a13b6b7662"},{"path":"source-baselines/index.json","bytes":5266,"digest":"sha256:ad636b4cbaf57e27c81093aee9963bcd2b276df7ac243f04b423bf989db314a8"},{"path":"templates/extensions/specific-repository.md","bytes":38,"digest":"sha256:f8aad350dfecd91923c12fe215c1e21dd6741635e44869110391ba9c5c89d137"},{"path":"templates/guides/agent-instructions.md","bytes":260,"digest":"sha256:91f0a4b1d664cf6ab261499b3f7cb093bd2337d6567d07599faebd8fe58dc8d3"},{"path":"templates/guides/autonomous-work.md","bytes":194,"digest":"sha256:384fdebdb4ffd5d6c1b7b197cc02e3f9d3e7327acd7151dc7ce29c0e53e99486"},{"path":"templates/guides/backend.md","bytes":201,"digest":"sha256:0bffbf1e74c9b8c506ec1c612aac0dfaedcfb6f1b7aafc8d1a2d6029cd1170ea"},{"path":"templates/guides/bun.md","bytes":26,"digest":"sha256:3a0640850dc84c8dd6c6b35f10864ae6d3d9c153147699b7bfced6332315633d"},{"path":"templates/guides/cli-surface.md","bytes":34,"digest":"sha256:7cdd6ecc6575c4ef165858ced6409278430a89e3f67baa4281151dc13e2742bc"},{"path":"templates/guides/docs-layout.md","bytes":34,"digest":"sha256:51e9033d80e6742321b47b19c85034c9b4d15b36b9c84230512e3a9a4ef90628"},{"path":"templates/guides/domain-multi-context.md","bytes":112,"digest":"sha256:56b7000576757f37da275123ab75ff5f84d2e26bd88c9f67034a24f3d4883a41"},{"path":"templates/guides/domain-single-context.md","bytes":104,"digest":"sha256:f75666666408761a67d23855f5f78e4526328404343f09a036f8987895c69c0d"},{"path":"templates/guides/domain.md","bytes":59,"digest":"sha256:5c4195e0348b6e5a57679fc833e58b09f4b08080b1cec90a946d0b666310739e"},{"path":"templates/guides/external-triage.md","bytes":38,"digest":"sha256:f26c4164ff69bf44263d9993e4209a9305b8979f228f6f519f139e83f9b51ae9"},{"path":"templates/guides/frontend.md","bytes":265,"digest":"sha256:3243626ac7b55130ac198ba32a350966a26f6a9801a4a4fee259817b2f7eb5f9"},{"path":"templates/guides/go.md","bytes":25,"digest":"sha256:7f597711bdab9fe85ecda9f5895687e436009446ee8416ed96f7bc27fe7b00fd"},{"path":"templates/guides/issue-tracker.md","bytes":36,"digest":"sha256:48840599c91f2f3cf2fd79700f41e54bc3edbb4fa19e4861247e19bac0077fb1"},{"path":"templates/guides/monorepo.md","bytes":31,"digest":"sha256:e1866f1633f3bbc5b230b5c5290b71ae47110dce4d4344c264a9eb325a477bca"},{"path":"templates/guides/rust.md","bytes":27,"digest":"sha256:e0e693135266e96cabec918b14f0cf51c7bf1a126d900001cc418f982f8bbec2"},{"path":"templates/guides/secondbrain.md","bytes":34,"digest":"sha256:a59dc0ae149d4bb3bdd633f79ba91fd17e6d3d3d930c346e5757b5f85ab69002"},{"path":"templates/guides/skill-dispatch.md","bytes":253,"digest":"sha256:0f054b846da5b17409dde049b3b182489e6c24e84734d4a5ffc47a9f504e8a7c"},{"path":"templates/guides/spec-docs-layout.md","bytes":39,"digest":"sha256:6d9ddb174c4f69cb1f9abe428a6be677e9ea8dbecc19ae0fa2f53fdc3749de40"},{"path":"templates/guides/spec-routing.md","bytes":35,"digest":"sha256:cc578e73968c15bdcaad35270454e91462b4413d937cfd984404ef22d765482c"},{"path":"templates/guides/tui-surface.md","bytes":34,"digest":"sha256:bbc422cb32df905d526a291cbfd5b81fd86f0d405f5f650cc5d8eb0a864c4eb2"},{"path":"templates/guides/typescript-bun.md","bytes":41,"digest":"sha256:ee14609660ebe1da17eff0d12c3f42857cba4c92be0f723346d17e3d64bb6574"},{"path":"templates/index.json","bytes":7125,"digest":"sha256:272cbfd4fa2ea380acfc76ce4d75eaf2ae026b7501c34ee48a8645644eb72f4c"},{"path":"templates/root/autonomous-work.md","bytes":102,"digest":"sha256:abd0be4c74295597be5dca6a307e41d6bdcfa5113370f5bbc30d30725c927e35"},{"path":"templates/root/backend.md","bytes":76,"digest":"sha256:f65a96aaa0b79d40d209df159c30bd6ac791e36c415b8ba2dc52a1032c5785c3"},{"path":"templates/root/bun.md","bytes":89,"digest":"sha256:48a4bcd7c9b86d8f3be6965ff8b7b33aad77148aa598d49ed8efa74acd3c8e20"},{"path":"templates/root/cli-surface.md","bytes":79,"digest":"sha256:ec022b0b1767d88ebc849f2ee7d8a2d266a36d419f8084b9cce8599d0619a125"},{"path":"templates/root/context-workflow.md","bytes":129,"digest":"sha256:52c53e99fed6c5d59388e58f7854c96f8130da553d80bb90f04935f67eea45c5"},{"path":"templates/root/core.md","bytes":213,"digest":"sha256:ffa6f25825e1d5c5bf52373022d7035b004fd78dd55f99a187d89c4b78ddeabb"},{"path":"templates/root/external-triage.md","bytes":91,"digest":"sha256:5ea18c6c7aea8f03d62af0b824b947ddecbaf861fbf9b81df2e8fb2b2c0e2c54"},{"path":"templates/root/frontend.md","bytes":99,"digest":"sha256:cddad0b38dff9cbeae571dc8514f5d2770e528725f8931272389bd8a675e08bb"},{"path":"templates/root/go.md","bytes":67,"digest":"sha256:aa3eafd5ebcec7aa64ae7fddf1c5b954512e831121934760f615c12ec52f6527"},{"path":"templates/root/monorepo.md","bytes":74,"digest":"sha256:76951b431357333d5b3bbbe718529797834c5de14ee6cd886bce175b5ef892e9"},{"path":"templates/root/repository-extension.md","bytes":103,"digest":"sha256:f24b6015a05eab4cdbb830eb485f9958b659d9a99d46b53d8538a813f6705fe1"},{"path":"templates/root/rust.md","bytes":73,"digest":"sha256:40c4f50d3a020406816612b9f9e0c6cbd545626401e447d4963ba2d30d8e40e3"},{"path":"templates/root/secondbrain.md","bytes":87,"digest":"sha256:dbb11588c2dbca5fd803eccc889586cac75824e9083c570bd1cad34fda901857"},{"path":"templates/root/spec-workflow.md","bytes":125,"digest":"sha256:c292c5b2b98facb56ef0f6da0a61f38544377ddb6339c63d5287ca7dec4f2360"},{"path":"templates/root/tui-surface.md","bytes":75,"digest":"sha256:5657d3abaddf480a535d00a8bfe94bcd3e0b6230aaaa3aac2f9ce89fdc7e20b0"},{"path":"templates/root/typescript.md","bytes":76,"digest":"sha256:9673f69acb604560153c93c46aa6a8a03ecedc81c3eae15e03b0b1b763dbcde7"}],"profiles":["go-cli-tui","rust-cli","standard-typescript-monorepo"],"modules":["autonomous-work","backend","bun","cli-surface","context-workflow","core","external-triage","frontend","go","monorepo","repository-extension","rust","secondbrain","spec-workflow","tui-surface","typescript"],"decisions":["auth.provider","autonomous.enabled","domain.layout","http.contract","identifier.strategy","language.generated","repository.extension.enabled","runtime.backend","runtime.design","secondbrain.enabled","spec.scaffold","triage.external","verification.gate"],"templates":["template.extension.repository-rules","template.guide.agent-instructions","template.guide.autonomous-work","template.guide.backend","template.guide.bun","template.guide.cli-surface","template.guide.docs-layout","template.guide.domain","template.guide.domain.multi-context","template.guide.domain.single-context","template.guide.external-triage","template.guide.frontend","template.guide.go","template.guide.issue-tracker","template.guide.monorepo","template.guide.rust","template.guide.secondbrain","template.guide.skill-dispatch","template.guide.spec-docs-layout","template.guide.spec-routing","template.guide.tui-surface","template.guide.typescript-bun","template.root.autonomous-work","template.root.backend","template.root.bun","template.root.cli-surface","template.root.context-workflow","template.root.core","template.root.external-triage","template.root.frontend","template.root.go","template.root.monorepo","template.root.repository-extension","template.root.rust","template.root.secondbrain","template.root.spec-workflow","template.root.tui-surface","template.root.typescript"],"setups":["go-cli","rust-cli","typescript-bun"],"retentionTransitions":["transition.legacy-typescript-bun-to-portable-v3","transition.managed-v2-to-portable-v3"]} +{"schemaVersion":"roundfix/baseline-catalog/v1","files":[{"path":"contract-v1.json","bytes":13525,"digest":"sha256:0c32b7ac1b6dd1a2a04d830b300d592b7b195aec7bb32ff05575e8d19f154497"},{"path":"coverage.json","bytes":4660,"digest":"sha256:9faa73bf86fad383570b0098a9aa26185ea8d689f9866038ab37b8a34b9a01dc"},{"path":"decisions.json","bytes":8438,"digest":"sha256:58acbb05cf46057923e8844b28e2a97efa898d6200ae9e0773971b6287b05651"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/AGENTS.md","bytes":2911,"digest":"sha256:1876cc64ced89e60cfb0188eaff71d8b588c80d1149740ba368d1afc969e1d8f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/agent-instructions.md","bytes":4182,"digest":"sha256:eb769f99afa32a901723e1962a06fca09d856c8d37284b36c9aed071bd6e4f86"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/autonomous-work.md","bytes":789,"digest":"sha256:76065b3f1b452f647765d01bfb802ca3a0757d7212ba75b8e55d1d6242b0c977"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/backend.md","bytes":1083,"digest":"sha256:b8459f66c5fec1d424768fdd2b0b2f4ec70b9038208c1d5a58086720586195fd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/docs-layout.md","bytes":3546,"digest":"sha256:a188cfaae89ed195ca31460af3ec5d0dfad0bc1241e1958950ed4e0035de86f6"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/domain.md","bytes":614,"digest":"sha256:089a408493f450ef5713f3f62b20b51323b6d08617e6e43a12e38d794676ae82"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/external-triage.md","bytes":385,"digest":"sha256:4e6b2cfafd491161e7666b304ce0d2d6b19ad705613a6471999c84fda1d63e6a"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/frontend.md","bytes":825,"digest":"sha256:3cd5cba8160ea9f6419ffe82920f7641d09eb7126a78ab50606dfe045732cc02"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/issue-tracker.md","bytes":353,"digest":"sha256:f2139d498fb79358a240d8c99ab1718764217cf28aec0fb024beadfa2b0231cc"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/monorepo.md","bytes":371,"digest":"sha256:47cf6a0ac72c2bcacfc69e5566b1cdcc2102e552b1d91592e5cb1033d30be9e3"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/secondbrain.md","bytes":1589,"digest":"sha256:fab2b2896ad03e0436414b83e5cd80e7fdc07b96d9231b798194b36314a8e7a7"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/skill-dispatch.md","bytes":14414,"digest":"sha256:d424a3e360031e39913bf385f87012e6d1ab79a84024701092859640d2fd05dd"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/spec-routing.md","bytes":2269,"digest":"sha256:ce1b67f65ce8ad8a5dc1505e2201833d0e477ff91a6161c395db1188ae83ce9f"},{"path":"formatter-fixtures/standard-typescript-monorepo/golden/docs/agents/typescript-bun.md","bytes":1378,"digest":"sha256:ec114298fee6ef89bbf0423a6b8a467630296264fc49e989d89af3301093b96e"},{"path":"lock-hash-compatibility-v1.json","bytes":366,"digest":"sha256:6a83f5c12ea6fe0a0aa2e2c710ef44b4cc922d2965240376bcd0f12254d765d0"},{"path":"modules/autonomous-work.json","bytes":2400,"digest":"sha256:61cb4100b7a9378f4d582964d6ac7fac52e005f5b27d0e5c359bb80a9fce7f85"},{"path":"modules/backend.json","bytes":5655,"digest":"sha256:c5b3d012e5ee3ec4dabcb5ea679fbc89932eb9760215c0c91d0bc8bb45a4f806"},{"path":"modules/bun.json","bytes":2557,"digest":"sha256:ca869344629ff4ede015e761c87afb01c20c371c05260cef56f36a7376e53777"},{"path":"modules/cli-surface.json","bytes":1721,"digest":"sha256:04e84d41b65452dc06d43ce43e2aedec3cc05d100d18aacc150f808ae80f7d32"},{"path":"modules/context-workflow.json","bytes":11142,"digest":"sha256:fb61822b2ce29891d6a06963b225a4da6935859df83ae1aa3fde9fa3bf0b0d30"},{"path":"modules/core.json","bytes":18107,"digest":"sha256:69dd99c698a50f651b0bd0b3021b2abbfba645955fe534735e97bff4ef0f1098"},{"path":"modules/external-triage.json","bytes":1159,"digest":"sha256:5ea90217b042667a3e00211941d4cb1bafb24294187635bfa277765c05894366"},{"path":"modules/frontend.json","bytes":8713,"digest":"sha256:6a701d874b2217bb7f59cddfdd0d0d8a8bb41c6452fc63b67251e5beddc25f7d"},{"path":"modules/go.json","bytes":3322,"digest":"sha256:d9877c86f59206b3bf6bdc1bf73fdfd18fdcc496bc67770413963587276b0533"},{"path":"modules/monorepo.json","bytes":1385,"digest":"sha256:e6485fd6dacb765a866dd9ff79f057d2ebc85cb57c603774fe7ca453cf50f917"},{"path":"modules/repository-extension.json","bytes":1094,"digest":"sha256:a2adeac11000706839944c2ecca225e524da6f38534e54efc9cb0a26f5413f47"},{"path":"modules/rust.json","bytes":2328,"digest":"sha256:46537524b19a5eb1b61dd07d7215b2ea607c1b78a481a21eb13926d40ecdc0b2"},{"path":"modules/secondbrain.json","bytes":4333,"digest":"sha256:3ce59d0ddfcd0e2e5de34d9a4541a27bc62c6e72800da1cd02353ba2f2aa2024"},{"path":"modules/spec-workflow.json","bytes":6441,"digest":"sha256:574f08d79b15e4d9a6a38c3a9a85636f5a208aa9e43051383815056fc03c6e53"},{"path":"modules/tui-surface.json","bytes":1658,"digest":"sha256:06e18ef65d3a7184aaa3a80a21d2279b3376ab77f07bb8b47d23497b0fb6fa62"},{"path":"modules/typescript.json","bytes":8526,"digest":"sha256:7a2a82f7efb28df7dbc343f1b5a7b0ab176e8a74105eb6077c7b5cf6648bdbc6"},{"path":"profiles/go-cli-tui.json","bytes":1709,"digest":"sha256:231220ac521a8298d86440f6e262d61b9c2fa249459dd9206973d8db6e6941bb"},{"path":"profiles/rust-cli.json","bytes":1627,"digest":"sha256:f2338dbf8d0085a6150293c157b6986714de122b2ebd732e33a09f0614686c81"},{"path":"profiles/standard-typescript-monorepo.json","bytes":10907,"digest":"sha256:803f62fb451f9fda23a58b7bfe5f5367a45d8d44bb63c3f96e33b259393cc5ba"},{"path":"retention/transition.legacy-typescript-bun-to-portable-v3.json","bytes":12425,"digest":"sha256:3b166a42eb1f9f23202afc48399eb7aa9b0e8563d9c40095ad6bf710a5d1c599"},{"path":"retention/transition.managed-v2-to-portable-v3.json","bytes":11185,"digest":"sha256:8a0e9dd06aa6a5988c010f6c1286c39002c65e5aae6762e9af6956424524c3e9"},{"path":"setups/go-cli.json","bytes":14203,"digest":"sha256:28e2ad53684c2c097affa4cf70c32a59265435dd105cdcfa57314bd18a6af46f"},{"path":"setups/rust-cli.json","bytes":12476,"digest":"sha256:82cdba03b2c77c9a0c4a6ad737e7de5e90a3fcf6827c18242793b8032766d9c0"},{"path":"setups/typescript-bun.json","bytes":40649,"digest":"sha256:264105e707256c67c30ee4a7417d387fab0537347ad78e36f7abc361857a9f56"},{"path":"skill-activations.json","bytes":3330,"digest":"sha256:bbf1978828a2d383c9cee675a612c17281224e0a1ce5df4b09ba13af4ccd3667"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/accounting.json","bytes":17193,"digest":"sha256:69e39f9df0eb54d08ecb739dc3dfc9dd80c0cf76016aca3d6a0566e48cfaa1f5"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/baseline.json","bytes":498,"digest":"sha256:dca6a12ae24a77fc9104dfcccb507c50dee0e5257e0bf0bd9d86c0b04ce7ffe0"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/AGENTS.md","bytes":583,"digest":"sha256:3b6b36fd045ecb683aec29e0ceac52b3704604815d8035297bfeb8438602494e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/agent-instructions.md","bytes":8996,"digest":"sha256:fe584b8901ae2521dcba7014ca11f3557109a4d91d7c56a21852f027a4bf205e"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/autonomous-work.md","bytes":1548,"digest":"sha256:3deff187dba70156ca834529741c4be7df84c5a66f8462ba01594fee35d40336"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/backend.md","bytes":2197,"digest":"sha256:c553c12e3cb13b0435a219fd52ac735bf4f9f3aad05fdb943fd6922cef0a97ac"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/docs-layout.md","bytes":7671,"digest":"sha256:8a78a3543a0a52ac93ef83ab36f953e813be068e96957553483c928cbad439bd"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/domain.md","bytes":947,"digest":"sha256:54c79c324d20b412c07c634f490ad18eeace0562caffe5507b3c83cebf9aaf20"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/external-triage.md","bytes":335,"digest":"sha256:0ec2089110c6a673f305e86f436538c7c07aeef46d29a4e68674defb8778e2cc"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/frontend.md","bytes":1934,"digest":"sha256:4a3609c27bf100aaf0d7de934ad43c0f6a561a15600517422a325fcc6120bd14"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/issue-tracker.md","bytes":890,"digest":"sha256:d86ab246d701a54b8d61c5dcdc1d29fe3281115c028e8694f10574ce99e537c7"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/monorepo.md","bytes":366,"digest":"sha256:c59091add143e67b3773df78d95cc4c8f11d43909655ef1c09dd64c0cc903b3d"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/secondbrain.md","bytes":3312,"digest":"sha256:4b58f3ed96f654d8b99ccc9a03718505f4c43a6170eceef47e26484b5f95bffb"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/skill-dispatch.md","bytes":374,"digest":"sha256:90bbf02e35bba9c707e4713eef4de9a6db36ba3fa93393531287aaf94fce8f8b"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/spec-routing.md","bytes":3094,"digest":"sha256:dfc867401e547d4370f20c578a22e6b82cc80d7b677072f31bc11cdba4851085"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/corpus/docs/agents/typescript-bun.md","bytes":2561,"digest":"sha256:7295fe2e544af37e6d6777537fa7ee512b78f3da932d736cc7eabc8d190a30e8"},{"path":"source-baselines/baseline.standard-typescript-monorepo-0.0.1/manifest.json","bytes":37175,"digest":"sha256:e9a9e91f8dbbea8dc416a03fe813b9c370a2e012118f548b18f1c6a13b6b7662"},{"path":"source-baselines/index.json","bytes":5266,"digest":"sha256:ad636b4cbaf57e27c81093aee9963bcd2b276df7ac243f04b423bf989db314a8"},{"path":"templates/extensions/specific-repository.md","bytes":38,"digest":"sha256:f8aad350dfecd91923c12fe215c1e21dd6741635e44869110391ba9c5c89d137"},{"path":"templates/guides/agent-instructions.md","bytes":260,"digest":"sha256:91f0a4b1d664cf6ab261499b3f7cb093bd2337d6567d07599faebd8fe58dc8d3"},{"path":"templates/guides/autonomous-work.md","bytes":194,"digest":"sha256:384fdebdb4ffd5d6c1b7b197cc02e3f9d3e7327acd7151dc7ce29c0e53e99486"},{"path":"templates/guides/backend.md","bytes":201,"digest":"sha256:0bffbf1e74c9b8c506ec1c612aac0dfaedcfb6f1b7aafc8d1a2d6029cd1170ea"},{"path":"templates/guides/bun.md","bytes":26,"digest":"sha256:3a0640850dc84c8dd6c6b35f10864ae6d3d9c153147699b7bfced6332315633d"},{"path":"templates/guides/cli-surface.md","bytes":34,"digest":"sha256:7cdd6ecc6575c4ef165858ced6409278430a89e3f67baa4281151dc13e2742bc"},{"path":"templates/guides/docs-layout.md","bytes":34,"digest":"sha256:51e9033d80e6742321b47b19c85034c9b4d15b36b9c84230512e3a9a4ef90628"},{"path":"templates/guides/domain-multi-context.md","bytes":112,"digest":"sha256:56b7000576757f37da275123ab75ff5f84d2e26bd88c9f67034a24f3d4883a41"},{"path":"templates/guides/domain-single-context.md","bytes":104,"digest":"sha256:f75666666408761a67d23855f5f78e4526328404343f09a036f8987895c69c0d"},{"path":"templates/guides/domain.md","bytes":59,"digest":"sha256:5c4195e0348b6e5a57679fc833e58b09f4b08080b1cec90a946d0b666310739e"},{"path":"templates/guides/external-triage.md","bytes":38,"digest":"sha256:f26c4164ff69bf44263d9993e4209a9305b8979f228f6f519f139e83f9b51ae9"},{"path":"templates/guides/frontend.md","bytes":265,"digest":"sha256:3243626ac7b55130ac198ba32a350966a26f6a9801a4a4fee259817b2f7eb5f9"},{"path":"templates/guides/go.md","bytes":25,"digest":"sha256:7f597711bdab9fe85ecda9f5895687e436009446ee8416ed96f7bc27fe7b00fd"},{"path":"templates/guides/issue-tracker.md","bytes":36,"digest":"sha256:48840599c91f2f3cf2fd79700f41e54bc3edbb4fa19e4861247e19bac0077fb1"},{"path":"templates/guides/monorepo.md","bytes":31,"digest":"sha256:e1866f1633f3bbc5b230b5c5290b71ae47110dce4d4344c264a9eb325a477bca"},{"path":"templates/guides/rust.md","bytes":27,"digest":"sha256:e0e693135266e96cabec918b14f0cf51c7bf1a126d900001cc418f982f8bbec2"},{"path":"templates/guides/secondbrain.md","bytes":34,"digest":"sha256:a59dc0ae149d4bb3bdd633f79ba91fd17e6d3d3d930c346e5757b5f85ab69002"},{"path":"templates/guides/skill-dispatch.md","bytes":253,"digest":"sha256:0f054b846da5b17409dde049b3b182489e6c24e84734d4a5ffc47a9f504e8a7c"},{"path":"templates/guides/spec-docs-layout.md","bytes":39,"digest":"sha256:6d9ddb174c4f69cb1f9abe428a6be677e9ea8dbecc19ae0fa2f53fdc3749de40"},{"path":"templates/guides/spec-routing.md","bytes":35,"digest":"sha256:cc578e73968c15bdcaad35270454e91462b4413d937cfd984404ef22d765482c"},{"path":"templates/guides/tui-surface.md","bytes":34,"digest":"sha256:bbc422cb32df905d526a291cbfd5b81fd86f0d405f5f650cc5d8eb0a864c4eb2"},{"path":"templates/guides/typescript-bun.md","bytes":41,"digest":"sha256:ee14609660ebe1da17eff0d12c3f42857cba4c92be0f723346d17e3d64bb6574"},{"path":"templates/index.json","bytes":7125,"digest":"sha256:272cbfd4fa2ea380acfc76ce4d75eaf2ae026b7501c34ee48a8645644eb72f4c"},{"path":"templates/root/autonomous-work.md","bytes":102,"digest":"sha256:abd0be4c74295597be5dca6a307e41d6bdcfa5113370f5bbc30d30725c927e35"},{"path":"templates/root/backend.md","bytes":76,"digest":"sha256:f65a96aaa0b79d40d209df159c30bd6ac791e36c415b8ba2dc52a1032c5785c3"},{"path":"templates/root/bun.md","bytes":89,"digest":"sha256:48a4bcd7c9b86d8f3be6965ff8b7b33aad77148aa598d49ed8efa74acd3c8e20"},{"path":"templates/root/cli-surface.md","bytes":79,"digest":"sha256:ec022b0b1767d88ebc849f2ee7d8a2d266a36d419f8084b9cce8599d0619a125"},{"path":"templates/root/context-workflow.md","bytes":129,"digest":"sha256:52c53e99fed6c5d59388e58f7854c96f8130da553d80bb90f04935f67eea45c5"},{"path":"templates/root/core.md","bytes":213,"digest":"sha256:ffa6f25825e1d5c5bf52373022d7035b004fd78dd55f99a187d89c4b78ddeabb"},{"path":"templates/root/external-triage.md","bytes":91,"digest":"sha256:5ea18c6c7aea8f03d62af0b824b947ddecbaf861fbf9b81df2e8fb2b2c0e2c54"},{"path":"templates/root/frontend.md","bytes":99,"digest":"sha256:cddad0b38dff9cbeae571dc8514f5d2770e528725f8931272389bd8a675e08bb"},{"path":"templates/root/go.md","bytes":67,"digest":"sha256:aa3eafd5ebcec7aa64ae7fddf1c5b954512e831121934760f615c12ec52f6527"},{"path":"templates/root/monorepo.md","bytes":74,"digest":"sha256:76951b431357333d5b3bbbe718529797834c5de14ee6cd886bce175b5ef892e9"},{"path":"templates/root/repository-extension.md","bytes":103,"digest":"sha256:f24b6015a05eab4cdbb830eb485f9958b659d9a99d46b53d8538a813f6705fe1"},{"path":"templates/root/rust.md","bytes":73,"digest":"sha256:40c4f50d3a020406816612b9f9e0c6cbd545626401e447d4963ba2d30d8e40e3"},{"path":"templates/root/secondbrain.md","bytes":87,"digest":"sha256:dbb11588c2dbca5fd803eccc889586cac75824e9083c570bd1cad34fda901857"},{"path":"templates/root/spec-workflow.md","bytes":125,"digest":"sha256:c292c5b2b98facb56ef0f6da0a61f38544377ddb6339c63d5287ca7dec4f2360"},{"path":"templates/root/tui-surface.md","bytes":75,"digest":"sha256:5657d3abaddf480a535d00a8bfe94bcd3e0b6230aaaa3aac2f9ce89fdc7e20b0"},{"path":"templates/root/typescript.md","bytes":76,"digest":"sha256:9673f69acb604560153c93c46aa6a8a03ecedc81c3eae15e03b0b1b763dbcde7"}],"profiles":["go-cli-tui","rust-cli","standard-typescript-monorepo"],"modules":["autonomous-work","backend","bun","cli-surface","context-workflow","core","external-triage","frontend","go","monorepo","repository-extension","rust","secondbrain","spec-workflow","tui-surface","typescript"],"decisions":["auth.provider","autonomous.enabled","domain.layout","http.contract","identifier.strategy","language.generated","repository.extension.enabled","runtime.backend","runtime.design","secondbrain.enabled","spec.scaffold","triage.external","verification.gate"],"templates":["template.extension.repository-rules","template.guide.agent-instructions","template.guide.autonomous-work","template.guide.backend","template.guide.bun","template.guide.cli-surface","template.guide.docs-layout","template.guide.domain","template.guide.domain.multi-context","template.guide.domain.single-context","template.guide.external-triage","template.guide.frontend","template.guide.go","template.guide.issue-tracker","template.guide.monorepo","template.guide.rust","template.guide.secondbrain","template.guide.skill-dispatch","template.guide.spec-docs-layout","template.guide.spec-routing","template.guide.tui-surface","template.guide.typescript-bun","template.root.autonomous-work","template.root.backend","template.root.bun","template.root.cli-surface","template.root.context-workflow","template.root.core","template.root.external-triage","template.root.frontend","template.root.go","template.root.monorepo","template.root.repository-extension","template.root.rust","template.root.secondbrain","template.root.spec-workflow","template.root.tui-surface","template.root.typescript"],"setups":["go-cli","rust-cli","typescript-bun"],"retentionTransitions":["transition.legacy-typescript-bun-to-portable-v3","transition.managed-v2-to-portable-v3"]} diff --git a/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json b/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json index 688c9b3..b2126af 100644 --- a/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json +++ b/internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json @@ -30,7 +30,7 @@ "id": "rust-cli" }, { - "digest": "1d48345a1d38ee643dc308c2a0688aba689ec01762fb94d8c711ca053ac0f9bd", + "digest": "886c338c717ff084675db5563d60ade18b54b37dad7f30c10a817e4cf464fbbe", "id": "typescript-bun" } ], @@ -920,7 +920,7 @@ ] } ], - "digest": "1d48345a1d38ee643dc308c2a0688aba689ec01762fb94d8c711ca053ac0f9bd", + "digest": "886c338c717ff084675db5563d60ade18b54b37dad7f30c10a817e4cf464fbbe", "id": "typescript-bun", "schemaVersion": "setup-context-driven/setup-snapshot/0.0.1", "skills": [ @@ -1935,7 +1935,7 @@ "treeDigest": "cc7137c53d51fea605c0be017cf6eecdb72add7405e5c3c9f327dbc26d4572fa" }, { - "contentDigest": "d5269bac642a8ef4c1c8439eff308594e36ad786fe14d9f3047a8e731998f59f", + "contentDigest": "1e4ea59e0d6e553e42c6c63e16ad95165a78be8bbf31b8e0cd8b56ce13cc9146", "name": "roundfix", "path": "skills/06-review-repair/roundfix", "source": { diff --git a/internal/baseline/testdata/parity-corpus/v1/manifest.json b/internal/baseline/testdata/parity-corpus/v1/manifest.json index 7d17d05..b038c9c 100644 --- a/internal/baseline/testdata/parity-corpus/v1/manifest.json +++ b/internal/baseline/testdata/parity-corpus/v1/manifest.json @@ -22,7 +22,7 @@ { "bytes": 83662, "path": "fixtures/asset-sync.json", - "sha256": "7eb997602f8612c2386e1c54fc03a80d60e088b85222a37575b2822c63eb25d9" + "sha256": "1166f01b5f2f5c0f0fe361813e065fd9aa415d0633101fb6348903071644d24f" }, { "bytes": 330237, From 3f383b2e7cf1b238eac4425e1b082135778fcc07 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 21:19:15 -0300 Subject: [PATCH 44/48] docs: qa report for 0051-doctor-readiness-contract-reconciliation (partial) Roundfix-Spec: 0051-doctor-readiness-contract-reconciliation --- .../command-evidence.md | 309 ++++++++++++++++++ .../qa/qa-report-2026-07-26.md | 204 ++++++------ 2 files changed, 403 insertions(+), 110 deletions(-) create mode 100644 docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md new file mode 100644 index 0000000..155735f --- /dev/null +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md @@ -0,0 +1,309 @@ +# Command evidence — build 1736dbc + +All commands ran from the repository root unless a section names another +working directory. + +## PC-01 — Project Constraints and protected tooling scope + +Both `_prd.md` and `_techspec.md` account for: + +- identifier strategy: not applicable, with + `docs/agents/domain.md` as the operative source; +- authentication and HTTP: not applicable, with + `docs/agents/cli.md` as the operative source; +- active ADR obligations: applicable, citing ADR-0049, ADR-0055, ADR-0066, + ADR-0072, and ADR-0077 through `docs/agents/domain.md`; +- Tooling Authority: applicable, with express 2026-07-26 maintainer + authorization for the exact nine protected paths and + `docs/agents/agent-instructions.md` as the operative source. + +All six Task files have `status: completed`. + +`rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only -r c27c8a1defa10405e0f159d1520ec33f0b28a7bb` +exited 0: + +```text +docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md +go.mod +go.sum +``` + +`rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only -r fbc2ae4d2eabf232b5aeaf072c423667bce4b7b2` +exited 0: + +```text +.agents/skills/roundfix/SKILL.md +docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md +skills/roundfix/SKILL.md +``` + +`rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only -r 1736dbc77683ab3704a6169a067eba09eb05d923` +exited 0: + +```text +docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md +internal/baseline/assets/setups/typescript-bun.json +internal/baseline/testdata/catalog.digest +internal/baseline/testdata/catalog.normalized.json +internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json +internal/baseline/testdata/parity-corpus/v1/manifest.json +``` + +Each protected Task commit contains only its assigned Task file and its exact +authorized paths. The current worktree delta contains only this QA report and +its evidence. + +## SG-01 — Full repository Verification + +`rtk make verify` exited 0 on build `1736dbc`: + +```text +rtk go test ./... +Go test: 2420 passed in 23 packages +rtk go test -count=1 ./skills -run 'TestNoPythonBaselineRuntime|TestThinSetupSkill|TestCheckRejectsExecutableSetupEngineArtifacts|TestRecommendedSkillsMatchLock' +Go test: 4 passed in 1 packages +rtk go run -buildvcs=false ./cmd/roundfix skills check +Roundfix skill check passed: roundfix, write-idea, write-prd, write-techspec, write-tasks, setup-context-driven, implement-task, implement-spec, brainstorming, council, business-analyst, archive-spec, qa-gate, evidence-gate +rtk go build -buildvcs=false -ldflags "-X 'roundfix/internal/app.BuildCommit=1736dbc-dirty' -X 'roundfix/internal/app.BuildTime=2026-07-26 21:05:00 -0300'" -o bin/roundfix ./cmd/roundfix +``` + +The post-gate +`rtk git -c core.fsmonitor=false status --porcelain=v1` output contains only +the current QA report and its new evidence directory. + +## J-03 — Outside-Git working-directory separation + +The built `bin/roundfix doctor` ran twice from the empty non-Git directory +`/private/tmp/roundfix-qa-0051-outside-git`. Both executions exited 1 with the +same ordered report. The environment's existing Codex signature problem made +`profiles:` and `codex:` fail, while the Repository Skill Set line was exactly: + +```text +skills: failed (Repository Skill Set readiness requires a Git repository; next: run roundfix doctor from a Git repository) +``` + +The scratch directory was empty before and after both runs. The repeated public +output was byte-stable. Independent confirmation +`rtk go test ./internal/cli -run 'TestRunDoctorMissingRepositoryRoot' -count=1` +exited 0 with one passing test; that test proves the profile seam receives the +process working directory and the Repository Skill Set checker is not called. + +## J-01 — Cooperative cancellation + +A disposable Git repository under +`/private/tmp/roundfix-qa-0051-cancel-git-1736dbc` contained the current +Repository Skill Set plus a 4 GiB external-skill file to keep the public +filesystem walk active. The built `roundfix doctor` ran in a PTY. Ctrl-C during +that walk terminated the command immediately with exit 1 and no readiness +report after the interrupt marker. The scratch repository was removed after +the probe. + +Independent confirmations: + +- `rtk go test ./skills -run 'Test(CheckRepository|SkillFolderHash)' -count=1` + exited 0: 40 tests passed. +- `rtk go test -race ./skills ./internal/cli -run + 'Test(CheckRepository|SkillFolderHash|RunDoctor)' -count=1` exited 0: 58 + tests passed across two packages. + +Together the public interrupt and named package evidence cover prompt +termination, pre-cancelled and during-read cancellation, error-chain +inspection, traversal stop, context propagation through Doctor, context-first +caller compilation, and the affected race surface. + +## J-02 — Total external hash order + +- Two fresh executions of + `rtk go test ./internal/skillhash -run 'TestSum' -count=1` each exited 0 + with eight passing tests. +- `rtk go test ./internal/baseline -run 'TestSkillsRestore' -count=1` exited 0 + with 14 passing tests. +- `rtk go test -race ./internal/skillhash ./internal/baseline -run + 'Test(Sum|SkillsRestore)' -count=1` exited 0 with 22 passing tests across two + packages. + +The repeated fresh runs independently confirm the pinned tie-corpus digest. +The named group also covers American English primary collation, normalized raw +path tie-breaking, punctuation and Unicode compatibility, path/content +negative cases, caller-order preservation, the shared Baseline consumer, and +the affected race surface. + +## J-04 — Fail-closed ownership remediation + +A disposable Git repository contained the current Repository Skill Set plus +one extra file in the Roundfix-owned `roundfix` skill and one in the externally +managed `coding-guidelines` skill. Two executions of the built Doctor command +exited 1 with byte-stable ordered output. The exact skills line was: + +```text +skills: failed (outdated: coding-guidelines, roundfix; next: roundfix skills install --target project && bunx skills experimental_install && bunx skills update -p -y) +``` + +Both public runs continued to the independent `codex:` check. A tar-stream +SHA-256 of `.agents` and `skills-lock.json` was +`a5f7207bd5acf7ff6156d2bb440a94191828cddb2a86e4e3de19545886efe7b8` +before and after both runs, proving diagnosis did not mutate the scratch +Repository Skill Set. The scratch repository was removed after capture. + +Independent confirmations: + +- `rtk go test ./internal/cli -run + 'TestRunDoctorRepositorySkillReadiness' -count=1` exited 0 with seven passing + coordinator cases. +- `rtk go test ./skills -run + 'TestCheckRepositoryRejectsSymlinkedAuthoritiesBeforeReadingTargets' + -count=1` exited 0 with four passing ownership/confinement cases. + +Those cases cover external-only remediation for a symlinked lock, target +non-read, shared/owned/external classifications, conservative unclassified +errors, the exact mixed chain, and eager line order. + +## J-05 — Tool-produced module metadata + +Fresh evidence that completed: + +- `rtk go mod edit -json` exited 0 and reports + `golang.org/x/text v0.40.0` without `Indirect`. +- `rtk go mod verify` exited 0 with `all modules verified`. +- `rtk git -c core.fsmonitor=false diff --exit-code -- go.mod go.sum` exited 0, + proving the current build has no module-file delta. +- `rtk git -c core.fsmonitor=false show + c27c8a1defa10405e0f159d1520ec33f0b28a7bb -- go.mod` shows only + `golang.org/x/text v0.40.0` moving from the indirect block to the direct + block; the selected version is unchanged. + +`rtk go mod tidy -diff` could not complete because the daemon sandbox denied +reads under `/Users/marcio/Library/Caches/go-build` with repeated +`operation not permitted` diagnostics. This is an environment constraint, not +a product diagnostic. Per the sandboxed QA contract, the command was not +retried through a cache workaround. Unblocking requires a full-access session +that can run the exact command against the host Go cache. + +## AC-01 — Repository Skill Set inspection + +The fresh J-01 commands are the complete named AC-01 group: 40 focused +`CheckRepository`/`SkillFolderHash` tests and 58 affected race-tested cases +passed. J-04 adds four explicit symlink-authority and target-non-read cases. +The public interrupt supplies the actor-level cancellation evidence. Together +they cover all six Task 03 acceptance criteria. + +## AC-02 — Doctor coordinator and public surface + +The built Doctor command ran inside the real Git repository twice during this +gate. Both runs kept the ordered six-line interface. Repository Skill Set +readiness passed: + +```text +skills: ok (39 required: 14 Roundfix-owned, 25 external) +``` + +The machine's pre-existing invalid/missing Codex code signature made +`profiles:` and `codex:` fail with the documented actions, and Doctor exited +1. This parity deviation does not prevent validating the Spec's independent, +eager Repository Skill Set contract: `skills:` still ran after `profiles:` +failed and before `codex:`. + +The invalid-input probe +`rtk ./bin/roundfix doctor --unexpected` exited 2 with: + +```text +roundfix: doctor failed: flag provided but not defined: -unexpected +Run 'roundfix doctor --help' for usage. +``` + +Focused and independent checks: + +- `rtk go test ./internal/cli -run 'Test(RunDoctor|Doctor)' -count=1`: + 18 passed. +- `rtk go test -race ./internal/cli -run 'Test(RunDoctor|Doctor)' -count=1`: + 18 passed. +- `rtk grep -n 'func TestRunDoctor' internal/cli/doctor_test.go` found all nine + Doctor behavior entry points. +- The same grep in `internal/cli/cli_test.go` exited 1 with no match. +- A fresh Git status contains only the QA report and this evidence directory. + +The public command, invalid-input probe, focused suite, race suite, source +placement, and status confirmation cover all seven Task 04 acceptance +criteria. + +## AC-03 — Synchronized Roundfix guidance + +- `rtk cmp .agents/skills/roundfix/SKILL.md skills/roundfix/SKILL.md` exited 0. +- `rtk make skills-sync-check` exited 0 with four passing tests. +- Exact-string greps found the canonical missing-Git result at line 52 and the + mixed fail-closed chain at line 58 in both Skill copies. +- `rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only -r + fbc2ae4d2eabf232b5aeaf072c423667bce4b7b2` lists only the canonical Skill, + shipped copy, and `task_05.md`. + +The current bytes, repository synchronization guard, exact public wording, and +Daemon-owned Task commit prove all five Task 05 acceptance criteria. + +## AC-04 — Reconciled derived snapshot + +- `rtk go test ./skills -run + 'TestAuthorialSkillSync/typescript-bun.json' -count=1` exited 0 with two + passing results. +- `rtk go test ./internal/baseline -run + 'Test(CatalogCompatibility|AssetsSyncCompatibilityMatchesMaintainedPythonContract|BaselineCompatibilityCorpus)' + -count=1` exited 0 with three passing owners. +- The full `rtk make verify` result in SG-01 passed after the last derived + artifact edit. +- `rtk git -c core.fsmonitor=false diff-tree --no-commit-id --name-only -r + 1736dbc77683ab3704a6169a067eba09eb05d923` lists only the five authorized + derived artifacts and `task_06.md`. +- A current `git diff --exit-code` over those five artifacts exited 0. + +The authorial, catalog, asset-sync, parity, full-gate, commit-scope, and +unchanged-current-state evidence proves all five Task 06 acceptance criteria +and Core Feature 7. + +## DOC-01 — Executable and exact user guidance + +`rtk ./bin/roundfix doctor --help` exited 0 and exposes only +`roundfix doctor`. It states that Doctor checks profiles and Repository Skill +Set independently and is offline, read-only, and mutates nothing. + +The intended-reader walkthrough in `docs/user-guide/commands.md` matches the +real commands exercised in AC-02, J-03, and J-04: + +- lines 54–56 specify one stdout line per check and nonzero on failure; +- lines 70–80 describe Repository Skill Set and Codex checks; +- lines 83–90 specify independent order and canonical missing-Git behavior; +- line 106 contains the exact owned-then-external `&&` chain; +- line 114 states the Repository Skill Set check is offline and read-only. + +`rtk go test ./internal/cli -run +'Test(RunCommandHelp|ProfilesDocumentationContractMatchesPublicGuidance)' +-count=1` exited 0 with 11 passing tests. The help, guide, Skill pair, and real +inside/outside/mixed commands agree without undocumented steps. + +## NG-01 — Non-Goals and no-mutation boundary + +`rtk git -c core.fsmonitor=false diff --name-only +53a15221530e73c910e32ae665c29e9e86aef0cd^ HEAD` lists only this active Spec, +the authorized implementation/test/docs/module/Skill paths, and the five +derived Baseline artifacts. + +`rtk git -c core.fsmonitor=false diff --exit-code +53a15221530e73c910e32ae665c29e9e86aef0cd^ HEAD -- +docs/specs/_archived skills-lock.json skills/recommended.txt .coderabbit.yaml +.roundfixrc.yml` exited 0 with no output. + +The Baseline-only path projection contains exactly: + +```text +internal/baseline/assets/setups/typescript-bun.json +internal/baseline/testdata/catalog.digest +internal/baseline/testdata/catalog.normalized.json +internal/baseline/testdata/parity-corpus/v1/fixtures/asset-sync.json +internal/baseline/testdata/parity-corpus/v1/manifest.json +``` + +`internal/cli/doctor.go` still injects +`checkSkills func(context.Context, string)` and calls it only when the +repository root is non-empty; no `HealthChecker` ownership move occurred. The +invalid-argument and help probes prove no Doctor flag or schema was added. +Public Doctor runs executed no network repair and preserved repository and +scratch state. The final worktree status contains only this QA report and its +evidence. All QA scratch repositories were removed. diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md index 7a6300a..a7c88ba 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md @@ -1,9 +1,9 @@ --- spec: 0051-doctor-readiness-contract-reconciliation date: 2026-07-26 -build: bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac +build: 1736dbc77683ab3704a6169a067eba09eb05d923 status: closed -verdict: fail +verdict: partial surfaces: [cli, backend, docs] --- @@ -11,155 +11,139 @@ surfaces: [cli, backend, docs] ## Scope and environment -Full gate for all five PRD user stories, every Task acceptance criterion, -the declared CLI/backend/docs surfaces, and the Spec Non-Goals. The real -application entry point is a binary built from commit -`bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac`; scratch repositories and -temporary application state stay outside the repository. The actor is a -Roundfix operator unless a row names a maintainer, developer, Agent, or -documentation reader. - -Platform: macOS, local filesystem, `America/Sao_Paulo`. Network access is not -part of this offline Doctor contract. Evidence belongs under -`qa/evidence/2026-07-26-doctor-readiness/`. +Full gate for all five PRD user stories, every acceptance criterion in Tasks +01–06, all seven Core Features, the CLI/backend/docs surfaces, and every +Non-Goal. The real application entry point is a binary built from commit +`1736dbc77683ab3704a6169a067eba09eb05d923`. Scratch repositories and application +state stay outside the repository. + +Actors: Roundfix operator, maintainer, developer, Agent, and documentation +reader. Platform: macOS; local filesystem; timezone +`America/Sao_Paulo`. Doctor is an offline, diagnosis-only command. Evidence for +this run belongs under +`qa/evidence/2026-07-26-doctor-readiness-1736dbc/`. + +Machine parity note: the installed Codex binary at +`/Users/marcio/.local/bin/codex` has an invalid or missing code signature, so +real Doctor runs report `profiles: failed` and `codex: failed`. The Spec's +Repository Skill Set check still executes independently and is directly +testable; no remediation was run. + +The previous same-date run on build +`bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac` ended `fail` at `make verify` +because the TypeScript/Bun Baseline snapshot carried a stale canonical +Roundfix Skill digest. Task 06 records the reproduction and derived-artifact +repair. This report replaces the required same-date path for the fresh build +while retaining that failure as run history. ## Project Constraint audit Passed. Both constraint snapshots account for identifier strategy, authentication/HTTP, active ADR obligations, and Tooling Authority with -operative `docs/agents/` sources. Daemon-owned commit -`99af3a78f9cebdfa4f4fd5619aaf267a7f2cbf23` changed only `go.mod`, `go.sum`, -and `task_01.md`; commit -`bea42860bc7b1ed0a5d83ed14eb80ebeb58565ac` changed only the canonical and -shipped Roundfix Skill pair plus `task_05.md`. The current delta contains only -this Spec's `qa/` directory. +operative `docs/agents/` sources. Daemon-owned Task commits +`c27c8a1defa10405e0f159d1520ec33f0b28a7bb`, +`fbc2ae4d2eabf232b5aeaf072c423667bce4b7b2`, and +`1736dbc77683ab3704a6169a067eba09eb05d923` contain only their assigned Task +file and exact protected allowlist. The current delta contains only this QA +report and evidence. ## Static gate -`rtk make verify` failed at `go test ./...`: 2,418 tests passed, 2 failed, -and 2 skipped across 23 packages. Both failures are -`TestAuthorialSkillSync` for `typescript-bun.json`. The snapshot records -Roundfix digest `d5269b…f59f`; the canonical Skill now computes -`1e4ea5…9146`. The exact focused reproduction fails identically, so this is a -code/product-state blocker rather than an environment constraint. +Passed. `rtk make verify` exited 0 on build `1736dbc`: 2,420 tests passed +across 23 packages, the four focused setup/skill contract tests passed, the +Repository Skill Set check passed, and the CLI build completed. A fresh status +read contains only this QA report and evidence. ## Planned matrix -The report opened with every row `pending`. The terminal statuses below record -the static-gate stop without crediting unexecuted flows. +Every row opened as `pending` before the first check. | ID | Story / criterion / sweep | Actor and surface | Entry point and steps | Expected observable, independent confirmation, persistence | Evidence | Status | | --- | --- | --- | --- | --- | --- | --- | -| PC-01 | Project Constraints and protected tooling scope | Maintainer · docs/git | Read both constraint snapshots; inspect each Task commit with `git diff-tree`; inspect the current delta | All four constraint categories are reasoned and sourced; Tasks 01 and 05 stay inside their exact protected allowlists; no unauthorized tooling path appears | `command-evidence.md` | pass | -| SG-01 | Full repository Verification | Maintainer · backend/cli/docs | Run `rtk make verify` from the repository root | Exit 0 with formatting, tests, skill synchronization, and build green; a fresh status read shows only QA artifacts | `command-evidence.md` | fail | -| J-01 | US1; Task 03 AC 1–3 and 6: cooperative cancellation | Operator · CLI/backend | Run the public cancellation path, then the focused cancellation and race groups | Cancellation exits promptly; `context.Canceled`/deadline remain discoverable; traversal stops; Doctor passes its command context; all callers use the context-first API | `command-evidence.md` | skipped | -| J-02 | US2; Task 02 AC 1–5: total external hash order | Maintainer · backend | Run the complete `TestSum` group, Baseline consumer group, and affected race group | Every tie-corpus permutation has one digest; primary collation, negative digest, non-mutation, and shared Baseline consumer contracts pass; rerun is identical | `command-evidence.md` | skipped | -| J-03 | US3; Task 04 AC 1–3: outside-Git working-directory separation | Developer · CLI | Run the built `roundfix doctor` from a non-Git scratch directory twice | Profile proof gets a usable process directory; Repository Skill Set inspection is not substituted with that directory; output contains the canonical missing-Git detail and action; both runs are stable and scratch state is unchanged | `command-evidence.md` | skipped | -| J-04 | US4; Task 03 AC 4–5 and Task 04 AC 4–5: fail-closed ownership remediation | Agent · CLI/backend | Run Doctor in a scratch Git repository that needs both repair groups; run focused typed-ownership cases | Mixed output contains one owned-then-external `&&` chain; lock symlink is external-only; shared/owned/external classifications remain distinct; later checks still run in order; rerun and state digest match | `command-evidence.md` | skipped | -| J-05 | US5; Task 01 AC 1–4: tool-produced module metadata | Maintainer · backend | Run `go mod edit -json`, `go mod tidy -diff`, `go mod verify`, and compare selected versions with the Task evidence | `golang.org/x/text v0.40.0` is direct; tidy emits no diff; modules verify; no dependency version drift; rerun remains clean | `command-evidence.md` | skipped | -| AC-01 | Task 03 AC 1–6: Repository Skill Set inspection contract | Maintainer · backend | Run the full named `CheckRepository`/`SkillFolderHash` group and affected race group | Cancellation, confinement, ownership, symlink target non-read, no-mutation, and context-first compilation all pass | `command-evidence.md` | skipped | -| AC-02 | Task 04 AC 1–7: Doctor coordinator and public surface | Operator · CLI/docs | Run Doctor inside the repository, the complete Doctor focused/race groups, and source-location checks | Git root reaches profile and skill checks; line order/stdout/stderr/exit contracts and eager checks pass; Doctor tests live in `doctor_test.go`; public run does not mutate state | `command-evidence.md` | skipped | -| AC-03 | Task 05 AC 1–5: synchronized Roundfix guidance | Agent · docs | Compare canonical/shipped skill bytes, run `make skills-sync-check`, grep both exact contracts, and inspect Task 05 commit paths | Skill files are byte-identical, sync checks pass, canonical missing-Git and mixed-chain text exists, and the protected commit allowlist is exact | `command-evidence.md` | skipped | -| DOC-01 | Core Feature 6: user guidance is executable and exact | Documentation reader · docs/CLI | Follow the Doctor guide examples through the built binary and compare help/guide/skill terms | Commands are runnable without undocumented knowledge; the canonical term, read-only promise, remediation chain, no new flag/schema, and stdout/stderr contract agree | `command-evidence.md` | skipped | -| NG-01 | Non-Goals and no-mutation boundary | Maintainer · git/CLI | Compare Spec-range changed paths, archived-Spec trees, protected excluded files, Baseline artifacts, and repository/scratch digests before and after flows | No history rewrite, archived Spec edit, upstream-managed skill/lock/recommendation edit, Baseline artifact change, config edit, new Doctor flag/schema, network repair, or automatic mutation occurred | `command-evidence.md` | skipped | +| PC-01 | Project Constraints and protected tooling scope | Maintainer · docs/git | Read both constraint snapshots and operative guides; inspect Task commits with `git diff-tree`; inspect the current delta | All four categories are reasoned and sourced; protected Tasks stay inside exact allowlists; no unauthorized tooling path appears | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| SG-01 | Full repository Verification | Maintainer · backend/cli/docs | Run `rtk make verify` from the repository root | Exit 0 with formatting, tests, synchronization guards, and build green; a fresh status read shows only QA artifacts | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| J-01 | US1; Task 03 AC 1–3 and 6: cooperative cancellation | Operator · CLI/backend | Interrupt the public Doctor command, then run the named cancellation and affected race groups | Doctor exits on cancellation; cancellation/deadline remain discoverable; traversal stops; Doctor passes its context; only context-first public APIs remain | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| J-02 | US2; Task 02 AC 1–5: total external hash order | Maintainer · backend | Run the complete `TestSum` group, Baseline consumer group, and affected race group twice | Every tie-corpus permutation has one digest; primary collation, negative digest, non-mutation, and shared consumer contracts pass identically on rerun | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| J-03 | US3; Task 04 AC 1–3: outside-Git working-directory separation | Developer · CLI | Run the built `roundfix doctor` twice from a non-Git scratch directory | Profile proof receives a usable process directory; Repository Skill Set inspection is not substituted with it; canonical missing-Git detail and action are stable; scratch state is unchanged | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| J-04 | US4; Task 03 AC 4–5 and Task 04 AC 4–5: fail-closed ownership remediation | Agent · CLI/backend | Run Doctor against mixed owned/external failure state; run typed-ownership and public coordinator cases | Mixed output is one owned-then-external `&&` chain; lock symlink is external-only; independent checks remain eager and ordered; rerun preserves state | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| J-05 | US5; Task 01 AC 1–5: tool-produced module metadata | Maintainer · backend | Run `go mod edit -json`, `go mod tidy -diff`, `go mod verify`, and compare Task commit paths | `golang.org/x/text v0.40.0` is direct; tidy emits no diff; modules verify; no version drift or unauthorized path appears; rerun remains clean | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | blocked | +| AC-01 | Task 03 AC 1–6: Repository Skill Set inspection | Maintainer · backend | Run the full named `CheckRepository`/`SkillFolderHash` group and affected race group | Cancellation, confinement, ownership, symlink target non-read, no-mutation, and context-first compilation all pass | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| AC-02 | Task 04 AC 1–7: Doctor coordinator and public surface | Operator · CLI/docs | Run Doctor inside Git, focused/race groups, invalid-argument probe, and source-location checks | Git root reaches profile and skills checks; text, order, streams, exit codes, eager checks, and test placement match; no new flag/schema or mutation appears | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| AC-03 | Task 05 AC 1–5: synchronized Roundfix guidance | Agent · docs | Compare canonical/shipped bytes, run `make skills-sync-check`, grep exact contracts, and inspect Task 05 commit paths | Skill copies are byte-identical; sync checks pass; canonical missing-Git and mixed-chain text exists; allowlist is exact | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| AC-04 | Task 06 AC 1–5 and Core Feature 7: reconciled derived snapshot | Maintainer · backend/docs | Run authorial sync, catalog/parity compatibility, digest-chain, and changed-path checks | Canonical skill digest, setup snapshot, normalized catalog/digest, parity fixture, and manifest agree; runtime/source/lock authorities remain unchanged | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| DOC-01 | Core Feature 6: executable and exact user guidance | Documentation reader · docs/CLI | Follow Doctor guide commands through the built binary and compare help, guide, and Skill terminology | Commands need no undocumented knowledge; canonical term, read-only promise, fail-closed chain, streams, and no-new-flag contract agree | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | +| NG-01 | Non-Goals and no-mutation boundary | Maintainer · git/CLI | Compare Spec-range paths, archived Specs, excluded files, Baseline scope, and repository/scratch state before and after flows | No history rewrite, archived edit, upstream skill/lock/recommendation edit, out-of-scope Baseline/config edit, HealthChecker move, new schema/flag/network call, or automatic repair occurred | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | ### Behavior probes -- Cancellation/interrupt: cancel while Doctor is active; relevant because the - primary regression kept repository inspection alive after command - cancellation. -- Deep-link/reopen: run outside Git twice from the same scratch directory; - relevant because fallback working-directory and missing-root output must not - depend on prior state. -- Invalid input: pass an unexpected Doctor argument; relevant because the Spec - forbids a new flag or schema and must preserve exit behavior. -- Repeat failure/no mutation: run the mixed-ownership failure twice and compare - scratch and repository state; relevant because Doctor is diagnosis-only. +- Cancellation/interrupt: interrupt Doctor while active because the primary + regression let Repository Skill Set work outlive command cancellation. +- Deep-link/reopen: run outside Git twice from the same scratch directory + because working-directory fallback and missing-root output must not depend + on prior state. +- Invalid input: pass an unexpected Doctor argument because the Spec forbids a + new flag or output schema and preserves public exit behavior. +- Repeat failure/no mutation: run mixed-ownership failure twice and compare + repository and scratch state because Doctor is diagnosis-only. ## Acceptance traceability -| Source | Criteria mapped | +| Source | Rows | | --- | --- | -| PRD User Stories 1–5 | J-01, J-02, J-03, J-04, J-05 | +| PRD User Stories 1–5 | J-01 through J-05 | | Task 01 AC 1–5 | J-05, PC-01 | | Task 02 AC 1–5 | J-02 | | Task 03 AC 1–6 | J-01, J-04, AC-01 | | Task 04 AC 1–7 | J-03, J-04, AC-02, DOC-01 | | Task 05 AC 1–5 | AC-03, PC-01 | -| PRD Core Features 1–6 | J-01, J-02, J-03, J-04, J-05, AC-03, DOC-01 | -| PRD Success Metrics | J-01 through J-05, SG-01, AC-01 through AC-03 | +| Task 06 AC 1–5 | AC-04, PC-01, SG-01 | +| PRD Core Features 1–7 | J-01 through J-05, AC-03, AC-04, DOC-01 | +| PRD Success Metrics | SG-01, J-01 through J-05, AC-01 through AC-04 | | PRD Non-Goals | NG-01 | ## Results | # | Story / criterion / sweep | Actor and surface | Status | Evidence | | --- | --- | --- | --- | --- | -| PC-01 | Project Constraints and protected tooling scope | Maintainer · docs/git | pass | `command-evidence.md` | -| SG-01 | Full repository Verification | Maintainer · backend/cli/docs | fail | `command-evidence.md` | -| J-01 | Cooperative cancellation | Operator · CLI/backend | skipped | Static gate failed before flow QA. | -| J-02 | Total external hash order | Maintainer · backend | skipped | Static gate failed before flow QA. | -| J-03 | Outside-Git working-directory separation | Developer · CLI | skipped | Static gate failed before flow QA. | -| J-04 | Fail-closed ownership remediation | Agent · CLI/backend | skipped | Static gate failed before flow QA. | -| J-05 | Tool-produced module metadata | Maintainer · backend | skipped | Static gate failed before flow QA. | -| AC-01 | Repository Skill Set inspection contract | Maintainer · backend | skipped | Static gate failed before flow QA. | -| AC-02 | Doctor coordinator and public surface | Operator · CLI/docs | skipped | Static gate failed before flow QA. | -| AC-03 | Synchronized Roundfix guidance | Agent · docs | skipped | Static gate failed before flow QA; its narrower `make skills-sync-check` diagnostic passed. | -| DOC-01 | Executable, exact user guidance | Documentation reader · docs/CLI | skipped | Static gate failed before flow QA. | -| NG-01 | Non-Goals and no-mutation boundary | Maintainer · git/CLI | skipped | Static gate failed before flow QA. | +| PC-01 | Project Constraints and protected tooling scope | Maintainer · docs/git | pass | Exact constraint sources, authorization, and commit paths recorded in `command-evidence.md`. | +| SG-01 | Full repository Verification | Maintainer · backend/cli/docs | pass | `rtk make verify` exited 0; 2,420 tests, skill checks, and build passed. | +| J-01 | Cooperative cancellation | Operator · CLI/backend | pass | Public Ctrl-C stopped an active Repository Skill Set walk; 40 focused and 58 race-tested cases passed. | +| J-02 | Total external hash order | Maintainer · backend | pass | Two identical eight-test hash runs, 14 consumer tests, and 22 race-tested cases passed. | +| J-03 | Outside-Git working-directory separation | Developer · CLI | pass | Two stable public runs, empty before/after scratch state, and the focused seam test passed. | +| J-04 | Fail-closed ownership remediation | Agent · CLI/backend | pass | Two stable public mixed-failure runs, unchanged state digest, seven coordinator cases, and four typed-ownership cases passed. | +| J-05 | Tool-produced module metadata | Maintainer · backend | blocked | Direct requirement, module verification, unchanged files, and version-preserving commit passed; exact tidy check hit host Go-cache sandbox denial. | +| AC-01 | Repository Skill Set inspection | Maintainer · backend | pass | Complete focused/race groups plus public cancellation and symlink probes passed. | +| AC-02 | Doctor coordinator and public surface | Operator · CLI/docs | pass | Real inside-Git and invalid-input commands, 18 focused/race cases, source placement, and no-mutation checks passed. | +| AC-03 | Synchronized Roundfix guidance | Agent · docs | pass | Byte comparison, four sync guards, exact wording, and Task commit allowlist passed. | +| AC-04 | Reconciled derived snapshot | Maintainer · backend/docs | pass | Two authorial results, three catalog/parity owners, full gate, and exact commit scope passed. | +| DOC-01 | Executable and exact user guidance | Documentation reader · docs/CLI | pass | Public help, guide walkthrough, real flows, Skill text, and 11 documentation/help tests agree. | +| NG-01 | Non-Goals and no-mutation boundary | Maintainer · git/CLI | pass | Spec-range, excluded-path, exact Baseline-scope, dependency seam, no-mutation, and scratch cleanup checks passed. | ## Findings -### BUG-001 — Baseline setup snapshot rejects the synchronized Roundfix Skill - -Impact: Blocks-Completion. Severity: Critical. Priority: P0. -Persona: maintainer. Journey step: SG-01, repository Verification. - -Expected: `rtk make verify` exits 0 on the assembled Spec build. - -Actual: `TestAuthorialSkillSync/typescript-bun.json` reports snapshot digest -`d5269bac642a8ef4c1c8439eff308594e36ad786fe14d9f3047a8e731998f59f` -while the canonical Roundfix Skill computes -`1e4ea59e0d6e553e42c6c63e16ad95165a78be8bbf31b8e0cd8b56ce13cc9146`. - -Reproduction: - -1. From the repository root, run `rtk make verify`. -2. Independently run `rtk proxy go test ./skills -run - 'TestAuthorialSkillSync/typescript-bun.json' -count=1 -v`. -3. Observe the same digest mismatch and non-zero exit. - -Root cause: Task 05 changed the canonical/shipped Roundfix Skill bytes without -refreshing the Baseline setup snapshot digest. Its declared -`make skills-sync-check` gate passes because that target does not include -`TestAuthorialSkillSync`. The Spec simultaneously excludes Baseline artifact -changes, so the current approved scope cannot satisfy the repository's full -Verification invariant. - -Affected rows: SG-01 directly; all remaining flow and criterion rows are -skipped under the mandatory code-failure stop. +No product findings. The installed Codex signature problem is an environment +readiness result surfaced correctly by Doctor, not a Spec regression. ## Blocked and skipped -Skipped J-01 through NG-01 after SG-01 failed. The QA gate requires flow -execution to stop on a code-caused static failure; these are not environment -blocks and receive no pass credit. - -Unblocking action: reconcile the Spec's Baseline Non-Goal and tooling -authorization with the repository's authorial-skill snapshot invariant, -implement the authorized correction, and rerun this gate from `rtk make -verify`. +J-05 is blocked by the daemon sandbox: `rtk go mod tidy -diff` cannot read +`/Users/marcio/Library/Caches/go-build` and reports `operation not permitted`. +Run the exact command in a full-access session to close this row. No row is +skipped. ## Coverage -Planned coverage remains complete: 5/5 stories, 28/28 Task acceptance -criteria, 6/6 Core Features, all Non-Goals, and four behavior probes are -mapped. Executed coverage stopped at the required static gate: 1 row passed, -1 failed, 10 skipped; 0/5 real user-story journeys ran and no skipped Task -criterion received pass credit. +Planned coverage was complete: 5/5 user stories, 33/33 Task acceptance +criteria, 7/7 Core Features, all Non-Goals, and four risk-based behavior +probes. Executed: 13/13 rows; 12 pass, 1 blocked, 0 fail, 0 skipped. + +Four user stories passed and one is blocked only on the exact tidy command. +Thirty-two Task acceptance criteria passed and one is blocked. Six Core +Features passed and one shares the tidy block. Every Non-Goal and all four +behavior probes were exercised. ## Final verdict -Fail. The assembled build cannot pass repository Verification because the -canonical Roundfix Skill and the `typescript-bun.json` Baseline snapshot carry -different content digests. Counts: 1 pass, 1 fail, 0 blocked, 10 skipped; one -Blocks-Completion finding. +Partial. Run `rtk go mod tidy -diff` in a full-access session and rerun J-05; +all other rows pass. Counts: 12 pass, 0 fail, 1 blocked, 0 skipped; 0 findings +at every user-impact tier. From 817828ded76b014eb919f6a9eb51b1c32aa17c17 Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 21:23:39 -0300 Subject: [PATCH 45/48] docs: close Doctor readiness QA after full-access proof --- .../command-evidence.md | 13 ++++----- .../qa/qa-report-2026-07-26.md | 27 +++++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md index 155735f..502e8a5 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md @@ -172,12 +172,13 @@ Fresh evidence that completed: `golang.org/x/text v0.40.0` moving from the indirect block to the direct block; the selected version is unchanged. -`rtk go mod tidy -diff` could not complete because the daemon sandbox denied -reads under `/Users/marcio/Library/Caches/go-build` with repeated -`operation not permitted` diagnostics. This is an environment constraint, not -a product diagnostic. Per the sandboxed QA contract, the command was not -retried through a cache workaround. Unblocking requires a full-access session -that can run the exact command against the host Go cache. +The daemon sandbox could not complete `rtk go mod tidy -diff` because it +denied reads under `/Users/marcio/Library/Caches/go-build`. After the Run +closed and its commits were integrated, the supervisor executed the same +literal command from the integrated branch in a full-access session. It exited +0 with no output. No alternate cache, environment override, dependency change, +or module-file write was used. This closes the environment-only block with the +exact required evidence. ## AC-01 — Repository Skill Set inspection diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md index a7c88ba..52cefaa 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md +++ b/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md @@ -3,7 +3,7 @@ spec: 0051-doctor-readiness-contract-reconciliation date: 2026-07-26 build: 1736dbc77683ab3704a6169a067eba09eb05d923 status: closed -verdict: partial +verdict: pass surfaces: [cli, backend, docs] --- @@ -66,7 +66,7 @@ Every row opened as `pending` before the first check. | J-02 | US2; Task 02 AC 1–5: total external hash order | Maintainer · backend | Run the complete `TestSum` group, Baseline consumer group, and affected race group twice | Every tie-corpus permutation has one digest; primary collation, negative digest, non-mutation, and shared consumer contracts pass identically on rerun | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | | J-03 | US3; Task 04 AC 1–3: outside-Git working-directory separation | Developer · CLI | Run the built `roundfix doctor` twice from a non-Git scratch directory | Profile proof receives a usable process directory; Repository Skill Set inspection is not substituted with it; canonical missing-Git detail and action are stable; scratch state is unchanged | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | | J-04 | US4; Task 03 AC 4–5 and Task 04 AC 4–5: fail-closed ownership remediation | Agent · CLI/backend | Run Doctor against mixed owned/external failure state; run typed-ownership and public coordinator cases | Mixed output is one owned-then-external `&&` chain; lock symlink is external-only; independent checks remain eager and ordered; rerun preserves state | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | -| J-05 | US5; Task 01 AC 1–5: tool-produced module metadata | Maintainer · backend | Run `go mod edit -json`, `go mod tidy -diff`, `go mod verify`, and compare Task commit paths | `golang.org/x/text v0.40.0` is direct; tidy emits no diff; modules verify; no version drift or unauthorized path appears; rerun remains clean | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | blocked | +| J-05 | US5; Task 01 AC 1–5: tool-produced module metadata | Maintainer · backend | Run `go mod edit -json`, `go mod tidy -diff`, `go mod verify`, and compare Task commit paths | `golang.org/x/text v0.40.0` is direct; tidy emits no diff; modules verify; no version drift or unauthorized path appears; rerun remains clean | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | | AC-01 | Task 03 AC 1–6: Repository Skill Set inspection | Maintainer · backend | Run the full named `CheckRepository`/`SkillFolderHash` group and affected race group | Cancellation, confinement, ownership, symlink target non-read, no-mutation, and context-first compilation all pass | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | | AC-02 | Task 04 AC 1–7: Doctor coordinator and public surface | Operator · CLI/docs | Run Doctor inside Git, focused/race groups, invalid-argument probe, and source-location checks | Git root reaches profile and skills checks; text, order, streams, exit codes, eager checks, and test placement match; no new flag/schema or mutation appears | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | | AC-03 | Task 05 AC 1–5: synchronized Roundfix guidance | Agent · docs | Compare canonical/shipped bytes, run `make skills-sync-check`, grep exact contracts, and inspect Task 05 commit paths | Skill copies are byte-identical; sync checks pass; canonical missing-Git and mixed-chain text exists; allowlist is exact | `evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md` | pass | @@ -111,7 +111,7 @@ Every row opened as `pending` before the first check. | J-02 | Total external hash order | Maintainer · backend | pass | Two identical eight-test hash runs, 14 consumer tests, and 22 race-tested cases passed. | | J-03 | Outside-Git working-directory separation | Developer · CLI | pass | Two stable public runs, empty before/after scratch state, and the focused seam test passed. | | J-04 | Fail-closed ownership remediation | Agent · CLI/backend | pass | Two stable public mixed-failure runs, unchanged state digest, seven coordinator cases, and four typed-ownership cases passed. | -| J-05 | Tool-produced module metadata | Maintainer · backend | blocked | Direct requirement, module verification, unchanged files, and version-preserving commit passed; exact tidy check hit host Go-cache sandbox denial. | +| J-05 | Tool-produced module metadata | Maintainer · backend | pass | Direct requirement, module verification, unchanged files, version-preserving commit, and the exact full-access tidy check passed. | | AC-01 | Repository Skill Set inspection | Maintainer · backend | pass | Complete focused/race groups plus public cancellation and symlink probes passed. | | AC-02 | Doctor coordinator and public surface | Operator · CLI/docs | pass | Real inside-Git and invalid-input commands, 18 focused/race cases, source placement, and no-mutation checks passed. | | AC-03 | Synchronized Roundfix guidance | Agent · docs | pass | Byte comparison, four sync guards, exact wording, and Task commit allowlist passed. | @@ -126,24 +126,21 @@ readiness result surfaced correctly by Doctor, not a Spec regression. ## Blocked and skipped -J-05 is blocked by the daemon sandbox: `rtk go mod tidy -diff` cannot read -`/Users/marcio/Library/Caches/go-build` and reports `operation not permitted`. -Run the exact command in a full-access session to close this row. No row is -skipped. +None. The daemon sandbox initially blocked J-05 from reading the host Go +cache. The exact `rtk go mod tidy -diff` command was then executed from the +integrated branch in a full-access supervisor session and exited 0 with no +output or module-file mutation. ## Coverage Planned coverage was complete: 5/5 user stories, 33/33 Task acceptance criteria, 7/7 Core Features, all Non-Goals, and four risk-based behavior -probes. Executed: 13/13 rows; 12 pass, 1 blocked, 0 fail, 0 skipped. +probes. Executed: 13/13 rows; 13 pass, 0 blocked, 0 fail, 0 skipped. -Four user stories passed and one is blocked only on the exact tidy command. -Thirty-two Task acceptance criteria passed and one is blocked. Six Core -Features passed and one shares the tidy block. Every Non-Goal and all four -behavior probes were exercised. +All five user stories, all 33 Task acceptance criteria, and all seven Core +Features passed. Every Non-Goal and all four behavior probes were exercised. ## Final verdict -Partial. Run `rtk go mod tidy -diff` in a full-access session and rerun J-05; -all other rows pass. Counts: 12 pass, 0 fail, 1 blocked, 0 skipped; 0 findings -at every user-impact tier. +Pass. Counts: 13 pass, 0 fail, 0 blocked, 0 skipped; 0 findings at every +user-impact tier. From 9a6b7f9433b9779afe75f38d833b780ceb2555ed Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 21:24:00 -0300 Subject: [PATCH 46/48] chore: archive Doctor readiness contract reconciliation --- .../0051-doctor-readiness-contract-reconciliation/_prd.md | 5 ++++- .../0051-doctor-readiness-contract-reconciliation/_tasks.md | 0 .../_techspec.md | 0 .../2026-07-26-doctor-readiness-1736dbc/command-evidence.md | 0 .../evidence/2026-07-26-doctor-readiness/command-evidence.md | 0 .../qa/qa-report-2026-07-26.md | 0 .../0051-doctor-readiness-contract-reconciliation/task_01.md | 0 .../0051-doctor-readiness-contract-reconciliation/task_02.md | 0 .../0051-doctor-readiness-contract-reconciliation/task_03.md | 0 .../0051-doctor-readiness-contract-reconciliation/task_04.md | 0 .../0051-doctor-readiness-contract-reconciliation/task_05.md | 0 .../0051-doctor-readiness-contract-reconciliation/task_06.md | 0 12 files changed, 4 insertions(+), 1 deletion(-) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/_prd.md (98%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/_tasks.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/_techspec.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/task_01.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/task_02.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/task_03.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/task_04.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/task_05.md (100%) rename docs/specs/{ => _archived}/0051-doctor-readiness-contract-reconciliation/task_06.md (100%) diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/_prd.md similarity index 98% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/_prd.md index bd98113..98b3c1b 100644 --- a/docs/specs/0051-doctor-readiness-contract-reconciliation/_prd.md +++ b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/_prd.md @@ -1,10 +1,13 @@ --- spec: 0051-doctor-readiness-contract-reconciliation -status: active +status: archived created: 2026-07-26 surfaces: [cli, backend, docs] +archived: "2026-07-26" +source_slug: 0051-doctor-readiness-contract-reconciliation --- + # Doctor Readiness Contract Reconciliation A follow-up review found that the Doctor Command's Repository Skill Set check diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/_tasks.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/_tasks.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/_tasks.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/_techspec.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/_techspec.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/_techspec.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness-1736dbc/command-evidence.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/qa/evidence/2026-07-26-doctor-readiness/command-evidence.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/qa/qa-report-2026-07-26.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_01.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/task_01.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_01.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_02.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/task_02.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_02.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_03.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/task_03.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_03.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_04.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/task_04.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_04.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_05.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/task_05.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_05.md diff --git a/docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md b/docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_06.md similarity index 100% rename from docs/specs/0051-doctor-readiness-contract-reconciliation/task_06.md rename to docs/specs/_archived/0051-doctor-readiness-contract-reconciliation/task_06.md From 75795f7718567bdef54f3ddf8e4fc32179d37ecf Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 23:03:30 -0300 Subject: [PATCH 47/48] fix: resolve Roundfix batch 001 --- internal/cli/cli.go | 2 +- internal/cli/cli_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 882014c..a2653ab 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -3778,7 +3778,7 @@ Options: roundfix doctor Diagnoses this machine's readiness for Runs. Checks Node.js, the minimum -supported acpx version, the effective adapter, and required +supported acpx version, the effective adapter, the required Agent Selection Profiles, the Repository Skill Set, and codex runtime hygiene. The aggregate profiles: line exact-proves every distinct tuple. The skills: line compares Roundfix-owned artifacts with the running binary and external diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 724eabc..b10ab0b 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -264,7 +264,7 @@ func TestRunCommandHelp(t *testing.T) { { name: "doctor", args: []string{"doctor", "--help"}, - contains: []string{"roundfix doctor", "Repository Skill Set", "skills:", "codex runtime hygiene", "offline", "read-only", "mutates nothing"}, + contains: []string{"roundfix doctor", "the effective adapter, the required\nAgent Selection Profiles", "Repository Skill Set", "skills:", "codex runtime hygiene", "offline", "read-only", "mutates nothing"}, }, { name: "upgrade", From 072ae5ba1e95db870ce10b695fab7a6d51ba4b2a Mon Sep 17 00:00:00 2001 From: Marcio Altoe Date: Sun, 26 Jul 2026 23:03:40 -0300 Subject: [PATCH 48/48] docs: review rounds for pr 37 --- .../_reviews/pr-37/round-001/issue_001.md | 121 ++++++++++++++++++ .../_reviews/pr-37/round-001/issue_002.md | 90 +++++++++++++ .../_reviews/pr-37/round-001/issue_003.md | 87 +++++++++++++ .../_reviews/pr-37/round-001/issue_004.md | 115 +++++++++++++++++ docs/specs/_reviews/pr-37/round-001/round.md | 14 ++ 5 files changed, 427 insertions(+) create mode 100644 docs/specs/_reviews/pr-37/round-001/issue_001.md create mode 100644 docs/specs/_reviews/pr-37/round-001/issue_002.md create mode 100644 docs/specs/_reviews/pr-37/round-001/issue_003.md create mode 100644 docs/specs/_reviews/pr-37/round-001/issue_004.md create mode 100644 docs/specs/_reviews/pr-37/round-001/round.md diff --git a/docs/specs/_reviews/pr-37/round-001/issue_001.md b/docs/specs/_reviews/pr-37/round-001/issue_001.md new file mode 100644 index 0000000..e613d4d --- /dev/null +++ b/docs/specs/_reviews/pr-37/round-001/issue_001.md @@ -0,0 +1,121 @@ +--- +source: coderabbit +pr: "37" +round: 1 +round_created_at: "2026-07-27T01:53:02Z" +status: invalid +head_repository: marcioaltoe/roundfix +head_branch: ma/0036-doctor-skill-readiness +head_sha: 9a6b7f9433b9779afe75f38d833b780ceb2555ed +file: .agents/skills/golang-testing/SKILL.md +line: 88 +severity: minor +author: coderabbitai[bot] +source_ref: thread:PRRT_kwDOS0qyts6T6oNH,comment:PRRC_kwDOS0qyts7Zy0Nf +review_hash: 26882155352a19baadcc93e050cd5e07df4c8e39682273b026790f6a893d9691 +duplicate_of: "" +source_review_id: "4783144632" +source_review_submitted_at: "2026-07-27T01:52:01Z" +--- + +# Issue 001: _ Maintainability & Code Quality_ _ Minor_ _ Quick win_ + +## Review Comment + +_📐 Maintainability & Code Quality_ | _🟡 Minor_ | _⚡ Quick win_ + +**Specify a language for the fenced block.** + +The fence at Line 77 has no language identifier, triggering MD040. Use `text` because this block documents filename mappings. + +
+Proposed fix + +```diff +-``` ++```text +``` +
+ + + +
+📝 Committable suggestion + +> ‼️ **IMPORTANT** +> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. + +```suggestion +Name the test file after the source file it tests, not after the function or method under test. Go's convention is one test file per source file (`foo.go` -> `foo_test.go`), because tools (`go test`, coverage reports, IDE "jump to test" navigation, `gotests`) and reviewers all resolve tests by source file, not by symbol. A source file usually declares several functions/methods; splitting its tests by symbol name scatters them across many files and breaks that file-to-file mapping. + +``` + +
+ + + +
+🧰 Tools + +
+🪛 LanguageTool + +[style] ~85-~85: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase. +Context: ...d be helloworld_test.go ``` Exception: very large source files MAY be split into multiple... + +(EN_WEAK_ADJECTIVE) + +
+
+🪛 markdownlint-cli2 (0.23.0) + +[warning] 77-77: Fenced code blocks should have a language specified + +(MD040, fenced-code-language) + +
+ +
+ +
+🤖 Prompt for AI Agents + +```` +Verify each finding against current code. Fix only still-valid issues, skip the +rest with a brief reason, keep changes minimal, and validate. + +In @.agents/skills/golang-testing/SKILL.md around lines 75 - 88, Specify the +fenced code block in the documentation around the test-file naming examples with +the text language identifier, changing the opening fence to ```text while +preserving the filename mapping content. +```` + +
+ + + + + + + +_Source: Linters/SAST tools_ + + + +## Triage + +- Decision: `INVALID` +- Notes: The missing fence language is present in the fetched upstream + artifact, but `.agents/skills/golang-testing/SKILL.md` is an externally + managed Repository Skill Set member recorded in `skills-lock.json`, not a + repository-owned skill. `docs/agents/skill-dispatch.md` prohibits adapting + upstream-managed skills locally and requires proposed changes to go to their + upstream source. Changing this snapshot would also invalidate its recorded + immutable hash, so this Batch must not apply the suggested local edit. + +## Verification + +- Inspected the `golang-testing` entry in `skills-lock.json`; it records + `marcioaltoe/skills` as the source and the current snapshot hash. +- Inspected commit `672eb5c`, which refreshed this file and its lock hash + together from the external Repository Skill Set source. diff --git a/docs/specs/_reviews/pr-37/round-001/issue_002.md b/docs/specs/_reviews/pr-37/round-001/issue_002.md new file mode 100644 index 0000000..92c0074 --- /dev/null +++ b/docs/specs/_reviews/pr-37/round-001/issue_002.md @@ -0,0 +1,90 @@ +--- +source: coderabbit +pr: "37" +round: 1 +round_created_at: "2026-07-27T01:53:02Z" +status: invalid +head_repository: marcioaltoe/roundfix +head_branch: ma/0036-doctor-skill-readiness +head_sha: 9a6b7f9433b9779afe75f38d833b780ceb2555ed +file: .agents/skills/golang-testing/SKILL.md +line: 170 +severity: major +author: coderabbitai[bot] +source_ref: thread:PRRT_kwDOS0qyts6T6oNU,comment:PRRC_kwDOS0qyts7Zy0Nt +review_hash: f30131620eec8f46f02219ff8447dc907cfb0f2c25f5e199eb665de1a3c30c2a +duplicate_of: "" +source_review_id: "4783144632" +source_review_submitted_at: "2026-07-27T01:52:02Z" +--- + +# Issue 002: _ Maintainability & Code Quality_ _ Major_ _ Quick win_ + +## Review Comment + +_📐 Maintainability & Code Quality_ | _🟠 Major_ | _⚡ Quick win_ + +**Remove or reframe the Testify guidance for this repository.** + +The repository-wide `**/*_test.go` rule requires only the standard-library `testing` package and explicitly forbids introducing testify. This newly added section presents Testify as supported, so it conflicts with the authoritative testing policy. Rewrite the examples using `t.Errorf`/`t.Fatalf`, or clearly mark this guidance as non-applicable here. + +
+🧰 Tools + +
+🪛 LanguageTool + +[style] ~145-~145: Consider shortening this phrase to just ‘whether’, unless you mean ‘regardless of whether’. +Context: ...y hiding which case broke. This happens whether or not the subtest calls `t.Parallel()`. ```g... + +(WHETHER) + +
+ +
+ +
+🤖 Prompt for AI Agents + +``` +Verify each finding against current code. Fix only still-valid issues, skip the +rest with a brief reason, keep changes minimal, and validate. + +In @.agents/skills/golang-testing/SKILL.md around lines 143 - 170, Reframe the +“Assert Scope Leaking into Subtests” section to comply with the repository’s +standard-library-only testing policy. Remove Testify references and rewrite both +examples and the verification guidance using subtest-local testing.T methods +such as t.Errorf or t.Fatalf, while preserving the requirement that failures are +attributed to the correct subtest. +``` + +
+ + + + + + + +_Source: Coding guidelines_ + + + +## Triage + +- Decision: `INVALID` +- Notes: This finding treats a generic, externally managed skill snapshot as + repository-authored guidance. The repository's narrower Go rules in + `docs/agents/go.md` and `docs/agents/specific-repository.md` already require + stdlib `testing` and forbid Testify, so those rules govern Roundfix code even + when an upstream skill describes Testify usage for other repositories. + Rewriting the snapshot locally would violate the upstream-skill ownership + and immutable-hash contracts in `docs/agents/skill-dispatch.md` and + `skills-lock.json`. No repository change is appropriate. + +## Verification + +- Inspected the complete upstream `golang-testing` snapshot and the + repository's narrower Go testing rules. +- Inspected the `golang-testing` lock entry and commit `672eb5c`, which refresh + the external skill artifact and its hash as one immutable snapshot. diff --git a/docs/specs/_reviews/pr-37/round-001/issue_003.md b/docs/specs/_reviews/pr-37/round-001/issue_003.md new file mode 100644 index 0000000..5f0620f --- /dev/null +++ b/docs/specs/_reviews/pr-37/round-001/issue_003.md @@ -0,0 +1,87 @@ +--- +source: coderabbit +pr: "37" +round: 1 +round_created_at: "2026-07-27T01:53:02Z" +status: invalid +head_repository: marcioaltoe/roundfix +head_branch: ma/0036-doctor-skill-readiness +head_sha: 9a6b7f9433b9779afe75f38d833b780ceb2555ed +file: .roundfixrc.yml +line: 20 +severity: major +author: coderabbitai[bot] +source_ref: thread:PRRT_kwDOS0qyts6T6oNW,comment:PRRC_kwDOS0qyts7Zy0Nv +review_hash: d67c146ea165d91e2d59a01280b64b3f03a4fe346fe43dc4d8e82629e2f6e743 +duplicate_of: "" +source_review_id: "4783144632" +source_review_submitted_at: "2026-07-27T01:52:02Z" +--- + +# Issue 003: _ Maintainability & Code Quality_ _ Major_ _ Heavy lift_ + +## Review Comment + +_📐 Maintainability & Code Quality_ | _🟠 Major_ | _🏗️ Heavy lift_ + +**Move concrete model fallbacks out of the project config.** + +These entries bind Roundfix project profiles to the runtime-owned `gpt-5.6-terra` model. Keep profiles capability-oriented and resolve provider/model fallback selection from runtime-owned configuration instead. + +As per coding guidelines, “Do not store runtime-owned model configuration, credentials, or adapter settings in Roundfix Project or User Config.” + + + + + + +Also applies to: 29-29, 47-47, 56-56 + +
+🤖 Prompt for AI Agents + +``` +Verify each finding against current code. Fix only still-valid issues, skip the +rest with a brief reason, keep changes minimal, and validate. + +In @.roundfixrc.yml at line 20, Remove the concrete gpt-5.6-terra model +assignments from all affected Roundfix profiles in .roundfixrc.yml, including +the entries referenced at lines 20, 29, 47, and 56. Keep the profiles +capability-oriented and rely on runtime-owned configuration for provider/model +fallback resolution. +``` + +
+ + + + + + + +_Source: Coding guidelines_ + + + +## Triage + +- Decision: `INVALID` +- Notes: The requested removal contradicts Roundfix's accepted Agent Selection + architecture. `CONTEXT.md` defines every Agent Selection as an exact ACP + Runtime, Agent Model, and reasoning-effort tuple, and defines each Agent + Selection Profile as one Preferred Selection plus a non-empty Fallback + Chain. ADR-0049 explicitly states that exact runtime, official model + identifier, and reasoning effort remain Roundfix-owned and that Project + Config may provide complete higher-precedence profiles. The concrete Terra + fallback entries therefore implement the repository's authoritative, + reproducible Project Config policy; removing only their model fields would + make the profiles incomplete rather than capability-oriented. + +## Verification + +- Inspected `CONTEXT.md` definitions for Agent Selection, Agent Selection + Profile, Fallback Chain, and Default Agent Model. +- Inspected accepted ADR-0049 and ADR-0055; both require exact configured + tuples and reject inheritance from runtime-local model configuration. +- Inspected commit `8564e67`, which deliberately updated the four project + fallback selections to `gpt-5.6-terra`. diff --git a/docs/specs/_reviews/pr-37/round-001/issue_004.md b/docs/specs/_reviews/pr-37/round-001/issue_004.md new file mode 100644 index 0000000..068ce64 --- /dev/null +++ b/docs/specs/_reviews/pr-37/round-001/issue_004.md @@ -0,0 +1,115 @@ +--- +source: coderabbit +pr: "37" +round: 1 +round_created_at: "2026-07-27T01:53:02Z" +status: resolved +head_repository: marcioaltoe/roundfix +head_branch: ma/0036-doctor-skill-readiness +head_sha: 9a6b7f9433b9779afe75f38d833b780ceb2555ed +file: internal/cli/cli.go +line: 3782 +severity: minor +author: coderabbitai[bot] +source_ref: thread:PRRT_kwDOS0qyts6T6oNY,comment:PRRC_kwDOS0qyts7Zy0Nw +review_hash: 8a59c87afe1e2d66f3bb4a1180c3b9facbd58b6ba39dc9de619aee2e03b5b9e0 +duplicate_of: "" +source_review_id: "4783144632" +source_review_submitted_at: "2026-07-27T01:52:02Z" +--- + +# Issue 004: _ Maintainability & Code Quality_ _ Minor_ _ Quick win_ + +## Review Comment + +_📐 Maintainability & Code Quality_ | _🟡 Minor_ | _⚡ Quick win_ + +**Stray `and` breaks the enumeration in `doctor` help text.** + +The list now reads "…the effective adapter, and required Agent Selection Profiles, the Repository Skill Set, and codex runtime hygiene", leaving a mid-list conjunction. Drop the first `and`. `cli_test.go` only asserts substrings, so this change is test-safe. + + + + + +
+✏️ Proposed wording fix + +```diff + Diagnoses this machine's readiness for Runs. Checks Node.js, the minimum +-supported acpx version, the effective adapter, and required +-Agent Selection Profiles, the Repository Skill Set, and codex runtime hygiene. ++supported acpx version, the effective adapter, the required ++Agent Selection Profiles, the Repository Skill Set, and codex runtime hygiene. +``` +
+ +As per coding guidelines: "Help text must be concise and truthful and reflect implemented behavior". + + + +
+📝 Committable suggestion + +> ‼️ **IMPORTANT** +> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. + +```suggestion +Diagnoses this machine's readiness for Runs. Checks Node.js, the minimum +supported acpx version, the effective adapter, the required +Agent Selection Profiles, the Repository Skill Set, and codex runtime hygiene. +``` + +
+ + + +
+🤖 Prompt for AI Agents + +``` +Verify each finding against current code. Fix only still-valid issues, skip the +rest with a brief reason, keep changes minimal, and validate. + +In `@internal/cli/cli.go` around lines 3780 - 3782, Update the doctor help text +near the readiness description to remove the stray conjunction before “required +Agent Selection Profiles,” leaving the enumeration as “the effective adapter, +required Agent Selection Profiles, the Repository Skill Set, and codex runtime +hygiene.” +``` + +
+ + + + + + + +_Source: Coding guidelines_ + + + +## Triage + +- Decision: `VALID` +- Notes: The current `doctor --help` output reproduced the malformed + mid-enumeration conjunction. Fixed the public help text by changing + "the effective adapter, and required" to "the effective adapter, the + required". + +## Resolution + +- Updated `internal/cli/cli.go` with the grammatical readiness-check + enumeration. +- Strengthened `TestRunCommandHelp/doctor` in + `internal/cli/cli_test.go` to assert the corrected public phrase. + +## Verification + +- `rtk go run ./cmd/roundfix doctor --help` — reproduced the original malformed + phrase before the fix. +- `rtk go test ./internal/cli -run 'TestRunCommandHelp/doctor' -count=1` — + failed with the regression assertion before the production edit, then passed + after the fix (`2 passed`). +- `rtk go test ./internal/cli -count=1` — passed (`752 passed`). diff --git a/docs/specs/_reviews/pr-37/round-001/round.md b/docs/specs/_reviews/pr-37/round-001/round.md new file mode 100644 index 0000000..d44ca2a --- /dev/null +++ b/docs/specs/_reviews/pr-37/round-001/round.md @@ -0,0 +1,14 @@ +--- +source: coderabbit +pr: "37" +round: 1 +round_created_at: "2026-07-27T01:53:02Z" +head_repository: marcioaltoe/roundfix +head_branch: ma/0036-doctor-skill-readiness +head_sha: 9a6b7f9433b9779afe75f38d833b780ceb2555ed +issue_count: 4 +--- + +# Round 001 + +Fetched 4 Review Issue(s).