Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion .claude/research/harness-upgrade-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,67 @@ This table is exactly the decoding logic `/setup-harness` applies in phase 4:

- [x] **7j β€” Two-layer config (plan section A).** βœ… **Landed 2026-07-10 β€” agentic-dev-workflow PR #8** (`feature/stage7j-two-layer-config`, base main, MERGED). Section A's global/project config layering β€” specced in the grilled decisions but **never scoped into the 7a–7i checklist** (a real build gap, surfaced when the "one PC, same conventions on every project" use case came up; it had been silently absent while the plan read "complete"). New **`core/setup/config.mjs`** β€” the resolver, mirroring the hook-policy layering: `resolveConfig` = global (`~/.claude/workflow.config.yaml`) merged with the nearest project `workflow.config.yaml`, **project wins**; `splitConfig` partitions a full config into `{ global, project }` **losslessly** (tracker host/driver β†’ global, repo/labels β†’ project); CLI `resolve`/`split`. **`generate.mjs`** β€” `planGlobalArtifacts` writes the machine-wide layer (global `workflow.config.yaml` + shared `hooks.policy.json` + tool-defaults rule, no `repos[]`/`CLAUDE.md`/lint); `planArtifacts` gains `configScope:"project"` for the lean project slice (default `"full"` unchanged). **`bootstrap.mjs`** β€” `wireEnforcementHooks` wires the git/content guardrails machine-wide (PreToolUse/Bash + PostToolUse/Edit|Write|MultiEdit), opt-in via `--enforce-globally` or automatic once a global `hooks.policy.json` exists (`loadPolicy` already merged the global policy; now the hooks actually fire globally). Skills resolve the **effective** (global+project) config β€” all 54 adapters regenerated; `setup-harness.md` gained the `--global` flow + merge semantics; README documents "one PC, same conventions everywhere". Proof **`scripts/test-config-layers.mjs`** (resolve/override, lossless split, on-disk resolution honoring `CLAUDE_CONFIG_DIR`, global generation, lean project slice, `loadPolicy` layering, enforcement wiring incl. the bootstrap opt-in) wired into CI (now **9 gates**). LF-only, no NUL bytes.

**βœ… Stage 7 COMPLETE (2026-07-10) β€” and with it the entire harness-upgrade plan.** All checklist boxes (7a–7j) are ticked; the pack (`thomasluizon/agentic-dev-workflow`) is the smart, AI-installed harness the plan set out to build: `bootstrap` β†’ `/setup-harness` (research β†’ interview β†’ doc decode β†’ tier every rule β†’ gated decomposition β†’ generate + self-verify) β†’ `/update-harness` (monthly web-grounded staleness), all dual-target (Claude Code + opencode), with the **two-layer config** (global machine defaults + project overrides, section A) so a uniform PC is configured once, and zero project/policy leakage enforced in CI. The one thing that genuinely needs a human at the keyboard: run `/setup-harness` interactively on your real machine to generate its overlay (the grill asks you questions no fixture can answer).
**βœ… Stage 7 COMPLETE (2026-07-10) β€” and with it the entire harness-upgrade plan as originally specced.** All checklist boxes (7a–7j) are ticked; the pack (`thomasluizon/agentic-dev-workflow`) is the smart, AI-installed harness the plan set out to build: `bootstrap` β†’ `/setup-harness` (research β†’ interview β†’ doc decode β†’ tier every rule β†’ gated decomposition β†’ generate + self-verify) β†’ `/update-harness` (monthly web-grounded staleness), all dual-target (Claude Code + opencode), with the **two-layer config** (global machine defaults + project overrides, section A) so a uniform PC is configured once, and zero project/policy leakage enforced in CI. The one thing that genuinely needs a human at the keyboard: run `/setup-harness` interactively on your real machine to generate its overlay (the grill asks you questions no fixture can answer). **A new requirement surfaced after this β€” Stage 8 (repo-clean mode), now βœ… COMPLETE (2026-07-10).**

## Stage 8 β€” Repo-clean (global-only) install mode βœ… COMPLETE (2026-07-10 β€” agentic-dev-workflow PR #10, MERGED)

