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.
Files changed (51) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +68 -0
  3. package/agents/isolated-worker.md +128 -0
  4. package/agents/verifier.md +158 -0
  5. package/bin/dispatch +700 -0
  6. package/bin/doclint +460 -0
  7. package/bin/drain +507 -0
  8. package/bin/drain-pick.py +168 -0
  9. package/bin/drain-prompt.md +67 -0
  10. package/bin/drain-run.sh +342 -0
  11. package/bin/entropy-machines-init +285 -0
  12. package/bin/handoff +1151 -0
  13. package/bin/init +232 -0
  14. package/bin/post-fold-audit +377 -0
  15. package/bin/serve +724 -0
  16. package/bin/status +208 -0
  17. package/bin/tracker +153 -0
  18. package/docs/AGENT-QUICKSTART.md +86 -0
  19. package/docs/CONFIG.md +68 -0
  20. package/docs/NPM.md +91 -0
  21. package/docs/SERVE.md +74 -0
  22. package/docs/TRACKER-ADAPTER.md +66 -0
  23. package/doctrine/HANDOFF-PROMPT.md +63 -0
  24. package/doctrine/README.md +62 -0
  25. package/doctrine/ROLES.md +27 -0
  26. package/doctrine/WORKFLOW.md +87 -0
  27. package/hooks/commit-msg +24 -0
  28. package/hooks/post-checkout +354 -0
  29. package/hooks/pre-commit +33 -0
  30. package/lib/PRD-001-orientation.html +1180 -0
  31. package/lib/REPORT-TEMPLATE.html +413 -0
  32. package/lib/changelog-collate.mjs +328 -0
  33. package/lib/changelog-guard.sh +157 -0
  34. package/lib/changelog-new.mjs +70 -0
  35. package/lib/config.mjs +283 -0
  36. package/lib/config.py +317 -0
  37. package/lib/doc-template.html +807 -0
  38. package/lib/entropy-drain.plist.in +59 -0
  39. package/lib/entropy-drain.service.in +53 -0
  40. package/lib/entropy-drain.timer.in +36 -0
  41. package/lib/fail-first.mjs +901 -0
  42. package/lib/handoff-guard.sh +623 -0
  43. package/lib/install-hooks.sh +169 -0
  44. package/lib/notes.py +675 -0
  45. package/lib/preflight-tree.mjs +82 -0
  46. package/lib/roots.sh +212 -0
  47. package/lib/themes/daylight.css +84 -0
  48. package/lib/themes/high-contrast.css +36 -0
  49. package/lib/tracker-file +333 -0
  50. package/lib/tracker-view.py +784 -0
  51. package/package.json +38 -0
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env bash
2
+ # Repo post-checkout hook — VERSION-CONTROLLED, like its commit-msg and
3
+ # pre-commit siblings, and installed by the same `npm run hooks:install`.
4
+ # install-hooks.sh iterates every file in hooks/, so this one was
5
+ # picked up the moment it landed; the installer needed no change to find it.
6
+ # (It did gain a check that the resolved hook dir is one `git worktree add`
7
+ # actually consults — see the note at the bottom of install-hooks.sh.)
8
+ #
9
+ # WHAT IT DOES. When `git worktree add` creates a NEW worktree, symlink every
10
+ # path in `worktree.linkPaths` (config.json) into it from the main checkout,
11
+ # and give it a PRIVATE SCRATCHPAD reachable as `.scratch`.
12
+ #
13
+ # WHY. A worktree gets the TRACKED tree and nothing else. A dependency
14
+ # directory is untracked; a gitignored or nested-repo directory is absent
15
+ # outright. So a fresh agent worktree has neither, and the gap used to be
16
+ # closed by a sentence pasted into every dispatch brief — a mechanism measured
17
+ # at 96% adherence when pasted and 7-8% when not, i.e. it works, but the
18
+ # missing few percent is silent.
19
+ # The dependency half is the more dangerous one: many runtimes resolve modules
20
+ # by walking UP the directory tree, so a worktree with no local dependency
21
+ # directory can resolve a test runner out of a SIBLING agent's worktree. That
22
+ # is how one agent ran someone else's code and reported it green
23
+ #. lib/preflight-tree.mjs now refuses
24
+ # instead of guessing, and a symlink is exactly what it asks for.
25
+ #
26
+ # HOW IT KNOWS IT IS A NEW WORKTREE, and not the ordinary checkout this hook
27
+ # also fires on. All three must hold. Verified by probing a real repo on
28
+ # 2026-08-27 rather than taken from the docs:
29
+ #
30
+ # $3 = 1 a branch checkout, not `git checkout -- <path>`
31
+ # (a file checkout passes 0)
32
+ # $1 = the null sha only a brand-new worktree — and `git clone` — has
33
+ # no previous HEAD. A checkout inside an ALREADY
34
+ # EXISTING worktree passes a real sha here, which is
35
+ # the case this test alone excludes.
36
+ # git-dir != common-dir we are in a LINKED worktree. This is what excludes
37
+ # `git clone`, which also passes the null sha, and
38
+ # the main checkout, where the two are equal.
39
+ #
40
+ # THE MAIN CHECKOUT IS RESOLVED, NEVER HARDCODED. `git rev-parse
41
+ # --git-common-dir` is the main .git even when called from a worktree, so its
42
+ # parent is the tree holding the real node_modules. A literal path would be
43
+ # wrong on every other machine, and wrong on any machine holding a second
44
+ # checkout of the same project.
45
+ #
46
+ # WRITE ACCESS THROUGH A LINKED PATH IS DELIBERATE, NOT AN OVERSIGHT.
47
+ # A symlink is read-write, so an agent can mutate a linked tracker store — and
48
+ # the main session writes it constantly, since dispatch and handoff both append
49
+ # to the note log on every dispatch and every landing. We link anyway, because
50
+ # the lost-update race that argues against it is CLOSED one layer down, in the
51
+ # tracker backend: it wraps every load-modify-save in an exclusive lock, writes
52
+ # through a per-pid temp file and an atomic replace, and appends the note log
53
+ # as whole JSONL lines under that same lock. File locks key on the INODE, and a
54
+ # symlinked path resolves to the same inode as the main checkout's, so a
55
+ # worktree writer and a main-session writer serialize against each other
56
+ # exactly as two main-checkout writers already do.
57
+ #
58
+ # A BACKEND THAT DOES NOT LOCK MAKES THIS UNSAFE. That is the contract's
59
+ # requirement, not a nicety — see docs/TRACKER-ADAPTER.md on `remember`.
60
+ #
61
+ # SO, WHEN TWO WRITERS RACE: they serialize. The second blocks in flock until
62
+ # the first releases, then re-reads and applies on top of the first's result.
63
+ # os.replace is atomic, so no reader ever sees a torn file, and the per-pid tmp
64
+ # name means two writers cannot collide on the temp path. Nothing is lost. That
65
+ # lock exists precisely because the race was measured live before it landed —
66
+ # 8/10 concurrent claims either crashed on a shared ".tmp" name or vanished.
67
+ #
68
+ # WHAT THAT DOES NOT COVER, said out loud rather than left implied:
69
+ # 1. The lock only binds processes that go through the tracker backend. An
70
+ # editor, a shell redirect or a one-liner writing the store directly
71
+ # bypasses it. A read-only wrapper would not have closed that hole
72
+ # either, which is why there is not one — it would be a second mechanism
73
+ # for a job the lock already does, and "two mechanisms for one job is how
74
+ # a stale one survives" is the reason this hook exists at all.
75
+ # 2. A file lock serializes WRITES, not INTENT. Two agents that each legitimately
76
+ # `set status=` on one issue both succeed and the later one wins. That is
77
+ # a dispatcher coordination question, not a data race, and no file lock
78
+ # can adjudicate it.
79
+ # 3. A backend may have internal helpers that save without taking the lock
80
+ # themselves, safe only because every caller already holds it. That
81
+ # coupling belongs to the backend, not to this hook.
82
+ #
83
+ # THE THIRD LINK IS A SCRATCHPAD, AND IT IS THE SAME BUG ONE LAYER DOWN.
84
+ # N agents get N worktrees so they cannot clobber each other's edits — and then
85
+ # every one of them writes its throwaway files into ONE shared scratchpad,
86
+ # because that is the only scratch path any of them is handed. A worker once
87
+ # reported a sibling overwriting its mutator script mid-run. Nothing was
88
+ # corrupted that time because the results were already captured; that is luck,
89
+ # not isolation.
90
+ #
91
+ # The scratchpad is where the files whose correctness is hardest to re-check
92
+ # from the repo afterwards live: mutation scripts, probe harnesses, captured
93
+ # suite output. A mutation harness whose mutator is silently replaced mid-run
94
+ # prints a full, confident, WRONG table — every row a real number, none of them
95
+ # about the code the agent thinks it mutated.
96
+ #
97
+ # So: <main_tree>/.entropy-machines/scratch/<worktree-name>/, linked in as `.scratch`.
98
+ # The key is the worktree's own directory name, which for a dispatched agent IS
99
+ # `agent-<id>` — the same id that names the worktree — so the mapping is
100
+ # greppable rather than derived.
101
+ #
102
+ # WHY THE REAL DIRECTORY LIVES OUTSIDE THE WORKTREE. A scratchpad inside the
103
+ # worktree would be private and cleaned up for free, which is most of what is
104
+ # wanted — but the worktree is REMOVED once its work is landed, and the scratch
105
+ # evidence dies with it. That is an argument about where the evidence goes, not
106
+ # where the working files go, and one directory serves both: it is private
107
+ # while the agent runs and it is still there when the lander wants to see what
108
+ # the agent actually ran. Nothing prunes it; it is small, and a directory that
109
+ # deletes evidence on a schedule is worse than one that grows.
110
+ #
111
+ # WHAT THIS DOES NOT ENFORCE, said plainly. Nothing makes an agent WRITE there.
112
+ # The path exists and is announced; an agent that keeps using the shared
113
+ # session scratchpad is not stopped by anything here. The mechanical half of
114
+ # this concern belongs to a mutator script, which fingerprints itself and
115
+ # REFUSES when it changes mid-run — that one does not depend on any agent
116
+ # honouring a convention.
117
+ #
118
+ # IT MUST NEVER BLOCK THE WORKTREE. Deliberately no `set -e`: every failure
119
+ # warns on stderr and the hook still exits 0. A worktree with no node_modules
120
+ # is a bad day; a `git worktree add` that fails outright is a worse one.
121
+
122
+ set -u
123
+
124
+ warn() {
125
+ echo "post-checkout: $*" >&2
126
+ }
127
+
128
+ # Always succeed, whatever happened above this line.
129
+ trap 'exit 0' EXIT
130
+
131
+ # ENTROPY_MACHINES_HOME (the harness DIRECTORY — where lib/config.py actually lives,
132
+ # which may be the repo root or a subdirectory of it) is baked in and exported
133
+ # by the installed shim (lib/install-hooks.sh). The fallback here only matters
134
+ # when this hook is run directly, bypassing the shim — by hand, or from a
135
+ # test. Best-effort and silent on failure, matching this whole hook's fail-open
136
+ # philosophy: config_link_paths() below already no-ops with nothing linked if
137
+ # ENTROPY_MACHINES_HOME cannot be resolved.
138
+ #
139
+ # This hook does not call entropy_machines_require_root — it resolves the repo's main
140
+ # tree itself, below, via `--git-common-dir`, because it needs that value for
141
+ # the new-worktree test too, not only for config lookup.
142
+ if [ -z "${ENTROPY_MACHINES_HOME:-}" ]; then
143
+ . "$(dirname "$0")/../lib/roots.sh" 2>/dev/null && ENTROPY_MACHINES_HOME="$(entropy_machines_home "$0" 2>/dev/null)"
144
+ fi
145
+
146
+ prev="${1:-}"
147
+ flag="${3:-0}"
148
+
149
+ # --- is this a brand-new worktree? -----------------------------------------
150
+
151
+ # A file checkout (`git checkout -- path`) passes 0 here.
152
+ [ "$flag" = "1" ] || exit 0
153
+
154
+ # The null sha, and only the null sha: 40 (or 64) zeros. Anything containing a
155
+ # non-zero character is a real previous HEAD, i.e. a checkout inside a worktree
156
+ # that already exists and already has its links.
157
+ [ -n "$prev" ] || exit 0
158
+ case "$prev" in
159
+ *[!0]*) exit 0 ;;
160
+ esac
161
+
162
+ abspath_dir() {
163
+ # Absolute path of a directory, without --path-format=absolute (git 2.31+)
164
+ # so this works on older git too.
165
+ case "$1" in
166
+ /*) printf '%s\n' "$1" ;;
167
+ *) ( cd "$1" 2>/dev/null && pwd ) ;;
168
+ esac
169
+ }
170
+
171
+ git_dir="$(git rev-parse --git-dir 2>/dev/null)" || exit 0
172
+ common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" || exit 0
173
+ git_dir="$(abspath_dir "$git_dir")"
174
+ common_dir="$(abspath_dir "$common_dir")"
175
+ [ -n "$git_dir" ] && [ -n "$common_dir" ] || exit 0
176
+
177
+ # Equal in the main checkout and in a fresh `git clone`; different only in a
178
+ # linked worktree. This is the test that keeps a clone from linking onto itself.
179
+ [ "$git_dir" != "$common_dir" ] || exit 0
180
+
181
+ here="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
182
+ main_tree="$(dirname "$common_dir")"
183
+ [ -n "$here" ] && [ -n "$main_tree" ] || exit 0
184
+ # Never link a tree to itself. STRICTLY REDUNDANT given the git-dir test above
185
+ # — the only way here == main_tree is a plain main checkout, which that test has
186
+ # already excluded — and deliberately kept anyway as a one-line failsafe against
187
+ # a future edit loosening it. Said plainly because NO TEST ISOLATES THIS LINE:
188
+ # delete it and every test still passes. It is not load-bearing; do not add
189
+ # behaviour behind it.
190
+ [ "$here" != "$main_tree" ] || exit 0
191
+
192
+ # --- link, but never clobber -----------------------------------------------
193
+
194
+ # link_one <name-in-this-worktree> [target-path]
195
+ # The target defaults to the SAME name in the main checkout, which is what
196
+ # the configured link paths want. The scratchpad passes its own target
197
+ # because the two names differ on purpose: `.scratch` here, a per-worktree
198
+ # directory under the main checkout's .entropy-machines/scratch/ there.
199
+ link_one() {
200
+ name="$1"
201
+ target="${2:-$main_tree/$name}"
202
+ dest="$here/$name"
203
+
204
+ if [ ! -e "$target" ]; then
205
+ warn "$name does not exist in the main checkout ($main_tree) — not linked."
206
+ return 0
207
+ fi
208
+
209
+ # -L before -e: a symlink pointing at nothing is still a symlink, and -e is
210
+ # false for it.
211
+ if [ -L "$dest" ]; then
212
+ current="$(readlink "$dest" 2>/dev/null || true)"
213
+ if [ "$current" = "$target" ]; then
214
+ return 0
215
+ fi
216
+ warn "$name is already a symlink to '$current', not '$target' — left alone."
217
+ return 0
218
+ fi
219
+
220
+ if [ -e "$dest" ]; then
221
+ warn "$name already exists here and is not a symlink — left alone. This"
222
+ warn " worktree will use its own copy, not the main checkout's."
223
+ return 0
224
+ fi
225
+
226
+ if ln -s "$target" "$dest" 2>/dev/null; then
227
+ linked="${linked:+$linked, }$name"
228
+ else
229
+ warn "could not link $name -> $target. Link it by hand if a suite refuses."
230
+ fi
231
+ return 0
232
+ }
233
+
234
+ # Read worktree.linkPaths from the MAIN checkout's config.json — the worktree
235
+ # may not have one yet, and even if it does, the main checkout's copy is the
236
+ # one whose dependency directories we are linking to.
237
+ #
238
+ # Deliberately fail-open and silent: a hook that aborts a `git worktree add`
239
+ # because a config key is missing would make this repo's own installation the
240
+ # thing that breaks other people's git. No config, no Python, malformed JSON —
241
+ # all mean "link nothing", and the warning at the end still names what was and
242
+ # was not linked.
243
+ config_link_paths() {
244
+ [ -f "${ENTROPY_MACHINES_HOME:-$main_tree}/config.json" ] || return 0
245
+ command -v python3 >/dev/null 2>&1 || return 0
246
+ [ -n "${ENTROPY_MACHINES_HOME:-}" ] || return 0
247
+
248
+ # TWO PATHS, ONE ROOT. lib/config.py's own root resolution (find_repo_root)
249
+ # walks up from `os.getcwd()` looking for config.json — it does not care
250
+ # where its own file lives on disk. So cwd must be the MAIN tree for the
251
+ # lookup to resolve against a tree that certainly exists (a post-checkout
252
+ # hook's cwd is the NEW worktree, which may still be mid-creation). But
253
+ # lib/config.py the SCRIPT FILE lives in the harness DIRECTORY, which may be
254
+ # a subdirectory: "$main_tree/lib/config.py" does not exist there, only
255
+ # "$ENTROPY_MACHINES_HOME/lib/config.py" does. So: cd into the main tree, but exec the
256
+ # harness's copy of the loader. The subshell keeps the cd off the caller.
257
+ ( cd "$main_tree" 2>/dev/null || exit 0
258
+ python3 "$ENTROPY_MACHINES_HOME/lib/config.py" get worktree.linkPaths 2>/dev/null ) \
259
+ | tr -d '[]",' | tr ' ' '\n' | sed '/^$/d'
260
+ }
261
+
262
+ linked=""
263
+
264
+ # CONFIG.JSON IS UNTRACKED AND ONLY EXISTS IN THE MAIN CHECKOUT'S HARNESS DIR.
265
+ # Without this link every tool that reads $ENTROPY_MACHINES_HOME/config.json
266
+ # refuses inside a worktree. Hardcoded because config.json is what tells us
267
+ # what ELSE to link — it cannot come from its own content.
268
+ if [ -n "${ENTROPY_MACHINES_HOME:-}" ]; then
269
+ case "$ENTROPY_MACHINES_HOME" in
270
+ "$main_tree") link_one "config.json" ;;
271
+ "$main_tree"/*) link_one "${ENTROPY_MACHINES_HOME#"$main_tree"/}/config.json" ;;
272
+ esac
273
+ fi
274
+
275
+ # WHICH PATHS GET LINKED IS CONFIG, NOT CODE. A hardcoded list here was wrong
276
+ # for every project but the one this hook was written in. Each entry is a path
277
+ # relative to the main checkout; one that does not exist there is skipped in
278
+ # silence, because a project that simply has no dependency directory is not an
279
+ # error.
280
+ for _p in $(config_link_paths); do
281
+ [ -n "$_p" ] || continue
282
+ link_one "$_p"
283
+ done
284
+
285
+ # --- the private scratchpad -------------------------------------------------
286
+ # Created, not just linked: unlike the other two there is nothing in the main
287
+ # checkout to point at until this hook makes it. Everything below is
288
+ # best-effort and silent on the happy path, so a re-run of the hook over an
289
+ # already-linked worktree prints nothing.
290
+
291
+ scratch_name="$(basename "$here")"
292
+ # Rooted at $main_tree (the repo's main checkout), not ENTROPY_MACHINES_HOME — the
293
+ # scratchpad belongs to the project being worked on, not to wherever inside it
294
+ # the harness happens to be vendored.
295
+ scratch_root="$main_tree/${ENTROPY_MACHINES_SCRATCH_ROOT:-.entropy-machines/scratch}"
296
+ scratch_target="$scratch_root/$scratch_name"
297
+
298
+ if [ ! -d "$scratch_target" ] && ! mkdir -p "$scratch_target" 2>/dev/null; then
299
+ warn "could not create $scratch_target — this worktree has no private scratchpad."
300
+ fi
301
+
302
+ if [ -d "$scratch_target" ]; then
303
+ scratch_readme="$scratch_target/README"
304
+ if [ ! -e "$scratch_readme" ]; then
305
+ {
306
+ echo "Private scratchpad for one git worktree."
307
+ echo
308
+ echo "worktree: $here"
309
+ echo "created: $(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo unknown)"
310
+ echo
311
+ echo "Reachable from that worktree as .scratch — put mutators, probe"
312
+ echo "harnesses and captured suite output here, NOT in the session"
313
+ echo "scratchpad, which every concurrent agent shares. A sibling agent"
314
+ echo "overwrote a worker's mutator there mid-run, which is why this"
315
+ echo "directory exists."
316
+ echo
317
+ echo "This directory OUTLIVES the worktree: it is under the main"
318
+ echo "checkout, so it is still readable after the work is landed and the"
319
+ echo "worktree removed. Nothing prunes it."
320
+ } >"$scratch_readme" 2>/dev/null || warn "could not write $scratch_readme"
321
+ else
322
+ # Two worktrees resolving to one scratchpad name is the very thing this
323
+ # directory exists to prevent, so it is said out loud rather than merged.
324
+ scratch_owner="$(sed -n 's/^worktree: //p' "$scratch_readme" 2>/dev/null | head -1)"
325
+ if [ -n "$scratch_owner" ] && [ "$scratch_owner" != "$here" ]; then
326
+ warn ".scratch target $scratch_target was created for $scratch_owner,"
327
+ warn " not $here — two worktrees share one scratchpad name. Files written"
328
+ warn " through either link land in the same directory."
329
+ fi
330
+ fi
331
+ link_one .scratch "$scratch_target"
332
+ fi
333
+
334
+ if [ -n "$linked" ]; then
335
+ warn "linked $linked from $main_tree"
336
+ case "$linked" in
337
+ *tracker*|*issues*|*plan*|*docs*)
338
+ warn " A linked path is SHARED AND WRITABLE, not a copy. If it holds"
339
+ warn " tracker state, a mutating command here changes the REAL tracker."
340
+ warn " Writes serialize (the backend takes a lock), so nothing is lost"
341
+ warn " — but there is no sandbox to undo them in."
342
+ ;;
343
+ esac
344
+ case "$linked" in
345
+ *.scratch*)
346
+ warn " .scratch is PRIVATE TO THIS WORKTREE and outlives it:"
347
+ warn " $scratch_target"
348
+ warn " Write mutators and captured output there, not in the session"
349
+ warn " scratchpad every concurrent agent shares."
350
+ ;;
351
+ esac
352
+ fi
353
+
354
+ exit 0
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # Repo pre-commit hook — VERSION-CONTROLLED, unlike the copy in .git/hooks.
3
+ #
4
+ # The installed shim (lib/install-hooks.sh) execs THIS working-tree file
5
+ # directly, by an ABSOLUTE path baked in at install time, so the installed
6
+ # artefact never goes stale and reviewing the hook means reviewing this file.
7
+ # The baked path is the harness in the MAIN checkout, so every worktree runs
8
+ # THIS copy, not its own checked-in one — deliberate: a gate that a worker
9
+ # could weaken by editing its own worktree would not be a gate.
10
+ #
11
+ # ONE ROOT. lib/changelog-guard.sh is a harness file living one directory up
12
+ # from this one, inside the same repository as the commit being made. The shim
13
+ # exports ENTROPY_MACHINES_HOME (it lives in .git/hooks and cannot find lib/ relatively);
14
+ # the fallback below only matters when this file is run directly — by hand, or
15
+ # from a test — bypassing the shim.
16
+ #
17
+ # `git diff --cached`, which changelog-guard.sh reads, operates on the working
18
+ # tree git already put us in: git runs pre-commit with cwd at the working-tree
19
+ # root, and nothing here changes that. In a linked worktree that is the
20
+ # worktree, which is correct — the staged files being committed are there.
21
+ #
22
+ # Keep it fast. It runs on every commit; anything slower than a `git diff`
23
+ # belongs in CI.
24
+
25
+ set -euo pipefail
26
+
27
+ . "$(dirname "$0")/../lib/roots.sh"
28
+
29
+ : "${ENTROPY_MACHINES_HOME:=$(entropy_machines_home "$0")}"
30
+ export ENTROPY_MACHINES_HOME
31
+ entropy_machines_require_root pre-commit
32
+
33
+ exec "$ENTROPY_MACHINES_HOME/lib/changelog-guard.sh" --staged