@bongos/core 1.19.1033 → 1.19.1035
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 +204 -99
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +4 -0
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +120 -4
- package/docs/api-reference.md +9 -2
- package/docs/architecture.md +3 -2
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/module-api-changelog.md +5 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/db.js +19 -0
- package/modules/autonomy/routes/autonomy.js +28 -0
- package/modules/government/catalog.js +2 -2
- 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/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-assurance.js +17 -1
- 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 +80 -0
- package/scripts/gds/autobongos-run.js +10 -2
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- 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/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +155 -4
- 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/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +157 -11
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +30 -8
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/upgrade.js +3 -3
- 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/core-update.js +23 -1
- package/src/bongos/routes/core-update.js +27 -2
- 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/autobongos_cadence.mjs +74 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +240 -4
- package/tests/credential_presence.mjs +153 -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/integration_parked_ratchet.mjs +24 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/routine_observability_api.mjs +195 -0
- package/tests/ship_assurance.mjs +86 -3
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +67 -1
- package/tests/start_env_guard.mjs +16 -0
- package/tests/upgrade.mjs +20 -1
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();
|
|
@@ -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',
|
|
@@ -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)
|
|
@@ -20,6 +20,8 @@ const { plainCardLines } = require('./plain-cards.js');
|
|
|
20
20
|
// gives, and the two can never drift.
|
|
21
21
|
const { preflightChecks } = require('./preflight.js');
|
|
22
22
|
const { computeRawDiffHash, reconstructPanelIssues, runNoArtifactGrader, runSubagentGrader, shouldReuseGrade } = require('./ship-grade.js');
|
|
23
|
+
// task 1002903: the severity gate's own reading of which low scores had no finding.
|
|
24
|
+
const { unbackedLowScores } = require('../../modules/grading/grader-workers/worker-verdict.js');
|
|
23
25
|
|
|
24
26
|
// ---------- Shared grade-step helpers (used by shipMain AND regradeMain) ----------
|
|
25
27
|
//
|
|
@@ -396,6 +398,37 @@ function formatGradeOutcome(g, taskId, cliFinding = null) {
|
|
|
396
398
|
return out.join('\n');
|
|
397
399
|
}
|
|
398
400
|
|
|
401
|
+
// task 1002903: a reviewer scored below threshold and named NO major/blocker
|
|
402
|
+
// finding, so the severity gate (task 1002663) downgraded it — the ship parks for
|
|
403
|
+
// a human rather than hard-failing. The ordinary block below would print "FAIL"
|
|
404
|
+
// and "Fix the issues above" over an empty list, which is how task 1002721's
|
|
405
|
+
// builder read a real grade as a broken one. Sits below the outage and
|
|
406
|
+
// authorization shapes, and steps aside when any finding IS substantive or a
|
|
407
|
+
// gating worker hard-failed (then the ordinary block and its findings apply).
|
|
408
|
+
// The record's own list wins; the workers are the fallback for a grade written
|
|
409
|
+
// before signals.unbacked_scores existed.
|
|
410
|
+
const gradeThreshold = Number(g.threshold);
|
|
411
|
+
const unbacked = Array.isArray(g.signals?.unbacked_scores)
|
|
412
|
+
? g.signals.unbacked_scores
|
|
413
|
+
: Number.isFinite(gradeThreshold) ? unbackedLowScores(g.signals?.workers, { threshold: gradeThreshold }) : [];
|
|
414
|
+
const hasSubstantive = (g.issues || []).some((i) => i && (i.severity === 'blocker' || i.severity === 'major'));
|
|
415
|
+
if (!g.passed && unbacked.length > 0 && !hasSubstantive && g.signals?.panel_outcome !== 'fail') {
|
|
416
|
+
out.push('');
|
|
417
|
+
out.push(`Subagent grade: NOT PASSED — UNBACKED LOW SCORE · a reviewer scored below ${g.threshold ?? '?'} without naming a finding — NOT a findings-backed rejection`);
|
|
418
|
+
for (const u of unbacked) {
|
|
419
|
+
out.push(` ${u.kind}: ${u.axis} ${u.score}/10 — no major or blocker finding stands behind it`);
|
|
420
|
+
}
|
|
421
|
+
if (g.notes_md) out.push(g.notes_md.split('\n').map((l) => ' ' + l).join('\n'));
|
|
422
|
+
out.push('');
|
|
423
|
+
out.push('There is nothing listed to fix. The severity gate does not let an unbacked score hard-fail a ship, so the task stays at completed for a human instead.');
|
|
424
|
+
out.push('Re-grade once — an unbacked score often does not repeat:');
|
|
425
|
+
out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
|
|
426
|
+
out.push('If it repeats on work you can defend, file an override-request with your evidence (recorded; an Archon decides):');
|
|
427
|
+
out.push(` node scripts/gds/api.js POST /api/bongos/tasks/${taskId}/override-request --body-file rationale.json # {"rationale":"<why the grade is wrong, specifically>"}`);
|
|
428
|
+
out.push(...plainCardLines('grade_unbacked_score'));
|
|
429
|
+
return out.join('\n');
|
|
430
|
+
}
|
|
431
|
+
|
|
399
432
|
const verdict = g.passed ? 'PASS' : 'FAIL';
|
|
400
433
|
out.push('');
|
|
401
434
|
out.push(`Subagent grade: ${verdict} · blended ${g.score}/10 · threshold ${g.threshold} (both axes)`);
|
package/scripts/gds/ship-land.js
CHANGED
|
@@ -16,6 +16,9 @@ const { gitArgvOk, gitOk, sanitizeGitHubRefs, shellOk } = require('./ship-git.js
|
|
|
16
16
|
const { buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded, deployedCommitStatus, fetchDeployedVersion, ghArgvOk, ghJson, pushVia, resolveInstanceBase } = require('./ship-deploy-target.js');
|
|
17
17
|
const { normalizeApiError } = require('./ship-io.js');
|
|
18
18
|
const { plainCardLines } = require('./plain-cards.js');
|
|
19
|
+
// The server's own reader of a PR's check rollup (task 1003988), reused so the laptop
|
|
20
|
+
// poll names a red PR exactly the way publish-status does (task 1003764). Dependency-free.
|
|
21
|
+
const { readCheckRollup, UNIT_REPORT_CONTEXT } = require('../../modules/lifecycle/check-rollup.js');
|
|
19
22
|
|
|
20
23
|
// ---------- the land outcome (task 1002541, part 1) ----------
|
|
21
24
|
//
|
|
@@ -284,7 +287,19 @@ function strandNextStep(taskId, via) {
|
|
|
284
287
|
// too: the server flags a strand needs_rebase (definitive shapes within a sweep,
|
|
285
288
|
// ambiguous ones at ~30 min), /builder-start warns, and new claims gate on it.
|
|
286
289
|
// Pure + exported so the wording is pinned by tests/ship_ci_deploy.mjs.
|
|
287
|
-
|
|
290
|
+
//
|
|
291
|
+
// task 1003764: `red` — the PR was RED on the poll's last read. "NOT a failure" is then
|
|
292
|
+
// false: a required check that failed does not pass by waiting, so the banner says the
|
|
293
|
+
// land is stuck on it and defers to the red lines above for the remedy.
|
|
294
|
+
function landPendingLines(taskId, { merged, red } = {}) {
|
|
295
|
+
if (red) {
|
|
296
|
+
return [
|
|
297
|
+
' ci-land: Published, but the PR is RED (the check named above), so it has NOT merged and will not',
|
|
298
|
+
' until that check passes. The task stays at \'confirmed\'. Do what the lines above say, then re-run',
|
|
299
|
+
` \`node scripts/gds/ship.js ${taskId}\` — it resumes from here (task 941). If it stays red, the server flags`,
|
|
300
|
+
' the task needs_rebase — /builder-start will warn you and new claims are gated until it lands or is released.',
|
|
301
|
+
];
|
|
302
|
+
}
|
|
288
303
|
return [
|
|
289
304
|
` ci-land: ✓ Published — auto-merge is armed${merged ? ' and the PR has merged' : ''}. This is NOT a failure.`,
|
|
290
305
|
` CI is still finishing; GitHub will ${merged ? 'deploy' : 'merge + deploy'} automatically once the required checks pass,`,
|
|
@@ -298,7 +313,9 @@ function landPendingLines(taskId, { merged } = {}) {
|
|
|
298
313
|
function printLandPending(taskId, opts = {}) {
|
|
299
314
|
console.log('');
|
|
300
315
|
for (const line of landPendingLines(taskId, opts)) console.log(line);
|
|
301
|
-
|
|
316
|
+
// The land_pending card says there is nothing to do, which a red PR makes untrue; the
|
|
317
|
+
// red line already printed the failing_check card (task 1003764).
|
|
318
|
+
if (!opts.red) for (const line of plainCardLines('land_pending')) console.log(line);
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
// task 1002645 — the honest close-out when the PR MERGED but the server refused the
|
|
@@ -349,9 +366,97 @@ function redPrLine(md) {
|
|
|
349
366
|
// redPrLines — the red line followed by the same reason in plain words (task 1003989).
|
|
350
367
|
// The card is keyed on the merge reason; when the failing tests were readable that
|
|
351
368
|
// reason is `failing_check:<name>`, which resolves to the failing_check card.
|
|
352
|
-
|
|
369
|
+
// task 1003764: the merge trap sits between them. `opts` = { taskId, via } for its wording.
|
|
370
|
+
function redPrLines(md, opts = {}) {
|
|
353
371
|
const red = redPrLine(md);
|
|
354
|
-
return red ? [red, ...plainCardLines((md && md.reason) || 'failing_check')] : [];
|
|
372
|
+
return red ? [red, ...mergeTrapLines(md, opts), ...plainCardLines((md && md.reason) || 'failing_check')] : [];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// The unit tests that gate GENERATED files — each compares a committed artifact with a
|
|
376
|
+
// fresh build of it. `fitness` holds the combined gate (fitness.js "generated artifacts
|
|
377
|
+
// fresh": copy registry, API docs, file map, nested CLAUDE.md blocks, the §13 snippet).
|
|
378
|
+
// tests/ship_land_red_pr.mjs fails if one of these names stops being a real test file.
|
|
379
|
+
const FRESHNESS_TESTS = new Set(['fitness', 'api_docs', 'api_client', 'file_map', 'repo_map', 'session_index', 'interaction_posture']);
|
|
380
|
+
|
|
381
|
+
// failingTestNames — "2 failed: a, b" (the unit-report line, run-unit-tests.js
|
|
382
|
+
// describeFailures) → ['a', 'b']. A line cut at the status limit ends in '…'; the cut
|
|
383
|
+
// name is dropped rather than guessed at. Pure.
|
|
384
|
+
function failingTestNames(text) {
|
|
385
|
+
const m = /^\s*\d+\s+failed:\s*(.*)$/.exec(String(text || ''));
|
|
386
|
+
if (!m) return [];
|
|
387
|
+
const names = m[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
388
|
+
if (names.length && names[names.length - 1].endsWith('…')) names.pop();
|
|
389
|
+
return names;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// mergeTrapLines — task 1003764. CI tests the MERGE, not the branch: a pull_request run
|
|
393
|
+
// checks out the PR merged into the CURRENT main, so a branch that was green when pushed
|
|
394
|
+
// goes red when main moves under it, and re-running the test on the branch shows nothing
|
|
395
|
+
// wrong. Task 1003747's land sat "waiting" for ~20 minutes of git archaeology on exactly
|
|
396
|
+
// that — main moved six commits, the copy registry went stale in the merged tree only.
|
|
397
|
+
// Every red PR gets the trap named; a failing freshness gate also gets its healing command,
|
|
398
|
+
// which differs by push path (a credential-less machine cannot merge main locally, and the
|
|
399
|
+
// server heals it for them — publish-reconciler refreshStaleGenerated). Pure + exported.
|
|
400
|
+
function mergeTrapLines(md, { taskId = '<id>', via = 'local' } = {}) {
|
|
401
|
+
const lines = [
|
|
402
|
+
' ci-land: CI tests your branch MERGED with the current main, not the branch alone — if the failure',
|
|
403
|
+
via === 'server'
|
|
404
|
+
? ' does not reproduce on your branch, main moved under the PR after it was pushed.'
|
|
405
|
+
: ' does not reproduce on your branch, main moved under it: `git fetch origin && git merge origin/main`, then re-run it.',
|
|
406
|
+
];
|
|
407
|
+
const gates = failingTestNames(md && md.failing_tests).filter((n) => FRESHNESS_TESTS.has(n));
|
|
408
|
+
if (!gates.length) return lines;
|
|
409
|
+
const named = gates.map((g) => `\`${g}\``).join(', ');
|
|
410
|
+
lines.push(` ci-land: ${named} ${gates.length > 1 ? 'are freshness gates' : 'is a freshness gate'}, so "fix it" here means regenerate: a generated file (copy`);
|
|
411
|
+
lines.push(' registry, API docs, repo map…) is stale in the MERGED tree even though your branch was fresh.');
|
|
412
|
+
if (via === 'server') {
|
|
413
|
+
lines.push(' The server regenerates stale generated files on the merged tree itself (every ~5 min, up to 3 tries),');
|
|
414
|
+
lines.push(' so this usually clears without you; if it stays red the server flags the task needs_rebase.');
|
|
415
|
+
} else {
|
|
416
|
+
lines.push(` Heal it: \`git fetch origin && git merge origin/main\`, then re-run \`node scripts/gds/ship.js ${taskId}\` — the ship`);
|
|
417
|
+
lines.push(' regenerates every generated file on the merged tree before it pushes (`node scripts/gds/ship-check.js` shows which).');
|
|
418
|
+
}
|
|
419
|
+
lines.push(' A busy main can re-stale it while CI re-runs; a repeat is that race, not a new fault.');
|
|
420
|
+
return lines;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// redBlocksMerge — is this red the kind that stops the merge? A failing REQUIRED check
|
|
424
|
+
// (`failing_check…`) does; a failing optional one does not (GitHub still merges an UNSTABLE
|
|
425
|
+
// PR), so only the first may turn the closing banner red (task 1003764). Pure + exported.
|
|
426
|
+
function redBlocksMerge(md) {
|
|
427
|
+
return /^failing_check/.test(String((md && md.reason) || ''));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// A check conclusion (CheckRun) or state (status context) that means FAILED. Pending and
|
|
431
|
+
// expected are waiting, not red — reporting them as failures would be the mirror image of
|
|
432
|
+
// the bug this names (a failure dressed as waiting).
|
|
433
|
+
const RED_CONCLUSIONS = new Set(['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE']);
|
|
434
|
+
|
|
435
|
+
// localMergeDriver — task 1003764. The laptop path watches its PR with the builder's own
|
|
436
|
+
// gh, and read only state + mergeCommit, so a red required check was invisible for the
|
|
437
|
+
// whole wait. This reads `gh pr view --json statusCheckRollup,mergeStateStatus,headRefOid`
|
|
438
|
+
// through the server's parser and answers in publish-status's merge_driver shape, so both
|
|
439
|
+
// poll paths hand redPrLines the same thing. `failingTestsFor(sha)` reads the unit-report
|
|
440
|
+
// status (gh pr view omits a status's description) and is asked only once that status is
|
|
441
|
+
// itself red, i.e. posted. Null when nothing is red. Pure + exported.
|
|
442
|
+
function localMergeDriver(pr, failingTestsFor = () => null) {
|
|
443
|
+
if (!pr || pr.state !== 'OPEN' || !Array.isArray(pr.statusCheckRollup)) return null;
|
|
444
|
+
const { checks } = readCheckRollup({ contexts: { nodes: pr.statusCheckRollup } });
|
|
445
|
+
const red = checks.filter((c) => RED_CONCLUSIONS.has(String(c.conclusion || '').toUpperCase()));
|
|
446
|
+
if (!red.length) return null;
|
|
447
|
+
const kind = pr.mergeStateStatus === 'UNSTABLE' ? 'non_required_check_failing' : 'failing_check';
|
|
448
|
+
const reported = red.some((c) => c.name === UNIT_REPORT_CONTEXT);
|
|
449
|
+
return { reason: `${kind}:${red[0].name}`, failing_tests: (reported && failingTestsFor(pr.headRefOid)) || null };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// unitReportFor — the failing-test line the `unit` workflow posted on `sha`, read with this
|
|
453
|
+
// machine's gh through the same parser the server uses. Null when unreadable; the red line
|
|
454
|
+
// then names the check instead, as it did before the report existed. `gh` is injectable.
|
|
455
|
+
function unitReportFor(sha, gh = ghJson) {
|
|
456
|
+
if (!/^[0-9a-f]{7,64}$/i.test(String(sha || ''))) return null;
|
|
457
|
+
const combined = gh(['api', `repos/{owner}/{repo}/commits/${sha}/status`]);
|
|
458
|
+
const nodes = ((combined && combined.statuses) || []).map((s) => ({ ...s, __typename: 'StatusContext' }));
|
|
459
|
+
return readCheckRollup({ contexts: { nodes } }).failing_tests;
|
|
355
460
|
}
|
|
356
461
|
|
|
357
462
|
// task 1003499 — a CONFLICTING PR is a land that cannot happen, however green it is.
|
|
@@ -386,7 +491,7 @@ function conflictPrLines(taskId) {
|
|
|
386
491
|
|
|
387
492
|
// noteConflict — one poll's worth of conflict tracking, shared by both loops. Prints
|
|
388
493
|
// the lines above when the PR BECOMES conflicting (not on every 10s poll, and once
|
|
389
|
-
// more if main moves on and undoes a heal), and returns
|
|
494
|
+
// more if main moves on and undoes a heal), and returns whether it conflicts now.
|
|
390
495
|
// A failed read keeps the last answer rather than inventing a new one.
|
|
391
496
|
function noteConflict(taskId, pr, wasConflicting) {
|
|
392
497
|
if (!pr) return wasConflicting;
|
|
@@ -434,14 +539,27 @@ async function pollServerPublish(taskId, branch, deps = {}) {
|
|
|
434
539
|
let blockedReason = null;
|
|
435
540
|
let redLine = null;
|
|
436
541
|
let conflicting = false;
|
|
542
|
+
let stuckRed = false; // task 1003764: a failing required check on the LAST read turns the closing banner red
|
|
543
|
+
let readFailure = null;
|
|
437
544
|
for (let i = 0; i < TRIES; i++) {
|
|
438
545
|
const r = await readStatus({ id: taskId, query: { branch } });
|
|
546
|
+
if (!r.ok) {
|
|
547
|
+
// task 1003764: a failed status read used to be skipped in silence, so a broken
|
|
548
|
+
// diagnostic looked exactly like slow CI. Say so once per distinct failure.
|
|
549
|
+
const { code } = normalizeApiError(r.data);
|
|
550
|
+
const why = `HTTP ${r.status}${code ? ` ${code}` : ''}`;
|
|
551
|
+
if (why !== readFailure) {
|
|
552
|
+
readFailure = why;
|
|
553
|
+
console.log(` ci-land: could not read the land status from the server (${why}) — still polling.`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
439
556
|
if (r.ok && r.data) {
|
|
440
557
|
const red = redPrLine(r.data.merge_driver);
|
|
441
558
|
if (red && red !== redLine) {
|
|
442
559
|
redLine = red;
|
|
443
|
-
for (const line of redPrLines(r.data.merge_driver)) console.log(line);
|
|
560
|
+
for (const line of redPrLines(r.data.merge_driver, { taskId, via: 'server' })) console.log(line);
|
|
444
561
|
}
|
|
562
|
+
stuckRed = !!red && redBlocksMerge(r.data.merge_driver);
|
|
445
563
|
conflicting = noteConflict(taskId, r.data.merge_driver, conflicting);
|
|
446
564
|
const outcome = classifyLandOutcome(r.data);
|
|
447
565
|
if (outcome === 'shipped') {
|
|
@@ -469,7 +587,7 @@ async function pollServerPublish(taskId, branch, deps = {}) {
|
|
|
469
587
|
return landBailed(`the PR merged but the server could not prove the land is this task's: ${blockedReason}`, `node scripts/gds/ship.js ${taskId}`);
|
|
470
588
|
}
|
|
471
589
|
if (conflicting) return landConflicted(taskId);
|
|
472
|
-
printLandPending(taskId);
|
|
590
|
+
printLandPending(taskId, { red: stuckRed });
|
|
473
591
|
return landPending();
|
|
474
592
|
}
|
|
475
593
|
|
|
@@ -793,11 +911,22 @@ function pollCiDeploy(taskId, branch, headSha, deps = {}) {
|
|
|
793
911
|
console.log(' ci-land: waiting for auto-merge (checks + branch protection)...');
|
|
794
912
|
let mergeSha = null;
|
|
795
913
|
let conflicting = false;
|
|
914
|
+
// task 1003764: say WHY it has not merged. This loop read only state + mergeCommit,
|
|
915
|
+
// so a red required check — the likeliest reason — went unnamed for the whole wait.
|
|
916
|
+
// The unit-report line (which failing tests) is fetched once per head sha.
|
|
917
|
+
let redLine = null;
|
|
918
|
+
let stuckRed = false;
|
|
919
|
+
let report = { sha: undefined, text: null };
|
|
920
|
+
const failingTestsFor = (sha) => {
|
|
921
|
+
if (sha !== report.sha) report = { sha, text: unitReportFor(sha, readJson) };
|
|
922
|
+
return report.text;
|
|
923
|
+
};
|
|
796
924
|
for (let i = 0; i < MERGE_TRIES; i++) {
|
|
797
925
|
// task 1003499: mergeable + mergeStateStatus ride the same read, so a conflict is
|
|
798
926
|
// named on the poll it appears; the two-field read is the fallback for a gh that
|
|
799
927
|
// predates those fields, which keeps this no worse than before on any machine.
|
|
800
|
-
|
|
928
|
+
// task 1003764: so do headRefOid + statusCheckRollup, so a red check is named too.
|
|
929
|
+
const pr = readJson(['pr', 'view', branch, '--json', 'state,mergeCommit,mergeable,mergeStateStatus,headRefOid,statusCheckRollup'])
|
|
801
930
|
|| readJson(['pr', 'view', branch, '--json', 'state,mergeCommit']);
|
|
802
931
|
if (pr && pr.state === 'MERGED') {
|
|
803
932
|
mergeSha = pr.mergeCommit && pr.mergeCommit.oid;
|
|
@@ -807,6 +936,15 @@ function pollCiDeploy(taskId, branch, headSha, deps = {}) {
|
|
|
807
936
|
console.log(` ci-land: PR was CLOSED without merging (a required check failed, or it was closed manually). Fix it and re-run \`node scripts/gds/ship.js ${taskId}\` to resume. Task stays at 'confirmed'.`);
|
|
808
937
|
return landBailed('the PR was closed without merging (a check failed, or it was closed manually)', `node scripts/gds/ship.js ${taskId}`);
|
|
809
938
|
}
|
|
939
|
+
if (pr) {
|
|
940
|
+
const md = localMergeDriver(pr, failingTestsFor);
|
|
941
|
+
const red = redPrLine(md);
|
|
942
|
+
if (red && red !== redLine) {
|
|
943
|
+
redLine = red;
|
|
944
|
+
for (const line of redPrLines(md, { taskId, via: 'local' })) console.log(line);
|
|
945
|
+
}
|
|
946
|
+
stuckRed = !!red && redBlocksMerge(md);
|
|
947
|
+
}
|
|
810
948
|
conflicting = noteConflict(taskId, pr, conflicting);
|
|
811
949
|
pause();
|
|
812
950
|
}
|
|
@@ -814,14 +952,15 @@ function pollCiDeploy(taskId, branch, headSha, deps = {}) {
|
|
|
814
952
|
// task 1049: PR open + auto-merge armed but not merged in-window = the publish
|
|
815
953
|
// SUCCEEDED and is still in CI — success-pending, not a failure. Unless it is
|
|
816
954
|
// CONFLICTING (task 1003499): then nothing is still in CI, and it never merges.
|
|
955
|
+
// Or RED (task 1003764): then the banner says the land is stuck on that check.
|
|
817
956
|
if (conflicting) return landConflicted(taskId);
|
|
818
|
-
printLandPending(taskId);
|
|
957
|
+
printLandPending(taskId, { red: stuckRed });
|
|
819
958
|
return landPending();
|
|
820
959
|
}
|
|
821
960
|
console.log(` ci-land: merged to main (${mergeSha.slice(0, 8)}). Waiting for the prod deploy...`);
|
|
822
961
|
|
|
823
962
|
for (let i = 0; i < DEPLOY_TRIES; i++) {
|
|
824
|
-
const runs =
|
|
963
|
+
const runs = readJson([
|
|
825
964
|
'run', 'list', '--workflow', 'deploy-prod.yml', '--branch', 'main',
|
|
826
965
|
'--json', 'headSha,status,conclusion', '--limit', '15',
|
|
827
966
|
]);
|
|
@@ -846,7 +985,7 @@ function pollCiDeploy(taskId, branch, headSha, deps = {}) {
|
|
|
846
985
|
}
|
|
847
986
|
return landed();
|
|
848
987
|
}
|
|
849
|
-
|
|
988
|
+
pause();
|
|
850
989
|
}
|
|
851
990
|
// task 1049: merged but the deploy run hasn't confirmed in-window — still
|
|
852
991
|
// success-pending (it deploys on its own), not a failure.
|
|
@@ -890,6 +1029,13 @@ module.exports = {
|
|
|
890
1029
|
landConflicted,
|
|
891
1030
|
pollCiDeploy,
|
|
892
1031
|
pollServerPublish,
|
|
1032
|
+
// task 1003764: name WHY a published PR has not merged when a check is red.
|
|
1033
|
+
FRESHNESS_TESTS,
|
|
1034
|
+
failingTestNames,
|
|
1035
|
+
mergeTrapLines,
|
|
1036
|
+
redBlocksMerge,
|
|
1037
|
+
localMergeDriver,
|
|
1038
|
+
unitReportFor,
|
|
893
1039
|
reconciledCardStatus,
|
|
894
1040
|
strandCommand,
|
|
895
1041
|
strandNextStep,
|
|
@@ -32,6 +32,7 @@ const { gateMergeResult } = require('./ship-merge-gate.js');
|
|
|
32
32
|
const { dbOnlyIgnoredNotice } = require('./ship-honesty.js');
|
|
33
33
|
const { currentBranch, mainWorktreeBusy, regenerateApiClient, regenerateApiDocs, regenerateCopyInventory, regenerateDiagrams, regenerateFileMap, regenerateRepoMap, regenerateSessionIndex } = require('./ship-regen.js');
|
|
34
34
|
const { REPO_ROOT } = require('./ship-state.js');
|
|
35
|
+
const { envPrefix } = require('../../src/instance-config');
|
|
35
36
|
|
|
36
37
|
// zeroCommitVerdict — dbOnly is deliberately consulted ONLY at zero commits
|
|
37
38
|
// ahead: a branch carrying real commits must be merged, whatever flag was typed.
|
|
@@ -287,7 +288,7 @@ async function mergeLocally(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
|
287
288
|
|
|
288
289
|
console.log(' pushing merged main to origin...');
|
|
289
290
|
// SEC #863: route through push-main.js, which carries the
|
|
290
|
-
//
|
|
291
|
+
// <PREFIX>_ALLOW_MAIN_PUSH override so the pre-push permission gate lets THIS
|
|
291
292
|
// graded+confirmed pipeline push through while still blocking ad-hoc
|
|
292
293
|
// `git push origin main`. process.execPath (quoted) for the same
|
|
293
294
|
// cross-platform reason as the other node spawns here (#660).
|
|
@@ -298,7 +299,7 @@ async function mergeLocally(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
|
298
299
|
// the ci PR path before this point; reaching this with protection on
|
|
299
300
|
// means an env-forced 'laptop' or a failed protection probe.
|
|
300
301
|
if (mainIsProtected() === true) {
|
|
301
|
-
return landBailed(
|
|
302
|
+
return landBailed(`main is branch-protected — the direct push can never land; unset the DEPLOY_MODE env override (${envPrefix()}_DEPLOY_MODE, or the legacy OTB_DEPLOY_MODE) — the ci PR path lands it`, `node scripts/gds/ship.js ${taskId}`);
|
|
302
303
|
}
|
|
303
304
|
return landBailed('could not push the merged main to origin', '/merge-mode');
|
|
304
305
|
}
|