agent-harness-kit 0.5.1 → 0.7.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/README.md +29 -0
- package/bin/cli.mjs +10 -1
- package/package.json +1 -1
- package/src/core/detect-stack.mjs +32 -0
- package/src/core/render-templates.mjs +111 -4
- package/src/templates/.claude/hooks/hooks.json +87 -0
- package/src/templates/CLAUDE.md.hbs +1 -1
- package/src/templates/CLAUDE.md.vi.hbs +70 -0
- package/src/templates/_adapter-kotlin/harness/structural-check.mjs.hbs +286 -0
- package/src/templates/_adapter-rust/harness/structural-check.mjs.hbs +333 -60
- package/src/templates/_adapter-swift/harness/structural-check.mjs.hbs +285 -0
- package/src/templates/harness.config.json.hbs +5 -3
- package/src/templates/scripts/_lib/approx-tokens.mjs +48 -0
- package/src/templates/scripts/_lib/json-pick.mjs +278 -0
- package/src/templates/scripts/harness-report.mjs +95 -1
- package/src/templates/scripts/pre-compact.sh.hbs +121 -0
- package/src/templates/scripts/pre-push.sh +42 -8
- package/src/templates/scripts/precompletion-checklist.sh.hbs +143 -24
- package/src/templates/scripts/pretooluse-bash-guard.sh.hbs +146 -0
- package/src/templates/scripts/session-end.sh.hbs +48 -0
- package/src/templates/scripts/session-start.sh.hbs +139 -0
- package/src/templates/scripts/structural-test-on-edit.sh.hbs +56 -4
- package/src/templates/scripts/telemetry-on-skill.sh +32 -10
- package/src/templates/.claude/hooks/hooks.json.hbs +0 -39
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"source": {
|
|
12
12
|
"source": "github",
|
|
13
13
|
"repo": "tuanle96/agent-harness-kit",
|
|
14
|
-
"ref": "v0.
|
|
14
|
+
"ref": "v0.7.0"
|
|
15
15
|
},
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.7.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.7.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
|
@@ -191,6 +191,35 @@ developer. The `eval-runner` skill enforces a per-run budget set in
|
|
|
191
191
|
| Flask | python | `flask` | `flask --app app run --debug` | v0.1 |
|
|
192
192
|
| Go / Rust | core only | none | (manual) | v0.4 |
|
|
193
193
|
|
|
194
|
+
## CI: real-claude E2E test (v0.7+)
|
|
195
|
+
|
|
196
|
+
The kit ships a CI job that spawns the real `claude` binary against a fresh
|
|
197
|
+
init of itself and asserts that the SessionStart hook actually fires (with
|
|
198
|
+
the expected `additionalContext` payload). This catches the class of bug
|
|
199
|
+
that v0.6's silent-no-op hooks fell into — every synthetic test passed for
|
|
200
|
+
seven releases while not a single hook ever triggered inside a real Claude
|
|
201
|
+
Code session.
|
|
202
|
+
|
|
203
|
+
**Behavior:**
|
|
204
|
+
|
|
205
|
+
- Locally: `npm test` skips the E2E case cleanly when no `ANTHROPIC_API_KEY`
|
|
206
|
+
is set (1 skip, 0 fail).
|
|
207
|
+
- CI with secret: the `e2e-claude` job runs the driver, installs
|
|
208
|
+
`@anthropic-ai/claude-code` globally, and exercises one claude turn
|
|
209
|
+
(~$0.01–0.05). Failure means a hook system regression.
|
|
210
|
+
- CI without secret (fork PRs): emits a GitHub Actions warning and exits 0
|
|
211
|
+
so the PR can still merge — but you lose the v0.6-class bug guard for
|
|
212
|
+
that run.
|
|
213
|
+
|
|
214
|
+
To enable on your own fork: configure the `ANTHROPIC_API_KEY` repository
|
|
215
|
+
secret in GitHub Actions settings.
|
|
216
|
+
|
|
217
|
+
Local run (uses whatever auth the `claude` binary already has):
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
AHK_E2E_USE_LOCAL_AUTH=1 node scripts/e2e-claude-cli.mjs
|
|
221
|
+
```
|
|
222
|
+
|
|
194
223
|
## License
|
|
195
224
|
|
|
196
225
|
MIT.
|
package/bin/cli.mjs
CHANGED
|
@@ -43,6 +43,11 @@ program
|
|
|
43
43
|
"force init for languages without a structural-test adapter yet (go, rust, swift, kotlin)",
|
|
44
44
|
false,
|
|
45
45
|
)
|
|
46
|
+
.option(
|
|
47
|
+
"--lang <code>",
|
|
48
|
+
"human language for the CLAUDE.md template (en, vi)",
|
|
49
|
+
"en",
|
|
50
|
+
)
|
|
46
51
|
.action(async (opts) => {
|
|
47
52
|
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
|
|
48
53
|
console.log(pc.bold(pc.cyan(`\nagent-harness-kit v${pkg.version}\n`)));
|
|
@@ -104,7 +109,10 @@ program
|
|
|
104
109
|
// initing at the root of a Go/Rust/Swift/Kotlin project produces a
|
|
105
110
|
// half-broken harness (structural test missing, ts-morph engine pinned).
|
|
106
111
|
// Skills + reviewers still work, but the user should opt in explicitly.
|
|
107
|
-
|
|
112
|
+
// Swift + Kotlin shipped regex-based adapters in v0.7; Go + Rust earlier.
|
|
113
|
+
// No language is "unsupported" today — the set is empty, kept as a hook
|
|
114
|
+
// for future languages that don't yet have an adapter.
|
|
115
|
+
const UNSUPPORTED = new Set([]);
|
|
108
116
|
if (UNSUPPORTED.has(stack.language) && !opts.allowUnsupported) {
|
|
109
117
|
console.log(
|
|
110
118
|
pc.yellow(
|
|
@@ -187,6 +195,7 @@ program
|
|
|
187
195
|
installHooks,
|
|
188
196
|
installCi,
|
|
189
197
|
kitVersion: pkg.version,
|
|
198
|
+
humanLanguage: opts.lang || "en",
|
|
190
199
|
});
|
|
191
200
|
|
|
192
201
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-harness-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|
|
@@ -68,6 +68,22 @@ export async function detectStack(cwd) {
|
|
|
68
68
|
// primary language. Walks 1 level deep into common monorepo dirs.
|
|
69
69
|
await probePolyglot(cwd, result);
|
|
70
70
|
|
|
71
|
+
// Rust workspace — must be checked BEFORE package.json because a polyglot
|
|
72
|
+
// repo (Rust backend + Next.js frontend) typically has BOTH at the root,
|
|
73
|
+
// and we want the Rust adapter installed by default since structural-test
|
|
74
|
+
// enforcement matters more for the workspace than for the marketing site.
|
|
75
|
+
// Single-crate Cargo.toml falls through to the legacy check at the bottom.
|
|
76
|
+
const rootCargo = await readTextSafe(resolve(cwd, "Cargo.toml"));
|
|
77
|
+
if (rootCargo && /^\s*\[workspace\]/m.test(rootCargo)) {
|
|
78
|
+
result.language = "rust";
|
|
79
|
+
result.framework = "rust-workspace";
|
|
80
|
+
result.packageManager = "cargo";
|
|
81
|
+
result.monorepo = true;
|
|
82
|
+
result.suggestedPreset = "generic";
|
|
83
|
+
result.availablePresets = ["generic"];
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
// JavaScript/TypeScript.
|
|
72
88
|
const pkg = await readJsonSafe(resolve(cwd, "package.json"));
|
|
73
89
|
if (pkg) {
|
|
@@ -141,6 +157,22 @@ export async function detectStack(cwd) {
|
|
|
141
157
|
return result;
|
|
142
158
|
}
|
|
143
159
|
|
|
160
|
+
// Swift / Kotlin — regex-based adapters (added in v0.7).
|
|
161
|
+
if (await exists(resolve(cwd, "Package.swift"))) {
|
|
162
|
+
result.language = "swift";
|
|
163
|
+
result.packageManager = "swift";
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
(await exists(resolve(cwd, "build.gradle.kts"))) ||
|
|
168
|
+
(await exists(resolve(cwd, "settings.gradle.kts"))) ||
|
|
169
|
+
(await exists(resolve(cwd, "build.gradle")))
|
|
170
|
+
) {
|
|
171
|
+
result.language = "kotlin";
|
|
172
|
+
result.packageManager = "gradle";
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
144
176
|
return result;
|
|
145
177
|
}
|
|
146
178
|
|
|
@@ -43,6 +43,11 @@ const EXEC_BITS = new Set([
|
|
|
43
43
|
"scripts/precompletion-checklist.sh",
|
|
44
44
|
"scripts/telemetry-on-skill.sh",
|
|
45
45
|
"scripts/harness-report.mjs",
|
|
46
|
+
// v0.7 hook expansion — SessionStart / PreToolUse / PreCompact / SessionEnd.
|
|
47
|
+
"scripts/session-start.sh",
|
|
48
|
+
"scripts/pretooluse-bash-guard.sh",
|
|
49
|
+
"scripts/pre-compact.sh",
|
|
50
|
+
"scripts/session-end.sh",
|
|
46
51
|
]);
|
|
47
52
|
|
|
48
53
|
export function registerHelpers() {
|
|
@@ -86,11 +91,19 @@ async function* walk(dir) {
|
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
|
|
94
|
+
// SUPPORTED_LANGS — human-language variants the kit ships for CLAUDE.md.
|
|
95
|
+
// Adding a new locale: drop `CLAUDE.md.<code>.hbs` alongside the English
|
|
96
|
+
// CLAUDE.md.hbs, add the code here, and the picker in pathForStack()
|
|
97
|
+
// will route it. Default is "en" → uses the suffix-less CLAUDE.md.hbs.
|
|
98
|
+
export const SUPPORTED_HUMAN_LANGS = new Set(["en", "vi"]);
|
|
99
|
+
|
|
100
|
+
function buildContext({ projectName, preset, layers, stack, kitVersion, humanLanguage }) {
|
|
90
101
|
const installCmd = (() => {
|
|
91
102
|
if (stack.language === "python") return "pip install -e '.[dev]'";
|
|
92
103
|
if (stack.language === "go") return "go mod download";
|
|
93
104
|
if (stack.language === "rust") return "cargo build";
|
|
105
|
+
if (stack.language === "swift") return "swift package resolve";
|
|
106
|
+
if (stack.language === "kotlin") return "./gradlew build --refresh-dependencies";
|
|
94
107
|
if (stack.packageManager === "pnpm") return "pnpm install";
|
|
95
108
|
if (stack.packageManager === "yarn") return "yarn";
|
|
96
109
|
if (stack.packageManager === "bun") return "bun install";
|
|
@@ -109,6 +122,8 @@ function buildContext({ projectName, preset, layers, stack, kitVersion }) {
|
|
|
109
122
|
if (stack.language === "python") return "python -m app";
|
|
110
123
|
if (stack.language === "go") return "go run ./cmd/...";
|
|
111
124
|
if (stack.language === "rust") return "cargo run";
|
|
125
|
+
if (stack.language === "swift") return "swift run";
|
|
126
|
+
if (stack.language === "kotlin") return "./gradlew run";
|
|
112
127
|
return "npm run dev";
|
|
113
128
|
}
|
|
114
129
|
})();
|
|
@@ -119,6 +134,8 @@ function buildContext({ projectName, preset, layers, stack, kitVersion }) {
|
|
|
119
134
|
if (stack.language === "python") return "pytest -x";
|
|
120
135
|
if (stack.language === "go") return "go test ./...";
|
|
121
136
|
if (stack.language === "rust") return "cargo test";
|
|
137
|
+
if (stack.language === "swift") return "swift test";
|
|
138
|
+
if (stack.language === "kotlin") return "./gradlew test";
|
|
122
139
|
return "npm test";
|
|
123
140
|
}
|
|
124
141
|
})();
|
|
@@ -126,6 +143,8 @@ function buildContext({ projectName, preset, layers, stack, kitVersion }) {
|
|
|
126
143
|
if (stack.language === "python") return "ruff check .";
|
|
127
144
|
if (stack.language === "go") return "go vet ./...";
|
|
128
145
|
if (stack.language === "rust") return "cargo clippy --all-targets -- -D warnings";
|
|
146
|
+
if (stack.language === "swift") return "swift build --warnings-as-errors";
|
|
147
|
+
if (stack.language === "kotlin") return "./gradlew detekt || ./gradlew lint";
|
|
129
148
|
return "npm run lint";
|
|
130
149
|
})();
|
|
131
150
|
|
|
@@ -144,10 +163,13 @@ function buildContext({ projectName, preset, layers, stack, kitVersion }) {
|
|
|
144
163
|
testCmd,
|
|
145
164
|
lintCmd,
|
|
146
165
|
kitVersion,
|
|
166
|
+
humanLanguage: humanLanguage || "en",
|
|
147
167
|
isTypescript: stack.language === "typescript",
|
|
148
168
|
isPython: stack.language === "python",
|
|
149
169
|
isGo: stack.language === "go",
|
|
150
170
|
isRust: stack.language === "rust",
|
|
171
|
+
isSwift: stack.language === "swift",
|
|
172
|
+
isKotlin: stack.language === "kotlin",
|
|
151
173
|
isNextjs: stack.framework === "nextjs",
|
|
152
174
|
isFastapi: stack.framework === "fastapi",
|
|
153
175
|
isExpress: stack.framework === "express",
|
|
@@ -161,7 +183,18 @@ function buildContext({ projectName, preset, layers, stack, kitVersion }) {
|
|
|
161
183
|
// Decide whether a template path should be rendered for this stack/preset.
|
|
162
184
|
// Adapter-specific files live under templates/_adapter-<id>/ and are merged
|
|
163
185
|
// into the root.
|
|
164
|
-
function pathForStack(rel, stack) {
|
|
186
|
+
function pathForStack(rel, stack, humanLanguage = "en") {
|
|
187
|
+
// CLAUDE.md locale routing. `CLAUDE.md.hbs` (no language suffix) is the
|
|
188
|
+
// English default. `CLAUDE.md.<lang>.hbs` is rendered into the same
|
|
189
|
+
// target path (`CLAUDE.md`) when the user picks that locale. The
|
|
190
|
+
// suffixed file is skipped otherwise — and so is the unsuffixed file
|
|
191
|
+
// when a non-default locale is active, so only one variant lands.
|
|
192
|
+
if (/^CLAUDE\.md(?:\.[a-z]{2,5})?\.hbs$/.test(rel)) {
|
|
193
|
+
const m = rel.match(/^CLAUDE\.md(?:\.([a-z]{2,5}))?\.hbs$/);
|
|
194
|
+
const fileLang = m[1] || "en";
|
|
195
|
+
if (fileLang !== humanLanguage) return null;
|
|
196
|
+
return "CLAUDE.md.hbs"; // canonical target — strip locale suffix
|
|
197
|
+
}
|
|
165
198
|
if (rel.startsWith("_adapter-typescript/")) {
|
|
166
199
|
const stripped = rel.slice("_adapter-typescript/".length);
|
|
167
200
|
if (stack.language === "typescript") return stripped;
|
|
@@ -184,6 +217,16 @@ function pathForStack(rel, stack) {
|
|
|
184
217
|
if (rel.startsWith("_adapter-rust/")) {
|
|
185
218
|
return stack.language === "rust" ? rel.slice("_adapter-rust/".length) : null;
|
|
186
219
|
}
|
|
220
|
+
if (rel.startsWith("_adapter-swift/")) {
|
|
221
|
+
const stripped = rel.slice("_adapter-swift/".length);
|
|
222
|
+
if (stack.language === "swift") return stripped;
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (rel.startsWith("_adapter-kotlin/")) {
|
|
226
|
+
const stripped = rel.slice("_adapter-kotlin/".length);
|
|
227
|
+
if (stack.language === "kotlin") return stripped;
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
187
230
|
if (rel.startsWith("_preset-nextjs/")) {
|
|
188
231
|
return stack.framework === "nextjs" ? rel.slice("_preset-nextjs/".length) : null;
|
|
189
232
|
}
|
|
@@ -200,6 +243,48 @@ function sha256(buf) {
|
|
|
200
243
|
return createHash("sha256").update(buf).digest("hex");
|
|
201
244
|
}
|
|
202
245
|
|
|
246
|
+
// Merge .claude/hooks/hooks.json#hooks into .claude/settings.json#hooks.
|
|
247
|
+
// Claude Code only reads hooks from settings.json — a free-standing
|
|
248
|
+
// hooks.json is ignored by the runtime. Kept as a file because the plugin
|
|
249
|
+
// install path references it via .claude-plugin/plugin.json. Idempotent:
|
|
250
|
+
// re-running produces the same settings.json bytes when source hasn't
|
|
251
|
+
// changed. Returns {changed, rawContent} for the lockfile.
|
|
252
|
+
export async function mergeHooksIntoSettings(cwd) {
|
|
253
|
+
const settingsPath = resolve(cwd, ".claude/settings.json");
|
|
254
|
+
const hooksPath = resolve(cwd, ".claude/hooks/hooks.json");
|
|
255
|
+
const hooksRaw = await readFile(hooksPath, "utf8");
|
|
256
|
+
let hooksJson;
|
|
257
|
+
try {
|
|
258
|
+
hooksJson = JSON.parse(hooksRaw);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
throw new Error(`mergeHooksIntoSettings: invalid JSON in ${hooksPath}: ${e.message}`);
|
|
261
|
+
}
|
|
262
|
+
if (!hooksJson.hooks || typeof hooksJson.hooks !== "object") {
|
|
263
|
+
return { changed: false, rawContent: "" };
|
|
264
|
+
}
|
|
265
|
+
let settings = {};
|
|
266
|
+
if (existsSync(settingsPath)) {
|
|
267
|
+
const raw = await readFile(settingsPath, "utf8");
|
|
268
|
+
try {
|
|
269
|
+
settings = JSON.parse(raw);
|
|
270
|
+
} catch {
|
|
271
|
+
// Corrupt settings.json — refuse to clobber user edits. Surface clearly.
|
|
272
|
+
throw new Error(`mergeHooksIntoSettings: ${settingsPath} is not valid JSON`);
|
|
273
|
+
}
|
|
274
|
+
// Idempotency: if hooks key already matches source verbatim, nothing to do.
|
|
275
|
+
if (
|
|
276
|
+
settings.hooks &&
|
|
277
|
+
JSON.stringify(settings.hooks) === JSON.stringify(hooksJson.hooks)
|
|
278
|
+
) {
|
|
279
|
+
return { changed: false, rawContent: Buffer.from(raw) };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
settings.hooks = hooksJson.hooks;
|
|
283
|
+
const out = JSON.stringify(settings, null, 2) + "\n";
|
|
284
|
+
await writeFile(settingsPath, out);
|
|
285
|
+
return { changed: true, rawContent: Buffer.from(out) };
|
|
286
|
+
}
|
|
287
|
+
|
|
203
288
|
export async function renderAll({
|
|
204
289
|
cwd,
|
|
205
290
|
projectName,
|
|
@@ -209,9 +294,15 @@ export async function renderAll({
|
|
|
209
294
|
installHooks,
|
|
210
295
|
installCi,
|
|
211
296
|
kitVersion,
|
|
297
|
+
humanLanguage = "en",
|
|
212
298
|
}) {
|
|
213
299
|
registerHelpers();
|
|
214
|
-
|
|
300
|
+
if (!SUPPORTED_HUMAN_LANGS.has(humanLanguage)) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Unsupported humanLanguage "${humanLanguage}". Supported: ${[...SUPPORTED_HUMAN_LANGS].join(", ")}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const ctx = buildContext({ projectName, preset, layers, stack, kitVersion, humanLanguage });
|
|
215
306
|
|
|
216
307
|
const written = [];
|
|
217
308
|
const skipped = [];
|
|
@@ -219,7 +310,7 @@ export async function renderAll({
|
|
|
219
310
|
|
|
220
311
|
for await (const abs of walk(TEMPLATES_ROOT)) {
|
|
221
312
|
const relFromTemplates = relative(TEMPLATES_ROOT, abs).split("\\").join("/");
|
|
222
|
-
const stackRel = pathForStack(relFromTemplates, stack);
|
|
313
|
+
const stackRel = pathForStack(relFromTemplates, stack, humanLanguage);
|
|
223
314
|
if (stackRel === null) continue;
|
|
224
315
|
if (relFromTemplates.startsWith("_ci/") && !installCi) continue;
|
|
225
316
|
if (relFromTemplates.includes("hooks.json") && !installHooks) continue;
|
|
@@ -258,6 +349,22 @@ export async function renderAll({
|
|
|
258
349
|
written.push(targetRel);
|
|
259
350
|
}
|
|
260
351
|
|
|
352
|
+
// Critical fix (v0.7): merge .claude/hooks/hooks.json into
|
|
353
|
+
// .claude/settings.json#hooks. Claude Code ONLY reads hooks from
|
|
354
|
+
// settings.json — a free-standing .claude/hooks/hooks.json is ignored.
|
|
355
|
+
// Pre-v0.7 the kit silently no-op'd every hook on the scaffold path
|
|
356
|
+
// because it shipped only the free-standing file. Plugin install path
|
|
357
|
+
// continues to read .claude/hooks/hooks.json (per plugin.json manifest)
|
|
358
|
+
// so the file is kept; we just additionally inject it where Claude Code
|
|
359
|
+
// actually looks.
|
|
360
|
+
if (installHooks && existsSync(resolve(cwd, ".claude/hooks/hooks.json"))) {
|
|
361
|
+
const merged = await mergeHooksIntoSettings(cwd);
|
|
362
|
+
if (merged.changed) {
|
|
363
|
+
lockfile.files[".claude/settings.json"] = sha256(merged.rawContent);
|
|
364
|
+
written.push(".claude/settings.json (merged hooks)");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
261
368
|
// Write the lockfile last (after we know the full set of files).
|
|
262
369
|
const lockTarget = resolve(cwd, ".harness/installed.json");
|
|
263
370
|
await mkdir(dirname(lockTarget), { recursive: true });
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-hooks.json",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "startup|resume|compact",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "bash scripts/session-start.sh",
|
|
11
|
+
"timeout": 10
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"PreToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Bash",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "bash scripts/pretooluse-bash-guard.sh",
|
|
23
|
+
"timeout": 5
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"PostToolUse": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "bash scripts/structural-test-on-edit.sh",
|
|
35
|
+
"timeout": 30
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"matcher": "Skill",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "bash scripts/telemetry-on-skill.sh",
|
|
45
|
+
"timeout": 5
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"PreCompact": [
|
|
51
|
+
{
|
|
52
|
+
"matcher": "",
|
|
53
|
+
"hooks": [
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "bash scripts/pre-compact.sh",
|
|
57
|
+
"timeout": 5
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"Stop": [
|
|
63
|
+
{
|
|
64
|
+
"matcher": "",
|
|
65
|
+
"hooks": [
|
|
66
|
+
{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": "bash scripts/precompletion-checklist.sh",
|
|
69
|
+
"timeout": 20
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"SessionEnd": [
|
|
75
|
+
{
|
|
76
|
+
"matcher": "",
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "bash scripts/session-end.sh",
|
|
81
|
+
"timeout": 5
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -10,7 +10,7 @@ an encyclopedia.
|
|
|
10
10
|
- Dev: `{{devCmd}}`
|
|
11
11
|
- Test: `{{testCmd}}`
|
|
12
12
|
- Lint: `{{lintCmd}}`
|
|
13
|
-
- Structural: `{{#if isPython}}python -m harness.structural_test{{else}}{{#if isGo}}go run harness/structural_check.go{{else}}{{#if isRust}}node harness/structural-check.mjs{{else}}npm run harness:check{{/if}}{{/if}}{{/if}}` (must pass before any PR)
|
|
13
|
+
- Structural: `{{#if isPython}}python -m harness.structural_test{{else}}{{#if isGo}}go run harness/structural_check.go{{else}}{{#if isRust}}node harness/structural-check.mjs{{else}}{{#if isSwift}}node harness/structural-check.mjs{{else}}{{#if isKotlin}}node harness/structural-check.mjs{{else}}npm run harness:check{{/if}}{{/if}}{{/if}}{{/if}}{{/if}}` (must pass before any PR)
|
|
14
14
|
|
|
15
15
|
## Architecture (brief)
|
|
16
16
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# {{projectName}} — Ghi chú làm việc cho Agent
|
|
2
|
+
|
|
3
|
+
{{description}} Dự án {{language}}/{{framework}}. Solo-dev hobby. File này
|
|
4
|
+
**cố ý ngắn** — nó là **Mục lục**, không phải Bách khoa toàn thư.
|
|
5
|
+
|
|
6
|
+
## Build & Run
|
|
7
|
+
|
|
8
|
+
- Cài đặt: `{{installCmd}}`
|
|
9
|
+
- Dev: `{{devCmd}}`
|
|
10
|
+
- Test: `{{testCmd}}`
|
|
11
|
+
- Lint: `{{lintCmd}}`
|
|
12
|
+
- Structural: `{{#if isPython}}python -m harness.structural_test{{else}}{{#if isGo}}go run harness/structural_check.go{{else}}{{#if isRust}}node harness/structural-check.mjs{{else}}{{#if isSwift}}node harness/structural-check.mjs{{else}}{{#if isKotlin}}node harness/structural-check.mjs{{else}}npm run harness:check{{/if}}{{/if}}{{/if}}{{/if}}{{/if}}` (phải pass trước mọi PR)
|
|
13
|
+
|
|
14
|
+
## Kiến trúc (tóm tắt)
|
|
15
|
+
|
|
16
|
+
Thứ tự layer, ép buộc bằng test cơ học:
|
|
17
|
+
|
|
18
|
+
**{{layersJoined}}** — code chỉ được phụ thuộc theo chiều tiến. Các mối
|
|
19
|
+
quan tâm cắt ngang (cross-cutting) vào qua `providers/`.
|
|
20
|
+
|
|
21
|
+
Sơ đồ đầy đủ và lý do: `docs/architecture.md` (chỉ tiếng Anh).
|
|
22
|
+
|
|
23
|
+
## Nguyên tắc vàng (phải giữ)
|
|
24
|
+
|
|
25
|
+
1. Ưu tiên utility chung trong `src/shared/` thay vì viết helper mới.
|
|
26
|
+
2. Validate ở biên hệ thống; không bao giờ "đoán mò" hình dạng dữ liệu.
|
|
27
|
+
3. Mỗi test là end-to-end qua một feature trong `feature_list.json`.
|
|
28
|
+
|
|
29
|
+
Danh sách đầy đủ: `docs/golden-principles.md`.
|
|
30
|
+
|
|
31
|
+
## Đọc khi cần (read on demand)
|
|
32
|
+
|
|
33
|
+
- `docs/architecture.md` — đọc khi thêm module hoặc dời code.
|
|
34
|
+
- `docs/adr/` — đọc khi đổi public API.
|
|
35
|
+
- `docs/golden-principles.md` — đọc trước mọi refactor.
|
|
36
|
+
- `feature_list.json` — đọc trước khi tuyên bố một feature đã xong.
|
|
37
|
+
- `.harness/PROGRESS.md` — đọc đầu session; ghi cuối session.
|
|
38
|
+
|
|
39
|
+
## Skills nên dùng
|
|
40
|
+
|
|
41
|
+
- `/inspect-module <path>` khi cần hiểu code đã có.
|
|
42
|
+
- `/add-feature <description>` khi thêm khả năng mới — không freestyle.
|
|
43
|
+
- `/structural-test-author <layer>` khi thêm rule kiến trúc mới.
|
|
44
|
+
- `/garbage-collection` mỗi thứ Sáu hoặc trước khi tag release.
|
|
45
|
+
- `/eval-runner` trước khi merge bất kỳ thay đổi nào ở skill / agent file.
|
|
46
|
+
|
|
47
|
+
## Subagents nên ủy thác (KHÔNG inline review)
|
|
48
|
+
|
|
49
|
+
- `architecture-reviewer` — cho mọi thay đổi cross-layer.
|
|
50
|
+
- `security-reviewer` — cho mọi thay đổi liên quan auth, input, secret.
|
|
51
|
+
- `reliability-reviewer` — cho mọi error path mới, retry loop, async boundary.
|
|
52
|
+
|
|
53
|
+
## Workflow contract
|
|
54
|
+
|
|
55
|
+
1. Bắt đầu session: chạy `/inspect-module .` và đọc `.harness/PROGRESS.md`.
|
|
56
|
+
2. Chọn MỘT feature trong `feature_list.json` có `passes: false`.
|
|
57
|
+
3. Triển khai. Chạy structural test. Nếu fail thì FIX trước khi tiếp tục.
|
|
58
|
+
4. Tự verify bằng subagent reviewer phù hợp.
|
|
59
|
+
5. Commit với message mô tả. Append một dòng vào `.harness/PROGRESS.md`.
|
|
60
|
+
6. Update `feature_list.json` (`passes: true`) **chỉ khi** end-to-end test pass.
|
|
61
|
+
|
|
62
|
+
## Cấm
|
|
63
|
+
|
|
64
|
+
- Không thêm layer mới mà không có ADR.
|
|
65
|
+
- Không disable structural test để PR pass.
|
|
66
|
+
- Không viết code mà structural test không thể phân tích (no dynamic
|
|
67
|
+
imports across layers).
|
|
68
|
+
- Không update CLAUDE.md mà không qua `/propose-harness-improvement`.
|
|
69
|
+
- Không cho CLAUDE.md vượt 200 instruction (hoặc maxTokens nếu đã set)
|
|
70
|
+
— Stop hook sẽ block. Phần thừa cho vào `docs/` hoặc @-import.
|