@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.
Files changed (151) hide show
  1. package/.bongos-core.json +322 -117
  2. package/.claude/skills/builder-start/SKILL.md +1 -1
  3. package/.claude/skills/grade-audit/SKILL.md +1 -1
  4. package/.claude/skills/grade-recover/SKILL.md +2 -1
  5. package/.claude/skills/grader-health/SKILL.md +5 -1
  6. package/.claude/skills/merge-mode/SKILL.md +1 -1
  7. package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
  8. package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
  9. package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
  10. package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
  11. package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
  12. package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
  13. package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
  14. package/docs/adr/README.md +4 -4
  15. package/docs/api/openapi.json +1 -1
  16. package/docs/architecture.md +4 -3
  17. package/docs/canonical-permissions.md +2 -2
  18. package/docs/copy-inventory.md +34 -30
  19. package/docs/copy-registry.json +69 -33
  20. package/docs/file-map.md +4 -3
  21. package/docs/module-api-changelog.md +4 -0
  22. package/docs/recipes/autobongos-windows-host.md +3 -1
  23. package/docs/recipes/local-dev.md +45 -3
  24. package/docs/recipes/packaging-the-core.md +3 -1
  25. package/docs/recipes/redteam-patrol.md +2 -2
  26. package/docs/recipes/upgrading-the-core.md +1 -1
  27. package/modules/autonomy/cadence.js +52 -0
  28. package/modules/autonomy/gauge.js +57 -9
  29. package/modules/government/protected-surfaces.json +1 -0
  30. package/modules/grading/grade-engine.js +267 -0
  31. package/modules/grading/grader-subagent.js +4 -3
  32. package/modules/grading/grader-workers/index.js +67 -11
  33. package/modules/grading/grader-workers/worker-verdict.js +32 -2
  34. package/modules/grading/grader.js +109 -88
  35. package/modules/hall-ui/public/deploy.js +35 -0
  36. package/modules/hall-ui/public/project-settings.css +7 -0
  37. package/modules/hall-ui/records/builders-roster.md +10 -0
  38. package/modules/hall-ui/records/catalog.md +10 -0
  39. package/modules/hall-ui/records/collaboration.md +14 -0
  40. package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
  41. package/modules/hall-ui/records/oversight-family.md +19 -0
  42. package/modules/hall-ui/records/panel-family.md +19 -0
  43. package/modules/hall-ui/records/reading-rooms.md +12 -0
  44. package/modules/hall-ui/records/record-family.md +13 -0
  45. package/modules/hall-ui/records/reference-room.md +9 -0
  46. package/modules/hall-ui/records/roster-and-ledger.md +9 -0
  47. package/modules/hall-ui/records/sky.md +23 -0
  48. package/modules/lifecycle/dependency-advisory.js +2 -1
  49. package/modules/lifecycle/routes/tasks.js +19 -4
  50. package/modules/lifecycle/ship-preflight.js +91 -0
  51. package/modules/provisioning/control-plane-guard.js +14 -13
  52. package/modules/provisioning/core-upgrade.js +30 -17
  53. package/modules/provisioning/public-refusal.js +44 -0
  54. package/modules/provisioning/routes/disconnect.js +2 -1
  55. package/modules/provisioning/routes/env-manifest.js +5 -4
  56. package/modules/provisioning/routes/provisioning.js +16 -15
  57. package/modules/provisioning/routes/repo-private.js +2 -1
  58. package/package-lock.json +2 -2
  59. package/package.json +1 -1
  60. package/release-notes.json +7481 -0
  61. package/scripts/gds/address-park-sweep.sh +3 -1
  62. package/scripts/gds/autobongos-run.js +91 -3
  63. package/scripts/gds/backfill-cost.js +20 -9
  64. package/scripts/gds/baseline-staleness.js +1 -1
  65. package/scripts/gds/claim.js +2 -2
  66. package/scripts/gds/credential-presence.js +281 -0
  67. package/scripts/gds/db-backup-nightly.sh +9 -4
  68. package/scripts/gds/db-backup-offsite.sh +4 -2
  69. package/scripts/gds/deploy-main-audit-gate.sh +11 -4
  70. package/scripts/gds/dev-lib.js +212 -5
  71. package/scripts/gds/dev.js +102 -50
  72. package/scripts/gds/doctor.js +3 -2
  73. package/scripts/gds/grade-correlation-audit.js +280 -0
  74. package/scripts/gds/grade-replay.js +237 -0
  75. package/scripts/gds/hooks/pre-push.js +7 -3
  76. package/scripts/gds/main-audit.js +19 -10
  77. package/scripts/gds/memory-map.js +3 -1
  78. package/scripts/gds/onboarding-config.js +3 -2
  79. package/scripts/gds/package-core.js +72 -1
  80. package/scripts/gds/plain-cards.js +4 -0
  81. package/scripts/gds/preflight.js +12 -4
  82. package/scripts/gds/provision-core-upgrade.js +282 -80
  83. package/scripts/gds/provision-repo-auth.js +15 -0
  84. package/scripts/gds/provision.js +1 -1
  85. package/scripts/gds/publish-credential-check.js +3 -0
  86. package/scripts/gds/push-main.js +14 -3
  87. package/scripts/gds/push-path-brief.js +3 -2
  88. package/scripts/gds/redteam-patrol.js +4 -4
  89. package/scripts/gds/release-notes.js +271 -0
  90. package/scripts/gds/run-routine.js +18 -5
  91. package/scripts/gds/run-unit-tests.js +14 -3
  92. package/scripts/gds/seed-board-room-tasks.js +1 -1
  93. package/scripts/gds/session-cost.js +12 -3
  94. package/scripts/gds/ship-deploy-target.js +11 -6
  95. package/scripts/gds/ship-flow.js +5 -3
  96. package/scripts/gds/ship-grade-steps.js +33 -0
  97. package/scripts/gds/ship-land.js +241 -16
  98. package/scripts/gds/ship-merge.js +3 -2
  99. package/scripts/gds/ship-preflight-steps.js +86 -11
  100. package/scripts/gds/ship.js +1 -1
  101. package/scripts/gds/stray-unit-sweep.sh +3 -1
  102. package/scripts/gds/upgrade-migrate.js +67 -0
  103. package/scripts/gds/upgrade.js +9 -7
  104. package/scripts/hall-preview/README.md +20 -3
  105. package/scripts/hall-preview/live-identity.js +1 -1
  106. package/scripts/hall-preview/refresh-fixtures.js +71 -11
  107. package/scripts/hall-preview/scrub-identity.js +296 -0
  108. package/src/bongos/routes/security.js +3 -3
  109. package/src/build-info.js +4 -2
  110. package/src/module-api.js +1 -1
  111. package/tests/adr_0089_scope.mjs +70 -0
  112. package/tests/adr_0162_citation.mjs +184 -0
  113. package/tests/autobongos_cadence.mjs +74 -0
  114. package/tests/autobongos_loop.mjs +114 -0
  115. package/tests/autonomy_gauge.mjs +56 -0
  116. package/tests/consumer_layout_boot.mjs +363 -31
  117. package/tests/core_upgrade_door.mjs +40 -6
  118. package/tests/core_upgrade_runner.mjs +329 -5
  119. package/tests/credential_presence.mjs +153 -0
  120. package/tests/db_backup_nightly.mjs +21 -3
  121. package/tests/db_backup_offsite.mjs +81 -0
  122. package/tests/deploy_audit_gate.mjs +5 -2
  123. package/tests/deploy_page_projects.mjs +63 -2
  124. package/tests/dev_lib.mjs +2 -1
  125. package/tests/dev_windows.mjs +324 -0
  126. package/tests/env_knobs_neutral_prefix.mjs +203 -0
  127. package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
  128. package/tests/government_protected_surfaces.mjs +10 -0
  129. package/tests/grade_correlation_audit.mjs +159 -0
  130. package/tests/grade_engine.mjs +249 -0
  131. package/tests/grade_engine_replay.mjs +99 -0
  132. package/tests/grader_prompt_window_bound.mjs +132 -0
  133. package/tests/grader_unbacked_score.mjs +194 -0
  134. package/tests/hall_preview_scrub.mjs +214 -0
  135. package/tests/hall_records.mjs +85 -0
  136. package/tests/helpers.mjs +25 -0
  137. package/tests/instance_db_migrated_on_code_move.mjs +8 -1
  138. package/tests/integration_parked_ratchet.mjs +24 -1
  139. package/tests/plain_cards.mjs +1 -1
  140. package/tests/provisioning_public_refusal.mjs +195 -0
  141. package/tests/publish_status_branch.mjs +128 -0
  142. package/tests/redteam_patrol.mjs +1 -1
  143. package/tests/release_notes.mjs +327 -0
  144. package/tests/shell_stat_portability.mjs +49 -0
  145. package/tests/ship_land_conflict.mjs +181 -0
  146. package/tests/ship_land_red_pr.mjs +236 -0
  147. package/tests/ship_preflight.mjs +226 -3
  148. package/tests/start_env_guard.mjs +16 -0
  149. package/tests/sweep_app_account.mjs +15 -2
  150. package/tests/upgrade.mjs +20 -1
  151. package/tests/upgrade_migrate.mjs +195 -0
