@bongos/core 1.17.0 → 1.17.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 (230) hide show
  1. package/.bongos-core.json +363 -312
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/CLAUDE.md +11 -238
  15. package/GOVERNANCE.md +1 -1
  16. package/README.md +1 -1
  17. package/bin/bongos.js +1 -0
  18. package/clients/bongos-client/README.md +4 -4
  19. package/clients/bongos-client/bongos-client.global.js +14 -1
  20. package/clients/bongos-client/index.cjs +14 -1
  21. package/clients/bongos-client/index.d.ts +19 -2
  22. package/clients/bongos-client/index.mjs +11 -1
  23. package/clients/bongos-client/package.json +1 -1
  24. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  25. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  26. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  27. package/docs/adr/0020-security-model-public-stub.md +1 -1
  28. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  29. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  30. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  31. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  32. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  33. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  34. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  35. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  36. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  37. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  38. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  39. package/docs/adr/0057-container-cost-ledger.md +3 -3
  40. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  41. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  42. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  43. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  44. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  45. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  46. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  47. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  48. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  49. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  50. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  51. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  52. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  53. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  54. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  55. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  56. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  57. package/docs/adr/README.md +14 -10
  58. package/docs/api/openapi.json +315 -6
  59. package/docs/api-reference.md +10 -5
  60. package/docs/architecture.md +3 -3
  61. package/docs/branding-contract.md +5 -4
  62. package/docs/canonical-permissions.md +1 -1
  63. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  64. package/docs/file-map.md +20 -19
  65. package/docs/handoff-template.md +2 -2
  66. package/docs/modules-contract.md +8 -1
  67. package/docs/onboarding/diagrams/README.md +7 -7
  68. package/docs/onboarding/drachmae-and-karma.md +18 -18
  69. package/docs/onboarding/primer.md +9 -9
  70. package/docs/recipes/audit-replay.md +2 -2
  71. package/docs/recipes/builder-box-lifecycle.md +2 -2
  72. package/docs/recipes/claude-design-loop.md +1 -1
  73. package/docs/recipes/instance-deploy.md +4 -4
  74. package/docs/recipes/instance-first-run.md +51 -0
  75. package/docs/recipes/ops-gotchas.md +1 -1
  76. package/docs/recipes/packaging-the-core.md +5 -5
  77. package/docs/recipes/private-npm-distribution.md +39 -12
  78. package/docs/recipes/self-host.md +1 -1
  79. package/docs/recipes/staging-environment.md +1 -1
  80. package/docs/recipes/upgrading-the-core.md +63 -8
  81. package/docs/research/cursor-competitive-analysis.md +1 -1
  82. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  83. package/migrations/core_191_module_submissions.sql +45 -0
  84. package/modules/autonomy/CLAUDE.md +11 -45
  85. package/modules/builder-settings/CLAUDE.md +11 -54
  86. package/modules/dev-box/CLAUDE.md +11 -34
  87. package/modules/discord/CLAUDE.md +11 -46
  88. package/modules/discord/module.json +2 -1
  89. package/modules/economy/CLAUDE.md +11 -44
  90. package/modules/economy/achievements.js +9 -1
  91. package/modules/economy/credits.js +10 -1
  92. package/modules/grading/CLAUDE.md +11 -38
  93. package/modules/hall-ui/CLAUDE.md +11 -57
  94. package/modules/hall-ui/public/builders.js +71 -4
  95. package/modules/hall-ui/public/goal-inbox.js +191 -0
  96. package/modules/hall-ui/public/goals-page.js +227 -22
  97. package/modules/hall-ui/public/goals.html +16 -0
  98. package/modules/hall-ui/public/hall-widgets.js +14 -1
  99. package/modules/hall-ui/public/index.html +3 -0
  100. package/modules/hall-ui/public/new-project-wizard.js +6 -4
  101. package/modules/hall-ui/public/style.css +43 -0
  102. package/modules/ideas/CLAUDE.md +11 -33
  103. package/modules/lifecycle/CLAUDE.md +11 -59
  104. package/modules/lifecycle/routes/goals.js +80 -0
  105. package/modules/memory/CLAUDE.md +11 -36
  106. package/modules/onboarding/CLAUDE.md +11 -51
  107. package/modules/provisioning/CLAUDE.md +15 -0
  108. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  109. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  110. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  111. package/modules/provisioning/module.json +17 -0
  112. package/modules/provisioning/onboard-plan.js +37 -0
  113. package/modules/provisioning/provisioning.js +539 -0
  114. package/modules/provisioning/rate-limit.js +35 -0
  115. package/modules/provisioning/routes/provisioning.js +607 -0
  116. package/modules/provisioning/tests/provisioning.mjs +232 -0
  117. package/modules/provisioning/ui/.gitkeep +0 -0
  118. package/modules/security/CLAUDE.md +11 -40
  119. package/modules/security/secret-scrub.js +10 -0
  120. package/modules/sessions/CLAUDE.md +11 -35
  121. package/modules/status-ui/CLAUDE.md +11 -43
  122. package/package-lock.json +2 -2
  123. package/package.json +7 -2
  124. package/public-landing/create.html +58 -12
  125. package/public-landing/index.html +40 -21
  126. package/scripts/discord/README.md +1 -1
  127. package/scripts/gds/CLAUDE.md +11 -140
  128. package/scripts/gds/claim.js +98 -48
  129. package/scripts/gds/claude-materialize.js +174 -20
  130. package/scripts/gds/claude-md-template.js +84 -0
  131. package/scripts/gds/cli-lib.js +112 -2
  132. package/scripts/gds/cutover-codemod.js +241 -0
  133. package/scripts/gds/fitness.js +54 -0
  134. package/scripts/gds/gen-api-client.js +10 -10
  135. package/scripts/gds/init.js +112 -16
  136. package/scripts/gds/lint-infra-exec.js +70 -0
  137. package/scripts/gds/lint-workflow-exec.js +63 -0
  138. package/scripts/gds/migration-namespace.js +1 -1
  139. package/scripts/gds/mirror-lineage-check.js +14 -5
  140. package/scripts/gds/mirror-redact.js +8 -0
  141. package/scripts/gds/module.js +118 -4
  142. package/scripts/gds/npmrc.js +93 -0
  143. package/scripts/gds/onboard.js +17 -24
  144. package/scripts/gds/package-core.js +74 -17
  145. package/scripts/gds/provision.js +102 -17
  146. package/scripts/gds/publish-manifest.js +60 -2
  147. package/scripts/gds/run-routine.js +51 -13
  148. package/scripts/gds/run-unit-tests.js +4 -0
  149. package/scripts/gds/seed-adopt-tasks.js +1 -1
  150. package/scripts/gds/status.js +6 -1
  151. package/scripts/gds/task.js +166 -0
  152. package/scripts/gds/update-channel.js +186 -0
  153. package/scripts/gds/upgrade.js +172 -25
  154. package/src/bongos/CLAUDE.md +11 -60
  155. package/src/bongos/auth.js +24 -0
  156. package/src/bongos/module-submissions.js +56 -0
  157. package/src/bongos/route-rank-check.js +6 -0
  158. package/src/bongos/routes/CLAUDE.md +11 -39
  159. package/src/bongos/routes/auth.js +6 -1
  160. package/src/bongos/routes/modules.js +123 -21
  161. package/src/bongos/serve-internal.js +10 -1
  162. package/src/branding.js +1 -0
  163. package/src/instance-config.js +1 -1
  164. package/src/module-api.js +24 -1
  165. package/src/module-loader/loader.js +36 -4
  166. package/src/module-loader/manifest-schema.js +16 -1
  167. package/tests/branding.mjs +11 -0
  168. package/tests/claim_error_surface.mjs +107 -0
  169. package/tests/claude_materialize.mjs +125 -0
  170. package/tests/consumer_layout_boot.mjs +302 -0
  171. package/tests/cutover_codemod.mjs +184 -0
  172. package/tests/device_flow_start.mjs +96 -0
  173. package/tests/first_run_engine_check.mjs +79 -0
  174. package/tests/goal_membership_requests.mjs +33 -3
  175. package/tests/hook_core_resolve.mjs +125 -0
  176. package/tests/hook_user_agents.mjs +62 -0
  177. package/tests/infra_exec_paths.mjs +104 -0
  178. package/tests/init.mjs +106 -6
  179. package/tests/instance_mismatch_guard.mjs +101 -0
  180. package/tests/mirror_lineage_proof.mjs +14 -0
  181. package/tests/module_cli.mjs +130 -2
  182. package/tests/module_loader.mjs +42 -0
  183. package/tests/module_manifest.mjs +19 -0
  184. package/tests/module_submissions.mjs +135 -0
  185. package/tests/npmrc.mjs +105 -0
  186. package/tests/onboard.mjs +62 -0
  187. package/tests/package_core.mjs +76 -22
  188. package/tests/platform_boot.mjs +5 -2
  189. package/tests/provision.mjs +107 -8
  190. package/tests/provisioning_github_app_routes.mjs +44 -2
  191. package/tests/publish_manifest.mjs +52 -1
  192. package/tests/run_routine.mjs +26 -1
  193. package/tests/run_routine_two_root.mjs +77 -0
  194. package/tests/scrubber_corpus.mjs +17 -0
  195. package/tests/served_paths_shipped.mjs +2 -2
  196. package/tests/task_cli.mjs +99 -0
  197. package/tests/update_channel.mjs +186 -0
  198. package/tests/upgrade.mjs +220 -25
  199. package/tests/workflow_exec_paths.mjs +98 -0
  200. package/docs/design/container-exterior-tiles.md +0 -49
  201. package/docs/design/container-exterior.md +0 -81
  202. package/docs/design/container-interior.md +0 -77
  203. package/docs/design/world-map-scroll.md +0 -41
  204. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  205. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  206. package/docs/recipes/cloudbongos-standup.md +0 -111
  207. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  208. package/docs/recipes/parallel-load-test.md +0 -32
  209. package/docs/recipes/pixel-art-pipeline.md +0 -411
  210. package/docs/recipes/staging-data.md +0 -50
  211. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  212. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  213. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  214. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  215. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  216. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  217. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  218. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  219. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  220. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  221. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  222. package/scripts/gds/art-stage.js +0 -152
  223. package/server.js +0 -212
  224. package/tests/art_key.mjs +0 -121
  225. package/tests/day_cycle.mjs +0 -197
  226. package/tests/snapshot_picker.mjs +0 -233
  227. package/tests/sprite_facing.mjs +0 -35
  228. package/tests/terrain_picker.mjs +0 -298
  229. package/tests/turn_in_place.mjs +0 -280
  230. package/tests/walkability.mjs +0 -188
