@bongos/core 1.19.1080 → 1.20.1

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 (258) hide show
  1. package/.bongos-core.json +573 -188
  2. package/.claude/skills/planning-session/SKILL.md +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +34 -0
  5. package/clients/bongos-client/index.cjs +34 -0
  6. package/clients/bongos-client/index.d.ts +53 -4
  7. package/clients/bongos-client/index.mjs +34 -0
  8. package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
  9. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  10. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
  11. package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
  12. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  13. package/docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +2 -0
  14. package/docs/adr/0347-every-store-module-ships-a-how-to.md +135 -0
  15. package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
  16. package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
  17. package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
  18. package/docs/adr/README.md +4 -0
  19. package/docs/api/openapi.json +995 -40
  20. package/docs/api-reference.md +33 -8
  21. package/docs/architecture.md +6 -2
  22. package/docs/copy-inventory.md +620 -553
  23. package/docs/copy-registry.json +1467 -820
  24. package/docs/file-map.md +2 -0
  25. package/docs/module-api-changelog.md +8 -0
  26. package/docs/modules-contract.md +1 -0
  27. package/docs/page-inventory.json +38 -4
  28. package/docs/page-readings.json +1674 -1566
  29. package/migrations/core_260_goals_working_area.sql +63 -0
  30. package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
  31. package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
  32. package/modules/agents/lib/validate.js +43 -0
  33. package/modules/autonomy/gauge.js +38 -2
  34. package/modules/grading/grader-subagent.js +37 -2
  35. package/modules/grading/grader-workers/reader.js +65 -5
  36. package/modules/hall-ui/public/approval-queue.css +33 -7
  37. package/modules/hall-ui/public/approval-queue.js +9 -2
  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/brand-holes.js +121 -0
  42. package/modules/hall-ui/public/collab.html +1 -1
  43. package/modules/hall-ui/public/collab.js +1 -1
  44. package/modules/hall-ui/public/copy-desk.html +1 -1
  45. package/modules/hall-ui/public/deploy.html +6 -1
  46. package/modules/hall-ui/public/deploy.js +22 -8
  47. package/modules/hall-ui/public/diagrams.html +1 -1
  48. package/modules/hall-ui/public/dom-utils.js +20 -0
  49. package/modules/hall-ui/public/drachmae.html +1 -1
  50. package/modules/hall-ui/public/fleet.html +1 -1
  51. package/modules/hall-ui/public/gate.html +1 -1
  52. package/modules/hall-ui/public/goals.html +1 -1
  53. package/modules/hall-ui/public/government.html +1 -1
  54. package/modules/hall-ui/public/idea.html +1 -1
  55. package/modules/hall-ui/public/idea.js +28 -0
  56. package/modules/hall-ui/public/ideas.html +1 -1
  57. package/modules/hall-ui/public/ideas.js +19 -3
  58. package/modules/hall-ui/public/index.html +5 -1
  59. package/modules/hall-ui/public/modules.html +1 -1
  60. package/modules/hall-ui/public/primer.html +1 -1
  61. package/modules/hall-ui/public/profile-nudge.js +21 -4
  62. package/modules/hall-ui/public/profile.html +1 -1
  63. package/modules/hall-ui/public/profile.js +1 -1
  64. package/modules/hall-ui/public/project-settings.html +1 -1
  65. package/modules/hall-ui/public/ranks.html +1 -1
  66. package/modules/hall-ui/public/roadmap.html +1 -1
  67. package/modules/hall-ui/public/roster.html +1 -1
  68. package/modules/hall-ui/public/sessions.html +1 -1
  69. package/modules/hall-ui/public/settings.html +19 -30
  70. package/modules/hall-ui/public/settings.js +24 -177
  71. package/modules/hall-ui/public/settings.states.json +1 -1
  72. package/modules/hall-ui/public/shell.js +4 -2
  73. package/modules/hall-ui/public/studio.css +14 -8
  74. package/modules/hall-ui/public/studio.html +5 -4
  75. package/modules/hall-ui/public/task.html +1 -1
  76. package/modules/hall-ui/public/thinking.css +6 -3
  77. package/modules/hall-ui/public/thinking.html +1 -1
  78. package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
  79. package/modules/hall-ui/public/tweak-editor.css +142 -11
  80. package/modules/hall-ui/public/tweak-editor.html +25 -9
  81. package/modules/hall-ui/public/tweak-editor.js +243 -10
  82. package/modules/hall-ui/public/watch.html +1 -1
  83. package/modules/hall-ui/public/work.html +1 -1
  84. package/modules/hall-ui/records/approval-queue.md +6 -0
  85. package/modules/hall-ui/records/tweak-editor.md +6 -0
  86. package/modules/ideas/ratify.js +36 -8
  87. package/modules/ideas/routes/ratify-goal.js +6 -0
  88. package/modules/lifecycle/db-analytics.js +79 -10
  89. package/modules/lifecycle/db-claims.js +48 -1
  90. package/modules/lifecycle/db-deps-criteria.js +94 -40
  91. package/modules/lifecycle/db-goals.js +4 -1
  92. package/modules/lifecycle/db-grade.js +9 -1
  93. package/modules/lifecycle/db-relevance-flags.js +183 -0
  94. package/modules/lifecycle/db-versions.js +4 -3
  95. package/modules/lifecycle/db.js +19 -0
  96. package/modules/lifecycle/done-when.js +5 -3
  97. package/modules/lifecycle/est-advisory.js +31 -4
  98. package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
  99. package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
  100. package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
  101. package/modules/lifecycle/module.json +2 -1
  102. package/modules/lifecycle/routes/claims.js +8 -1
  103. package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
  104. package/modules/lifecycle/routes/goals.js +1 -1
  105. package/modules/lifecycle/routes/tasks.js +27 -24
  106. package/modules/lifecycle/routes/visuals.js +9 -0
  107. package/modules/lifecycle/ship-preflight.js +7 -11
  108. package/modules/lifecycle/task-visuals.js +109 -5
  109. package/modules/npm-release/module.json +2 -1
  110. package/modules/npm-release/preview/commands.js +81 -0
  111. package/modules/npm-release/preview/divert.js +95 -0
  112. package/modules/npm-release/preview/env.js +78 -0
  113. package/modules/npm-release/preview/proxy.js +107 -0
  114. package/modules/npm-release/preview/runtime.js +83 -0
  115. package/modules/npm-release/preview/supervisor.js +175 -0
  116. package/modules/npm-release/public/work.js +65 -1
  117. package/modules/npm-release/routes/preview.js +133 -0
  118. package/modules/provisioning/app-status.js +83 -0
  119. package/modules/provisioning/migrations/provisioning_028_render_standup.sql +46 -0
  120. package/modules/provisioning/module.json +4 -2
  121. package/modules/provisioning/pollers/app-liveness.js +110 -0
  122. package/modules/provisioning/provisioning.js +5 -5
  123. package/modules/provisioning/render-lookup.js +68 -0
  124. package/modules/provisioning/render-standup.js +134 -0
  125. package/modules/provisioning/routes/render-standup.js +165 -0
  126. package/modules/provisioning/starter-bundles.js +4 -1
  127. package/modules/public-landing/public/account-private.states.json +14 -0
  128. package/modules/public-landing/public/account.html +513 -0
  129. package/modules/public-landing/public/account.probes.json +37 -0
  130. package/modules/public-landing/public/account.states.json +16 -0
  131. package/modules/public-landing/public/index.html +5 -2
  132. package/modules/public-landing/public/projects.html +661 -18
  133. package/modules/public-landing/public/projects.states.json +8 -1
  134. package/modules/render-deploy/deploys.js +203 -0
  135. package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
  136. package/modules/render-deploy/module.json +25 -0
  137. package/modules/render-deploy/public/deploy.css +10 -0
  138. package/modules/render-deploy/public/deploy.js +249 -0
  139. package/modules/render-deploy/render.js +56 -0
  140. package/modules/render-deploy/routes/act.js +88 -0
  141. package/modules/render-deploy/routes/app.js +65 -0
  142. package/modules/render-deploy/routes/history.js +38 -0
  143. package/modules/specialities/db.js +27 -8
  144. package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
  145. package/modules/specialities/routes/specialities.js +51 -3
  146. package/modules/specialities/skills.js +94 -0
  147. package/modules/specialities/specialities.js +56 -9
  148. package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
  149. package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
  150. package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
  151. package/modules/ui-design/kit/lib.js +3 -1
  152. package/modules/ui-design/kit/serve.js +96 -0
  153. package/package-lock.json +2 -2
  154. package/package.json +1 -1
  155. package/release-notes.json +173 -0
  156. package/scripts/gds/agents-sync.js +13 -3
  157. package/scripts/gds/autobongos-run.js +80 -2
  158. package/scripts/gds/autobongos-service.cmd +12 -0
  159. package/scripts/gds/copy-apply.js +14 -0
  160. package/scripts/gds/dev-box-guard.js +3 -2
  161. package/scripts/gds/fitness.js +9 -0
  162. package/scripts/gds/grade-correlation-audit.js +42 -12
  163. package/scripts/gds/grade-replay.js +1 -1
  164. package/scripts/gds/provision-core-upgrade.js +38 -7
  165. package/scripts/gds/provision-pin-key.js +176 -0
  166. package/scripts/gds/provision-render.js +189 -0
  167. package/scripts/gds/provision-teardown.js +9 -2
  168. package/scripts/gds/provision-units.js +31 -0
  169. package/scripts/gds/provision.js +12 -12
  170. package/scripts/gds/publish-manifest.js +1 -0
  171. package/scripts/gds/render-api.js +197 -0
  172. package/scripts/gds/render-payload.js +84 -0
  173. package/scripts/gds/run-unit-tests.js +10 -0
  174. package/scripts/gds/ship-finish.js +19 -20
  175. package/scripts/gds/smoke-dependencies.sh +37 -8
  176. package/scripts/gds/status.js +12 -4
  177. package/scripts/gds/upgrade.js +2 -2
  178. package/scripts/public-mirror-export.js +7 -1
  179. package/src/bongos/module-scope-map.js +13 -0
  180. package/src/bongos/serve-internal.js +3 -1
  181. package/src/module-api.js +10 -1
  182. package/src/platform-server.js +39 -0
  183. package/tests/account_privacy_flags.mjs +10 -6
  184. package/tests/agents_authoring.mjs +15 -1
  185. package/tests/agents_sync.mjs +106 -5
  186. package/tests/agents_validate.mjs +45 -0
  187. package/tests/api_path_404.mjs +6 -0
  188. package/tests/autobongos_cadence.mjs +7 -0
  189. package/tests/autobongos_loop.mjs +136 -1
  190. package/tests/autonomy_gauge.mjs +62 -0
  191. package/tests/blocker_hall_live_proof.mjs +275 -0
  192. package/tests/claim_gate_ci_unblock.mjs +116 -0
  193. package/tests/claim_gate_rebase.mjs +15 -3
  194. package/tests/collab_page.mjs +10 -0
  195. package/tests/conductor_main_e2e.mjs +97 -0
  196. package/tests/core_262_drop_box_blocked_db.mjs +143 -0
  197. package/tests/core_upgrade_runner.mjs +1 -0
  198. package/tests/dependency_writes_atomic.mjs +191 -0
  199. package/tests/deploy_page_projects.mjs +2 -1
  200. package/tests/effective_visibility_predicate.mjs +15 -0
  201. package/tests/est_advisory.mjs +27 -0
  202. package/tests/goal_map_page.mjs +26 -0
  203. package/tests/goal_task_relevance.mjs +518 -0
  204. package/tests/goal_working_area.mjs +146 -0
  205. package/tests/grade_attempts.mjs +61 -0
  206. package/tests/grade_attribution.mjs +23 -0
  207. package/tests/grade_correlation_audit.mjs +54 -1
  208. package/tests/grader_reader_lens.mjs +55 -2
  209. package/tests/grader_root_outage.mjs +6 -1
  210. package/tests/grader_subagent_tools_arg.mjs +102 -0
  211. package/tests/hall_approval_queue.mjs +54 -2
  212. package/tests/hall_settings_world.mjs +3 -1
  213. package/tests/hall_tweak_editor.mjs +316 -11
  214. package/tests/hub_account_page.mjs +394 -0
  215. package/tests/idea_detail_page.mjs +64 -0
  216. package/tests/idea_goal_ratification.mjs +64 -0
  217. package/tests/idea_spark_hall.mjs +60 -9
  218. package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
  219. package/tests/landing_page.mjs +6 -3
  220. package/tests/module-scope-map.mjs +31 -8
  221. package/tests/module_api.mjs +1 -0
  222. package/tests/module_loader.mjs +1 -1
  223. package/tests/nav_permission_atoms.mjs +1 -1
  224. package/tests/no_phantom_mirror_workflow.mjs +24 -0
  225. package/tests/npm_release_preview_commands.mjs +89 -0
  226. package/tests/npm_release_preview_divert.mjs +164 -0
  227. package/tests/npm_release_preview_env.mjs +100 -0
  228. package/tests/npm_release_preview_proxy.mjs +162 -0
  229. package/tests/npm_release_preview_routes.mjs +208 -0
  230. package/tests/npm_release_preview_supervisor.mjs +203 -0
  231. package/tests/pin_write_key.mjs +383 -0
  232. package/tests/planning_session_skill.mjs +27 -0
  233. package/tests/platform_boot.mjs +71 -0
  234. package/tests/profile_nudge_links.mjs +86 -0
  235. package/tests/profile_ui_cross_project.mjs +1 -1
  236. package/tests/projects_hub.mjs +2 -2
  237. package/tests/projects_hub_app_status.mjs +249 -0
  238. package/tests/projects_hub_app_step.mjs +282 -52
  239. package/tests/projects_hub_render_connect.mjs +295 -0
  240. package/tests/provision_render.mjs +362 -0
  241. package/tests/provision_settings_apply.mjs +62 -0
  242. package/tests/provisioning_app_status.mjs +204 -0
  243. package/tests/provisioning_render_route.mjs +336 -0
  244. package/tests/provisioning_settings_apply.mjs +1 -1
  245. package/tests/provisioning_teardown_intent.mjs +2 -2
  246. package/tests/render_api.mjs +153 -0
  247. package/tests/render_check.mjs +2 -1
  248. package/tests/render_deploy.mjs +495 -0
  249. package/tests/ship_preflight.mjs +61 -8
  250. package/tests/smoke_dependencies_witness.mjs +46 -0
  251. package/tests/speciality_skills.mjs +214 -0
  252. package/tests/studio_room_height.mjs +70 -0
  253. package/tests/task_detail_includes.mjs +10 -0
  254. package/tests/task_visuals_instance_root.mjs +150 -0
  255. package/tests/tweak_batch_apply.mjs +21 -0
  256. package/tests/ui_design_kit.mjs +36 -4
  257. package/tests/upgrade.mjs +1 -1
  258. package/tests/upgrade_persist_pin.mjs +24 -0
