agent-harness-kit 0.10.0 → 0.10.2

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.
@@ -11,9 +11,9 @@
11
11
  "source": {
12
12
  "source": "github",
13
13
  "repo": "tuanle96/agent-harness-kit",
14
- "ref": "v0.10.0"
14
+ "ref": "v0.10.2"
15
15
  },
16
- "version": "0.10.0",
16
+ "version": "0.10.2",
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.10.0",
3
+ "version": "0.10.2",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-harness-kit",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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": {
@@ -20,6 +20,7 @@ import {
20
20
  pathForStack,
21
21
  buildContext,
22
22
  mergeHooksIntoSettings,
23
+ mergeStatusLineIntoSettings,
23
24
  USER_OWNED_FILES as USER_OWNED_FROM_RENDERER,
24
25
  EXEC_BITS,
25
26
  SUPPORTED_HUMAN_LANGS,
@@ -313,6 +314,16 @@ export async function upgrade({ cwd, kitVersion, yes }) {
313
314
  }
314
315
  }
315
316
 
317
+ // v0.8 — statusLine injection. Mirrors renderAll's tail (render-templates.mjs:474-480).
318
+ // Idempotent; never clobbers a user-customised statusLine.command pointing elsewhere.
319
+ if (existsSync(resolve(cwd, "scripts/statusline.mjs"))) {
320
+ const sl = await mergeStatusLineIntoSettings(cwd);
321
+ if (sl.changed) {
322
+ lockfile.files[".claude/settings.json"] = sha256(sl.rawContent);
323
+ console.log(pc.dim(` ${pc.green("~")} .claude/settings.json (statusLine merged)`));
324
+ }
325
+ }
326
+
316
327
  lockfile.version = kitVersion;
317
328
  await writeFile(lockPath, JSON.stringify(lockfile, null, 2) + "\n");
318
329
 
@@ -83,17 +83,41 @@ function outboundDeps(target) {
83
83
  return [...out].slice(0, 50);
84
84
  }
85
85
 
86
- function inboundDeps(target) {
86
+ function inboundDeps(target, cfg) {
87
87
  const relTarget = relative(ROOT, resolve(ROOT, target));
88
88
  const name = relTarget.split("/").pop().replace(/\.[a-z]+$/i, "");
89
89
  if (!name) return [];
90
90
  const seen = new Set();
91
+ const patterns = [];
92
+
93
+ // Standard pattern: import/from/require referencing the directory name.
94
+ // Works for TS/JS/Python where the import path mirrors the dir name.
95
+ patterns.push(
96
+ new RegExp(`(import|from|require\\().*['"][^'"]*${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`),
97
+ );
98
+
99
+ // Rust workspace pattern: when domain has `useIdentPattern` (e.g.
100
+ // "unibot_{layer}"), the crate is `use`d under its layer-derived ident
101
+ // — NOT the dir name. For `crates/unibot-types/` with pattern
102
+ // "unibot_{layer}", we should also match `use unibot_types::`. Without
103
+ // this branch the inbound list silently misses every workspace caller.
104
+ const layerInfo = whichLayer(target, cfg);
105
+ if (layerInfo) {
106
+ const domain = (cfg?.domains || []).find((d) => (d.name || "default") === layerInfo.domain);
107
+ if (domain?.useIdentPattern) {
108
+ const ident = domain.useIdentPattern.replace("{layer}", layerInfo.layer);
109
+ const escaped = ident.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
110
+ patterns.push(new RegExp(`\\b(?:pub\\s+)?use\\s+${escaped}\\b`));
111
+ }
112
+ }
113
+
91
114
  // Search the whole project root for references back to the target
92
115
  // 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]);
116
+ for (const re of patterns) {
117
+ for (const line of scan(".", re)) {
118
+ const m = line.match(/^([^:]+):\d+:/);
119
+ if (m && m[1] !== relTarget && !m[1].startsWith(`${relTarget}/`)) seen.add(m[1]);
120
+ }
97
121
  }
98
122
  return [...seen].slice(0, 30);
99
123
  }
@@ -103,14 +127,22 @@ function readLayers() {
103
127
  catch { return null; }
104
128
  }
105
129
 
130
+ // Resolve the layer for a module path. Honors `layerDirPattern` on the
131
+ // domain so workspaces that prefix layer directories (e.g. Rust workspace
132
+ // `crates/unibot-types/` with `layerDirPattern: "unibot-{layer}"`) match
133
+ // correctly. Without this, paths with custom prefixes would silently fail
134
+ // to match and return layer:null — the bug that ships /inspect-module's
135
+ // most useful columns blank.
106
136
  function whichLayer(target, cfg) {
107
137
  if (!cfg?.domains) return null;
108
138
  const rel = relative(ROOT, resolve(ROOT, target));
109
139
  for (const d of cfg.domains) {
110
140
  if (!d?.layers || !d.root) continue;
141
+ const pattern = d.layerDirPattern || "{layer}";
111
142
  for (const layer of d.layers) {
112
- const prefix = `${d.root}/${layer}/`;
113
- if (rel.startsWith(prefix) || rel === `${d.root}/${layer}`) {
143
+ const dirName = pattern.replace("{layer}", layer);
144
+ const prefix = `${d.root}/${dirName}/`;
145
+ if (rel.startsWith(prefix) || rel === `${d.root}/${dirName}`) {
114
146
  return { domain: d.name || "default", layer };
115
147
  }
116
148
  }
@@ -135,7 +167,7 @@ function main() {
135
167
  layer: whichLayer(target, cfg),
136
168
  exports: listExports(target),
137
169
  outbound: outboundDeps(target),
138
- inbound: inboundDeps(target),
170
+ inbound: inboundDeps(target, cfg),
139
171
  recent: recentCommits(target),
140
172
  };
141
173
  process.stdout.write(JSON.stringify(out, null, 2) + "\n");
@@ -4,13 +4,17 @@
4
4
  # Claude Code docs).
5
5
  #
6
6
  # Output line shape:
7
- # YYYY-MM-DD HH:MM | session_end | <reason> | <branch> | <sha>
7
+ # YYYY-MM-DD HH:MM | session_end | <reason> | <branch> | <sha> | <session_id>
8
8
  #
9
9
  # Example:
10
- # 2026-05-16 19:00 | session_end | clear | main | abc1234
10
+ # 2026-05-16 19:00 | session_end | clear | main | abc1234 | sess_abc123
11
11
  #
12
12
  # Reasons (per Claude Code docs): clear, resume, logout, prompt_input_exit,
13
13
  # bypass_permissions_disabled, other.
14
+ #
15
+ # Dedup: a line is only appended when (session_id, reason) differs from the
16
+ # most recent matching entry in PROGRESS.md. Prevents the duplicate-spam
17
+ # bug where Claude Code fires SessionEnd more than once on a single teardown.
14
18
  set -eo pipefail
15
19
 
16
20
  INPUT=$(cat)
@@ -30,10 +34,21 @@ jp() {
30
34
  fi
31
35
  }
32
36
 
33
- REASON="unknown"
37
+ REASON=""
38
+ SESSION_ID=""
34
39
  if have_jp; then
35
- REASON=$(echo "$INPUT" | jp '.end_reason // "unknown"' 2>/dev/null || echo "unknown")
40
+ # Fallback chain: prefer .end_reason (current Claude Code key), accept
41
+ # .reason as a legacy synonym. Done as two separate jp calls because the
42
+ # Node-fallback (json-pick.mjs) only supports a single `// default` per
43
+ # expression — chaining `// .reason //` would parse-fail there.
44
+ REASON=$(echo "$INPUT" | jp '.end_reason // ""' 2>/dev/null || echo "")
45
+ if [ -z "$REASON" ] || [ "$REASON" = "null" ]; then
46
+ REASON=$(echo "$INPUT" | jp '.reason // ""' 2>/dev/null || echo "")
47
+ fi
48
+ SESSION_ID=$(echo "$INPUT" | jp '.session_id // ""' 2>/dev/null || echo "")
49
+ [ "$SESSION_ID" = "null" ] && SESSION_ID=""
36
50
  fi
51
+ [ -z "$REASON" ] || [ "$REASON" = "null" ] && REASON="unknown"
37
52
 
38
53
  BR="(no-git)"
39
54
  SHA="(no-git)"
@@ -44,7 +59,21 @@ fi
44
59
 
45
60
  mkdir -p .harness
46
61
  TS=$(date +"%Y-%m-%d %H:%M")
47
- echo "$TS | session_end | $REASON | $BR | $SHA" >> .harness/PROGRESS.md
62
+ LINE="$TS | session_end | $REASON | $BR | $SHA | $SESSION_ID"
63
+
64
+ # Idempotency guard: when the SessionEnd hook fires twice on the same
65
+ # teardown (Claude Code sometimes emits both `clear` and a follow-up
66
+ # `prompt_input_exit`, or repeats the same reason), drop the duplicate
67
+ # rather than spamming PROGRESS.md. Match on (session_id, reason): if
68
+ # both are present in the most recent entry for this session, skip.
69
+ DEDUP_KEY="| $REASON | $BR | $SHA | $SESSION_ID"
70
+ if [ -f .harness/PROGRESS.md ] && \
71
+ [ -n "$SESSION_ID" ] && \
72
+ tail -n 5 .harness/PROGRESS.md 2>/dev/null | grep -qF "$DEDUP_KEY"; then
73
+ : # duplicate within the last 5 entries — skip silently
74
+ else
75
+ echo "$LINE" >> .harness/PROGRESS.md
76
+ fi
48
77
 
49
78
  # Rollup side-car — writes a JSONL record to .harness/telemetry.jsonl.
50
79
  # Best-effort: never blocks the cleanup-only SessionEnd contract.