@bongos/core 1.19.1073 → 1.19.1074

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/.bongos-core.json +320 -465
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -60
  5. package/clients/bongos-client/index.cjs +4 -60
  6. package/clients/bongos-client/index.d.ts +5 -90
  7. package/clients/bongos-client/index.mjs +4 -60
  8. package/config/branding.neutral.json +1 -5
  9. package/config/modules.neutral.json +2 -3
  10. package/config/scheduled-routines.json +0 -2
  11. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  12. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  13. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  14. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  15. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
  16. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  17. package/docs/adr/README.md +1 -0
  18. package/docs/api/openapi.json +171 -1661
  19. package/docs/api-reference.md +9 -41
  20. package/docs/architecture.md +16 -1
  21. package/docs/branding-contract.md +0 -3
  22. package/docs/copy-inventory.md +454 -479
  23. package/docs/copy-registry.json +746 -978
  24. package/docs/file-map.md +5 -34
  25. package/docs/module-api-changelog.md +2 -0
  26. package/docs/modules-contract.md +13 -1
  27. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  28. package/docs/onboarding/diagrams/README.md +3 -3
  29. package/docs/onboarding/diagrams/assertions.json +2 -22
  30. package/docs/onboarding/primer.md +9 -28
  31. package/docs/page-inventory.json +4 -1
  32. package/docs/page-readings.json +1032 -1077
  33. package/docs/recipes/render-pilot.md +29 -10
  34. package/migrations/core_257_module_store_registry.sql +87 -0
  35. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  36. package/modules/agents/module.json +1 -0
  37. package/modules/autonomy/module.json +1 -0
  38. package/modules/builder-settings/module.json +1 -0
  39. package/modules/builder-settings/render-prefs.js +4 -5
  40. package/modules/copy-desk/docx.js +78 -5
  41. package/modules/copy-desk/module.json +1 -0
  42. package/modules/copy-desk/routes/copy-desk.js +9 -9
  43. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  44. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  45. package/modules/discord/module.json +1 -0
  46. package/modules/economy/module.json +1 -0
  47. package/modules/economy/routes/credits.js +1 -2
  48. package/modules/government/catalog.js +13 -17
  49. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  50. package/modules/government/module.json +1 -0
  51. package/modules/government/protected-surfaces.json +3 -11
  52. package/modules/government/resolver.js +3 -46
  53. package/modules/government/routes/government.js +0 -2
  54. package/modules/government/session-scopes.js +43 -83
  55. package/modules/government/session-scopes.json +8 -18
  56. package/modules/grading/module.json +1 -0
  57. package/modules/hall-ui/module.json +1 -0
  58. package/modules/hall-ui/public/board-lib.js +2 -2
  59. package/modules/hall-ui/public/builders.js +29 -40
  60. package/modules/hall-ui/public/collab.html +1 -1
  61. package/modules/hall-ui/public/collab.js +1 -1
  62. package/modules/hall-ui/public/diagrams.html +3 -3
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/gate.js +2 -2
  65. package/modules/hall-ui/public/gate.states.json +1 -1
  66. package/modules/hall-ui/public/hall-render.js +43 -79
  67. package/modules/hall-ui/public/index.html +5 -1
  68. package/modules/hall-ui/public/modules.js +1 -0
  69. package/modules/hall-ui/public/oversight.css +5 -5
  70. package/modules/hall-ui/public/palette.js +0 -11
  71. package/modules/hall-ui/public/primer.js +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings-sessions.js +1 -1
  74. package/modules/hall-ui/public/settings.css +0 -1
  75. package/modules/hall-ui/public/settings.html +4 -33
  76. package/modules/hall-ui/public/settings.js +3 -33
  77. package/modules/hall-ui/public/settings.states.json +2 -2
  78. package/modules/hall-ui/public/studio.html +1 -1
  79. package/modules/hall-ui/public/style.css +10 -13
  80. package/modules/hall-ui/public/task.html +4 -1
  81. package/modules/hall-ui/public/task.js +36 -1
  82. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  83. package/modules/hall-ui/public/tweak-editor.css +16 -0
  84. package/modules/hall-ui/public/tweak-editor.html +26 -3
  85. package/modules/hall-ui/public/tweak-editor.js +32 -1
  86. package/modules/hall-ui/public/work.js +1 -1
  87. package/modules/hall-ui/records/tweak-editor.md +1 -0
  88. package/modules/ideas/module.json +1 -0
  89. package/modules/lifecycle/db-claim-reads.js +1 -58
  90. package/modules/lifecycle/db-ship.js +0 -5
  91. package/modules/lifecycle/db.js +0 -4
  92. package/modules/lifecycle/kickoff-checklist.js +0 -4
  93. package/modules/lifecycle/lifecycle.js +0 -8
  94. package/modules/lifecycle/module.json +1 -0
  95. package/modules/lifecycle/routes/tasks.js +5 -7
  96. package/modules/memory/module.json +1 -0
  97. package/modules/npm-release/module.json +1 -0
  98. package/modules/onboarding/module.json +1 -0
  99. package/modules/onboarding/onboarding-state.js +17 -36
  100. package/modules/onboarding/routes/access-requests.js +3 -3
  101. package/modules/platform-identity/module.json +1 -0
  102. package/modules/platform-identity/platform-identity.js +13 -5
  103. package/modules/platform-identity/routes/sso.js +2 -2
  104. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  105. package/modules/provisioning/module.json +1 -0
  106. package/modules/provisioning/provisioning.js +2 -2
  107. package/modules/provisioning/starter-bundles.js +5 -25
  108. package/modules/public-landing/module.json +1 -0
  109. package/modules/public-landing/public/assets/cosmos.css +1 -1
  110. package/modules/public-landing/public/contact.html +1 -1
  111. package/modules/public-landing/public/privacy.html +1 -1
  112. package/modules/public-landing/public/projects.html +101 -32
  113. package/modules/public-landing/public/projects.probes.json +2 -2
  114. package/modules/public-landing/public/projects.states.json +4 -3
  115. package/modules/public-landing/public/terms.html +1 -1
  116. package/modules/security/module.json +1 -0
  117. package/modules/sessions/module.json +1 -0
  118. package/modules/specialities/module.json +1 -0
  119. package/modules/status-ui/module.json +1 -0
  120. package/modules/ui-design/module.json +1 -0
  121. package/package-lock.json +2 -2
  122. package/package.json +1 -1
  123. package/release-notes.json +78 -0
  124. package/scripts/gds/artifact-format.js +203 -0
  125. package/scripts/gds/audit-authorship.sh +1 -13
  126. package/scripts/gds/claim.js +0 -15
  127. package/scripts/gds/cli-lib.js +8 -8
  128. package/scripts/gds/diagram-facts.js +12 -23
  129. package/scripts/gds/do-api.js +13 -80
  130. package/scripts/gds/doc-cli-guard.js +2 -0
  131. package/scripts/gds/doctor.js +1 -1
  132. package/scripts/gds/feedback-latest.js +139 -9
  133. package/scripts/gds/fitness-checks-identity.js +2 -7
  134. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  135. package/scripts/gds/fitness-ratchets.js +4 -4
  136. package/scripts/gds/fitness.js +10 -10
  137. package/scripts/gds/gen-diagrams.js +49 -71
  138. package/scripts/gds/http-api-client.js +12 -19
  139. package/scripts/gds/init.js +1 -1
  140. package/scripts/gds/lint-infra-exec.js +1 -1
  141. package/scripts/gds/local-preview-lib.js +8 -39
  142. package/scripts/gds/local-preview.js +2 -15
  143. package/scripts/gds/migration-namespace.js +3 -3
  144. package/scripts/gds/module-artifact.js +212 -0
  145. package/scripts/gds/module.js +95 -6
  146. package/scripts/gds/package-core.js +5 -116
  147. package/scripts/gds/provision-config.js +2 -2
  148. package/scripts/gds/provision-net.js +2 -3
  149. package/scripts/gds/provision.js +9 -9
  150. package/scripts/gds/publish-manifest.js +1 -2
  151. package/scripts/gds/regen-instance-docs.js +1 -1
  152. package/scripts/gds/run-unit-tests.js +2 -16
  153. package/scripts/gds/sandbox-stage.js +39 -341
  154. package/scripts/gds/ship-flow.js +6 -6
  155. package/scripts/gds/ship-land.js +3 -4
  156. package/scripts/gds/ship-regen.js +10 -41
  157. package/scripts/gds/ship.js +4 -24
  158. package/scripts/gds/start.js +0 -15
  159. package/scripts/hall-preview/README.md +5 -5
  160. package/scripts/hall-preview/server.js +6 -6
  161. package/scripts/render-diagrams.sh +1 -1
  162. package/src/bongos/api-errors.js +1 -1
  163. package/src/bongos/auth-github.js +2 -3
  164. package/src/bongos/auth.js +51 -126
  165. package/src/bongos/db-kernel.js +2 -3
  166. package/src/bongos/db.js +5 -28
  167. package/src/bongos/module-scope-map.js +14 -29
  168. package/src/bongos/module-store.js +141 -0
  169. package/src/bongos/route-rank-check.js +3 -16
  170. package/src/bongos/routes/auth.js +0 -116
  171. package/src/bongos/routes/builders.js +5 -15
  172. package/src/bongos/routes/instance.js +8 -14
  173. package/src/bongos/routes/me.js +6 -22
  174. package/src/bongos/routes/modules.js +83 -0
  175. package/src/bongos/routes.js +5 -0
  176. package/src/module-api.js +13 -6
  177. package/src/module-loader/catalog.js +3 -0
  178. package/src/module-loader/loader.js +4 -4
  179. package/src/module-loader/manifest-schema.js +15 -7
  180. package/src/module-seams.js +2 -3
  181. package/src/modules.js +42 -14
  182. package/tests/agents_spend_guard.mjs +1 -1
  183. package/tests/api_client.mjs +0 -15
  184. package/tests/auth_page_gate.mjs +10 -10
  185. package/tests/canonical_profile_url.mjs +1 -3
  186. package/tests/claim_action.mjs +4 -1
  187. package/tests/claim_from_task_and_home.mjs +127 -0
  188. package/tests/consumer_layout_boot.mjs +2 -0
  189. package/tests/copy_desk_page_docx.mjs +33 -2
  190. package/tests/core_upgrade_runner.mjs +6 -6
  191. package/tests/deploy_divergence_line.mjs +11 -11
  192. package/tests/diagram_facts_offset.mjs +1 -1
  193. package/tests/do_api.mjs +106 -0
  194. package/tests/feedback_latest.mjs +122 -0
  195. package/tests/fitness.mjs +44 -42
  196. package/tests/gate_approvals.mjs +0 -1
  197. package/tests/go_live.mjs +3 -3
  198. package/tests/government_abuse_matrix.mjs +20 -19
  199. package/tests/government_ownership_scope.mjs +33 -35
  200. package/tests/government_parity.mjs +2 -2
  201. package/tests/government_protected_surfaces.mjs +56 -37
  202. package/tests/government_require_permission.mjs +3 -3
  203. package/tests/government_seed.mjs +37 -4
  204. package/tests/government_session_scope.mjs +92 -366
  205. package/tests/hall_error_envelope.mjs +4 -0
  206. package/tests/hall_landing_boot.mjs +4 -1
  207. package/tests/hall_palette.mjs +1 -16
  208. package/tests/hall_record_world.mjs +6 -0
  209. package/tests/hall_tweak_editor.mjs +52 -1
  210. package/tests/host_topology_skips.mjs +1 -2
  211. package/tests/html_comment_nesting.mjs +137 -0
  212. package/tests/infra_exec_paths.mjs +9 -10
  213. package/tests/init.mjs +4 -5
  214. package/tests/instance_manifest.mjs +18 -25
  215. package/tests/kickoff_checklist.mjs +1 -1
  216. package/tests/lib_sh_resolution.mjs +0 -221
  217. package/tests/local_preview.mjs +0 -12
  218. package/tests/module-scope-map.mjs +9 -9
  219. package/tests/module_catalog.mjs +1 -1
  220. package/tests/module_cli.mjs +15 -15
  221. package/tests/module_contributions.mjs +6 -6
  222. package/tests/module_loader.mjs +6 -6
  223. package/tests/module_manifest.mjs +13 -4
  224. package/tests/module_store_publish.mjs +307 -0
  225. package/tests/module_store_publish_route.mjs +121 -0
  226. package/tests/module_store_registry_migration.mjs +70 -0
  227. package/tests/modules.mjs +56 -26
  228. package/tests/onboarding_route_signals.mjs +21 -31
  229. package/tests/onboarding_state.mjs +35 -49
  230. package/tests/permission_path.mjs +0 -3
  231. package/tests/platform_boot.mjs +1 -2
  232. package/tests/profile_route.mjs +1 -6
  233. package/tests/projects_hub.mjs +5 -5
  234. package/tests/projects_hub_app_step.mjs +138 -0
  235. package/tests/projects_hub_module_picker.mjs +2 -2
  236. package/tests/projects_hub_pre_uat.mjs +4 -5
  237. package/tests/provision.mjs +7 -7
  238. package/tests/provisioning_starter_bundles.mjs +0 -22
  239. package/tests/publish_branch_route.mjs +10 -18
  240. package/tests/publish_manifest.mjs +1 -2
  241. package/tests/rank_tier_single_source.mjs +1 -13
  242. package/tests/repo_map.mjs +3 -4
  243. package/tests/runner_drift.mjs +3 -21
  244. package/tests/sandbox_stage.mjs +57 -390
  245. package/tests/session_rename_fallback.mjs +0 -21
  246. package/tests/ship_error_shape.mjs +1 -1
  247. package/tests/task_visual_slots.mjs +31 -0
  248. package/tests/terms_acceptance.mjs +1 -1
  249. package/tests/upgrade.mjs +4 -4
  250. package/tests/watch_sealed_floor.mjs +0 -1
  251. package/tests/wizard_draft_resume.mjs +2 -2
  252. package/tests/wizard_intent_resume.mjs +21 -10
  253. package/tests/wizard_preselect_why.mjs +18 -18
  254. package/docs/onboarding/browser-terminal-guide.md +0 -73
  255. package/docs/recipes/managed-settings-remote-control.md +0 -416
  256. package/modules/dev-box/CLAUDE.md +0 -15
  257. package/modules/dev-box/box-access.js +0 -623
  258. package/modules/dev-box/box-credential.js +0 -94
  259. package/modules/dev-box/box-onboard.js +0 -486
  260. package/modules/dev-box/boxes.js +0 -1038
  261. package/modules/dev-box/db.js +0 -39
  262. package/modules/dev-box/module.json +0 -19
  263. package/modules/dev-box/routes/box.js +0 -1223
  264. package/scripts/gds/box-auth-check.js +0 -160
  265. package/scripts/gds/box-infra.js +0 -321
  266. package/scripts/gds/box-sync.js +0 -216
  267. package/scripts/gds/box.js +0 -1332
  268. package/scripts/gds/cf-tunnel.js +0 -558
  269. package/scripts/gds/smoke-box.sh +0 -150
  270. package/scripts/gds/tree-preflight.js +0 -241
  271. package/src/bongos/app-pair.js +0 -172
  272. package/tests/app_pair.mjs +0 -168
  273. package/tests/box_access.mjs +0 -578
  274. package/tests/box_auth_check.mjs +0 -114
  275. package/tests/box_code_staleness.mjs +0 -129
  276. package/tests/box_connect_e2e.mjs +0 -196
  277. package/tests/box_cost.mjs +0 -70
  278. package/tests/box_credential.mjs +0 -125
  279. package/tests/box_dns_repoint.mjs +0 -153
  280. package/tests/box_env.mjs +0 -64
  281. package/tests/box_host_keys.mjs +0 -126
  282. package/tests/box_infra_resolve.mjs +0 -246
  283. package/tests/box_onboard.mjs +0 -317
  284. package/tests/box_scope_session.mjs +0 -119
  285. package/tests/box_sweep_docs.mjs +0 -119
  286. package/tests/box_sync.mjs +0 -55
  287. package/tests/box_sync_scope_report.mjs +0 -126
  288. package/tests/box_task_scope.mjs +0 -169
  289. package/tests/box_task_scope_pause.mjs +0 -66
  290. package/tests/box_terminal_ssrf.mjs +0 -96
  291. package/tests/boxes.mjs +0 -1585
  292. package/tests/cf_ruleset_preflight.mjs +0 -142
  293. package/tests/cf_tunnel.mjs +0 -496
  294. package/tests/docs_scrubber_damage.mjs +0 -111
  295. package/tests/module_scope_active_claims.mjs +0 -94
  296. package/tests/ship_api_client_pathspec.mjs +0 -100
  297. package/tests/tree_preflight.mjs +0 -243