@@ -23,9 +23,20 @@
23
23
  const fs = require('fs');
24
24
  const path = require('path');
25
25
  const { execFileSync } = require('child_process');
26
-
27
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
28
- const TASKS_DIR = path.join(REPO_ROOT, '.claude', 'scheduled-tasks');
26
+ const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-config');
27
+
28
+ // TWO roots (ADR 0108 configurable-root; task 2138). run-routine.js is a CORE script, so it
29
+ // ships alongside src/, scripts/gds/*, scripts/discord/*, and modules/ — all under
30
+ // resolveCoreRoot(), which is where core step scripts + the grader resolve from. The ROUTINES
31
+ // themselves are TOPOLOGY: `bongos init/upgrade` materializes .claude/scheduled-tasks/ INTO the
32
+ // instance (claude-materialize.js), so they're found under resolveInstanceRoot(). Today both
33
+ // roots are the one checkout — BYTE-IDENTICAL to the old `path.resolve(__dirname,'..','..')`;
34
+ // post-split TASKS_DIR points at the instance repo while core scripts resolve inside
35
+ // node_modules/@bongos/core. (Do NOT reintroduce a hardcoded __dirname/../.. — see scripts/gds/CLAUDE.md.)
36
+ const CORE_ROOT = resolveCoreRoot();
37
+ const INSTANCE_ROOT = resolveInstanceRoot();
38
+ const TASKS_DIR = path.join(INSTANCE_ROOT, '.claude', 'scheduled-tasks');
39
+ const DISCORD_POST = path.join(CORE_ROOT, 'scripts', 'discord', 'post.js');
29
40
  const VALID_MODES = new Set(['deterministic', 'hybrid', 'judgment']);
