@bongos/core 1.21.176 → 1.21.178

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 (214) hide show
  1. package/.bongos-core.json +186 -496
  2. package/.claude/skills/planning-session/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +0 -6
  5. package/clients/bongos-client/index.cjs +0 -6
  6. package/clients/bongos-client/index.d.ts +0 -6
  7. package/clients/bongos-client/index.mjs +0 -6
  8. package/config/scheduled-routines.json +3 -8
  9. package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +3 -2
  10. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
  11. package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
  12. package/docs/api/openapi.json +1 -87
  13. package/docs/api-reference.md +1 -8
  14. package/docs/canonical-permissions.md +4 -4
  15. package/docs/copy-inventory.md +25 -39
  16. package/docs/copy-registry.json +73 -200
  17. package/docs/file-map.md +6 -11
  18. package/docs/handoff-template.md +1 -1
  19. package/docs/module-api-changelog.md +4 -0
  20. package/docs/modules-contract.md +2 -2
  21. package/docs/onboarding/diagrams/README.md +3 -7
  22. package/docs/onboarding/diagrams/assertions.json +3 -3
  23. package/docs/page-inventory.json +4 -7
  24. package/docs/page-readings.json +1520 -1569
  25. package/docs/recipes/ops-gotchas.md +3 -1
  26. package/modules/autonomy/db.js +9 -0
  27. package/modules/autonomy/module.json +2 -2
  28. package/modules/autonomy/pollers/daily-checks.js +262 -0
  29. package/modules/autonomy/pollers/routine-timers.js +7 -5
  30. package/modules/autonomy/rank-roles.js +64 -0
  31. package/modules/autonomy/server-schedule.js +37 -0
  32. package/modules/government/catalog.js +10 -7
  33. package/modules/government/decision-board.js +0 -1
  34. package/modules/government/decision-rules.js +0 -1
  35. package/modules/government/docket.js +0 -1
  36. package/modules/government/protected-surfaces.js +1 -1
  37. package/modules/government/protected-surfaces.json +2 -3
  38. package/modules/hall-ui/public/atlas.html +1 -1
  39. package/modules/hall-ui/public/blockers.html +1 -1
  40. package/modules/hall-ui/public/board-room.html +1 -1
  41. package/modules/hall-ui/public/city.html +1 -1
  42. package/modules/hall-ui/public/collab.html +1 -1
  43. package/modules/hall-ui/public/commands.html +1 -1
  44. package/modules/hall-ui/public/copy-desk.html +1 -1
  45. package/modules/hall-ui/public/deploy.html +1 -1
  46. package/modules/hall-ui/public/diagrams.html +1 -1
  47. package/modules/hall-ui/public/drachmae.html +1 -1
  48. package/modules/hall-ui/public/fleet.html +1 -1
  49. package/modules/hall-ui/public/gate.css +4 -26
  50. package/modules/hall-ui/public/gate.html +12 -27
  51. package/modules/hall-ui/public/gate.js +26 -176
  52. package/modules/hall-ui/public/gate.states.json +4 -22
  53. package/modules/hall-ui/public/goal-map.html +1 -1
  54. package/modules/hall-ui/public/goals.html +1 -1
  55. package/modules/hall-ui/public/government.html +1 -1
  56. package/modules/hall-ui/public/idea.html +1 -1
  57. package/modules/hall-ui/public/ideas.html +1 -1
  58. package/modules/hall-ui/public/index.html +1 -1
  59. package/modules/hall-ui/public/modules.html +1 -1
  60. package/modules/hall-ui/public/my-agents.html +1 -1
  61. package/modules/hall-ui/public/primer.html +1 -1
  62. package/modules/hall-ui/public/profile.html +1 -1
  63. package/modules/hall-ui/public/project-settings.html +1 -1
  64. package/modules/hall-ui/public/ranks.html +1 -1
  65. package/modules/hall-ui/public/roadmap.html +1 -1
  66. package/modules/hall-ui/public/roster.html +1 -1
  67. package/modules/hall-ui/public/sessions.html +1 -1
  68. package/modules/hall-ui/public/settings.html +1 -1
  69. package/modules/hall-ui/public/shell.js +1 -1
  70. package/modules/hall-ui/public/street-card.js +5 -9
  71. package/modules/hall-ui/public/street-desk.js +2 -2
  72. package/modules/hall-ui/public/street-inside.js +1 -1
  73. package/modules/hall-ui/public/street.html +1 -1
  74. package/modules/hall-ui/public/studio.html +1 -1
  75. package/modules/hall-ui/public/task.html +1 -1
  76. package/modules/hall-ui/public/thinking.html +1 -1
  77. package/modules/hall-ui/public/tweak-editor.html +1 -1
  78. package/modules/hall-ui/public/watch.html +1 -1
  79. package/modules/hall-ui/public/work.html +1 -1
  80. package/modules/lifecycle/matter-acts.js +0 -6
  81. package/modules/lifecycle/module.json +0 -1
  82. package/package-lock.json +2 -2
  83. package/package.json +1 -1
  84. package/release-notes.json +36 -0
  85. package/scripts/bongos/adopt-preflight.js +3 -3
  86. package/scripts/bongos/api-alias-caller-scan.js +2 -2
  87. package/scripts/bongos/audit-authorship.sh +1 -8
  88. package/scripts/bongos/audit-rank-roles.js +15 -56
  89. package/scripts/bongos/bump-version.js +46 -9
  90. package/scripts/bongos/codemod-rename-scripts-gds.js +0 -1
  91. package/scripts/bongos/diagram-facts.js +1 -1
  92. package/scripts/bongos/do-api.js +1 -1
  93. package/scripts/bongos/doc-cli-guard.js +2 -0
  94. package/scripts/bongos/doc-control-claims.js +2 -2
  95. package/scripts/bongos/fitness-checks-identity.js +4 -15
  96. package/scripts/bongos/gen-diagrams.js +3 -3
  97. package/scripts/bongos/government-walk.js +3 -9
  98. package/scripts/bongos/hard-floor.js +52 -0
  99. package/scripts/bongos/publish-manifest.js +3 -3
  100. package/scripts/bongos/pulse.js +5 -3
  101. package/scripts/bongos/routine-schedule.js +38 -3
  102. package/scripts/bongos/run-unit-tests.js +17 -5
  103. package/scripts/bongos/ship-regen.js +2 -3
  104. package/scripts/gds/{goal-names.js → hard-floor.js} +3 -4
  105. package/scripts/security/dep-audit.js +50 -124
  106. package/src/bongos/module-scope-map.js +2 -2
  107. package/src/bongos/permission-path-check.js +1 -1
  108. package/src/bongos/route-rank-check.js +1 -8
  109. package/src/bongos/routes.js +1 -2
  110. package/src/module-api.js +1 -1
  111. package/src/modules.js +1 -1
  112. package/tests/acclaim_feed_evidence_db.mjs +42 -23
  113. package/tests/adr_0089_scope.mjs +7 -5
  114. package/tests/autonomy_daily_checks.mjs +201 -0
  115. package/tests/bump_version.mjs +52 -18
  116. package/tests/claude_materialize.mjs +12 -12
  117. package/tests/copy_desk_flags_db.mjs +22 -14
  118. package/tests/effective_visibility_predicate.mjs +0 -18
  119. package/tests/gating_review_retired.mjs +1 -1
  120. package/tests/government_act_alone_buildings.mjs +1 -3
  121. package/tests/government_board_membership_db.mjs +35 -22
  122. package/tests/government_protected_surfaces.mjs +29 -13
  123. package/tests/governor_docket.mjs +0 -2
  124. package/tests/governor_matter_acts.mjs +6 -8
  125. package/tests/hall_city.mjs +1 -1
  126. package/tests/hall_matter_card.mjs +2 -2
  127. package/tests/hall_oversight_world.mjs +16 -12
  128. package/tests/hall_street_desk.mjs +4 -3
  129. package/tests/hall_street_inside.mjs +1 -1
  130. package/tests/helpers.mjs +34 -0
  131. package/tests/howto_state.mjs +33 -56
  132. package/tests/howto_state_db.mjs +68 -0
  133. package/tests/idea_credit_streams_db.mjs +11 -3
  134. package/tests/integration_parked_ratchet.mjs +7 -1
  135. package/tests/nav_permission_atoms.mjs +1 -1
  136. package/tests/page_notes_db.mjs +12 -8
  137. package/tests/page_tweak_no_rearm.mjs +50 -0
  138. package/tests/publish_manifest.mjs +64 -0
  139. package/tests/routine_capture_kind.mjs +18 -7
  140. package/tests/routine_schedule.mjs +42 -0
  141. package/tests/search_isolation.mjs +5 -103
  142. package/tests/search_isolation_db.mjs +140 -0
  143. package/tests/session_earnings_mirror_db.mjs +2 -0
  144. package/tests/workflow_exec_paths.mjs +9 -11
  145. package/modules/lifecycle/routes/gate-approvals.js +0 -299
  146. package/scripts/bongos/artifact-staleness.js +0 -267
  147. package/scripts/bongos/ci-grade.js +0 -82
  148. package/scripts/bongos/classify-discipline.js +0 -153
  149. package/scripts/bongos/codemod-rename-src-gds.js +0 -145
  150. package/scripts/bongos/coreB-carve-proof.sh +0 -725
  151. package/scripts/bongos/gate-review.js +0 -330
  152. package/scripts/bongos/goal-names.js +0 -102
  153. package/scripts/bongos/rename-goals.js +0 -109
  154. package/scripts/bongos/seed-adopt-tasks.js +0 -75
  155. package/scripts/bongos/seed-api-contract-goal-tasks.js +0 -141
  156. package/scripts/bongos/seed-board-room-tasks.js +0 -196
  157. package/scripts/bongos/seed-bongos-app-tasks.js +0 -294
  158. package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +0 -81
  159. package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +0 -100
  160. package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +0 -307
  161. package/scripts/bongos/seed-bongos-module-system-tasks.js +0 -346
  162. package/scripts/bongos/seed-bongos-tasks.js +0 -214
  163. package/scripts/bongos/seed-bongos-v1-seo-tasks.js +0 -104
  164. package/scripts/bongos/seed-bv1-community-tasks.js +0 -131
  165. package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +0 -133
  166. package/scripts/bongos/seed-bv1-profile-page-tasks.js +0 -161
  167. package/scripts/bongos/seed-bv1-r01-delta-tasks.js +0 -154
  168. package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +0 -277
  169. package/scripts/bongos/seed-gds-v4-tasks.js +0 -624
  170. package/scripts/bongos/seed-governance-tasks.js +0 -340
  171. package/scripts/bongos/seed-medusa-tasks.js +0 -440
  172. package/scripts/bongos/seed-module-upstreaming.js +0 -219
  173. package/scripts/bongos/seed-productization-tasks.js +0 -235
  174. package/scripts/bongos/seed-project-admin-console-tasks.js +0 -88
  175. package/scripts/bongos/seed-provisioning-tasks.js +0 -83
  176. package/scripts/bongos/seed-strict-versioning-tasks.js +0 -275
  177. package/scripts/bongos/seed-v2-world-tasks.js +0 -283
  178. package/scripts/gds/artifact-staleness.js +0 -7
  179. package/scripts/gds/ci-grade.js +0 -7
  180. package/scripts/gds/classify-discipline.js +0 -7
  181. package/scripts/gds/codemod-rename-src-gds.js +0 -7
  182. package/scripts/gds/coreB-carve-proof.sh +0 -8
  183. package/scripts/gds/gate-review.js +0 -7
  184. package/scripts/gds/rename-goals.js +0 -7
  185. package/scripts/gds/seed-adopt-tasks.js +0 -7
  186. package/scripts/gds/seed-api-contract-goal-tasks.js +0 -7
  187. package/scripts/gds/seed-board-room-tasks.js +0 -6
  188. package/scripts/gds/seed-bongos-app-tasks.js +0 -7
  189. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -7
  190. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -7
  191. package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -7
  192. package/scripts/gds/seed-bongos-module-system-tasks.js +0 -7
  193. package/scripts/gds/seed-bongos-tasks.js +0 -7
  194. package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -6
  195. package/scripts/gds/seed-bv1-community-tasks.js +0 -6
  196. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -6
  197. package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -6
  198. package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -6
  199. package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -6
  200. package/scripts/gds/seed-gds-v4-tasks.js +0 -7
  201. package/scripts/gds/seed-governance-tasks.js +0 -7
  202. package/scripts/gds/seed-medusa-tasks.js +0 -7
  203. package/scripts/gds/seed-module-upstreaming.js +0 -7
  204. package/scripts/gds/seed-productization-tasks.js +0 -7
  205. package/scripts/gds/seed-project-admin-console-tasks.js +0 -6
  206. package/scripts/gds/seed-provisioning-tasks.js +0 -7
  207. package/scripts/gds/seed-strict-versioning-tasks.js +0 -7
  208. package/scripts/gds/seed-v2-world-tasks.js +0 -7
  209. package/scripts/migrate_cost_ledger.sh +0 -178
  210. package/tests/artifact_staleness.mjs +0 -207
  211. package/tests/gate_approvals.mjs +0 -611
  212. package/tests/gate_review.mjs +0 -152
  213. package/tests/goal_names.mjs +0 -120
  214. package/tests/newcomer_generator.mjs +0 -48
