@bongos/core 1.19.1036 → 1.19.1037

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.bongos-core.json +43 -48
  2. package/.claude/skills/grade-recover/SKILL.md +1 -1
  3. package/docs/adr/0302-the-third-identical-failure-stops-the-session-and-asks.md +3 -2
  4. package/docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md +2 -2
  5. package/docs/adr/README.md +1 -1
  6. package/docs/module-api-changelog.md +2 -0
  7. package/modules/economy/credits.js +1 -1
  8. package/modules/grading/grader-config.js +3 -12
  9. package/modules/grading/grader-no-artifact.js +227 -2
  10. package/modules/grading/grader-prompt.js +1 -1
  11. package/modules/grading/grader-rubric.json +1 -86
  12. package/modules/grading/grader-score.js +1 -169
  13. package/modules/grading/grader-subagent.js +1 -51
  14. package/modules/grading/grader.js +26 -211
  15. package/modules/lifecycle/db-ship.js +3 -3
  16. package/package-lock.json +2 -2
  17. package/package.json +1 -1
  18. package/release-notes.json +10 -0
  19. package/scripts/gds/claim.js +2 -1
  20. package/scripts/gds/grade-smoke.js +3 -3
  21. package/scripts/gds/render-check.js +1 -1
  22. package/scripts/gds/ship-check.js +1 -1
  23. package/scripts/gds/ship-flow.js +1 -1
  24. package/scripts/gds/ship-grade-steps.js +2 -2
  25. package/scripts/gds/visual-review-nudge.js +2 -1
  26. package/src/module-api.js +1 -1
  27. package/tests/adr_0162_gate_citation.mjs +167 -0
  28. package/tests/grader.mjs +48 -126
  29. package/tests/grader_unbacked_score.mjs +1 -1
  30. package/tests/regrade_actual_minutes.mjs +18 -17
  31. package/tests/render_check.mjs +1 -1
  32. package/tests/skill_grade_audit.mjs +3 -3
  33. package/tests/skill_grader_health.mjs +2 -2
  34. package/scripts/gds/audit-ships.js +0 -319
  35. package/tests/audit_ships_git_argv.mjs +0 -173
@@ -75,23 +75,28 @@
75
75
  // module (which receives it via dependency injection to avoid a circular
76
76
  // require).
77
77
  //
78
- // ## Legacy v1 (still exported)
78
+ // ## Retired (task 1002842)
79
79
  //
80
- // gradeDeterministic(...) — v1 deterministic path, kept for
81
- // scripts/gds/audit-ships.js (retroactive audit). NOT invoked at ship time.
80
+ // The v1 deterministic process-audit grader (gradeDeterministic and its nine
81
+ // per-criterion scorers) and its one caller, scripts/gds/audit-ships.js, are
82
+ // deleted: the live panel grades every ship at ship time, so a retroactive
83
+ // heuristic audit has nothing left to cover. aggregateShots / runOneShot moved
84
+ // to grader-no-artifact.js, the species that depends on them; they are still
85
+ // re-exported here, and the trivial-diff fast path below still runs one shot.
82
86
 
83
87
  const path = require('node:path');
84
88
  const fs = require('node:fs');
85
89
  const { spawn } = require('node:child_process');
86
90
 
87
- const { ASSUMED_BUILDER_MODEL, DEFAULT_GRADER_MODEL, DEFAULT_SHOTS, FALLTHROUGH_CONFIDENCE, RUBRIC, THRESHOLD, V1, V3, VERSION, generatedProvenance, permissionPathPrePass, routeRankPrePass, workers } = require('./grader-config.js');
91
+ const { ASSUMED_BUILDER_MODEL, DEFAULT_GRADER_MODEL, FALLTHROUGH_CONFIDENCE, RUBRIC, THRESHOLD, VERSION, generatedProvenance, permissionPathPrePass, routeRankPrePass, workers } = require('./grader-config.js');
88
92
  const { clamp, isTrivialDiff, median, r1, selectGraderModel, workerModelFor } = require('./grader-model.js');
89
93
  const { buildPrompt, extractJson } = require('./grader-prompt.js');
90
94
  // task 1004011: the second grade species — a grade over the CLAIM on an
91
95
  // --allow-empty ship, where there is no diff for the panel above to read.
92
- const { GRADER_KIND: NO_ARTIFACT_GRADER_KIND, buildNoArtifactPrompt, gradeNoArtifact } = require('./grader-no-artifact.js');
93
- const { aggregateShots, changeMagnitude, hasPanelEvidence, isSubstantiveIssue, serverSideGuards, validateAndScore } = require('./grader-score.js');
94
- const { buildContextBundle, diffAddedLinesByFile, extractAssistantResult, extractCostUsd, runOneShot, runSubagent, runSubagentCached } = require('./grader-subagent.js');
96
+ // task 1002842: the generalist-shot plumbing now lives there too.
97
+ const { GRADER_KIND: NO_ARTIFACT_GRADER_KIND, aggregateShots, buildNoArtifactPrompt, gradeNoArtifact, runOneShot } = require('./grader-no-artifact.js');
98
+ const { changeMagnitude, hasPanelEvidence, isSubstantiveIssue, serverSideGuards, validateAndScore } = require('./grader-score.js');
99
+ const { buildContextBundle, diffAddedLinesByFile, extractAssistantResult, extractCostUsd, runSubagent, runSubagentCached } = require('./grader-subagent.js');
95
100
  // task 1002903: the severity gate's unbacked low scores, named once for the record.
