entropy-machines 0.1.1
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/LICENSE +93 -0
- package/README.md +68 -0
- package/agents/isolated-worker.md +128 -0
- package/agents/verifier.md +158 -0
- package/bin/dispatch +700 -0
- package/bin/doclint +460 -0
- package/bin/drain +507 -0
- package/bin/drain-pick.py +168 -0
- package/bin/drain-prompt.md +67 -0
- package/bin/drain-run.sh +342 -0
- package/bin/entropy-machines-init +285 -0
- package/bin/handoff +1151 -0
- package/bin/init +232 -0
- package/bin/post-fold-audit +377 -0
- package/bin/serve +724 -0
- package/bin/status +208 -0
- package/bin/tracker +153 -0
- package/docs/AGENT-QUICKSTART.md +86 -0
- package/docs/CONFIG.md +68 -0
- package/docs/NPM.md +91 -0
- package/docs/SERVE.md +74 -0
- package/docs/TRACKER-ADAPTER.md +66 -0
- package/doctrine/HANDOFF-PROMPT.md +63 -0
- package/doctrine/README.md +62 -0
- package/doctrine/ROLES.md +27 -0
- package/doctrine/WORKFLOW.md +87 -0
- package/hooks/commit-msg +24 -0
- package/hooks/post-checkout +354 -0
- package/hooks/pre-commit +33 -0
- package/lib/PRD-001-orientation.html +1180 -0
- package/lib/REPORT-TEMPLATE.html +413 -0
- package/lib/changelog-collate.mjs +328 -0
- package/lib/changelog-guard.sh +157 -0
- package/lib/changelog-new.mjs +70 -0
- package/lib/config.mjs +283 -0
- package/lib/config.py +317 -0
- package/lib/doc-template.html +807 -0
- package/lib/entropy-drain.plist.in +59 -0
- package/lib/entropy-drain.service.in +53 -0
- package/lib/entropy-drain.timer.in +36 -0
- package/lib/fail-first.mjs +901 -0
- package/lib/handoff-guard.sh +623 -0
- package/lib/install-hooks.sh +169 -0
- package/lib/notes.py +675 -0
- package/lib/preflight-tree.mjs +82 -0
- package/lib/roots.sh +212 -0
- package/lib/themes/daylight.css +84 -0
- package/lib/themes/high-contrast.css +36 -0
- package/lib/tracker-file +333 -0
- package/lib/tracker-view.py +784 -0
- package/package.json +38 -0
package/bin/dispatch
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Pre-flight gate for dispatching a writing agent.
|
|
3
|
+
#
|
|
4
|
+
# bin/dispatch i-foo --files "src/x.ts tests/x.test.ts" --brief "one-line task"
|
|
5
|
+
#
|
|
6
|
+
# Exit 0 means SAFE TO DISPATCH; anything else names the hazard. It does not
|
|
7
|
+
# spawn the agent — your agent runner does that — it refuses the mistakes
|
|
8
|
+
# that have each burned a real run:
|
|
9
|
+
#
|
|
10
|
+
# 1. NON-ROOT CWD. A worktree is created in whatever repo the dispatching
|
|
11
|
+
# session's cwd belongs to. A session parked one directory off from the
|
|
12
|
+
# repo root has, more than once, handed an agent a worktree of the
|
|
13
|
+
# WRONG repository — none of the files it needed were in it. This
|
|
14
|
+
# checks the caller's cwd before anything else.
|
|
15
|
+
# 2. UNCOMMITTED WORK INSIDE THE AGENT'S OWN FILE SCOPE. A worker's
|
|
16
|
+
# worktree branches from local HEAD, so unpushed commits are visible —
|
|
17
|
+
# nothing has to be pushed before dispatching. What is still invisible
|
|
18
|
+
# is uncommitted edits: hand an agent a file you are mid-edit in and it
|
|
19
|
+
# works from the committed version, and its result silently drops your
|
|
20
|
+
# changes. Only the declared scope is checked; a dirty tree elsewhere is
|
|
21
|
+
# none of this agent's business.
|
|
22
|
+
# 3. ALREADY-LANDED WORK. If git log already mentions the issue id, the
|
|
23
|
+
# task may be partly or fully done. The audit comes before the dispatch.
|
|
24
|
+
# 4. UNRECORDED BRIEFS. The brief + file scope are appended to the tracker
|
|
25
|
+
# notes log in the same act, so any session can see what was handed out
|
|
26
|
+
# and scopes can be checked for overlap before agents collide in a file.
|
|
27
|
+
#
|
|
28
|
+
# THE SCOPE LIST IS A DENYLIST, NOT AN ALLOWLIST. --files used to declare the
|
|
29
|
+
# files the agent MAY write: a prediction made at the moment of least
|
|
30
|
+
# information, by the one party who has not read the code, and correcting it
|
|
31
|
+
# later costs a round trip. The dispatcher does not know what the agent will
|
|
32
|
+
# need. It knows one thing that is true and small: which files OTHER agents
|
|
33
|
+
# are holding right now, because it dispatched them. So the ENFORCED, PASTED
|
|
34
|
+
# thing is the exclusion set, and --files stays an advisory prediction that
|
|
35
|
+
# is still recorded for the audit trail.
|
|
36
|
+
#
|
|
37
|
+
# Every recorded scope failure under the old allowlist model was an
|
|
38
|
+
# exclusion with nothing behind it: a shared, append-only interface file
|
|
39
|
+
# left out of scope, so a change to it shipped undeclared; a wire-format
|
|
40
|
+
# producer left out of scope because its directory wasn't listed, so a field
|
|
41
|
+
# shipped with no writer; a worker's own brand-new changelog fragment
|
|
42
|
+
# refused at landing purely because the scope line had been drawn too
|
|
43
|
+
# narrow to get past a gate.
|
|
44
|
+
#
|
|
45
|
+
# THE CLAIM SET NEEDS NO NEW STATE. This script writes a DISPATCH note to the
|
|
46
|
+
# tracker and bin/handoff writes the matching HANDOFF note that releases it,
|
|
47
|
+
# so live claims are derivable from the notes log: an issue whose last event
|
|
48
|
+
# is a DISPATCH holds its scope, a HANDOFF releases it, and a claim by the id
|
|
49
|
+
# being dispatched now is not a claim against this agent. An unreachable
|
|
50
|
+
# tracker degrades to "unavailable" and the context file tells the agent to
|
|
51
|
+
# treat the advisory list as hard — an unreadable claim log is not an empty
|
|
52
|
+
# one.
|
|
53
|
+
#
|
|
54
|
+
# COLLISION DETECTION STAYS AT THE LAND STEP. `bin/handoff --lift` compares
|
|
55
|
+
# what the worker changed against the recorded denylist and refuses a
|
|
56
|
+
# claimed file just as hard as an out-of-allowlist one used to be refused; a
|
|
57
|
+
# file outside --files that nobody claimed is reported for the lander to
|
|
58
|
+
# read, not refused.
|
|
59
|
+
#
|
|
60
|
+
# IT ALSO WRITES THE AGENT'S CONTEXT FILE. Everything this script already
|
|
61
|
+
# knows was, for a long time, printed only to the DISPATCHER's terminal and
|
|
62
|
+
# reached the agent only if a human retyped it. Measured over 224 subagent
|
|
63
|
+
# transcripts / 152 worktree agents on an earlier version of this workflow: a
|
|
64
|
+
# rule PASTED into the brief was followed ~96% of the time; the same rule
|
|
65
|
+
# sitting only in a file the agent was told to go read was followed ~38%;
|
|
66
|
+
# left unmentioned, ~8%. Compliance tracks what the brief PASTES, not what it
|
|
67
|
+
# points at. So the issue text, the carry-forward notes, `git log --grep`,
|
|
68
|
+
# the scope and the brief are all WRITTEN to a file, and the brief block's
|
|
69
|
+
# first line is the absolute path to it.
|
|
70
|
+
#
|
|
71
|
+
# WHY A WORKTREE'S BASE IS LOCAL HEAD, AND WHY THAT NEEDS CHECK 1. Basing a
|
|
72
|
+
# worktree on the pushed remote instead requires a push before every
|
|
73
|
+
# dispatch, which serialises landing across concurrent sessions for no
|
|
74
|
+
# safety benefit — agents are isolated by their worktree, not by their base
|
|
75
|
+
# commit. Basing it on local HEAD instead means a worktree is created in
|
|
76
|
+
# whatever repo the dispatching shell's cwd resolves to, so a session that
|
|
77
|
+
# has drifted into a different directory gets a worktree of the WRONG repo
|
|
78
|
+
# with no obvious symptom until the agent can't find its own files. Check 1
|
|
79
|
+
# is what closes that.
|
|
80
|
+
#
|
|
81
|
+
# --anyway skips check 3 only (the git-log hits are still printed); checks 1
|
|
82
|
+
# and 2 have no override — there is no correct dispatch from a wrong cwd or
|
|
83
|
+
# over your own uncommitted edits.
|
|
84
|
+
set -e
|
|
85
|
+
|
|
86
|
+
usage() { echo "usage: bin/dispatch <issue-id> --files \"<paths>\" --brief \"<text>\" [--anyway] [--dry-run]" >&2; exit 2; }
|
|
87
|
+
|
|
88
|
+
id=""; files=""; brief=""; anyway=0; dry=0
|
|
89
|
+
while [ $# -gt 0 ]; do
|
|
90
|
+
case "$1" in
|
|
91
|
+
--files) files="$2"; shift 2 ;;
|
|
92
|
+
--brief) brief="$2"; shift 2 ;;
|
|
93
|
+
--anyway) anyway=1; shift ;;
|
|
94
|
+
# Run every check and print the brief block, but write nothing to the
|
|
95
|
+
# log. Added after a smoke test of this script left a phantom DISPATCH
|
|
96
|
+
# note behind — a note that can be annotated but never removed, and that
|
|
97
|
+
# reads to any later session as an agent someone forgot to land.
|
|
98
|
+
--dry-run) dry=1; shift ;;
|
|
99
|
+
-*) usage ;;
|
|
100
|
+
*) [ -n "$id" ] && usage; id="$1"; shift ;;
|
|
101
|
+
esac
|
|
102
|
+
done
|
|
103
|
+
[ -n "$id" ] && [ -n "$files" ] && [ -n "$brief" ] || usage
|
|
104
|
+
|
|
105
|
+
. "$(dirname "$0")/../lib/roots.sh"
|
|
106
|
+
ENTROPY_MACHINES_HOME=$(entropy_machines_home "$0")
|
|
107
|
+
entropy_machines_require_root dispatch
|
|
108
|
+
CONFIG_PY="$ENTROPY_MACHINES_HOME/lib/config.py"
|
|
109
|
+
NOTES_PY="$ENTROPY_MACHINES_HOME/lib/notes.py"
|
|
110
|
+
LIBDIR="$ENTROPY_MACHINES_HOME/lib"
|
|
111
|
+
: "${ENTROPY_ACTOR:=dispatch}"
|
|
112
|
+
export ENTROPY_ACTOR
|
|
113
|
+
|
|
114
|
+
cfg_get() { python3 "$CONFIG_PY" get "$1" 2>/dev/null; }
|
|
115
|
+
|
|
116
|
+
if [ "$(pwd -P)" != "$ENTROPY_MACHINES_ROOT" ]; then
|
|
117
|
+
echo "dispatch: REFUSED — cwd is $(pwd -P), not the project root ($ENTROPY_MACHINES_ROOT)." >&2
|
|
118
|
+
echo " Worktrees are created in the repo the dispatching session's cwd is in." >&2
|
|
119
|
+
echo " Run: cd $ENTROPY_MACHINES_ROOT" >&2
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# The handoff gate is a commit-msg hook and nothing else — see
|
|
124
|
+
# lib/handoff-guard.sh for why it is local-only by design. That makes an
|
|
125
|
+
# uninstalled hook a silent total loss of the gate, in a checkout that looks
|
|
126
|
+
# identical to one that has it. Checked HERE because dispatch is the last
|
|
127
|
+
# moment before the only activity the gate protects.
|
|
128
|
+
#
|
|
129
|
+
# BOTH halves are checked: the installed shim, AND the checked-in hook it
|
|
130
|
+
# delegates to. The hooks directory is SHARED by every worktree of this
|
|
131
|
+
# repo, but the checked-in hook file it execs is per-worktree — so a
|
|
132
|
+
# worktree whose checkout predates the hook has the shim, has a dead gate,
|
|
133
|
+
# and checking only the shim would report OK.
|
|
134
|
+
hooks=$(git rev-parse --git-path hooks)
|
|
135
|
+
case "$hooks" in /*) ;; *) hooks="$ENTROPY_MACHINES_ROOT/$hooks" ;; esac
|
|
136
|
+
if [ ! -x "$hooks/commit-msg" ]; then
|
|
137
|
+
echo "dispatch: REFUSED — no commit-msg shim in $hooks." >&2
|
|
138
|
+
echo " Without it nothing requires a handoff when you land this agent, and the" >&2
|
|
139
|
+
echo " only record of what it found and did not fix dies with its worktree." >&2
|
|
140
|
+
echo " Run: $ENTROPY_MACHINES_HOME/lib/install-hooks.sh" >&2
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
143
|
+
if [ ! -x "$ENTROPY_MACHINES_HOME/hooks/commit-msg" ]; then
|
|
144
|
+
echo "dispatch: REFUSED — the commit-msg shim is installed but the harness has" >&2
|
|
145
|
+
echo " no hooks/commit-msg, so the shim exits 0 and the gate is dead." >&2
|
|
146
|
+
echo " $ENTROPY_MACHINES_HOME is on a commit older than the hook." >&2
|
|
147
|
+
echo " Merge or rebase the harness onto a commit that has it, or land this work" >&2
|
|
148
|
+
echo " from a harness checkout that does." >&2
|
|
149
|
+
exit 1
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
branch=$(git branch --show-current)
|
|
153
|
+
head=$(git rev-parse --short HEAD)
|
|
154
|
+
echo "dispatch: agent will branch from $branch @ $head (base: local HEAD)."
|
|
155
|
+
|
|
156
|
+
# Uncommitted edits are invisible inside the worktree. Only a collision with
|
|
157
|
+
# the agent's OWN declared scope is a hazard, so that is all this refuses on
|
|
158
|
+
# — a dirty tree elsewhere is normal and none of this agent's business.
|
|
159
|
+
#
|
|
160
|
+
# Scope entries carry annotations ("path/(own)", "dir/*.md", "dir/") and the
|
|
161
|
+
# matching logic that understands them lives in lib/notes.py — path_under()
|
|
162
|
+
# — so this and every other scope check in this file and bin/handoff read
|
|
163
|
+
# one annotation grammar the same way, instead of five independent sed
|
|
164
|
+
# reimplementations of it.
|
|
165
|
+
dirty=$(git status --porcelain 2>/dev/null | awk '{print $NF}')
|
|
166
|
+
clash=$(python3 -c '
|
|
167
|
+
import sys
|
|
168
|
+
sys.path.insert(0, sys.argv[1])
|
|
169
|
+
import notes as notes_mod
|
|
170
|
+
scope = sys.argv[2].split()
|
|
171
|
+
dirty = sys.argv[3].split()
|
|
172
|
+
print(" ".join(d for d in dirty if notes_mod.path_under(d, scope)))
|
|
173
|
+
' "$LIBDIR" "$files" "$dirty")
|
|
174
|
+
if [ -n "$clash" ]; then
|
|
175
|
+
echo "dispatch: REFUSED — you have uncommitted edits inside the agent's own file scope:" >&2
|
|
176
|
+
for c in $clash; do echo " $c" >&2; done
|
|
177
|
+
echo " The agent inherits committed HEAD, so it would work from the old version" >&2
|
|
178
|
+
echo " and its result would silently drop these edits. Commit them first." >&2
|
|
179
|
+
exit 1
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
hits=$(git log --oneline --grep="$id" | head -5)
|
|
183
|
+
if [ -n "$hits" ]; then
|
|
184
|
+
echo "dispatch: git log already mentions $id:" >&2
|
|
185
|
+
echo "$hits" >&2
|
|
186
|
+
if [ "$anyway" != "1" ]; then
|
|
187
|
+
echo " Audit what landed before briefing (bin/tracker show $id), then rerun with --anyway." >&2
|
|
188
|
+
exit 1
|
|
189
|
+
fi
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# THE DENYLIST — what OTHER agents are holding right now
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Derived from the shared notes log, which already carries both halves: this
|
|
196
|
+
# script writes a DISPATCH note and bin/handoff writes a HANDOFF note that
|
|
197
|
+
# releases it. Last event per id wins, so a re-dispatched issue reclaims its
|
|
198
|
+
# files and a handed-off one releases them. No new state, no registry file.
|
|
199
|
+
#
|
|
200
|
+
# A CLAIM EXPIRES after DISPATCH_CLAIM_HOURS (default 24). The notes log is
|
|
201
|
+
# append-only with no delete, so a dispatch that was never handed off would
|
|
202
|
+
# otherwise hold its files forever, and the end state of an ever-growing
|
|
203
|
+
# denylist is the allowlist this inversion exists to avoid, with worse
|
|
204
|
+
# ergonomics. The changelog fragment directory, if configured, is never
|
|
205
|
+
# claimable — every agent writes its own file there by design, so a claim on
|
|
206
|
+
# the directory would wall every OTHER agent off its own fragment.
|
|
207
|
+
#
|
|
208
|
+
# THREE STATES, and they are not the same. A real list is enforced at lift.
|
|
209
|
+
# An empty list ("(none)") means the log was read and nobody holds anything,
|
|
210
|
+
# so every file is the agent's. "(unavailable)" means the log could not be
|
|
211
|
+
# read at all, and that degrades to the OLD allowlist behaviour on both
|
|
212
|
+
# ends: the context file tells the agent to treat --files as hard, and
|
|
213
|
+
# bin/handoff falls back to refusing anything outside it. An unreadable
|
|
214
|
+
# claim log is not an empty one.
|
|
215
|
+
never_claimed=""
|
|
216
|
+
if [ "$(cfg_get changelog.enabled)" = "true" ]; then
|
|
217
|
+
never_claimed=$(cfg_get changelog.fragmentDir)
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
claims=""; deny=""; claim_lines=""; deny_ok=0
|
|
221
|
+
if claims_raw=$("$ENTROPY_MACHINES_HOME/bin/tracker" notes 2>/dev/null); then
|
|
222
|
+
deny_ok=1
|
|
223
|
+
hours="${DISPATCH_CLAIM_HOURS:-24}"
|
|
224
|
+
if [ -n "$never_claimed" ]; then
|
|
225
|
+
claims=$(printf '%s\n' "$claims_raw" | python3 "$NOTES_PY" claims --self "$id" --hours "$hours" --exclude "$never_claimed" 2>/dev/null || true)
|
|
226
|
+
else
|
|
227
|
+
claims=$(printf '%s\n' "$claims_raw" | python3 "$NOTES_PY" claims --self "$id" --hours "$hours" 2>/dev/null || true)
|
|
228
|
+
fi
|
|
229
|
+
deny=$(printf '%s\n' "$claims" | awk -F'\t' '$1=="PATHS"{print $2}')
|
|
230
|
+
# One line per claiming issue, so the agent can see WHO holds a file and
|
|
231
|
+
# the lander can go ask them. A flat path list alone reads as an
|
|
232
|
+
# arbitrary wall.
|
|
233
|
+
claim_lines=$(printf '%s\n' "$claims" | awk -F'\t' '$1=="CLAIM"{printf " %s\n (claimed by %s)\n", $3, $2}')
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
if [ "$deny_ok" -eq 0 ]; then deny_field="(unavailable)"
|
|
237
|
+
elif [ -z "$deny" ]; then deny_field="(none)"
|
|
238
|
+
else deny_field="$deny"; fi
|
|
239
|
+
|
|
240
|
+
# --files overlapping a live claim is dispatch rule 3 — two issues in one
|
|
241
|
+
# file serialize, they do not run concurrently. Printed, not refused:
|
|
242
|
+
# re-dispatching an issue over its own predecessor's files is legitimate,
|
|
243
|
+
# and the lander is the one who can tell that from a genuine collision. The
|
|
244
|
+
# lift-side refusal is the mechanism; this is the early warning.
|
|
245
|
+
overlap=$(python3 -c '
|
|
246
|
+
import sys
|
|
247
|
+
sys.path.insert(0, sys.argv[1])
|
|
248
|
+
import notes as notes_mod
|
|
249
|
+
files = sys.argv[2].split()
|
|
250
|
+
deny = sys.argv[3].split()
|
|
251
|
+
seen, out = set(), []
|
|
252
|
+
for d in deny:
|
|
253
|
+
if d in seen:
|
|
254
|
+
continue
|
|
255
|
+
if any(notes_mod.paths_overlap(f, d) for f in files):
|
|
256
|
+
out.append(d); seen.add(d)
|
|
257
|
+
print(" ".join(out))
|
|
258
|
+
' "$LIBDIR" "$files" "$deny")
|
|
259
|
+
if [ -n "$overlap" ]; then
|
|
260
|
+
echo "dispatch: WARNING — your --files overlaps a LIVE claim by another issue:" >&2
|
|
261
|
+
for o in $overlap; do echo " $o" >&2; done
|
|
262
|
+
echo " Two issues touching one file serialize; they do not run concurrently." >&2
|
|
263
|
+
echo " Land or hand off the other agent first, or narrow this one." >&2
|
|
264
|
+
echo " bin/handoff --lift will refuse these files when this agent returns." >&2
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
# THE INTERROGATION FIELD. A note carrying it is an agent that was actually
|
|
268
|
+
# told, in its own brief, that it would be questioned before its work was
|
|
269
|
+
# lifted — that is what lets bin/handoff enforce a contract the agent was
|
|
270
|
+
# actually given, rather than one invented at landing time. Notes without it
|
|
271
|
+
# — hand-done work, or a note written before this landed — are reported and
|
|
272
|
+
# never refused.
|
|
273
|
+
interrogation_field="required"
|
|
274
|
+
|
|
275
|
+
if [ "$dry" -eq 1 ]; then
|
|
276
|
+
echo "dispatch: --dry-run — all checks passed, nothing recorded. Would log a"
|
|
277
|
+
echo " DISPATCH note: scope=$files brief=$brief interrogation=$interrogation_field denylist=$deny_field"
|
|
278
|
+
else
|
|
279
|
+
if [ "$deny_ok" -eq 1 ]; then
|
|
280
|
+
payload=$(python3 "$NOTES_PY" encode --verb DISPATCH --actor "$ENTROPY_ACTOR" \
|
|
281
|
+
--field "brief=$brief" --field "interrogation=$interrogation_field" \
|
|
282
|
+
--field "scope=$files" --field "denylist=$deny")
|
|
283
|
+
else
|
|
284
|
+
payload=$(python3 "$NOTES_PY" encode --verb DISPATCH --actor "$ENTROPY_ACTOR" \
|
|
285
|
+
--field "brief=$brief" --field "interrogation=$interrogation_field" \
|
|
286
|
+
--field "scope=$files")
|
|
287
|
+
fi
|
|
288
|
+
if ! out=$("$ENTROPY_MACHINES_HOME/bin/tracker" remember --issue "$id" "$payload" 2>&1); then
|
|
289
|
+
echo "dispatch: REFUSED — could not record the brief on $id." >&2
|
|
290
|
+
echo " bin/tracker said: $out" >&2
|
|
291
|
+
echo " Nothing is dispatched blind: without this note, no other session can" >&2
|
|
292
|
+
echo " see what was handed out, and the denylist above is unrecorded. Fix" >&2
|
|
293
|
+
echo " the tracker (docs/TRACKER-ADAPTER.md, docs/CONFIG.md) and rerun." >&2
|
|
294
|
+
exit 1
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# CLAIM THE ISSUE IN THE SAME ACT THAT RECORDS THE BRIEF. Recording a
|
|
298
|
+
# DISPATCH note with no claim left status untouched, so a second session
|
|
299
|
+
# could see an issue with a live agent on it as free work. The adapter's
|
|
300
|
+
# `claim` op does this atomically and is never fatal here — losing the
|
|
301
|
+
# dispatch over a claim hiccup would be worse than an unclaimed issue.
|
|
302
|
+
if claim_out=$("$ENTROPY_MACHINES_HOME/bin/tracker" claim "$id" 2>&1); then
|
|
303
|
+
case "$claim_out" in
|
|
304
|
+
*'"claimed": false'*|*'"claimed":false'*)
|
|
305
|
+
echo "dispatch: NOTE — $id is not notstarted; not claiming it. Re-dispatching" >&2
|
|
306
|
+
echo " finished or already-progressing work is a correction; set the status" >&2
|
|
307
|
+
echo " yourself if that is what you mean." >&2 ;;
|
|
308
|
+
*) echo "dispatch: $id claimed." ;;
|
|
309
|
+
esac
|
|
310
|
+
else
|
|
311
|
+
claim_rc=$?
|
|
312
|
+
if [ "$claim_rc" -eq 4 ]; then
|
|
313
|
+
echo "dispatch: WARNING — $id is already claimed by another actor: $claim_out" >&2
|
|
314
|
+
else
|
|
315
|
+
echo "dispatch: WARNING — could not claim $id; it may still read unclaimed to other sessions ($claim_out)." >&2
|
|
316
|
+
fi
|
|
317
|
+
fi
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# THE AGENT'S CONTEXT FILE
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# WHY A FILE AND NOT MORE TERMINAL OUTPUT. Whether the tracker and any local
|
|
324
|
+
# docs directory are reachable from inside a worktree depends on this
|
|
325
|
+
# project's config.json worktree.linkPaths and on whether this clone's
|
|
326
|
+
# hooks are installed — don't assume either way. The one channel that
|
|
327
|
+
# reliably lands is what the brief PASTES (see the measured numbers in the
|
|
328
|
+
# header), so everything is written here and the brief's first line is this
|
|
329
|
+
# file's path.
|
|
330
|
+
#
|
|
331
|
+
# WHY PER-ISSUE AND NOT A SINGLE FILE AT THE ROOT. A dispatch round briefs N
|
|
332
|
+
# agents back to back from one checkout; a fixed filename means agent A's
|
|
333
|
+
# file is overwritten by agent B's dispatch before A ever reads it, and A
|
|
334
|
+
# then reads B's issue, scope and brief as its own — worse than absent,
|
|
335
|
+
# because nothing looks wrong. One file per issue cannot collide.
|
|
336
|
+
#
|
|
337
|
+
# WHY IT LIVES IN THE MAIN CHECKOUT. It is untracked, and `git worktree add`
|
|
338
|
+
# materialises a commit — untracked files are not carried in. So the path
|
|
339
|
+
# printed in the brief is ABSOLUTE and points back at this checkout, which
|
|
340
|
+
# the agent can read (its worktree lives under it).
|
|
341
|
+
ctxdir="$ENTROPY_MACHINES_ROOT/.dispatch-context"
|
|
342
|
+
ctx="$ctxdir/$id.md"
|
|
343
|
+
|
|
344
|
+
# The three carry-forward fields an incoming agent can act on, read out of
|
|
345
|
+
# the LAST HANDOFF note on this issue via lib/notes.py's verb-anchored
|
|
346
|
+
# lookup (not a free-text scan). verified/scope-released are the lander's
|
|
347
|
+
# bookkeeping, not the next agent's context, so they are left out.
|
|
348
|
+
notes_raw=$("$ENTROPY_MACHINES_HOME/bin/tracker" notes --issue "$id" 2>/dev/null || true)
|
|
349
|
+
carry=$(printf '%s\n' "$notes_raw" | python3 "$NOTES_PY" record --issue "$id" --verb HANDOFF --last 2>/dev/null | python3 -c '
|
|
350
|
+
import json, sys
|
|
351
|
+
line = sys.stdin.readline()
|
|
352
|
+
if not line.strip():
|
|
353
|
+
raise SystemExit(0)
|
|
354
|
+
rec = json.loads(line)
|
|
355
|
+
fields = rec.get("fields") or {}
|
|
356
|
+
for key, label in (("next", "next-agent"), ("found", "found-not-fixed"), ("assumed", "assumed")):
|
|
357
|
+
v = fields.get(key)
|
|
358
|
+
if not v:
|
|
359
|
+
continue
|
|
360
|
+
items = v if isinstance(v, list) else [v]
|
|
361
|
+
for item in items:
|
|
362
|
+
item = str(item).strip()
|
|
363
|
+
if item:
|
|
364
|
+
print(f" - [{label}] {item}")
|
|
365
|
+
' 2>/dev/null || true)
|
|
366
|
+
|
|
367
|
+
# Human-readable rendering of a JSONL note stream, shared by the context
|
|
368
|
+
# file's notes section and the terminal's PRIOR NOTES section below. Reads
|
|
369
|
+
# already-parsed records (json.loads per line) — the note-format parsing
|
|
370
|
+
# itself (verb anchoring, scope grammar) stays in lib/notes.py; this only
|
|
371
|
+
# formats fields for a person to read.
|
|
372
|
+
render_notes() {
|
|
373
|
+
python3 -c '
|
|
374
|
+
import json, sys
|
|
375
|
+
any_line = False
|
|
376
|
+
for line in sys.stdin:
|
|
377
|
+
line = line.strip()
|
|
378
|
+
if not line:
|
|
379
|
+
continue
|
|
380
|
+
try:
|
|
381
|
+
r = json.loads(line)
|
|
382
|
+
except json.JSONDecodeError:
|
|
383
|
+
continue
|
|
384
|
+
any_line = True
|
|
385
|
+
ts, verb, actor = r.get("ts", "?"), r.get("verb", "?"), r.get("actor", "?")
|
|
386
|
+
fields = r.get("fields") or {}
|
|
387
|
+
parts = []
|
|
388
|
+
for k in sorted(fields):
|
|
389
|
+
v = fields[k]
|
|
390
|
+
if isinstance(v, list):
|
|
391
|
+
v = ", ".join(str(x) for x in v)
|
|
392
|
+
parts.append(f"{k}: {v}")
|
|
393
|
+
print(f" [{ts}] {verb} (by {actor}) " + " | ".join(parts))
|
|
394
|
+
if not any_line:
|
|
395
|
+
print(" (none)")
|
|
396
|
+
'
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if [ "$dry" -eq 0 ]; then
|
|
400
|
+
issue_json=$("$ENTROPY_MACHINES_HOME/bin/tracker" show "$id" 2>&1) || issue_json="UNAVAILABLE — bin/tracker could not be reached. It said:
|
|
401
|
+
|
|
402
|
+
$issue_json
|
|
403
|
+
|
|
404
|
+
Whether the tracker is reachable from inside your worktree depends on this
|
|
405
|
+
project's config.json worktree.linkPaths and on whether this clone's hooks
|
|
406
|
+
are installed. Ask the dispatching session for the issue text if you need it."
|
|
407
|
+
|
|
408
|
+
notes_rendered=$(printf '%s\n' "$notes_raw" | render_notes)
|
|
409
|
+
|
|
410
|
+
# Not capped: an id with a long history is exactly the case the audit is
|
|
411
|
+
# for.
|
|
412
|
+
landed=$(git log --oneline --grep="$id" || true)
|
|
413
|
+
[ -n "$landed" ] || landed="(nothing — git log --oneline --grep=$id is empty; no commit in this checkout names this issue)"
|
|
414
|
+
|
|
415
|
+
carry_text="$carry"
|
|
416
|
+
[ -n "$carry_text" ] || carry_text="(nothing recorded — no earlier HANDOFF note on this issue)"
|
|
417
|
+
|
|
418
|
+
suites_text=$(cfg_get suites | python3 -c '
|
|
419
|
+
import json, sys
|
|
420
|
+
try:
|
|
421
|
+
suites = json.loads(sys.stdin.read() or "[]")
|
|
422
|
+
except Exception:
|
|
423
|
+
suites = []
|
|
424
|
+
if not suites:
|
|
425
|
+
print(" (none configured — see config.json suites)")
|
|
426
|
+
for s in suites:
|
|
427
|
+
name = s.get("name", "?")
|
|
428
|
+
cmd = " ".join(s.get("cmd") or [])
|
|
429
|
+
tag = s.get("tag")
|
|
430
|
+
suffix = " [" + tag + "]" if tag else ""
|
|
431
|
+
print(" " + name + suffix + ": " + cmd)
|
|
432
|
+
')
|
|
433
|
+
|
|
434
|
+
link_paths=$(cfg_get worktree.linkPaths)
|
|
435
|
+
|
|
436
|
+
mkdir -p "$ctxdir"
|
|
437
|
+
{
|
|
438
|
+
echo "# Dispatch context — $id"
|
|
439
|
+
echo
|
|
440
|
+
echo "Written by bin/dispatch on $(date -u '+%Y-%m-%dT%H:%M:%SZ') from $ENTROPY_MACHINES_ROOT ($branch @ $head)."
|
|
441
|
+
echo "READ THIS BEFORE YOU DO ANYTHING ELSE. Everything you cannot look up for"
|
|
442
|
+
echo "yourself was captured here at dispatch time — whether the tracker and any"
|
|
443
|
+
echo "local docs directory are reachable from your worktree depends on this"
|
|
444
|
+
echo "project's config.json worktree.linkPaths and on whether this clone's"
|
|
445
|
+
echo "hooks are installed; don't assume either way."
|
|
446
|
+
echo
|
|
447
|
+
echo "## Files you must NOT write — CLAIMED by another agent right now"
|
|
448
|
+
echo
|
|
449
|
+
if [ "$deny_ok" -eq 0 ]; then
|
|
450
|
+
echo " UNAVAILABLE — bin/tracker could not be reached, so the live-claim"
|
|
451
|
+
echo " log could not be read. Treat the advisory list below as a HARD"
|
|
452
|
+
echo " boundary instead: an unreadable claim log is not an empty one."
|
|
453
|
+
elif [ -z "$deny" ]; then
|
|
454
|
+
echo " (none — no other issue holds a live claim this round)"
|
|
455
|
+
echo
|
|
456
|
+
echo "Every file in this repo is yours to write if the fix genuinely needs it."
|
|
457
|
+
else
|
|
458
|
+
echo "$claim_lines"
|
|
459
|
+
echo
|
|
460
|
+
echo "Every OTHER file in this repo is yours to write if the fix genuinely"
|
|
461
|
+
echo "needs it. Say in HANDOFF.md's changed: line what you took beyond the"
|
|
462
|
+
echo "advisory list below, so the lander reads that diff on purpose."
|
|
463
|
+
fi
|
|
464
|
+
echo
|
|
465
|
+
echo "Derived from the shared notes log, not from a guess: an issue whose last"
|
|
466
|
+
echo "event is a DISPATCH holds its scope, a HANDOFF releases it, and a claim"
|
|
467
|
+
echo "by this issue's own id is not a claim against you."
|
|
468
|
+
echo
|
|
469
|
+
echo "### If your fix needs a CLAIMED file: STOP AND REPORT. This is a refusal,"
|
|
470
|
+
echo "### not a preference."
|
|
471
|
+
echo
|
|
472
|
+
echo "An agent that finds it needs a claimed file has three options — write it"
|
|
473
|
+
echo "anyway and risk clobbering a concurrent agent, ship the half it was"
|
|
474
|
+
echo "allowed to write, or stop. This workflow has, more than once, watched an"
|
|
475
|
+
echo "agent take the middle option and report success: two halves of one"
|
|
476
|
+
echo "change individually green, the change itself broken, because each half"
|
|
477
|
+
echo "was written by an agent that could not reach the other's file. Neither"
|
|
478
|
+
echo "agent said it was blocked. The cost lands on the lander, who is the one"
|
|
479
|
+
echo "context that can see both halves — exactly the serialisation this model"
|
|
480
|
+
echo "exists to avoid. So:"
|
|
481
|
+
echo
|
|
482
|
+
echo " 1. STOP at the boundary. Do not write the claimed file, and do not"
|
|
483
|
+
echo " ship the half of the change that does not work without it."
|
|
484
|
+
echo " 2. Leave the in-scope work UNCOMMITTED AND COHERENT — it must build"
|
|
485
|
+
echo " and test on its own. If it cannot, say so in changed: rather than"
|
|
486
|
+
echo " leaving a green half that is wrong on the wire."
|
|
487
|
+
echo " 3. Put the blocked half in HANDOFF.md's found: line, naming the EXACT"
|
|
488
|
+
echo " file and what it must contain. 'needed a file outside scope' is"
|
|
489
|
+
echo " not enough for the lander to finish it."
|
|
490
|
+
echo " 4. Say the same in your final report, in the same words."
|
|
491
|
+
echo
|
|
492
|
+
echo "WHAT ENFORCES THIS: nothing mechanical, and pretending otherwise would be"
|
|
493
|
+
echo "worse than saying it. Nothing can detect a half-change from outside — the"
|
|
494
|
+
echo "mechanism is this wording plus the lander reading your found: line, and"
|
|
495
|
+
echo "bin/handoff refuses a record with no --found and no --next unless the"
|
|
496
|
+
echo "lander passes --clean as a positive claim."
|
|
497
|
+
echo
|
|
498
|
+
echo "## What the dispatcher expected you to touch — ADVISORY, not a boundary"
|
|
499
|
+
echo
|
|
500
|
+
echo " $files"
|
|
501
|
+
echo
|
|
502
|
+
echo "This is a prediction made before anyone read the code, by the party that"
|
|
503
|
+
echo "has not read it. It is recorded for the audit trail and it is NOT the"
|
|
504
|
+
echo "limit of what you may write. The enforced limit is the claimed list above."
|
|
505
|
+
echo
|
|
506
|
+
echo "## Your brief"
|
|
507
|
+
echo
|
|
508
|
+
echo " $brief"
|
|
509
|
+
echo
|
|
510
|
+
echo "## You will be QUESTIONED before your work is lifted"
|
|
511
|
+
echo
|
|
512
|
+
echo "When you stop, you are still resumable and you still hold context nobody"
|
|
513
|
+
echo "else has. The lander will put these questions to you and record your"
|
|
514
|
+
echo "answers on the issue; bin/handoff refuses to record the landing until it"
|
|
515
|
+
echo "has them. Gather them AS YOU WORK — they are much harder to reconstruct"
|
|
516
|
+
echo "afterwards, and a wrong answer is worse than a blank one."
|
|
517
|
+
echo
|
|
518
|
+
qs=$("$ENTROPY_MACHINES_HOME/bin/handoff" "$id" --questions 2>/dev/null || true)
|
|
519
|
+
if [ -n "$qs" ]; then echo "$qs"
|
|
520
|
+
else echo " (unavailable — bin/handoff --questions could not be read here)"; fi
|
|
521
|
+
echo
|
|
522
|
+
echo "None of these can be answered with \"no\", and that is deliberate. Both"
|
|
523
|
+
echo "recorded half-changes above were reported as successes by the agent that"
|
|
524
|
+
echo "shipped them, so \"are you complete?\" is worse than asking nothing. At"
|
|
525
|
+
echo "least one of your answers must be substantive — \"nothing\" across the"
|
|
526
|
+
echo "whole set is refused, so silence has to be a claim you made rather than"
|
|
527
|
+
echo "an absence."
|
|
528
|
+
echo
|
|
529
|
+
echo "## The issue (bin/tracker show $id)"
|
|
530
|
+
echo
|
|
531
|
+
echo "$issue_json"
|
|
532
|
+
echo
|
|
533
|
+
echo "## This issue's notes (bin/tracker notes --issue $id)"
|
|
534
|
+
echo
|
|
535
|
+
echo "$notes_rendered"
|
|
536
|
+
echo
|
|
537
|
+
echo "## What already landed for this issue (git log --oneline --grep=$id)"
|
|
538
|
+
echo
|
|
539
|
+
echo "IF THIS IS NON-EMPTY, AUDIT IT BEFORE WRITING CODE. An issue whose desc has"
|
|
540
|
+
echo "gone stale reads as unstarted work — treat a non-empty match as a reason"
|
|
541
|
+
echo "to look before reimplementing something that already landed under this id."
|
|
542
|
+
echo
|
|
543
|
+
echo "$landed"
|
|
544
|
+
echo
|
|
545
|
+
echo "## What earlier work on this issue left you"
|
|
546
|
+
echo
|
|
547
|
+
echo "$carry_text"
|
|
548
|
+
echo
|
|
549
|
+
echo "## This project's suites (config.json)"
|
|
550
|
+
echo
|
|
551
|
+
echo "$suites_text"
|
|
552
|
+
echo
|
|
553
|
+
echo "Never run this project's full build/release command in your worktree —"
|
|
554
|
+
echo "some rewrite committed bookkeeping (a collated changelog, a checkpoint"
|
|
555
|
+
echo "file) as a side effect. Use the suites above."
|
|
556
|
+
echo
|
|
557
|
+
echo "## Shared paths linked into your worktree, IF this clone's hooks are"
|
|
558
|
+
echo "## installed (config.json worktree.linkPaths: ${link_paths:-[]})"
|
|
559
|
+
echo
|
|
560
|
+
echo "Don't assume either way — a resolver that can't find a dependency here"
|
|
561
|
+
echo "will sometimes walk up and resolve it out of a SIBLING agent's worktree"
|
|
562
|
+
echo "instead, which means testing someone else's code and reporting it as"
|
|
563
|
+
echo "yours. If a suite refuses to run because it can't resolve its own"
|
|
564
|
+
echo "dependencies, that is the refusal working as designed — don't work"
|
|
565
|
+
echo "around it by hand."
|
|
566
|
+
echo
|
|
567
|
+
echo "## Before you finish: write HANDOFF.md at the root of YOUR worktree"
|
|
568
|
+
echo
|
|
569
|
+
echo "Exactly these four keys, ONE LINE each, no prose, under six lines total:"
|
|
570
|
+
echo
|
|
571
|
+
echo " changed: <what you changed>"
|
|
572
|
+
echo " found: <seen but not fixed, out of scope — or: none>"
|
|
573
|
+
echo " assumed: <what you took as given — or: none>"
|
|
574
|
+
echo " next: <what the next agent needs to know — or: none>"
|
|
575
|
+
echo
|
|
576
|
+
echo "Do not commit it; it is gitignored. found and next are why the file exists"
|
|
577
|
+
echo "— your diff already says what changed. Do not commit, push or merge anything"
|
|
578
|
+
echo "at all: the dispatching session lifts your worktree and lands it."
|
|
579
|
+
} > "$ctx"
|
|
580
|
+
echo "dispatch: context written — $ctx"
|
|
581
|
+
fi
|
|
582
|
+
|
|
583
|
+
# The issue's own note thread, printed at the moment a brief is being
|
|
584
|
+
# written, so a prior agent's findings on the same issue don't stay unread
|
|
585
|
+
# by the next one. This was fetched BEFORE the DISPATCH note above was
|
|
586
|
+
# written, so it is naturally prior — no filtering needed.
|
|
587
|
+
if [ -n "$notes_raw" ]; then
|
|
588
|
+
echo
|
|
589
|
+
echo "PRIOR NOTES ON $id — read before briefing; fold anything load-bearing into the brief:"
|
|
590
|
+
printf '%s\n' "$notes_raw" | render_notes | sed 's/^/ /'
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
echo
|
|
594
|
+
if [ "$dry" -eq 1 ]; then
|
|
595
|
+
echo "dispatch: OK (dry run) — nothing recorded on $id."
|
|
596
|
+
else
|
|
597
|
+
echo "dispatch: OK — brief + scope recorded on $id."
|
|
598
|
+
fi
|
|
599
|
+
echo "Dispatch the worker defined in agents/isolated-worker.md."
|
|
600
|
+
echo " Claude Code: Agent tool, subagent_type: isolated-worker."
|
|
601
|
+
echo " A different runner: give it its own worktree, this brief directly (not"
|
|
602
|
+
echo " just pointed at it), and a way for you to read back its HANDOFF.md —"
|
|
603
|
+
echo " see the note at the top of agents/isolated-worker.md."
|
|
604
|
+
echo " issue: $id"
|
|
605
|
+
echo " advisory --files: $files"
|
|
606
|
+
echo " ENFORCED denylist: $deny_field"
|
|
607
|
+
echo " brief: $brief"
|
|
608
|
+
echo
|
|
609
|
+
# Pasted verbatim into the agent's brief. Four keys, one line each, and the
|
|
610
|
+
# instruction says so twice — an agent given a free-form "write up what you
|
|
611
|
+
# found" returns prose the lander then has to read and compress, which is
|
|
612
|
+
# the work this was meant to remove. The file is gitignored, so the agent
|
|
613
|
+
# cannot accidentally add it to the tree.
|
|
614
|
+
echo "PASTE INTO THE AGENT'S BRIEF:"
|
|
615
|
+
echo " BRIEF: $brief"
|
|
616
|
+
echo
|
|
617
|
+
# FIRST LINE after the brief, and the load-bearing one — see the measured
|
|
618
|
+
# numbers in the header. This holds regardless of whether your runner
|
|
619
|
+
# auto-loads a project-instructions file into subagents: proximity to the
|
|
620
|
+
# prompt is what changes behaviour, not presence in context.
|
|
621
|
+
echo " FIRST, before anything else: read this file, in full, with one Read:"
|
|
622
|
+
echo " $ctx"
|
|
623
|
+
echo " It is your issue text, what already landed under this id, your scope and"
|
|
624
|
+
echo " brief, what earlier agents left you, and this project's suites. It is in"
|
|
625
|
+
echo " the MAIN checkout, NOT in your worktree; the path above is absolute, use"
|
|
626
|
+
echo " it as-is."
|
|
627
|
+
if [ "$dry" -eq 1 ]; then
|
|
628
|
+
echo " (--dry-run: that file was NOT written. Rerun without --dry-run.)"
|
|
629
|
+
fi
|
|
630
|
+
# The cwd check, stated to the AGENT. This script already refuses a
|
|
631
|
+
# non-root cwd, but that runs when the brief is written and the dispatch
|
|
632
|
+
# call happens after — a `cd` in between still misroutes the worktree, and
|
|
633
|
+
# has once taken out three agents at once this way. Nothing in the tool
|
|
634
|
+
# chain can catch that on the way in, so the agent checks on the way out.
|
|
635
|
+
#
|
|
636
|
+
# --git-common-dir answers WHICH REPO IS THIS A WORKTREE OF. From a linked
|
|
637
|
+
# worktree it resolves to the MAIN repo's .git as an ABSOLUTE path, so it
|
|
638
|
+
# carries the repo's own path. From a non-worktree checkout at its root it
|
|
639
|
+
# prints a bare relative `.git`, which is why the wording calls that out as
|
|
640
|
+
# its own reportable state rather than leaving the agent to interpret it.
|
|
641
|
+
# --show-toplevel is the wrong check here: it prints the WORKTREE's own
|
|
642
|
+
# path, which can never equal the main repo's path, so it can never confirm
|
|
643
|
+
# which repo the worktree belongs to.
|
|
644
|
+
echo " Your scope is a DENYLIST, not an allowlist. These files are claimed by"
|
|
645
|
+
echo " another agent RIGHT NOW and you must NOT write them: $deny_field"
|
|
646
|
+
echo " Everything else is yours if the fix needs it. If your fix needs a CLAIMED"
|
|
647
|
+
echo " file: STOP — do not write it, and do NOT ship the half you were allowed"
|
|
648
|
+
echo " to write. Leave the in-scope work coherent and name the exact file and"
|
|
649
|
+
echo " what it must contain in HANDOFF.md's found: line. The context file above"
|
|
650
|
+
echo " has the full protocol and says what does and does not enforce it."
|
|
651
|
+
echo " SECOND, before you write anything: run \`git rev-parse --git-common-dir\`."
|
|
652
|
+
echo " It prints the MAIN repo's .git — which repo your worktree branched from."
|
|
653
|
+
echo " It MUST contain /$(basename "$ENTROPY_MACHINES_ROOT")/. A bare relative \`.git\` means you"
|
|
654
|
+
echo " are not in a worktree at all: report that."
|
|
655
|
+
echo " If either is wrong, STOP and report it. NEVER cd to make it pass and"
|
|
656
|
+
echo " never write to the shared checkout — a cd is the failure, not the fix."
|
|
657
|
+
echo " Do NOT use --show-toplevel here: yours is the worktree path, so it can"
|
|
658
|
+
echo " never end in /$(basename "$ENTROPY_MACHINES_ROOT") and tells you nothing about the repo."
|
|
659
|
+
# THE SCRATCHPAD, IN THE PASTE BLOCK RATHER THAN ONLY IN A DOC. The same
|
|
660
|
+
# measurement that shapes the rest of this block puts a pasted rule at ~96%
|
|
661
|
+
# and one sitting only in a doc the agent was told to go read at far less —
|
|
662
|
+
# and a scratchpad the worker never hears about is the shared one it was
|
|
663
|
+
# already using.
|
|
664
|
+
echo " YOUR PRIVATE SCRATCHPAD is \`.scratch\` at the root of your worktree —"
|
|
665
|
+
echo " mutators, probe harnesses, captured suite output go THERE, not in any"
|
|
666
|
+
echo " scratchpad shared with the agents running beside you. Two concurrent"
|
|
667
|
+
echo " agents have overwritten each other's throwaway files by both reaching"
|
|
668
|
+
echo " for a shared scratch directory instead. If a tool refuses to write"
|
|
669
|
+
echo " through .scratch, say so in HANDOFF.md — do not fall back to a shared"
|
|
670
|
+
echo " scratchpad silently."
|
|
671
|
+
# Earlier agents' carry-forward lines, INSIDE the paste block rather than
|
|
672
|
+
# only in the context file above it, for the same measured reason.
|
|
673
|
+
if [ -n "$carry" ]; then
|
|
674
|
+
echo " WHAT EARLIER WORK ON THIS ISSUE LEFT YOU (you may not be able to look"
|
|
675
|
+
echo " this up yourself — see the context file for how to check):"
|
|
676
|
+
echo "$carry"
|
|
677
|
+
echo
|
|
678
|
+
fi
|
|
679
|
+
echo " Before you finish, write HANDOFF.md at the root of your worktree."
|
|
680
|
+
echo " Exactly these four keys, ONE LINE each, no prose:"
|
|
681
|
+
echo " changed: <what you changed>"
|
|
682
|
+
echo " found: <seen but not fixed, out of scope — or: none>"
|
|
683
|
+
echo " assumed: <what you took as given — or: none>"
|
|
684
|
+
echo " next: <what the next agent needs to know — or: none>"
|
|
685
|
+
echo " Keep it under six lines total. Do not commit it; it is gitignored."
|
|
686
|
+
echo " AFTER YOU STOP YOU WILL BE QUESTIONED, while you are still resumable, and"
|
|
687
|
+
echo " your work is not lifted until your answers are recorded. The questions are"
|
|
688
|
+
echo " adversarial on purpose — what you did NOT run, which of your assertions is"
|
|
689
|
+
echo " weakest, what you took on trust, what in the brief you skipped, what you"
|
|
690
|
+
echo " would check next. They are listed in the context file above; note the"
|
|
691
|
+
echo " answers as you work. \"Nothing\" to every one of them is refused."
|
|
692
|
+
echo
|
|
693
|
+
echo "WHEN YOU LAND IT, before committing:"
|
|
694
|
+
echo " bin/handoff $id --interrogate # the questions; send them to the LIVE agent"
|
|
695
|
+
echo " bin/handoff $id --record-interrogation --answer \"...\" # what it said"
|
|
696
|
+
echo " bin/handoff $id --from <agent-worktree> --lift # copies the files IN, refusing any that drifted"
|
|
697
|
+
echo " ...then verify in THIS tree, then:"
|
|
698
|
+
echo " bin/handoff $id --from <agent-worktree> --verified \"what YOU re-ran\""
|
|
699
|
+
echo " (or spell the fields out: --changed \"...\" [--found \"...\"] [--next \"...\"] [--clean])"
|
|
700
|
+
echo " The commit-msg hook refuses a commit naming $id until that exists."
|