agent-harness-kit 0.8.0 → 0.10.0

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 (62) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +11 -1
  4. package/bin/cli.mjs +21 -0
  5. package/package.json +1 -1
  6. package/src/core/doctor.mjs +24 -0
  7. package/src/core/render-templates.mjs +29 -0
  8. package/src/core/upgrade.mjs +81 -60
  9. package/src/templates/.claude/agents/api-consistency-reviewer.md.vi +37 -0
  10. package/src/templates/.claude/agents/architecture-reviewer.md.vi.hbs +45 -0
  11. package/src/templates/.claude/agents/performance-reviewer.md.vi +39 -0
  12. package/src/templates/.claude/agents/reliability-reviewer.md.vi +42 -0
  13. package/src/templates/.claude/agents/security-reviewer.md.vi +43 -0
  14. package/src/templates/.claude/hooks/hooks.json +22 -0
  15. package/src/templates/.claude/output-styles/harness-terse.md +42 -0
  16. package/src/templates/.claude/settings.json.hbs +1 -0
  17. package/src/templates/.claude/skills/add-adr/SKILL.md.vi +64 -0
  18. package/src/templates/.claude/skills/add-feature/SKILL.md.vi.hbs +50 -0
  19. package/src/templates/.claude/skills/debug-flow/SKILL.md.vi.hbs +42 -0
  20. package/src/templates/.claude/skills/deliver-html/SKILL.md.hbs +96 -0
  21. package/src/templates/.claude/skills/deliver-html/SKILL.md.vi.hbs +89 -0
  22. package/src/templates/.claude/skills/deliver-html/assets/report.css +233 -0
  23. package/src/templates/.claude/skills/deliver-html/scripts/wrap-html.mjs +0 -0
  24. package/src/templates/.claude/skills/deliver-html/templates/audit-report.html.tmpl +29 -0
  25. package/src/templates/.claude/skills/deliver-html/templates/decision-doc.html.tmpl +29 -0
  26. package/src/templates/.claude/skills/deliver-html/templates/status-report.html.tmpl +29 -0
  27. package/src/templates/.claude/skills/doc-drift-scan/SKILL.md.vi +52 -0
  28. package/src/templates/.claude/skills/eval-runner/SKILL.md.vi +59 -0
  29. package/src/templates/.claude/skills/garbage-collection/SKILL.md.vi.hbs +58 -0
  30. package/src/templates/.claude/skills/i18n-add-locale/SKILL.md +52 -0
  31. package/src/templates/.claude/skills/i18n-add-locale/SKILL.md.vi +56 -0
  32. package/src/templates/.claude/skills/i18n-add-locale/scripts/locale-scaffold.mjs +120 -0
  33. package/src/templates/.claude/skills/inspect-app/SKILL.md.vi +61 -0
  34. package/src/templates/.claude/skills/inspect-module/SKILL.md.vi.hbs +57 -0
  35. package/src/templates/.claude/skills/map-domain/SKILL.md +42 -0
  36. package/src/templates/.claude/skills/map-domain/SKILL.md.vi +42 -0
  37. package/src/templates/.claude/skills/map-domain/scripts/domain-map.mjs +145 -0
  38. package/src/templates/.claude/skills/propose-harness-improvement/SKILL.md.vi +49 -0
  39. package/src/templates/.claude/skills/propose-harness-improvement/scripts/improvement-bundle.mjs +172 -0
  40. package/src/templates/.claude/skills/refactor-feature/SKILL.md +60 -0
  41. package/src/templates/.claude/skills/refactor-feature/SKILL.md.vi +64 -0
  42. package/src/templates/.claude/skills/refactor-feature/scripts/feature-diff.mjs +146 -0
  43. package/src/templates/.claude/skills/review-this-pr/SKILL.md +59 -0
  44. package/src/templates/.claude/skills/review-this-pr/SKILL.md.vi +63 -0
  45. package/src/templates/.claude/skills/review-this-pr/scripts/pr-review-driver.mjs +152 -0
  46. package/src/templates/.claude/skills/structural-test-author/SKILL.md.vi.hbs +50 -0
  47. package/src/templates/.claude/skills/write-skill/SKILL.md.vi +43 -0
  48. package/src/templates/.harness/eval/rubrics/feature-step-done.mjs +148 -0
  49. package/src/templates/.harness/eval/tasks/feature-step-done.answer.md +53 -0
  50. package/src/templates/.harness/eval/tasks/feature-step-done.json +10 -0
  51. package/src/templates/.harness/eval/tasks/feature-step-done.prompt.md +43 -0
  52. package/src/templates/.mcp.json.example +35 -0
  53. package/src/templates/CLAUDE.md.hbs +1 -0
  54. package/src/templates/CLAUDE.md.vi.hbs +1 -0
  55. package/src/templates/docs/adr/0002-html-first-for-humans.md.hbs +116 -0
  56. package/src/templates/docs/golden-principles.md.hbs +32 -0
  57. package/src/templates/scripts/precompletion-checklist.sh.hbs +43 -0
  58. package/src/templates/scripts/pretooluse-edit-guard.sh.hbs +115 -0
  59. package/src/templates/scripts/session-end.sh.hbs +6 -0
  60. package/src/templates/scripts/session-rollup.mjs +96 -0
  61. package/src/templates/scripts/session-start.sh.hbs +25 -0
  62. package/src/templates/scripts/subagent-stop.sh.hbs +76 -0
