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
|
@@ -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
|
package/hooks/pre-commit
ADDED
|
@@ -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
|