@nurix/etna 0.19.92 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/bin/etna.js +0 -11
  2. package/data/catalog/rules/frontend.md +1 -1
  3. package/data/catalog/rules/general.md +3 -3
  4. package/data/catalog/rules/nomenclature.md +0 -4
  5. package/data/catalog/rules/pnpm.md +1 -1
  6. package/data/catalog/rules/testing.md +1 -0
  7. package/data/harness-index.json +1 -1
  8. package/data/harnesses/code/CLAUDE.md +14 -14
  9. package/data/harnesses/code/agents/bookkeeper.md +3 -0
  10. package/data/harnesses/code/agents/maintenance.md +3 -14
  11. package/data/harnesses/code/agents/performance-review.md +1 -1
  12. package/data/harnesses/code/codex/agents/performance-review.toml +1 -1
  13. package/data/harnesses/code/harness.json +1 -1
  14. package/data/harnesses/code/hooks/awareness.mjs +5 -169
  15. package/data/harnesses/code/hooks/compliance.mjs +21 -17
  16. package/data/harnesses/code/hooks/lib/ledger.mjs +36 -13
  17. package/data/harnesses/code/hooks/lib/sweep.mjs +172 -0
  18. package/data/harnesses/code/rules/frontend.md +1 -1
  19. package/data/harnesses/code/rules/general.md +3 -3
  20. package/data/harnesses/code/rules/nomenclature.md +0 -4
  21. package/data/harnesses/code/rules/pnpm.md +1 -1
  22. package/data/harnesses/code/rules/testing.md +1 -0
  23. package/data/harnesses/code/skills/boundary/SKILL.md +1 -1
  24. package/data/harnesses/code/skills/documentation/SKILL.md +1 -1
  25. package/data/harnesses/code/skills/documentation/templates/behavioral-checklist.md +1 -1
  26. package/data/harnesses/code/skills/documentation/templates/bookkeeping.md +12 -8
  27. package/data/harnesses/code/skills/documentation/templates/plan.md +9 -16
  28. package/data/harnesses/code/skills/testing/SKILL.md +1 -2
  29. package/data/harnesses/delivery/agents/maintenance.md +3 -14
  30. package/data/harnesses/delivery/harness.json +1 -1
  31. package/data/harnesses/delivery/hooks/generate-ownership-index.mjs +3 -1
  32. package/data/harnesses/delivery/rules/nomenclature.md +0 -4
  33. package/data/harnesses/delivery/skills/documentation/SKILL.md +1 -1
  34. package/data/harnesses/delivery/skills/documentation/templates/behavioral-checklist.md +1 -1
  35. package/data/harnesses/delivery/skills/documentation/templates/bookkeeping.md +12 -8
  36. package/data/harnesses/delivery/skills/documentation/templates/plan.md +9 -16
  37. package/data/harnesses/etna/agents/maintenance.md +3 -14
  38. package/data/harnesses/etna/agents/performance-review.md +1 -1
  39. package/data/harnesses/etna/codex/agents/performance-review.toml +1 -1
  40. package/data/harnesses/etna/harness.json +1 -1
  41. package/data/harnesses/etna/hooks/generate-ownership-index.mjs +3 -1
  42. package/data/harnesses/etna/rules/general.md +3 -3
  43. package/data/harnesses/etna/rules/nomenclature.md +0 -4
  44. package/data/harnesses/etna/rules/testing.md +1 -0
  45. package/data/harnesses/etna/skills/boundary/SKILL.md +1 -1
  46. package/data/harnesses/etna/skills/documentation/SKILL.md +1 -1
  47. package/data/harnesses/etna/skills/documentation/templates/behavioral-checklist.md +1 -1
  48. package/data/harnesses/etna/skills/documentation/templates/bookkeeping.md +12 -8
  49. package/data/harnesses/etna/skills/documentation/templates/plan.md +9 -16
  50. package/lib/codex/agents-md.mjs +1 -35
  51. package/lib/codex/hooks.mjs +0 -29
  52. package/lib/install/materialize.mjs +4 -34
  53. package/lib/install/plan.mjs +54 -10
  54. package/package.json +3 -3
  55. package/data/harnesses/code/hooks/check-bookkeeping-clean.mjs +0 -28
  56. package/data/harnesses/code/hooks/check-changelog-presence.mjs +0 -36
  57. package/data/harnesses/code/hooks/check-completion-integrity.mjs +0 -72
  58. package/data/harnesses/code/hooks/check-ownership.mjs +0 -55
  59. package/lib/codex/index.mjs +0 -63
  60. package/lib/codex/plugin-manifest.mjs +0 -42
  61. package/lib/codex/subagents.mjs +0 -26
@@ -20,11 +20,7 @@ Names state intent in full words — clarity over brevity; no abbreviations unle
20
20
 
21
21
  | Identifier | Shape | Example |
22
22
  | --- | --- | --- |
23
- | Variable | noun phrase | `paymentStatus` |
24
- | Function / method | verb phrase naming the action | `validateInput` |
25
23
  | Boolean | `is` / `has` / `can` / `should` prefix — **never** a bare adjective | `hasPermission` |
26
24
  | Array / collection | plural noun | `errorMessages` |
27
- | Class / component | singular noun phrase | `RequestHandler` |
28
25
  | Event handler | `handle` or `on` prefix | `handleSubmit`, `onPageLoad` |
29
26
  | Interface | `I` prefix acceptable; type aliases stay plain | `IConfigOptions`, `AuthResponse` |
30
- | Async function | verb phrase; `Async` suffix acceptable | `loadConfigAsync` |
@@ -113,7 +113,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
113
113
  | Plan | Record | `YYYY-MM-DD-kebab-case.md` | Freezes at `Complete` / `Abandoned` |
114
114
  | Changelog entry | Record | `YYYY-MM-DD-kebab-case.md` | Frozen; master index append-only |
115
115
  | Migration runbook | Record | `YYYY-MM-DD-kebab-case.md` | Freezes once executed, outcome stamped |
116
- | Research | Record | `history/research/{topic}/` | Frozen snapshot |
116
+ | Research | Record | in progress: `docs/research/{topic}/` · harvested: `docs/history/brainstorms/{topic}/` | Frozen at harvest |
117
117
  | Memory | Record | `{agent-name}.md` | Append-only dated ledger |
118
118
 
119
119
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  Truth document. Use for: **how to verify one feature's promised behavior by driving the real product** — the browser, the CLI binary, HTTP against the running service. One file per feature, named for the feature doc that owns the code (`docs/features/auth.md` → `docs/tests/auth.md`). No frontmatter; a checklist owns no code.
4
4
 
