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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -0
- package/bin/cli.mjs +15 -1
- package/package.json +1 -1
- package/src/core/detect-stack.mjs +16 -0
- package/src/core/doctor.mjs +23 -0
- package/src/core/render-templates.mjs +198 -6
- package/src/templates/.claude/hooks/hooks.json +111 -0
- package/src/templates/.claude/settings.json.hbs +1 -1
- package/src/templates/.claude/skills/doc-drift-scan/SKILL.md +15 -10
- package/src/templates/.claude/skills/doc-drift-scan/scripts/scan-paths.mjs +64 -0
- package/src/templates/.claude/skills/garbage-collection/SKILL.md.hbs +14 -5
- package/src/templates/.claude/skills/garbage-collection/scripts/gc-classify.mjs +77 -0
- package/src/templates/.claude/skills/inspect-module/SKILL.md.hbs +17 -14
- package/src/templates/.claude/skills/inspect-module/scripts/module-summary.mjs +144 -0
- package/src/templates/CLAUDE.md.hbs +10 -6
- package/src/templates/CLAUDE.md.vi.hbs +74 -0
- package/src/templates/_adapter-kotlin/harness/structural-check.mjs.hbs +286 -0
- package/src/templates/_adapter-rust/harness/structural-check.mjs.hbs +292 -100
- 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/notify-on-block.sh.hbs +73 -0
- package/src/templates/scripts/pre-compact.sh.hbs +121 -0
- package/src/templates/scripts/pre-push.sh +28 -3
- package/src/templates/scripts/precompletion-checklist.sh.hbs +131 -22
- 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/statusline.mjs +63 -0
- package/src/templates/scripts/structural-test-on-edit.sh.hbs +31 -8
- package/src/templates/scripts/telemetry-on-skill.sh +32 -10
- package/src/templates/scripts/userprompt-guard.sh.hbs +100 -0
- 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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. **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
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.
|