@@ -0,0 +1,236 @@
1
+ // tests/ship_land_red_pr.mjs — a stalled land names its red check (task 1003764, gap 2).
2
+ //
3
+ // WHY. Task 1003747's ship sat at "waiting for auto-merge + prod deploy" while a
4
+ // required check was red, and ended on "This is NOT a failure". The cause — CI tests
5
+ // the branch MERGED with main, and main had moved six commits, staling the copy
6
+ // registry in the merged tree only — took ~20 minutes of git archaeology to find,
7
+ // though it was one API call away. The laptop poll (pollCiDeploy) never looked at
8
+ // checks at all; the server poll named them but not the merge trap, and not how a
9
+ // freshness gate heals. These drive both polls with fake gh / server reads (no
10
+ // network, no sleeping) and pin the pure pieces they are built from.
11
+ //
12
+ // Run: node tests/ship_land_red_pr.mjs
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import fs from 'node:fs';
16
+ import { createRequire } from 'node:module';
17
+ import {
18
+ FRESHNESS_TESTS, failingTestNames, landPendingLines, localMergeDriver, mergeTrapLines,
19
+ pollCiDeploy, pollServerPublish, redBlocksMerge, redPrLines, unitReportFor,
20
+ } from '../scripts/gds/ship-land.js';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const githubPush = require('../modules/lifecycle/github-push.js');
24
+ const { CARDS } = require('../scripts/gds/plain-cards.js');
25
+
26
+ let passed = 0;
27
+ let failed = 0;
28
+ async function test(name, fn) {
29
+ try { await fn(); passed++; console.log(` ok ${name}`); }
30
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
31
+ }
32
+
33
+ // Run fn with console.log captured; returns { result, out }.
34
+ async function capture(fn) {
35
+ const lines = [];
36
+ const orig = console.log;
37
+ console.log = (...a) => { lines.push(a.join(' ')); };
38
+ try { return { result: await fn(), out: lines.join('\n'), lines }; }
39
+ finally { console.log = orig; }
40
+ }
41
+
42
+ const SHA = 'a'.repeat(40);
43
+ // What `gh pr view --json state,mergeCommit,mergeStateStatus,headRefOid,statusCheckRollup`
44
+ // really returns (shape taken from a live PR on this repo): check runs, then statuses.
45
+ const run = (name, conclusion, status = 'COMPLETED') => ({ __typename: 'CheckRun', name, conclusion, status, workflowName: 'unit' });
46
+ const ctx = (context, state) => ({ __typename: 'StatusContext', context, state, targetUrl: '' });
47
+ const openPr = (rollup, mergeStateStatus = 'BLOCKED') => ({ state: 'OPEN', mergeCommit: null, mergeStateStatus, headRefOid: SHA, statusCheckRollup: rollup });
48
+ const RED_ROLLUP = [run('unit', 'FAILURE'), run('report', 'SUCCESS'), ctx('gate-author-trust', 'SUCCESS'), ctx('unit-report', 'FAILURE')];
49
+ const COMBINED_STATUS = { state: 'failure', statuses: [
50
+ { context: 'gate-author-trust', state: 'success', description: 'Metic+ author' },
51
+ { context: 'unit-report', state: 'failure', description: '1 failed: fitness' },
52
+ ] };
53
+
54
+ // ---- localMergeDriver: the laptop's view of a red PR, in publish-status's shape ----
55
+
56
+ await test('localMergeDriver names the failing required check and asks for the tests once the report is posted', async () => {
57
+ const asked = [];
58
+ const md = localMergeDriver(openPr(RED_ROLLUP), (sha) => { asked.push(sha); return '1 failed: fitness'; });
59
+ assert.deepEqual(md, { reason: 'failing_check:unit', failing_tests: '1 failed: fitness' });
60
+ assert.deepEqual(asked, [SHA], 'the unit-report is read for the PR head sha');
61
+ });
62
+
63
+ await test('localMergeDriver does not ask for tests before the unit-report status exists', async () => {
64
+ let asked = 0;
65
+ const md = localMergeDriver(openPr([run('unit', 'FAILURE')]), () => { asked++; return 'x'; });
66
+ assert.deepEqual(md, { reason: 'failing_check:unit', failing_tests: null });
67
+ assert.equal(asked, 0);
68
+ });
69
+
70
+ await test('localMergeDriver: pending, expected and in-progress are waiting, not red', async () => {
71
+ for (const rollup of [
72
+ [run('unit', '', 'IN_PROGRESS')],
73
+ [ctx('unit-report', 'PENDING')],
74
+ [ctx('some-required-status', 'EXPECTED')],
75
+ [run('unit', 'SUCCESS'), run('lint', 'NEUTRAL'), run('docs', 'SKIPPED'), ctx('unit-report', 'SUCCESS')],
76
+ ]) assert.equal(localMergeDriver(openPr(rollup)), null, JSON.stringify(rollup));
77
+ });
78
+
79
+ await test('localMergeDriver: an UNSTABLE PR means the failing check is optional', async () => {
80
+ assert.equal(localMergeDriver(openPr([run('lint', 'FAILURE')], 'UNSTABLE')).reason, 'non_required_check_failing:lint');
81
+ });
82
+
83
+ await test('localMergeDriver: nothing to say for a merged/closed PR, or a gh that returned no rollup', async () => {
84
+ assert.equal(localMergeDriver(null), null);
85
+ assert.equal(localMergeDriver({ ...openPr(RED_ROLLUP), state: 'MERGED' }), null);
86
+ assert.equal(localMergeDriver({ state: 'OPEN', mergeCommit: null }), null, 'the legacy two-field read has no rollup');
87
+ });
88
+
89
+ await test('localMergeDriver agrees with the server\'s mergeBlockReason on a red required check', async () => {
90
+ const server = githubPush.mergeBlockReason({
91
+ merged: false, pr_state: 'open', mergeable: 'MERGEABLE', merge_state: 'BLOCKED', review_decision: null,
92
+ rollup_state: 'FAILURE', checks: [{ name: 'unit', conclusion: 'FAILURE', status: 'COMPLETED' }], checks_unreadable: false,
93
+ });
94
+ assert.equal(localMergeDriver(openPr([run('unit', 'FAILURE')])).reason, server);
95
+ });
96
+
97
+ // ---- unitReportFor: the failing-test line, via the same parser the server uses ----
98
+
99
+ await test('unitReportFor reads the unit-report status for the sha', async () => {
100
+ const calls = [];
101
+ const text = unitReportFor(SHA, (args) => { calls.push(args); return COMBINED_STATUS; });
102
+ assert.equal(text, '1 failed: fitness');
103
+ assert.deepEqual(calls, [['api', `repos/{owner}/{repo}/commits/${SHA}/status`]]);
104
+ });
105
+
106
+ await test('unitReportFor: a green report, an unreadable one, or a bad sha → null (and a bad sha never reaches gh)', async () => {
107
+ assert.equal(unitReportFor(SHA, () => ({ statuses: [{ context: 'unit-report', state: 'success', description: 'all unit tests passed' }] })), null);
108
+ assert.equal(unitReportFor(SHA, () => null), null);
109
+ assert.equal(unitReportFor('HEAD; rm -rf /', () => { throw new Error('must not call gh'); }), null);
110
+ });
111
+
112
+ // ---- the merge trap + the freshness heal ----
113
+
114
+ await test('failingTestNames parses the unit-report line and drops a name the status limit cut', async () => {
115
+ assert.deepEqual(failingTestNames('1 failed: fitness'), ['fitness']);
116
+ assert.deepEqual(failingTestNames('3 failed: api_docs, fitness, security'), ['api_docs', 'fitness', 'security']);
117
+ assert.deepEqual(failingTestNames('40 failed: a_test, b_test, some_lo…'), ['a_test', 'b_test']);
118
+ assert.deepEqual(failingTestNames(null), []);
119
+ assert.deepEqual(failingTestNames('all unit tests passed'), []);
120
+ });
121
+
122
+ await test('every name in FRESHNESS_TESTS is a real test file', async () => {
123
+ for (const name of FRESHNESS_TESTS) {
124
+ assert.ok(fs.existsSync(new URL(`./${name}.mjs`, import.meta.url)), `tests/${name}.mjs does not exist`);
125
+ }
126
+ });
127
+
128
+ await test('every red PR names the trap: CI tests the MERGE, not the branch', async () => {
129
+ const local = mergeTrapLines({ reason: 'failing_check:unit' }, { taskId: 42, via: 'local' }).join('\n');
130
+ assert.match(local, /MERGED with the current main, not the branch alone/);
131
+ assert.match(local, /git fetch origin && git merge origin\/main/, 'the laptop can reproduce on the merge');
132
+ const server = mergeTrapLines({ reason: 'failing_check:unit' }, { taskId: 42, via: 'server' }).join('\n');
133
+ assert.match(server, /MERGED with the current main/);
134
+ assert.ok(!/git merge origin\/main/.test(server), 'a credential-less machine cannot merge main locally — never tell it to');
135
+ });
136
+
137
+ await test('a failing freshness gate gets its healing command, per push path', async () => {
138
+ const md = { reason: 'failing_check:unit-report', failing_tests: '1 failed: fitness' };
139
+ const local = mergeTrapLines(md, { taskId: 42, via: 'local' }).join('\n');
140
+ assert.match(local, /`fitness` is a freshness gate/);
141
+ assert.match(local, /stale in the MERGED tree/);
142
+ assert.match(local, /node scripts\/gds\/ship\.js 42/, 'names the re-ship for THIS task');
143
+ assert.match(local, /ship-check\.js/);
144
+ const server = mergeTrapLines(md, { taskId: 42, via: 'server' }).join('\n');
145
+ assert.match(server, /The server regenerates stale generated files/);
146
+ const plain = mergeTrapLines({ reason: 'failing_check:unit-report', failing_tests: '1 failed: security' }, { taskId: 42 }).join('\n');
147
+ assert.ok(!/freshness gate/.test(plain), 'an ordinary failing test gets no regenerate advice');
148
+ });
149
+
150
+ await test('redPrLines: the red line first, then the trap, then the plain-words card', async () => {
151
+ const lines = redPrLines({ reason: 'failing_check:unit-report', failing_tests: '1 failed: fitness' }, { taskId: 42, via: 'local' });
152
+ assert.match(lines[0], /the PR is RED — 1 failed: fitness/);
153
+ assert.match(lines[1], /MERGED with the current main/);
154
+ const at = lines.findIndex((l) => l.includes('[otb-plain]'));
155
+ assert.ok(at > 2, 'the card still follows');
156
+ assert.ok(lines.slice(at).join('\n').includes(CARDS.failing_check.happened));
157
+ assert.deepEqual(redPrLines({ reason: 'review_required' }), [], 'nothing red → nothing printed');
158
+ });
159
+
160
+ await test('redBlocksMerge: only a failing REQUIRED check stops the merge', async () => {
161
+ assert.equal(redBlocksMerge({ reason: 'failing_check:unit' }), true);
162
+ assert.equal(redBlocksMerge({ reason: 'failing_check_unreadable' }), true);
163
+ assert.equal(redBlocksMerge({ reason: 'non_required_check_failing:lint' }), false, 'GitHub still merges an UNSTABLE PR');
164
+ assert.equal(redBlocksMerge(null), false);
165
+ });
166
+
167
+ await test('landPendingLines: a RED PR never gets "NOT a failure"; a green-but-slow one keeps it', async () => {
168
+ const red = landPendingLines(42, { red: true }).join('\n');
169
+ assert.ok(!/NOT a failure/.test(red), 'a failing required check does not pass by waiting');
170
+ assert.match(red, /RED/);
171
+ assert.match(red, /node scripts\/gds\/ship\.js 42/);
172
+ assert.match(landPendingLines(42).join('\n'), /This is NOT a failure/);
173
+ });
174
+
175
+ // ---- the two poll loops, end to end with fakes ----
176
+
177
+ await test('pollCiDeploy (laptop): a red PR is named once, the trap and heal are printed, and the banner is red', async () => {
178
+ const seen = { prView: 0, status: 0 };
179
+ const gh = (args) => {
180
+ if (args[0] === 'pr' && args[1] === 'view') { seen.prView++; return openPr(RED_ROLLUP); }
181
+ if (args[0] === 'api') { seen.status++; return COMBINED_STATUS; }
182
+ throw new Error(`unexpected gh call: ${args.join(' ')}`);
183
+ };
184
+ const { result, out } = await capture(() => pollCiDeploy(42, 'task-42', SHA, { ghJson: gh, pause: () => {} }));
185
+ assert.equal(result.pending, true, 'still a pending land — the check can yet pass');
186
+ assert.equal(seen.prView, 30, 'polled the whole window');
187
+ assert.equal(seen.status, 1, 'the unit-report is read once per head sha, not once per poll');
188
+ assert.equal(out.split('the PR is RED — 1 failed: fitness').length - 1, 1, 'named once, not thirty times');
189
+ assert.match(out, /MERGED with the current main/);
190
+ assert.match(out, /`fitness` is a freshness gate/);
191
+ assert.match(out, /node scripts\/gds\/ship\.js 42/);
192
+ assert.ok(!/This is NOT a failure/.test(out), 'the closing banner must not contradict the red line');
193
+ assert.ok(!out.includes(CARDS.land_pending.happened), 'nor the "nothing to do" card');
194
+ });
195
+
196
+ await test('pollCiDeploy: a gh that rejects the new fields falls back to the old read and still sees the merge', async () => {
197
+ const gh = (args) => {
198
+ if (args[0] === 'pr' && args.includes('state,mergeCommit')) return { state: 'MERGED', mergeCommit: { oid: 'b'.repeat(40) } };
199
+ if (args[0] === 'pr') return null; // unknown JSON field → gh exits non-zero → ghJson null
200
+ if (args[0] === 'run') return [];
201
+ throw new Error(`unexpected gh call: ${args.join(' ')}`);
202
+ };
203
+ const { result, out } = await capture(() => pollCiDeploy(42, 'task-42', SHA, { ghJson: gh, pause: () => {} }));
204
+ assert.match(out, /merged to main \(bbbbbbbb\)/);
205
+ assert.equal(result.pending, true);
206
+ });
207
+
208
+ await test('pollCiDeploy: a green PR that is merely slow keeps the reassuring banner', async () => {
209
+ const gh = (args) => (args[0] === 'pr' ? openPr([run('unit', '', 'IN_PROGRESS')]) : null);
210
+ const { out } = await capture(() => pollCiDeploy(42, 'task-42', SHA, { ghJson: gh, pause: () => {} }));
211
+ assert.ok(!/RED/.test(out));
212
+ assert.match(out, /This is NOT a failure/);
213
+ });
214
+
215
+ await test('pollServerPublish (credential-less): a failing status read is said once; a red PR gets the server-path heal', async () => {
216
+ let n = 0;
217
+ const readStatus = async () => {
218
+ n++;
219
+ if (n <= 3) return { ok: false, status: 502, data: { error: { code: 'status_failed', message: 'x' } } };
220
+ return { ok: true, status: 200, data: {
221
+ pr_state: 'open', merged: false, deploy_status: 'pending', reconcile_blocked: null,
222
+ merge_driver: { reason: 'failing_check:unit-report', failing_tests: '1 failed: fitness' },
223
+ } };
224
+ };
225
+ const { result, out } = await capture(() => pollServerPublish(42, 'task-42', { readStatus, pause: () => {} }));
226
+ assert.equal(result.pending, true);
227
+ assert.equal(out.split('could not read the land status from the server (HTTP 502 status_failed)').length - 1, 1,
228
+ 'three identical failures are reported once');
229
+ assert.equal(out.split('the PR is RED — 1 failed: fitness').length - 1, 1);
230
+ assert.match(out, /The server regenerates stale generated files/);
231
+ assert.ok(!/git merge origin\/main/.test(out), 'never tells a credential-less machine to merge main');
232
+ assert.ok(!/This is NOT a failure/.test(out));
233
+ });
234
+
235
+ console.log(`\nship-land red PR (task 1003764) tests: ${passed} passed, ${failed} failed`);
236
+ process.exitCode = failed > 0 ? 1 : 0;
@@ -12,9 +12,12 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
 
