@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.
- package/README.md +31 -10
- package/bin/etna.js +58 -1
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +12 -18
- package/data/harnesses/code/agents/brainstorm.md +14 -21
- package/data/harnesses/code/agents/change-review.md +6 -6
- package/data/harnesses/code/agents/harness-manager.md +51 -11
- package/data/harnesses/code/agents/maintenance.md +43 -6
- package/data/harnesses/code/agents/performance-review.md +1 -1
- package/data/harnesses/code/agents/spec-mode.md +2 -2
- package/data/harnesses/code/harness.json +6 -0
- package/data/harnesses/code/hooks/hooks.json +41 -0
- package/data/harnesses/code/hooks/lib/redact.js +37 -0
- package/data/harnesses/code/hooks/post-commit.js +136 -0
- package/data/harnesses/code/hooks/post-tool-failure.js +136 -0
- package/data/harnesses/code/hooks/session-stop.js +207 -0
- package/data/harnesses/code/rules/general.md +1 -1
- package/data/harnesses/code/rules/nomenclature.md +1 -1
- package/data/harnesses/code/skills/boundary/SKILL.md +22 -4
- package/data/harnesses/code/skills/boundary/references/boundary-taxonomy.md +229 -161
- package/data/harnesses/code/skills/brainstorm/README.md +76 -227
- package/data/harnesses/code/skills/brainstorm/SKILL.md +79 -567
- package/data/harnesses/code/skills/brainstorm/harvest.md +26 -0
- package/data/harnesses/code/skills/brainstorm/templates/build.md +37 -0
- package/data/harnesses/code/skills/brainstorm/templates/decisions.md +12 -25
- package/data/harnesses/code/skills/brainstorm/templates/domain-model.md +25 -0
- package/data/harnesses/code/skills/brainstorm/templates/non-goals.md +2 -2
- package/data/harnesses/code/skills/brainstorm/templates/questions.md +18 -67
- package/data/harnesses/code/skills/brainstorm/templates/state.md +5 -5
- package/data/harnesses/code/skills/brainstorm/templates/tech.md +23 -0
- package/data/harnesses/code/skills/brainstorm/templates/thesis.md +21 -0
- package/data/harnesses/code/skills/distiller/SKILL.md +214 -0
- package/data/harnesses/code/skills/distiller/references/docs-mode.md +526 -0
- package/data/harnesses/code/skills/distiller/references/method.md +166 -0
- package/data/harnesses/code/skills/documentation/SKILL.md +1 -1
- package/data/harnesses/code/skills/documentation/references/templates.md +78 -5
- package/data/harnesses/code/skills/workflow-mapper/SKILL.md +1 -1
- package/data/harnesses/kb-agi/CLAUDE.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-agi-init.md +2 -2
- package/data/harnesses/kb-agi/agents/kb-catch-stale.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-find-gaps.md +2 -2
- package/data/harnesses/kb-agi/agents/kb-fix-links.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-refresh-frontdoor.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-refresh-summaries.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-sync-metadata.md +1 -1
- package/data/harnesses/kb-agi/harness.json +6 -0
- package/package.json +1 -1
- package/data/harnesses/code/skills/brainstorm/greenfield.md +0 -99
- 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,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: [
|
|
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
|
|
94
|
-
|
|
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
|
|