@nurix/etna 0.19.91 → 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.
- package/bin/etna.js +0 -11
- package/data/catalog/rules/frontend.md +1 -1
- package/data/catalog/rules/general.md +3 -3
- package/data/catalog/rules/nomenclature.md +0 -4
- package/data/catalog/rules/pnpm.md +1 -1
- package/data/catalog/rules/testing.md +1 -0
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +14 -14
- package/data/harnesses/code/agents/bookkeeper.md +4 -1
- package/data/harnesses/code/agents/maintenance.md +3 -14
- package/data/harnesses/code/agents/performance-review.md +1 -1
- package/data/harnesses/code/codex/agents/performance-review.toml +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/awareness.mjs +5 -169
- package/data/harnesses/code/hooks/compliance.mjs +38 -18
- package/data/harnesses/code/hooks/lib/ledger.mjs +36 -13
- package/data/harnesses/code/hooks/lib/state.mjs +85 -1
- package/data/harnesses/code/hooks/lib/sweep.mjs +172 -0
- package/data/harnesses/code/rules/frontend.md +1 -1
- package/data/harnesses/code/rules/general.md +3 -3
- package/data/harnesses/code/rules/nomenclature.md +0 -4
- package/data/harnesses/code/rules/pnpm.md +1 -1
- package/data/harnesses/code/rules/testing.md +1 -0
- package/data/harnesses/code/skills/boundary/SKILL.md +1 -1
- package/data/harnesses/code/skills/documentation/SKILL.md +1 -1
- package/data/harnesses/code/skills/documentation/templates/behavioral-checklist.md +1 -1
- package/data/harnesses/code/skills/documentation/templates/bookkeeping.md +14 -8
- package/data/harnesses/code/skills/documentation/templates/plan.md +9 -16
- package/data/harnesses/code/skills/testing/SKILL.md +1 -2
- package/data/harnesses/delivery/agents/maintenance.md +3 -14
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/hooks/generate-ownership-index.mjs +3 -1
- package/data/harnesses/delivery/rules/nomenclature.md +0 -4
- package/data/harnesses/delivery/skills/documentation/SKILL.md +1 -1
- package/data/harnesses/delivery/skills/documentation/templates/behavioral-checklist.md +1 -1
- package/data/harnesses/delivery/skills/documentation/templates/bookkeeping.md +14 -8
- package/data/harnesses/delivery/skills/documentation/templates/plan.md +9 -16
- package/data/harnesses/etna/agents/maintenance.md +3 -14
- package/data/harnesses/etna/agents/performance-review.md +1 -1
- package/data/harnesses/etna/codex/agents/performance-review.toml +1 -1
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/hooks/generate-ownership-index.mjs +3 -1
- package/data/harnesses/etna/rules/general.md +3 -3
- package/data/harnesses/etna/rules/nomenclature.md +0 -4
- package/data/harnesses/etna/rules/testing.md +1 -0
- package/data/harnesses/etna/skills/boundary/SKILL.md +1 -1
- package/data/harnesses/etna/skills/documentation/SKILL.md +1 -1
- package/data/harnesses/etna/skills/documentation/templates/behavioral-checklist.md +1 -1
- package/data/harnesses/etna/skills/documentation/templates/bookkeeping.md +14 -8
- package/data/harnesses/etna/skills/documentation/templates/plan.md +9 -16
- package/lib/codex/agents-md.mjs +1 -35
- package/lib/codex/hooks.mjs +0 -29
- package/lib/install/materialize.mjs +4 -34
- package/lib/install/plan.mjs +54 -10
- package/package.json +3 -3
- package/data/harnesses/code/hooks/check-bookkeeping-clean.mjs +0 -24
- package/data/harnesses/code/hooks/check-changelog-presence.mjs +0 -36
- package/data/harnesses/code/hooks/check-completion-integrity.mjs +0 -72
- package/data/harnesses/code/hooks/check-ownership.mjs +0 -55
- package/lib/codex/index.mjs +0 -63
- package/lib/codex/plugin-manifest.mjs +0 -42
- package/lib/codex/subagents.mjs +0 -26
|
@@ -11,7 +11,8 @@ import { readStdin, resolveProjectDir } from "./lib/ownership.mjs";
|
|
|
11
11
|
import { readSafe, relIfInside, git, commandRoot, backtickTokens, SOURCE_EXT } from "./lib/repo.mjs";
|
|
12
12
|
import { openGatedPlans, inPlanScope } from "./lib/plans.mjs";
|
|
13
13
|
import { stagedExempt } from "./lib/exempt.mjs";
|
|
14
|
-
import { readQueue, countRows, askedThisSession, recordAsked } from "./lib/state.mjs";
|
|
14
|
+
import { readQueue, countRows, rowsBlockingCommit, askedThisSession, recordAsked } from "./lib/state.mjs";
|
|
15
|
+
import { liveRows } from "./lib/sweep.mjs";
|
|
15
16
|
import { frozenStatus } from "./lib/frozen.mjs";
|
|
16
17
|
import { undischargedRows } from "./lib/ledger.mjs";
|
|
17
18
|
import { appendEvent, unresolvedDenials, sessionLogPath, malformedEventLines } from "./lib/session-log.mjs";
|
|
@@ -150,7 +151,15 @@ function commitGate(projectDir, sessionId) {
|
|
|
150
151
|
.split("\n")
|
|
151
152
|
.map((s) => s.trim())
|
|
152
153
|
.filter(Boolean);
|
|
153
|
-
|
|
154
|
+
// A rename-only staged set is on the changelog rule's exemption list; the gate can now honor it
|
|
155
|
+
// the way CI's caller already does. At least one line, and every line a rename — one edit joins
|
|
156
|
+
// the set and the wall returns.
|
|
157
|
+
const nameStatus = (git(projectDir, ["diff", "--cached", "--name-status", "-M"]) || "")
|
|
158
|
+
.split("\n")
|
|
159
|
+
.map((s) => s.trim())
|
|
160
|
+
.filter(Boolean);
|
|
161
|
+
const allRenames = nameStatus.length > 0 && nameStatus.every((line) => line.startsWith("R"));
|
|
162
|
+
if (staged.length > 0 && !stagedExempt(staged) && !allRenames) {
|
|
154
163
|
if (!staged.some((f) => f.startsWith("docs/history/changelog/"))) {
|
|
155
164
|
return deny(
|
|
156
165
|
projectDir,
|
|
@@ -161,24 +170,31 @@ function commitGate(projectDir, sessionId) {
|
|
|
161
170
|
);
|
|
162
171
|
}
|
|
163
172
|
}
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
// The bookkeeping wall is scoped to the staged set — the grammar and its fail-closed cases are
|
|
174
|
+
// `lib/state.mjs` § What a row implicates. Rows about files this commit does not touch are
|
|
175
|
+
// somebody else's, and holding a commit behind rows its session may not drain is a wait with no
|
|
176
|
+
// action that clears it. The read goes through `liveRows` first: a gate tests conditions at gate
|
|
177
|
+
// time, never a cached record of them — a row whose condition died since the last sweep must not
|
|
178
|
+
// wall anything. Pure by design; the detector's next sweep deletes what this filtered out.
|
|
179
|
+
const { blocking, unrelated } = rowsBlockingCommit(liveRows(projectDir, readQueue(projectDir)), staged);
|
|
180
|
+
if (blocking.length > 0) {
|
|
181
|
+
const shown = blocking.slice(0, 8);
|
|
182
|
+
const lines = shown.map(({ row, hits }) =>
|
|
183
|
+
hits.length > 0
|
|
184
|
+
? ` - ${row.kind}: ${row.body}\n ↳ implicates staged ${hits.join(", ")}`
|
|
185
|
+
: ` - ${row.kind}: ${row.body}\n ↳ names no staged path — a row like this blocks every commit`
|
|
186
|
+
);
|
|
187
|
+
if (blocking.length > shown.length) lines.push(` - …and ${blocking.length - shown.length} more`);
|
|
166
188
|
return deny(
|
|
167
189
|
projectDir,
|
|
168
190
|
sessionId,
|
|
169
191
|
"bookkeeping",
|
|
170
192
|
"git commit",
|
|
171
|
-
`BLOCKED — bookkeeping. ${
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// stop is, and an undisclosed failure must not ride out inside one.
|
|
177
|
-
const open = unresolvedDenials(projectDir, sessionId).filter((pair) => !pair.startsWith("changelog ·") && !pair.startsWith("bookkeeping ·"));
|
|
178
|
-
if (open.length > 0) {
|
|
179
|
-
return decision(
|
|
180
|
-
"deny",
|
|
181
|
-
`BLOCKED — session gate log. ${open.length} denial(s) this session have no resolved line (${open.join("; ")}). Append \`resolved\` to the session log — retried-ok happens automatically on a successful retry; otherwise \`waived → {ref}\` or \`disclosed → {changelog entry}\`. Never delete log lines.`
|
|
193
|
+
`BLOCKED — bookkeeping. ${blocking.length} unresolved row(s) in docs/operations/bookkeeping.md implicate this commit (each re-verified against the repo just now — these are live):\n${lines.join("\n")}\n` +
|
|
194
|
+
"Dispatch the bookkeeper, resolve them, or convert to `waived:` with a reason and expiry; a successful retry re-tests everything again." +
|
|
195
|
+
(unrelated > 0
|
|
196
|
+
? ` (${unrelated} further unresolved row(s) implicate nothing staged here — another session's work, and not yours to drain.)`
|
|
197
|
+
: "")
|
|
182
198
|
);
|
|
183
199
|
}
|
|
184
200
|
|
|
@@ -423,9 +439,13 @@ function stopGate(projectDir, payload) {
|
|
|
423
439
|
if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
|
|
424
440
|
|
|
425
441
|
// The Stop wall is scoped to this session's own rows (plus sid-less ones) — a concurrent
|
|
426
|
-
// session's open work must not block this one's stop.
|
|
427
|
-
|
|
428
|
-
|
|
442
|
+
// session's open work must not block this one's stop. End-of-session accountability is per
|
|
443
|
+
// session and stays that way; the commit wall scopes by staged path instead, and CI by nothing.
|
|
444
|
+
// Filtered through `liveRows` (sid first — fewer verifier runs): the wall blocks on conditions
|
|
445
|
+
// that hold NOW, not on rows the last sweep happened to leave written down.
|
|
446
|
+
const mine = liveRows(
|
|
447
|
+
projectDir,
|
|
448
|
+
readQueue(projectDir).filter((row) => !/\bsid=/.test(row.body) || row.body.includes(`sid=${payload.session_id}`))
|
|
429
449
|
);
|
|
430
450
|
const counts = countRows(mine);
|
|
431
451
|
if (counts.unresolved > 0 && !bookkeeperDispatched(payload.transcript_path)) {
|
|
@@ -1,31 +1,54 @@
|
|
|
1
|
-
// harness/code/hooks/lib/ledger.mjs — the
|
|
2
|
-
// A row is discharged when its Outcome cell states one of the
|
|
3
|
-
//
|
|
1
|
+
// harness/code/hooks/lib/ledger.mjs — the plan's two ledgers, read the way the walls read them.
|
|
2
|
+
// A row is discharged when its Outcome cell states one of the closed outcomes. The plan file is
|
|
3
|
+
// the checked truth — a discharge held only in conversation memory does not exist, which is what
|
|
4
4
|
// forces the gate to re-read the ledger from disk. Tables without an Outcome column predate the
|
|
5
5
|
// contract and are out of scope: the same legacy tolerance the Gates: line gets, so an upgrade
|
|
6
6
|
// cannot brick a plan already Complete.
|
|
7
7
|
|
|
8
8
|
const OUTCOME = /^(satisfied|owes nothing|blocked)\b/i;
|
|
9
|
+
const DOC_OUTCOME = /^(updated|owes nothing|blocked)\b/i;
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// The table rows of one `## <heading>` section, split into trimmed cells, plus the Outcome column
|
|
12
|
+
// index (null when the table predates the column — legacy, out of contract).
|
|
13
|
+
function sectionTable(planText, heading) {
|
|
14
|
+
const start = (planText || "").search(new RegExp(`^##\\s+${heading}\\b`, "m"));
|
|
15
|
+
if (start === -1) return null;
|
|
14
16
|
const body = [];
|
|
15
17
|
for (const line of planText.slice(start).split("\n").slice(1)) {
|
|
16
|
-
if (/^##\s/.test(line)) break;
|
|
18
|
+
if (/^##\s/.test(line)) break;
|
|
17
19
|
body.push(line);
|
|
18
20
|
}
|
|
19
21
|
const rows = body.filter((line) => /^\s*\|/.test(line));
|
|
20
|
-
if (rows.length < 3) return
|
|
22
|
+
if (rows.length < 3) return null; // header + divider + at least one data row
|
|
21
23
|
const header = rows[0].split("|").map((cell) => cell.trim().toLowerCase());
|
|
22
24
|
const col = header.indexOf("outcome");
|
|
23
|
-
|
|
25
|
+
return { rows: rows.slice(2).map((line) => line.split("|").map((cell) => cell.trim())), col: col === -1 ? null : col };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Row numbers (the `#` column) of every ledger row whose Outcome cell is empty or off-vocabulary.
|
|
29
|
+
export function undischargedRows(planText) {
|
|
30
|
+
const table = sectionTable(planText, "Test Obligations");
|
|
31
|
+
if (!table || table.col === null) return [];
|
|
24
32
|
const out = [];
|
|
25
|
-
for (const
|
|
26
|
-
const cells = line.split("|").map((cell) => cell.trim());
|
|
33
|
+
for (const cells of table.rows) {
|
|
27
34
|
if (!/^\d+$/.test(cells[1] || "")) continue;
|
|
28
|
-
if (!OUTCOME.test(cells[col] || "")) out.push(cells[1]);
|
|
35
|
+
if (!OUTCOME.test(cells[table.col] || "")) out.push(cells[1]);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Doc names of every Doc Obligations row whose Outcome cell is empty or off-vocabulary. Cells may
|
|
41
|
+
// be bolded (**updated**); a table without the column is legacy and passes — same tolerance as the
|
|
42
|
+
// test ledger, so the column's arrival cannot brick a plan already Complete.
|
|
43
|
+
export function undischargedDocRows(planText) {
|
|
44
|
+
const table = sectionTable(planText, "Doc Obligations");
|
|
45
|
+
if (!table || table.col === null) return [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const cells of table.rows) {
|
|
48
|
+
const doc = (cells[1] || "").replace(/`/g, "");
|
|
49
|
+
if (!doc || /^-+$/.test(doc)) continue;
|
|
50
|
+
const outcome = (cells[table.col] || "").replace(/\*/g, "").trim();
|
|
51
|
+
if (!DOC_OUTCOME.test(outcome)) out.push(doc);
|
|
29
52
|
}
|
|
30
53
|
return out;
|
|
31
54
|
}
|
|
@@ -9,15 +9,20 @@
|
|
|
9
9
|
// only while its expires= date is in the future; past it, the deferral is over and the wall returns.
|
|
10
10
|
// Rows are also re-verified against the repo before the walls read them (awareness.mjs's sweep),
|
|
11
11
|
// so a row whose condition died is deleted rather than waiting for its date.
|
|
12
|
+
//
|
|
13
|
+
// The three walls read the queue at three widths: the commit wall takes only the rows implicating
|
|
14
|
+
// the commit's staged set (§ What a row implicates), the Stop wall takes the ending session's own
|
|
15
|
+
// rows, and CI takes every row. Accountability is per session; a commit is per set of files.
|
|
12
16
|
|
|
13
17
|
import fs from "node:fs";
|
|
14
18
|
import path from "node:path";
|
|
19
|
+
import { globToRegExp } from "./ownership.mjs";
|
|
15
20
|
|
|
16
21
|
export const QUEUE_PATH = ["docs", "operations", "bookkeeping.md"];
|
|
17
22
|
|
|
18
23
|
export const QUEUE_HEADER = `# Bookkeeping
|
|
19
24
|
|
|
20
|
-
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
|
|
25
|
+
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\`.\n\n**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.
|
|
21
26
|
`;
|
|
22
27
|
|
|
23
28
|
export function queueFile(projectDir) {
|
|
@@ -143,6 +148,85 @@ export function countRows(rows, today = new Date().toISOString().slice(0, 10)) {
|
|
|
143
148
|
return c;
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
// The unresolved rows themselves — the same set countRows() tallies, as rows rather than a number.
|
|
152
|
+
export function unresolvedRows(rows, today = new Date().toISOString().slice(0, 10)) {
|
|
153
|
+
return rows.filter((row) => {
|
|
154
|
+
if (row.kind !== "waived") return true;
|
|
155
|
+
const expires = waiverExpiry(row);
|
|
156
|
+
return !expires || expires < today;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- What a row implicates -------------------------------------------------------------------
|
|
161
|
+
//
|
|
162
|
+
// THE COMMIT WALL IS SCOPED BY PATH. Every mechanical finding names the file it is about, so a
|
|
163
|
+
// commit can be walled by the rows implicating ITS staged set rather than by the whole queue.
|
|
164
|
+
// A global wall is unattributable the moment agents share a checkout: sibling agents also share a
|
|
165
|
+
// session id, so an agent ready to commit was held behind rows a concurrent sibling raised — rows
|
|
166
|
+
// the ownership etiquette (drain only what your own edits raised) forbids it from draining, which
|
|
167
|
+
// makes the wall a wait with no action that clears it. Measured on the fleet: a 15-minute stall
|
|
168
|
+
// behind a sibling's ten rows, and two agents in 5–6-minute mutual politeness loops.
|
|
169
|
+
//
|
|
170
|
+
// The shapes below mirror the ones awareness.mjs's ROW_VERIFIERS re-test — one grammar, two
|
|
171
|
+
// readers. Each rule captures every path the row names: the file the finding is about AND the doc
|
|
172
|
+
// that owns or records it, since staging either one is a commit answering for that row.
|
|
173
|
+
//
|
|
174
|
+
// A ROW THIS CANNOT PARSE IMPLICATES EVERYTHING. Scoping may narrow a wall on evidence, never on
|
|
175
|
+
// ignorance — so an unrecognized row, a judgment row, and a commit with nothing staged to compare
|
|
176
|
+
// against all keep the old repo-wide block.
|
|
177
|
+
const ROW_PATH_RULES = [
|
|
178
|
+
/·\s*doc-implicated\s*·\s*(\S+)\s+changed;\s+owning doc\s+(\S+?)\s+may need/,
|
|
179
|
+
/·\s*doc-lint\s*·\s*(\S+)\s+has\s/,
|
|
180
|
+
/·\s*header-missing\s*·\s*(\S+)\s+lacks\s/,
|
|
181
|
+
/·\s*index-row-missing\s*·\s*(\S+)\s+is not linked from\s+(\S+)/,
|
|
182
|
+
/·\s*package-rejected\s*·\s*(\S+)\s+declares\s/,
|
|
183
|
+
/·\s*frozen-record-edited\s*·\s*(\S+)\s+is a frozen record/,
|
|
184
|
+
/·\s*dead-glob\s*·\s*owns: glob\s+`([^`]+)`\s+in\s+(\S+)\s+matches/,
|
|
185
|
+
/→\s*lands-in:\s*(\S+\/\S+)/, // a note: row names the doc its context lands in
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
// The paths a row implicates, or null when its grammar yields none.
|
|
189
|
+
export function rowPaths(row) {
|
|
190
|
+
for (const re of ROW_PATH_RULES) {
|
|
191
|
+
const m = re.exec(row.body);
|
|
192
|
+
if (!m) continue;
|
|
193
|
+
const paths = m.slice(1).filter(Boolean);
|
|
194
|
+
if (paths.length > 0) return paths;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// True when one of a row's tokens names a staged file. A token carrying `*` — the glob a dead-glob
|
|
200
|
+
// row quotes — compiles through the ownership index's engine; a plain token matches a staged file
|
|
201
|
+
// exactly, or as the directory it sits in.
|
|
202
|
+
function tokenHitsStaged(token, staged) {
|
|
203
|
+
if (token.includes("*")) {
|
|
204
|
+
const re = globToRegExp(token);
|
|
205
|
+
return staged.some((f) => re.test(f));
|
|
206
|
+
}
|
|
207
|
+
return staged.some((f) => f === token || f.startsWith(token + "/"));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The unresolved rows a commit staging `staged` must answer for, each with the staged paths it
|
|
211
|
+
// implicates. `needs-session:` rows and expired waivers are judgments rather than mechanical
|
|
212
|
+
// findings — they carry no reliable path and stay global by kind, which is the semantics they had
|
|
213
|
+
// before scoping. Returns { blocking, unrelated }: `unrelated` counts the rows this commit is
|
|
214
|
+
// walked past, which the block message reports so scoping is visible rather than silent.
|
|
215
|
+
export function rowsBlockingCommit(rows, staged, today = new Date().toISOString().slice(0, 10)) {
|
|
216
|
+
const blocking = [];
|
|
217
|
+
let unrelated = 0;
|
|
218
|
+
for (const row of unresolvedRows(rows, today)) {
|
|
219
|
+
const paths = row.kind === "auto" || row.kind === "note" ? rowPaths(row) : null;
|
|
220
|
+
const hits = paths && staged.length > 0 ? paths.filter((p) => tokenHitsStaged(p, staged)) : null;
|
|
221
|
+
if (hits && hits.length === 0) {
|
|
222
|
+
unrelated += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
blocking.push({ row, hits: hits || [] });
|
|
226
|
+
}
|
|
227
|
+
return { blocking, unrelated };
|
|
228
|
+
}
|
|
229
|
+
|
|
146
230
|
// Format one auto: row per the grammar; the sid tail is the only provenance kept —
|
|
147
231
|
// a transcript path or timestamp is machine noise in a PR-reviewed queue.
|
|
148
232
|
export function formatAutoRow({ date, kind, description, action, sessionId }) {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// harness/code/hooks/lib/sweep.mjs — row re-verification, shared by the detector and the walls.
|
|
2
|
+
//
|
|
3
|
+
// THE QUEUE IS DERIVED, NOT ACCUMULATED. Every finding here is recomputable from the repo, so a row
|
|
4
|
+
// is a cache of a condition rather than a record of an event — and a cache nothing invalidates is
|
|
5
|
+
// an archive. Two consumers, two shapes: `sweepResolvedRows` (the detector's writer — deletes dead
|
|
6
|
+
// rows from the queue file) and `liveRows` (the walls' pure read — same verifiers, NO file writes,
|
|
7
|
+
// because a PreToolUse gate or a CI check must never mutate the queue: on `git commit -a` a sweep's
|
|
8
|
+
// deletions would ride into the commit unauthored).
|
|
9
|
+
//
|
|
10
|
+
// THIS RUNS OVER `waived:` ROWS TOO, WHICH IS THE POINT. A waiver defers a finding; it does not
|
|
11
|
+
// make the finding immortal. Without this, waiving was strictly WORSE than leaving a row alone —
|
|
12
|
+
// the one existing resolver filtered `kind === "auto"`, so waiving a self-clearing row opted it out
|
|
13
|
+
// of its own return path forever. The condition, not the row's kind, decides.
|
|
14
|
+
//
|
|
15
|
+
// A kind with no verifier here is not swept — it ages out through `expires=` instead. Add a
|
|
16
|
+
// verifier whenever a finding becomes mechanically checkable; silence is the only failure mode
|
|
17
|
+
// this file has, so a verifier that is unsure must return false and leave the row standing.
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { globToRegExp, listRepoFiles } from "./ownership.mjs";
|
|
23
|
+
import { readSafe } from "./repo.mjs";
|
|
24
|
+
import { resolveRows } from "./state.mjs";
|
|
25
|
+
import { rejectedPackages } from "./approved.mjs";
|
|
26
|
+
|
|
27
|
+
// Spawn the installed doc-lint over one file; returns finding lines (empty when absent/clean).
|
|
28
|
+
export function docLint(projectDir, rel) {
|
|
29
|
+
const script = path.join(projectDir, ".claude", "skills", "documentation", "scripts", "doc-lint.mjs");
|
|
30
|
+
if (!fs.existsSync(script)) return [];
|
|
31
|
+
const res = spawnSync("node", [script, rel], { cwd: projectDir, encoding: "utf8", timeout: 5000 });
|
|
32
|
+
const out = `${res.stdout || ""}`;
|
|
33
|
+
return out.split("\n").filter((line) => /:\d+ \[(block|warn)\]/.test(line));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// True when a source file's first lines carry its repo-relative path (the header-comment rule).
|
|
37
|
+
export function hasHeaderComment(projectDir, rel) {
|
|
38
|
+
const head = readSafe(path.join(projectDir, rel)).split("\n").slice(0, 5).join("\n");
|
|
39
|
+
return head.includes(rel) || head.includes(path.basename(rel));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Verifier keys the detector no longer emits — swept when machine-raised, never taught to authors.
|
|
43
|
+
export const RETIRED_KINDS = ["doc-placement", "boundary-scan-suggested"];
|
|
44
|
+
|
|
45
|
+
export const ROW_VERIFIERS = {
|
|
46
|
+
// "<rel> lacks its file-path header comment" — dead once the header is there (or the file is gone).
|
|
47
|
+
"header-missing": (projectDir, body) => {
|
|
48
|
+
const m = /·\s+header-missing\s+·\s+(\S+)\s+lacks/.exec(body);
|
|
49
|
+
if (!m) return false;
|
|
50
|
+
const rel = m[1];
|
|
51
|
+
if (!fs.existsSync(path.join(projectDir, rel))) return true;
|
|
52
|
+
return hasHeaderComment(projectDir, rel);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// "owns: glob `<glob>` in <doc> matches no files" — dead once the glob matches something again.
|
|
56
|
+
"dead-glob": (projectDir, body, ctx) => {
|
|
57
|
+
const m = /`([^`]+)`/.exec(body);
|
|
58
|
+
if (!m) return false;
|
|
59
|
+
const re = globToRegExp(m[1]);
|
|
60
|
+
return ctx.repoFiles.some((f) => re.test(f));
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// "<rel> is a frozen record and carries uncommitted modifications" — dead once it is committed.
|
|
64
|
+
// Immutability begins at commit, so the finding exists only while the edit is still in flight.
|
|
65
|
+
"frozen-record-edited": (projectDir, body) => {
|
|
66
|
+
const m = /·\s+frozen-record-edited\s+·\s+(\S+)\s+is a frozen record/.exec(body);
|
|
67
|
+
if (!m) return false;
|
|
68
|
+
const rel = m[1];
|
|
69
|
+
if (!fs.existsSync(path.join(projectDir, rel))) return true;
|
|
70
|
+
const res = spawnSync("git", ["diff", "--quiet", "HEAD", "--", rel], { cwd: projectDir, timeout: 5000 });
|
|
71
|
+
// status 0 = no uncommitted modifications. A git failure leaves the row standing.
|
|
72
|
+
return res.status === 0;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// "<src> changed; owning doc <doc> may need an update" — dead once the doc is newer than the
|
|
76
|
+
// source that implicated it, which is the same freshness test the drift report already applies.
|
|
77
|
+
"doc-implicated": (projectDir, body) => {
|
|
78
|
+
const m = /·\s+doc-implicated\s+·\s+(\S+)\s+changed;\s+owning doc\s+(\S+?)\s+may need/.exec(body);
|
|
79
|
+
if (!m) return false;
|
|
80
|
+
const [, src, doc] = m;
|
|
81
|
+
const mtime = (rel) => {
|
|
82
|
+
try {
|
|
83
|
+
return fs.statSync(path.join(projectDir, rel)).mtimeMs;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const docTime = mtime(doc);
|
|
89
|
+
const srcTime = mtime(src);
|
|
90
|
+
if (docTime === null) return false; // the doc is gone — a real finding, not a resolved one
|
|
91
|
+
if (srcTime === null) return true; // the source is gone; nothing implicates the doc any more
|
|
92
|
+
return docTime >= srcTime;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// "<rel> has N doc-lint finding(s)" — dead once a re-run comes back clean (or the doc is gone).
|
|
96
|
+
"doc-lint": (projectDir, body) => {
|
|
97
|
+
const m = /·\s+doc-lint\s+·\s+(\S+)\s+has/.exec(body);
|
|
98
|
+
if (!m) return false;
|
|
99
|
+
if (!fs.existsSync(path.join(projectDir, m[1]))) return true;
|
|
100
|
+
return docLint(projectDir, m[1]).length === 0;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// "<rel> is not linked from docs/README.md" — dead once the index names it (or it is gone).
|
|
104
|
+
"index-row-missing": (projectDir, body) => {
|
|
105
|
+
const m = /·\s+index-row-missing\s+·\s+(\S+)\s+is not linked/.exec(body);
|
|
106
|
+
if (!m) return false;
|
|
107
|
+
if (!fs.existsSync(path.join(projectDir, m[1]))) return true;
|
|
108
|
+
return readSafe(path.join(projectDir, "docs", "README.md")).includes(path.basename(m[1]));
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// "<rel> declares `<name>` (package-rejected)" — dead once the manifest drops the package
|
|
112
|
+
// or the table stops rejecting it.
|
|
113
|
+
"package-rejected": (projectDir, body) => {
|
|
114
|
+
const m = /·\s+package-rejected\s+·\s+(\S+)\s+declares\s+`([^`]+)`/.exec(body);
|
|
115
|
+
if (!m) return false;
|
|
116
|
+
const [, rel, name] = m;
|
|
117
|
+
let manifest;
|
|
118
|
+
try {
|
|
119
|
+
manifest = JSON.parse(readSafe(path.join(projectDir, rel)));
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return !fs.existsSync(path.join(projectDir, rel)); // gone ⇒ resolved; unparseable ⇒ standing
|
|
122
|
+
}
|
|
123
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
124
|
+
if (!(name in declared)) return true;
|
|
125
|
+
return !rejectedPackages(projectDir).has(name);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Retired detector kinds — the harness no longer raises these, so a standing row caches a
|
|
129
|
+
// condition nothing can re-derive. Swept unconditionally — but only machine-raised rows: a
|
|
130
|
+
// note: or waived: row whose free text merely mentions the phrase is a human's, not ours.
|
|
131
|
+
"doc-placement": (projectDir, body, ctx, row) => row.kind === "auto",
|
|
132
|
+
"boundary-scan-suggested": (projectDir, body, ctx, row) => row.kind === "auto",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// The repo-file list is only needed by the dead-glob verifier — walk the tree lazily, once.
|
|
136
|
+
function verifierContext(projectDir, repoFiles) {
|
|
137
|
+
let cached = repoFiles || null;
|
|
138
|
+
return {
|
|
139
|
+
get repoFiles() {
|
|
140
|
+
if (!cached) cached = listRepoFiles(projectDir);
|
|
141
|
+
return cached;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// True when the row's condition no longer holds. Unsure (no verifier, verifier threw) ⇒ false.
|
|
147
|
+
function conditionDead(projectDir, row, ctx) {
|
|
148
|
+
if (row.kind === "needs-session") return false; // a human judgment, never swept mechanically
|
|
149
|
+
for (const [kind, verify] of Object.entries(ROW_VERIFIERS)) {
|
|
150
|
+
if (!row.body.includes(kind)) continue;
|
|
151
|
+
try {
|
|
152
|
+
return verify(projectDir, row.body, ctx, row);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return false; // never drop a row because a verifier threw
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The walls' read: rows whose condition still holds, re-tested against the repo as it is NOW.
|
|
161
|
+
// Pure — the queue file is untouched; the detector's next sweep deletes what this filtered out.
|
|
162
|
+
export function liveRows(projectDir, rows, repoFiles) {
|
|
163
|
+
const ctx = verifierContext(projectDir, repoFiles);
|
|
164
|
+
return rows.filter((row) => !conditionDead(projectDir, row, ctx));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The detector's writer: delete every row — `auto:` or `waived:` — whose condition no longer
|
|
168
|
+
// holds. Returns the count removed.
|
|
169
|
+
export function sweepResolvedRows(projectDir, repoFiles) {
|
|
170
|
+
const ctx = verifierContext(projectDir, repoFiles);
|
|
171
|
+
return resolveRows(projectDir, (row) => conditionDead(projectDir, row, ctx));
|
|
172
|
+
}
|
|
@@ -29,7 +29,7 @@ Every package compiles under `strict: true` and `noUncheckedIndexedAccess: true`
|
|
|
29
29
|
|
|
30
30
|
- **Never** `any` to silence the checker. Unknown value → type `unknown`, then narrow (type guard / `zod` parse).
|
|
31
31
|
- **Never** weaken strictness: no `// @ts-nocheck`; `// @ts-expect-error` only with a same-line reason.
|
|
32
|
-
-
|
|
32
|
+
- Annotate at boundaries (exports, props, contracts).
|
|
33
33
|
|
|
34
34
|
### Generics
|
|
35
35
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
|
|
2
2
|
|
|
3
|
-
How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here.
|
|
3
|
+
How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -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
|
|
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 | `
|
|
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 `
|
|
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
|
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
# Bookkeeping ledger (docs/operations/bookkeeping.md)
|
|
2
2
|
|
|
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
|
|
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
|
-
|
|
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
|
+
|
|
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.
|
|
6
8
|
|
|
7
9
|
```
|
|
8
10
|
# Bookkeeping
|
|
9
11
|
|
|
10
|
-
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
|
|
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
|
+
|
|
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.
|
|
11
15
|
|
|
12
|
-
- auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id}
|
|
13
|
-
- note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid
|
|
14
|
-
- needs-session: YYYY-MM-DD · {what the bookkeeper could not ground} → main session resolves | refs={
|
|
15
|
-
- waived: YYYY-MM-DD · {
|
|
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
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
|
|
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`.
|