@nurix/etna 0.13.0 → 0.15.1

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.
Files changed (49) hide show
  1. package/README.md +31 -10
  2. package/bin/etna.js +58 -1
  3. package/data/harness-index.json +1 -1
  4. package/data/harnesses/code/CLAUDE.md +12 -18
  5. package/data/harnesses/code/agents/brainstorm.md +14 -21
  6. package/data/harnesses/code/agents/change-review.md +6 -6
  7. package/data/harnesses/code/agents/harness-manager.md +51 -11
  8. package/data/harnesses/code/agents/maintenance.md +43 -6
  9. package/data/harnesses/code/agents/performance-review.md +1 -1
  10. package/data/harnesses/code/agents/spec-mode.md +2 -2
  11. package/data/harnesses/code/harness.json +6 -0
  12. package/data/harnesses/code/hooks/hooks.json +41 -0
  13. package/data/harnesses/code/hooks/lib/redact.js +37 -0
  14. package/data/harnesses/code/hooks/post-commit.js +136 -0
  15. package/data/harnesses/code/hooks/post-tool-failure.js +136 -0
  16. package/data/harnesses/code/hooks/session-stop.js +207 -0
  17. package/data/harnesses/code/rules/general.md +1 -1
  18. package/data/harnesses/code/rules/nomenclature.md +1 -1
  19. package/data/harnesses/code/skills/boundary/SKILL.md +22 -4
  20. package/data/harnesses/code/skills/boundary/references/boundary-taxonomy.md +229 -161
  21. package/data/harnesses/code/skills/brainstorm/README.md +76 -227
  22. package/data/harnesses/code/skills/brainstorm/SKILL.md +79 -567
  23. package/data/harnesses/code/skills/brainstorm/harvest.md +26 -0
  24. package/data/harnesses/code/skills/brainstorm/templates/build.md +37 -0
  25. package/data/harnesses/code/skills/brainstorm/templates/decisions.md +12 -25
  26. package/data/harnesses/code/skills/brainstorm/templates/domain-model.md +25 -0
  27. package/data/harnesses/code/skills/brainstorm/templates/non-goals.md +2 -2
  28. package/data/harnesses/code/skills/brainstorm/templates/questions.md +18 -67
  29. package/data/harnesses/code/skills/brainstorm/templates/state.md +5 -5
  30. package/data/harnesses/code/skills/brainstorm/templates/tech.md +23 -0
  31. package/data/harnesses/code/skills/brainstorm/templates/thesis.md +21 -0
  32. package/data/harnesses/code/skills/distiller/SKILL.md +214 -0
  33. package/data/harnesses/code/skills/distiller/references/docs-mode.md +526 -0
  34. package/data/harnesses/code/skills/distiller/references/method.md +166 -0
  35. package/data/harnesses/code/skills/documentation/SKILL.md +1 -1
  36. package/data/harnesses/code/skills/documentation/references/templates.md +78 -5
  37. package/data/harnesses/code/skills/workflow-mapper/SKILL.md +1 -1
  38. package/data/harnesses/kb-agi/CLAUDE.md +1 -1
  39. package/data/harnesses/kb-agi/agents/kb-agi-init.md +2 -2
  40. package/data/harnesses/kb-agi/agents/kb-catch-stale.md +1 -1
  41. package/data/harnesses/kb-agi/agents/kb-find-gaps.md +2 -2
  42. package/data/harnesses/kb-agi/agents/kb-fix-links.md +1 -1
  43. package/data/harnesses/kb-agi/agents/kb-refresh-frontdoor.md +1 -1
  44. package/data/harnesses/kb-agi/agents/kb-refresh-summaries.md +1 -1
  45. package/data/harnesses/kb-agi/agents/kb-sync-metadata.md +1 -1
  46. package/data/harnesses/kb-agi/harness.json +6 -0
  47. package/package.json +1 -1
  48. package/data/harnesses/code/skills/brainstorm/greenfield.md +0 -99
  49. package/data/harnesses/code/skills/brainstorm/templates/inventory.md +0 -44