@@ -0,0 +1,183 @@
1
+ // modules/lifecycle/db-relevance-flags.js — the ADR 0101 Part 1 relevance/
2
+ // anti-hack judge's data layer (task 1001691, goal 1000089): the flag table
3
+ // (lifecycle_014) plus the CLAIM-time friction gate.
4
+ //
5
+ // THE BOUNDARY (ADR 0093 §1): this file reaches core ONLY through the
6
+ // published doorway ../../src/module-api.js.
7
+ //
8
+ // relevanceFrictionFailures is the goalMembershipFailures precedent
9
+ // (claim-feed.js): a pure-ish, DB-read gate check called from all THREE claim
10
+ // surfaces (db-claims.js's claimTask / claimTasksBatch / validateClaimBatch),
11
+ // so a flagged task cannot be claimed via any of them while friction_required
12
+ // is true and the flag sits open. It is a SEPARATE file (not inlined into
13
+ // db-claims.js) for the same reason claim-feed.js is: the table it reads is
14
+ // this file's own domain, and a second claim surface duplicating the query
15
+ // inline is exactly the bypass ADR 0101 Part 1 must not create.
16
+
17
+ 'use strict';
18
+
19
+ const api = require('../../src/module-api');
20
+ const { pool, withTx } = api;
21
+
22
+ // createRelevanceFlag — INSERT the judge's verdict. Only ever called when the
23
+ // judge flagged the task (verdict === 'flag'); an 'ok' verdict writes nothing,
24
+ // same as idea_inbox only ever holding a captured idea.
25
+ async function createRelevanceFlag({
26
+ taskId, goalId, builderId, confidence, reasoning, model, costUsd, frictionRequired,
27
+ }) {
28
+ const { rows } = await pool.query(
29
+ `INSERT INTO lifecycle_goal_task_relevance_flags
30
+ (task_id, goal_id, flagged_builder_id, confidence, reasoning, model, cost_usd, friction_required)
31
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
32
+ RETURNING *`,
33
+ [taskId, goalId, builderId ?? null, confidence, reasoning ?? null, model ?? null, costUsd ?? 0, frictionRequired === true]
34
+ );
35
+ return rows[0];
36
+ }
37
+
38
+ // countPriorRelevanceFlagsForBuilder — the repeat-gaming signal the judge's
39
+ // frictionRequired() weighs. Counts every flag ever raised against this
40
+ // builder EXCEPT one a Metic already reviewed and called a false positive —
41
+ // a reviewed-and-cleared flag is not evidence of a pattern, and counting it
42
+ // would let one wrong judge call permanently escalate friction on an innocent
43
+ // builder's every future task.
44
+ async function countPriorRelevanceFlagsForBuilder(builderId) {
45
+ if (builderId == null) return 0;
46
+ const { rows } = await pool.query(
47
+ `SELECT count(*)::int AS n
48
+ FROM lifecycle_goal_task_relevance_flags
49
+ WHERE flagged_builder_id = $1
50
+ AND decision IS DISTINCT FROM 'false_positive'`,
51
+ [builderId]
52
+ );
53
+ return rows[0] ? rows[0].n : 0;
54
+ }
55
+
56
+ // listPendingRelevanceFlags — the Metic review queue (mirrors /goal-review's
57
+ // / /idea-triage's queue cadence): every open flag, oldest first, with enough
58
+ // of the task + goal to triage without a second round trip.
59
+ //
60
+ // DELIBERATELY NOT ADR-0112-SCOPED to the reader's own goal membership,
61
+ // unlike sky.js/goal-graph.js/activity.js's "a private goal loses EVERY piece
62
+ // of prose" rule. Those are general-purpose reads a builder's own membership
63
+ // bounds; this is a POLICING surface — the reviewer's authority (perm
64
+ // task.promote, Metic+) is exactly the authority to see what is being flagged
65
+ // as possible gaming, private goal or not, the same way an idea's Full-Idea
66
+ // text is shown in full to the Board Room rather than redacted per-viewer.
67
+ // Narrowing it to members-only would let a private goal's own gaming go
68
+ // unreviewed by anyone outside it.
69
+ async function listPendingRelevanceFlags({ limit = 50 } = {}) {
70
+ const { rows } = await pool.query(
71
+ `SELECT f.*, t.title AS task_title, t.description AS task_description,
72
+ t.touches AS task_touches, g.title AS goal_title
73
+ FROM lifecycle_goal_task_relevance_flags f
74
+ JOIN tasks t ON t.id = f.task_id
75
+ JOIN goals g ON g.id = f.goal_id
76
+ WHERE f.status = 'open'
77
+ ORDER BY f.created_at ASC
78
+ LIMIT $1`,
79
+ [Math.max(1, Math.min(200, Number(limit) || 50))]
80
+ );
81
+ return rows;
82
+ }
83
+
84
+ // decideRelevanceFlag — a Metic resolves the flag. Either decision CLEARS the
85
+ // friction hold (status flips to 'resolved'): the hold exists to get a human's
86
+ // eyes on it, never to police the verdict forever (ADR 0101 Part 1: "never a
87
+ // silent, opaque AI no"). Returns null when there is no OPEN flag with that id
88
+ // — an already-decided or nonexistent flag is not re-decided.
89
+ //
90
+ // SELF-DECIDE IS REFUSED, unlike db-overrides.js's artist-gate release (which
91
+ // PERMITS and merely records a self-release — the normal case on a solo
92
+ // instance). Here it is the opposite: the whole point of the friction hold is
93
+ // a Metic CO-SIGN on the flagged builder's OWN work, so letting that same
94
+ // builder clear their own hold defeats it outright (hacker finding) — never
95
+ // exempted by rank, since a Metic/Archon author is exactly who a repeat/high-
96
+ // confidence flag is watching. A genuinely solo instance still has the
97
+ // existing task_override_requests escape valve for anything this refuses.
98
+ //
99
+ // AUDITED regardless of outcome (api.insertAuditLog) — best-effort: the
100
+ // decision is durable in the flag row, and a failed audit write must not roll
101
+ // back a Metic's actual decision, only log loudly.
102
+ //
103
+ // LOCKED, not two independent statements: the self-decide check and the
104
+ // UPDATE run inside ONE transaction with the row taken FOR UPDATE first — the
105
+ // removal-proposals.js / db-overrides.js precedent — so two concurrent
106
+ // decides on the same flag can't both read 'open' and race past the check;
107
+ // the second finds the row already 'resolved' and returns null, never a
108
+ // second self-decide-checked write.
109
+ async function decideRelevanceFlag({ flagId, decidedByBuilderId, decision, note }) {
110
+ const flag = await withTx(async (client) => {
111
+ const { rows: flagRows } = await client.query(
112
+ `SELECT flagged_builder_id FROM lifecycle_goal_task_relevance_flags WHERE id = $1 AND status = 'open' FOR UPDATE`,
113
+ [flagId]
114
+ );
115
+ if (flagRows.length === 0) return null;
116
+ if (flagRows[0].flagged_builder_id != null && String(flagRows[0].flagged_builder_id) === String(decidedByBuilderId)) {
117
+ throw Object.assign(new Error('a flagged builder may not decide their own relevance flag'), {
118
+ code: 'SELF_DECIDE_FORBIDDEN',
119
+ });
120
+ }
121
+ const { rows } = await client.query(
122
+ `UPDATE lifecycle_goal_task_relevance_flags
123
+ SET status = 'resolved', decision = $2, decided_by_builder_id = $3,
124
+ decided_at = now(), decision_note = $4
125
+ WHERE id = $1 AND status = 'open'
126
+ RETURNING *`,
127
+ [flagId, decision, decidedByBuilderId, note ?? null]
128
+ );
129
+ return rows[0] || null;
130
+ });
131
+ if (flag) {
132
+ try {
133
+ await api.insertAuditLog({
134
+ builderId: decidedByBuilderId,
135
+ route: '/api/bongos/goal-task-relevance/:id/decide',
136
+ method: 'POST',
137
+ requestBodyRedacted: { flag_id: String(flag.id), task_id: String(flag.task_id), decision },
138
+ responseStatus: 200,
139
+ });
140
+ } catch (e) {
141
+ api.logger('lifecycle').error(`[relevance-flag] decide ${flag.id}: audit write failed — ${e && e.message}`);
142
+ }
143
+ }
144
+ return flag;
145
+ }
146
+
147
+ // relevanceFrictionFailures — the CLAIM-time gate (the goalMembershipFailures
148
+ // shape, claim-feed.js). `client` is the caller's open txn client (claimTask)
149
+ // or the pool itself (validateClaimBatch's read-only dry run) — same
150
+ // convention goalMembershipFailures already uses. Returns
151
+ // [{ task_id, refusal }], empty when nothing is held.
152
+ async function relevanceFrictionFailures({ client, tasks }) {
153
+ const ids = (tasks || []).map((t) => Number(t.id)).filter((n) => Number.isFinite(n));
154
+ if (ids.length === 0) return [];
155
+ const { rows } = await client.query(
156
+ `SELECT task_id FROM lifecycle_goal_task_relevance_flags
157
+ WHERE task_id = ANY($1::bigint[]) AND friction_required = true AND status = 'open'`,
158
+ [ids]
159
+ );
160
+ const held = new Set(rows.map((r) => Number(r.task_id)));
161
+ if (held.size === 0) return [];
162
+ return ids
163
+ .filter((id) => held.has(id))
164
+ .map((id) => ({
165
+ task_id: id,
166
+ refusal: {
167
+ code: 'RELEVANCE_REVIEW_PENDING',
168
+ message: 'The ADR 0101 relevance judge flagged this task as possible scope-gaming; '
169
+ + 'it needs a Metic to review before it can be claimed. This never permanently blocks '
170
+ + 'the task — either decision (confirmed or false positive) releases it.',
171
+ hint: 'A Metic can review it via GET /goal-task-relevance/pending and '
172
+ + 'POST /goal-task-relevance/:id/decide.',
173
+ },
174
+ }));
175
+ }
176
+
177
+ module.exports = {
178
+ createRelevanceFlag,
179
+ countPriorRelevanceFlagsForBuilder,
180
+ listPendingRelevanceFlags,
181
+ decideRelevanceFlag,
182
+ relevanceFrictionFailures,
183
+ };
@@ -425,7 +425,8 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
425
425
  });
