@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
|
@@ -580,6 +580,9 @@ module.exports = function ssoRoutes() {
|
|
|
580
580
|
// here, like disciplines: the shape is checked and hard-capped in
|
|
581
581
|
// upsertActivity, next to the data, so no route can forget it.
|
|
582
582
|
recent_ships: {},
|
|
583
|
+
// Each craft's own counts (task 1004440): untyped here for the same reason,
|
|
584
|
+
// shaped and capped in upsertActivity (craft-rollup.js sanitizeCrafts).
|
|
585
|
+
crafts: {},
|
|
583
586
|
})) return;
|
|
584
587
|
const b = req.body || {};
|
|
585
588
|
try {
|
|
@@ -617,6 +620,7 @@ module.exports = function ssoRoutes() {
|
|
|
617
620
|
rank: b.rank || null,
|
|
618
621
|
disciplines: Array.isArray(b.disciplines) ? b.disciplines : [],
|
|
619
622
|
recentShips: b.recent_ships,
|
|
623
|
+
crafts: b.crafts,
|
|
620
624
|
// The AUTHENTICATED client's registered origin — the bound every pushed
|
|
621
625
|
// ship link must fall inside (task 1002803). Taken from the client row,
|
|
622
626
|
// never from the body, for the same reason client_id is.
|
|
@@ -602,7 +602,7 @@ await ta('upsertActivity upserts per (client_id, github_id) with the talent snap
|
|
|
602
602
|
// an empty list when the sender reported none, never NULL. Its own cap,
|
|
603
603
|
// shape and link bound are covered by tests/hub_recent_ships.mjs. The last param
|
|
604
604
|
// is the report's as-of stamp (task 1004268): null when the sender gave none.
|
|
605
|
-
assert.deepEqual(c.params, ['c1', 42, 100, 5, 3, 'metic', ['engineer'], '[]', null]);
|
|
605
|
+
assert.deepEqual(c.params, ['c1', 42, 100, 5, 3, 'metic', ['engineer'], '[]', null, '{}']);
|
|
606
606
|
});
|
|
607
607
|
|
|
608
608
|
await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-ladder rank → null, disciplines validated + capped', async () => {
|
|
@@ -250,14 +250,22 @@ function makeCoreUpgradeHandler({ provisioning, pool, parseId, validateOrRespond
|
|
|
250
250
|
* and not asking it is what keeps the one comparison in the system (the runner's channel
|
|
251
251
|
* resolution) the only one that can be wrong.
|
|
252
252
|
*
|
|
253
|
-
* An
|
|
254
|
-
* Not knowing what is on offer is not evidence that the box recovered,
|
|
255
|
-
* to fail in is showing an old failure, never hiding a live one.
|
|
253
|
+
* An UNREAD registry — `registryRead` false, the snapshot's `available` empty — leaves a
|
|
254
|
+
* failure STANDING. Not knowing what is on offer is not evidence that the box recovered,
|
|
255
|
+
* and the direction to fail in is showing an old failure, never hiding a live one. A
|
|
256
|
+
* registry that answered with nothing on offer is different: the box is current, and a
|
|
257
|
+
* failure about some other version is history (task 1004534).
|
|
256
258
|
*/
|
|
257
|
-
function lastMoveSuperseded(target, served, recommended) {
|
|
259
|
+
function lastMoveSuperseded(target, served, recommended, registryRead = false) {
|
|
258
260
|
if (!target) return false;
|
|
259
261
|
if (served && String(served) === String(target)) return true;
|
|
260
|
-
|
|
262
|
+
if (recommended) return String(recommended) !== String(target);
|
|
263
|
+
// NOTHING ON OFFER FROM A REGISTRY THAT ANSWERED is an up-to-date box, not an unknown
|
|
264
|
+
// (task 1004534). The snapshot stores an empty `available` when the registry could not
|
|
265
|
+
// be read, so only that case leaves the failure standing. On 2026-10-02 cloudbongos
|
|
266
|
+
// served 1.21.5 with nothing newer in its rule, and a day-old failure about 1.20.73
|
|
267
|
+
// still led its /deploy page because a null recommendation was read as "unknown".
|
|
268
|
+
return !!(registryRead && served);
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
/**
|
|
@@ -371,7 +379,7 @@ function previewFromSnapshot(inst, snap, openIntent, lastMove) {
|
|
|
371
379
|
// already moved past (task 1004172 — lastMoveSuperseded says why at length). The
|
|
372
380
|
// CLIENT must not work this out: the page's standing rule is that it compares no
|
|
373
381
|
// versions, and this is a version question wearing a different hat.
|
|
374
|
-
superseded: lastMoveSuperseded(lastMove.target_version, served, recommended),
|
|
382
|
+
superseded: lastMoveSuperseded(lastMove.target_version, served, recommended, available.length > 0),
|
|
375
383
|
// Truncated because it reaches a DOM and upgrade.js's refusals can carry a whole
|
|
376
384
|
// `git status` (the dirty-tree one lists every file). Never set on a DONE move:
|
|
377
385
|
// a done row's last_error is the note below, and a move that worked is not an error.
|
|
@@ -468,6 +476,25 @@ function updateChannelOf(inst) {
|
|
|
468
476
|
return UPDATE_CHANNELS.includes(c) ? c : DEFAULT_UPDATE_CHANNEL;
|
|
469
477
|
}
|
|
470
478
|
|
|
479
|
+
/**
|
|
480
|
+
* THIS hall's own update rule — the control-plane row's — for the offers the core makes
|
|
481
|
+
* outside /deploy: Settings → Software update and the hall's update banner (task 1004535).
|
|
482
|
+
* Those live in the core, which cannot read a module's table, so this is handed to them
|
|
483
|
+
* through the `provisioning.selfUpdateChannel` seam. Before it they asked with the platform
|
|
484
|
+
* default (patch) while the runner and the sweep followed the stored rule (minor on
|
|
485
|
+
* cloudbongos), so a step-up release would have read "up to date" there as it installed.
|
|
486
|
+
* null — no row, an unknown value, a failed read — means "use the default"; never throws.
|
|
487
|
+
*/
|
|
488
|
+
async function selfUpdateChannel(db) {
|
|
489
|
+
try {
|
|
490
|
+
const { rows } = await db.query("SELECT update_channel FROM provisioning_instances WHERE hosting_shape = 'control-plane' LIMIT 1");
|
|
491
|
+
const c = rows[0] && rows[0].update_channel;
|
|
492
|
+
return UPDATE_CHANNELS.includes(c) ? c : null;
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
471
498
|
/**
|
|
472
499
|
* POST /provisioning/instances/:id/update-channel — set how far this project's core
|
|
473
500
|
* may move (task 1004445). Body: { channel: 'pinned' | 'patch' | 'minor' }.
|
|
@@ -518,5 +545,5 @@ function makeUpdateChannelHandler({ provisioning, pool, parseId, validateOrRespo
|
|
|
518
545
|
};
|
|
519
546
|
}
|
|
520
547
|
|
|
521
|
-
module.exports = { UPDATE_CHANNELS, DEFAULT_UPDATE_CHANNEL, updateChannelOf, makeUpdateChannelHandler, CORE_UPGRADE_FROM, lastCoreMove, lastMoveSuperseded, parseRepoNote, preflightForOffer, UPGRADE_SHAPES, VERSION_RE, isCoreVersion, coreUpgradeFault, enqueueCoreUpgrade, makeCoreUpgradeHandler, previewFromSnapshot, makeCoreUpgradePreviewHandler, makeControlPlaneHandler };
|
|
548
|
+
module.exports = { UPDATE_CHANNELS, DEFAULT_UPDATE_CHANNEL, updateChannelOf, selfUpdateChannel, makeUpdateChannelHandler, CORE_UPGRADE_FROM, lastCoreMove, lastMoveSuperseded, parseRepoNote, preflightForOffer, UPGRADE_SHAPES, VERSION_RE, isCoreVersion, coreUpgradeFault, enqueueCoreUpgrade, makeCoreUpgradeHandler, previewFromSnapshot, makeCoreUpgradePreviewHandler, makeControlPlaneHandler };
|
|
522
549
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const api = require('../../src/module-api');
|
|
7
7
|
const { pool } = api;
|
|
8
8
|
const provisioning = require('./provisioning');
|
|
9
|
+
const { selfUpdateChannel } = require('./core-upgrade');
|
|
9
10
|
|
|
10
11
|
// The `provisioning.projectPublishIdentity` port (task 1003578, ADR 0247 D4): the name and
|
|
11
12
|
// description the publish gate judges, for the control-plane project a hub catalog row was
|
|
@@ -49,6 +50,7 @@ function registerProvisioningSeams() {
|
|
|
49
50
|
api.registerProvider('provisioning.projectPlanets', (hosts) => provisioning.planetChoicesForHosts(pool, hosts));
|
|
50
51
|
api.registerProvider('provisioning.projectOwnedBy', (target) => provisioning.projectOwnedBy(pool, target));
|
|
51
52
|
api.registerProvider('provisioning.projectPublishIdentity', (target) => projectPublishIdentity(pool, target));
|
|
53
|
+
api.registerProvider('provisioning.selfUpdateChannel', () => selfUpdateChannel(pool));
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
module.exports = { registerProvisioningSeams, projectPublishIdentity };
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"in-new-terms-refused": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["click","#termsAck"],["wait",200]],"expect":{"visible":["#panel-fork"],"hidden":["#panel-what"],"attr":[["#wizStart","disabled",""],["#wizDemo","disabled",""]]}},
|
|
42
42
|
"in-new-fork-demo": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizDemo"],["wait",300]],"expect":{"visible":["#panel-fork","#demoSetup","#demoFitVerdict","#demoInvites","#wizBack"],"hidden":["#forkIntro"],"attr":[["#wizDemo","aria-expanded","true"]]}},
|
|
43
43
|
"in-new-demo-what": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizDemo"],["wait",300],["fill","#demoIdea","A voting app for movie night"],["click","#demoStart"],["wait",400]],"expect":{"visible":["#panel-what","#demoTag"],"hidden":["#panel-fork"],"text":[["#wizEyebrow","New demo"]]}},
|
|
44
|
+
"in-new-demo-review-address": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizDemo"],["wait",300],["fill","#demoIdea","A voting app for movie night"],["click","#demoStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-what button[type=submit]"],["wait",700],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",300],["fill","#f-slug","mercury"],["wait",900]],"expect":{"visible":["#panel-review","#reviewRows .setRow[data-row=\"address\"] .opts","#f-slug","#addrConfirm","#f-noaddr"],"hidden":["#panel-checks"],"attr":[["#reviewGo","disabled",""]],"text":[["#wizEyebrow","New demo"]]}},
|
|
44
45
|
"in-new-what": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400]],"expect":{"visible":["#panel-what","#newName","#typeCards","#f-desc","#wizBack"],"hidden":["#panel-fork","#demoTag"],"attr":[["#wizRoom","data-screen","what"]]}},
|
|
45
46
|
"in-new-name-prefilled": {"auth":true,"url":"/projects?view=new&name=Mercury","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400]],"expect":{"visible":["#panel-what","#newName"]}},
|
|
46
47
|
"in-new-checks": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#panel-what button[type=submit]"],["wait",400]],"expect":{"visible":["#panel-checks","#physCountry","#physIntent"],"hidden":["#physAges","#physForms","#checksStop"],"attr":[["#wizRoom","data-screen","checks"]]}},
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.6",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.6",
|
|
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.21.
|
|
3
|
+
"version": "1.21.6",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -8730,5 +8730,63 @@
|
|
|
8730
8730
|
"id": "1004439",
|
|
8731
8731
|
"text": "The builders' Discord feed now announces artist and ideator moments, not only shipped tasks. When the board ratifies an idea, or a Full Idea passes its completeness check, the feed posts one line naming the person, with a link"
|
|
8732
8732
|
}
|
|
8733
|
+
],
|
|
8734
|
+
"1.21.6": [
|
|
8735
|
+
{
|
|
8736
|
+
"id": "1004524",
|
|
8737
|
+
"text": "The governor now has a home: a small animated city at /builders/city, drawn exactly like the prototype. The nine civic buildings stand round the plaza, and every open goal is a district with a building for each of its tasks: f"
|
|
8738
|
+
},
|
|
8739
|
+
{
|
|
8740
|
+
"id": "1004542",
|
|
8741
|
+
"text": "Releases are unblocked. Three database tests had stopped passing since the hub-profile change, because they built their test database without its new column, and that held up every new release. They now include it."
|
|
8742
|
+
},
|
|
8743
|
+
{
|
|
8744
|
+
"id": "1004541",
|
|
8745
|
+
"text": "Builders can now choose their main role in Settings. Under their roles they pick which one leads their profile and their leaderboard entry, or let their work decide. Unticking a role that was their main one hands the choice ba"
|
|
8746
|
+
},
|
|
8747
|
+
{
|
|
8748
|
+
"id": "1004507",
|
|
8749
|
+
"text": "Every new project, demos included, now asks the founder to confirm its web address before anything is set up. The suggested address is only a suggestion until they confirm or change it."
|
|
8750
|
+
},
|
|
8751
|
+
{
|
|
8752
|
+
"id": "1004538",
|
|
8753
|
+
"text": "Autobongos no longer raises a false \"shipped but no code\" alarm when a merge is just slow. It waits up to a day, keeps re-checking, and closes the alarm by itself once the code lands. The stale alarm about task 1004513 is now"
|
|
8754
|
+
},
|
|
8755
|
+
{
|
|
8756
|
+
"id": "1004539",
|
|
8757
|
+
"text": "Autobongos now tells you when it is idle because of a setting you can fix, like no goals picked, instead of showing \"alive\" for hours. After a short grace, the Gate page and Settings show a warning with the fix, and Discord ge"
|
|
8758
|
+
},
|
|
8759
|
+
{
|
|
8760
|
+
"id": "1004536",
|
|
8761
|
+
"text": "Autobongos now stops after a task fails the grade twice in one run and hands it to the owner instead of paying for more grade rounds. Grader outages don't count toward the two."
|
|
8762
|
+
},
|
|
8763
|
+
{
|
|
8764
|
+
"id": "1004535",
|
|
8765
|
+
"text": "The Software update page in Settings and the update banner at the top of the hall now go by the update rule each project chose on /deploy. They no longer fall back to the platform default, so they can no longer say \"up to date"
|
|
8766
|
+
},
|
|
8767
|
+
{
|
|
8768
|
+
"id": "1004534",
|
|
8769
|
+
"text": "The deploy page no longer tells an up-to-date project that an update is \"waiting on your decision\". An update that had already happened was being reported as blocked by the update rule; it now counts as done, and an old failur"
|
|
8770
|
+
},
|
|
8771
|
+
{
|
|
8772
|
+
"id": "1004520",
|
|
8773
|
+
"text": "The nightly update check no longer trips over test4, a deleted test project, so its skip count now only shows real projects."
|
|
8774
|
+
},
|
|
8775
|
+
{
|
|
8776
|
+
"id": "1004299",
|
|
8777
|
+
"text": "New projects now start on the version the owner released, not on whatever unreleased version the Bongos hall happens to be running."
|
|
8778
|
+
},
|
|
8779
|
+
{
|
|
8780
|
+
"id": "1004451",
|
|
8781
|
+
"text": "cloudbongos.com now takes a step-up release on its own because of the update rule set on /deploy: on 1 Oct it moved from 1.20.83 to 1.21.1 unattended, under the \"minor\" rule, while the old roster file still said \"patch\"."
|
|
8782
|
+
},
|
|
8783
|
+
{
|
|
8784
|
+
"id": "1004519",
|
|
8785
|
+
"text": "hoodify-finance had been stuck on an old version since mid-September; the update lane now recognises the files its own last update left behind, so it can resume taking updates."
|
|
8786
|
+
},
|
|
8787
|
+
{
|
|
8788
|
+
"id": "1004440",
|
|
8789
|
+
"text": "A builder's profile on the Cloud Bongos hub now shows each craft's own work. An artist sees their approved pages, an ideator sees their filed, Full and board-ratified ideas, and the roles shown come from the work someone has a"
|
|
8790
|
+
}
|
|
8733
8791
|
]
|
|
8734
8792
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/gds/autobongos-grade-cap.js — the grade cap for ONE autonomous run
|
|
4
|
+
// (task 1004536).
|
|
5
|
+
//
|
|
6
|
+
// THE RULE (owner, 2026-10-02): a task that fails the grade twice goes to the
|
|
7
|
+
// owner instead of being retried. In one worker run a task gets its first grade
|
|
8
|
+
// plus at most ONE re-grade. Evidence for why: grader-refusal loops were 48% of
|
|
9
|
+
// all Autobongos spend for zero ships — task 1004512 cost $63.81 over three grade
|
|
10
|
+
// rounds, task 1003564 $13.74. The worker prompt already said "return
|
|
11
|
+
// cannot_complete if the grader refuses", and the worker ignored it. A prompt
|
|
12
|
+
// line is not a limit.
|
|
13
|
+
//
|
|
14
|
+
// WHERE IT IS ENFORCED — one boundary, one courtesy:
|
|
15
|
+
// 1. THE BOUNDARY is the supervisor (autobongos-run.js), and its count comes
|
|
16
|
+
// from the SERVER: every graded round is POSTed to /tasks/:id/grade and
|
|
17
|
+
// appended to `grade_attempts` (an append-only table the worker has no write
|
|
18
|
+
// path to). The supervisor notes the newest attempt before spawning, counts
|
|
19
|
+
// the rounds after it (roundsFromAttempts), kills a worker still running
|
|
20
|
+
// once two graded FAILs exist, and ends the run as needs_human whatever the
|
|
21
|
+
// worker's verdict says. Nothing on the worker's machine — env, files —
|
|
22
|
+
// feeds this count, so a worker cannot erase it. (A round the worker never
|
|
23
|
+
// POSTs cannot advance the task either, so it is not a way around the cap.)
|
|
24
|
+
// 2. THE COURTESY is ship.js refusing to START a third grade (checkGradeCap at
|
|
25
|
+
// its entry, before any flag is read). The worker's own process holds that
|
|
26
|
+
// check's inputs — a run ledger named in env — so it is NOT the boundary; a
|
|
27
|
+
// worker could clear the variable. It exists because it is free and it stops
|
|
28
|
+
// an honest-but-stubborn worker before a third panel is paid for, rather
|
|
29
|
+
// than up to one supervisor poll later.
|
|
30
|
+
//
|
|
31
|
+
// The local ledger is ACTIVE ONLY when AUTOBONGOS_GRADE_LEDGER is set, which only
|
|
32
|
+
// the supervisor does. A human running ship.js by hand is never capped.
|
|
33
|
+
//
|
|
34
|
+
// A GRADER OUTAGE DOES NOT COUNT. `signals.panel_outcome === 'unavailable'` means
|
|
35
|
+
// the panel could not run (#943) — no findings, nothing to fix, not a quality
|
|
36
|
+
// verdict. Counting it would hand a task to the owner because the grader was down.
|
|
37
|
+
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
const LEDGER_ENV = 'AUTOBONGOS_GRADE_LEDGER';
|
|
42
|
+
// The first grade + ONE re-grade. Not configurable on purpose: a cap the worker
|
|
43
|
+
// could raise through env would be a cap it could talk its way past.
|
|
44
|
+
const GRADE_CAP = 2;
|
|
45
|
+
// ship.js's exit code for a capped run. Distinct from 1 (generic) and 2 (usage /
|
|
46
|
+
// refused input) so a supervisor or a test can tell the cap from any other stop.
|
|
47
|
+
const REFUSED_EXIT = 6;
|
|
48
|
+
|
|
49
|
+
function ledgerPathFromEnv(env = process.env) {
|
|
50
|
+
const p = env[LEDGER_ENV];
|
|
51
|
+
return p && String(p).trim() ? String(p).trim() : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// isOutage — the grader never ran. Mirrors the #943 test the ship card and
|
|
55
|
+
// ship-grade-steps use (signals.panel_outcome === 'unavailable').
|
|
56
|
+
function isOutage(grade) {
|
|
57
|
+
return !!(grade && grade.signals && grade.signals.panel_outcome === 'unavailable');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// readLedger — every row, oldest first. A missing or unreadable file is an empty
|
|
61
|
+
// ledger; a malformed line is skipped rather than fatal.
|
|
62
|
+
function readLedger(file) {
|
|
63
|
+
if (!file) return [];
|
|
64
|
+
let raw = '';
|
|
65
|
+
try { raw = fs.readFileSync(file, 'utf8'); } catch (_) { return []; }
|
|
66
|
+
const rows = [];
|
|
67
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
68
|
+
if (!line.trim()) continue;
|
|
69
|
+
try { rows.push(JSON.parse(line)); } catch (_) { /* skip a torn line */ }
|
|
70
|
+
}
|
|
71
|
+
return rows;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// gradeCapState — pure over ledger rows: how many GRADED rounds (outages
|
|
75
|
+
// excluded) this task has had in the run, and whether the cap is reached.
|
|
76
|
+
function gradeCapState(rows, taskId) {
|
|
77
|
+
const mine = (rows || []).filter((r) => r && String(r.task_id) === String(taskId));
|
|
78
|
+
const graded = mine.filter((r) => !r.outage);
|
|
79
|
+
const fails = graded.filter((r) => r.passed !== true);
|
|
80
|
+
const passed = graded.some((r) => r.passed === true);
|
|
81
|
+
return {
|
|
82
|
+
graded: graded.length,
|
|
83
|
+
fails: fails.length,
|
|
84
|
+
outages: mine.length - graded.length,
|
|
85
|
+
passed,
|
|
86
|
+
capped: graded.length >= GRADE_CAP,
|
|
87
|
+
// The run should be handed to the owner: the cap is reached and none passed.
|
|
88
|
+
exhausted: !passed && fails.length >= GRADE_CAP,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// checkGradeCap — may ship.js start a grade for this task? `{ ok: true }` when no
|
|
93
|
+
// ledger is in play (a human ship) or the cap has room.
|
|
94
|
+
function checkGradeCap({ taskId, env = process.env } = {}) {
|
|
95
|
+
const file = ledgerPathFromEnv(env);
|
|
96
|
+
if (!file) return { ok: true, active: false };
|
|
97
|
+
const state = gradeCapState(readLedger(file), taskId);
|
|
98
|
+
// `exhausted`, not `capped`: a task whose second round PASSED still needs
|
|
99
|
+
// ship.js to resume its merge leg, and that must not be refused.
|
|
100
|
+
if (!state.exhausted) return { ok: true, active: true, state };
|
|
101
|
+
return {
|
|
102
|
+
ok: false, active: true, state,
|
|
103
|
+
lines: [
|
|
104
|
+
`Ship refused: task ${taskId} has already been graded ${state.graded} time(s) in this autonomous run — the cap is the first grade plus ONE re-grade (task 1004536).`,
|
|
105
|
+
'A task that fails the grade twice goes to the owner instead of being retried.',
|
|
106
|
+
'Stop now and return cannot_complete, quoting the grader\'s findings — the supervisor files the blocker.',
|
|
107
|
+
'(A GRADER UNAVAILABLE outcome is an outage and was not counted.)',
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// recordGradeOutcome — append one graded round. Best-effort: a write failure
|
|
113
|
+
// must never fail the ship that just graded.
|
|
114
|
+
function recordGradeOutcome({ taskId, grade, env = process.env, now = () => new Date().toISOString() } = {}) {
|
|
115
|
+
const file = ledgerPathFromEnv(env);
|
|
116
|
+
if (!file || !grade) return false;
|
|
117
|
+
const row = {
|
|
118
|
+
task_id: Number(taskId),
|
|
119
|
+
at: now(),
|
|
120
|
+
passed: grade.passed === true,
|
|
121
|
+
outage: isOutage(grade),
|
|
122
|
+
score: grade.score ?? null,
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
126
|
+
fs.appendFileSync(file, `${JSON.stringify(row)}\n`);
|
|
127
|
+
return true;
|
|
128
|
+
} catch (_) { return false; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── the server's count (the boundary) ───────────────────────────────────────────
|
|
132
|
+
//
|
|
133
|
+
// `GET /tasks/:id?include=grade,grade_attempts` serves each recorded round as
|
|
134
|
+
// { attempted_at, passed, workers: [{ kind, verdict, error, severities }] }.
|
|
135
|
+
// The grader's own outage test (#943, grader.js) is "every GATING worker failed
|
|
136
|
+
// to run"; the Narc is advisory (#481), so it is left out of that test the same
|
|
137
|
+
// way. A round with no gating workers listed is not called an outage — it is
|
|
138
|
+
// counted, because an unknown shape must not silently buy another round.
|
|
139
|
+
const ADVISORY_KINDS = new Set(['narc']);
|
|
140
|
+
function attemptIsOutage(attempt) {
|
|
141
|
+
const ws = Array.isArray(attempt && attempt.workers) ? attempt.workers : [];
|
|
142
|
+
const gating = ws.filter((w) => w && !ADVISORY_KINDS.has(w.kind));
|
|
143
|
+
return gating.length > 0 && gating.every((w) => w.error != null && w.error !== '');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// newestAttemptAt — the baseline, read BEFORE the worker starts. Rounds are
|
|
147
|
+
// counted by being newer than this server timestamp, so the supervisor's own
|
|
148
|
+
// clock never enters the comparison.
|
|
149
|
+
function newestAttemptAt(attempts) {
|
|
150
|
+
let newest = null;
|
|
151
|
+
for (const a of attempts || []) {
|
|
152
|
+
const t = Date.parse(a && a.attempted_at);
|
|
153
|
+
if (Number.isFinite(t) && (newest === null || t > newest)) newest = t;
|
|
154
|
+
}
|
|
155
|
+
return newest;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// roundsFromAttempts — the server's rounds after the baseline, in ledger-row
|
|
159
|
+
// shape so gradeCapState reads both sources the same way.
|
|
160
|
+
function roundsFromAttempts(attempts, taskId, afterMs = null) {
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const a of attempts || []) {
|
|
163
|
+
const t = Date.parse(a && a.attempted_at);
|
|
164
|
+
if (!Number.isFinite(t) || (afterMs !== null && t <= afterMs)) continue;
|
|
165
|
+
out.push({ task_id: Number(taskId), at: a.attempted_at, passed: a.passed === true, outage: attemptIsOutage(a) });
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// readServerAttempts — the task's recorded rounds, or null when unreadable. Null
|
|
171
|
+
// is "unknown", never "zero": the caller falls back to the local ledger.
|
|
172
|
+
async function readServerAttempts(api, taskId) {
|
|
173
|
+
try {
|
|
174
|
+
if (!api || !api.tasks || typeof api.tasks.getTasksId !== 'function') return null;
|
|
175
|
+
const r = await api.tasks.getTasksId({ id: Number(taskId), query: { include: 'grade,grade_attempts' } });
|
|
176
|
+
if (!r || !r.ok || !r.data) return null;
|
|
177
|
+
const grade = r.data.grade;
|
|
178
|
+
if (!grade) return []; // never graded: a real zero
|
|
179
|
+
return Array.isArray(grade.attempts) ? grade.attempts : null;
|
|
180
|
+
} catch (_) { return null; }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
LEDGER_ENV, GRADE_CAP, REFUSED_EXIT,
|
|
185
|
+
ledgerPathFromEnv, isOutage, readLedger, gradeCapState, checkGradeCap, recordGradeOutcome,
|
|
186
|
+
attemptIsOutage, newestAttemptAt, roundsFromAttempts, readServerAttempts,
|
|
187
|
+
};
|
|
@@ -103,6 +103,10 @@ function buildWorkerPrompt({ task, packPath = 'docs/packs/engineer.md' } = {}) {
|
|
|
103
103
|
'',
|
|
104
104
|
'Ship it yourself when the work is done and verified: `node scripts/gds/ship.js <id> --no-surprise --notes \"...\" --summary \"...\"`. Do NOT pass --skip-grade and do not force any gate; if the grader or the merge refuses, that is the system protecting main — return cannot_complete and say so.',
|
|
105
105
|
'',
|
|
106
|
+
// task 1004536: stated here so the worker does not waste a session trying —
|
|
107
|
+
// but ship.js and the supervisor enforce it, because this line alone was ignored.
|
|
108
|
+
'GRADE CAP: this run gets the first grade plus at most ONE re-grade (`node scripts/gds/ship.js <id> --regrade`). If the grade fails twice, STOP: do not fix and re-grade again — return cannot_complete with the grader\'s findings quoted, and the task goes to the owner. ship.js refuses a third grade in this run and the supervisor ends it. A "GRADER UNAVAILABLE" outcome is an outage, not a fail, and does not count toward the two.',
|
|
109
|
+
'',
|
|
106
110
|
'When you are done, return the structured verdict. `undetermined_decisions` must list every choice you made that the spec did not settle — retry counts, naming, error shapes, anything a reviewer would want to have been asked about. An empty list is a claim that the spec determined everything; do not make it lightly.',
|
|
107
111
|
].join('\n');
|
|
108
112
|
}
|