@@ -0,0 +1,207 @@
1
+ // harness/code/hooks/session-stop.js — SessionEnd hook: append one cheap session-summary ledger entry.
2
+
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+ const { execFileSync } = require("child_process");
9
+ const { redact } = require("./lib/redact.js");
10
+
11
+ // Read all of stdin synchronously; returns "" if nothing is piped.
12
+ function readStdin() {
13
+ try {
14
+ return fs.readFileSync(0, "utf8");
15
+ } catch (e) {
16
+ return "";
17
+ }
18
+ }
19
+
20
+ // Resolve the project root: CLAUDE_PROJECT_DIR env, else payload cwd, else process cwd.
21
+ function resolveProjectDir(payload) {
22
+ return process.env.CLAUDE_PROJECT_DIR || (payload && payload.cwd) || process.cwd();
23
+ }
24
+
25
+ // Run a git command in projectDir; returns trimmed stdout or fallback on any error.
26
+ function git(projectDir, args, fallback) {
27
+ try {
28
+ return execFileSync("git", ["-C", projectDir, ...args], {
29
+ encoding: "utf8",
30
+ stdio: ["ignore", "pipe", "ignore"],
31
+ }).trim() || fallback;
32
+ } catch (e) {
33
+ return fallback;
34
+ }
35
+ }
36
+
37
+ // Slug a raw actor name into kebab-case ASCII.
38
+ function slugActor(name) {
39
+ return String(name || "")
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9]+/g, "-")
42
+ .replace(/^-+|-+$/g, "");
43
+ }
44
+
45
+ // Determine the actor: git user.name (slugged) → $USER → "agent".
46
+ function resolveActor(projectDir) {
47
+ const fromGit = slugActor(git(projectDir, ["config", "user.name"], ""));
48
+ if (fromGit) return fromGit;
49
+ const fromEnv = slugActor(process.env.USER || "");
50
+ if (fromEnv) return fromEnv;
51
+ return "agent";
52
+ }
53
+
54
+ // Truncate a single line to n chars with an ellipsis marker.
55
+ function truncate(text, n) {
56
+ const clean = String(text || "").replace(/\s+/g, " ").trim();
57
+ return clean.length > n ? clean.slice(0, n) + "…" : clean;
58
+ }
59
+
60
+ // Parse the JSONL transcript into a cheap mechanical summary.
61
+ function summarizeTranscript(transcriptPath) {
62
+ const summary = { firstPrompt: "", assistantTurns: 0, files: new Set(), lastAssistant: "", sawCommit: false };
63
+ try {
64
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return summary;
65
+ const lines = fs.readFileSync(transcriptPath, "utf8").split("\n");
66
+ for (const line of lines) {
67
+ if (!line.trim()) continue;
68
+ let row;
69
+ try {
70
+ row = JSON.parse(line);
71
+ } catch (e) {
72
+ continue;
73
+ }
74
+ const msg = row && row.message ? row.message : row;
75
+ const role = (msg && msg.role) || row.type;
76
+ if (role === "user" && !summary.firstPrompt) {
77
+ summary.firstPrompt = extractText(msg);
78
+ }
79
+ if (role === "assistant") {
80
+ summary.assistantTurns += 1;
81
+ const text = extractText(msg);
82
+ if (text) summary.lastAssistant = text;
83
+ collectToolUse(msg, summary);
84
+ }
85
+ }
86
+ } catch (e) {
87
+ // Best-effort summary; ignore parse issues.
88
+ }
89
+ return summary;
90
+ }
91
+
92
+ // Extract plain text from a message whose content is a string or content-block array.
93
+ function extractText(msg) {
94
+ if (!msg || !msg.content) return "";
95
+ if (typeof msg.content === "string") return msg.content;
96
+ if (Array.isArray(msg.content)) {
97
+ return msg.content
98
+ .filter((b) => b && b.type === "text" && typeof b.text === "string")
99
+ .map((b) => b.text)
100
+ .join(" ");
101
+ }
102
+ return "";
103
+ }
104
+
105
+ // Scan an assistant message's tool_use blocks for edited files and git commits.
106
+ function collectToolUse(msg, summary) {
107
+ if (!msg || !Array.isArray(msg.content)) return;
108
+ for (const block of msg.content) {
109
+ if (!block || block.type !== "tool_use") continue;
110
+ const name = block.name || "";
111
+ const input = block.input || {};
112
+ if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(name) && input.file_path) {
113
+ summary.files.add(input.file_path);
114
+ }
115
+ if (name === "Bash" && typeof input.command === "string" && /\bgit\s+commit\b/.test(input.command)) {
116
+ summary.sawCommit = true;
117
+ }
118
+ }
119
+ }
120
+
121
+ // Compute the ledger day-file path for a given actor and date.
122
+ function ledgerPath(projectDir, date, actor) {
123
+ const dir = path.join(projectDir, "docs", "memory", "ledger");
124
+ return { dir, file: path.join(dir, `${date}--${actor}.md`) };
125
+ }
126
+
127
+ // Build the day-file header block.
128
+ function header(date, actor) {
129
+ return `# Ledger — ${date} — ${actor}\nAppend-only event ledger. Raw input to distillation — see docs/memory/.\n`;
130
+ }
131
+
132
+ // Skip if the day-file's last entry was written by this session within the current minute.
133
+ function isDuplicate(file, sessionId, hhmm) {
134
+ try {
135
+ if (!sessionId || !fs.existsSync(file)) return false;
136
+ const text = fs.readFileSync(file, "utf8");
137
+ const idx = text.lastIndexOf("\n## ");
138
+ const tail = idx >= 0 ? text.slice(idx) : text;
139
+ return tail.includes(`session ${sessionId}`) && tail.includes(`## ${hhmm} `);
140
+ } catch (e) {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ // Main: assemble and append one session-summary entry, never blocking.
146
+ function main() {
147
+ const raw = readStdin();
148
+ let payload = {};
149
+ try {
150
+ payload = raw ? JSON.parse(raw) : {};
151
+ } catch (e) {
152
+ payload = {};
153
+ }
154
+
155
+ const projectDir = resolveProjectDir(payload);
156
+ const actor = resolveActor(projectDir);
157
+ const repo = path.basename(projectDir);
158
+ const branch = git(projectDir, ["rev-parse", "--abbrev-ref", "HEAD"], "-");
159
+
160
+ const now = new Date();
161
+ const pad = (n) => String(n).padStart(2, "0");
162
+ const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
163
+ const hhmm = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
164
+
165
+ const summary = summarizeTranscript(payload.transcript_path);
166
+ const sessionId = payload.session_id || "";
167
+
168
+ // Nothing meaningful to record (garbage/empty payload, unreadable transcript) → silent no-op.
169
+ if (summary.assistantTurns === 0 && !summary.firstPrompt) return;
170
+
171
+ const { dir, file } = ledgerPath(projectDir, date, actor);
172
+ if (isDuplicate(file, sessionId, hhmm)) return;
173
+
174
+ const type = summary.sawCommit ? "decision" : summary.assistantTurns > 0 ? "action" : "observation";
175
+ const fileCount = summary.files.size;
176
+ const whatBits = [];
177
+ if (summary.firstPrompt) whatBits.push(`task "${truncate(redact(summary.firstPrompt), 100)}"`);
178
+ whatBits.push(`${summary.assistantTurns} turns`);
179
+ if (fileCount) whatBits.push(`${fileCount} file${fileCount === 1 ? "" : "s"} edited`);
180
+ const what = whatBits.join(", ") || "session ended";
181
+
182
+ const lines = [`## ${hhmm} · ${actor} · ${repo}@${branch}`, `- type: ${type}`, `- what: ${redact(what)}`];
183
+ if (summary.lastAssistant) {
184
+ lines.push(`- why: ${truncate(redact(summary.lastAssistant), 100)}`);
185
+ }
186
+ const refs = [];
187
+ if (sessionId) refs.push(`session ${sessionId}`);
188
+ if (fileCount) refs.push(`${fileCount} files`);
189
+ if (refs.length) lines.push(`- refs: ${redact(refs.join(" · "))}`);
190
+
191
+ const exists = fs.existsSync(file);
192
+ fs.mkdirSync(dir, { recursive: true });
193
+ const block = (exists ? "\n" : header(date, actor) + "\n") + lines.join("\n") + "\n";
194
+ fs.appendFileSync(file, block, "utf8");
195
+ }
196
+
197
+ try {
198
+ main();
199
+ } catch (e) {
200
+ try {
201
+ process.stderr.write(`session-stop hook: ${e && e.message ? e.message : e}\n`);
202
+ } catch (e2) {
203
+ // Nothing more we can do.
204
+ }
205
+ } finally {
206
+ process.exit(0);
207
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  applyTo: "**"
3
- plugins: [nurix-harness]
3
+ plugins: [code]
4
4
  ---
5
5
 
6
6
  # Technology Choices & Standards
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  applyTo: "**"
3
- plugins: [nurix-harness]
3
+ plugins: [code]
4
4
  ---
5
5
 
6
6
  # Nomenclature Guidelines
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: boundary
3
3
  description: The single home for a repo's typed application boundaries — the cross-cutting contracts (db-schema, wire envelope, store shape, auth, tenancy, event schemas, edge bindings, …) where editing one module silently breaks another. One skill, four modes — `generate` (baseline all boundaries from the detected stack + IA), `create` (add/update one boundary), `check` (pre-implementation: does this plan touch a boundary? severity-ranked impact), `init` (reconcile: ensure files exist, scan live code for violations + drift, backfill new catalog types). All four read one shared knowledge root, `references/boundary-taxonomy.md`, so they can never drift apart. Triggers on "application boundary", "baseline boundaries", "map boundaries", "generate boundaries", "create a boundary", "introspect", "check this plan", "impact analysis", "what could break", "scan for boundary violations", "boundary drift", "/boundary".
4
- plugins: [nurix-harness]
4
+ plugins: [code]
5
5
  ---
6
6
 
7
7
  # Application Boundary Skill
@@ -83,6 +83,16 @@ instantiate one file per boundary that actually exists.
83
83
  checklist (never a transcribed copy — it rots); `transcribe` boundaries (the
84
84
  composed store tree, the response envelope, the IA topology, tenancy) get the
85
85
  contract canonically in the file.
86
+ - **Greenfield branch — contract decided, no code yet.** If the repo is
87
+ pre-code (ADRs/domain-model decide the contract but the source module isn't
88
+ scaffolded), a `point` boundary has no `file:line` to mirror. Don't skip it
89
+ and don't fabricate a pointer — per the taxonomy's *Greenfield / pre-code
90
+ boundaries*, emit it as **conventions-with-planned-pointer**: record the
91
+ decided conventions inline, carry a `planned <path>` pointer, mark it `point*`
92
+ (a pre-code `transcribe` source is `transcribe*`), and make the last
93
+ safe-change item "replace the planned pointer with the real `file:line` once
94
+ code lands." Link any item that hinges on an unresolved decision to its
95
+ open-question/ADR.
86
96
  4. **Set severity** from the taxonomy default, adjusted by the concrete repo +
87
97
  project memory. Severity is load-bearing — it drives `check` review depth and
88
98
  `init` scan priority.
@@ -90,8 +100,13 @@ instantiate one file per boundary that actually exists.
90
100
  line. The distinct vertices named across all `Bounds` lines are the
91
101
  **module-discovery feed** `harness-manager` reads to propose modules — name
92
102
  them precisely.
93
- 6. **Write `docs/boundaries/README.md`** as the index: every boundary, its type,
94
- severity, and which technology packs are active.
103
+ 6. **Write `docs/boundaries/README.md`** as the index per the taxonomy's index
104
+ spec: a table with a **Doc-mode column**
105
+ (`| Boundary | Type | Severity | Doc-mode | Bounds (summary) |`), then the
106
+ standing blocks — **Active technology packs**, **Folds applied** (which catalog
107
+ types collapsed into which file — surface the per-entry fold-notes as a ledger),
108
+ and **Skipped** (types omitted + one-line reason each). Add the `point*`/`transcribe*`
109
+ footnote if any boundary is pre-code.
95
110
  7. **Don't invent boundaries the repo doesn't have.** Only instantiate types the
96
111
  detected stack/IA actually exhibits; `log()` what you skipped and why.
97
112
 
@@ -224,7 +239,10 @@ newly-exhibited boundary types backfilled.
224
239
  types; `log()` skips. **A clean report is valid** — empty violations/intersections
225
240
  means say so, don't pad.
226
241
  - **Docs never outrank code.** For `point` boundaries, mirror with `file:line`;
227
- don't fork the shape.
242
+ don't fork the shape. `file:line` stays primary for point-mode boundaries; an
243
+ `anchors:` query (see the documentation skill's frontmatter reference) MAY
244
+ supplement it for refactor-resilience — optional and additive, it re-resolves
245
+ the shape when the line moves, never replaces the pointer.
228
246
  - **Be specific** — file paths, line numbers, boundary names, the exact violating
229
247
  string or code shape.
230
248