30
41
  const VALID_SINKS = new Set(['discord', 'gds', 'none']);
31
42
  const ALERT_CHANNEL = process.env.OTB_ROUTINE_ALERT_CHANNEL || 'ship_feed';
@@ -104,10 +115,36 @@ function tokenize(cmd) {
104
115
  return out;
105
116
  }
106
117
 
118
+ function isNodeExec(a) {
119
+ const base = path.basename(String(a || '')).toLowerCase();
120
+ return a === process.execPath || base === 'node' || base === 'node.exe';
121
+ }
122
+
123
+ // Pure: resolve a routine step's argv so it runs in BOTH layouts (ADR 0108; task 2138).
124
+ // • `bongos exec <rel> [args]` (the materialized invocation form) → run <rel> under the CORE
125
+ // root, exactly like `bongos exec` itself does (bin/bongos.js resolveExecTarget).
126
+ // • `node <path> [args]` → resolve <path>: a `scripts/...` core path under coreRoot; any other
127
+ // relative path (notably the `.claude/scheduled-tasks/<name>/<x>.js` companions) under the
128
+ // instance root. Absolute paths and non-node commands pass through untouched.
129
+ // Commands come from committed SKILL.md (trusted), so this is path resolution, not sandboxing.
130
+ // Today coreRoot === instanceRoot (single checkout) so this is byte-identical to the old run;
131
+ // only after the split do the two roots diverge. Exported + unit-tested.
132
+ function resolveStepArgv(argv, { coreRoot = CORE_ROOT, instanceRoot = INSTANCE_ROOT } = {}) {
133
+ if (!argv || !argv.length) return argv || [];
134
+ if (argv[0] === 'bongos' && argv[1] === 'exec' && argv[2]) {
135
+ return [process.execPath, path.join(coreRoot, argv[2]), ...argv.slice(3)];
136
+ }
137
+ if (isNodeExec(argv[0]) && argv[1] && !path.isAbsolute(argv[1])) {
138
+ const base = argv[1].startsWith('scripts/') ? coreRoot : instanceRoot;
139
+ return [process.execPath, path.join(base, argv[1]), ...argv.slice(2)];
140
+ }
141
+ return argv;
142
+ }
143
+
107
144
  function opsAlert(message) {
108
145
  try {
109
- execFileSync(process.execPath, [path.join('scripts', 'discord', 'post.js'), '--channel', ALERT_CHANNEL, String(message).slice(0, 1800)],
110
- { cwd: REPO_ROOT, stdio: 'ignore' });
146
+ execFileSync(process.execPath, [DISCORD_POST, '--channel', ALERT_CHANNEL, String(message).slice(0, 1800)],
147
+ { cwd: INSTANCE_ROOT, stdio: 'ignore' });
111
148
  } catch (_) { /* best-effort: the alert must never mask the real failure */ }
112
149
  }
113
150
 
@@ -116,19 +153,20 @@ function routeOutput(sink, stdout, routineName) {
116
153
  if (text) console.log(text);
117
154
  if (sink === 'discord' && text) {
118
155
  try {
119
- execFileSync(process.execPath, [path.join('scripts', 'discord', 'post.js'), '--channel', ALERT_CHANNEL, `[${routineName}] ${text}`.slice(0, 1800)],
120
- { cwd: REPO_ROOT, stdio: 'ignore' });
156
+ execFileSync(process.execPath, [DISCORD_POST, '--channel', ALERT_CHANNEL, `[${routineName}] ${text}`.slice(0, 1800)],
157
+ { cwd: INSTANCE_ROOT, stdio: 'ignore' });
121
158
  } catch (_) { /* best-effort */ }
122
159
  }
123
160
  // sink 'gds' / 'none': the step's command performed its own GDS side effects.
124
161
  }
125
162
 
