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/status
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""status — print live factory state. Reads; writes nothing.
|
|
3
|
+
|
|
4
|
+
bin/status
|
|
5
|
+
|
|
6
|
+
Three questions, answered from the tracker and the docs directory:
|
|
7
|
+
|
|
8
|
+
what is ready to work on (bin/tracker ready)
|
|
9
|
+
what is in flight (DISPATCH notes with no HANDOFF yet)
|
|
10
|
+
which docs have open questions (unanswered .response boxes)
|
|
11
|
+
|
|
12
|
+
It is a READ. Nothing here files, claims, dispatches or edits — so it is safe
|
|
13
|
+
to run at the top of any session, on a shared checkout, or in a loop.
|
|
14
|
+
|
|
15
|
+
This is the one surviving half of what used to be `bin/adopt prime`, the body
|
|
16
|
+
of a Claude Code SessionStart hook that bin/adopt installed into
|
|
17
|
+
.claude/settings.json. That whole mechanism — a hashed managed block written
|
|
18
|
+
into someone else's instruction file, a consent prompt, a settings.json edit,
|
|
19
|
+
per-runner profiles — is gone. The PRINTING is what was actually worth having,
|
|
20
|
+
and it is worth having as a command a human or an agent can just run.
|
|
21
|
+
"""
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
# ENTROPY_MACHINES_HOME is the harness DIRECTORY — where bin/tracker sits — which may be
|
|
29
|
+
# the repo root or a subdirectory of it. Not a root and not a separate repo;
|
|
30
|
+
# see lib/roots.sh.
|
|
31
|
+
ENTROPY_MACHINES_HOME = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
32
|
+
sys.path.insert(0, os.path.join(ENTROPY_MACHINES_HOME, "lib"))
|
|
33
|
+
import config # noqa: E402 -- needs ENTROPY_MACHINES_HOME on the path first
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def entropy_machines_root():
|
|
37
|
+
"""Mirrors lib/roots.sh's entropy_machines_root(): the repository's main checkout,
|
|
38
|
+
or None. `git rev-parse --git-common-dir` -- NOT --show-toplevel, which
|
|
39
|
+
prints a linked WORKTREE's own path; .entropy/ is gitignored and absent
|
|
40
|
+
from every worktree, so that would report an empty tracker as "0 ready"
|
|
41
|
+
rather than as an error. Falls back to walking up for config.json when
|
|
42
|
+
there is no git."""
|
|
43
|
+
try:
|
|
44
|
+
p = subprocess.run(
|
|
45
|
+
["git", "rev-parse", "--git-common-dir"],
|
|
46
|
+
capture_output=True, text=True, timeout=10,
|
|
47
|
+
)
|
|
48
|
+
except (OSError, subprocess.SubprocessError):
|
|
49
|
+
p = None
|
|
50
|
+
if p is not None and p.returncode == 0 and p.stdout.strip():
|
|
51
|
+
common = p.stdout.strip()
|
|
52
|
+
if not os.path.isabs(common):
|
|
53
|
+
common = os.path.join(os.getcwd(), common)
|
|
54
|
+
return os.path.realpath(os.path.join(common, os.pardir))
|
|
55
|
+
d = os.getcwd()
|
|
56
|
+
while True:
|
|
57
|
+
if os.path.exists(os.path.join(d, "config.json")):
|
|
58
|
+
return d
|
|
59
|
+
parent = os.path.dirname(d)
|
|
60
|
+
if parent == d:
|
|
61
|
+
return None
|
|
62
|
+
d = parent
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def docs_dir_for(root):
|
|
66
|
+
"""config.json's docs.dir, defaulting to entropy-machines-docs -- the same
|
|
67
|
+
default bin/init writes to and bin/serve reads from. Never raises."""
|
|
68
|
+
try:
|
|
69
|
+
with open(os.path.join(ENTROPY_MACHINES_HOME, "config.json"), encoding="utf-8") as f:
|
|
70
|
+
cfg = json.load(f)
|
|
71
|
+
d = (cfg.get("docs") or {}).get("dir")
|
|
72
|
+
if isinstance(d, str) and d.strip():
|
|
73
|
+
return d.strip().strip("/")
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
return "entropy-machines-docs"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def scan_unanswered_docs(root):
|
|
80
|
+
"""Best-effort: dialogue docs in this harness's convention mark each
|
|
81
|
+
question with <div class="response" data-resp="KEY"> and record answers in
|
|
82
|
+
a <script id="responses-data"> JSON blob (see bin/serve,
|
|
83
|
+
lib/doc-template.html). Returns [(path, unanswered_count), ...] for docs
|
|
84
|
+
with at least one unanswered question. Never raises -- a doc this cannot
|
|
85
|
+
parse is skipped."""
|
|
86
|
+
# THE CONFIGURED DOCS DIRECTORY. bin/init installs the orientation PRD into
|
|
87
|
+
# docs.dir and bin/serve serves it from there; this must agree with both.
|
|
88
|
+
docs = os.path.join(root, docs_dir_for(root))
|
|
89
|
+
if not os.path.isdir(docs):
|
|
90
|
+
return []
|
|
91
|
+
out = []
|
|
92
|
+
resp_re = re.compile(r'class="response"[^>]*data-resp="([^"]+)"')
|
|
93
|
+
data_re = re.compile(r'<script[^>]*id="responses-data"[^>]*>(.*?)</script>', re.DOTALL)
|
|
94
|
+
for fname in sorted(os.listdir(docs)):
|
|
95
|
+
if not fname.endswith(".html"):
|
|
96
|
+
continue
|
|
97
|
+
path = os.path.join(docs, fname)
|
|
98
|
+
try:
|
|
99
|
+
with open(path, encoding="utf-8") as f:
|
|
100
|
+
content = f.read()
|
|
101
|
+
# Strip HTML comments first. lib/doc-template.html's own header
|
|
102
|
+
# comment contains a literal data-resp example -- counting it made
|
|
103
|
+
# every doc built from the template report one phantom question
|
|
104
|
+
# nobody could ever answer, so a fully-answered doc kept nagging.
|
|
105
|
+
content = re.sub(r"<!--[\s\S]*?-->", "", content)
|
|
106
|
+
keys = set(resp_re.findall(content))
|
|
107
|
+
if not keys:
|
|
108
|
+
continue
|
|
109
|
+
m = data_re.search(content)
|
|
110
|
+
answered = set()
|
|
111
|
+
if m:
|
|
112
|
+
try:
|
|
113
|
+
answered = {k for k, v in json.loads(m.group(1)).items() if str(v).strip()}
|
|
114
|
+
except (json.JSONDecodeError, AttributeError):
|
|
115
|
+
pass
|
|
116
|
+
unanswered_n = len(keys - answered)
|
|
117
|
+
if unanswered_n:
|
|
118
|
+
out.append((path, unanswered_n))
|
|
119
|
+
except OSError:
|
|
120
|
+
continue
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv):
|
|
125
|
+
if argv and argv[0] in ("-h", "--help"):
|
|
126
|
+
print(__doc__.strip("\n"))
|
|
127
|
+
return 0
|
|
128
|
+
if argv:
|
|
129
|
+
print("usage: bin/status", file=sys.stderr)
|
|
130
|
+
return 2
|
|
131
|
+
|
|
132
|
+
config.refuse_nested_clone("status")
|
|
133
|
+
root = entropy_machines_root()
|
|
134
|
+
if not root or not os.path.isfile(os.path.join(ENTROPY_MACHINES_HOME, "config.json")):
|
|
135
|
+
print("entropy-machines: not configured here -- run %s/bin/init" % ENTROPY_MACHINES_HOME)
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
tracker = os.path.join(ENTROPY_MACHINES_HOME, "bin", "tracker")
|
|
139
|
+
|
|
140
|
+
def run_tracker(args):
|
|
141
|
+
try:
|
|
142
|
+
p = subprocess.run(
|
|
143
|
+
[tracker] + args, cwd=root, capture_output=True, text=True, timeout=15,
|
|
144
|
+
)
|
|
145
|
+
except (OSError, subprocess.SubprocessError):
|
|
146
|
+
return None
|
|
147
|
+
if p.returncode != 0:
|
|
148
|
+
return None
|
|
149
|
+
return p.stdout
|
|
150
|
+
|
|
151
|
+
ready_out = run_tracker(["ready"])
|
|
152
|
+
if ready_out is None:
|
|
153
|
+
print("entropy-machines: tracker unavailable (bin/tracker ready failed)")
|
|
154
|
+
else:
|
|
155
|
+
ready = []
|
|
156
|
+
for line in ready_out.splitlines():
|
|
157
|
+
line = line.strip()
|
|
158
|
+
if not line:
|
|
159
|
+
continue
|
|
160
|
+
try:
|
|
161
|
+
ready.append(json.loads(line))
|
|
162
|
+
except json.JSONDecodeError:
|
|
163
|
+
continue
|
|
164
|
+
if ready:
|
|
165
|
+
print("entropy-machines: %d issue(s) ready -- `bin/tracker ready`" % len(ready))
|
|
166
|
+
for i in ready[:10]:
|
|
167
|
+
print(" %s %s" % (i.get("id", "?"), i.get("title") or i.get("summary") or ""))
|
|
168
|
+
if len(ready) > 10:
|
|
169
|
+
print(" ... and %d more" % (len(ready) - 10))
|
|
170
|
+
else:
|
|
171
|
+
print("entropy-machines: 0 issues ready")
|
|
172
|
+
|
|
173
|
+
notes_out = run_tracker(["notes"])
|
|
174
|
+
if notes_out is not None:
|
|
175
|
+
state = {}
|
|
176
|
+
for line in notes_out.splitlines():
|
|
177
|
+
line = line.strip()
|
|
178
|
+
if not line:
|
|
179
|
+
continue
|
|
180
|
+
try:
|
|
181
|
+
rec = json.loads(line)
|
|
182
|
+
except json.JSONDecodeError:
|
|
183
|
+
continue
|
|
184
|
+
issue = rec.get("issue")
|
|
185
|
+
if not issue:
|
|
186
|
+
continue
|
|
187
|
+
verb = rec.get("verb")
|
|
188
|
+
if verb == "DISPATCH":
|
|
189
|
+
state[issue] = "dispatched"
|
|
190
|
+
elif verb == "HANDOFF":
|
|
191
|
+
state[issue] = "handed_off"
|
|
192
|
+
in_flight = [i for i, st in state.items() if st == "dispatched"]
|
|
193
|
+
if in_flight:
|
|
194
|
+
print("entropy-machines: %d issue(s) in flight (dispatched, not yet handed off): %s"
|
|
195
|
+
% (len(in_flight), ", ".join(sorted(in_flight))))
|
|
196
|
+
|
|
197
|
+
unanswered = scan_unanswered_docs(root)
|
|
198
|
+
if unanswered:
|
|
199
|
+
total_q = sum(n for _, n in unanswered)
|
|
200
|
+
print("entropy-machines: %d unanswered question(s) across %d doc(s) in %s/:"
|
|
201
|
+
% (total_q, len(unanswered), docs_dir_for(root)))
|
|
202
|
+
for path, n in unanswered:
|
|
203
|
+
print(" %s -- %d unanswered" % (os.path.relpath(path, root), n))
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
sys.exit(main(sys.argv[1:]))
|
package/bin/tracker
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Adapter dispatcher for the issue tracker. Every caller in this harness goes
|
|
3
|
+
# through this file instead of a backend directly — see
|
|
4
|
+
# docs/TRACKER-ADAPTER.md for the six-command contract every backend
|
|
5
|
+
# implements, and docs/CONFIG.md for config.json.
|
|
6
|
+
#
|
|
7
|
+
# bin/tracker show i-foo
|
|
8
|
+
# bin/tracker notes --issue i-foo
|
|
9
|
+
# bin/tracker remember --issue i-foo "..."
|
|
10
|
+
# bin/tracker claim i-foo
|
|
11
|
+
# bin/tracker ready
|
|
12
|
+
# bin/tracker set i-foo status=done
|
|
13
|
+
# bin/tracker render
|
|
14
|
+
#
|
|
15
|
+
# `render` is the ONE word this file answers itself. It is not a seventh
|
|
16
|
+
# adapter operation and never reaches a backend: it writes TRACKER.html, a
|
|
17
|
+
# read-only browsable view of the store, into docs.dir (lib/tracker-view.py).
|
|
18
|
+
# Rendering is a view over what a backend already holds, so requiring it of
|
|
19
|
+
# every future backend would be asking each one to ship an HTML generator.
|
|
20
|
+
#
|
|
21
|
+
# `tracker.backend` in config.json picks the implementation: "file" runs the
|
|
22
|
+
# built-in lib/tracker-file over a flat JSON file, "command" shells out to an
|
|
23
|
+
# external tracker binary. This file reads that config once and never again —
|
|
24
|
+
# a backend never re-reads config.json itself (docs/CONFIG.md rule 3).
|
|
25
|
+
#
|
|
26
|
+
# WHY THE WHOLE DISPATCH RUNS IN A SUBSHELL — this is not ergonomics, it is a
|
|
27
|
+
# correctness guard inherited unchanged from the tool this one replaced. That
|
|
28
|
+
# tool's entire job was to `cd` into a hardcoded nested tracker directory
|
|
29
|
+
# before running its backend, because the backend resolved its storage
|
|
30
|
+
# relative to the process's cwd. A caller whose shell keeps its working
|
|
31
|
+
# directory between commands — an agent harness with a persistent shell is
|
|
32
|
+
# exactly this shape — had that `cd` leak into every later command in the
|
|
33
|
+
# same session, silently, and a worktree or a dispatch created after the leak
|
|
34
|
+
# was rooted in the wrong repository. It happened three times before the fix,
|
|
35
|
+
# twice in one day, taking out two dispatched agents at once.
|
|
36
|
+
#
|
|
37
|
+
# This file no longer needs to `cd` anywhere itself — the file backend takes
|
|
38
|
+
# its storage path as configuration, not as an ambient cwd — but the guard is
|
|
39
|
+
# kept anyway and generalized: a `command` backend is arbitrary external code,
|
|
40
|
+
# and nothing stops it from changing directory or leaving other state behind.
|
|
41
|
+
# Everything below runs inside `( ... )`, so nothing the resolved backend does
|
|
42
|
+
# to the working directory survives past this script returning. Do not let
|
|
43
|
+
# the final backend invocation become a bare top-level `exec`; that removes
|
|
44
|
+
# the one property this file exists to guarantee.
|
|
45
|
+
set -e
|
|
46
|
+
|
|
47
|
+
usage() {
|
|
48
|
+
cat >&2 <<'EOF'
|
|
49
|
+
usage: bin/tracker show <id>
|
|
50
|
+
bin/tracker notes [--issue <id>]
|
|
51
|
+
bin/tracker remember --issue <id> <text>
|
|
52
|
+
bin/tracker claim <id>
|
|
53
|
+
bin/tracker ready
|
|
54
|
+
bin/tracker set <id> <key>=<value> [<key>=<value> ...]
|
|
55
|
+
bin/tracker render
|
|
56
|
+
EOF
|
|
57
|
+
exit 2
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[ $# -ge 1 ] || usage
|
|
61
|
+
|
|
62
|
+
. "$(dirname "$0")/../lib/roots.sh"
|
|
63
|
+
ENTROPY_MACHINES_HOME=$(entropy_machines_home "$0")
|
|
64
|
+
entropy_machines_require_root tracker
|
|
65
|
+
|
|
66
|
+
config="$ENTROPY_MACHINES_HOME/config.json"
|
|
67
|
+
|
|
68
|
+
# One read of config.json, here, once. Prints four tab-separated fields:
|
|
69
|
+
# backend "file" or "command" (defaults to "file")
|
|
70
|
+
# file backend path tracker.file.path, defaulted
|
|
71
|
+
# command bin tracker.command.bin
|
|
72
|
+
# command args tracker.command.args, space-joined (an argument that
|
|
73
|
+
# itself contains a space is not representable this
|
|
74
|
+
# way — the same limitation any space-joined shell
|
|
75
|
+
# config carries; a backend needing one should read its
|
|
76
|
+
# own richer config, not rely on this passthrough)
|
|
77
|
+
info=$(python3 - "$config" <<'PY'
|
|
78
|
+
import json, sys
|
|
79
|
+
with open(sys.argv[1], encoding="utf-8") as f:
|
|
80
|
+
cfg = json.load(f)
|
|
81
|
+
t = cfg.get("tracker") or {}
|
|
82
|
+
backend = t.get("backend") or "file"
|
|
83
|
+
file_path = ((t.get("file") or {}).get("path")) or ".entropy/issues.json"
|
|
84
|
+
command = t.get("command") or {}
|
|
85
|
+
cmd_bin = command.get("bin") or ""
|
|
86
|
+
cmd_args = " ".join(command.get("args") or [])
|
|
87
|
+
# Fields 5 and 6 are for `render` only. They are read HERE, in the one place
|
|
88
|
+
# this harness reads config.json per command (docs/CONFIG.md rule 3), rather
|
|
89
|
+
# than in lib/tracker-view.py — which must stay as ignorant of the config file
|
|
90
|
+
# as a backend is.
|
|
91
|
+
docs_dir = ((cfg.get("docs") or {}).get("dir")) or "entropy-machines-docs"
|
|
92
|
+
project_name = ((cfg.get("project") or {}).get("name")) or ""
|
|
93
|
+
print("\t".join([backend, file_path, cmd_bin, cmd_args, docs_dir, project_name]))
|
|
94
|
+
PY
|
|
95
|
+
) || {
|
|
96
|
+
echo "tracker: REFUSED — could not read tracker config out of $config." >&2
|
|
97
|
+
exit 2
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
backend=$(printf '%s' "$info" | cut -f1)
|
|
101
|
+
file_path=$(printf '%s' "$info" | cut -f2)
|
|
102
|
+
cmd_bin=$(printf '%s' "$info" | cut -f3)
|
|
103
|
+
cmd_args=$(printf '%s' "$info" | cut -f4)
|
|
104
|
+
docs_dir=$(printf '%s' "$info" | cut -f5)
|
|
105
|
+
project_name=$(printf '%s' "$info" | cut -f6)
|
|
106
|
+
|
|
107
|
+
# RENDER IS INTERCEPTED HERE, before the backend dispatch, so no backend is
|
|
108
|
+
# ever handed a word the six-command contract does not contain. It still runs
|
|
109
|
+
# inside the same subshell guard everything else here does.
|
|
110
|
+
if [ "$1" = "render" ]; then
|
|
111
|
+
shift
|
|
112
|
+
view="$ENTROPY_MACHINES_HOME/lib/tracker-view.py"
|
|
113
|
+
if [ ! -f "$view" ]; then
|
|
114
|
+
echo "tracker: REFUSED — bin/tracker render needs $view, which is missing." >&2
|
|
115
|
+
exit 2
|
|
116
|
+
fi
|
|
117
|
+
(
|
|
118
|
+
cd "$ENTROPY_MACHINES_ROOT" &&
|
|
119
|
+
ENTROPY_MACHINES_TRACKER_PATH="$file_path" \
|
|
120
|
+
ENTROPY_MACHINES_TRACKER_BACKEND="$backend" \
|
|
121
|
+
ENTROPY_MACHINES_DOCS_DIR="$docs_dir" \
|
|
122
|
+
ENTROPY_MACHINES_PROJECT_NAME="$project_name" \
|
|
123
|
+
exec python3 "$view" "$@"
|
|
124
|
+
)
|
|
125
|
+
exit $?
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
case "$backend" in
|
|
129
|
+
file)
|
|
130
|
+
impl="$ENTROPY_MACHINES_HOME/lib/tracker-file"
|
|
131
|
+
if [ ! -x "$impl" ]; then
|
|
132
|
+
echo "tracker: REFUSED — tracker.backend is \"file\" but $impl is missing" >&2
|
|
133
|
+
echo " or not executable." >&2
|
|
134
|
+
exit 2
|
|
135
|
+
fi
|
|
136
|
+
# ENTROPY_MACHINES_ROOT is already exported by entropy_machines_require_root; lib/tracker-file
|
|
137
|
+
# reads it as the root its storage path is relative to.
|
|
138
|
+
( cd "$ENTROPY_MACHINES_ROOT" && ENTROPY_MACHINES_TRACKER_PATH="$file_path" exec "$impl" "$@" )
|
|
139
|
+
;;
|
|
140
|
+
command)
|
|
141
|
+
if [ -z "$cmd_bin" ]; then
|
|
142
|
+
echo "tracker: REFUSED — tracker.backend is \"command\" but tracker.command.bin" >&2
|
|
143
|
+
echo " is empty in $config." >&2
|
|
144
|
+
exit 2
|
|
145
|
+
fi
|
|
146
|
+
( cd "$ENTROPY_MACHINES_ROOT" && exec "$cmd_bin" $cmd_args "$@" )
|
|
147
|
+
;;
|
|
148
|
+
*)
|
|
149
|
+
echo "tracker: REFUSED — unknown tracker.backend \"$backend\" in $config" >&2
|
|
150
|
+
echo " (want \"file\" or \"command\")." >&2
|
|
151
|
+
exit 2
|
|
152
|
+
;;
|
|
153
|
+
esac
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Agent quickstart
|
|
2
|
+
|
|
3
|
+
This doc is for the coding agent bootstrapping and running the harness.
|
|
4
|
+
If you are the project owner (a human), start at [../README.md](../README.md).
|
|
5
|
+
The *why*: [../doctrine/](../doctrine/).
|
|
6
|
+
|
|
7
|
+
Four roles move one issue through a fixed cycle — dispatch, work, verify, fold —
|
|
8
|
+
enforced by shell gates, not convention. Decisions happen in served HTML docs with
|
|
9
|
+
response boxes, not in chat. Plain tracked files, one git root; prefix the commands
|
|
10
|
+
below if they sit in a subdirectory.
|
|
11
|
+
|
|
12
|
+
## Step 0 — no `config.json` yet
|
|
13
|
+
|
|
14
|
+
Your first job and the whole job. Don't stop for permission between steps.
|
|
15
|
+
|
|
16
|
+
1. **Point `CLAUDE.md` and/or `AGENTS.md` at it** — both if both exist, rewriting
|
|
17
|
+
nothing already there:
|
|
18
|
+
|
|
19
|
+
```markdown
|
|
20
|
+
## entropy-machines
|
|
21
|
+
Vendored at `<path>`. **Owner** decides and ticks. **Worker** does one scoped
|
|
22
|
+
issue in an isolated worktree and commits nothing. **Verifier** sweeps a sprint
|
|
23
|
+
once, on a clean tree. **Orchestrator** dispatches, folds, lands — the only
|
|
24
|
+
committer. `<path>/bin/status` = live state, `bin/tracker ready` = find work,
|
|
25
|
+
`bin/dispatch` = the gate before any writing agent, `bin/handoff` = the only way
|
|
26
|
+
work lands. Doctrine: `<path>/doctrine/`.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. **`bin/init`** — writes `config.json`, gitignores `.entropy-machines/`, copies the
|
|
30
|
+
orientation PRD into `docs.dir`. A second run exits 2; that refusal is working.
|
|
31
|
+
|
|
32
|
+
3. **Baseline discovery — record only what you RAN.** `init` writes `suites: []`
|
|
33
|
+
rather than guess. **Run** the candidate test/typecheck/build commands and enter
|
|
34
|
+
only those that passed; one that fails is a finding, not an entry. Then fill the
|
|
35
|
+
PRD's "What we found in your repo" page, saying what you couldn't verify. Leave
|
|
36
|
+
the open questions — the owner's.
|
|
37
|
+
|
|
38
|
+
4. **Start `bin/serve`** — actually start it, backgrounded; it holds the terminal.
|
|
39
|
+
Binds `127.0.0.1` and prints its URL. Run `bin/doclint` first.
|
|
40
|
+
|
|
41
|
+
Hand over that URL and **stop**. A PRD you answered yourself produces issues nobody
|
|
42
|
+
agreed to.
|
|
43
|
+
|
|
44
|
+
## The commands
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
bin/status # read-only: ready, in flight, unanswered
|
|
48
|
+
bin/tracker show|notes|remember|claim|ready|set # six operations, no more
|
|
49
|
+
bin/dispatch <id> --files "…" --brief "…" # the gate; paste its block verbatim
|
|
50
|
+
bin/handoff <id> --from <worktree> --verified "<what YOU ran>"
|
|
51
|
+
bin/serve [port] · bin/doclint [path…]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`tracker ready` is the state of the world, not a doc's account of it: it excludes
|
|
55
|
+
held (`heldWhy`) and gated (`gate`), neither a status value — `status` holds only
|
|
56
|
+
`notstarted`/`progress`/`done`. `set` auto-vivifies; there is no `add`.
|
|
57
|
+
`dispatch --files` is advisory; what's enforced is the denylist of files other
|
|
58
|
+
agents hold, pasted into your brief. `handoff --from` lifts the worker's four
|
|
59
|
+
lines; `--verified` never is. See [CONFIG.md](CONFIG.md), [SERVE.md](SERVE.md),
|
|
60
|
+
[TRACKER-ADAPTER.md](TRACKER-ADAPTER.md).
|
|
61
|
+
|
|
62
|
+
## Never
|
|
63
|
+
|
|
64
|
+
Commit, unless you are the orchestrator landing via `handoff`. Claim a command you
|
|
65
|
+
did not run. Write a file another live dispatch holds — stop, ship nothing, name it
|
|
66
|
+
in `HANDOFF.md`'s `found:`. Touch another worktree, or run the full build (it
|
|
67
|
+
rewrites committed bookkeeping — use `suites`). Relay another agent's verification.
|
|
68
|
+
Land held work.
|
|
69
|
+
|
|
70
|
+
## Limits
|
|
71
|
+
|
|
72
|
+
- **`handoff` cannot read `dispatch`'s notes** (JSONL vs. the older free-text
|
|
73
|
+
form), so `--lift` copies a held file instead of refusing, interrogation is never
|
|
74
|
+
required, and a recorded handoff never releases the claim. Read dispatch's
|
|
75
|
+
overlap warning yourself.
|
|
76
|
+
- **`--lift` exits 1 on `.scratch`** — delete that symlink from the worktree, rerun.
|
|
77
|
+
- **Claude-native in practice.** The gates are POSIX shell; automatic parallel
|
|
78
|
+
workers in isolated worktrees is a Claude Code capability. Another runner gets
|
|
79
|
+
the same gates one issue at a time, using `agents/isolated-worker.md` by hand.
|
|
80
|
+
- **Isolation can silently not happen.** `isolation: worktree` branches the repo the
|
|
81
|
+
*calling session's cwd* is in, so driving one project from a shell in another
|
|
82
|
+
hands every worker the wrong repo — or one shared checkout where agents see each
|
|
83
|
+
other's edits. Workers check `git rev-parse --git-common-dir` (never
|
|
84
|
+
`--show-toplevel`); orchestrators verify cwd, keep scopes disjoint and never `git
|
|
85
|
+
add -A` while a lane is live. No gate covers it —
|
|
86
|
+
[../doctrine/WORKFLOW.md](../doctrine/WORKFLOW.md).
|
package/docs/CONFIG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# config.json — the project contract
|
|
2
|
+
|
|
3
|
+
Everything the harness would otherwise hardcode about its host project, at the
|
|
4
|
+
repo root. Nothing in `bin/` or `lib/` may name a language, package manager or
|
|
5
|
+
directory not read from here. It is also the root marker.
|
|
6
|
+
|
|
7
|
+
`bin/init` writes the first one. It files **no issues** (a PRD is what creates
|
|
8
|
+
issues) and guesses at no command it hasn't run — a fabricated suite makes the
|
|
9
|
+
harness report a broken command as a broken project.
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"project": { "name": "my-project", "protectedPaths": [] },
|
|
14
|
+
"tracker": { "backend": "file", "file": { "path": ".entropy-machines/issues.json" } },
|
|
15
|
+
"docs": { "dir": "entropy-machines-docs", "theme": "high-contrast" },
|
|
16
|
+
"suites": [ { "name": "unit", "cmd": ["npm", "test"] },
|
|
17
|
+
{ "name": "e2e", "cmd": ["npm", "run", "e2e"], "tag": "slow" } ],
|
|
18
|
+
"generate": { "cmd": ["npm", "run", "gen"], "outputs": [] },
|
|
19
|
+
"changelog": { "enabled": true, "fragmentDir": "changelog.d",
|
|
20
|
+
"collatedFile": "docs/CHANGELOG.md",
|
|
21
|
+
"marker": "<!-- BEGIN COLLATED changelog.d -->" },
|
|
22
|
+
"guards": { "testPathPatterns": ["tests/**"], "nonCodePatterns": ["**/*.md"],
|
|
23
|
+
"buildFailureMarkers": [] },
|
|
24
|
+
"worktree": { "linkPaths": ["node_modules"] },
|
|
25
|
+
"unattended": { "enabled": false, "scheduler": "launchd",
|
|
26
|
+
"agent": { "cmd": ["claude", "-p"] } }
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every key is optional — deep-merged over `lib/config.py`'s `DEFAULTS`.
|
|
31
|
+
|
|
32
|
+
| Key | What it means |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `project.protectedPaths` | Off-limits to a dispatched agent without an override. |
|
|
35
|
+
| `tracker.backend` | `file`, or `command` for your own tracker — [TRACKER-ADAPTER.md](TRACKER-ADAPTER.md). |
|
|
36
|
+
| `docs.dir` / `.theme` | What `bin/serve` serves; `theme` names a file in `lib/themes/`. |
|
|
37
|
+
| `suites` | What "the suites" means. `post-fold-audit` and the verifier run these; `tag` is skippable. |
|
|
38
|
+
| `generate` | `handoff` runs it and fails the fold if anything moves. |
|
|
39
|
+
| `changelog` | Whether a fragment is required per commit. Defaults on. |
|
|
40
|
+
| `guards` | How fail-first classifies a revert, and tells a build failure from a test failure. |
|
|
41
|
+
| `worktree.linkPaths` | Symlinked into every worktree. Without `node_modules` a runtime resolves out of a SIBLING worktree and the agent verifies someone else's code. |
|
|
42
|
+
| `unattended` | The drain loop. `scheduler` is `launchd` (macOS) or `systemd` (a `systemctl --user` timer); omit the key and `bin/drain install` detects one, refusing if neither is present. `on/off/status/now/at` work anywhere. |
|
|
43
|
+
|
|
44
|
+
## Themes and root
|
|
45
|
+
|
|
46
|
+
`docs.theme` resolves to `lib/themes/<name>.css` — `high-contrast` (default,
|
|
47
|
+
dark-first) or `daylight` (light-first). A theme is a `:root` token block and
|
|
48
|
+
nothing else, so layout stays in the templates; every theme must define the same
|
|
49
|
+
names with a real value in the bare `:root`, since one defined only under
|
|
50
|
+
`[data-theme=…]` renders unstyled by default.
|
|
51
|
+
`tests/cases/themes-ship-and-apply.sh` catches that by diffing the name sets.
|
|
52
|
+
How `bin/serve` inlines it: [SERVE.md](SERVE.md).
|
|
53
|
+
|
|
54
|
+
The harness is vendored as **plain tracked files**, never a git repository of
|
|
55
|
+
its own — a nested `.git` shadows the enclosing repo, so commands operate on the
|
|
56
|
+
wrong one; `git clone` and `git submodule` are refused by name. **Root** is
|
|
57
|
+
always the MAIN CHECKOUT, via `--git-common-dir`, never `--show-toplevel`: they
|
|
58
|
+
differ inside a linked worktree, and `.entropy-machines/` lives in the main checkout.
|
|
59
|
+
`lib/roots.sh` is canonical; `lib/config.py` and `lib/config.mjs` mirror it.
|
|
60
|
+
|
|
61
|
+
## Rules for contributors
|
|
62
|
+
|
|
63
|
+
1. A new hardcoded path, command or noun in `bin/` or `lib/` is a bug — add a key.
|
|
64
|
+
2. Every key has a working default, or the harness refuses naming the missing
|
|
65
|
+
key. Silent fallback is what made the original un-portable.
|
|
66
|
+
3. Read once per process and passed down; never re-read from library code.
|
|
67
|
+
4. Anything resolving root independently uses `--git-common-dir` and cites
|
|
68
|
+
`lib/roots.sh`. Three implementations already drifted apart once.
|
package/docs/NPM.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# The npm package
|
|
2
|
+
|
|
3
|
+
`npx entropy-machines init` is a **delivery mechanism, not a runtime**. It
|
|
4
|
+
copies the harness into your repository as plain tracked files and runs
|
|
5
|
+
`bin/init`. After that npm is gone: nothing vendored imports from
|
|
6
|
+
`node_modules`, nothing shells out to `node`, and the harness runs on git, a
|
|
7
|
+
POSIX shell and Python 3. Deleting `node_modules/` changes nothing.
|
|
8
|
+
|
|
9
|
+
The other route is identical in outcome and needs no Node at all — clone this
|
|
10
|
+
repo and copy `bin/`, `lib/`, `docs/`, `doctrine/`, `hooks/` and `agents/`
|
|
11
|
+
into your project.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx entropy-machines init # -> entropy-machines/ at the repo root
|
|
17
|
+
npx entropy-machines init --dir . # -> vendored at the repo root instead
|
|
18
|
+
npx entropy-machines --help
|
|
19
|
+
npx entropy-machines --version
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`init` copies the six directories plus `LICENSE`, then runs the vendored
|
|
23
|
+
`bin/init` with the repository root as its working directory. To undo it,
|
|
24
|
+
delete the directory. There is no uninstall command, no install receipt and
|
|
25
|
+
no version check, on purpose.
|
|
26
|
+
|
|
27
|
+
## Why a subdirectory by default
|
|
28
|
+
|
|
29
|
+
`entropy-machines/` is the default because vendoring at the repo root merges
|
|
30
|
+
the harness's `bin/`, `lib/` and `docs/` into whatever the project already
|
|
31
|
+
has under those names, and those are three of the most commonly taken
|
|
32
|
+
directory names there are. A subdirectory has none of that: it is one
|
|
33
|
+
directory to add, one to `rm -rf`, and `git log` on it shows only harness
|
|
34
|
+
changes. `lib/roots.sh` derives `ENTROPY_MACHINES_HOME` from each script's own `$0`,
|
|
35
|
+
so both layouts work; `docs/AGENT-QUICKSTART.md` already tells the agent to prefix
|
|
36
|
+
every `bin/…` command with the vendored path.
|
|
37
|
+
|
|
38
|
+
This is **not** the old nested clone. There is no `.git` in the vendored
|
|
39
|
+
directory, so `git` from anywhere inside your project still answers with your
|
|
40
|
+
project — one root, as `lib/roots.sh` requires. `entropy_machines_refuse_nested_clone`
|
|
41
|
+
refuses the layout that does have one.
|
|
42
|
+
|
|
43
|
+
Use `--dir .` if you want the root layout the README describes, and every
|
|
44
|
+
`bin/…` command in the docs verbatim.
|
|
45
|
+
|
|
46
|
+
## What it refuses, before writing anything
|
|
47
|
+
|
|
48
|
+
| Situation | Why |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Not inside a git repository | Vendored files have to be tracked by something. |
|
|
51
|
+
| The destination already holds any of the six directories | An older harness may have edited doctrine in it; your own `bin/` is not ours to merge into. Pass `--dir <path>`. |
|
|
52
|
+
| The destination is inside the package's own directory, or a checkout of this package is inside the destination | It would copy itself over itself, and in a `npm link`ed checkout that tree is where a real `.git` lives. An *installed* copy at `<repo>/node_modules/entropy-machines` is exempt — that is what `npm i -D entropy-machines` plus `--dir .` looks like, and the two trees never overlap. |
|
|
53
|
+
| The destination is outside the repository | Vendoring means committed alongside your code. |
|
|
54
|
+
|
|
55
|
+
A `.git` directory is never copied, at any depth — filtered by path segment,
|
|
56
|
+
not by trusting the tarball to lack one. A nested `.git` shadows the parent
|
|
57
|
+
repo for every git query and silently misroutes every command in the harness;
|
|
58
|
+
it is the single failure the vendored layout exists to prevent. `__pycache__`,
|
|
59
|
+
`node_modules` and `.DS_Store` are dropped the same way.
|
|
60
|
+
|
|
61
|
+
## Publishing
|
|
62
|
+
|
|
63
|
+
`package.json` has no dependencies and no build step.
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm pack --dry-run # the exact file list that will ship
|
|
67
|
+
npm publish
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`files` is a whitelist of the six directories, with `!**/__pycache__` and
|
|
71
|
+
`!**/*.pyc` negations — a `.npmignore` does **not** filter inside a
|
|
72
|
+
whitelisted directory, so the negations are what keeps compiled Python out.
|
|
73
|
+
`planning/`, `.entropy-machines/`, `tests/`, `example/`, `entropy-machines-docs/` and
|
|
74
|
+
`CONTRIBUTING.md` are excluded by not being listed; npm excludes `.git`
|
|
75
|
+
unconditionally. `LICENSE`, `README.md` and `package.json` are always
|
|
76
|
+
included — they ship even though `files` does not name them, so the shipped
|
|
77
|
+
list is the six directories plus those three.
|
|
78
|
+
|
|
79
|
+
Verified against a real `npm pack` on 2026-08-30: the tarball's file list is
|
|
80
|
+
exactly the six directories' tracked contents plus those three, with every
|
|
81
|
+
executable bit intact (20 of the vendored files land as `100755`).
|
|
82
|
+
`bin/entropy-machines-init` is the one packaged file NOT vendored, because it
|
|
83
|
+
is the npm wrapper rather than part of the harness.
|
|
84
|
+
|
|
85
|
+
`npm pack` reads the WORKING TREE, not HEAD. An uncommitted or untracked file
|
|
86
|
+
inside one of the six directories ships. Pack from a clean tree, and read the
|
|
87
|
+
`npm notice` file list before publishing rather than trusting `files`.
|
|
88
|
+
|
|
89
|
+
The package version is the npm artifact's version. It is not a harness
|
|
90
|
+
version and nothing checks it — there is no staleness detection anywhere in
|
|
91
|
+
this project.
|