@bongos/core 1.19.768 → 1.19.769

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/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.768",
6
- "core_contract": "1.19.768",
7
- "source_commit": "ec2107bdda1e0c2ee8404d14e539b83a836f767d",
5
+ "core_version": "1.19.769",
6
+ "core_contract": "1.19.769",
7
+ "source_commit": "5552e7252b67cc3192446a9c48ca2cd4faf10368",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-16T14:46:45.582Z",
9
+ "built_at": "2026-09-16T14:54:06.807Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 483,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2123,
14
+ "functional_verbatim": 2125,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2631,
20
- "tree_sha256": "9d2871bffc966902b2a18d7c59fc92a2e5ea137e0bee5675716ca3fbee9e01ef",
19
+ "file_count": 2633,
20
+ "tree_sha256": "8c9135931a565436af92fb7fc38953f0bf6b9ca8c6b608b91e9751c88dd62fe0",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2512,7 +2512,7 @@
2512
2512
  {
2513
2513
  "path": "docs/module-api-changelog.md",
2514
2514
  "mode": "0000644",
2515
- "sha256": "5c95283ebfac1200dcabcba2fa712b6b041be345412b92cd1f896e3648d9ecd0"
2515
+ "sha256": "ffaeae011cd55895b3c16a2ee903e0593aa42a22cfceb3307c5228452c737948"
2516
2516
  },
