@bongos/core 1.21.84 → 1.21.85

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,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.84",
6
- "core_contract": "1.21.84",
7
- "source_commit": "77cd95ff8cbddfb714a4927414a4d980720ac2ca",
5
+ "core_version": "1.21.85",
6
+ "core_contract": "1.21.85",
7
+ "source_commit": "8a47b19700478214f214b9df7bbfc4cd5ed93958",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-09T02:56:20.018Z",
9
+ "built_at": "2026-10-09T04:31:08.324Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 590,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 2996,
14
+ "functional_verbatim": 2997,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3615,
20
- "tree_sha256": "e5d1d484866739a4ebb4545ddf68af311452e34b73ba4d6a81353e0497ca0ad3",
19
+ "file_count": 3616,
20
+ "tree_sha256": "22dc94ef353678e33bf64bb41ca709aa63dc8f29cc4bebcd10a09a84f2dc32c9",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2862,7 +2862,7 @@
2862
2862
  {
2863
2863
  "path": "docs/module-api-changelog.md",
2864
2864
  "mode": "0000644",
2865
- "sha256": "295cb4db0fe62dc0ac7a45eda7008205e5d1ee79b593e2a240232ed1a7d2d8db"
2865
+ "sha256": "cbcee180651b88158a4cc173e12c3f73ee842743bcaf9e13629d5ef96ee26e56"
2866
2866
  },
