@bongos/core 1.17.1 → 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.
- package/.bongos-core.json +244 -264
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +9 -6
- package/docs/architecture.md +2 -2
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +18 -17
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +6 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/private-npm-distribution.md +17 -0
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +55 -0
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/modules/discord/module.json +2 -1
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/hall-ui/public/builders.js +71 -4
- package/modules/hall-ui/public/goal-inbox.js +191 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -1
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/new-project-wizard.js +1 -0
- package/modules/hall-ui/public/style.css +43 -0
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/secret-scrub.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +30 -9
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/init.js +59 -3
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/onboard.js +4 -0
- package/scripts/gds/package-core.js +18 -3
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +116 -9
- package/src/bongos/auth.js +14 -1
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/module-api.js +12 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +20 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +25 -2
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +47 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/onboard.mjs +13 -0
- package/tests/package_core.mjs +8 -0
- package/tests/provisioning_github_app_routes.mjs +15 -0
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +131 -0
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// modules/provisioning/tests/provisioning.mjs
|
|
2
|
+
//
|
|
3
|
+
// DB-free unit smoke for the provisioning module's domain slice (ADR 0111). Focus:
|
|
4
|
+
// the P4 (#1945) Caddy on-demand-TLS ALLOW-LIST helpers — normalizeHostname (pure)
|
|
5
|
+
// and isProvisionedHostname (a fake { query }) — plus a sanity check of the P1 pure
|
|
6
|
+
// helpers. No DB, no network: `db` is a recording fake. Auto-discovered + run by
|
|
7
|
+
// scripts/gds/run-unit-tests.js when the `provisioning` module is enabled (it is on
|
|
8
|
+
// OTB). Run standalone: node modules/provisioning/tests/provisioning.mjs
|
|
9
|
+
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const P = require('../provisioning.js');
|
|
15
|
+
const RL = require('../rate-limit.js');
|
|
16
|
+
|
|
17
|
+
let passed = 0, failed = 0;
|
|
18
|
+
function t(name, fn) {
|
|
19
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
20
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
21
|
+
}
|
|
22
|
+
async function ta(name, fn) {
|
|
23
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
24
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('normalizeHostname (on-demand-TLS allow-list, P4):');
|
|
28
|
+
|
|
29
|
+
t('lowercases, trims, drops a trailing dot + a :port', () => {
|
|
30
|
+
assert.equal(P.normalizeHostname(' Mercury.Example.COM. '), 'mercury.example.com');
|
|
31
|
+
assert.equal(P.normalizeHostname('mercury.example.com:443'), 'mercury.example.com');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
t('rejects a bare TLD / empty / malformed names (fail-closed → the caller denies)', () => {
|
|
35
|
+
assert.equal(P.normalizeHostname('com'), '');
|
|
36
|
+
assert.equal(P.normalizeHostname(''), '');
|
|
37
|
+
assert.equal(P.normalizeHostname(null), '');
|
|
38
|
+
assert.equal(P.normalizeHostname('-bad.example.com'), '');
|
|
39
|
+
assert.equal(P.normalizeHostname('bad-.example.com'), '');
|
|
40
|
+
assert.equal(P.normalizeHostname('has space.example.com'), '');
|
|
41
|
+
assert.equal(P.normalizeHostname('a..b.com'), '');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
t('accepts a normal multi-label host', () => {
|
|
45
|
+
assert.equal(P.normalizeHostname('a.b.example.com'), 'a.b.example.com');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log('\nisProvisionedHostname (the Caddy `ask` allow-list, P4):');
|
|
49
|
+
|
|
50
|
+
// A fake db whose query returns the status row a lookup fn decides, keyed on $1.
|
|
51
|
+
function fakeDb(statusFor) {
|
|
52
|
+
return { query: async (_sql, params) => {
|
|
53
|
+
const s = statusFor(String(params[0]));
|
|
54
|
+
return { rows: s ? [{ status: s }] : [] };
|
|
55
|
+
} };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await ta('true ONLY for a provisioning|active instance; false for every other state or unknown', async () => {
|
|
59
|
+
const byHost = {
|
|
60
|
+
'live.example.com': 'active',
|
|
61
|
+
'mid.example.com': 'provisioning',
|
|
62
|
+
'gone.example.com': 'torn_down',
|
|
63
|
+
'req.example.com': 'requested',
|
|
64
|
+
'err.example.com': 'error',
|
|
65
|
+
'down.example.com': 'tearing_down',
|
|
66
|
+
};
|
|
67
|
+
const db = fakeDb((h) => byHost[h] || null);
|
|
68
|
+
assert.equal(await P.isProvisionedHostname(db, 'live.example.com'), true);
|
|
69
|
+
assert.equal(await P.isProvisionedHostname(db, 'mid.example.com'), true);
|
|
70
|
+
assert.equal(await P.isProvisionedHostname(db, 'gone.example.com'), false);
|
|
71
|
+
assert.equal(await P.isProvisionedHostname(db, 'req.example.com'), false);
|
|
72
|
+
assert.equal(await P.isProvisionedHostname(db, 'err.example.com'), false);
|
|
73
|
+
assert.equal(await P.isProvisionedHostname(db, 'down.example.com'), false);
|
|
74
|
+
assert.equal(await P.isProvisionedHostname(db, 'unknown.example.com'), false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await ta('normalizes the SNI before matching, and never queries a malformed host (fail-closed, no DB hit)', async () => {
|
|
78
|
+
let queried = 0;
|
|
79
|
+
const db = { query: async (_sql, params) => {
|
|
80
|
+
queried++;
|
|
81
|
+
return { rows: params[0] === 'live.example.com' ? [{ status: 'active' }] : [] };
|
|
82
|
+
} };
|
|
83
|
+
assert.equal(await P.isProvisionedHostname(db, 'LIVE.example.com.:443'), true, 'normalized SNI matches');
|
|
84
|
+
const before = queried;
|
|
85
|
+
assert.equal(await P.isProvisionedHostname(db, 'com'), false);
|
|
86
|
+
assert.equal(await P.isProvisionedHostname(db, ''), false);
|
|
87
|
+
assert.equal(queried, before, 'no query issued for a malformed host');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log('\nmakeAskRateLimiter (tls-check ask rate limit, P4 SEC review of task 1945):');
|
|
91
|
+
|
|
92
|
+
function fakeRes() {
|
|
93
|
+
return {
|
|
94
|
+
_status: 200, _json: null, _headers: {},
|
|
95
|
+
set(k, v) { this._headers[k] = v; return this; },
|
|
96
|
+
status(c) { this._status = c; return this; },
|
|
97
|
+
json(b) { this._json = b; return this; },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function call(mw, ip) {
|
|
101
|
+
let nexted = false;
|
|
102
|
+
const res = fakeRes();
|
|
103
|
+
mw({ ip }, res, () => { nexted = true; });
|
|
104
|
+
return { nexted, res };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
t('allows up to `max` per window, then 429s with Retry-After', () => {
|
|
108
|
+
let clock = 1_000_000;
|
|
109
|
+
const mw = RL.makeAskRateLimiter({ windowMs: 60_000, max: 3, now: () => clock });
|
|
110
|
+
for (let i = 0; i < 3; i++) {
|
|
111
|
+
const { nexted, res } = call(mw, '1.2.3.4');
|
|
112
|
+
assert.equal(nexted, true, `request ${i + 1} within cap passes`);
|
|
113
|
+
assert.equal(res._status, 200);
|
|
114
|
+
}
|
|
115
|
+
const over = call(mw, '1.2.3.4');
|
|
116
|
+
assert.equal(over.nexted, false, 'over-cap request is blocked (next not called)');
|
|
117
|
+
assert.equal(over.res._status, 429);
|
|
118
|
+
assert.ok(Number(over.res._headers['Retry-After']) >= 1, 'sets Retry-After');
|
|
119
|
+
assert.equal(over.res._json.error, 'rate_limited');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
t('the window slides — after windowMs elapses, requests flow again', () => {
|
|
123
|
+
let clock = 0;
|
|
124
|
+
const mw = RL.makeAskRateLimiter({ windowMs: 1_000, max: 1, now: () => clock });
|
|
125
|
+
assert.equal(call(mw, 'a').nexted, true);
|
|
126
|
+
assert.equal(call(mw, 'a').nexted, false, 'second within the window is blocked');
|
|
127
|
+
clock += 1_001; // step past the window
|
|
128
|
+
assert.equal(call(mw, 'a').nexted, true, 'allowed again after the window slides');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
t('per-IP buckets are independent', () => {
|
|
132
|
+
let clock = 5;
|
|
133
|
+
const mw = RL.makeAskRateLimiter({ windowMs: 60_000, max: 1, now: () => clock });
|
|
134
|
+
assert.equal(call(mw, 'ip-a').nexted, true);
|
|
135
|
+
assert.equal(call(mw, 'ip-a').nexted, false, 'ip-a is capped');
|
|
136
|
+
assert.equal(call(mw, 'ip-b').nexted, true, 'ip-b has its own bucket');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log('\nP1 pure helpers (sanity):');
|
|
140
|
+
|
|
141
|
+
t('isValidSlug basic contract', () => {
|
|
142
|
+
assert.equal(P.isValidSlug('mercury'), true);
|
|
143
|
+
assert.equal(P.isValidSlug('a'), false); // too short
|
|
144
|
+
assert.equal(P.isValidSlug('-x'), false); // leading dash
|
|
145
|
+
assert.equal(P.isValidSlug('a--b'), false); // double dash
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log('\nGitHub App Manifest — pure builders (task 2080, ADR 0133):');
|
|
149
|
+
|
|
150
|
+
t('githubAppName prefers the DOMAIN (unique + reads on the consent screen)', () => {
|
|
151
|
+
// The domain is globally unique, so "demo" never collides once named after its host.
|
|
152
|
+
assert.equal(P.githubAppName({ domain: 'demo.cloudbongos.com', brand: 'Cloud Bongos', project: 'demo' }), 'demo.cloudbongos.com');
|
|
153
|
+
assert.equal(P.githubAppName({ domain: 'Mercury.Example.com.:443', brand: 'X', project: 'mercury' }), 'mercury.example.com'); // normalized (case/port/dot)
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
t('githubAppName falls back to brand-qualified project when there is no usable domain', () => {
|
|
157
|
+
// no domain → "<brand> - <project>", still collision-resistant vs a bare slug
|
|
158
|
+
assert.equal(P.githubAppName({ brand: 'Cloud Bongos', project: 'demo' }), 'Cloud Bongos - demo');
|
|
159
|
+
// a domain over GitHub's 34-char limit → fall back to brand-qualified project
|
|
160
|
+
assert.equal(P.githubAppName({ domain: 'a-really-long-subdomain.enterprise.example.com', brand: 'Cloud Bongos', project: 'demo' }), 'Cloud Bongos - demo');
|
|
161
|
+
assert.equal(P.githubAppName({ brand: '', project: 'mercury' }), 'mercury'); // no brand → project
|
|
162
|
+
assert.equal(P.githubAppName({ brand: 'Cloud Bongos', project: '' }), 'Cloud Bongos'); // no project → brand
|
|
163
|
+
assert.equal(P.githubAppName({}), 'Cloud Bongos'); // last-resort default
|
|
164
|
+
const long = P.githubAppName({ brand: 'Cloud Bongos', project: 'x'.repeat(50) });
|
|
165
|
+
assert.ok(long.length <= 34 && long.startsWith('Cloud Bongos - '), `capped + brand kept: ${long}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
t('buildGithubAppManifest points redirect at the MOTHERSHIP, callbacks at the INSTANCE, minimal perms, domain-named', () => {
|
|
169
|
+
const m = P.buildGithubAppManifest({
|
|
170
|
+
instanceName: 'mercury', slug: 'mercury', brand: 'Cloud Bongos',
|
|
171
|
+
instanceDomain: 'mercury.example.com', mothershipOrigin: 'https://cloudbongos.com',
|
|
172
|
+
});
|
|
173
|
+
assert.equal(m.name, 'mercury.example.com'); // the App is named after its own domain
|
|
174
|
+
assert.equal(m.url, 'https://mercury.example.com');
|
|
175
|
+
// redirect_url = the control-plane callback (where GitHub returns the code)
|
|
176
|
+
assert.equal(m.redirect_url, 'https://cloudbongos.com/api/bongos/provisioning/github-app/callback');
|
|
177
|
+
// callback_urls = the INSTANCE's own sign-in callback, BOTH prefixes for robustness
|
|
178
|
+
assert.deepEqual(m.callback_urls, [
|
|
179
|
+
'https://mercury.example.com/api/gds/auth/web/callback',
|
|
180
|
+
'https://mercury.example.com/api/bongos/auth/web/callback',
|
|
181
|
+
]);
|
|
182
|
+
// sign-in needs no repo/account permissions; no active webhook; not public
|
|
183
|
+
assert.deepEqual(m.default_permissions, {});
|
|
184
|
+
assert.equal(m.hook_attributes.active, false);
|
|
185
|
+
assert.equal(m.public, false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
t('buildGithubAppManifest tolerates a trailing slash on the mothership origin', () => {
|
|
189
|
+
const m = P.buildGithubAppManifest({ instanceName: 'M', slug: 'm', instanceDomain: 'm.example.com', mothershipOrigin: 'https://cloudbongos.com/' });
|
|
190
|
+
assert.equal(m.redirect_url, 'https://cloudbongos.com/api/bongos/provisioning/github-app/callback');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
t('manifestPostUrl: personal vs org, state url-encoded', () => {
|
|
194
|
+
assert.equal(P.manifestPostUrl({ account: 'personal', state: 'abc' }), 'https://github.com/settings/apps/new?state=abc');
|
|
195
|
+
assert.equal(P.manifestPostUrl({ account: 'org', orgLogin: 'acme', state: 'a b' }), 'https://github.com/organizations/acme/settings/apps/new?state=a%20b');
|
|
196
|
+
// org requested but no org login → falls back to personal (never a broken URL)
|
|
197
|
+
assert.equal(P.manifestPostUrl({ account: 'org', state: 'x' }), 'https://github.com/settings/apps/new?state=x');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
console.log('\nGitHub App Manifest — data access (fake { query }):');
|
|
201
|
+
|
|
202
|
+
await ta('attachManifestCode is single-use + TTL-guarded (only a pending, un-expired row advances)', async () => {
|
|
203
|
+
const seen = [];
|
|
204
|
+
const db = { query: async (sql, params) => {
|
|
205
|
+
seen.push({ sql, params });
|
|
206
|
+
// Emulate the guarded UPDATE ... WHERE status='pending' AND expires_at > now():
|
|
207
|
+
// the fake returns a row only when the caller's id is the "fresh" one.
|
|
208
|
+
if (/UPDATE provisioning_oauth_manifests/.test(sql) && /status = 'code_received'/.test(sql)) {
|
|
209
|
+
return { rows: params[0] === 1 ? [{ id: 1, status: 'code_received', github_code: params[1] }] : [] };
|
|
210
|
+
}
|
|
211
|
+
return { rows: [] };
|
|
212
|
+
} };
|
|
213
|
+
const fresh = await P.attachManifestCode(db, 1, 'CODE123');
|
|
214
|
+
assert.ok(fresh && fresh.status === 'code_received' && fresh.github_code === 'CODE123');
|
|
215
|
+
const stale = await P.attachManifestCode(db, 2, 'CODE456'); // wrong/expired → no row
|
|
216
|
+
assert.equal(stale, null);
|
|
217
|
+
// the guard (status + expiry) lives in the SQL, not JS — assert it's actually there
|
|
218
|
+
assert.match(seen[0].sql, /status = 'pending' AND expires_at > now\(\)/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await ta('claimNextOAuthManifest flips code_received→converting, gated on an ACTIVE instance, SKIP LOCKED', async () => {
|
|
222
|
+
let sql = '';
|
|
223
|
+
const db = { query: async (q) => { sql = q; return { rows: [{ id: 7, status: 'converting' }] }; } };
|
|
224
|
+
const row = await P.claimNextOAuthManifest(db);
|
|
225
|
+
assert.equal(row.id, 7);
|
|
226
|
+
assert.match(sql, /SET status = 'converting'/);
|
|
227
|
+
assert.match(sql, /FOR UPDATE OF m SKIP LOCKED/);
|
|
228
|
+
assert.match(sql, /m\.status = 'code_received' AND i\.status = 'active'/);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
console.log(`\nprovisioning module: ${passed} passed, ${failed} failed`);
|
|
232
|
+
process.exit(failed ? 1 : 0);
|
|
File without changes
|
|
@@ -146,10 +146,19 @@ function redactHighEntropy(text) {
|
|
|
146
146
|
const ENV_SECRET_KEY_RE =
|
|
147
147
|
/(?:api[_-]?key|secret|token|password|passwd|pwd|passphrase|access[_-]?key|private[_-]?key|client[_-]?secret|credential|cookie)/i;
|
|
148
148
|
const ENV_SECRET_PREFIX_RE = /^(?:gds_|pms_|mac_|apple|anthropic|stripe|cf_|cloudflare|do_token|digitalocean)/i;
|
|
149
|
+
// Benign shell/runtime vars whose NAME trips a secret-word substring but whose VALUE is
|
|
150
|
+
// never a credential. PWD and OLDPWD contain "pwd" (the password alternative in
|
|
151
|
+
// ENV_SECRET_KEY_RE), yet hold a working-directory PATH — so without this exclusion a
|
|
152
|
+
// legitimate publish fails closed whenever that path appears verbatim in a functional file
|
|
153
|
+
// (e.g. a /Users/<name> anti-pattern comment), passing or failing purely on the operator's
|
|
154
|
+
// last `cd` (task 2130). Exact-match only, so real password vars (MYSQL_PWD, DB_PWD, …)
|
|
155
|
+
// still match below.
|
|
156
|
+
const ENV_BENIGN_KEY_RE = /^(?:PWD|OLDPWD)$/;
|
|
149
157
|
function collectEnvSecrets(env = process.env) {
|
|
150
158
|
const out = [];
|
|
151
159
|
for (const [k, v] of Object.entries(env || {})) {
|
|
152
160
|
if (typeof v !== 'string' || v.length < 8) continue;
|
|
161
|
+
if (ENV_BENIGN_KEY_RE.test(k)) continue;
|
|
153
162
|
if (ENV_SECRET_KEY_RE.test(k) || ENV_SECRET_PREFIX_RE.test(k)) out.push(v);
|
|
154
163
|
}
|
|
155
164
|
return out;
|
|
@@ -196,6 +205,7 @@ module.exports = {
|
|
|
196
205
|
redactHighEntropy,
|
|
197
206
|
ENV_SECRET_KEY_RE,
|
|
198
207
|
ENV_SECRET_PREFIX_RE,
|
|
208
|
+
ENV_BENIGN_KEY_RE,
|
|
199
209
|
collectEnvSecrets,
|
|
200
210
|
scrubSecrets,
|
|
201
211
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.17.
|
|
9
|
+
"version": "1.17.2",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/platform-server.js",
|
|
12
12
|
"./serve-internal": "./src/bongos/serve-internal.js",
|
|
13
|
-
"./module-api": "./src/module-api.js"
|
|
13
|
+
"./module-api": "./src/module-api.js",
|
|
14
|
+
"./bongos/api-path-404": "./src/bongos/api-path-404.js",
|
|
15
|
+
"./bongos/uptime-poller": "./src/bongos/uptime-poller.js",
|
|
16
|
+
"./module-loader/loader": "./src/module-loader/loader.js",
|
|
17
|
+
"./modules": "./src/modules.js",
|
|
18
|
+
"./module-seams": "./src/module-seams.js"
|
|
14
19
|
},
|
|
15
20
|
"engines": {
|
|
16
21
|
"node": ">=22"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Discord integration (outbound)
|
|
2
2
|
|
|
3
|
-
Posts
|
|
3
|
+
Posts Example build updates into the team Discord. This replaced the
|
|
4
4
|
Google Chat outbound broadcast (Discord Phase 2, GDS task [#607](https://example.com/builders#/task/607)).
|
|
5
5
|
|
|
6
6
|
Discord is a **one-way window into the GDS** — these scripts only POST. No
|
package/scripts/gds/claim.js
CHANGED
|
@@ -143,7 +143,88 @@ function printClaimCard(task, widgetsEnabled = true) {
|
|
|
143
143
|
} catch (_) { /* never block a claim on the card */ }
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
// task 1476: render a failed claim as GUIDANCE, not a raw object dump. The API
|
|
147
|
+
// returns the R01 error envelope (ADR 0116): { error: { code, message, details } }.
|
|
148
|
+
// Older paths returned a flat { error: 'CODE', ...fields } shape. This normalizes
|
|
149
|
+
// BOTH — without it `r.data.error` is now an object (never === a string), so every
|
|
150
|
+
// branch below silently missed and the refusal printed as `[object Object]`. Pure
|
|
151
|
+
// (no console / no process.exit) + exported so it can be unit-tested against the
|
|
152
|
+
// real envelope shapes. Returns { lines, exit }.
|
|
153
|
+
function formatClaimFailure(r, taskId) {
|
|
154
|
+
const lines = [];
|
|
155
|
+
const errRaw = r && r.data ? r.data.error : undefined;
|
|
156
|
+
const isEnvelope = errRaw !== null && typeof errRaw === 'object';
|
|
157
|
+
const code = isEnvelope ? errRaw.code : errRaw;
|
|
158
|
+
const message = isEnvelope ? errRaw.message : undefined;
|
|
159
|
+
// Structured fields live under .details in the envelope; fall back to the
|
|
160
|
+
// top-level body for the legacy flat shape (so both keep working).
|
|
161
|
+
const d = { ...(r && r.data ? r.data : {}), ...((isEnvelope && errRaw.details) || {}) };
|
|
162
|
+
|
|
163
|
+
// #766: a generic onboarding stage gate (kept rendering cleanly, not as raw JSON).
|
|
164
|
+
if (code === 'stage_required') {
|
|
165
|
+
lines.push(`Task #${taskId} can't be claimed yet.`);
|
|
166
|
+
lines.push(` ${d.what_to_do || `Clear "${d.required_stage_label || d.required_stage}" first.`}`);
|
|
167
|
+
return { lines, exit: 1 };
|
|
168
|
+
}
|
|
169
|
+
// task 1476 (the core case): a session may hold at most ONE active claim
|
|
170
|
+
// (task 1642 / ADR 0097). This is the most common refusal and previously fell
|
|
171
|
+
// straight through to the raw dump — the exact "reads like you can't hold more
|
|
172
|
+
// than one task" complaint from idea 218. Surface it as guidance AND teach how
|
|
173
|
+
// to parallelize (a separate worktree/session per task, or the hall bulk-claim).
|
|
174
|
+
if (code === 'session_already_has_active_claim' || code === 'session_claim_check_failed') {
|
|
175
|
+
const n = d.active_task_id;
|
|
176
|
+
lines.push(n
|
|
177
|
+
? `This session already holds task #${n}${d.active_task_title ? `: ${d.active_task_title}` : ''}.`
|
|
178
|
+
: 'This session already holds an active claim.');
|
|
179
|
+
lines.push(` ${d.what_to_do || (n
|
|
180
|
+
? `Run /builder-ship ${n} (or /builder-release ${n}) first, or open a fresh worktree/session for this task.`
|
|
181
|
+
: 'Ship or release the in-flight claim, or start a fresh worktree/session.')}`);
|
|
182
|
+
lines.push(' One claim per session keeps each ship isolated (task 1642). To work tasks in PARALLEL, open a separate worktree/session per task — each holds its own single claim — or use the hall work board’s multi-select bulk-claim.');
|
|
183
|
+
return { lines, exit: 1 };
|
|
184
|
+
}
|
|
185
|
+
if (code === 'newcomer_must_ship_first') {
|
|
186
|
+
lines.push(`Task #${taskId} can't be claimed yet — your first work isn't shipped.`);
|
|
187
|
+
lines.push(` You're holding an active claim on #${d.active_task_id}: ${d.active_task_title}`);
|
|
188
|
+
lines.push(` ${d.what_to_do || 'Ship or release the current claim first.'}`);
|
|
189
|
+
return { lines, exit: 1 };
|
|
190
|
+
}
|
|
191
|
+
if (code === 'ALREADY_CLAIMED') {
|
|
192
|
+
lines.push(`Task #${taskId} was just claimed by another session. Try a different task.`);
|
|
193
|
+
return { lines, exit: 1 };
|
|
194
|
+
}
|
|
195
|
+
if (code === 'MODULE_CONFLICT') {
|
|
196
|
+
lines.push(`Task #${taskId} has module_key='${d.module_key}', but another active claim already owns that module:`);
|
|
197
|
+
for (const t of d.conflictTaskTitles || []) lines.push(` - ${t}`);
|
|
198
|
+
lines.push('Two tasks in the same module cannot run concurrently. Ship or release the in-flight claim first.');
|
|
199
|
+
return { lines, exit: 1 };
|
|
200
|
+
}
|
|
201
|
+
if (code === 'TOUCHES_CONFLICT') {
|
|
202
|
+
lines.push(`Task #${taskId} touches files currently in flight on another claim:`);
|
|
203
|
+
for (const t of d.conflictTaskTitles || []) lines.push(` - ${t}`);
|
|
204
|
+
lines.push('Pick a task with disjoint touches[], or run it in a separate session once the other ships.');
|
|
205
|
+
return { lines, exit: 1 };
|
|
206
|
+
}
|
|
207
|
+
if (code === 'TASK_NOT_READY') {
|
|
208
|
+
lines.push(`Task #${taskId} is not ready (current status: ${d.currentStatus}).`);
|
|
209
|
+
if (d.hint) lines.push(` ${d.hint}`);
|
|
210
|
+
return { lines, exit: 1 };
|
|
211
|
+
}
|
|
212
|
+
if (code === 'TASK_NOT_FOUND') {
|
|
213
|
+
lines.push(`Task #${taskId} not found — check the id (run /builder-start to list claimable tasks).`);
|
|
214
|
+
return { lines, exit: 1 };
|
|
215
|
+
}
|
|
216
|
+
if (code === 'DEPS_NOT_SHIPPED') {
|
|
217
|
+
lines.push(`Task #${taskId} can't be claimed yet — these dependencies are not shipped:`);
|
|
218
|
+
for (const dep of d.deps || []) lines.push(` - #${dep.id} [${dep.status}] ${dep.title}`);
|
|
219
|
+
lines.push('Ship those first (or release them) and try again.');
|
|
220
|
+
return { lines, exit: 1 };
|
|
221
|
+
}
|
|
222
|
+
// Fallback — still readable: the message + code, never a raw [object Object].
|
|
223
|
+
lines.push(`Claim failed (${r ? r.status : '?'})${code ? ` [${code}]` : ''}${message ? `: ${message}` : ''}.`);
|
|
224
|
+
return { lines, exit: 1 };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function main() {
|
|
147
228
|
await requireSession();
|
|
148
229
|
const args = process.argv.slice(2);
|
|
149
230
|
const taskId = Number(args[0]);
|
|
@@ -283,51 +364,20 @@ function printClaimCard(task, widgetsEnabled = true) {
|
|
|
283
364
|
printClaimCard(task, widgetsEnabled);
|
|
284
365
|
return;
|
|
285
366
|
}
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
console.error(
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
if (r.data?.error === 'ALREADY_CLAIMED') {
|
|
301
|
-
console.error(`Task #${taskId} was just claimed by another session. Try a different task.`);
|
|
302
|
-
process.exit(1);
|
|
303
|
-
}
|
|
304
|
-
if (r.data?.error === 'MODULE_CONFLICT') {
|
|
305
|
-
console.error(`Task #${taskId} has module_key='${r.data.module_key}', but another active claim already owns that module:`);
|
|
306
|
-
for (const t of r.data.conflictTaskTitles || []) console.error(` - ${t}`);
|
|
307
|
-
console.error('Two tasks in the same module cannot run concurrently. Ship or release the in-flight claim first.');
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
if (r.data?.error === 'TOUCHES_CONFLICT') {
|
|
311
|
-
console.error(`Task #${taskId} touches files currently in flight on another claim:`);
|
|
312
|
-
for (const t of r.data.conflictTaskTitles || []) console.error(` - ${t}`);
|
|
313
|
-
console.error('Pick a task with disjoint touches[].');
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
if (r.data?.error === 'TASK_NOT_READY') {
|
|
317
|
-
console.error(`Task #${taskId} is not ready (current status: ${r.data.currentStatus}).`);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
if (r.data?.error === 'DEPS_NOT_SHIPPED') {
|
|
321
|
-
console.error(`Task #${taskId} can't be claimed yet — these dependencies are not shipped:`);
|
|
322
|
-
for (const d of r.data.deps || []) {
|
|
323
|
-
console.error(` - #${d.id} [${d.status}] ${d.title}`);
|
|
324
|
-
}
|
|
325
|
-
console.error('Ship those first (or release them) and try again.');
|
|
367
|
+
// Any non-201 comes here. formatClaimFailure normalizes the R01 error envelope
|
|
368
|
+
// and renders the refusal as guidance (task 1476) — one place, unit-tested.
|
|
369
|
+
const failure = formatClaimFailure(r, taskId);
|
|
370
|
+
for (const line of failure.lines) console.error(line);
|
|
371
|
+
process.exit(failure.exit);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Only auto-run when invoked directly (`node claim.js …`); a `require()` (the
|
|
375
|
+
// unit test) just imports formatClaimFailure without executing the claim flow.
|
|
376
|
+
if (require.main === module) {
|
|
377
|
+
main().catch((err) => {
|
|
378
|
+
console.error('fatal:', err);
|
|
326
379
|
process.exit(1);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
console.error('fatal:', err);
|
|
332
|
-
process.exit(1);
|
|
333
|
-
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = { formatClaimFailure };
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
// copy+transform. hooks/ + settings.json are TOPOLOGY (PROTECTED_TOPOLOGY_EXCLUDE) → NOT in the
|
|
16
16
|
// package; this module copies+transforms them when they ARE present at the core root (today's single
|
|
17
17
|
// checkout, and the core repo itself), and no-ops with a note when they aren't (an installed package).
|
|
18
|
-
//
|
|
19
|
-
//
|
|
18
|
+
// Hooks self-resolve their core require()s at RUNTIME via the copied _core-resolve.js sibling
|
|
19
|
+
// (task 2139, the former W3 follow-up), so no require-path rewriting happens here; shipping a
|
|
20
|
+
// topology STARTER for a package-only install (hooks absent at the core root) stays a follow-up (task 1883).
|
|
20
21
|
//
|
|
21
22
|
// SELF-COPY GUARD. When the instance dir IS the core root (running init inside the core checkout /
|
|
22
23
|
// OTB today), there is nothing to materialize — a self-copy would be a no-op at best and corrupting
|
|
@@ -70,9 +71,9 @@ function bongosify(text, verbs = VERBS) {
|
|
|
70
71
|
// branding objects, so no source host survives. Focused + FULL-HOST (a subdomain
|
|
71
72
|
// like demo.cloudbongos.com is preserved), fail-open (a missing field drops that
|
|
72
73
|
// one rule), a no-op when source == target, and it touches ONLY the copied output
|
|
73
|
-
// (never the running source hooks).
|
|
74
|
-
// require('scripts/gds/…') paths
|
|
75
|
-
//
|
|
74
|
+
// (never the running source hooks). It does NOT rewrite hooks' internal
|
|
75
|
+
// require('scripts/gds/…') paths — those self-resolve at runtime via the copied
|
|
76
|
+
// _core-resolve.js sibling (task 2139); this rule lands the origin/config-dir neutralization.
|
|
76
77
|
|
|
77
78
|
function reEsc(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
78
79
|
function hostOf(origin) { try { return new URL(String(origin)).host || null; } catch { return null; } }
|
|
@@ -214,7 +215,7 @@ function transformSettings(srcPath) {
|
|
|
214
215
|
const HOOK_TEXT_EXT = new Set(['.js', '.mjs', '.cjs', '.json', '.sh', '.md', '.txt']); // hooks are .js — rebrand them too
|
|
215
216
|
|
|
216
217
|
function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite = true, log = () => {} }) {
|
|
217
|
-
const summary = { skipped: false, skills: 0, settings: false, hooks: 0, skippedFiles: [], excludedSkills: [], rebrandRules: 0, claudeMd: false, notes: [] };
|
|
218
|
+
const summary = { skipped: false, skills: 0, settings: false, hooks: 0, scheduledTasks: 0, skippedFiles: [], excludedSkills: [], rebrandRules: 0, claudeMd: false, notes: [] };
|
|
218
219
|
if (path.resolve(instanceDir) === path.resolve(coreRoot)) {
|
|
219
220
|
summary.skipped = true;
|
|
220
221
|
summary.notes.push('instance dir is the core root — nothing to materialize (byte-identical no-op).');
|
|
@@ -273,8 +274,8 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
// 3. hooks — topology; copied WITH an identity rebrand (their .js internals carried
|
|
276
|
-
// the founder origin/config-dir). require(
|
|
277
|
-
//
|
|
277
|
+
// the founder origin/config-dir). Their core require()s self-resolve at runtime via
|
|
278
|
+
// the copied _core-resolve.js sibling (task 2139) — no path rewriting needed here.
|
|
278
279
|
const hooksRes = copyTreeTransformed(path.join(srcClaude, 'hooks'), path.join(destClaude, 'hooks'), {
|
|
279
280
|
dryRun, overwrite, transform: rebrand, textExt: HOOK_TEXT_EXT,
|
|
280
281
|
});
|
|
@@ -284,7 +285,27 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
284
285
|
summary.notes.push('.claude/hooks not present at the core root (topology, excluded from the package) — provide them from a starter.');
|
|
285
286
|
} else {
|
|
286
287
|
log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${hooksRes.files} hook file(s) → .claude/hooks/ (founder identity rebranded)${left((hooksRes.skipped || []).length)}`);
|
|
287
|
-
summary.notes.push('hooks: founder origin/config-dir rebranded to this instance; their internal `require(scripts/gds/…)` paths
|
|
288
|
+
summary.notes.push('hooks: founder origin/config-dir rebranded to this instance; their internal `require(scripts/gds/…)` paths self-resolve inside the package at runtime via the copied _core-resolve.js sibling (task 2139).');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 3b. scheduled-tasks — the cron routines (System 4 dispatcher, ADR 0078; task 2138). TOPOLOGY
|
|
292
|
+
// like hooks (NOT in the package — PROTECTED_TOPOLOGY_EXCLUDE), so materialized by copy when
|
|
293
|
+
// present at the core root and a no-op-with-note in a package-only install. Bodies (.md) are
|
|
294
|
+
// bongosified so a judgment routine's `node scripts/gds/x.js` model-instructions become
|
|
295
|
+
// `bongos exec …`; the deterministic routines' `.claude/scheduled-tasks/<x>.js` step commands
|
|
296
|
+
// are NOT matched by bongosify and stay as-is. Companion .js are rebranded but keep their
|
|
297
|
+
// paths: they resolve core require()s at runtime via the copied _core-resolve.js sibling
|
|
298
|
+
// (task 2139), and run-routine.js finds the routines under resolveInstanceRoot() post-split.
|
|
299
|
+
const routinesRes = copyTreeTransformed(path.join(srcClaude, 'scheduled-tasks'), path.join(destClaude, 'scheduled-tasks'), {
|
|
300
|
+
dryRun, overwrite, transform: (t) => rebrand(bongosify(t)), textExt: HOOK_TEXT_EXT,
|
|
301
|
+
});
|
|
302
|
+
summary.scheduledTasks = routinesRes.files;
|
|
303
|
+
for (const rel of routinesRes.skipped || []) summary.skippedFiles.push(`scheduled-tasks/${rel}`);
|
|
304
|
+
if (!routinesRes.present) {
|
|
305
|
+
summary.notes.push('.claude/scheduled-tasks not present at the core root (topology, excluded from the package) — provide them from a starter.');
|
|
306
|
+
} else {
|
|
307
|
+
log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${routinesRes.files} scheduled-task file(s) → .claude/scheduled-tasks/ (bodies→bongos, founder identity rebranded)${left((routinesRes.skipped || []).length)}`);
|
|
308
|
+
summary.notes.push('scheduled-tasks: cron routines materialized; run-routine.js resolves them under resolveInstanceRoot() and their companion .js resolve core via the copied _core-resolve.js sibling (task 2138).');
|
|
288
309
|
}
|
|
289
310
|
|
|
290
311
|
// 4. CLAUDE.md — the project-root framing an agent reads every session (task 2104).
|