426
426
  }
427
427
  // The successor carries what makes the goal itself — its prose, its scope
428
- // wall, its category and its visibility. It does NOT carry `is_maintenance`
428
+ // wall, its category, its visibility and its working area (task 1004057 —
429
+ // area 6 on the next version is still area 6). It does NOT carry `is_maintenance`
429
430
  // (the successor version gets its own via ensureMaintenanceGoal) and it
430
431
  // does not carry admissions: an override that widened THIS version's scope
431
432
  // is spent, and re-creating the row would launder a one-time exception into
@@ -435,9 +436,9 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
435
436
  SELECT COALESCE(MAX(sort_order) + 1, 0) AS n FROM goals WHERE version_id = $2
436
437
  )
437
438
  INSERT INTO goals (version_id, title, subtitle, description, scope_modules,
438
- status, visibility, accepting_requests, created_by, sort_order, category_id)
439
+ status, visibility, accepting_requests, created_by, sort_order, category_id, working_area)
439
440
  SELECT $2, g.title, g.subtitle, g.description, g.scope_modules,
440
- 'open', g.visibility, g.accepting_requests, g.created_by, next_order.n, g.category_id
441
+ 'open', g.visibility, g.accepting_requests, g.created_by, next_order.n, g.category_id, g.working_area
441
442
  FROM goals g, next_order