@@ -11,9 +11,9 @@
11
11
  "source": {
12
12
  "source": "github",
13
13
  "repo": "tuanle96/agent-harness-kit",
14
- "ref": "v0.8.0"
14
+ "ref": "v0.10.0"
15
15
  },
16
- "version": "0.8.0",
16
+ "version": "0.10.0",
17
17
  "description": "Solo-dev harness engineering kit — layered architecture, GC ritual, structural tests, review subagents.",
18
18
  "category": "development",
19
19
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-harness-kit",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Solo-dev harness engineering kit — layered architecture, garbage-collection ritual, structural tests, review subagents. Optimized for Claude Code 2.1+.",
5
5
  "author": {
6
6
  "name": "Tuan Le"
package/README.md CHANGED
@@ -59,8 +59,9 @@ Option B: install as a Claude Code plugin
59
59
  | `/add-adr` | Add a numbered Architecture Decision Record |
60
60
  | `/doc-drift-scan` | Find stale path/command references in `docs/` |
61
61
  | `/debug-flow` | Run the failing flow before fixing it |
62
+ | `/deliver-html` | Ship an analysis/audit/plan as a self-contained HTML |
62
63
 
63
- ## Philosophy (4 axioms)
64
+ ## Philosophy (5 axioms)
64
65
 
65
66
  1. **CLAUDE.md is a table of contents, not an encyclopedia** (HumanLayer
66
67
  measured ~150–200 instructions as the reliable cap; OpenAI's own root file
@@ -76,6 +77,15 @@ Option B: install as a Claude Code plugin
76
77
  differentiator — see [Honest expectations](#honest-expectations).
77
78
  4. **Garbage collection over Friday cleanup, scaled to solo** (OpenAI's
78
79
  ritual, shrunk to top-3 fixes per week).
80
+ 5. **HTML for human deliverables, Markdown for agent files.** Markdown is
81
+ the right format for files an agent reads-and-edits (CLAUDE.md, SKILL.md,
82
+ ADRs); HTML is the right format for documents a HUMAN reads-and-decides
83
+ (audit reports, analyses, plans, decision docs). A long Markdown
84
+ deliverable invites the human to scroll, miss the conclusion, and ask the
85
+ agent to clarify — burning more tokens than the HTML markup costs. The
86
+ `/deliver-html` skill writes self-contained HTML at repo root with a
87
+ shared dark-theme CSS; the rule is documented in golden principle #11 and
88
+ ADR-0002.
79
89
 
80
90
  ## Directory the kit drops into your repo
81
91
 
package/bin/cli.mjs CHANGED
@@ -52,6 +52,11 @@ program
52
52
  "--model <id>",
53
53
  "Claude model to pin in .claude/settings.json (e.g. claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5)",
54
54
  )
55
+ .option(
56
+ "--with-mcp",
57
+ "copy .mcp.json.example to .mcp.json (enables Playwright + GitHub MCP servers — credentials still required)",
58
+ false,
59
+ )
55
60
  .action(async (opts) => {
56
61
  const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
57
62
  console.log(pc.bold(pc.cyan(`\nagent-harness-kit v${pkg.version}\n`)));
@@ -203,6 +208,22 @@ program
203
208
  model: opts.model,
204
209
  });
205
210
 
211
+ // --with-mcp: promote the shipped .mcp.json.example to .mcp.json so
212
+ // the user gets a working starting point. Idempotent — if .mcp.json
213
+ // already exists we leave it alone (user owns it after first write).
214
+ if (opts.withMcp) {
215
+ const examplePath = resolve(cwd, ".mcp.json.example");
216
+ const mcpPath = resolve(cwd, ".mcp.json");
217
+ const { existsSync: fsExists } = await import("node:fs");
218
+ const { copyFile } = await import("node:fs/promises");
219
+ if (fsExists(examplePath) && !fsExists(mcpPath)) {
220
+ await copyFile(examplePath, mcpPath);
221
+ result.written.push(".mcp.json (from --with-mcp)");
222
+ } else if (fsExists(mcpPath)) {
223
+ console.log(pc.yellow(` ~ .mcp.json already present — left untouched (--with-mcp skipped overwrite)`));
224
+ }
225
+ }
226
+
206
227
  console.log("");
207
228
  for (const f of result.written) {
208
229
  console.log(` ${pc.green("✓")} ${pc.dim("wrote")} ${f}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-harness-kit",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Solo-dev harness engineering kit for Claude Code. Layered architecture, structural tests, garbage-collection ritual, review subagents — without the enterprise overhead.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,6 +100,30 @@ export async function doctor({ cwd, kitVersion }) {
100
100
  }
101
101
  }
102
102
 
103
+ // 4b. MCP probe — warn (don't fail) when .mcp.json is missing but the
104
+ // example file is shipped. Recommended skills (review-this-pr, garbage-
105
+ // collection) work better with Playwright + GitHub MCP servers wired up;
106
+ // running without is fine, just degrades gracefully.
107
+ const mcpPath = resolve(cwd, ".mcp.json");
108
+ const mcpExamplePath = resolve(cwd, ".mcp.json.example");
109
+ const hasMcp = existsSync(mcpPath);
110
+ const hasMcpExample = existsSync(mcpExamplePath);
111
+ if (hasMcp) {
112
+ try {
113
+ const m = JSON.parse(await readFile(mcpPath, "utf8"));
114
+ const count = m.mcpServers ? Object.keys(m.mcpServers).length : 0;
115
+ check(`.mcp.json (${count} server${count === 1 ? "" : "s"})`, count > 0,
116
+ count > 0 ? "" : "no mcpServers configured");
117
+ } catch (e) {
118
+ allOk = false;
119
+ console.log(pc.red(` ✗ .mcp.json is not valid JSON: ${e.message}`));
120
+ }
121
+ } else if (hasMcpExample) {
122
+ console.log(
123
+ ` ${pc.yellow("•")} .mcp.json ${pc.dim("— not enabled (copy .mcp.json.example to .mcp.json to wire up Playwright/GitHub MCP)")}`,
124
+ );
125
+ }
126
+
103
127
  // 5. Model pin in .claude/settings.json (B4). Catches obvious typos
104
128
  // that would silently no-op in Claude Code.
105
129
  const settingsPath = resolve(cwd, ".claude/settings.json");
@@ -48,6 +48,10 @@ export const EXEC_BITS = new Set([
48
48
  "scripts/pretooluse-bash-guard.sh",
49
49
  "scripts/pre-compact.sh",
50
50
  "scripts/session-end.sh",
51
+ // v0.9 hook expansion — SubagentStop + PreToolUse(Edit|Write|MultiEdit) + rollup side-car.
52
+ "scripts/subagent-stop.sh",
53
+ "scripts/pretooluse-edit-guard.sh",
54
+ "scripts/session-rollup.mjs",
51
55
  ]);
52
56
 
53
57
  export function registerHelpers() {
@@ -81,6 +85,11 @@ export function registerHelpers() {
81
85
 
82
86
  async function* walk(dir) {
83
87
  const entries = await readdir(dir, { withFileTypes: true });
88
+ // Sort lexically so locale variants (e.g. "SKILL.md.vi.hbs") sort AFTER
89
+ // their masters ("SKILL.md.hbs") and overwrite them via the
90
+ // identical-target writeFile path. Without this, OS readdir order makes
91
+ // locale overrides non-deterministic.
92
+ entries.sort((a, b) => a.name.localeCompare(b.name));
84
93
  for (const e of entries) {
85
94
  const full = join(dir, e.name);
86
95
  if (e.isDirectory()) {
@@ -210,6 +219,26 @@ export function pathForStack(rel, stack, humanLanguage = "en") {
210
219
  if (fileLang !== humanLanguage) return null;
211
220
  return "CLAUDE.md.hbs"; // canonical target — strip locale suffix
212
221
  }
222
+ // Generic locale routing for .md.<lang>(.hbs)? under .claude/skills/* or
223
+ // .claude/agents/*. Variants sort AFTER masters in walk order, so when
224
+ // both exist the variant overwrites the master via identical writeFile
225
+ // path. Two forms (mirroring locale-scaffold.mjs):
226
+ // Master .md.hbs → variant .md.<lang>.hbs (Handlebars-active)
227
+ // Master .md → variant .md.<lang> (plain copy)
228
+ // Active-locale-only emission: variants for other locales return null.
229
+ const localeVariantHbs = rel.match(/^\.claude\/(?:skills|agents)\/.*\.md\.([a-z]{2,5})\.hbs$/);
230
+ if (localeVariantHbs) {
231
+ if (localeVariantHbs[1] !== humanLanguage) return null;
232
+ return rel.replace(/\.md\.[a-z]{2,5}\.hbs$/, ".md.hbs");
233
+ }
234
+ // "hbs" is excluded because *.md.hbs is the master (Handlebars-active),
235
+ // not a locale variant. Without this guard, "SKILL.md.hbs" would match
236
+ // with locale="hbs" and get nulled out under the en routing path.
237
+ const localeVariantPlain = rel.match(/^\.claude\/(?:skills|agents)\/.*\.md\.([a-z]{2,5})$/);
238
+ if (localeVariantPlain && localeVariantPlain[1] !== "hbs") {
239
+ if (localeVariantPlain[1] !== humanLanguage) return null;
240
+ return rel.replace(/\.md\.[a-z]{2,5}$/, ".md");
241
+ }
213
242
  if (rel.startsWith("_adapter-typescript/")) {
214
243
  const stripped = rel.slice("_adapter-typescript/".length);
215
244
  if (stack.language === "typescript") return stripped;
@@ -7,7 +7,7 @@
7
7
  // 3. Never touch USER_OWNED_FILES (CLAUDE.md, docs/architecture.md, etc.).
8
8
  // 4. Print a concise summary and update the lockfile.
9
9
 
10
- import { readFile, writeFile, mkdir, readdir, stat } from "node:fs/promises";
10
+ import { readFile, writeFile, mkdir, readdir, chmod } from "node:fs/promises";
11
11
  import { existsSync } from "node:fs";
12
12
  import { resolve, join, relative, dirname } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
@@ -15,7 +15,15 @@ import { createHash } from "node:crypto";
15
15
  import { confirm } from "@inquirer/prompts";
16
16
  import pc from "picocolors";
17
17
  import Handlebars from "handlebars";
18
- import { registerHelpers } from "./render-templates.mjs";
18
+ import {
19
+ registerHelpers,
20
+ pathForStack,
21
+ buildContext,
22
+ mergeHooksIntoSettings,
23
+ USER_OWNED_FILES as USER_OWNED_FROM_RENDERER,
24
+ EXEC_BITS,
25
+ SUPPORTED_HUMAN_LANGS,
26
+ } from "./render-templates.mjs";
19
27
  import { detectStack } from "./detect-stack.mjs";
20
28
 
21
29
  // Sync the two version-pinned fields in harness.config.json after a kit
@@ -101,16 +109,29 @@ export async function ensureWritePermissions(cwd) {
101
109
  const __dirname = dirname(fileURLToPath(import.meta.url));
102
110
  const TEMPLATES_ROOT = resolve(__dirname, "..", "templates");
103
111
 
104
- const USER_OWNED_FILES = new Set([
105
- "CLAUDE.md",
106
- "AGENTS.md",
107
- "docs/architecture.md",
108
- "docs/core-beliefs.md",
109
- "docs/golden-principles.md",
110
- "docs/tech-debt-tracker.md",
111
- "feature_list.json",
112
- "harness.config.json",
113
- ]);
112
+ // Single source of truth lives in render-templates.mjs. Re-exported here under
113
+ // the legacy local name to avoid a wider rename in this module.
114
+ const USER_OWNED_FILES = USER_OWNED_FROM_RENDERER;
115
+
116
+ // Read user preferences from the existing harness.config.json so the rendered
117
+ // templates pick up the user's chosen model + locale instead of falling back
118
+ // to template defaults that would silently overwrite them. Returns soft
119
+ // defaults when the file is missing or invalid; never throws.
120
+ async function readUserPreferences(cwd) {
121
+ const cfgPath = resolve(cwd, "harness.config.json");
122
+ if (!existsSync(cfgPath)) return { humanLanguage: "en", model: undefined };
123
+ try {
124
+ const cfg = JSON.parse(await readFile(cfgPath, "utf8"));
125
+ const humanLanguage = cfg?.claudeMd?.humanLanguage || "en";
126
+ const model = cfg?.models?.main;
127
+ return {
128
+ humanLanguage: SUPPORTED_HUMAN_LANGS.has(humanLanguage) ? humanLanguage : "en",
129
+ model,
130
+ };
131
+ } catch {
132
+ return { humanLanguage: "en", model: undefined };
133
+ }
134
+ }
114
135
 
115
136
  function sha256(buf) {
116
137
  return createHash("sha256").update(buf).digest("hex");
@@ -171,38 +192,33 @@ export async function upgrade({ cwd, kitVersion, yes }) {
171
192
  );
172
193
 
173
194
  const stack = await detectStack(cwd);
174
- const ctx = {
195
+
196
+ // Pick up user-chosen model + locale from harness.config.json so the
197
+ // rendered templates carry them forward instead of falling back to
198
+ // template defaults. Anything missing falls back to safe defaults inside
199
+ // buildContext.
200
+ const { humanLanguage, model } = await readUserPreferences(cwd);
201
+ registerHelpers();
202
+ const ctx = buildContext({
175
203
  projectName: "your-project",
204
+ preset: "generic",
176
205
  layers: ["types", "config", "repo", "service", "runtime", "ui"],
177
- layersJoined: "types → config → repo → service → runtime → ui",
178
- language: stack.language,
179
- framework: stack.framework,
180
- packageManager: stack.packageManager,
181
- isTypescript: stack.language === "typescript",
182
- isPython: stack.language === "python",
183
- isNextjs: stack.framework === "nextjs",
184
- isFastapi: stack.framework === "fastapi",
206
+ stack,
185
207
  kitVersion,
186
- };
208
+ humanLanguage,
209
+ model,
210
+ });
187
211
 
188
- const updates = []; // { rel, action: 'overwrite'|'sidecar'|'skip', reason }
212
+ // { rel, action, reason, content } — rendered content is captured here so
213
+ // the apply step doesn't have to re-walk the template tree (which was the
214
+ // source of the v0.7 bug where rust/go/swift/kotlin adapter prefixes leaked
215
+ // straight into the user's project as literal directory names).
216
+ const updates = [];
189
217
 
190
218
  for await (const abs of walk(TEMPLATES_ROOT)) {
191
219
  const relFromTemplates = relative(TEMPLATES_ROOT, abs).split("\\").join("/");
192
- if (relFromTemplates.startsWith("_adapter-typescript/") && stack.language !== "typescript")
193
- continue;
194
- if (relFromTemplates.startsWith("_adapter-python/") && stack.language !== "python")
195
- continue;
196
- if (relFromTemplates.startsWith("_preset-nextjs/") && stack.framework !== "nextjs")
197
- continue;
198
- if (relFromTemplates.startsWith("_preset-fastapi/") && stack.framework !== "fastapi")
199
- continue;
200
- const stackRel = relFromTemplates
201
- .replace(/^_adapter-typescript\//, "")
202
- .replace(/^_adapter-python\//, "")
203
- .replace(/^_preset-nextjs\//, "")
204
- .replace(/^_preset-fastapi\//, "")
205
- .replace(/^_ci\//, "");
220
+ const stackRel = pathForStack(relFromTemplates, stack, humanLanguage);
221
+ if (stackRel === null) continue;
206
222
  const targetRel = stackRel.endsWith(".hbs")
207
223
  ? stackRel.slice(0, -".hbs".length)
208
224
  : stackRel;
@@ -215,7 +231,6 @@ export async function upgrade({ cwd, kitVersion, yes }) {
215
231
  let newContent;
216
232
  if (abs.endsWith(".hbs")) {
217
233
  const raw = await readFile(abs, "utf8");
218
- registerHelpers();
219
234
  const tpl = Handlebars.compile(raw, { noEscape: true });
220
235
  newContent = tpl(ctx);
221
236
  } else {
@@ -230,7 +245,7 @@ export async function upgrade({ cwd, kitVersion, yes }) {
230
245
  const targetExists = existsSync(targetAbs);
231
246
 
232
247
  if (!targetExists) {
233
- updates.push({ rel: targetRel, action: "overwrite", reason: "new" });
248
+ updates.push({ rel: targetRel, action: "overwrite", reason: "new", content: newContent });
234
249
  continue;
235
250
  }
236
251
  const currentBuf = await readFile(targetAbs);
@@ -241,9 +256,9 @@ export async function upgrade({ cwd, kitVersion, yes }) {
241
256
  continue;
242
257
  }
243
258
  if (currentSha === previousSha) {
244
- updates.push({ rel: targetRel, action: "overwrite", reason: "user-untouched" });
259
+ updates.push({ rel: targetRel, action: "overwrite", reason: "user-untouched", content: newContent });
245
260
  } else {
246
- updates.push({ rel: targetRel, action: "sidecar", reason: "user-modified" });
261
+ updates.push({ rel: targetRel, action: "sidecar", reason: "user-modified", content: newContent });
247
262
  }
248
263
  }
249
264
 
@@ -266,29 +281,35 @@ export async function upgrade({ cwd, kitVersion, yes }) {
266
281
  }
267
282
  }
268
283
 
269
- // Apply.
284
+ // Apply — content was rendered in the first pass; just write it out.
270
285
  for (const u of [...overwrites, ...sidecars]) {
271
- const sourceTplRel = u.rel; // simplified: regenerate
272
- let abs = resolve(TEMPLATES_ROOT, sourceTplRel + ".hbs");
273
- if (!existsSync(abs)) abs = resolve(TEMPLATES_ROOT, sourceTplRel);
274
- if (stack.language === "typescript" && !existsSync(abs))
275
- abs = resolve(TEMPLATES_ROOT, "_adapter-typescript", sourceTplRel);
276
- if (stack.language === "python" && !existsSync(abs))
277
- abs = resolve(TEMPLATES_ROOT, "_adapter-python", sourceTplRel);
278
- if (!existsSync(abs)) continue; // skip — the kit may have removed this file
279
- let content;
280
- if (abs.endsWith(".hbs")) {
281
- const raw = await readFile(abs, "utf8");
282
- const tpl = Handlebars.compile(raw, { noEscape: true });
283
- content = tpl(ctx);
284
- } else {
285
- content = await readFile(abs);
286
- }
287
286
  const targetAbs = resolve(cwd, u.action === "sidecar" ? u.rel + ".harness-new" : u.rel);
288
287
  await mkdir(dirname(targetAbs), { recursive: true });
289
- await writeFile(targetAbs, content);
288
+ await writeFile(targetAbs, u.content);
289
+ if (EXEC_BITS.has(u.rel) && u.action === "overwrite") {
290
+ try {
291
+ await chmod(targetAbs, 0o755);
292
+ } catch {
293
+ // ignore on platforms where chmod is a no-op
294
+ }
295
+ }
290
296
  if (u.action === "overwrite") {
291
- lockfile.files[u.rel] = sha256(typeof content === "string" ? Buffer.from(content) : content);
297
+ lockfile.files[u.rel] = sha256(
298
+ typeof u.content === "string" ? Buffer.from(u.content) : u.content,
299
+ );
300
+ }
301
+ }
302
+
303
+ // Critical fix (v0.7 + idempotent upgrade): merge .claude/hooks/hooks.json
304
+ // into .claude/settings.json#hooks. Claude Code ONLY reads hooks from
305
+ // settings.json — a stand-alone hooks.json is silently ignored. renderAll
306
+ // does this on init; upgrade has to redo it because hooks.json may have
307
+ // changed across versions (and pre-v0.7 installs never had it merged).
308
+ if (existsSync(resolve(cwd, ".claude/hooks/hooks.json"))) {
309
+ const merged = await mergeHooksIntoSettings(cwd);
310
+ if (merged.changed) {
311
+ lockfile.files[".claude/settings.json"] = sha256(merged.rawContent);
312
+ console.log(pc.dim(` ${pc.green("~")} .claude/settings.json (hooks merged)`));
292
313
  }
293
314
  }
294
315
 
@@ -0,0 +1,37 @@
1
+ <!-- LOCALE_TODO: translate body to vi -->
2
+ <!-- Source: .claude/agents/api-consistency-reviewer.md -->
3
+ <!-- Edit only the markdown body — keep frontmatter verbatim so the kit's renderer + Claude Code parse it identically across locales. -->
4
+
5
+ ---
6
+ name: api-consistency-reviewer
7
+ description: Use this agent after adding or modifying any public API endpoint, exported function, CLI command, or RPC handler. Verifies naming, response shape, error format, and versioning conventions match `docs/api-conventions.md` (or the kit's defaults if that file doesn't exist). Read-only.
8
+ tools: Read, Grep, Glob, Bash(git diff:*)
9
+ model: haiku
10
+ ---
11
+
12
+ Compare changed public surfaces against `docs/api-conventions.md` (if absent,
13
+ fall back to: response shape `{ data, error }`, camelCase keys for JS/TS,
14
+ snake_case for Python). Flag:
15
+
16
+ - response-shape drift (e.g. `{ success, data, error }` vs `{ ok, result }`)
17
+ - naming convention violations (camelCase vs snake_case mixing within one
18
+ payload)
19
+ - missing versioning on breaking changes (no `/v2/` prefix, no `deprecated`
20
+ flag)
21
+ - exported symbols without JSDoc / docstring on a NEW public function
22
+ - error response shape that doesn't match existing handlers
23
+
24
+ ## Output format
25
+
26
+ ```
27
+ PASS — public surfaces are consistent
28
+ ```
29
+
30
+ or a numbered fix list:
31
+
32
+ ```
33
+ 1. <path>:<line> — <convention violated> — <fix>
34
+ 2. ...
35
+ ```
36
+
37
+ Do not modify files. Be terse.
@@ -0,0 +1,45 @@
1
+ <!-- LOCALE_TODO: translate body to vi -->
2
+ <!-- Source: .claude/agents/architecture-reviewer.md.hbs -->
3
+ <!-- Edit only the markdown body — keep frontmatter verbatim so the kit's renderer + Claude Code parse it identically across locales. -->
4
+
5
+ ---
6
+ name: architecture-reviewer
7
+ description: Use this agent when the Stop hook surfaces a `multi-layer-review` flag (changes span ≥2 layers in a single domain — mechanical count, not self-judgment), or when a change adds a new domain / modifies imports across module boundaries. Verifies the {{layersJoined}} rule, provider boundaries, and golden-principles.md compliance. Read-only — never modifies files.
8
+ tools: Read, Grep, Glob, Bash({{#if isPython}}python -m harness.structural_test{{else}}npm run harness:check{{/if}}), Bash(git diff:*)
9
+ model: sonnet
10
+ ---
11
+
12
+ You are a senior software architect reviewing a single PR's diff for
13
+ layered-architecture compliance. You are the **inferential sensor** that
14
+ complements the **computational sensor** (the structural test).
15
+
16
+ When invoked:
17
+
18
+ 1. Run `git diff HEAD~1` (or against the PR base) to see exactly what changed.
19
+ 2. Run {{#if isPython}}`python -m harness.structural_test`{{else}}`npm run harness:check`{{/if}} to see deterministic
20
+ violations first. If it fails, your job is to translate the failure into
21
+ a remediation plan, not duplicate it.
22
+ 3. For each changed file: identify which layer it belongs to from
23
+ `harness.config.json`. Flag any cross-layer import that goes "backward"
24
+ or skips a layer.
25
+ 4. Check that any new cross-cutting concern enters via the `providers/`
26
+ interface, not via direct import.
27
+ 5. Check that any new public type is defined in the `types/` layer, not
28
+ inline in a service.
29
+
30
+ ## Output format (always)
31
+
32
+ ```
33
+ ### Architecture review
34
+ **Verdict:** PASS | FAIL | NEEDS-DISCUSSION
35
+ **Layer-correct:** ✅ / ❌
36
+ **Provider-clean:** ✅ / ❌
37
+ **Findings:**
38
+ 1. <path:line> — <description>
39
+ 2. ...
40
+ **Remediation plan:**
41
+ - <specific edit, no rewrites>
42
+ ```
43
+
44
+ Do not modify any files. Do not run tests beyond the structural test. If
45
+ unsure, return NEEDS-DISCUSSION with concrete questions.
@@ -0,0 +1,39 @@
1
+ <!-- LOCALE_TODO: translate body to vi -->
2
+ <!-- Source: .claude/agents/performance-reviewer.md -->
3
+ <!-- Edit only the markdown body — keep frontmatter verbatim so the kit's renderer + Claude Code parse it identically across locales. -->
4
+
5
+ ---
6
+ name: performance-reviewer
7
+ description: Use this agent after adding loops over large collections, database queries, render paths, or anything in a hot path. Catches N+1 queries, missing memoization, accidental quadratic loops, and unindexed sorts. Read-only. Runs on Haiku for speed.
8
+ tools: Read, Grep, Glob
9
+ model: haiku
10
+ ---
11
+
12
+ You are a performance reviewer. Be brief — this runs on Haiku for speed.
13
+
14
+ Check for, in order:
15
+
16
+ 1. **N+1 queries.** Any `for x in xs: db.get(x.id)`-shaped pattern, or
17
+ `await Promise.all(xs.map(async x => db.findOne(...)))` against a database
18
+ with a way to batch.
19
+ 2. **O(n²) loops.** Nested iteration over the same collection without an
20
+ early break or an index.
21
+ 3. **Missing memoization** on a pure expensive function called in a render
22
+ hot path or per-request.
23
+ 4. **Synchronous IO in an async/await context** (`fs.readFileSync`,
24
+ `db.queryBlocking`).
25
+ 5. **Unbounded list growth.** `accumulator.push(...)` in a loop over an
26
+ external feed without a cap.
27
+
28
+ ## Output format
29
+
30
+ For each finding, one line:
31
+
32
+ ```
33
+ <path>:<line> — <pattern> — <suggested fix in ≤ 1 line>
34
+ ```
35
+
36
+ If clean: `PASS — no obvious hot spots`.
37
+
38
+ Be terse. Do not modify files. If a finding is speculative, mark it `(maybe)`
39
+ and explain in ≤ 5 words.
@@ -0,0 +1,42 @@
1
+ <!-- LOCALE_TODO: translate body to vi -->
2
+ <!-- Source: .claude/agents/reliability-reviewer.md -->
3
+ <!-- Edit only the markdown body — keep frontmatter verbatim so the kit's renderer + Claude Code parse it identically across locales. -->
4
+
5
+ ---
6
+ name: reliability-reviewer
7
+ description: Use this agent immediately after adding any error handling, retry loop, async boundary, timeout, or external call (HTTP/DB/queue/file). Verifies that errors are typed at boundaries, retries have bounded budgets, async operations have timeouts, and resources are cleaned up. Read-only.
8
+ tools: Read, Grep, Glob, Bash(git diff:*)
9
+ model: sonnet
10
+ ---
11
+
12
+ You are a senior reliability engineer. Focus areas, in priority order:
13
+
14
+ 1. **Boundary error handling.** Every external call (HTTP, DB, file, queue)
15
+ must have an explicit error path. No bare `except:` (Python) or empty
16
+ `catch` (TS). Errors should be typed (`Result<T,E>` or tagged union).
17
+ 2. **Retry budgets.** Every retry loop must have BOTH a max-attempts AND a
18
+ deadline. Reject infinite `while True` / `while (true)` over external
19
+ calls. Reject exponential backoff without a cap.
20
+ 3. **Timeouts.** Every `fetch` / `httpx` / `requests` / `axios` call needs an
21
+ explicit timeout. The default ones are hours-long — that's never what you
22
+ want.
23
+ 4. **Idempotency.** Write operations should be idempotent or guarded with a
24
+ key. Flag `POST` / `INSERT` without a deduplication mechanism that runs
25
+ inside a retry loop.
26
+ 5. **Resource cleanup.** Every `open()` in Python must use `with`. Every TS
27
+ file/socket/stream must have a `try/finally close` or `using` declaration
28
+ (TC39 explicit-resource-management).
29
+ 6. **Cancellation.** Long-running async work without an `AbortSignal` /
30
+ `asyncio.CancelledError` handler is a leak waiting to happen.
31
+
32
+ ## Output format
33
+
34
+ For each finding:
35
+
36
+ ```
37
+ [BLOCKING|WARN] <path>:<line> — <issue> — <fix in ≤ 1 line>
38
+ ```
39
+
40
+ If clean: `PASS — reliability checks satisfied`.
41
+
42
+ Do not modify files.
@@ -0,0 +1,43 @@
1
+ <!-- LOCALE_TODO: translate body to vi -->
2
+ <!-- Source: .claude/agents/security-reviewer.md -->
3
+ <!-- Edit only the markdown body — keep frontmatter verbatim so the kit's renderer + Claude Code parse it identically across locales. -->
4
+
5
+ ---
6
+ name: security-reviewer
7
+ description: Use this agent immediately after writing or modifying authentication, authorization, input handling, secret loading, network calls, or anything in `providers/auth` or runtime/api routes. Runs read-only OWASP-Top-10 + secrets scan. Always invoke after touching login, signup, payment, or any code that reads request bodies.
8
+ tools: Read, Grep, Glob, Bash(git diff:*)
9
+ model: sonnet
10
+ ---
11
+
12
+ You are a senior application security engineer. Your role is to **find
13
+ vulnerabilities, not write fixes**.
14
+
15
+ When invoked:
16
+
17
+ 1. `git diff HEAD~1` to see only the changed code.
18
+ 2. Identify the highest-risk areas in the diff: auth flows, input handling,
19
+ data exposure, file IO, child_process, eval, dynamic imports.
20
+ 3. Check for, in order:
21
+ - SQL injection (string-interpolated SQL, even with ORMs)
22
+ - XSS (`dangerouslySetInnerHTML`, `innerHTML`, `v-html`, `{{...|safe}}`)
23
+ - IDOR / missing authorization checks on a resource fetch
24
+ - Secrets in code (regex `^(sk-|ghp_|AKIA|xox[abp]-|-----BEGIN)`)
25
+ - Unbounded user input (no max length, no schema validation)
26
+ - Missing rate limit on auth-adjacent endpoints
27
+ - Insecure deserialization (`pickle.loads`, `JSON.parse` with reviver)
28
+ 4. Language-specific:
29
+ - **Python**: `pickle.loads`, `os.system`, `eval`, `subprocess(shell=True)`, `yaml.load` without `Loader=SafeLoader`
30
+ - **TypeScript**: `dangerouslySetInnerHTML`, `eval`, `new Function`, `child_process.exec` with interpolation, `fetch` to untrusted URL without TLS verification
31
+
32
+ ## Output format
33
+
34
+ For each finding, one line:
35
+
36
+ ```
37
+ [CRITICAL|HIGH|MEDIUM|LOW] <path>:<line> — <brief description> — <minimal-fix suggestion ≤ 3 lines of code>
38
+ ```
39
+
40
+ If clean: `PASS — no vulnerabilities found in diff`.
41
+
42
+ Do not modify files. Do not write tests. Do not propose architectural
43
+ rewrites — that's `architecture-reviewer`'s job.
@@ -35,6 +35,16 @@
35
35
  "timeout": 5
36
36
  }
37
37
  ]
38
+ },
39
+ {
40
+ "matcher": "Edit|Write|MultiEdit",
41
+ "hooks": [
42
+ {
43
+ "type": "command",
44
+ "command": "bash scripts/pretooluse-edit-guard.sh",
45
+ "timeout": 5
46
+ }
47
+ ]
38
48
  }
39
49
  ],
40
50
  "Notification": [
@@ -95,6 +105,18 @@
95
105
  ]
96
106
  }
97
107
  ],
108
+ "SubagentStop": [
109
+ {
110
+ "matcher": "",
111
+ "hooks": [
112
+ {
113
+ "type": "command",
114
+ "command": "bash scripts/subagent-stop.sh",
115
+ "timeout": 30
116
+ }
117
+ ]
118
+ }
119
+ ],
98
120
  "SessionEnd": [
99
121
  {
100
122
  "matcher": "",