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,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// The npm delivery mechanism, and nothing else.
|
|
5
|
+
//
|
|
6
|
+
// `npx entropy-machines init` copies the harness directories out of this
|
|
7
|
+
// package and into the user's git repository as PLAIN TRACKED FILES, then
|
|
8
|
+
// runs the vendored `bin/init`. After that, npm is out of the picture
|
|
9
|
+
// permanently: nothing vendored imports from node_modules, nothing shells
|
|
10
|
+
// out to node, and the harness runs on git + a POSIX shell + Python 3.
|
|
11
|
+
//
|
|
12
|
+
// The other supported route is `git clone` + `cp -R` of the same
|
|
13
|
+
// directories. Both end in the same tracked tree; this file is a
|
|
14
|
+
// convenience, not a second install format.
|
|
15
|
+
//
|
|
16
|
+
// THE ONE PROPERTY THAT MATTERS MOST: no `.git` is ever copied. A nested
|
|
17
|
+
// `.git` shadows the parent repo for every git query and silently misroutes
|
|
18
|
+
// every command in the harness — it is the exact failure the vendored layout
|
|
19
|
+
// exists to avoid. The copy filter rejects it by path segment, so it cannot
|
|
20
|
+
// arrive by any route (a published tarball has no `.git`, but `npm link`
|
|
21
|
+
// against a clone does).
|
|
22
|
+
//
|
|
23
|
+
// Deliberately NOT here, because the owner deleted a 1072-line installer to
|
|
24
|
+
// get rid of them: managed blocks, hash fences, install receipts, staleness
|
|
25
|
+
// or version checking, an uninstall path, and any write into the user's
|
|
26
|
+
// .claude/ or CLAUDE.md. Undoing this is `rm -rf` of one directory.
|
|
27
|
+
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const { spawnSync } = require('node:child_process');
|
|
31
|
+
|
|
32
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
33
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
34
|
+
|
|
35
|
+
// What gets vendored. `docs/` is included on top of the five directories the
|
|
36
|
+
// README names because the harness points at it from inside itself —
|
|
37
|
+
// lib/roots.sh's refusal message cites `<harness>/docs/CONFIG.md`, and
|
|
38
|
+
// docs/QUICKSTART.md is the document the next agent is told to read.
|
|
39
|
+
const HARNESS_DIRS = ['bin', 'lib', 'docs', 'doctrine', 'hooks', 'agents'];
|
|
40
|
+
|
|
41
|
+
// Never copied, at any depth. `.git` is the load-bearing one.
|
|
42
|
+
const NEVER_COPY = new Set(['.git', '__pycache__', 'node_modules', '.DS_Store']);
|
|
43
|
+
|
|
44
|
+
// This file is the npm wrapper, not part of the harness. It lives in bin/ so
|
|
45
|
+
// it is where a reader looks for it, and it is left behind on the way in.
|
|
46
|
+
const NPM_ENTRY = path.basename(__filename);
|
|
47
|
+
|
|
48
|
+
const DEFAULT_DIR = 'entropy-machines';
|
|
49
|
+
|
|
50
|
+
function die(lines) {
|
|
51
|
+
for (const line of [].concat(lines)) console.error(line);
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function usage() {
|
|
56
|
+
console.log(`entropy-machines ${PKG.version}
|
|
57
|
+
|
|
58
|
+
npx entropy start # init (if needed) + open the PRD
|
|
59
|
+
npx entropy init [--dir <path>] # vendor the harness only
|
|
60
|
+
|
|
61
|
+
start: vendors the harness if config.json is absent, then launches
|
|
62
|
+
bin/serve — which opens your first PRD in a browser.
|
|
63
|
+
|
|
64
|
+
init: copies the harness (${HARNESS_DIRS.join(', ')}) into your git
|
|
65
|
+
repository as plain tracked files, then runs the vendored bin/init.
|
|
66
|
+
|
|
67
|
+
--dir <path> where to put them, relative to the current directory.
|
|
68
|
+
Default: ${DEFAULT_DIR}/ at the repository root.
|
|
69
|
+
Pass --dir . to vendor at the repository root instead.
|
|
70
|
+
--help, -h this text
|
|
71
|
+
--version, -v package version
|
|
72
|
+
|
|
73
|
+
Nothing is written if any refusal fires. To undo, delete the directory.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The repository's MAIN checkout, resolved the same way lib/roots.sh does it:
|
|
77
|
+
// --git-common-dir, not --show-toplevel, so a linked worktree still answers
|
|
78
|
+
// with the main checkout.
|
|
79
|
+
function repoRoot() {
|
|
80
|
+
const r = spawnSync('git', ['rev-parse', '--git-common-dir'], {
|
|
81
|
+
cwd: process.cwd(),
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
});
|
|
84
|
+
if (r.error || r.status !== 0) return null;
|
|
85
|
+
const common = r.stdout.trim();
|
|
86
|
+
if (!common) return null;
|
|
87
|
+
const abs = path.isAbsolute(common) ? common : path.resolve(process.cwd(), common);
|
|
88
|
+
try {
|
|
89
|
+
return fs.realpathSync(path.dirname(abs));
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isInside(child, parent) {
|
|
96
|
+
const rel = path.relative(parent, child);
|
|
97
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function copyFilter(src) {
|
|
101
|
+
const rel = path.relative(PKG_ROOT, src);
|
|
102
|
+
if (rel.split(path.sep).some((seg) => NEVER_COPY.has(seg))) return false;
|
|
103
|
+
if (rel === path.join('bin', NPM_ENTRY)) return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function init(argv) {
|
|
108
|
+
let dirArg = null;
|
|
109
|
+
for (let i = 0; i < argv.length; i++) {
|
|
110
|
+
const a = argv[i];
|
|
111
|
+
if (a === '--dir') {
|
|
112
|
+
dirArg = argv[++i];
|
|
113
|
+
if (!dirArg) die('entropy-machines: --dir needs a path.');
|
|
114
|
+
} else if (a.startsWith('--dir=')) {
|
|
115
|
+
dirArg = a.slice('--dir='.length);
|
|
116
|
+
} else {
|
|
117
|
+
die([`entropy-machines: unrecognized argument: ${a}`, 'Run `entropy-machines --help`.']);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- REFUSALS. All of them, before any write. ----
|
|
122
|
+
|
|
123
|
+
const root = repoRoot();
|
|
124
|
+
if (!root) {
|
|
125
|
+
die([
|
|
126
|
+
'entropy-machines: REFUSED — not inside a git repository.',
|
|
127
|
+
` Looked from ${process.cwd()}.`,
|
|
128
|
+
' The harness is vendored as tracked files, so it needs a repo to be',
|
|
129
|
+
' tracked in. cd into your project (or `git init` it) and re-run.',
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const dest = dirArg ? path.resolve(process.cwd(), dirArg) : path.join(root, DEFAULT_DIR);
|
|
134
|
+
|
|
135
|
+
if (!isInside(dest, root)) {
|
|
136
|
+
die([
|
|
137
|
+
`entropy-machines: REFUSED — ${dest} is outside the repository at ${root}.`,
|
|
138
|
+
' Vendoring means the files are committed alongside your code.',
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Vendoring into the package's own tree would have it copy itself over
|
|
143
|
+
// itself — and, in a `npm link`ed clone, that tree is where the `.git`
|
|
144
|
+
// this must never touch actually lives.
|
|
145
|
+
//
|
|
146
|
+
// The REVERSE containment (this package sitting inside the destination) is
|
|
147
|
+
// NOT that failure when the package is an installed dependency of the
|
|
148
|
+
// target repo: `npm i -D entropy-machines` + `--dir .` puts PKG_ROOT at
|
|
149
|
+
// <repo>/node_modules/entropy-machines and dest at <repo>, and the copy is
|
|
150
|
+
// still PKG_ROOT/<d> -> dest/<d> with no overlap and no `.git` in reach.
|
|
151
|
+
// Refusing it broke the `--dir .` layout that --help and docs/NPM.md both
|
|
152
|
+
// advertise, for every local install, with a message telling the user to
|
|
153
|
+
// run from the project they were already in. A checkout or a linked clone
|
|
154
|
+
// living inside the repo is a different thing and still refused.
|
|
155
|
+
const pkgUnderNodeModules = path
|
|
156
|
+
.relative(dest, PKG_ROOT)
|
|
157
|
+
.split(path.sep)
|
|
158
|
+
.includes('node_modules');
|
|
159
|
+
if (isInside(dest, PKG_ROOT) || (isInside(PKG_ROOT, dest) && !pkgUnderNodeModules)) {
|
|
160
|
+
die([
|
|
161
|
+
`entropy-machines: REFUSED — ${dest} and this package's own directory overlap.`,
|
|
162
|
+
` (${PKG_ROOT})`,
|
|
163
|
+
' Run this from the project you want the harness in, not from a',
|
|
164
|
+
' checkout of the harness itself.',
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const collisions = HARNESS_DIRS.filter((d) => fs.existsSync(path.join(dest, d)));
|
|
169
|
+
if (collisions.length) {
|
|
170
|
+
die([
|
|
171
|
+
`entropy-machines: REFUSED — ${dest} already has: ${collisions.join(', ')}.`,
|
|
172
|
+
' Not overwriting them. If that is an older copy of the harness, the',
|
|
173
|
+
' doctrine and config in it may have been edited on purpose; diff it',
|
|
174
|
+
' or delete it yourself. If it is your own code, vendor somewhere',
|
|
175
|
+
` else: entropy-machines init --dir <path>`,
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---- WRITES. ----
|
|
180
|
+
|
|
181
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
182
|
+
for (const d of HARNESS_DIRS) {
|
|
183
|
+
fs.cpSync(path.join(PKG_ROOT, d), path.join(dest, d), {
|
|
184
|
+
recursive: true,
|
|
185
|
+
filter: copyFilter,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Elastic 2.0 asks that the notice travel with the source. Never clobbers.
|
|
190
|
+
const licenseDst = path.join(dest, 'LICENSE');
|
|
191
|
+
let licenseNote = '';
|
|
192
|
+
if (fs.existsSync(licenseDst)) {
|
|
193
|
+
licenseNote = ' (LICENSE already present, left alone)';
|
|
194
|
+
} else {
|
|
195
|
+
fs.copyFileSync(path.join(PKG_ROOT, 'LICENSE'), licenseDst);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const shown = path.relative(root, dest) || '.';
|
|
199
|
+
console.log(`entropy-machines ${PKG.version}: vendored into ${dest}`);
|
|
200
|
+
for (const d of HARNESS_DIRS) console.log(` ${path.join(shown, d)}/`);
|
|
201
|
+
console.log(` ${path.join(shown, 'LICENSE')}${licenseNote}`);
|
|
202
|
+
console.log('');
|
|
203
|
+
|
|
204
|
+
// ---- Hand over to the harness. Everything past here is shell + Python. ----
|
|
205
|
+
|
|
206
|
+
const vendoredInit = path.join(dest, 'bin', 'init');
|
|
207
|
+
console.log(`entropy-machines: running ${path.join(shown, 'bin', 'init')}`);
|
|
208
|
+
const r = spawnSync(vendoredInit, [], { cwd: root, stdio: 'inherit' });
|
|
209
|
+
if (r.error) {
|
|
210
|
+
console.error(`entropy-machines: could not run ${vendoredInit}: ${r.error.message}`);
|
|
211
|
+
console.error(' The files ARE vendored. Run it yourself from the repo root.');
|
|
212
|
+
process.exit(2);
|
|
213
|
+
}
|
|
214
|
+
if (r.status !== 0) {
|
|
215
|
+
console.error('');
|
|
216
|
+
console.error('entropy-machines: the files are vendored, but bin/init did not');
|
|
217
|
+
console.error(' finish. Fix what it named above and re-run it from the repo');
|
|
218
|
+
console.error(` root: ${path.join(shown, 'bin', 'init')}`);
|
|
219
|
+
process.exit(r.status === null ? 2 : r.status);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log('entropy-machines: done. Next —');
|
|
224
|
+
// Named paths only, never a guessed docs directory — that one is config
|
|
225
|
+
// (docs.dir), and CONFIG.md rule 1 makes a hardcoded path in bin/ a bug.
|
|
226
|
+
// NEVER `git add .` for the root layout. This command is reached over npm,
|
|
227
|
+
// so the repo it just vendored into very likely holds a node_modules/ and a
|
|
228
|
+
// package-lock.json that init's .gitignore does not cover — `git add .`
|
|
229
|
+
// commits them. Name the paths the harness actually wrote.
|
|
230
|
+
const addPaths =
|
|
231
|
+
shown === '.'
|
|
232
|
+
? `${HARNESS_DIRS.join(' ')} LICENSE .gitignore`
|
|
233
|
+
: `${shown} .gitignore`;
|
|
234
|
+
console.log(` 1. commit what init just wrote: git add ${addPaths}`);
|
|
235
|
+
console.log(` 2. point your coding agent at ${path.join(shown, 'docs', 'AGENT-QUICKSTART.md')}`);
|
|
236
|
+
console.log(` 3. ${path.join(shown, 'bin', 'serve')} — answer the PRD it opens`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function findHarness(root) {
|
|
240
|
+
// The harness is either at root (--dir .) or in the DEFAULT_DIR subdirectory.
|
|
241
|
+
// config.json lives inside the harness dir, not at root.
|
|
242
|
+
for (const [dir, label] of [[root, '.'], [path.join(root, DEFAULT_DIR), DEFAULT_DIR]]) {
|
|
243
|
+
if (fs.existsSync(path.join(dir, 'config.json'))) return { dir, shown: label };
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function start(argv) {
|
|
249
|
+
const root = repoRoot();
|
|
250
|
+
if (!root) die(['entropy-machines: not inside a git repository.']);
|
|
251
|
+
|
|
252
|
+
if (!findHarness(root)) {
|
|
253
|
+
console.log('entropy-machines: no config.json found, running init first.\n');
|
|
254
|
+
init(argv);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const loc = findHarness(root);
|
|
258
|
+
if (!loc) {
|
|
259
|
+
die(['entropy-machines: config.json not found at root or in ' + DEFAULT_DIR + '/.']);
|
|
260
|
+
}
|
|
261
|
+
const serve = path.join(loc.dir, 'bin', 'serve');
|
|
262
|
+
if (!fs.existsSync(serve)) {
|
|
263
|
+
die([`entropy-machines: ${path.join(loc.shown, 'bin', 'serve')} not found.`]);
|
|
264
|
+
}
|
|
265
|
+
console.log(`\nentropy-machines: starting serve from ${loc.shown}/\n`);
|
|
266
|
+
const r = spawnSync('sh', [serve], { cwd: loc.dir, stdio: 'inherit' });
|
|
267
|
+
process.exit(r.status ?? 1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function main(argv) {
|
|
271
|
+
const cmd = argv[0];
|
|
272
|
+
if (cmd === '--help' || cmd === '-h' || cmd === 'help' || cmd === undefined) {
|
|
273
|
+
usage();
|
|
274
|
+
process.exit(cmd === undefined ? 2 : 0);
|
|
275
|
+
}
|
|
276
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
277
|
+
console.log(PKG.version);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (cmd === 'init') return init(argv.slice(1));
|
|
281
|
+
if (cmd === 'start') return start(argv.slice(1));
|
|
282
|
+
die([`entropy-machines: unknown command: ${cmd}`, 'Run `entropy-machines --help`.']);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
main(process.argv.slice(2));
|