96
101
  const { unbackedLowScores } = require('./grader-workers/worker-verdict.js');
97
102
  // task 1002839: the central engine the code panel below registers into (ADR 0089 §1).
@@ -478,8 +483,8 @@ function aggregatePanel(workerResults, meta = {}) {
478
483
  grader_version: VERSION,
479
484
  // #486: the panel's own API spend (USD), for cost_log (source=grader).
480
485
  panel_cost_usd: panelCostUsd,
481
- // rubric_scores keep the v3 keys so the trend dashboard + audit-ships
482
- // legacy v1 entry-points continue to read them. Phase 2.1 added
486
+ // rubric_scores keep the v3 keys so the trend dashboard and the server's
487
+ // validateAndScore re-validation continue to read them. Phase 2.1 added
483
488
  // security_score; Phase 2.2 adds efficiency_score. Both additive —
484
489
  // consumers that don't know about them just ignore the fields.
485
490
  rubric_scores: {
@@ -651,12 +656,17 @@ async function gradeSubagent({ task, valueSummary, diff, changedFiles, diffStats
651
656
  });
652
657
  }
653
658
 
654
- // ---------- v3 multi-shot path (kept exported for explicit diagnostics) ----------
659
+ // ---------- the trivial-diff fast path (#513) ----------
655
660
  //
656
- // Same shape as the original v3 gradeSubagent; renamed so the public name
657
- // (gradeSubagent) points at the v4 panel. Useful for re-running a single
658
- // generalist prompt N times for diagnostic comparison against the panel.
659
- async function gradeSubagentV3MultiShot({ task, valueSummary, diff, changedFiles, diffStats, shots = DEFAULT_SHOTS, builderModel, runOpts = {} }) {
661
+ // A small copy/docs-only diff gets ONE generalist shot instead of the four-worker
662
+ // panel — "lighter, not skipped" (ADR 0089 §7 keeps it). The name is historical:
663
+ // the v3 multi-shot GENERATION is retired (task 1002842 removed its 3-shot default
664
+ // and rubric block), and this is what is left of it — the working fast path.
665
+ // Moving it onto a v4 panel needs a reviewer type that reports both
666
+ // functional_fidelity and code_quality, which no v4 worker does; that is a cost
667
+ // and behaviour decision, not a cleanup. `runShot` is a test seam defaulted to
668
+ // the real spawner (the same pattern as gradeNoArtifact); production passes none.
669
+ async function gradeSubagentV3MultiShot({ task, valueSummary, diff, changedFiles, diffStats, shots = 1, builderModel, runOpts = {}, runShot = runOneShot }) {
660
670
  const prompt = buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, generatedProvenance: await generatedProvenance.verifyGeneratedProvenanceAsync({ changedFiles, cwd: runOpts.cwd }) }); // task 1002795
661
671
  const graderModel = selectGraderModel({ builderModel });
662
672
  const N = Math.max(1, Math.trunc(shots));
@@ -664,7 +674,7 @@ async function gradeSubagentV3MultiShot({ task, valueSummary, diff, changedFiles
664
674
  const startedAt = Date.now();
665
675
  const shotPromises = [];
666
676
  for (let i = 0; i < N; i++) {
667
- shotPromises.push(runOneShot({ prompt, model: graderModel, shotIndex: i, runOpts }));
677
+ shotPromises.push(runShot({ prompt, model: graderModel, shotIndex: i, runOpts }));
668
678
  }
669
679
  const shotResults = await Promise.all(shotPromises);
670
680
  const totalMs = Date.now() - startedAt;
@@ -676,189 +686,6 @@ async function gradeSubagentV3MultiShot({ task, valueSummary, diff, changedFiles
676
686
  });
677
687
  }
678
688
 
679
- // =========================================================================
680
- // Legacy v1 (deterministic) — kept for scripts/gds/audit-ships.js only.
681
- // =========================================================================
682
-
683
- function scoreEstActualBand(actualMinutes, estMinutes) {
684
- if (!Number.isFinite(actualMinutes) || actualMinutes <= 0) return 5;
685
- if (!Number.isFinite(estMinutes) || estMinutes <= 0) return 6;
686
- const ratio = actualMinutes / estMinutes;
687
- if (ratio >= 0.5 && ratio <= 2.0) return 10;
688
- if (ratio < 0.5) {
689
- if (ratio <= 0.1) return 0;
690
- return r1(10 * ((ratio - 0.1) / 0.4));
691
- }
692
- if (ratio > 2.0) {
693
- if (ratio >= 10) return 0;
694
- return r1(10 * (1 - (ratio - 2.0) / 8.0));
695
- }
696
- return 5;
697
- }
698
-
699
- function scoreValueSummary(valueSummary) {
700
- const s = (valueSummary || '').trim();
701
- if (s.length === 0) return 0;
702
- if (s.length < 40) return 5;
703
- if (s.length < 120) return 8;
704
- return /\b(add|fix|update|remove|refactor|wire|land|migrate|enable|disable|introduce|deprecate|ship|deploy|prevent|capture|gate|allocate|carry|expose|surface)\b/i.test(s)
705
- ? 10
706
- : 8;
707
- }
708
-
709
- function scoreDiffHygiene(diffStats) {
710
- if (!diffStats) return 5;
711
- const files = Number(diffStats.files_changed) || 0;
712
- const added = Number(diffStats.lines_added) || 0;
713
- const removed = Number(diffStats.lines_removed) || 0;
714
- if (files === 0 || added + removed === 0) return 0;
715
- if (files > 50) return 5;
716
- return 10;
717
- }
718
-
719
- function scoreScannerSmoke(verification) {
720
- if (!verification) return 5;
721
- return verification.scanner_clean && verification.smoke_clean ? 10 : 5;
722
- }
723
-
724
- function scoreScopingSignal(scopingConfidence) {
725
- if (scopingConfidence == null) return 6;
726
- const c = Number(scopingConfidence);
727
- if (c <= 0) return 2;
728
- if (c === 1) return 4;
729
- if (c === 2) return 6;
730
- if (c === 3) return 7;
731
- if (c === 4) return 8;
732
- return 10;
733
- }
734
-
735
- function scoreLocNetNegative(diffStats) {
736
- if (!diffStats) return 5;
737
- const added = Number(diffStats.lines_added) || 0;
738
- const removed = Number(diffStats.lines_removed) || 0;
739
- if (removed > added) return 10;
740
- if (removed === added) return 5;
741
- return 0;
742
- }
743
-
744
- function scoreLearningRow(learningRow) {
745
- if (!learningRow) return 0;
746
- const body = (learningRow.body_md || '').trim();
747
- if (body.length > 100) return 10;
748
- if (body.length > 0) return 5;
749
- return 0;
750
- }
751
-
752
- function scoreArtRubricAvg(value) {
753
- if (!Number.isFinite(value)) return null;
754
- return clamp(r1(value), 0, 10);
755
- }
756
-
757
- function blendWeightsForKind(kind) {
758
- const blended = {};
759
- for (const [name, def] of Object.entries(V1.weights.common)) {
760
- blended[name] = def.weight;
761
- }
762
- const kindBonus = V1.weights.kind_specific[kind];
763
- if (kindBonus) {
764
- for (const [name, def] of Object.entries(kindBonus)) {
765
- if (name === 'loc_net_negative' && 'diff_hygiene' in blended) delete blended.diff_hygiene;
766
- if (name === 'learning_row' && 'diff_hygiene' in blended) delete blended.diff_hygiene;
767
- blended[name] = def.weight;
768
- }
769
- }
770
- const total = Object.values(blended).reduce((a, b) => a + b, 0);
771
- for (const k of Object.keys(blended)) blended[k] = blended[k] / total;
772
- return blended;
773
- }
774
-
775
- // Legacy v1 entry. Renamed from `grade()` in V3.R18 (#209). Now ONLY
776
- // called from scripts/gds/audit-ships.js (retroactive #204 audit). The
777
- // runtime ship path uses gradeSubagent (v3 multi-shot).
778
- function gradeDeterministic({ task, verification, actualMinutes, valueSummary, learningRow }) {
779
- const kind = task?.kind || 'unclassified';
780
- const weights = blendWeightsForKind(kind);
781
- const rubricScores = {};
782
- const signals = {
783
- kind,
784
- est_minutes: task?.est_minutes ?? null,
785
- actual_minutes: actualMinutes ?? null,
786
- scoping_confidence: task?.scoping_confidence ?? null,
787
- scanner_clean: verification?.scanner_clean ?? null,
788
- smoke_clean: verification?.smoke_clean ?? null,
789
- diff_stats: verification?.diff_stats ?? null,
790
- art_rubric_avg: verification?.art_rubric_avg ?? null,
791
- value_summary_chars: (valueSummary || '').trim().length,
792
- };
793
-
794
- if ('est_actual_band' in weights) {
795
- rubricScores.est_actual_band = scoreEstActualBand(actualMinutes, task?.est_minutes);
796
- }
797
- if ('value_summary' in weights) {
798
- rubricScores.value_summary = scoreValueSummary(valueSummary);
799
- }
800
- if ('diff_hygiene' in weights) {
801
- rubricScores.diff_hygiene = scoreDiffHygiene(verification?.diff_stats);
802
- }
803
- if ('scanner_smoke' in weights) {
804
- rubricScores.scanner_smoke = scoreScannerSmoke(verification);
805
- }
806
- if ('scoping_signal' in weights) {
807
- rubricScores.scoping_signal = scoreScopingSignal(task?.scoping_confidence);
808
- }
809
- if ('loc_net_negative' in weights) {
810
- rubricScores.loc_net_negative = scoreLocNetNegative(verification?.diff_stats);
811
- }
812
- if ('learning_row' in weights) {
813
- rubricScores.learning_row = scoreLearningRow(learningRow);
814
- }
815
- if ('art_rubric_avg' in weights) {
816
- const s = scoreArtRubricAvg(verification?.art_rubric_avg);
817
- if (s == null) {
818
- const w = weights.art_rubric_avg;
819
- delete weights.art_rubric_avg;
820
- const remaining = Object.values(weights).reduce((a, b) => a + b, 0);
821
- if (remaining > 0) {
822
- for (const k of Object.keys(weights)) weights[k] = weights[k] / remaining;
823
- }
824
- } else {
825
- rubricScores.art_rubric_avg = s;
826
- }
827
- }
828
-
829
- let score = 0;
830
- for (const [name, w] of Object.entries(weights)) {
831
- if (rubricScores[name] != null) score += rubricScores[name] * w;
832
- }
833
- score = r1(clamp(score, 0, 10));
834
-
835
- const threshold = V1.threshold;
836
- const passed = score >= threshold;
837
-
838
- const lines = [];
839
- lines.push(`Auto-grader ${V1.version} (deprecated) — task_kind=${kind}.`);
840
- lines.push(`Score ${score}/10 vs threshold ${threshold} → ${passed ? 'PASS' : 'FAIL'}.`);
841
- lines.push('');
842
- lines.push('Per-criterion:');
843
- for (const [name, w] of Object.entries(weights)) {
844
- const s = rubricScores[name];
845
- if (s == null) continue;
846
- const contrib = r1(s * w);
847
- lines.push(` - ${name.padEnd(20)} ${String(s).padStart(4)}/10 × ${w.toFixed(2)} = ${contrib}`);
848
- }
849
-
850
- return {
851
- score,
852
- passed,
853
- threshold,
854
- grader_kind: 'auto',
855
- grader_version: V1.version,
856
- rubric_scores: rubricScores,
857
- signals,
858
- notes_md: lines.join('\n'),
859
- };
860
- }
861
-
862
689
  module.exports = {
863
690
  // v4 surface (active panel-based gate)
864
691
  aggregatePanel,
@@ -888,25 +715,13 @@ module.exports = {
888
715
  runSubagent,
889
716
  runSubagentCached, // System 5 (task 1359): opt-in content-addressed cached path
890
717
  runOneShot,
891
- gradeSubagentV3MultiShot, // explicit v3 multi-shot diagnostic path
718
+ gradeSubagentV3MultiShot, // the trivial-diff fast path (#513) — one generalist shot
892
719
  extractJson,
893
720
  extractAssistantResult,
894
721
  extractCostUsd, // #486: parse total_cost_usd from the envelope
895
722
  THRESHOLD,
896
723
  VERSION,
897
724
  DEFAULT_GRADER_MODEL,
898
- DEFAULT_SHOTS,
899
725
  ASSUMED_BUILDER_MODEL,
900
- // legacy v1 (audit-ships.js only)
901
- gradeDeterministic,
902
- scoreEstActualBand,
903
- scoreValueSummary,
904
- scoreDiffHygiene,
905
- scoreScannerSmoke,
906
- scoreScopingSignal,
907
- scoreLocNetNegative,
908
- scoreLearningRow,
909
- scoreArtRubricAvg,
910
- blendWeightsForKind,
911
726
  RUBRIC,
912
727
  };
@@ -277,9 +277,9 @@ async function resolveClaim({ claimId, builderId, outcome, notesMd, valueSummary
277
277
  // The ordering below is NOT for the grader, whatever this comment used to
278
278
  // say. V3.R18 (#209) moved the grader out of resolveClaim — see the block
279
279
  // after this one — and the live panel never receives actual_minutes at all
280
- // (modules/grading/grader-prompt.js buildPrompt prints est_minutes only;
281
- // scoreEstActualBand lives in gradeDeterministic, whose one non-test caller
282
- // is the retroactive audit in scripts/gds/audit-ships.js). The seal must
280
+ // (modules/grading/grader-prompt.js buildPrompt prints est_minutes only; the
281
+ // one scorer that read actual_minutes was retired with the v1 grader in task
282
+ // 1002842). The seal must
283
283
  // still precede the recompute, because the recompute SUMs the row it writes.
284
284
  // tests/regrade_actual_minutes.mjs pins the panel half of that.
285
285
  await sealSessionLog(client, {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1036",
3
+ "version": "1.19.1037",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1036",
9
+ "version": "1.19.1037",
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.1036",
3
+ "version": "1.19.1037",
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",
@@ -7497,5 +7497,15 @@
7497
7497
  "id": "1004174",
7498
7498
  "text": "The owner of a hosted project can now upgrade it themselves: the \"you're behind\" banner on their project links to the Deploy page on the hub, which lets a project's owner move their own project, and only their own."
7499
7499
  }
7500
+ ],
7501
+ "1.19.1037": [
7502
+ {
7503
+ "id": "1002842",
7504
+ "text": "The grader's retired first generation and an unused one-off audit script are removed, and the pieces a newer reviewer still depends on now live with it — nothing about how work is graded changes."
7505
+ },
7506
+ {
7507
+ "id": "1004280",
7508
+ "text": "The project's own notes no longer claim a past decision banned all new checks; each one now gives its real reason, and a test stops the wrong claim coming back."
7509
+ }
7500
7510
  ]
7501
7511
  }
@@ -739,7 +739,8 @@ async function main() {
739
739
  // last thing the builder reads before deciding how to work — task 1267.
740
740
  printDisciplineMode(task);
741
741
  // task 1004202: a task that changes a page is told to use /design and get its
742
- // human's OK on the before/after. A nudge, never a gate (ADR 0162).
742
+ // human's OK on the before/after. A nudge, never a gate: gating on that OK would
743
+ // put a person's approval in the land path, which ADR 0162 retired.
743
744
  try {
744
745
  const nudge = visualReviewNudge(task);
745
746
  if (nudge) console.log(`\n${nudge.join('\n')}`);
@@ -2,7 +2,7 @@
2
2
  // Subagent grader smoke test — GDS-V3 task V3.R18 (#209) → MAS Phase 1 #395.
3
3
  //
4
4
  // Runs two synthetic ship scenarios end-to-end through the v3 multi-shot
5
- // subagent grader (DEFAULT_SHOTS × cross-family model, unanimous-PASS gate).
5
+ // subagent grader (the worker panel, cross-family model, unanimous-PASS gate).
6
6
  // Asserts:
7
7
  //
8
8
  // - Known-GOOD ship: a tiny, complete change → grade passes (unanimous
@@ -148,8 +148,8 @@ function summarize(result, label) {
148
148
  // -------- Main --------
149
149
 
150
150
  (async () => {
151
- console.log(`Running subagent grader smoke (v${grader.VERSION}, model=${grader.DEFAULT_GRADER_MODEL}, ${grader.DEFAULT_SHOTS}× shots, threshold=${grader.THRESHOLD})`);
152
- console.log('Two scenarios will run sequentially. Each spawns 3 parallel shots.');
151
+ console.log(`Running subagent grader smoke (v${grader.VERSION}, model=${grader.DEFAULT_GRADER_MODEL}, ${grader.WORKERS.DEFAULT_PANEL.length}-worker panel, threshold=${grader.THRESHOLD})`);
152
+ console.log(`Two scenarios will run sequentially. Each runs the ${grader.WORKERS.DEFAULT_PANEL.length}-worker panel.`);
153
153
 
154
154
  let assertFailures = 0;
155
155
 
@@ -7,7 +7,7 @@
7
7
  // count that still ran past its card (task 1003480) and a second grid child landing on the first (task 1003078)
8
8
  // both shipped looking correct in code and were found only by opening a browser.
9
9
  //
10
- // A SIGNAL, never a gate (ADR 0162): /ship-check shows it as an advisory row; ship.js and the grader never
10
+ // A SIGNAL, never a gate (ADR 0329, decisions 3-4): /ship-check shows it as an advisory row; ship.js and the grader never
11
11
  // read it. The measurement is the kit's `text-fit` + `sibling-overlap` floors (modules/ui-design/kit/lib.js);
12
12
  // this file only picks the pages, runs the kit's renderer at 1280 + 390, and keeps those two floors.
13
13
  //
@@ -25,7 +25,7 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
25
25
 
26
26
  // Each step: a label, the argv to run (node scripts run with THIS node), the command that heals it,
27
27
  // and how to read a non-zero exit. `optional` steps are skipped when their script is absent. An `advisory`
28
- // step is a signal, never a gate (ADR 0162): a non-zero exit reads `warn` (3 = no browser reads `skipped`).
28
+ // step is a signal, never a gate (ADR 0329, decision 3): a non-zero exit reads `warn` (3 = no browser reads `skipped`).
29
29
  // `onlyWith` names the flag that adds a step; `skipWith` the flag that leaves one out.
30
30
  const STEPS = [
31
31
  { key: 'fitness', label: 'fitness.js (architecture + budgets + freshness gate)', argv: ['scripts/gds/fitness.js'], fix: 'read the ✗ lines above; each names its fix' },
@@ -496,7 +496,7 @@ async function shipMain() {
496
496
  passed,
497
497
  // touches[] cleanup [4/8] (task 879): the drift scanner is gone; with no
498
498
  // scanner there is nothing to drift, so this audit-trail signal
499
- // (grader.scoreScannerSmoke) is vacuously clean.
499
+ // (read by the retired v1 grader, task 1002842) is vacuously clean.
500
500
  scanner_clean: true,
501
501
  smoke_clean: smoke.clean,
502
502
  },
@@ -314,7 +314,7 @@ function formatOutageDiagnostic(g, cliFinding) {
314
314
 
315
315
  // cliFinding: the preflight `claude CLI` row, injected by printGradeOutcome so a
316
316
  // dark panel can say "your CLI is logged out" instead of leaving the builder to
317
- // guess. Report-only — it never changes the outcome (ADR 0162: no new gates).
317
+ // guess. Report-only — it never changes the outcome: it explains an outage, it does not decide one.
318
318
  function formatGradeOutcome(g, taskId, cliFinding = null) {
319
319
  const out = [];
320
320
  // #943: a TOTAL grader outage (every gating worker failed to spawn / the
@@ -466,7 +466,7 @@ function printGradeOutcome(g, taskId) {
466
466
  // task 1003313: on a TOTAL outage only, ask the existing preflight whether the
467
467
  // local `claude` CLI is launchable and logged in — the two faults that produce
468
468
  // this exact all-workers-dead signature. Report-only and outage-only, so the
469
- // happy path pays nothing and no ship is newly gated (ADR 0162).
469
+ // happy path pays nothing and no ship's outcome depends on the probe.
470
470
  let cliFinding = null;
471
471
  if (g?.signals?.panel_outcome === 'unavailable') {
472
472
  try {
@@ -5,7 +5,8 @@
5
5
  // real browser, and get the human's OK on the before/after before shipping — the
6
6
  // project default for every builder since the owner's 2026-09-23 decision.
7
7
  //
8
- // A NUDGE, never a gate (ADR 0162). "Is this UI work" is the ONE predicate the
8
+ // A NUDGE, never a gate: as a gate, the human's OK would be a person's approval in
9
+ // the land path, which ADR 0162 retired. "Is this UI work" is the ONE predicate the
9
10
  // artist review and the grader's copy lens already use (lifecycle/cascade.js), so
10
11
  // the three cannot disagree. Nothing has changed yet at claim time, so it is fed
11
12
  // the paths the task NAMES: its touches[] and any modules/*/public/ path written
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.1036'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1037'; // 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,167 @@
1
+ // tests/adr_0162_gate_citation.mjs — "no new gates" is not what ADR 0162 decided; no surface
2
+ // may cite 0162 as the reason something is a signal and not a gate (task 1004280).
3
+ //
4
+ // WHAT WENT WRONG. Task 1003573 corrected one slogan cited to ADR 0162 ("verdicts are advisory,
5
+ // never self-executing" — ADR 0158 §2's rule; tests/adr_0162_citation.mjs pins that). A sibling
6
+ // slogan rode along: "no new gates (ADR 0162)", "a signal, never a gate (ADR 0162)", "NOT A
7
+ // PIPELINE GATE (ADR 0162)". It is broader than the ADR. 0162 retired REVIEW gates — a gate that
8
+ // needs a particular person's approval before work can land — and its own table keeps four
9
+ // automatic controls, three of which fail CLOSED (the grader among them). ADR 0241 ("What ADR 0162
10
+ // actually forbids") states the test 0162 leaves behind: an automatic, state-read, rank-consistent
11
+ // gate with a named escape hatch is admissible. So "this is not a gate because 0162" cites a rule
12
+ // 0162 does not make, and it hides the surface's real reason for staying advisory.
13
+ //
14
+ // The surfaces keep their choice (most were right to stay advisory); only the reason changed:
15
+ // 0162 for "no human approval in the land path", and the surface's own reason otherwise.
16
+ //
17
+ // WHAT THIS PINS. (1) The premise, read from the ADRs: 0162 keeps fail-closed automatic controls
18
+ // and never says "no new gates"; 0241 says what 0162 forbids. (2) No tracked surface cites 0162
19
+ // with the gate shorthand, in any of the shapes the 2026-09-25 sweep found.
20
+ //
21
+ // Run: node --test tests/adr_0162_gate_citation.mjs
22
+
23
+ import { test } from 'node:test';
24
+ import assert from 'node:assert/strict';
25
+ import { readFileSync } from 'node:fs';
26
+ import { spawnSync } from 'node:child_process';
27
+ import path from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const read = (rel) => readFileSync(path.join(ROOT, rel), 'utf8');
32
+
33
+ const ADR_0162 = 'docs/adr/0162-review-gates-retired-rank-consistent-ci-only.md';
34
+ const ADR_0241 = 'docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md';
35
+
36
+ // A match is a line that gives 0162 as the reason something is not (or must not be) a gate.
37
+ const GATE_SHORTHANDS = [
38
+ // The advisory claim, then 0162 as its source: "no new gates (ADR 0162)", "A SIGNAL, never a
39
+ // gate (ADR 0162)", "NOT A PIPELINE GATE (ADR 0162)", "no ship is newly gated (ADR 0162)",
40
+ // "WITHOUT adding a gate (ADR 0162)", "It gates NOTHING (ADR 0162 / ADR 0241)", "a new gate on
41
+ // the ship path (ADR 0162)", "It had to stay a signal (ADR 0162)", "never flips its exit (ADR 0162)".
42
+ /\b(?:no new gates?|never a gate|not a (?:pipeline )?gate|newly gated|adding a gate|gates nothing|new gate on the ship path|stay a signal|never flips its exit)\b[^\n()]{0,40}\(ADR 0162\b/i,
43
+ // 0162, then the shorthand as its gloss: "ADR 0162 — no new gates", "(ADR 0162: no new gates)",
44
+ // "[ADR 0162](…) (no new gates)".
45
+ /\b0162\b[\])]?\s*(?:\(|—|–|:)\s*no new gates?\b/i,
46
+ // A describe-only posture cited to 0162: "(describe, never confirm/flip — ADR 0162)". That is
47
+ // 0158 §2's rule — the sibling test's subject, in a shape it does not match.
48
+ /\bnever confirm\/flip\s*[—–-]\s*ADR 0162\b/i,
49
+ ];
50
+
51
+ // Deliberately NOT corrected, each with its reason. Shrink-only: when one is fixed, delete its
52
+ // row — the last test fails until you do, so a stale exemption cannot outlive what it excused.
53
+ const KNOWN_DEFERRED = new Map([
54
+ // HARD-FLOOR surfaces (scripts/gds/gate-review.js HARD_FLOOR_GLOBS lists `.claude/hooks/`):
55
+ // even a comment-only edit escalates to the owner, which a citation fix does not justify on
56
+ // its own. Correct them with the next real edit (the redteam-patrol.yml precedent, task 1003573).
57
+ ['.claude/hooks/allow-rule-guard.js', 'hard-floor gate surface'],
58
+ ['.claude/hooks/git-guard.js', 'hard-floor gate surface'],
59
+ // A hall PAGE script: any edit under modules/*/public/ is UI work, so its ship files an artist
60
+ // review that holds the deploy (ADR 0241). A comment fix does not justify that; correct it
61
+ // with the next real edit to the page.
62
+ ['modules/hall-ui/public/studio.js', 'page script — an edit files an artist review'],
63
+ ]);
64
+
65
+ const HISTORICAL = /^docs\/session-logs\//;
66
+ // This file and its sibling quote the shapes they hunt, as examples.
67
+ const SELF = new Set(['tests/adr_0162_gate_citation.mjs', 'tests/adr_0162_citation.mjs']);
68
+ const TEXT = /\.(?:md|js|mjs|cjs|json|ya?ml|sql|sh|html|txt)$/;
69
+ // A line that NAMES the slogan as its subject is discussing it, not citing by it.
70
+ const DISCUSSES_SLOGAN = /paraphrase|shorthand/i;
71
+ // Markdown link targets carry the ADR's filename, which would sit between a citation and its gloss.
72
+ const stripLinkTargets = (s) => s.replace(/\]\([^)\s]*\)/g, ']');
73
+ const continuation = (s) => (s || '').replace(/^\s*(?:\/\/+|#+|--|\*|>|-)?\s*/, '');
74
+
75
+ function offendersIn(rel, text) {
76
+ const lines = stripLinkTargets(text).split('\n');
77
+ const hits = [];
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const here = lines[i];
80
+ if (DISCUSSES_SLOGAN.test(here)) continue;
81
+ // Join the next line so a citation wrapped mid-phrase still reads as one; only a match
82
+ // that STARTS on this line is this line's.
83
+ const window = `${here} ${continuation(lines[i + 1])}`;
84
+ if (!/0162/.test(window)) continue;
85
+ for (const re of GATE_SHORTHANDS) {
86
+ const m = re.exec(window);
87
+ if (m && m.index < here.length) { hits.push(`${rel}:${i + 1}: ${here.trim().slice(0, 140)}`); break; }
88
+ }
89
+ }
90
+ return hits;
91
+ }
92
+
93
+ function trackedTextFiles() {
94
+ const r = spawnSync('git', ['ls-files', '-z'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
95
+ if (r.status !== 0) return null; // a packaged core is not a git checkout
96
+ return r.stdout.split('\0').filter((f) => f && TEXT.test(f));
97
+ }
98
+
99
+ test('the premise: ADR 0162 keeps automatic gates that fail closed, and never says "no new gates"', () => {
100
+ const adr0162 = read(ADR_0162);
101
+ assert.match(adr0162, /Review requirements on `main` are \*\*retired permanently\*\*/, 'what 0162 retired');
102
+ const failsClosed = adr0162.split('\n').filter((l) => /^\|.*\|\s*closed\s*\|\s*$/.test(l));
103
+ assert.ok(failsClosed.length >= 3,
104
+ `0162's compensating-controls table should keep >=3 controls that fail CLOSED (found ${failsClosed.length}) — `
105
+ + 'if the ADR changed, re-ask whether "no new gates" became its rule before re-pointing these citations');
106
+ assert.doesNotMatch(adr0162, /no new gates?/i, 'ADR 0162 never says "no new gates"');
107
+ assert.match(read(ADR_0241), /^## What ADR 0162 actually forbids$/m, 'ADR 0241 is where the narrower rule is written');
108
+ });
109
+
110
+ test('the detector recognises every shape the sweep corrected, and passes the true citations', () => {
111
+ const flag = (line) => offendersIn('probe.md', line).length > 0;
112
+ for (const bad of [
113
+ '- **ADR 0162 — no new gates.** This skill describes and recovers; it never inserts a human-approval step',
114
+ '// A SIGNAL, never a gate (ADR 0162): /ship-check shows it as an advisory row; ship.js and the grader never',
115
+ '// step is a signal, never a gate (ADR 0162): a non-zero exit reads `warn` (3 = no browser reads `skipped`).',
116
+ '// A NUDGE, never a gate (ADR 0162). "Is this UI work" is the ONE predicate the',
117
+ ' // human\'s OK on the before/after. A nudge, never a gate (ADR 0162).',
118
+ '// guess. Report-only — it never changes the outcome (ADR 0162: no new gates).',
119
+ ' // happy path pays nothing and no ship is newly gated (ADR 0162).',
120
+ '- **Relates to:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (no new gates), [ADR 0197](0197-the-ui-design-module.md) (the kit)',
121
+ 'or both. It had to stay a signal (ADR 0162).',
122
+ '- **Block the fourth attempt.** That is a new gate on the ship path (ADR 0162), and a wrong',
123
+ 'shows them as an ADVISORY row that never flips its exit (ADR 0162). Not a grader worker',
124
+ '// NOT A PIPELINE GATE (ADR 0162): it gates no merge, no CI check, no ship.',
125
+ '// It gates NOTHING (ADR 0162 / ADR 0241): nothing here holds a ship or a deploy.',
126
+ '// pass and WITHOUT adding a gate (ADR 0162). Every case below that has a low',
127
+ '// read-only (describe, never confirm/flip — ADR 0162).',
128
+ ]) assert.ok(flag(bad), `should flag: ${bad}`);
129
+
130
+ for (const good of [
131
+ '- **Related:** ADR 0162 (no new human gates on the ship path)',
132
+ 'No review requirement — gates are CI checks, not human approvals (ADR 0162).',
133
+ '- **Relates to:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (review gates retired: no human approval in the land path)',
134
+ 'no confirms, no status flips, and no new approval step on anyone\'s ship (ADR 0162). The remediation lines',
135
+ '// A NUDGE, never a gate: as a gate, the human\'s OK would be a person\'s approval in\n// the land path, which ADR 0162 retired.',
136
+ '// A SIGNAL, never a gate (ADR 0329, decisions 3-4): /ship-check shows it as an advisory row',
137
+ 'It never holds a builder\'s ship, which is what keeps it compatible with ADR 0162.',
138
+ 'This matters, because the shorthand "no new gates (ADR 0162)" is broader than the ADR',
139
+ ]) assert.ok(!flag(good), `should pass: ${good}`);
140
+ });
141
+
142
+ test('no tracked surface cites ADR 0162 as the reason something is not a gate', (t) => {
143
+ const files = trackedTextFiles();
144
+ if (!files) return t.skip('not a git checkout — nothing tracked to scan');
145
+ const offenders = [];
146
+ for (const rel of files) {
147
+ if (SELF.has(rel) || HISTORICAL.test(rel) || KNOWN_DEFERRED.has(rel)) continue;
148
+ let text;
149
+ try { text = readFileSync(path.join(ROOT, rel), 'utf8'); } catch { continue; }
150
+ if (!text.includes('0162')) continue;
151
+ offenders.push(...offendersIn(rel, text));
152
+ }
153
+ assert.deepEqual(offenders, [],
154
+ 'these cite ADR 0162 for "no new gates" / "a signal, never a gate". 0162 retired gates that need a '
155
+ + 'particular person\'s approval in the land path, and keeps automatic gates that fail closed (ADR 0241). '
156
+ + 'Cite 0162 for "no human approval in the land path", and name the surface\'s own reason for staying advisory.');
157
+ });
158
+
159
+ test('every deferred exemption still has something to excuse (shrink-only)', (t) => {
160
+ if (!trackedTextFiles()) return t.skip('not a git checkout');
161
+ for (const [rel, why] of KNOWN_DEFERRED) {
162
+ let text = '';
163
+ try { text = read(rel); } catch { /* deleted: the exemption is stale too */ }
164
+ assert.ok(offendersIn(rel, text).length > 0,
165
+ `${rel} (${why}) no longer cites 0162 for the gate shorthand — delete its KNOWN_DEFERRED row`);
166
+ }
167
+ });