@nurix/etna 0.18.9 → 0.18.11

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 (108) hide show
  1. package/README.md +6 -5
  2. package/bin/etna.js +35 -0
  3. package/data/harness-index.json +9 -1
  4. package/data/harnesses/code/CLAUDE.md +8 -9
  5. package/data/harnesses/code/agents/change-review.md +1 -1
  6. package/data/harnesses/code/agents/harness-manager.md +15 -12
  7. package/data/harnesses/code/agents/maintenance.md +12 -12
  8. package/data/harnesses/code/agents/spec-mode.md +3 -3
  9. package/data/harnesses/code/harness.json +2 -2
  10. package/data/harnesses/code/skills/distiller/references/docs-mode.md +7 -7
  11. package/data/harnesses/code/skills/documentation/SKILL.md +25 -31
  12. package/data/harnesses/code/skills/documentation/references/templates.md +61 -24
  13. package/data/harnesses/etna/.prettierignore +12 -0
  14. package/data/harnesses/etna/.prettierrc.json +3 -0
  15. package/data/harnesses/etna/.vscode/settings.json +10 -0
  16. package/data/harnesses/etna/CLAUDE.md +160 -0
  17. package/data/harnesses/etna/agents/brainstorm.md +35 -0
  18. package/data/harnesses/etna/agents/change-review.md +251 -0
  19. package/data/harnesses/etna/agents/harness-manager.md +184 -0
  20. package/data/harnesses/etna/agents/maintenance.md +122 -0
  21. package/data/harnesses/etna/agents/performance-review.md +223 -0
  22. package/data/harnesses/etna/agents/spec-mode.md +56 -0
  23. package/data/harnesses/etna/harness.json +11 -0
  24. package/data/harnesses/etna/hooks/hooks.json +27 -0
  25. package/data/harnesses/etna/hooks/lib/redact.mjs +35 -0
  26. package/data/harnesses/etna/hooks/prompt-recall.mjs +155 -0
  27. package/data/harnesses/etna/hooks/session-reflect.mjs +285 -0
  28. package/data/harnesses/etna/rules/general.md +86 -0
  29. package/data/harnesses/etna/rules/kb.md +13 -0
  30. package/data/harnesses/etna/rules/nomenclature.md +41 -0
  31. package/data/harnesses/etna/skills/boundary/SKILL.md +49 -0
  32. package/data/harnesses/etna/skills/boundary/references/boundary-taxonomy.md +796 -0
  33. package/data/harnesses/etna/skills/brainstorm/README.md +136 -0
  34. package/data/harnesses/etna/skills/brainstorm/SKILL.md +135 -0
  35. package/data/harnesses/etna/skills/brainstorm/harvest.md +26 -0
  36. package/data/harnesses/etna/skills/brainstorm/templates/backlog.md +39 -0
  37. package/data/harnesses/etna/skills/brainstorm/templates/build.md +37 -0
  38. package/data/harnesses/etna/skills/brainstorm/templates/decisions.md +29 -0
  39. package/data/harnesses/etna/skills/brainstorm/templates/doctrine.md +31 -0
  40. package/data/harnesses/etna/skills/brainstorm/templates/domain-model.md +25 -0
  41. package/data/harnesses/etna/skills/brainstorm/templates/glossary.md +20 -0
  42. package/data/harnesses/etna/skills/brainstorm/templates/milestones.md +43 -0
  43. package/data/harnesses/etna/skills/brainstorm/templates/non-goals.md +17 -0
  44. package/data/harnesses/etna/skills/brainstorm/templates/plan-doc.md +61 -0
  45. package/data/harnesses/etna/skills/brainstorm/templates/questions.md +36 -0
  46. package/data/harnesses/etna/skills/brainstorm/templates/references.md +28 -0
  47. package/data/harnesses/etna/skills/brainstorm/templates/rejected.md +29 -0
  48. package/data/harnesses/etna/skills/brainstorm/templates/state.md +1 -0
  49. package/data/harnesses/etna/skills/brainstorm/templates/tech.md +23 -0
  50. package/data/harnesses/etna/skills/brainstorm/templates/thesis.md +21 -0
  51. package/data/harnesses/etna/skills/distiller/SKILL.md +90 -0
  52. package/data/harnesses/etna/skills/distiller/references/docs-mode.md +299 -0
  53. package/data/harnesses/etna/skills/distiller/references/method.md +94 -0
  54. package/data/harnesses/etna/skills/documentation/SKILL.md +124 -0
  55. package/data/harnesses/etna/skills/documentation/references/examples.md +162 -0
  56. package/data/harnesses/etna/skills/documentation/references/style-guide.md +308 -0
  57. package/data/harnesses/etna/skills/documentation/references/templates.md +894 -0
  58. package/data/harnesses/etna/skills/workflow-mapper/SKILL.md +451 -0
  59. package/data/harnesses/etna/skills/workflow-mapper/references/patterns.md +214 -0
  60. package/data/harnesses/mobile/.prettierignore +12 -0
  61. package/data/harnesses/mobile/.prettierrc.json +3 -0
  62. package/data/harnesses/mobile/.vscode/settings.json +10 -0
  63. package/data/harnesses/mobile/CLAUDE.md +129 -0
  64. package/data/harnesses/mobile/agents/brainstorm.md +35 -0
  65. package/data/harnesses/mobile/agents/change-review.md +251 -0
  66. package/data/harnesses/mobile/agents/harness-manager.md +184 -0
  67. package/data/harnesses/mobile/agents/maintenance.md +122 -0
  68. package/data/harnesses/mobile/agents/performance-review.md +223 -0
  69. package/data/harnesses/mobile/agents/spec-mode.md +56 -0
  70. package/data/harnesses/mobile/harness.json +11 -0
  71. package/data/harnesses/mobile/hooks/hooks.json +27 -0
  72. package/data/harnesses/mobile/hooks/lib/redact.mjs +35 -0
  73. package/data/harnesses/mobile/hooks/prompt-recall.mjs +155 -0
  74. package/data/harnesses/mobile/hooks/session-reflect.mjs +285 -0
  75. package/data/harnesses/mobile/rules/general.md +86 -0
  76. package/data/harnesses/mobile/rules/nomenclature.md +41 -0
  77. package/data/harnesses/mobile/rules/react-native.md +132 -0
  78. package/data/harnesses/mobile/rules/typescript.md +40 -0
  79. package/data/harnesses/mobile/skills/boundary/SKILL.md +49 -0
  80. package/data/harnesses/mobile/skills/boundary/references/boundary-taxonomy.md +796 -0
  81. package/data/harnesses/mobile/skills/brainstorm/README.md +136 -0
  82. package/data/harnesses/mobile/skills/brainstorm/SKILL.md +135 -0
  83. package/data/harnesses/mobile/skills/brainstorm/harvest.md +26 -0
  84. package/data/harnesses/mobile/skills/brainstorm/templates/backlog.md +39 -0
  85. package/data/harnesses/mobile/skills/brainstorm/templates/build.md +37 -0
  86. package/data/harnesses/mobile/skills/brainstorm/templates/decisions.md +29 -0
  87. package/data/harnesses/mobile/skills/brainstorm/templates/doctrine.md +31 -0
  88. package/data/harnesses/mobile/skills/brainstorm/templates/domain-model.md +25 -0
  89. package/data/harnesses/mobile/skills/brainstorm/templates/glossary.md +20 -0
  90. package/data/harnesses/mobile/skills/brainstorm/templates/milestones.md +43 -0
  91. package/data/harnesses/mobile/skills/brainstorm/templates/non-goals.md +17 -0
  92. package/data/harnesses/mobile/skills/brainstorm/templates/plan-doc.md +61 -0
  93. package/data/harnesses/mobile/skills/brainstorm/templates/questions.md +36 -0
  94. package/data/harnesses/mobile/skills/brainstorm/templates/references.md +28 -0
  95. package/data/harnesses/mobile/skills/brainstorm/templates/rejected.md +29 -0
  96. package/data/harnesses/mobile/skills/brainstorm/templates/state.md +1 -0
  97. package/data/harnesses/mobile/skills/brainstorm/templates/tech.md +23 -0
  98. package/data/harnesses/mobile/skills/brainstorm/templates/thesis.md +21 -0
  99. package/data/harnesses/mobile/skills/distiller/SKILL.md +90 -0
  100. package/data/harnesses/mobile/skills/distiller/references/docs-mode.md +299 -0
  101. package/data/harnesses/mobile/skills/distiller/references/method.md +94 -0
  102. package/data/harnesses/mobile/skills/documentation/SKILL.md +124 -0
  103. package/data/harnesses/mobile/skills/documentation/references/examples.md +162 -0
  104. package/data/harnesses/mobile/skills/documentation/references/style-guide.md +308 -0
  105. package/data/harnesses/mobile/skills/documentation/references/templates.md +894 -0
  106. package/data/harnesses/mobile/skills/workflow-mapper/SKILL.md +451 -0
  107. package/data/harnesses/mobile/skills/workflow-mapper/references/patterns.md +214 -0
  108. package/package.json +1 -1