@@ -1,299 +0,0 @@
1
- // src/bongos/routes/gate-approvals.js — Archon gate-surface PR approval from the hall
2
- // (task 1179, ADR 0058 amendment).
3
- //
4
- // WHY. The gate-review CI check (ADR 0058) clears an ESCALATE (hard floor) ONLY on
5
- // an owner approval, and a NEEDS_TRUST without a Metic+ author likewise blocks until
6
- // the owner approves. Historically that owner approval was a GitHub APPROVED review
7
- // by the repo owner on the head sha — i.e. the owner had to leave a GitHub/terminal review.
8
- // This router gives Archons a browser-only equivalent (feedback: auth flows must be
9
- // UI-first), trust-consistent with the existing server-set `gate-author-trust`
10
- // status: the server re-checks the caller's Archon rank server-side (ADR 0016) and
11
- // sets a `gate-owner-approved` commit status that the gate-review workflow honors as
12
- // owner approval. A PR author cannot forge it (only the server holds the credential).
13
- //
14
- // GET /gate-approvals — open PRs whose gate-review check is red,
15
- // with the verdict + per-file reasons (Archon)
16
- // POST /gate-approvals/:pr/approve — record owner approval + re-run the check (Archon)
17
- //
18
- // The list's "is this awaiting approval" signal is the LIVE gate-review check
19
- // conclusion (authoritative), not a re-derivation; classifyChanges() is reused only
20
- // to render WHY each PR escalated, so the hall shows exactly the CI reasoning.
21
-
22
- const express = require('express');
23
- // Intra-module + the published doorway — a module never requires a core internal.
24
- const api = require('../../../src/module-api');
25
- const auth = api;
26
- const githubPush = require('../github-push');
27
- // The page-tweak hold (task 1004323 / BV2.TW12, ADR 0341 D7): the approve below
28
- // re-arms auto-merge on the reopened PR, and a page tweak held for its artist must
29
- // never be armed from here. Called through the module object so tests can stub it.
30
- const pageTweakReads = require('../page-tweak-reads');
31
-
32
- // Would POST /gate-approvals/:pr/approve let this viewer through? The same two
33
- // checks it mounts, read for the list (task 1004829): the hall draws a held PR as
34
- // an ask card only on true, so a Metic who may read the gate never sees an Approve
35
- // the route would refuse. Fail-closed like requirePermission: no government port,
36
- // or one that throws, is false; an absent act-alone check passes, as the
37
- // requireActAlone gate does.
38
- async function viewerMayApprove(builderId) {
39
- try {
40
- const government = api.resolveOptional('government');
41
- if (!government || typeof government.builderHasPermissions !== 'function') return false;
42
- if (!(await government.builderHasPermissions(Number(builderId), ['gate_approval.decide']))) return false;
43
- if (typeof government.checkActAlone !== 'function') return true;
44
- const out = await government.checkActAlone('release', builderId);
45
- return !!(out && out.ok === true);
46
- } catch {
47
- // authority that cannot be established is a denial (ADR 0016); the list still answers
48
- return false;
49
- }
50
- }
51
- const { HELD_NOT_ARMED } = require('../page-tweak-hold');
52
- const { parseId } = api;
53
- // The SAME pure classifier the CI gate runs — reused (never duplicated) so the hall
54
- // shows identical verdict + reasons. Safe to require: gate-review.js guards its CLI
55
- // entrypoint behind `require.main === module`, so importing it runs no side effects.
56
- const { classifyChanges, resolveAction } = require('../../../scripts/bongos/gate-review');
57
-
58
- // Pull a GDS task id out of a server-published PR's title ("task 1163: …") or body
59
- // ("… for task 1163."). Returns a number or null. Display-only.
60
- function taskIdFromPr(pr) {
61
- const hay = `${pr.title || ''}\n${pr.body || ''}`;
62
- const m = /\btask\s+(\d{1,7})\b/i.exec(hay);
63
- return m ? Number(m[1]) : null;
64
- }
65
-
66
- // Find a named check/status in the head-commit rollup (CheckRun.name or
67
- // StatusContext.context — listOpenPullRequests flattens both to `name`).
68
- function findCheck(pr, name) {
69
- return (pr.checks || []).find((c) => c.name === name) || null;
70
- }
71
- const isSuccess = (c) => c && String(c.conclusion || '').toUpperCase() === 'SUCCESS';
72
- const isFailure = (c) => c && String(c.conclusion || '').toUpperCase() === 'FAILURE';
73
-
74
- // Run the pure classifier over a PR's changed files to produce the display verdict +
75
- // reasons. blobAt returns null (no file content fetched): the path-level hard-floor /
76
- // sensitive-surface rules — which produce the human reasons — do not need content;
77
- // the content-only AUTO-narrowing certifications fail-closed to a stricter verdict,
78
- // which is the safe direction for a read-only "why is this stuck" view.
79
- function summarizeGate(files) {
80
- const r = classifyChanges({ files, blobAt: () => null, diffBytes: 0 });
81
- const reasons = [
82
- ...(r.escalations || []).map((e) => ({ path: e.path, reason: e.reason, hard_floor: true })),
83
- ...(r.needsTrust || []).map((e) => ({ path: e.path, reason: e.reason, hard_floor: false })),
84
- ];
85
- return { verdict: r.verdict, reasons, surfaces: r.surfaces || [] };
86
- }
87
-
88
- // The awaiting list itself: open, non-draft PRs whose gate-review check is
89
- // COMPLETED and red, each with its verdict and reasons. Shared by GET
90
- // /gate-approvals and the governor's docket (task 1004523), so the two can never
91
- // disagree about what is waiting. `withFiles: false` skips the per-PR file read
92
- // (the docket needs only the count and the titles, not the per-file reasons).
93
- async function listAwaitingGateApprovals({ withFiles = true } = {}) {
94
- const prs = await githubPush.listOpenPullRequests({});
95
- const awaiting = [];
96
- let checking = 0;
97
- for (const pr of prs) {
98
- if (pr.is_draft) continue;
99
- const gate = findCheck(pr, 'gate-review');
100
- if (!gate) continue; // not a gate-reviewed PR (or it hasn't run yet)
101
- if (String(gate.status || '').toUpperCase() !== 'COMPLETED') { checking += 1; continue; }
102
- if (!isFailure(gate)) continue; // green/neutral → not awaiting
103
- // gate-review is red → blocked awaiting trust/owner approval. Render WHY.
104
- let summary = { verdict: 'unknown', reasons: [], surfaces: [], files: [] };
105
- if (withFiles) {
106
- try {
107
- const files = await githubPush.getPullRequestFiles({ number: pr.number });
108
- summary = summarizeGate(files);
109
- summary.files = files.map((f) => ({ path: f.path, status: f.status }));
110
- } catch (_) {
111
- summary.files = [];
112
- }
113
- }
114
- awaiting.push({
115
- number: pr.number,
116
- title: pr.title,
117
- url: pr.url,
118
- task: taskIdFromPr(pr),
119
- author: pr.author,
120
- head_sha: pr.head_sha,
121
- verdict: summary.verdict,
122
- reasons: summary.reasons,
123
- files: summary.files,
124
- author_trusted: isSuccess(findCheck(pr, 'gate-author-trust')),
125
- already_approved: isSuccess(findCheck(pr, githubPush.GATE_OWNER_APPROVED_CONTEXT)),
126
- gate_review_url: gate.url || null,
127
- });
128
- }
129
- return { prs, awaiting, checking };
130
- }
131
-
132
- // The governor's docket (task 1004523): a PR held at the gate stands at Release,
133
- // shown only to a viewer holding `gate_approval.read` (this list route's own atom)
134
- // and needing only a viewer who may clear it (`gate_approval.decide`). An
135
- // unconfigured server credential is a read error, so the docket reports the
136
- // source `unavailable` rather than an empty, falsely calm Release.
137
- function contributeGateApprovalsToDocket() {
138
- api.contribute('governor.docket', {
139
- source: 'gate_approvals',
140
- building: 'release',
141
- gate: { permission: 'gate_approval.read' },
142
- read: async (ctx) => {
143
- if (!githubPush.isConfigured()) throw new Error('publish_unconfigured');
144
- const { awaiting } = await listAwaitingGateApprovals({ withFiles: false });
145
- const mayDecide = ctx.has('gate_approval.decide');
146
- return awaiting.filter((pr) => !pr.already_approved).map((pr) => ({
147
- id: pr.number,
148
- rule: 'decide',
149
- state: mayDecide ? 'needs' : 'moving',
150
- title: pr.title || `Pull request ${pr.number}`,
151
- short: 'held at the gate',
152
- where: 'release · held at the gate',
153
- opened_at: null,
154
- deadline: null,
155
- proposer: pr.author ? { login: pr.author } : null,
156
- tally: null,
157
- href: '/gate',
158
- ref: { type: 'pull_request', id: pr.number },
159
- }));
160
- },
161
- });
162
- }
163
-
164
- // The approval itself, shared by the route above and a passed Release matter
165
- // (task 1004561, ../matter-acts.js) so the two can never differ. Returns
166
- // { ok: true, body } or { ok: false, code, status, message }; throws on an
167
- // unexpected failure (the caller says it). `login` is who approved, as the
168
- // status description names it.
169
- async function approveGatePr({ number, login }) {
170
- if (!githubPush.isConfigured()) {
171
- return { ok: false, code: 'publish_unconfigured', status: 503, message: 'No server GitHub credential configured — cannot approve.' };
172
- }
173
- // Resolve the live OPEN PR + its current head sha (the approval is sha-pinned,
174
- // so a later commit's head carries no approval — red-team #2b protection).
175
- const prs = await githubPush.listOpenPullRequests({});
176
- const pr = prs.find((p) => p.number === number);
177
- if (!pr) return { ok: false, code: 'pr_not_open', status: 404, message: `No open PR #${number}.` };
178
- if (!pr.head_sha) return { ok: false, code: 'no_head_sha', status: 409, message: 'Could not resolve the PR head commit.' };
179
-
180
- const gate = findCheck(pr, 'gate-review');
181
- if (gate && isSuccess(gate)) {
182
- return { ok: true, body: { ok: true, pr: number, already_clear: true, message: 'gate-review is already green — nothing to approve.' } };
183
- }
184
-
185
- // 1. Set the trustless owner-approval status on the head sha.
186
- const set = await githubPush.setGateOwnerApproved({ sha: pr.head_sha, login, targetUrl: pr.url });
187
- if (!set) {
188
- // 500, not 502 — Cloudflare masks an origin 502 with its branded page (see the route's catch).
189
- return { ok: false, code: 'status_set_failed', status: 500, message: 'Could not set the gate-owner-approved status.' };
190
- }
191
-
192
- // 2. Re-trigger gate-review (close+reopen → `pull_request: reopened`) so the
193
- // required check re-evaluates the new approval; re-arm auto-merge.
194
- // A PAGE TWEAK HELD FOR ITS ARTIST is reopened but NOT re-armed (task
195
- // 1004323): clearing the gate check is the owner's act on the PR, and
196
- // approving the page is the artist's (ADR 0341 D7). Asked fail-closed: a
197
- // read that throws answers held, and an ordinary confirmed task left
198
- // un-armed still lands through the publish reconciler's sweep.
199
- const prTask = taskIdFromPr(pr);
200
- const held = prTask != null && await pageTweakReads.heldForArtistFailClosed(prTask);
201
- let recheck = { reopened: false, auto_merge: false };
202
- try {
203
- recheck = await githubPush.recheckPullRequest(held ? { number, arm: false, heldReason: HELD_NOT_ARMED.reason } : { number });
204
- } catch (e) {
205
- // The status IS set; if the re-trigger failed, the next push or a manual
206
- // re-run still clears it, and the publish reconciler sweep merges once green.
207
- console.error('[gds] gate-approval recheck failed', e);
208
- return {
209
- ok: true,
210
- body: {
211
- ok: true, pr: number, head_sha: pr.head_sha, status_set: true, recheck: 'failed',
212
- message: 'Approval recorded. Re-run of the gate check did not fire — it will clear on the next push or a manual re-run.',
213
- },
214
- };
215
- }
216
-
217
- return {
218
- ok: true,
219
- body: {
220
- ok: true,
221
- pr: number,
222
- head_sha: pr.head_sha,
223
- status_set: true,
224
- recheck,
225
- held_for_artist: held,
226
- message: held
227
- ? 'Approval recorded. Re-running the gate check. This is a page tweak waiting for its artist, so it lands only once the artist approves it in the studio.'
228
- : 'Approval recorded. Re-running the gate check — the PR merges once it goes green.',
229
- },
230
- };
231
- }
232
-
233
- module.exports = function buildGateApprovalsRouter() {
234
- contributeGateApprovalsToDocket();
235
- const router = express.Router();
236
-
237
- // GET /gate-approvals — open PRs awaiting owner approval (gate-review red), with the
238
- // verdict + per-file reasons + changed files + diff link. rank: metic+archon.
239
- // BV1.R105 left this on requireRank because `gate_approval.decide` was declared over
240
- // the /approve WRITE only and no atom named the read. ADR 0157 extends the catalog
241
- // with exactly that atom — `gate_approval.read`, floor metic — so the mapping is now
242
- // one the catalog makes. The split is load-bearing, not cosmetic: `page.view.government`
243
- // opens /gate to Metic, and without a Metic-readable data endpoint that page would
244
- // render nothing but a 403. Approving a held PR stays archon (`.decide`, system).
245
- router.get('/gate-approvals', auth.requireBuilder, auth.requirePermission('gate_approval.read'), async (req, res) => {
246
- if (!githubPush.isConfigured()) {
247
- return res.fail('publish_unconfigured', { status: 503, message: 'No server GitHub credential configured — cannot read pull requests.' });
248
- }
249
- try {
250
- const { prs, awaiting, checking } = await listAwaitingGateApprovals();
251
- // The PR check rollup is unreadable when the server credential lacks `Checks: read`
252
- // (operator blocker): listOpenPullRequests then returns partialErrors and every PR's
253
- // `checks` is empty, so the awaiting list is silently empty. Surface that so the hall
254
- // can say "checks unreadable — grant the App Checks:read" rather than "nothing stuck".
255
- const checksReadable = !(prs.partialErrors && prs.partialErrors.length);
256
- res.json({
257
- configured: true,
258
- awaiting,
259
- count: awaiting.length,
260
- checking,
261
- viewer_may_approve: await viewerMayApprove(req.builder.id),
262
- checks_readable: checksReadable,
263
- ...(checksReadable ? {} : {
264
- note: 'PR check rollup is unreadable — the server GitHub App is missing the Checks:read permission, so gate-review-red PRs cannot be listed. Grant it and reinstall.',
265
- }),
266
- });
267
- } catch (err) {
268
- console.error('[gds] GET /gate-approvals', err);
269
- // 500, not 502: Cloudflare replaces an origin 502/504 with its own branded
270
- // gateway-error page, which swallows this JSON body. A 500 is passed through, so
271
- // the structured error actually reaches the caller (the "never a bare 502" rule).
272
- res.fail('gate_list_failed', { status: 500, message: err.message, details: { detail: err.detail || null } });
273
- }
274
- });
275
-
276
- // POST /gate-approvals/:pr/approve — record owner approval for a stuck gate PR and
277
- // re-run the gate-review check so it clears. rank: archon (clears the hard floor —
278
- // pinned in route-rank-check.EXPECTED_RANKS for downgrade protection).
279
- router.post('/gate-approvals/:pr/approve', auth.requireBuilder, auth.requirePermission('gate_approval.decide'), auth.requireActAlone('release'), async (req, res) => {
280
- const number = parseId(req, res, { param: 'pr', code: 'bad_pr', positiveInt: true });
281
- if (number == null) return;
282
- try {
283
- const out = await approveGatePr({ number, login: req.builder.github_login });
284
- if (!out.ok) return res.fail(out.code, { status: out.status, message: out.message });
285
- res.json(out.body);
286
- } catch (err) {
287
- console.error('[gds] POST /gate-approvals/:pr/approve', err);
288
- // 500, not 502: Cloudflare replaces an origin 502/504 with its own branded
289
- // gateway-error page, swallowing this JSON. The reported symptom — a bare
290
- // "error code: 502" interstitial instead of a structured error — was exactly this.
291
- // A 500 is passed through, so the caller sees { error, message, detail }.
292
- res.fail('gate_approve_failed', { status: 500, message: err.message, details: { detail: err.detail || null } });
293
- }
294
- });
295
-
296
- return router;
297
- };
298
-
299
- module.exports.approveGatePr = approveGatePr;
@@ -1,267 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/bongos/artifact-staleness.js — are the gitignored generated artifacts on
3
- // THIS machine actually current with the commit it has checked out? (task 1001930,
4
- // ADR 0110 pt.5)
5
- //
6
- // WHY THIS EXISTS. ADR 0110 moved `docs/repo-map.md` and `docs/session-log-index.md`
7
- // off the git graph: they are 100%-generated, two parallel branches collided on them
8
- // constantly, so they became untracked files regenerated at deploy. That trade was
9
- // deliberate, and the ADR names the debt it took on in its own Consequences:
10
- //
11
- // "A generator that breaks at deploy leaves a stale/absent file with no blocking
12
- // backstop; the LOUD ::ERROR:: line is the alarm (mandatory, not optional)."
13
- //
14
- // That alarm is a line in the droplet's journald. Nobody reads journald. The per-PR
15
- // freshness gate cannot help either — the files are gitignored, so they never appear
16
- // in a diff for CI to check (`--check-whole`'s own comment says it plainly: "used by
17
- // nothing hard; deploy owns it"). So if the deploy regen step silently stops running,
18
- // every agent keeps reading a symbol map of a codebase that has moved on, and nothing
19
- // anywhere says so. This check is the backstop that posture depends on.
20
- //
21
- // WHAT IT COMPARES. For each registered artifact, its own generator's `--check-whole`
22
- // answers "would regenerating this file change it?" — run against the checkout that is
23
- // actually serving. A difference means the deploy generator did not run, or ran and
24
- // broke. Asking the generator rather than reimplementing the comparison is the point:
25
- // there is exactly one definition of "fresh" per artifact and it lives with the thing
26
- // that writes it, so this check cannot drift away from what deploy produces.
27
- //
28
- // ABSENT AND STALE ARE REPORTED SEPARATELY, and the split is operational, not cosmetic:
29
- // absent means the regen step never ran here at all (a deploy line was dropped, or a
30
- // fresh clone was never generated into), stale means it ran once and has not since.
31
- // Those send someone to two different places, so the report must not blur them.
32
- //
33
- // ── WHERE THIS MAY RUN — read before scheduling it anywhere new ───────────────
34
- //
35
- // ONLY where the artifacts are SUPPOSED to already exist: a deployed instance after its
36
- // source-fetch regen. NEVER in CI. A fresh `git clone` legitimately
37
- // has no `docs/repo-map.md` — it is gitignored, so it is not in the repository at all
38
- // — and this check would report it absent on every single run. A monitor that is red
39
- // every run teaches its reader to ignore it, which is the exact failure ADR 0110's
40
- // warn-only posture already has. That is why this ships as a scheduled routine on the
41
- // instance and NOT as a `.github/workflows/` job, unlike its sibling publish-watch.js
42
- // (which reads git and the registry, and so is true anywhere).
43
- //
44
- // THE MAIN GUARD. "Fresh" here means "matches what THIS commit generates". On a
45
- // deployed instance that commit is main, because deploy does `git reset --hard
46
- // origin/main` — so the question the caller actually wants ("does the served artifact
47
- // lag main's HEAD?") and the question `--check-whole` answers are the same question.
48
- // In a feature worktree they are NOT: the artifact would be compared against that
49
- // branch, and a pass or a fail would both be meaningless. So a checkout that is not at
50
- // origin/main is INCONCLUSIVE (exit 2), never a finding — a monitor that cannot tell
51
- // which question it just answered must say so rather than guess.
52
- 'use strict';
53
-
54
- const { spawnSync } = require('child_process');
55
- const fs = require('fs');
56
- const path = require('path');
57
-
58
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
59
- const GIT_TIMEOUT_MS = 15000;
60
- const CHECK_TIMEOUT_MS = 120000;
61
-
62
- // The gitignored, whole-file generated artifacts this check covers.
63
- //
64
- // MEMBERSHIP RULE — an artifact belongs here when all three hold: it is gitignored (so
65
- // no PR gate can see it), it is written by a deploy-time generator (so it can silently
66
- // stop being written), and its generator exposes `--check-whole` (so freshness has one
67
- // definition, owned by the writer). tests/artifact_staleness.mjs pins this list against
68
- // .gitignore and against the generators' real flags, so an artifact that gains a
69
- // `--check-whole` and is left out of here fails that test rather than going unwatched.
70
- //
71
- // DELIBERATELY NOT HERE, and each for a reason that is not "we forgot":
72
- // • modules/hall-ui/public/atlas.json (gen-atlas.js, ADR 0094) — gitignored and
73
- // deploy-generated, but its payload embeds `generatedAt: new Date()`, so a
74
- // regenerate-and-compare differs on every run by construction. Checking it needs a
75
- // comparison over `views` only, or a freshness-by-age test against the last deploy
76
- // — a different mechanism, not this one. gen-atlas has no --check mode at all today.
77
- // • docs/onboarding/diagrams/*.svg|png — rendering them needs a headless Chromium the
78
- // droplet does not have (the reason ADR 0110 deferred the diagram half in the first
79
- // place), so freshness cannot be evaluated where this runs. Their FACTS are already
80
- // backstopped by the diagram-drift routine, which reads assertions.json instead.
81
- const ARTIFACTS = Object.freeze([
82
- Object.freeze({
83
- rel: 'docs/repo-map.md',
84
- generator: 'gen-repo-map.js',
85
- what: 'the symbol skeleton agents navigate the codebase by (ADR 0063)',
86
- }),
87
- Object.freeze({
88
- rel: 'docs/session-log-index.md',
89
- generator: 'gen-session-index.js',
90
- what: 'the full session-log index the CLAUDE.md §13 snippet links out to (ADR 0062 §8)',
91
- }),
92
- ]);
93
-
94
- function git(args, run = spawnSync) {
95
- const raw = gitRaw(args, run);
96
- return raw === null ? null : raw.trim();
97
- }
98
-
99
- // Untrimmed, because `git status --porcelain` is column-significant: a line is
100
- // `XY<space>PATH` and an unstaged modification's X is a SPACE (" M path"). Trimming the
101
- // buffer eats that leading space on the FIRST line only, so a slice(3) then swallows the
102
- // path's first character — which is exactly how this printed "cripts/gds/…" until
103
- // tests/artifact_staleness.mjs caught it.
104
- function gitRaw(args, run = spawnSync) {
105
- const r = run('git', args, { cwd: REPO_ROOT, encoding: 'utf8', timeout: GIT_TIMEOUT_MS });
106
- if (!r || r.error || r.status !== 0) return null;
107
- return String(r.stdout || '');
108
- }
109
-
110
- // The paths in a porcelain listing, status columns removed.
111
- function porcelainPaths(raw) {
112
- return raw.split('\n').filter((l) => l.length > 3).map((l) => l.slice(3).trim()).filter(Boolean);
113
- }
114
-
115
- // Is this checkout a fair thing to compare the artifacts against — at origin/main AND
116
- // clean? Returns { ok } or { ok:false, reason }; git being unable to answer is equally
117
- // inconclusive.
118
- //
119
- // The CLEAN half is not belt-and-braces, and it was found by running this check against
120
- // its own uncommitted source file: a generator regenerates from the WORKING TREE, so one
121
- // uncommitted file that moves a symbol makes repo-map.md "stale" against a main it in
122
- // fact matches. On the droplet the tree is clean by construction (deploy does
123
- // `git reset --hard`), so requiring it costs a correct run nothing — while without it
124
- // every hand-run from a working checkout invents a finding, which is precisely how a
125
- // monitor teaches its reader to ignore it. Gitignored files (the artifacts themselves)
126
- // never appear in `--porcelain`, so they cannot trip this.
127
- function atMain(run = spawnSync) {
128
- const head = git(['rev-parse', 'HEAD'], run);
129
- const main = git(['rev-parse', 'origin/main'], run);
130
- if (!head) return { ok: false, reason: 'could not read HEAD (not a git checkout?)' };
131
- if (!main) return { ok: false, reason: 'could not read origin/main (no remote ref here?)' };
132
- if (head !== main) {
133
- const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], run) || '(detached)';
134
- return { ok: false, reason: `checkout is at ${branch} ${head.slice(0, 8)}, not origin/main ${main.slice(0, 8)}` };
135
- }
136
- const dirty = gitRaw(['status', '--porcelain'], run);
137
- if (dirty === null) return { ok: false, reason: 'could not read the working tree state' };
138
- const paths = porcelainPaths(dirty);
139
- if (paths.length) {
140
- const shown = paths.slice(0, 3).join(', ');
141
- const more = paths.length > 3 ? `, +${paths.length - 3} more` : '';
142
- return { ok: false, reason: `working tree is not clean (${shown}${more}) — the generators would compare against those edits, not main` };
143
- }
144
- return { ok: true, sha: head };
145
- }
146
-
147
- // Ask ONE artifact's own generator whether it is current.
148
- // fresh exit 0 — regenerating would change nothing
149
- // stale exit 2 — it would (absent counts as stale; `present` says which)
150
- // unknown anything else — the generator itself could not answer, which is not a
151
- // finding about the artifact and must never be reported as one.
152
- function checkArtifact(a, { root = REPO_ROOT, run = spawnSync, exists = fs.existsSync } = {}) {
153
- const present = exists(path.join(root, a.rel));
154
- const r = run(process.execPath, [path.join(root, 'scripts', 'bongos', a.generator), '--check-whole'],
155
- { cwd: root, encoding: 'utf8', timeout: CHECK_TIMEOUT_MS });
156
- if (!r || r.error) {
157
- return { ...a, state: 'unknown', present, detail: r && r.error ? r.error.message : 'generator did not run' };
158
- }
159
- if (r.status === 0) return { ...a, state: 'fresh', present };
160
- if (r.status === 2) return { ...a, state: present ? 'stale' : 'absent', present };
161
- const said = String(r.stderr || r.stdout || '').trim().split('\n').pop() || `exit ${r.status}`;
162
- return { ...a, state: 'unknown', present, detail: said };
163
- }
164
-
165
- function check({ root = REPO_ROOT, run = spawnSync, exists = fs.existsSync, artifacts = ARTIFACTS, requireMain = true } = {}) {
166
- if (requireMain) {
167
- const m = atMain(run);
168
- if (!m.ok) { const e = new Error(m.reason); e.inconclusive = true; throw e; }
169
- }
170
- const results = artifacts.map((a) => checkArtifact(a, { root, run, exists }));
171
- return {
172
- results,
173
- fresh: results.filter((r) => r.state === 'fresh'),
174
- stale: results.filter((r) => r.state === 'stale'),
175
- absent: results.filter((r) => r.state === 'absent'),
176
- unknown: results.filter((r) => r.state === 'unknown'),
177
- };
178
- }
179
-
180
- // Plain language: this lands in Discord #ship-news, where the audience is the owner,
181
- // not whoever wrote the generator.
182
- function report(r) {
183
- const bad = [...r.absent, ...r.stale];
184
- if (!bad.length && !r.unknown.length) {
185
- return `Generated navigation docs are current — ${r.fresh.length} artifact(s) match this commit.`;
186
- }
187
- const lines = [];
188
- if (bad.length) {
189
- lines.push(`⚠ ${bad.length} generated file(s) on this instance no longer match the code being served.`);
190
- lines.push('');
191
- for (const a of r.absent) lines.push(` • ${a.rel} — MISSING entirely. Nothing has generated it here. It is ${a.what}.`);
192
- for (const a of r.stale) lines.push(` • ${a.rel} — out of date: it no longer matches what this commit would produce. It is ${a.what}.`);
193
- lines.push('');
194
- lines.push('These are rebuilt by the deploy step, so this means that step stopped running or broke.');
195
- lines.push('Nothing blocks on them, which is why it went unreported until now — anyone reading these');
196
- lines.push('files is being given an out-of-date picture. Check the deploy log for its regen lines');
197
- lines.push('(they print ::ERROR:: when a generator fails).');
198
- }
199
- if (r.unknown.length) {
200
- if (bad.length) lines.push('');
201
- lines.push(`Could not check ${r.unknown.length} artifact(s):`);
202
- for (const a of r.unknown) lines.push(` • ${a.rel} — ${a.detail}`);
203
- }
204
- return lines.join('\n');
205
- }
206
-
207
- // Deliver the finding here rather than through run-routine.js's `sink: discord`.
208
- // run-routine treats a non-zero step as a FAILED step and aborts BEFORE it routes
209
- // stdout, so exiting 1 on a finding would throw that finding away. publish-watch.js
210
- // and artifact-audit.js carry the same shape for the same reason; their routine steps
211
- // all stay `sink: none` and the script performs its own side effect.
212
- async function deliver(text) {
213
- const { postWebhook } = require(path.join(__dirname, '..', 'discord', 'lib.js'));
214
- await postWebhook('ship_news', text);
215
- }
216
-
217
- async function main() {
218
- const argv = process.argv.slice(2);
219
- const json = argv.includes('--json');
220
-
221
- let r;
222
- try {
223
- r = check({ requireMain: !argv.includes('--any-ref') });
224
- } catch (err) {
225
- // Exit 2 = COULD NOT RUN, kept distinct from 1 (ran, found something) so a
226
- // scheduler can go red on the former and merely report the latter.
227
- console.error(`artifact-staleness could not complete: ${err && err.message ? err.message : err}`);
228
- if (err && err.inconclusive) {
229
- console.error(' Freshness is only meaningful against the commit being served. Re-run on the');
230
- console.error(' deployed instance, or pass --any-ref to compare against whatever is checked out.');
231
- }
232
- process.exit(2);
233
- }
234
-
235
- const text = report(r);
236
- const found = r.stale.length + r.absent.length;
237
-
238
- if (json) { console.log(JSON.stringify(r, null, 2)); process.exit(found ? 1 : 0); }
239
-
240
- // A healthy run is deliberately quiet — it tells a person who ran it by hand
241
- // (stderr) and posts nothing. A daily "all fine" is how a channel becomes wallpaper.
242
- if (!found) {
243
- console.error(text);
244
- // An artifact we could not check is not a finding, but it must not be silent
245
- // either: a permanently-unknown artifact is unwatched without anyone deciding so.
246
- process.exit(r.unknown.length ? 2 : 0);
247
- }
248
-
249
- console.log(text);
250
- if (argv.includes('--no-post')) process.exit(1);
251
-
252
- try {
253
- await deliver(text);
254
- console.error('posted the finding to Discord #ship-news.');
255
- process.exit(1);
256
- } catch (err) {
257
- // Found something and could not say it — the original defect one layer down, so
258
- // it is the LOUDEST outcome: exit 3 is >= 2, which turns the scheduled run red.
259
- console.error(`\nFOUND STALE ARTIFACTS AND COULD NOT DELIVER THE FINDING: ${err && err.message ? err.message : err}`);
260
- console.error('The finding is above. Set DISCORD_SHIP_NEWS_WEBHOOK so this reaches #ship-news.');
261
- process.exit(3);
262
- }
263
- }
264
-
265
- if (require.main === module) main();
266
-
267
- module.exports = { ARTIFACTS, atMain, checkArtifact, check, report };
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/bongos/ci-grade.js — run the quality grader IN CI as a fail-closed PR
3
- // gate (#855, ADR 0042). This is the security KEYSTONE for the ci deploy mode.
4
- //
5
- // WHY it must run in CI (not "read the recorded grade"):
6
- // POST /api/bongos/tasks/:id/grade records the grader scores FROM THE REQUEST
7
- // BODY — the server does not independently recompute them. In laptop mode the
8
- // grader runs on the OPERATOR's trusted machine, so that's fine. In ci mode the
9
- // merge trigger moves to an UNTRUSTED builder machine; a tampered local ship.js
10
- // could POST a fake passing grade, so a gate that merely READS the recorded
11
- // grade is theater. The only tamper-proof gate runs the grader where the
12
- // builder can't touch it: here, in GitHub Actions, against the PR diff.
13
- //
14
- // It reuses the EXACT production grading path (ship.js runSubagentGrader → the
15
- // adversarial worker panel / trivial-diff shot), so a CI pass means the same
16
- // thing a ship-time pass means. Exit 0 = pass, non-zero = fail (FAIL-CLOSED: any
17
- // grader error also exits non-zero).
18
- //
19
- // SKIP-SAFE until armed: if no grader auth is wired (CLAUDE_CODE_OAUTH_TOKEN /
20
- // ANTHROPIC_API_KEY), it prints SKIP and exits 0 — so merging this workflow
21
- // changes nothing until the operator (a) adds the auth secret and (b) marks this
22
- // a REQUIRED check in branch protection. Mirrors the inert-until-cutover pattern
23
- // of .github/workflows/deploy-prod.yml (#597). See ADR 0042 cutover precondition #1.
24
- //
25
- // Inputs (from the workflow env):
26
- // GRADE_BASE_REF base ref to diff against (default origin/main)
27
- // GRADE_TASK_ID the PR's task id (context only; grading is diff-based)
28
- // GRADE_TASK_TITLE the PR title (context for the grader prompt)
29
- // GRADE_VALUE_SUMMARY the PR value summary (context for the grader prompt)
30
-
31
- const { execFileSync } = require('node:child_process');
32
- const ship = require('./ship.js');
33
-
34
- function git(args) {
35
- return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
36
- }
37
-
38
- (async () => {
39
- // Auth presence — the grader spawns the `claude` CLI; without auth it cannot
40
- // run. Until the operator wires it, stay green-but-not-gating (see header).
41
- const hasAuth = !!(process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY);
42
- if (!hasAuth) {
43
- console.log(
44
- 'grade-gate: SKIP — no grader auth (CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY) configured. ' +
45
- 'This check is green-but-not-gating until the operator wires the secret AND marks it a required check ' +
46
- '(ADR 0042 cutover precondition #1). It is NOT a real gate yet.'
47
- );
48
- process.exit(0);
49
- }
50
-
51
- const base = process.env.GRADE_BASE_REF || 'origin/main';
52
- let baseline;
53
- try {
54
- baseline = git(['merge-base', base, 'HEAD']);
55
- } catch (_) {
56
- baseline = base; // shallow checkout / no common ancestor — diff against the ref directly
57
- }
58
-
59
- const changedFiles = ship.computeChangedFiles(baseline);
60
- if (!changedFiles.length) {
61
- console.log('grade-gate: no changes vs base — pass (nothing to grade).');
62
- process.exit(0);
63
- }
64
-
65
- const task = {
66
- id: process.env.GRADE_TASK_ID || null,
67
- title: process.env.GRADE_TASK_TITLE || '(pull request)',
68
- };
69
- const valueSummary = process.env.GRADE_VALUE_SUMMARY || '';
70
-
71
- console.log(`grade-gate: grading ${changedFiles.length} changed file(s) vs ${baseline.slice(0, 12)} ...`);
72
- const result = await ship.runSubagentGrader({ task, valueSummary, baseline, changedFiles });
73
-
74
- console.log(`grade-gate: verdict = ${result.passed ? 'PASS' : 'FAIL'}`);
75
- if (result.notes_md) console.log(String(result.notes_md).slice(0, 6000));
76
- // Fail-closed: only an explicit pass exits 0.
77
- process.exit(result.passed ? 0 : 1);
78
- })().catch((err) => {
79
- // FAIL-CLOSED: a grader error must block the merge, never wave it through.
80
- console.error('grade-gate: FAIL (grader error — failing closed):', err && err.message ? err.message : err);
81
- process.exit(1);
82
- });