442
443
  WHERE g.id = $1
443
444
  RETURNING id`,
@@ -141,6 +141,7 @@ const dbTasksFacade = Object.fromEntries(
141
141
  const {
142
142
  addDependency,
143
143
  addTaskCriterion,
144
+ addTaskDependencies,
144
145
  addTaskDependency,
145
146
  attachTaskDepSummaries,
146
147
  countTasksWithoutCriterionLink,
@@ -170,6 +171,9 @@ const {
170
171
  gradeByBuilder,
171
172
  gradeTrendByDay,
172
173
  logGradeAttempt,
174
+ gradeAttemptVerdicts,
175
+ workerVerdicts,
176
+ VERDICT_LIMITS,
173
177
  mergeLeaseStatus,
174
178
  recentGradesForBuilder,
175
179
  recentShippedTasks,
@@ -202,6 +206,12 @@ const {
202
206
  listOverrideRequests,
203
207
  } = require('./db-overrides.js');
204
208
  const { applyGrade } = require('./db-grade.js');
209
+ const {
210
+ createRelevanceFlag,
211
+ countPriorRelevanceFlagsForBuilder,
212
+ listPendingRelevanceFlags,
213
+ decideRelevanceFlag,
214
+ } = require('./db-relevance-flags.js');
205
215
  const {
206
216
  getActiveClaimsForBuilder,
207
217
  getActiveClaimsForSession,
@@ -295,6 +305,7 @@ module.exports = {
295
305
  getTaskDependencies,
296
306
  getTaskDependents,
297
307
  attachTaskDepSummaries,
308
+ addTaskDependencies,
298
309
  addTaskDependency,
299
310
  removeTaskDependency,
300
311
  setTaskDependencies,
@@ -354,9 +365,17 @@ module.exports = {
354
365
  listOverrideRequests,
355
366
  decideOverrideRequest,
356
367
  logGradeAttempt,
368
+ gradeAttemptVerdicts, // task 1004292: every round's per-worker verdicts, for the correlation audit
369
+ workerVerdicts,
370
+ VERDICT_LIMITS,
357
371
  countGradeAttempts,
358
372
  gradeAttemptStats, // task 1002661: the grade_attempts remediation-rate reader
359
373
  acquireMergeLease,
360
374
  releaseMergeLease,
361
375
  mergeLeaseStatus,
376
+ // ADR 0101 Part 1 (task 1001691) — the relevance/anti-hack judge's flag table.
377
+ createRelevanceFlag,
378
+ countPriorRelevanceFlagsForBuilder,
379
+ listPendingRelevanceFlags,
380
+ decideRelevanceFlag,
362
381
  };
@@ -78,7 +78,7 @@ async function listCriteria(versionId) {
78
78
  // and criteriaGroupedByGoal so the goals query lives in one place.
79
79
  async function listGoalsForVersion(versionId) {
80
80
  const { rows } = await pool.query(
81
- `SELECT id, title, status, scope_modules
81
+ `SELECT id, title, status, scope_modules, working_area
82
82
  FROM goals
83
83
  WHERE version_id = $1
84
84
  ORDER BY created_at, id`,
