@bongos/core 1.19.645 → 1.19.646

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 CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.645",
6
- "core_contract": "1.19.645",
7
- "source_commit": "14b1d4c19840d818f49d383ed7c50189913860ac",
5
+ "core_version": "1.19.646",
6
+ "core_contract": "1.19.646",
7
+ "source_commit": "9648a16f755cae6136582c0d0ca4930a566a11af",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-10T18:02:00.629Z",
9
+ "built_at": "2026-09-10T18:10:26.956Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 470,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2600,
20
- "tree_sha256": "39c1aaa7c37d7537a2b25d18b8de6e6096cef248e1549d529398fe0d2282e2c2",
20
+ "tree_sha256": "69d3719087a6b6e7fb7c1a0d4889c6bf272f0258a7d7cc72458179525775f6df",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-review/SKILL.md",
@@ -172,7 +172,7 @@
172
172
  {
173
173
  "path": ".claude/skills/grade-recover/SKILL.md",
174
174
  "mode": "0000644",
175
- "sha256": "51cf032ec08279f0f6cbd22a528493917b674ce87f007bcc54eb9ef94a8aad22"
175
+ "sha256": "4dbbf04c19558b33332d9ba1bb8747198ded19e1a3af06ef72ede58ee4eb95ad"
176
176
  },
