agent-harness-kit 0.8.0 → 0.9.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/cli.mjs +21 -0
- package/package.json +1 -1
- package/src/core/doctor.mjs +24 -0
- package/src/core/render-templates.mjs +29 -0
- package/src/core/upgrade.mjs +81 -60
- package/src/templates/.claude/agents/api-consistency-reviewer.md.vi +37 -0
- package/src/templates/.claude/agents/architecture-reviewer.md.vi.hbs +45 -0
- package/src/templates/.claude/agents/performance-reviewer.md.vi +39 -0
- package/src/templates/.claude/agents/reliability-reviewer.md.vi +42 -0
- package/src/templates/.claude/agents/security-reviewer.md.vi +43 -0
- package/src/templates/.claude/hooks/hooks.json +22 -0
- package/src/templates/.claude/output-styles/harness-terse.md +42 -0
- package/src/templates/.claude/settings.json.hbs +1 -0
- package/src/templates/.claude/skills/add-adr/SKILL.md.vi +64 -0
- package/src/templates/.claude/skills/add-feature/SKILL.md.vi.hbs +50 -0
- package/src/templates/.claude/skills/debug-flow/SKILL.md.vi.hbs +42 -0
- package/src/templates/.claude/skills/doc-drift-scan/SKILL.md.vi +52 -0
- package/src/templates/.claude/skills/eval-runner/SKILL.md.vi +59 -0
- package/src/templates/.claude/skills/garbage-collection/SKILL.md.vi.hbs +58 -0
- package/src/templates/.claude/skills/i18n-add-locale/SKILL.md +52 -0
- package/src/templates/.claude/skills/i18n-add-locale/SKILL.md.vi +56 -0
- package/src/templates/.claude/skills/i18n-add-locale/scripts/locale-scaffold.mjs +120 -0
- package/src/templates/.claude/skills/inspect-app/SKILL.md.vi +61 -0
- package/src/templates/.claude/skills/inspect-module/SKILL.md.vi.hbs +57 -0
- package/src/templates/.claude/skills/map-domain/SKILL.md +42 -0
- package/src/templates/.claude/skills/map-domain/SKILL.md.vi +42 -0
- package/src/templates/.claude/skills/map-domain/scripts/domain-map.mjs +145 -0
- package/src/templates/.claude/skills/propose-harness-improvement/SKILL.md.vi +49 -0
- package/src/templates/.claude/skills/propose-harness-improvement/scripts/improvement-bundle.mjs +172 -0
- package/src/templates/.claude/skills/refactor-feature/SKILL.md +60 -0
- package/src/templates/.claude/skills/refactor-feature/SKILL.md.vi +64 -0
- package/src/templates/.claude/skills/refactor-feature/scripts/feature-diff.mjs +146 -0
- package/src/templates/.claude/skills/review-this-pr/SKILL.md +59 -0
- package/src/templates/.claude/skills/review-this-pr/SKILL.md.vi +63 -0
- package/src/templates/.claude/skills/review-this-pr/scripts/pr-review-driver.mjs +152 -0
- package/src/templates/.claude/skills/structural-test-author/SKILL.md.vi.hbs +50 -0
- package/src/templates/.claude/skills/write-skill/SKILL.md.vi +43 -0
- package/src/templates/.harness/eval/rubrics/feature-step-done.mjs +148 -0
- package/src/templates/.harness/eval/tasks/feature-step-done.answer.md +53 -0
- package/src/templates/.harness/eval/tasks/feature-step-done.json +10 -0
- package/src/templates/.harness/eval/tasks/feature-step-done.prompt.md +43 -0
- package/src/templates/.mcp.json.example +35 -0
- package/src/templates/scripts/pretooluse-edit-guard.sh.hbs +115 -0
- package/src/templates/scripts/session-end.sh.hbs +6 -0
- package/src/templates/scripts/session-rollup.mjs +96 -0
- package/src/templates/scripts/session-start.sh.hbs +25 -0
- 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.
|
|
14
|
+
"ref": "v0.9.0"
|
|
15
15
|
},
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.9.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.
|
|
3
|
+
"version": "0.9.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/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.
|
|
3
|
+
"version": "0.9.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": {
|
package/src/core/doctor.mjs
CHANGED
|
@@ -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;
|
package/src/core/upgrade.mjs
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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(
|
|
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": "",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-terse
|
|
3
|
+
description: Solo-dev terse output style for the agent-harness-kit. Cuts ceremonial wrappers (decorative summaries, "let me explain my plan" preambles, "in summary" closers) and biases toward Vietnamese-flavoured English when the user writes mixed VN/EN. Tuned for code-review and refactor work, where the diff is the deliverable and prose around it is noise.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Output style: harness-terse
|
|
7
|
+
|
|
8
|
+
You are operating inside the agent-harness-kit — a solo-developer Claude
|
|
9
|
+
Code harness with structural tests, skill side-cars, and a tight
|
|
10
|
+
human-in-the-loop pattern. The user reads diffs and tool calls directly;
|
|
11
|
+
prose is for genuine signal, not narration.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
1. **No decorative summaries.** Skip "I'll start by…", "Now let me…",
|
|
16
|
+
"In summary…" and other rituals. State what changed, in one or two
|
|
17
|
+
sentences.
|
|
18
|
+
2. **No "let me read the file" preambles.** State the action and call
|
|
19
|
+
the tool — the user sees both.
|
|
20
|
+
3. **Diff > prose.** When a code change is the deliverable, point at the
|
|
21
|
+
files and let the diff speak. Only add prose where the diff is not
|
|
22
|
+
self-explanatory.
|
|
23
|
+
4. **Use `path:line` for code references** so the user can jump.
|
|
24
|
+
5. **Match the user's language.** If they write in Vietnamese, reply in
|
|
25
|
+
Vietnamese. If mixed VN/EN, mirror their balance.
|
|
26
|
+
6. **End turns with what changed + what's next, one sentence each.** No
|
|
27
|
+
bullet lists summarising the previous turn — the user sees the tool
|
|
28
|
+
calls.
|
|
29
|
+
7. **When uncertain, ask one focused question.** Don't pad with multiple
|
|
30
|
+
"or alternatively" branches.
|
|
31
|
+
8. **Skills are first-class.** When a user request maps to a skill,
|
|
32
|
+
invoke it rather than freestyle the work. Skills carry the harness's
|
|
33
|
+
safety net (structural tests, baseline monotonic, side-cars).
|
|
34
|
+
|
|
35
|
+
## What this style is NOT for
|
|
36
|
+
|
|
37
|
+
- Long-form explanations to non-technical stakeholders.
|
|
38
|
+
- Tutorial / educational responses where worked-out reasoning matters.
|
|
39
|
+
- First-time user onboarding where the user explicitly asks for verbose
|
|
40
|
+
guidance.
|
|
41
|
+
|
|
42
|
+
In those cases, fall back to Claude Code's default style.
|