@nurix/etna 0.19.45 → 0.19.47
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/apollo/agents/bookkeeper.md +2 -2
- package/data/harnesses/apollo/harness.json +2 -2
- package/data/harnesses/apollo/hooks/awareness.mjs +10 -0
- package/data/harnesses/apollo/hooks/compliance.mjs +9 -3
- package/data/harnesses/apollo/hooks/lib/state.mjs +6 -5
- package/data/harnesses/code/agents/bookkeeper.md +2 -2
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/awareness.mjs +10 -0
- package/data/harnesses/code/hooks/compliance.mjs +9 -3
- package/data/harnesses/code/hooks/lib/state.mjs +6 -5
- package/package.json +3 -2
package/data/harness-index.json
CHANGED
|
@@ -15,8 +15,8 @@ Ground every edit in this ladder, in order. Never invent content; a fact you can
|
|
|
15
15
|
|
|
16
16
|
1. **`auto:` rows are recomputable from code + docs on any machine — fix directly.** A `dead-glob` row: read the owning doc, find where the files went (`git log --follow`, glob search), update its `owns:` frontmatter. A `header-missing` row: add the file-path header comment in the file's native comment syntax. A `doc-lint` row: fix the finding, or add `<!-- doc-lint-disable: id -->` with a reason beside it when the flagged text is deliberate. An `index-row-missing` row: add the doc's row to `docs/README.md`. A `doc-implicated` row: read the diff of the named source file (`git diff` / `git log -p`), and update the owning doc **only where the diff contradicts documented behavior**; if nothing documented changed, the row's resolution is deleting it.
|
|
17
17
|
2. **`note:` rows — expand strictly from the stated context.** The one-line context after the area is everything the session chose to hand you; land it in the destination the row names (`lands-in:` an owning doc, the backlog, or memory), written in that document's genre.
|
|
18
|
-
3. **A thin note, same machine only** —
|
|
19
|
-
4. **Still ungrounded** — replace the row with a `needs-session:` row (`- needs-session: {date} · {what you could not ground} → main session resolves | refs={original
|
|
18
|
+
3. **A thin note, same machine only** — derive the transcript from the row's `sid=` tail: the session JSONL lives at `~/.claude/projects/{project-dir-slug}/{sid}.jsonl` (the slug is the project path with `/` → `-`). Search it for the row's file path or area and read only that window — never the whole file. The sid is best-effort and dead on other clones — expected; treat an unreadable or missing transcript as "no window".
|
|
19
|
+
4. **Still ungrounded** — replace the row with a `needs-session:` row (`- needs-session: {date} · {what you could not ground} → main session resolves | refs={original row}`) and stop working that row.
|
|
20
20
|
|
|
21
21
|
## Rules
|
|
22
22
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "apollo",
|
|
3
3
|
"name": "apollo",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.4",
|
|
5
5
|
"files": [
|
|
6
6
|
".prettierignore",
|
|
7
7
|
".prettierrc.json",
|
|
@@ -14,5 +14,5 @@
|
|
|
14
14
|
"docs/history/backlog.md",
|
|
15
15
|
"docs/memory/main.md"
|
|
16
16
|
],
|
|
17
|
-
"digest": "
|
|
17
|
+
"digest": "7b3bfe5d305e"
|
|
18
18
|
}
|
|
@@ -123,6 +123,14 @@ function driftBanner(projectDir) {
|
|
|
123
123
|
const waivers = unexpiredWaivers(queueRows, "dead-glob", today());
|
|
124
124
|
const unwaivedDead = dead.filter((d) => !waivers.some((w) => w.body.includes(d.glob)));
|
|
125
125
|
|
|
126
|
+
// A queued dead-glob row covered by an unexpired waiver is resolved history — drop it,
|
|
127
|
+
// so waiving a finding class actually empties the rows it supersedes.
|
|
128
|
+
resolveRows(projectDir, (row) => {
|
|
129
|
+
if (row.kind !== "auto" || !row.body.includes("dead-glob")) return false;
|
|
130
|
+
const m = /`([^`]+)`/.exec(row.body);
|
|
131
|
+
return !!m && waivers.some((w) => w.body.includes(`\`${m[1]}\``));
|
|
132
|
+
});
|
|
133
|
+
|
|
126
134
|
// Docs older than the code they own (doc mtime < newest owned-file mtime).
|
|
127
135
|
const staleDocs = [];
|
|
128
136
|
for (const entry of scanFeatureDocs(projectDir)) {
|
|
@@ -313,7 +321,9 @@ function detectorForFile(projectDir, rel, meta, rows) {
|
|
|
313
321
|
|
|
314
322
|
function detectRenames(projectDir, command, meta, rows) {
|
|
315
323
|
if (!/\bgit mv\b|\bmv\b/.test(command || "")) return;
|
|
324
|
+
const waivers = unexpiredWaivers(readQueue(projectDir), "dead-glob", today());
|
|
316
325
|
for (const { glob, doc } of deadGlobs(projectDir)) {
|
|
326
|
+
if (waivers.some((w) => w.body.includes(`\`${glob}\``))) continue;
|
|
317
327
|
rows.push(
|
|
318
328
|
formatAutoRow({
|
|
319
329
|
...meta,
|
|
@@ -232,7 +232,8 @@ function bashGate(projectDir, command, sessionId) {
|
|
|
232
232
|
return commitGate(projectDir);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
// `git branch <name>` creates; flag-led forms (--show-current, --list, -d, -vv) query or manage
|
|
236
|
+
if (statement(String.raw`git\s+(checkout\s+-b|switch\s+-c|branch\s+(?!-)\S+|worktree\s+add)`).test(command)) {
|
|
236
237
|
return askOnce(
|
|
237
238
|
projectDir,
|
|
238
239
|
sessionId,
|
|
@@ -290,12 +291,17 @@ function plansCompletedThisSession(projectDir) {
|
|
|
290
291
|
function stopGate(projectDir, payload) {
|
|
291
292
|
if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
|
|
292
293
|
|
|
293
|
-
|
|
294
|
+
// The Stop wall is scoped to this session's own rows (plus sid-less ones) — a concurrent
|
|
295
|
+
// session's open work must not block this one's stop. The commit wall stays global.
|
|
296
|
+
const mine = readQueue(projectDir).filter(
|
|
297
|
+
(row) => !/\bsid=/.test(row.body) || row.body.includes(`sid=${payload.session_id}`)
|
|
298
|
+
);
|
|
299
|
+
const counts = countRows(mine);
|
|
294
300
|
if (counts.unresolved > 0 && !bookkeeperDispatched(payload.transcript_path)) {
|
|
295
301
|
return {
|
|
296
302
|
decision: "block",
|
|
297
303
|
reason:
|
|
298
|
-
`${counts.unresolved} unresolved bookkeeping rows (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
|
|
304
|
+
`${counts.unresolved} unresolved bookkeeping rows from this session (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
|
|
299
305
|
"Dispatch the `bookkeeper` agent to drain them, resolve them now, or convert to `waived:` rows with reason + expiry. " +
|
|
300
306
|
"If implementation of the open plan is complete, also report it and offer the verification gate — never run it unbidden.",
|
|
301
307
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// harness/code/hooks/lib/state.mjs — the bookkeeping queue (docs/operations/bookkeeping.md) and the
|
|
2
2
|
// machine-local state dir (.claude/state/). The row grammar is the contract between the awareness
|
|
3
3
|
// detector (writer), the bookkeeper agent (drain), the compliance gates (walls), and CI (backstop):
|
|
4
|
-
// - auto: {YYYY-MM-DD} · {finding-kind} · {description} → {action} | sid={session_id}
|
|
5
|
-
// - note: {YYYY-MM-DD} · {area} · {context} → lands-in: {owning doc | backlog | memory} | sid=…
|
|
4
|
+
// - auto: {YYYY-MM-DD} · {finding-kind} · {description} → {action} | sid={session_id}
|
|
5
|
+
// - note: {YYYY-MM-DD} · {area} · {context} → lands-in: {owning doc | backlog | memory} | sid=…
|
|
6
6
|
// - needs-session: {YYYY-MM-DD} · {what could not be grounded} → main session resolves | refs={ts}
|
|
7
7
|
// - waived: {YYYY-MM-DD} · {what is deferred} · reason: {why} → expires={YYYY-MM-DD}
|
|
8
8
|
// Unresolved = auto + note + needs-session. waived: rows are resolved history until expires= passes.
|
|
@@ -57,9 +57,10 @@ export function countRows(rows) {
|
|
|
57
57
|
return c;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Format one auto: row per the grammar; sid
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// Format one auto: row per the grammar; the sid tail is the only provenance kept —
|
|
61
|
+
// a transcript path or timestamp is machine noise in a PR-reviewed queue.
|
|
62
|
+
export function formatAutoRow({ date, kind, description, action, sessionId }) {
|
|
63
|
+
return `- auto: ${date} · ${kind} · ${description} → ${action} | sid=${sessionId || "-"}`;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// Append rows to the queue, creating the file with its genre header when absent.
|
|
@@ -15,8 +15,8 @@ Ground every edit in this ladder, in order. Never invent content; a fact you can
|
|
|
15
15
|
|
|
16
16
|
1. **`auto:` rows are recomputable from code + docs on any machine — fix directly.** A `dead-glob` row: read the owning doc, find where the files went (`git log --follow`, glob search), update its `owns:` frontmatter. A `header-missing` row: add the file-path header comment in the file's native comment syntax. A `doc-lint` row: fix the finding, or add `<!-- doc-lint-disable: id -->` with a reason beside it when the flagged text is deliberate. An `index-row-missing` row: add the doc's row to `docs/README.md`. A `doc-implicated` row: read the diff of the named source file (`git diff` / `git log -p`), and update the owning doc **only where the diff contradicts documented behavior**; if nothing documented changed, the row's resolution is deleting it.
|
|
17
17
|
2. **`note:` rows — expand strictly from the stated context.** The one-line context after the area is everything the session chose to hand you; land it in the destination the row names (`lands-in:` an owning doc, the backlog, or memory), written in that document's genre.
|
|
18
|
-
3. **A thin note, same machine only** —
|
|
19
|
-
4. **Still ungrounded** — replace the row with a `needs-session:` row (`- needs-session: {date} · {what you could not ground} → main session resolves | refs={original
|
|
18
|
+
3. **A thin note, same machine only** — derive the transcript from the row's `sid=` tail: the session JSONL lives at `~/.claude/projects/{project-dir-slug}/{sid}.jsonl` (the slug is the project path with `/` → `-`). Search it for the row's file path or area and read only that window — never the whole file. The sid is best-effort and dead on other clones — expected; treat an unreadable or missing transcript as "no window".
|
|
19
|
+
4. **Still ungrounded** — replace the row with a `needs-session:` row (`- needs-session: {date} · {what you could not ground} → main session resolves | refs={original row}`) and stop working that row.
|
|
20
20
|
|
|
21
21
|
## Rules
|
|
22
22
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "code",
|
|
3
3
|
"name": "code",
|
|
4
|
-
"version": "3.5.
|
|
4
|
+
"version": "3.5.12",
|
|
5
5
|
"files": [
|
|
6
6
|
".prettierignore",
|
|
7
7
|
".prettierrc.json",
|
|
8
8
|
".vscode/settings.json"
|
|
9
9
|
],
|
|
10
10
|
"seeds": [],
|
|
11
|
-
"digest": "
|
|
11
|
+
"digest": "ddfeda99a408"
|
|
12
12
|
}
|
|
@@ -123,6 +123,14 @@ function driftBanner(projectDir) {
|
|
|
123
123
|
const waivers = unexpiredWaivers(queueRows, "dead-glob", today());
|
|
124
124
|
const unwaivedDead = dead.filter((d) => !waivers.some((w) => w.body.includes(d.glob)));
|
|
125
125
|
|
|
126
|
+
// A queued dead-glob row covered by an unexpired waiver is resolved history — drop it,
|
|
127
|
+
// so waiving a finding class actually empties the rows it supersedes.
|
|
128
|
+
resolveRows(projectDir, (row) => {
|
|
129
|
+
if (row.kind !== "auto" || !row.body.includes("dead-glob")) return false;
|
|
130
|
+
const m = /`([^`]+)`/.exec(row.body);
|
|
131
|
+
return !!m && waivers.some((w) => w.body.includes(`\`${m[1]}\``));
|
|
132
|
+
});
|
|
133
|
+
|
|
126
134
|
// Docs older than the code they own (doc mtime < newest owned-file mtime).
|
|
127
135
|
const staleDocs = [];
|
|
128
136
|
for (const entry of scanFeatureDocs(projectDir)) {
|
|
@@ -313,7 +321,9 @@ function detectorForFile(projectDir, rel, meta, rows) {
|
|
|
313
321
|
|
|
314
322
|
function detectRenames(projectDir, command, meta, rows) {
|
|
315
323
|
if (!/\bgit mv\b|\bmv\b/.test(command || "")) return;
|
|
324
|
+
const waivers = unexpiredWaivers(readQueue(projectDir), "dead-glob", today());
|
|
316
325
|
for (const { glob, doc } of deadGlobs(projectDir)) {
|
|
326
|
+
if (waivers.some((w) => w.body.includes(`\`${glob}\``))) continue;
|
|
317
327
|
rows.push(
|
|
318
328
|
formatAutoRow({
|
|
319
329
|
...meta,
|
|
@@ -232,7 +232,8 @@ function bashGate(projectDir, command, sessionId) {
|
|
|
232
232
|
return commitGate(projectDir);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
// `git branch <name>` creates; flag-led forms (--show-current, --list, -d, -vv) query or manage
|
|
236
|
+
if (statement(String.raw`git\s+(checkout\s+-b|switch\s+-c|branch\s+(?!-)\S+|worktree\s+add)`).test(command)) {
|
|
236
237
|
return askOnce(
|
|
237
238
|
projectDir,
|
|
238
239
|
sessionId,
|
|
@@ -290,12 +291,17 @@ function plansCompletedThisSession(projectDir) {
|
|
|
290
291
|
function stopGate(projectDir, payload) {
|
|
291
292
|
if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
|
|
292
293
|
|
|
293
|
-
|
|
294
|
+
// The Stop wall is scoped to this session's own rows (plus sid-less ones) — a concurrent
|
|
295
|
+
// session's open work must not block this one's stop. The commit wall stays global.
|
|
296
|
+
const mine = readQueue(projectDir).filter(
|
|
297
|
+
(row) => !/\bsid=/.test(row.body) || row.body.includes(`sid=${payload.session_id}`)
|
|
298
|
+
);
|
|
299
|
+
const counts = countRows(mine);
|
|
294
300
|
if (counts.unresolved > 0 && !bookkeeperDispatched(payload.transcript_path)) {
|
|
295
301
|
return {
|
|
296
302
|
decision: "block",
|
|
297
303
|
reason:
|
|
298
|
-
`${counts.unresolved} unresolved bookkeeping rows (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
|
|
304
|
+
`${counts.unresolved} unresolved bookkeeping rows from this session (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
|
|
299
305
|
"Dispatch the `bookkeeper` agent to drain them, resolve them now, or convert to `waived:` rows with reason + expiry. " +
|
|
300
306
|
"If implementation of the open plan is complete, also report it and offer the verification gate — never run it unbidden.",
|
|
301
307
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// harness/code/hooks/lib/state.mjs — the bookkeeping queue (docs/operations/bookkeeping.md) and the
|
|
2
2
|
// machine-local state dir (.claude/state/). The row grammar is the contract between the awareness
|
|
3
3
|
// detector (writer), the bookkeeper agent (drain), the compliance gates (walls), and CI (backstop):
|
|
4
|
-
// - auto: {YYYY-MM-DD} · {finding-kind} · {description} → {action} | sid={session_id}
|
|
5
|
-
// - note: {YYYY-MM-DD} · {area} · {context} → lands-in: {owning doc | backlog | memory} | sid=…
|
|
4
|
+
// - auto: {YYYY-MM-DD} · {finding-kind} · {description} → {action} | sid={session_id}
|
|
5
|
+
// - note: {YYYY-MM-DD} · {area} · {context} → lands-in: {owning doc | backlog | memory} | sid=…
|
|
6
6
|
// - needs-session: {YYYY-MM-DD} · {what could not be grounded} → main session resolves | refs={ts}
|
|
7
7
|
// - waived: {YYYY-MM-DD} · {what is deferred} · reason: {why} → expires={YYYY-MM-DD}
|
|
8
8
|
// Unresolved = auto + note + needs-session. waived: rows are resolved history until expires= passes.
|
|
@@ -57,9 +57,10 @@ export function countRows(rows) {
|
|
|
57
57
|
return c;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Format one auto: row per the grammar; sid
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// Format one auto: row per the grammar; the sid tail is the only provenance kept —
|
|
61
|
+
// a transcript path or timestamp is machine noise in a PR-reviewed queue.
|
|
62
|
+
export function formatAutoRow({ date, kind, description, action, sessionId }) {
|
|
63
|
+
return `- auto: ${date} · ${kind} · ${description} → ${action} | sid=${sessionId || "-"}`;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// Append rows to the queue, creating the file with its genre header when absent.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nurix/etna",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.47",
|
|
4
4
|
"description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"node": ">=18.0.0"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
|
+
"test": "node --test 'tests/**/*.test.mjs'",
|
|
22
23
|
"build:data": "node scripts/build-data.js",
|
|
23
24
|
"prepack": "node scripts/build-data.js",
|
|
24
25
|
"validate:codex-agents": "node lib/codex/validate-codex-agents.mjs"
|
|
@@ -41,5 +42,5 @@
|
|
|
41
42
|
"agent",
|
|
42
43
|
"plugin"
|
|
43
44
|
],
|
|
44
|
-
"etnaFingerprint": "
|
|
45
|
+
"etnaFingerprint": "fa53d6fe5dfc"
|
|
45
46
|
}
|