**βœ… Stage 8 COMPLETE (2026-07-10) β€” the harness now supports BOTH in-repo and repo-clean installs.** Landed via **agentic-dev-workflow PR #10** (`feature/stage8-repo-clean-install`, base main, squash-MERGED; agentic main is UNPROTECTED so no `--admin`). All 14 locked decisions built exactly as specced. New **`core/hooks/logic/store.mjs`** (dependency-free, hot-path-safe): `resolveRepoRoot` (pure-fs `.git` walk-up; a linked-worktree `.git` FILE is followed via `gitdir:`+`commondir` to the MAIN worktree root so all worktrees share one entry β€” verified against real `git worktree` in the proof), deterministic `slugForPath` (mirrors CC's `~/.claude/projects/<encoded>`), `storeRoot`/`storeEntryDir`, `index.json` + `registerStoreEntry`, `readStorePolicy`/`readStoreConfigText`/`buildInjectedContext` (the shared facts+rules assembly BOTH injectors call). **Layering `DEFAULT < global < store < in-repo`** for `loadPolicy` (hooks/logic) AND `resolveConfig` (setup) β€” in-repo wins; a mode-1 repo with no store entry is untouched; config + enforcement dual-target for free (both adapters key off cwd). **FACT/RULE injectors:** new CC `UserPromptSubmit` hook `adapters/claude-code/hooks/project-facts.mjs` + opencode `experimental.chat.system.transform` (signature VERIFIED against the installed `@opencode-ai/plugin` SDK 1.14, `(input,{system:string[]})`); no store entry β‡’ silent no-op, fail-open, soft size cap. **`bootstrap` `wireFactsInjector`** (machine-wide, idempotent, auto-vendored); **`generate.mjs` `layout:"store"`** remap (`CLAUDE.md`β†’`facts.md`, `.claude/*`β†’flat, lintβ†’gaps). **Install mode is the FIRST interview question** (in-repo committed / gitignored / repo-clean), pre-filled + confirmed; `--repo-clean` is an optional accelerator only. **sync + `/update-harness` + `staleness.mjs` store-aware** (`collectStoreFiles`/`scan-store`, repo-clean `reapplyOverlay`). **`setup-harness.md`** documents the repo-clean generate/verify/migration flow + the two known constraints (LINT β†’ content-scan-hook fallback on store policy; SKILL β†’ machine-wide install + store record, not auto-discovered). **Proof `scripts/test-repo-clean.mjs`** (temp HOME + two fixture git repos, the REAL vendored CC hook + the REAL opencode plugin + a REAL linked worktree) wired as the **10th CI gate**; adapter count unchanged (54 β€” the injector is an authored hook, not a skill). All 10 gates green (genericity + hook-engine + setup + generate + wiring + bootstrap + update-harness + config-layers + **repo-clean** + adapters-sync) + GitGuardian; generic (tilde paths only), dual-target, cross-platform (no bash-isms), LF-only, no NUL bytes. The single residual human step for the whole initiative is unchanged: run `/setup-harness` interactively on the real machine (and pick repo-clean there if the company PC forbids in-repo AI files).

### Original spec (as built)

## Stage 8 β€” Repo-clean (global-only) install mode (locked spec)