5
- **This is the driving script, and it is the source the suite is drawn from.** At the verification gate it does double duty: rung 1 drives the real surface *against these steps*, and rung 2 pins the durable subset *of these assertions* as suite tests. Writing it is therefore not a report written afterwards — it is authored or updated **before** the driving, so the gate has something to execute and the tests have something to be derived from. Which assertions earn a permanent test is decided by `verification.md` § What earns a durable test, and each section records the answer on its `Pinned:` line.
5
+ **This is the driving script, and it is the source the suite is drawn from.** At the verification gate it does double duty: rung 1 drives the real surface *against these steps*, and rung 2 pins the durable subset *of these assertions* as suite tests. Writing it is therefore not a report written afterwards — it is authored or updated **before** the driving, so the gate has something to execute and the tests have something to be derived from. Which assertions earn a permanent test is decided by the `testing` skill (gate mode) § What earns a suite test, and each section records the answer on its `Pinned:` line.
6
6
 
7
7
  A checklist outlives the tests it seeds. Where a Playwright script breaks on every selector change, a checklist states the behavior and lets the implementation move under it — so it is written in the product's own vocabulary (labels, URLs, rows) and never in the code's (function names, props, internal state).
8
8
 
@@ -2,19 +2,23 @@
2
2
 
3
3
  Truth document with an unusual lifecycle: a **work queue**, not an archive. Rows are appended mechanically by the awareness hook (`auto:`) and deliberately by working sessions (`note:`), drained by the `bookkeeper` agent, and walled at Stop, at commit, and in CI. What survives in git — `waived:` and `needs-session:` rows — is exactly what a PR reviewer should see.
4
4
 
5
- **Each wall reads the queue at its own width.** Stop takes the ending session's own rows, because accountability for an arc is per session. The commit gate takes only the rows **implicating that commit's staged files** — a row names the file its finding is about, and holding a commit behind rows raised by somebody else's edits is a wait nothing the committer may do can clear. CI takes every row on the branch, where there is no staged set to scope by. Fail-closed cases stay repo-wide at the commit gate: a row whose grammar names no path, a `needs-session:` row, an expired waiver, and a commit staging nothing at all.
5
+ **Each wall reads the queue at its own width, and re-verifies conditions at gate time.** Stop takes the ending session's own rows, because accountability for an arc is per session. The commit gate takes only the rows **implicating that commit's staged files** — a row names the file its finding is about, and holding a commit behind rows raised by somebody else's edits is a wait nothing the committer may do can clear. CI takes every row on the branch, where there is no staged set to scope by. All three re-test each row against the repo before blocking, so a row whose condition has died walls nothing. Fail-closed cases stay repo-wide at the commit gate: a row whose grammar names no path, a `needs-session:` row, an expired waiver, and a commit staging nothing at all.
6
6
 
7
- The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:` row's `expires=` date is enforced: CI honors the waiver until the date, then fails. `waived:` rows whose finding kind is `dead-glob` double as the dead-glob allowlist.
7
+ The fenced block's prose is a **verbatim quote of the queue header the hooks self-heal** (`QUEUE_HEADER` in the hooks' `lib/state.mjs`) — code is canonical; a wording change starts there. The row grammar lines beneath it are taught here and parsed by the hooks; keep both exact.
8
8
 
9
9
  ```
10
10
  # Bookkeeping
11
11
 
12
- The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit goes through only when every row **implicating one of its staged files** is either **resolved, which deletes it**, or converted to `waived:`. A row about files the commit does not touch belongs to whoever raised it; a row naming no file, and a commit staging none, block everything. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
12
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit goes through only when every row **implicating one of its staged files** is either **resolved, which deletes it**, or converted to `waived:`. A row about files the commit does not touch belongs to whoever raised it; a row naming no file, and a commit staging none, block everything. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`.
13
13
 
14
- - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
15
- - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
16
- - needs-session: YYYY-MM-DD · {what the bookkeeper could not ground} → main session resolves | refs={original row ts}
17
- - waived: YYYY-MM-DD · {what is deferred} · reason: {why} → expires=YYYY-MM-DD
14
+ **Every row is a cache of a condition, not a record of an event.** A session-start sweep re-tests each row against the repo and deletes the ones whose condition has died — `waived:` rows included, because a waiver defers a finding and never makes one immortal. What the sweep cannot mechanically re-test ages out instead: a `waived:` row carries `expires=`, is PR-reviewable history until that date, and counts as **unresolved** past it — which returns the wall and re-summons the bookkeeper to re-judge whether the reason still holds.
15
+
16
+ - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id}
17
+ - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid={session_id}
18
+ - needs-session: YYYY-MM-DD · {what the bookkeeper could not ground} → main session resolves | refs={implicated file}
19
+ - waived: YYYY-MM-DD · {finding-kind} · `{subject}` · reason: {why} → expires=YYYY-MM-DD
18
20
  ```
19
21
 
20
- Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `package-rejected` · `frozen-record-edited`.
22
+ **A waiver covers a finding only when it names the finding's kind and its subject in backticks** — the matcher (`waiverCovers`) tests the backticked subject exactly, so a waiver written without them silences nothing and the wall stands. A `waived:` row's `expires=` date is enforced: CI honors the waiver until the date, then fails. `dead-glob` waivers double as the dead-glob allowlist; `ownership-overlap` waivers cover the CI ownership check the same way.
23
+
24
+ Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `package-rejected` · `frozen-record-edited`. The CI ownership check additionally accepts `waived:` rows of kind `ownership-overlap`.
@@ -6,16 +6,16 @@ The `boundary` skill parses the **Proposed Changes** sections to check boundary
6
6
 
7
7
  **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
8
8
 
9
- The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. The author writes `Docs: mapped YYYY-MM-DD (N docs)` when the Doc Obligations table is filled. Never hand-edit the line to skip a gate.
9
+ The **`Gates:`** line carries one machine-read segment. The compliance hook greps **`Boundary:`** — implementation edits inside the plan's scope are blocked while it reads `Boundary: pending`; the `boundary` skill rewrites it to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`. The other two gates are enforced by **section presence**: a plan with no `## Test Obligations` or no `## Doc Obligations` section blocks the same edits — the sections are the artifacts, and the gate derives from them, not from a claim about them. Never hand-edit the line to skip a gate.
10
10
 
