agent-harness-kit 0.7.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 +26 -0
- package/package.json +1 -1
- package/src/core/doctor.mjs +47 -0
- package/src/core/render-templates.mjs +119 -5
- 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 +46 -0
- package/src/templates/.claude/output-styles/harness-terse.md +42 -0
- package/src/templates/.claude/settings.json.hbs +2 -1
- 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 +15 -10
- package/src/templates/.claude/skills/doc-drift-scan/SKILL.md.vi +52 -0
- package/src/templates/.claude/skills/doc-drift-scan/scripts/scan-paths.mjs +64 -0
- package/src/templates/.claude/skills/eval-runner/SKILL.md.vi +59 -0
- package/src/templates/.claude/skills/garbage-collection/SKILL.md.hbs +14 -5
- package/src/templates/.claude/skills/garbage-collection/SKILL.md.vi.hbs +58 -0
- package/src/templates/.claude/skills/garbage-collection/scripts/gc-classify.mjs +77 -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.hbs +17 -14
- package/src/templates/.claude/skills/inspect-module/SKILL.md.vi.hbs +57 -0
- package/src/templates/.claude/skills/inspect-module/scripts/module-summary.mjs +144 -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/CLAUDE.md.hbs +9 -5
- package/src/templates/CLAUDE.md.vi.hbs +9 -5
- package/src/templates/scripts/notify-on-block.sh.hbs +73 -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/statusline.mjs +63 -0
- package/src/templates/scripts/subagent-stop.sh.hbs +76 -0
- package/src/templates/scripts/userprompt-guard.sh.hbs +100 -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
|
@@ -48,6 +48,15 @@ program
|
|
|
48
48
|
"human language for the CLAUDE.md template (en, vi)",
|
|
49
49
|
"en",
|
|
50
50
|
)
|
|
51
|
+
.option(
|
|
52
|
+
"--model <id>",
|
|
53
|
+
"Claude model to pin in .claude/settings.json (e.g. claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5)",
|
|
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
|
+
)
|
|
51
60
|
.action(async (opts) => {
|
|
52
61
|
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
|
|
53
62
|
console.log(pc.bold(pc.cyan(`\nagent-harness-kit v${pkg.version}\n`)));
|
|
@@ -196,8 +205,25 @@ program
|
|
|
196
205
|
installCi,
|
|
197
206
|
kitVersion: pkg.version,
|
|
198
207
|
humanLanguage: opts.lang || "en",
|
|
208
|
+
model: opts.model,
|
|
199
209
|
});
|
|
200
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
|
+
|
|
201
227
|
console.log("");
|
|
202
228
|
for (const f of result.written) {
|
|
203
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
|
@@ -6,6 +6,7 @@ import { readFile } from "node:fs/promises";
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
8
|
import pc from "picocolors";
|
|
9
|
+
import { SUPPORTED_MODELS } from "./render-templates.mjs";
|
|
9
10
|
|
|
10
11
|
function check(name, ok, info = "") {
|
|
11
12
|
const mark = ok ? pc.green("✓") : pc.red("✗");
|
|
@@ -99,6 +100,52 @@ export async function doctor({ cwd, kitVersion }) {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
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
|
+
|
|
127
|
+
// 5. Model pin in .claude/settings.json (B4). Catches obvious typos
|
|
128
|
+
// that would silently no-op in Claude Code.
|
|
129
|
+
const settingsPath = resolve(cwd, ".claude/settings.json");
|
|
130
|
+
if (existsSync(settingsPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const s = JSON.parse(await readFile(settingsPath, "utf8"));
|
|
133
|
+
if (typeof s.model === "string" && s.model.length > 0) {
|
|
134
|
+
if (SUPPORTED_MODELS.has(s.model)) {
|
|
135
|
+
check(`model pin (${s.model})`, true);
|
|
136
|
+
} else {
|
|
137
|
+
allOk = false;
|
|
138
|
+
console.log(
|
|
139
|
+
pc.red(` ✗ model pin in .claude/settings.json — "${s.model}" not recognized.`),
|
|
140
|
+
);
|
|
141
|
+
console.log(
|
|
142
|
+
pc.dim(` Known: ${[...SUPPORTED_MODELS].join(", ")}. Re-run \`agent-harness-kit init --model <id>\`.`),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch { /* covered by settings parse check elsewhere */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
102
149
|
console.log("");
|
|
103
150
|
if (!allOk) {
|
|
104
151
|
process.exit(1);
|
|
@@ -23,7 +23,7 @@ const TEMPLATES_ROOT = resolve(__dirname, "..", "templates");
|
|
|
23
23
|
// Files that the user is expected to edit — they win every time, even on
|
|
24
24
|
// fresh init we won't overwrite if they exist. This list is hard-coded
|
|
25
25
|
// because it's tiny and security-sensitive.
|
|
26
|
-
const USER_OWNED_FILES = new Set([
|
|
26
|
+
export const USER_OWNED_FILES = new Set([
|
|
27
27
|
"CLAUDE.md",
|
|
28
28
|
"AGENTS.md",
|
|
29
29
|
"docs/architecture.md",
|
|
@@ -35,7 +35,7 @@ const USER_OWNED_FILES = new Set([
|
|
|
35
35
|
]);
|
|
36
36
|
|
|
37
37
|
// Paths that should be made executable after rendering.
|
|
38
|
-
const EXEC_BITS = new Set([
|
|
38
|
+
export const EXEC_BITS = new Set([
|
|
39
39
|
"scripts/dev-up.sh",
|
|
40
40
|
"scripts/pre-push.sh",
|
|
41
41
|
"scripts/install-git-hooks.sh",
|
|
@@ -48,6 +48,10 @@ 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()) {
|
|
@@ -97,7 +106,21 @@ async function* walk(dir) {
|
|
|
97
106
|
// will route it. Default is "en" → uses the suffix-less CLAUDE.md.hbs.
|
|
98
107
|
export const SUPPORTED_HUMAN_LANGS = new Set(["en", "vi"]);
|
|
99
108
|
|
|
100
|
-
|
|
109
|
+
// SUPPORTED_MODELS — IDs the kit accepts via `--model` and writes verbatim
|
|
110
|
+
// into `.claude/settings.json#model`. The kit does NOT try to be a model
|
|
111
|
+
// registry — it just rejects obvious typos before they silently no-op in
|
|
112
|
+
// Claude Code (which falls back to the org default on unknown IDs).
|
|
113
|
+
export const SUPPORTED_MODELS = new Set([
|
|
114
|
+
"claude-opus-4-7",
|
|
115
|
+
"claude-sonnet-4-6",
|
|
116
|
+
"claude-haiku-4-5",
|
|
117
|
+
// Legacy IDs we still accept on upgrade paths.
|
|
118
|
+
"claude-sonnet-4-5",
|
|
119
|
+
"claude-haiku-3-5",
|
|
120
|
+
]);
|
|
121
|
+
export const DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
122
|
+
|
|
123
|
+
export function buildContext({ projectName, preset, layers, stack, kitVersion, humanLanguage, model }) {
|
|
101
124
|
const installCmd = (() => {
|
|
102
125
|
if (stack.language === "python") return "pip install -e '.[dev]'";
|
|
103
126
|
if (stack.language === "go") return "go mod download";
|
|
@@ -164,6 +187,7 @@ function buildContext({ projectName, preset, layers, stack, kitVersion, humanLan
|
|
|
164
187
|
lintCmd,
|
|
165
188
|
kitVersion,
|
|
166
189
|
humanLanguage: humanLanguage || "en",
|
|
190
|
+
model: model || DEFAULT_MODEL,
|
|
167
191
|
isTypescript: stack.language === "typescript",
|
|
168
192
|
isPython: stack.language === "python",
|
|
169
193
|
isGo: stack.language === "go",
|
|
@@ -183,7 +207,7 @@ function buildContext({ projectName, preset, layers, stack, kitVersion, humanLan
|
|
|
183
207
|
// Decide whether a template path should be rendered for this stack/preset.
|
|
184
208
|
// Adapter-specific files live under templates/_adapter-<id>/ and are merged
|
|
185
209
|
// into the root.
|
|
186
|
-
function pathForStack(rel, stack, humanLanguage = "en") {
|
|
210
|
+
export function pathForStack(rel, stack, humanLanguage = "en") {
|
|
187
211
|
// CLAUDE.md locale routing. `CLAUDE.md.hbs` (no language suffix) is the
|
|
188
212
|
// English default. `CLAUDE.md.<lang>.hbs` is rendered into the same
|
|
189
213
|
// target path (`CLAUDE.md`) when the user picks that locale. The
|
|
@@ -195,6 +219,26 @@ function pathForStack(rel, stack, humanLanguage = "en") {
|
|
|
195
219
|
if (fileLang !== humanLanguage) return null;
|
|
196
220
|
return "CLAUDE.md.hbs"; // canonical target — strip locale suffix
|
|
197
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
|
+
}
|
|
198
242
|
if (rel.startsWith("_adapter-typescript/")) {
|
|
199
243
|
const stripped = rel.slice("_adapter-typescript/".length);
|
|
200
244
|
if (stack.language === "typescript") return stripped;
|
|
@@ -243,6 +287,59 @@ function sha256(buf) {
|
|
|
243
287
|
return createHash("sha256").update(buf).digest("hex");
|
|
244
288
|
}
|
|
245
289
|
|
|
290
|
+
// Inject a statusLine block into .claude/settings.json. Idempotent: if the
|
|
291
|
+
// existing statusLine already references the kit's script, leave it; otherwise
|
|
292
|
+
// set it to invoke scripts/statusline.mjs via node. Doesn't clobber a
|
|
293
|
+
// user-customised type:"command" entry that points at a different command.
|
|
294
|
+
//
|
|
295
|
+
// Returns {changed, rawContent} for the lockfile bookkeeping (mirrors the
|
|
296
|
+
// mergeHooksIntoSettings contract).
|
|
297
|
+
export async function mergeStatusLineIntoSettings(cwd) {
|
|
298
|
+
const settingsPath = resolve(cwd, ".claude/settings.json");
|
|
299
|
+
const scriptPath = resolve(cwd, "scripts/statusline.mjs");
|
|
300
|
+
if (!existsSync(scriptPath)) return { changed: false, rawContent: "" };
|
|
301
|
+
let settings = {};
|
|
302
|
+
let raw = "";
|
|
303
|
+
if (existsSync(settingsPath)) {
|
|
304
|
+
raw = await readFile(settingsPath, "utf8");
|
|
305
|
+
try {
|
|
306
|
+
settings = JSON.parse(raw);
|
|
307
|
+
} catch {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`mergeStatusLineIntoSettings: ${settingsPath} is not valid JSON`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const desired = {
|
|
314
|
+
type: "command",
|
|
315
|
+
command: "node scripts/statusline.mjs",
|
|
316
|
+
};
|
|
317
|
+
// Preserve a user-customised entry if it already points elsewhere. We only
|
|
318
|
+
// inject when statusLine is absent OR explicitly references our script.
|
|
319
|
+
const cur = settings.statusLine;
|
|
320
|
+
if (
|
|
321
|
+
cur &&
|
|
322
|
+
typeof cur === "object" &&
|
|
323
|
+
cur.type === "command" &&
|
|
324
|
+
typeof cur.command === "string" &&
|
|
325
|
+
!/statusline\.mjs/.test(cur.command)
|
|
326
|
+
) {
|
|
327
|
+
return { changed: false, rawContent: Buffer.from(raw) };
|
|
328
|
+
}
|
|
329
|
+
if (
|
|
330
|
+
cur &&
|
|
331
|
+
typeof cur === "object" &&
|
|
332
|
+
cur.type === desired.type &&
|
|
333
|
+
cur.command === desired.command
|
|
334
|
+
) {
|
|
335
|
+
return { changed: false, rawContent: Buffer.from(raw) };
|
|
336
|
+
}
|
|
337
|
+
settings.statusLine = desired;
|
|
338
|
+
const out = JSON.stringify(settings, null, 2) + "\n";
|
|
339
|
+
await writeFile(settingsPath, out);
|
|
340
|
+
return { changed: true, rawContent: Buffer.from(out) };
|
|
341
|
+
}
|
|
342
|
+
|
|
246
343
|
// Merge .claude/hooks/hooks.json#hooks into .claude/settings.json#hooks.
|
|
247
344
|
// Claude Code only reads hooks from settings.json — a free-standing
|
|
248
345
|
// hooks.json is ignored by the runtime. Kept as a file because the plugin
|
|
@@ -295,6 +392,7 @@ export async function renderAll({
|
|
|
295
392
|
installCi,
|
|
296
393
|
kitVersion,
|
|
297
394
|
humanLanguage = "en",
|
|
395
|
+
model,
|
|
298
396
|
}) {
|
|
299
397
|
registerHelpers();
|
|
300
398
|
if (!SUPPORTED_HUMAN_LANGS.has(humanLanguage)) {
|
|
@@ -302,7 +400,12 @@ export async function renderAll({
|
|
|
302
400
|
`Unsupported humanLanguage "${humanLanguage}". Supported: ${[...SUPPORTED_HUMAN_LANGS].join(", ")}`,
|
|
303
401
|
);
|
|
304
402
|
}
|
|
305
|
-
|
|
403
|
+
if (model && !SUPPORTED_MODELS.has(model)) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Unsupported model "${model}". Supported: ${[...SUPPORTED_MODELS].join(", ")}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const ctx = buildContext({ projectName, preset, layers, stack, kitVersion, humanLanguage, model });
|
|
306
409
|
|
|
307
410
|
const written = [];
|
|
308
411
|
const skipped = [];
|
|
@@ -365,6 +468,17 @@ export async function renderAll({
|
|
|
365
468
|
}
|
|
366
469
|
}
|
|
367
470
|
|
|
471
|
+
// v0.8 — statusLine injection. Runs after hook merge so the lockfile
|
|
472
|
+
// captures the final settings.json bytes. Idempotent; never clobbers a
|
|
473
|
+
// user-customised entry that points elsewhere.
|
|
474
|
+
if (existsSync(resolve(cwd, "scripts/statusline.mjs"))) {
|
|
475
|
+
const stat = await mergeStatusLineIntoSettings(cwd);
|
|
476
|
+
if (stat.changed) {
|
|
477
|
+
lockfile.files[".claude/settings.json"] = sha256(stat.rawContent);
|
|
478
|
+
written.push(".claude/settings.json (statusLine)");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
368
482
|
// Write the lockfile last (after we know the full set of files).
|
|
369
483
|
const lockTarget = resolve(cwd, ".harness/installed.json");
|
|
370
484
|
await mkdir(dirname(lockTarget), { recursive: true });
|
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.
|