@@ -0,0 +1,203 @@
1
+ // scripts/gds/artifact-format.js — the ONE packaging format Bongos ships code in:
2
+ // a deterministic ustar+gzip tarball plus a compression-independent tree pin
3
+ // (per-file sha256 + tree_sha256). Split out of package-core.js (task 1004271) so the
4
+ // module store (ADR 0338 D1: "no second packaging format") builds and VERIFIES module
5
+ // versions with the exact functions the core release uses — and so the always-on
6
+ // server can read a tarball without loading package-core.js's release machinery.
7
+ // Pure: no I/O beyond zlib/crypto, no git, no network.
8
+
9
+ const zlib = require('node:zlib');
10
+ const crypto = require('node:crypto');
11
+
12
+ function sha256hex(buf) {
13
+ return crypto.createHash('sha256').update(buf).digest('hex');
14
+ }
15
+
16
+ // The PIN. A canonical, compression-independent digest of a packaged tree: one
17
+ // "path\0mode\0filesha256\n" line per file, byte-sorted by path. A consumer
18
+ // recomputes this from the unpacked tree to prove an install matches the pin.
19
+ // `files` = [{ path, mode, sha256 }]. Modes are normalized so the pin is stable
20
+ // regardless of the checkout's umask.
21
+ function canonicalTreeHash(files) {
22
+ const lines = (files || [])
23
+ .map((f) => `${f.path}\u0000${normalizeMode(f.mode)}\u0000${f.sha256}`)
24
+ .sort();
25
+ return sha256hex(Buffer.from(lines.join('\n') + '\n', 'utf8'));
26
+ }
27
+
28
+ // Collapse ANY mode form — git's 100644/100755, our canonical 0000644/0000755, or a
29
+ // raw filesystem octal string like "755" — to two canonical tar modes, keyed only on
30
+ // the executable bit. IDEMPOTENT (normalizeMode(normalizeMode(x)) === normalizeMode(x))
31
+ // AND exec-aware, so the tree pin reproduces byte-identically whether a consumer
32
+ // recomputes it from the manifest's files[] (already-canonical modes) or from the
33
+ // unpacked tree's stat() modes. Expects a STRING (git mode / canonical mode / octal).
34
+ function normalizeMode(mode) {
35
+ const s = String(mode);
36
+ if (s === '100755' || s === '0000755' || s === '755') return '0000755';
37
+ const n = parseInt(s, 8);
38
+ if (Number.isFinite(n) && (n & 0o111)) return '0000755';
39
+ return '0000644';
40
+ }
41
+
42
+ // ---- a minimal, correct, DETERMINISTIC ustar+gzip writer (node-only, #686) ----
43
+ // Deterministic (fixed mtime/uid/gid, normalized mode, sorted entries) so the same
44
+ // source yields byte-identical bytes — a bonus over the tree_sha256 pin. Long paths
45
+ // use the ustar name/prefix split, falling back to a GNU 'L' longname entry (read
46
+ // by GNU tar, bsdtar/macOS, and node-tar) for the rare path that won't split.
47
+
48
+ function octal(n, width) {
49
+ // width-1 octal digits, zero-padded, then a NUL — the classic numeric field.
50
+ return Buffer.from(n.toString(8).padStart(width - 1, '0').slice(-(width - 1)) + '\0', 'ascii');
51
+ }
52
+
53
+ function tarChecksum(block) {
54
+ let sum = 0;
55
+ for (let i = 0; i < 512; i++) sum += (i >= 148 && i < 156) ? 0x20 : block[i];
56
+ return sum;
57
+ }
58
+
59
+ // Build one 512-byte ustar header. `name`/`prefix` must already fit (<=100 / <=155).
60
+ function ustarHeader({ name, prefix = '', mode, size, typeflag }) {
61
+ const h = Buffer.alloc(512, 0);
62
+ h.write(name, 0, 100, 'utf8');
63
+ octal(mode, 8).copy(h, 100);
64
+ octal(0, 8).copy(h, 108); // uid
65
+ octal(0, 8).copy(h, 116); // gid
66
+ octal(size, 12).copy(h, 124);
67
+ octal(0, 12).copy(h, 136); // mtime = 0 (determinism)
68
+ h.write(typeflag, 156, 1, 'ascii');
69
+ h.write('ustar\0', 257, 6, 'ascii');
70
+ h.write('00', 263, 2, 'ascii');
71
+ if (prefix) h.write(prefix, 345, 155, 'utf8');
72
+ // checksum field: 6 octal digits + NUL + space, computed with the field = spaces.
73
+ const cksum = tarChecksum(h);
74
+ h.write(cksum.toString(8).padStart(6, '0').slice(-6) + '\0 ', 148, 8, 'ascii');
75
+ return h;
76
+ }
77
+
78
+ // Split a >100-byte path into { name<=100, prefix<=155 } at a '/' boundary, or
79
+ // return null if no split fits (→ caller uses the GNU longname fallback).
80
+ function splitUstarName(fullPath) {
81
+ const bytes = Buffer.byteLength(fullPath, 'utf8');
82
+ if (bytes <= 100) return { name: fullPath, prefix: '' };
83
+ // Find the LAST '/' such that the tail (after it) is <=100 and the head <=155.
84
+ for (let i = fullPath.length - 1; i > 0; i--) {
85
+ if (fullPath[i] !== '/') continue;
86
+ const prefix = fullPath.slice(0, i);
87
+ const name = fullPath.slice(i + 1);
88
+ if (Buffer.byteLength(name, 'utf8') <= 100 && Buffer.byteLength(prefix, 'utf8') <= 155) {
89
+ return { name, prefix };
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function padTo512(buf) {
96
+ const rem = buf.length % 512;
97
+ return rem === 0 ? buf : Buffer.concat([buf, Buffer.alloc(512 - rem, 0)]);
98
+ }
99
+
100
+ // GNU longname pseudo-entry: a header (typeflag 'L', name '././@LongLink') whose
101
+ // BODY is the full path (incl. trailing NUL), immediately followed by the real
102
+ // file entry. Read by GNU tar, bsdtar/macOS, and node-tar.
103
+ function gnuLongName(fullPath) {
104
+ const payload = Buffer.from(fullPath + '\0', 'utf8');
105
+ const hdr = ustarHeader({ name: '././@LongLink', mode: 0o644, size: payload.length, typeflag: 'L' });
106
+ return Buffer.concat([hdr, padTo512(payload)]);
107
+ }
108
+
109
+ // entries: [{ name, mode, buf }] — `name` includes the TAR_ROOT prefix. Returns a
110
+ // gzip'd tar Buffer. Pure + deterministic given sorted entries.
111
+ function buildTar(entries) {
112
+ const sorted = [...entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
113
+ const chunks = [];
114
+ for (const e of sorted) {
115
+ const mode = e.mode & 0o7777;
116
+ const split = splitUstarName(e.name);
117
+ if (split) {
118
+ chunks.push(ustarHeader({ name: split.name, prefix: split.prefix, mode, size: e.buf.length, typeflag: '0' }));
119
+ } else {
120
+ chunks.push(gnuLongName(e.name));
121
+ chunks.push(ustarHeader({ name: e.name.slice(0, 100), mode, size: e.buf.length, typeflag: '0' }));
122
+ }
123
+ chunks.push(padTo512(e.buf));
124
+ }
125
+ // two zero blocks terminate the archive
126
+ chunks.push(Buffer.alloc(1024, 0));
127
+ const tar = Buffer.concat(chunks);
128
+ return zlib.gzipSync(tar, { level: 9 });
129
+ }
130
+
131
+
132
+
133
+ // ---- the matching READER (task 1004271) -------------------------------------
134
+ // Reads back exactly what buildTar writes — plain ustar file entries (name/prefix
135
+ // split) and GNU 'L' longname entries — into [{ name, mode, buf }]. It is the store's
136
+ // side of the format, so it is strict rather than permissive: any other entry type
137
+ // (a directory, a symlink, a pax header), a size or count over the caps, a truncated
138
+ // archive or a bad header checksum is a THROWN error, never a skipped entry. A server
139
+ // that verifies uploads must not quietly accept a shape it did not check.
140
+ //
141
+ // Two entry points over one parser: readTar (sync — the CLI and tests) and
142
+ // readTarAsync, which inflates on libuv's thread pool so a server verifying an upload
143
+ // does not hold the event loop for the decompression. The parse itself is a single
144
+ // linear pass over the inflated bytes.
145
+ const DEFAULT_READ_LIMITS = { maxUnpackedBytes: 64 * 1024 * 1024, maxFiles: 5000 };
146
+ const gunzipAsync = require('node:util').promisify(zlib.gunzip);
147
+
148
+ const unreadable = (maxUnpackedBytes, e) =>
149
+ new Error(`not a readable gzip tarball within ${maxUnpackedBytes} bytes (${e.code || e.message})`);
150
+
151
+ function readTar(tgz, { maxUnpackedBytes = DEFAULT_READ_LIMITS.maxUnpackedBytes, maxFiles = DEFAULT_READ_LIMITS.maxFiles } = {}) {
152
+ let tar;
153
+ try { tar = zlib.gunzipSync(tgz, { maxOutputLength: maxUnpackedBytes }); }
154
+ catch (e) { throw unreadable(maxUnpackedBytes, e); }
155
+ return parseTar(tar, { maxFiles });
156
+ }
157
+
158
+ async function readTarAsync(tgz, { maxUnpackedBytes = DEFAULT_READ_LIMITS.maxUnpackedBytes, maxFiles = DEFAULT_READ_LIMITS.maxFiles } = {}) {
159
+ let tar;
160
+ try { tar = await gunzipAsync(tgz, { maxOutputLength: maxUnpackedBytes }); }
161
+ catch (e) { throw unreadable(maxUnpackedBytes, e); }
162
+ return parseTar(tar, { maxFiles });
163
+ }
164
+
165
+ function parseTar(tar, { maxFiles }) {
166
+ const cstr = (b, from, len) => {
167
+ const s = b.subarray(from, from + len);
168
+ const z = s.indexOf(0);
169
+ return (z < 0 ? s : s.subarray(0, z)).toString('utf8');
170
+ };
171
+ const out = [];
172
+ let off = 0;
173
+ let longName = null;
174
+ while (true) {
175
+ if (off + 512 > tar.length) throw new Error('tarball is truncated (no end-of-archive marker)');
176
+ const h = tar.subarray(off, off + 512);
177
+ if (h.every((x) => x === 0)) break;
178
+ const stored = parseInt(cstr(h, 148, 8).trim(), 8);
179
+ if (stored !== tarChecksum(h)) throw new Error(`tar header checksum mismatch at byte ${off}`);
180
+ const size = parseInt(cstr(h, 124, 12).trim() || '0', 8);
181
+ if (!Number.isFinite(size) || size < 0) throw new Error(`bad entry size at byte ${off}`);
182
+ const type = String.fromCharCode(h[156] || 48);
183
+ const bodyStart = off + 512;
184
+ const bodyEnd = bodyStart + size;
185
+ if (bodyEnd > tar.length) throw new Error('tarball is truncated mid-entry');
186
+ const body = tar.subarray(bodyStart, bodyEnd);
187
+ off = bodyStart + size + ((512 - (size % 512)) % 512);
188
+ if (type === 'L') { longName = cstr(body, 0, body.length); continue; }
189
+ if (type !== '0' && type !== '\u0000') throw new Error(`unsupported tar entry type "${type}" — only regular files are allowed`);
190
+ const prefix = cstr(h, 345, 155);
191
+ const name = longName || (prefix ? `${prefix}/${cstr(h, 0, 100)}` : cstr(h, 0, 100));
192
+ longName = null;
193
+ out.push({ name, mode: parseInt(cstr(h, 100, 8).trim() || '0', 8), buf: Buffer.from(body) });
194
+ if (out.length > maxFiles) throw new Error(`tarball has more than ${maxFiles} files`);
195
+ }
196
+ return out;
197
+ }
198
+
199
+ module.exports = {
200
+ sha256hex, canonicalTreeHash, normalizeMode,
201
+ octal, tarChecksum, ustarHeader, splitUstarName, padTo512, gnuLongName, buildTar,
202
+ readTar, readTarAsync,
203
+ };
@@ -151,13 +151,6 @@ ALLOWLIST=(
151
151
  # hardcoded in code.
152
152
  'CODEOWNERS'
153
153
 
154
- # Dev Box app distribution (#904, ADR 0044): the PUBLIC releases repo lives
155
- # under the same GitHub account that owns the main repo — repo-hosting
156
- # identity (same nature as provision-staging.sh above), not a builder
157
- # identity. The test pins the committed config's repo string.
158
- 'config/devbox-app.json'
159
- 'tests/devbox_downloads.mjs'
160
-
161
154
  # This audit script's own self-reference (it documents what it's looking for).
162
155
  'scripts/gds/audit-authorship.sh'
163
156
 
@@ -173,21 +166,18 @@ ALLOWLIST=(
173
166
 
174
167
  # Test DATA that pins a real contract (task 1002640) — identity in an
175
168
  # assertion IS the contract, not a mis-attribution: owner approval RECORDS
176
- # the Archon's login (gate_approvals), the terminal-URL SSRF guard validates
177
- # term-<login> hostnames (box_terminal_ssrf), packaging/provisioning tests
169
+ # the Archon's login (gate_approvals), packaging/provisioning tests
178
170
  # exercise the real repo owner + the mirror-redaction floor. Listed per file
179
171
  # (never a blanket tests/ exclusion) so a NEW test file is still scanned.
180
172
  'tests/provision.mjs'
181
173
  'tests/package_core.mjs'
182
174
  'tests/init.mjs'
183
- 'tests/box_terminal_ssrf.mjs'
184
175
  'tests/gate_approvals.mjs'
185
176
  'tests/publish_reconciler.mjs'
186
177
  'tests/dev_lib.mjs'
187
178
  'tests/ship_card.mjs'
188
179
  'tests/scrubber_corpus.mjs'
189
180
  'tests/module_submissions.mjs'
190
- 'tests/boxes.mjs'
191
181
  'tests/provisioning_onboard_mode.mjs'
192
182
  'tests/instance_manifest.mjs'
193
183
  'tests/bongos_downloads.mjs'
@@ -199,10 +189,8 @@ ALLOWLIST=(
199
189
  'tests/currency_label.mjs'
200
190
  'tests/cloud_bongos_pack.mjs'
201
191
  'tests/bump_version.mjs'
202
- 'tests/box_scope_session.mjs'
203
192
  'tests/auth_inactive_builder.mjs'
204
193
  'tests/auth_anthropic_identity.mjs'
205
- 'modules/dev-box/app/test/unit.mjs'
206
194
 
207
195
  # The mirror-redaction floor: PRIOR_INSTANCE_FLOOR carries the prior
208
196
  # instance's owner literal precisely so packaging can SCRUB it from every
@@ -7,21 +7,6 @@
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
-
25
10
  const { execSync } = require('node:child_process');
26
11
  const fs = require('node:fs');
27
12
  const path = require('node:path');
@@ -139,8 +139,8 @@ function hallBase(base = API_BASE) {
139
139
  // still be mid-close when the caller's process.exit() runs, tripping a
140
140
  // UV_HANDLE_CLOSING assertion that corrupts piped JSON stdout. Connection reuse
141
141
  // buys these one-shot processes nothing, so route requests through a dispatcher
142
- // with keep-alive effectively disabled (mirrors cf-tunnel.js's dispatcher
143
- // pattern). Degrades to the default dispatcher if undici can't be required.
142
+ // with keep-alive effectively disabled. Degrades to the default dispatcher if
143
+ // undici can't be required.
144
144
  //
145
145
  // task 1002734 — THIS ALONE IS NOT ENOUGH, and the measurements say so: with the
146
146
  // dispatcher wired in and undici resolving fine, `node scripts/gds/api.js GET
@@ -1069,12 +1069,11 @@ function detectWorktreeName() {
1069
1069
  // summary — that emitCard consumes directly.
1070
1070
  //
1071
1071
  // WHY THIS READS THE PAYLOAD AND NEVER require()s render-prefs.js. The ladder
1072
- // lives in modules/builder-settings/render-prefs.js, and requiring it from here
1073
- // would work on a full checkout and BREAK on a task-scoped dev box, which
1074
- // carries scripts/ and src/bongos/ but not every modules/ directory (task
1075
- // 1003098 — tree-preflight exists precisely to fail an entry point that reaches
1076
- // for module code, and start.js is one). So the server resolves the rung and
1077
- // ships the answers on `render.flags`; this function only picks them up.
1072
+ // lives in modules/builder-settings/render-prefs.js, and a CLI reaches a
1073
+ // module's logic through the server, never by require — the module may be
1074
+ // disabled on the instance, and a require would couple every CLI to one
1075
+ // module's internals. So the server resolves the rung and ships the answers on
1076
+ // `render.flags`; this function only picks them up.
1078
1077
  //
1079
1078
  // TWO SERVER SHAPES, deliberately. `render.flags` is the ladder (task 1002518);
1080
1079
  // `render.widgets_enabled` is the boolean that predates it. A CLI newer than the
@@ -1174,6 +1173,7 @@ module.exports = {
1174
1173
  repairMsysMangledPath,
1175
1174
  resolveQuiet,
1176
1175
  readSessionToken,
1176
+ CLI_USER_AGENT,
1177
1177
  apiCall,
1178
1178
  cliExit,
1179
1179
  isDispatcherIncompatError,
@@ -174,18 +174,12 @@ const CONCEPTUAL_TASK_IDS = {
174
174
  246: 'R26 — karma surfaced on profile + sortable on leaderboard',
175
175
  };
176
176
 
177
- // 04-architecture (the DEV BOX + box→prod flow) — where a builder's work runs
178
- // and how code reaches prod. Same "LIVE iff its backing GDS task is 'shipped'"
179
- // contract as the memory chain above; these were merged into the single
180
- // 04-architecture diagram (was the standalone 06-dev-box). Live ids verified
181
- // 2026-06-14 — the whole chain is now shipped, including 1027 + 1028. Each entry
182
- // documents the piece it drives in scripts/gds/gen-diagrams.js `architectureMmd`:
183
- // 904 — the OTB Dev Box desktop app (menu-bar/tray on-off switch)
184
- // 774 — the browser-only box terminal (ttyd over an outbound tunnel)
185
- // 897 — the per-builder sandbox live game preview on the box
186
- // 919 — the scoped dev-box session (box token deny-by-default)
187
- // 1025 — server-mediated branch publish (box uploads bundle, server pushes)
188
- // 1027 — hardware-verify of the box→prod flow end-to-end (now SHIPPED)
177
+ // 04-architecture (the ship flow) — how code on a builder's machine reaches
178
+ // prod. Same "LIVE iff its backing GDS task is 'shipped'" contract as the memory
179
+ // chain above. Each entry documents the piece it drives in
180
+ // scripts/gds/gen-diagrams.js `architectureMmd`:
181
+ // 1025 — server-mediated branch publish (a checkout with no GitHub push
182
+ // credential uploads a bundle, the server pushes it)
189
183
  // 1028 — replaced the standing push PAT with a GitHub App (now SHIPPED)
190
184
  //
191
185
  // Every id here MUST drive a piece of the diagram. An id no consumer reads is
@@ -194,15 +188,10 @@ const CONCEPTUAL_TASK_IDS = {
194
188
  // stop 03 + 04 from regenerating at all (the failure shape task 1002667 fixed
195
189
  // for the offset case). Do NOT re-add 859 (CI self-deploy): the deploy leg is
196
190
  // the standalone AUTO-PULL of ADR 0108/0129, not a task-gated Actions job, so
197
- // gen-diagrams.js deliberately excludes it — see its comment at `box → prod`.
191
+ // gen-diagrams.js deliberately excludes it — see its comment at `ship flow`.
198
192
  // Removed in task 1002674.
199
- const DEV_BOX_TASK_IDS = {
200
- 904: 'devbox-app — desktop on-off switch',
201
- 774: 'browser-only box terminal (ttyd over tunnel)',
202
- 897: 'per-builder sandbox live preview',
203
- 919: 'scoped dev-box session (box token deny-by-default)',
204
- 1025: 'server-mediated branch publish (box uploads bundle, server pushes)',
205
- 1027: 'hardware-verify the box-to-prod flow end-to-end',
193
+ const SHIP_FLOW_TASK_IDS = {
194
+ 1025: 'server-mediated branch publish (a checkout with no push credential uploads a bundle, the server pushes)',
206
195
  1028: 'GitHub App hardening (replace the standing push PAT)',
207
196
  };
208
197
 
@@ -290,10 +279,10 @@ async function conceptualFacts({ repo, migs, api }) {
290
279
  // process via cli-lib's guidance path (see the loop's degrade-to-null below).
291
280
  api = await cliClient({ allowUnauthenticated: true });
292
281
  }
293
- // 03's pieces + 04-architecture's memory chain + its dev-box/ship-flow pieces
282
+ // 03's pieces + 04-architecture's memory chain + its ship-flow pieces
294
283
  // share one fetch + one taskStatus map; any unreachable id degrades the WHOLE
295
284
  // conceptual bundle (so 03 + 04 skip together rather than emit partial data).
296
- const ALL_TASK_IDS = { ...CONCEPTUAL_TASK_IDS, ...DEV_BOX_TASK_IDS };
285
+ const ALL_TASK_IDS = { ...CONCEPTUAL_TASK_IDS, ...SHIP_FLOW_TASK_IDS };
297
286
  const ids = Object.keys(ALL_TASK_IDS).map(Number);
298
287
  const status = {};
299
288
  let lockedOffset = null;
@@ -360,7 +349,7 @@ module.exports = {
360
349
  repoSlug,
361
350
  conceptualFacts,
362
351
  CONCEPTUAL_TASK_IDS,
363
- DEV_BOX_TASK_IDS,
352
+ SHIP_FLOW_TASK_IDS,
364
353
  IMPORT_ID_OFFSETS,
365
354
  gatherFacts,
366
355
  };
@@ -1,15 +1,12 @@
1
1
  // scripts/gds/do-api.js — a thin DigitalOcean API v2 client for the control plane.
2
2
  //
3
- // Shared control-plane client (ADR 0111 §2, task #1943): the DO client both the
4
- // dev-box CLI (scripts/gds/box.js) and the instance-provisioning runner
5
- // (scripts/gds/provision.js) use. It lived in modules/dev-box/ historically, but
6
- // it is operator-CLI code (never the web tier), so it belongs in the control-plane
7
- // tier alongside its callers — and a module may not import a sibling module, so two
8
- // CLIs can only share it from here. Originally #598 (box lifecycle).
3
+ // Control-plane client (ADR 0111 §2, task #1943): the DO client the
4
+ // instance-provisioning runner (scripts/gds/provision.js) and its teardown
5
+ // (provision-teardown.js) use. It is operator-CLI code (never the web tier), so it
6
+ // lives in the control-plane tier alongside its callers. Originally #598.
9
7
  //
10
- // The ONLY network module in the box-lifecycle subsystem. Everything that
11
- // touches DO goes through here so the rest of the code (boxes.js decisions,
12
- // box.js orchestration) stays testable without a live account.
8
+ // Everything that touches DO goes through here so the provisioning code stays
9
+ // testable without a live account.
13
10
  //
14
11
  // Design:
15
12
  // - Injectable transport (`fetchImpl`) so tests pass a fake fetch and assert
@@ -23,10 +20,8 @@
23
20
  // - Live mode requires a token (DO_API_TOKEN); the constructor throws clearly
24
21
  // if asked to run live without one — it never silently no-ops a real call.
25
22
  //
26
- // We implement only the calls the lifecycle needs:
27
- // createDroplet · getDroplet · deleteDroplet · listDroplets
28
- // snapshotDroplet (action) · getAction (poll) · deleteSnapshot
29
- // createDropletFromSnapshot
23
+ // We implement only the calls provisioning needs:
24
+ // createDroplet · getDroplet · deleteDroplet · settleDroplet (poll to active)
30
25
 
31
26
  const { JsonHttpApi, makeApiError } = require('./http-api-client');
32
27
 
@@ -54,7 +49,7 @@ class DoApi extends JsonHttpApi {
54
49
 
55
50
  // ---- droplets --------------------------------------------------------------
56
51
 
57
- // image: either a base image slug (e.g. 'docker-20-04') OR a snapshot id (int).
52
+ // image: a base image slug (e.g. 'ubuntu-24-04-x64').
58
53
  async createDroplet({ name, region, size, image, userData, tags, sshKeys }) {
59
54
  const body = {
60
55
  name, region, size, image,
@@ -79,53 +74,9 @@ class DoApi extends JsonHttpApi {
79
74
  return this._req('DELETE', `/droplets/${dropletId}`, null, { dryRunResult: { deleted: true } });
80
75
  }
81
76
 
82
- // List every droplet carrying `tag` (our fleet tag, e.g. otb-builder-box). The
83
- // input to the drift reconcile (task 1289): compare these REAL droplets against
84
- // the GDS box rows to find zombies (a droplet no live box claims) and ghosts (a
85
- // box row whose droplet vanished). per_page=200 covers the whole fleet in one
86
- // page at our scale (<100 boxes); a future larger fleet would follow
87
- // links.pages.next. Returns { droplets: [...] }; dry-run returns an empty list
88
- // (the reconcile builds its OWN live read client so planning still sees drift).
89
- async listDroplets({ tag } = {}) {
90
- const qs = tag ? `?tag_name=${encodeURIComponent(tag)}&per_page=200` : '?per_page=200';
91
- return this._req('GET', `/droplets${qs}`, null, { dryRunResult: { droplets: [] } });
92
- }
93
-
94
- // ---- snapshots (via droplet actions) --------------------------------------
95
-
96
- // Snapshot a running droplet. DO recommends powering off first for a clean
97
- // snapshot; the orchestrator powers off then calls this. Returns an ACTION
98
- // whose completion must be polled (getAction) before the droplet is destroyed.
99
- async snapshotDroplet(dropletId, snapshotName) {
100
- return this._req('POST', `/droplets/${dropletId}/actions`, { type: 'snapshot', name: snapshotName }, {
101
- dryRunResult: { action: { id: `dryrun-action-${dropletId}`, status: 'completed', type: 'snapshot' } },
102
- });
103
- }
104
-
105
- async powerOffDroplet(dropletId) {
106
- return this._req('POST', `/droplets/${dropletId}/actions`, { type: 'power_off' }, {
107
- dryRunResult: { action: { id: `dryrun-poweroff-${dropletId}`, status: 'completed' } },
108
- });
109
- }
110
-
111
- async getAction(actionId) {
112
- return this._req('GET', `/actions/${actionId}`, null, {
113
- dryRunResult: { action: { id: actionId, status: 'completed' } },
114
- });
115
- }
116
-
117
- async deleteSnapshot(snapshotId) {
118
- return this._req('DELETE', `/snapshots/${snapshotId}`, null, { dryRunResult: { deleted: true } });
119
- }
120
-
121
- async createDropletFromSnapshot({ name, region, size, snapshotId, userData, tags, sshKeys }) {
122
- return this.createDroplet({ name, region, size, image: snapshotId, userData, tags, sshKeys });
123
- }
124
-
125
- // Shared poll primitive behind waitForAction + settleDroplet (the box
126
- // subsystem's two DO poll loops, unified per the #1092 cleanup). Calls step(i)
127
- // each round; step returns { done:true, value } to stop, or a falsy value to keep
128
- // polling. Returns the stopping value, or { timedOut:true } after `tries` rounds.
77
+ // Poll primitive behind settleDroplet (#1092). Calls step(i) each round; step
78
+ // returns { done:true, value } to stop, or a falsy value to keep polling.
79
+ // Returns the stopping value, or { timedOut:true } after `tries` rounds.
129
80
  // `sleepFn` lets tests drive the loop with no real clock.
130
81
  async _poll({ tries = 60, sleepMs = 5000, sleepFn } = {}, step) {
131
82
  const sleep = sleepFn || ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -137,26 +88,8 @@ class DoApi extends JsonHttpApi {
137
88
  return { timedOut: true };
138
89
  }
139
90
 
140
- // Poll an action to completion. Returns true if 'completed', throws on 'errored'.
141
- // sleepFn lets tests drive the loop deterministically (no real sleeping).
142
- async waitForAction(actionId, { tries = 60, sleepMs = 5000, sleepFn } = {}) {
143
- if (this.dryRun) return true;
144
- const out = await this._poll({ tries, sleepMs, sleepFn }, async () => {
145
- const { action } = await this.getAction(actionId);
146
- if (action.status === 'completed') return { done: true, value: true };
147
- if (action.status === 'errored') {
148
- throw new DoApiError(`DO action ${actionId} errored`, { body: action });
149
- }
150
- return null;
151
- });
152
- if (out && out.timedOut) throw new DoApiError(`DO action ${actionId} did not complete in time`);
153
- return out;
154
- }
155
-
156
91
  // Wait until a freshly-created droplet is 'active' and return its public IPv4.
157
- // Lives here (alongside waitForAction) so the box subsystem's two DO poll loops
158
- // share one primitive; scripts/gds/box.js calls it as a method. dry-run returns
159
- // no IP (it never created a real droplet).
92
+ // dry-run returns no IP (it never created a real droplet).
160
93
  async settleDroplet(dropletId, { tries = 60, sleepMs = 5000, sleepFn } = {}) {
161
94
  if (this.dryRun) return { ip: null };
162
95
  const out = await this._poll({ tries, sleepMs, sleepFn }, async () => {
@@ -41,6 +41,8 @@ const DOC_CLI_ALLOW = new Map([
41
41
  'art-stage.js ships with the art-pipeline HOST module, which publish-manifest.js excludes from core — absent from a core-only checkout by design; the skill says so inline.'],
42
42
  ['.claude/skills/otb-tile-generate/SKILL.md::scripts/gds/art-stage.js',
43
43
  'same as paint/SKILL.md — the art staging CLI travels with the excluded art-pipeline module, not the portable core.'],
44
+ ['docs/session-logs/2026-09-10-task-1003507-the-box-that-could-not-be-reaped.md::scripts/gds/box.js',
45
+ 'a dated session log — the historical record, never rewritten — of work on a CLI the dev-box removal deleted (task 1003893); it describes what was run then, not what to run now.'],
44
46
  ]);
45
47
 
46
48
  // `node scripts/gds/foo.js` … up to the end of the line/segment, so the flags that
@@ -167,7 +167,7 @@ function checkToolchain() {
167
167
  // Check 3 — Git plumbing
168
168
  // --------------------------------------------------------------------------
169
169
  // Pure verdict for the hooks gate, extracted so the truth table is testable
170
- // (tests/doctor_hooks.mjs) — the box-auth-check `decideBoxAuth` precedent.
170
+ // (tests/doctor_hooks.mjs).
171
171
  //
172
172
  // `.husky/` is INSTANCE topology, not shipped in the portable core (README
173
173
  // "Secrets scanning"). So its absence is not a builder error: it means ADR 0043