@@ -226,7 +226,8 @@ async function criterionProgress(versionId) {
226
226
  }
227
227
 
228
228
  // Group the flat criteria under their goals (Version → Goals → Criteria, ADR 0086
229
- // §2). Pure: takes the goal rows (id, title, status, scope_modules) + the built
229
+ // §2). Pure: takes the goal rows (id, title, status, scope_modules, working_area
230
+ // — the ADR 0264 area number or null, task 1004057) + the built
230
231
  // criteria, returns the goals[] each carrying its criteria in cnum order. A goal
231
232
  // with no criteria is kept (renders gracefully). Criteria whose goal_id matches no
232
233
  // goal row (shouldn't happen post-backfill, but defended) collect under a trailing
@@ -239,6 +240,7 @@ function buildGoals(goalRows, criteria) {
239
240
  title: g.title,
240
241
  status: g.status,
241
242
  scope_modules: g.scope_modules || [],
243
+ working_area: g.working_area == null ? null : Number(g.working_area),
242
244
  criteria: [],
243
245
  });
244
246
  }
@@ -249,7 +251,7 @@ function buildGoals(goalRows, criteria) {
249
251
  }
250
252
  const goals = [...byGoal.values()];
251
253
  if (ungrouped.length) {
252
- goals.push({ id: null, title: '(ungrouped)', status: 'open', scope_modules: [], criteria: ungrouped });
254
+ goals.push({ id: null, title: '(ungrouped)', status: 'open', scope_modules: [], working_area: null, criteria: ungrouped });
253
255
  }
