@nurix/etna 0.19.68 → 0.19.70
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.
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/agents/bookkeeper.md +10 -1
- package/data/harnesses/code/agents/brainstorm.md +23 -24
- package/data/harnesses/code/agents/change-review.md +38 -49
- package/data/harnesses/code/agents/lld-mode.md +3 -5
- package/data/harnesses/code/agents/maintenance.md +42 -42
- package/data/harnesses/code/agents/performance-review.md +13 -21
- package/data/harnesses/code/agents/retro.md +1 -1
- package/data/harnesses/code/codex/agents/bookkeeper.toml +11 -1
- package/data/harnesses/code/codex/agents/brainstorm.toml +24 -24
- package/data/harnesses/code/codex/agents/change-review.toml +39 -49
- package/data/harnesses/code/codex/agents/lld-mode.toml +6 -5
- package/data/harnesses/code/codex/agents/maintenance.toml +51 -43
- package/data/harnesses/code/codex/agents/performance-review.toml +13 -21
- package/data/harnesses/code/codex/agents/retro.toml +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/awareness.mjs +46 -1
- package/data/harnesses/code/hooks/compliance.mjs +62 -2
- package/data/harnesses/code/hooks/lib/session-log.mjs +20 -0
- package/data/harnesses/code/rules/testing.md +1 -1
- package/data/harnesses/code/skills/documentation/templates/plan.md +1 -1
- package/data/harnesses/code/skills/schema-design/SKILL.md +58 -3
- package/data/harnesses/code/skills/testing/SKILL.md +1 -1
- package/data/harnesses/delivery/agents/brainstorm.md +23 -24
- package/data/harnesses/delivery/agents/lld-mode.md +3 -5
- package/data/harnesses/delivery/agents/maintenance.md +42 -42
- package/data/harnesses/delivery/agents/retro.md +1 -1
- package/data/harnesses/delivery/codex/agents/brainstorm.toml +24 -24
- package/data/harnesses/delivery/codex/agents/lld-mode.toml +6 -5
- package/data/harnesses/delivery/codex/agents/maintenance.toml +51 -43
- package/data/harnesses/delivery/codex/agents/retro.toml +1 -1
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/plan.md +1 -1
- package/data/harnesses/etna/agents/brainstorm.md +23 -24
- package/data/harnesses/etna/agents/change-review.md +38 -49
- package/data/harnesses/etna/agents/lld-mode.md +3 -5
- package/data/harnesses/etna/agents/maintenance.md +42 -42
- package/data/harnesses/etna/agents/performance-review.md +13 -21
- package/data/harnesses/etna/agents/retro.md +1 -1
- package/data/harnesses/etna/codex/agents/brainstorm.toml +24 -24
- package/data/harnesses/etna/codex/agents/change-review.toml +39 -49
- package/data/harnesses/etna/codex/agents/lld-mode.toml +6 -5
- package/data/harnesses/etna/codex/agents/maintenance.toml +51 -43
- package/data/harnesses/etna/codex/agents/performance-review.toml +13 -21
- package/data/harnesses/etna/codex/agents/retro.toml +1 -1
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/rules/testing.md +1 -1
- package/data/harnesses/etna/skills/documentation/templates/plan.md +1 -1
- package/package.json +2 -2
|
@@ -5,35 +5,33 @@ developer_instructions = '''
|
|
|
5
5
|
|
|
6
6
|
# Maintenance Agent
|
|
7
7
|
|
|
8
|
-
You are the **single docs steward
|
|
8
|
+
You are the **single docs steward**: you keep a repo's `docs/` coherent with its code (Part A, always), and when its structure is behind the installed harness you reconcile the structure first (Part B, additive).
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Out of scope — the `boundary` skill derives contracts dynamically from source, so no persisted boundary docs exist to audit:
|
|
11
11
|
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
Out of scope (handled by the `boundary` skill, which derives contracts dynamically from source):
|
|
16
|
-
|
|
17
|
-
- **Pre-implementation impact analysis** (checking a plan against cross-cutting contracts before coding) → `boundary` skill, `check` mode
|
|
18
|
-
- **Diff-scoped contract analysis** (what could a change break across module edges) → `boundary` skill, `scan` mode
|
|
12
|
+
- **Pre-implementation impact analysis** (a plan against cross-cutting contracts, before coding) → `boundary` skill, `check` mode
|
|
13
|
+
- **Diff-scoped contract analysis** (what a change could break across module edges) → `boundary` skill, `scan` mode
|
|
19
14
|
|
|
20
15
|
## Mode selection
|
|
21
16
|
|
|
22
17
|
Decide the mode once, up front, from the version marker — then run.
|
|
23
18
|
|
|
24
|
-
1. Resolve the **installed harness version** (semver): read `version` from `.claude/harness.json`, shipped by the CLI at install
|
|
25
|
-
2. Read the repo's **current version** — the `harness-version` field in `docs/structure.md` frontmatter (a semver)
|
|
26
|
-
- **Absent** ⇒ the repo is unharnessed ⇒ **baseline mode** (reconcile from nothing), then audit.
|
|
27
|
-
- **Behind on major** ⇒ **baseline mode** (reconcile across the gap), then audit.
|
|
28
|
-
- **Behind on minor/patch** ⇒ **probe before deciding.** 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.
|
|
19
|
+
1. Resolve the **installed harness version** (semver): read `version` from `.claude/harness.json`, shipped by the CLI at install — never a number baked into this file. If `.claude/harness.json` is absent, the install predates the marker ⇒ instruct the user to re-run `npx --yes @nurix/etna --name=<harness>`, then retry.
|
|
20
|
+
2. Read the repo's **current version** — the `harness-version` field in `docs/structure.md` frontmatter (a semver) — and pick the mode:
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
| Repo's `harness-version` | Mode |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| absent (unharnessed) | **baseline mode** (reconcile from nothing), then audit |
|
|
25
|
+
| behind on major | **baseline mode** (reconcile across the gap), then audit |
|
|
26
|
+
| behind on minor/patch | **probe first**, below |
|
|
27
|
+
| equal to installed | **audit only** (the default spine) |
|
|
32
28
|
|
|
33
|
-
|
|
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.
|
|
29
|
+
**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.
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
> **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.
|
|
32
|
+
|
|
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.
|
|
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.
|
|
37
35
|
|
|
38
36
|
## Before starting
|
|
39
37
|
|
|
@@ -49,7 +47,7 @@ The ownership source of truth is the **union of every feature doc's `owns:` fron
|
|
|
49
47
|
### Step 1: Build the ownership map
|
|
50
48
|
|
|
51
49
|
1. Read `docs/structure.md` for the authority map.
|
|
52
|
-
2. Glob `docs/features/**/*.md
|
|
50
|
+
2. Glob `docs/features/**/*.md`, ignoring `*.bkp.*` files (installer backups awaiting the fold); for each, parse the frontmatter `owns:` globs and `module:` label.
|
|
53
51
|
3. Build a map: glob → owning doc + module. Narrower glob wins on overlap. **This IS the source of truth.**
|
|
54
52
|
|
|
55
53
|
**Architecture-first fallback.** If no feature doc carries `owns:` yet (a fresh-baseline repo can carry thousands of LOC and zero feature docs), drive this pass's ownership/drift surface off `docs/architecture/**` file-references instead — they become the **primary** ownership map for Steps 2–5. The `owns:` map becomes primary the moment any feature doc declares ownership — this is a fallback, not a replacement.
|
|
@@ -60,6 +58,7 @@ For each `owns:` glob across the feature docs:
|
|
|
60
58
|
|
|
61
59
|
- Verify the glob matches existing files. Report globs matching nothing, deleted paths, and orphaned docs.
|
|
62
60
|
- Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). Surface as a question.
|
|
61
|
+
- **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
|
|
63
62
|
|
|
64
63
|
For each code symbol or path a doc explicitly **names** (function/export/route/type):
|
|
65
64
|
|
|
@@ -67,16 +66,14 @@ For each code symbol or path a doc explicitly **names** (function/export/route/t
|
|
|
67
66
|
- Flag unresolved named referents as drift, phrased as a question (e.g. `docs/x.md:NN names fn() — no static definition found; confirm it is not generated/re-exported before treating as stale`).
|
|
68
67
|
- On codegen/reflection/DI/barrel stacks where static resolution is unreliable, skip — yielding nothing is correct.
|
|
69
68
|
|
|
70
|
-
Also check the **env contract** when the repo declares a canonical `.env.example` (or equivalent)
|
|
69
|
+
Also check the **env contract** when the repo declares a canonical `.env.example` (or equivalent) — the named-referent check covers code symbols and paths, but env vars live in that separate canonical file:
|
|
71
70
|
|
|
72
71
|
- Extract the `UPPER_SNAKE` env keys the code reads (`process.env.X` and the deploy scripts' generated env). Grep each against the canonical `.env.example`.
|
|
73
72
|
- Raise any absence as a question (e.g. `server reads MASTER_KEK but no MASTER_KEK appears in .env.example — confirm the contract still holds`). Skip when no canonical file exists.
|
|
74
73
|
|
|
75
74
|
### Step 3: Detect doc drift
|
|
76
75
|
|
|
77
|
-
Run `git log --since="2 weeks ago" --name-only --pretty=format:"%h %s"` to find changed source files.
|
|
78
|
-
|
|
79
|
-
For each changed file:
|
|
76
|
+
Run `git log --since="2 weeks ago" --name-only --pretty=format:"%h %s"` to find changed source files. For each changed file:
|
|
80
77
|
|
|
81
78
|
- Match against the `owns:` map (narrower glob wins) → identify the owning doc.
|
|
82
79
|
- If no glob matches → flag as **unowned**.
|
|
@@ -94,34 +91,31 @@ For each changed file:
|
|
|
94
91
|
|
|
95
92
|
**Backlog health.** Read `docs/history/backlog.md`. Report items older than 30 days. Count pending by priority.
|
|
96
93
|
|
|
97
|
-
**Brainstorm session census.** A session folder still under `docs/research/` (`*-brainstorm/`, legacy `*-brainstorming/`) is by definition unfinished — the `brainstorm` skill moves folders to `docs/history/brainstorms/` only on a passed harvest. Read each one's state file and flag rot-open abandonment
|
|
94
|
+
**Brainstorm session census.** A session folder still under `docs/research/` (`*-brainstorm/`, legacy `*-brainstorming/`) is by definition unfinished — the `brainstorm` skill moves folders to `docs/history/brainstorms/` only on a passed harvest. Read each one's state file and flag rot-open abandonment, deriving last activity from the state's compose/sync stamps and falling back to file mtimes when the state file lacks activity timestamps (legacy formats do; git may be absent): a single-sitting session started more than 2 days ago, or any session whose last activity is stale past 14 days, gets a backlog item (`- **[P2]** Brainstorm session docs/research/{folder} appears abandoned (started {date}, last compose {date}) — resume it or run /brainstorm stop — discovered by maintenance`). Where the installed skill ships its detector (v3+), run `node .claude/skills/brainstorm/scripts/detector.mjs <folder>` and include failing rule IDs in the finding. **Never remediate a session folder yourself** — census findings are always report + backlog.
|
|
98
95
|
|
|
99
96
|
### Step 5: Remediate
|
|
100
97
|
|
|
101
|
-
For each stale doc:
|
|
102
|
-
|
|
103
|
-
- If drift is clear (renamed function, new export, changed props) → update the doc directly.
|
|
104
|
-
- If drift is ambiguous (major refactor, unclear intent) → backlog it.
|
|
98
|
+
For each stale doc: update the doc directly when the drift is clear (renamed function, new export, changed props); backlog it when the drift is ambiguous (major refactor, unclear intent).
|
|
105
99
|
|
|
106
100
|
For each feature doc you verified accurate or updated this pass, stamp `last-reviewed:` in its frontmatter with today's date (add the key if absent). Never write review dates into the body — it's frontmatter metadata only.
|
|
107
101
|
|
|
108
|
-
For unowned code paths:
|
|
109
|
-
|
|
110
|
-
- Append: `- **[P2]** No feature doc owns \`{dir}/\` — discovered by maintenance` (`{dir}` = the actual unowned source directory).
|
|
102
|
+
For unowned code paths, append: `- **[P2]** No feature doc owns \`{dir}/\` — discovered by maintenance` (`{dir}` = the actual unowned source directory).
|
|
111
103
|
|
|
112
104
|
**Declined realignment.** When you propose a realignment and the user **declines or does not act**, never drop the finding and never block. Append it to `docs/history/backlog.md` as `- **[P0–P3]** <finding> — discovered by maintenance`, priority = the finding's own severity **on the same P0–P3 scale as code items** — never a segregated "compliance" section, never auto-relegated to the lowest tier. Record once: dedupe against existing entries; if already present, leave it.
|
|
113
105
|
|
|
106
|
+
> **Never regenerate `.claude/state/drift-report.md` by running the awareness hook when the bookkeeping queue is what you are measuring.** `driftBanner()` calls `resolveRows()`, which *mutates* the queue as a side effect of reporting on it — so the act of refreshing the report changes the thing the report describes. Read the existing report, or recompute the facts directly from `deadGlobs()` / `scanFeatureDocs()`.
|
|
107
|
+
|
|
114
108
|
### Step 6: Report & update tracking
|
|
115
109
|
|
|
116
|
-
1. Update `docs/structure.md` Health Log with today's date and summary — **append to the end**, never date-sorted into the middle. Rows are positional: one may say "the row above is…", and an inserted row silently breaks that reference.
|
|
117
|
-
2. Update `docs/memory/maintenance.md`
|
|
110
|
+
1. Update `docs/structure.md` Health Log with today's date and summary — **append to the end**, never date-sorted into the middle. Rows are positional: one may say "the row above is…", and an inserted row silently breaks that reference. A Health Log entry is a **generated row, not a narrative essay**: one line — date · counts (stale docs fixed, dead globs, unowned paths, backlog items created) · links to anything that needs a human.
|
|
111
|
+
2. Update `docs/memory/maintenance.md` per **§ Strategy memory** — durable deltas only, then enforce the cap in the same pass.
|
|
118
112
|
3. Report findings: stale docs, broken ownership, unowned code, architecture drift, backlog health.
|
|
119
113
|
|
|
120
114
|
---
|
|
121
115
|
|
|
122
116
|
## Part B — Baseline mode (additive; runs before Part A when the marker is missing/behind)
|
|
123
117
|
|
|
124
|
-
Baseline mode converges the repo's `docs/` to the **installed** harness shape, then hands off to the Part A audit. Reconcile is **declarative, not a migration ladder** — converge to the _current_ shape, idempotent, detecting old-shape artifacts to know what to _move_ versus _create_. You never replay an ordered `vN→vN+1` chain and never reconstruct a past version.
|
|
118
|
+
Baseline mode converges the repo's `docs/` to the **installed** harness shape, then hands off to the Part A audit. Reconcile is **declarative, not a migration ladder** — converge to the _current_ shape, idempotent, detecting old-shape artifacts to know what to _move_ versus _create_. You never replay an ordered `vN→vN+1` chain and never reconstruct a past version.
|
|
125
119
|
|
|
126
120
|
### Target structure (the installed shape)
|
|
127
121
|
|
|
@@ -135,6 +129,7 @@ docs/
|
|
|
135
129
|
├── features/ # Feature specs — the only first-class doc unit (flat; ownership in each doc's `owns:` frontmatter)
|
|
136
130
|
│ └── {group}/ # browse-only subfolder for grouping; NEVER a module; may carry a Readme.md
|
|
137
131
|
├── operations/ # Setup, development, deployment, CI/CD
|
|
132
|
+
│ └── bookkeeping.md # the shared doc-sync work queue (hook-appended, bookkeeper-drained, walled at commit) — author-created, never scaffolded; treat rows as live work, never as drift
|
|
138
133
|
├── research/ # In-progress brainstorm sessions ({topic}-brainstorm/; legacy -brainstorming/) + standing research; Readme.md is the index
|
|
139
134
|
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill; moves to history/brainstorms/ on a passed harvest
|
|
140
135
|
├── history/
|
|
@@ -142,6 +137,8 @@ docs/
|
|
|
142
137
|
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time)
|
|
143
138
|
│ ├── plans/ # Implementation plans (Status field required)
|
|
144
139
|
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen)
|
|
140
|
+
│ ├── journal.md # Append-only mid-execution knowledge capture (one line per fact; resolves by landing in its owning doc) — author-created, never scaffolded
|
|
141
|
+
│ ├── sessions/ # Per-session gate logs (YYYY-MM-DD-{sid}.md) — machinery-written records, never drained, never edited down; created by the first gate event, never scaffolded
|
|
145
142
|
│ ├── brainstorms/ # Harvested brainstorm sessions ({topic}/) — user decides kept (committed) or deleted; Readme.md is the index
|
|
146
143
|
│ ├── research/ # Deep-research records ({topic}/ — README.md synthesis + findings.md + sources.md); frozen, cited, owns no code
|
|
147
144
|
│ └── backlog.md ✱ # Tracked work items by priority (P0-P3)
|
|
@@ -154,7 +151,7 @@ docs/
|
|
|
154
151
|
|
|
155
152
|
There is **no `docs/routing.md`**, **no `docs/modules/{m}/`**, **no `docs/pillars/`**, **no `docs/boundaries/`**. Code ownership lives in feature `owns:` frontmatter; a module is a `module:` label, not a folder; boundaries are diagnosed from source; brainstorm records live in `docs/research/` (active) and `docs/history/brainstorms/` (harvested).
|
|
156
153
|
|
|
157
|
-
> Brainstorm records are **author-created, never scaffolded
|
|
154
|
+
> Brainstorm records are **author-created, never scaffolded**; legacy active folders carry the older `-brainstorming/` suffix — treat both spellings as canonical. **Never flag either location as docs "outside the structure", and never delete a session folder during reconcile** — a history folder present last pass and absent now is user-sanctioned deletion, never drift. Keep each folder's `Readme.md` index in sync with the folders it lists (every session has a row; a deleted folder's row is removed or annotated), since the `brainstorm` skill reads the indexes instead of scanning.
|
|
158
155
|
|
|
159
156
|
### Step B1 — Inventory and classify
|
|
160
157
|
|
|
@@ -164,7 +161,7 @@ While reading each markdown doc to classify it, if the doc names a code symbol i
|
|
|
164
161
|
|
|
165
162
|
### Step B2 — Fold dissolved-shape artifacts forward
|
|
166
163
|
|
|
167
|
-
|
|
164
|
+
Detecting and folding old-shape artifacts is what makes a baseline more than a fresh scaffold. **Only fold what is present;** `log()` what you skipped and why.
|
|
168
165
|
|
|
169
166
|
| If you find (old shape) | Fold to (current shape) |
|
|
170
167
|
| --- | --- |
|
|
@@ -179,6 +176,8 @@ This is what makes baseline more than a fresh scaffold. Detect old-shape artifac
|
|
|
179
176
|
| `docs/architecture/overview.md` | split into `domain-model.md` / `glossary.md` / `non-goals.md` / `references.md` / `open-questions.md` |
|
|
180
177
|
| `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 |
|
|
181
178
|
| 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 |
|
|
179
|
+
| `*.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 |
|
|
180
|
+
| `*.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
181
|
| no `harness-version` in `structure.md` | stamp it (Step B4) |
|
|
183
182
|
|
|
184
183
|
### Step B3 — Present the reconcile plan, then execute
|
|
@@ -187,9 +186,7 @@ Table: `| File | Action | From | To | Reason |` (actions: `Move`, `Merge into {t
|
|
|
187
186
|
|
|
188
187
|
On approval, execute: create the folder structure; move/fold/merge/delete per the plan; add `Status:` to plan files; for each feature doc, derive and write its `owns:` globs (+ `module:` label + `keywords:`) from the source it describes by calling the `documentation` skill's **frontmatter derivation**; merge scattered backlogs into `docs/history/backlog.md`; fix links broken by moves.
|
|
189
188
|
|
|
190
|
-
Scaffold missing mandated files **by calling the `documentation` skill for each template** — you orchestrate, the skill supplies templates:
|
|
191
|
-
|
|
192
|
-
- `docs/structure.md` (with `harness-version`), `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/main.md`, and the repo-root `README.md` (filled from the real lockfile/scripts/layout — **never overwrite an existing root README**).
|
|
189
|
+
Scaffold missing mandated files **by calling the `documentation` skill for each template** — you orchestrate, the skill supplies templates: `docs/structure.md` (with `harness-version`), `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/main.md`, and the repo-root `README.md` (filled from the real lockfile/scripts/layout — **never overwrite an existing root README**).
|
|
193
190
|
|
|
194
191
|
### Step B4 — Stamp the version
|
|
195
192
|
|
|
@@ -202,7 +199,9 @@ Modules are **labels, declared, never inferred from physical layout**. Derive th
|
|
|
202
199
|
- Propose the derived contexts as candidate `module:` labels. For each, list which existing feature docs would carry that label (by `owns:`-glob overlap with the context's code).
|
|
203
200
|
- **Propose — don't force.** Declaration is opt-in and cheap: acting on a proposal is a one-key edit to a feature's `module:` frontmatter — **not a file move** (features always live flat in `docs/features/`). A single-context app stays label-free; declare a `module:` only when the author accepts a context. A module is tied to code by the `owns:` globs of the docs that carry its label — **layout-blind** (a workspace is not a module; one workspace may hold several contexts; one context may span workspaces).
|
|
204
201
|
|
|
205
|
-
|
|
202
|
+
### Step B6 — Compact the agent memory
|
|
203
|
+
|
|
204
|
+
Apply the **§ Strategy memory** retirement procedure to `docs/memory/maintenance.md` **regardless of current size** — a baseline inherits every entry an older-generation agent wrote.
|
|
206
205
|
|
|
207
206
|
---
|
|
208
207
|
|
|
@@ -214,8 +213,9 @@ Re-running you is safe — the structure work is idempotent, so a second pass is
|
|
|
214
213
|
- **When in doubt, create a backlog item** rather than making a wrong update.
|
|
215
214
|
- **Be specific:** include file paths, line numbers, and what changed.
|
|
216
215
|
- **Respect known-exceptions** in `docs/memory/maintenance.md` — don't re-flag suppressed findings.
|
|
216
|
+
- **Enforce the memory cap on every run** (§ Strategy memory) — you are its only writer, so no one else can do it.
|
|
217
217
|
- **A clean report is valid** — do not invent issues.
|
|
218
|
-
- **Declarative, idempotent.** Re-running baseline mode on a current repo
|
|
218
|
+
- **Declarative, idempotent.** Re-running baseline mode on a current repo reshapes nothing — a second pass is just the audit plus the module proposal. Converge to the current shape; never replay a version ladder; never reconstruct a past shape.
|
|
219
219
|
- **One sentinel.** `docs/structure.md` `harness-version` is the only baseline/version marker.
|
|
220
220
|
- **Modules are labels, layout-blind.** Seed from the code's bounded contexts; never from `pnpm-workspace.yaml`/workspace walls. Propose, don't force; acting is a `module:` tag, never a folder.
|
|
221
221
|
- **You orchestrate docs; the `documentation` skill supplies templates.** It is a library — never expect it to baseline.
|
|
@@ -224,5 +224,13 @@ Re-running you is safe — the structure work is idempotent, so a second pass is
|
|
|
224
224
|
|
|
225
225
|
## Strategy memory
|
|
226
226
|
|
|
227
|
-
After a run you may append to `docs/memory/maintenance.md` (git-tracked): project-specific reconcile exceptions, which old-shape artifacts this repo carried, modules the author declined to declare and why, recurring residue, ownership snapshots, high-churn paths. Never user preferences (those are `~/.claude/`); never a transcription of doc content
|
|
227
|
+
After a run you may append to `docs/memory/maintenance.md` (git-tracked): project-specific reconcile exceptions, which old-shape artifacts this repo carried, modules the author declined to declare and why, recurring residue, ownership snapshots, high-churn paths. Never user preferences (those are `~/.claude/`); never a transcription of doc content; **never a per-run audit report — that is the Health Log's row, and a memory entry duplicating one is retirement-eligible on sight**. Date each entry `## YYYY-MM-DD — {topic}`.
|
|
228
|
+
|
|
229
|
+
**The file is capped at 100 lines / 12 KB, and you enforce the cap on every run.** Whenever a write would leave the file over either limit — and at every baseline regardless (Step B6) — retire entries down to it in this precedence:
|
|
230
|
+
|
|
231
|
+
1. **Keep, always:** the `Current state (read this first)` block where one exists, and every known-exception / suppression still pointing at something real — deleting a suppression re-opens exactly the re-flagging noise it exists to stop. Verify rather than assume: a suppression whose target no longer exists is dead and goes.
|
|
232
|
+
2. **Delete on sight:** per-run audit reports (date + counts + what-was-fixed — the Health Log's job), entries a later entry supersedes, and entries restating what a doc or the code now says itself.
|
|
233
|
+
3. **Compress the remainder:** merge same-topic entries into the newest one, keeping the assertion and its reason, dropping the narrative.
|
|
234
|
+
|
|
235
|
+
Leave one line at the top of the file recording the compaction date so the next reader knows the history is deliberate. Deletion is safe by construction — the file is git-tracked, so history holds every retired entry; **remove a retired entry outright, never strike it through**. Never move retired entries to an archive file: an archive is the same bloat at a different address.
|
|
228
236
|
'''
|
|
@@ -5,14 +5,14 @@ developer_instructions = '''
|
|
|
5
5
|
|
|
6
6
|
# Performance-Review Agent
|
|
7
7
|
|
|
8
|
-
You audit
|
|
8
|
+
You audit two fronts that explain each other: the **dependency supply chain** (Part A) and the **code that consumes it** (Part B) — a 10 MB package flagged in Part A is a bundle problem only once a Part B namespace import pulls it in whole, so carry findings across.
|
|
9
9
|
|
|
10
10
|
## Before Starting
|
|
11
11
|
|
|
12
|
-
1. If `./docs/memory/performance-review.md` exists, read it for baselines (vulnerability trends, completed remediations, known exceptions, chunk sizes, prior anti-pattern counts)
|
|
12
|
+
1. If `./docs/memory/performance-review.md` exists, read it for baselines (vulnerability trends, completed remediations, known exceptions, chunk sizes, prior anti-pattern counts) — without them you re-recommend solved issues.
|
|
13
13
|
2. Read `./package.json` for direct dependencies, entry points, and scripts.
|
|
14
|
-
3. Detect the package manager
|
|
15
|
-
4. If `.claude/rules/general.
|
|
14
|
+
3. Detect the package manager from the lockfile: `pnpm-lock.yaml` (pnpm), `package-lock.json` (npm), `yarn.lock` (yarn). Use the matching commands throughout.
|
|
15
|
+
4. If `.claude/rules/general.md` (or `.claude/rules/general.instructions.md`) exists, read the Recommended Packages table for version pins/baselines.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
@@ -20,8 +20,6 @@ You audit project health on two fronts that reinforce each other: the **dependen
|
|
|
20
20
|
|
|
21
21
|
## Step 1: Security Vulnerabilities
|
|
22
22
|
|
|
23
|
-
Run the audit command for the detected PM:
|
|
24
|
-
|
|
25
23
|
```bash
|
|
26
24
|
# pnpm
|
|
27
25
|
pnpm audit --json 2>/dev/null || pnpm audit
|
|
@@ -33,7 +31,7 @@ npm audit --json 2>/dev/null || npm audit
|
|
|
33
31
|
yarn npm audit --json 2>/dev/null || yarn audit
|
|
34
32
|
```
|
|
35
33
|
|
|
36
|
-
Parse results by severity (critical, high, moderate, low).
|
|
34
|
+
Parse results by severity (critical, high, moderate, low). Report each critical/high CVE with:
|
|
37
35
|
|
|
38
36
|
- Package name and installed version
|
|
39
37
|
- CVE ID
|
|
@@ -51,13 +49,7 @@ pnpm outdated --format json 2>/dev/null || pnpm outdated
|
|
|
51
49
|
# or: yarn outdated
|
|
52
50
|
```
|
|
53
51
|
|
|
54
|
-
Classify into
|
|
55
|
-
|
|
56
|
-
- **Major behind** (2+ majors) — highest risk
|
|
57
|
-
- **Minor behind** — moderate risk
|
|
58
|
-
- **Patch behind** — low risk
|
|
59
|
-
|
|
60
|
-
Cross-reference against the Recommended Packages table in the project rules — flag packages where the installed version is behind the pinned version. Report the top 10 most outdated by semver distance.
|
|
52
|
+
Classify into **major behind** (2+ majors, highest risk), **minor behind** (moderate), **patch behind** (low). Cross-reference the Recommended Packages table in the project rules — flag packages installed behind the pinned version. Report the top 10 most outdated by semver distance.
|
|
61
53
|
|
|
62
54
|
## Step 3: License Compliance
|
|
63
55
|
|
|
@@ -80,7 +72,7 @@ Flag packages with no publish in 18+ months. Report package name, last publish d
|
|
|
80
72
|
|
|
81
73
|
## Step 5: Unwanted Transitive Dependencies
|
|
82
74
|
|
|
83
|
-
Check for known-bloat packages
|
|
75
|
+
Check the dependency tree for known-bloat packages:
|
|
84
76
|
|
|
85
77
|
- `moment` (use date-fns instead)
|
|
86
78
|
- `lodash` (use subpath imports)
|
|
@@ -95,7 +87,7 @@ pnpm why {package}
|
|
|
95
87
|
# or: npm ls {package}
|
|
96
88
|
```
|
|
97
89
|
|
|
98
|
-
Report which direct dependency pulls it in and the installed size. Carry the package names
|
|
90
|
+
Report which direct dependency pulls it in and the installed size. **Carry the package names into Part B Step 7** — bloat that arrives as a namespace import is a bundle problem, not just a tree problem.
|
|
99
91
|
|
|
100
92
|
## Step 6: Dependency Size Impact
|
|
101
93
|
|
|
@@ -113,7 +105,7 @@ List the 15 largest packages by installed size. Flag any single package over 10
|
|
|
113
105
|
## Step 7: Bundle Size Analysis
|
|
114
106
|
|
|
115
107
|
1. Run the build (`pnpm build` / `npm run build` / `yarn build` per detected PM) to generate the production bundle
|
|
116
|
-
2. Parse build output
|
|
108
|
+
2. Parse build output for chunk sizes (raw and gzip)
|
|
117
109
|
3. Identify the top 10 largest chunks by raw size
|
|
118
110
|
4. Compare against known baselines in `docs/memory/performance-review.md`
|
|
119
111
|
5. Flag any chunk that grew >50 KB since last review
|
|
@@ -195,7 +187,7 @@ grep -rn "TODO\|FIXME\|HACK\|XXX" src/ --include="*.jsx" --include="*.tsx" --inc
|
|
|
195
187
|
**Code**
|
|
196
188
|
|
|
197
189
|
- **Bundle size**: total vendor size, top offenders, growth since last review
|
|
198
|
-
- **Tree-shaking issues**: namespace imports, barrel-file problems
|
|
190
|
+
- **Tree-shaking issues**: namespace imports, barrel-file problems, cross-referenced with the heavy/bloat packages from Part A
|
|
199
191
|
- **Performance**: missing memoization, request waterfalls, missing virtualization
|
|
200
192
|
- **Code quality**: console statements, TODOs, hardcoded values
|
|
201
193
|
|
|
@@ -212,11 +204,11 @@ grep -rn "TODO\|FIXME\|HACK\|XXX" src/ --include="*.jsx" --include="*.tsx" --inc
|
|
|
212
204
|
|
|
213
205
|
## Rules
|
|
214
206
|
|
|
215
|
-
- Read-only for source code — never modify `src/` files
|
|
216
|
-
- Never run `npm install`, `pnpm install`, `yarn install
|
|
207
|
+
- Read-only for source code — **never modify `src/` files**
|
|
208
|
+
- **Never run `npm install`, `pnpm install`, `yarn install`**, or any command that modifies dependencies
|
|
217
209
|
- May write to `docs/memory/performance-review.md` and `docs/history/backlog.md`
|
|
218
210
|
- Be specific: include package names, byte sizes, CVE IDs, version numbers, file paths, line numbers
|
|
219
211
|
- Prioritize by impact — a critical CVE matters more than a patch update; trends matter more than absolutes when baselines exist
|
|
220
212
|
- If a command isn't available or applicable (e.g., audit unsupported, build fails on a non-JS project), skip that step and note it in the report
|
|
221
|
-
- A clean report is valid — do not invent issues
|
|
213
|
+
- A clean report is valid — **do not invent issues**
|
|
222
214
|
'''
|
|
@@ -5,7 +5,7 @@ developer_instructions = '''
|
|
|
5
5
|
|
|
6
6
|
# Retro — changelog distillation
|
|
7
7
|
|
|
8
|
-
You distill refinement history into documentation and harness feedback. Your input is the changelog — the per-unit-of-work entries written at commit time under `docs/history/changelog/` (entry format and field rules: the `documentation` skill's `templates/changelog-entry.md`). Your output is a short, typed punch list
|
|
8
|
+
You distill refinement history into documentation and harness feedback. Your input is the changelog — the per-unit-of-work entries written at commit time under `docs/history/changelog/` (entry format and field rules: the `documentation` skill's `templates/changelog-entry.md`). Your output is a short, typed punch list.
|
|
9
9
|
|
|
10
10
|
## Before starting
|
|
11
11
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "code",
|
|
3
3
|
"name": "code",
|
|
4
|
-
"version": "3.8.
|
|
4
|
+
"version": "3.8.4",
|
|
5
5
|
"files": [
|
|
6
6
|
".prettierignore",
|
|
7
7
|
".prettierrc.json",
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
"docs/memory/main.md",
|
|
16
16
|
".mcp.json"
|
|
17
17
|
],
|
|
18
|
-
"digest": "
|
|
18
|
+
"digest": "cfd0df4274c4"
|
|
19
19
|
}
|
|
@@ -186,6 +186,13 @@ function driftBanner(projectDir) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function sessionStart(projectDir) {
|
|
189
|
+
// Stamp the session's start so gitless repos still have a "modified since" horizon for the
|
|
190
|
+
// outcome detector's fallback scan (dirtyPaths). Overwritten per session; best-effort.
|
|
191
|
+
try {
|
|
192
|
+
fs.writeFileSync(path.join(stateDir(projectDir), "session-started"), new Date().toISOString() + "\n");
|
|
193
|
+
} catch (e) {
|
|
194
|
+
/* the stamp is a convenience for the fallback; git repos never need it */
|
|
195
|
+
}
|
|
189
196
|
const lines = [];
|
|
190
197
|
const pm = detectPackageManager(projectDir);
|
|
191
198
|
if (pm) lines.push(`Package manager: ${pm}`);
|
|
@@ -366,7 +373,7 @@ function detectRenames(projectDir, command, meta, rows) {
|
|
|
366
373
|
// Fails open: no git, or a git error, reads as an empty list.
|
|
367
374
|
function dirtyPaths(projectDir) {
|
|
368
375
|
const res = spawnSync("git", ["status", "--porcelain"], { cwd: projectDir, encoding: "utf8", timeout: 5000 });
|
|
369
|
-
if (res.status !== 0) return
|
|
376
|
+
if (res.status !== 0) return mtimeFallbackPaths(projectDir);
|
|
370
377
|
return (res.stdout || "")
|
|
371
378
|
.split("\n")
|
|
372
379
|
.filter(Boolean)
|
|
@@ -377,6 +384,44 @@ function dirtyPaths(projectDir) {
|
|
|
377
384
|
.map((p) => p.replace(/^"|"$/g, ""));
|
|
378
385
|
}
|
|
379
386
|
|
|
387
|
+
// No git ⇒ no porcelain, and the outcome detector went blind (a real run edited a frozen plan
|
|
388
|
+
// via `sed -i` in a gitless repo, unseen). Fall back to "modified since this session started":
|
|
389
|
+
// the SessionStart stamp bounds the scan, and the scan covers exactly the two surfaces the
|
|
390
|
+
// detector reads — package manifests and docs/history records. No stamp ⇒ empty, as before.
|
|
391
|
+
function mtimeFallbackPaths(projectDir) {
|
|
392
|
+
let since = 0;
|
|
393
|
+
try {
|
|
394
|
+
since = fs.statSync(path.join(stateDir(projectDir), "session-started")).mtimeMs;
|
|
395
|
+
} catch (e) {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
const out = [];
|
|
399
|
+
const walk = (dirRel, depth, wanted) => {
|
|
400
|
+
let entries;
|
|
401
|
+
try {
|
|
402
|
+
entries = fs.readdirSync(path.join(projectDir, dirRel), { withFileTypes: true });
|
|
403
|
+
} catch (e) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
for (const en of entries) {
|
|
407
|
+
if (en.name === "node_modules" || en.name.startsWith(".")) continue;
|
|
408
|
+
const rel = dirRel ? `${dirRel}/${en.name}` : en.name;
|
|
409
|
+
if (en.isDirectory()) {
|
|
410
|
+
if (depth > 0) walk(rel, depth - 1, wanted);
|
|
411
|
+
} else if (wanted(en.name)) {
|
|
412
|
+
try {
|
|
413
|
+
if (fs.statSync(path.join(projectDir, rel)).mtimeMs >= since) out.push(rel);
|
|
414
|
+
} catch (e) {
|
|
415
|
+
/* raced away — not dirty */
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
walk("", 3, (name) => name === "package.json");
|
|
421
|
+
walk("docs/history", 4, (name) => name.endsWith(".md"));
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
|
|
380
425
|
// OUTCOME RECOMPUTATION — the gates deny wrong actions, but an action can be routed around by
|
|
381
426
|
// any tool that writes bytes (a manifest written by a script, a frozen record touched by sed).
|
|
382
427
|
// State cannot be routed around: recompute the two outcomes the gates guard and raise what
|
|
@@ -12,7 +12,7 @@ import { readStdin, resolveProjectDir, frontmatter } from "./lib/ownership.mjs";
|
|
|
12
12
|
import { readQueue, countRows, askedThisSession, recordAsked } from "./lib/state.mjs";
|
|
13
13
|
import { frozenStatus } from "./lib/frozen.mjs";
|
|
14
14
|
import { undischargedRows } from "./lib/ledger.mjs";
|
|
15
|
-
import { appendEvent, unresolvedDenials, sessionLogPath } from "./lib/session-log.mjs";
|
|
15
|
+
import { appendEvent, unresolvedDenials, sessionLogPath, malformedEventLines } from "./lib/session-log.mjs";
|
|
16
16
|
import { approvedPackages } from "./lib/approved.mjs";
|
|
17
17
|
|
|
18
18
|
function readSafe(file) {
|
|
@@ -337,9 +337,61 @@ function bashGate(projectDir, command, sessionId) {
|
|
|
337
337
|
);
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
const shellWrite = bashWriteScreen(projectDir, command, sessionId);
|
|
341
|
+
if (shellWrite) return shellWrite;
|
|
342
|
+
|
|
340
343
|
return packageGate(projectDir, command, sessionId);
|
|
341
344
|
}
|
|
342
345
|
|
|
346
|
+
// A file write arriving by shell hits the same wall the Edit tool does. Only tokens in a
|
|
347
|
+
// WRITING position count — redirect targets, `tee`/`mv`/`cp` destinations, and any path on an
|
|
348
|
+
// in-place editor line (`sed -i`, `perl -i`) — so reading a frozen record into a pipe stays
|
|
349
|
+
// legal. A token that isn't an existing repo path falls through frozenStatus/protected as a
|
|
350
|
+
// no-op, which keeps sed expressions and flags harmless without shell-parsing them.
|
|
351
|
+
function bashWriteTargets(command) {
|
|
352
|
+
const targets = [];
|
|
353
|
+
let m;
|
|
354
|
+
const redirect = />{1,2}\s*([^\s;|&<>]+)/g;
|
|
355
|
+
while ((m = redirect.exec(command))) targets.push(m[1]);
|
|
356
|
+
const tee = /\btee\s+(?:-\S+\s+)*([^\s;|&<>]+)/g;
|
|
357
|
+
while ((m = tee.exec(command))) targets.push(m[1]);
|
|
358
|
+
const moveCopy = /\b(?:mv|cp)\s+(?:-\S+\s+)*\S+\s+([^\s;|&<>]+)/g;
|
|
359
|
+
while ((m = moveCopy.exec(command))) targets.push(m[1]);
|
|
360
|
+
if (/\b(?:sed|perl)\s+(?:-\S*\s+)*-\S*i/.test(command) || /\bsed\s+--in-place\b/.test(command)) {
|
|
361
|
+
for (const t of command.split(/[\s"']+/)) {
|
|
362
|
+
if (t && !t.startsWith("-") && /[/.]/.test(t)) targets.push(t);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return targets;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function bashWriteScreen(projectDir, command, sessionId) {
|
|
369
|
+
for (const target of bashWriteTargets(command)) {
|
|
370
|
+
const rel = relIfInside(projectDir, target);
|
|
371
|
+
if (!rel) continue;
|
|
372
|
+
const frozen = frozenStatus(projectDir, rel);
|
|
373
|
+
if (frozen.frozen) {
|
|
374
|
+
return deny(
|
|
375
|
+
projectDir,
|
|
376
|
+
sessionId,
|
|
377
|
+
"frozen-record",
|
|
378
|
+
rel,
|
|
379
|
+
`BLOCKED — frozen record. \`${rel}\` is committed history and is never edited (${frozen.reason}) — a shell write is still a write. A correction is a new dated entry that links back.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (protectedPaths(projectDir).includes(rel)) {
|
|
383
|
+
return deny(
|
|
384
|
+
projectDir,
|
|
385
|
+
sessionId,
|
|
386
|
+
"protected-file",
|
|
387
|
+
rel,
|
|
388
|
+
`BLOCKED — protected file. \`${rel}\` is on the protected list — a shell write is still a write.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
343
395
|
// ---------------------------------------------------------------------------
|
|
344
396
|
// Stop
|
|
345
397
|
// ---------------------------------------------------------------------------
|
|
@@ -463,13 +515,21 @@ function stopGate(projectDir, payload) {
|
|
|
463
515
|
const openDenials = unresolvedDenials(projectDir, payload.session_id);
|
|
464
516
|
if (openDenials.length > 0) {
|
|
465
517
|
const rel = path.relative(projectDir, sessionLogPath(projectDir, payload.session_id));
|
|
518
|
+
// A resolution that doesn't parse reads to the session as "my resolution didn't count"
|
|
519
|
+
// with no explanation — name the offending lines and the grammar instead of staying mute.
|
|
520
|
+
const bad = malformedEventLines(projectDir, payload.session_id);
|
|
521
|
+
const badNote =
|
|
522
|
+
bad.length > 0
|
|
523
|
+
? ` NOTE: ${bad.length} line(s) look like events but don't parse — the grammar is \`- HH:MM:SS denied|resolved · gate · target[ · detail]\` (a resolved line missing its timestamp is invisible to the wall): ${bad.join(" | ")}.`
|
|
524
|
+
: "";
|
|
466
525
|
return {
|
|
467
526
|
decision: "block",
|
|
468
527
|
reason:
|
|
469
528
|
`${openDenials.length} gate denial(s) this session have no resolved line in ${rel}: ${openDenials.join("; ")}. ` +
|
|
470
529
|
"For each, either retry the operation properly (a success appends `retried-ok` automatically), convert it to a waiver and append " +
|
|
471
530
|
"`resolved · {gate} · {target} · waived → {ref}`, or record the failure honestly in the changelog entry and append " +
|
|
472
|
-
"`resolved · {gate} · {target} · disclosed → {entry file}`. Never delete log lines — the log is a record, not a queue."
|
|
531
|
+
"`resolved · {gate} · {target} · disclosed → {entry file}`. Never delete log lines — the log is a record, not a queue." +
|
|
532
|
+
badNote,
|
|
473
533
|
};
|
|
474
534
|
}
|
|
475
535
|
|
|
@@ -73,3 +73,23 @@ export function unresolvedDenials(projectDir, sessionId) {
|
|
|
73
73
|
}
|
|
74
74
|
return [...open.keys()];
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
// Lines that LOOK like events but don't parse — a resolution missing its timestamp token, a
|
|
78
|
+
// hand-typed variant of the grammar. The pair-matcher above skips them silently, which reads
|
|
79
|
+
// to the session as "my resolution didn't count" with no explanation (a real session burned
|
|
80
|
+
// its tail debugging exactly this). The walls surface these loudly instead.
|
|
81
|
+
export function malformedEventLines(projectDir, sessionId) {
|
|
82
|
+
let text = "";
|
|
83
|
+
try {
|
|
84
|
+
text = fs.readFileSync(sessionLogPath(projectDir, sessionId), "utf8");
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const bad = [];
|
|
89
|
+
for (const line of text.split("\n")) {
|
|
90
|
+
if (!/^- .*\b(denied|resolved)\b\s*·/.test(line)) continue;
|
|
91
|
+
if (/^- \S+ (denied|resolved) · ([^·]+) · ([^·\n]+?)(?: · |$)/.test(line)) continue;
|
|
92
|
+
bad.push(line.trim());
|
|
93
|
+
}
|
|
94
|
+
return bad;
|
|
95
|
+
}
|
|
@@ -55,7 +55,7 @@ Test packages install at `latest` — outside the approved-packages pin set. Fak
|
|
|
55
55
|
- **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
|
|
56
56
|
- **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob.
|
|
57
57
|
- **No test doubles for what you own.** Prefer real instances — in-memory SQLite for drizzle, the actual Express app via `supertest` — over mocks; mock **only** true externals (network via `msw`, time via the runner's fake timers).
|
|
58
|
-
- **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone
|
|
58
|
+
- **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone, in any order, **and from a cold shell** — never against a hand-started dev server (Workers: `@cloudflare/vitest-pool-workers`; Express: `supertest` against the app instance).
|
|
59
59
|
|
|
60
60
|
## Discipline
|
|
61
61
|
|
|
@@ -15,7 +15,7 @@ The **`Gates:`** line is machine-read: the compliance hook greps exactly the for
|
|
|
15
15
|
**Gates:** Boundary: pending · Test-plan: pending · Docs: 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
|
|
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) -->
|
|
19
19
|
|
|
20
20
|
## Background
|
|
21
21
|
|