@bongos/core 1.21.4 → 1.21.6
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 +233 -108
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
- package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +37 -2
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +541 -658
- package/docs/copy-registry.json +1205 -2340
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
- package/docs/onboarding/diagrams/assertions.json +1 -1
- package/docs/page-inventory.json +47 -28
- package/docs/page-readings.json +1128 -1339
- package/modules/autonomy/config-idle.js +192 -0
- package/modules/autonomy/governor-docket.js +101 -0
- package/modules/autonomy/routes/autonomy.js +27 -0
- package/modules/autonomy/runner-health.js +22 -1
- package/modules/discord/board-broadcast.js +3 -1
- package/modules/discord/craft-broadcast.js +107 -0
- package/modules/discord/routes/discord.js +10 -0
- package/modules/discord/ship-broadcast.js +30 -2
- package/modules/economy/reward.js +3 -0
- package/modules/government/board.js +20 -5
- package/modules/government/docket-vocab.js +82 -0
- package/modules/government/docket.js +458 -0
- package/modules/government/routes/government.js +34 -0
- package/modules/hall-ui/public/city-draw.js +438 -0
- package/modules/hall-ui/public/city.css +110 -0
- package/modules/hall-ui/public/city.html +71 -0
- package/modules/hall-ui/public/city.js +177 -0
- package/modules/hall-ui/public/city.states.json +30 -0
- package/modules/hall-ui/public/gate.js +11 -4
- package/modules/hall-ui/public/genesis-home.js +9 -10
- package/modules/hall-ui/public/profile.css +4 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/modules/hall-ui/public/settings-autobongos.js +19 -1
- package/modules/hall-ui/public/settings.css +26 -0
- package/modules/hall-ui/public/settings.html +8 -0
- package/modules/hall-ui/public/settings.js +43 -3
- package/modules/ideas/governor-docket.js +93 -0
- package/modules/ideas/routes/blockers.js +5 -0
- package/modules/lifecycle/governor-docket.js +192 -0
- package/modules/lifecycle/routes/gate-approvals.js +78 -33
- package/modules/lifecycle/routes/lifecycle.js +3 -0
- package/modules/onboarding/routes/access-requests.js +68 -27
- package/modules/platform-identity/craft-rollup.js +72 -0
- package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
- package/modules/platform-identity/platform-identity.js +12 -6
- package/modules/platform-identity/routes/sso.js +4 -0
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/modules/provisioning/core-upgrade.js +34 -7
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/seams.js +2 -0
- package/modules/public-landing/public/assets/cosmos.css +6 -50
- package/modules/public-landing/public/projects.html +1170 -1659
- package/modules/public-landing/public/projects.probes.json +18 -18
- package/modules/public-landing/public/projects.states.json +24 -31
- package/modules/security/routes/reports.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +72 -0
- package/scripts/gds/autobongos-grade-cap.js +187 -0
- package/scripts/gds/autobongos-loop.js +4 -0
- package/scripts/gds/autobongos-run.js +154 -13
- package/scripts/gds/autobongos-verify.js +211 -14
- package/scripts/gds/provision-core-upgrade.js +14 -2
- package/scripts/gds/provision-repo.js +77 -13
- package/scripts/gds/ship-flow.js +5 -0
- package/scripts/gds/ship-preflight-steps.js +2 -1
- package/scripts/gds/ship.js +10 -0
- package/scripts/gds/update-sweep.js +24 -8
- package/scripts/gds/upgrade-outcome.js +1 -0
- package/src/bongos/auth-admission.js +95 -13
- package/src/bongos/db-kernel.js +1 -1
- package/src/bongos/module-rechecks.js +77 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/core-update.js +68 -2
- package/src/bongos/routes/modules.js +17 -0
- package/src/bongos/serve-internal.js +14 -0
- package/src/bongos/software-update.js +3 -2
- package/src/module-api.js +1 -1
- package/tests/activity_rollup_order_db.mjs +3 -1
- package/tests/activity_snapshots_db.mjs +2 -0
- package/tests/autobongos_grade_cap.mjs +125 -0
- package/tests/autobongos_loop.mjs +230 -1
- package/tests/autobongos_verify.mjs +239 -3
- package/tests/autonomy_config_idle.mjs +272 -0
- package/tests/core_update_banner.mjs +29 -0
- package/tests/core_upgrade_door.mjs +31 -1
- package/tests/core_upgrade_runner.mjs +20 -0
- package/tests/discord_craft_broadcast.mjs +162 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_routes.mjs +3 -1
- package/tests/governor_docket.mjs +552 -0
- package/tests/hall_audit.mjs +10 -1
- package/tests/hall_city.mjs +369 -0
- package/tests/hall_page_gate_map.mjs +5 -0
- package/tests/hub_craft_rollup.mjs +350 -0
- package/tests/onboard.mjs +10 -4
- package/tests/platform_boot.mjs +1 -1
- package/tests/profile_rollup_consent.mjs +2 -0
- package/tests/profile_route.mjs +16 -2
- package/tests/projects_hub.mjs +196 -89
- package/tests/projects_hub_app_status.mjs +3 -1
- package/tests/projects_hub_app_step.mjs +72 -48
- package/tests/projects_hub_dns_ready.mjs +10 -2
- package/tests/projects_hub_look.mjs +32 -22
- package/tests/projects_hub_module_picker.mjs +112 -62
- package/tests/projects_hub_pre_uat.mjs +48 -41
- package/tests/provision.mjs +78 -0
- package/tests/public_landing_projects.mjs +12 -6
- package/tests/settings_main_role.mjs +190 -0
- package/tests/software_update.mjs +56 -1
- package/tests/update_subscription_engine.mjs +55 -3
- package/tests/wizard_demo.mjs +37 -11
- package/tests/wizard_draft_resume.mjs +34 -22
- package/tests/wizard_front_door.mjs +173 -103
- package/tests/wizard_intent_resume.mjs +28 -25
- package/tests/wizard_physics.mjs +94 -76
- package/tests/wizard_physics_more.mjs +428 -277
- package/tests/wizard_six_screens.mjs +126 -0
|
@@ -72,6 +72,33 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
72
72
|
// fail loudly here rather than silently reclassify a held claim as untouched.
|
|
73
73
|
const HELD_STATUSES = new Set(['active', 'claimed', 'in_progress']);
|
|
74
74
|
|
|
75
|
+
// LAND_GRACE_MS — how long a SHIPPED task may have nothing on main before that
|
|
76
|
+
// is an owner's problem (task 1004538).
|
|
77
|
+
//
|
|
78
|
+
// The one place this file deliberately waits. Everywhere else the session that
|
|
79
|
+
// was meant to finish the job has exited, so waiting cannot change the verdict.
|
|
80
|
+
// A shipped task is different: the ship is done, and what is outstanding is the
|
|
81
|
+
// LAND, which runs on the server's own clock and can legitimately take hours on a
|
|
82
|
+
// red main. Task 1004513 was flagged at 22:24Z and its merge landed ~11 hours
|
|
83
|
+
// later; the owner got a false alarm (blocker 1000185). A day covers that with
|
|
84
|
+
// room to spare, and a genuinely codeless ship is already credited, so a day's
|
|
85
|
+
// delay in saying so costs nothing. AUTOBONGOS_LAND_GRACE_HOURS overrides it.
|
|
86
|
+
const LAND_GRACE_MS = (() => {
|
|
87
|
+
const h = Number(process.env.AUTOBONGOS_LAND_GRACE_HOURS);
|
|
88
|
+
return Number.isFinite(h) && h > 0 ? h * 3600_000 : 24 * 3600_000;
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
// When the ship happened, from the LEDGER — never from this process, so a runner
|
|
92
|
+
// restart cannot reset the clock. `shipped_at` first: `updated_at` moves on any
|
|
93
|
+
// touch. Null when neither parses, which classifyLedger reads as "no grace".
|
|
94
|
+
function shippedAtMs(task) {
|
|
95
|
+
for (const k of ['shipped_at', 'updated_at']) {
|
|
96
|
+
const t = Date.parse(task && task[k]);
|
|
97
|
+
if (Number.isFinite(t)) return t;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
// ---------------------------------------------------------------------------
|
|
76
103
|
// The pure half
|
|
77
104
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +119,7 @@ const HELD_STATUSES = new Set(['active', 'claimed', 'in_progress']);
|
|
|
92
119
|
// modules/ideas/blockers.js is exactly ['ready']), which is why
|
|
93
120
|
// the caller releases BEFORE it links.
|
|
94
121
|
// halt — the run stops here rather than picking another task.
|
|
95
|
-
function classifyLedger({ task, artifact = null, nowMs = Date.now() } = {}) {
|
|
122
|
+
function classifyLedger({ task, artifact = null, nowMs = Date.now(), landGraceMs = LAND_GRACE_MS } = {}) {
|
|
96
123
|
const none = { releaseClaim: false, fileBlocker: false, linkBlocker: false, halt: false };
|
|
97
124
|
|
|
98
125
|
if (!task) {
|
|
@@ -110,6 +137,19 @@ function classifyLedger({ task, artifact = null, nowMs = Date.now() } = {}) {
|
|
|
110
137
|
|
|
111
138
|
if (status === 'shipped') {
|
|
112
139
|
if (artifact && artifact.checked && !artifact.onMain) {
|
|
140
|
+
// Not yet is not never (task 1004538). Inside the grace window this is a
|
|
141
|
+
// land still in flight: nothing is filed, and the runner re-probes it on
|
|
142
|
+
// later iterations (recheckLands). With no readable clock the grace cannot
|
|
143
|
+
// be measured, so it falls through to the strict verdict below.
|
|
144
|
+
const shippedAt = shippedAtMs(task);
|
|
145
|
+
if (shippedAt !== null && nowMs - shippedAt < landGraceMs) {
|
|
146
|
+
const hours = Math.round(landGraceMs / 3600_000);
|
|
147
|
+
return {
|
|
148
|
+
...none, verified: false, shape: 'pending_land', status, pendingLand: true,
|
|
149
|
+
reason: 'shipped, but main does not carry a commit naming it yet — the land can take hours on a red main',
|
|
150
|
+
remedy: `nothing yet: the runner re-checks main on later iterations and files a blocker only if it is still missing ${hours}h after the ship`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
113
153
|
// The flip happened with nothing behind it — tasks 1002459 and 1002792,
|
|
114
154
|
// both credited, neither carrying a line of code on any branch. The task is
|
|
115
155
|
// NOT released and NOT linked: it is shipped, credits are paid, and parking
|
|
@@ -240,26 +280,50 @@ const runGit = (args, cwd) => new Promise((resolve) => {
|
|
|
240
280
|
//
|
|
241
281
|
// Deliberately weak and deliberately cheap. It answers "is there a trace", not
|
|
242
282
|
// "which commit is it": a body mention counts, because the question being asked
|
|
243
|
-
// is the 1002459 question — was anything at all produced.
|
|
244
|
-
//
|
|
283
|
+
// is the 1002459 question — was anything at all produced.
|
|
284
|
+
//
|
|
285
|
+
// WHAT COUNTS AS NAMING THE TASK (task 1004538). `(task N)` alone was too
|
|
286
|
+
// narrow: the commits PR 1456 landed for task 1004513 never wrote it — the merge
|
|
287
|
+
// subject names the ship BRANCH (`…/autobongos-1004513-978953`, or
|
|
288
|
+
// `…/task-1004440` for a hand ship). So the old probe said "no artifact" even
|
|
289
|
+
// after the land. Now git narrows by the bare id (fixed string, cheap) and
|
|
290
|
+
// namesTask() makes the exact call in JS, never matching the prefix of a longer
|
|
291
|
+
// id. The SUBJECT may name it as `task N` / `task-N` / `autobongos-N-`; the BODY
|
|
292
|
+
// only as `(task N)`, because bodies routinely DISCUSS other tasks ("task 1004513
|
|
293
|
+
// was flagged…" is in this very file's commit), and a mention is not an artifact.
|
|
245
294
|
//
|
|
246
295
|
// A probe that could not run returns `checked: false`, which classifyLedger
|
|
247
296
|
// treats as no evidence either way.
|
|
248
|
-
|
|
297
|
+
function namesTask(subject, body, id) {
|
|
298
|
+
const inSubject = new RegExp(`(?:\\btask[\\s-]+|\\bautobongos-)${id}(?!\\d)`, 'i');
|
|
299
|
+
const inBody = new RegExp(`\\(task ${id}\\)`, 'i');
|
|
300
|
+
return inSubject.test(String(subject || '')) || inBody.test(String(body || ''));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// `fetch: false` skips the fetch and reads the FETCH_HEAD a caller has just
|
|
304
|
+
// fetched itself — recheckLands fetches once per pass, not once per task.
|
|
305
|
+
async function probeArtifact(taskId, { git = runGit, cwd = REPO_ROOT, fetch = true } = {}) {
|
|
249
306
|
const id = String(taskId);
|
|
250
307
|
if (!/^\d{1,19}$/.test(id)) return { checked: false, reason: 'task id is not a plain number' };
|
|
251
308
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
309
|
+
if (fetch) {
|
|
310
|
+
const fetched = await git(['fetch', '-q', 'origin', 'main'], cwd);
|
|
311
|
+
if (!fetched.ok) {
|
|
312
|
+
return { checked: false, reason: `git fetch failed: ${(fetched.stderr || '').trim().slice(-200)}` };
|
|
313
|
+
}
|
|
255
314
|
}
|
|
256
|
-
|
|
315
|
+
// %x1f between fields, %x1e after each commit: a body may hold any text.
|
|
316
|
+
const log = await git(['log', 'FETCH_HEAD', '--max-count=50', '--format=%H%x1f%s%x1f%b%x1e', '-F', `--grep=${id}`], cwd);
|
|
257
317
|
if (!log.ok) return { checked: false, reason: `git log failed: ${(log.stderr || '').trim().slice(-200)}` };
|
|
258
318
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
319
|
+
for (const rec of log.stdout.split('\x1e')) {
|
|
320
|
+
const [sha, subject, body] = rec.replace(/^\s+/, '').split('\x1f');
|
|
321
|
+
if (!sha || !sha.trim()) continue;
|
|
322
|
+
if (namesTask(subject, body, id)) {
|
|
323
|
+
return { checked: true, onMain: true, sha: sha.trim(), subject: subject || null };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { checked: true, onMain: false, sha: null, subject: null };
|
|
263
327
|
}
|
|
264
328
|
|
|
265
329
|
// fetchTask returns the row or null, and says WHY on a null. The reason is the
|
|
@@ -330,7 +394,7 @@ async function verifyShip({ taskId, taskTitle = null, worker = null }, deps = {}
|
|
|
330
394
|
artifact = await probe(taskId, { cwd: deps.cwd || REPO_ROOT });
|
|
331
395
|
}
|
|
332
396
|
|
|
333
|
-
const v = classifyLedger({ task, artifact, nowMs: deps.nowMs || Date.now() });
|
|
397
|
+
const v = classifyLedger({ task, artifact, nowMs: deps.nowMs || Date.now(), ...(deps.landGraceMs ? { landGraceMs: deps.landGraceMs } : {}) });
|
|
334
398
|
const out = { ...v, task_id: String(taskId), artifact, released: false, blocker: null };
|
|
335
399
|
if (readError) out.read_error = readError;
|
|
336
400
|
if (!act) return out;
|
|
@@ -360,6 +424,136 @@ async function verifyShip({ taskId, taskTitle = null, worker = null }, deps = {}
|
|
|
360
424
|
return out;
|
|
361
425
|
}
|
|
362
426
|
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Pending lands, across iterations and restarts (task 1004538)
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
// pendingLandIds — which tasks are still waiting on a land, from the run log.
|
|
432
|
+
//
|
|
433
|
+
// The run log is append-only JSONL on disk, so this survives a restart; nothing
|
|
434
|
+
// lives only in memory. The LATEST verdict per task wins: a `worked` or
|
|
435
|
+
// `land_recheck` row whose ledger_shape is `pending_land` keeps it on the list,
|
|
436
|
+
// anything later (shipped, shipped_no_artifact, …) takes it off. The grace clock
|
|
437
|
+
// itself is not here — it is the ledger's `shipped_at`, re-read on every check.
|
|
438
|
+
function pendingLandIds(rows) {
|
|
439
|
+
const latest = new Map();
|
|
440
|
+
for (const r of rows || []) {
|
|
441
|
+
if (!r || typeof r !== 'object' || !r.task_id || !r.ledger_shape) continue;
|
|
442
|
+
if (r.event !== 'worked' && r.event !== 'land_recheck') continue;
|
|
443
|
+
latest.set(String(r.task_id), r.ledger_shape);
|
|
444
|
+
}
|
|
445
|
+
return Array.from(latest).filter(([, shape]) => shape === 'pending_land').map(([id]) => id);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const NO_ARTIFACT_REF = /^task-(\d{1,19})-shipped_no_artifact$/;
|
|
449
|
+
|
|
450
|
+
// openNoArtifactBlockers — every open blocker this file filed as
|
|
451
|
+
// `shipped_no_artifact`, read from the DATABASE (so a blocker filed by a
|
|
452
|
+
// previous process, or before this code existed, is still found).
|
|
453
|
+
//
|
|
454
|
+
// GET /blockers lists OPEN blockers only (listOpenBlockers) — the owner's
|
|
455
|
+
// to-do list, normally tens of rows — so one or two pages is the real cost; the
|
|
456
|
+
// route has no source filter, hence the client-side match. Paged by the route's
|
|
457
|
+
// own `page.total`. MAX_PAGES is a runaway guard, and hitting it is reported, not
|
|
458
|
+
// silently treated as "looked everywhere".
|
|
459
|
+
const BLOCKER_PAGE = 200;
|
|
460
|
+
const MAX_BLOCKER_PAGES = 25;
|
|
461
|
+
async function openNoArtifactBlockers(api) {
|
|
462
|
+
const out = [];
|
|
463
|
+
for (let page = 0; ; page++) {
|
|
464
|
+
if (page >= MAX_BLOCKER_PAGES) {
|
|
465
|
+
throw new Error(`more than ${MAX_BLOCKER_PAGES * BLOCKER_PAGE} open blockers — stopped paging; the ones past that were not checked`);
|
|
466
|
+
}
|
|
467
|
+
const offset = page * BLOCKER_PAGE;
|
|
468
|
+
const r = await api.blockers.getBlockers({ query: { limit: BLOCKER_PAGE, offset } });
|
|
469
|
+
if (!r || !r.ok) throw new Error(`GET /blockers → ${r ? r.status : 'no response'}`);
|
|
470
|
+
const rows = (r.data && r.data.blockers) || [];
|
|
471
|
+
for (const b of rows) {
|
|
472
|
+
const m = b && b.source === 'autobongos' && NO_ARTIFACT_REF.exec(String(b.source_ref || ''));
|
|
473
|
+
if (m && (!b.status || b.status === 'open')) out.push({ id: String(b.id), taskId: m[1] });
|
|
474
|
+
}
|
|
475
|
+
const total = r.data && r.data.page && Number(r.data.page.total);
|
|
476
|
+
if (!rows.length || (Number.isFinite(total) ? offset + rows.length >= total : rows.length < BLOCKER_PAGE)) break;
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// recheckLands — one pass, run by the runner once per loop.
|
|
482
|
+
//
|
|
483
|
+
// 1. Every pending land is verified again. Inside the grace window it stays
|
|
484
|
+
// `pending_land` and nothing is filed; past it, verifyShip files the
|
|
485
|
+
// `shipped_no_artifact` blocker exactly as before. Shipped tasks are never
|
|
486
|
+
// released, so no release function is passed.
|
|
487
|
+
// 2. Every OPEN `shipped_no_artifact` blocker is re-probed, and resolved with
|
|
488
|
+
// a note once main carries the commit — the false alarm closes itself.
|
|
489
|
+
// Only a probe that actually ran and found the commit resolves anything.
|
|
490
|
+
//
|
|
491
|
+
// I/O per pass: at most ONE `git fetch` (only when there is something to probe),
|
|
492
|
+
// one `git log` per distinct task id, and the blocker list only when
|
|
493
|
+
// `sweepBlockers` is not false — the runner sweeps hourly, not every loop.
|
|
494
|
+
//
|
|
495
|
+
// Never throws: a pass that fails is reported in `errors` and retried next loop.
|
|
496
|
+
async function recheckLands({ pendingIds = [], sweepBlockers = true } = {}, deps = {}) {
|
|
497
|
+
const api = deps.api || await (require('./cli-lib').cliClient)();
|
|
498
|
+
const cwd = deps.cwd || REPO_ROOT;
|
|
499
|
+
const git = deps.git || runGit;
|
|
500
|
+
const out = { rechecked: [], resolved: [], errors: [] };
|
|
501
|
+
|
|
502
|
+
let open = [];
|
|
503
|
+
if (sweepBlockers) {
|
|
504
|
+
try { open = await openNoArtifactBlockers(api); }
|
|
505
|
+
catch (e) { out.errors.push(`blocker list: ${e && e.message ? e.message : String(e)}`); }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// One fetch for the whole pass; every probe then reads the same FETCH_HEAD.
|
|
509
|
+
// An injected probe owns its own I/O, so none is done for it.
|
|
510
|
+
let fetchFailed = null;
|
|
511
|
+
if (!deps.probeArtifact && (pendingIds.length || open.length)) {
|
|
512
|
+
const f = await git(['fetch', '-q', 'origin', 'main'], cwd);
|
|
513
|
+
if (!f.ok) fetchFailed = `git fetch failed: ${(f.stderr || '').trim().slice(-200)}`;
|
|
514
|
+
}
|
|
515
|
+
const rawProbe = deps.probeArtifact || ((id) => (fetchFailed
|
|
516
|
+
? { checked: false, reason: fetchFailed }
|
|
517
|
+
: probeArtifact(id, { git, cwd, fetch: false })));
|
|
518
|
+
const memo = new Map();
|
|
519
|
+
const probe = (id) => {
|
|
520
|
+
if (!memo.has(String(id))) {
|
|
521
|
+
memo.set(String(id), (async () => rawProbe(id, { cwd }))()
|
|
522
|
+
.catch((e) => ({ checked: false, reason: e && e.message ? e.message : String(e) })));
|
|
523
|
+
}
|
|
524
|
+
return memo.get(String(id));
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
for (const id of pendingIds) {
|
|
528
|
+
try {
|
|
529
|
+
const v = await verifyShip({ taskId: id }, {
|
|
530
|
+
api, cwd, probeArtifact: (tid) => probe(tid),
|
|
531
|
+
...(deps.fileBlocker ? { fileBlocker: deps.fileBlocker } : {}),
|
|
532
|
+
...(deps.nowMs ? { nowMs: deps.nowMs } : {}),
|
|
533
|
+
...(deps.landGraceMs ? { landGraceMs: deps.landGraceMs } : {}),
|
|
534
|
+
});
|
|
535
|
+
out.rechecked.push({
|
|
536
|
+
task_id: String(id), ledger_status: v.status, ledger_shape: v.shape, verified: !!v.verified,
|
|
537
|
+
ledger_reason: v.reason || null,
|
|
538
|
+
blocker_id: v.blocker && v.blocker.blockerId ? v.blocker.blockerId : null,
|
|
539
|
+
});
|
|
540
|
+
} catch (e) { out.errors.push(`task ${id}: ${e && e.message ? e.message : String(e)}`); }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
for (const b of open) {
|
|
544
|
+
const a = await probe(b.taskId);
|
|
545
|
+
if (!a || !a.checked || !a.onMain) continue;
|
|
546
|
+
const sha = a.sha ? String(a.sha).slice(0, 9) : 'a commit';
|
|
547
|
+
const note = `Auto-resolved by autobongos-verify.js (task 1004538): main now carries ${sha}${a.subject ? ` ("${String(a.subject).slice(0, 120)}")` : ''}, which names task ${b.taskId}. The land was slow, not missing.`;
|
|
548
|
+
try {
|
|
549
|
+
const r = await api.blockers.postBlockersIdResolve({ id: Number(b.id), body: { resolution_note: note } });
|
|
550
|
+
if (r && r.ok) out.resolved.push({ blocker_id: b.id, task_id: b.taskId, sha });
|
|
551
|
+
else out.errors.push(`resolve blocker ${b.id} → ${r ? r.status : 'no response'}`);
|
|
552
|
+
} catch (e) { out.errors.push(`resolve blocker ${b.id}: ${e && e.message ? e.message : String(e)}`); }
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
|
|
363
557
|
// ---------------------------------------------------------------------------
|
|
364
558
|
// CLI
|
|
365
559
|
// ---------------------------------------------------------------------------
|
|
@@ -399,4 +593,7 @@ if (require.main === module) {
|
|
|
399
593
|
});
|
|
400
594
|
}
|
|
401
595
|
|
|
402
|
-
module.exports = {
|
|
596
|
+
module.exports = {
|
|
597
|
+
classifyLedger, blockerFor, probeArtifact, fileBlocker, verifyShip, failureReason,
|
|
598
|
+
LAND_GRACE_MS, namesTask, pendingLandIds, recheckLands,
|
|
599
|
+
};
|
|
@@ -117,7 +117,7 @@ function parseSchemaPending(stdout) {
|
|
|
117
117
|
* @param {string} installed the version running now
|
|
118
118
|
* @param {string} channel this instance's update channel
|
|
119
119
|
*/
|
|
120
|
-
function targetFault(to, available, installed, channel, channelAllows) {
|
|
120
|
+
function targetFault(to, available, installed, channel, channelAllows, compare) {
|
|
121
121
|
if (!Array.isArray(available) || available.length === 0) {
|
|
122
122
|
return `could not read the published core versions from the registry — nothing was changed; the release may still be publishing (a tag is minted before the publish completes)`;
|
|
123
123
|
}
|
|
@@ -136,6 +136,18 @@ function targetFault(to, available, installed, channel, channelAllows) {
|
|
|
136
136
|
if (!installed) {
|
|
137
137
|
return `could not read the core version installed at this instance — nothing was changed. An upgrade that cannot say where it is starting from cannot be bounded by the '${channel}' channel.`;
|
|
138
138
|
}
|
|
139
|
+
// A TARGET THE BOX HAS ALREADY REACHED IS NOT A CHANNEL QUESTION (task 1004534).
|
|
140
|
+
// channelAllows answers "may an unattended jump go here" and says no to anything not
|
|
141
|
+
// strictly newer, so on 2026-10-01 a press for 1.20.73, landed a minute earlier by the
|
|
142
|
+
// sweep, was reported as "outside this project's 'minor' update channel … across a major
|
|
143
|
+
// version or onto a prerelease" — false on every count, and it put a "your call" card on
|
|
144
|
+
// /deploy for a project that was simply up to date. Already there: no fault, so the
|
|
145
|
+
// caller's already-serving no-op answers it. Already past it: say that, in words that
|
|
146
|
+
// classify as a downgrade, never as the update rule.
|
|
147
|
+
if (installed === to) return null;
|
|
148
|
+
if (compare && compare(to, installed) < 0) {
|
|
149
|
+
return `core ${to} is older than the core this project already runs (it already runs core ${installed}) — nothing was changed. A move never goes back to an older core on its own.`;
|
|
150
|
+
}
|
|
139
151
|
if (channelAllows && !channelAllows(installed, to, channel)) {
|
|
140
152
|
return `core ${to} is outside this project's '${channel}' update channel — nothing was changed. ${channelNextStep(channel)}`;
|
|
141
153
|
}
|
|
@@ -276,7 +288,7 @@ async function coreUpgradeInstance(inst, deps, intent) {
|
|
|
276
288
|
const channel = instanceChannel(inst, channelMod);
|
|
277
289
|
const listed = offeredVersions(channelMod, inst, instanceDir);
|
|
278
290
|
const available = (listed && listed.versions) || [];
|
|
279
|
-
const fault = targetFault(to, available, installed, channel, channelMod.channelAllows);
|
|
291
|
+
const fault = targetFault(to, available, installed, channel, channelMod.channelAllows, channelMod.compareSemver);
|
|
280
292
|
if (fault) return failed(outcome.runnerReason(fault), fault);
|
|
281
293
|
if (installed && installed === to) {
|
|
282
294
|
// `bongos upgrade` would no-op on this too, but saying so here keeps a re-drained
|
|
@@ -19,6 +19,7 @@ const { CONFIG, REPO_ROOT, coreVersionSafe, provisionerBotEmail, provisionerBotN
|
|
|
19
19
|
// upgrade.js can reach these three without pulling provision*.js into the merge
|
|
20
20
|
// resolver's executed closure; re-exported below so every existing caller is unchanged.
|
|
21
21
|
const { instanceRepoRemote, parseTargetRef, resolveOwnerGithubToken } = require('./provision-repo-auth.js');
|
|
22
|
+
const { CORE_PKG, compareSemver, isStable } = require('./update-channel.js');
|
|
22
23
|
|
|
23
24
|
function dbName(inst) { return inst.db_name || inst.slug; }
|
|
24
25
|
|
|
@@ -500,8 +501,49 @@ function coreCheckoutHasGit({ repoRoot = REPO_ROOT, fsImpl = fs } = {}) {
|
|
|
500
501
|
return fsImpl.existsSync(path.join(repoRoot, '.git'));
|
|
501
502
|
}
|
|
502
503
|
|
|
504
|
+
// The core version THIS control plane runs — what the ladder below would vend: the
|
|
505
|
+
// pinned tarball's version, else the registry-installed package's, else the checkout's
|
|
506
|
+
// CORE_VERSION. fs reads only → dry-run safe.
|
|
507
|
+
function runningCoreVersion({ instanceRoot, coreV }) {
|
|
508
|
+
const pinned = installedCoreTarball({ instanceRoot });
|
|
509
|
+
if (pinned) return require('./upgrade').versionFromTgzPath(path.basename(pinned)) || coreV;
|
|
510
|
+
const packSrc = installedCorePackDir({ instanceRoot });
|
|
511
|
+
return packSrc ? packSrc.version : coreV;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Which core a hosted project is given (task 1004299, ADR 0361 D5). PURE. Since merges
|
|
515
|
+
// publish as candidates, the platform hall runs ahead of the registry's `latest` label by
|
|
516
|
+
// design — and the ladder below vends whatever the hall runs, so every new project would
|
|
517
|
+
// be born on a version nobody released. When the hall is AHEAD of the release, the project
|
|
518
|
+
// gets the release instead. Equal (or the hall behind it, which a release can't normally
|
|
519
|
+
// produce) → the running core, byte-exact, as before.
|
|
520
|
+
//
|
|
521
|
+
// No readable label → the running core, deliberately (fail-open). Until candidates are
|
|
522
|
+
// armed the running core IS the release; vendoring is the path that works with no
|
|
523
|
+
// registry at all (ADR 0292); and refusing project creation over a registry blip is worse
|
|
524
|
+
// for the person creating it than a newer-than-released core in the rare outage window.
|
|
525
|
+
function projectCoreDecision({ running, released }) {
|
|
526
|
+
if (!released || !isStable(released)) return { version: running, source: 'running', reason: 'no readable released (latest) label — using the core this hall runs' };
|
|
527
|
+
if (compareSemver(running, released) > 0) return { version: released, source: 'released', reason: `this hall runs ${running}, ahead of the release` };
|
|
528
|
+
return { version: running, source: 'running', reason: `this hall runs the released core ${released}` };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// The I/O half: read the `latest` label through the injected exec (so a dry run and the
|
|
532
|
+
// test harness's stub exec never touch the network), then decide. `npm view` is fine
|
|
533
|
+
// here — provisioning is a runner script, not a request handler (ADR 0293's objection).
|
|
534
|
+
function projectCoreTarget({ instanceRoot, coreV, exec, log, tag = '[core]' }) {
|
|
535
|
+
const running = runningCoreVersion({ instanceRoot, coreV });
|
|
536
|
+
const res = exec(`npm view ${CORE_PKG} dist-tags.latest`, { allowFail: true });
|
|
537
|
+
const label = String((res && res.ok !== false && res.stdout) || '').trim();
|
|
538
|
+
const decision = projectCoreDecision({ running, released: label || null });
|
|
539
|
+
log(` ${tag} ${decision.source === 'released' ? `giving the project the RELEASED core ${decision.version}` : `giving the project core ${decision.version}`} — ${decision.reason} (task 1004299)`);
|
|
540
|
+
return decision;
|
|
541
|
+
}
|
|
542
|
+
|
|
503
543
|
// Resolve a vendorable @bongos/core tarball — the four-rung ladder, freshest-correct
|
|
504
|
-
// first, shared by the fresh scaffold and the re-provision pin refresh (task 1003050)
|
|
544
|
+
// first, shared by the fresh scaffold and the re-provision pin refresh (task 1003050).
|
|
545
|
+
// A `target` from projectCoreTarget with source 'released' short-circuits it: the
|
|
546
|
+
// project gets the released version packed from the registry, not this hall's core.
|
|
505
547
|
// 1. a `file:`-pinned control plane reuses its own pinned tarball (task 1002275 —
|
|
506
548
|
// rebuilding via package-core.js aborts with an EMPTY core set when this runner
|
|
507
549
|
// IS a consumer instance, leaving new repos empty);
|
|
@@ -516,7 +558,16 @@ function coreCheckoutHasGit({ repoRoot = REPO_ROOT, fsImpl = fs } = {}) {
|
|
|
516
558
|
// rung 3's reproducible build is the better answer for the identical file set.
|
|
517
559
|
// Returns { tgz, packDest } — packDest (set only on rung 2) is a temp dir the CALLER
|
|
518
560
|
// must `rm -rf` once the tarball has been vendored.
|
|
519
|
-
function resolveVendorableCoreTarball({ instanceRoot, repoRoot = REPO_ROOT, exec, log, coreV, tag = '[core]' }) {
|
|
561
|
+
function resolveVendorableCoreTarball({ instanceRoot, repoRoot = REPO_ROOT, exec, log, coreV, tag = '[core]', target = null }) {
|
|
562
|
+
if (target && target.source === 'released') {
|
|
563
|
+
// target.version passed isStable (digits and dots only), so it is shell-safe.
|
|
564
|
+
const v = target.version;
|
|
565
|
+
const packDest = path.join(os.tmpdir(), `bongos-core-released-${v}`);
|
|
566
|
+
log(` ${tag} packing the released core ${v} from the registry — not the candidate this hall runs (task 1004299)`);
|
|
567
|
+
exec(`mkdir -p ${packDest}`);
|
|
568
|
+
exec(`npm pack ${CORE_PKG}@${v} --pack-destination ${packDest}`);
|
|
569
|
+
return { tgz: path.join(packDest, `bongos-core-${v}.tgz`), packDest };
|
|
570
|
+
}
|
|
520
571
|
const pinned = installedCoreTarball({ instanceRoot });
|
|
521
572
|
if (pinned) {
|
|
522
573
|
log(` ${tag} reusing the pinned core tarball this control plane runs (${path.basename(pinned)}) — no source rebuild (task 1002275)`);
|
|
@@ -629,9 +680,12 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
629
680
|
throw new Error('cannot resolve the core version (src/module-api CORE_VERSION) — run the scaffold from the control-plane checkout.');
|
|
630
681
|
}
|
|
631
682
|
const remote = instanceRepoRemote(repo);
|
|
683
|
+
// The released core, not this hall's candidate (task 1004299).
|
|
684
|
+
const target = projectCoreTarget({ instanceRoot: deps.instanceRoot, coreV, exec, log, tag: '[scaffold] core:' });
|
|
685
|
+
const pinV = target.version;
|
|
632
686
|
log(mode === 'adopt'
|
|
633
|
-
? ` [scaffold] ${repo.owner}/${repo.name} → ${root} (ADOPT: clone → layer additively → pin core ${
|
|
634
|
-
: ` [scaffold] ${repo.owner}/${repo.name} → ${root} (clone → materialize → pin core ${
|
|
687
|
+
? ` [scaffold] ${repo.owner}/${repo.name} → ${root} (ADOPT: clone → layer additively → pin core ${pinV} → commit → fast-forward push)`
|
|
688
|
+
: ` [scaffold] ${repo.owner}/${repo.name} → ${root} (clone → materialize → pin core ${pinV} → commit → push)`);
|
|
635
689
|
|
|
636
690
|
// Owner login = the founding firstAdmin. Best-effort; the spec falls back to the repo owner.
|
|
637
691
|
const ownerLogin = await ownerLoginOf(inst, db);
|
|
@@ -743,7 +797,7 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
743
797
|
// Vendored tarball — resolveVendorableCoreTarball's four-rung ladder (shared
|
|
744
798
|
// with the re-provision pin refresh, task 1003050).
|
|
745
799
|
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
746
|
-
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:',
|
|
800
|
+
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:', target,
|
|
747
801
|
});
|
|
748
802
|
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
|
|
749
803
|
// init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
|
|
@@ -775,8 +829,8 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
775
829
|
exec(`git -C ${root} config user.email "${provisionerBotEmail()}"`);
|
|
776
830
|
exec(`git -C ${root} add -A`);
|
|
777
831
|
const commitMsg = mode === 'adopt'
|
|
778
|
-
? `chore: adopt Cloud Bongos onto ${inst.slug} on a pinned @bongos/core ${
|
|
779
|
-
: `chore: scaffold ${inst.slug} on a pinned @bongos/core ${
|
|
832
|
+
? `chore: adopt Cloud Bongos onto ${inst.slug} on a pinned @bongos/core ${pinV} (provision.js, ADR 0121)`
|
|
833
|
+
: `chore: scaffold ${inst.slug} on a pinned @bongos/core ${pinV} (provision.js, ADR 0108)`;
|
|
780
834
|
exec(`git -C ${root} commit -m "${commitMsg}"`, { allowFail: true });
|
|
781
835
|
|
|
782
836
|
// Push. NO --force on either path, ever (see the function header): the ADR 0155 `repo`
|
|
@@ -1078,16 +1132,23 @@ async function refreshStandaloneCorePin(inst, deps) {
|
|
|
1078
1132
|
let pkg = null;
|
|
1079
1133
|
try { pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); } catch { /* unreadable → skip below */ }
|
|
1080
1134
|
const pinnedDep = pkg && ((pkg.dependencies || {})['@bongos/core'] || (pkg.devDependencies || {})['@bongos/core']);
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1135
|
+
if (!/^file:/.test(String(pinnedDep || '').trim())) {
|
|
1136
|
+
const plan = planCorePinRefresh({ pinnedDep, coreVersion: coreV });
|
|
1137
|
+
log(` [core-pin] ${plan.reason}`); return plan;
|
|
1138
|
+
}
|
|
1139
|
+
// Heal toward the core a project is GIVEN — the release when this hall runs ahead of it —
|
|
1140
|
+
// or the pin a brand-new project was just born on is bumped straight back to the
|
|
1141
|
+
// candidate in the same provisioning run (task 1004299).
|
|
1142
|
+
const target = projectCoreTarget({ instanceRoot: deps.instanceRoot, coreV, exec, log, tag: '[core-pin]' });
|
|
1143
|
+
const plan = planCorePinRefresh({ pinnedDep, coreVersion: target.version });
|
|
1144
|
+
if (plan.action === 'current') { log(` [core-pin] committed pin already matches the core this project is given (${target.version})`); return plan; }
|
|
1145
|
+
log(` [core-pin] committed pin ${safeVersionLabel(plan.pinned)} differs from the core this project is given (${target.version}) — refreshing so the next repo-driven deploy cannot roll the instance back (task 1003050)`);
|
|
1085
1146
|
|
|
1086
1147
|
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
1087
|
-
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[core-pin]',
|
|
1148
|
+
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[core-pin]', target,
|
|
1088
1149
|
});
|
|
1089
1150
|
try {
|
|
1090
|
-
const targetVersion = upgrade.versionFromTgzPath(path.basename(tgz)) ||
|
|
1151
|
+
const targetVersion = upgrade.versionFromTgzPath(path.basename(tgz)) || target.version;
|
|
1091
1152
|
const vendorRel = upgrade.vendorTarball({ instanceDir: root, fromTgz: tgz, targetVersion });
|
|
1092
1153
|
// Prune superseded vendored tarballs (+ manifests) so the repo carries exactly ONE
|
|
1093
1154
|
// core. fs.rmSync, NOT exec — a filename comes off the instance's own (lower-trust)
|
|
@@ -1176,6 +1237,9 @@ module.exports = {
|
|
|
1176
1237
|
refreshStandaloneCorePin,
|
|
1177
1238
|
resolveOwnerGithubToken,
|
|
1178
1239
|
resolveVendorableCoreTarball,
|
|
1240
|
+
projectCoreDecision,
|
|
1241
|
+
projectCoreTarget,
|
|
1242
|
+
runningCoreVersion,
|
|
1179
1243
|
safeVersionLabel,
|
|
1180
1244
|
scaffoldStandaloneRepo,
|
|
1181
1245
|
seedFirstVersionCmd,
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -31,6 +31,9 @@ const tweakHold = require('../../modules/lifecycle/page-tweak-hold.js');
|
|
|
31
31
|
const { evaluateRegradeEligibility, evaluateResumeAction, resumeMerge, shouldConfirmWithoutGrade } = require('./ship-resume.js');
|
|
32
32
|
const { fileNoArtifactEscalation, noArtifactRefusalLines } = require('./ship-escalation.js');
|
|
33
33
|
const { noteServerReward } = require('./ship-state.js');
|
|
34
|
+
// task 1004536: each graded round lands in the autonomous run's ledger (inert
|
|
35
|
+
// without one), so ship.js can refuse a third grade at its entry point.
|
|
36
|
+
const { recordGradeOutcome } = require('./autobongos-grade-cap.js');
|
|
34
37
|
|
|
35
38
|
// ---------- Main flow ----------
|
|
36
39
|
|
|
@@ -695,6 +698,7 @@ async function shipMain() {
|
|
|
695
698
|
}
|
|
696
699
|
|
|
697
700
|
const g = gradePost.data.grade;
|
|
701
|
+
recordGradeOutcome({ taskId, grade: g });
|
|
698
702
|
gradeObj = g;
|
|
699
703
|
gradePassed = g.passed;
|
|
700
704
|
advanceToMerge = gradePost.data.advanceToMerge;
|
|
@@ -1101,6 +1105,7 @@ async function regradeMain() {
|
|
|
1101
1105
|
}
|
|
1102
1106
|
|
|
1103
1107
|
const g = gradePost.data.grade;
|
|
1108
|
+
recordGradeOutcome({ taskId, grade: g });
|
|
1104
1109
|
const postGradeStatus = gradePost.data.taskStatus;
|
|
1105
1110
|
printGradeOutcome(g, taskId);
|
|
1106
1111
|
printCreditsAndAchievements({
|
|
@@ -82,7 +82,8 @@ const TOOLING_PATHSPECS = Object.freeze([
|
|
|
82
82
|
'scripts/gds/ship*.js', // ship.js + every ship-*.js carved out of it
|
|
83
83
|
'scripts/gds/smoke-gds.js',
|
|
84
84
|
'scripts/gds/cli-lib.js',
|
|
85
|
-
'
|
|
85
|
+
'scripts/gds/autobongos-grade-cap.js', // ship.js's entry refuses a capped run with it (task 1004536)
|
|
86
|
+
'modules/grading/grader*', // grader.js, grader-*.js, grader-rubric.json, grader-workers/
|
|
86
87
|
'modules/grading/grade-engine.js', // grader.js requires it (task 1002839)
|
|
87
88
|
]);
|
|
88
89
|
|
package/scripts/gds/ship.js
CHANGED
|
@@ -348,6 +348,16 @@ if (require.main === module) {
|
|
|
348
348
|
console.log(require('./ship-deploy-target.js').resolveDeployMode());
|
|
349
349
|
process.exit(0);
|
|
350
350
|
}
|
|
351
|
+
// task 1004536: the autonomous-run grade cap — the first grade plus ONE
|
|
352
|
+
// re-grade. Checked HERE, before either flow and before any flag is read, so
|
|
353
|
+
// --regrade, a plain re-ship that resumes the grade, and --skip-grade are all
|
|
354
|
+
// refused alike. Inert unless the Autobongos supervisor set the run's ledger.
|
|
355
|
+
const { checkGradeCap, REFUSED_EXIT } = require('./autobongos-grade-cap.js');
|
|
356
|
+
const gradeCap = checkGradeCap({ taskId: Number(process.argv[2]) });
|
|
357
|
+
if (!gradeCap.ok) {
|
|
358
|
+
for (const line of gradeCap.lines) console.error(line);
|
|
359
|
+
process.exit(REFUSED_EXIT);
|
|
360
|
+
}
|
|
351
361
|
// #678: --regrade re-grades a grade-failed `completed` task (no claim needed).
|
|
352
362
|
// Dispatched here so it owns its own arg-parsing + session bootstrap.
|
|
353
363
|
const main = process.argv.slice(2).includes('--regrade') ? regradeMain : shipMain;
|
|
@@ -103,6 +103,9 @@ function git(dir, args, run = spawnSync) {
|
|
|
103
103
|
return { ok, stdout: (r && r.stdout) || '', stderr: (r && r.stderr) || '', status: r ? r.status : null };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// The sweep's own materialize preview — the fallback for an instance core that predates it.
|
|
107
|
+
function ownWillRewrite() { return require('./claude-materialize.js').materializeWillRewrite; }
|
|
108
|
+
|
|
106
109
|
// The instance-relative paths THIS upgrade writes into the instance's own tree — the provenance
|
|
107
110
|
// answer pinState's third bucket is sorted by.
|
|
108
111
|
//
|
|
@@ -118,11 +121,21 @@ function git(dir, args, run = spawnSync) {
|
|
|
118
121
|
// It is the same function upgrade.js's own clean-tree pre-flight waives dirt by, so the lane and
|
|
119
122
|
// the bump it spawns agree on what "this upgrade rewrote" means.
|
|
120
123
|
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
124
|
+
// A CORE TOO OLD TO ANSWER (task 1004519). materializeWillRewrite shipped in 1.19.933, and the
|
|
125
|
+
// wedge it exists for is exactly the one that strands an instance on an OLDER core: hoodify-finance
|
|
126
|
+
// took one bump to 1.19.871, which materialized 33 skills and committed nothing, and its core then
|
|
127
|
+
// could not say those files were its own — so the set came back empty and every sweep halted on
|
|
128
|
+
// them, forever. When the instance's core ships claude-materialize.js but not the preview, the
|
|
129
|
+
// preview is asked of the sweep's OWN copy instead, run over the instance's INSTALLED core. That is
|
|
130
|
+
// still provenance, not a glob: the file set is walked off that core's .claude/, the very files its
|
|
131
|
+
// own upgrade materializes. And the sweep's copy is no longer the stale one the paragraph above
|
|
132
|
+
// warns about — it runs from the platform's installed core (task 1004307), always the newest here.
|
|
133
|
+
//
|
|
134
|
+
// FAIL-SOFT, and the direction is deliberate. An instance whose core ships no materialize at all,
|
|
135
|
+
// whose installed core has no .claude/skills to walk, or whose materialize throws, yields nothing
|
|
136
|
+
// here: every dirty path stays in `other` and the guard is exactly as strict as it was before this
|
|
137
|
+
// function existed. The failure mode is "still wedged", never "committed a stranger's work".
|
|
138
|
+
function laneGeneratedPaths(instDir, { resolveFrom = coreModulePathFrom, load = require, own = ownWillRewrite } = {}) {
|
|
126
139
|
const out = new Set();
|
|
127
140
|
try {
|
|
128
141
|
const p = resolveFrom(instDir, 'scripts/gds/upgrade.js');
|
|
@@ -130,13 +143,16 @@ function laneGeneratedPaths(instDir, { resolveFrom = coreModulePathFrom, load =
|
|
|
130
143
|
} catch (_) { /* an older core, or one that will not load here — stay strict */ }
|
|
131
144
|
try {
|
|
132
145
|
const p = resolveFrom(instDir, 'scripts/gds/claude-materialize.js');
|
|
133
|
-
|
|
134
|
-
if (willRewrite) {
|
|
146
|
+
if (p) {
|
|
135
147
|
// coreRoot is derived from where the module RESOLVED (<core>/scripts/gds/x.js → <core>), not
|
|
136
148
|
// from a package-name guess: an instance may carry the core under any of its published names,
|
|
137
149
|
// and in the monolith layout the core root IS the instance root (where materialize no-ops).
|
|
138
150
|
const coreRoot = path.resolve(path.dirname(p), '..', '..');
|
|
139
|
-
|
|
151
|
+
const theirs = load(p).materializeWillRewrite;
|
|
152
|
+
// Only fall back where there is a real .claude/ to walk — an empty source still "writes" the
|
|
153
|
+
// provenance manifest, and that is a guess, not an answer.
|
|
154
|
+
const willRewrite = theirs || (fs.existsSync(path.join(coreRoot, '.claude', 'skills')) ? own() : null);
|
|
155
|
+
if (willRewrite) for (const f of willRewrite({ instanceDir: instDir, coreRoot }, () => {}) || []) out.add(f);
|
|
140
156
|
}
|
|
141
157
|
} catch (_) { /* same — a set we could not derive is an empty one, never a guessed one */ }
|
|
142
158
|
return [...out];
|
|
@@ -136,6 +136,7 @@ function runnerReason(text) {
|
|
|
136
136
|
if (/^could not read the published core versions/.test(s)) return 'registry_unreadable';
|
|
137
137
|
if (/is not published yet/.test(s)) return 'not_published';
|
|
138
138
|
if (/could not read the core version installed/.test(s)) return 'installed_unreadable';
|
|
139
|
+
if (/is older than the core this project already runs/.test(s)) return 'downgrade';
|
|
139
140
|
if (/update channel/.test(s)) return 'outside_channel';
|
|
140
141
|
return 'unknown';
|
|
141
142
|
}
|