14
14
  const require = createRequire(import.meta.url);
15
- const { parsePorcelain, trackedDirty, classifyEmptyShip, cleanupMacFinderDupRefs, findMainWorktree, closeoutPlan } = require(
16
- '../modules/lifecycle/ship-preflight.js',
17
- );
15
+ const {
16
+ parsePorcelain, trackedDirty, classifyEmptyShip, cleanupMacFinderDupRefs, findMainWorktree, closeoutPlan,
17
+ untrackedPaths, untrackedLeftBehind, untrackedWarningLines,
18
+ } = require('../modules/lifecycle/ship-preflight.js');
19
+ // task 1001436: the preflight step itself + the ship-input resolver it is handed.
20
+ const { preflight, shipInputPaths, staleToolingFiles, TOOLING_PATHSPECS } = require('../scripts/gds/ship-preflight-steps.js');
18
21
 
19
22
  const tests = [];
20
23
  const test = (name, fn) => tests.push({ name, fn });
@@ -352,6 +355,226 @@ test('closeoutPlan: stashLabel embeds the task id', () => {
352
355
  assert.equal(closeoutPlan('?? x\n', 1234).stashLabel, 'otb-postship-1234');
353
356
  });
354
357
 
358
+ // ---- the residual phantom ship: untracked files left behind (task 1001436) ----
359
+ // A branch with REAL committed work ships; a new file beside it that was never
360
+ // `git add`ed silently does not. The preflight now names those files — a warning,
361
+ // never a refusal (trackedDirty must keep ignoring scratch).
362
+
363
+ test('untrackedPaths: only ?? lines, in order, unquoted; tracked changes are not listed', () => {
364
+ const out = ' M src/a.js\n?? tests/new_case.mjs\nA src/b.js\n?? "docs/with space.md"\n';
365
+ assert.deepEqual(untrackedPaths(out), ['tests/new_case.mjs', 'docs/with space.md']);
366
+ assert.deepEqual(untrackedPaths(''), []);
367
+ assert.deepEqual(untrackedPaths(null), []);
368
+ });
369
+
370
+ test('untrackedLeftBehind: nothing committed → [] (the empty-diff guard already refuses and lists them)', () => {
371
+ const out = '?? tests/new_case.mjs\n';
372
+ assert.deepEqual(untrackedLeftBehind(out, { committedFileCount: 0 }), []);
373
+ assert.deepEqual(untrackedLeftBehind(out), [], 'default opts behave as "nothing committed"');
374
+ // ...and that empty case is exactly the one classifyEmptyShip refuses on:
375
+ assert.equal(classifyEmptyShip(out).reason, 'uncommitted-work');
376
+ });
377
+
378
+ test('untrackedLeftBehind: committed work + an untracked file → that file is named', () => {
379
+ const out = ' M src/unrelated.js\n?? tests/new_case.mjs\n?? docs/adr/0999-x.md\n';
380
+ assert.deepEqual(untrackedLeftBehind(out, { committedFileCount: 3 }), ['tests/new_case.mjs', 'docs/adr/0999-x.md']);
381
+ });
382
+
383
+ test('untrackedLeftBehind: the ship\'s own input files are excluded, whatever their spelling', () => {
384
+ const out = '?? notes.md\n?? summary.txt\n?? shots/after.png\n?? tests/new_case.mjs\n';
385
+ const inputs = ['./notes.md', 'summary.txt', 'shots\\after.png'];
386
+ assert.deepEqual(untrackedLeftBehind(out, { committedFileCount: 1, inputs }), ['tests/new_case.mjs']);
387
+ });
388
+
389
+ test('untrackedWarningLines: [] when there is nothing to say', () => {
390
+ assert.deepEqual(untrackedWarningLines([]), []);
391
+ assert.deepEqual(untrackedWarningLines(null), []);
392
+ });
393
+
394
+ test('untrackedWarningLines: names the count, the task, each file, and says it does not block', () => {
395
+ const text = untrackedWarningLines(['tests/new_case.mjs', 'docs/x.md'], { taskId: 1001436 }).join('\n');
396
+ assert.match(text, /2 untracked file\(s\) in this worktree will NOT ship with task 1001436/);
397
+ assert.match(text, /- tests\/new_case\.mjs/);
398
+ assert.match(text, /- docs\/x\.md/);
399
+ assert.match(text, /Warning only — not blocking; the ship proceeds/);
400
+ });
401
+
402
+ test('untrackedWarningLines: a long list is capped, with the remainder counted', () => {
403
+ const files = Array.from({ length: 17 }, (_, i) => `scratch/f${i}.txt`);
404
+ const lines = untrackedWarningLines(files, { cap: 15 });
405
+ assert.equal(lines.filter((l) => l.startsWith(' - ')).length, 15);
406
+ assert.ok(lines.includes(' … and 2 more'));
407
+ assert.match(lines.join('\n'), /17 untracked file\(s\)/);
408
+ });
409
+
410
+ test('shipInputPaths: prose/visual flags resolve to repo-relative paths; stdin and out-of-tree paths add nothing', () => {
411
+ const root = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'otb-shipinputs-')));
412
+ const outside = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'otb-shipinputs-out-')));
413
+ try {
414
+ fs.mkdirSync(path.join(root, 'sub'));
415
+ for (const f of ['notes.md', 'summary.txt', 'sub/shot.png', 'resp.md']) fs.writeFileSync(path.join(root, f), 'x');
416
+ fs.writeFileSync(path.join(outside, 'notes.md'), 'x');
417
+ const args = [
418
+ '1001436', '--notes-file', 'notes.md', '--summary-file=summary.txt',
419
+ '--visual', path.join(root, 'sub', 'shot.png'), '--response-file', '-',
420
+ ];
421
+ assert.deepEqual(shipInputPaths(args, { cwd: root, repoRoot: root }), ['notes.md', 'summary.txt', 'sub/shot.png']);
422
+ // Run from a subdirectory: the relative flag value resolves against cwd, the result against the root.
423
+ assert.deepEqual(shipInputPaths(['1', '--response-file', '../resp.md'], { cwd: path.join(root, 'sub'), repoRoot: root }), ['resp.md']);
424
+ // A file outside the checkout cannot be an untracked file in it.
425
+ assert.deepEqual(shipInputPaths(['1', '--notes-file', path.join(outside, 'notes.md')], { cwd: root, repoRoot: root }), []);
426
+ // A flag with no value (last token) is ignored rather than resolved to the root.
427
+ assert.deepEqual(shipInputPaths(['1', '--notes-file'], { cwd: root, repoRoot: root }), []);
428
+ assert.deepEqual(shipInputPaths([], { cwd: root, repoRoot: root }), []);
429
+ } finally {
430
+ fs.rmSync(root, { recursive: true, force: true });
431
+ fs.rmSync(outside, { recursive: true, force: true });
432
+ }
433
+ });
434
+
435
+ // The behaviour, not just the helpers: run the REAL preflight() in a throwaway
436
+ // repo with a local "origin", and read what it prints. Two worlds: a branch
437
+ // carrying committed work (the warning must fire, minus inputs and ignored
438
+ // files), and a branch with nothing committed (it must stay silent — the
439
+ // empty-diff guard owns that case).
440
+ function withPreflightRepo(fn) {
441
+ const tmp = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'otb-untracked-')));
442
+ const origin = path.join(tmp, 'origin.git');
443
+ const work = path.join(tmp, 'work');
444
+ const run = (cmd, cwd = work) => execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
445
+ const prevCwd = process.cwd();
446
+ try {
447
+ execSync(`git init -q --bare -b main "${origin}"`);
448
+ execSync(`git clone -q "${origin}" "${work}"`, { stdio: ['ignore', 'pipe', 'pipe'] });
449
+ run('git config user.email t@t.t && git config user.name t && git config commit.gpgsign false');
450
+ run('git checkout -q -b main');
451
+ fs.writeFileSync(path.join(work, '.gitignore'), '*.log\n');
452
+ fs.writeFileSync(path.join(work, 'a.txt'), 'x');
453
+ run('git add -A && git commit -q -m init && git push -q origin main');
454
+ process.chdir(work);
455
+ return fn({ work, run });
456
+ } finally {
457
+ process.chdir(prevCwd);
458
+ fs.rmSync(tmp, { recursive: true, force: true });
459
+ }
460
+ }
461
+
462
+ function capturePreflight(claim, opts) {
463
+ const out = [];
464
+ const err = [];
465
+ const log = console.log;
466
+ const error = console.error;
467
+ console.log = (...a) => out.push(a.join(' '));
468
+ console.error = (...a) => err.push(a.join(' '));
469
+ try {
470
+ return { result: preflight(claim, opts), out: out.join('\n'), err: err.join('\n') };
471
+ } finally {
472
+ console.log = log;
473
+ console.error = error;
474
+ }
475
+ }
476
+
477
+ test('preflight (real git): committed work + a forgotten new file → warns, still OK, inputs + ignored files unlisted', () => {
478
+ withPreflightRepo(({ work, run }) => {
479
+ run('git checkout -q -b feature');
480
+ fs.mkdirSync(path.join(work, 'src'));
481
+ fs.writeFileSync(path.join(work, 'src', 'fix.js'), 'module.exports = 1;\n');
482
+ run('git add src/fix.js && git commit -q -m "the committed half"');
483
+ fs.mkdirSync(path.join(work, 'tests'));
484
+ fs.writeFileSync(path.join(work, 'tests', 'forgot.mjs'), '// never git-added\n');
485
+ fs.writeFileSync(path.join(work, 'notes.md'), 'handoff notes\n');
486
+ fs.writeFileSync(path.join(work, 'debug.log'), 'ignored by .gitignore\n');
487
+
488
+ const { result, out, err } = capturePreflight({ task_id: 4242, head_sha: null }, { inputFiles: ['notes.md'] });
489
+ assert.equal(result.ok, true, 'a warning must never refuse the ship');
490
+ // A wholly-untracked directory is still named file by file (--untracked-files=all).
491
+ assert.deepEqual(result.untrackedLeftBehind, ['tests/forgot.mjs']);
492
+ assert.match(err, /1 untracked file\(s\) in this worktree will NOT ship with task 4242/);
493
+ assert.match(err, /- tests\/forgot\.mjs/);
494
+ assert.doesNotMatch(err, /notes\.md/, 'the ship\'s own --notes-file is not a deliverable');
495
+ assert.doesNotMatch(err, /debug\.log/, 'an ignored file is never listed');
496
+ assert.match(out, /preflight: tracked files clean \(1 untracked file\(s\) will not ship/);
497
+ });
498
+ });
499
+
500
+ test('preflight (real git): nothing committed on the branch → no untracked warning', () => {
501
+ withPreflightRepo(({ work, run }) => {
502
+ run('git checkout -q -b feature');
503
+ fs.writeFileSync(path.join(work, 'forgot.md'), 'never git-added\n');
504
+ const { result, out, err } = capturePreflight({ task_id: 4243, head_sha: null }, {});
505
+ assert.equal(result.ok, true);
506
+ assert.deepEqual(result.untrackedLeftBehind, []);
507
+ assert.doesNotMatch(err, /Untracked-file warning/);
508
+ assert.match(out, /preflight: working tree clean/);
509
+ });
510
+ });
511
+
512
+ // ---- stale ship-tooling guard (task 1004273) ----
513
+ // The guard named ship.js + grader.js only; two carves moved the logic into ship-*.js
514
+ // and grader-*.js / grader-workers/ / grade-engine.js, so a branch behind main on any
515
+ // of those shipped with a stale copy — the #662 shape the guard exists for.
516
+
517
+ test('stale-tooling guard: every tracked ship*.js and grader file is covered by TOOLING_PATHSPECS', () => {
518
+ const root = path.resolve(import.meta.dirname, '..');
519
+ const ls = (...spec) => execSync(`git ls-files -z -- ${spec.map((s) => `'${s}'`).join(' ')}`, { cwd: root, encoding: 'utf8' })
520
+ .split('\0').filter(Boolean);
521
+ // Chosen by an independent rule (the filename), then asked of git's own pathspec matcher.
522
+ const shipTooling = ls('scripts/gds', 'modules/grading').filter((f) =>
523
+ /^scripts\/gds\/ship[^/]*\.js$/.test(f) || /^modules\/grading\/(?:grader|grade-engine)/.test(f));
524
+ assert.ok(shipTooling.length >= 25, `only ${shipTooling.length} ship/grader file(s) found — the scan is reading the wrong tree`);
525
+ assert.ok(shipTooling.includes('scripts/gds/ship-land.js') && shipTooling.includes('modules/grading/grader-score.js'));
526
+ const covered = new Set(ls(...TOOLING_PATHSPECS));
527
+ const missed = shipTooling.filter((f) => !covered.has(f));
528
+ assert.deepEqual(missed, [], `ship/grade tooling the stale-tooling guard would not refuse: ${missed.join(', ')}`);
529
+ });
530
+
531
+ // Publish the throwaway repo's main to its local bare "origin".
532
+ const publishMain = (run) => run(['git', 'push', '-q', 'origin', 'main'].join(' '));
533
+
534
+ test('preflight (real git): main moved a carved ship-*.js and added a new grader file → refused stale-tooling', () => {
535
+ withPreflightRepo(({ work, run }) => {
536
+ fs.mkdirSync(path.join(work, 'scripts', 'gds'), { recursive: true });
537
+ fs.mkdirSync(path.join(work, 'modules', 'grading'), { recursive: true });
538
+ fs.writeFileSync(path.join(work, 'scripts', 'gds', 'ship-land.js'), '// v1\n');
539
+ fs.writeFileSync(path.join(work, 'modules', 'grading', 'grader-score.js'), '// v1\n');
540
+ run('git add -A && git commit -q -m tooling');
541
+ publishMain(run);
542
+ run('git checkout -q -b feature');
543
+ fs.writeFileSync(path.join(work, 'src.txt'), 'the branch\'s own work\n');
544
+ run('git add -A && git commit -q -m work');
545
+ // main moves: one carved file changes, one file appears that this checkout never had.
546
+ run('git checkout -q main');
547
+ fs.writeFileSync(path.join(work, 'scripts', 'gds', 'ship-land.js'), '// v2\n');
548
+ fs.writeFileSync(path.join(work, 'modules', 'grading', 'grader-new.js'), '// new carve\n');
549
+ run('git add -A && git commit -q -m "main moves the tooling"');
550
+ publishMain(run);
551
+ run('git checkout -q feature');
552
+ assert.ok(!fs.existsSync(path.join(work, 'modules', 'grading', 'grader-new.js')), 'the new file is on main only');
553
+
554
+ const { result, out } = capturePreflight({ task_id: 4244, head_sha: null }, {});
555
+ assert.equal(result.ok, false);
556
+ assert.equal(result.reason, 'stale-tooling');
557
+ assert.match(out, /- scripts\/gds\/ship-land\.js/, 'a carved ship-*.js is tooling');
558
+ assert.match(out, /- modules\/grading\/grader-new\.js/, 'a file only main has is still matched');
559
+ assert.deepEqual(staleToolingFiles({ cwd: work }).sort(), ['modules/grading/grader-new.js', 'scripts/gds/ship-land.js']);
560
+ });
561
+ });
562
+
563
+ test('preflight (real git): the branch\'s OWN edit to a ship-*.js is not stale tooling', () => {
564
+ withPreflightRepo(({ work, run }) => {
565
+ fs.mkdirSync(path.join(work, 'scripts', 'gds'), { recursive: true });
566
+ fs.writeFileSync(path.join(work, 'scripts', 'gds', 'ship-land.js'), '// v1\n');
567
+ run('git add -A && git commit -q -m tooling');
568
+ publishMain(run);
569
+ run('git checkout -q -b feature');
570
+ fs.writeFileSync(path.join(work, 'scripts', 'gds', 'ship-land.js'), '// the fix this task ships\n');
571
+ run('git add -A && git commit -q -m "fix the land"');
572
+ const { result } = capturePreflight({ task_id: 4245, head_sha: null }, {});
573
+ assert.equal(result.ok, true);
574
+ assert.deepEqual(staleToolingFiles({ cwd: work }), []);
575
+ });
576
+ });
577
+
355
578
  // ---- runner ----
356
579
 
357
580
  let pass = 0;
@@ -118,6 +118,22 @@ t('a hosted origin says the core installs with no token, and never re-offers the
118
118
  assert.doesNotMatch(card, /\bprivate\b/i, 'must not call the package private');
119
119
  });
