entropy-machines 0.1.7 → 0.1.8

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/bin/dispatch CHANGED
@@ -199,6 +199,17 @@ if [ -n "$hits" ]; then
199
199
  fi
200
200
  fi
201
201
 
202
+ # CHECK — issue existence. A nonexistent issue is usually a typo. Checked
203
+ # after scope/history so safety guards always fire first.
204
+ if ! "$ENTROPY_MACHINES_HOME/bin/tracker" show "$id" >/dev/null 2>&1; then
205
+ if [ "$force" != "1" ]; then
206
+ echo "dispatch: REFUSED — issue '$id' not found in the tracker." >&2
207
+ echo " Check the id for typos, or pass --force to dispatch anyway." >&2
208
+ exit 1
209
+ fi
210
+ echo "dispatch: WARNING — issue '$id' not found in the tracker (--force overrides)."
211
+ fi
212
+
202
213
  # CHECK 4 — recently-touched files. A declared file that was touched in the
203
214
  # last 5 commits on any branch is hot: the change may still be landing, the
204
215
  # agent may be re-doing work that already shipped, or the base commit already
@@ -661,6 +672,7 @@ fi
661
672
  echo
662
673
  if [ "$dry" -eq 1 ]; then
663
674
  echo "dispatch: OK (dry run) — nothing recorded on $id."
675
+ exit 0
664
676
  else
665
677
  echo "dispatch: OK — brief + scope recorded on $id."
666
678
  fi
