@bongos/core 1.19.847 → 1.19.849

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.
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+ //
3
+ // scripts/gds/ship-escalation.js — the exit for a no-artifact close (task 1003950)
4
+ //
5
+ // THE DEAD END THIS REPLACES. A task that is legitimately artifact-free — an ops
6
+ // verification, a DB-only close, or one whose premise turned out to be already-done —
7
+ // could not be closed by any builder below archon, and the two documented paths pointed
8
+ // at each other:
9
+ //
10
+ // ship.js NNNN --allow-empty → implies --skip-grade → 403 task.confirm.grade_bypass
11
+ // → "re-run WITHOUT --skip-grade and use --regrade"
12
+ // ship.js NNNN --regrade → "Nothing to grade: the committed diff is empty"
13
+ // → "if this task is DELIBERATELY artifact-free, use --allow-empty"
14
+ //
15
+ // Neither exit is reachable at metic. The task parks at `completed` with the claim
16
+ // released — the strand shape the reconciler cannot heal (no branch, no diff, and
17
+ // `completed` sits BEFORE the confirm gate the reconciler sweeps from). Reproduced live
18
+ // 2026-09-16 on task 1003822.
19
+ //
20
+ // WHAT THIS DOES, AND DELIBERATELY DOES NOT DO. It does not move the gate. The owner
21
+ // decision recorded on task 1003950 scoped this to the third of the three shapes the
22
+ // bug offered — "give the refusal a working escalation that does not require the builder
23
+ // to know an archon personally" — because `task.confirm.grade_bypass` is `system: true`
24
+ // in modules/government/catalog.js, one of the four trust-boundary keys ADR 0157 names
25
+ // in its escalation-chain argument. Lowering or forking it is a governance migration and
26
+ // an ADR, filed as task 1004010; grading the NOTES instead of the diff is task 1004011.
27
+ //
28
+ // So the close stays archon-gated. What stops is the LOOP: the refusal now files the
29
+ // escalation on the builder's behalf and terminates, instead of returning them to a flag
30
+ // that will send them back here.
31
+ //
32
+ // IDEMPOTENT BY TASK, not by run — the same reasoning artifact-audit.js carries.
33
+ // POST /blockers dedupes on (source, source_ref), so a builder who re-runs --allow-empty
34
+ // three times while waiting gets ONE blocker, not three. A check that spams is a check
35
+ // that gets muted, and this one is aimed at the surface a person actually reads.
36
+
37
+ const NO_ARTIFACT_SOURCE = 'no-artifact-close';
38
+
39
+ // One blocker per task. Deliberately NOT keyed on the builder or the run: two builders
40
+ // hitting the same stuck task should land on the same blocker, not race to file two.
41
+ function escalationSourceRef(taskId) {
42
+ return `${NO_ARTIFACT_SOURCE}:${taskId}`;
43
+ }
44
+
45
+ function escalationTitle(taskId, title) {
46
+ const t = String(title || '').slice(0, 90);
47
+ return `No-artifact close needs an archon: task ${taskId}${t ? ` — ${t}` : ''}`;
48
+ }
49
+
50
+ // Pure so the wording is unit-testable without git or network. The body has to carry
51
+ // enough for the archon to make the call WITHOUT re-interviewing the builder — that is
52
+ // the whole point of filing it rather than telling them to go find someone.
53
+ function escalationBody({ taskId, title, floor, summary, notes, builder }) {
54
+ const reason = [summary, notes].filter(Boolean).join('\n\n');
55
+ const lines = [
56
+ `Task **${taskId}** is claimed to be legitimately artifact-free and cannot be closed by its builder.`,
57
+ '',
58
+ `- **Task:** ${title || '(untitled)'}`,
59
+ `- **Declared by:** ${builder || 'the claim holder'} (ran \`ship.js ${taskId} --allow-empty\`)`,
60
+ `- **Blocked on:** \`task.confirm.grade_bypass\`${floor ? ` (floor: ${floor})` : ''}`,
61
+ '',
62
+ '### Why it is stuck',
63
+ '',
64
+ 'The committed diff is empty and the working tree is clean, so the subagent grader has',
65
+ 'nothing to read. Confirming without a passing grade is archon-gated, and the builder',
66
+ 'holds no route past it. Left alone the task parks at `completed` with the claim',
67
+ 'released — the strand shape the publish reconciler cannot heal, because `completed`',
68
+ 'sits before the `confirmed` gate it sweeps from.',
69
+ '',
70
+ '### The reason the builder gave',
71
+ '',
72
+ reason || '_(none given — the ship carried no --summary or --notes)_',
73
+ '',
74
+ '### What is being asked of you',
75
+ '',
76
+ 'Decide whether this task is genuinely artifact-free. That claim is the thing worth',
77
+ 'checking here; the absent diff is not evidence either way.',
78
+ '',
79
+ `- If yes: \`POST /api/bongos/tasks/${taskId}/confirm\`, or run \`node scripts/gds/ship.js ${taskId} --allow-empty\` yourself.`,
80
+ '- If no: the work was never done, or never committed — release the claim back to `ready` and say so here.',
81
+ '',
82
+ 'Filed automatically by the ship path (task 1003950). Re-running `--allow-empty` will',
83
+ 'not file a second copy.',
84
+ ];
85
+ return lines.join('\n');
86
+ }
87
+
88
+ // The terminating refusal. THE ONE INVARIANT: these lines must never send the builder to
89
+ // --regrade, because --regrade on an empty diff sends them straight back to --allow-empty.
90
+ // That mutual advice IS the bug (task 1003950). Pure + exported so a test can assert the
91
+ // loop stays closed rather than trusting a reviewer to notice it reopening.
92
+ function noArtifactRefusalLines({ taskId, floor, escalation }) {
93
+ const lines = [
94
+ '',
95
+ 'This task has no artifact to grade, and confirming without a passing grade is',
96
+ `permission-gated: task.confirm.grade_bypass${floor ? ` (floor: ${floor})` : ''}.`,
97
+ 'This is an authorization wall, not a verdict on your work.',
98
+ '',
99
+ ];
100
+ if (escalation && escalation.filed) {
101
+ lines.push(`Escalated for you: blocker ${escalation.id} is now on the archon review queue,`);
102
+ lines.push('carrying this task, the reason you gave, and what is being asked. You do not');
103
+ lines.push('need to know an archon personally, and there is nothing further to re-run.');
104
+ } else if (escalation && escalation.reason === 'already-filed') {
105
+ lines.push('Already escalated: a blocker for this task is open on the archon review queue.');
106
+ lines.push('Re-running will not speed it up. Nothing further to do here.');
107
+ } else {
108
+ // Fail-open: the close is still blocked, so the builder must not be told it worked.
109
+ const why = escalation && escalation.reason ? ` (${escalation.reason})` : '';
110
+ lines.push(`Could not file the escalation automatically${why}.`);
111
+ lines.push('File it by hand so this does not sit silently — post a blocker naming this task:');
112
+ lines.push(' node scripts/gds/api.js POST /blockers --body-file <path-to-json>');
113
+ }
114
+ lines.push('');
115
+ lines.push(`Your claim on task ${taskId} is unchanged. Do NOT re-run with --regrade:`);
116
+ lines.push('the diff is empty, so a re-grade has nothing to read and will point you back here.');
117
+ return lines;
118
+ }
119
+
120
+ // Best-effort by design: a failure to file must not turn a blocked close into a crash,
121
+ // but it must also never read as success — noArtifactRefusalLines prints the manual
122
+ // fallback when `filed` is false.
123
+ async function fileNoArtifactEscalation({ taskId, title, floor, summary, notes, builder }, { call } = {}) {
124
+ const post = call || require('./cli-lib').apiCall;
125
+ let r;
126
+ try {
127
+ r = await post('POST', '/api/bongos/blockers', {
128
+ title: escalationTitle(taskId, title),
129
+ body_md: escalationBody({ taskId, title, floor, summary, notes, builder }),
130
+ source: NO_ARTIFACT_SOURCE,
131
+ source_ref: escalationSourceRef(taskId),
132
+ });
133
+ } catch (err) {
134
+ return { filed: false, reason: `POST /blockers threw: ${err && err.message}` };
135
+ }
136
+ if (!r || !r.ok) return { filed: false, reason: `POST /blockers -> ${r && r.status}` };
137
+ if (r.data && r.data.skipped) return { filed: false, reason: 'already-filed' };
138
+ return { filed: true, id: r.data && r.data.blocker && r.data.blocker.id };
139
+ }
140
+
141
+ module.exports = {
142
+ NO_ARTIFACT_SOURCE,
143
+ escalationSourceRef,
144
+ escalationTitle,
145
+ escalationBody,
146
+ noArtifactRefusalLines,
147
+ fileNoArtifactEscalation,
148
+ };
@@ -28,6 +28,7 @@ const { pushVia } = require('./ship-deploy-target.js');
28
28
 