11
11
  ```
12
12
  # {Feature Name}
13
13
 
14
14
  **Status:** Draft | In Progress | Complete | Abandoned
15
- **Gates:** Boundary: pending · Test-plan: pending · Docs: pending
15
+ **Gates:** Boundary: pending
16
16
  **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
17
17
  **Started:** YYYY-MM-DD
18
- **Completed:** YYYY-MM-DD <!-- fill in the SAME edit that flips Status to Complete: the flip freezes the file instantly, so a date left for a second edit is unreachable (the frozen-record gate rightly blocks it) -->
18
+ **Completed:** YYYY-MM-DD <!-- fill in the SAME edit that flips Status to Complete — along with every ledger Outcome cell: the flip freezes the file instantly, so anything left for a second edit is unreachable (the frozen-record gate rightly blocks it) -->
19
19
 
20
20
  ## Background
21
21
 
@@ -52,9 +52,9 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
52
52
  them against the injected ownership index. Hand-authored: this is a lookup, not research.
53
53
  Amendable while the plan is open; discharged at Completion. -->
54
54
 
55
- | Doc | Claim that changes | `owns:` still accurate? |
56
- | --- | --- | --- |
57
- | `docs/features/{feature}.md` | what it says today that this plan makes false | yes / no — new paths to add |
55
+ | Doc | Claim that changes | `owns:` still accurate? | Outcome |
56
+ | --- | --- | --- | --- |
57
+ | `docs/features/{feature}.md` | what it says today that this plan makes false | yes / no — new paths to add | written empty; discharged at Completion — **updated** / **owes nothing** (with the reason) / **blocked** (→ backlog) |
58
58
 
59
59
  **"None" is valid** — a plan touching only unowned paths owes no doc update, and says so explicitly. It is not a free pass: `boundary` builds its own touch list from the same Proposed Changes and reports docs this table missed.
60
60
 
@@ -67,16 +67,9 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
67
67
  ## Test Obligations
68
68
 
69
69
  <!-- Written by the `testing` skill (plan-gate mode) at the plan gate (after `boundary`). One row per
70
- promised behavior: layer · target file · oracle · fixtures. Amendable while the
71
- plan is open; discharged at the verification gate. Do not hand-author. -->
72
-
73
- ## Done Criteria
74
-
75
- - [ ] Implementation complete
76
- - [ ] Verification gate passed — every Test Obligations row discharged (satisfied / owes nothing / blocked → backlog), gate run date and results noted here
77
- - [ ] Every Doc Obligations row discharged — **updated** / **owes nothing** (with the reason) / **blocked** (→ backlog item, so the debt outlives the frozen plan)
78
- - [ ] Changelog entry created in `docs/history/changelog/`, carrying the gate's results
79
- - [ ] Plan marked `Status: Complete`, **Completed:** date filled
70
+ promised behavior: layer · target file · oracle · fixtures · an empty Outcome cell the
71
+ verification gate fills (satisfied / owes nothing / blocked → backlog). Amendable while the
72
+ plan is open. Do not hand-author. -->
80
73
 
81
74
  ## Open Questions
82
75
 
@@ -28,9 +28,7 @@ Decide the mode once, up front, from the version marker — then run.
28
28
  | behind on minor/patch | **probe first**, below |
29
29
  | equal to installed | **audit only** (the default spine) |
30
30
 
31
- **The probe.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** any **scaffolded file** in the target tree is missing (`docs/README.md`, `docs/structure.md`, `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/main.md`), the shape *did* change ⇒ **baseline mode**. Only when the probe finds nothing ⇒ re-stamp the version (no reshape), then audit.
32
-
33
- > **The version digit is a hint; the fold table is the authority.** A shape change ships in a minor whenever the capability behind it is additive — `docs/history/ledger/` dissolving into `docs/history/changelog/` arrived in a minor, as did `docs/history/migrations/` — so "minor ⇒ nothing to reshape" silently strands dissolved-shape artifacts in the repo forever.
31
+ **The probe.** Run the Step B2 detection pass read-only: if any `*.bkp.*` file sits under `docs/` (a fold awaiting its pass), **or** any **scaffolded file** in the target tree is missing (`docs/README.md`, `docs/structure.md`, `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/main.md`), the shape *did* change ⇒ **baseline mode**. Only when the probe finds nothing ⇒ re-stamp the version (no reshape), then audit.
34
32
 
35
33
  > **Never probe on a missing folder.** Git does not track empty directories, so an unused folder is absent on every clone — reading that as drift puts the repo in baseline mode *forever*. Only the five scaffolded files above are shape signals; every folder in the target tree is created on first use.
36
34
  3. **Precondition for baseline mode:** a root `CLAUDE.md` must exist. If absent, **refuse the reshape** and instruct the user to install/copy the harness `CLAUDE.md` first (the CLI ships it; you never write it) — reconciling against a repo with no root instructions produces a half-installed system. Detect-and-instruct, not a hard gate on the audit.
@@ -166,21 +164,12 @@ Detecting and folding old-shape artifacts is what makes a baseline more than a f
166
164
 
167
165
  | If you find (old shape) | Fold to (current shape) |
168
166
  | --- | --- |
169
- | a persisted `docs/routing.md` ownership map | migrate each File-Pattern row's ownership into the owning feature doc's `owns:` frontmatter (Pattern → an `owns:` glob; Module column → the doc's `module:` label), then **delete `docs/routing.md`** |
170
- | feature docs carrying `owns`/`touches`/`depends_on` legacy frontmatter | collapse to the single `owns:` key (drop `touches`/`depends_on`; their paths fold into `owns:` if the doc genuinely owns them, else drop) |
171
- | a `docs/modules/{m}/` tree | move each module's `features/` flat into `docs/features/` with the module name as a `module:` tag on each doc; fold module-local `pillars/`/prose into `docs/architecture/`; delete the `docs/modules/` tree |
172
- | repo-wide `docs/pillars/` | cross-module doctrine → `docs/architecture/`; delete the folder |
173
- | `docs/boundaries/` (legacy persisted boundaries) | delete — boundary analysis is a plan-time diagnostic (the `boundary` skill reads contracts fresh from source); fold any human-authored knowledge (external consumers, intent, accepted exceptions) into `docs/architecture/` |
174
- | `docs/OWNERSHIP_INDEX.md` (legacy derived ownership index) | delete — ownership is the union of feature `owns:` frontmatter, surfaced by the generated index |
175
- | `docs/history/ledger/YYYY-MM.md` (the Refinement Ledger, now merged into the changelog) | split each `### ` block into its own `docs/history/changelog/YYYY-MM-DD-{three-words}.md` entry — content preserved **verbatim**, heading promoted to `#`, three words naming the work; append one row per entry to `docs/history/CHANGELOG.md`; delete the `docs/history/ledger/` folder. Records are moved, never rewritten |
176
- | `docs/operations/guardrails.md` | fold durable contract notes into `docs/architecture/` |
177
- | `docs/architecture/overview.md` | split into `domain-model.md` / `glossary.md` / `non-goals.md` / `references.md` / `open-questions.md` |
178
- | `docs/architecture/adr/` (legacy folder name), or `ADR-NNN-*.md` records anywhere under `docs/architecture/` | **rename to the AD genre.** The folder becomes `docs/architecture/decisions/`; each record file `ADR-NNN-{slug}.md` becomes `AD-NNN-{slug}.md`, keeping its number and slug. Inside each record, update the retired vocabulary — "ADR", "Architecture Decision Record", and any supersession framing ("once accepted, an ADR is never edited; a later ADR supersedes it") — to the AD model: one AD per subject, **updated in place**, its journey confined to a `Decision Changelog` tail. A record's **reasoning is never rewritten** by a reconcile; only its genre vocabulary, its own title/id, and paths it cites. The folder's `Readme.md` is an **index, not a record** — reframe its genre description while preserving authored content such as a candidate-decision list verbatim. Repoint live references **inside `docs/`**, including one AD citing another; leave frozen records' links as point-in-time. A live reference **outside** your write scope — most often the root `README.md`, which you never overwrite — cannot be repointed: record it as a backlog item naming the file and line rather than leaving the residue silent |
179
- | a legacy `docs/brainstorm/{topic}/`, a singular `docs/history/brainstorm/`, or a root `brainstorm/`/`brainstorming/` folder | move each **active** record to `docs/research/{topic}-brainstorm/` and each **harvested** one to `docs/history/brainstorms/{topic}/` (dropping the suffix); create/refresh both `Readme.md` indexes; delete the emptied folder |
180
167
  | `*.bkp.*` files under `docs/` (the installer's first-contact backups of the project's pre-existing files) | **fold the content, then delete the backup.** Classify each backup's content by nature and land it in the current shape — a backed-up changelog's rows into `docs/history/CHANGELOG.md` and per-entry files, a backed-up structure/readme's real claims into the scaffolded truth doc, a backed-up feature doc's content merged into its sibling (or adopted whole if the sibling is a pristine seed). Content is folded, never discarded silently; what cannot be classified goes to the backlog with the backup kept. When the fold lands, delete the `.bkp` file |