2867
2867
  {
2868
2868
  "path": "docs/modules-contract.md",
@@ -7202,7 +7202,7 @@
7202
7202
  {
7203
7203
  "path": "modules/lifecycle/page-tweak-hold.js",
7204
7204
  "mode": "0000644",
7205
- "sha256": "3938cb60e74e910c57ef84384bfd0363c0700e0368dbea98ee880c7b9fe60aeb"
7205
+ "sha256": "54703e1d2814402218057c65691e5fd1f11572eb9e667c1e0148ecb5cc9efe80"
7206
7206
  },
7207
7207
  {
7208
7208
  "path": "modules/lifecycle/page-tweak-reads.js",
@@ -7377,7 +7377,7 @@
7377
7377
  {
7378
7378
  "path": "modules/lifecycle/routes/tasks.js",
7379
7379
  "mode": "0000644",
7380
- "sha256": "4019166678703aee51179d0b92a5aaefa728a5f992b7b472703e61c1e55c30f7"
7380
+ "sha256": "efd442d161d9b212d1e9aa4cad06aa428b1e4c74ac67d22658591ca7c28f2621"
7381
7381
  },
7382
7382
  {
7383
7383
  "path": "modules/lifecycle/routes/version-route-authz.js",
@@ -7402,7 +7402,7 @@
7402
7402
  {
7403
7403
  "path": "modules/lifecycle/ship-card.js",
7404
7404
  "mode": "0000644",
7405
- "sha256": "1935e6f0a3fb9089c2aacd9bbdf1ff1296487065c8c5a25beaf96306d88a9f5b"
7405
+ "sha256": "fc803defc468a0e625d82f305d00118ec77bb63d74d7b75b4d3ba03bdd16659d"
7406
7406
  },
7407
7407
  {
7408
7408
  "path": "modules/lifecycle/ship-files.js",
@@ -10097,12 +10097,12 @@
10097
10097
  {
10098
10098
  "path": "package-lock.json",
10099
10099
  "mode": "0000644",
10100
- "sha256": "24e60fc4fd189a4e9d735e54e99b84734ff1a656551665196974d9355cbedfc3"
10100
+ "sha256": "2784cf3cf476fda15614a347da7e556824a63f50a49cbb74323ceeec704c4b8e"
10101
10101
  },
10102
10102
  {
10103
10103
  "path": "package.json",
10104
10104
  "mode": "0000644",
10105
- "sha256": "4be21978dcd4470cf27cd34d9f788325adb8e1c1c57a437c6a3abdc94bda9448"
10105
+ "sha256": "8fe03b497ac409587e5ded79331fb0ff035d19052cb7c575daf50f07fe8439ee"
10106
10106
  },
10107
10107
  {
10108
10108
  "path": "public-docs/index.html",
@@ -10122,7 +10122,7 @@
10122
10122
  {
10123
10123
  "path": "release-notes.json",
10124
10124
  "mode": "0000644",
10125
- "sha256": "f214849f7282c7218740a89c5ad9e3e464bdf6913043eefd4d104e55f9439898"
10125
+ "sha256": "24198ec47220ec2534ed8d6bf187965bf03d9044831ac3eaa50fd45ea141959d"
10126
10126
  },
10127
10127
  {
10128
10128
  "path": "scripts/cron/style_guide_audit.sh",
@@ -11582,12 +11582,12 @@
11582
11582
  {
11583
11583
  "path": "scripts/gds/ship-finish.js",
11584
11584
  "mode": "0000644",
11585
- "sha256": "8635a70645640302d420974c85fce6578a4e6b26393aff57b3620808771d5b4c"
11585
+ "sha256": "23856ecf2ed0224fcc69fa4e67413d2248462813896e3b832a20f3a2d0a886e6"
11586
11586
  },
11587
11587
  {
11588
11588
  "path": "scripts/gds/ship-flow.js",
11589
11589
  "mode": "0000644",
11590
- "sha256": "cdd4e7a24b0fd3418c911e331635483db16a18b6e0326401bc54806da83f8174"
11590
+ "sha256": "d114ae690d86270db76770fc3531a5530d1ab64a031a2528368864f4b330ac56"
11591
11591
  },
11592
11592
  {
11593
11593
  "path": "scripts/gds/ship-git.js",
@@ -11597,7 +11597,7 @@
11597
11597
  {
11598
11598
  "path": "scripts/gds/ship-grade-steps.js",
11599
11599
  "mode": "0000644",
11600
- "sha256": "89025445be960efd1faedbea57dc6ffdef42bdd637e47409d42671eb3744ffd1"
11600
+ "sha256": "4a0187643d8e0d74b0199f913c24fbd14acf035c27d6040d4ecdfcf905732398"
11601
11601
  },
11602
11602
  {
11603
11603
  "path": "scripts/gds/ship-grade.js",
@@ -11617,7 +11617,7 @@
11617
11617
  {
11618
11618
  "path": "scripts/gds/ship-land.js",
11619
11619
  "mode": "0000644",
11620
- "sha256": "1dc660f9eff1e93b53567cb3406ad6aa0bc80768e93ec905031a5e5d7d37dceb"
11620
+ "sha256": "e518039d82343b5ff3bb21e1a6e0a5a8e0b9368d05b791be4910d76ea075378e"
11621
11621
  },
11622
11622
  {
11623
11623
  "path": "scripts/gds/ship-memory-sync.js",
@@ -12372,7 +12372,7 @@
12372
12372
  {
12373
12373
  "path": "src/module-api.js",
12374
12374
  "mode": "0000644",
12375
- "sha256": "91e35f3a22e967483fc2c4a6390b61839c88d0cceae5acf54e5417315158f1d2"
12375
+ "sha256": "901e1e5ef994837bd4ae48f226af1cd104c34a0dd993383c7980eca13bc61d2f"
12376
12376
  },
12377
12377
  {
12378
12378
  "path": "src/module-loader/catalog.js",
@@ -14874,6 +14874,11 @@
14874
14874
  "mode": "0000644",
14875
14875
  "sha256": "ca7fbfcca238b4ffed576ca1b3fca5d848f4d9d3eab8321353325d54ff505810"
14876
14876
  },
14877
+ {
14878
+ "path": "tests/held_tweak_reporting.mjs",
14879
+ "mode": "0000644",
14880
+ "sha256": "f58a95e019d9e5dd22c3fa2321734cbb7c323b356184198e7d3cb4507ee3d04a"
14881
+ },
14877
14882
  {
14878
14883
  "path": "tests/help_requests.mjs",
14879
14884
  "mode": "0000644",
@@ -16027,7 +16032,7 @@
16027
16032
  {
16028
16033
  "path": "tests/page_tweak_hold.mjs",
16029
16034
  "mode": "0000644",
16030
- "sha256": "0ab837626d1d9f70f8dcc0037f9162dfbc596fa5b846820f2108bdd539126825"
16035
+ "sha256": "c3c3b7d44baacd33d1b42db4609dcb8312d8ba46ac4dab38abc57371a4bde74b"
16031
16036
  },
16032
16037
  {
16033
16038
  "path": "tests/paintbrush.mjs",
@@ -3000,5 +3000,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3000
3000
  landed since 1.21.82 with no explicit bump. run 37869654816. (task 1002620)
3001
3001
  1.21.84 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3002
3002
  landed since 1.21.83 with no explicit bump. run 37876887587. (task 1002620)
3003
+ 1.21.85 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3004
+ landed since 1.21.84 with no explicit bump. run 37884200093. (task 1002620)
3003
3005
  ---------------------------------------------------------------------------
3004
3006
  ```
@@ -65,7 +65,19 @@ const HELD_NOT_ARMED = Object.freeze({
65
65
  reason: 'held for the artist (ADR 0341 D7): a page tweak lands only after its artist approves it',
66
66
  });
67
67
 
68
+ // The closing sentence of the PR body a ship opens (task 1004379). The stock
69
+ // text promised auto-merge, and a held round's PR is deliberately not armed, so
70
+ // the reader of a held PR was told the opposite of what would happen. Both PR
71
+ // writers (the server's publish route and ship.js's ci-land) end with this.
72
+ const ARMED_PR_SENTENCE = 'Auto-merge is enabled — GitHub lands it once the required checks pass.';
73
+ const HELD_PR_SENTENCE = 'Auto-merge is NOT enabled: this page tweak waits for its artist to approve it in the studio (ADR 0341 D7). It lands only after that approval.';
74
+ function prMergeSentence(held) {
75
+ return held ? HELD_PR_SENTENCE : ARMED_PR_SENTENCE;
76
+ }
77
+
68
78
  module.exports = {
79
+ HELD_PR_SENTENCE,
80
+ prMergeSentence,
69
81
  HELD_NOT_ARMED,
70
82
  HOLD_STATE,
71
83
  isPageTweak,
@@ -36,7 +36,7 @@ const { gradeQualityWhere } = require('../db-shared');
36
36
  const { helperJoinSql } = require('../page-tweak-artist'); // task 1004432: a page tweak's applier as a helper
37
37
  const auth = api;
38
38
  const db = require('../db');
39
- const pageTweakReads = require('../page-tweak-reads'); const { HELD_NOT_ARMED } = require('../page-tweak-hold'); // ADR 0341 D7 (tests stub the read)
39
+ const pageTweakReads = require('../page-tweak-reads'); const { HELD_NOT_ARMED, prMergeSentence } = require('../page-tweak-hold'); // ADR 0341 D7 (tests stub the read)
40
40
  const hierarchy = require('../hierarchy');
41
41
  const { suggestCriteria } = require('../criterion-suggest');
42
42
  // The orphan-task advisory (task 1002539) — pure, fail-open; see goal-advisory.js.
@@ -1176,12 +1176,13 @@ module.exports = function buildTasksRouter() {
1176
1176
  const builderLogin = await db.getLastClaimHolder(id)
1177
1177
  .then((h) => (h ? h.github_login : req.builder.github_login), () => null);
1178
1178
  const builderLine = prAssign.prBuilderLine(builderLogin);
1179
+ const held = await pageTweakReads.heldForArtistFailClosed(id); // ADR 0341 D7: read before the body names it (task 1004379)
1179
1180
  const body =
1180
1181
  `Auto-opened by GDS server-mediated publish (ADR 0055) for task ${id}.` +
1181
1182
  `${builderLine ? ` ${builderLine}` : ''}\n\n` +
1182
1183
  `${deHashRefs(valueSummary) || '(no value summary)'}\n\n` +
1183
1184
  `Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
1184
- `Auto-merge is enabled — GitHub lands it once the required checks pass.`;
1185
+ prMergeSentence(held);
1185
1186
  const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
1186
1187
  // Best-effort by contract (pr-assign.js never rejects); the catch is for a future
1187
1188
  // edit that forgets — an assignment must not be able to fail a pushed ship.
@@ -1191,7 +1192,7 @@ module.exports = function buildTasksRouter() {
1191
1192
  // builder nothing, and the commonest cause is a repo with allow_auto_merge OFF —
1192
1193
  // a permanent condition that reads exactly like a transient blip.
1193
1194
  // task 1004322 (ADR 0341 D7): a page tweak held for its artist is PUBLISHED, never armed.
1194
- const autoMergeResult = (await pageTweakReads.heldForArtistFailClosed(id)) ? HELD_NOT_ARMED : await githubPush.enableAutoMergeDetailed({ nodeId: pr.node_id });
1195
+ const autoMergeResult = held ? HELD_NOT_ARMED : await githubPush.enableAutoMergeDetailed({ nodeId: pr.node_id });
1195
1196
  const autoMerge = autoMergeResult.ok;
1196
1197
  // task 1139 (ADR 0058): attest the author's trust tier on the pushed sha so the
1197
1198
  // gate-review check can auto-approve a Metic+ author's gate-surface PR with no
@@ -78,6 +78,15 @@ const STAGES = Object.freeze({
78
78
  headline: (id) => `Task #${id} grade failed`,
79
79
  at: 4, currentKind: 'failed',
80
80
  },
81
+ // task 1004379: a page tweak whose grade PASSED and that now waits for its
82
+ // artist (ADR 0341 D7). It sits at completed, which stageFor() would read as a
83
+ // failed grade, so the caller names this stage explicitly (ship-finish.js
84
+ // finishHeld). The grade is done; the confirm node is the artist's to take.
85
+ held: {
86
+ tone: 'info', icon: 'ti-palette', statusWord: 'Waiting for the artist',
87
+ headline: (id) => `Task #${id} passed — waiting for the artist`,
88
+ at: 4, currentKind: 'held',
89
+ },
81
90
  });
82
91
 
83
92
  // Map a post-grade task status to a ship-family card stage. (claimed/review are
@@ -139,6 +148,8 @@ const GRADE_FAILED = 'grade failed';
139
148
  // task 1002892: the confirm node's word when the grade did not RUN. Two words, so
140
149
  // the " → " joint logic that already keeps 'grade failed' intact covers it too.
141
150
  const NOT_GRADED = 'not graded';
151
+ // task 1004379: the confirm node's word while a passed page tweak waits for its artist.
152
+ const AWAITING_ARTIST = 'awaiting artist';
142
153
 
143
154
  // The journey, mapped: one entry per PIPELINE node, conjugated + classified by
144
155
  // position relative to the current node (`cfg.at`). Both renderers consume this
@@ -158,6 +169,7 @@ function trailSteps(cfg) {
158
169
  // cfg.trailWord is set only by the outage override in buildCardModel; every
159
170
  // other caller leaves it undefined and gets GRADE_FAILED exactly as before.
160
171
  if (cfg.currentKind === 'failed') return { word: cfg.trailWord || GRADE_FAILED, state: 'current', checked: false, bold: true };
172
+ if (cfg.currentKind === 'held') return { word: AWAITING_ARTIST, state: 'current', checked: false, bold: true };
161
173
  if (cfg.currentKind === 'done') return { word: f.done, state: 'current', checked: true, bold: true };
162
174
  return { word: f.active, state: 'current', checked: false, bold: true }; // 'active'
163
175
  }
@@ -219,6 +231,7 @@ const DISCIPLINE_SKILL = { ui: 'design' };
219
231
  function nextActionFor(stage, { taskId, discipline, sandboxUrl } = {}) {
220
232
  if (stage === 'shipped') return { kind: 'prompt', label: 'Start next task', cmd: '/builder-start' };
221
233
  if (stage === 'confirmed') return { kind: 'prompt', label: 'Land it', cmd: '/merge-mode' };
234
+ if (stage === 'held') return { kind: 'prompt', label: 'Start next task', cmd: '/builder-start' };
222
235
  if (stage === 'failed') return { kind: 'prompt', label: 'Fix & re-grade', cmd: `node scripts/gds/ship.js ${taskId} --regrade` };
223
236
  if (stage === 'review') return { kind: 'link', label: 'Open in sandbox', url: sandboxUrl || '' };
224
237
  if (stage === 'claimed') {
@@ -391,7 +404,9 @@ function buildCardModel(input = {}) {
391
404
  }
392
405
  // Credits
393
406
  let creditsText = null;
394
- if (stage === 'failed') {
407
+ if (stage === 'held') {
408
+ creditsText = 'none yet — paid when the artist approves and it lands';
409
+ } else if (stage === 'failed') {
395
410
  creditsText = 'none — held at completed';
396
411
  } else if (credits.awarded != null) {
397
412
  const total = fmtInt(credits.total);
@@ -448,6 +463,8 @@ function buildCardModel(input = {}) {
448
463
  // states the automatic reality rather than implying a required command.
449
464
  deployText = 'landing automatically';
450
465
  }
466
+ } else if (stage === 'held') {
467
+ deployText = 'waiting for the artist to approve it in the studio';
451
468
  } else {
452
469
  deployText = 'not deployed';
453
470
  }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.84",
3
+ "version": "1.21.85",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.84",
9
+ "version": "1.21.85",
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.21.84",
3
+ "version": "1.21.85",
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",
@@ -9330,5 +9330,11 @@
9330
9330
  "id": "1004629",
9331
9331
  "text": "New projects now ask how finished work should ship: Auto (recommended) or Manual. The answer becomes the project's ship mode, which the owner can change later on the settings page."
9332
9332
  }
9333
+ ],
9334
+ "1.21.85": [
9335
+ {
9336
+ "id": "1004379",
9337
+ "text": "A page tweak that passed its grade and is waiting for its artist no longer reports itself as a failed grade: the closing card, the PR description and the credits line now all say it passed and is waiting for the artist's appro"
9338
+ }
9333
9339
  ]
9334
9340
  }
@@ -244,7 +244,8 @@ async function finishHeld({ taskId, summary, postGradeStatus, card }, deps = {})
244
244
  console.error(` Re-run to publish it again (the grade is not re-run): node scripts/gds/ship.js ${taskId}`);
245
245
  }
246
246
  const usage = await (deps.postShipSync || postShipSync)(taskId, 'confirmed');
247
- await (deps.printCompletionCard || printCompletionCard)({ taskId, postGradeStatus, card, usage, land });
247
+ // task 1004379: name the stage — at 'completed' the card would otherwise read a failed grade.
248
+ await (deps.printCompletionCard || printCompletionCard)({ taskId, postGradeStatus, card, usage, land, stage: 'held' });
248
249
  return land;
249
250
  }
250
251
 
@@ -270,7 +271,7 @@ function diffstatFor(baseline) {
270
271
  // in-feed HTML to a temp file and prints an [otb-card-html] marker the
271
272
  // builder-ship skill picks up to render it in the chat feed. Every step is
272
273
  // best-effort — a summary card must NEVER fail or undo a completed ship.
273
- async function printCompletionCard({ taskId, postGradeStatus, card, usage, land = null }) {
274
+ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land = null, stage = undefined }) {
274
275
  if (!card) return;
275
276
  try {
276
277
  const shipCard = require('../../modules/lifecycle/ship-card');
@@ -329,6 +330,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
329
330
  }
330
331
 
331
332
  const cardInputs = {
333
+ stage, // task 1004379: 'held' from finishHeld; undefined ⇒ derived from postGradeStatus
332
334
  postGradeStatus,
333
335
  taskId,
334
336
  title: card.title,
@@ -722,7 +722,7 @@ async function shipMain() {
722
722
 
723
723
  printCreditsAndAchievements({
724
724
  postGradeStatus, credits, creditsRaw, creditsMultiplier, ideaBonusInfo,
725
- justUnlocked: postGradeJustUnlocked,
725
+ justUnlocked: postGradeJustUnlocked, held,
726
726
  });
727
727
 
728
728
  // ---- Step 3: confirmed→shipped tail (merge + deploy → memory/session sync →
@@ -1107,6 +1107,7 @@ async function regradeMain() {
1107
1107
  const g = gradePost.data.grade;
1108
1108
  recordGradeOutcome({ taskId, grade: g });
1109
1109
  const postGradeStatus = gradePost.data.taskStatus;
1110
+ const held = gradePost.data.held === true; // task 1004322: a page tweak's pass waits for its artist
1110
1111
  printGradeOutcome(g, taskId);
1111
1112
  printCreditsAndAchievements({
1112
1113
  postGradeStatus,
@@ -1115,13 +1116,14 @@ async function regradeMain() {
1115
1116
  creditsMultiplier: gradePost.data.creditsMultiplier ?? null,
1116
1117
  ideaBonusInfo: gradePost.data.ideaBonus ?? null,
1117
1118
  justUnlocked: gradePost.data.just_unlocked || [],
1119
+ held, // task 1004379: a held pass is not 'grade not passed'
1118
1120
  });
1119
1121
 
1120
1122
  // On a pass the task is now confirmed — run the SAME confirmed→shipped tail as a
1121
1123
  // normal ship (finishShip, #1093/C9) so a re-grade lands identically.
1122
1124
  await finishShip({
1123
1125
  taskId, summary, advanceToMerge: gradePost.data.advanceToMerge, skipMerge, dbOnly,
1124
- held: gradePost.data.held === true, // task 1004322: a page tweak's pass waits for its artist
1126
+ held,
1125
1127
  postGradeStatus,
1126
1128
  doneLine: g.passed ? 'Done — re-grade passed.' : 'Done — re-grade still failing; fix and try again.',
1127
1129
  // task 1270: completion card for the re-grade path too.
@@ -478,9 +478,14 @@ function printGradeOutcome(g, taskId) {
478
478
 
479
479
  // Print the credits line + multiplier breakdown + idea bonus + unlocked
480
480
  // achievements from a grade/confirm response.
481
- function printCreditsAndAchievements({ postGradeStatus, credits, creditsRaw, creditsMultiplier, ideaBonusInfo, justUnlocked }) {
481
+ function printCreditsAndAchievements({ postGradeStatus, credits, creditsRaw, creditsMultiplier, ideaBonusInfo, justUnlocked, held = false }) {
482
482
  console.log('');
483
- if (postGradeStatus === 'completed') {
483
+ if (held) {
484
+ // task 1004379: a page tweak's PASS waits at completed for its artist (ADR
485
+ // 0341 D7). Reading 'completed' as 'grade not passed' told the reader the
486
+ // opposite of what happened.
487
+ console.log(`Credits awarded: ${credits} (none yet — the grade passed; credits land when the artist approves and it merges)`);
488
+ } else if (postGradeStatus === 'completed') {
484
489
  console.log(`Credits awarded: ${credits} (none — task at completed, grade not passed)`);
485
490
  } else if (postGradeStatus === 'confirmed') {
486
491
  console.log(`Credits awarded: ${credits} (none yet — credits land when the task merges to main)`);
@@ -19,6 +19,7 @@ const { plainCardLines } = require('./plain-cards.js');
19
19
  // The server's own reader of a PR's check rollup (task 1003988), reused so the laptop
20
20
  // poll names a red PR exactly the way publish-status does (task 1003764). Dependency-free.
21
21
  const { readCheckRollup, UNIT_REPORT_CONTEXT } = require('../../modules/lifecycle/check-rollup.js');
22
+ const { prMergeSentence } = require('../../modules/lifecycle/page-tweak-hold.js');
22
23
 
23
24
  // ---------- the land outcome (task 1002541, part 1) ----------
24
25
  //
@@ -927,8 +928,7 @@ async function ciLandLocal(taskId, branch, valueSummary, { hold = false } = {})
927
928
  `Auto-opened by ship.js (deploy mode: ci) for task ${taskId}.\n\n` +
928
929
  `${sanitizeGitHubRefs(valueSummary) || '(no value summary)'}\n\n` +
929
930
  `Merging this PR triggers .github/workflows/deploy-prod.yml to deploy to ` +
930
- `${deployTargetNote}. Auto-merge is enabled — GitHub lands it once the ` +
931
- `required checks pass.`;
931
+ `${deployTargetNote}. ${prMergeSentence(hold)}`; // task 1004379: a held round's PR says it waits
932
932
  if (!existing || existing.state === 'CLOSED' || existing.state === 'MERGED') {
933
933
  console.log(' ci-land: opening PR -> main...');
934
934
  if (!ghArgvOk(buildPrCreateArgv(branch, title, body))) {
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.84'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.85'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,164 @@
1
+ // tests/held_tweak_reporting.mjs — a held page tweak is reported as a PASS that
2
+ // waits for its artist, never as a failed grade (task 1004379, ADR 0341 D7).
3
+ //
4
+ // WHY. The first live Tweak Mode rounds (1004345, 1004348) passed their grade and
5
+ // held at completed for the artist, as designed — and then three surfaces told the
6
+ // reader the opposite: the closing card said "grade failed" and offered --regrade,
7
+ // the PR the server opened said "Auto-merge is enabled", and a passing --regrade
8
+ // printed "Credits awarded: 0 (grade not passed)". Each case below drives the real
9
+ // code a held round runs through:
10
+ // 1. finishHeld hands the card a 'held' stage, and that card reads as a pass;
11
+ // 2. the publish route's PR body names the hold (and an ordinary PR is unchanged);
12
+ // 3. the credits line a passing (re-)grade prints on a held round says it passed.
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import { createRequire } from 'node:module';
16
+ import http from 'node:http';
17
+ import { makeRunner } from './helpers.mjs';
18
+
19
+ process.env.NODE_ENV = 'test';
20
+ const require = createRequire(import.meta.url);
21
+ const { test, summary } = makeRunner();
22
+
23
+ const shipCard = require('../modules/lifecycle/ship-card.js');
24
+ const shipFinish = require('../scripts/gds/ship-finish.js');
25
+ const shipLand = require('../scripts/gds/ship-land.js');
26
+ const { printCreditsAndAchievements } = require('../scripts/gds/ship-grade-steps.js');
27
+
28
+ const capture = async (fn) => {
29
+ const out = [];
30
+ const orig = console.log;
31
+ console.log = (...a) => out.push(a.join(' '));
32
+ try { await fn(); } finally { console.log = orig; }
33
+ return out.join('\n');
34
+ };
35
+
36
+ // ---- 1. the closing card --------------------------------------------------
37
+
38
+ const twelveMinor = Array.from({ length: 12 }, (_, i) => ({ severity: 'minor', file: `f${i}`, summary: 'nit' }));
39
+
40
+ await test('finishHeld names the held stage to the card, even though the task sits at completed', async () => {
41
+ let cardArgs = null;
42
+ await capture(() => shipFinish.finishHeld({ taskId: 5001, summary: 's', postGradeStatus: 'completed', card: { title: 't' } }, {
43
+ publishHeld: async () => shipLand.landHeld({ prUrl: 'https://github.com/o/r/pull/77' }),
44
+ postShipSync: async () => null,
45
+ printCompletionCard: async (a) => { cardArgs = a; },
46
+ }));
47
+ assert.equal(cardArgs.stage, 'held');
48
+ assert.equal(cardArgs.postGradeStatus, 'completed');
49
+ });
50
+
51
+ await test('a held pass prints a hold card: no "grade failed", no --regrade, a passing grade row', () => {
52
+ const model = shipCard.buildCardModel({
53
+ stage: 'held', postGradeStatus: 'completed', taskId: 5001, title: 't',
54
+ grade: { passed: true, issues: twelveMinor, panel: 4 },
55
+ credits: { awarded: 0 },
56
+ });
57
+ const text = shipCard.renderMonospaceCard(model);
58
+ const html = shipCard.renderCardHtml(model);
59
+ assert.equal(model.stage, 'held');
60
+ assert.match(model.cfg.headline(5001), /waiting for the artist/);
61
+ for (const out of [text, html]) {
62
+ assert.doesNotMatch(out, /grade failed/i, 'the card must not call a pass a failure');
63
+ assert.doesNotMatch(out, /Needs fixes/);
64
+ assert.doesNotMatch(out, /--regrade/);
65
+ assert.doesNotMatch(out, /fail · 12 issues/);
66
+ assert.match(out, /awaiting artist/);
67
+ }
68
+ const row = (k) => (model.rows.find((r) => r.k === k) || {}).v;
69
+ assert.match(row('Grade'), /^pass/);
70
+ assert.match(row('Deploy'), /artist/);
71
+ assert.match(row('Credits'), /artist approves/);
72
+ assert.equal(model.action.cmd, '/builder-start');
73
+ });
74
+
75
+ await test('an ordinary stop at completed (a real failed grade) still reads as a failed grade', () => {
76
+ const model = shipCard.buildCardModel({
77
+ postGradeStatus: 'completed', taskId: 5002, title: 't',
78
+ grade: { passed: false, issues: twelveMinor },
79
+ });
80
+ assert.equal(model.stage, 'failed');
81
+ assert.match(shipCard.renderMonospaceCard(model), /grade failed/);
82
+ assert.match(model.action.cmd, /--regrade/);
83
+ });
84
+
85
+ // ---- 2. the PR body -------------------------------------------------------
86
+
87
+ const express = require('express');
88
+ const api = require('../src/module-api.js');
89
+ const db = require('../modules/lifecycle/db.js');
90
+ const gp = require('../modules/lifecycle/github-push.js');
91
+ const prAssign = require('../modules/lifecycle/pr-assign.js');
92
+ const pageTweakReads = require('../modules/lifecycle/page-tweak-reads.js');
93
+ const hold = require('../modules/lifecycle/page-tweak-hold.js');
94
+
95
+ let heldNow = false;
96
+ let prBody = null;
97
+ api.requireBuilder = (req, res, next) => { req.builder = { id: 1, rank: 'archon', github_login: 'the-archon' }; next(); };
98
+ db.getLastClaimHolder = async () => ({ builder_id: 5, github_login: 'some-builder' });
99
+ db.setClaimPublishedSha = async () => {};
100
+ gp.isConfigured = () => true;
101
+ gp.pushBundle = async () => ({ head_sha: 'a'.repeat(40) });
102
+ gp.openOrFindPullRequest = async ({ body }) => { prBody = body; return { number: 77, url: 'https://github.com/o/r/pull/77', node_id: 'N' }; };
103
+ gp.enableAutoMergeDetailed = async () => ({ ok: true });
104
+ gp.setCommitStatus = async () => true;
105
+ prAssign.assignPullRequest = async () => ({ assigned: true });
106
+ pageTweakReads.heldForArtistFailClosed = async () => heldNow;
107
+
108
+ const app = express();
109
+ app.use((req, res, next) => { res.fail = (code, status, extra) => res.status(typeof status === 'number' ? status : (status && status.status) || 500).json({ error: code, ...(extra || {}) }); next(); });
110
+ app.use(require('../modules/lifecycle/routes/tasks.js')());
111
+ const server = http.createServer(app);
112
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
113
+ const base = `http://127.0.0.1:${server.address().port}`;
114
+ const publish = async () => {
115
+ prBody = null;
116
+ const res = await fetch(`${base}/tasks/5001/publish-branch`, {
117
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({ branch: 'task-5001', head_sha: 'a'.repeat(40), bundle_b64: Buffer.from('b').toString('base64'), value_summary: 'Page tweak.' }),
119
+ });
120
+ return { status: res.status, body: await res.json() };
121
+ };
122
+
123
+ await test("a held round's PR body says it waits for the artist, and never promises auto-merge", async () => {
124
+ heldNow = true;
125
+ const r = await publish();
126
+ assert.equal(r.status, 201, JSON.stringify(r.body));
127
+ assert.equal(r.body.auto_merge, false);
128
+ assert.ok(prBody.endsWith(hold.HELD_PR_SENTENCE), prBody);
129
+ assert.match(prBody, /artist/);
130
+ assert.doesNotMatch(prBody, /Auto-merge is enabled/);
131
+ });
132
+
133
+ await test('every other PR body keeps the auto-merge sentence it always had', async () => {
134
+ heldNow = false;
135
+ const r = await publish();
136
+ assert.equal(r.status, 201);
137
+ assert.equal(r.body.auto_merge, true);
138
+ assert.match(prBody, /Auto-merge is enabled — GitHub lands it once the required checks pass\.$/);
139
+ });
140
+ server.close();
141
+
142
+ // ---- 3. the credits line after a passing (re-)grade -----------------------
143
+
144
+ await test('a passing grade on a held round prints a pass, not "grade not passed"', async () => {
145
+ const text = await capture(() => printCreditsAndAchievements({ postGradeStatus: 'completed', credits: 0, justUnlocked: [], held: true }));
146
+ assert.doesNotMatch(text, /grade not passed/);
147
+ assert.match(text, /grade passed/);
148
+ assert.match(text, /artist approves/);
149
+ });
150
+
151
+ await test('a real failed grade at completed still says the grade did not pass', async () => {
152
+ const text = await capture(() => printCreditsAndAchievements({ postGradeStatus: 'completed', credits: 0, justUnlocked: [] }));
153
+ assert.match(text, /grade not passed/);
154
+ });
155
+
156
+ await test('both grade paths (ship and --regrade) hand the hold to the credits line', () => {
157
+ const fs = require('node:fs');
158
+ const src = fs.readFileSync(new URL('../scripts/gds/ship-flow.js', import.meta.url), 'utf8');
159
+ const calls = src.split('printCreditsAndAchievements({').slice(1).map((c) => c.slice(0, c.indexOf('});')));
160
+ const withHold = calls.filter((c) => /\bheld\b/.test(c));
161
+ assert.equal(withHold.length, 2, 'the ship grade and the re-grade both pass held');
162
+ });
163
+
164
+ summary();
@@ -206,8 +206,7 @@ await test('finishShip routes a held pass away from mergeAndDeploy (the source s
206
206
  const mergeAt = body.indexOf('await mergeAndDeploy(');
207
207
  assert.ok(holdAt > 0 && mergeAt > holdAt, 'the hold is decided BEFORE any merge is attempted');
208
208
  const flow = fs.readFileSync(require.resolve('../scripts/gds/ship-flow.js'), 'utf8');
209
- assert.equal((flow.match(/held = gradePost\.data\.held === true/g) || []).length, 1, 'shipMain reads the hold off the grade response');
210
- assert.equal((flow.match(/held: gradePost\.data\.held === true/g) || []).length, 1, 'regradeMain does too');
209
+ assert.equal((flow.match(/\bheld = gradePost\.data\.held === true/g) || []).length, 2, 'shipMain and regradeMain both read the hold off the grade response');
211
210
  assert.match(flow, /tweakHold\.isHeldForArtist\(task, tRes\.data\.grade/, 'a re-run of a held task re-publishes instead of re-grading');
212
211
  });
213
212