126
163
  function runStep(step, routineName, { dryRun }) {
127
- const argv = tokenize(step.command);
128
- if (!argv.length) return '';
164
+ const tokens = tokenize(step.command);
165
+ if (!tokens.length) return '';
129
166
  if (dryRun) { console.log(`[dry-run] would run (${step.sink}): ${step.command}`); return ''; }
167
+ const argv = resolveStepArgv(tokens);
130
168
  const out = execFileSync(argv[0], argv.slice(1), {
131
- cwd: REPO_ROOT,
169
+ cwd: INSTANCE_ROOT, // routines run in the instance repo (companions + git-touching steps)
132
170
  encoding: 'utf8',
133
171
  env: { ...process.env, OTB_COST_CRON_ROUTINE: routineName },
134
172
  });
@@ -140,7 +178,7 @@ function runJudgment(routineName, skillBody, { dryRun }) {
140
178
  if (dryRun) { console.log(`[dry-run] judgment mode → would spawn claude -p over ${routineName}/SKILL.md`); return; }
141
179
  // Status quo: a wrapped session reading the SKILL body. Reuse the shared spawn.
142
180
  const grader = require('../../modules/grading/grader');
143
- return grader.runSubagent({ prompt: skillBody, opts: { cwd: REPO_ROOT } });
181
+ return grader.runSubagent({ prompt: skillBody, opts: { cwd: INSTANCE_ROOT } });
144
182
  }
145
183
 
146
184
  async function main() {
@@ -176,7 +214,7 @@ async function main() {
176
214
  try { const j = JSON.parse(String(out).trim().split('\n').pop() || '{}'); if (j && j.needs_judgment) payload = j.needs_judgment; } catch (_) {}
177
215
  if (payload) {
178
216
  const grader = require('../../modules/grading/grader');
179
- await grader.runSubagent({ prompt: String(payload), opts: { cwd: REPO_ROOT } });
217
+ await grader.runSubagent({ prompt: String(payload), opts: { cwd: INSTANCE_ROOT } });
180
218
  }
181
219
  }
182
220
  }
@@ -194,4 +232,4 @@ if (require.main === module) {
194
232
  main();
195
233
  }
196
234
 
197
- module.exports = { parseRoutineFrontmatter, tokenize, VALID_MODES, VALID_SINKS };
235
+ module.exports = { parseRoutineFrontmatter, tokenize, resolveStepArgv, isNodeExec, VALID_MODES, VALID_SINKS };
@@ -65,6 +65,10 @@ const SUPPORT_MODULES = new Set(['helpers']);
65
65
  const INTEGRATION = new Set([
66
66
  'asset_version', 'auth_flow_slice1', 'backfill_sessions', 'bfg', 'bfg_write',
67
67
  'box_connect_e2e', 'box_onboard', 'boxes', 'cf_tunnel', 'cli_token_reissue',
68
+ // consumer_layout_boot builds the core .tgz, `npm install`s it, and boots a live
69
+ // platform-server — network + package build + a spawned server, run by its own
70
+ // .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
71
+ 'consumer_layout_boot',
68
72
  'discord_autojoin', 'discord_oauth', 'linkify_refs', 'memory_isolation',
69
73
  'memory_pull', 'parallel-load', 'session_records', 'setup_smoke',
70
74
  'status-probe', 'subdomain_auth',
@@ -19,7 +19,7 @@ const RUNBOOK_TASK = 1981; // goal-35 canonical runbook; A05 depends on it (exte
19
19
  const TASKS = [
20
20
  { ref: 'A01', kind: 'feature', priority: 4, est: 180, crit: [CRIT], deps: [], extDeps: [],
21
21
  title: 'bongos init --adopt: mode skeleton + additive config layering (no clobber)',
22
- description: 'Add an `--adopt` mode to `scripts/gds/init.js` (ADR 0121 §Decision 1-2). Instead of the greenfield "stand up a NEW instance" path (which refuses where config/branding.json exists and whose --force OVERWRITES), --adopt LAYERS onto the existing tree: add the pinned @cloudbongos/core dependency + the lean config/ overrides + .claude/ ADDITIVELY. For any pre-existing file (package.json, an existing .claude/, a CI workflow) it MERGES or LEAVES-AND-REPORTS — it never overwrites, and it must NOT reuse the --force code path. Writes only the lean branding.json overrides (rest resolves from *.neutral.json). Serves criterion adopt-existing-repo. deps: none (the ADR-0108 configurable-root substrate is shipped).' },
22
+ description: 'Add an `--adopt` mode to `scripts/gds/init.js` (ADR 0121 §Decision 1-2). Instead of the greenfield "stand up a NEW instance" path (which refuses where config/branding.json exists and whose --force OVERWRITES), --adopt LAYERS onto the existing tree: add the pinned @bongos/core dependency + the lean config/ overrides + .claude/ ADDITIVELY. For any pre-existing file (package.json, an existing .claude/, a CI workflow) it MERGES or LEAVES-AND-REPORTS — it never overwrites, and it must NOT reuse the --force code path. Writes only the lean branding.json overrides (rest resolves from *.neutral.json). Serves criterion adopt-existing-repo. deps: none (the ADR-0108 configurable-root substrate is shipped).' },
23
23
  { ref: 'A02', kind: 'feature', priority: 4, est: 150, crit: [CRIT], deps: [], extDeps: [],
24
24
  title: 'bongos init --adopt: detect the existing stack + layout',
25
25
  description: 'Before writing anything, --adopt inspects the existing tree — language/runtime, package manager, test + CI setup, directory layout — so the enabled modules, the generated repo-map, and the core<->host fitness boundary (ADR 0062 §1) fit what is already there instead of assuming the reference layout (ADR 0121 §Decision 4). Output: a detected-stack summary consumed by A03 (real-state capture) and A04 (pre-flight). Serves criterion adopt-existing-repo. deps: none.' },
@@ -19,7 +19,7 @@
19
19
  //
20
20
  // Exit codes: 0 ok · 1 network/api error · 2 bad invocation · 3 not found.
21
21
 
22
- const { API_BASE, cliClient } = require('./cli-lib');
22
+ const { API_BASE, cliClient, assertInstanceMatch } = require('./cli-lib');
23
23
 
24
24
  // The active internal version; override with GDS_STATUS_DEFAULT_VERSION (or pass
25
25
  // --version). Bumped GDS-V4 → BONGOS-V1 (task 1709, ADR 0103): GDS-V4 was
@@ -319,6 +319,11 @@ async function main() {
319
319
  console.log('Usage: node scripts/gds/status.js [--version ID] [Cn] [--json]');
320
320
  process.exit(0);
321
321
  }
322
+ // Fail closed if this repo's configured instance != the signed-in session's (task
323
+ // 2124 / demo-audit C2): fetchProgress uses a raw fetch(API_BASE/…) that bypasses the
324
+ // apiCall/cliClient guard, so without this `bongos status` would silently print another
325
+ // instance's progress. Skip only in --hook mode (the automated card path stays fail-open).
326
+ if (!opts.hook) await assertInstanceMatch();
322
327
  const data = await fetchProgress(opts.version);
323
328
 
324
329
  // Resolve the requested criterion (if any) once — every output mode needs it.
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/task.js — `bongos task` : create + show a Bongos task from the terminal.
3
+ //
4
+ // task 2123 (demo-audit fix C1). The bongos CLI had start/claim/ship/status/… but no
5
+ // way to CREATE a task — so seeding the first task (a new owner's opening move) meant
6
+ // hand-driving POST /api/bongos/tasks through `bongos exec scripts/gds/api.js`. This is
7
+ // the first-class front door.
8
+ //
9
+ // Usage:
10
+ // bongos task new --title "…" --description "…" [--kind feature] [--discipline engineer]
11
+ // [--priority 3] [--version <id>] [--goal <id>] [--criteria C1,C2]
12
+ // [--touches a/,b/] [--est-minutes N] [--credits N] [--ready] [--json]
13
+ // bongos task show <id> [--json]
14
+ //
15
+ // Task CREATION is Archon-gated SERVER-SIDE (creating work shapes scope, ADR 0016). This
16
+ // CLI adds no authority — a lower rank gets a clear 403 message pointing at idea capture.
17
+
18
+ const { cliClient, requireSession, arg, hasFlag } = require('./cli-lib');
19
+
20
+ // Valid enums (mirrors the server's task-classifier vocab; a friendly client-side check
21
+ // so an obvious typo fails before the round-trip — the server stays authoritative).
22
+ const VALID_KINDS = ['feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture', 'cleanup', 'decision', 'blocker-resolution', 'verify'];
23
+ const VALID_DISCIPLINES = ['engineer', 'artist', 'ideator', 'ui'];
24
+
25
+ function csv(v) {
26
+ return String(v || '').split(',').map((s) => s.trim()).filter(Boolean);
27
+ }
28
+
29
+ // PURE argv → { body, criteria, errors } — exported for the unit test (no network).
30
+ // `args` is the argv slice AFTER the `new` subcommand.
31
+ function buildTaskBody(args) {
32
+ const errors = [];
33
+ const title = arg('--title', args);
34
+ const description = arg('--description', args);
35
+ if (!title) errors.push('--title is required');
36
+ if (!description) errors.push('--description is required');
37
+
38
+ const kind = arg('--kind', args) || 'feature';
39
+ if (!VALID_KINDS.includes(kind)) errors.push(`--kind must be one of: ${VALID_KINDS.join(', ')}`);
40
+ const discipline = arg('--discipline', args) || 'engineer';
41
+ if (!VALID_DISCIPLINES.includes(discipline)) errors.push(`--discipline must be one of: ${VALID_DISCIPLINES.join(', ')}`);
42
+
43
+ const num = (flag) => { const v = arg(flag, args); return v == null ? undefined : Number(v); };
44
+ const priority = num('--priority');
45
+ if (priority !== undefined && (!Number.isInteger(priority) || priority < 1 || priority > 5)) {
46
+ errors.push('--priority must be an integer 1–5');
47
+ }
48
+ const goalRaw = arg('--goal', args);
49
+ const goalId = goalRaw == null ? undefined : Number(goalRaw);
50
+ if (goalRaw != null && (!Number.isInteger(goalId) || goalId <= 0)) errors.push('--goal must be a positive integer task-goal id');
51
+
52
+ const body = {
53
+ title,
54
+ description,
55
+ kind,
56
+ discipline,
57
+ status: hasFlag('--ready', args) ? 'ready' : 'backlog',
58
+ source: 'bongos-task-cli',
59
+ };
60
+ const version = arg('--version', args);
61
+ if (version) body.version_id = version;
62
+ if (goalId !== undefined) body.goal_id = goalId;
63
+ if (priority !== undefined) body.priority = priority;
64
+ const est = num('--est-minutes'); if (est !== undefined) body.est_minutes = est;
65
+ const md = num('--manual-degree'); if (md !== undefined) body.manual_degree = md;
66
+ const credits = num('--credits'); if (credits !== undefined) body.credits_reward = credits;
67
+ const touches = csv(arg('--touches', args)); if (touches.length) body.touches = touches;
68
+ const criteria = csv(arg('--criteria', args));
69
+ if (criteria.length) body.criterion_ids = criteria;
70
+
71
+ return { body, criteria, errors };
72
+ }
73
+
74
+ function usage() {
75
+ console.error('usage:');
76
+ console.error(' bongos task new --title "…" --description "…" [--kind feature] [--discipline engineer]');
77
+ console.error(' [--priority 3] [--version <id>] [--goal <id>] [--criteria C1,C2]');
78
+ console.error(' [--touches a/,b/] [--est-minutes N] [--credits N] [--ready] [--json]');
79
+ console.error(' bongos task show <id> [--json]');
80
+ console.error(`\n kinds: ${VALID_KINDS.join(' | ')}`);
81
+ console.error(` disciplines: ${VALID_DISCIPLINES.join(' | ')}`);
82
+ }
83
+
84
+ function hallLink(base, id) {
85
+ try { return `${new URL(base).origin}/builders#/task/${id}`; } catch { return `task #${id}`; }
86
+ }
87
+
88
+ // The version_id a task defaults to when --version is omitted: the most-recent
89
+ // BUILDING version (the capture.js precedent, so idea-capture and task-create agree).
90
+ // Returns null when none is building — the caller then requires an explicit --version.
91
+ async function resolveDefaultVersion(api) {
92
+ const r = await api.versions.getVersions();
93
+ if (!r.ok) return null;
94
+ const building = (r.data?.versions ?? []).filter((v) => v.status === 'building');
95
+ if (building.length === 0) return null;
96
+ building.sort((a, b) => {
97
+ const ta = a.started_at ? new Date(a.started_at).getTime() : 0;
98
+ const tb = b.started_at ? new Date(b.started_at).getTime() : 0;
99
+ return tb - ta;
100
+ });
101
+ return building[0].id;
102
+ }
103
+
104
+ async function cmdNew(args) {
105
+ const { body, errors } = buildTaskBody(args);
106
+ if (errors.length) { console.error('task new: ' + errors.join('; ') + '\n'); usage(); process.exit(2); }
107
+ const api = await cliClient();
108
+ // version_id is required by the API; default to the active (building) version so a
109
+ // new owner never has to know the id, but keep --version as the explicit override.
110
+ if (!body.version_id) {
111
+ body.version_id = await resolveDefaultVersion(api);
112
+ if (!body.version_id) {
113
+ console.error('task new: could not resolve an active version — pass --version <id> (see `bongos status`).');
114
+ process.exit(2);
115
+ }
116
+ }
117
+ const r = await api.tasks.postTasks({ body });
118
+ if (r.status === 403) {
119
+ console.error('Creating a task is Archon-gated (creating work shapes scope). Your rank is lower, so the server declined.');
120
+ console.error('To propose it instead, capture an idea for triage:');
121
+ console.error(' bongos exec scripts/gds/capture.js "<your idea>"');
122
+ process.exit(1);
123
+ }
124
+ if (r.status !== 201 || !r.data?.task) {
125
+ console.error(`task new failed (${r.status}):`, r.data);
126
+ process.exit(1);
127
+ }
128
+ const task = r.data.task;
129
+ if (hasFlag('--json', args)) { console.log(JSON.stringify(task, null, 2)); return; }
130
+ const { API_BASE } = require('./cli-lib');
131
+ console.log(`✓ Created task #${task.id} [${task.status}] — ${task.title}`);
132
+ console.log(` kind=${task.kind} · discipline=${task.discipline}${task.priority ? ` · P${task.priority}` : ''}${task.goal_id ? ` · goal ${task.goal_id}` : ''}`);
133
+ console.log(` ${hallLink(API_BASE, task.id)}`);
134
+ if (task.status !== 'ready') console.log(` (status '${task.status}' — pass --ready to make it claimable now, deps permitting)`);
135
+ console.log(` next: bongos claim ${task.id}`);
136
+ }
137
+
138
+ async function cmdShow(args) {
139
+ const id = Number(args[0]);
140
+ if (!Number.isInteger(id) || id <= 0) { console.error('usage: bongos task show <id>'); process.exit(2); }
141
+ const api = await cliClient();
142
+ const r = await api.tasks.getTasksId({ id });
143
+ if (r.status === 404) { console.error(`task #${id} not found.`); process.exit(1); }
144
+ if (r.status !== 200 || !r.data?.task) { console.error(`task show failed (${r.status}):`, r.data); process.exit(1); }
145
+ const task = r.data.task;
146
+ if (hasFlag('--json', args)) { console.log(JSON.stringify(task, null, 2)); return; }
147
+ console.log(`#${task.id} [${task.status}] — ${task.title}`);
148
+ console.log(`kind=${task.kind} · discipline=${task.discipline}${task.priority ? ` · P${task.priority}` : ''}${task.goal_id ? ` · goal ${task.goal_id}` : ''}${task.version_id ? ` · ${task.version_id}` : ''}`);
149
+ if (task.description) console.log(`\n${task.description}`);
150
+ }
151
+
152
+ async function main() {
153
+ const args = process.argv.slice(2);
154
+ const sub = args[0];
155
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') { usage(); process.exit(sub ? 0 : 2); }
156
+ await requireSession();
157
+ if (sub === 'new') return cmdNew(args.slice(1));
158
+ if (sub === 'show') return cmdShow(args.slice(1));
159
+ console.error(`task: unknown subcommand "${sub}"\n`); usage(); process.exit(2);
160
+ }
161
+
162
+ if (require.main === module) {
163
+ main().catch((err) => { console.error('fatal:', err); process.exit(1); });
164
+ }
165
+
166
+ module.exports = { buildTaskBody, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink };
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ //
4
+ // scripts/gds/update-channel.js — the update-subscription POLICY core (task 2150, ADR 0136).
5
+ //
6
+ // "How does a consumer instance subscribe to regular @bongos/core updates?" The owner-chosen
7
+ // answer (2026-07-09): **patch-only by default, health-gated, auto-rollback, opt-in, autonomy-gated.**
8
+ // This module is the PURE, testable heart of that policy — the semver + channel math + the
9
+ // subscription-manifest loader. It launches nothing and mutates nothing; the scheduled routine
10
+ // (.claude/scheduled-tasks/core-update-subscription/subscribe.js) drives the actual `bongos upgrade`
11
+ // per subscribed instance using what this module resolves.
12
+ //
13
+ // A per-instance UPDATE CHANNEL bounds how far an unattended upgrade may jump:
14
+ // pinned — never auto-upgrade (manual `bongos upgrade` only).
15
+ // patch — take a higher PATCH within the same major.minor (1.17.1 → 1.17.4, never 1.18.0). DEFAULT.
16
+ // minor — take a higher MINOR within the same major (1.17.1 → 1.18.0, never 2.0.0).
17
+ // A MAJOR bump is NEVER automatic under any channel — a major is a breaking change that wants a human.
18
+ // Prereleases (1.17.2-rc.1) are never auto-target either — only stable releases.
19
+ //
20
+ // WHY the registry as the source (not vendored tarballs): a subscription needs a LIVE source of
21
+ // truth for "what's the newest published core?" — that's `npm view @bongos/core versions` against the
22
+ // private registry (ADR 0134, task 2090). The vendored path is for a one-off manual bump.
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+ const { spawnSync } = require('node:child_process');
27
+
28
+ const CORE_PKG = '@bongos/core';
29
+ const VALID_CHANNELS = ['pinned', 'patch', 'minor'];
30
+ const DEFAULT_CHANNEL = 'patch';
31
+ // The subscription manifest an operator edits to opt instances in. Ships EMPTY (instances: []) in the
32
+ // core template — a fresh instance subscribes nothing until the operator lists a target here.
33
+ const SUBSCRIPTIONS_REL = path.join('config', 'update-subscriptions.json');
34
+
35
+ // ---- semver (stable x.y.z, optional prerelease tag) -------------------------
36
+ // A deliberately small parser — the repo avoids an npm-semver dependency in the CLI path (see
37
+ // upgrade.js versionFromDep). We only need compare + the major/minor/patch fields.
38
+
39
+ // Parse "1.17.2" / "v1.17.2" / "1.17.2-rc.1" → { major, minor, patch, pre } or null. `pre` is the
40
+ // prerelease tag ('' for a plain stable release), used only to EXCLUDE prereleases from auto-targets.
41
+ function parseSemver(v) {
42
+ const m = String(v == null ? '' : v).trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
43
+ if (!m) return null;
44
+ return { major: +m[1], minor: +m[2], patch: +m[3], pre: m[4] || '' };
45
+ }
46
+
47
+ function isStable(v) {
48
+ const p = parseSemver(v);
49
+ return !!p && p.pre === '';
50
+ }
51
+
52
+ // Compare two version strings by (major, minor, patch). Returns -1 / 0 / 1. A stable release sorts
53
+ // ABOVE its own prereleases (1.17.2 > 1.17.2-rc.1), matching semver precedence — enough for "is this
54
+ // candidate newer than what's installed?" Unparseable inputs sort as -Infinity (never a target).
55
+ function compareSemver(a, b) {
56
+ const pa = parseSemver(a);
57
+ const pb = parseSemver(b);
58
+ if (!pa && !pb) return 0;
59
+ if (!pa) return -1;
60
+ if (!pb) return 1;
61
+ for (const k of ['major', 'minor', 'patch']) {
62
+ if (pa[k] !== pb[k]) return pa[k] < pb[k] ? -1 : 1;
63
+ }
64
+ // Same x.y.z: a prerelease is LOWER than the stable release. (Two prereleases: not ordered
65
+ // precisely here — we never auto-target a prerelease, so their relative order is immaterial.)
66
+ if (pa.pre && !pb.pre) return -1;
67
+ if (!pa.pre && pb.pre) return 1;
68
+ return 0;
69
+ }
70
+
71
+ function normalizeChannel(channel) {
72
+ const c = String(channel == null ? '' : channel).trim().toLowerCase();
73
+ return VALID_CHANNELS.includes(c) ? c : DEFAULT_CHANNEL;
74
+ }
75
+
76
+ // Would `channel` permit an unattended jump from `installed` to `candidate`? True only when candidate
77
+ // is a STABLE release strictly newer than installed AND within the channel's blast radius. Any invalid
78
+ // input, an unknown installed version, a prerelease candidate, or a major bump ⇒ false (fail-closed).
79
+ function channelAllows(installed, candidate, channel) {
80
+ const ch = normalizeChannel(channel);
81
+ if (ch === 'pinned') return false;
82
+ const from = parseSemver(installed);
83
+ const to = parseSemver(candidate);
84
+ if (!from || !to) return false;
85
+ if (to.pre) return false; // never auto-target a prerelease
86
+ if (compareSemver(candidate, installed) <= 0) return false; // must be strictly newer
87
+ if (to.major !== from.major) return false; // a major bump is never automatic
88
+ if (ch === 'patch' && to.minor !== from.minor) return false; // patch stays on the same minor
89
+ return true; // 'minor' allows any higher minor within the major
90
+ }
91
+
92
+ // The highest version in `available` that `channel` permits from `installed`, or null if none
93
+ // (nothing newer, pinned, only prereleases/majors ahead, or an unknown installed version). This is
94
+ // the one function the routine asks: "given where this instance is and what it subscribes to, what —
95
+ // if anything — should I upgrade it to right now?"
96
+ function resolveChannelTarget({ installed, channel, available } = {}) {
97
+ const list = Array.isArray(available) ? available : [];
98
+ let best = null;
99
+ for (const v of list) {
100
+ if (!channelAllows(installed, v, channel)) continue;
101
+ if (best === null || compareSemver(v, best) > 0) best = v;
102
+ }
103
+ return best;
104
+ }
105
+
106
+ // ---- registry: what versions of the core are published? ---------------------
107
+
108
+ // List published versions of @bongos/core from the registry via `npm view … versions --json`
109
+ // (ADR 0134 — the private scoped package resolves through the instance's env-fed .npmrc, so run this
110
+ // with cwd inside an instance that has one + NPM_TOKEN in env). Fail-SOFT: any error returns
111
+ // { ok:false } so the routine skips that instance rather than crashing the whole sweep. `run` +
112
+ // `pkg` are injectable for tests.
113
+ function listAvailableVersions({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env } = {}) {
114
+ let res;
115
+ try {
116
+ res = run('npm', ['view', pkg, 'versions', '--json'], { cwd, env, encoding: 'utf8' });
117
+ } catch (e) {
118
+ return { ok: false, error: e && e.message ? e.message : String(e), versions: [] };
119
+ }
120
+ if (!res || res.status !== 0) {
121
+ const reason = (res && (res.stderr || res.error && res.error.message)) || `exit ${res && res.status}`;
122
+ return { ok: false, error: String(reason).trim().split('\n')[0] || 'npm view failed', versions: [] };
123
+ }
124
+ return parseNpmViewVersions(res.stdout);
125
+ }
126
+
127
+ // Parse `npm view … versions --json` stdout. npm prints a JSON ARRAY for a multi-version package, a
128
+ // bare JSON STRING for a single-version one, and nothing for an unpublished name. Normalizes all
129
+ // three to { ok:true, versions:[…] }. Exported + unit-tested (the shape has bitten callers before).
130
+ function parseNpmViewVersions(stdout) {
131
+ const s = String(stdout == null ? '' : stdout).trim();
132
+ if (!s) return { ok: true, versions: [] };
133
+ let parsed;
134
+ try { parsed = JSON.parse(s); } catch (e) { return { ok: false, error: `unparseable npm output: ${e.message}`, versions: [] }; }
135
+ if (Array.isArray(parsed)) return { ok: true, versions: parsed.map(String) };
136
+ if (typeof parsed === 'string') return { ok: true, versions: [parsed] };
137
+ return { ok: false, error: 'unexpected npm view shape', versions: [] };
138
+ }
139
+
140
+ // ---- subscription manifest --------------------------------------------------
141
+
142
+ // Read + normalize config/update-subscriptions.json — the operator-edited opt-in list of instances
143
+ // this control plane keeps up to date. Never throws: a missing/malformed file ⇒ an empty roster
144
+ // (subscribe nothing), because an unreadable manifest must not upgrade anything. Each entry is
145
+ // normalized to { slug, dir, service, healthUrl, channel } with the channel validated + defaulted.
146
+ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SUBSCRIPTIONS_REL } = {}) {
147
+ const abs = path.isAbsolute(file) ? file : path.join(instanceDir, file);
148
+ let raw;
149
+ try { raw = fsImpl.readFileSync(abs, 'utf8'); } catch (_) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: false }; }
150
+ let json;
151
+ try { json = JSON.parse(raw); } catch (e) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: true, error: `malformed JSON: ${e.message}` }; }
152
+ const defaultChannel = normalizeChannel(json.defaultChannel);
153
+ const rows = Array.isArray(json.instances) ? json.instances : [];
154
+ const instances = rows
155
+ .filter((r) => r && typeof r === 'object' && r.dir) // an entry without a dir can't be upgraded — drop it
156
+ .map((r) => ({
157
+ slug: String(r.slug || r.dir),
158
+ dir: String(r.dir),
159
+ service: r.service ? String(r.service) : null,
160
+ healthUrl: r.healthUrl ? String(r.healthUrl) : null,
161
+ channel: normalizeChannel(r.channel == null ? defaultChannel : r.channel),
162
+ // Per-instance env merged into the `bongos upgrade` child (e.g. PGDATABASE so the migrate step
163
+ // hits THIS instance's DB, not the control plane's — see the live-op learnings for demo). Only
164
+ // string values are kept; anything else is ignored.
165
+ env: (r.env && typeof r.env === 'object' && !Array.isArray(r.env))
166
+ ? Object.fromEntries(Object.entries(r.env).filter(([, v]) => typeof v === 'string'))
167
+ : {},
168
+ }));
169
+ return { defaultChannel, instances, source: abs, present: true };
170
+ }
171
+
172
+ module.exports = {
173
+ CORE_PKG,
174
+ VALID_CHANNELS,
175
+ DEFAULT_CHANNEL,
176
+ SUBSCRIPTIONS_REL,
177
+ parseSemver,
178
+ isStable,
179
+ compareSemver,
180
+ normalizeChannel,
181
+ channelAllows,
182
+ resolveChannelTarget,
183
+ listAvailableVersions,
184
+ parseNpmViewVersions,
185
+ loadSubscriptions,
186
+ };