@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// modules/grading/grade-engine.js — the central grading engine: ONE named
|
|
2
|
+
// pipeline every work-type is graded through, and the registry a work-type's
|
|
3
|
+
// grader registers into (ADR 0089 §1, task 1002839).
|
|
4
|
+
//
|
|
5
|
+
// WHY. ADR 0089 decided the grader is a central, work-type-parameterized engine:
|
|
6
|
+
// the orchestration, the deterministic security pre-passes, the gate and the
|
|
7
|
+
// persisted record are written once, and a work-type contributes only what is
|
|
8
|
+
// genuinely its own — its judges, the evidence they read, and how their verdicts
|
|
9
|
+
// combine. Until this file that pipeline existed only as the body of
|
|
10
|
+
// gradeSubagent, hard-wired to the code panel, so a second work-type had no way
|
|
11
|
+
// in. The stages below are the same ones, in the same order, now named.
|
|
12
|
+
//
|
|
13
|
+
// THE PIPELINE (PIPELINE_STAGES, in order):
|
|
14
|
+
// select resolve the work-type: an explicit key, else task.discipline, else
|
|
15
|
+
// task.kind, else the default (the code panel).
|
|
16
|
+
// pre_pass the deterministic route-rank + permission-path checks. ENGINE-OWNED
|
|
17
|
+
// and zero-LLM: a work-type cannot skip, reorder or soften them.
|
|
18
|
+
// evidence the work-type gathers what its judges read (code: the post-change
|
|
19
|
+
// file bundle + generated-file provenance, read once).
|
|
20
|
+
// fan_out every judge runs independently, in parallel.
|
|
21
|
+
// aggregate the work-type combines its judges' verdicts (code: aggregatePanel).
|
|
22
|
+
// gate ENGINE-OWNED deterministic veto: a pre-pass that hard-failed fails
|
|
23
|
+
// the grade whatever the aggregate said — no verdict, from any model,
|
|
24
|
+
// upgrades a fail over a protected path (ADR 0016 / 0043 / 0080). For
|
|
25
|
+
// the code panel it never fires, because aggregatePanel already fails
|
|
26
|
+
// on a deterministic fail; it is what makes the guarantee hold for a
|
|
27
|
+
// work-type whose aggregate does not.
|
|
28
|
+
// persist shape the record the lifecycle stores: per-judge signals, cost, and
|
|
29
|
+
// the run meta. Grading owns no table (ADR 0091 §4) — the write itself
|
|
30
|
+
// stays db.applyGrade behind POST /tasks/:id/grade, where
|
|
31
|
+
// serverSideGuards re-runs the same pre-passes on authoritative data.
|
|
32
|
+
//
|
|
33
|
+
// THE REGISTRY HAS TWO DOORS, because ADR 0083 forbids one module requiring
|
|
34
|
+
// another. The grading module's own work-types register directly
|
|
35
|
+
// (registerWorkType — the code panel does, from grader.js). Any other module
|
|
36
|
+
// contributes one as a provider on the kernel seam port `grader.workType.<key>`,
|
|
37
|
+
// through the doorway — the only door it has. resolveWorkType reads both, local
|
|
38
|
+
// first, so a seam reset (tests do one) can never lose the default.
|
|
39
|
+
//
|
|
40
|
+
// REPLAY. replayRecorded re-runs aggregate + gate over a grade's PERSISTED
|
|
41
|
+
// per-judge signals — zero model calls. It is how a change to either stage is
|
|
42
|
+
// proven against real recorded grades before it touches the live gate
|
|
43
|
+
// (scripts/gds/grade-replay.js, tests/grade_engine_replay.mjs).
|
|
44
|
+
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
const api = require('../../src/module-api');
|
|
48
|
+
const { ASSUMED_BUILDER_MODEL, permissionPathPrePass, routeRankPrePass } = require('./grader-config.js');
|
|
49
|
+
const { selectGraderModel } = require('./grader-model.js');
|
|
50
|
+
const { extractAssistantResult, runSubagentCached } = require('./grader-subagent.js');
|
|
51
|
+
|
|
52
|
+
const PIPELINE_STAGES = Object.freeze(['select', 'pre_pass', 'evidence', 'fan_out', 'aggregate', 'gate', 'persist']);
|
|
53
|
+
|
|
54
|
+
const WORK_TYPE_PORT_PREFIX = 'grader.workType.';
|
|
55
|
+
const WORK_TYPE_KEY_RE = /^[a-z][a-z0-9_-]{0,39}$/;
|
|
56
|
+
// What a work-type must supply. gatherEvidence and recordEvidence are optional
|
|
57
|
+
// (a work-type whose judges read only the task + diff needs neither).
|
|
58
|
+
const REQUIRED_HOOKS = Object.freeze(['resolveJudges', 'modelFor', 'runJudge', 'aggregate', 'unavailable']);
|
|
59
|
+
// The pseudo-judge kinds the deterministic pre-passes produce. Replay rebuilds
|
|
60
|
+
// the pre-pass results from them so its gate judges what the live gate judged.
|
|
61
|
+
const DETERMINISTIC_KINDS = Object.freeze(['route-rank', 'permission-path']);
|
|
62
|
+
|
|
63
|
+
const local = new Map(); // key -> work-type registered directly by this module
|
|
64
|
+
let defaultKey = null;
|
|
65
|
+
|
|
66
|
+
function workTypePort(key) {
|
|
67
|
+
return `${WORK_TYPE_PORT_PREFIX}${key}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Refuse a malformed work-type at REGISTRATION, not on the first ship that
|
|
71
|
+
// resolves it — a missing hook would otherwise surface as a TypeError mid-grade.
|
|
72
|
+
function validateWorkType(def) {
|
|
73
|
+
if (!def || typeof def !== 'object') throw new TypeError('grade engine: a work-type must be an object');
|
|
74
|
+
if (typeof def.key !== 'string' || !WORK_TYPE_KEY_RE.test(def.key)) {
|
|
75
|
+
throw new TypeError(`grade engine: work-type key ${JSON.stringify(def.key)} must match ${WORK_TYPE_KEY_RE}`);
|
|
76
|
+
}
|
|
77
|
+
const missing = REQUIRED_HOOKS.filter((h) => typeof def[h] !== 'function');
|
|
78
|
+
for (const h of ['gatherEvidence', 'recordEvidence']) {
|
|
79
|
+
if (def[h] != null && typeof def[h] !== 'function') missing.push(h);
|
|
80
|
+
}
|
|
81
|
+
if (missing.length) throw new TypeError(`grade engine: work-type '${def.key}' is missing hook(s): ${missing.join(', ')}`);
|
|
82
|
+
return def;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Register one of THIS module's work-types. Idempotent for the same definition
|
|
86
|
+
// (grader.js is required from several places); a second, different definition
|
|
87
|
+
// for a key is a wiring bug and throws, like a duplicate seam provider.
|
|
88
|
+
function registerWorkType(def, { isDefault = false } = {}) {
|
|
89
|
+
validateWorkType(def);
|
|
90
|
+
const held = local.get(def.key);
|
|
91
|
+
if (held && held !== def) throw new Error(`grade engine: work-type '${def.key}' is already registered`);
|
|
92
|
+
local.set(def.key, def);
|
|
93
|
+
if (isDefault) defaultKey = def.key;
|
|
94
|
+
return def;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A work-type contributed by another module through the seam, or null. An
|
|
98
|
+
// invalid contribution is refused loudly and treated as absent, so a broken
|
|
99
|
+
// module degrades to the default grader instead of crashing every grade.
|
|
100
|
+
function seamWorkType(key) {
|
|
101
|
+
const port = workTypePort(key);
|
|
102
|
+
if (!api.hasProvider(port)) return null;
|
|
103
|
+
const def = api.resolve(port);
|
|
104
|
+
try {
|
|
105
|
+
validateWorkType(def);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(`grade engine: ignoring invalid work-type on port ${port}: ${err.message}`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return def.key === key ? def : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function lookup(key) {
|
|
114
|
+
if (typeof key !== 'string' || !key) return null;
|
|
115
|
+
return local.get(key) || seamWorkType(key);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// select — explicit key, then the task's discipline, then its kind, then the
|
|
119
|
+
// default. Never throws once a default is registered: an unknown or mistyped
|
|
120
|
+
// key grades on the default rather than stranding the ship (the task 1002659
|
|
121
|
+
// posture — a grade step that throws strands the ship).
|
|
122
|
+
function resolveWorkType({ workType, task } = {}) {
|
|
123
|
+
if (workType && typeof workType === 'object') return validateWorkType(workType);
|
|
124
|
+
const t = task || {};
|
|
125
|
+
for (const key of [workType, t.discipline, t.kind]) {
|
|
126
|
+
const def = lookup(key);
|
|
127
|
+
if (def) return def;
|
|
128
|
+
}
|
|
129
|
+
const def = defaultKey ? local.get(defaultKey) : null;
|
|
130
|
+
if (!def) throw new Error('grade engine: no default work-type registered (modules/grading/grader.js registers the code panel)');
|
|
131
|
+
return def;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Every work-type the engine can currently resolve, sorted — local and seam.
|
|
135
|
+
function listWorkTypes() {
|
|
136
|
+
const keys = new Set(local.keys());
|
|
137
|
+
for (const port of api.listPorts()) {
|
|
138
|
+
if (!port.startsWith(WORK_TYPE_PORT_PREFIX)) continue;
|
|
139
|
+
const key = port.slice(WORK_TYPE_PORT_PREFIX.length);
|
|
140
|
+
if (seamWorkType(key)) keys.add(key);
|
|
141
|
+
}
|
|
142
|
+
return [...keys].sort();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// pre_pass — both deterministic checks, in this order, for every work-type. Each
|
|
146
|
+
// is null (zero cost) when its trigger is absent; the rest of the pipeline sees
|
|
147
|
+
// only the ones that ran.
|
|
148
|
+
function runPrePasses({ changedFiles, builderRank }) {
|
|
149
|
+
return [routeRankPrePass(changedFiles), permissionPathPrePass(changedFiles, builderRank)].filter(Boolean);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// gate — the deterministic veto. Downgrade-only: it can turn a non-fail into a
|
|
153
|
+
// fail, never the reverse, and it returns the aggregate UNTOUCHED when there is
|
|
154
|
+
// nothing to enforce (always, for the code panel).
|
|
155
|
+
function applyDeterministicGate(result, prePasses) {
|
|
156
|
+
const failed = prePasses.filter((p) => p && p.workerResult && p.workerResult.verdict === 'fail');
|
|
157
|
+
if (!failed.length) return result;
|
|
158
|
+
const signals = (result && result.signals) || {};
|
|
159
|
+
if (result && result.passed !== true && signals.panel_outcome === 'fail') return result;
|
|
160
|
+
const kinds = failed.map((p) => p.workerResult.kind);
|
|
161
|
+
return {
|
|
162
|
+
...result,
|
|
163
|
+
passed: false,
|
|
164
|
+
signals: { ...signals, panel_outcome: 'fail', confidence: 0, deterministic_veto: kinds },
|
|
165
|
+
notes_md: `${(result && result.notes_md) || ''}\n\nGrade engine — DETERMINISTIC VETO: ${kinds.join(', ')} hard-failed, so this grade FAILS whatever the judges said (ADR 0016 / 0043 / 0080: no verdict may upgrade a fail over a protected path).`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// persist — append the run meta to the record's signals. Appended AFTER
|
|
170
|
+
// aggregate and gate, so the key order is exactly what aggregatePanel produced
|
|
171
|
+
// when it was handed the meta directly.
|
|
172
|
+
function persist(result, meta) {
|
|
173
|
+
return { ...result, signals: { ...(result.signals || {}), ...meta } };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The model call every judge makes: runSubagent wrapped in the content-addressed
|
|
177
|
+
// LLM cache (System 5) — the same prompt + model replays at $0. Absent llm-cache,
|
|
178
|
+
// a plain run. `taskType` is part of the cache key, so a work-type keeps its own.
|
|
179
|
+
function cachedModelCall(taskType) {
|
|
180
|
+
return ({ prompt, opts }) => runSubagentCached({
|
|
181
|
+
prompt,
|
|
182
|
+
opts,
|
|
183
|
+
cache: api.llmCache
|
|
184
|
+
? api.llmCache.makeCacheSpec({ prompt, model: opts?.model, task_type: taskType })
|
|
185
|
+
: null,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The whole pipeline for one grade. `deps.runSubagent` replaces the model call
|
|
190
|
+
// (tests; production never passes it). Returns the work-type's grade-result.
|
|
191
|
+
async function runGrade(input = {}, deps = {}) {
|
|
192
|
+
const { task, valueSummary, diff, changedFiles, diffStats, judges, builderModel, builderRank, priorRound, runOpts = {} } = input;
|
|
193
|
+
|
|
194
|
+
// select
|
|
195
|
+
const wt = resolveWorkType({ workType: input.workType, task });
|
|
196
|
+
|
|
197
|
+
// #943: bypass / disabled short-circuit — BEFORE resolving judges or spawning
|
|
198
|
+
// anything. Spawning into an outage/bypass window is what manufactured the
|
|
199
|
+
// "FAIL · 0/10" false verdict in session 1659.
|
|
200
|
+
if (runOpts.bypass === true || process.env.GRADER_BYPASS_ENABLED === '1') {
|
|
201
|
+
const reason = runOpts.bypass === true ? 'bypass_requested' : 'grader_bypass_enabled';
|
|
202
|
+
return wt.unavailable({
|
|
203
|
+
reason,
|
|
204
|
+
note: 'Grader bypassed before worker spawn (GRADER_BYPASS_ENABLED / runOpts.bypass) — no panel ran. Land via manual-confirm (/confirm).',
|
|
205
|
+
meta: { builder_model_assumed: builderModel || ASSUMED_BUILDER_MODEL, bypassed: true },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const panel = wt.resolveJudges(judges);
|
|
209
|
+
const graderModel = selectGraderModel({ builderModel });
|
|
210
|
+
|
|
211
|
+
// pre_pass
|
|
212
|
+
const prePasses = runPrePasses({ changedFiles, builderRank });
|
|
213
|
+
const deterministicFindings = prePasses.flatMap((p) => p.findings);
|
|
214
|
+
|
|
215
|
+
// evidence
|
|
216
|
+
const ctx = { task, valueSummary, diff, changedFiles, diffStats, builderModel, builderRank, priorRound, runOpts, graderModel, deterministicFindings };
|
|
217
|
+
const evidence = wt.gatherEvidence ? await wt.gatherEvidence(ctx) : {};
|
|
218
|
+
|
|
219
|
+
// fan_out
|
|
220
|
+
const runSubagentFn = deps.runSubagent || cachedModelCall(wt.cacheTaskType || `grade_${wt.key}`);
|
|
221
|
+
const startedAt = Date.now();
|
|
222
|
+
const results = await Promise.all(panel.map((judge) => wt.runJudge(judge, {
|
|
223
|
+
...ctx,
|
|
224
|
+
evidence,
|
|
225
|
+
model: wt.modelFor(judge, graderModel, builderModel),
|
|
226
|
+
runSubagentFn,
|
|
227
|
+
extractAssistantResultFn: extractAssistantResult,
|
|
228
|
+
})));
|
|
229
|
+
// The deterministic checks join as pseudo-judges, after the model judges, so
|
|
230
|
+
// an aggregate's any-fail rule sees them like any other verdict.
|
|
231
|
+
for (const p of prePasses) results.push(p.workerResult);
|
|
232
|
+
const totalMs = Date.now() - startedAt;
|
|
233
|
+
|
|
234
|
+
// aggregate → gate → persist
|
|
235
|
+
const gated = applyDeterministicGate(wt.aggregate(results, {}), prePasses);
|
|
236
|
+
return persist(gated, {
|
|
237
|
+
builder_model_assumed: builderModel || ASSUMED_BUILDER_MODEL,
|
|
238
|
+
grader_model: graderModel,
|
|
239
|
+
total_ms: totalMs,
|
|
240
|
+
...(wt.recordEvidence ? wt.recordEvidence(evidence, ctx) : {}),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Re-run aggregate + gate over a grade's persisted per-judge signals
|
|
245
|
+
// (signals.workers — the deterministic pseudo-judges included). Pure: no model
|
|
246
|
+
// call, no I/O. The pre-pass results are rebuilt from the recorded pseudo-judges,
|
|
247
|
+
// so the gate judges exactly what the live run's gate judged.
|
|
248
|
+
function replayRecorded({ signals, workType } = {}) {
|
|
249
|
+
const wt = resolveWorkType({ workType });
|
|
250
|
+
const judges = Array.isArray(signals && signals.workers) ? signals.workers : [];
|
|
251
|
+
const prePasses = judges
|
|
252
|
+
.filter((w) => w && w.model === 'deterministic' && DETERMINISTIC_KINDS.includes(w.kind))
|
|
253
|
+
.map((w) => ({ findings: w.issues || [], workerResult: w }));
|
|
254
|
+
return applyDeterministicGate(wt.aggregate(judges, {}), prePasses);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
PIPELINE_STAGES,
|
|
259
|
+
WORK_TYPE_PORT_PREFIX,
|
|
260
|
+
applyDeterministicGate,
|
|
261
|
+
listWorkTypes,
|
|
262
|
+
registerWorkType,
|
|
263
|
+
replayRecorded,
|
|
264
|
+
resolveWorkType,
|
|
265
|
+
runGrade,
|
|
266
|
+
workTypePort,
|
|
267
|
+
};
|
|
@@ -19,9 +19,10 @@ function runSubagent({ prompt, opts = {} }) {
|
|
|
19
19
|
const timeoutMs = opts.timeoutMs ?? 240_000;
|
|
20
20
|
const cwd = opts.cwd ?? process.cwd();
|
|
21
21
|
const model = opts.model ?? DEFAULT_GRADER_MODEL;
|
|
22
|
-
// Env escape hatch (
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// Env escape hatch (<PREFIX>_CLAUDE_BIN — BONGOS_ on a vanilla instance, the
|
|
23
|
+
// legacy OTB_ still read — then the unprefixed CLAUDE_BIN) for installs where the
|
|
24
|
+
// CLI isn't named `claude` on PATH — covers any OS, not just Windows.
|
|
25
|
+
const bin = opts.claudeBin ?? api.resolveEnv('CLAUDE_BIN') ?? process.env.CLAUDE_BIN ?? 'claude';
|
|
25
26
|
|
|
26
27
|
// THE PERMISSION MODE IS ROOT-DEPENDENT (task 1003574). `bypassPermissions` maps
|
|
27
28
|
// to --dangerously-skip-permissions, and the CLI REFUSES that when euid is 0:
|
|
@@ -87,8 +87,22 @@ const TRANSIENT_WORKER_ERRORS = new Set([
|
|
|
87
87
|
// errored no-finding Quality — the tag is the diagnosis, not the trigger).
|
|
88
88
|
// The pattern is the API's exact sentence, so a diff that merely mentions the
|
|
89
89
|
// phrase cannot trip it.
|
|
90
|
-
|
|
90
|
+
//
|
|
91
|
+
// task 1003648: the separator is NOT a colon. The line the CLI really prints —
|
|
92
|
+
// stored verbatim on task 1003298's grade row — is
|
|
93
|
+
// "Prompt is too long · the request is ~251248 tokens (limit 200000) ..."
|
|
94
|
+
// and the colon form above was a paraphrase, so the tag never fired on a real
|
|
95
|
+
// overflow: it was filed as SUBAGENT_NONZERO_EXIT and retried as if transient.
|
|
96
|
+
// Any short run of non-word characters between the halves matches both.
|
|
97
|
+
const PROMPT_TOO_LONG_RE = /prompt is too long\W{1,5}the request is ~?\d+ tokens/i;
|
|
91
98
|
const PROMPT_TOO_LONG = 'PROMPT_TOO_LONG';
|
|
99
|
+
// The numbers in that same sentence, recorded so a bounded row says how far
|
|
100
|
+
// over the window it was. Absent from a line that does not carry them.
|
|
101
|
+
const OVERFLOW_NUMBERS_RE = /the request is ~?(\d+) tokens \(limit (\d+)\)/i;
|
|
102
|
+
function overflowNumbers(said) {
|
|
103
|
+
const m = OVERFLOW_NUMBERS_RE.exec(String(said || ''));
|
|
104
|
+
return m ? { request_tokens: Number(m[1]), window_tokens: Number(m[2]) } : {};
|
|
105
|
+
}
|
|
92
106
|
|
|
93
107
|
// #467: per-worker subagent timeout. The global 240s applied to EVERY worker,
|
|
94
108
|
// which timed out the Read-heavy ones (Narc walks ACs + reads files — hence
|
|
@@ -195,13 +209,14 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
195
209
|
// builder response for a delta-aware re-grade; every worker renders it via
|
|
196
210
|
// the shared scaffold. contextBundle (task 1002660) likewise: the post-change
|
|
197
211
|
// file contents read once by the caller, rendered by the shared scaffold.
|
|
198
|
-
const
|
|
212
|
+
const promptInputs = { task, valueSummary, diff, changedFiles, diffStats, deterministicFindings, priorRound, contextBundle };
|
|
213
|
+
const prompt = worker.buildPrompt(promptInputs);
|
|
199
214
|
|
|
200
215
|
// One subagent attempt → verdict. Pulled out so #465 can re-run it once on a
|
|
201
216
|
// transient error without duplicating the spawn/parse/verdict plumbing.
|
|
202
217
|
// #467: per-worker timeout (Read-heavy workers get longer; see the map above).
|
|
203
218
|
const timeoutMs = timeoutForWorker(worker, runOpts);
|
|
204
|
-
async function attempt() {
|
|
219
|
+
async function attempt(promptText) {
|
|
205
220
|
let raw = null;
|
|
206
221
|
let runMs = null;
|
|
207
222
|
let costUsd = null;
|
|
@@ -209,9 +224,10 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
209
224
|
// task 1003313: the WHY behind subprocessError. Null on a clean run.
|
|
210
225
|
let exitCode = null;
|
|
211
226
|
let stderrTail = null;
|
|
227
|
+
let overflow = null; // task 1003648: the refusal's own numbers, on a PROMPT_TOO_LONG only
|
|
212
228
|
const t0 = Date.now();
|
|
213
229
|
try {
|
|
214
|
-
const { stdout, run_ms, cost_usd } = await runSubagentFn({ prompt, opts: { ...runOpts, model, timeoutMs } });
|
|
230
|
+
const { stdout, run_ms, cost_usd } = await runSubagentFn({ prompt: promptText, opts: { ...runOpts, model, timeoutMs } });
|
|
215
231
|
runMs = run_ms;
|
|
216
232
|
// #486 (ADR 0028 path 1): runSubagent attaches the `claude -p` envelope's
|
|
217
233
|
// total_cost_usd. Carry it out so aggregatePanel can sum the panel's own
|
|
@@ -232,7 +248,10 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
232
248
|
// the #467 budget signal, never a refusal.
|
|
233
249
|
if (err.code !== 'SUBAGENT_TIMEOUT') {
|
|
234
250
|
const said = String(err.stderr || '').trim() || String(err.stdout || '');
|
|
235
|
-
if (PROMPT_TOO_LONG_RE.test(said))
|
|
251
|
+
if (PROMPT_TOO_LONG_RE.test(said)) {
|
|
252
|
+
subprocessError = PROMPT_TOO_LONG;
|
|
253
|
+
overflow = overflowNumbers(said);
|
|
254
|
+
}
|
|
236
255
|
}
|
|
237
256
|
}
|
|
238
257
|
const parsed = subprocessError
|
|
@@ -243,18 +262,47 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
243
262
|
run_ms: runMs,
|
|
244
263
|
subprocess_error: subprocessError,
|
|
245
264
|
});
|
|
246
|
-
return { verdict, runMs, costUsd, exitCode, stderrTail };
|
|
265
|
+
return { verdict, runMs, costUsd, exitCode, stderrTail, overflow };
|
|
247
266
|
}
|
|
248
267
|
|
|
249
|
-
let { verdict, runMs, costUsd, exitCode, stderrTail } = await attempt();
|
|
250
268
|
// #465: retry ONCE on a transient error (the freshest attempt wins, even if it
|
|
251
269
|
// also errored — one retry only). A real verdict or a SUBAGENT_TIMEOUT is
|
|
252
270
|
// never retried. NB (#486): the freshest attempt's cost wins; a transient
|
|
253
271
|
// first attempt's spend is not added (slight, rare undercount).
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
retried
|
|
257
|
-
|
|
272
|
+
async function attemptWithRetry(promptText) {
|
|
273
|
+
const first = await attempt(promptText);
|
|
274
|
+
if (!TRANSIENT_WORKER_ERRORS.has(first.verdict.error)) return { ...first, retried: false };
|
|
275
|
+
return { ...(await attempt(promptText)), retried: true };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let { verdict, runMs, costUsd, exitCode, stderrTail, overflow, retried } = await attemptWithRetry(prompt);
|
|
279
|
+
|
|
280
|
+
// task 1003648 — a prompt too big for this worker's model window degrades
|
|
281
|
+
// instead of failing outright. The CLI measures the real window and the real
|
|
282
|
+
// overhead (its own system prompt, tool definitions and auto-loaded context —
|
|
283
|
+
// ~150k tokens of task 1003298's 251k-token refusal), so the refusal is the
|
|
284
|
+
// authoritative "does not fit"; the one part of the prompt that can go without
|
|
285
|
+
// losing any evidence is the post-change file bundle, because the worker can
|
|
286
|
+
// Read those files itself. So: retry ONCE without it. The diff, the task and the
|
|
287
|
+
// prior round all stay — the retried worker reviews the same change, it just
|
|
288
|
+
// fetches the files instead of being handed them. If even that does not fit,
|
|
289
|
+
// the worker stays PROMPT_TOO_LONG: an explicit lens-unavailable, never a score.
|
|
290
|
+
// No bundle to drop → nothing smaller to try, so no second refusal is spent.
|
|
291
|
+
let promptBounded = null;
|
|
292
|
+
const bundledFiles = contextBundle && Array.isArray(contextBundle.files) ? contextBundle.files : [];
|
|
293
|
+
if (verdict.error === PROMPT_TOO_LONG && bundledFiles.length > 0) {
|
|
294
|
+
// Generated-file provenance is verified, trusted text — it stays.
|
|
295
|
+
const smaller = worker.buildPrompt({ ...promptInputs, contextBundle: { ...contextBundle, files: [] } });
|
|
296
|
+
promptBounded = {
|
|
297
|
+
reason: PROMPT_TOO_LONG,
|
|
298
|
+
bundle_dropped: true,
|
|
299
|
+
bundle_files: bundledFiles.length,
|
|
300
|
+
prompt_chars: prompt.length,
|
|
301
|
+
retry_prompt_chars: smaller.length,
|
|
302
|
+
...(overflow || {}),
|
|
303
|
+
};
|
|
304
|
+
({ verdict, runMs, costUsd, exitCode, stderrTail, overflow, retried } = await attemptWithRetry(smaller));
|
|
305
|
+
promptBounded.retry_error = verdict.error ?? null;
|
|
258
306
|
}
|
|
259
307
|
|
|
260
308
|
return {
|
|
@@ -277,6 +325,14 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
277
325
|
...(verdict.error && stderrTail ? { stderr_tail: stderrTail } : {}),
|
|
278
326
|
// Narc-specific (carried opaquely; aggregatePanel doesn't need to know):
|
|
279
327
|
...(verdict.ac_checklist ? { ac_checklist: verdict.ac_checklist } : {}),
|
|
328
|
+
// task 1002903: the severity gate's marker. Rebuilding the row field by field
|
|
329
|
+
// used to drop it, so a stored grade could not say which low score was the
|
|
330
|
+
// one no finding backed — only the worker's prose did. Omitted when absent,
|
|
331
|
+
// so an ordinary worker row keeps its exact shape.
|
|
332
|
+
...(verdict.policy_downgrade ? { policy_downgrade: verdict.policy_downgrade } : {}),
|
|
333
|
+
// task 1003648: present only when the first prompt overflowed the window and
|
|
334
|
+
// a smaller one was tried, so an ordinary row keeps its exact shape.
|
|
335
|
+
...(promptBounded ? { prompt_bounded: promptBounded } : {}),
|
|
280
336
|
};
|
|
281
337
|
}
|
|
282
338
|
|
|
@@ -57,6 +57,10 @@ function finalizeVerdict({ verdict, scores, issues, extra = {}, notesPrefix, not
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// The marker the policy below stamps on a worker it downgraded. Exported so every
|
|
61
|
+
// reader (aggregatePanel, the ship card) names the one string instead of retyping it.
|
|
62
|
+
const UNBACKED_SCORE_DOWNGRADE = 'subthreshold_score_without_substantive_finding';
|
|
63
|
+
|
|
60
64
|
// #1002663 severity-gate policy: style findings are advisory, and a
|
|
61
65
|
// sub-threshold score must be BACKED by a substantive finding. Every worker
|
|
62
66
|
// prompt already demands "a score below threshold must be accompanied by a
|
|
@@ -81,9 +85,35 @@ function applySeverityGatePolicy(result, { threshold, substantiveFail = false })
|
|
|
81
85
|
return {
|
|
82
86
|
...result,
|
|
83
87
|
verdict: 'concerns',
|
|
84
|
-
policy_downgrade:
|
|
88
|
+
policy_downgrade: UNBACKED_SCORE_DOWNGRADE,
|
|
85
89
|
notes_md: `${result.notes_md}\n[severity-gate] sub-threshold score with no major/blocker finding — fail downgraded to concerns (style findings are advisory)`.trim(),
|
|
86
90
|
};
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
// task 1002903 — the UNBACKED low scores in a panel: every sub-threshold axis of a
|
|
94
|
+
// worker the policy above downgraded, as [{ kind, axis, score }] in worker order.
|
|
95
|
+
//
|
|
96
|
+
// WHY THIS EXISTS. A downgraded worker still carries its low score, and that score
|
|
97
|
+
// still reaches the record (the axis median, the blended score) and the server's
|
|
98
|
+
// ff/cq gate. So the grade reads "FAIL 5.5" with no finding behind it, and the card
|
|
99
|
+
// told the builder to "fix the issues above" when there were none — the shape task
|
|
100
|
+
// 1002721 was filed as. Naming these lets the record and the card say what they
|
|
101
|
+
// are: a low mark nobody could point at, not a rejection with work to do.
|
|
102
|
+
//
|
|
103
|
+
// Keyed on the policy's own marker, so it can only ever list what the policy
|
|
104
|
+
// already decided: a low score that IS backed (the worker stays 'fail'), an errored
|
|
105
|
+
// worker, and the exempt Hacker (never downgraded) cannot appear. Pure; tolerates
|
|
106
|
+
// any input shape because signals.workers comes back off the wire.
|
|
107
|
+
function unbackedLowScores(workerResults, { threshold }) {
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const w of (Array.isArray(workerResults) ? workerResults : [])) {
|
|
110
|
+
if (!w || typeof w !== 'object' || w.policy_downgrade !== UNBACKED_SCORE_DOWNGRADE) continue;
|
|
111
|
+
const scores = w.scores && typeof w.scores === 'object' ? w.scores : {};
|
|
112
|
+
for (const [axis, score] of Object.entries(scores)) {
|
|
113
|
+
if (Number.isFinite(score) && score < threshold) out.push({ kind: String(w.kind || '?'), axis, score });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { failResult, parseCommon, finalizeVerdict, applySeverityGatePolicy, unbackedLowScores, UNBACKED_SCORE_DOWNGRADE };
|