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 +12 -0
- package/bin/dispatch-gate +116 -0
- package/bin/dispatch-run +245 -0
- package/bin/doclint +5 -8
- package/bin/drain +1 -1
- package/bin/entropy-machines-init +1 -1
- package/bin/handoff +65 -3
- package/bin/init +99 -2
- package/bin/install-hooks +4 -0
- package/bin/mcp-serve +25 -0
- package/bin/post-fold-audit +1 -1
- package/bin/serve +116 -701
- package/bin/status +7 -8
- package/bin/tracker +1 -1
- package/bin/worktree-gc +136 -0
- package/docs/SERVE.md +1 -1
- package/hooks/dispatch-gate-writes +51 -0
- package/hooks/mcp-gate +60 -0
- package/hooks/post-commit +37 -0
- package/lib/PRD-001-orientation.html +55 -87
- package/lib/PRD-005-navigation.html +4 -4
- package/lib/api.py +110 -15
- package/lib/config.py +4 -1
- package/lib/cycle.py +6 -10
- package/lib/docstate.py +8 -186
- package/lib/mcp_server.py +862 -0
- package/lib/migrate_db.py +41 -24
- package/lib/report_gen.py +494 -0
- package/lib/roots.sh +14 -3
- package/lib/schema.sql +9 -4
- package/lib/tracker-file +24 -1
- package/lib/tracker-view.py +102 -53
- package/package.json +3 -2
- package/ui/dist/assets/index-DMElrDfK.js +35 -0
- package/ui/dist/assets/index-EQlN7omS.css +1 -0
- package/ui/dist/favicon.svg +3 -0
- package/ui/dist/index.html +43 -0
- package/bin/sprint-report +0 -424
- package/lib/REPORT-TEMPLATE.html +0 -1303
- package/lib/doc-template.html +0 -1164
- package/lib/doctracker.py +0 -709
- package/lib/enhance.py +0 -1568
- package/lib/sprint-report-template.html +0 -367
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
|
package/bin/dispatch-run
ADDED
|
@@ -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.
|
|
44
|
-
|
|
45
|
-
a
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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."
|