@nurix/etna 0.19.90 → 0.19.91
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/harness.json +2 -2
- package/data/harnesses/code/hooks/awareness.mjs +97 -130
- package/data/harnesses/code/hooks/check-changelog-presence.mjs +4 -25
- package/data/harnesses/code/hooks/check-completion-integrity.mjs +1 -8
- package/data/harnesses/code/hooks/check-ownership.mjs +6 -8
- package/data/harnesses/code/hooks/compliance.mjs +43 -183
- package/data/harnesses/code/hooks/lib/approved.mjs +1 -9
- package/data/harnesses/code/hooks/lib/claims.mjs +1 -4
- package/data/harnesses/code/hooks/lib/exempt.mjs +32 -0
- package/data/harnesses/code/hooks/lib/frozen.mjs +24 -4
- package/data/harnesses/code/hooks/lib/ownership.mjs +24 -28
- package/data/harnesses/code/hooks/lib/plans.mjs +63 -0
- package/data/harnesses/code/hooks/lib/repo.mjs +68 -0
- package/data/harnesses/code/hooks/lib/session-log.mjs +2 -5
- package/data/harnesses/code/hooks/lib/state.mjs +12 -4
- package/data/harnesses/code/skills/documentation/templates/bookkeeping.md +1 -1
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/skills/documentation/templates/bookkeeping.md +1 -1
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/documentation/templates/bookkeeping.md +1 -1
- package/package.json +2 -2
package/data/harness-index.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "code",
|
|
3
3
|
"name": "code",
|
|
4
|
-
"version": "3.10.
|
|
4
|
+
"version": "3.10.14",
|
|
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": "98eb07166e9f"
|
|
19
19
|
}
|
|
@@ -26,11 +26,13 @@ import {
|
|
|
26
26
|
formatNeedsSessionRow,
|
|
27
27
|
appendRows,
|
|
28
28
|
resolveRows,
|
|
29
|
-
|
|
29
|
+
waiverCovers,
|
|
30
30
|
stateDir,
|
|
31
31
|
recordTouched,
|
|
32
32
|
} from "./lib/state.mjs";
|
|
33
|
-
import {
|
|
33
|
+
import { readSafe, relIfInside, today, SOURCE_EXT, commandRoot } from "./lib/repo.mjs";
|
|
34
|
+
import { listPlans } from "./lib/plans.mjs";
|
|
35
|
+
import { frozenStatusAtHead } from "./lib/frozen.mjs";
|
|
34
36
|
import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
|
|
35
37
|
import {
|
|
36
38
|
recordSession,
|
|
@@ -42,24 +44,6 @@ import {
|
|
|
42
44
|
} from "./lib/claims.mjs";
|
|
43
45
|
import { appendEvent } from "./lib/session-log.mjs";
|
|
44
46
|
|
|
45
|
-
const today = () => new Date().toISOString().slice(0, 10);
|
|
46
|
-
|
|
47
|
-
function readSafe(file) {
|
|
48
|
-
try {
|
|
49
|
-
return fs.readFileSync(file, "utf8");
|
|
50
|
-
} catch (e) {
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function readJsonSafe(file) {
|
|
56
|
-
try {
|
|
57
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
58
|
-
} catch (e) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
47
|
// ---------------------------------------------------------------------------
|
|
64
48
|
// SessionStart
|
|
65
49
|
// ---------------------------------------------------------------------------
|
|
@@ -79,7 +63,12 @@ function detectPackageManager(projectDir) {
|
|
|
79
63
|
}
|
|
80
64
|
|
|
81
65
|
function harnessLine(projectDir) {
|
|
82
|
-
|
|
66
|
+
let marker = null;
|
|
67
|
+
try {
|
|
68
|
+
marker = JSON.parse(readSafe(path.join(projectDir, ".claude", "harness.json")));
|
|
69
|
+
} catch (e) {
|
|
70
|
+
/* absent or unparseable — no line */
|
|
71
|
+
}
|
|
83
72
|
if (!marker || !marker.version) return null;
|
|
84
73
|
const structure = readSafe(path.join(projectDir, "docs", "structure.md"));
|
|
85
74
|
const stamped = (/harness-version:\s*([\w.-]+)/.exec(frontmatter(structure) || structure) || [])[1];
|
|
@@ -104,26 +93,14 @@ function memoryLine(projectDir) {
|
|
|
104
93
|
}
|
|
105
94
|
|
|
106
95
|
function openPlanLines(projectDir) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for (const file of entries) {
|
|
116
|
-
const text = readSafe(path.join(dir, file));
|
|
117
|
-
const status = (/^\*\*Status:\*\*\s*(.+)$/m.exec(text) || [])[1]?.trim();
|
|
118
|
-
if (!status || /Complete|Abandoned/.test(status)) continue;
|
|
119
|
-
const gates = (/^\*\*Gates:\*\*\s*(.+)$/m.exec(text) || [])[1]?.trim();
|
|
120
|
-
const ledgerRows = (text.match(/^\|\s*\d+\s*\|/gm) || []).length;
|
|
121
|
-
const parts = [`Open plan: ${file} (${status}`];
|
|
122
|
-
if (gates) parts.push(` · Gates: ${gates}`);
|
|
123
|
-
if (ledgerRows) parts.push(` · ledger: ${ledgerRows} rows`);
|
|
124
|
-
lines.push(parts.join("") + ")");
|
|
125
|
-
}
|
|
126
|
-
return lines;
|
|
96
|
+
return listPlans(projectDir)
|
|
97
|
+
.filter((p) => !/Complete|Abandoned/.test(p.status))
|
|
98
|
+
.map((p) => {
|
|
99
|
+
const parts = [`Open plan: ${p.file} (${p.status}`];
|
|
100
|
+
if (p.gates) parts.push(` · Gates: ${p.gates}`);
|
|
101
|
+
if (p.ledgerRows) parts.push(` · ledger: ${p.ledgerRows} rows`);
|
|
102
|
+
return parts.join("") + ")";
|
|
103
|
+
});
|
|
127
104
|
}
|
|
128
105
|
|
|
129
106
|
// --- the re-verification sweep ---------------------------------------------------------------
|
|
@@ -190,6 +167,45 @@ const ROW_VERIFIERS = {
|
|
|
190
167
|
if (srcTime === null) return true; // the source is gone; nothing implicates the doc any more
|
|
191
168
|
return docTime >= srcTime;
|
|
192
169
|
},
|
|
170
|
+
|
|
171
|
+
// "<rel> has N doc-lint finding(s)" — dead once a re-run comes back clean (or the doc is gone).
|
|
172
|
+
"doc-lint": (projectDir, body) => {
|
|
173
|
+
const m = /·\s+doc-lint\s+·\s+(\S+)\s+has/.exec(body);
|
|
174
|
+
if (!m) return false;
|
|
175
|
+
if (!fs.existsSync(path.join(projectDir, m[1]))) return true;
|
|
176
|
+
return docLint(projectDir, m[1]).length === 0;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// "<rel> is not linked from docs/README.md" — dead once the index names it (or it is gone).
|
|
180
|
+
"index-row-missing": (projectDir, body) => {
|
|
181
|
+
const m = /·\s+index-row-missing\s+·\s+(\S+)\s+is not linked/.exec(body);
|
|
182
|
+
if (!m) return false;
|
|
183
|
+
if (!fs.existsSync(path.join(projectDir, m[1]))) return true;
|
|
184
|
+
return readSafe(path.join(projectDir, "docs", "README.md")).includes(path.basename(m[1]));
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// "<rel> declares `<name>` (package-rejected)" — dead once the manifest drops the package
|
|
188
|
+
// or the table stops rejecting it.
|
|
189
|
+
"package-rejected": (projectDir, body) => {
|
|
190
|
+
const m = /·\s+package-rejected\s+·\s+(\S+)\s+declares\s+`([^`]+)`/.exec(body);
|
|
191
|
+
if (!m) return false;
|
|
192
|
+
const [, rel, name] = m;
|
|
193
|
+
let manifest;
|
|
194
|
+
try {
|
|
195
|
+
manifest = JSON.parse(readSafe(path.join(projectDir, rel)));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return !fs.existsSync(path.join(projectDir, rel)); // gone ⇒ resolved; unparseable ⇒ standing
|
|
198
|
+
}
|
|
199
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
200
|
+
if (!(name in declared)) return true;
|
|
201
|
+
return !rejectedPackages(projectDir).has(name);
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// Retired detector kinds — the harness no longer raises these, so a standing row caches a
|
|
205
|
+
// condition nothing can re-derive. Swept unconditionally — but only machine-raised rows: a
|
|
206
|
+
// note: or waived: row whose free text merely mentions the phrase is a human's, not ours.
|
|
207
|
+
"doc-placement": (projectDir, body, ctx, row) => row.kind === "auto",
|
|
208
|
+
"boundary-scan-suggested": (projectDir, body, ctx, row) => row.kind === "auto",
|
|
193
209
|
};
|
|
194
210
|
|
|
195
211
|
// Delete every row — `auto:` or `waived:` — whose condition no longer holds. Returns the count.
|
|
@@ -200,7 +216,7 @@ export function sweepResolvedRows(projectDir, repoFiles) {
|
|
|
200
216
|
for (const [kind, verify] of Object.entries(ROW_VERIFIERS)) {
|
|
201
217
|
if (!row.body.includes(kind)) continue;
|
|
202
218
|
try {
|
|
203
|
-
return verify(projectDir, row.body, ctx);
|
|
219
|
+
return verify(projectDir, row.body, ctx, row);
|
|
204
220
|
} catch (e) {
|
|
205
221
|
return false; // never delete a row because a verifier threw
|
|
206
222
|
}
|
|
@@ -215,56 +231,40 @@ function driftBanner(projectDir) {
|
|
|
215
231
|
// Re-verify before counting: the walls must act on conditions that are live NOW, not on rows
|
|
216
232
|
// that happen to still be written down.
|
|
217
233
|
const swept = sweepResolvedRows(projectDir, repoFiles);
|
|
218
|
-
const
|
|
234
|
+
const docs = scanFeatureDocs(projectDir);
|
|
235
|
+
const dead = deadGlobs(projectDir, repoFiles, docs);
|
|
219
236
|
const queueRows = readQueue(projectDir);
|
|
220
237
|
const counts = countRows(queueRows);
|
|
221
|
-
const
|
|
222
|
-
const unwaivedDead = dead.filter((d) => !waivers.some((w) => w.body.includes(d.glob)));
|
|
223
|
-
|
|
224
|
-
// A waiver becomes a wall the day it expires, so the queue says so a week ahead — the deferral
|
|
225
|
-
// ending should be a date somebody saw coming, not a commit that suddenly blocks.
|
|
226
|
-
const soon = new Date(Date.now() + 7 * 864e5).toISOString().slice(0, 10);
|
|
227
|
-
const expiringSoon = queueRows
|
|
228
|
-
.filter((r) => r.kind === "waived")
|
|
229
|
-
.map((r) => {
|
|
230
|
-
const d = (/expires=(\d{4}-\d{2}-\d{2})/.exec(r.body) || [])[1];
|
|
231
|
-
if (!d || d < today() || d > soon) return null;
|
|
232
|
-
// Drop the row's own raised-date prefix and its reason tail — the expiry and the finding are
|
|
233
|
-
// what a reader is scanning for here.
|
|
234
|
-
const finding = r.body.replace(/^\d{4}-\d{2}-\d{2}\s+·\s+/, "").split(" · reason:")[0];
|
|
235
|
-
return `expires ${d} · ${finding.slice(0, 120)}`;
|
|
236
|
-
})
|
|
237
|
-
.filter(Boolean)
|
|
238
|
-
.sort();
|
|
238
|
+
const unwaivedDead = dead.filter((d) => !waiverCovers(queueRows, "dead-glob", d.glob, today()));
|
|
239
239
|
|
|
240
240
|
// A queued dead-glob row covered by an unexpired waiver is resolved history — drop it,
|
|
241
241
|
// so waiving a finding class actually empties the rows it supersedes.
|
|
242
242
|
resolveRows(projectDir, (row) => {
|
|
243
243
|
if (row.kind !== "auto" || !row.body.includes("dead-glob")) return false;
|
|
244
244
|
const m = /`([^`]+)`/.exec(row.body);
|
|
245
|
-
return !!m &&
|
|
245
|
+
return !!m && waiverCovers(queueRows, "dead-glob", m[1], today());
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
// Docs older than the code they own (doc mtime < newest owned-file mtime)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
docTime = fs.statSync(path.join(projectDir, entry.doc)).mtimeMs;
|
|
254
|
-
} catch (e) {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
const res = entry.owns.map((g) => globToRegExp(g));
|
|
258
|
-
let newest = 0;
|
|
259
|
-
for (const file of repoFiles) {
|
|
260
|
-
if (!res.some((re) => re.test(file))) continue;
|
|
248
|
+
// Docs older than the code they own (doc mtime < newest owned-file mtime) — one stat per
|
|
249
|
+
// repo file, cached across docs, instead of docs × files stats.
|
|
250
|
+
const mtimes = new Map();
|
|
251
|
+
const mtimeOf = (rel) => {
|
|
252
|
+
if (!mtimes.has(rel)) {
|
|
261
253
|
try {
|
|
262
|
-
|
|
254
|
+
mtimes.set(rel, fs.statSync(path.join(projectDir, rel)).mtimeMs);
|
|
263
255
|
} catch (e) {
|
|
264
|
-
|
|
256
|
+
mtimes.set(rel, null);
|
|
265
257
|
}
|
|
266
258
|
}
|
|
267
|
-
|
|
259
|
+
return mtimes.get(rel);
|
|
260
|
+
};
|
|
261
|
+
const staleDocs = [];
|
|
262
|
+
for (const entry of docs) {
|
|
263
|
+
const docTime = mtimeOf(entry.doc);
|
|
264
|
+
if (docTime === null) continue;
|
|
265
|
+
const res = entry.owns.map((g) => globToRegExp(g));
|
|
266
|
+
const stale = repoFiles.some((file) => res.some((re) => re.test(file)) && (mtimeOf(file) ?? 0) > docTime);
|
|
267
|
+
if (stale) staleDocs.push(entry.doc);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
const report = [
|
|
@@ -277,9 +277,6 @@ function driftBanner(projectDir) {
|
|
|
277
277
|
...staleDocs.map((d) => `- ${d}`),
|
|
278
278
|
"",
|
|
279
279
|
`## Bookkeeping queue (${counts.unresolved} unresolved: ${counts.auto} auto · ${counts.note} note · ${counts.needsSession} needs-session · ${counts.expiredWaived} expired-waived; ${counts.waived} waived total, ${swept} swept as resolved this session)`,
|
|
280
|
-
...(expiringSoon.length
|
|
281
|
-
? ["", `## Waivers expiring within 7 days (${expiringSoon.length})`, ...expiringSoon.map((w) => `- ${w}`)]
|
|
282
|
-
: []),
|
|
283
280
|
].join("\n");
|
|
284
281
|
try {
|
|
285
282
|
fs.writeFileSync(path.join(stateDir(projectDir), "drift-report.md"), report + "\n");
|
|
@@ -301,16 +298,10 @@ function driftBanner(projectDir) {
|
|
|
301
298
|
|
|
302
299
|
function sessionStart(projectDir, payload = {}) {
|
|
303
300
|
// Stamp the session's start so gitless repos still have a "modified since" horizon for the
|
|
304
|
-
// outcome detector's fallback scan (dirtyPaths).
|
|
305
|
-
// the per-sid twin exists because two concurrent sessions clobber the shared one. Best-effort.
|
|
301
|
+
// outcome detector's fallback scan (dirtyPaths). Best-effort.
|
|
306
302
|
const sid = payload.session_id;
|
|
307
|
-
const stamp = new Date().toISOString() + "\n";
|
|
308
303
|
try {
|
|
309
|
-
fs.writeFileSync(path.join(stateDir(projectDir), "session-started"),
|
|
310
|
-
if (sid) {
|
|
311
|
-
const short = String(sid).replace(/[^a-zA-Z0-9-]/g, "").slice(0, 8);
|
|
312
|
-
fs.writeFileSync(path.join(stateDir(projectDir), `session-started-${short}`), stamp);
|
|
313
|
-
}
|
|
304
|
+
fs.writeFileSync(path.join(stateDir(projectDir), "session-started"), new Date().toISOString() + "\n");
|
|
314
305
|
} catch (e) {
|
|
315
306
|
/* the stamp is a convenience for the fallback; git repos never need it */
|
|
316
307
|
}
|
|
@@ -350,6 +341,14 @@ function sessionStart(projectDir, payload = {}) {
|
|
|
350
341
|
const memory = memoryLine(projectDir);
|
|
351
342
|
if (memory) lines.push(memory);
|
|
352
343
|
lines.push(...openPlanLines(projectDir).slice(0, 4));
|
|
344
|
+
// A gate that crashed or skipped a write left a trace here — surface the count so the
|
|
345
|
+
// failure log finally has its reader.
|
|
346
|
+
try {
|
|
347
|
+
const failures = readSafe(path.join(stateDir(projectDir), "hook-failures.log")).split("\n").filter(Boolean);
|
|
348
|
+
if (failures.length) lines.push(`Hook failures logged: ${failures.length} (.claude/state/hook-failures.log) — enforcement may have gaps.`);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
/* the count is a convenience */
|
|
351
|
+
}
|
|
353
352
|
try {
|
|
354
353
|
const drift = driftBanner(projectDir);
|
|
355
354
|
if (drift) lines.push(drift);
|
|
@@ -373,15 +372,6 @@ function sessionStart(projectDir, payload = {}) {
|
|
|
373
372
|
// PostToolUse — the silent detector
|
|
374
373
|
// ---------------------------------------------------------------------------
|
|
375
374
|
|
|
376
|
-
const SOURCE_EXT = /\.(m?[jt]sx?|py|rs|go|rb|java|kt|swift|c|h|cpp|hpp|cs|sh|sql|css|scss|vue|svelte)$/;
|
|
377
|
-
|
|
378
|
-
function relIfInside(projectDir, filePath) {
|
|
379
|
-
if (!filePath) return null;
|
|
380
|
-
const rel = path.relative(projectDir, path.resolve(projectDir, filePath));
|
|
381
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
382
|
-
return rel.split(path.sep).join("/");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
375
|
// Spawn the installed doc-lint over one file; returns finding lines (empty when absent/clean).
|
|
386
376
|
function docLint(projectDir, rel) {
|
|
387
377
|
const script = path.join(projectDir, ".claude", "skills", "documentation", "scripts", "doc-lint.mjs");
|
|
@@ -441,20 +431,6 @@ function detectorForFile(projectDir, rel, meta, rows) {
|
|
|
441
431
|
);
|
|
442
432
|
}
|
|
443
433
|
}
|
|
444
|
-
const segment = rel.split("/")[1];
|
|
445
|
-
if (rel.startsWith("docs/") && segment && !segment.endsWith(".md")) {
|
|
446
|
-
const structure = readSafe(path.join(projectDir, "docs", "structure.md"));
|
|
447
|
-
if (structure && !structure.includes(`${segment}/`) && !structure.includes(`docs/${segment}`)) {
|
|
448
|
-
rows.push(
|
|
449
|
-
formatAutoRow({
|
|
450
|
-
...meta,
|
|
451
|
-
kind: "doc-placement",
|
|
452
|
-
description: `${rel} sits in a folder absent from docs/structure.md's authority map`,
|
|
453
|
-
action: "move the doc per the add-a-doc table, or record the new folder in structure.md",
|
|
454
|
-
})
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
434
|
if (rel.startsWith("docs/features/") && !readSafe(path.join(projectDir, "docs", "README.md")).includes(path.basename(rel))) {
|
|
459
435
|
rows.push(
|
|
460
436
|
formatAutoRow({
|
|
@@ -489,24 +465,14 @@ function detectorForFile(projectDir, rel, meta, rows) {
|
|
|
489
465
|
})
|
|
490
466
|
);
|
|
491
467
|
}
|
|
492
|
-
if (/(schema|init\.sql|migration|auth|webhook|\benv\b|wrangler)/i.test(rel)) {
|
|
493
|
-
rows.push(
|
|
494
|
-
formatAutoRow({
|
|
495
|
-
...meta,
|
|
496
|
-
kind: "boundary-scan-suggested",
|
|
497
|
-
description: `${rel} is contract-heavy`,
|
|
498
|
-
action: "run the boundary skill scoped to the diff",
|
|
499
|
-
})
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
468
|
}
|
|
503
469
|
}
|
|
504
470
|
|
|
505
471
|
function detectRenames(projectDir, command, meta, rows) {
|
|
506
472
|
if (!/\bgit mv\b|\bmv\b/.test(command || "")) return;
|
|
507
|
-
const
|
|
473
|
+
const queueRows = readQueue(projectDir);
|
|
508
474
|
for (const { glob, doc } of deadGlobs(projectDir)) {
|
|
509
|
-
if (
|
|
475
|
+
if (waiverCovers(queueRows, "dead-glob", glob, today())) continue;
|
|
510
476
|
rows.push(
|
|
511
477
|
formatAutoRow({
|
|
512
478
|
...meta,
|
|
@@ -612,7 +578,9 @@ function detectOutcomes(projectDir, meta, rows) {
|
|
|
612
578
|
}
|
|
613
579
|
}
|
|
614
580
|
if (rel.startsWith("docs/history/")) {
|
|
615
|
-
|
|
581
|
+
// Frozen AT HEAD, not in the worktree: the completion flip makes a plan dirty+Complete in
|
|
582
|
+
// one edit, and the worktree predicate walled the very commit that ceremony requires.
|
|
583
|
+
const frozen = frozenStatusAtHead(projectDir, rel);
|
|
616
584
|
if (frozen.frozen && !raised(`${rel} is a frozen record`)) {
|
|
617
585
|
rows.push(
|
|
618
586
|
formatAutoRow({
|
|
@@ -638,11 +606,10 @@ function postToolUse(projectDir, payload) {
|
|
|
638
606
|
const rows = [];
|
|
639
607
|
if (payload.tool_name === "Bash") {
|
|
640
608
|
// A completed `git commit` releases the claim compliance took at PreToolUse — the `end` line
|
|
641
|
-
// is what lets a waiting session verify the actor finished. Root resolution
|
|
642
|
-
//
|
|
643
|
-
if (/(
|
|
644
|
-
const
|
|
645
|
-
const root = cdMatch && path.isAbsolute(cdMatch[1]) && fs.existsSync(cdMatch[1]) ? cdMatch[1] : projectDir;
|
|
609
|
+
// is what lets a waiting session verify the actor finished. Root resolution is the gate's own
|
|
610
|
+
// `commandRoot`, shared, so a claim and its release can never resolve to different repos.
|
|
611
|
+
if (/(?:^|\n|&&|;|\|)\s*(?:sudo\s+)?git\s+commit\b/.test(input.command || "")) {
|
|
612
|
+
const root = commandRoot(projectDir, payload, input.command);
|
|
646
613
|
try {
|
|
647
614
|
releaseActivity(root, "commit", payload.session_id);
|
|
648
615
|
appendEvent(root, payload.session_id, "coord", "commit-claim", "released");
|
|
@@ -3,15 +3,11 @@
|
|
|
3
3
|
// judged mechanically: lockfile-only, harness-file-only, pure renames, changelog housekeeping,
|
|
4
4
|
// or a bot author. Exit 1 when a non-exempt HEAD stages no entry.
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { changelogExemptFile } from "./lib/exempt.mjs";
|
|
7
|
+
import { git as gitAt } from "./lib/repo.mjs";
|
|
8
8
|
|
|
9
9
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
10
|
-
|
|
11
|
-
function git(args) {
|
|
12
|
-
const res = spawnSync("git", args, { cwd: projectDir, encoding: "utf8" });
|
|
13
|
-
return res.status === 0 ? (res.stdout || "").trim() : null;
|
|
14
|
-
}
|
|
10
|
+
const git = (args) => gitAt(projectDir, args);
|
|
15
11
|
|
|
16
12
|
const nameStatus = git(["diff-tree", "--root", "--no-commit-id", "--name-status", "-r", "HEAD"]) || "";
|
|
17
13
|
const entries = nameStatus
|
|
@@ -22,28 +18,11 @@ const entries = nameStatus
|
|
|
22
18
|
return { status, file: files[files.length - 1] };
|
|
23
19
|
});
|
|
24
20
|
|
|
25
|
-
const LOCKFILES = /^(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|Cargo\.lock)$/;
|
|
26
|
-
const HARNESS_PREFIXES = [".claude/", ".agents/", ".codex/"];
|
|
27
|
-
const HARNESS_FILES = [
|
|
28
|
-
"AGENTS.md",
|
|
29
|
-
"CLAUDE.md",
|
|
30
|
-
".mcp.json",
|
|
31
|
-
".vscode/settings.json",
|
|
32
|
-
".prettierignore",
|
|
33
|
-
".prettierrc.json",
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const exemptFile = (f) =>
|
|
37
|
-
LOCKFILES.test(path.basename(f)) ||
|
|
38
|
-
HARNESS_PREFIXES.some((p) => f.startsWith(p)) ||
|
|
39
|
-
HARNESS_FILES.includes(f) ||
|
|
40
|
-
f.startsWith("docs/history/changelog/") ||
|
|
41
|
-
f === "docs/history/CHANGELOG.md";
|
|
42
21
|
|
|
43
22
|
const author = git(["log", "-1", "--format=%an <%ae>"]) || "";
|
|
44
23
|
const isBot = /\[bot\]|actions@github\.com/i.test(author);
|
|
45
24
|
const allRenames = entries.length > 0 && entries.every((e) => e.status.startsWith("R"));
|
|
46
|
-
const allExempt = entries.length === 0 || entries.every((e) =>
|
|
25
|
+
const allExempt = entries.length === 0 || entries.every((e) => changelogExemptFile(e.file));
|
|
47
26
|
const hasEntry = entries.some((e) => e.file.startsWith("docs/history/changelog/") && e.file.endsWith(".md"));
|
|
48
27
|
|
|
49
28
|
if (isBot || allRenames || allExempt || hasEntry) {
|
|
@@ -8,18 +8,11 @@ import fs from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { frontmatter, field } from "./lib/ownership.mjs";
|
|
10
10
|
import { undischargedRows } from "./lib/ledger.mjs";
|
|
11
|
+
import { readSafe } from "./lib/repo.mjs";
|
|
11
12
|
|
|
12
13
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
13
14
|
const plansDir = path.join(projectDir, "docs", "history", "plans");
|
|
14
15
|
|
|
15
|
-
function readSafe(file) {
|
|
16
|
-
try {
|
|
17
|
-
return fs.readFileSync(file, "utf8");
|
|
18
|
-
} catch (e) {
|
|
19
|
-
return "";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
let failures = 0;
|
|
24
17
|
const fail = (msg) => {
|
|
25
18
|
failures += 1;
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
// leaves "most specific wins" unable to pick an owner
|
|
7
7
|
//
|
|
8
8
|
// Either is silenced by an unexpired `waived:` row in docs/operations/bookkeeping.md naming its
|
|
9
|
-
// kind plus the offending glob (dead-glob) or the shared path (ownership-overlap)
|
|
9
|
+
// kind plus the offending glob (dead-glob) or the shared path (ownership-overlap) in backticks.
|
|
10
10
|
// Exit 1 on anything unwaived.
|
|
11
11
|
//
|
|
12
12
|
// A glob at strictly HIGHER specificity carving a sub-path out of a broader `**` is the designed
|
|
13
13
|
// layering, not a conflict — only an exact tie is reported.
|
|
14
14
|
|
|
15
15
|
import { deadGlobs, listRepoFiles, overlaps } from "./lib/ownership.mjs";
|
|
16
|
-
import { readQueue,
|
|
16
|
+
import { readQueue, waiverCovers } from "./lib/state.mjs";
|
|
17
17
|
|
|
18
18
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
19
19
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -22,12 +22,10 @@ const files = listRepoFiles(projectDir);
|
|
|
22
22
|
const queue = readQueue(projectDir);
|
|
23
23
|
|
|
24
24
|
const dead = deadGlobs(projectDir, files);
|
|
25
|
-
const
|
|
26
|
-
const unwaivedDead = dead.filter((d) => !deadWaivers.some((w) => w.body.includes(d.glob)));
|
|
25
|
+
const unwaivedDead = dead.filter((d) => !waiverCovers(queue, "dead-glob", d.glob, today));
|
|
27
26
|
|
|
28
27
|
const conflicts = overlaps(projectDir, files);
|
|
29
|
-
const
|
|
30
|
-
const unwaivedOverlaps = conflicts.filter((c) => !overlapWaivers.some((w) => w.body.includes(c.example)));
|
|
28
|
+
const unwaivedOverlaps = conflicts.filter((c) => !waiverCovers(queue, "ownership-overlap", c.example, today));
|
|
31
29
|
|
|
32
30
|
if (unwaivedDead.length === 0 && unwaivedOverlaps.length === 0) {
|
|
33
31
|
console.log(
|
|
@@ -39,7 +37,7 @@ if (unwaivedDead.length === 0 && unwaivedOverlaps.length === 0) {
|
|
|
39
37
|
if (unwaivedDead.length > 0) {
|
|
40
38
|
console.error(`check-ownership: ${unwaivedDead.length} unwaived dead owns: glob(s):`);
|
|
41
39
|
for (const d of unwaivedDead) console.error(` ${d.glob} → ${d.doc}`);
|
|
42
|
-
console.error("Fix the owning doc's owns: frontmatter, or add a `waived:` dead-glob row with reason + expires=.");
|
|
40
|
+
console.error("Fix the owning doc's owns: frontmatter, or add a `waived:` dead-glob row naming the glob in backticks, with reason + expires=.");
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
if (unwaivedOverlaps.length > 0) {
|
|
@@ -50,7 +48,7 @@ if (unwaivedOverlaps.length > 0) {
|
|
|
50
48
|
console.error(` ${c.b.glob} → ${c.b.doc}`);
|
|
51
49
|
}
|
|
52
50
|
console.error(
|
|
53
|
-
"Narrow one doc's glob so a single owner wins, or add a `waived:` ownership-overlap row naming the shared path with reason + expires=.",
|
|
51
|
+
"Narrow one doc's glob so a single owner wins, or add a `waived:` ownership-overlap row naming the shared path in backticks with reason + expires=.",
|
|
54
52
|
);
|
|
55
53
|
}
|
|
56
54
|
|