@bongos/core 1.18.19 → 1.19.2

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 (251) hide show
  1. package/.bongos-core.json +514 -194
  2. package/.claude/skills/builder-box/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +1 -1
  4. package/.claude/skills/goal-create/SKILL.md +1 -1
  5. package/.claude/skills/idea-triage/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +5 -5
  7. package/.claude/skills/scan-before-install/SKILL.md +165 -0
  8. package/clients/bongos-client/README.md +1 -1
  9. package/clients/bongos-client/bongos-client.global.js +63 -41
  10. package/clients/bongos-client/index.cjs +63 -41
  11. package/clients/bongos-client/index.d.ts +78 -44
  12. package/clients/bongos-client/index.mjs +63 -41
  13. package/docs/adr/0020-security-model-public-stub.md +1 -1
  14. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
  15. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  16. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
  17. package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
  18. package/docs/adr/0156-user-set-display-name-wins.md +62 -0
  19. package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
  20. package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
  21. package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
  22. package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
  23. package/docs/adr/README.md +8 -0
  24. package/docs/api/openapi.json +1209 -233
  25. package/docs/api-reference.md +61 -46
  26. package/docs/architecture.md +1 -1
  27. package/docs/canonical-permissions.md +61 -16
  28. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  29. package/docs/file-map.md +4 -1
  30. package/docs/onboarding/diagrams/assertions.json +3 -3
  31. package/docs/recipes/core-release-pipeline.md +179 -0
  32. package/docs/recipes/packaging-the-core.md +2 -0
  33. package/docs/recipes/parallel-goal-waves.md +1 -3
  34. package/docs/recipes/private-npm-distribution.md +2 -0
  35. package/docs/recipes/upgrading-the-core.md +2 -0
  36. package/migrations/core_197_display_name_source.sql +56 -0
  37. package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
  38. package/modules/autonomy/routes/autonomy.js +1 -1
  39. package/modules/builder-settings/db.js +7 -1
  40. package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
  41. package/modules/dev-box/routes/box.js +11 -9
  42. package/modules/discord/auth.js +3 -1
  43. package/modules/discord/routes/discord.js +8 -2
  44. package/modules/discord/ship-broadcast.js +48 -1
  45. package/modules/governance/catalog.js +70 -20
  46. package/modules/governance/db.js +102 -0
  47. package/modules/governance/migrations/governance_006_version_create.sql +38 -0
  48. package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
  49. package/modules/governance/routes/governance.js +235 -6
  50. package/modules/grading/grader-workers/index.js +18 -9
  51. package/modules/hall-ui/public/gate.js +6 -2
  52. package/modules/hall-ui/public/goals-page.js +6 -2
  53. package/modules/hall-ui/public/governance.css +100 -0
  54. package/modules/hall-ui/public/governance.html +77 -0
  55. package/modules/hall-ui/public/governance.js +353 -0
  56. package/modules/hall-ui/public/harbor.js +14 -8
  57. package/modules/hall-ui/public/instances-panel.js +3 -1
  58. package/modules/hall-ui/public/new-project-wizard.js +6 -0
  59. package/modules/hall-ui/public/people.html +51 -0
  60. package/modules/hall-ui/public/people.js +100 -0
  61. package/modules/hall-ui/public/profile.html +2 -1
  62. package/modules/hall-ui/public/profile.js +9 -3
  63. package/modules/hall-ui/public/sessions.js +4 -2
  64. package/modules/hall-ui/public/shell.js +28 -7
  65. package/modules/hall-ui/public/task-detail.js +6 -3
  66. package/modules/hall-ui/public/watch.js +17 -9
  67. package/modules/ideas/blockers.js +44 -7
  68. package/modules/ideas/routes/blockers.js +12 -7
  69. package/modules/ideas/routes/inbox.js +2 -2
  70. package/modules/lifecycle/db.js +84 -17
  71. package/modules/lifecycle/github-push.js +37 -10
  72. package/modules/lifecycle/publish-reconciler.js +128 -31
  73. package/modules/lifecycle/routes/claims.js +10 -4
  74. package/modules/lifecycle/routes/dependencies.js +2 -2
  75. package/modules/lifecycle/routes/done-when.js +4 -4
  76. package/modules/lifecycle/routes/gate-approvals.js +9 -3
  77. package/modules/lifecycle/routes/goals.js +5 -5
  78. package/modules/lifecycle/routes/tasks.js +16 -16
  79. package/modules/lifecycle/routes/versions.js +13 -2
  80. package/modules/lifecycle/ship-card.js +38 -7
  81. package/modules/memory/routes/memory.js +2 -2
  82. package/modules/onboarding/routes/access-requests.js +3 -3
  83. package/modules/platform-identity/routes/projects.js +10 -6
  84. package/modules/platform-identity/routes/scouting.js +2 -2
  85. package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
  86. package/modules/provisioning/provisioning.js +24 -6
  87. package/modules/provisioning/routes/provisioning.js +24 -6
  88. package/modules/security/artifact-scan/aggregate.js +133 -0
  89. package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
  90. package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
  91. package/modules/security/artifact-scan/detectors/credentials.js +112 -0
  92. package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
  93. package/modules/security/artifact-scan/detectors/hooks.js +116 -0
  94. package/modules/security/artifact-scan/detectors/injection.js +118 -0
  95. package/modules/security/artifact-scan/detectors/mcp.js +102 -0
  96. package/modules/security/artifact-scan/detectors/network.js +67 -0
  97. package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
  98. package/modules/security/artifact-scan/detectors/permissions.js +91 -0
  99. package/modules/security/artifact-scan/detectors/shared.js +219 -0
  100. package/modules/security/artifact-scan/fence-local.js +99 -0
  101. package/modules/security/artifact-scan/fetch.js +330 -0
  102. package/modules/security/artifact-scan/floor.js +319 -0
  103. package/modules/security/artifact-scan/limits.js +66 -0
  104. package/modules/security/artifact-scan/manifest.js +203 -0
  105. package/modules/security/artifact-scan/mirror.js +158 -0
  106. package/modules/security/artifact-scan/quarantine.js +149 -0
  107. package/modules/security/artifact-scan/rebuild-spec.js +120 -0
  108. package/modules/security/artifact-scan/report.js +155 -0
  109. package/modules/security/artifact-scan/sanitize.js +317 -0
  110. package/modules/security/migrations/security_001_tool_scans.sql +145 -0
  111. package/modules/security/module.json +2 -2
  112. package/modules/security/routes/reports.js +11 -7
  113. package/modules/security/routes/scans.js +115 -0
  114. package/modules/security/scan-store.js +214 -0
  115. package/modules/security/tests/aggregate.mjs +283 -0
  116. package/modules/security/tests/artifact_scan.mjs +591 -0
  117. package/modules/security/tests/floor.mjs +549 -0
  118. package/modules/security/tests/mirror.mjs +350 -0
  119. package/modules/security/tests/sanitize_rebuild.mjs +499 -0
  120. package/modules/security/tests/scan_store.mjs +250 -0
  121. package/modules/security/tests/tool_scans_migration.mjs +116 -0
  122. package/modules/sessions/routes/sessions.js +6 -5
  123. package/package-lock.json +2 -2
  124. package/package.json +1 -1
  125. package/public-landing/create.html +13 -3
  126. package/scripts/gds/abandon.js +1 -1
  127. package/scripts/gds/audit-authorship.sh +6 -0
  128. package/scripts/gds/box.js +1 -1
  129. package/scripts/gds/claim.js +15 -0
  130. package/scripts/gds/cli-lib.js +16 -4
  131. package/scripts/gds/gating.js +28 -13
  132. package/scripts/gds/gen-api-docs.js +1 -1
  133. package/scripts/gds/goal.js +234 -0
  134. package/scripts/gds/init.js +70 -15
  135. package/scripts/gds/main-audit.js +80 -11
  136. package/scripts/gds/mirror-redact.js +6 -0
  137. package/scripts/gds/newcomer-floor.js +2 -2
  138. package/scripts/gds/provision.js +230 -94
  139. package/scripts/gds/run-unit-tests.js +9 -1
  140. package/scripts/gds/sandbox-stage.js +66 -9
  141. package/scripts/gds/search-ingest-md.js +1 -0
  142. package/scripts/gds/sequence.js +67 -8
  143. package/scripts/gds/ship.js +183 -50
  144. package/scripts/hall-preview/server.js +1 -1
  145. package/scripts/migrate.sh +5 -5
  146. package/src/bongos/auth.js +129 -27
  147. package/src/bongos/db-kernel.js +66 -5
  148. package/src/bongos/db.js +6 -7
  149. package/src/bongos/github-repos.js +19 -7
  150. package/src/bongos/goal-scope-check.js +1 -1
  151. package/src/bongos/module-scope-map.js +3 -1
  152. package/src/bongos/permission-path-check.js +1 -1
  153. package/src/bongos/route-rank-check.js +73 -30
  154. package/src/bongos/routes/audit-log.js +4 -2
  155. package/src/bongos/routes/auth.js +8 -4
  156. package/src/bongos/routes/backup.js +7 -4
  157. package/src/bongos/routes/builders.js +23 -11
  158. package/src/bongos/routes/github.js +15 -4
  159. package/src/bongos/routes/llm-cache.js +5 -2
  160. package/src/bongos/routes/modules.js +9 -5
  161. package/src/bongos/routes/security.js +21 -12
  162. package/src/bongos/serve-internal.js +22 -7
  163. package/src/module-api.js +86 -13
  164. package/src/platform-server.js +14 -0
  165. package/tests/api_client.mjs +18 -2
  166. package/tests/api_path_404.mjs +3 -0
  167. package/tests/art_stage.mjs +2 -1
  168. package/tests/atlas.mjs +32 -4
  169. package/tests/auth_admission_gate.mjs +17 -5
  170. package/tests/auth_error_brand.mjs +41 -21
  171. package/tests/auth_resolve_failure.mjs +155 -0
  172. package/tests/blocker_link.mjs +88 -0
  173. package/tests/bongos_downloads.mjs +2 -1
  174. package/tests/box_dns_repoint.mjs +2 -0
  175. package/tests/box_ship_permissions.mjs +2 -0
  176. package/tests/box_terminal_ssrf.mjs +23 -17
  177. package/tests/claim_error_surface.mjs +23 -0
  178. package/tests/claude_materialize.mjs +4 -0
  179. package/tests/cli_box_scope.mjs +40 -0
  180. package/tests/cloud_bongos_pack.mjs +34 -4
  181. package/tests/conductor.mjs +2 -1
  182. package/tests/criterion_authoring_gate.mjs +28 -5
  183. package/tests/currency_label.mjs +10 -1
  184. package/tests/deploy_instance.mjs +2 -0
  185. package/tests/devbox_downloads.mjs +2 -1
  186. package/tests/display_name.mjs +95 -6
  187. package/tests/eviction_policy.mjs +2 -1
  188. package/tests/file_map.mjs +10 -0
  189. package/tests/first_admin_bootstrap.mjs +10 -2
  190. package/tests/first_run_engine_check.mjs +2 -1
  191. package/tests/fitness.mjs +26 -6
  192. package/tests/gate_approvals.mjs +15 -2
  193. package/tests/github_push_land_proof.mjs +50 -4
  194. package/tests/github_repos_helpers.mjs +30 -5
  195. package/tests/github_token_store.mjs +23 -0
  196. package/tests/goal_abuse_matrix.mjs +13 -3
  197. package/tests/goal_cli.mjs +89 -0
  198. package/tests/governance_abuse_matrix.mjs +386 -0
  199. package/tests/governance_fold.mjs +156 -0
  200. package/tests/governance_fold_bench.mjs +50 -0
  201. package/tests/governance_live_proof.mjs +201 -0
  202. package/tests/governance_page_gates.mjs +21 -9
  203. package/tests/governance_parity.mjs +11 -2
  204. package/tests/governance_require_permission.mjs +125 -1
  205. package/tests/governance_routes.mjs +334 -0
  206. package/tests/grader.mjs +7 -3
  207. package/tests/hall_nav.mjs +14 -4
  208. package/tests/helpers.mjs +73 -0
  209. package/tests/hook_core_resolve.mjs +2 -1
  210. package/tests/host_topology_skips.mjs +97 -0
  211. package/tests/infra_exec_paths.mjs +14 -4
  212. package/tests/init.mjs +128 -0
  213. package/tests/known_failing_ratchet.mjs +89 -0
  214. package/tests/lifecycle-card.mjs +2 -1
  215. package/tests/main_audit.mjs +72 -1
  216. package/tests/memory_isolation.mjs +56 -9
  217. package/tests/migrate_root_resolution.mjs +1 -1
  218. package/tests/mirror_redact.mjs +19 -0
  219. package/tests/module_api.mjs +3 -1
  220. package/tests/module_loader.mjs +4 -0
  221. package/tests/modules.mjs +35 -7
  222. package/tests/modules_route.mjs +29 -5
  223. package/tests/onboarding_observability.mjs +49 -11
  224. package/tests/onboarding_route_signals.mjs +1 -0
  225. package/tests/one_active_claim_per_session.mjs +80 -0
  226. package/tests/pre_compact.mjs +2 -1
  227. package/tests/preview_server.mjs +2 -1
  228. package/tests/profile_route.mjs +1 -0
  229. package/tests/provision.mjs +170 -0
  230. package/tests/provisioning_onboard_mode.mjs +180 -0
  231. package/tests/publish_manifest.mjs +16 -3
  232. package/tests/publish_reconciler.mjs +181 -10
  233. package/tests/rank_tier_single_source.mjs +205 -0
  234. package/tests/recall_hints.mjs +2 -1
  235. package/tests/repo_map.mjs +4 -0
  236. package/tests/route_permission_check.mjs +14 -5
  237. package/tests/rubric_fixtures.mjs +3 -0
  238. package/tests/run_routine_two_root.mjs +2 -0
  239. package/tests/sandbox_stage.mjs +69 -7
  240. package/tests/search_ingest.mjs +7 -0
  241. package/tests/security_docs.mjs +28 -6
  242. package/tests/sequence.mjs +64 -0
  243. package/tests/session-cards.mjs +2 -1
  244. package/tests/session_start_freshness.mjs +2 -1
  245. package/tests/ship_broadcast_server.mjs +63 -1
  246. package/tests/ship_card.mjs +66 -0
  247. package/tests/ship_ci_deploy.mjs +66 -1
  248. package/tests/ship_smoke_gate.mjs +76 -0
  249. package/tests/versions_route.mjs +15 -0
  250. package/tests/workflow_exec_paths.mjs +16 -4
  251. package/tests/worktree_guard.mjs +2 -1
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ // Goal board CLI — list goals, or show one goal's criteria + tasks + what to claim next.
3
+ //
4
+ // Usage:
5
+ // node scripts/gds/goal.js list [--version V] [--status open|achieved|archived|all]
6
+ // node scripts/gds/goal.js show <id>
7
+ //
8
+ // Read-only. Default list filter is --status open.
9
+
10
+ const { cliClient, requireSession, arg } = require('./cli-lib');
11
+
12
+ const GOAL_GLYPH = { open: '○', achieved: '✓', archived: '·' };
13
+ const IN_FLIGHT = new Set(['active', 'completed', 'confirmed']);
14
+ const LIST_STATUSES = ['open', 'achieved', 'archived', 'all'];
15
+
16
+ function usageExit() {
17
+ console.error('usage: node scripts/gds/goal.js list [--version V] [--status open|achieved|archived|all]');
18
+ console.error(' node scripts/gds/goal.js show <id>');
19
+ process.exit(2);
20
+ }
21
+
22
+ // abandoned is a terminal state (deliberately cut), not pending work — excluded
23
+ // from every denominator, the same rule migration 017 applies to version_progress.
24
+ function liveTasks(tasks) {
25
+ return tasks.filter((t) => t.status !== 'abandoned');
26
+ }
27
+
28
+ function rollup(tasks) {
29
+ const live = liveTasks(tasks);
30
+ const shipped = live.filter((t) => t.status === 'shipped').length;
31
+ const ready = live.filter((t) => t.status === 'ready').length;
32
+ const inFlight = live.filter((t) => IN_FLIGHT.has(t.status)).length;
33
+ const parts = [`${shipped}/${live.length} shipped`];
34
+ if (inFlight) parts.push(`${inFlight} in flight`);
35
+ if (ready) parts.push(`${ready} ready`);
36
+ return parts.join(' · ');
37
+ }
38
+
39
+ async function list(api, args) {
40
+ const version = arg('--version', args) || null;
41
+ if (args.includes('--version') && (!version || !/^[\w.-]+$/.test(version))) usageExit();
42
+ const status = arg('--status', args) || 'open';
43
+ if (!LIST_STATUSES.includes(status)) usageExit();
44
+ const query = {};
45
+ if (version) query.version = version;
46
+ if (status !== 'all') query.status = status;
47
+
48
+ const g = await api.goals.getGoals({ query });
49
+ if (!g.ok) {
50
+ console.error('goals list failed:', JSON.stringify(g.data));
51
+ // process.exitCode (not process.exit): hard-exit while undici keep-alive
52
+ // sockets tear down trips a libuv assertion on Windows.
53
+ process.exitCode = 1;
54
+ return;
55
+ }
56
+ const goals = g.data.goals || [];
57
+ if (goals.length === 0) {
58
+ console.log('No goals match.');
59
+ return;
60
+ }
61
+
62
+ // One goal_id-filtered fetch per listed goal, in parallel — returns exactly
63
+ // the rows the rollups need, never a version-wide or table-wide over-fetch.
64
+ const results = await Promise.all(goals.map((goal) => api.tasks.getTasks({ query: { goal_id: goal.id } })));
65
+ const failed = results.find((r) => !r.ok);
66
+ if (failed) {
67
+ console.error('tasks fetch failed (rollups would be wrong):', JSON.stringify(failed.data));
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ const byGoal = new Map(goals.map((goal, i) => [String(goal.id), results[i].data.tasks || []]));
72
+
73
+ for (const goal of goals) {
74
+ const glyph = GOAL_GLYPH[goal.status] || '?';
75
+ const tasks = byGoal.get(String(goal.id)) || [];
76
+ const tail = liveTasks(tasks).length ? ` [${rollup(tasks)}]` : '';
77
+ console.log(`${glyph} #${goal.id} ${goal.version_id} ${goal.title}${tail}`);
78
+ }
79
+ console.log('');
80
+ console.log(`${goals.length} goal${goals.length === 1 ? '' : 's'} · node scripts/gds/goal.js show <id> for detail`);
81
+ }
82
+
83
+ function criterionGlyph(c) {
84
+ if (c.satisfied) return '✓';
85
+ if (c.pending_review) return '◇';
86
+ return '○';
87
+ }
88
+
89
+ function criterionState(c) {
90
+ if (c.satisfied) return 'satisfied';
91
+ if (c.pending_review) return 'met — pending review (/goal-review)';
92
+ const linked = Number(c.linked_tasks) || 0;
93
+ if (linked === 0) return 'no linked tasks yet';
94
+ const n = Number(c.unshipped_tasks) || 0;
95
+ return `${n} of ${linked} linked task${linked === 1 ? '' : 's'} unshipped`;
96
+ }
97
+
98
+ async function show(api, id) {
99
+ const [g, t] = await Promise.all([
100
+ api.goals.getGoalsId({ id }),
101
+ api.tasks.getTasks({ query: { goal_id: id } }),
102
+ ]);
103
+ if (!g.ok) {
104
+ console.error(`goal ${id} fetch failed:`, JSON.stringify(g.data));
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ if (!t.ok) {
109
+ console.error(`tasks fetch for goal ${id} failed:`, JSON.stringify(t.data));
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+ const { goal, members, criteria, dependencies } = g.data;
114
+ const tasks = t.data.tasks || [];
115
+
116
+ console.log(`#${goal.id} ${goal.title}`);
117
+ console.log(`${goal.version_id} · ${goal.status} · modules: ${(goal.scope_modules || []).join(', ') || '—'}`);
118
+ const allMembers = members || [];
119
+ const lead = allMembers.filter((m) => m.role === 'lead').map((m) => `@${m.github_login}`);
120
+ if (lead.length) {
121
+ console.log(`lead: ${lead.join(', ')}${allMembers.length > lead.length ? ` (+${allMembers.length - lead.length} members)` : ''}`);
122
+ } else if (allMembers.length) {
123
+ console.log(`members: ${allMembers.length} (no lead)`);
124
+ }
125
+ for (const d of dependencies || []) {
126
+ console.log(`prerequisite goal: #${d.id ?? d.goal_id} ${d.title || ''} ${d.satisfied ? '(satisfied)' : '(NOT satisfied)'}`);
127
+ }
128
+ if (goal.description) {
129
+ console.log('');
130
+ console.log(goal.description);
131
+ }
132
+
133
+ if ((criteria || []).length) {
134
+ console.log('');
135
+ console.log('Done-when criteria:');
136
+ for (const c of criteria) {
137
+ console.log(` ${criterionGlyph(c)} ${c.criterion_id} — ${criterionState(c)}`);
138
+ }
139
+ }
140
+
141
+ if (tasks.length === 0) {
142
+ console.log('');
143
+ console.log('No tasks filed under this goal yet.');
144
+ return;
145
+ }
146
+ await printTaskBuckets(api, tasks);
147
+ }
148
+
149
+ // Pure bucketing, split from printing so it unit-tests without a live API.
150
+ function bucketTasks(tasks) {
151
+ return {
152
+ shipped: tasks.filter((x) => x.status === 'shipped'),
153
+ inFlight: tasks.filter((x) => IN_FLIGHT.has(x.status)),
154
+ ready: tasks.filter((x) => x.status === 'ready'),
155
+ gated: tasks.filter((x) => x.status === 'backlog' || x.status === 'blocked'),
156
+ abandoned: tasks.filter((x) => x.status === 'abandoned'),
157
+ };
158
+ }
159
+
160
+ async function printTaskBuckets(api, tasks) {
161
+ const buckets = bucketTasks(tasks);
162
+ const live = liveTasks(tasks);
163
+
164
+ // Explain WHY each gated task waits — parallel, with per-task catch: one
165
+ // failed dependency fetch degrades to "dependencies unknown" for that task
166
+ // instead of aborting the listing.
167
+ const waits = new Map();
168
+ const waitsUnknown = new Set();
169
+ await Promise.all(buckets.gated.map(async (task) => {
170
+ try {
171
+ const d = await api.tasks.getTasksIdDependencies({ id: task.id });
172
+ if (!d.ok) { waitsUnknown.add(String(task.id)); return; }
173
+ const unshipped = (d.data.dependencies || []).filter((dep) => dep.status !== 'shipped');
174
+ if (unshipped.length) waits.set(String(task.id), unshipped.map((dep) => `task ${dep.id} (${dep.status})`).join(', '));
175
+ } catch {
176
+ waitsUnknown.add(String(task.id));
177
+ }
178
+ }));
179
+
180
+ console.log('');
181
+ const abandonedNote = buckets.abandoned.length ? `, ${buckets.abandoned.length} abandoned` : '';
182
+ console.log(`Tasks (${buckets.shipped.length}/${live.length} shipped${abandonedNote}):`);
183
+ for (const x of buckets.shipped) console.log(` ✓ ${x.id} ${x.title}`);
184
+ for (const x of buckets.inFlight) console.log(` ▶ ${x.id} ${x.title} [${x.status}]`);
185
+ for (const x of buckets.ready) console.log(` ● ${x.id} ${x.title} [ready — claimable now]`);
186
+ for (const x of buckets.gated) {
187
+ const key = String(x.id);
188
+ const why = waits.has(key) ? ` — waits on ${waits.get(key)}`
189
+ : waitsUnknown.has(key) ? ' — dependencies unknown (fetch failed)' : '';
190
+ console.log(` ○ ${x.id} ${x.title} [${x.status}${why}]`);
191
+ }
192
+ for (const x of buckets.abandoned) console.log(` ✕ ${x.id} ${x.title} [abandoned — excluded from progress]`);
193
+
194
+ console.log('');
195
+ if (buckets.ready.length) {
196
+ console.log(`Next: node scripts/gds/claim.js ${buckets.ready[0].id}`);
197
+ } else if (buckets.inFlight.length) {
198
+ console.log('Nothing claimable — work in flight must land first.');
199
+ } else if (buckets.gated.length) {
200
+ const allWait = buckets.gated.every((x) => waits.has(String(x.id)));
201
+ console.log(allWait
202
+ ? 'Nothing claimable — every remaining task waits on an unshipped dependency.'
203
+ : 'Nothing claimable — remaining tasks are backlog/blocked and not yet promoted to ready.');
204
+ } else if (live.length === 0) {
205
+ console.log('No live tasks — everything filed here was abandoned.');
206
+ } else {
207
+ console.log('All tasks shipped — run /goal-review to confirm criteria.');
208
+ }
209
+ }
210
+
211
+ async function main() {
212
+ await requireSession();
213
+ const args = process.argv.slice(2);
214
+ const cmd = args[0];
215
+ const api = await cliClient();
216
+
217
+ if (cmd === 'list') {
218
+ await list(api, args.slice(1));
219
+ } else if (cmd === 'show') {
220
+ if (!/^\d+$/.test(args[1] || '')) usageExit();
221
+ await show(api, Number(args[1]));
222
+ } else {
223
+ usageExit();
224
+ }
225
+ }
226
+
227
+ if (require.main === module) {
228
+ main().catch((err) => {
229
+ console.error('goal.js failed:', err && err.message ? err.message : err);
230
+ process.exitCode = 1;
231
+ });
232
+ }
233
+
234
+ module.exports = { rollup, criterionGlyph, criterionState, bucketTasks, liveTasks };
@@ -364,7 +364,11 @@ function kickoffTasks(spec) {
364
364
  if (!provisioningRequest(spec)) tasks.push(productionTopologyKickoff());
365
365
  tasks.push({
366
366
  title: 'Fill the instance look with your own LLM',
367
- kind: 'ideator',
367
+ // 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
368
+ // kind and the task silently vanished from the checklist (the one non-infra
369
+ // kickoff step). Deciding the instance look is a decision by an ideator.
370
+ kind: 'decision',
371
+ discipline: 'ideator',
368
372
  description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
369
373
  'own LLM, not a generator). Hand docs/branding-fill-prompt.md to your AI to design a palette/fonts/wordmark, or keep the vanilla look.',
370
374
  });
@@ -451,6 +455,9 @@ function versionPayload(spec) {
451
455
  id: v.id,
452
456
  name: v.name,
453
457
  status: v.status || 'building',
458
+ // versions.track is NOT NULL (no DB default) — always send it explicitly.
459
+ // An instance's first version is its product track unless the spec says otherwise.
460
+ track: v.track === 'internal' ? 'internal' : 'product',
454
461
  done_when: v.done_when,
455
462
  criteria: [{ criterion_id: v.criterion_id ? slugify(v.criterion_id) : 'first-goal', criterion_md: v.done_when }],
456
463
  };
@@ -728,12 +735,19 @@ async function seedViaApi(spec, { dryRun, log = console.log, api = apiCall } = {
728
735
  else log(` ✓ created version ${vp.id} + 1 done-when goal`);
729
736
  const criterionId = vp.criteria[0].criterion_id;
730
737
 
731
- // A tracked kickoff/starter task (status ready, engineer discipline). Returns true on success.
738
+ // A tracked kickoff/starter task (status ready). Returns true on success.
739
+ // The whole point of filing these is "nothing is silently skipped" — so a
740
+ // rejected POST must be LOUD: every failure is collected with its reason and
741
+ // reported against the expected count, never absorbed into a smaller total.
742
+ let attempted = 0;
743
+ const failures = [];
732
744
  const postTask = async (t, extra = {}) => {
745
+ attempted++;
733
746
  const res = await api('POST', '/api/gds/tasks', {
734
747
  version_id: vp.id, title: t.title, description: t.description || '', status: 'ready',
735
- kind: t.kind || 'infra', discipline: 'engineer', source: 'bongos-init', ...extra,
748
+ kind: t.kind || 'infra', discipline: t.discipline || 'engineer', source: 'bongos-init', ...extra,
736
749
  });
750
+ if (!res.ok) failures.push({ title: t.title, status: res.status, detail: JSON.stringify(res.data) });
737
751
  return res.ok;
738
752
  };
739
753
 
@@ -771,8 +785,12 @@ async function seedViaApi(spec, { dryRun, log = console.log, api = apiCall } = {
771
785
  }
772
786
 
773
787
  const kickoffLabel = provisioned ? `${kickoff.length} kickoff + provisioning request` : `${kickoff.length} kickoff`;
774
- log(` ✓ filed ${created} task(s) (${starters.length} starter + ${kickoffLabel})`);
775
- return { seeded: true, created, provisioned };
788
+ if (failures.length) {
789
+ log(` ✗ ${failures.length} of ${attempted} task(s) FAILED to file — the checklist below is INCOMPLETE:`);
790
+ for (const f of failures) log(` ✗ "${f.title}" (${f.status}): ${f.detail}`);
791
+ }
792
+ log(` ${failures.length ? '⚠' : '✓'} filed ${created} of ${attempted} task(s) (${starters.length} starter + ${kickoffLabel})`);
793
+ return { seeded: true, created, attempted, failures, provisioned };
776
794
  }
777
795
 
778
796
  // ---------------------------------------------------------------------------
@@ -795,7 +813,12 @@ function confirmProceed({ input = process.stdin, output = process.stdout } = {})
795
813
  // seeds the repo's REAL version/goal + IMPORTS its existing backlog (task 2044), NOT a
796
814
  // synthetic "V1 — Prove the loop". `--dry-run` previews everything and writes nothing;
797
815
  // `--no-seed` layers without seeding. Returns a process exit code.
798
- async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, log = console.log }) {
816
+ // `coreDep`/`vendorPlan` (task 1002562): adopt used to ALWAYS write the caret range,
817
+ // which resolves NOWHERE on npm — so an adopted repo could not `npm ci --omit=dev`, the
818
+ // exact command the provisioner runs on the box. Greenfield already had --vendor-core for
819
+ // this; passing the same resolved pin through here makes an adopted repo equally
820
+ // installable. Omitted (a plain `bongos init --adopt` on a laptop) → the caret, unchanged.
821
+ async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, coreDep = null, vendorPlan = null, log = console.log }) {
799
822
  log(`\nAdopting Cloud Bongos onto the existing repo at "${dir}"${dryRun ? ' (dry run — nothing will be written or created)' : ''}:`);
800
823
  log(' Layering — pre-existing files are left untouched; nothing is overwritten.');
801
824
 
@@ -827,7 +850,30 @@ async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, log
827
850
  }
828
851
 
829
852
  const cfg = layerConfigs(spec, { dir, dryRun, log });
830
- const pkg = layerPackageJson(spec, { dir, dryRun, log });
853
+ const pkg = layerPackageJson(spec, { dir, dryRun, log, ...(coreDep ? { coreRange: coreDep } : {}) });
854
+
855
+ // Vendor the pinned core tarball + generate the lockfile, mirroring the greenfield
856
+ // --vendor-core steps, so the adopted repo is `npm ci --omit=dev`-installable (ADR 0108).
857
+ // Skipped when layerPackageJson found the dep ALREADY present — re-pinning someone's
858
+ // existing declaration is an overwrite, and adopt never overwrites.
859
+ if (vendorPlan && pkg.action !== 'present' && pkg.action !== 'unparseable') {
860
+ const upgrade = require('./upgrade');
861
+ if (dryRun) {
862
+ log(`\n [dry-run] would vendor ${vendorPlan.tgz} → ${vendorPlan.vendorRel} and run \`npm install\` to generate package-lock.json`);
863
+ } else {
864
+ log('\nVendoring the pinned @bongos/core + generating the lockfile:');
865
+ upgrade.vendorTarball({ instanceDir: dir, fromTgz: vendorPlan.tgz, targetVersion: vendorPlan.version });
866
+ log(` ✓ vendored ${vendorPlan.vendorRel}`);
867
+ const inst = upgrade.npmInstall(dir);
868
+ if (!inst.ok) {
869
+ log(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — package-lock.json not generated; run \`npm install\` in ${dir} by hand before provisioning.`);
870
+ } else {
871
+ log(' ✓ npm install → package-lock.json generated (node_modules/@bongos/core installed)');
872
+ }
873
+ }
874
+ } else if (vendorPlan && pkg.action === 'present') {
875
+ log(` • ${CORE_PKG} was already declared — left as-is; the vendored tarball was NOT pinned over it.`);
876
+ }
831
877
 
832
878
  log('\nMaterializing the .claude/ methodology surface (additive — existing files left untouched):');
833
879
  const claude = materializeClaude({ coreRoot: resolveCoreRoot(), instanceDir: dir, dryRun, overwrite: false, log });
@@ -846,7 +892,10 @@ async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, log
846
892
  ? `${backlog.tasks.length} imported task(s) from ${backlog.source}${backlog.truncated ? ` (capped at ${BACKLOG_MAX})` : ''}`
847
893
  : 'no backlog found (capture tasks later)';
848
894
  log(`\nReflecting real state — seeding version "${spec.version && spec.version.id}" + goal + ${importNote}:`);
849
- await seedViaApi(spec, { dryRun, log });
895
+ const seedResult = (await seedViaApi(spec, { dryRun, log })) || {};
896
+ if (Array.isArray(seedResult.failures) && seedResult.failures.length) {
897
+ log(`\n ⚠ ${seedResult.failures.length} seed task(s) failed to file (details above) — re-file them by hand or re-run the seed; the tracked checklist is incomplete until they exist.`);
898
+ }
850
899
  } else {
851
900
  log('\n (--no-seed: layered config only; skipped real-state seeding)');
852
901
  }
@@ -907,17 +956,16 @@ async function main(argv) {
907
956
  return 1;
908
957
  }
909
958
 
910
- // Brownfield (ADR 0121): a distinct LAYER + real-state path — never the greenfield
911
- // scaffold/overwrite below. Its own report + next-steps live in runAdopt.
912
- if (adopt) return await runAdopt(spec, { dir, dryRun, noSeed, accept });
913
-
914
- // Greenfield core-dep resolution (ADR 0108 gap 1/4, task 2053). With --vendor-core, pin
959
+ // Core-dep resolution (ADR 0108 gap 1/4, task 2053). With --vendor-core, pin
915
960
  // package.json to the vendored `file:` tarball (the caret range resolves NOWHERE on npm,
916
961
  // so a caret-only scaffold can't `npm ci`); the tarball is copied + the lockfile generated
917
- // just below. Without it, the scaffold carries the declared caret (a local/dev scaffold the
962
+ // below. Without it, the scaffold carries the declared caret (a local/dev scaffold the
918
963
  // owner resolves another way). Reuses the bongos-upgrade vendoring + install steps.
964
+ //
965
+ // Resolved BEFORE the adopt branch (task 1002562) because BOTH paths need it: an adopted
966
+ // repo has to be `npm ci`-installable by the provisioner exactly like a scaffolded one.
919
967
  const upgrade = require('./upgrade');
920
- let coreDep = null; // null → writeConfigs writes the caret range
968
+ let coreDep = null; // null → writeConfigs/layerPackageJson write the caret range
921
969
  let vendorPlan = null;
922
970
  if (vendorCore) {
923
971
  const tgz = path.resolve(vendorCore);
@@ -935,6 +983,10 @@ async function main(argv) {
935
983
  vendorPlan = { tgz, version, vendorRel };
936
984
  }
937
985
 
986
+ // Brownfield (ADR 0121): a distinct LAYER + real-state path — never the greenfield
987
+ // scaffold/overwrite below. Its own report + next-steps live in runAdopt.
988
+ if (adopt) return await runAdopt(spec, { dir, dryRun, noSeed, accept, coreDep, vendorPlan });
989
+
938
990
  console.log(`\nScaffolding "${spec.identity.productName}"${dryRun ? ' (dry run — nothing will be written or created)' : ''}:`);
939
991
  writeConfigs(spec, { dir, force, dryRun, coreDep });
940
992
 
@@ -1024,6 +1076,9 @@ async function main(argv) {
1024
1076
  else console.log(' (--no-seed: skipped version/task seeding)');
1025
1077
 
1026
1078
  console.log(`\n${dryRun ? 'Dry run complete.' : 'Done.'} Next:`);
1079
+ if (Array.isArray(seedResult.failures) && seedResult.failures.length) {
1080
+ console.log(` ⚠ ${seedResult.failures.length} seed task(s) failed to file (details above) — re-file them by hand or re-run the seed; the tracked checklist is incomplete until they exist.`);
1081
+ }
1027
1082
  console.log(' • fill the look: hand docs/branding-fill-prompt.md to your own LLM');
1028
1083
  console.log(` • sign in as ${spec.firstAdmin} to become the owner, then run \`bongos start\` to see the kickoff tasks`);
1029
1084
  console.log(' • work through the kickoff tasks the way you would any task: claim → ship');
@@ -34,12 +34,13 @@
34
34
  // node scripts/gds/main-audit.js --revert # also print `git revert` plan
35
35
  // node scripts/gds/main-audit.js --strict # also block on unresolved authors
36
36
  // node scripts/gds/main-audit.js --db # resolve ranks from Postgres instead of the API
37
+ // node scripts/gds/main-audit.js --require-roster # CI: an empty roster is a loud DID-NOT-RUN, not a vacuous green
37
38
 
38
39
  'use strict';
39
40
 
40
41
  const { execSync } = require('node:child_process');
41
42
  const ppc = require(require('node:path').join(__dirname, '..', '..', 'src', 'bongos', 'permission-path-check.js'));
42
- const { cliClient } = require('./cli-lib');
43
+ const { cliClient, API_BASE } = require('./cli-lib');
43
44
 
44
45
  // ---------- pure analyzer (tested in tests/main_audit.mjs) ----------
45
46
 
@@ -87,7 +88,7 @@ function analyzeCommits(commits, rankByIdentity, { strict = false } = {}) {
87
88
  author: c.authorName,
88
89
  rank,
89
90
  files: hits.map((h) => h.file),
90
- summary: `commit ${short(c.sha)} by '${c.authorName}' (rank '${rank}', below Metic) modified permission-sensitive path(s) [${fileList}] — this is the SEC #863 violation class. It must be reverted and re-landed by an Archon if genuinely needed.`,
91
+ summary: `commit ${short(c.sha)} by '${c.authorName}' (rank '${rank}', below Metic) modified permission-sensitive path(s) [${fileList}] — this is the SEC #863 violation class. It must be reverted and re-landed by a Metic+ builder if genuinely needed.`,
91
92
  });
92
93
  }
93
94
  }
@@ -120,6 +121,34 @@ function git(args) {
120
121
  return execSync(`git ${args}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
121
122
  }
122
123
 
124
+ // How far back the default scan reaches, in first-parent steps.
125
+ const DEFAULT_WINDOW = 200;
126
+
127
+ // First-parent depth of a ref. `<ref>~N` walks FIRST parents N times, so this —
128
+ // not the total commit count — is what bounds the window. On a repo where every
129
+ // land is a --no-ff merge the two diverge sharply: this one has 526 commits
130
+ // reachable from origin/main but a first-parent chain only 186 deep.
131
+ function firstParentDepth(ref) {
132
+ try {
133
+ return Number(git(`rev-list --count --first-parent ${ref}`).trim());
134
+ } catch (_) {
135
+ return NaN;
136
+ }
137
+ }
138
+
139
+ // Clamp the window to what the history actually holds. Returns null when there
140
+ // is nothing to audit, so the caller reports that plainly instead of emitting a
141
+ // `<ref>~200..<ref>` that git rejects as an unknown revision (task 1002566).
142
+ function clampWindow(depth, want = DEFAULT_WINDOW) {
143
+ if (typeof depth !== 'number' || !Number.isFinite(depth) || depth < 2) return null;
144
+ return Math.min(want, depth - 1);
145
+ }
146
+
147
+ function defaultRange(ref, depth, want = DEFAULT_WINDOW) {
148
+ const n = clampWindow(depth, want);
149
+ return n === null ? null : `${ref}~${n}..${ref}`;
150
+ }
151
+
123
152
  // Parse `git log --name-only` with a NUL-delimited custom format into commits.
124
153
  function parseCommits(range) {
125
154
  // %x00 = NUL. Format: NUL sha NUL parents NUL authorName NUL authorEmail NUL subject \n files...
@@ -128,7 +157,9 @@ function parseCommits(range) {
128
157
  try {
129
158
  out = git(`log ${range} --name-only --format=${JSON.stringify(fmt)}`);
130
159
  } catch (e) {
131
- return { error: `git log failed for range ${range}: ${e.message}` };
160
+ // Say plainly that nothing was audited. A bare git error here used to read
161
+ // as a transient glitch, which is how a dead check passed for a clean one.
162
+ return { error: `DID NOT RUN — git log could not resolve range ${range}, so no commits were audited: ${e.message}` };
132
163
  }
133
164
  const records = out.split('\x00').slice(1); // first chunk is empty / pre-first-NUL
134
165
  const commits = [];
@@ -194,8 +225,25 @@ async function fetchRankMap({ useDb = false } = {}) {
194
225
  try {
195
226
  const api = await cliClient({ allowUnauthenticated: true });
196
227
  const res = await api.builders.getBuildersRoster();
197
- if (res && res.ok) return rowsToMap(res.data?.builders || []);
198
- } catch (_) { /* offline / not archon — map stays empty → unresolved authors */ }
228
+ if (res && res.ok) {
229
+ const map = rowsToMap(res.data?.builders || []);
230
+ if (Object.keys(map).length) return map;
231
+ }
232
+ } catch (_) { /* offline / no session — fall through to the public surface */ }
233
+ // Public fallback (task 1002612): CI has no GDS session, and with an empty
234
+ // rank map every author resolves to null so a blocker can NEVER fire — a
235
+ // vacuous green. github_login + rank are public facts on this platform (the
236
+ // status dashboard serves them), and the public leaderboard is server-
237
+ // authoritative, so resolving from it is as trustworthy as the roster route.
238
+ // Caveat: a zero-activity builder may be absent → resolves as UNRESOLVED
239
+ // ('review' finding), never as falsely-trusted.
240
+ try {
241
+ const res = await fetch(`${API_BASE}/api/gds/public/leaderboard`);
242
+ if (res.ok) {
243
+ const data = await res.json();
244
+ return rowsToMap(data?.leaderboard || []);
245
+ }
246
+ } catch (_) { /* offline — map stays empty → unresolved authors */ }
199
247
  return {};
200
248
  }
201
249
 
@@ -221,12 +269,22 @@ async function main() {
221
269
  range = process.env.OTB_AUDIT_RANGE;
222
270
  }
223
271
  if (!range || range === true) {
224
- if (verifyHead) {
225
- range = 'HEAD~200..HEAD';
226
- } else {
272
+ const ref = verifyHead ? 'HEAD' : 'origin/main';
273
+ if (!verifyHead) {
227
274
  // Default: recent origin/main. Fetch first so we're auditing the truth.
228
275
  try { git('fetch origin main'); } catch (_) { /* offline — audit local */ }
229
- range = 'origin/main~200..origin/main';
276
+ }
277
+ // Clamp to the ref's real first-parent depth. A fixed ~200 silently made
278
+ // this audit unrunnable on any repo with a shorter chain (task 1002566).
279
+ const depth = firstParentDepth(ref);
280
+ range = defaultRange(ref, depth);
281
+ if (!range) {
282
+ console.error(
283
+ `main-audit: DID NOT RUN — cannot build a default range for ${ref}: its first-parent history is `
284
+ + `${Number.isFinite(depth) ? `${depth} commit(s)` : 'unreadable'} deep. `
285
+ + 'Pass --range <A..B> or set OTB_AUDIT_RANGE. This is a check that did not execute, not a clean audit.'
286
+ );
287
+ process.exit(2);
230
288
  }
231
289
  }
232
290
 
@@ -237,7 +295,15 @@ async function main() {
237
295
  }
238
296
  const rankMap = await fetchRankMap({ useDb });
239
297
  if (Object.keys(rankMap).length === 0) {
240
- console.error('main-audit: WARNING — could not load the GDS roster (offline / not Archon / no DB). Authors will be unresolved; run as an Archon or with --db (on the droplet) for a definitive audit.');
298
+ // --require-roster (task 1002612): the CI caller refuses to "pass" an audit
299
+ // that could not have failed. With no ranks, every author is unresolved and
300
+ // a blocker can never fire — that is a check that did not execute, not a
301
+ // clean audit (the same false-green shape parseCommits guards against).
302
+ if (process.argv.includes('--require-roster')) {
303
+ console.error('main-audit: DID NOT RUN — the roster is unavailable (offline / no session / public leaderboard unreachable). An empty rank map can never produce a blocker, so exiting green would be a vacuous pass. Fix connectivity or drop --require-roster for a best-effort local read.');
304
+ process.exit(2);
305
+ }
306
+ console.error('main-audit: WARNING — could not load the GDS roster (offline / insufficient rank / no DB). Authors will be unresolved; run as a Metic+ builder or with --db (on the droplet) for a definitive audit.');
241
307
  }
242
308
 
243
309
  const result = analyzeCommits(parsed.commits, rankMap, { strict });
@@ -276,4 +342,7 @@ if (require.main === module) {
276
342
  });
277
343
  }
278
344
 
279
- module.exports = { analyzeCommits, resolveRank, parseCommits };
345
+ module.exports = {
346
+ analyzeCommits, resolveRank, parseCommits, rowsToMap,
347
+ clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW,
348
+ };
@@ -108,6 +108,12 @@ function isPublicRoutableIPv6(s) {
108
108
  if (!g) return false;
109
109
  if (g.slice(0, 7).every((x) => x === 0)) return false; // :: (unspecified) and ::1 (loopback)
110
110
  const h = g[0];
111
+ // ::/8 is IETF-RESERVED (RFC 4291 §2.4) — no host is reachable there, so an address
112
+ // in it is never a real leak. Excluding it is what stops degenerate shorthand like
113
+ // `5::` / `c9::` from matching: those are 2-3 chars that random BINARY bytes hit
114
+ // constantly, which flagged 7 phantom "public-ip" leaks across committed .png/.webp
115
+ // assets and left the C9 full-tree mirror gate permanently red (task 1002470).
116
+ if (h < 0x0100) return false; // reserved ::/8
111
117
  if (h >= 0xfe80 && h <= 0xfebf) return false; // link-local fe80::/10
112
118
  if (h >= 0xfc00 && h <= 0xfdff) return false; // ULA fc00::/7
113
119
  if (h >= 0xff00) return false; // multicast ff00::/8
@@ -377,7 +377,7 @@ function report(summary, FLAGS, { checkOnly }) {
377
377
  if (checkOnly) { console.log('(--check: no stamping)'); return; }
378
378
  if (summary.noop) { console.log('All chores at or above floor — nothing to stamp.'); return; }
379
379
 
380
- console.log(`\nReview gate: ${summary.autoPromote ? "AUTO-PROMOTE (copies land status='ready', live now)" : "REVIEW (copies land status='backlog'; an Archon promotes to 'ready')"}${FLAGS.dryRun ? ' [DRY RUN — nothing created]' : ''}`);
380
+ console.log(`\nReview gate: ${summary.autoPromote ? "AUTO-PROMOTE (copies land status='ready', live now)" : "REVIEW (copies land status='backlog'; a Metic+ builder promotes to 'ready')"}${FLAGS.dryRun ? ' [DRY RUN — nothing created]' : ''}`);
381
381
  for (const g of summary.stamped) {
382
382
  const who = `${g.discipline}/${g.template_id}`;
383
383
  if (g.error) console.log(` ✗ ${who}: ${g.error}`);
@@ -386,7 +386,7 @@ function report(summary, FLAGS, { checkOnly }) {
386
386
  else console.log(` ✓ ${who}: task #${g.task_id} [${g.status}] "${g.title}" (ref ${g.source_ref})`);
387
387
  }
388
388
  if (!summary.autoPromote && !FLAGS.dryRun && summary.stamped.some((g) => g.task_id)) {
389
- console.log('\nNext: an Archon promotes the new backlog copies to ready:');
389
+ console.log('\nNext: a Metic+ builder promotes the new backlog copies to ready:');
390
390
  console.log(' node scripts/gds/api.js POST /api/gds/tasks/<id>/promote (or the builders\' hall)');
391
391
  }
392
392
  }