254
256
  return goals;
255
257
  }
@@ -129,10 +129,37 @@ function adviseEstimate({ estMinutes, kind, summary, taskId = null }) {
129
129
  // Lives HERE rather than in db.js: db.js is over the size ratchet's cap and this
130
130
  // is the only caller. The pool comes through the published module doorway.
131
131
  // Deliberately not fail-open — buildEstAdvisory below is the single failure site.
132
- async function calibrationSummary({ pool } = {}) {
132
+ //
133
+ // CACHED (task 1003656): the function is a full historical aggregate (percentile_cont
134
+ // per kind over every shipped row), yet the population only moves when a task ships —
135
+ // not on every create. A short in-process TTL bounds the staleness to a minute (the
136
+ // advisory is a scale hint over ~1,000 rows; one more ship cannot move a median
137
+ // noticeably), concurrent creates share one in-flight query, and a failure is never
138
+ // cached so the next create retries. Keyed on the pool so a test's fake pool is never
139
+ // served another pool's rows.
140
+ const CALIBRATION_TTL_MS = 60 * 1000;
141
+ let calibrationCache = null; // { pool, at, rows } | { pool, inflight }
142
+
143
+ function resetCalibrationCache() { calibrationCache = null; }
144
+
145
+ async function calibrationSummary({ pool, now = Date.now } = {}) {
133
146
  const p = pool || require('../../src/module-api').getPool();
134
- const { rows } = await p.query('SELECT kind, median_ratio, sample_size FROM pms_calibration_summary()');
135
- return rows || [];
147
+ const c = calibrationCache;
148
+ if (c && c.pool === p) {
149
+ if (c.inflight) return c.inflight;
150
+ if (now() - c.at < CALIBRATION_TTL_MS) return c.rows;
151
+ }
152
+ const inflight = p.query('SELECT kind, median_ratio, sample_size FROM pms_calibration_summary()')
153
+ .then(({ rows }) => {
154
+ const out = rows || [];
155
+ calibrationCache = { pool: p, at: now(), rows: out };
156
+ return out;
157
+ }, (e) => {
158
+ if (calibrationCache && calibrationCache.inflight === inflight) calibrationCache = null;
159
+ throw e;
160
+ });
161
+ calibrationCache = { pool: p, inflight };
162
+ return inflight;
136
163
  }
137
164
 
138
165
  // The DB-touching wrapper. NON-BLOCKING BY CONSTRUCTION — any failure yields null
@@ -159,4 +186,4 @@ async function buildEstAdvisory({ db, task } = {}) {
159
186
  }
160
187
  }
161
188
 
162
- module.exports = { adviseEstimate, pickRatio, buildEstAdvisory, calibrationSummary, MIN_MATERIAL_DELTA, SCALE_OK_RATIO, MIN_KIND_SAMPLES, MIN_OVERALL_SAMPLES };
189
+ module.exports = { adviseEstimate, pickRatio, buildEstAdvisory, calibrationSummary, resetCalibrationCache, CALIBRATION_TTL_MS, MIN_MATERIAL_DELTA, SCALE_OK_RATIO, MIN_KIND_SAMPLES, MIN_OVERALL_SAMPLES };
@@ -0,0 +1,153 @@
1
+ // modules/lifecycle/goal-task-relevance-advisory.js — wires the ADR 0101 Part 1
2
+ // judge (goal-task-relevance-judge.js) live on POST /goals/:id/tasks (task
3
+ // 1001691, goal 1000089).
4
+ //
5
+ // FIRE-AND-FORGET, DELIBERATELY — the one place this file departs from the
6
+ // goal-advisory.js / dependency-advisory.js precedent it otherwise follows.
7
+ // Those advisories are pure DB reads and are AWAITED inline so their verdict
8
+ // can ride the 201 response. This one spawns a real model call (a `claude -p`
9
+ // subprocess via grade.runSubagent, seconds at the cheapest rung), and ADR
10
+ // 0101 Part 1 requires the judge be advisory and never slow down — let alone
11
+ // fail — the create it reviews. So runRelevanceJudge is invoked WITHOUT await
12
+ // right after the task is created (goals.js) and reports through the flag
13
+ // table instead of the create response, the same non-blocking, post-commit
14
+ // posture modules/lifecycle/cascade.js already uses for ship-time rules ("it
15
+ // can never fail the ship it reacts to").
16
+ //
17
+ // Swallows every failure — a judge outage must degrade to "no flag was
18
+ // raised", never to a 500 on an already-committed task.
19
+
20
+ 'use strict';
21
+
22
+ const { assessGoalTaskRelevance } = require('./goal-task-relevance-judge.js');
23
+
24
+ // THE SPAWN BUDGET (hacker/efficiency findings, rounds 2-3): an any-builder
25
+ // write (POST /goals/:id/tasks) fires this judge un-awaited, so with no cap
26
+ // here a create-burst (a planning session authoring N goal-tasks in a loop)
27
+ // would fan out to N concurrent `claude -p` subprocesses — unbounded host
28
+ // CPU/memory and unbounded billable spend, worse still once a failing rung
29
+ // escalates (runSubagentAsRunner) rather than aborting after one attempt.
30
+ // Two axes, deliberately scoped DIFFERENTLY:
31
+ // - CONCURRENCY is GLOBAL (host CPU/memory is a genuinely shared resource —
32
+ // capping it per builder would still let N builders each run their own
33
+ // cap concurrently and sum past the host's real limit).
34
+ // - the RATE WINDOW is PER BUILDER (round-3 finding: a global window meant
35
+ // the exact actor this control polices could burst MAX_JUDGES_PER_WINDOW
36
+ // innocuous creates, then file the gaming task while the window sat
37
+ // exhausted — invisible AND collateral, since it blinded every OTHER
38
+ // builder's judge too, not just their own). Per-builder scoping removes
39
+ // the cross-builder blast radius entirely; it does NOT remove a single
40
+ // actor's ability to exhaust their OWN window before their one gaming
41
+ // task — an accepted residual, consistent with the ADR's own "a wrong or
42
+ // missed judge call costs a review, never a builder's ability to work"
43
+ // stance (Part 1's whole reason to be advisory rather than a hard gate).
44
+ // Process-local, in-memory — no new table, no cross-request coordination
45
+ // needed for a single instance's app host. Over either cap, the judge is
46
+ // SKIPPED (never queued, never blocking) — advisory means the check itself
47
+ // may go missing under load, exactly like a judge outage degrading to "no
48
+ // flag was raised". The per-builder map is capped (evicts the oldest entry)
49
+ // so an unbounded stream of distinct builder ids can't grow it forever.
50
+ const MAX_CONCURRENT_JUDGES = 2;
51
+ const MAX_JUDGES_PER_WINDOW_PER_BUILDER = 10;
52
+ const WINDOW_MS = 60_000;
53
+ const MAX_TRACKED_BUILDERS = 5_000;
54
+ let inFlight = 0;
55
+ const perBuilderWindows = new Map(); // builderId -> { windowStartedAt, count }
56
+
57
+ function judgeBudgetAllows(builderId) {
58
+ if (inFlight >= MAX_CONCURRENT_JUDGES) return false;
59
+ const key = builderId == null ? 'anonymous' : String(builderId);
60
+ const now = Date.now();
61
+ const entry = perBuilderWindows.get(key);
62
+ if (!entry || now - entry.windowStartedAt >= WINDOW_MS) return true; // fresh window
63
+ return entry.count < MAX_JUDGES_PER_WINDOW_PER_BUILDER;
64
+ }
65
+
66
+ // consumeJudgeBudget — call ONLY after judgeBudgetAllows() passed, atomically
67
+ // with that check (both run synchronously in runRelevanceJudge, no await
68
+ // between them, so two concurrent calls can't both pass the check and both
69
+ // consume against a stale count).
70
+ //
71
+ // ALWAYS delete-then-set, never mutate an existing entry in place (round-4
72
+ // quality finding): a Map keeps a key's ORIGINAL insertion position across a
73
+ // plain `.set()` on an already-present key, so mutating `entry.count` in
74
+ // place left a persistently active builder's key frozen at the front of
75
+ // iteration order forever — exactly the "oldest" position the eviction below
76
+ // deletes from, so the MOST active builder was the first one evicted once
77
+ // MAX_TRACKED_BUILDERS distinct ids had ever been seen. Deleting before
78
+ // re-setting moves a touched key to the END, so the front of the map is
79
+ // always the genuinely least-recently-touched builder — real LRU eviction.
80
+ function consumeJudgeBudget(builderId) {
81
+ const key = builderId == null ? 'anonymous' : String(builderId);
82
+ const now = Date.now();
83
+ const entry = perBuilderWindows.get(key);
84
+ const fresh = !entry || now - entry.windowStartedAt >= WINDOW_MS;
85
+ perBuilderWindows.delete(key);
86
+ perBuilderWindows.set(key, fresh ? { windowStartedAt: now, count: 1 } : { windowStartedAt: entry.windowStartedAt, count: entry.count + 1 });
87
+ if (perBuilderWindows.size > MAX_TRACKED_BUILDERS) {
88
+ perBuilderWindows.delete(perBuilderWindows.keys().next().value);
89
+ }
90
+ }
91
+
92
+ // runRelevanceJudge — the whole non-blocking pipeline: resolve the grade port
93
+ // (best-effort — a grading-disabled instance simply runs no judge), read the
94
+ // builder's prior-flag count, assess, and persist a flag row IF the verdict
95
+ // was 'flag'. An 'ok' verdict writes nothing (the idea_inbox precedent: only
96
+ // what needs a human's attention becomes a row).
97
+ async function runRelevanceJudge({ db, api, goal, task }) {
98
+ const log = api.logger('lifecycle');
99
+ try {
100
+ const grade = api.resolveOptional('grade');
101
+ if (!grade) return null; // grading module off — no judge, not an error
102
+ if (!judgeBudgetAllows(task.created_by)) {
103
+ log.info('relevance-judge skipped (over the in-flight/per-builder-rate budget)', { task_id: task.id, builder_id: task.created_by });
104
+ return null;
105
+ }
106
+ consumeJudgeBudget(task.created_by);
107
+ inFlight += 1;
108
+ let assessment;
109
+ try {
110
+ const priorFlagCount = await db.countPriorRelevanceFlagsForBuilder(task.created_by);
111
+ assessment = await assessGoalTaskRelevance({
112
+ goal, task, priorFlagCount, grade, cascadeDispatch: api.cascadeDispatch,
113
+ });
114
+ } finally {
115
+ inFlight -= 1;
116
+ }
117
+ // Spend visibility (src/bongos/CLAUDE.md: "a new spend path that skips
118
+ // these is invisible to the ledger"). An 'ok' verdict writes no flag row
119
+ // (only what needs a human's attention becomes one), so its cost would
120
+ // otherwise vanish entirely — logged here at minimum. It does NOT reach
121
+ // cost_log: that table is written by builder-facing CLI flows through an
122
+ // authenticated session (scripts/gds/ship-grade-steps.js), and this judge
123
+ // runs inside the live server with no such session to post through; wiring
124
+ // a live-server spend port for the economy module is real work beyond this
125
+ // task's scope, tracked as a known gap rather than silently left unstated.
126
+ if (assessment) {
127
+ log.info('relevance-judge spend', { task_id: task.id, model: assessment.model, cost_usd: assessment.cost_usd, verdict: assessment.verdict });
128
+ }
129
+ if (!assessment || assessment.verdict !== 'flag') return null;
130
+ return await db.createRelevanceFlag({
131
+ taskId: task.id,
132
+ goalId: goal.id,
133
+ builderId: task.created_by,
134
+ confidence: assessment.confidence,
135
+ reasoning: assessment.reasoning,
136
+ model: assessment.model,
137
+ costUsd: assessment.cost_usd,
138
+ frictionRequired: assessment.friction_required,
139
+ });
140
+ } catch (e) {
141
+ log.error('relevance-judge (non-blocking, post-create)', e && e.message);
142
+ return null;
143
+ }
144
+ }
145
+
146
+ // Exported for tests/goal_task_relevance.mjs — pure state, no pool, so the
147
+ // budget logic is unit-tested without spawning anything.
148
+ function _resetJudgeBudgetForTests() {
149
+ inFlight = 0;
150
+ perBuilderWindows.clear();
151
+ }
152
+
153
+ module.exports = { runRelevanceJudge, _resetJudgeBudgetForTests };