**Why.** On some company machines the repo working directory must stay pristine β€” you **cannot add or commit ANY AI/Claude files inside a repo** (`.claude/`, `CLAUDE.md`, `workflow.config.yaml`, `hooks.policy.json`, lint scaffolds β€” none of it). Today `/setup-harness` β€” even with the 7j two-layer config β€” always writes the *project* overlay INTO the repo, so on such a machine the harness is only usable as bare global `~/.claude` instructions. `/setup-harness` must support a **repo-clean / global-only mode** where **every artifact lives under `~/.claude`** (or another out-of-repo location), keyed/scoped to the project, with **ZERO files written into the repo**. (This is the user's real situation on a company PC.)

**Constraint (RESOLVED 2026-07-10 β€” grilled + locked): varies per machine.** Some machines forbid any file in the working dir (strict-(b): locked/scanned checkout), some only forbid commits (gitignored untracked OK). So the harness ships **three modes**, and `/setup-harness` **proactively asks** which to use: (1) in-repo committed [default, current], (2) in-repo gitignored, (3) repo-clean/global-only (strict-(b), the superset). No `--repo-clean` flag is required β€” mode is a first-class interview question, pre-filled from a detected recommendation and confirmed (a flag is only an optional accelerator).

**What's missing (the gap 7j left):**
- 7j's project layer still writes `<project>/workflow.config.yaml`, `<project>/CLAUDE.md`, `<project>/.claude/*`, `<project>/hooks.policy.json`.
- `core/setup/config.mjs` `resolveConfig` finds the project config by walking UP from cwd for an **in-repo** `workflow.config.yaml`.
- `core/hooks/logic/config.mjs` `loadPolicy` finds the project policy by walking UP from the edited file for an **in-repo** `hooks.policy.json`.
- The FACT tier relies on an **in-repo** `CLAUDE.md`; `harness.answers.yaml`/`harness.manifest.json` are written to the repo root too.

**Verification DONE (2026-07-10, via `claude-code-guide` + live docs + opencode plugin docs):** (a) NO native out-of-repo per-project mechanism for the FACT/instructions tier β€” `~/.claude/CLAUDE.md` and `~/.claude/rules/*.md` load for **all** projects; a rule's `paths:` scopes by **file glob, not project root**; so project facts/rules cannot be scoped to one repo without a repo file. (b) Every hook receives `cwd` on stdin and `CLAUDE_PROJECT_DIR` (= git root) in its env; `UserPromptSubmit` can inject via `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":…}}`. (c) settings.json: the user layer is global; project settings load only from the launch dir (no per-project-under-`~/.claude` native map); git root is the project boundary; `~/.claude.json` already keys per-project MCP config by **absolute path** (the keying precedent). (d) opencode has **no stable** prompt hook, but `experimental.chat.system.transform` (push into the system array) + `chat.message` can inject context, and `tool.execute.before/after` already carry the cwd. β‡’ **config + enforcement tiers are dual-target for free** (our resolver/`loadPolicy` key off cwd; both adapters pass it); **only the FACT/RULE injection tier needs a built mechanism** β€” a global cwd-keyed hook per tool.

**LOCKED design (grilled + locked 2026-07-10; all 14 decisions = the recommended option, plus the mode-selector-is-a-question correction):**

| # | Decision | Choice |
|---|---|---|
| 1 | Constraint | varies per machine β†’ 3 modes |
| 2 | Store key | absolute repo-root path (git toplevel), machine-local |
| 3 | FACT delivery | global `UserPromptSubmit` hook injects by cwd (no native mechanism exists) |
| 4 | Mode select | proactive interview question, not flag-driven |
| 5 | Store layout | dir-per-project + `index.json`, deterministic path-slug |
| 6 | Root/worktree | pure-fs `.git` walk-up; worktrees β†’ shared main root |
| 7 | Precedence | `DEFAULT < global < store < in-repo` (in-repo wins; mode-1 repos unchanged) |
| 8 | Injector | facts + project rules, every turn, lean, no-op without a store entry |
| 9 | Migration | re-run setup in the target mode + manifest-guided gated cleanup |
| 10 | Global req | repo-clean requires the global install; auto-run bootstrap if missing |
| 11 | Wiring | auto-wire injector + enforcement on the store's first project |
| 12 | Multi-repo | one store entry per git root, sharing config/policy |
| 13 | opencode | full parity via `experimental.chat.system.transform`, degrade-safe |
| 14 | Refresh | `sync` + `/update-harness` store-aware, in-scope now |

**Store** (`~/.claude/harness/`, honoring `CLAUDE_CONFIG_DIR`): `index.json` (`{ "<abs repo root>": { slug, mode, group } }`) + `projects/<slug>/{workflow.config.yaml, hooks.policy.json, facts.md, rules/*, harness.answers.yaml, harness.manifest.json, harness.mode}`. `slug` = deterministic sanitized abs path (CC's own `~/.claude/projects/<encoded>` convention) so a hook computes it from cwd with NO index read on the hot path; the index is for listing/migration/collision only.

**New module `core/hooks/logic/store.mjs`** (dependency-free, hot-path safe, in `logic/` so both the hot-path hooks AND `setup/` import it with no layering inversion): `resolveRepoRoot(startDir)` (walk up for `.git`; if `.git` is a FILE = a linked worktree, parse its `gitdir:` pointer β†’ derive the MAIN worktree root so all worktrees share one entry; pure fs, no git shell-out), `slugForPath`, `storeRoot`/`storeEntryDir`, `readStorePolicy/Config/Facts(startDir)`, index read/write, and `buildInjectedContext(repoRoot)` (the shared facts+rules assembly BOTH injectors call β€” single logic core).

**Resolver/policy:** `resolveConfig` = `deepMerge(deepMerge(global, store), inRepo)`; `loadPolicy` inserts the store policy just below the in-repo layer. Both stay pure/testable. Because the CC hooks + opencode plugin already pass `cwd`, **config + enforcement are dual-target for free** β€” only FACT/RULE injection needs a built hook.

**FACT/RULE injection:** CC = new authored hook `adapters/claude-code/hooks/project-facts.mjs` (UserPromptSubmit β†’ `buildInjectedContext` β†’ `additionalContext`; no store entry β‡’ silent exit 0; fail-open; soft size cap with a note). opencode = extend `adapters/opencode/plugin/agentic-harness.js` with `experimental.chat.system.transform` (degrade-safe; signature verified vs the installed SDK at build time).

**Bootstrap:** add `wireFactsInjector`; setup repo-clean, on the FIRST store entry, ensures the global install (auto-runs `bootstrap` if missing) + auto-wires injector + enforcement (idempotent, backed up). `project-facts.mjs` is auto-vendored (buildPlan already copies all hook files).

**Generator:** `planArtifacts(..., { layout: "store" })` remaps `CLAUDE.md`β†’`facts.md`, `.claude/rules/*`β†’`rules/*`, writes to the store entry dir. Store config is self-contained (full) by default, the lean project slice when a `--global` layer exists.

**Known repo-clean constraints (documented, never silent):** LINT tier can't add a repo linter rule β†’ falls back to the content-scan hook on store policy (recorded in the gaps report); SKILL tier β€” machine-specialized `investigate`/`ship` are machine-wide (install via `--global`); bespoke project skills are written to the store for record/sync + surfaced by the injector but NOT auto-discovered as slash-commands (CC discovers skills only under `~/.claude/skills` or `<repo>/.claude/skills`).

**Setup UX:** mode is a proactive question (in-repo committed / in-repo gitignored / repo-clean), pre-filled from detection, confirmed; flags are optional accelerators only.

**Refresh:** `sync.mjs` re-applies from the store's `harness.answers.yaml` + `harness.manifest.json`; `/update-harness` scans the store's installed files; both point their root at the store entry when the repo is repo-clean.

**Same bar as 7a–7j:** generic, dual-target, gated, cross-platform (no bash-isms, LF-only, no NUL bytes), a self-testing proof (`scripts/test-repo-clean.mjs`) wired into `genericity-check.yml`, README + `setup-harness.md` + this plan updated. One gated PR (cohesive feature); CI green before merge; ask before merging.

**Acceptance** (proof = `scripts/test-repo-clean.mjs`, the 10th CI gate): on a fixture repo, repo-clean setup produces **ZERO files inside the repo**, yet the effective config + policy + facts all resolve for that repo from `~/.claude`; enforcement BLOCKS a protected-branch push (an ordinary feature push passes) via the store policy; the CC injector emits the project's facts for repo1 and **no-ops** for an unrelated repo2; a linked worktree of repo1 resolves to repo1's store entry; repo2 is unaffected. Every existing gate (genericity + hook-engine + setup + generate + wiring + bootstrap + update-harness + config-layers + adapters-sync) stays green; `core/` + the engine adapters stay generic (tilde paths only, no home-path literals) and dual-target.

**βœ… Stage 8 has landed β€” the harness now supports both in-repo and repo-clean installs.**
Loading