agent-harness-kit 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +29 -0
  4. package/bin/cli.mjs +15 -1
  5. package/package.json +1 -1
  6. package/src/core/detect-stack.mjs +16 -0
  7. package/src/core/doctor.mjs +23 -0
  8. package/src/core/render-templates.mjs +198 -6
  9. package/src/templates/.claude/hooks/hooks.json +111 -0
  10. package/src/templates/.claude/settings.json.hbs +1 -1
  11. package/src/templates/.claude/skills/doc-drift-scan/SKILL.md +15 -10
  12. package/src/templates/.claude/skills/doc-drift-scan/scripts/scan-paths.mjs +64 -0
  13. package/src/templates/.claude/skills/garbage-collection/SKILL.md.hbs +14 -5
  14. package/src/templates/.claude/skills/garbage-collection/scripts/gc-classify.mjs +77 -0
  15. package/src/templates/.claude/skills/inspect-module/SKILL.md.hbs +17 -14
  16. package/src/templates/.claude/skills/inspect-module/scripts/module-summary.mjs +144 -0
  17. package/src/templates/CLAUDE.md.hbs +10 -6
  18. package/src/templates/CLAUDE.md.vi.hbs +74 -0
  19. package/src/templates/_adapter-kotlin/harness/structural-check.mjs.hbs +286 -0
  20. package/src/templates/_adapter-rust/harness/structural-check.mjs.hbs +292 -100
  21. package/src/templates/_adapter-swift/harness/structural-check.mjs.hbs +285 -0
  22. package/src/templates/harness.config.json.hbs +5 -3
  23. package/src/templates/scripts/_lib/approx-tokens.mjs +48 -0
  24. package/src/templates/scripts/_lib/json-pick.mjs +278 -0
  25. package/src/templates/scripts/harness-report.mjs +95 -1
  26. package/src/templates/scripts/notify-on-block.sh.hbs +73 -0
  27. package/src/templates/scripts/pre-compact.sh.hbs +121 -0
  28. package/src/templates/scripts/pre-push.sh +28 -3
  29. package/src/templates/scripts/precompletion-checklist.sh.hbs +131 -22
  30. package/src/templates/scripts/pretooluse-bash-guard.sh.hbs +146 -0
  31. package/src/templates/scripts/session-end.sh.hbs +48 -0
  32. package/src/templates/scripts/session-start.sh.hbs +139 -0
  33. package/src/templates/scripts/statusline.mjs +63 -0
  34. package/src/templates/scripts/structural-test-on-edit.sh.hbs +31 -8
  35. package/src/templates/scripts/telemetry-on-skill.sh +32 -10
  36. package/src/templates/scripts/userprompt-guard.sh.hbs +100 -0
  37. package/src/templates/.claude/hooks/hooks.json.hbs +0 -39
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // scan-paths.mjs — deterministic step for /doc-drift-scan.
3
+ // Walks docs/ + CLAUDE.md, extracts backtick paths, checks existsSync.
4
+ // Output JSON: { stats, drift }.
5
+
6
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
7
+ import { join, relative, resolve } from "node:path";
8
+
9
+ const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
10
+ const PATH_IN_BACKTICKS = /`([^`\s][^`]*?)`/g;
11
+ const PATH_LIKE = /^[^|&;$][\w./@-]+\/[\w./@-]+/;
12
+
13
+ function walkText() {
14
+ const out = [];
15
+ if (existsSync(join(ROOT, "CLAUDE.md"))) out.push(join(ROOT, "CLAUDE.md"));
16
+ if (existsSync(join(ROOT, "docs"))) {
17
+ for (const f of walkRecursive(join(ROOT, "docs"))) {
18
+ if (/\.(md|markdown|mdx)$/i.test(f)) out.push(f);
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+ function* walkRecursive(d) {
24
+ for (const e of readdirSync(d, { withFileTypes: true })) {
25
+ const p = join(d, e.name);
26
+ if (e.isDirectory()) yield* walkRecursive(p);
27
+ else yield p;
28
+ }
29
+ }
30
+ function extractPaths(body) {
31
+ const found = new Set();
32
+ let m;
33
+ while ((m = PATH_IN_BACKTICKS.exec(body)) !== null) {
34
+ const candidate = m[1].trim();
35
+ if (!PATH_LIKE.test(candidate)) continue;
36
+ if (/^https?:\/\//.test(candidate)) continue;
37
+ found.add(candidate);
38
+ }
39
+ return [...found];
40
+ }
41
+ function fileExistsRelative(p) {
42
+ const clean = p.replace(/:\d+(-\d+)?$/, "");
43
+ return existsSync(resolve(ROOT, clean));
44
+ }
45
+
46
+ function main() {
47
+ const files = walkText();
48
+ const drift = [];
49
+ const stats = { docs_scanned: files.length, refs_found: 0, refs_missing: 0 };
50
+ for (const doc of files) {
51
+ let body;
52
+ try { body = readFileSync(doc, "utf8"); } catch { continue; }
53
+ for (const ref of extractPaths(body)) {
54
+ stats.refs_found++;
55
+ if (!fileExistsRelative(ref)) {
56
+ stats.refs_missing++;
57
+ drift.push({ doc: relative(ROOT, doc), ref });
58
+ }
59
+ }
60
+ }
61
+ process.stdout.write(JSON.stringify({ stats, drift }, null, 2) + "\n");
62
+ }
63
+
64
+ main();
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: garbage-collection
3
3
  description: Use this skill on Fridays, before tagging a release, or when the user mentions "cleanup", "tech debt", "AI slop", "GC", or "garbage collection". Runs the deterministic linters, structural tests, and doc-drift scans, then proposes the top-3 highest-leverage cleanups (with risk/cost/benefit) — does NOT auto-merge. This is the solo-dev shrunk version of OpenAI's Friday garbage-collection ritual.
4
- allowed-tools: Read, Glob, Grep, Bash(npm run:*), Bash(pytest:*), Bash(ruff:*), Bash(git:*), Bash(gh:*)
4
+ allowed-tools: Read, Glob, Grep, Bash(npm run:*), Bash(pytest:*), Bash(ruff:*), Bash(git:*), Bash(gh:*), Bash(node .claude/skills/garbage-collection/scripts/gc-classify.mjs:*)
5
5
  suggested-turns: 15
6
6
  ---
7
7
 
@@ -19,10 +19,19 @@ suggested-turns: 15
19
19
  - **Doc drift** (a path in `docs/architecture.md` no longer exists) →
20
20
  invoke `doc-drift-scan` skill.
21
21
  - **Hand-rolled helper** matching a shared utility → propose replacement.
22
- 3. **Score** each candidate fix on three 1–5 dimensions:
23
- - **Risk**: 1 = trivial rename, 5 = touches multiple modules.
24
- - **Cost**: tokens + minutes.
25
- - **Benefit**: how often this drift recurs (read `.harness/gc-history.json`).
22
+ 3. **Score** each candidate fix on three 1–5 dimensions via the side-car
23
+ script (replaces the previous LLM-scored turn deterministic and
24
+ auditable):
25
+
26
+ ```bash
27
+ node .claude/skills/garbage-collection/scripts/gc-classify.mjs \
28
+ --baseline .harness/gc-<date>.json \
29
+ --history .harness/gc-history.json
30
+ ```
31
+
32
+ The script applies the mechanical rubric: `risk = 1 + ceil(touched/3)`,
33
+ `cost = 1 + ceil(lines/30)`, `benefit = recurrenceCount(class)`. Read
34
+ the JSON `candidates[]` sorted by `(benefit desc, cost asc, risk asc)`.
26
35
  4. **Propose ONLY the top 3** cleanups (solo-dev cap; OpenAI does dozens, you
27
36
  do 3). Open them as separate PRs with `gh pr create --label gc --draft`.
28
37
  5. **Append a row** to `.harness/gc-history.json`:
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // gc-classify.mjs — deterministic scoring step for /garbage-collection.
3
+ // Replaces "LLM-scored risk/cost/benefit" turn with mechanical rubric.
4
+ //
5
+ // Usage:
6
+ // gc-classify.mjs --baseline <gc-snapshot.json> [--history <hist.json>] [--out <file>]
7
+ //
8
+ // Rubric:
9
+ // risk = 1 + ceil(touched_files / 3) capped at 5
10
+ // cost = 1 + ceil(lines_to_change / 30) capped at 5
11
+ // benefit = recurrenceCount(class) from gc-history capped at 5
12
+
13
+ import { readFileSync, existsSync, writeFileSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+
16
+ function loadJSON(p, fallback = null) {
17
+ try { return JSON.parse(readFileSync(p, "utf8")); } catch { return fallback; }
18
+ }
19
+
20
+ function parseArgs(argv) {
21
+ const out = { baseline: null, history: ".harness/gc-history.json", out: null };
22
+ for (let i = 0; i < argv.length; i++) {
23
+ if (argv[i] === "--baseline") out.baseline = argv[++i];
24
+ else if (argv[i] === "--history") out.history = argv[++i];
25
+ else if (argv[i] === "--out") out.out = argv[++i];
26
+ }
27
+ if (!out.baseline) {
28
+ console.error("usage: gc-classify.mjs --baseline <gc-snapshot.json> [--history <hist.json>] [--out <file>]");
29
+ process.exit(2);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ function recurrenceCount(history, klass) {
35
+ if (!history?.runs) return 1;
36
+ let n = 0;
37
+ for (const run of history.runs) {
38
+ if (Array.isArray(run.classes_seen) && run.classes_seen.includes(klass)) n++;
39
+ }
40
+ return n;
41
+ }
42
+
43
+ function cap5(n) { return Math.max(1, Math.min(5, n)); }
44
+
45
+ function classify(baseline, history) {
46
+ const violations = Array.isArray(baseline?.violations) ? baseline.violations : [];
47
+ return violations.map((v) => {
48
+ const touched = Number(v.files_touched) || 1;
49
+ const lines = Number(v.lines_estimate) || 5;
50
+ return {
51
+ class: v.class || "unknown",
52
+ path: v.path || "(unspecified)",
53
+ summary: v.summary || `${v.class} at ${v.path || "(unspecified)"}`,
54
+ risk: cap5(1 + Math.ceil(touched / 3)),
55
+ cost: cap5(1 + Math.ceil(lines / 30)),
56
+ benefit: cap5(recurrenceCount(history, v.class)),
57
+ };
58
+ });
59
+ }
60
+
61
+ function main() {
62
+ const { baseline, history: histPath, out } = parseArgs(process.argv.slice(2));
63
+ const base = loadJSON(resolve(baseline));
64
+ if (!base) {
65
+ console.error(`gc-classify: cannot read baseline at ${baseline}`);
66
+ process.exit(2);
67
+ }
68
+ const hist = existsSync(resolve(histPath)) ? loadJSON(resolve(histPath), { runs: [] }) : { runs: [] };
69
+ const scored = classify(base, hist);
70
+ scored.sort((a, b) => b.benefit - a.benefit || a.cost - b.cost || a.risk - b.risk);
71
+ const payload = { total: scored.length, candidates: scored };
72
+ const text = JSON.stringify(payload, null, 2);
73
+ if (out) writeFileSync(resolve(out), text + "\n");
74
+ else process.stdout.write(text + "\n");
75
+ }
76
+
77
+ main();
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: inspect-module
3
3
  description: Use this skill whenever the user mentions "explore", "inspect", "understand", "what does X do", "where is Y", or before adding a new feature in an unfamiliar area. Produces a structured map of one module — files, exports, dependencies, layer assignment, and recent commits — without reading the entire codebase. Always invoke this skill before editing an unfamiliar module so the agent has accurate context, not guesses.
4
- allowed-tools: Read, Glob, Grep, Bash(git log:*), Bash(git ls-tree:*), Bash(tree:*)
4
+ allowed-tools: Read, Glob, Grep, Bash(git log:*), Bash(git ls-tree:*), Bash(tree:*), Bash(node .claude/skills/inspect-module/scripts/module-summary.mjs:*)
5
5
  suggested-turns: 6
6
6
  ---
7
7
 
@@ -15,19 +15,22 @@ feature Y, what's in the area?", "explore <path>", "show me the shape of
15
15
 
16
16
  1. **Resolve the target.** If the user gave a feature name (not a path), grep
17
17
  `feature_list.json` for it. If multiple paths match, ask the user which.
18
- 2. **Recent activity.** Run `git log --oneline -20 -- <target>` and read the
19
- top 3 commit messages.
20
- 3. **Layer.** Cross-reference the path against `harness.config.json` →
21
- `domains[].layers` to determine the layer.
22
- 4. **Public surface.** List exported symbols:
23
- - TypeScript: `grep -nE "^export " <target>/**/*.ts`
24
- - Python: `grep -nE "^def |^class |^[A-Z_][A-Z0-9_]+ ?=" <target>/**/*.py`
25
- 5. **Dependencies.** List inbound imports (who depends on this module) and
26
- outbound imports (what this module depends on). Verify both respect the
27
- forward-only layer order; if not, **stop and report the violation** before
28
- proceeding with any change plan.
29
- 6. **Risks.** Flag any of: dynamic imports, eval, shell-out with
30
- interpolation, missing tests for an exported function.
18
+ 2. **One-shot summary (deterministic).** Run the side-car script bundles
19
+ exports + inbound + outbound deps + layer + recent commits into one JSON
20
+ blob, replacing three LLM turns of grep:
21
+
22
+ ```bash
23
+ node .claude/skills/inspect-module/scripts/module-summary.mjs <target>
24
+ ```
25
+
26
+ Read the JSON. If `layer` is `null`, the file is outside any configured
27
+ layer root flag that and ask whether the user wants to add it.
28
+ 3. **Forward-only check.** Walk `outbound[]` and verify each crosses layers
29
+ forward only (never backward). The structural test enforces this
30
+ mechanically too, but flagging here short-circuits a wasted write step.
31
+ 4. **Risks.** Flag any of: dynamic imports, eval, shell-out with
32
+ interpolation, missing tests for an exported function. (LLM judgment —
33
+ the side-car reports facts, not risks.)
31
34
 
32
35
  ## Output contract
33
36
 
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // module-summary.mjs — deterministic step for /inspect-module.
3
+ // Bundles exports + outbound + inbound + layer + recent commits in JSON.
4
+ // Prefer ripgrep, fallback grep -rE.
5
+
6
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
7
+ import { resolve, relative, join } from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+
10
+ const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
11
+
12
+ function bail(msg) {
13
+ console.error("module-summary: " + msg);
14
+ process.exit(2);
15
+ }
16
+
17
+ // Walk a path (file or directory) and yield matching source files. Skip
18
+ // node_modules, .git, dist, build — folders that contain mountains of
19
+ // irrelevant exports and blow up the result set.
20
+ const SOURCE_EXTS = /\.(ts|tsx|js|jsx|mjs|cjs|py|rs|go|swift|kt|kts)$/i;
21
+ const SKIP_DIRS = new Set(["node_modules", ".git", ".harness", "dist", "build", "target", ".next"]);
22
+
23
+ function* walkSources(absPath) {
24
+ let st;
25
+ try { st = statSync(absPath); } catch { return; }
26
+ if (st.isFile()) {
27
+ if (SOURCE_EXTS.test(absPath)) yield absPath;
28
+ return;
29
+ }
30
+ if (!st.isDirectory()) return;
31
+ for (const entry of readdirSync(absPath, { withFileTypes: true })) {
32
+ if (entry.name.startsWith(".") && entry.name !== "." && entry.name !== "..") {
33
+ if (SKIP_DIRS.has(entry.name)) continue;
34
+ }
35
+ if (SKIP_DIRS.has(entry.name)) continue;
36
+ yield* walkSources(join(absPath, entry.name));
37
+ }
38
+ }
39
+
40
+ // scan: read each file line-by-line, run the regex, collect matches with
41
+ // per-line annotation `path:line: content`. Pure Node — no external grep
42
+ // dependency, so the script works the same on macOS local, Linux CI,
43
+ // minimal Alpine, etc. (Previous shell-out to grep failed on CI with an
44
+ // empty result set; root cause: spawn-time differences between BSD and
45
+ // GNU grep when the target argument is a single file. Node fs is the
46
+ // portable answer.)
47
+ function scan(target, regex) {
48
+ const lines = [];
49
+ const absTarget = resolve(ROOT, target);
50
+ for (const file of walkSources(absTarget)) {
51
+ let body;
52
+ try { body = readFileSync(file, "utf8"); } catch { continue; }
53
+ const rel = relative(ROOT, file);
54
+ const fileLines = body.split("\n");
55
+ for (let i = 0; i < fileLines.length; i++) {
56
+ if (regex.test(fileLines[i])) {
57
+ lines.push(`${rel}:${i + 1}: ${fileLines[i]}`);
58
+ }
59
+ }
60
+ }
61
+ return lines;
62
+ }
63
+
64
+ function listExports(target) {
65
+ const out = new Set();
66
+ for (const line of scan(target, /^export /)) {
67
+ const m = line.match(/^([^:]+):(\d+):\s*export\s+(.*)$/);
68
+ if (m) out.add(`${m[3].slice(0, 80)} (${m[1]}:${m[2]})`);
69
+ }
70
+ for (const line of scan(target, /^(def |class )/)) {
71
+ const m = line.match(/^([^:]+):(\d+):\s*(def|class)\s+(\w+)/);
72
+ if (m) out.add(`${m[3]} ${m[4]} (${m[1]}:${m[2]})`);
73
+ }
74
+ return [...out].slice(0, 50);
75
+ }
76
+
77
+ function outboundDeps(target) {
78
+ const out = new Set();
79
+ for (const line of scan(target, /^(import |from |use crate)/)) {
80
+ const m = line.match(/^[^:]+:\d+:\s*(.+)$/);
81
+ if (m) out.add(m[1].trim().slice(0, 100));
82
+ }
83
+ return [...out].slice(0, 50);
84
+ }
85
+
86
+ function inboundDeps(target) {
87
+ const relTarget = relative(ROOT, resolve(ROOT, target));
88
+ const name = relTarget.split("/").pop().replace(/\.[a-z]+$/i, "");
89
+ if (!name) return [];
90
+ const seen = new Set();
91
+ // Search the whole project root for references back to the target
92
+ // module. Filter out self-references.
93
+ const re = new RegExp(`(import|from|require\\().*['"][^'"]*${name.replace(/[.*+?^${}()|[\\\]\\\\]/g, "\\\\$&")}`);
94
+ for (const line of scan(".", re)) {
95
+ const m = line.match(/^([^:]+):\d+:/);
96
+ if (m && m[1] !== relTarget && !m[1].endsWith(`/${relTarget}`)) seen.add(m[1]);
97
+ }
98
+ return [...seen].slice(0, 30);
99
+ }
100
+
101
+ function readLayers() {
102
+ try { return JSON.parse(readFileSync(resolve(ROOT, "harness.config.json"), "utf8")); }
103
+ catch { return null; }
104
+ }
105
+
106
+ function whichLayer(target, cfg) {
107
+ if (!cfg?.domains) return null;
108
+ const rel = relative(ROOT, resolve(ROOT, target));
109
+ for (const d of cfg.domains) {
110
+ if (!d?.layers || !d.root) continue;
111
+ for (const layer of d.layers) {
112
+ const prefix = `${d.root}/${layer}/`;
113
+ if (rel.startsWith(prefix) || rel === `${d.root}/${layer}`) {
114
+ return { domain: d.name || "default", layer };
115
+ }
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ function recentCommits(target) {
122
+ const r = spawnSync("git", ["log", "--oneline", "-5", "--", target], { cwd: ROOT, encoding: "utf8" });
123
+ if (r.status !== 0) return [];
124
+ return (r.stdout || "").split("\n").filter(Boolean);
125
+ }
126
+
127
+ function main() {
128
+ const target = process.argv[2];
129
+ if (!target) bail("missing target path argument");
130
+ const abs = resolve(ROOT, target);
131
+ if (!existsSync(abs)) bail(`target not found: ${target}`);
132
+ const cfg = readLayers();
133
+ const out = {
134
+ module: relative(ROOT, abs),
135
+ layer: whichLayer(target, cfg),
136
+ exports: listExports(target),
137
+ outbound: outboundDeps(target),
138
+ inbound: inboundDeps(target),
139
+ recent: recentCommits(target),
140
+ };
141
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
142
+ }
143
+
144
+ main();
@@ -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
 
@@ -31,11 +31,15 @@ Full list: `docs/golden-principles.md`.
31
31
 
32
32
  ## Where to look (read on demand)
33
33
 
34
- - `docs/architecture.md` — read when adding a new module or moving code.
35
- - `docs/adr/` — read when changing public APIs.
36
- - `docs/golden-principles.md` — read before any refactor.
37
- - `feature_list.json` — read before claiming a feature is done.
38
- - `.harness/PROGRESS.md` read at session start; write at session end.
34
+ The lines below use Claude Code 2.1+ `@`-imports Claude loads the file
35
+ into context only when this section is referenced, keeping the working
36
+ CLAUDE.md tiny.
37
+
38
+ - @docs/architecture.md when adding a new module or moving code.
39
+ - @docs/adr/ — when changing public APIs.
40
+ - @docs/golden-principles.md — before any refactor.
41
+ - @feature_list.json — before claiming a feature is done.
42
+ - `.harness/PROGRESS.md` — read at session start; append at session end (kit-managed, not @-imported).
39
43
 
40
44
  ## Skills you should use
41
45
 
@@ -0,0 +1,74 @@
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
+ Các dòng dưới dùng cú pháp `@`-import của Claude Code 2.1+ — Claude chỉ
34
+ nạp file vào context khi section này được tham chiếu, giữ CLAUDE.md
35
+ luôn gọn.
36
+
37
+ - @docs/architecture.md — khi thêm module hoặc dời code.
38
+ - @docs/adr/ — khi đổi public API.
39
+ - @docs/golden-principles.md — trước mọi refactor.
40
+ - @feature_list.json — trước khi tuyên bố một feature đã xong.
41
+ - `.harness/PROGRESS.md` — đọc đầu session; append cuối session (kit quản lý, không @-import).
42
+
43
+ ## Skills nên dùng
44
+
45
+ - `/inspect-module <path>` khi cần hiểu code đã có.
46
+ - `/add-feature <description>` khi thêm khả năng mới — không freestyle.
47
+ - `/structural-test-author <layer>` khi thêm rule kiến trúc mới.
48
+ - `/garbage-collection` mỗi thứ Sáu hoặc trước khi tag release.
49
+ - `/eval-runner` trước khi merge bất kỳ thay đổi nào ở skill / agent file.
50
+
51
+ ## Subagents nên ủy thác (KHÔNG inline review)
52
+
53
+ - `architecture-reviewer` — cho mọi thay đổi cross-layer.
54
+ - `security-reviewer` — cho mọi thay đổi liên quan auth, input, secret.
55
+ - `reliability-reviewer` — cho mọi error path mới, retry loop, async boundary.
56
+
57
+ ## Workflow contract
58
+
59
+ 1. Bắt đầu session: chạy `/inspect-module .` và đọc `.harness/PROGRESS.md`.
60
+ 2. Chọn MỘT feature trong `feature_list.json` có `passes: false`.
61
+ 3. Triển khai. Chạy structural test. Nếu fail thì FIX trước khi tiếp tục.
62
+ 4. Tự verify bằng subagent reviewer phù hợp.
63
+ 5. Commit với message mô tả. Append một dòng vào `.harness/PROGRESS.md`.
64
+ 6. Update `feature_list.json` (`passes: true`) **chỉ khi** end-to-end test pass.
65
+
66
+ ## Cấm
67
+
68
+ - Không thêm layer mới mà không có ADR.
69
+ - Không disable structural test để PR pass.
70
+ - Không viết code mà structural test không thể phân tích (no dynamic
71
+ imports across layers).
72
+ - Không update CLAUDE.md mà không qua `/propose-harness-improvement`.
73
+ - Không cho CLAUDE.md vượt 200 instruction (hoặc maxTokens nếu đã set)
74
+ — Stop hook sẽ block. Phần thừa cho vào `docs/` hoặc @-import.