@bongos/core 1.21.5 → 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 +138 -68
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
- package/docs/api/openapi.json +2 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +156 -144
- package/docs/copy-registry.json +286 -158
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
- package/docs/onboarding/diagrams/assertions.json +1 -1
- package/docs/page-inventory.json +31 -4
- package/docs/page-readings.json +121 -94
- package/modules/autonomy/config-idle.js +192 -0
- package/modules/autonomy/routes/autonomy.js +22 -0
- package/modules/autonomy/runner-health.js +22 -1
- 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/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/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/projects.states.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +58 -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/routes/core-update.js +27 -2
- 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/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/profile_rollup_consent.mjs +2 -0
- package/tests/profile_route.mjs +16 -2
- package/tests/provision.mjs +78 -0
- 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_physics_more.mjs +19 -0
|
@@ -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
|
}
|
|
@@ -132,6 +132,69 @@ async function recentShipsForRollup(builderId) {
|
|
|
132
132
|
}));
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// Each craft by its own work, for the hub's profile tiles (task 1004440 /
|
|
136
|
+
// WA6.RP10, goal 1000095). Until now the rollup said "Roles" from the declared
|
|
137
|
+
// preference alone and counted only shipped tasks, so an artist or an ideator who
|
|
138
|
+
// had done real work read on the hub as an engineer with nothing shipped.
|
|
139
|
+
//
|
|
140
|
+
// A craft is EARNED when this builder holds credits in it — the same rule the
|
|
141
|
+
// local profile uses to show a role (src/bongos/role-stats.js, task 1004433),
|
|
142
|
+
// read through the same ports. This is a kernel file, so it reads those ports
|
|
143
|
+
// by key rather than importing role-stats (ADR 0091 §1). For each earned craft it
|
|
144
|
+
// sends that craft's headline counts; an unearned craft sends nothing at all.
|
|
145
|
+
// Counts and sums only: no idea title, no page wording, no task list.
|
|
146
|
+
//
|
|
147
|
+
// `disciplines` becomes the earned crafts, in the builder's preferred order;
|
|
148
|
+
// with nothing earned (or no economy module to ask) it falls back to the declared
|
|
149
|
+
// preference, exactly as before. A port that cannot be asked leaves its numbers
|
|
150
|
+
// null ("could not ask"), never 0. Never throws: a failed read drops its craft's
|
|
151
|
+
// numbers, not the report.
|
|
152
|
+
const ROLLUP_CRAFTS = Object.freeze(['engineer', 'artist', 'ideator']);
|
|
153
|
+
const countOrNull = (v) => (Number.isInteger(v) && v >= 0 ? v : null);
|
|
154
|
+
async function readCraftRollup(builderId, { shipped = 0, preferred = [] } = {}) {
|
|
155
|
+
const fallback = { counts: null, disciplines: Array.isArray(preferred) ? preferred : [] };
|
|
156
|
+
try {
|
|
157
|
+
const reward = seams.resolveOptional('reward');
|
|
158
|
+
const byCraftMap = reward && typeof reward.creditsByCraft === 'function'
|
|
159
|
+
? await reward.creditsByCraft([builderId]) : null;
|
|
160
|
+
const byCraft = byCraftMap ? byCraftMap.get(String(builderId)) : null;
|
|
161
|
+
if (!byCraft) return fallback;
|
|
162
|
+
const prefCrafts = fallback.disciplines
|
|
163
|
+
.map((d) => (typeof reward.craftForPreference === 'function' ? reward.craftForPreference(d) : d))
|
|
164
|
+
.filter((c) => ROLLUP_CRAFTS.includes(c));
|
|
165
|
+
const earned = [...prefCrafts, ...ROLLUP_CRAFTS]
|
|
166
|
+
.filter((c, i, a) => a.indexOf(c) === i && Number(byCraft[c]) > 0);
|
|
167
|
+
if (!earned.length) return fallback;
|
|
168
|
+
const ask = async (port) => {
|
|
169
|
+
const fn = seams.resolveOptional(port);
|
|
170
|
+
if (typeof fn !== 'function') return null;
|
|
171
|
+
try { return (await fn(builderId)) || null; } catch { return null; }
|
|
172
|
+
};
|
|
173
|
+
const [artist, ideator] = await Promise.all([
|
|
174
|
+
earned.includes('artist') ? ask('copy-desk.artistStats') : null,
|
|
175
|
+
earned.includes('ideator') ? ask('ideas.ideatorStats') : null,
|
|
176
|
+
]);
|
|
177
|
+
const credits = (c) => countOrNull(Math.round(Number(byCraft[c])));
|
|
178
|
+
const counts = {};
|
|
179
|
+
if (earned.includes('engineer')) counts.engineer = { shipped: countOrNull(shipped), credits: credits('engineer') };
|
|
180
|
+
if (earned.includes('artist')) {
|
|
181
|
+
counts.artist = { pages_approved: countOrNull(artist && artist.pages_approved), credits: credits('artist') };
|
|
182
|
+
}
|
|
183
|
+
if (earned.includes('ideator')) {
|
|
184
|
+
counts.ideator = {
|
|
185
|
+
filed: countOrNull(ideator && ideator.filed),
|
|
186
|
+
full_ideas: countOrNull(ideator && ideator.full_ideas),
|
|
187
|
+
ratified: countOrNull(ideator && ideator.ratified),
|
|
188
|
+
credits: credits('ideator'),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return { counts, disciplines: earned };
|
|
192
|
+
} catch (err) {
|
|
193
|
+
log.info('[auth] craft rollup read failed (non-blocking):', err.message);
|
|
194
|
+
return fallback;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
135
198
|
// Best-effort cross-project activity rollup to the hub (ADR 0141 §4): report this
|
|
136
199
|
// builder's descriptive talent snapshot (credits, ships, karma, rank, disciplines,
|
|
137
200
|
// and since task 1002803 their last few shipped works) for the cross-project
|
|
@@ -162,26 +225,45 @@ async function reportActivityRollup({ builderId, githubId = null }) {
|
|
|
162
225
|
const row = b.rows[0];
|
|
163
226
|
const ghId = githubId ?? row.github_id;
|
|
164
227
|
if (!/^\d+$/.test(String(ghId ?? ''))) return;
|
|
165
|
-
|
|
228
|
+
// SHIPPED works only (task 1004440): a count with no status filter also
|
|
229
|
+
// counted work that was handed back after shipping. The same predicate as the
|
|
230
|
+
// instance's own profile (lifecycle countShippedTasksForBuilder).
|
|
231
|
+
const shipped = await pool.query(
|
|
232
|
+
`SELECT count(*)::int AS n FROM tasks WHERE shipped_by = $1 AND status = 'shipped'`,
|
|
233
|
+
[builderId],
|
|
234
|
+
);
|
|
235
|
+
const shippedCount = shipped.rows[0] ? shipped.rows[0].n : 0;
|
|
166
236
|
const recentShips = await recentShipsForRollup(builderId);
|
|
167
|
-
await
|
|
237
|
+
const craft = await readCraftRollup(builderId, { shipped: shippedCount, preferred: row.preferred_disciplines });
|
|
238
|
+
const body = {
|
|
239
|
+
client_id: idp.clientId,
|
|
240
|
+
client_secret: idp.clientSecret,
|
|
241
|
+
github_id: ghId,
|
|
242
|
+
credits: Number(row.total_credits) || 0,
|
|
243
|
+
tasks_shipped: shippedCount,
|
|
244
|
+
karma: Number(row.karma) || 0,
|
|
245
|
+
rank: row.rank || null,
|
|
246
|
+
disciplines: craft.disciplines,
|
|
247
|
+
recent_ships: recentShips,
|
|
248
|
+
};
|
|
249
|
+
if (craft.counts) body.crafts = craft.counts;
|
|
250
|
+
const send = (payload) => fetch(`${idp.origin}/api/bongos/sso/activity/rollup`, {
|
|
168
251
|
method: 'POST',
|
|
169
252
|
headers: {
|
|
170
253
|
'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': userAgent(),
|
|
171
254
|
'Bongos-Report-As-Of': asOf,
|
|
172
255
|
},
|
|
173
|
-
body: JSON.stringify(
|
|
174
|
-
client_id: idp.clientId,
|
|
175
|
-
client_secret: idp.clientSecret,
|
|
176
|
-
github_id: ghId,
|
|
177
|
-
credits: Number(row.total_credits) || 0,
|
|
178
|
-
tasks_shipped: shipped.rows[0] ? shipped.rows[0].n : 0,
|
|
179
|
-
karma: Number(row.karma) || 0,
|
|
180
|
-
rank: row.rank || null,
|
|
181
|
-
disciplines: Array.isArray(row.preferred_disciplines) ? row.preferred_disciplines : [],
|
|
182
|
-
recent_ships: recentShips,
|
|
183
|
-
}),
|
|
256
|
+
body: JSON.stringify(payload),
|
|
184
257
|
});
|
|
258
|
+
const res = await send(body);
|
|
259
|
+
// A hub older than `crafts` validates the body strictly and refuses the
|
|
260
|
+
// unknown field with a 400, which would lose the whole report. So a refused
|
|
261
|
+
// report that carried crafts goes again once without them: the totals still
|
|
262
|
+
// land, and the craft numbers arrive once that hub upgrades.
|
|
263
|
+
if (body.crafts && res && res.status === 400) {
|
|
264
|
+
const { crafts: _unsent, ...older } = body;
|
|
265
|
+
await send(older);
|
|
266
|
+
}
|
|
185
267
|
} catch (err) {
|
|
186
268
|
log.info('[auth] hub activity rollup failed (non-blocking):', err.message);
|
|
187
269
|
}
|
package/src/bongos/db-kernel.js
CHANGED
|
@@ -55,7 +55,7 @@ async function getBuilderById(id) {
|
|
|
55
55
|
// AFTER INSERT triggers on credit_log + karma_log respectively.
|
|
56
56
|
const { rows } = await pool.query(
|
|
57
57
|
`SELECT id, github_id, github_login, display_name, avatar_url, total_credits, karma, created_at, rank,
|
|
58
|
-
status, deactivated_at, reactivated_at, deactivated_reason, preferred_disciplines, system_role,
|
|
58
|
+
status, deactivated_at, reactivated_at, deactivated_reason, preferred_disciplines, main_discipline, system_role,
|
|
59
59
|
anthropic_email, anthropic_user_id, monthly_budget_usd
|
|
60
60
|
FROM builders WHERE id = $1`,
|
|
61
61
|
[id]
|
|
@@ -129,7 +129,27 @@ function contributeCoreUpdateToDocket(read = readCoreUpdate) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// This hall's own update rule, from the provisioning module when it runs (task 1004535):
|
|
133
|
+
// the rule set on /deploy, which the runner and the sweep already follow (ADR 0357: the
|
|
134
|
+
// /deploy row beats the sweep's roster file, which lives in the box's own checkout and is
|
|
135
|
+
// not this server's to read). No module, no row, or a failed read → null, and the readers
|
|
136
|
+
// fall back to the platform default exactly as before.
|
|
137
|
+
function storedSelfChannel() {
|
|
138
|
+
const read = seams.resolveOptional('provisioning.selfUpdateChannel');
|
|
139
|
+
return typeof read === 'function' ? read() : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// { channel, source } — WHICH rule the answer was computed under, so the page can say so:
|
|
143
|
+
// 'deploy' (the project's stored rule) or 'default' (none could be read). Never throws.
|
|
144
|
+
async function resolveChannel(selfChannel) {
|
|
145
|
+
let channel = null;
|
|
146
|
+
try { channel = (await selfChannel()) || null; } catch { channel = null; }
|
|
147
|
+
return { channel, source: channel ? 'deploy' : 'default' };
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
function buildCoreUpdateRouter({
|
|
151
|
+
coreUpdate = readCoreUpdate,
|
|
152
|
+
selfChannel = storedSelfChannel,
|
|
133
153
|
upgrades = readCoreUpgrades,
|
|
134
154
|
softwareUpdate = readSoftwareUpdate,
|
|
135
155
|
canUpdate = holdsCorePin,
|
|
@@ -155,7 +175,8 @@ function buildCoreUpdateRouter({
|
|
|
155
175
|
try { running = require('../../module-api').CORE_VERSION; } catch { /* version unknown */ }
|
|
156
176
|
// No `deploy_url` any more (task 1004296): the banner on a hall without a door of its
|
|
157
177
|
// own links to Settings → Software update, and the hand-off lives on GET /software-update.
|
|
158
|
-
|
|
178
|
+
const { channel, source } = await resolveChannel(selfChannel);
|
|
179
|
+
res.json({ ok: true, update: { ...(await coreUpdate({ running, channel })), channel_source: source } });
|
|
159
180
|
}, { errorCode: 'core_update_failed', message: false }),
|
|
160
181
|
);
|
|
161
182
|
|
|
@@ -186,7 +207,11 @@ function buildCoreUpdateRouter({
|
|
|
186
207
|
asyncHandler('GET /software-update', async (req, res) => {
|
|
187
208
|
let running = null;
|
|
188
209
|
try { running = require('../../module-api').CORE_VERSION; } catch { /* version unknown */ }
|
|
189
|
-
|
|
210
|
+
// canUpdate does not depend on the rule, so it runs beside the rule read, not after it.
|
|
211
|
+
const [update, can] = await Promise.all([
|
|
212
|
+
resolveChannel(selfChannel).then(async ({ channel, source }) => ({ ...(await softwareUpdate({ running, channel })), channel_source: source })),
|
|
213
|
+
canUpdate(req),
|
|
214
|
+
]);
|
|
190
215
|
res.json({ ok: true, update, door: updateDoor(doorInputs()), can_update: can === true });
|
|
191
216
|
}, { errorCode: 'software_update_failed', message: false }),
|
|
192
217
|
);
|
|
@@ -941,6 +941,16 @@ function mountInternalSurfaces(app) {
|
|
|
941
941
|
// statically, and a comment in that gap drops the regex out of the map, which
|
|
942
942
|
// makes the page test as UNGATED while looking gated here. Prose goes above.
|
|
943
943
|
const BOARD_ROOM_PAGE_RE = /^\/builders\/board-room(?:\.html)?\/?$/;
|
|
944
|
+
// /city — the Governor City's home (task 1004524, goal 1000125). Gated on the
|
|
945
|
+
// `board.vote.cast` atom, like the Board Room, because that is the atom its one
|
|
946
|
+
// governed read (GET /government/docket) checks: the page's reach equals the
|
|
947
|
+
// right to read what it draws, so no holder is refused a city they could read
|
|
948
|
+
// and no one else is served its shell. Its two goal reads are requireBuilder,
|
|
949
|
+
// a wider floor.
|
|
950
|
+
//
|
|
951
|
+
// NB as at PROJECT_SETTINGS_PAGE_RE: nothing between the `=` and the literal
|
|
952
|
+
// (tests/hall_page_gate_map.mjs parses this file statically). Prose goes above.
|
|
953
|
+
const CITY_PAGE_RE = /^\/builders\/city(?:\.html)?\/?$/;
|
|
944
954
|
// /deploy — the owner's deploy door (task 1003159, ADR 0293). Gated on the
|
|
945
955
|
// `core.pin.move` atom, the same key its preview and its move both check, so the
|
|
946
956
|
// shell's reach equals the right to press the button. Archon-floor and system:true,
|
|
@@ -1023,6 +1033,10 @@ function mountInternalSurfaces(app) {
|
|
|
1023
1033
|
if (BOARD_ROOM_PAGE_RE.test(norm)) {
|
|
1024
1034
|
return gdsAuth.requireBoardVotePage(req, res, next);
|
|
1025
1035
|
}
|
|
1036
|
+
// task 1004524: the Governor City, on the docket's own atom (see CITY_PAGE_RE).
|
|
1037
|
+
if (CITY_PAGE_RE.test(norm)) {
|
|
1038
|
+
return gdsAuth.requireBoardVotePage(req, res, next);
|
|
1039
|
+
}
|
|
1026
1040
|
// ADR 0293: the deploy door, on the same atom its two routes check.
|
|
1027
1041
|
if (DEPLOY_PAGE_RE.test(norm)) {
|
|
1028
1042
|
return gdsAuth.requireCorePinPage(req, res, next);
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
//
|
|
19
19
|
// THE CHANNEL RULE IS NOT RESTATED HERE, as in core-update.js: `resolveChannelTarget` is the
|
|
20
20
|
// upgrade runner's own function, so this never offers a version the runner would refuse.
|
|
21
|
-
//
|
|
22
|
-
//
|
|
21
|
+
// The channel is the project's own update rule when the route can read it (the rule set on
|
|
22
|
+
// /deploy, via the provisioning.selfUpdateChannel seam — task 1004535), else the runner's
|
|
23
|
+
// default. The banner (core-update.js) is handed the same rule, so the two cannot disagree.
|
|
23
24
|
//
|
|
24
25
|
// A READING, NEVER A GUESS. `reading` is 'ok' only when the registry answered; "up to date"
|
|
25
26
|
// is `offered: null` on an ok reading and nothing else. Notes that could not be read say so,
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.6'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -74,7 +74,9 @@ const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg
|
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
76
|
for (const f of ['platform_identity_005_builder_activity.sql', 'platform_identity_012_recent_ships.sql',
|
|
77
|
-
'platform_identity_022_activity_snapshots.sql', 'platform_identity_026_activity_reported_as_of.sql'
|
|
77
|
+
'platform_identity_022_activity_snapshots.sql', 'platform_identity_026_activity_reported_as_of.sql',
|
|
78
|
+
// upsertActivity also writes crafts (029, task 1004440): 42703 without it.
|
|
79
|
+
'platform_identity_029_activity_crafts.sql']) {
|
|
78
80
|
await q(migrationSql(f));
|
|
79
81
|
}
|
|
80
82
|
await q(migrationSql('platform_identity_026_activity_reported_as_of.sql')); // idempotent: replays cleanly
|
|
@@ -99,6 +99,8 @@ try {
|
|
|
99
99
|
await q(migrationSql('platform_identity_022_activity_snapshots.sql'));
|
|
100
100
|
// upsertActivity also writes reported_as_of (task 1004268) — same reason as 012 above.
|
|
101
101
|
await q(migrationSql('platform_identity_026_activity_reported_as_of.sql'));
|
|
102
|
+
// ...and each craft's counts (029, task 1004440), same reason again.
|
|
103
|
+
await q(migrationSql('platform_identity_029_activity_crafts.sql'));
|
|
102
104
|
await cleanup();
|
|
103
105
|
|
|
104
106
|
// 1 — the writer snapshots the LIVE row, and only when it changed.
|