120
120
 
121
+ // task 1004288: the card above was corrected when the core went public, but the SKILL that
122
+ // routes the same fresh-clone wall kept teaching the old posture — "a PRIVATE package", "404s
123
+ // on any machine with no NPM_TOKEN", "the expected tokenless outcome". An agent that believes
124
+ // that reads a real 404 (a version never published, a mistyped name) as normal and moves on.
125
+ t('the builder-start skill agrees with the card: the core is public, and a 404 is not normal', () => {
126
+ const { readFileSync } = require('node:fs');
127
+ const path = require('node:path');
128
+ const skill = readFileSync(path.join(import.meta.dirname, '..', '.claude', 'skills', 'builder-start', 'SKILL.md'), 'utf8');
129
+ assert.doesNotMatch(skill, /PRIVATE package|private package/i, 'must not call @bongos/core a private package (ADR 0283)');
130
+ assert.doesNotMatch(skill, /expected tokenless outcome/i, 'must not present a tokenless 404 as expected');
131
+ assert.doesNotMatch(skill, /404s on any machine with no `?NPM_TOKEN/i, 'must not tell the agent a tokenless install 404s');
132
+ assert.match(skill, /PUBLIC npm package/, 'names the core as public');
133
+ assert.match(skill, /ADR 0283/, 'cites the decision that made it public');
134
+ assert.match(skill, /404 on `@bongos\/core` is therefore \*\*not\*\* a normal outcome/, 'says a 404 now means something is wrong');
135
+ });
136
+
121
137
  t('isLocalOrigin: loopback is local, everything else is remote', () => {
122
138
  for (const o of ['http://localhost:3000', 'http://app.localhost:3000', 'http://127.0.0.1:3000', 'http://[::1]:3000']) {
123
139
  assert.equal(isLocalOrigin(o), true, `${o} should be local`);
@@ -17,13 +17,17 @@ const REPO = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
17
17
  const skip = spawnSync('bash', ['-c', 'true']).error ? 'bash not on PATH' : false;
18
18
  const slash = (p) => p.replace(/\\/g, '/');
19
19
 
20
- function stubs() {
20
+ // `bsdStat` swaps in macOS's stat, which refuses GNU's `-c` outright and names an owner
21
+ // with `-f %Su` instead (task 1004170).
22
+ function stubs({ bsdStat = false } = {}) {
21
23
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sweep-stubs-'));
22
24
  const sudoLog = slash(path.join(dir, 'sudo.log'));
23
25
  const statLog = slash(path.join(dir, 'stat.log'));
24
26
  const bin = (name, body) => fs.writeFileSync(path.join(dir, name), `#!/usr/bin/env bash\n${body}\n`, { mode: 0o755 });
25
27
  bin('sudo', `echo "$*" >> "${sudoLog}"; exit 0`);
26
- bin('stat', `echo "$*" >> "${statLog}"; echo checkout-owner`);
28
+ bin('stat', `echo "$*" >> "${statLog}"; ` + (bsdStat
29
+ ? 'case "$1 $2" in "-c "*) echo "stat: illegal option -- c" >&2; exit 1 ;; "-f %Su") echo checkout-owner ;; *) exit 1 ;; esac'
30
+ : 'echo checkout-owner'));
27
31
  bin('systemctl', 'exit 0');
28
32
  const lines = (f) => (fs.existsSync(f) ? fs.readFileSync(f, 'utf8').split('\n').filter(Boolean) : []);
29
33
  return { dir, sudo: () => lines(sudoLog), stat: () => lines(statLog) };
@@ -109,4 +113,13 @@ for (const [name, src] of [['address-park-sweep', PARK], ['stray-unit-sweep', ST
109
113
  assert.match(s.stat()[0] || '', new RegExp(`^-c %U .*${path.basename(inst)}$`), 'the instance root, not the package');
110
114
  assert.match(s.sudo()[0] || '', /^-n -u checkout-owner psql /);
111
115
  });
116
+
117
+ test(`${name}: on macOS, where stat refuses GNU's -c, the checkout's owner still acts (task 1004170)`, { skip }, () => {
118
+ // It used to read '' as the owner there and refuse with "cannot use '' as the app account".
119
+ const s = stubs({ bsdStat: true });
120
+ const inst = fs.mkdtempSync(path.join(os.tmpdir(), 'sweep-inst-'));
121
+ run(src, s, { INSTANCE_DIR: slash(inst) });
122
+ assert.deepEqual(s.stat().map((l) => l.split(' ').slice(0, 2).join(' ')), ['-c %U', '-f %Su'], 'GNU is asked first and BSD answers');
123
+ assert.match(s.sudo()[0] || '', /^-n -u checkout-owner psql /);
124
+ });
112
125
  }
package/tests/upgrade.mjs CHANGED
@@ -1543,7 +1543,7 @@ t('persistPin: a refused origin push on a WIRED private instance is loud — nam
1543
1543
  assert.equal(r.refused, 'read-only-deploy-key');
1544
1544
  // The tokenless refusal is a plain sentence, not the bare status line.
1545
1545
  assert.match(r.error, /read-only deploy key at \/srv\/bongos\/acme\/\.git\/cloudbongos-deploy-key/, 'names the key');
1546
- assert.match(r.error, /no owner GitHub token is stored/, 'and why nothing else was tried');
1546
+ assert.match(r.error, /no owner GitHub token was available/, 'and why nothing else was tried');
1547
1547
  assert.notEqual(r.error, 'git push failed');
1548
1548
  const text = warnings.join('\n');
1549
1549
  assert.match(text, /PIN NOT PUSHED/, 'shouts');
@@ -1655,6 +1655,25 @@ t('resolvePinPushAuth: the owner token is spent only when one is stored AND the
1655
1655
  assert.equal(await u.resolvePinPushAuth(dir, { log: hush, instance: inst, getGithubToken: async () => { throw new Error('secret-box off'); } }, at('git@github.com:acme/widgets.git')), null);
1656
1656
  });
1657
1657
 
1658
+ t('resolvePinPushAuth: on a hosted project\u2019s own box a missing row SAYS WHY, instead of falling back silently (task 1004065)', async () => {
1659
+ // The project's own database has no provisioning table (ADR 0281), so the lookup
1660
+ // answers null there — and on a checkout wired with the read-only deploy key that is
1661
+ // not "the origin path", it is a push that cannot succeed.
1662
+ const dir = { instanceDir: '/srv/bongos/acme' };
1663
+ const wired = { config: { status: 0, stdout: 'ssh -F /dev/null -i /srv/bongos/acme/.git/cloudbongos-deploy-key -o IdentitiesOnly=yes\n' }, remote: { status: 0, stdout: 'git@github.com:acme/widgets.git\n' } };
1664
+ const said = [];
1665
+ const r = await u.resolvePinPushAuth(dir, { log: (m) => said.push(m), getInstanceBySlug: async () => null }, pinRun(wired));
1666
+ assert.equal(r, null, 'still the remote path — there is no token to use');
1667
+ const line = said.join('\n');
1668
+ assert.match(line, /no provisioning row is readable from this database/, 'names what is missing');
1669
+ assert.match(line, /read-only deploy key at \/srv\/bongos\/acme\/\.git\/cloudbongos-deploy-key/, 'and why the fallback cannot work');
1670
+ assert.match(line, /pushes the pin from the hub/, 'and where the push actually happens');
1671
+ // An UNWIRED checkout with no row (a laptop, a public instance) is the real origin path: quiet.
1672
+ const quiet = [];
1673
+ await u.resolvePinPushAuth(dir, { log: (m) => quiet.push(m), getInstanceBySlug: async () => null }, pinRun({ remote: wired.remote }));
1674
+ assert.deepEqual(quiet, [], 'no noise where the fallback is correct');
1675
+ });
1676
+
1658
1677
  t('runUpgrade --commit-pin: on a wired private instance the pin lands via the owner token, end to end', async () => {
1659
1678
  const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
1660
1679
  const src = mkdtempSync(join(tmpdir(), 'otb-upgrade-src-'));