@@ -0,0 +1,116 @@
1
+ #!/bin/sh
2
+ # PreToolUse hook: refuse Agent calls for tracked issues that have no
3
+ # DISPATCH note in the tracker log. Forces all issue work through the
4
+ # dispatch pipeline (bin/dispatch-run → worktree → auto-verify → handoff).
5
+ #
6
+ # Same mechanism as bin/no-fork-gate. Registered in .claude/settings.json
7
+ # as a PreToolUse hook on "Agent".
8
+ #
9
+ # WHAT IT CHECKS:
10
+ # 1. Does the Agent call's prompt mention a tracked issue id (i-<slug>)?
11
+ # 2. If so, does the tracker's note log have a DISPATCH note for that id?
12
+ # 3. If no DISPATCH note, REFUSE — the agent must go through dispatch.
13
+ #
14
+ # WHAT IT ALLOWS:
15
+ # - Agent calls with no issue id in the prompt (general work, not tracked)
16
+ # - Agent calls where the issue has been properly dispatched
17
+ # - Verifier agents (prompt starts with "VERIFIER for issue")
18
+ #
19
+ # WHY THE PROMPT, NOT THE NAME. The Agent tool's "name" field is optional
20
+ # and often missing. The prompt always contains the issue id when work is
21
+ # being done on a tracked issue.
22
+
23
+ set -e
24
+
25
+ # TOOL_INPUT is the JSON body of the Agent tool call, set by Claude Code.
26
+ prompt=$(printf '%s' "$TOOL_INPUT" | python3 -c '
27
+ import json, sys
28
+ try:
29
+ data = json.loads(sys.stdin.read())
30
+ print(data.get("prompt", ""))
31
+ except Exception:
32
+ pass
33
+ ')
34
+
35
+ # Allow verifier agents — they are launched BY the dispatch pipeline.
36
+ case "$prompt" in
37
+ "VERIFIER for issue"*) exit 0 ;;
38
+ "Verify"*) exit 0 ;;
39
+ esac
40
+
41
+ # Extract issue ids from the prompt.
42
+ ids=$(printf '%s' "$prompt" | grep -oE '\bi-[a-z0-9][a-z0-9-]{4,}' | sort -u || true)
43
+
44
+ # No issue ids → not tracked work → allow.
45
+ [ -n "$ids" ] || exit 0
46
+
47
+ # Resolve the project root.
48
+ if [ -z "${ENTROPY_MACHINES_HOME:-}" ]; then
49
+ # Walk up from this script to find the harness.
50
+ ENTROPY_MACHINES_HOME="$(cd "$(dirname "$0")/.." && pwd -P)"
51
+ fi
52
+ . "$ENTROPY_MACHINES_HOME/lib/roots.sh"
53
+ root=$(entropy_machines_root 2>/dev/null || true)
54
+
55
+ # No project root → can't check → allow (don't block on misconfiguration).
56
+ [ -n "$root" ] || exit 0
57
+
58
+ # Read the tracker's note log.
59
+ tracker="$ENTROPY_MACHINES_HOME/bin/tracker"
60
+ [ -x "$tracker" ] || exit 0
61
+
62
+ for id in $ids; do
63
+ # Check whether the issue exists in the tracker at all.
64
+ issue_json=$("$tracker" show "$id" 2>/dev/null || true)
65
+ if [ -z "$issue_json" ]; then
66
+ # Issue not in the tracker — not tracked work. Allow.
67
+ continue
68
+ fi
69
+
70
+ # Issue exists. Check whether it's already done — done issues don't need
71
+ # dispatch (verifiers, follow-ups, etc.).
72
+ is_done=$(printf '%s' "$issue_json" | python3 -c '
73
+ import json, sys
74
+ try:
75
+ d = json.loads(sys.stdin.read())
76
+ if d.get("status") == "done":
77
+ print("yes")
78
+ except Exception:
79
+ pass
80
+ ' 2>/dev/null || true)
81
+ [ "$is_done" = "yes" ] && continue
82
+
83
+ # Issue exists and is not done. Check for a DISPATCH note.
84
+ notes=$("$tracker" notes --issue "$id" 2>/dev/null || true)
85
+ has_dispatch=""
86
+ if [ -n "$notes" ]; then
87
+ has_dispatch=$(printf '%s' "$notes" | python3 -c '
88
+ import json, sys
89
+ for line in sys.stdin:
90
+ line = line.strip()
91
+ if not line:
92
+ continue
93
+ try:
94
+ r = json.loads(line)
95
+ except Exception:
96
+ continue
97
+ verb = r.get("verb", "")
98
+ text = r.get("text", "")
99
+ if isinstance(text, str) and text.startswith("DISPATCH "):
100
+ print("yes")
101
+ break
102
+ if verb == "DISPATCH":
103
+ print("yes")
104
+ break
105
+ ' 2>/dev/null || true)
106
+ fi
107
+
108
+ if [ -z "$has_dispatch" ]; then
109
+ echo "DENIED: Agent call references issue $id, but no DISPATCH note exists in the tracker." >&2
110
+ echo " Use the MCP dispatch_issue tool or bin/dispatch-run to dispatch through the full pipeline." >&2
111
+ echo " This ensures pre-flight checks, worktree isolation, and auto-verification." >&2
112
+ exit 2
113
+ fi
114
+ done
115
+
116
+ exit 0
@@ -0,0 +1,245 @@
1
+ #!/bin/sh
2
+ # One-shot attended dispatch: pre-flight → worker → verify → report.
3
+ #
4
+ # bin/dispatch-run <issue-id> --brief "one-line task" --files "path1 path2"
5
+ # [--anyway] [--force] [--timeout <seconds>]
6
+ #
7
+ # Chains the full dispatch pipeline as a single blocking command:
8
+ # 1. bin/dispatch (pre-flight checks, context file, DISPATCH note, claim)
9
+ # 2. Worker agent (claude -p, isolated worktree, writes HANDOFF.md)
10
+ # 3. bin/auto-verify inline (polls for HANDOFF.md, zero tokens)
11
+ # 4. Verifier agent (claude -p, interrogate → lift → test → verify)
12
+ # 5. Exits — orchestrator picks up at fold (review + commit)
13
+ #
14
+ # Modeled on bin/drain-run.sh, which does the same for scheduled batch work.
15
+ # The difference: drain-run picks its own issues; this one takes an explicit
16
+ # issue id and runs interactively (no armed flag, no branch guard).
17
+ #
18
+ # The MCP dispatch_issue tool shells out to this script. The orchestrator
19
+ # (Claude Code) calls the MCP tool; this script runs the pipeline; the
20
+ # orchestrator gets back a result when verification is complete.
21
+ #
22
+ # EXIT CODES:
23
+ # 0 verified and ready for fold 3 bin/dispatch pre-flight refused
24
+ # 1 internal error 4 worker timed out
25
+ # 2 usage error 5 auto-verify timed out
26
+ # 6 verifier timed out / failed
27
+ set -e
28
+
29
+ usage() {
30
+ echo "usage: bin/dispatch-run <issue-id> --brief \"<text>\" --files \"<paths>\" [--anyway] [--force] [--timeout <sec>]" >&2
31
+ exit 2
32
+ }
33
+
34
+ id=""; brief=""; files=""; anyway=""; force=""; timeout_s=5400
35
+ while [ $# -gt 0 ]; do
36
+ case "$1" in
37
+ --brief) brief="$2"; shift 2 ;;
38
+ --files) files="$2"; shift 2 ;;
39
+ --anyway) anyway="--anyway"; shift ;;
40
+ --force) force="--force"; shift ;;
41
+ --timeout) timeout_s="$2"; shift 2 ;;
42
+ -h|--help) usage ;;
43
+ -*) usage ;;
44
+ *) [ -n "$id" ] && usage; id="$1"; shift ;;
45
+ esac
46
+ done
47
+ [ -n "$id" ] && [ -n "$brief" ] && [ -n "$files" ] || usage
48
+
49
+ . "$(dirname "$0")/../lib/roots.sh"
50
+ ENTROPY_MACHINES_HOME=$(entropy_machines_home "$0")
51
+ entropy_machines_require_root dispatch-run
52
+
53
+ cd "$ENTROPY_MACHINES_ROOT"
54
+
55
+ CONFIG_PY="$ENTROPY_MACHINES_HOME/lib/config.py"
56
+
57
+ cfg() {
58
+ python3 "$CONFIG_PY" get "$1" 2>/dev/null || echo "$2"
59
+ }
60
+
61
+ stamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
62
+ slug=$(date -u +%Y%m%d-%H%M%S)
63
+ outdir="$ENTROPY_MACHINES_ROOT/.entropy-machines/dispatch-runs/$slug"
64
+ mkdir -p "$outdir"
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # STEP 1 — bin/dispatch (pre-flight, context file, DISPATCH note)
68
+ # ---------------------------------------------------------------------------
69
+ echo "dispatch-run($id): step 1 — pre-flight and context file"
70
+ dispatch_args="$id --files \"$files\" --brief \"$brief\""
71
+ [ -n "$anyway" ] && dispatch_args="$dispatch_args $anyway"
72
+ [ -n "$force" ] && dispatch_args="$dispatch_args $force"
73
+
74
+ dispatch_out="$outdir/dispatch.out"
75
+ dispatch_err="$outdir/dispatch.err"
76
+ if ! eval "$ENTROPY_MACHINES_HOME/bin/dispatch $dispatch_args" >"$dispatch_out" 2>"$dispatch_err"; then
77
+ echo "dispatch-run($id): REFUSED — bin/dispatch pre-flight failed:" >&2
78
+ cat "$dispatch_err" >&2
79
+ exit 3
80
+ fi
81
+
82
+ echo "dispatch-run($id): pre-flight passed."
83
+
84
+ # Parse the agent call JSON from bin/dispatch's structured output.
85
+ worker_json=$(sed -n '/^--- AGENT CALL ---$/,/^--- END AGENT CALL ---$/p' "$dispatch_out" | grep -v '^---')
86
+ if [ -z "$worker_json" ]; then
87
+ echo "dispatch-run($id): ERROR — could not parse agent call from bin/dispatch output" >&2
88
+ exit 1
89
+ fi
90
+
91
+ # Extract the worker prompt from the JSON.
92
+ worker_prompt=$(printf '%s' "$worker_json" | python3 -c '
93
+ import json, sys
94
+ call = json.loads(sys.stdin.read())
95
+ print(call["prompt"])
96
+ ')
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # STEP 1.5 — Create an isolated worktree for the worker
100
+ # ---------------------------------------------------------------------------
101
+ # dispatch-run manages the worktree lifecycle itself. The orchestrator flow
102
+ # uses the Agent tool's isolation: "worktree" parameter, but claude -p has
103
+ # no such parameter — so we create and populate the worktree here.
104
+ # git worktree add triggers the post-checkout hook, which links shared paths
105
+ # (config.json, node_modules, etc.) automatically if hooks are installed.
106
+ wt_branch="dispatch/$id/$slug"
107
+ wt_dir="$ENTROPY_MACHINES_ROOT/.claude/worktrees/dispatch-$id-$slug"
108
+
109
+ echo "dispatch-run($id): creating worktree at $wt_dir"
110
+ mkdir -p "$(dirname "$wt_dir")"
111
+ if ! git worktree add -b "$wt_branch" "$wt_dir" HEAD 2>"$outdir/worktree.err"; then
112
+ echo "dispatch-run($id): ERROR — could not create worktree:" >&2
113
+ cat "$outdir/worktree.err" >&2
114
+ exit 1
115
+ fi
116
+
117
+ echo "$id" > "$wt_dir/.dispatch-issue"
118
+ echo "dispatch-run($id): worktree ready, .dispatch-issue written."
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # STEP 2 — Worker agent (isolated worktree)
122
+ # ---------------------------------------------------------------------------
123
+ echo "dispatch-run($id): step 2 — launching worker agent in $wt_dir"
124
+
125
+ # Agent CLI is pluggable, same as drain-run.
126
+ set --
127
+ while IFS= read -r tok; do
128
+ [ -n "$tok" ] && set -- "$@" "$tok"
129
+ done <<AGENTCMD
130
+ $(python3 - "$ENTROPY_MACHINES_ROOT/config.json" <<'PY'
131
+ import json, sys
132
+ try:
133
+ data = json.load(open(sys.argv[1]))
134
+ cmd = data.get("unattended", {}).get("agent", {}).get("cmd") or ["claude", "-p"]
135
+ except Exception:
136
+ cmd = ["claude", "-p"]
137
+ for tok in cmd:
138
+ print(tok)
139
+ PY
140
+ )
141
+ AGENTCMD
142
+
143
+ (cd "$wt_dir" && exec "$@" "$worker_prompt") > "$outdir/worker.json" 2> "$outdir/worker.err" &
144
+ worker_pid=$!
145
+ deadline=$(( $(date +%s) + timeout_s ))
146
+
147
+ while kill -0 "$worker_pid" 2>/dev/null; do
148
+ if [ "$(date +%s)" -gt "$deadline" ]; then
149
+ kill -TERM "$worker_pid" 2>/dev/null || true
150
+ sleep 5
151
+ kill -KILL "$worker_pid" 2>/dev/null || true
152
+ echo "dispatch-run($id): TIMEOUT — worker killed after $((timeout_s / 60))m" >&2
153
+ exit 4
154
+ fi
155
+ sleep 10
156
+ done
157
+
158
+ wait "$worker_pid" || true
159
+ echo "dispatch-run($id): worker finished."
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # STEP 3 — Check for HANDOFF.md (worker already finished, check directly)
163
+ # ---------------------------------------------------------------------------
164
+ # No polling needed: dispatch-run created the worktree and waited for the
165
+ # worker synchronously. The worktree path is known; just check it.
166
+ worker_wt="$wt_dir"
167
+
168
+ if [ -f "$worker_wt/HANDOFF.md" ]; then
169
+ echo "dispatch-run($id): HANDOFF.md found at $worker_wt"
170
+ else
171
+ echo "dispatch-run($id): worker finished but left no HANDOFF.md at $worker_wt" >&2
172
+ echo " The worker may have exited early or failed. Check:" >&2
173
+ echo " $outdir/worker.err" >&2
174
+ echo " $outdir/worker.json" >&2
175
+ exit 5
176
+ fi
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # STEP 4 — Verifier agent
180
+ # ---------------------------------------------------------------------------
181
+ echo "dispatch-run($id): step 4 — launching verifier agent"
182
+
183
+ ctx="$ENTROPY_MACHINES_ROOT/.dispatch-context/$id.md"
184
+
185
+ verifier_prompt="VERIFIER for issue $id.
186
+
187
+ A worker agent completed implementation in worktree: $worker_wt
188
+ Dispatch context: $ctx
189
+
190
+ Read BOTH files before doing anything else.
191
+
192
+ ## STEP 1 — INTERROGATE THE WORKER
193
+ Run: bin/handoff $id --interrogate
194
+ Use ListAgents to find the worker agent named \"$id\". Send ALL questions via SendMessage.
195
+ Wait for the worker to respond. Record the answers:
196
+ bin/handoff $id --record-interrogation --answer \"<answers>\"
197
+ If the worker cannot be reached, answer from artifacts:
198
+ 1. Read $worker_wt/HANDOFF.md
199
+ 2. Read the changed files in the worktree
200
+
201
+ ## STEP 2 — LIFT
202
+ Run: bin/handoff $id --from $worker_wt --lift
203
+ If any files are rejected (main drifted), merge manually.
204
+
205
+ ## STEP 3 — TEST
206
+ Run: tests/run
207
+ All tests must pass. Fix failures caused by the lift.
208
+
209
+ ## STEP 4 — VERIFY (do NOT commit)
210
+ Run: bin/handoff $id --from $worker_wt --verified \"auto-verified: tests passed\"
211
+ If HANDOFF.md had found: none AND next: none, add --clean.
212
+ If it had real found: or next: values, pass --found \"...\" --next \"...\"
213
+
214
+ DO NOT commit or push. Report what you verified and stop.
215
+ The orchestrator reviews your work and commits."
216
+
217
+ "$@" "$verifier_prompt" > "$outdir/verifier.json" 2> "$outdir/verifier.err" &
218
+ verifier_pid=$!
219
+ deadline=$(( $(date +%s) + timeout_s ))
220
+
221
+ while kill -0 "$verifier_pid" 2>/dev/null; do
222
+ if [ "$(date +%s)" -gt "$deadline" ]; then
223
+ kill -TERM "$verifier_pid" 2>/dev/null || true
224
+ sleep 5
225
+ kill -KILL "$verifier_pid" 2>/dev/null || true
226
+ echo "dispatch-run($id): TIMEOUT — verifier killed after $((timeout_s / 60))m" >&2
227
+ exit 6
228
+ fi
229
+ sleep 10
230
+ done
231
+
232
+ wait "$verifier_pid" || true
233
+ echo "dispatch-run($id): verifier finished."
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # REPORT — ready for fold
237
+ # ---------------------------------------------------------------------------
238
+ echo
239
+ echo "dispatch-run($id): READY FOR FOLD"
240
+ echo " worker worktree: $worker_wt"
241
+ echo " dispatch context: $ctx"
242
+ echo " logs: $outdir/"
243
+ echo
244
+ echo "Review the staged changes in this checkout, then commit."
245
+ echo "The commit-msg hook (handoff-guard) will enforce the handoff record."
package/bin/doclint CHANGED
@@ -40,11 +40,9 @@ WHAT IT ENFORCES, AND WHY THOSE TWO THINGS
40
40
  this rule exists to catch is a doc that LOOKS answerable and is not.
41
41
 
42
42
  WHAT IT DELIBERATELY DOES NOT ENFORCE: the look. Colours, fonts, type scale,
43
- layout, whether there is a theme toggle — all of it is the author's. The house
44
- style in lib/REPORT-TEMPLATE.html is a good default and a starting point, not
45
- a conformance target. Somebody using this harness may want a report that looks
46
- nothing like ours, and that is fine; a report that fetches from the internet is
47
- not.
43
+ layout, whether there is a theme toggle — all of it is the author's. Somebody
44
+ using this harness may want a report that looks nothing like ours, and that is
45
+ fine; a report that fetches from the internet is not.
48
46
 
49
47
  TARGETED REGEX, NOT AN HTML PARSER. Deliberate, and the same thing the rest of
50
48
  this harness does (bin/serve's save merge and answer counts, bin/status's
@@ -57,7 +55,7 @@ slip past. A doc built by hand or from the template does not do those things.
57
55
 
58
56
  HTML COMMENTS ARE MASKED BEFORE ANYTHING IS COUNTED. A commented-out remote
59
57
  link loads nothing, and — the expensive half — a comment that DOCUMENTS the
60
- answer-box markup is not a question. lib/doc-template.html once carried a
58
+ answer-box markup is not a question. The old doc template once carried a
61
59
  literal example in its header comment; every regex reader of these docs counted
62
60
  it, so each doc reported one phantom unanswered question nobody could answer
63
61
  and "fully answered" was unreachable. Masking keeps line numbers intact.
@@ -438,8 +436,7 @@ def main(argv):
438
436
  "answer in place.")
439
437
  print(" It never fetches from an external site, and every section "
440
438
  "has a box to")
441
- print(" answer in. Start from lib/REPORT-TEMPLATE.html. The look is "
442
- "yours; these")
439
+ print(" answer in. The look is yours; these")
443
440
  print(" two rules are not.")
444
441
  return 1
445
442
 
package/bin/drain CHANGED
@@ -35,7 +35,7 @@ os=$(uname)
35
35
  # refusal is this script's job for the keys that truly have none, e.g. an
36
36
  # unsupported scheduler below).
37
37
  cfg() {
38
- python3 - "$ENTROPY_MACHINES_HOME/config.json" "$1" "$2" <<'PY'
38
+ python3 - "${ENTROPY_MACHINES_CONFIG:-$ENTROPY_MACHINES_HOME/config.json}" "$1" "$2" <<'PY'
39
39
  import json, sys
40
40
  path, key, default = sys.argv[1], sys.argv[2], sys.argv[3]
41
41
  try:
@@ -42,7 +42,7 @@ const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf
42
42
  // README names because the harness points at it from inside itself —
43
43
  // lib/roots.sh's refusal message cites `<harness>/docs/CONFIG.md`, and
44
44
  // docs/QUICKSTART.md is the document the next agent is told to read.
45
- const HARNESS_DIRS = ['bin', 'lib', 'docs', 'doctrine', 'hooks', 'agents'];
45
+ const HARNESS_DIRS = ['bin', 'lib', 'docs', 'doctrine', 'hooks', 'agents', 'ui'];
46
46
 
47
47
  // Never copied, at any depth. `.git` is the load-bearing one.
48
48
  const NEVER_COPY = new Set(['.git', '__pycache__', 'node_modules', '.DS_Store']);
package/bin/handoff CHANGED
@@ -165,7 +165,7 @@ set -e
165
165
  usage() {
166
166
  echo "usage: bin/handoff <issue-id> --changed \"...\" --verified \"...\"" >&2
167
167
  echo " [--found \"...\"]... [--assumed \"...\"]... [--next \"...\"] [--clean]" >&2
168
- echo " [--drift-reviewed \"why the worker's version is right\"]" >&2
168
+ echo " [--drift-reviewed \"why the worker's version is right\"] [--no-cleanup]" >&2
169
169
  echo " or: bin/handoff <issue-id> --from <agent-worktree> --verified \"...\"" >&2
170
170
  echo " or: bin/handoff <issue-id> --from <agent-worktree> --lift (copy, record nothing)" >&2
171
171
  echo " or: bin/handoff <issue-id> --interrogate [--ask \"...\"] (print the questions)" >&2
@@ -176,7 +176,7 @@ usage() {
176
176
  id=""; changed=""; verified=""; next_note=""; clean=0; dry=0
177
177
  found=""; assumed=""; from=""; lift=0; drift_reviewed=""
178
178
  interrogate=0; record_interrogation=0; ask=""; answers=""; n_answers=0; blank_answer=0
179
- no_interrogation=""
179
+ no_interrogation=""; no_cleanup=0
180
180
  while [ $# -gt 0 ]; do
181
181
  case "$1" in
182
182
  --changed) changed="$2"; shift 2 ;;
@@ -216,6 +216,10 @@ while [ $# -gt 0 ]; do
216
216
  # The escape hatch, and it lands on the record as a positive claim rather
217
217
  # than an absence — the same shape as --clean and --drift-reviewed.
218
218
  --no-interrogation) no_interrogation="$2"; shift 2 ;;
219
+ # Suppress the automatic cleanup of the source worktree after a successful
220
+ # record. The rare case where the lander wants to keep the worktree around
221
+ # (e.g. to re-read the worker's full tree during a subsequent review pass).
222
+ --no-cleanup) no_cleanup=1; shift ;;
219
223
  # Print the note and write nothing. The log is shared and append-only with
220
224
  # no delete, so "let me see what this records" needs an answer that is not
221
225
  # "record it and find out".
@@ -1158,7 +1162,7 @@ echo " The commit-msg guard for $id is now satisfied."
1158
1162
  #
1159
1163
  # ONLY DOCS THAT APPEAR IN THE CHANGED-FILES LIST ($tmp/changes) ARE OPENED.
1160
1164
  # No fallback to dispatch scope or dispatch context — those opened every doc
1161
- # merely REFERENCED by a file like enhance.py, not docs that actually changed.
1165
+ # merely REFERENCED by an infrastructure file, not docs that actually changed.
1162
1166
  #
1163
1167
  # BEST-EFFORT: wrapped in a subshell whose failure is caught and discarded. A
1164
1168
  # doc that cannot be opened, a serve that cannot start, a port that cannot be
@@ -1228,3 +1232,61 @@ _open_changed_docs() {
1228
1232
  done
1229
1233
  }
1230
1234
  _open_changed_docs 2>/dev/null || true
1235
+
1236
+ # --- auto-clean the source worktree (best-effort) ----------------------------
1237
+ #
1238
+ # After a successful record, the source worktree's work has been lifted into
1239
+ # this checkout and the handoff note is written — the worktree has no further
1240
+ # use. Leaving it around wastes disk and accumulates stale branches; over 70
1241
+ # agent-* worktrees were found in a single checkout during a sprint.
1242
+ #
1243
+ # ONLY AGENT WORKTREES ARE CLEANED. The pattern `.claude/worktrees/agent-*` is
1244
+ # what bin/dispatch creates and what bin/auto-verify polls for. A --from that
1245
+ # points at any other directory is left alone — it may be a human's worktree
1246
+ # or a worktree managed by another tool.
1247
+ #
1248
+ # BEST-EFFORT: the record already succeeded, so a cleanup failure must not
1249
+ # turn a green landing red. The git branch is also deleted — `git worktree
1250
+ # add` creates a `worktree-agent-*` branch that serves no purpose after the
1251
+ # worktree is gone.
1252
+ #
1253
+ # --no-cleanup suppresses this, for the rare case where the lander wants to
1254
+ # keep the worktree around (e.g. to re-read the worker's tree during review).
1255
+ # It is parsed at the top alongside the other flags.
1256
+ _cleanup_source_worktree() {
1257
+ [ -n "$from" ] || return 0
1258
+ [ "$no_cleanup" -eq 0 ] || return 0
1259
+ [ "$dry" -eq 0 ] || return 0
1260
+
1261
+ # Resolve the absolute path, same as scan_worktree does.
1262
+ _cwt=$(cd "$from" 2>/dev/null && pwd -P) || return 0
1263
+
1264
+ # Only agent worktrees — never a human's worktree or anything else.
1265
+ case "$_cwt" in */.claude/worktrees/agent-*) ;; *) return 0 ;; esac
1266
+
1267
+ # The worktree directory must still exist.
1268
+ [ -d "$_cwt" ] || return 0
1269
+
1270
+ # Derive the branch name git worktree add would have created.
1271
+ _wt_basename=$(basename "$_cwt")
1272
+ _wt_branch="worktree-$_wt_basename"
1273
+
1274
+ # `git worktree remove` is the proper cleanup — it removes the directory AND
1275
+ # the worktree bookkeeping under .git/worktrees/. --force is needed because
1276
+ # the worktree has uncommitted changes (the agent's edits, which have already
1277
+ # been lifted).
1278
+ if git -C "$ENTROPY_MACHINES_ROOT" worktree remove --force "$_cwt" 2>/dev/null; then
1279
+ echo " cleaned up worktree: $_cwt"
1280
+ else
1281
+ # Fall back to manual removal if `git worktree remove` fails (e.g. old git).
1282
+ rm -rf "$_cwt" 2>/dev/null
1283
+ git -C "$ENTROPY_MACHINES_ROOT" worktree prune 2>/dev/null
1284
+ echo " cleaned up worktree (manual): $_cwt"
1285
+ fi
1286
+
1287
+ # Delete the per-worktree branch — it was created solely for the worktree and
1288
+ # carries no commits the agent should not have made.
1289
+ git -C "$ENTROPY_MACHINES_ROOT" branch -D "$_wt_branch" 2>/dev/null && \
1290
+ echo " deleted branch: $_wt_branch" || true
1291
+ }
1292
+ _cleanup_source_worktree 2>/dev/null || true
package/bin/init CHANGED
@@ -65,7 +65,15 @@ if ! project=$(entropy_machines_root); then
65
65
  exit 2
66
66
  fi
67
67
 
68
- target="$ENTROPY_MACHINES_HOME/config.json"
68
+ # Vendored (plain tracked files next to project code): config.json lives next
69
+ # to bin/ and lib/. npm install (node_modules/) or external path: config.json
70
+ # goes at the project root. node_modules is inside the project tree but is NOT
71
+ # a vendored layout — it is a package manager install.
72
+ case "$ENTROPY_MACHINES_HOME" in
73
+ */node_modules/*) target="$project/config.json" ;;
74
+ "$project"|"$project"/?*) target="$ENTROPY_MACHINES_HOME/config.json" ;;
75
+ *) target="$project/config.json" ;;
76
+ esac
69
77
 
70
78
  if [ -f "$target" ] && [ -z "$force" ]; then
71
79
  echo "init: REFUSED — config.json already exists at $target." >&2
@@ -188,6 +196,31 @@ else
188
196
  prd_note="init: wrote $prd_dst — open it. It is the first PRD, and the"
189
197
  fi
190
198
 
199
+ manifest="$project/$docs_dir/manifest.json"
200
+ if [ ! -f "$manifest" ]; then
201
+ if [ -n "$prd_written" ]; then
202
+ # The PRD was just copied in, so it is a real doc the tracker/serve layer
203
+ # needs to know about — an empty manifest would leave PRD-001 written to
204
+ # disk but invisible to anything that reads manifest.json for status.
205
+ cat > "$manifest" <<'EOF'
206
+ {
207
+ "docs": {
208
+ "PRD-001-orientation": {
209
+ "file": "PRD-001-orientation.html",
210
+ "title": "Orientation",
211
+ "status": "open",
212
+ "version": 0
213
+ }
214
+ }
215
+ }
216
+ EOF
217
+ else
218
+ # No PRD was written this run (it already existed and --force wasn't
219
+ # given), so there is nothing to register yet.
220
+ printf '{}\n' > "$manifest"
221
+ fi
222
+ fi
223
+
191
224
  # STEP 6 — prove the tracker actually answers before claiming success.
192
225
  # A read, not a write: nothing is filed here. An init that reports success
193
226
  # while the tracker is unreachable is the exact failure this harness exists
@@ -212,6 +245,65 @@ if ! (cd "$project" && "$ENTROPY_MACHINES_HOME/bin/tracker" ready >/dev/null 2>&
212
245
  exit 2
213
246
  fi
214
247
 
248
+ # STEP 7 — build the SQLite database so bin/serve's dashboard works on first
249
+ # run. Non-fatal: a missing db is inconvenient (the dashboard says "API
250
+ # unreachable") but not broken — the user can always run migrate-db by hand.
251
+ # Must come after the manifest write (step 5) so the PRD gets migrated.
252
+ (cd "$project" && "$ENTROPY_MACHINES_HOME/bin/migrate-db" >/dev/null 2>&1) || \
253
+ printf "${YELLOW}init: warning — could not build the database; run \`${CYAN}bin/migrate-db${YELLOW}\` before serving.${RESET}\n" >&2
254
+
255
+ # STEP 8 — install Claude Code hooks so agents are forced through the dispatch
256
+ # pipeline. Three gates: dispatch-gate-writes blocks Edit/Write on main,
257
+ # mcp-gate blocks Bash calls to bin/* when MCP is connected, dispatch-gate
258
+ # and no-fork-gate block Agent misuse. Non-fatal: missing hooks degrade to
259
+ # convention enforcement via CLAUDE.md.
260
+ claude_dir="$project/.claude"
261
+ claude_settings="$claude_dir/settings.json"
262
+ if [ ! -f "$claude_settings" ]; then
263
+ mkdir -p "$claude_dir"
264
+ # Paths are relative to the project root, adjusted for harness location.
265
+ case "$ENTROPY_MACHINES_HOME" in
266
+ "$project") hook_prefix="" ;;
267
+ "$project"/?*) hook_prefix="${ENTROPY_MACHINES_HOME#"$project"/}/" ;;
268
+ *) hook_prefix="$ENTROPY_MACHINES_HOME/" ;;
269
+ esac
270
+ cat > "$claude_settings" <<SETTINGS
271
+ {
272
+ "hooks": {
273
+ "PreToolUse": [
274
+ {
275
+ "matcher": "Agent",
276
+ "hooks": [
277
+ {"type": "command", "command": "${hook_prefix}bin/no-fork-gate"},
278
+ {"type": "command", "command": "${hook_prefix}bin/dispatch-gate"}
279
+ ]
280
+ },
281
+ {
282
+ "matcher": "Edit",
283
+ "hooks": [
284
+ {"type": "command", "command": "${hook_prefix}hooks/dispatch-gate-writes"}
285
+ ]
286
+ },
287
+ {
288
+ "matcher": "Write",
289
+ "hooks": [
290
+ {"type": "command", "command": "${hook_prefix}hooks/dispatch-gate-writes"}
291
+ ]
292
+ },
293
+ {
294
+ "matcher": "Bash",
295
+ "hooks": [
296
+ {"type": "command", "command": "${hook_prefix}hooks/mcp-gate"}
297
+ ]
298
+ }
299
+ ]
300
+ },
301
+ "enableAllProjectMcpServers": true
302
+ }
303
+ SETTINGS
304
+ add_ignore ".claude/"
305
+ fi
306
+
215
307
  printf "${GREEN}init: wrote $target${RESET}\n"
216
308
  printf "${GREEN}$prd_note${RESET}\n"
217
309
  if [ -n "$prd_written" ]; then
@@ -235,5 +327,10 @@ esac
235
327
  printf "${YELLOW}init: warning — could not render TRACKER.html; run \`${CYAN}$cmd_prefix/tracker render${YELLOW}\`${RESET}\n" >&2
236
328
 
237
329
  printf "${GREEN}init: tracker is live and empty${RESET} — \`${CYAN}$cmd_prefix/tracker ready${RESET}\` answers.\n"
238
- printf "${BOLD}init: next — run \`${CYAN}$cmd_prefix/serve${RESET}${BOLD}\` and answer the PRD it opens.${RESET}\n"
330
+ printf "${BOLD}init: next —${RESET}\n"
331
+ printf " ${BOLD}1.${RESET} \`${CYAN}$cmd_prefix/install-hooks${RESET}\` install git hooks (per-clone, not shared)\n"
332
+ printf " ${BOLD}2.${RESET} \`${CYAN}$cmd_prefix/serve${RESET}\` start the factory\n"
333
+ if [ -n "$prd_written" ]; then
334
+ printf " ${BOLD}Open the orientation PRD at ${CYAN}/prds/PRD-001-orientation${RESET}\n"
335
+ fi
239
336
  echo " Filing the issues it produces is step one."
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # bin/install-hooks — install git hook shims for this clone.
3
+ # Delegates to lib/install-hooks.sh.
4
+ exec "$(dirname "$0")/../lib/install-hooks.sh" "$@"