@bongos/core 1.19.1081 → 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.
- package/.bongos-core.json +526 -191
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +32 -0
- package/clients/bongos-client/index.cjs +32 -0
- package/clients/bongos-client/index.d.ts +51 -6
- package/clients/bongos-client/index.mjs +32 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +851 -19
- package/docs/api-reference.md +33 -9
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +6 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1674 -1566
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +44 -1
- package/modules/provisioning/routes/render-standup.js +81 -6
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +163 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +62 -31
- package/scripts/gds/provision-teardown.js +7 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +10 -10
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +19 -0
- package/scripts/gds/render-payload.js +39 -65
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +121 -28
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +126 -6
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- 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
|
|
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`,
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
135
|
-
|
|
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 };
|