2517
2517
  {
2518
2518
  "path": "docs/modules-contract.md",
@@ -7577,12 +7577,12 @@
7577
7577
  {
7578
7578
  "path": "package-lock.json",
7579
7579
  "mode": "0000644",
7580
- "sha256": "4f7ef5cc202176fb1c83a20baea17017b6adb0892051832a4bd6a440d57f1fcc"
7580
+ "sha256": "17b6a246e7e14d3949244aff29ed7e63dc5bc5d65a7aeff2a463f8283fa5b4cc"
7581
7581
  },
7582
7582
  {
7583
7583
  "path": "package.json",
7584
7584
  "mode": "0000644",
7585
- "sha256": "ee2355bc92153c9c139cf79e19f22b80dda73a5a2abdd102b67830cb3bee0d14"
7585
+ "sha256": "df0b9ff8dceb4cafd08db0a28bcbe92b1cbee04fefe4f811574257af0929901c"
7586
7586
  },
7587
7587
  {
7588
7588
  "path": "public-docs/index.html",
@@ -7812,7 +7812,7 @@
7812
7812
  {
7813
7813
  "path": "scripts/gds/build-cli-package.js",
7814
7814
  "mode": "0000644",
7815
- "sha256": "314004ef2af628fca5f572daca5e2404d1c2129b3ad650d715294cc26cc2fe13"
7815
+ "sha256": "f3c15da424b2652ecb511d9a794cda1c037d500b8d810fb71bab2e31f41a0ca6"
7816
7816
  },
7817
7817
  {
7818
7818
  "path": "scripts/gds/bump-version.js",
@@ -7847,7 +7847,7 @@
7847
7847
  {
7848
7848
  "path": "scripts/gds/claim.js",
7849
7849
  "mode": "0000644",
7850
- "sha256": "22ac9d57c1ccc9fd76bd390d069c06896e3da4cd076a6c554a4335f8392b0783"
7850
+ "sha256": "2d225377a6ab3bdc8e8cb195d5e6588517275179fe49100c42362c88a3465324"
7851
7851
  },
7852
7852
  {
7853
7853
  "path": "scripts/gds/classify-discipline.js",
@@ -8847,7 +8847,7 @@
8847
8847
  {
8848
8848
  "path": "scripts/gds/ship.js",
8849
8849
  "mode": "0000644",
8850
- "sha256": "a64455fdf9771e4ccb04b49d42b9bb0cb0fe45bc6cb36d4e5f3abe0c31fe99ff"
8850
+ "sha256": "9c279a8852e46758e498334fbeb25e94be2895930a6c65b400c2c1baaac1c270"
8851
8851
  },
8852
8852
  {
8853
8853
  "path": "scripts/gds/skill-lint.js",
@@ -8912,7 +8912,7 @@
8912
8912
  {
8913
8913
  "path": "scripts/gds/start.js",
8914
8914
  "mode": "0000644",
8915
- "sha256": "7e4b7cd97e9a414cf55e1118c902b4ab17239d3810fe26e5b90aa18d6a948432"
8915
+ "sha256": "8cf839a0db44d7b647cdd0ea394bdd31db924e54d4840a5f407f515c098c9de7"
8916
8916
  },
8917
8917
  {
8918
8918
  "path": "scripts/gds/status.js",
@@ -8939,6 +8939,11 @@
8939
8939
  "mode": "0000644",
8940
8940
  "sha256": "d3e542cf977f9aadad8f856defa6d809f3604e184cd261fbee7738aa1da736ea"
8941
8941
  },
8942
+ {
8943
+ "path": "scripts/gds/tree-preflight.js",
8944
+ "mode": "0000644",
8945
+ "sha256": "0655b96b842ba4073fbc72ca76204591b3339cc649f2f76986816ddfbe88ff6d"
8946
+ },
8942
8947
  {
8943
8948
  "path": "scripts/gds/triage.js",
8944
8949
  "mode": "0000644",
@@ -9402,7 +9407,7 @@
9402
9407
  {
9403
9408
  "path": "src/module-api.js",
9404
9409
  "mode": "0000644",
9405
- "sha256": "163989f8fd93a919241ac12e7627303618fc6d2d47b75f79e1b6fb875874a534"
9410
+ "sha256": "3237d6ea069eae19ed9baf41a528984e21010defcfc5a0d96d34fd8eeca2fb4a"
9406
9411
  },
9407
9412
  {
9408
9413
  "path": "src/module-loader/catalog.js",
@@ -12979,6 +12984,11 @@
12979
12984
  "mode": "0000644",
12980
12985
  "sha256": "61b50103967187dbff82f957d2faf6cc9ca5079b83531a5925fdeae61eb3c921"
12981
12986
  },
12987
+ {
12988
+ "path": "tests/tree_preflight.mjs",
12989
+ "mode": "0000644",
12990
+ "sha256": "d8382f7a608fc7511cd6e00e3a9efcb0283b8128b39cbe229fb07ae590b20c4b"
12991
+ },
12982
12992
  {
12983
12993
  "path": "tests/triage_development.mjs",
12984
12994
  "mode": "0000644",
@@ -2010,5 +2010,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2010
2010
  landed since 1.19.766 with no explicit bump. run 35109111074. (task 1002620)
2011
2011
  1.19.768 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2012
2012
  landed since 1.19.767 with no explicit bump. run 35110800163. (task 1002620)
2013
+ 1.19.769 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2014
+ landed since 1.19.768 with no explicit bump. run 35111616379. (task 1002620)
2013
2015
  ---------------------------------------------------------------------------
2014
2016
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.768",
3
+ "version": "1.19.769",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.768",
9
+ "version": "1.19.769",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.768",
3
+ "version": "1.19.769",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -56,6 +56,11 @@ const FILES = [
56
56
  'scripts/gds/install-git-hooks.js', // lazy dep of setup.js
57
57
  'scripts/gds/preflight.js',
58
58
  'scripts/gds/surface.js', // who the output is for: agent vs terminal (task 1003680)
59
+ // The partial-checkout preflight (task 1003098). start/claim/ship require it
60
+ // ABOVE their first module require, so an absent file here breaks every verb in
61
+ // the published package -- which is exactly how tests/cli_package.mjs caught it.
62
+ // Pure: node builtins only, no repo, no network.
63
+ 'scripts/gds/tree-preflight.js',
59
64
 
60
65
  // Read the board and move your own work along.
61
66
  'scripts/gds/start.js',
@@ -7,6 +7,21 @@
7
7
  // On success: prints the claim id, the task title, and the touches[] so the
8
8
  // builder knows the parallel-safety boundary they're working within.
9
9
 
10
+ // TREE PREFLIGHT (task 1003098). A task-scoped dev box carries scripts/ and
11
+ // src/bongos/ but not every modules/ directory, so the requires below can fail —
12
+ // and Node's loader names only the FIRST missing one, which turned recovery into
13
+ // guess-read-fix-repeat. This enumerates every missing requirement in one message
14
+ // and exits before the first such require runs. It must stay ABOVE them.
15
+ //
16
+ // A PRE-REQUEST hard exit, like the usage banner and worktree guard: nothing has
17
+ // been sent yet, so there is no in-flight request for cliExit to drain.
18
+ const { action: _surfaceAction } = require('./surface.js');
19
+ const { checkTree: _checkTree } = require('./tree-preflight.js');
20
+ {
21
+ const _fault = _checkTree(__filename, { command: _surfaceAction("claim") });
22
+ if (_fault) { console.error(_fault); process.exit(1); }
23
+ }
24
+
10
25
  const { execSync } = require('node:child_process');
11
26
  const fs = require('node:fs');
12
27
  const path = require('node:path');
@@ -77,6 +77,21 @@
77
77
 
78
78
  'use strict';
79
79
 
80
+ // TREE PREFLIGHT (task 1003098). A task-scoped dev box carries scripts/ and
81
+ // src/bongos/ but not every modules/ directory, so the requires below can fail —
82
+ // and Node's loader names only the FIRST missing one, which turned recovery into
83
+ // guess-read-fix-repeat. This enumerates every missing requirement in one message
84
+ // and exits before the first such require runs. It must stay ABOVE them.
85
+ //
86
+ // A PRE-REQUEST hard exit, like the usage banner and worktree guard: nothing has
87
+ // been sent yet, so there is no in-flight request for cliExit to drain.
88
+ const { action: _surfaceAction } = require('./surface.js');
89
+ const { checkTree: _checkTree } = require('./tree-preflight.js');
90
+ {
91
+ const _fault = _checkTree(__filename, { command: _surfaceAction("ship") });
92
+ if (_fault) { console.error(_fault); process.exit(1); }
93
+ }
94
+
80
95
  const { buildMergeArgv, gitArgvOk, sanitizeGitHubRefs } = require('./ship-git.js');
81
96
  const { apiErrorLine, normalizeApiError, postLandShipOutcome } = require('./ship-io.js');
82
97
  const { deriveTagsFromPaths } = require('./ship-surprise.js');
@@ -52,6 +52,21 @@
52
52
  //
53
53
  // Returns JSON if --json is passed (for the slash command to parse cleanly).
54
54
 
55
+ // TREE PREFLIGHT (task 1003098). A task-scoped dev box carries scripts/ and
56
+ // src/bongos/ but not every modules/ directory, so the requires below can fail —
57
+ // and Node's loader names only the FIRST missing one, which turned recovery into
58
+ // guess-read-fix-repeat. This enumerates every missing requirement in one message
59
+ // and exits before the first such require runs. It must stay ABOVE them.
60
+ //
61
+ // A PRE-REQUEST hard exit, like the usage banner and worktree guard: nothing has
62
+ // been sent yet, so there is no in-flight request for cliExit to drain.
63
+ const { action: _surfaceAction } = require('./surface.js');
64
+ const { checkTree: _checkTree } = require('./tree-preflight.js');
65
+ {
66
+ const _fault = _checkTree(__filename, { command: _surfaceAction("start") });
67
+ if (_fault) { console.error(_fault); process.exit(1); }
68
+ }
69
+
55
70
  const { cliClient, requireSession, fmtUsd, printOnboardingNudge, printCostWarning, printNeedsNudge, hallBase, API_BASE, rewardPolicy, costPlusOnlyNote, isDispatcherIncompatError } = require('./cli-lib');
56
71
  const { contextPack, formatContextPackCompact } = require('./context-pack');
57
72
  const { depsFinding } = require('./preflight');
@@ -0,0 +1,241 @@
1
+ 'use strict';
2
+
3
+ // scripts/gds/tree-preflight.js — does this checkout contain the code the command
4
+ // is about to require? (task 1003098)
5
+ //
6
+ // THE FAILURE THIS REPLACES. On a TASK-SCOPED dev box — a supported, server-driven
7
+ // configuration — `GET /box/source-access` returns a sparse path list that carries
8
+ // `scripts`, `src/bongos`, `tests`, `clients`, `config` and NOT `modules/`, except
9
+ // the one module the claimed task is scoped to. So the platform itself produces a
10
+ // checkout in which its own entry-point commands cannot run, and they said so like
11
+ // this:
12
+ //
13
+ // $ node scripts/gds/ship.js
14
+ // Error: Cannot find module '../../modules/lifecycle/ship-preflight'
15
+ // Require stack: scripts/gds/ship-state.js
16
+ //
17
+ // A raw stack trace, naming ONE missing thing. Fix that one and the next run names
18
+ // the next — the filer lost a whole session to guess-read-fix-repeat, and their
19
+ // workaround (`git sparse-checkout disable` plus commenting out the re-scope cron)
20
+ // is not something a builder should have to discover.
21
+ //
22
+ // ENUMERATION IS THE WHOLE POINT. Node's loader is lazy by construction: it fails
23
+ // at the first unresolvable specifier and cannot tell you about the second, because
24
+ // it never got there. That is correct behaviour for a module loader and useless as
25
+ // a diagnosis. This runs BEFORE the first such require and reports every missing
26
+ // requirement it can see in ONE message.
27
+ //
28
+ // THE REQUIRED LIST IS COMPUTED, NEVER DECLARED. A hand-kept list of "what ship.js
29
+ // needs" is a second copy of the require graph, and it would be wrong within a
30
+ // month — ship.js alone fans out across sixteen ship-*.js phase files. So the
31
+ // preflight WALKS the graph the same way the loader would: read the entry file,
32
+ // find its `require('./x')` siblings, follow them, and collect every
33
+ // `require('../../modules/...')` it passes. Pure string work over the file system;
34
+ // nothing is executed, so a missing module cannot throw during the check that
35
+ // exists to report it.
36
+ //
37
+ // WHY IT DOES NOT SIMPLY TRY/CATCH EACH REQUIRE. Individual guards are what the
38
+ // codebase already does where a module is genuinely OPTIONAL (context-pack.js's
39
+ // classifier, cli-lib.js's reward policy) and they are right for that: the command
40
+ // still works, slightly diminished. These requires are not optional — ship.js
41
+ // cannot ship without `modules/lifecycle/ship-preflight`. Wrapping them would turn
42
+ // a loud failure into a confusing one.
43
+ //
44
+ // PURE + INJECTED (`readFile`, `exists`) so the whole thing is testable with a fake
45
+ // file system and no box — which is the layer task 1003098 asks for the test at.
46
+
47
+ const fsMod = require('node:fs');
48
+ const pathMod = require('node:path');
49
+
50
+ const REPO_ROOT = pathMod.resolve(__dirname, '..', '..');
51
+
52
+ // A require of this repo's OWN module code, which is what a scoped checkout drops.
53
+ // Matches the two spellings the CLI uses; the quote style is captured so a require
54
+ // written with double quotes is not silently invisible to the walk.
55
+ const MODULE_REQUIRE_RE = /require\(\s*['"](\.\.\/\.\.\/modules\/[^'"]+)['"]\s*\)/g;
56
+ // A sibling require inside scripts/gds/ — the edges the walk follows.
57
+ const LOCAL_REQUIRE_RE = /require\(\s*['"](\.\/[^'"]+)['"]\s*\)/g;
58
+
59
+ // Comments are stripped before scanning. A header that QUOTES a require to explain
60
+ // why it was removed would otherwise be walked as a live edge — the defect filed as
61
+ // task 1003832 against a test that scanned raw source, and one this file would hit
62
+ // immediately, because its own header above quotes the exact failing require.
63
+ function stripComments(src) {
64
+ return String(src)
65
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
66
+ .replace(/^\s*\/\/.*$/gm, ' ');
67
+ }
68
+
69
+ // Is the character at `index` inside a try{} block?
70
+ //
71
+ // THE DISTINCTION THIS DRAWS IS THE WHOLE CORRECTNESS OF THE PREFLIGHT. Several of
72
+ // the CLI's module requires are deliberately guarded — context-pack.js's classifier
73
+ // and cli-lib.js's reward policy both sit in a try and fall back honestly, so those
74
+ // commands RUN on a scoped box. Reporting them as missing requirements would refuse
75
+ // a command that works, which is a worse failure than the one being fixed: a builder
76
+ // who could have shipped is told they cannot.
77
+ //
78
+ // A brace walk, not a parser — this repo ships none (the route-rank-check /
79
+ // exec-path-guard precedent). It tracks a stack of block kinds and answers whether
80
+ // any enclosing block was opened by `try`. Comments are already stripped by the
81
+ // caller; string and regex literals containing braces are the known blind spot, and
82
+ // the failure direction is benign: a miscount can only mis-scope a block, and the
83
+ // classification is proved against the real files by tests/tree_preflight.mjs, which
84
+ // pins that start.js's requires read optional and ship.js's read required.
85
+ function insideTry(src, index) {
86
+ const stack = [];
87
+ for (let i = 0; i < index && i < src.length; i++) {
88
+ const c = src[i];
89
+ if (c === '{') {
90
+ // Look back past whitespace for the keyword that opened this block.
91
+ const before = src.slice(Math.max(0, i - 200), i);
92
+ stack.push(/\btry\s*$/.test(before) ? 'try' : 'block');
93
+ } else if (c === '}') {
94
+ stack.pop();
95
+ }
96
+ }
97
+ return stack.includes('try');
98
+ }
99
+
100
+ function matchAll(src, re) {
101
+ const out = [];
102
+ let m;
103
+ re.lastIndex = 0;
104
+ while ((m = re.exec(src)) !== null) out.push({ spec: m[1], index: m.index });
105
+ return out;
106
+ }
107
+
108
+ // Resolve a relative specifier the way require would, to a repo-relative posix path.
109
+ function resolveRel(fromFile, spec, { root = REPO_ROOT } = {}) {
110
+ const abs = pathMod.resolve(pathMod.dirname(fromFile), spec);
111
+ const rel = pathMod.relative(root, abs).split(pathMod.sep).join('/');
112
+ return rel;
113
+ }
114
+
115
+ // Add the `.js` a bare specifier omits. Only ever appended — a spec that already
116
+ // names an extension is left exactly as written, so a `.json` require still points
117
+ // at the file it names.
118
+ function withJsExt(rel) {
119
+ return /\.[a-z]+$/i.test(rel) ? rel : `${rel}.js`;
120
+ }
121
+
122
+ /**
123
+ * Every module-code path the entry point's require graph reaches. PURE.
124
+ *
125
+ * Follows `./sibling` edges inside scripts/gds/ transitively and collects the
126
+ * `../../modules/...` leaves. A file it cannot read is skipped rather than thrown
127
+ * on: this runs on a checkout that is by definition incomplete, and a preflight
128
+ * that crashes while diagnosing a partial tree is worse than the trace it replaces.
129
+ *
130
+ * @returns {string[]} repo-relative posix paths, deduped, stable order
131
+ */
132
+ function requiredModulePaths(entryFile, { readFile, root = REPO_ROOT } = {}) {
133
+ const read = readFile || ((f) => fsMod.readFileSync(f, 'utf8'));
134
+ const seen = new Set();
135
+ const found = new Set();
136
+ const queue = [pathMod.resolve(entryFile)];
137
+
138
+ while (queue.length) {
139
+ const file = queue.shift();
140
+ if (seen.has(file)) continue;
141
+ seen.add(file);
142
+ let src;
143
+ try { src = stripComments(read(file)); } catch (_) { continue; }
144
+
145
+ for (const { spec, index } of matchAll(src, MODULE_REQUIRE_RE)) {
146
+ // A guarded require is not a requirement — see insideTry.
147
+ if (insideTry(src, index)) continue;
148
+ found.add(withJsExt(resolveRel(file, spec, { root })));
149
+ }
150
+ for (const { spec } of matchAll(src, LOCAL_REQUIRE_RE)) {
151
+ const abs = pathMod.resolve(pathMod.dirname(file), spec);
152
+ queue.push(/\.[a-z]+$/i.test(abs) ? abs : `${abs}.js`);
153
+ }
154
+ }
155
+ return [...found].sort();
156
+ }
157
+
158
+ /**
159
+ * Which of those paths are not on disk. PURE. Returns repo-relative paths.
160
+ */
161
+ function missingPaths(paths, { exists, root = REPO_ROOT } = {}) {
162
+ const has = exists || ((rel) => fsMod.existsSync(pathMod.join(root, rel)));
163
+ return paths.filter((rel) => !has(rel));
164
+ }
165
+
166
+ // The distinct top-level module directories a set of missing paths falls under —
167
+ // what the builder actually has to get back, and what box-sync speaks in.
168
+ function missingModuleDirs(paths) {
169
+ const dirs = new Set();
170
+ for (const p of paths) {
171
+ const m = String(p).match(/^modules\/([^/]+)\//);
172
+ if (m) dirs.add(`modules/${m[1]}`);
173
+ }
174
+ return [...dirs].sort();
175
+ }
176
+
177
+ /**
178
+ * ONE actionable message, or null when the tree is complete. PURE.
179
+ *
180
+ * Names the CONDITION (a scoped checkout, not a broken install), EVERY missing
181
+ * requirement, and BOTH remedies — re-scope, or take the whole tree. Two are given
182
+ * because they solve different problems: box-sync re-reads the server's path list
183
+ * and is right when the claim moved, while sparse-checkout disable is the escape
184
+ * hatch when the scope itself is what is wrong.
185
+ *
186
+ * It does not promise which one applies, because that is exactly what this layer
187
+ * cannot know — it can see that the code is absent, not why.
188
+ */
189
+ function preflightMessage(missing, { command = 'this command' } = {}) {
190
+ if (!missing || !missing.length) return null;
191
+ const dirs = missingModuleDirs(missing);
192
+ const lines = [
193
+ `${command} needs code this checkout does not contain.`,
194
+ '',
195
+ `Missing (${missing.length}):`,
196
+ ...missing.map((p) => ` - ${p}`),
197
+ '',
198
+ 'This is normal on a TASK-SCOPED dev box: the box carries scripts/, src/bongos/,',
199
+ 'tests/ and the module your claimed task is scoped to — not every module. It is',
200
+ 'not a broken install, and re-running will not fix it.',
201
+ '',
202
+ 'Fix it with ONE of:',
203
+ ' node scripts/gds/box-sync.js re-scope this box to what you hold now',
204
+ ' git sparse-checkout disable take the whole tree instead',
205
+ ];
206
+ if (dirs.length) {
207
+ lines.splice(lines.indexOf('') + 1, 0, `Whole directories absent: ${dirs.join(', ')}`);
208
+ }
209
+ return lines.join('\n');
210
+ }
211
+
212
+ /**
213
+ * The one call an entry point makes. Returns null when the tree is complete, or the
214
+ * message to print before exiting non-zero.
215
+ *
216
+ * Every dependency is injectable so the test drives it with a fake file system —
217
+ * and so a failure inside the preflight can never be the reason a command dies.
218
+ */
219
+ function checkTree(entryFile, { readFile, exists, root = REPO_ROOT, command } = {}) {
220
+ try {
221
+ const required = requiredModulePaths(entryFile, { readFile, root });
222
+ const missing = missingPaths(required, { exists, root });
223
+ return preflightMessage(missing, { command: command || pathMod.basename(entryFile) });
224
+ } catch (_) {
225
+ // A preflight that throws is a preflight that replaced a clear stack trace with
226
+ // a worse one. Silence here means the command proceeds and fails the old way,
227
+ // which is the outcome this file improves on — never the one it causes.
228
+ return null;
229
+ }
230
+ }
231
+
232
+ module.exports = {
233
+ REPO_ROOT,
234
+ requiredModulePaths,
235
+ missingPaths,
236
+ missingModuleDirs,
237
+ preflightMessage,
238
+ checkTree,
239
+ stripComments,
240
+ insideTry,
241
+ };
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.768'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.769'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,243 @@
1
+ // tests/tree_preflight.mjs — the partial-checkout preflight (task 1003098).
2
+ //
3
+ // THE BUG. On a task-scoped dev box — a supported, server-driven configuration —
4
+ // `scripts/gds/ship.js` died with a raw stack trace naming ONE missing module. Fix
5
+ // that one, run again, learn the next. The filer lost a session to it, and their
6
+ // workaround (`git sparse-checkout disable` plus disabling the re-scope cron) is
7
+ // not something a builder should have to discover. Node's loader cannot do better:
8
+ // it stops at the first unresolvable specifier and never reaches the second.
9
+ //
10
+ // THE TWO FAILURE DIRECTIONS, and both are tested, because only one is obvious:
11
+ //
12
+ // under-reporting — the original bug: name one missing thing, hide the other six.
13
+ // OVER-reporting — the regression a naive fix causes: several of the CLI's
14
+ // module requires are deliberately try-guarded, so those
15
+ // commands RUN on a scoped box. Refusing them would tell a
16
+ // builder who could have shipped that they cannot, which is
17
+ // worse than the trace it replaces.
18
+ //
19
+ // Pure: a fake file system throughout, per the task's own DONE-WHEN. The cases at
20
+ // the end are the empirical anchor — they run the classifier against the REAL
21
+ // scripts/gds files and pin it to behaviour observed on a simulated scoped tree:
22
+ // start.js survives, claim.js dies on one module, ship.js on seven.
23
+ //
24
+ // Run: node tests/tree_preflight.mjs
25
+
26
+ import { strict as assert } from 'node:assert';
27
+ import { createRequire } from 'node:module';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { dirname, join } from 'node:path';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const tp = require('../scripts/gds/tree-preflight.js');
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ const repoRoot = join(__dirname, '..');
36
+
37
+ let passed = 0;
38
+ let failed = 0;
39
+ const t = (name, fn) => {
40
+ try { fn(); passed += 1; console.log(` PASS ${name}`); }
41
+ catch (e) { failed += 1; console.log(` FAIL ${name}\n ${e.message}`); }
42
+ };
43
+
44
+ // A fake scripts/gds tree. Keys are absolute-ish paths as the walker resolves them.
45
+ function fakeFs(files) {
46
+ return {
47
+ readFile: (f) => {
48
+ const key = String(f).replace(/\\/g, '/');
49
+ const hit = Object.keys(files).find((k) => key.endsWith(k));
50
+ if (hit === undefined) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }
51
+ return files[hit];
52
+ },
53
+ };
54
+ }
55
+
56
+ console.log('\nthe walk — what the command actually needs:');
57
+
58
+ t('collects a direct module require', () => {
59
+ const fs = fakeFs({ 'scripts/gds/a.js': "const x = require('../../modules/lifecycle/dead-deps.js');" });
60
+ assert.deepEqual(
61
+ tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
62
+ ['modules/lifecycle/dead-deps.js'],
63
+ );
64
+ });
65
+
66
+ t('FOLLOWS sibling requires — the whole reason a declared list would be wrong', () => {
67
+ // ship.js reaches its modules through sixteen ship-*.js phase files; a hand-kept
68
+ // list of "what ship.js needs" is a second copy of the require graph.
69
+ const fs = fakeFs({
70
+ 'scripts/gds/entry.js': "const p = require('./phase.js');",
71
+ 'scripts/gds/phase.js': "const g = require('../../modules/grading/grader.js');",
72
+ });
73
+ assert.deepEqual(
74
+ tp.requiredModulePaths('/repo/scripts/gds/entry.js', { ...fs, root: '/repo' }),
75
+ ['modules/grading/grader.js'],
76
+ );
77
+ });
78
+
79
+ t('a require cycle terminates', () => {
80
+ const fs = fakeFs({
81
+ 'scripts/gds/a.js': "require('./b.js'); require('../../modules/lifecycle/one.js');",
82
+ 'scripts/gds/b.js': "require('./a.js'); require('../../modules/lifecycle/two.js');",
83
+ });
84
+ assert.deepEqual(
85
+ tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
86
+ ['modules/lifecycle/one.js', 'modules/lifecycle/two.js'],
87
+ );
88
+ });
89
+
90
+ t('an unreadable file is skipped, never thrown on', () => {
91
+ // This runs on a checkout that is BY DEFINITION incomplete. A preflight that
92
+ // crashes while diagnosing a partial tree is worse than the trace it replaces.
93
+ const fs = fakeFs({ 'scripts/gds/a.js': "require('./gone.js'); require('../../modules/lifecycle/one.js');" });
94
+ assert.deepEqual(
95
+ tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
96
+ ['modules/lifecycle/one.js'],
97
+ );
98
+ });
99
+
100
+ t('a bare specifier gains .js; one that names an extension is left alone', () => {
101
+ const fs = fakeFs({
102
+ 'scripts/gds/a.js': "require('../../modules/lifecycle/ship-preflight');require('../../modules/lifecycle/d.json');",
103
+ });
104
+ assert.deepEqual(
105
+ tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
106
+ ['modules/lifecycle/d.json', 'modules/lifecycle/ship-preflight.js'],
107
+ );
108
+ });
109
+
110
+ console.log('\nOVER-reporting — the regression a naive fix causes:');
111
+
112
+ t('a try-guarded require is NOT a requirement', () => {
113
+ // context-pack.js and cli-lib.js both guard theirs and fall back honestly, which
114
+ // is why /builder-start works on a scoped box today. Reporting them would refuse
115
+ // a command that runs.
116
+ const fs = fakeFs({
117
+ 'scripts/gds/a.js': "let c=null; try { c = require('../../modules/lifecycle/task-classifier'); } catch (_) {}",
118
+ });
119
+ assert.deepEqual(tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }), []);
120
+ });
121
+
122
+ t('a require AFTER a closed try is still a requirement', () => {
123
+ // The brace walk must pop. If it did not, one guarded require near the top would
124
+ // silently excuse every require below it in the file.
125
+ const fs = fakeFs({
126
+ 'scripts/gds/a.js': "try { require('../../modules/lifecycle/opt.js'); } catch (_) {}\nrequire('../../modules/lifecycle/need.js');",
127
+ });
128
+ assert.deepEqual(
129
+ tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
130
+ ['modules/lifecycle/need.js'],
131
+ );
132
+ });
133
+
134
+ t('a require quoted inside a COMMENT is not walked', () => {
135
+ // tree-preflight.js's own header quotes the failing require verbatim to explain
136
+ // the bug. A raw-text scan would read that prose as a live edge — the defect
137
+ // filed as task 1003832 against a test that scanned raw source.
138
+ const fs = fakeFs({
139
+ 'scripts/gds/a.js': "// require('../../modules/grading/nope.js')\n/* require('../../modules/grading/also.js') */\nrequire('../../modules/lifecycle/real.js');",
140
+ });
141
+ assert.deepEqual(
142
+ tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
143
+ ['modules/lifecycle/real.js'],
144
+ );
145
+ });
146
+
147
+ console.log('\nthe message — one run, every missing thing:');
148
+
149
+ t('nothing missing → null, so a complete checkout is a silent no-op', () => {
150
+ assert.equal(tp.preflightMessage([], { command: '/builder-ship' }), null);
151
+ assert.equal(tp.preflightMessage(null, { command: '/builder-ship' }), null);
152
+ });
153
+
154
+ t('EVERY missing requirement is listed, not the first', () => {
155
+ // The whole ask. Seven for ship.js is seven guess-read-fix cycles avoided.
156
+ const missing = ['modules/a/one.js', 'modules/b/two.js', 'modules/b/three.js'];
157
+ const msg = tp.preflightMessage(missing, { command: '/builder-ship' });
158
+ for (const m of missing) assert.ok(msg.includes(m), `names ${m}`);
159
+ assert.match(msg, /Missing \(3\)/, 'and says how many');
160
+ });
161
+
162
+ t('it names the CONDITION and BOTH remedies', () => {
163
+ const msg = tp.preflightMessage(['modules/a/one.js'], { command: '/builder-ship' });
164
+ assert.match(msg, /task-scoped/i, 'names why the code is absent');
165
+ assert.match(msg, /not a broken install/i, 'and rules out the wrong diagnosis');
166
+ assert.match(msg, /box-sync\.js/, 're-scope');
167
+ assert.match(msg, /sparse-checkout disable/, 'or take the whole tree');
168
+ assert.match(msg, /\/builder-ship/, 'and says which command was refused');
169
+ });
170
+
171
+ t('it rolls the missing files up to whole directories', () => {
172
+ const msg = tp.preflightMessage(['modules/lifecycle/a.js', 'modules/lifecycle/b.js', 'modules/grading/c.js']);
173
+ assert.match(msg, /modules\/grading, modules\/lifecycle/, 'deduped and sorted');
174
+ });
175
+
176
+ console.log('\ncheckTree — what the entry points call:');
177
+
178
+ t('a complete tree returns null', () => {
179
+ const fs = fakeFs({ 'scripts/gds/a.js': "require('../../modules/lifecycle/one.js');" });
180
+ assert.equal(tp.checkTree('/repo/scripts/gds/a.js', { ...fs, root: '/repo', exists: () => true }), null);
181
+ });
182
+
183
+ t('a partial tree returns the message', () => {
184
+ const fs = fakeFs({ 'scripts/gds/a.js': "require('../../modules/lifecycle/one.js');" });
185
+ const msg = tp.checkTree('/repo/scripts/gds/a.js', { ...fs, root: '/repo', exists: () => false, command: '/builder-x' });
186
+ assert.match(msg, /modules\/lifecycle\/one\.js/);
187
+ });
188
+
189
+ t('a preflight that THROWS returns null rather than replacing one bad trace with another', () => {
190
+ const boom = { readFile: () => { throw new Error('boom'); }, exists: () => { throw new Error('boom'); } };
191
+ assert.equal(tp.checkTree('/repo/scripts/gds/a.js', { ...boom, root: '/repo' }), null);
192
+ });
193
+
194
+ console.log('\nthe empirical anchor — the REAL CLI files:');
195
+
196
+ t('start.js needs NO unguarded module code, matching that it runs on a scoped box', () => {
197
+ // Observed: with modules/ hidden, start.js prints its card normally. If this
198
+ // list is ever non-empty, /builder-start starts refusing on a box where it works.
199
+ const req = tp.requiredModulePaths(join(repoRoot, 'scripts/gds/start.js'), { root: repoRoot });
200
+ assert.deepEqual(req, [], `start.js must survive a scoped box; got ${req.join(', ')}`);
201
+ });
202
+
203
+ t('claim.js and ship.js DO need module code, matching the traces they died with', () => {
204
+ const claim = tp.requiredModulePaths(join(repoRoot, 'scripts/gds/claim.js'), { root: repoRoot });
205
+ assert.ok(claim.includes('modules/lifecycle/dead-deps.js'),
206
+ 'the exact module claim.js died on');
207
+ const ship = tp.requiredModulePaths(join(repoRoot, 'scripts/gds/ship.js'), { root: repoRoot });
208
+ assert.ok(ship.includes('modules/lifecycle/ship-preflight.js'),
209
+ 'the exact module ship.js died on');
210
+ assert.ok(ship.length >= 5,
211
+ `ship.js fans out across phase files; the walk found ${ship.length}, so the one-per-run cost was real`);
212
+ });
213
+
214
+ t('the three entry points actually CALL the preflight, above their first require', () => {
215
+ // The check is worthless if it runs after the require that dies. Asserted by
216
+ // position, because that is the property — not merely that the call exists.
217
+ for (const f of ['start.js', 'claim.js', 'ship.js']) {
218
+ const src = require('node:fs').readFileSync(join(repoRoot, 'scripts/gds', f), 'utf8');
219
+ const call = src.indexOf('_checkTree(__filename');
220
+ assert.ok(call > 0, `${f} calls the preflight`);
221
+ const firstModuleRequire = src.search(/require\(\s*['"]\.\.\/\.\.\/modules\//);
222
+ if (firstModuleRequire > 0) {
223
+ assert.ok(call < firstModuleRequire, `${f}: the preflight must run BEFORE the first modules/ require`);
224
+ }
225
+ }
226
+ });
227
+
228
+ t('the preflight names the command for the READER, never a hard-coded slash command', () => {
229
+ // The message this preflight prints is reader-facing, and `/builder-ship` does not exist
230
+ // outside Claude Code — a person running the public `bongos` CLI would be handed a command
231
+ // they cannot type. tests/cli_surface.mjs scans start.js for exactly this, but nothing
232
+ // scanned claim.js or ship.js, and the first draft of this task hard-coded all three.
233
+ for (const f of ['start.js', 'claim.js', 'ship.js']) {
234
+ const src = require('node:fs').readFileSync(join(repoRoot, 'scripts/gds', f), 'utf8');
235
+ const line = src.split('\n').find((l) => l.includes('_checkTree(__filename'));
236
+ assert.ok(line, `${f} calls the preflight`);
237
+ assert.ok(!/\/builder-/.test(line), `${f}: hard-coded slash command in the preflight message`);
238
+ assert.match(line, /_surfaceAction\(/, `${f}: the command name must come from the surface switch`);
239
+ }
240
+ });
241
+
242
+ console.log(`\ntree_preflight: ${passed} passed, ${failed} failed`);
243
+ if (failed) process.exit(1);