@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -140,10 +140,25 @@ async function lookupInstanceRow(instanceDir, deps = {}) {
|
|
|
140
140
|
} catch { return null; }
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// What upgrade.js's pin push says when lookupInstanceRow found NO row on a checkout wired
|
|
144
|
+
// with a read-only deploy key (task 1004065). PURE.
|
|
145
|
+
//
|
|
146
|
+
// That pairing is NOT the origin path the header above describes. The key says this is a
|
|
147
|
+
// hosted private project (ADR 0176), and its own database never has the provisioning row
|
|
148
|
+
// (ADR 0281) — so the owner's token cannot be looked up here, and the remote the push
|
|
149
|
+
// falls back to cannot write. Silence made that indistinguishable from a misconfiguration.
|
|
150
|
+
// A missing row anywhere else IS the origin path and stays quiet, which is why the caller
|
|
151
|
+
// only asks for this line when the key is present. Lives here rather than beside its
|
|
152
|
+
// caller because upgrade.js sits exactly at the oversized-file ratchet.
|
|
153
|
+
function missingRowNote(key, remote = 'origin') {
|
|
154
|
+
return ` • pin push: no provisioning row is readable from this database, so the owner's GitHub token cannot be looked up here — and ${remote} is the read-only deploy key at ${key}. A hosted project's own database never has that row (ADR 0281); the deploy door pushes the pin from the hub instead (task 1004065).`;
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
module.exports = {
|
|
144
158
|
instanceRepoRemote,
|
|
145
159
|
instanceRepoRemotes,
|
|
146
160
|
lookupInstanceRow,
|
|
161
|
+
missingRowNote,
|
|
147
162
|
parseTargetRef,
|
|
148
163
|
resolveOwnerGithubToken,
|
|
149
164
|
};
|
package/scripts/gds/provision.js
CHANGED
|
@@ -1308,7 +1308,7 @@ async function cmdRunIntents(deps) {
|
|
|
1308
1308
|
await db.query(`UPDATE provisioning_intents SET state='pending', last_error=$2, updated_at=now() WHERE id=$1`, [intent.id, result.error || 'op failed']);
|
|
1309
1309
|
}
|
|
1310
1310
|
errors++;
|
|
1311
|
-
} else { await provisioning.resolveIntent(db, intent.id, 'done', null); ok++; }
|
|
1311
|
+
} else { await provisioning.resolveIntent(db, intent.id, 'done', (result && result.doneNote) || null); ok++; } // done WITH a note: it landed, and this is what it could not finish (task 1004065)
|
|
1312
1312
|
} catch (e) {
|
|
1313
1313
|
await provisioning.resolveIntent(db, intent.id, 'error', e.message).catch(() => {});
|
|
1314
1314
|
// Surface the failure on the instance too, so a stuck instance is visible.
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
// vetoes a git-history-proven land). This tells you whether you are relying on that
|
|
17
17
|
// fallback instead of a real deploy gate.
|
|
18
18
|
//
|
|
19
|
+
// For WHETHER each credential is set at all (names only, no network), see its sibling
|
|
20
|
+
// scripts/gds/credential-presence.js and the inventory in docs/credentials.md (task 1002880).
|
|
21
|
+
//
|
|
19
22
|
// Exit codes: 0 = credential can read Actions (gate is real, or the workflow is
|
|
20
23
|
// legitimately absent), 1 = BLIND (remediation printed), 2 = nothing to check.
|
|
21
24
|
|
package/scripts/gds/push-main.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// This is the deploy step of the GRADED GDS pipeline: ship.js's auto-merge and
|
|
5
5
|
// /merge-mode call it after a task has passed the grader + the Archon-only
|
|
6
|
-
// confirm gate. It carries the
|
|
6
|
+
// confirm gate. It carries the <PREFIX>_ALLOW_MAIN_PUSH override so the SEC #863
|
|
7
7
|
// pre-push hook lets the legitimate, already-gated pipeline land main while
|
|
8
8
|
// still blocking an ad-hoc `git push origin main` (the Builder 25 move).
|
|
9
9
|
//
|
|
@@ -22,11 +22,22 @@
|
|
|
22
22
|
'use strict';
|
|
23
23
|
|
|
24
24
|
const { execFileSync } = require('node:child_process');
|
|
25
|
+
const { envPrefix } = require('../../src/instance-config');
|
|
26
|
+
|
|
27
|
+
// The push's environment: the caller's, plus the override under its CANONICAL
|
|
28
|
+
// spelling for this instance — <PREFIX>_ALLOW_MAIN_PUSH, BONGOS_ on a vanilla
|
|
29
|
+
// instance (task 1001441). The hook reads it through resolveEnv, where the
|
|
30
|
+
// canonical name is tried first; writing the legacy OTB_ spelling instead would
|
|
31
|
+
// still work until the legacy prefixes retire, and would earn a deprecation
|
|
32
|
+
// warning on every deploy push in the meantime.
|
|
33
|
+
function pushEnv(env = process.env) {
|
|
34
|
+
return { ...env, [`${envPrefix()}_ALLOW_MAIN_PUSH`]: '1' };
|
|
35
|
+
}
|
|
25
36
|
|
|
26
37
|
function pushMain(extra = []) {
|
|
27
38
|
execFileSync('git', ['push', 'origin', 'main', ...extra], {
|
|
28
39
|
stdio: 'inherit',
|
|
29
|
-
env:
|
|
40
|
+
env: pushEnv(),
|
|
30
41
|
shell: false,
|
|
31
42
|
});
|
|
32
43
|
}
|
|
@@ -42,4 +53,4 @@ if (require.main === module) {
|
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
module.exports = { pushMain };
|
|
56
|
+
module.exports = { pushMain, pushEnv };
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
// node scripts/gds/push-path-brief.js # human-readable: which path + why
|
|
31
31
|
// node scripts/gds/push-path-brief.js --line # ONE line or nothing; always exit 0
|
|
32
32
|
//
|
|
33
|
-
// Detection override (tests / forcing):
|
|
33
|
+
// Detection override (tests / forcing): <PREFIX>_PUSH_VIA_SERVER=1 (server) / =0 (local)
|
|
34
|
+
// — BONGOS_PUSH_VIA_SERVER on a vanilla instance; the legacy OTB_ spelling is still read.
|
|
34
35
|
|
|
35
36
|
const { hasFlag } = require('./cli-lib');
|
|
36
37
|
const { plainCardLines } = require('./plain-cards');
|
|
@@ -56,7 +57,7 @@ function briefLine(via) {
|
|
|
56
57
|
// ---- detection ---------------------------------------------------------------
|
|
57
58
|
|
|
58
59
|
// Lazy require so a broken or partial checkout degrades to silence, not a stack
|
|
59
|
-
// trace injected into the session. pushVia() honors
|
|
60
|
+
// trace injected into the session. pushVia() honors <PREFIX>_PUSH_VIA_SERVER, else
|
|
60
61
|
// probes for an authenticated gh (absent on every box → 'server' instantly).
|
|
61
62
|
function detectVia() {
|
|
62
63
|
return require('./ship-deploy-target.js').pushVia();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// The patrol is the third enforcement layer (prevent → gate → PATROL): checks
|
|
6
6
|
// too slow, too networked, or too history-wide for the merge gate, run weekly
|
|
7
7
|
// by the .claude/scheduled-tasks/redteam-weekly routine. It is ADVISORY by
|
|
8
|
-
// doctrine (ADR
|
|
8
|
+
// doctrine (ADR 0158 §2 — a verdict never self-executes): findings are FILED as
|
|
9
9
|
// security reports for Archon adjudication, never turned into gates here.
|
|
10
10
|
//
|
|
11
11
|
// Checks (all deterministic — no model reads anything):
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// log. Instead --file preflights the session and, when there is none, follows the
|
|
41
41
|
// main-audit.js --require-roster doctrine: report LOUDLY rather than green-and-
|
|
42
42
|
// vacuous. The exit code says whether a finding was LOST, not whether findings
|
|
43
|
-
// exist — findings are advisory (ADR
|
|
43
|
+
// exist — findings are advisory (ADR 0158 §2) and a patrol that goes red merely for
|
|
44
44
|
// doing its job trains everyone to ignore it:
|
|
45
45
|
//
|
|
46
46
|
// ran + filed → 0
|
|
@@ -271,7 +271,7 @@ function buildClassReport(className, findings, notes = []) {
|
|
|
271
271
|
target: `redteam-patrol/${className}`,
|
|
272
272
|
severity: maxSeverity(findings),
|
|
273
273
|
title: `[patrol] ${className}: ${findings.length} finding(s)`,
|
|
274
|
-
description: `Weekly deterministic red-team patrol (task 1003128; advisory per ADR
|
|
274
|
+
description: `Weekly deterministic red-team patrol (task 1003128; advisory per ADR 0158 §2 — adjudicate, then fix via ordinary tasks).`,
|
|
275
275
|
repro_steps_md: body,
|
|
276
276
|
};
|
|
277
277
|
}
|
|
@@ -289,7 +289,7 @@ function stepSummary(md) {
|
|
|
289
289
|
// that disposition deserves. Extracted so the honest-exit contract is a table a
|
|
290
290
|
// test can enumerate rather than branching buried in main().
|
|
291
291
|
//
|
|
292
|
-
// The load-bearing row is the last one. Findings are ADVISORY (ADR
|
|
292
|
+
// The load-bearing row is the last one. Findings are ADVISORY (ADR 0158 §2), so a
|
|
293
293
|
// run that finds three things and files them is a SUCCESS — going red merely for
|
|
294
294
|
// doing its job is how a weekly lane becomes noise everyone filters. Red is
|
|
295
295
|
// reserved for the single state where evidence EVAPORATES: findings exist,
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/gds/release-notes.js — what each core release brought in, as the summaries its
|
|
4
|
+
// ships wrote, generated when the core is PACKED and shipped inside the package as
|
|
5
|
+
// release-notes.json (task 1004218).
|
|
6
|
+
//
|
|
7
|
+
// WHY INSIDE THE PACKAGE. The Deploy page's "what this brings in" list used to be read out
|
|
8
|
+
// of a core git checkout on the machine asking (task 1004173), and no tenant box will ever
|
|
9
|
+
// have one: every project the wizard creates is a checkout of its OWN repo with the core
|
|
10
|
+
// vendored in at node_modules. The one machine that always has the full history is the one
|
|
11
|
+
// that builds the package — publish.yml checks out at fetch-depth 0 because package-core
|
|
12
|
+
// archives from git — so that is where the notes are written down, once, and every project
|
|
13
|
+
// that can download a core version can read what it carries.
|
|
14
|
+
//
|
|
15
|
+
// THREE PARTS, NO I/O OF ITS OWN. The PARSER (parseChangeRecords, moved here from
|
|
16
|
+
// provision-core-upgrade.js so there is exactly one), the GENERATOR (package-core.js hands
|
|
17
|
+
// it the ref's history), and the RANGE READER (the upgrade runner asks it for from→to).
|
|
18
|
+
// Git is injected, never required, so loading this costs package-core nothing.
|
|
19
|
+
//
|
|
20
|
+
// THE SHAPE. `{ "<version>": [ { id?, text }, … ] }`, ascending by version, one key per
|
|
21
|
+
// release — including a release whose merges were all housekeeping, which keeps an EMPTY
|
|
22
|
+
// list. That is the difference between "nothing landed" and "not covered": a range is
|
|
23
|
+
// answerable only when its target is a key, so a missing key can never be read as silence.
|
|
24
|
+
|
|
25
|
+
const CHANGE_TEXT_MAX = 400;
|
|
26
|
+
|
|
27
|
+
// The file's name at the package root, beside package.json.
|
|
28
|
+
const RELEASE_NOTES_FILE = 'release-notes.json';
|
|
29
|
+
|
|
30
|
+
// THE BOUND. The whole history to 1.19.1029 is ~400 KB pretty-printed (1,033 releases,
|
|
31
|
+
// 1,399 summaries, measured 2026-09-25), growing roughly 200 KB a month at 20–30 releases a
|
|
32
|
+
// day. Nothing needs trimming today; the bound exists so the file cannot grow unwatched.
|
|
33
|
+
// Past it, the OLDEST releases are dropped — the ranges an owner asks about are recent ones
|
|
34
|
+
// — and the reader then says those older ranges are not covered, rather than guessing.
|
|
35
|
+
const RELEASE_NOTES_MAX_BYTES = 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
// A release commit's subject — the lane's own carrier AND a human release both begin this
|
|
38
|
+
// way. The same marker findCoreCheckout's probe uses to recognise core history.
|
|
39
|
+
const RELEASE_SUBJECT_RE = /^release: core (\d+\.\d+\.\d+) /;
|
|
40
|
+
const CORE_VERSION_RE = /^[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
41
|
+
|
|
42
|
+
// One record per commit: sha, parents, subject, body. `--topo-order` is load-bearing — see
|
|
43
|
+
// buildReleaseNotes for why the order releases are visited in must be topological.
|
|
44
|
+
const LOG_ARGS = ['log', '--topo-order', '--format=%H%x1f%P%x1f%s%x1f%b%x1e'];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The merge summaries in a `git log --merges --format=%s%x1f%b%x1e` dump. PURE.
|
|
48
|
+
*
|
|
49
|
+
* TWO MERGE SHAPES CARRY A SUMMARY AND THE SECOND IS THE COMMON ONE. A hand-landed ship
|
|
50
|
+
* writes `Merge <branch>: <summary>`; the CI auto-merge writes `Merge pull request #N
|
|
51
|
+
* from <owner>/<branch>` with `task <id>: <summary>` in the BODY. Measured over
|
|
52
|
+
* 1.19.881→1.19.972: 114 usable summaries, of which 98 were the second shape. A parser
|
|
53
|
+
* that read subjects only would have found eleven of them and quietly called that the
|
|
54
|
+
* release.
|
|
55
|
+
*
|
|
56
|
+
* EVERYTHING ELSE IS DROPPED, and the drops are the majority: `Merge origin/main into
|
|
57
|
+
* <branch> (pre-stage freshness…)`, `Merge remote-tracking branch…`, a regen chore. Those
|
|
58
|
+
* are the mechanics of landing work, not the work, and listing them would bury the
|
|
59
|
+
* eleven sentences the owner actually wants under a hundred they do not.
|
|
60
|
+
*/
|
|
61
|
+
function parseChangeRecords(stdout) {
|
|
62
|
+
const out = [];
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const rec of String(stdout || '').split('\x1e')) {
|
|
65
|
+
const r = rec.trim();
|
|
66
|
+
if (!r) continue;
|
|
67
|
+
const i = r.indexOf('\x1f');
|
|
68
|
+
const subject = (i < 0 ? r : r.slice(0, i)).trim();
|
|
69
|
+
const body = i < 0 ? '' : r.slice(i + 1);
|
|
70
|
+
let text = null;
|
|
71
|
+
let id = null;
|
|
72
|
+
// The negative lookahead is what keeps the two housekeeping shapes out: both begin
|
|
73
|
+
// "Merge " and both would otherwise match a `<token>:` somewhere later in the line.
|
|
74
|
+
let m = /^Merge\s+(?!pull request\b|remote-tracking\b)([^\s:]+):\s*(\S.*)$/.exec(subject);
|
|
75
|
+
if (m) {
|
|
76
|
+
text = m[2];
|
|
77
|
+
} else {
|
|
78
|
+
const first = body.split('\n').map((l) => l.trim()).find(Boolean) || '';
|
|
79
|
+
m = /^task\s+(\d+):\s*(\S.*)$/.exec(first);
|
|
80
|
+
if (m) { id = m[1]; text = m[2]; }
|
|
81
|
+
}
|
|
82
|
+
if (!text) continue;
|
|
83
|
+
// It reaches a DOM, and a ship summary is occasionally a paragraph.
|
|
84
|
+
text = boundText(text);
|
|
85
|
+
if (seen.has(text)) continue; // a branch merged twice says its sentence once
|
|
86
|
+
seen.add(text);
|
|
87
|
+
out.push(id ? { id, text } : { text });
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function boundText(text) {
|
|
93
|
+
return text.length > CHANGE_TEXT_MAX ? `${text.slice(0, CHANGE_TEXT_MAX - 1)}…` : text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Order two three-number core versions. PURE; both must already match CORE_VERSION_RE. */
|
|
97
|
+
function compareCoreVersions(a, b) {
|
|
98
|
+
const x = String(a).split('.').map(Number);
|
|
99
|
+
const y = String(b).split('.').map(Number);
|
|
100
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i];
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Split a LOG_ARGS dump into `{ sha, parents, subject, body }` records, in log order. PURE. */
|
|
105
|
+
function parseLogRecords(stdout) {
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const rec of String(stdout || '').split('\x1e')) {
|
|
108
|
+
const r = rec.replace(/^\n+/, ''); // git terminates each record with a newline
|
|
109
|
+
if (!r.trim()) continue;
|
|
110
|
+
const [sha, parents, subject, ...body] = r.split('\x1f');
|
|
111
|
+
out.push({
|
|
112
|
+
sha: sha.trim(),
|
|
113
|
+
parents: String(parents || '').split(' ').filter(Boolean),
|
|
114
|
+
subject: subject || '',
|
|
115
|
+
body: body.join('\x1f'),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function uniqueByText(entries) {
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
return entries.filter((e) => (seen.has(e.text) ? false : seen.add(e.text)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* What each release brought in, from a whole history. PURE.
|
|
128
|
+
*
|
|
129
|
+
* `commits` is every commit the packed ref reaches, NEWEST FIRST in topological order (what
|
|
130
|
+
* LOG_ARGS asks git for). Returns `{ notes, releases, tail }`.
|
|
131
|
+
*
|
|
132
|
+
* EXACTLY `git log --merges <previous release>..<release>`, FOR EVERY RELEASE AT ONCE — the
|
|
133
|
+
* range the checkout-based reader asks for one pair at a time. Splitting the log wherever a
|
|
134
|
+
* release subject appears would be simpler and is wrong: a PR branch's commits are not
|
|
135
|
+
* ordered against main's by the log, so a merge made on a branch before one release but
|
|
136
|
+
* landed after it would be credited to the earlier release. So each release, oldest first,
|
|
137
|
+
* claims everything it can reach that no earlier release reached. The visiting order has to
|
|
138
|
+
* be topological for that to be the previous-release range — hence `--topo-order`.
|
|
139
|
+
*
|
|
140
|
+
* THE UNRELEASED TAIL — commits the ref reaches that no release commit does — belongs to the
|
|
141
|
+
* version being packed when that version has no release commit of its own yet (a pack of a
|
|
142
|
+
* hand-declared version), and to nobody otherwise: on a work merge the tail is the NEXT
|
|
143
|
+
* release's, whose number does not exist yet.
|
|
144
|
+
*/
|
|
145
|
+
function buildReleaseNotes(commits, { version = null } = {}) {
|
|
146
|
+
const bySha = new Map(commits.map((c) => [c.sha, c]));
|
|
147
|
+
const position = new Map(commits.map((c, i) => [c.sha, i]));
|
|
148
|
+
const claimed = new Set();
|
|
149
|
+
const claim = (tip) => {
|
|
150
|
+
const mine = [];
|
|
151
|
+
const stack = [tip];
|
|
152
|
+
while (stack.length) {
|
|
153
|
+
const sha = stack.pop();
|
|
154
|
+
if (claimed.has(sha) || !bySha.has(sha)) continue;
|
|
155
|
+
claimed.add(sha);
|
|
156
|
+
const c = bySha.get(sha);
|
|
157
|
+
mine.push(c);
|
|
158
|
+
for (const p of c.parents) if (!claimed.has(p)) stack.push(p);
|
|
159
|
+
}
|
|
160
|
+
return mine;
|
|
161
|
+
};
|
|
162
|
+
// Newest first within a release, as `git log` itself would list them.
|
|
163
|
+
const summarize = (segment) => parseChangeRecords(segment
|
|
164
|
+
.filter((c) => c.parents.length > 1)
|
|
165
|
+
.sort((a, b) => position.get(a.sha) - position.get(b.sha))
|
|
166
|
+
.map((c) => `${c.subject}\x1f${c.body}\x1e`)
|
|
167
|
+
.join(''));
|
|
168
|
+
|
|
169
|
+
const byVersion = new Map();
|
|
170
|
+
let releases = 0;
|
|
171
|
+
for (let i = commits.length - 1; i >= 0; i--) {
|
|
172
|
+
const m = RELEASE_SUBJECT_RE.exec(commits[i].subject);
|
|
173
|
+
if (!m) continue;
|
|
174
|
+
releases++;
|
|
175
|
+
// A version released twice keeps both halves; the second claims only what the first
|
|
176
|
+
// could not reach.
|
|
177
|
+
byVersion.set(m[1], uniqueByText([...(byVersion.get(m[1]) || []), ...summarize(claim(commits[i].sha))]));
|
|
178
|
+
}
|
|
179
|
+
const tail = commits.filter((c) => !claimed.has(c.sha));
|
|
180
|
+
if (version && CORE_VERSION_RE.test(version) && !byVersion.has(version)
|
|
181
|
+
&& [...byVersion.keys()].every((v) => compareCoreVersions(v, version) < 0)) {
|
|
182
|
+
byVersion.set(version, summarize(tail));
|
|
183
|
+
}
|
|
184
|
+
const notes = {};
|
|
185
|
+
for (const v of [...byVersion.keys()].sort(compareCoreVersions)) notes[v] = byVersion.get(v);
|
|
186
|
+
return { notes, releases, tail: tail.length };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Read a ref's history through an injected `git(args) → stdout` and build its notes.
|
|
191
|
+
* Returns `{ notes, releases, tail }`, or `{ skipped }` saying why there are none — never
|
|
192
|
+
* a partial answer presented as a whole one.
|
|
193
|
+
*/
|
|
194
|
+
function generateReleaseNotes({ git, ref, version }) {
|
|
195
|
+
let shallow;
|
|
196
|
+
try { shallow = String(git(['rev-parse', '--is-shallow-repository'])).trim() === 'true'; }
|
|
197
|
+
catch (e) { return { skipped: `could not ask git about this clone (${firstLine(e)})` }; }
|
|
198
|
+
// A depth-limited clone reaches a handful of commits, and notes built from it would
|
|
199
|
+
// credit the newest release with everything the clone happens to hold.
|
|
200
|
+
if (shallow) return { skipped: 'this is a shallow clone, so it carries no release history' };
|
|
201
|
+
let out;
|
|
202
|
+
try { out = git([...LOG_ARGS, ref]); }
|
|
203
|
+
catch (e) { return { skipped: `could not read the history of ${ref} (${firstLine(e)})` }; }
|
|
204
|
+
const built = buildReleaseNotes(parseLogRecords(out), { version });
|
|
205
|
+
if (!built.releases) return { skipped: 'this history holds no release commits' };
|
|
206
|
+
return built;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function firstLine(e) {
|
|
210
|
+
return String((e && e.message) || e || '').trim().split('\n')[0].slice(0, 160);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The file's bytes: the same pretty-printed JSON the package's other generated files use. */
|
|
214
|
+
function serializeReleaseNotes(notes) {
|
|
215
|
+
return Buffer.from(`${JSON.stringify(notes, null, 2)}\n`, 'utf8');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Hold the file under `maxBytes` by dropping the OLDEST releases. PURE.
|
|
220
|
+
* Returns `{ notes, dropped }`; the newest release is always kept.
|
|
221
|
+
*/
|
|
222
|
+
function boundReleaseNotes(notes, maxBytes = RELEASE_NOTES_MAX_BYTES) {
|
|
223
|
+
const versions = Object.keys(notes);
|
|
224
|
+
const keep = (from) => Object.fromEntries(versions.slice(from).map((v) => [v, notes[v]]));
|
|
225
|
+
const fits = (from) => serializeReleaseNotes(keep(from)).length <= maxBytes;
|
|
226
|
+
if (versions.length <= 1 || fits(0)) return { notes, dropped: [] };
|
|
227
|
+
// The fewest drops that fit, by bisection — the size only shrinks as more are dropped.
|
|
228
|
+
let lo = 1;
|
|
229
|
+
let hi = versions.length - 1;
|
|
230
|
+
while (lo < hi) {
|
|
231
|
+
const mid = (lo + hi) >> 1;
|
|
232
|
+
if (fits(mid)) hi = mid; else lo = mid + 1;
|
|
233
|
+
}
|
|
234
|
+
return { notes: keep(lo), dropped: versions.slice(0, lo) };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The entries for every release in `(from, to]`, newest release first. PURE.
|
|
239
|
+
*
|
|
240
|
+
* Returns `{ entries }` or `{ uncovered }` — a phrase saying why these notes cannot answer,
|
|
241
|
+
* worded to follow "the release notes …". NEVER an empty list for a range the notes do not
|
|
242
|
+
* reach: an empty list reads as "nothing changed", which is exactly what a restart must not
|
|
243
|
+
* be sold as. They reach a range when they list its target and begin at or before its
|
|
244
|
+
* start: the oldest release they hold is where their knowledge begins.
|
|
245
|
+
*
|
|
246
|
+
* The file may have come off the network, so every entry is re-checked here, not trusted.
|
|
247
|
+
*/
|
|
248
|
+
function notesBetween(notes, from, to) {
|
|
249
|
+
if (!notes || typeof notes !== 'object' || Array.isArray(notes)) return { uncovered: 'are not a list of releases' };
|
|
250
|
+
const versions = Object.keys(notes)
|
|
251
|
+
.filter((v) => CORE_VERSION_RE.test(v) && Array.isArray(notes[v]))
|
|
252
|
+
.sort(compareCoreVersions);
|
|
253
|
+
if (!versions.includes(to)) return { uncovered: `do not list core ${to}` };
|
|
254
|
+
if (compareCoreVersions(versions[0], from) > 0) return { uncovered: `begin at core ${versions[0]}, after ${from}` };
|
|
255
|
+
const inRange = versions.filter((v) => compareCoreVersions(v, from) > 0 && compareCoreVersions(v, to) <= 0);
|
|
256
|
+
const entries = [];
|
|
257
|
+
for (const v of inRange.reverse()) {
|
|
258
|
+
for (const e of notes[v]) {
|
|
259
|
+
if (!e || typeof e.text !== 'string' || !e.text.trim()) continue;
|
|
260
|
+
const text = boundText(e.text.trim());
|
|
261
|
+
entries.push(/^\d+$/.test(String(e.id || '')) ? { id: String(e.id), text } : { text });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { entries: uniqueByText(entries) };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
RELEASE_NOTES_FILE, RELEASE_NOTES_MAX_BYTES,
|
|
269
|
+
parseChangeRecords, parseLogRecords, buildReleaseNotes,
|
|
270
|
+
generateReleaseNotes, serializeReleaseNotes, boundReleaseNotes, notesBetween,
|
|
271
|
+
};
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Each step routes its stdout by `sink: discord|gds|none`. A failed step exits
|
|
17
17
|
// non-zero after a best-effort #ops alert. Runs the steps with
|
|
18
|
-
//
|
|
18
|
+
// <PREFIX>_COST_CRON_ROUTINE set so System 1 tags any (hybrid) model spend as
|
|
19
19
|
// cron:<routine>. Usage: node scripts/gds/run-routine.js <routine-name> [--dry-run]
|
|
20
20
|
|
|
21
21
|
'use strict';
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
const fs = require('fs');
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const { execFileSync } = require('child_process');
|
|
26
|
-
const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-config');
|
|
26
|
+
const { resolveCoreRoot, resolveInstanceRoot, resolveEnv, envPrefix } = require('../../src/instance-config');
|
|
27
27
|
|
|
28
28
|
// TWO roots (ADR 0108 configurable-root; task 2138). run-routine.js is a CORE script, so it
|
|
29
29
|
// ships alongside src/, scripts/gds/*, scripts/discord/*, and modules/ — all under
|
|
@@ -39,7 +39,20 @@ const TASKS_DIR = path.join(INSTANCE_ROOT, '.claude', 'scheduled-tasks');
|
|
|
39
39
|
const DISCORD_POST = path.join(CORE_ROOT, 'scripts', 'discord', 'post.js');
|
|
40
40
|
const VALID_MODES = new Set(['deterministic', 'hybrid', 'judgment']);
|
|
41
41
|
const VALID_SINKS = new Set(['discord', 'gds', 'none']);
|
|
42
|
-
|
|
42
|
+
// The #ops channel a failed step alerts: <PREFIX>_ROUTINE_ALERT_CHANNEL (BONGOS_ on
|
|
43
|
+
// a vanilla instance; the legacy OTB_ spelling is still read), else ship_feed.
|
|
44
|
+
function alertChannel(env = process.env) {
|
|
45
|
+
return resolveEnv('ROUTINE_ALERT_CHANNEL', { env }) || 'ship_feed';
|
|
46
|
+
}
|
|
47
|
+
const ALERT_CHANNEL = alertChannel();
|
|
48
|
+
|
|
49
|
+
// The environment a routine step runs with: the caller's, plus the routine's name
|
|
50
|
+
// under the canonical <PREFIX>_COST_CRON_ROUTINE so System 1 tags any model spend
|
|
51
|
+
// as cron:<routine>. session-cost.js reads it back through resolveEnv, which tries
|
|
52
|
+
// the canonical spelling first (task 1001441).
|
|
53
|
+
function stepEnv(routineName, env = process.env) {
|
|
54
|
+
return { ...env, [`${envPrefix()}_COST_CRON_ROUTINE`]: routineName };
|
|
55
|
+
}
|
|
43
56
|
|
|
44
57
|
function unquote(s) {
|
|
45
58
|
const t = String(s == null ? '' : s).trim();
|
|
@@ -177,7 +190,7 @@ function runStep(step, routineName, { dryRun }) {
|
|
|
177
190
|
const out = execFileSync(argv[0], argv.slice(1), {
|
|
178
191
|
cwd: INSTANCE_ROOT, // routines run in the instance repo (companions + git-touching steps)
|
|
179
192
|
encoding: 'utf8',
|
|
180
|
-
env:
|
|
193
|
+
env: stepEnv(routineName),
|
|
181
194
|
});
|
|
182
195
|
routeOutput(step.sink, out, routineName);
|
|
183
196
|
return out;
|
|
@@ -241,4 +254,4 @@ if (require.main === module) {
|
|
|
241
254
|
main();
|
|
242
255
|
}
|
|
243
256
|
|
|
244
|
-
module.exports = { parseRoutineFrontmatter, tokenize, resolveStepArgv, isNodeExec, VALID_MODES, VALID_SINKS };
|
|
257
|
+
module.exports = { parseRoutineFrontmatter, tokenize, resolveStepArgv, isNodeExec, VALID_MODES, VALID_SINKS, alertChannel, stepEnv };
|
|
@@ -95,8 +95,11 @@ const INTEGRATION = new Set([
|
|
|
95
95
|
'asset_version', 'auth_flow_slice1', 'backfill_sessions', 'bfg', 'bfg_write',
|
|
96
96
|
'box_connect_e2e', 'box_onboard', 'boxes', 'cf_tunnel', 'cli_token_reissue',
|
|
97
97
|
// consumer_layout_boot builds the core .tgz, `npm install`s it, and boots a live
|
|
98
|
-
// platform-server
|
|
99
|
-
//
|
|
98
|
+
// platform-server (task 2142), then installs the package from its own shipped lockfile,
|
|
99
|
+
// migrates brand-new EMPTY databases and serves a database-backed route from one (task
|
|
100
|
+
// 1002863) — network + package build + spawned servers + Postgres. It runs in the
|
|
101
|
+
// integration job of .github/workflows/integration.yml like the rest of this set; there
|
|
102
|
+
// never was a dedicated consumer-layout-boot workflow, whatever older notes say.
|
|
100
103
|
'consumer_layout_boot',
|
|
101
104
|
'claim_gate_open_blocker', 'claim_gate_rebase',
|
|
102
105
|
// task 1002709: core_221's re-keyed backstop index against a real Postgres.
|
|
@@ -408,11 +411,19 @@ function discoverModuleTests({ modulesDir = MODULES_DIR, isEnabled = isModuleEna
|
|
|
408
411
|
// scripts/gds/smoke-box.sh still invokes all three by path — deleting the files
|
|
409
412
|
// turned that script into an immediate MODULE_NOT_FOUND. They come out when the
|
|
410
413
|
// module does, in the change that removes it.
|
|
414
|
+
//
|
|
415
|
+
// consumer_layout_boot LEFT THIS LIST under task 1002863. Its "missing core tarball" was
|
|
416
|
+
// package-core's fail-closed leak gate reading the lane's throwaway database password (the
|
|
417
|
+
// word postgres, present in hundreds of core files) as a leaked credential and refusing to
|
|
418
|
+
// build. The harness now withholds database settings from the packager, and it is the only
|
|
419
|
+
// test that proves the PUBLISHED package installs, migrates an empty database and serves
|
|
420
|
+
// from it — parking it again re-opens that gap, which tests/integration_parked_ratchet.mjs
|
|
421
|
+
// now says out loud.
|
|
411
422
|
const INTEGRATION_PARKED = new Set([
|
|
412
423
|
'acclaim_feed_evidence_db',
|
|
413
424
|
'bfg', 'bfg_write', 'box_connect_e2e', 'box_onboard', 'boxes',
|
|
414
425
|
'claim_gate_open_blocker', 'claim_gate_rebase',
|
|
415
|
-
'clawback_confirmed_backlog', '
|
|
426
|
+
'clawback_confirmed_backlog', 'copy_desk_flags_db',
|
|
416
427
|
'government_board_membership_db', 'handle_machinery', 'hub_profile_leak',
|
|
417
428
|
'idea_credit_streams_db', 'memory_isolation', 'memory_pull', 'needs_rebase',
|
|
418
429
|
'profile_fields', 'project_visibility_db', 'reward_mode_cost_plus_only',
|
|
@@ -43,7 +43,7 @@ const TASKS = [
|
|
|
43
43
|
// ---- Phase A — decide + foundation (P5) ----
|
|
44
44
|
{ ref:'R01', kind:'decision', disc:'engineer', prio:5, est:150, crit:[C1,C3,C4], deps:[],
|
|
45
45
|
title:'R01 · ADR: the Board Room government model',
|
|
46
|
-
desc:`Record the government model as one decision so the schema has something to encode.\n\nMUST STATE: the board's TWO subjects (a Full Idea, and a change to the constitution itself) and why nothing else is votable; membership as a configured predicate resolved per request and uncached; the timed SITTING (absent votes are nulled, not blocking) and why that replaces quorum — the clock IS the deadlock exit; CONSENT rather than majority, with silence as consent and one reasoned objection returning the idea; a zero-vote close PASSING; the OPEN ballot; Discord as an outbound notification mirror that casts nothing; and the amendment rule — a constitutional change is decided under whatever rule the CURRENT constitution names, so flipping out of monarchy is a legal monarchy act.\n\nAlso record the two owner decisions taken against recommendation with their trade-offs intact: vote-karma (yes-clicking bias, accepted because item volume is too low to farm) and automatic window-open on grade pass (author loses timing control; concurrent windows become normal).\n\nGrounding: idea 1000738, idea 1000754, the Six Variables of Governance synthesis, ADR 0151 (permissions-as-atoms), ADR 0157, ADR
|
|
46
|
+
desc:`Record the government model as one decision so the schema has something to encode.\n\nMUST STATE: the board's TWO subjects (a Full Idea, and a change to the constitution itself) and why nothing else is votable; membership as a configured predicate resolved per request and uncached; the timed SITTING (absent votes are nulled, not blocking) and why that replaces quorum — the clock IS the deadlock exit; CONSENT rather than majority, with silence as consent and one reasoned objection returning the idea; a zero-vote close PASSING; the OPEN ballot; Discord as an outbound notification mirror that casts nothing; and the amendment rule — a constitutional change is decided under whatever rule the CURRENT constitution names, so flipping out of monarchy is a legal monarchy act.\n\nAlso record the two owner decisions taken against recommendation with their trade-offs intact: vote-karma (yes-clicking bias, accepted because item volume is too low to farm) and automatic window-open on grade pass (author loses timing control; concurrent windows become normal).\n\nGrounding: idea 1000738, idea 1000754, the Six Variables of Governance synthesis, ADR 0151 (permissions-as-atoms), ADR 0157, ADR 0158 §2 (verdicts advisory), ADR 0162 (no human approval in the land path), ADR 0174 (government/ranks/roles). ${SPEC}` },
|
|
47
47
|
|
|
48
48
|
{ ref:'R02', kind:'decision', disc:'engineer', prio:5, est:90, crit:[C1], deps:['R01'],
|
|
49
49
|
title:'R02 · The pass-event contract the ideator credit lane consumes',
|
|
@@ -27,6 +27,7 @@ const os = require('os');
|
|
|
27
27
|
const path = require('path');
|
|
28
28
|
const { cliClient, loadSession, arg } = require('./cli-lib');
|
|
29
29
|
const { priceUsage } = require('../../src/bongos/llm-pricing');
|
|
30
|
+
const { resolveEnv } = require('../../src/instance-config');
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
// Claude Code encodes a project transcript dir by replacing every non-alnum
|
|
@@ -256,9 +257,16 @@ function priceSubagents(sessionDir) {
|
|
|
256
257
|
// into TWO call-site-tagged rows — source='<base>:interactive' (source_ref =
|
|
257
258
|
// sessionId) and '<base>:subagent' (source_ref = sessionId+':sub'), distinct on
|
|
258
259
|
// the (source,source_ref) idempotency index — each carrying its tier split. A
|
|
259
|
-
// cron-launched session sets
|
|
260
|
+
// cron-launched session sets <PREFIX>_COST_CRON_ROUTINE=<name> so base='cron:<name>'
|
|
260
261
|
// instead of 'session'. The rows' amounts reconcile to the prior single-row
|
|
261
262
|
// figure (priorSingle vs rowsTotal, equal within independent-rounding epsilon).
|
|
263
|
+
// The routine a cron-launched session belongs to, or ''. run-routine.js writes
|
|
264
|
+
// the canonical <PREFIX>_COST_CRON_ROUTINE (stepEnv); a legacy OTB_ spelling set
|
|
265
|
+
// by an older runner is still read, with a deprecation warning (task 1001441).
|
|
266
|
+
function cronRoutineFromEnv(env = process.env) {
|
|
267
|
+
return (resolveEnv('COST_CRON_ROUTINE', { env }) || '').trim();
|
|
268
|
+
}
|
|
269
|
+
|
|
262
270
|
function buildCostRows({ sessionId, inter, sub, cronRoutine }) {
|
|
263
271
|
const base = cronRoutine ? `cron:${cronRoutine}` : 'session';
|
|
264
272
|
const id8 = String(sessionId).slice(0, 8);
|
|
@@ -350,8 +358,8 @@ async function main() {
|
|
|
350
358
|
if (!(total > 0)) return;
|
|
351
359
|
|
|
352
360
|
// task 1344: split into call-site-tagged, tier-carrying rows; cron sessions
|
|
353
|
-
// are tagged via
|
|
354
|
-
const cronRoutine = (
|
|
361
|
+
// are tagged via <PREFIX>_COST_CRON_ROUTINE (see cronRoutineFromEnv).
|
|
362
|
+
const cronRoutine = cronRoutineFromEnv();
|
|
355
363
|
const built = buildCostRows({ sessionId, inter, sub, cronRoutine });
|
|
356
364
|
const id8 = sessionId.slice(0, 8);
|
|
357
365
|
|
|
@@ -414,6 +422,7 @@ module.exports = {
|
|
|
414
422
|
newestTranscriptFor,
|
|
415
423
|
// task 1344 — pure tier-split + row-building helpers (unit-tested)
|
|
416
424
|
buildCostRows,
|
|
425
|
+
cronRoutineFromEnv,
|
|
417
426
|
emptyTiers,
|
|
418
427
|
addComponents,
|
|
419
428
|
addTiers,
|
|
@@ -12,13 +12,17 @@ const { spawnSync } = require('node:child_process');
|
|
|
12
12
|
const fs = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
14
|
const { loadSessionSync } = require('./cli-lib');
|
|
15
|
+
// The two env overrides below resolve through the branded prefix — <PREFIX>_DEPLOY_MODE /
|
|
16
|
+
// <PREFIX>_PUSH_VIA_SERVER (BONGOS_ on a vanilla instance) — with the legacy OTB_ spelling
|
|
17
|
+
// still honoured and warned about (task 1001441, R61 follow-up).
|
|
18
|
+
const { resolveEnv } = require('../../src/instance-config');
|
|
15
19
|
const { argvOk, argvOut } = require('./ship-git.js');
|
|
16
20
|
const { REPO_ROOT } = require('./ship-state.js');
|
|
17
21
|
|
|
18
22
|
// ---------- CI deploy mode (#679, ADR 0042) ----------
|
|
19
23
|
//
|
|
20
24
|
// Two ways the confirmed→shipped land can run, chosen by a SINGLE committed
|
|
21
|
-
// switch (config/deploy.json `mode`, env
|
|
25
|
+
// switch (config/deploy.json `mode`, env <PREFIX>_DEPLOY_MODE overrides):
|
|
22
26
|
//
|
|
23
27
|
// 'laptop' (default) — the operator's machine merges to main locally and, IFF a
|
|
24
28
|
// deploy target is configured (config/deploy.json "sshTarget" or
|
|
@@ -63,7 +67,7 @@ function parseDeployMode(envVal, configRaw) {
|
|
|
63
67
|
// it must ship via the 'ci' server-mediated publish path instead
|
|
64
68
|
// (pushVia()==='server', ADR 0055), which lets the GDS server push with its own
|
|
65
69
|
// credential. So when the parsed mode is 'laptop' but the machine can't push
|
|
66
|
-
// locally AND the mode was NOT pinned by an explicit
|
|
70
|
+
// locally AND the mode was NOT pinned by an explicit <PREFIX>_DEPLOY_MODE, fall back to
|
|
67
71
|
// 'ci' rather than strand every ship at merge. This is the scootermack 2026-06-22
|
|
68
72
|
// case: a checkout left at the pre-cutover 'laptop' default that can't pull
|
|
69
73
|
// forward to 'ci', so it kept attempting a push it had no credential for. An
|
|
@@ -109,9 +113,10 @@ function mainIsProtected() {
|
|
|
109
113
|
|
|
110
114
|
function resolveDeployMode() {
|
|
111
115
|
const configRaw = readDeployConfigRaw();
|
|
112
|
-
const
|
|
116
|
+
const envMode = resolveEnv('DEPLOY_MODE');
|
|
117
|
+
const mode = parseDeployMode(envMode, configRaw);
|
|
113
118
|
const envForced = ['ci', 'laptop'].includes(
|
|
114
|
-
String(
|
|
119
|
+
String(envMode || '').trim().toLowerCase()
|
|
115
120
|
);
|
|
116
121
|
// Probe `gh` ONLY when it can change the answer (resolved 'laptop', no env
|
|
117
122
|
// override) — the happy 'ci' path and an env-pinned mode spawn no extra gh,
|
|
@@ -313,7 +318,7 @@ function fetchDeployedVersion(url = deployedVersionUrl()) {
|
|
|
313
318
|
// credential-less origin). ship uploads a thin branch bundle to the
|
|
314
319
|
// GDS, which pushes + opens the PR + auto-merges with the SERVER's
|
|
315
320
|
// credential. The box holds nothing (#919 model preserved).
|
|
316
|
-
// Override with
|
|
321
|
+
// Override with <PREFIX>_PUSH_VIA_SERVER=1 (force server) / =0 (force local) for tests.
|
|
317
322
|
function ghAvailableAndAuthed() {
|
|
318
323
|
try {
|
|
319
324
|
const a = spawnSync('gh', ['auth', 'status'], {
|
|
@@ -329,7 +334,7 @@ function ghAvailableAndAuthed() {
|
|
|
329
334
|
}
|
|
330
335
|
|
|
331
336
|
function pushVia() {
|
|
332
|
-
const o = String(
|
|
337
|
+
const o = String(resolveEnv('PUSH_VIA_SERVER') || '').trim();
|
|
333
338
|
if (o === '1') return 'server';
|
|
334
339
|
if (o === '0') return 'local';
|
|
335
340
|
// No explicit override: a machine that can run an authenticated gh (laptop)
|