@@ -0,0 +1,35 @@
1
+ // harness/code/hooks/lib/redact.mjs — shared privacy filter: redacts secret shapes before ledger append.
2
+
3
+ // Patterns ordered most-specific first; each replaces a match with [redacted:{kind}].
4
+ const RULES = [
5
+ // PEM private/public key blocks (multiline).
6
+ { kind: "pem", re: /-----BEGIN[\s\S]*?-----END[^-]*-----/g },
7
+ // OpenAI / Anthropic-style keys (sk-..., sk-ant-...).
8
+ { kind: "api-key", re: /\bsk-(?:ant-)?[A-Za-z0-9_-]{8,}\b/g },
9
+ // AWS access key IDs.
10
+ { kind: "aws-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
11
+ // Bearer tokens in Authorization context.
12
+ { kind: "bearer", re: /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi },
13
+ // password= / passwd= / secret= / token= / api_key= style assignments (= or :).
14
+ {
15
+ kind: "credential",
16
+ re: /\b(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|client[_-]?secret)\b\s*[:=]\s*["']?[^\s"'&)\]},;[]{3,}/gi,
17
+ },
18
+ // Generic long hex/base64 token ONLY in a key-value credential context.
19
+ {
20
+ kind: "credential",
21
+ re: /\b(?:key|auth|cred|credential|signature|sig)\b\s*[:=]\s*["']?[A-Za-z0-9+/=_-]{32,}/gi,
22
+ },
23
+ ];
24
+
25
+ // Redact secret shapes in arbitrary text; returns a string with placeholders.
26
+ function redact(text) {
27
+ if (text == null) return "";
28
+ let out = String(text);
29
+ for (const rule of RULES) {
30
+ out = out.replace(rule.re, `[redacted:${rule.kind}]`);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ export { redact };
@@ -0,0 +1,155 @@
1
+ // plugins/code/hooks/prompt-recall.mjs — UserPromptSubmit hook: surface fact-cards relevant to the prompt via qmd BM25; stdout is injected as context.
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { execFileSync } from "node:child_process";
7
+
8
+ const MIN_PROMPT_CHARS = 24;
9
+ const MIN_SCORE = 0.45;
10
+ const MAX_CARDS = 3;
11
+ const MAX_STATEMENT_CHARS = 300;
12
+ const MAX_KEYWORDS = 8;
13
+
14
+ const STOPWORDS = new Set(
15
+ "a an and are as at be but by can could do does for from has have how i in is it its me my of on or our should so that the their then there these this to was we what when where which who why will with would you your".split(
16
+ " ",
17
+ ),
18
+ );
19
+
20
+ // Read all of stdin synchronously; returns "" if nothing is piped.
21
+ function readStdin() {
22
+ try {
23
+ return fs.readFileSync(0, "utf8");
24
+ } catch (e) {
25
+ return "";
26
+ }
27
+ }
28
+
29
+ // Resolve the project root: CLAUDE_PROJECT_DIR env, else payload cwd, else process cwd.
30
+ function resolveProjectDir(payload) {
31
+ return process.env.CLAUDE_PROJECT_DIR || (payload && payload.cwd) || process.cwd();
32
+ }
33
+
34
+ // Map a qmd://collection/relpath hit URI to the card file under docs/memory/facts/.
35
+ function cardPath(factsDir, fileUri) {
36
+ return path.join(factsDir, String(fileUri || "").replace(/^qmd:\/\/[^/]+\//, ""));
37
+ }
38
+
39
+ // A card's atomic statement: the body after frontmatter, whitespace-flattened and clipped.
40
+ function cardStatement(file) {
41
+ try {
42
+ const body = fs
43
+ .readFileSync(file, "utf8")
44
+ .replace(/^---\n[\s\S]*?\n---\n?/, "")
45
+ .replace(/\s+/g, " ")
46
+ .trim();
47
+ return body.length > MAX_STATEMENT_CHARS ? body.slice(0, MAX_STATEMENT_CHARS) + "…" : body;
48
+ } catch (e) {
49
+ return "";
50
+ }
51
+ }
52
+
53
+ // Run one qmd search mode against the facts collection; [] on any failure — recall is strictly optional.
54
+ function qmdHits(mode, query, collection) {
55
+ try {
56
+ const out = JSON.parse(
57
+ execFileSync("qmd", [mode, query, "--format", "json", "-n", String(MAX_CARDS + 2), "-c", collection], {
58
+ encoding: "utf8",
59
+ timeout: 10000,
60
+ stdio: ["ignore", "pipe", "ignore"],
61
+ }),
62
+ );
63
+ return Array.isArray(out) ? out : [];
64
+ } catch (e) {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ // Main: search the facts collection for the prompt and emit unseen high-score cards.
70
+ function main() {
71
+ if (process.env.ETNA_REFLECT) return;
72
+
73
+ const raw = readStdin();
74
+ let payload = {};
75
+ try {
76
+ payload = raw ? JSON.parse(raw) : {};
77
+ } catch (e) {
78
+ payload = {};
79
+ }
80
+
81
+ const prompt = String(payload.prompt || "").trim();
82
+ if (!prompt || prompt.startsWith("/") || prompt.length < MIN_PROMPT_CHARS) return;
83
+
84
+ const projectDir = resolveProjectDir(payload);
85
+ const factsDir = path.join(projectDir, "docs", "memory", "facts");
86
+ let hasCards = false;
87
+ try {
88
+ hasCards = fs.readdirSync(factsDir).some((f) => f.endsWith(".md"));
89
+ } catch (e) {
90
+ return;
91
+ }
92
+ if (!hasCards) return;
93
+
94
+ // BM25 keywords first (fast, strict-AND), vector similarity as the natural-language
95
+ // fallback (~1s warm) — `qmd query` (LLM expansion + rerank) is too slow to block a prompt.
96
+ const collection = `${path.basename(projectDir)}-facts`;
97
+ const keywords = Array.from(
98
+ new Set(
99
+ prompt
100
+ .toLowerCase()
101
+ .replace(/[^a-z0-9\s_./-]/g, " ")
102
+ .split(/\s+/)
103
+ .filter((w) => w.length > 2 && !STOPWORDS.has(w)),
104
+ ),
105
+ )
106
+ .slice(0, MAX_KEYWORDS)
107
+ .join(" ");
108
+ let hits = keywords ? qmdHits("search", keywords, collection) : [];
109
+ if (!hits.length) hits = qmdHits("vsearch", prompt.replace(/\s+/g, " ").slice(0, 300), collection);
110
+ if (!hits.length) return;
111
+
112
+ // Inject each card at most once per session.
113
+ const sessionKey = String(payload.session_id || "any").replace(/[^a-zA-Z0-9-]/g, "");
114
+ const stateFile = path.join(os.tmpdir(), `etna-recall-${sessionKey}.txt`);
115
+ let seen = [];
116
+ try {
117
+ seen = fs.readFileSync(stateFile, "utf8").split("\n").filter(Boolean);
118
+ } catch (e) {
119
+ seen = [];
120
+ }
121
+
122
+ const fresh = hits
123
+ .filter((h) => h && h.score >= MIN_SCORE && h.file && !seen.includes(h.file))
124
+ .slice(0, MAX_CARDS);
125
+ const lines = [];
126
+ for (const hit of fresh) {
127
+ const statement = cardStatement(cardPath(factsDir, hit.file));
128
+ if (statement) {
129
+ lines.push(`- ${statement} [${String(hit.file).replace(/^qmd:\/\/[^/]+\//, "docs/memory/facts/")}]`);
130
+ }
131
+ }
132
+ if (!lines.length) return;
133
+
134
+ try {
135
+ fs.appendFileSync(stateFile, fresh.map((h) => h.file).join("\n") + "\n", "utf8");
136
+ } catch (e) {
137
+ // Dedupe state is best-effort; a re-injection is harmless.
138
+ }
139
+
140
+ process.stdout.write(
141
+ `<memory-recall>\nProject memory possibly relevant to this prompt (distilled fact-cards):\n${lines.join("\n")}\n</memory-recall>\n`,
142
+ );
143
+ }
144
+
145
+ try {
146
+ main();
147
+ } catch (e) {
148
+ try {
149
+ process.stderr.write(`prompt-recall hook: ${e && e.message ? e.message : e}\n`);
150
+ } catch (e2) {
151
+ // Nothing more we can do.
152
+ }
153
+ } finally {
154
+ process.exit(0);
155
+ }
@@ -0,0 +1,285 @@
1
+ // plugins/code/hooks/session-reflect.mjs — SessionEnd hook: reflect the session into durable lessons (headless haiku pass) appended to the actor's memory inbox.
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { execFileSync, spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ import { redact } from "./lib/redact.mjs";
9
+
10
+ // ESM has no __filename; derive it from this module's URL for the detached worker re-spawn.
11
+ const __filename = fileURLToPath(import.meta.url);
12
+
13
+ // A session below all three thresholds carries no lesson worth a model call.
14
+ const MIN_TURNS = 8;
15
+ const MAX_MESSAGE_CHARS = 1500;
16
+ const MAX_DIGEST_CHARS = 80000;
17
+ const MAX_LESSONS = 6;
18
+
19
+ // Read all of stdin synchronously; returns "" if nothing is piped.
20
+ function readStdin() {
21
+ try {
22
+ return fs.readFileSync(0, "utf8");
23
+ } catch (e) {
24
+ return "";
25
+ }
26
+ }
27
+
28
+ // Resolve the project root: CLAUDE_PROJECT_DIR env, else payload cwd, else process cwd.
29
+ function resolveProjectDir(payload) {
30
+ return process.env.CLAUDE_PROJECT_DIR || (payload && payload.cwd) || process.cwd();
31
+ }
32
+
33
+ // Run a git command in projectDir; returns trimmed stdout or fallback on any error.
34
+ function git(projectDir, args, fallback) {
35
+ try {
36
+ return execFileSync("git", ["-C", projectDir, ...args], {
37
+ encoding: "utf8",
38
+ stdio: ["ignore", "pipe", "ignore"],
39
+ }).trim() || fallback;
40
+ } catch (e) {
41
+ return fallback;
42
+ }
43
+ }
44
+
45
+ // Slug a raw actor name into kebab-case ASCII.
46
+ function slugActor(name) {
47
+ return String(name || "")
48
+ .toLowerCase()
49
+ .replace(/[^a-z0-9]+/g, "-")
50
+ .replace(/^-+|-+$/g, "");
51
+ }
52
+
53
+ // Determine the actor: git user.name (slugged) → $USER → "agent".
54
+ function resolveActor(projectDir) {
55
+ const fromGit = slugActor(git(projectDir, ["config", "user.name"], ""));
56
+ if (fromGit) return fromGit;
57
+ const fromEnv = slugActor(process.env.USER || "");
58
+ if (fromEnv) return fromEnv;
59
+ return "agent";
60
+ }
61
+
62
+ // Truncate a single line to n chars with an ellipsis marker.
63
+ function clip(text, n) {
64
+ const clean = String(text || "").replace(/\s+/g, " ").trim();
65
+ return clean.length > n ? clean.slice(0, n) + "…" : clean;
66
+ }
67
+
68
+ // Extract plain text from a message whose content is a string or content-block array.
69
+ function extractText(msg) {
70
+ if (!msg || !msg.content) return "";
71
+ if (typeof msg.content === "string") return msg.content;
72
+ if (Array.isArray(msg.content)) {
73
+ return msg.content
74
+ .filter((b) => b && b.type === "text" && typeof b.text === "string")
75
+ .map((b) => b.text)
76
+ .join(" ");
77
+ }
78
+ return "";
79
+ }
80
+
81
+ // Strip harness-injected wrappers so only what the user actually typed survives.
82
+ function stripInjected(text) {
83
+ return String(text || "")
84
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
85
+ .replace(/<(local-command-caveat|local-command-stdout|local-command-stderr|command-name|command-message|command-args|command-contents)>[\s\S]*?<\/\1>/g, "")
86
+ .trim();
87
+ }
88
+
89
+ // Scan an assistant message's tool_use blocks for edited files and git commits.
90
+ function collectToolUse(msg, summary) {
91
+ if (!msg || !Array.isArray(msg.content)) return;
92
+ for (const block of msg.content) {
93
+ if (!block || block.type !== "tool_use") continue;
94
+ const name = block.name || "";
95
+ const input = block.input || {};
96
+ if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(name) && input.file_path) {
97
+ summary.files.add(input.file_path);
98
+ }
99
+ if (name === "Bash" && typeof input.command === "string" && /\bgit\s+commit\b/.test(input.command)) {
100
+ summary.sawCommit = true;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Walk the JSONL transcript once: turn/edit/commit counts plus a USER/ASSISTANT text digest.
106
+ function digestTranscript(transcriptPath) {
107
+ const out = { turns: 0, files: new Set(), sawCommit: false, lines: [] };
108
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return out;
109
+ for (const line of fs.readFileSync(transcriptPath, "utf8").split("\n")) {
110
+ if (!line.trim()) continue;
111
+ let row;
112
+ try {
113
+ row = JSON.parse(line);
114
+ } catch (e) {
115
+ continue;
116
+ }
117
+ if (row.isSidechain) continue;
118
+ const msg = row && row.message ? row.message : row;
119
+ const role = (msg && msg.role) || row.type;
120
+ if (role === "user") {
121
+ const text = stripInjected(extractText(msg));
122
+ if (!text || text.startsWith("Caveat: The messages below")) continue;
123
+ out.lines.push("USER: " + clip(text, MAX_MESSAGE_CHARS));
124
+ } else if (role === "assistant") {
125
+ out.turns += 1;
126
+ collectToolUse(msg, out);
127
+ const text = extractText(msg);
128
+ if (text) out.lines.push("ASSISTANT: " + clip(text, MAX_MESSAGE_CHARS));
129
+ }
130
+ }
131
+ return out;
132
+ }
133
+
134
+ // Compute the per-actor inbox file path.
135
+ function inboxPath(projectDir, actor) {
136
+ const dir = path.join(projectDir, "docs", "memory", "inbox");
137
+ return { dir, file: path.join(dir, `${actor}.md`) };
138
+ }
139
+
140
+ // True when this session already wrote an inbox entry (SessionEnd re-fires on resume).
141
+ function alreadyReflected(file, sessionId) {
142
+ try {
143
+ return Boolean(sessionId) && fs.existsSync(file) && fs.readFileSync(file, "utf8").includes(`session ${sessionId}`);
144
+ } catch (e) {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ // Build the inbox file header block.
150
+ function inboxHeader(actor) {
151
+ return `# Memory inbox — ${actor}\nSession lessons awaiting distillation — appended by the session-reflect hook, consumed by \`distiller facts\`.\n`;
152
+ }
153
+
154
+ // The full output contract sits AFTER the transcript — with it only up front, the model
155
+ // continues the conversation in the transcript instead of reflecting on it.
156
+ const PROMPT_HEADER = [
157
+ "You are a memory reflector. Below is the transcript of a FINISHED coding session,",
158
+ "delimited by <transcript> tags. Do not reply to it or continue it — you will be asked",
159
+ "to extract durable lessons from it after the closing tag.",
160
+ "",
161
+ "<transcript>",
162
+ "",
163
+ ].join("\n");
164
+
165
+ const PROMPT_FOOTER = [
166
+ "",
167
+ "</transcript>",
168
+ "",
169
+ "The session above is over. Extract ONLY durable lessons a future session in this",
170
+ "repository would benefit from knowing:",
171
+ "- corrections or preferences the user stated",
172
+ "- failures whose root cause was found, with the fix",
173
+ "- decisions made, with their rationale",
174
+ "- discovered constraints or non-obvious facts about the project or its tooling",
175
+ "Rules:",
176
+ "- Most sessions contain none. If nothing qualifies, output exactly: NONE",
177
+ `- Otherwise output at most ${MAX_LESSONS} bullets and nothing else — no preamble, no headers.`,
178
+ '- Each bullet starts with "- [project]" (true of this repo or its tooling) or "- [user]" (true of this user).',
179
+ "- Each bullet is one complete, self-contained sentence with names and identifiers spelled out.",
180
+ "- Routine work (commits made, files edited, tasks completed) is NOT a lesson. Never include secrets.",
181
+ ].join("\n");
182
+
183
+ // Hook mode: stash the payload and hand off to a detached worker so session exit is never delayed.
184
+ function hook() {
185
+ const raw = readStdin();
186
+ let payload = {};
187
+ try {
188
+ payload = raw ? JSON.parse(raw) : {};
189
+ } catch (e) {
190
+ payload = {};
191
+ }
192
+ if (!payload.transcript_path) return;
193
+
194
+ const stash = {
195
+ projectDir: resolveProjectDir(payload),
196
+ transcriptPath: payload.transcript_path,
197
+ sessionId: payload.session_id || "",
198
+ };
199
+ const stashFile = path.join(os.tmpdir(), `etna-reflect-${stash.sessionId || process.pid}.json`);
200
+ fs.writeFileSync(stashFile, JSON.stringify(stash), "utf8");
201
+ spawn(process.execPath, [__filename, "--worker", stashFile], { detached: true, stdio: "ignore" }).unref();
202
+ }
203
+
204
+ // Worker mode: filter, digest, run the reflection model, append surviving lessons to the inbox.
205
+ function worker(stashFile) {
206
+ let stash;
207
+ try {
208
+ stash = JSON.parse(fs.readFileSync(stashFile, "utf8"));
209
+ } finally {
210
+ try {
211
+ fs.rmSync(stashFile, { force: true });
212
+ } catch (e) {
213
+ // Stale stash files are harmless.
214
+ }
215
+ }
216
+
217
+ const { projectDir, transcriptPath, sessionId } = stash;
218
+ const digest = digestTranscript(transcriptPath);
219
+ if (digest.turns < MIN_TURNS && digest.files.size === 0 && !digest.sawCommit) return;
220
+
221
+ const actor = resolveActor(projectDir);
222
+ const { dir, file } = inboxPath(projectDir, actor);
223
+ if (alreadyReflected(file, sessionId)) return;
224
+
225
+ let text = redact(digest.lines.join("\n"));
226
+ if (text.length > MAX_DIGEST_CHARS) {
227
+ text = text.slice(0, 8000) + "\n[... middle of session elided ...]\n" + text.slice(-(MAX_DIGEST_CHARS - 8000));
228
+ }
229
+
230
+ const output = execFileSync("claude", ["-p", "--model", "haiku"], {
231
+ input: PROMPT_HEADER + text + PROMPT_FOOTER,
232
+ encoding: "utf8",
233
+ cwd: os.tmpdir(),
234
+ env: Object.assign({}, process.env, { ETNA_REFLECT: "1" }),
235
+ timeout: 240000,
236
+ maxBuffer: 4 * 1024 * 1024,
237
+ stdio: ["pipe", "pipe", "ignore"],
238
+ });
239
+
240
+ const lessons = output
241
+ .split("\n")
242
+ .map((l) => l.trim())
243
+ .filter((l) => /^- \[(project|user)\]\s+\S/.test(l))
244
+ .slice(0, MAX_LESSONS)
245
+ .map((l) => redact(l));
246
+ if (!lessons.length) return;
247
+
248
+ const repo = path.basename(projectDir);
249
+ const branch = git(projectDir, ["rev-parse", "--abbrev-ref", "HEAD"], "-");
250
+ const now = new Date();
251
+ const pad = (n) => String(n).padStart(2, "0");
252
+ const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
253
+
254
+ fs.mkdirSync(dir, { recursive: true });
255
+ if (alreadyReflected(file, sessionId)) return;
256
+ const exists = fs.existsSync(file);
257
+ const block =
258
+ (exists ? "\n" : inboxHeader(actor) + "\n") +
259
+ `## ${stamp} · session ${sessionId || "unknown"} · ${repo}@${branch}\n` +
260
+ lessons.join("\n") +
261
+ "\n";
262
+ fs.appendFileSync(file, block, "utf8");
263
+ }
264
+
265
+ function main() {
266
+ if (process.env.ETNA_REFLECT) return;
267
+ const workerIdx = process.argv.indexOf("--worker");
268
+ if (workerIdx !== -1) {
269
+ worker(process.argv[workerIdx + 1]);
270
+ } else {
271
+ hook();
272
+ }
273
+ }
274
+
275
+ try {
276
+ main();
277
+ } catch (e) {
278
+ try {
279
+ process.stderr.write(`session-reflect hook: ${e && e.message ? e.message : e}\n`);
280
+ } catch (e2) {
281
+ // Nothing more we can do.
282
+ }
283
+ } finally {
284
+ process.exit(0);
285
+ }
@@ -0,0 +1,86 @@
1
+ # General Standards
2
+
3
+ ## Runtime Versions
4
+
5
+ - **Node.js:** 22 LTS (latest)
6
+ - **Package manager:** detect from lockfile (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `package-lock.json` → npm). Don't hardcode one.
7
+ - **PostgreSQL:** 17 · **Redis:** 7.x (latest)
8
+
9
+ ## Recommended Packages
10
+
11
+ No alternatives. Just use these.
12
+
13
+ | Package | Version | Use | Don't use |
14
+ | ------------------------ | --------- | ---------------------- | -------------------------- |
15
+ | react | ^19.2.3 | UI components | - |
16
+ | vite | ^8.0.0 | Build tool, dev server | webpack, CRA |
17
+ | tailwindcss | ^4.1.18 | Styling | styled-components |
18
+ | @vitejs/plugin-react-swc | ^4.2.2 | JSX compilation | @vitejs/plugin-react |
19
+ | react-router-dom | ^7.11.0 | Routing | - |
20
+ | @reduxjs/toolkit | ^2.11.2 | State management | zustand, jotai |
21
+ | react-redux | ^9.2.0 | Redux React bindings | - |
22
+ | axios | ^1.13.2 | HTTP requests | fetch |
23
+ | date-fns | ^4.1.0 | Date formatting | moment, dayjs |
24
+ | @dnd-kit/core | ^6.3.1 | Drag and drop | react-beautiful-dnd |
25
+ | motion | ^12.23.26 | Animations | react-spring |
26
+ | sonner | ^2.0.7 | Toast notifications | react-toastify |
27
+ | react-window | ^2.2.3 | List virtualization | react-virtualized |
28
+ | nanoid | ^5.1.6 | ID generation | uuid |
29
+ | express | ^5.2.1 | HTTP server | fastify, koa |
30
+ | drizzle-orm | latest | Database ORM | prisma, sequelize, typeorm |
31
+ | drizzle-kit | latest | Schema tooling | - |
32
+ | zod | ^4.2.1 | Schema validation | yup, joi |
33
+ | jose | ^6.1.3 | JWT handling | jsonwebtoken |
34
+ | microdiff | ^1.5.0 | JSON object diffing | deep-diff, just-diff |
35
+ | react18-json-view | ^0.2.9 | JSON tree view | react-json-view |
36
+ | @tiptap/react | ^3.11.1 | Rich text editor | slate, quill, draft-js |
37
+ | shiki | ^3.22.0 | Syntax highlighting | prism, highlight.js |
38
+ | lucide-react | ^0.562.0 | Icons | react-icons, heroicons |
39
+ | @pdfme/generator | ^5.5.8 | PDF generation | jspdf, pdfkit |
40
+ | cmdk | ^1.1.1 | Command palette | kbar, ninja-keys |
41
+ | @monaco-editor/react | ^4.7.0 | Code editor | codemirror, ace |
42
+ | localforage | ^1.10.0 | Client-side storage | idb, dexie |
43
+ | react-markdown | ^10.1.0 | Markdown rendering | marked, markdown-it |
44
+ | fast-deep-equal | ^3.1.3 | Deep equality checks | lodash.isEqual |
45
+ | @ngneat/falso | latest | Fake data generation | @faker-js/faker, chance |
46
+ | pino | ^10.1.0 | Logging | winston |
47
+
48
+ ## Frontend
49
+
50
+ Tailwind v4 + shadcn-based UI; conventions below.
51
+
52
+ - **Build every UI through the `frontend-design` skill** — route the brief through it instead of hand-rolling layouts; it owns the archetype, personality tokens, and `@nurix/components` wiring.
53
+ - **Light mode is the default** — design for the light theme as the default; dark mode is opt-in, never the default.
54
+ - **@nurix/\* are restricted-scope npm packages** — installed from the npm registry, auto-authed by a read token in `~/.npmrc`. Just attempt the install; the token authenticates you automatically. Don't pre-empt or second-guess it. If the install actually fails, flag it to the user — never reach for alternatives, a substitute package, or `npm login`.
55
+ - **@nurix/components** — the UI component library (restricted-scope npm package, pre-built); import by subpath, e.g. `@nurix/components/button`.
56
+ - No components.json / shadcn CLI scaffold (.tsx, not .jsx) — consume the prebuilt library; never `npx shadcn add`.
57
+ - Vite: `@tailwindcss/vite` + `@vitejs/plugin-react-swc`. Pin react / react-dom / react/jsx-runtime / react/jsx-dev-runtime to the app's single copy via `resolve.alias` (@nurix/\* ship nested react — keep one hook dispatcher).
58
+ - Tailwind v4, CSS-only (no tailwind.config.js, no PostCSS): styling runs through the `@tailwindcss/vite` plugin.
59
+ - CSS entry `globals.css`, imports in order — `@import "./personality.css"` (token source) → `@import "tailwindcss"` → the library's `styles.css` — then `@theme inline {…}` mapping vars to tokens.
60
+ - Tokens: CSS vars in `:root, :root[data-theme="light"]` + the archetype selector `[data-style="…"]` (personality.css), consumed as semantic Tailwind tokens (bg-card, text-foreground). Honor the archetype's radius/theme decisions (e.g. re-assert `--radius-*` to 0 for a flat archetype; add `@custom-variant dark` only if the project opts into dark mode, and keep light as the default). The library bundles class-variance-authority, clsx, tailwind-merge.
61
+ - Plus general styling rules (spacing in 2/4/6/8, object-cover, truncate, motion, color tokens — never hardcoded hex in components).
62
+
63
+ ## Express 5.x
64
+
65
+ ```javascript
66
+ app.use(cors({ origin: process.env.FRONTEND_URL }));
67
+ app.use(compression());
68
+ app.use(express.json({ limit: '10mb' }));
69
+ app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
70
+ ```
71
+
72
+ - Middleware order: body parsers → custom middleware → routes → error handlers (centralized, last). Auth via middleware. Use Express Router for modular routes.
73
+
74
+ ## API Response Format
75
+
76
+ ```json
77
+ {
78
+ "status": 1,
79
+ "message": "Message sent successfully",
80
+ "data": { "delivered": [{ "user@example.com": 2 }] }
81
+ }
82
+ ```
83
+
84
+ Error: `{ "status": 0, "message": "Error description", "data": null }`
85
+
86
+ HTTP codes: `200`, `201` (created), `400`, `401` (not authenticated), `403` (not authorized), `404`, `500`
@@ -0,0 +1,13 @@
1
+ # kb.md — Patterns for Obsidian-like Folders
2
+
3
+ Conventions for any folder that is majority markdown — `docs/`, `research/`, an Obsidian vault, or any knowledge base. Rules grow over time; append new ones below.
4
+
5
+ ## Rules
6
+
7
+ ### 1. Every topic gets a `Readme.md`
8
+
9
+ When you start documenting a topic, create `Readme.md` as the folder's landing page. Don't use `index.md` or unscoped filenames — `Readme.md` is what readers and tools (GitHub, Obsidian) look for first, and it makes the folder self-introducing.
10
+
11
+ ### 2. Always link documents to each other
12
+
13
+ When one document references an idea, concept, or topic covered in another, link to it. Wikilinks (`[[other-doc]]`) inside Obsidian-native vaults; relative markdown links (`[label](../other-doc.md)`) elsewhere. If you mention it, you link it — that's how the folder becomes a knowledge graph instead of a pile of files.
@@ -0,0 +1,41 @@
1
+ # Nomenclature Guidelines
2
+
3
+ ## Naming Guidelines
4
+
5
+ - Use descriptive names that reveal intent, purpose, and usage
6
+ - Names should explain why something exists and how it's used
7
+ - Favor clarity over brevity
8
+ - Maintain consistency within the codebase
9
+ - Avoid abbreviations unless they're universally understood
10
+
11
+ ## Naming Styles
12
+
13
+ - Variables : Use nouns or noun phrases. Examples: `userData`, `firstName`, `paymentStatus`, `configOptions`
14
+ - Functions & Methods : Use verbs or verb phrases that describe the action. Examples: `calculateTotal`, `getUserInfo`, `validateInput`, `sendNotification`
15
+ - Boolean Variables : Prefix with is**, has**, can**, or should**. Examples: `isValid`, `hasPermission`, `canEdit`, `shouldRefresh`
16
+ - Arrays : Use plural nouns. Examples: `users`, `productList`, `errorMessages`, `activeConnections`
17
+ - Classes & Components : Use singular nouns or noun phrases. Examples: `RequestHandler`, `UserProfile`, `PaymentProcessor`, `DataValidator`
18
+ - Constants : Use uppercase with meaningful names. Examples: `DEFAULT_TIMEOUT`, `API_BASE_URL`, `MAX_RETRY_COUNT`, `COLOR_PRIMARY`
19
+ - Event Handlers : Use Prefix with handle or on. Examples: `handleSubmit`, `onUserLogin`, `handleInputChange`, `onPageLoad`
20
+ - Interfaces & Types : Use Consider prefixing with I for interfaces (if preferred). Examples: `IUserData`, `IConfigOptions`, `AuthResponse`, `RequestParams`
21
+ - Promises & Async Functions : Use Consider using future tense or suffixes like Async. Examples: `fetchUserData`, `getUserProfileAsync`, `loadConfigAsync`, `processPayment`
22
+ - File & Folder Structure : Use lowercase with descriptive names. Group by feature or function type. Example: `auth/login_service.js`
23
+
24
+ ## Casing Styles
25
+
26
+ - camelCase : Used in Variables, function names, method names, property names. Examples: `userData`, `calculateTotal`, `firstName`, `isValidEmail`
27
+ - PascalCase : Used in Class names, component names, type definitions. Examples: `UserProfile`, `DataService`, `AuthProvider`, `RequestHandler`
28
+ - snake_case : Used in File names, folder names. Examples: `user_service`, `api_utils`, `auth_middleware`, `data_models`
29
+ - UPPER_SNAKE_CASE : Used in Constants, environment variables. Examples: `MAX_RETRY_COUNT`, `API_KEY`, `DEFAULT_TIMEOUT`, `COLOR_PRIMARY`
30
+ - kebab-case : Used in URLs, CSS class names, HTML IDs. Examples: `nav-container`, `user-profile`, `button-primary`, `form-wrapper`
31
+
32
+ ## Database
33
+
34
+ Naming for D1 tables, columns, indexes, and their Drizzle mirrors. (For column **types** — PK shape, timestamp columns — see the Cloudflare D1 Schema rule, not here; this section governs names only.)
35
+
36
+ - **Tables** : snake_case. Entity / hub tables use the plural noun for the thing they hold. Examples: `users`, `apps`, `devices`, `services`.
37
+ - **Junction (M:N) tables** : name a many-to-many join `<entityA>_<entityB>_map` — **always**, even when the join carries its own columns (a `role`, a secret, a status). The `_map` suffix is the signal that a row is a relationship, not an entity; extra attributes on the join do **not** earn it a standalone noun. Use the **singular** stem of each side, owning/anchor entity first (mirror the existing tables — `app_*`). Examples: `app_user_map` (apps ⨯ users), `app_device_map` (apps ⨯ devices), `app_service_map` (apps ⨯ services).
38
+ - ❌ Don't invent a "nice" noun for a junction — no `members`, `installs`, `memberships`, `enrolments`. A reviewer should read the table name and know both sides and that it's a join.
39
+ - **Columns** : snake_case. Foreign keys are `<entity>_id` (`user_id`, `app_id`, `device_id`, `service_id`). Booleans follow the boolean rule (`is_*`/`has_*`); timestamps are `created_at` / `updated_at`.
40
+ - **Indexes** : `<table>_<col(s)>_idx`; unique indexes `<table>_<col(s)>_unique`. The composite unique on a junction reads `<table>_<colA>_<colB>_unique` — e.g. `app_user_map_app_user_unique`, `app_device_map_device_app_unique`.
41
+ - **Drizzle mirror** : the exported table object is the camelCase of the snake_case table name (`app_user_map` → `appUserMap`); column TS properties are the camelCase of the column (`created_at` → `createdAt`, `service_key` → `serviceKey`). The snake_case string passed to `sqliteTable("…")` and every index name must match `seed/init.sql` exactly (the G1 schema-parity boundary).
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: boundary
3
+ description: Boundary diagnostic for implementation plans. Given a plan file, reads the source files the plan touches, identifies the application boundaries they participate in — the cross-cutting contracts (db schema, wire envelope, auth, events, env/config, …) where editing one side silently breaks the other — and reports whether the planned changes would violate any of them, severity-ranked, with suggested plan amendments. Read-only; the report lands in chat. Can also run against a diff or named code area instead of a plan. Triggers on "boundary check", "check this plan", "what could break", "impact analysis", "/boundary".
4
+ ---
5
+
6
+ # Boundary Diagnostic
7
+
8
+ You diagnose whether a plan's changes would break an application boundary.
9
+
10
+ A boundary is a contract between two pieces of code (or code and infra) that share an assumption with **no compile-time link** between them: the column a raw SQL string names, the JSON envelope a client unwraps, the event string a dispatcher parses, the env var a deploy script writes. Changing one side ships green and breaks the other silently. Your job is to find those edges in the plan's path _before_ execution.
11
+
12
+ [`references/boundary-taxonomy.md`](references/boundary-taxonomy.md) is the catalog of boundary types (~170 across persistence, wire/API, frontend, auth/tenancy, async/messaging, config/build, runtime/infra), with per-type severities and safe-change checklists. Read it first — it tells you what to look for; the code tells you what exists.
13
+
14
+ ## Run
15
+
16
+ **Input:** a plan path (given, or the newest in `docs/history/plans/`) — or a diff / named code area for non-plan work. Everything below is identical either way; a diff's hunks stand in for the plan's steps.
17
+
18
+ 1. **Build the touch list.** Parse the plan's Proposed Changes: files to be created or modified, entities/state added or changed, API changes, anything the author flags as a cross-module contract. Note missing subsections.
19
+
20
+ 2. **Identify the boundaries in the touched code.** Read each touched file (and its immediate imports). Against the taxonomy, list the contracts this code participates in — the schema it queries, the envelope it emits, the events it publishes or parses, the auth checks it sits behind, the env it reads.
21
+
22
+ 3. **Trace the other side.** A boundary has two sides; the danger lives on the one the plan _doesn't_ mention. For each contract, grep the shared name or shape across the repo — the column name in other SQL strings, the response field in client unwrappers, the event kind in subscribers, the route in fetch calls and scripts, the env key in deploy tooling. Record consumers as `file:line`.
23
+
24
+ 4. **Judge each intersection.** Does the planned change alter a shape a traced consumer depends on, break an invariant the code establishes (append-only vocabularies, idempotency keys, fail-closed checks, ordering guarantees), or add a new cross-module edge? Severity:
25
+ - **CRITICAL** — can corrupt persisted data, or open an auth/money path.
26
+ - **HIGH** — silent cross-module breakage that would ship green.
27
+ - **MEDIUM** — drift-prone coupling (string-matched, duplicated constants).
28
+ - **LOW** — smell. Loud, immediately-visible breakage caps at HIGH.
29
+
30
+ 5. **Report in chat:**
31
+
32
+ ```
33
+ # Boundary check — {plan}
34
+
35
+ Touched files read: {N} · Boundaries identified: {N}
36
+ Violations: {N} ({critical}/{high}/{medium}/{low})
37
+
38
+ | # | Boundary | Planned change | Other side (file:line) | Severity |
39
+
40
+ ## Suggested plan amendments
41
+ ## Missing plan subsections
42
+ ```
43
+
44
+ ## Rules
45
+
46
+ - **Read-only.** You never edit the plan, the code, or any doc. The user folds your amendments into the plan.
47
+ - **Verify in source.** Never flag from plan text alone — read both sides of every boundary you report, and cite both as `file:line`.
48
+ - **No consumer, no finding.** An intersection with no traced other side isn't a violation; at most note it as a new edge the plan introduces.
49
+ - **A clean report is valid.** Zero violations means say so — don't pad.