29
29
  const { diffstatFor, finishShip } = require('./ship-finish.js');
30
30
  const { evaluateRegradeEligibility, evaluateResumeAction, resumeMerge, shouldConfirmWithoutGrade } = require('./ship-resume.js');
31
+ const { fileNoArtifactEscalation, noArtifactRefusalLines } = require('./ship-escalation.js');
31
32
  const { noteServerReward } = require('./ship-state.js');
32
33
 
33
34
  // ---------- Main flow ----------
@@ -70,6 +71,9 @@ async function shipMain() {
70
71
  // empty diff is refused (it used to slip through under --skip-grade — the
71
72
  // task-1363 phantom ship). Never overrides a dirty working tree.
72
73
  const allowEmpty = args.includes('--allow-empty');
74
+ // task 1003950: set only when the empty-diff guard below classifies this ship as a
75
+ // deliberate no-artifact close. Gates the auto-escalation at the confirm wall.
76
+ let noArtifactClose = false;
73
77
  // task 1002605 (G-18): explicit opt-in to continue past a FAILED production
74
78
  // smoke run. Without it a smoke FAIL now ABORTS before the claim resolves
75
79
  // (claim intact); with it, the resolve proceeds and the task deliberately
@@ -266,6 +270,12 @@ async function shipMain() {
266
270
  // There is no committed diff for the subagent grader to read, so confirm
267
271
  // directly (this is the DB-only path the old --skip-grade route served).
268
272
  if (!skipGrade) skipGrade = true;
273
+ // task 1003950: mark WHY the grader is being skipped. A --skip-grade ship past a
274
+ // failing grade and a genuinely artifact-free close both reach the same archon
275
+ // gate below, but only the second one may be escalated automatically — the first
276
+ // has a diff the grader DID read and reject, and auto-filing a blocker for it
277
+ // would be a builder routing their own rejected work to an override queue.
278
+ noArtifactClose = true;
269
279
  console.log('--allow-empty: no committed diff (no-artifact / DB-only ship) — confirming directly without the subagent grader.');
270
280
  }
271
281
 
@@ -570,6 +580,19 @@ async function shipMain() {
570
580
  const details = (errObj && typeof errObj === 'object' && errObj.details) || {};
571
581
  const floor = details.floors && details.floors['task.confirm.grade_bypass'];
572
582
  const floorNote = floor ? ` (floor: ${floor})` : '';
583
+ // task 1003950: these two cases used to print the same advice, and for a
584
+ // no-artifact close that advice was a LOOP — --regrade on an empty diff
585
+ // refuses and points back at --allow-empty, which lands here again. Split
586
+ // them: a rejected-grade bypass still has a real diff and a real remedy
587
+ // (fix the findings, re-grade); an artifact-free close has neither, so it
588
+ // gets the escalation filed for it and terminates.
589
+ if (noArtifactClose) {
590
+ const escalation = await fileNoArtifactEscalation({
591
+ taskId, title: ac.title, floor, summary, notes, builder: (me.data.builder || {}).github_login,
592
+ });
593
+ for (const line of noArtifactRefusalLines({ taskId, floor, escalation })) console.error(line);
594
+ process.exit(1);
595
+ }
573
596
  console.error('');
574
597
  console.error(`✖ Confirming without a passing grade is permission-gated: task.confirm.grade_bypass${floorNote}.`);
575
598
  console.error(' This is an authorization wall, not a verdict on your work.');
@@ -907,10 +930,17 @@ async function regradeMain() {
907
930
  if (confirm.status === 403 && errCode === 'permission_forbidden') {
908
931
  const details = (errObj && typeof errObj === 'object' && errObj.details) || {};
909
932
  const floor = details.floors && details.floors['task.confirm.grade_bypass'];
910
- console.error('');
911
- console.error(`✖ Confirming without a passing grade is permission-gated: task.confirm.grade_bypass${floor ? ` (floor: ${floor})` : ''}.`);
912
- console.error(' This is an authorization wall, not a verdict on your work.');
913
- console.error(` Ask a builder whose role holds it to POST /api/bongos/tasks/${taskId}/confirm.`);
933
+ // task 1003950: this branch is reached ONLY via shouldConfirmWithoutGrade,
934
+ // which requires both an empty committed diff and an explicit --allow-empty —
935
+ // so it is always a no-artifact close, and never a bypass of a failing grade.
936
+ // It used to end at "ask a builder whose role holds it", which is the exact
937
+ // dead end the bug names: a builder who knows no archon had nowhere to go.
938
+ const escalation = await fileNoArtifactEscalation({
939
+ // no `notes` on this path by design: regradeMain REFUSES --notes (see
940
+ // proseFlagPresent above), so the summary is the only reason the builder gave.
941
+ taskId, title: task && task.title, floor, summary, builder: builder.github_login,
942
+ });
943
+ for (const line of noArtifactRefusalLines({ taskId, floor, escalation })) console.error(line);
914
944
  process.exit(1);
915
945
  }
916
946
  console.error(`manual-confirm failed: ${apiErrorLine(confirm.data, confirm.status)}`);
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.847'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.849'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,248 @@
1
+ // tests/no_artifact_escalation.mjs
2
+ //
3
+ // Task 1003950 — a no-artifact close must TERMINATE, not loop.
4
+ //
5
+ // The bug: a legitimately artifact-free task (an ops verification, a DB-only close, one
6
+ // whose premise turned out already-done) could not be closed below archon, and the two
7
+ // documented exits advised each other:
8
+ //
9
+ // --allow-empty → 403 task.confirm.grade_bypass → "use --regrade"
10
+ // --regrade → "the committed diff is empty" → "use --allow-empty"
11
+ //
12
+ // The fix keeps the gate where it is (`task.confirm.grade_bypass` is `system: true` —
13
+ // one of the four trust-boundary keys ADR 0157 names; moving it is task 1004010) and
14
+ // instead FILES the escalation for the builder, then stops.
15
+ //
16
+ // What these tests protect, in order of how easily each would rot:
17
+ // 1. the loop stays closed — no refusal line may advise a command that comes back here;
18
+ // 2. the escalation is actually WIRED at both 403 sites, not merely exported;
19
+ // 3. only a no-artifact close is auto-escalated — a --skip-grade bypass of a FAILING
20
+ // grade must not be, or a builder could route their own rejected work to the
21
+ // override queue;
22
+ // 4. filing is idempotent per task, and a filing failure never reads as success.
23
+ //
24
+ // Run: node tests/no_artifact_escalation.mjs
25
+
26
+ import { strict as assert } from 'node:assert';
27
+ import { createRequire } from 'node:module';
28
+
29
+ import { shipCliSource } from './helpers.mjs';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const esc = require('../scripts/gds/ship-escalation.js');
33
+ const {
34
+ escalationSourceRef, escalationTitle, escalationBody,
35
+ noArtifactRefusalLines, fileNoArtifactEscalation, NO_ARTIFACT_SOURCE,
36
+ } = esc;
37
+
38
+ let passed = 0;
39
+ let failed = 0;
40
+ function test(name, fn) {
41
+ try {
42
+ const r = fn();
43
+ if (r && typeof r.then === 'function') {
44
+ return r.then(() => { passed++; console.log(` ok ${name}`); },
45
+ (e) => { failed++; console.error(`FAIL ${name}\n ${e.message}`); });
46
+ }
47
+ passed++; console.log(` ok ${name}`);
48
+ } catch (e) {
49
+ failed++; console.error(`FAIL ${name}\n ${e.message}`);
50
+ }
51
+ return Promise.resolve();
52
+ }
53
+
54
+ const text = (o) => noArtifactRefusalLines(o).join('\n');
55
+
56
+ // ---- 1. THE LOOP INVARIANT ---------------------------------------------------------
57
+ // This is the whole point of the task. Asserted as "no COMMAND that loops", not "the
58
+ // string --regrade never appears" — the refusal deliberately says *Do NOT* re-run with
59
+ // --regrade, and a naive absence check would forbid that warning and pass a version
60
+ // that quietly went back to recommending it.
61
+
62
+ await test('the refusal never tells the builder to run a command that returns them here', () => {
63
+ for (const escalation of [
64
+ { filed: true, id: 42 },
65
+ { filed: false, reason: 'already-filed' },
66
+ { filed: false, reason: 'POST /blockers -> 500' },
67
+ undefined,
68
+ ]) {
69
+ const out = text({ taskId: 1003822, floor: 'archon', escalation });
70
+ assert.ok(!/node scripts\/gds\/ship\.js\s+\d+\s+--regrade/.test(out),
71
+ 'a runnable --regrade command is the exact advice that loops');
72
+ assert.ok(!/node scripts\/gds\/ship\.js\s+\d+\s+--allow-empty/.test(out),
73
+ 'and re-running --allow-empty lands on this same refusal');
74
+ // The refusal deliberately contains "Do NOT re-run with --regrade". Drop that
75
+ // negated sentence, then assert nothing REMAINING recommends it. A blanket
76
+ // absence check would forbid the warning while still passing a version that
77
+ // quietly went back to advising the loop.
78
+ const withoutWarning = out.replace(/Do NOT re-run with --regrade[^]*/i, '');
79
+ assert.ok(!/(use|try|re-run with|switch to)\s+`?--regrade/i.test(withoutWarning),
80
+ 'nor may it advise --regrade in prose');
81
+ }
82
+ });
83
+
84
+ await test('the refusal explicitly warns AGAINST the old advice, so the loop is closed on the record', () => {
85
+ const out = text({ taskId: 1003822, floor: 'archon', escalation: { filed: true, id: 42 } });
86
+ assert.match(out, /Do NOT re-run with --regrade/,
87
+ 'the builder who read the old message needs to be told it was wrong');
88
+ assert.match(out, /nothing to read/, 'and told why, or they will assume it is a fluke');
89
+ });
90
+
91
+ await test('every refusal shape names what happens next, so none of them dead-ends', () => {
92
+ const filed = text({ taskId: 7, escalation: { filed: true, id: 42 } });
93
+ assert.match(filed, /blocker 42/, 'a filed escalation must name the blocker it filed');
94
+ assert.match(filed, /nothing further to re-run/i, 'and say the builder is done');
95
+
96
+ const dup = text({ taskId: 7, escalation: { filed: false, reason: 'already-filed' } });
97
+ assert.match(dup, /Already escalated/, 'a second run must not read as a failure');
98
+ assert.ok(!/Could not file/.test(dup), 'a dedupe is a success, not an error');
99
+
100
+ const broke = text({ taskId: 7, escalation: { filed: false, reason: 'POST /blockers -> 500' } });
101
+ assert.match(broke, /Could not file the escalation/, 'a real failure must say so');
102
+ assert.match(broke, /500/, 'and name the cause');
103
+ assert.match(broke, /by hand/, 'and give the manual fallback — the close is still blocked');
104
+ });
105
+
106
+ await test('the claim is stated as intact — a builder who thinks they lost it will not retry', () => {
107
+ const out = text({ taskId: 1003822, escalation: { filed: true, id: 1 } });
108
+ assert.match(out, /claim on task 1003822 is unchanged/);
109
+ });
110
+
111
+ await test('a missing floor degrades to a clean sentence rather than "floor: undefined"', () => {
112
+ const out = text({ taskId: 7, floor: undefined, escalation: { filed: true, id: 1 } });
113
+ assert.ok(!/undefined/.test(out), out);
114
+ assert.match(out, /task\.confirm\.grade_bypass\./, 'still names the atom');
115
+ });
116
+
117
+ // ---- 2. WIRED, NOT MERELY EXPORTED -------------------------------------------------
118
+ // A pure unit can be perfect and unreachable. These read the real ship CLI source.
119
+
120
+ await test('both confirm walls call the escalation — not just the one that is easy to find', () => {
121
+ const src = shipCliSource();
122
+ // `await` is what makes this count CALLS: shipCliSource globs ship-*.js, so the
123
+ // module's own `async function fileNoArtifactEscalation({` is in this source too.
124
+ const calls = src.match(/await fileNoArtifactEscalation\(\{/g) || [];
125
+ assert.equal(calls.length, 2,
126
+ `expected the escalation at BOTH 403 sites (first-ship and --regrade), found ${calls.length}`);
127
+ assert.match(src, /noArtifactRefusalLines\(\{ taskId, floor, escalation \}\)/,
128
+ 'and each must print the terminating refusal, not roll its own');
129
+ });
130
+
131
+ await test('ONLY a no-artifact close is auto-escalated, never a bypass of a FAILING grade', () => {
132
+ // The first-ship wall is reached by --skip-grade too, where the grader DID read a real
133
+ // diff and rejected it. Auto-filing a blocker there would let a builder route their own
134
+ // rejected work onto the override queue. The `noArtifactClose` guard is what separates
135
+ // them, so its absence is a security-shaped regression, not a cosmetic one.
136
+ const src = shipCliSource();
137
+ assert.match(src, /if \(noArtifactClose\) \{\s*\n\s*const escalation = await fileNoArtifactEscalation/,
138
+ 'the first-ship escalation must sit behind the noArtifactClose guard');
139
+ assert.match(src, /noArtifactClose = true;/,
140
+ 'and something must actually SET it, or the branch is unreachable and the guard is a lie');
141
+ // Reachability: the assignment has to live in the allow-empty branch specifically.
142
+ const allowEmptyBranch = src.slice(src.indexOf("verdict.reason === 'allow-empty'"));
143
+ assert.ok(allowEmptyBranch.indexOf('noArtifactClose = true;') > -1
144
+ && allowEmptyBranch.indexOf('noArtifactClose = true;') < 1200,
145
+ 'noArtifactClose must be set in the allow-empty branch, not somewhere it can never run');
146
+ });
147
+
148
+ await test('the --regrade call site passes no `notes` — that variable does not exist there', () => {
149
+ // Caught live while building this: regradeMain REFUSES --notes (proseFlagPresent), so
150
+ // `notes` is undeclared in that scope. Passing it parses fine and throws a
151
+ // ReferenceError at the exact moment a blocked builder needs the escalation — the one
152
+ // moment nothing else is going right. node --check cannot see this; only this can.
153
+ const src = shipCliSource();
154
+ const site = src.slice(src.indexOf('title: task && task.title'));
155
+ const call = site.slice(0, site.indexOf('})'));
156
+ assert.ok(!/\bnotes\b/.test(call),
157
+ 'the --regrade escalation must not reference `notes`: ' + call);
158
+ });
159
+
160
+ // ---- 3. IDEMPOTENCY + THE FILING CONTRACT ------------------------------------------
161
+
162
+ await test('the source_ref is per-TASK, so re-running files one blocker and not three', () => {
163
+ assert.equal(escalationSourceRef(1003822), 'no-artifact-close:1003822');
164
+ assert.equal(escalationSourceRef(1003822), escalationSourceRef('1003822'),
165
+ 'a numeric and string id must dedupe to the same ref, or a re-run files a twin');
166
+ assert.notEqual(escalationSourceRef(1), escalationSourceRef(2), 'different tasks stay distinct');
167
+ });
168
+
169
+ await test('fileNoArtifactEscalation: posts a well-formed blocker and returns its id', async () => {
170
+ let seen = null;
171
+ const r = await fileNoArtifactEscalation(
172
+ { taskId: 1003822, title: 'a stale premise', floor: 'archon', summary: 'already fixed by 1003529', builder: 'a-builder' },
173
+ { call: async (method, path, body) => { seen = { method, path, body }; return { ok: true, data: { blocker: { id: 77 } } }; } },
174
+ );
175
+ assert.deepEqual(r, { filed: true, id: 77 });
176
+ assert.equal(seen.method, 'POST');
177
+ assert.equal(seen.path, '/api/bongos/blockers');
178
+ assert.equal(seen.body.source, NO_ARTIFACT_SOURCE);
179
+ assert.equal(seen.body.source_ref, 'no-artifact-close:1003822');
180
+ assert.match(seen.body.title, /task 1003822/);
181
+ assert.match(seen.body.body_md, /already fixed by 1003529/, "the builder's reason must reach the archon");
182
+ assert.match(seen.body.body_md, /a-builder/, 'and so must who declared it');
183
+ });
184
+
185
+ await test('a dedupe response is reported as already-filed, not as a failure', async () => {
186
+ const r = await fileNoArtifactEscalation({ taskId: 1 },
187
+ { call: async () => ({ ok: true, data: { skipped: true } }) });
188
+ assert.deepEqual(r, { filed: false, reason: 'already-filed' });
189
+ });
190
+
191
+ await test('a filing failure NEVER reads as filed — the close is still blocked', async () => {
192
+ const bad = await fileNoArtifactEscalation({ taskId: 1 }, { call: async () => ({ ok: false, status: 500 }) });
193
+ assert.equal(bad.filed, false);
194
+ assert.match(bad.reason, /500/);
195
+
196
+ const threw = await fileNoArtifactEscalation({ taskId: 1 },
197
+ { call: async () => { throw new Error('ECONNREFUSED'); } });
198
+ assert.equal(threw.filed, false, 'a network error must not crash the ship path');
199
+ assert.match(threw.reason, /ECONNREFUSED/);
200
+
201
+ const nullish = await fileNoArtifactEscalation({ taskId: 1 }, { call: async () => null });
202
+ assert.equal(nullish.filed, false, 'a null response is not a success');
203
+ });
204
+
205
+ await test('the blocker body carries the decision the archon has to make, both ways', () => {
206
+ const body = escalationBody({ taskId: 9, title: 't', floor: 'archon', summary: 'ops verify only' });
207
+ assert.match(body, /If yes/, 'confirm path');
208
+ assert.match(body, /If no/, 'and the reject path — a one-sided ask gets rubber-stamped');
209
+ assert.match(body, /tasks\/9\/confirm/, 'with the actual route to run');
210
+ assert.match(body, /not file a second copy/, 'and reassurance that re-running is harmless');
211
+ });
212
+
213
+ await test('a reasonless ship still files a legible blocker rather than a blank one', () => {
214
+ const body = escalationBody({ taskId: 9 });
215
+ assert.match(body, /none given/, 'the absent reason must be visible, not an empty section');
216
+ assert.ok(!/undefined|\[object Object\]/.test(body), body);
217
+ assert.match(escalationTitle(9, null), /task 9/, 'and the title must survive a null title');
218
+ });
219
+
220
+ await test('the call sites resolve the builder login from me.data.builder, not me.data', () => {
221
+ // The grade caught this: `me.data.github_login` is ALWAYS undefined — /me returns
222
+ // { builder, ... } (src/bongos/routes/me.js), and ship-flow.js already proves the
223
+ // right pattern twice elsewhere. The failure is silent: escalationBody falls back to
224
+ // 'the claim holder', so the blocker still looks fine while naming nobody — and the
225
+ // whole reason it is filed is to spare the archon an interview.
226
+ //
227
+ // Asserted against SOURCE because both call sites sit behind a live confirm 403; the
228
+ // direct-call tests below cannot see the field path at all, which is exactly how 14
229
+ // green tests shipped the bug.
230
+ const src = shipCliSource();
231
+ assert.ok(!/me\.data\.github_login/.test(src),
232
+ 'me.data.github_login is always undefined — the login is nested under me.data.builder');
233
+ assert.match(src, /builder: \(me\.data\.builder \|\| \{\}\)\.github_login/,
234
+ 'the first-ship call site must reach through me.data.builder');
235
+ assert.match(src, /builder: builder\.github_login/,
236
+ 'the --regrade call site must use the `builder` binding already in scope there');
237
+ });
238
+
239
+ await test('an unresolved builder degrades to a legible line, not "undefined"', () => {
240
+ // The fallback above is what the bug hid behind, so it has to stay honest.
241
+ const body = escalationBody({ taskId: 9, builder: undefined });
242
+ assert.match(body, /the claim holder/, 'a missing login must read as a role, not a blank');
243
+ assert.ok(!/undefined/.test(body), body);
244
+ });
245
+
246
+ console.log('');
247
+ console.log(`no_artifact_escalation: ${passed} passed, ${failed} failed`);
248
+ process.exit(failed === 0 ? 0 : 1);
@@ -42,8 +42,11 @@ const {
42
42
  scanArtifactForLeaks, PRIOR_INSTANCE_LITERALS,
43
43
  // task 1002217 (audit H4) — the fixed neutral floor
44
44
  PRIOR_INSTANCE_FLOOR,
45
+ // task 1003995 — --from-worktree's enumerator, for a core checkout with no git history
46
+ walkWorktreeEntries,
45
47
  } = m;
46
48
  const { buildRedactionRules, collectSensitiveLiterals, redactText, findLeaks } = require('../scripts/gds/mirror-redact.js');
49
+ const { isPublishable } = require('../scripts/gds/publish-manifest.js');
47
50
 
48
51
  // A synthetic instance brand for the classification tests (never the real OTB literals).
49
52
  const TEST_BRANDING = {
@@ -747,5 +750,67 @@ t('idea 1000543 (the fix): the isNpmStripped filter makes the pin reproduce agai
747
750
  } finally { rmSync(root, { recursive: true, force: true }); }
748
751
  });
749
752
 
753
+ // ── --from-worktree: the git-less enumeration (task 1003995) ─────────────────────
754
+ // package-core builds the core set from `git ls-tree`, so a core checkout with no .git —
755
+ // the shipped self-host image, where .dockerignore excludes it by design — cannot build
756
+ // the tarball its provisioning scaffold vendors into a child instance. walkWorktreeEntries
757
+ // is the replacement source. It decides only what is OFFERED; isPublishable still decides
758
+ // what is KEPT, so the core set stays defined in exactly one place.
759
+ function worktreeFixture() {
760
+ const root = mkdtempSync(join(tmpdir(), 'wt-core-'));
761
+ const put = (rel, body = 'x') => {
762
+ mkdirSync(join(root, dirname(rel)), { recursive: true });
763
+ writeFileSync(join(root, rel), body);
764
+ };
765
+ put('src/module-api.js');
766
+ put('src/bongos/tasks.js'); // nested, to prove the walk recurses
767
+ put('scripts/gds/package-core.js');
768
+ put('CLAUDE.md'); // an allowlisted FILE entry, not a subtree
769
+ put('node_modules/@bongos/core/index.js'); // huge + never core: must never be descended
770
+ put('dist/bongos-core-1.0.0.tgz'); // this tool's own output
771
+ put('config/branding.json'); // instance identity: outside every allowlist root
772
+ return { root, cleanup: () => rmSync(root, { recursive: true, force: true }) };
773
+ }
774
+
775
+ t('walkWorktreeEntries: offers the allowlisted tree and never descends node_modules/ or dist/', () => {
776
+ const { root, cleanup } = worktreeFixture();
777
+ try {
778
+ const paths = walkWorktreeEntries(root).map((e) => e.path);
779
+ assert.deepEqual(
780
+ paths,
781
+ ['CLAUDE.md', 'scripts/gds/package-core.js', 'src/bongos/tasks.js', 'src/module-api.js'],
782
+ `unexpected set: ${paths.join(', ')}`,
783
+ );
784
+ assert.ok(!paths.some((p) => p.startsWith('node_modules/')), 'node_modules is never walked');
785
+ assert.ok(!paths.some((p) => p.startsWith('dist/')), 'the packager never re-enumerates its own output');
786
+ assert.ok(!paths.includes('config/branding.json'), 'instance identity is outside every allowlist root');
787
+ } finally { cleanup(); }
788
+ });
789
+
790
+ t('walkWorktreeEntries: rows match the git-tree shape — sorted blobs carrying a mode', () => {
791
+ const { root, cleanup } = worktreeFixture();
792
+ try {
793
+ const entries = walkWorktreeEntries(root);
794
+ assert.deepEqual([...entries].sort((a, b) => (a.path < b.path ? -1 : 1)), entries, 'sorted, so the pin is stable');
795
+ for (const e of entries) {
796
+ assert.equal(e.type, 'blob', 'only files, matching the ls-tree filter main() applies');
797
+ assert.ok(['100644', '100755'].includes(e.mode), `mode must be a git file mode, got ${e.mode}`);
798
+ }
799
+ } finally { cleanup(); }
800
+ });
801
+
802
+ t('walkWorktreeEntries: a DENIED path inside an allowlisted subtree is still offered — the denylist is main()’s job, not the walk’s', () => {
803
+ const { root, cleanup } = worktreeFixture();
804
+ try {
805
+ // A host-feature script lives under the allowlisted scripts/ root and is denied by name.
806
+ // If the walk pre-filtered it, the two enumeration paths would be applying different rules.
807
+ const denied = 'scripts/gds/art-stage.js';
808
+ writeFileSync(join(root, denied), 'x');
809
+ const offered = walkWorktreeEntries(root).map((e) => e.path);
810
+ assert.ok(offered.includes(denied), 'the walk offers it…');
811
+ assert.equal(isPublishable(denied), false, '…and isPublishable is what drops it');
812
+ } finally { cleanup(); }
813
+ });
814
+
750
815
  console.log(`\n${passed} passed, ${failed} failed`);
751
816
  process.exit(failed ? 1 : 0);