@nurix/etna 0.19.43 → 0.19.45
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 +33 -6
- package/data/harness-index.json +1 -1
- package/data/harnesses/apollo/agents/bookkeeper.md +27 -0
- package/data/harnesses/apollo/agents/maintenance.md +3 -1
- package/data/harnesses/apollo/harness.json +2 -2
- package/data/harnesses/apollo/hooks/awareness.mjs +370 -0
- package/data/harnesses/apollo/hooks/check-bookkeeping-clean.mjs +19 -0
- package/data/harnesses/apollo/hooks/check-changelog-presence.mjs +48 -0
- package/data/harnesses/apollo/hooks/check-completion-integrity.mjs +68 -0
- package/data/harnesses/apollo/hooks/check-ownership.mjs +22 -0
- package/data/harnesses/apollo/hooks/compliance.mjs +353 -0
- package/data/harnesses/apollo/hooks/hooks.json +16 -10
- package/data/harnesses/apollo/hooks/lib/frozen.mjs +58 -0
- package/data/harnesses/apollo/hooks/lib/ownership.mjs +223 -0
- package/data/harnesses/apollo/hooks/lib/state.mjs +156 -0
- package/data/harnesses/apollo/skills/boundary/SKILL.md +3 -1
- package/data/harnesses/apollo/skills/documentation/SKILL.md +2 -0
- package/data/harnesses/apollo/skills/documentation/templates/Readme.md +2 -0
- package/data/harnesses/apollo/skills/documentation/templates/bookkeeping.md +18 -0
- package/data/harnesses/apollo/skills/documentation/templates/journal.md +13 -0
- package/data/harnesses/apollo/skills/documentation/templates/plan.md +3 -0
- package/data/harnesses/apollo/skills/test-plan/SKILL.md +2 -0
- package/data/harnesses/code/agents/bookkeeper.md +27 -0
- package/data/harnesses/code/agents/maintenance.md +3 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/awareness.mjs +370 -0
- package/data/harnesses/code/hooks/check-bookkeeping-clean.mjs +19 -0
- package/data/harnesses/code/hooks/check-changelog-presence.mjs +48 -0
- package/data/harnesses/code/hooks/check-completion-integrity.mjs +68 -0
- package/data/harnesses/code/hooks/check-ownership.mjs +22 -0
- package/data/harnesses/code/hooks/compliance.mjs +353 -0
- package/data/harnesses/code/hooks/hooks.json +16 -10
- package/data/harnesses/code/hooks/lib/frozen.mjs +58 -0
- package/data/harnesses/code/hooks/lib/ownership.mjs +223 -0
- package/data/harnesses/code/hooks/lib/state.mjs +156 -0
- package/data/harnesses/code/skills/boundary/SKILL.md +3 -1
- package/data/harnesses/code/skills/documentation/SKILL.md +2 -0
- package/data/harnesses/code/skills/documentation/templates/Readme.md +2 -0
- package/data/harnesses/code/skills/documentation/templates/bookkeeping.md +18 -0
- package/data/harnesses/code/skills/documentation/templates/journal.md +13 -0
- package/data/harnesses/code/skills/documentation/templates/plan.md +3 -0
- package/data/harnesses/code/skills/test-plan/SKILL.md +2 -0
- package/data/harnesses/delivery/agents/maintenance.md +3 -1
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/skills/documentation/SKILL.md +2 -0
- package/data/harnesses/delivery/skills/documentation/templates/Readme.md +2 -0
- package/data/harnesses/delivery/skills/documentation/templates/bookkeeping.md +18 -0
- package/data/harnesses/delivery/skills/documentation/templates/journal.md +13 -0
- package/data/harnesses/delivery/skills/documentation/templates/plan.md +3 -0
- package/data/harnesses/etna/agents/maintenance.md +3 -1
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/boundary/SKILL.md +3 -1
- package/data/harnesses/etna/skills/documentation/SKILL.md +2 -0
- package/data/harnesses/etna/skills/documentation/templates/Readme.md +2 -0
- package/data/harnesses/etna/skills/documentation/templates/bookkeeping.md +18 -0
- package/data/harnesses/etna/skills/documentation/templates/journal.md +13 -0
- package/data/harnesses/etna/skills/documentation/templates/plan.md +3 -0
- package/package.json +2 -2
- package/data/harnesses/apollo/hooks/generate-ownership-index.mjs +0 -148
- package/data/harnesses/code/hooks/generate-ownership-index.mjs +0 -148
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// harness/apollo/hooks/generate-ownership-index.mjs — SessionStart hook: scan docs/features/**/*.md,
|
|
2
|
-
// extract the focal frontmatter (owns / module / keywords / status), and inject a compact
|
|
3
|
-
// code-path → owning-doc index via additionalContext. Fires on startup/resume/clear/compact so the
|
|
4
|
-
// map survives /clear and /compact and is regenerated fresh every time. Writes no file.
|
|
5
|
-
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
|
|
9
|
-
// Read all of stdin synchronously; returns "" if nothing is piped.
|
|
10
|
-
function readStdin() {
|
|
11
|
-
try {
|
|
12
|
-
return fs.readFileSync(0, "utf8");
|
|
13
|
-
} catch (e) {
|
|
14
|
-
return "";
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Resolve the project root: CLAUDE_PROJECT_DIR env, else payload cwd, else process cwd.
|
|
19
|
-
function resolveProjectDir(payload) {
|
|
20
|
-
return process.env.CLAUDE_PROJECT_DIR || (payload && payload.cwd) || process.cwd();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Recursively collect every .md file under dir (returns [] if the dir is absent).
|
|
24
|
-
function listMarkdown(dir) {
|
|
25
|
-
const out = [];
|
|
26
|
-
let entries;
|
|
27
|
-
try {
|
|
28
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
-
} catch (e) {
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
for (const entry of entries) {
|
|
33
|
-
const full = path.join(dir, entry.name);
|
|
34
|
-
if (entry.isDirectory()) out.push(...listMarkdown(full));
|
|
35
|
-
else if (entry.isFile() && entry.name.endsWith(".md")) out.push(full);
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Pull the leading `--- … ---` frontmatter block; "" if the file has none.
|
|
41
|
-
function frontmatter(text) {
|
|
42
|
-
const m = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
43
|
-
return m ? m[1] : "";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Trim surrounding whitespace, then strip one layer of matching quotes.
|
|
47
|
-
function unquote(s) {
|
|
48
|
-
return String(s).trim().replace(/^["']|["']$/g, "").trim();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Read one frontmatter key: scalar (`k: v`), inline array (`k: [a, b]`), or a following `- item` block.
|
|
52
|
-
// Returns a string for scalars, an array for lists, or undefined when the key is absent.
|
|
53
|
-
function field(block, key) {
|
|
54
|
-
const lines = block.split(/\r?\n/);
|
|
55
|
-
const head = new RegExp(`^${key}\\s*:\\s*(.*)$`);
|
|
56
|
-
for (let i = 0; i < lines.length; i++) {
|
|
57
|
-
const m = head.exec(lines[i]);
|
|
58
|
-
if (!m) continue;
|
|
59
|
-
const raw = m[1].trim();
|
|
60
|
-
if (raw === "") {
|
|
61
|
-
const items = [];
|
|
62
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
63
|
-
const li = /^\s*-\s+(.*)$/.exec(lines[j]);
|
|
64
|
-
if (!li) break;
|
|
65
|
-
items.push(unquote(li[1]));
|
|
66
|
-
}
|
|
67
|
-
return items;
|
|
68
|
-
}
|
|
69
|
-
if (raw.startsWith("[")) {
|
|
70
|
-
return raw.replace(/^\[|\]$/g, "").split(",").map((s) => unquote(s)).filter(Boolean);
|
|
71
|
-
}
|
|
72
|
-
return unquote(raw);
|
|
73
|
-
}
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Coerce a field that may be scalar/array/undefined into a clean string array.
|
|
78
|
-
function asList(v) {
|
|
79
|
-
if (v == null) return [];
|
|
80
|
-
return (Array.isArray(v) ? v : [v]).map((s) => String(s).trim()).filter(Boolean);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Build the ownership index text from every feature doc's frontmatter.
|
|
84
|
-
function buildIndex(projectDir) {
|
|
85
|
-
const featuresDir = path.join(projectDir, "docs", "features");
|
|
86
|
-
const files = listMarkdown(featuresDir);
|
|
87
|
-
const lines = [];
|
|
88
|
-
let deprecated = 0;
|
|
89
|
-
for (const file of files) {
|
|
90
|
-
let text;
|
|
91
|
-
try {
|
|
92
|
-
text = fs.readFileSync(file, "utf8");
|
|
93
|
-
} catch (e) {
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const block = frontmatter(text);
|
|
97
|
-
if (!block) continue;
|
|
98
|
-
const owns = asList(field(block, "owns"));
|
|
99
|
-
if (owns.length === 0) continue;
|
|
100
|
-
const status = (field(block, "status") || "active").toString().toLowerCase();
|
|
101
|
-
if (status === "deprecated") {
|
|
102
|
-
deprecated += 1;
|
|
103
|
-
continue; // keep deprecated docs out of the routing lines
|
|
104
|
-
}
|
|
105
|
-
const moduleLabel = (field(block, "module") || "—").toString();
|
|
106
|
-
const keywords = asList(field(block, "keywords"));
|
|
107
|
-
const doc = path.relative(projectDir, file).split(path.sep).join("/");
|
|
108
|
-
const kw = keywords.length ? `[${keywords.join(", ")}]` : "[]";
|
|
109
|
-
for (const glob of owns) {
|
|
110
|
-
lines.push(`${glob} → ${doc} · ${moduleLabel} · ${kw} · ${status}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (lines.length === 0) return "";
|
|
114
|
-
lines.sort();
|
|
115
|
-
const header =
|
|
116
|
-
"# Ownership index (generated) — code-path glob → owning feature doc · module · keywords · status\n" +
|
|
117
|
-
"# Match the file or topic you're about to touch, then read the owning doc before changing it.\n";
|
|
118
|
-
const footer = deprecated > 0 ? `\n# (${deprecated} deprecated feature doc(s) omitted)` : "";
|
|
119
|
-
return header + lines.join("\n") + footer;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function main() {
|
|
123
|
-
const raw = readStdin();
|
|
124
|
-
let payload = {};
|
|
125
|
-
try {
|
|
126
|
-
payload = raw ? JSON.parse(raw) : {};
|
|
127
|
-
} catch (e) {
|
|
128
|
-
payload = {};
|
|
129
|
-
}
|
|
130
|
-
const index = buildIndex(resolveProjectDir(payload));
|
|
131
|
-
if (!index) return; // no feature docs yet → inject nothing
|
|
132
|
-
process.stdout.write(
|
|
133
|
-
JSON.stringify({
|
|
134
|
-
hookSpecificOutput: {
|
|
135
|
-
hookEventName: "SessionStart",
|
|
136
|
-
additionalContext: index,
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
main();
|
|
144
|
-
} catch (e) {
|
|
145
|
-
// Best-effort: a routing index is a convenience, never a gate. Inject nothing on failure.
|
|
146
|
-
} finally {
|
|
147
|
-
process.exit(0);
|
|
148
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// harness/code/hooks/generate-ownership-index.mjs — SessionStart hook: scan docs/features/**/*.md,
|
|
2
|
-
// extract the focal frontmatter (owns / module / keywords / status), and inject a compact
|
|
3
|
-
// code-path → owning-doc index via additionalContext. Fires on startup/resume/clear/compact so the
|
|
4
|
-
// map survives /clear and /compact and is regenerated fresh every time. Writes no file.
|
|
5
|
-
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
|
|
9
|
-
// Read all of stdin synchronously; returns "" if nothing is piped.
|
|
10
|
-
function readStdin() {
|
|
11
|
-
try {
|
|
12
|
-
return fs.readFileSync(0, "utf8");
|
|
13
|
-
} catch (e) {
|
|
14
|
-
return "";
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Resolve the project root: CLAUDE_PROJECT_DIR env, else payload cwd, else process cwd.
|
|
19
|
-
function resolveProjectDir(payload) {
|
|
20
|
-
return process.env.CLAUDE_PROJECT_DIR || (payload && payload.cwd) || process.cwd();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Recursively collect every .md file under dir (returns [] if the dir is absent).
|
|
24
|
-
function listMarkdown(dir) {
|
|
25
|
-
const out = [];
|
|
26
|
-
let entries;
|
|
27
|
-
try {
|
|
28
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
-
} catch (e) {
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
for (const entry of entries) {
|
|
33
|
-
const full = path.join(dir, entry.name);
|
|
34
|
-
if (entry.isDirectory()) out.push(...listMarkdown(full));
|
|
35
|
-
else if (entry.isFile() && entry.name.endsWith(".md")) out.push(full);
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Pull the leading `--- … ---` frontmatter block; "" if the file has none.
|
|
41
|
-
function frontmatter(text) {
|
|
42
|
-
const m = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
43
|
-
return m ? m[1] : "";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Trim surrounding whitespace, then strip one layer of matching quotes.
|
|
47
|
-
function unquote(s) {
|
|
48
|
-
return String(s).trim().replace(/^["']|["']$/g, "").trim();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Read one frontmatter key: scalar (`k: v`), inline array (`k: [a, b]`), or a following `- item` block.
|
|
52
|
-
// Returns a string for scalars, an array for lists, or undefined when the key is absent.
|
|
53
|
-
function field(block, key) {
|
|
54
|
-
const lines = block.split(/\r?\n/);
|
|
55
|
-
const head = new RegExp(`^${key}\\s*:\\s*(.*)$`);
|
|
56
|
-
for (let i = 0; i < lines.length; i++) {
|
|
57
|
-
const m = head.exec(lines[i]);
|
|
58
|
-
if (!m) continue;
|
|
59
|
-
const raw = m[1].trim();
|
|
60
|
-
if (raw === "") {
|
|
61
|
-
const items = [];
|
|
62
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
63
|
-
const li = /^\s*-\s+(.*)$/.exec(lines[j]);
|
|
64
|
-
if (!li) break;
|
|
65
|
-
items.push(unquote(li[1]));
|
|
66
|
-
}
|
|
67
|
-
return items;
|
|
68
|
-
}
|
|
69
|
-
if (raw.startsWith("[")) {
|
|
70
|
-
return raw.replace(/^\[|\]$/g, "").split(",").map((s) => unquote(s)).filter(Boolean);
|
|
71
|
-
}
|
|
72
|
-
return unquote(raw);
|
|
73
|
-
}
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Coerce a field that may be scalar/array/undefined into a clean string array.
|
|
78
|
-
function asList(v) {
|
|
79
|
-
if (v == null) return [];
|
|
80
|
-
return (Array.isArray(v) ? v : [v]).map((s) => String(s).trim()).filter(Boolean);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Build the ownership index text from every feature doc's frontmatter.
|
|
84
|
-
function buildIndex(projectDir) {
|
|
85
|
-
const featuresDir = path.join(projectDir, "docs", "features");
|
|
86
|
-
const files = listMarkdown(featuresDir);
|
|
87
|
-
const lines = [];
|
|
88
|
-
let deprecated = 0;
|
|
89
|
-
for (const file of files) {
|
|
90
|
-
let text;
|
|
91
|
-
try {
|
|
92
|
-
text = fs.readFileSync(file, "utf8");
|
|
93
|
-
} catch (e) {
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const block = frontmatter(text);
|
|
97
|
-
if (!block) continue;
|
|
98
|
-
const owns = asList(field(block, "owns"));
|
|
99
|
-
if (owns.length === 0) continue;
|
|
100
|
-
const status = (field(block, "status") || "active").toString().toLowerCase();
|
|
101
|
-
if (status === "deprecated") {
|
|
102
|
-
deprecated += 1;
|
|
103
|
-
continue; // keep deprecated docs out of the routing lines
|
|
104
|
-
}
|
|
105
|
-
const moduleLabel = (field(block, "module") || "—").toString();
|
|
106
|
-
const keywords = asList(field(block, "keywords"));
|
|
107
|
-
const doc = path.relative(projectDir, file).split(path.sep).join("/");
|
|
108
|
-
const kw = keywords.length ? `[${keywords.join(", ")}]` : "[]";
|
|
109
|
-
for (const glob of owns) {
|
|
110
|
-
lines.push(`${glob} → ${doc} · ${moduleLabel} · ${kw} · ${status}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (lines.length === 0) return "";
|
|
114
|
-
lines.sort();
|
|
115
|
-
const header =
|
|
116
|
-
"# Ownership index (generated) — code-path glob → owning feature doc · module · keywords · status\n" +
|
|
117
|
-
"# Match the file or topic you're about to touch, then read the owning doc before changing it.\n";
|
|
118
|
-
const footer = deprecated > 0 ? `\n# (${deprecated} deprecated feature doc(s) omitted)` : "";
|
|
119
|
-
return header + lines.join("\n") + footer;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function main() {
|
|
123
|
-
const raw = readStdin();
|
|
124
|
-
let payload = {};
|
|
125
|
-
try {
|
|
126
|
-
payload = raw ? JSON.parse(raw) : {};
|
|
127
|
-
} catch (e) {
|
|
128
|
-
payload = {};
|
|
129
|
-
}
|
|
130
|
-
const index = buildIndex(resolveProjectDir(payload));
|
|
131
|
-
if (!index) return; // no feature docs yet → inject nothing
|
|
132
|
-
process.stdout.write(
|
|
133
|
-
JSON.stringify({
|
|
134
|
-
hookSpecificOutput: {
|
|
135
|
-
hookEventName: "SessionStart",
|
|
136
|
-
additionalContext: index,
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
main();
|
|
144
|
-
} catch (e) {
|
|
145
|
-
// Best-effort: a routing index is a convenience, never a gate. Inject nothing on failure.
|
|
146
|
-
} finally {
|
|
147
|
-
process.exit(0);
|
|
148
|
-
}
|