181
168
  | `*.bkp.*` files at the repo root or in config (`CLAUDE.bkp.md`, `.prettierrc.bkp.json`, …) | **never merge these.** Top-level config is harness-owned by design; list each one to the user as their call, and leave the file in place |
182
169
  | no `harness-version` in `structure.md` | stamp it (Step B4) |
183
170
 
171
+ **The support window: three past minor versions.** The fold table covers shapes at most three minors behind the installed harness — which today means the rows above, because every older dissolved construct (`docs/routing.md`, `docs/modules/`, pillars, the Refinement Ledger, the ADR genre, legacy brainstorm folders, …) predates the window. An older shape is **reported, never improvised**: name what the probe found, point the user at etna's dated migration runbooks (`docs/history/migrations/` in the forge), and recommend a fresh baseline over an archaeological fold — a repo more than three minors behind re-baselines rather than migrates.
172
+
184
173
  ### Step B3 — Present the reconcile plan, then execute
185
174
 
186
175
  Table: `| File | Action | From | To | Reason |` (actions: `Move`, `Merge into {target}`, `Delete`, `Keep`, `Create`, `Fold`). For a fresh baseline this is mostly `Create`; for an upgrade it is mostly `Fold`/`Move`. Ask once: **"Approve this reconcile?"** — you own this interaction (the `documentation` skill is a library and has none).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: performance-review
3
- description: Combined dependency + code health audit. Part A audits the dependency supply chain — security CVEs, outdated packages, license compliance, abandoned packages, unwanted transitive dependencies, install size. Part B audits code-level health — bundle size, tree-shaking issues, performance anti-patterns, code-quality flags. Run weekly or before releases. Merges the former `dep-health` and `review` agents so the package tree and the code that uses it are audited together.
3
+ description: Combined dependency + code health audit. Part A audits the dependency supply chain — security CVEs, outdated packages, license compliance, abandoned packages, unwanted transitive dependencies, install size. Part B audits code-level health — bundle size, tree-shaking issues, performance anti-patterns, code-quality flags. Run weekly or before releases. The package tree and the code that uses it are audited together.
4
4
  tools: Read, Glob, Grep, Bash
5
5
  model: opus
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  # dropped Claude tools: Read, Glob, Grep, Bash
2
2
  name = "performance-review"