177
177
  {
178
178
  "path": ".claude/skills/grader-health/SKILL.md",
@@ -2772,7 +2772,7 @@
2772
2772
  {
2773
2773
  "path": "docs/module-api-changelog.md",
2774
2774
  "mode": "0000644",
2775
- "sha256": "7decbd6be1709b8c7fe6173a659d2caa8594bb766d353bece00c88a1e316f57e"
2775
+ "sha256": "f4ceea78bdb8f6ba5fec64415aed58610a8ab011d3a8b550e517515904c8cfce"
2776
2776
  },
2777
2777
  {
2778
2778
  "path": "docs/modules-contract.md",
@@ -7722,12 +7722,12 @@
7722
7722
  {
7723
7723
  "path": "package-lock.json",
7724
7724
  "mode": "0000644",
7725
- "sha256": "de884c55a00c4f37989b00d9c574fdcde904f617941dc65ee21ece636efd63c8"
7725
+ "sha256": "8def408bd91446b93b180b2696465b9bedaf1f8ebe15c671cd4f66a41f6b03da"
7726
7726
  },
7727
7727
  {
7728
7728
  "path": "package.json",
7729
7729
  "mode": "0000644",
7730
- "sha256": "a2949e78ffe45c9e0442261dd19892266aa1930607dfaa309ce10754b119921e"
7730
+ "sha256": "62ee3c0059ddc570ef2fef4f2288b7714d9eb95f64d390f67720eb7d07c92cd5"
7731
7731
  },
7732
7732
  {
7733
7733
  "path": "public-docs/index.html",
@@ -7982,7 +7982,7 @@
7982
7982
  {
7983
7983
  "path": "scripts/gds/claude-materialize.js",
7984
7984
  "mode": "0000644",
7985
- "sha256": "2d05a5e1dcb3a37480d42f6e0b30b26432a3fe02d671482f2d3762acb7bdb556"
7985
+ "sha256": "cf333976f3c8fc9cfa1af57a121e5ee1f826d40fe7461bf1e32f939a59523663"
7986
7986
  },
7987
7987
  {
7988
7988
  "path": "scripts/gds/claude-md-template.js",
@@ -8862,7 +8862,7 @@
8862
8862
  {
8863
8863
  "path": "scripts/gds/ship-flow.js",
8864
8864
  "mode": "0000644",
8865
- "sha256": "95b5fa325ea0407dd6cd3d9ae3a03a0a0f0478a0bfcf76d77cf36b13a7c36bea"
8865
+ "sha256": "9853e719e4a695cc52ec9089298d05163210ed3b51ca9eceb7553bc0ed9c65bd"
8866
8866
  },
8867
8867
  {
8868
8868
  "path": "scripts/gds/ship-git.js",
@@ -8882,7 +8882,7 @@
8882
8882
  {
8883
8883
  "path": "scripts/gds/ship-honesty.js",
8884
8884
  "mode": "0000644",
8885
- "sha256": "32827178a46cc01c36c422e1adef433be99492e8444a8affbe51e5e8c1e8ce9f"
8885
+ "sha256": "29116265032b48115c31df794aa4689e7ce3f5eaeef152ea1ca2390dc2078c39"
8886
8886
  },
8887
8887
  {
8888
8888
  "path": "scripts/gds/ship-io.js",
@@ -9482,7 +9482,7 @@
9482
9482
  {
9483
9483
  "path": "src/module-api.js",
9484
9484
  "mode": "0000644",
9485
- "sha256": "e5d854562fcdec46abd9212cfdfc5c05bd1b29b43525d00abb9d6acb39dac48c"
9485
+ "sha256": "bb3a06612cea7d25b04efabda8659ed8a71b66859bab279f9b4e8a1e5dbadff3"
9486
9486
  },
9487
9487
  {
9488
9488
  "path": "src/module-loader/catalog.js",
@@ -10067,7 +10067,7 @@
10067
10067
  {
10068
10068
  "path": "tests/claude_materialize.mjs",
10069
10069
  "mode": "0000644",
10070
- "sha256": "e67c44808dda0823a4b2b0e4ddd4ea70735f33a41c335c2d4b168f39eb9807ad"
10070
+ "sha256": "b0474476af85716848114d345f4be67beb2e01ad3ceab36a25d474c3e9e536c5"
10071
10071
  },
10072
10072
  {
10073
10073
  "path": "tests/clawback_confirmed_backlog.mjs",
@@ -12517,7 +12517,7 @@
12517
12517
  {
12518
12518
  "path": "tests/ship_cannot_lie.mjs",
12519
12519
  "mode": "0000644",
12520
- "sha256": "82f6f951cc50c34860545be0f309e18a796ebb5baa712ccb341e1fd0dd72a233"
12520
+ "sha256": "d8d99b6cd465b04eaf13790b0bdfe9a76ce36d77660407d3a2c6a17ac7852e8c"
12521
12521
  },
12522
12522
  {
12523
12523
  "path": "tests/ship_card.mjs",
@@ -58,12 +58,14 @@ The panel ran and rejected the work. Recover by fixing, not by re-rolling:
58
58
  1. **Group `issues[]` by severity, then by worker.** Nit/minor findings are advisory by rubric policy — they cannot have caused the fail on their own, so do not start there. The fail came from blocker/major findings (or a gating axis under threshold).
59
59
  2. **For each blocker/major finding, add a checked-in `tests/*.mjs` case** that reproduces it, then fix until green. The grader treats throwaway verification as unverified — the committed test is what makes the fix real.
60
60
  3. **Fix real findings once; do not chase re-worded restatements** of a point you already answered (answer it in the commit or with a tracked task ref instead).
61
- 4. Commit on the claim branch — the grader reads committed history — then:
61
+ 4. Commit on the claim branch — the grader reads committed history — then re-grade, saying how each finding was answered:
62
62
 
63
63
  ```bash
64
- node scripts/gds/ship.js <id> --regrade
64
+ node scripts/gds/ship.js <id> --regrade --response-file response.md
65
65
  ```
66
66
 
67
+ **`--response` is how you answer a finding; `--notes` is not** (task 1003702). The panel is given the task, your value summary, the changed files and the diff — it never reads ship notes, on a re-grade or on the first ship. `--response` rides to the panel fenced as the prior round's answer, so an already-answered finding cannot re-veto on re-wording alone. Passing `--notes` used to be accepted and silently dropped; it is now refused with this same guidance. Evidence itself still belongs in the **diff** — a committed test is what makes a fix real (step 2), and the panel can read every changed file.
68
+
67
69
  ### Not recoverable here — `REFUSED (authorization)`
68
70
 
69
71
  A grade block naming protected surfaces above your rank is an authorization wall, not a quality verdict: no re-grade at your rank can land that diff as-is. Split the protected files out of the diff and re-ship the rest, or hand the task to a builder at/above the floor the refusal names.
@@ -1739,5 +1739,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1739
1739
  landed since 1.19.643 with no explicit bump. run 34509682128. (task 1002620)
1740
1740
  1.19.645 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1741
1741
  landed since 1.19.644 with no explicit bump. run 34511775606. (task 1002620)
1742
+ 1.19.646 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1743
+ landed since 1.19.645 with no explicit bump. run 34512629504. (task 1002620)
1742
1744
  ---------------------------------------------------------------------------
1743
1745
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.645",
3
+ "version": "1.19.646",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.645",
9
+ "version": "1.19.646",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.645",
3
+ "version": "1.19.646",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -431,7 +431,20 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
431
431
  landedNow[name] = { module: mod.key, source: `.claude/skills/${name}`, coreShipped: true };
432
432
  }
433
433
  }
434
- const withdrawable = Object.keys(landedPreviously).filter((n) => SKILL_NAME_RE.test(n) && !landedNow[n]);
434
+ // A name the CORE ships now is the core's, whatever an older manifest said — a core
435
+ // skill can take over a name a module once owned, and step 1 copies it in on this very
436
+ // run. Without this guard 1c deleted it again immediately (task 1003818, a regression
437
+ // in 1003632's own first cut). materializeModuleSkillsIntoCore states the same rule for
438
+ // the checkout side via its `tracked(name)` check; this is the instance-side twin.
439
+ //
440
+ // The `!skipSkills.has(name)` half is load-bearing, not belt-and-braces: when a DISABLED
441
+ // module declares a core-shipped name, step 1 SKIPS the copy (that is the task 2103b
442
+ // exclusion), so the instance's dir really is stale and must still be withdrawn. Keeping
443
+ // it on the strength of "the core ships this name" would resurrect exactly the bug 1c
444
+ // exists to fix. Deliberately asked of the CORE source tree rather than the roster, so a
445
+ // name no enabled module declares any more is still recognised as legitimately sourced.
446
+ const coreShipsNow = (name) => fs.existsSync(path.join(skillsSrc, name)) && !skipSkills.has(name);
447
+ const withdrawable = Object.keys(landedPreviously).filter((n) => SKILL_NAME_RE.test(n) && !landedNow[n] && !coreShipsNow(n));
435
448
  if (!overwrite) {
436
449
  // adopt mode never clobbers what the owner already has; withdrawing is a clobber.
437
450
  if (withdrawable.length) summary.notes.push(`${withdrawable.length} skill(s) of a now-disabled module were left in place (adopt mode does not withdraw): ${withdrawable.sort().join(', ')}.`);
@@ -9,7 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const { cliClient, requireSession, arg, detectWorktreeName } = require('./cli-lib');
12
- const { resolveProse, markShipProgress, smokeGateMessages } = require('./ship-honesty.js');
12
+ const { resolveProse, markShipProgress, smokeGateMessages, proseFlagPresent, regradeNotesRefusal } = require('./ship-honesty.js');
13
13
  const { classifyEmptyShip } = require('../../modules/lifecycle/ship-preflight');
14
14
  const { scopeForModule, isModuleKey } = require('../../src/bongos/module-scope-map');
15
15
  const { matchOne } = require('../../src/bongos/path-match');
@@ -684,6 +684,16 @@ async function regradeMain() {
684
684
  console.error('usage: node scripts/gds/ship.js <task-id> --regrade [--summary-file <path|-> | --summary "..."] [--response-file <path|-> | --response "..."] [--no-merge] [--force] [--verified] [--base <ref>]');
685
685
  process.exit(2);
686
686
  }
687
+ // task 1003702: refuse --notes rather than swallowing it. The reasoning — why
688
+ // a refusal and not a persist — is in ship-honesty.js above regradeNotesRefusal
689
+ // and is not repeated here. What is load-bearing at THIS call site is the
690
+ // POSITION: before the preflight and before any grade spend, because a refusal
691
+ // that first costs a 4-worker panel run is barely better than the silence it
692
+ // replaces. tests/ship_cannot_lie.mjs pins that order.
693
+ if (proseFlagPresent(args, 'notes')) {
694
+ for (const line of regradeNotesRefusal(taskId)) console.error(line);
695
+ process.exit(2);
696
+ }
687
697
  const regradeProse = resolveProse(args, ['summary', 'response']);
688
698
  const summaryArg = regradeProse.summary;
689
699
  // task 1002662: the builder's response to the prior round's findings — how
@@ -753,6 +763,17 @@ async function regradeMain() {
753
763
  // re-grade is the FIRST time this task actually ships. Fall back to the
754
764
  // task's stored value_summary so the merge commit + Discord #ship-news post
755
765
  // carry meaningful text even when --summary is omitted.
766
+ //
767
+ // AND IT NEVER REACHES THE TASK ROW — task 1003702's acceptance 3 asked this
768
+ // question; the answer is a real, separate defect, filed as task 1003817.
769
+ // `summary` from here feeds the grader prompt, the merge commit message and
770
+ // the PR title/body, and NOTHING writes it back to tasks.value_summary. All
771
+ // three candidate write paths were read and ruled out: publish-branch accepts
772
+ // value_summary but uses it only for PR text; PATCH /tasks/:id does not accept
773
+ // the field at all; the claim-resolve route is the sole writer and a re-grade
774
+ // has no claim (which is why claimLike is synthesized above). Fixing it needs
775
+ // a server route change, so it is deliberately NOT folded into this CLI fix.
776
+ // tests/ship_cannot_lie.mjs pins both halves of that so the answer cannot rot.
756
777
  const summary = summaryArg || task.value_summary || '';
757
778
 
758
779
  // The grader helpers want a claim-like object for touches[] (preflight's
@@ -848,6 +869,11 @@ async function regradeMain() {
848
869
  card: {
849
870
  title: task.title,
850
871
  valueSummary: summary,
872
+ // Empty by design, not by omission (task 1003702). A re-grade takes no
873
+ // --notes at all now — it is refused above — so there are none from THIS
874
+ // round to show. The first ship's notes are the durable record and live on
875
+ // the resolved claim; re-printing them here would suggest this round
876
+ // produced them.
851
877
  notes: '',
852
878
  files: committedFiles,
853
879
  baseline: graderBaseline,
@@ -143,8 +143,61 @@ function smokeGateMessages(taskId) {
143
143
  };
144
144
  }
145
145
 
146
+ // ---- --regrade must not swallow --notes (task 1003702) ----------------------
147
+ //
148
+ // THE THIRD WAY TO LOSE THE BUILDER'S PROSE, and the reason this lives in the
149
+ // prose-loss file rather than beside regradeMain. `--regrade` resolves only
150
+ // `summary` and `response`, so `--notes` parsed as an unknown flag, produced no
151
+ // error, and vanished. A builder answering a "no evidence in the notes" blocker
152
+ // passed it again in --notes, saw the ship run, and got the identical finding
153
+ // back marked unanswered — with nothing anywhere saying why.
154
+ //
155
+ // AND THE FIX IS A REFUSAL, NOT A PERSIST, because the premise the bug was filed
156
+ // under is wrong in the builder's favour: the panel NEVER reads ship notes, on a
157
+ // first ship or a re-grade. modules/grading/grader-prompt.js buildPrompt() puts
158
+ // the task row, the VALUE SUMMARY, the changed files, the diffstat and the diff
159
+ // in the prompt and nothing else, and the workers get the same bundle. So
160
+ // storing the notes would have fixed the silence and none of the substance: the
161
+ // builder would pass --notes, watch it save, and STILL get "unanswered" — a
162
+ // worse failure, because now it looks like it worked.
163
+ //
164
+ // The two channels that do reach the panel are named in the refusal: --response
165
+ // (fenced into the prior-round block, task 1002662) and the DIFF itself, which
166
+ // is why the workaround that actually worked on task 1003698 was committing the
167
+ // evidence as a file. Nothing is written and nothing is lost: the first ship's
168
+ // notes stay the durable ledger record, which is the one thing --notes on a
169
+ // re-grade could legitimately have meant.
170
+ function proseFlagPresent(args, name) {
171
+ const list = Array.isArray(args) ? args : [];
172
+ return list.some((a) => a === `--${name}` || String(a).startsWith(`--${name}=`)
173
+ || a === `--${name}-file` || String(a).startsWith(`--${name}-file=`));
174
+ }
175
+
176
+ // Pure so the refusal's WORDING is testable — a message that fails to name the
177
+ // alternative is the same dead end with an error code on it.
178
+ function regradeNotesRefusal(taskId) {
179
+ return [
180
+ '--regrade does not take --notes, and your notes have NOT been saved or graded.',
181
+ '',
182
+ 'The grader never reads ship notes — not on a re-grade, and not on the first ship.',
183
+ 'The panel is given the task, your VALUE SUMMARY, the changed files and the diff.',
184
+ 'So notes cannot answer a finding, and silently keeping them would only look like it had.',
185
+ '',
186
+ 'Put the evidence where the panel can actually see it:',
187
+ ` --response "..." answers the prior round's findings; it rides to the panel fenced.`,
188
+ ` --response-file <path> the safe form for prose (a shell eats backticks in an inline value).`,
189
+ ' the DIFF commit the proof — a test, a dry-run log, a session-log file.',
190
+ ' The panel reads the committed diff and can Read every changed file.',
191
+ '',
192
+ ` node scripts/gds/ship.js ${taskId} --regrade --response-file notes.md`,
193
+ '',
194
+ "The first ship's notes stay the durable record; nothing you wrote earlier was lost.",
195
+ ];
196
+ }
197
+
146
198
  module.exports = {
147
199
  resolveProse, markShipProgress, fatalRecoveryLines, smokeGateMessages,
200
+ proseFlagPresent, regradeNotesRefusal,
148
201
  _resetForTest: () => { shipProgress = { stage: 'preflight', taskId: null }; },
149
202
  _peekForTest: () => shipProgress,
150
203
  };
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.645'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.646'; // 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');
@@ -318,6 +318,36 @@ t('materializeClaude: a skill the module STOPPED declaring is withdrawn too', ()
318
318
  for (const d of [core, inst]) rmSync(d, { recursive: true, force: true });
319
319
  });
320
320
 
321
+ t('materializeClaude: a CORE skill that took over a module-owned name is NOT withdrawn — and a disabled module\'s core-shipped name still is (task 1003818)', () => {
322
+ // The regression this pins: step 1 copies the core's dir in, and 1c deleted it again in
323
+ // the SAME run, because no enabled module declared the name any more. The core-checkout
324
+ // twin has always guarded this via tracked() ("a core skill can take over a name a
325
+ // module once owned"); the instance side shipped without it in task 1003632.
326
+ const { core, inst, modDir } = withdrawFixture();
327
+ m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false }); // alpha lands, recorded
328
+
329
+ // the module stops declaring alpha; the core now ships its own
330
+ writeFileSync(join(modDir, 'module.json'), JSON.stringify({ key: 'foo', default: true, contributes: { skills: ['beta'] } }));
331
+ mkdirSync(join(core, '.claude', 'skills', 'alpha'), { recursive: true });
332
+ writeFileSync(join(core, '.claude', 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\n---\nCORE OWNS THIS NOW\n');
333
+ const kept = m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
334
+ assert.deepEqual(kept.withdrawnSkills, [], 'a name the core ships now is the core\'s, whatever the manifest said');
335
+ assert.match(readFileSync(join(inst, '.claude', 'skills', 'alpha', 'SKILL.md'), 'utf8'), /CORE OWNS THIS NOW/, 'and it is the core copy that survives');
336
+
337
+ // THE OTHER HALF, which the guard must not break: a DISABLED module declaring a
338
+ // core-shipped name means step 1 skips the copy, so the instance dir IS stale.
339
+ const inst2 = mkdtempSync(join(tmpdir(), 'wd-inst-'));
340
+ writeFileSync(join(modDir, 'module.json'), JSON.stringify({ key: 'foo', default: true, contributes: { skills: ['alpha', 'beta'] } }));
341
+ m.materializeClaude({ coreRoot: core, instanceDir: inst2, dryRun: false });
342
+ assert.ok(existsSync(join(inst2, '.claude', 'skills', 'alpha')), 'landed while enabled');
343
+ mkdirSync(join(inst2, 'config'), { recursive: true });
344
+ writeFileSync(join(inst2, 'config', 'modules.json'), JSON.stringify({ modules: { foo: false } }));
345
+ const gone = m.materializeClaude({ coreRoot: core, instanceDir: inst2, dryRun: false });
346
+ assert.ok(gone.withdrawnSkills.includes('alpha'), 'a disabled module\'s core-shipped name is still withdrawn — step 1 skipped it, so the dir is stale');
347
+ assert.ok(!existsSync(join(inst2, '.claude', 'skills', 'alpha')));
348
+ for (const d of [core, inst, inst2]) rmSync(d, { recursive: true, force: true });
349
+ });
350
+
321
351
  t('materializeClaude: a HAND-WRITTEN skill is never withdrawn, even when a disabled module declares its name', () => {
322
352
  const { core, inst } = withdrawFixture();
323
353
  // The owner wrote .claude/skills/alpha themselves; no materialize ever landed it,
@@ -14,13 +14,20 @@
14
14
  // lands the task at 'completed' before the grade runs, and 'completed' is
15
15
  // ALSO what a grade-failed task looks like — so a bare `fatal: <stack>`
16
16
  // left nobody able to tell how far it got.
17
+ //
18
+ // (3) --regrade must not swallow --notes. Same shape as (1): prose handed to
19
+ // ship.js that reaches nothing, silently (task 1003702).
17
20
  import { strict as assert } from 'node:assert';
18
21
  import { test } from 'node:test';
19
22
  import { createRequire } from 'node:module';
23
+ import { readFileSync } from 'node:fs';
20
24
 
21
25
  const require = createRequire(import.meta.url);
22
26
  const { argText } = require('../scripts/gds/cli-lib.js');
23
- const { fatalRecoveryLines, markShipProgress, _peekForTest } = require('../scripts/gds/ship-honesty.js');
27
+ const {
28
+ fatalRecoveryLines, markShipProgress, _peekForTest,
29
+ proseFlagPresent, regradeNotesRefusal,
30
+ } = require('../scripts/gds/ship-honesty.js');
24
31
 
25
32
  // Injected IO so no case touches the filesystem.
26
33
  const io = {
@@ -166,3 +173,98 @@ test('markShipProgress is the single writer, and ship.js re-exports the reader',
166
173
  assert.equal(typeof shipCli.fatalRecoveryLines, 'function', 'ship.js still exposes it');
167
174
  assert.equal(shipCli.fatalRecoveryLines, fatalRecoveryLines, 'same function, not a copy');
168
175
  });
176
+
177
+ // ── (3) --regrade refuses --notes instead of swallowing it (task 1003702) ────
178
+ //
179
+ // The third way to lose the builder's prose, and the same incident shape as (1):
180
+ // prose handed to ship.js that reaches nothing, with no error. --regrade
181
+ // resolved only summary + response, so --notes parsed as an unknown flag and
182
+ // vanished; a builder answering a "no evidence in the notes" blocker passed it
183
+ // again there and got the identical finding back marked unanswered.
184
+
185
+ test('every spelling of --notes is caught, and nothing else is', () => {
186
+ // All four forms argText accepts. Catching only the bare flag would leave
187
+ // --notes-file — the form the ship docs actively recommend for prose — silent,
188
+ // which is the exact defect wearing different clothes.
189
+ for (const argv of [
190
+ ['1', '--regrade', '--notes', 'x'],
191
+ ['1', '--regrade', '--notes=x'],
192
+ ['1', '--regrade', '--notes-file', 'n.md'],
193
+ ['1', '--regrade', '--notes-file=n.md'],
194
+ ]) {
195
+ assert.equal(proseFlagPresent(argv, 'notes'), true, `caught: ${argv.join(' ')}`);
196
+ }
197
+ // Not over-eager: the flags a re-grade legitimately takes must still pass, and
198
+ // a flag that merely CONTAINS the word is a different flag.
199
+ for (const argv of [
200
+ ['1', '--regrade'],
201
+ ['1', '--regrade', '--response-file', 'r.md'],
202
+ ['1', '--regrade', '--summary', 'x'],
203
+ ['1', '--regrade', '--no-notes-please'],
204
+ ['1', '--regrade', '--response', 'my --notes were wrong'],
205
+ ]) {
206
+ assert.equal(proseFlagPresent(argv, 'notes'), false, `not caught: ${argv.join(' ')}`);
207
+ }
208
+ // Generic over the flag name — it is not a --notes special case.
209
+ assert.equal(proseFlagPresent(['--response-file', 'r.md'], 'response'), true);
210
+ });
211
+
212
+ test('THE ACCEPTANCE: the refusal names where the evidence must go instead', () => {
213
+ // A non-zero exit that does not say what to do is the same dead end with an
214
+ // error code on it. The refusal has to carry the working channel, or a builder
215
+ // reads "not supported" and goes back to putting evidence nowhere.
216
+ const text = regradeNotesRefusal(1003698).join('\n');
217
+ assert.match(text, /--response/, 'names the channel that reaches the panel');
218
+ assert.match(text, /--response-file/, 'and its file form, the safe one for prose');
219
+ assert.match(text, /diff/i, 'and the diff, which is what the panel actually reads');
220
+ assert.match(text, /ship\.js 1003698 --regrade/, 'a runnable command with the real task id');
221
+ // It must not be reassuring about the thing that did not happen.
222
+ assert.match(text, /NOT been saved/, 'says plainly that the notes went nowhere');
223
+ // And it must not claim the notes were merely "not supported yet" — the
224
+ // substantive fact is that the grader never reads ship notes at all, so a
225
+ // builder does not go looking for a flag that would make them count.
226
+ assert.match(text, /never reads ship notes/, 'explains WHY, not just that it refused');
227
+ });
228
+
229
+ test('the refusal is wired into the regrade path, before any grade spend', () => {
230
+ // Wiring proof (the (1) lesson: a helper nothing calls is not a fix). Also
231
+ // pins the ORDER — a refusal that costs a 4-worker panel run first is barely
232
+ // better than the silence, so the guard must precede the preflight.
233
+ const src = readFileSync(new URL('../scripts/gds/ship-flow.js', import.meta.url), 'utf8');
234
+ const regrade = src.slice(src.indexOf('async function regradeMain'));
235
+ const guard = regrade.indexOf("proseFlagPresent(args, 'notes')");
236
+ assert.ok(guard !== -1, 'regradeMain consults the guard');
237
+ const preflight = regrade.indexOf('preflight(claimLike');
238
+ assert.ok(preflight !== -1 && guard < preflight, 'it refuses before the preflight and the panel');
239
+ // The usage line must not advertise a flag the path refuses.
240
+ const usage = regrade.slice(0, regrade.indexOf('resolveProse'));
241
+ assert.ok(!/--notes/.test(usage.replace(/proseFlagPresent[^\n]*/g, '')
242
+ .replace(/\/\/[^\n]*/g, '')), 'usage does not offer --notes');
243
+ });
244
+
245
+ test('ACCEPTANCE 3: --summary reaches the GRADER on a re-grade but never the task row', () => {
246
+ // Task 1003702's third acceptance asked whether --summary reaches the stored
247
+ // value_summary on a re-grade. It does NOT, and that is a separate defect
248
+ // filed as task 1003817 (it needs a server route change; this task's fix is
249
+ // CLI-only). Pinned here rather than left in prose for the reason this whole
250
+ // file exists: the panel reads the DIFF, so an answer that lives only in ship
251
+ // notes is an answer nobody can check — which is the very bug task 1003702 is.
252
+ const src = readFileSync(new URL('../scripts/gds/ship-flow.js', import.meta.url), 'utf8');
253
+ const regrade = src.slice(src.indexOf('async function regradeMain'));
254
+ const code = regrade.replace(/\/\/[^\n]*/g, ''); // comments describe, they do not write
255
+
256
+ // Half one — the threading is intact, so nothing regressed: --summary still
257
+ // falls back to the stored value and still reaches the grader and the card.
258
+ assert.match(code, /const summary = summaryArg \|\| task\.value_summary/,
259
+ 'the stored summary is still the fallback when --summary is omitted');
260
+ assert.match(code, /gradeAndRecord\(\{[\s\S]{0,200}?summary,/,
261
+ 'the summary still reaches the grader prompt');
262
+ assert.match(code, /valueSummary: summary/, 'and the completion card');
263
+
264
+ // Half two — and there is no writer. A `value_summary:` KEY would be a request
265
+ // payload; the legitimate read is `task.value_summary`, dot-access. If someone
266
+ // later adds the write (task 1003817), this assertion is what tells them to
267
+ // update the comment above `const summary` rather than leave it lying.
268
+ assert.ok(!/[^.]\bvalue_summary\s*:/.test(code),
269
+ 'regradeMain writes value_summary nowhere — see task 1003817');
270
+ });