3
- description = "Combined dependency + code health audit. Part A audits the dependency supply chain — security CVEs, outdated packages, license compliance, abandoned packages, unwanted transitive dependencies, install size. Part B audits code-level health — bundle size, tree-shaking issues, performance anti-patterns, code-quality flags. Run weekly or before releases. Merges the former `dep-health` and `review` agents so the package tree and the code that uses it are audited together."
3
+ description = "Combined dependency + code health audit. Part A audits the dependency supply chain — security CVEs, outdated packages, license compliance, abandoned packages, unwanted transitive dependencies, install size. Part B audits code-level health — bundle size, tree-shaking issues, performance anti-patterns, code-quality flags. Run weekly or before releases. The package tree and the code that uses it are audited together."
4
4
  developer_instructions = '''
5
5
 
6
6
  # Performance-Review Agent
@@ -8,5 +8,5 @@
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "269767b3abdf"
11
+ "digest": "5d3b72e18333"
12
12
  }
@@ -116,7 +116,9 @@ function asList(v) {
116
116
  // Build the ownership index text from every feature doc's frontmatter.
117
117
  function buildIndex(projectDir) {
118
118
  const featuresDir = path.join(projectDir, "docs", "features");
119
- const files = listMarkdown(featuresDir);
119
+ // *.bkp.* files are install-time backups awaiting the maintenance fold — a backed-up feature
120
+ // doc double-owning every path it names is the drift this filter prevents (parity: lib/ownership.mjs).
121
+ const files = listMarkdown(featuresDir).filter((f) => !f.includes(".bkp."));
120
122
  const lines = [];
121
123
  let deprecated = 0;
122
124
  let indexed = 0;
@@ -19,11 +19,11 @@ How a package's `src/` is laid out. A single-runtime package drops `server/` and
19
19
 
20
20
  **Never `core/`** — its membership test collapses to "is it important?", making it the queue for files nobody could place; a bare `lib/` holding everything non-visual fails the same way, which is what `shared/` prevents. A route's own nested `lib/` is private and is **never** hoisted, and **dependencies point down** — a shared folder importing one route folder is a defect.
21
21
 
22
- **A folder earns its place at ~4+ files, or by being a boundary somebody could delete whole.** Below that, inline it: a folder around one file is a longer filename, and a folder holding only another folder is cascade with nothing at it. Density — not depth, not root breadth — is what makes a tree read as sprawling. The boundary clause splits the one-file cases: a one-file route folder stays, a one-file component folder goes.
22
+ **A folder earns its place at ~4+ files, or by being a boundary somebody could delete whole.** Below that, inline it. The boundary clause splits the one-file cases: a one-file route folder stays, a one-file component folder goes.
23
23
 
24
- **Never judge dead code by a name grep** — a relative import never contains its own parent's path, so `grep "x/brand"` cannot see `from "./brand/Detail"`, and every folder imported only by its parent reads as dead. Test by transitive reachability from the real entry points, resolving specifiers as the bundler does (`.ts`, `.tsx`, `.css`, `/index.ts`; strip `?raw`/`?url`; map a `.js` specifier onto its `.ts` source), running the test tree as a **second, separate** entry set — reachable from tests alone means a suite pinning code nothing ships, and both go together. Ambient `.d.ts` files enter through tsconfig `include`, so a sweep always reports them dead. A retired mechanism spans more than source — grep the build config for the same concept before calling a removal complete. **Unreachable ≠ unwanted**: a routed surface rendering dummy data is reachable and still not product, and retiring it removes a route, which is a product decision.
24
+ **Never judge dead code by a name grep** — a relative import never contains its own parent's path, so every folder imported only by its parent reads as dead. Test by transitive reachability from the real entry points, resolving specifiers as the bundler does (`.ts`, `.tsx`, `.css`, `/index.ts`; strip `?raw`/`?url`; map a `.js` specifier onto its `.ts` source), with the test tree as a **second, separate** entry set — reachable from tests alone is a suite pinning code nothing ships. Ambient `.d.ts` files enter through tsconfig `include`, so a sweep always reports them dead; grep the build config for a retired mechanism before calling its removal complete. **Unreachable ≠ unwanted**: retiring a routed dummy surface removes a route — a product decision.
25
25
 
26
- **Reshape an existing tree largest-lever-first**, verifying typecheck plus the full suite between steps: purge unreachable files → collapse single-occupancy and pass-through folders → push domain-coupled components down → name the shared layer (the only step that adds a top-level name) → optionally add a path alias, which makes depth invisible at the import site and dissolves most "flatten it" instincts. **Delete and promote before you move** — a folder whose name misdescribes its contents just relocates the lie.
26
+ **Reshape an existing tree largest-lever-first**, verifying typecheck plus the full suite between steps: purge unreachable files → collapse single-occupancy and pass-through folders → push domain-coupled components down → name the shared layer (the only step that adds a top-level name) → optionally add a path alias (depth becomes invisible at the import site). **Delete and promote before you move** — a folder whose name misdescribes its contents just relocates the lie.
27
27
 
28
28
  ## Runtime Versions
29
29
 
@@ -20,11 +20,7 @@ Names state intent in full words — clarity over brevity; no abbreviations unle
20
20
 
21
21
  | Identifier | Shape | Example |
22
22
  | --- | --- | --- |
23
- | Variable | noun phrase | `paymentStatus` |
24
- | Function / method | verb phrase naming the action | `validateInput` |
25
23
  | Boolean | `is` / `has` / `can` / `should` prefix — **never** a bare adjective | `hasPermission` |
26
24
  | Array / collection | plural noun | `errorMessages` |
27
- | Class / component | singular noun phrase | `RequestHandler` |
28
25
  | Event handler | `handle` or `on` prefix | `handleSubmit`, `onPageLoad` |
29
26
  | Interface | `I` prefix acceptable; type aliases stay plain | `IConfigOptions`, `AuthResponse` |
30
- | Async function | verb phrase; `Async` suffix acceptable | `loadConfigAsync` |
@@ -32,6 +32,7 @@ Rules for the plan regime between the plan gate and the verification gate:
32
32
  - **Precondition: the ledger exists.** A plan with no Test Obligations section is blocked at the gate — run the `testing` skill (plan-gate mode) on it first; an empty ledger passes vacuously and the gap surfaces only after the file is frozen.
33
33
  - **The verdict is never delegated** (the root policy file § Delegation), and where browser work is main-session-only (its § Runtime Notes), the driving is too. Fan-out may prepare fixtures or draft test bodies; the driving and the verdict stay inline.
34
34
  - **The gate needs no stronger tier than the session default** — a standard-tier session suffices whatever tier built the plan; the plan and its ledger carry all the state the gate needs, so gating in a fresh session loses nothing.
35
+ - **Browser driving is the gate's final rung.** Playwright / Chrome CDP drives run once, at the end, after suites and cheap oracles are green — never interleaved mid-loop; they are the most expensive oracle in time and tokens.
35
36
  - **Report what happened, including failures.** A gate that reports green while a suite is red is worse than no gate; state the failing test and its output.
36
37
  - **Abandonment does not erase debt.** Before `Status: Abandoned` freezes a plan whose code was committed, convert its undischarged rows into backlog items; uncommitted code owes nothing.
37
38
 
@@ -65,7 +65,7 @@ The packs are a floor, not a fence: report any contract that passes the ships-gr
65
65
 
66
66
  **BLOCK** = any CRITICAL. HIGH findings yield **PASS WITH AMENDMENTS** — fold the amendments into the plan before coding. Order amendments sequencing-first: many violations dissolve by reordering steps (expand-then-contract, migrate-before-code) rather than changing what ships — prefer that fix, drawn from the entry's safe-change sequence.
67
67
 
68
- 6. **Record the verdict in the plan's `Gates:` line.** Rewrite only your own segment, to exactly `Boundary: passed YYYY-MM-DD` (PASS, or PASS WITH AMENDMENTS once the amendments are folded) or `Boundary: blocked YYYY-MM-DD` (BLOCK). The compliance hook greps these exact forms — implementation edits stay blocked while the segment reads `pending`. A plan with no `Gates:` line predates the format; add the line rather than skipping the write-back.
68
+ 6. **Record the verdict in the plan's `Gates:` line.** Rewrite only your own segment, to exactly `Boundary: passed YYYY-MM-DD` (PASS, or PASS WITH AMENDMENTS once the amendments are folded) or `Boundary: blocked YYYY-MM-DD` (BLOCK). The compliance hook greps exactly this segment — implementation edits inside the plan's scope stay blocked while it reads `Boundary: pending` (the other gates are enforced by section presence, not segment text). A plan with no `Gates:` line predates the format; add the line rather than skipping the write-back.
69
69
 
70
70
  ## Rules
71
71
 
@@ -113,7 +113,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
113
113
  | Plan | Record | `YYYY-MM-DD-kebab-case.md` | Freezes at `Complete` / `Abandoned` |
114
114
  | Changelog entry | Record | `YYYY-MM-DD-kebab-case.md` | Frozen; master index append-only |
115
115
  | Migration runbook | Record | `YYYY-MM-DD-kebab-case.md` | Freezes once executed, outcome stamped |
116
- | Research | Record | `history/research/{topic}/` | Frozen snapshot |
116
+ | Research | Record | in progress: `docs/research/{topic}/` · harvested: `docs/history/brainstorms/{topic}/` | Frozen at harvest |
117
117
  | Memory | Record | `{agent-name}.md` | Append-only dated ledger |
118
118
 
119
119
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  Truth document. Use for: **how to verify one feature's promised behavior by driving the real product** — the browser, the CLI binary, HTTP against the running service. One file per feature, named for the feature doc that owns the code (`docs/features/auth.md` → `docs/tests/auth.md`). No frontmatter; a checklist owns no code.
4
4
 
5
- **This is the driving script, and it is the source the suite is drawn from.** At the verification gate it does double duty: rung 1 drives the real surface *against these steps*, and rung 2 pins the durable subset *of these assertions* as suite tests. Writing it is therefore not a report written afterwards — it is authored or updated **before** the driving, so the gate has something to execute and the tests have something to be derived from. Which assertions earn a permanent test is decided by `verification.md` § What earns a durable test, and each section records the answer on its `Pinned:` line.
5
+ **This is the driving script, and it is the source the suite is drawn from.** At the verification gate it does double duty: rung 1 drives the real surface *against these steps*, and rung 2 pins the durable subset *of these assertions* as suite tests. Writing it is therefore not a report written afterwards — it is authored or updated **before** the driving, so the gate has something to execute and the tests have something to be derived from. Which assertions earn a permanent test is decided by the `testing` skill (gate mode) § What earns a suite test, and each section records the answer on its `Pinned:` line.
6
6
 
7
7
  A checklist outlives the tests it seeds. Where a Playwright script breaks on every selector change, a checklist states the behavior and lets the implementation move under it — so it is written in the product's own vocabulary (labels, URLs, rows) and never in the code's (function names, props, internal state).
8
8
 
@@ -2,19 +2,23 @@
2
2
 
3
3
  Truth document with an unusual lifecycle: a **work queue**, not an archive. Rows are appended mechanically by the awareness hook (`auto:`) and deliberately by working sessions (`note:`), drained by the `bookkeeper` agent, and walled at Stop, at commit, and in CI. What survives in git — `waived:` and `needs-session:` rows — is exactly what a PR reviewer should see.
4
4
 
5
- **Each wall reads the queue at its own width.** Stop takes the ending session's own rows, because accountability for an arc is per session. The commit gate takes only the rows **implicating that commit's staged files** — a row names the file its finding is about, and holding a commit behind rows raised by somebody else's edits is a wait nothing the committer may do can clear. CI takes every row on the branch, where there is no staged set to scope by. Fail-closed cases stay repo-wide at the commit gate: a row whose grammar names no path, a `needs-session:` row, an expired waiver, and a commit staging nothing at all.
5
+ **Each wall reads the queue at its own width, and re-verifies conditions at gate time.** Stop takes the ending session's own rows, because accountability for an arc is per session. The commit gate takes only the rows **implicating that commit's staged files** — a row names the file its finding is about, and holding a commit behind rows raised by somebody else's edits is a wait nothing the committer may do can clear. CI takes every row on the branch, where there is no staged set to scope by. All three re-test each row against the repo before blocking, so a row whose condition has died walls nothing. Fail-closed cases stay repo-wide at the commit gate: a row whose grammar names no path, a `needs-session:` row, an expired waiver, and a commit staging nothing at all.
6
6
 
7
- The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:` row's `expires=` date is enforced: CI honors the waiver until the date, then fails. `waived:` rows whose finding kind is `dead-glob` double as the dead-glob allowlist.
7
+ The fenced block's prose is a **verbatim quote of the queue header the hooks self-heal** (`QUEUE_HEADER` in the hooks' `lib/state.mjs`) — code is canonical; a wording change starts there. The row grammar lines beneath it are taught here and parsed by the hooks; keep both exact.
8
8
 
9
9
  ```
10
10
  # Bookkeeping
11
11
 
12
- The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit goes through only when every row **implicating one of its staged files** is either **resolved, which deletes it**, or converted to `waived:`. A row about files the commit does not touch belongs to whoever raised it; a row naming no file, and a commit staging none, block everything. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
12
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit goes through only when every row **implicating one of its staged files** is either **resolved, which deletes it**, or converted to `waived:`. A row about files the commit does not touch belongs to whoever raised it; a row naming no file, and a commit staging none, block everything. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`.
13
13
 
14
- - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
15
- - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
16
- - needs-session: YYYY-MM-DD · {what the bookkeeper could not ground} → main session resolves | refs={original row ts}
17
- - waived: YYYY-MM-DD · {what is deferred} · reason: {why} → expires=YYYY-MM-DD
14
+ **Every row is a cache of a condition, not a record of an event.** A session-start sweep re-tests each row against the repo and deletes the ones whose condition has died — `waived:` rows included, because a waiver defers a finding and never makes one immortal. What the sweep cannot mechanically re-test ages out instead: a `waived:` row carries `expires=`, is PR-reviewable history until that date, and counts as **unresolved** past it — which returns the wall and re-summons the bookkeeper to re-judge whether the reason still holds.
15
+
16
+ - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id}
17
+ - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid={session_id}
18
+ - needs-session: YYYY-MM-DD · {what the bookkeeper could not ground} → main session resolves | refs={implicated file}
19
+ - waived: YYYY-MM-DD · {finding-kind} · `{subject}` · reason: {why} → expires=YYYY-MM-DD
18
20
  ```
19
21
 
20
- Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `package-rejected` · `frozen-record-edited`.
22
+ **A waiver covers a finding only when it names the finding's kind and its subject in backticks** — the matcher (`waiverCovers`) tests the backticked subject exactly, so a waiver written without them silences nothing and the wall stands. A `waived:` row's `expires=` date is enforced: CI honors the waiver until the date, then fails. `dead-glob` waivers double as the dead-glob allowlist; `ownership-overlap` waivers cover the CI ownership check the same way.
23
+
24
+ Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `package-rejected` · `frozen-record-edited`. The CI ownership check additionally accepts `waived:` rows of kind `ownership-overlap`.
@@ -6,16 +6,16 @@ The `boundary` skill parses the **Proposed Changes** sections to check boundary
6
6
 
7
7
  **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
8
8
 
9
- The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. The author writes `Docs: mapped YYYY-MM-DD (N docs)` when the Doc Obligations table is filled. Never hand-edit the line to skip a gate.
9
+ The **`Gates:`** line carries one machine-read segment. The compliance hook greps **`Boundary:`** — implementation edits inside the plan's scope are blocked while it reads `Boundary: pending`; the `boundary` skill rewrites it to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`. The other two gates are enforced by **section presence**: a plan with no `## Test Obligations` or no `## Doc Obligations` section blocks the same edits — the sections are the artifacts, and the gate derives from them, not from a claim about them. Never hand-edit the line to skip a gate.
10
10
 
11
11
  ```
12
12
  # {Feature Name}
13
13
 
14
14
  **Status:** Draft | In Progress | Complete | Abandoned
15
- **Gates:** Boundary: pending · Test-plan: pending · Docs: pending
15
+ **Gates:** Boundary: pending
16
16
  **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
17
17
  **Started:** YYYY-MM-DD
18
- **Completed:** YYYY-MM-DD <!-- fill in the SAME edit that flips Status to Complete: the flip freezes the file instantly, so a date left for a second edit is unreachable (the frozen-record gate rightly blocks it) -->
18
+ **Completed:** YYYY-MM-DD <!-- fill in the SAME edit that flips Status to Complete — along with every ledger Outcome cell: the flip freezes the file instantly, so anything left for a second edit is unreachable (the frozen-record gate rightly blocks it) -->
19
19
 
20
20
  ## Background
21
21
 
@@ -52,9 +52,9 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
52
52
  them against the injected ownership index. Hand-authored: this is a lookup, not research.
53
53
  Amendable while the plan is open; discharged at Completion. -->
54
54
 
55
- | Doc | Claim that changes | `owns:` still accurate? |
56
- | --- | --- | --- |
57
- | `docs/features/{feature}.md` | what it says today that this plan makes false | yes / no — new paths to add |
55
+ | Doc | Claim that changes | `owns:` still accurate? | Outcome |
56
+ | --- | --- | --- | --- |
57
+ | `docs/features/{feature}.md` | what it says today that this plan makes false | yes / no — new paths to add | written empty; discharged at Completion — **updated** / **owes nothing** (with the reason) / **blocked** (→ backlog) |
58
58
 
59
59
  **"None" is valid** — a plan touching only unowned paths owes no doc update, and says so explicitly. It is not a free pass: `boundary` builds its own touch list from the same Proposed Changes and reports docs this table missed.
60
60
 
@@ -67,16 +67,9 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
67
67
  ## Test Obligations
68
68
 
69
69
  <!-- Written by the `testing` skill (plan-gate mode) at the plan gate (after `boundary`). One row per
70
- promised behavior: layer · target file · oracle · fixtures. Amendable while the
71
- plan is open; discharged at the verification gate. Do not hand-author. -->
72
-
73
- ## Done Criteria
74
-
75
- - [ ] Implementation complete
76
- - [ ] Verification gate passed — every Test Obligations row discharged (satisfied / owes nothing / blocked → backlog), gate run date and results noted here
77
- - [ ] Every Doc Obligations row discharged — **updated** / **owes nothing** (with the reason) / **blocked** (→ backlog item, so the debt outlives the frozen plan)
78
- - [ ] Changelog entry created in `docs/history/changelog/`, carrying the gate's results
79
- - [ ] Plan marked `Status: Complete`, **Completed:** date filled
70
+ promised behavior: layer · target file · oracle · fixtures · an empty Outcome cell the
71
+ verification gate fills (satisfied / owes nothing / blocked → backlog). Amendable while the
72
+ plan is open. Do not hand-author. -->
80
73
 
81
74
  ## Open Questions
82
75
 
@@ -8,17 +8,9 @@
8
8
  // Mirrors the installer's CLAUDE.md safety net: overwrite in place when git can recover it,
9
9
  // otherwise back up the existing file and warn.
10
10
 
11
- import { existsSync, lstatSync, renameSync, readdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
11
+ import { existsSync, readdirSync, statSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
13
 
14
- function isSymlink(path) {
15
- try {
16
- return lstatSync(path).isSymbolicLink();
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
14
  // Sorted base names of *.<ext> files directly under dir (e.g. rules/*.md → rule names).
23
15
  function listByExt(dir, ext) {
24
16
  if (!existsSync(dir)) return [];
@@ -86,29 +78,3 @@ export function buildCodexAdapter(bundledHarnessDir) {
86
78
  lines.push('');
87
79
  return lines.join('\n');
88
80
  }
89
-
90
- export function projectAgentsMd({ targetDir, bundledClaudeMd, bundledHarnessDir, projectHasGit }) {
91
- if (!existsSync(bundledClaudeMd)) return null;
92
- const dest = join(targetDir, 'AGENTS.md');
93
- const existed = existsSync(dest) || isSymlink(dest);
94
- let notice = null;
95
- let warning = null;
96
- if (existed) {
97
- if (projectHasGit) {
98
- notice = 'Overwrote existing AGENTS.md (tracked by git — `git checkout AGENTS.md` to restore).';
99
- } else {
100
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
101
- const backup = `${dest}.bak.${ts}`;
102
- renameSync(dest, backup);
103
- warning = `No .git here, so no version-control safety net — backed up existing AGENTS.md → ${backup}`;
104
- }
105
- }
106
- // Policy copied byte-for-byte; the generated adapter appended after a blank-line separator.
107
- // The policy prefix is left exactly as authored — only a separator + adapter follow it.
108
- const policy = readFileSync(bundledClaudeMd, 'utf8');
109
- const adapter = bundledHarnessDir ? buildCodexAdapter(bundledHarnessDir) : '';
110
- const separator = policy.endsWith('\n') ? '\n' : '\n\n';
111
- const body = adapter ? policy + separator + adapter : policy;
112
- writeFileSync(dest, body);
113
- return { installed: 'AGENTS.md → ./', notice, warning };
114
- }
@@ -14,8 +14,6 @@
14
14
  // points at the project root, not the session's nested cwd. The Claude source hooks.json
15
15
  // is left untouched — it stays valid for the CLI-installed .claude/settings.json path.
16
16
 
17
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
18
- import { join, dirname } from 'node:path';
19
17
 
20
18
  const PROJECT_DIR_TOKEN = /\$\{?CLAUDE_PROJECT_DIR\}?/;
21
19
 
@@ -28,30 +26,3 @@ export function toCodexCommand(command) {
28
26
  command
29
27
  );
30
28
  }
31
-
32
- export function projectHooks({ targetDir, bundledHarnessDir }) {
33
- const srcJson = join(bundledHarnessDir, 'hooks', 'hooks.json');
34
- if (!existsSync(srcJson)) return null;
35
- let parsed;
36
- try {
37
- parsed = JSON.parse(readFileSync(srcJson, 'utf8'));
38
- } catch {
39
- return null;
40
- }
41
- const out = {};
42
- for (const [event, groups] of Object.entries(parsed.hooks || {})) {
43
- const kept = [];
44
- for (const group of groups) {
45
- const hooks = (group.hooks || [])
46
- .filter((h) => h.type === 'command')
47
- .map((h) => ({ ...h, command: toCodexCommand(h.command) }));
48
- if (hooks.length) kept.push({ ...group, hooks });
49
- }
50
- if (kept.length) out[event] = kept;
51
- }
52
- if (!Object.keys(out).length) return null;
53
- const dest = join(targetDir, '.codex', 'hooks.json');
54
- mkdirSync(dirname(dest), { recursive: true });
55
- writeFileSync(dest, JSON.stringify({ hooks: out }, null, 2) + '\n');
56
- return { installed: '.codex/hooks.json' };
57
- }
@@ -36,9 +36,6 @@ import { projectSkills } from '../codex/skills.mjs';
36
36
  // mergeHooks is additive, so an upgrade must explicitly remove both the stale settings.json
37
37
  // entries pointing at these and the leftover script files they reference.
38
38
  const RETIRED_HOOKS = [
39
- 'session-stop.js',
40
- 'post-tool-failure.js',
41
- 'post-commit.js',
42
39
  'generate-ownership-index.mjs', // folded into awareness.mjs — unretired, the index would inject twice
43
40
  ];
44
41
 
@@ -89,28 +86,6 @@ function writeJson(path, value) {
89
86
  writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
90
87
  }
91
88
 
92
- // Clean up any leftover marketplace/enabledPlugins entries from a previous 0.1.x
93
- // install. After 0.2.0 we don't use the marketplace flow at all — leaving stale
94
- // entries causes Claude Code to also resolve the plugin from the global cache,
95
- // double-loading the same skills under a different namespace.
96
- function migrateFromPluginInstall(targetDir, pluginName) {
97
- const removed = [];
98
- const projectSettingsPath = join(targetDir, '.claude', 'settings.json');
99
- if (existsSync(projectSettingsPath)) {
100
- const s = readJson(projectSettingsPath);
101
- if (s.enabledPlugins) {
102
- const keys = Object.keys(s.enabledPlugins).filter((k) => k.startsWith(`${pluginName}@`));
103
- for (const k of keys) {
104
- delete s.enabledPlugins[k];
105
- removed.push(`enabledPlugins["${k}"] in ${projectSettingsPath}`);
106
- }
107
- if (Object.keys(s.enabledPlugins).length === 0) delete s.enabledPlugins;
108
- writeJson(projectSettingsPath, s);
109
- }
110
- }
111
- return removed;
112
- }
113
-
114
89
  // Remove settings.json hook entries and .claude/hooks/ files for retired hook scripts.
115
90
  // Bundle-aware: a name on the retired list is skipped while THIS bundle still ships it —
116
91
  // several harnesses share the list, and they retire the same script at different versions.
@@ -305,18 +280,17 @@ function mergeHooks(targetDir, bundledHooksJson) {
305
280
  * entries pointing at scripts this bundle no longer carries, and folding the harness's hooks into
306
281
  * a settings.json that already belongs to somebody.
307
282
  *
308
- * `pluginName` is used only for the legacy marketplace cleanup. `projectHasGit` decides the backup
309
283
  * policy for the harness-owned artifacts a refresh overwrites — git is the safety net when it is
310
284
  * there, a `.bak.<timestamp>` copy when it is not.
311
285
  *
312
286
  * Returns the accounting the caller renders: `installed`, `notices`, `warnings`, plus `owned` (the
313
287
  * manifest written into `.claude/harness.json`) and `meta` (the bundle marker).
314
288
  */
315
- export function materializeHarness({ bundledHarnessDir, targetDir, pluginName, projectHasGit }) {
316
- const { files, owned, meta } = planHarnessFiles(bundledHarnessDir);
289
+ export function materializeHarness({ bundledHarnessDir, targetDir }) {
290
+ const { files, owned, meta, capabilityNotes } = planHarnessFiles(bundledHarnessDir, targetDir);
317
291
 
318
292
  const installed = [];
319
- const notices = [];
293
+ const notices = [...(capabilityNotes || [])];
320
294
  const warnings = [];
321
295
 
322
296
  // Read BEFORE anything is written: this is what a prior install recorded as harness-owned, and
@@ -333,7 +307,6 @@ export function materializeHarness({ bundledHarnessDir, targetDir, pluginName, p
333
307
  // makes the fold duty visible to the next session. The harness is deliberately unapologetic
334
308
  // about its own process; the backup is how it stays honest about yours.
335
309
  const firstContact = isFirstContact(targetDir);
336
- void projectHasGit; // kept for caller compatibility; the .bkp rule does not branch on it
337
310
 
338
311
  // The Codex mirrors are rebuilt wholesale, not merged: they are byte projections of the Claude
339
312
  // tree, so a file left behind from a prior version is a skill Codex still sees after the harness
@@ -408,7 +381,7 @@ export function materializeHarness({ bundledHarnessDir, targetDir, pluginName, p
408
381
  // `--skill=<name>` and deliberately absent from `owned`, which is what makes it survive pruning —
409
382
  // was present for Claude and missing for Codex after every roll. Ordering is load-bearing: run
410
383
  // before the prunes and a skill this bundle dropped would be mirrored straight back.
411
- const mirrored = projectSkills({ targetDir, projectHasGit, snapshot: false });
384
+ const mirrored = projectSkills({ targetDir, snapshot: false });
412
385
  if (mirrored?.installed) installed.push(mirrored.installed);
413
386
 
414
387
  // Retire hooks earlier harness versions shipped, before merging, so the additive merge never
@@ -418,9 +391,6 @@ export function materializeHarness({ bundledHarnessDir, targetDir, pluginName, p
418
391
  const added = mergeHooks(targetDir, join(bundledHarnessDir, 'hooks', 'hooks.json'));
419
392
  if (added > 0) installed.push(`${added} hook(s) → .claude/settings.json`);
420
393
 
421
- for (const entry of migrateFromPluginInstall(targetDir, pluginName)) {
422
- notices.push(`removed stale ${entry}`);
423
- }
424
394
 
425
395
  return { installed, notices, warnings, owned, meta };
426
396
  }