@bongos/core 1.19.332 → 1.19.333

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 (58) hide show
  1. package/.bongos-core.json +160 -40
  2. package/docs/api/openapi.json +2 -2
  3. package/docs/api-reference.md +1 -1
  4. package/docs/copy-inventory.md +202 -202
  5. package/docs/copy-registry.json +384 -384
  6. package/modules/grading/grader-config.js +98 -0
  7. package/modules/grading/grader-model.js +150 -0
  8. package/modules/grading/grader-prompt.js +139 -0
  9. package/modules/grading/grader-score.js +706 -0
  10. package/modules/grading/grader-subagent.js +328 -0
  11. package/modules/grading/grader.js +5 -1287
  12. package/modules/hall-ui/public/builders.js +19 -1182
  13. package/modules/hall-ui/public/goals-page.js +11 -997
  14. package/modules/hall-ui/public/goals-render.js +1032 -0
  15. package/modules/hall-ui/public/goals.html +4 -0
  16. package/modules/hall-ui/public/hall-render.js +1211 -0
  17. package/modules/hall-ui/public/index.html +6 -0
  18. package/modules/lifecycle/routes/goal-route-authz.js +400 -0
  19. package/modules/lifecycle/routes/goals.js +1 -367
  20. package/modules/lifecycle/routes/task-route-helpers.js +219 -0
  21. package/modules/lifecycle/routes/task-write-routes.js +542 -0
  22. package/modules/lifecycle/routes/tasks.js +3 -696
  23. package/package-lock.json +2 -2
  24. package/package.json +1 -1
  25. package/scripts/gds/box-infra.js +280 -0
  26. package/scripts/gds/box.js +1 -245
  27. package/scripts/gds/fitness-checks-identity.js +320 -0
  28. package/scripts/gds/fitness-checks-vocabulary.js +230 -0
  29. package/scripts/gds/fitness-checks-write-validation.js +144 -0
  30. package/scripts/gds/fitness-lib.js +82 -0
  31. package/scripts/gds/fitness.js +55 -668
  32. package/scripts/gds/provision-config.js +204 -0
  33. package/scripts/gds/provision-net.js +221 -0
  34. package/scripts/gds/provision-repo.js +849 -0
  35. package/scripts/gds/provision-units.js +262 -0
  36. package/scripts/gds/provision.js +4 -1395
  37. package/src/bongos/auth-admission.js +264 -0
  38. package/src/bongos/auth-config.js +151 -0
  39. package/src/bongos/auth-discord.js +198 -0
  40. package/src/bongos/auth-github.js +336 -0
  41. package/src/bongos/auth-idp.js +330 -0
  42. package/src/bongos/auth.js +5 -1100
  43. package/src/module-api.js +3 -1
  44. package/tests/auth_admission_gate.mjs +3 -1
  45. package/tests/est_advisory.mjs +3 -1
  46. package/tests/goal_advisory.mjs +3 -2
  47. package/tests/goal_criterion_confirm_ui.mjs +6 -0
  48. package/tests/goal_subtitle.mjs +5 -2
  49. package/tests/goals_card_view.mjs +10 -3
  50. package/tests/grader_generated_provenance.mjs +3 -1
  51. package/tests/hall_palette.mjs +3 -1
  52. package/tests/helpers.mjs +58 -25
  53. package/tests/lib_sh_resolution.mjs +3 -1
  54. package/tests/membership_kind_lockstep.mjs +3 -1
  55. package/tests/migration_allocation.mjs +5 -2
  56. package/tests/patch_gate_ordering.mjs +3 -1
  57. package/tests/rank_tier_single_source.mjs +16 -3
  58. package/tests/reward_gate.mjs +3 -2
@@ -0,0 +1,98 @@
1
+ // modules/grading/grader-config.js — the rubric + tuning constants the grader is
2
+ // configured by, and the two DETERMINISTIC pre-passes (permission-path and
3
+ // route-rank) that run before any model does.
4
+ //
5
+ // Carved out of modules/grading/grader.js by task 1003206 (goal 1000079, the R03
6
+ // de-monolith plan) to bring the grader under the 1,500-line ratchet. A STRUCTURAL
7
+ // move: bodies unchanged, grader.js requires every name back, so the grade port
8
+ // surface and the panel's behaviour are exactly as before.
9
+
10
+ 'use strict';
11
+
12
+ const path = require('node:path');
13
+ const fs = require('node:fs');
14
+
15
+ const RUBRIC = JSON.parse(
16
+ fs.readFileSync(path.join(__dirname, 'grader-rubric.json'), 'utf8')
17
+ );
18
+
19
+ // V4 = the active panel-based gate. V3 / V1 are kept under their fields for
20
+ // the two paths that still read them: V3 = the trivial-diff multi-shot grader
21
+ // (ship.js #513), V1 = the audit-ships.js retroactive deterministic path. The
22
+ // v2 binding + its rubric block were dead (no reader) and removed in task 466.
23
+ const V4 = RUBRIC.v4;
24
+ const V3 = RUBRIC.v3;
25
+ const V1 = RUBRIC.v1;
26
+ const THRESHOLD = V4.threshold;
27
+ const VERSION = V4.version;
28
+ const LOW_CONFIDENCE = V4.low_confidence_threshold;
29
+ // DEFAULT_SHOTS is kept for the v3 multi-shot path (still exported for
30
+ // diagnostics); v4 doesn't use shots — it uses workers.
31
+ const DEFAULT_SHOTS = V3.shots || 3;
32
+ const ASSUMED_BUILDER_MODEL = V4.assumed_builder_model || 'claude-fable-5';
33
+ const DEFAULT_GRADER_MODEL = V4.default_model || 'claude-sonnet-5';
34
+ const FALLTHROUGH_CONFIDENCE = V4.fallthrough_confidence ?? 0.3;
35
+ // task 1002660: per-worker model tier overrides (rubric-configured). The
36
+ // Quality worker was 41% of panel spend with 75% nit findings — its cq/economy
37
+ // judgment doesn't need the full cross-model tier. Keys are worker kinds;
38
+ // values are model ids. workerModelFor guards the cross-model invariant.
39
+ const WORKER_MODEL_TIERS = V4.worker_model_tiers || {};
40
+ const workers = require('./grader-workers');
41
+ const generatedProvenance = require('./generated-provenance'); // task 1002795
42
+ // Carved into modules/grading/ (BV1.R73 / ADR 0091 §1+§4): the two deterministic
43
+ // trust classifiers are KERNEL (KERNEL_FILES, R70) — reached through the doorway,
44
+ // never a deep core require. The worker panel + extract-json are intra-module.
45
+ const api = require('../../src/module-api');
46
+ const routeRankCheck = api.routeRankCheck;
47
+ const permissionPathCheck = api.permissionPathCheck;
48
+
49
+ // Deterministic pre-pass: the sub-Metic permission-path gate (SEC #863). Runs
50
+ // ONLY when the change set touches a permission-sensitive path AND the builder
51
+ // rank resolves below Metic — otherwise null (zero cost / no finding). Like the
52
+ // route-rank pre-pass, the result is a pseudo-worker appended to the panel so
53
+ // aggregatePanel's any-fail gate hard-fails a sub-Metic ship of protected code
54
+ // with no gate-logic change. builderRank comes from the caller's live /me read
55
+ // (ship.js) or req.builder.rank (server); unknown rank fails OPEN here (the
56
+ // post-push main audit, which reads the real author rank from the DB, owns the
57
+ // unknown/dishonest case).
58
+ function permissionPathPrePass(changedFiles, builderRank) {
59
+ const result = permissionPathCheck.checkPermissionPaths({ changedFiles, builderRank });
60
+ if (!result.hardFail) return null;
61
+ return { findings: result.findings, workerResult: permissionPathCheck.toWorkerResult(result) };
62
+ }
63
+
64
+ // Deterministic pre-pass: the route-rank invariant (#475 / C7). Runs ONLY when
65
+ // a src/bongos/routes/ file is in the change set (zero cost otherwise). Returns
66
+ // the findings (fed to the Hacker worker as input) plus a pseudo-worker result
67
+ // appended to the panel so aggregatePanel's any-fail gate handles it directly.
68
+ // Shared verbatim by gradeSubagent (ship-time) and conductor-dispatch.js
69
+ // (mid-flight) so the route-parsing logic lives in exactly one place.
70
+ function routeRankPrePass(changedFiles) {
71
+ const touched = (changedFiles || []).some((f) => routeRankCheck.ROUTE_FILE_RE.test(f));
72
+ if (!touched) return null;
73
+ const result = routeRankCheck.checkRouteRanks();
74
+ return { findings: result.findings, workerResult: routeRankCheck.toWorkerResult(result) };
75
+ }
76
+
77
+
78
+ module.exports = {
79
+ ASSUMED_BUILDER_MODEL,
80
+ DEFAULT_GRADER_MODEL,
81
+ DEFAULT_SHOTS,
82
+ FALLTHROUGH_CONFIDENCE,
83
+ LOW_CONFIDENCE,
84
+ RUBRIC,
85
+ THRESHOLD,
86
+ V1,
87
+ V3,
88
+ V4,
89
+ VERSION,
90
+ WORKER_MODEL_TIERS,
91
+ api,
92
+ generatedProvenance,
93
+ permissionPathCheck,
94
+ permissionPathPrePass,
95
+ routeRankCheck,
96
+ routeRankPrePass,
97
+ workers,
98
+ };
@@ -0,0 +1,150 @@
1
+ // modules/grading/grader-model.js — small numeric helpers, the trivial-diff
2
+ // detector, and model selection for the grader and its panel workers.
3
+ //
4
+ // Carved out of modules/grading/grader.js by task 1003206 (goal 1000079, the R03
5
+ // de-monolith plan) to bring the grader under the 1,500-line ratchet. A STRUCTURAL
6
+ // move: bodies unchanged, grader.js requires every name back, so the grade port
7
+ // surface and the panel's behaviour are exactly as before.
8
+
9
+ 'use strict';
10
+
11
+ const { ASSUMED_BUILDER_MODEL, DEFAULT_GRADER_MODEL, WORKER_MODEL_TIERS, api } = require('./grader-config.js');
12
+
13
+ function r1(n) {
14
+ return Math.round(Number(n) * 10) / 10;
15
+ }
16
+
17
+ function clamp(n, lo, hi) {
18
+ return Math.max(lo, Math.min(hi, n));
19
+ }
20
+
21
+ function median(arr) {
22
+ const v = arr.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b);
23
+ if (v.length === 0) return null;
24
+ const mid = Math.floor(v.length / 2);
25
+ return v.length % 2 ? r1(v[mid]) : r1((v[mid - 1] + v[mid]) / 2);
26
+ }
27
+
28
+ // ---------- v4.1: trivial-diff fast-path (#513) ----------
29
+ //
30
+ // The #506 post-mortem: a +1/-1 welcome-copy tweak ran the full 4-worker
31
+ // adversarial Opus/Sonnet panel — disproportionate cost for a one-word change.
32
+ // isTrivialDiff classifies a change as "copy/docs only, small, and touching no
33
+ // logic path". ship.js routes those to a SINGLE generalist grader shot instead
34
+ // of the panel ("lighter, not skipped" — the 1-shot still scores ff+cq+economy
35
+ // +confidence so the server gate applies, and the deterministic scanner + smoke
36
+ // + route-rank checks run regardless). Everything else keeps the full panel.
37
+ //
38
+ // Deliberately conservative — defaults to "not trivial" (full panel) whenever
39
+ // in doubt (missing diffStats, zero files, any logic-path file, oversized diff).
40
+ const TRIVIAL_MAX_FILES = 3;
41
+ const TRIVIAL_MAX_LINES = 30;
42
+ // Copy/docs/front-end surfaces eligible for the lighter single-shot grade.
43
+ // (Both web surfaces moved under modules/<key>/public/: the status dashboard to
44
+ // modules/status-ui/public/ in BV1.R84 and the hall to modules/hall-ui/public/ in
45
+ // BV1.R85.)
46
+ const TRIVIAL_PATH_RE = /(?:^|\/)[^/]+\.md$|^docs\/|^public\/|^modules\/hall-ui\/public\/|^modules\/status-ui\/public\//;
47
+ // Logic / infra / test surfaces that force the full panel even on a tiny diff —
48
+ // these are where a subtle one-liner can do real damage, so they never qualify.
49
+ const LOGIC_PATH_RE = /^src\/|^scripts\/|^migrations\/|^server\.js$|^tests\/|^\.claude\//;
50
+ // Executable code AND config are NEVER trivial, even under public*/ (#513
51
+ // Hacker). A copy tweak and an XSS / client-authz regression are
52
+ // indistinguishable to a path+size heuristic, and the builders-hall + game
53
+ // front-ends carry auth flows and game logic — so any code extension forces the
54
+ // full adversarial panel. Config/data files (.json/.yaml/.env/…) are excluded
55
+ // too: a client-side config change is security-relevant and shouldn't get the
56
+ // lighter shot. Only genuinely static content (markdown, CSS/HTML, images,
57
+ // plain text) takes the single shot.
58
+ // HTML and SVG are executable in a browser (inline <script>, on* handlers,
59
+ // <svg onload>), so they are NOT "static" for grading purposes — a small
60
+ // edit could introduce client-side XSS that only the full panel's Hacker
61
+ // would catch. Excluded alongside code + config (#513 Hacker).
62
+ const NON_TRIVIAL_EXT_RE = /\.(?:js|mjs|cjs|jsx|ts|tsx|py|sh|bash|rb|go|sql|json|ya?ml|toml|ini|env|html?|svg)$|(?:^|\/)\.env(?:\.|$)/;
63
+
64
+ function isTrivialDiff({ changedFiles, diffStats } = {}) {
65
+ if (!diffStats || typeof diffStats !== 'object') return false;
66
+ const files = Array.isArray(changedFiles) ? changedFiles.filter(Boolean) : [];
67
+ if (files.length === 0) return false;
68
+ const fc = Number(diffStats.files_changed);
69
+ const lines = (Number(diffStats.lines_added) || 0) + (Number(diffStats.lines_removed) || 0);
70
+ // Cap on BOTH the shortstat count and the actual file-list length — a rename
71
+ // can disagree between the two, so take the stricter of the two (#513 nit).
72
+ if (!Number.isFinite(fc) || fc <= 0 || fc > TRIVIAL_MAX_FILES) return false;
73
+ if (files.length > TRIVIAL_MAX_FILES) return false;
74
+ if (lines <= 0 || lines > TRIVIAL_MAX_LINES) return false;
75
+ // Every changed file must be a copy/docs path, none in a logic path, and none
76
+ // an executable-code or config file (those always get the full panel).
77
+ return files.every((f) =>
78
+ TRIVIAL_PATH_RE.test(f) && !LOGIC_PATH_RE.test(f) && !NON_TRIVIAL_EXT_RE.test(f));
79
+ }
80
+
81
+ // ---------- v3: cross-family model selection (ADR 0024) ----------
82
+
83
+ // Returns the grader model given the builder's model — a DIFFERENT model from
84
+ // the builder is the cross-model adversarial guarantee (ADR 0024). With no
85
+ // builderModel the caller assumes the owner-top-model builder (CLAUDE.md §2),
86
+ // resolving to the default grader — the path selectGraderModel({}) takes.
87
+ //
88
+ // task 1002659 (Claude 5 refresh): top-tier builders (fable/mythos, opus)
89
+ // grade on the current mid tier; sonnet/haiku builders grade on the tier
90
+ // above. #117's throw-on-unknown is reversed: the throw fired INSIDE the
91
+ // grade step, stranding any ship whose builder id lacked an opus/sonnet/haiku
92
+ // substring (claude-fable-5 was exactly that) — a worse failure than the
93
+ // misconfiguration it guarded. Unknown ids now warn loudly and grade on
94
+ // DEFAULT_GRADER_MODEL; the id still reaches signals.builder_model_assumed,
95
+ // so /grader-health can spot the drift.
96
+ function selectGraderModel({ builderModel } = {}) {
97
+ const bm = (builderModel || ASSUMED_BUILDER_MODEL).toLowerCase();
98
+ if (bm.includes('fable') || bm.includes('mythos')) return 'claude-sonnet-5';
99
+ if (bm.includes('opus')) return 'claude-sonnet-5';
100
+ if (bm.includes('sonnet')) return 'claude-opus-5';
101
+ if (bm.includes('haiku')) return 'claude-opus-5';
102
+ console.error(
103
+ `selectGraderModel: unrecognized builder model ${JSON.stringify(builderModel)} — ` +
104
+ `no 'fable'/'mythos'/'opus'/'sonnet'/'haiku' token; grading on the default ` +
105
+ `${DEFAULT_GRADER_MODEL} (cross-model unverifiable — task 1002659, was a #117 throw)`
106
+ );
107
+ return DEFAULT_GRADER_MODEL;
108
+ }
109
+
110
+ // task 1002660: resolve one worker's model — the rubric tier override when one
111
+ // exists, else the panel's cross-model pick. The override is refused (falls
112
+ // back to graderModel) when it lands on the BUILDER's own family: tiering is
113
+ // a cost lever, never a hole in the ADR 0024 cross-model guarantee.
114
+ const MODEL_FAMILY_TOKENS = ['fable', 'mythos', 'opus', 'sonnet', 'haiku'];
115
+ function workerModelFor(kind, graderModel, builderModel) {
116
+ const tier = WORKER_MODEL_TIERS[kind];
117
+ if (!tier) return graderModel;
118
+ const bm = String(builderModel || ASSUMED_BUILDER_MODEL).toLowerCase();
119
+ const tierFamily = MODEL_FAMILY_TOKENS.find((t) => String(tier).toLowerCase().includes(t));
120
+ if (tierFamily && bm.includes(tierFamily)) return graderModel;
121
+ return tier;
122
+ }
123
+
124
+ // ---------- v3: prompt builder (default-fail framing) ----------
125
+
126
+ // The instance's project name, injected into the grader's identity line so a
127
+ // non-OTB Cloud Bongos instance is graded against ITS OWN project, never a
128
+ // hardcoded "Off the Boats" (ADR 0062 §3/§5; the branding contract's
129
+ // llmProjectName field). Reached through the doorway like every other host-
130
+ // context read in this module — never a deep core require. Resolved once at
131
+ // module load (the brand is a per-process singleton); falls back to a neutral
132
+ // phrase if branding fails to resolve so the gate never crashes on a config error.
133
+ const LLM_PROJECT = (() => {
134
+ try { return api.branding().llmProjectName || 'this project'; }
135
+ catch { return 'this project'; }
136
+ })();
137
+
138
+
139
+ module.exports = {
140
+ LLM_PROJECT,
141
+ MODEL_FAMILY_TOKENS,
142
+ TRIVIAL_MAX_FILES,
143
+ TRIVIAL_MAX_LINES,
144
+ clamp,
145
+ isTrivialDiff,
146
+ median,
147
+ r1,
148
+ selectGraderModel,
149
+ workerModelFor,
150
+ };
@@ -0,0 +1,139 @@
1
+ // modules/grading/grader-prompt.js — the adversarial reviewer prompt and the
2
+ // per-grade prompt builder.
3
+ //
4
+ // Carved out of modules/grading/grader.js by task 1003206 (goal 1000079, the R03
5
+ // de-monolith plan) to bring the grader under the 1,500-line ratchet. A STRUCTURAL
6
+ // move: bodies unchanged, grader.js requires every name back, so the grade port
7
+ // surface and the panel's behaviour are exactly as before.
8
+
9
+ 'use strict';
10
+
11
+ const path = require('node:path');
12
+ const { V3, generatedProvenance, workers } = require('./grader-config.js');
13
+ const { LLM_PROJECT } = require('./grader-model.js');
14
+
15
+ const PROMPT_HEADER = `You are an adversarial code-review subagent for the ${LLM_PROJECT} project. You are reviewing one shipped change to decide whether it deserves to ship.
16
+
17
+ Your job is to FIND WHAT IS WRONG with this change. **The default verdict is FAIL.** To return a PASS, you must affirmatively conclude — after a careful read of the diff and (using your Read tool) the relevant post-merge files — that no material problems remain.
18
+
19
+ Begin by hunting for these specific failure modes. When you find one, CITE the diff line or name the failing acceptance criterion:
20
+
21
+ - TODOs, FIXMEs, "for now", "temporary" or "stub" comments left in shipped code.
22
+ - Hardcoded shortcuts: assuming one builder, one user, one project root, one tenant, one environment, or one identity. Look for literal "lars", "lars589", absolute /Users paths, hardcoded ports, hardcoded IDs.
23
+ - Scope gaps: acceptance criteria in the description that the diff DOES NOT satisfy. Read the description's "Acceptance" section line by line.
24
+ - Demo / dummy / mock code that lies: returns a fixed value, fakes a successful result, mocks at the wrong layer, or pretends to call something it doesn't.
25
+ - Missing tests for new logic: any non-trivial new function, route handler, branch, or aggregation that ships without a covering test.
26
+ - Broken invariants: methodology rules in CLAUDE.md, cross-verified Python/JS autotile lookups, ground-vs-object cell split per ADR 0012, the "every change is backed by a claimed GDS task" rule, the trust-boundary rule per ADR 0016 ("server-enforced permissions"), the no-hardcoded-Archon-identity rule per V3.R12 #236.
27
+ - Dead code: helpers, exports, branches, or files added but never called from anywhere in the post-merge tree. Use Read on the receiving site if unsure.
28
+ - Reads/writes outside the declared touches[]: if the diff edits files the task didn't claim, that's a scope creep — call it out.
29
+
30
+ You return three scores (0-10), notes, an issues array, and a confidence value:
31
+
32
+ 1. functional_fidelity (0-10): did the diff implement what the description scoped? 10 = every acceptance criterion satisfied by code or explicitly out-of-scope per the description. 6 = mostly there with one NAMED gap. Below 6 = scope gaps, broken acceptance criteria, or demo code that lies.
33
+ 2. code_quality (0-10): is the code well-written for THIS codebase? 10 = production-quality, would-merge-without-comment. 6 = ships but with a clear improvement a reviewer would suggest. Below 6 = broken style, dead code, broken invariants, missing tests where warranted.
34
+ 3. economy (0-10, ADVISORY): is the change as small and simple as the task warrants? 10 = minimal and focused. 6 = a bit more than necessary. Below 6 = bloated, speculative abstraction, duplicated logic.
35
+
36
+ **The hard gate** (applied downstream, you do not enforce it yourself; just score honestly): functional_fidelity >= 6 AND code_quality >= 6 AND confidence >= 0.4. Below 6 on either axis is a FAIL. Economy is advisory only.
37
+
38
+ **For every issue you list:** include severity (blocker | major | minor | nit), the file path, and a one-line summary that quotes the offending diff line or names the failing acceptance criterion. Vague summaries like "code quality could be better" are not acceptable — be specific or omit it.
39
+
40
+ **Sanity checks:**
41
+ - A PASS with no issues[] entries is suspicious. If you found nothing material wrong, briefly say WHY in notes_md: which acceptance criteria you verified, which files you Read, which could-have-gone-wrong didn't.
42
+ - A FAIL with no issues[] entries is invalid. At least one concrete issue must explain the verdict.
43
+ - A score below 6 with no issue at severity major or blocker is inconsistent — name the major problem.
44
+
45
+ **Confidence:** 0-1, your self-reported confidence in your two scores. If you cannot judge (description too vague to grade against, diff too large to fit comfortably in context, key context missing), set confidence LOW (< 0.4). Low confidence falls through to manual confirm — be honest about uncertainty rather than guessing.
46
+
47
+ Return ONLY JSON matching this schema — no prose before or after, no markdown fences:
48
+ {
49
+ "functional_fidelity": <number 0-10>,
50
+ "code_quality": <number 0-10>,
51
+ "economy": <number 0-10>,
52
+ "notes_md": <string>,
53
+ "confidence": <number 0-1>,
54
+ "issues": [{"severity": "blocker"|"major"|"minor"|"nit", "file": <path>, "summary": <string>}, ...]
55
+ }
56
+
57
+ You have a Read tool. Spend reads checking that removed code is unreferenced elsewhere, that new helpers match surrounding style, that the diff's value_summary matches what the diff actually does, and that acceptance criteria you can't verify from the diff alone are actually satisfied in the post-merge file. Do not run tests, do not edit files, do not call the network.`;
58
+
59
+ function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, generatedProvenance: prov = null }) {
60
+ const t = task || {};
61
+ const desc = (t.description || '(no description)').trim();
62
+ const title = (t.title || '(no title)').trim();
63
+ const vs = (valueSummary || '(no value summary)').trim();
64
+ const files = Array.isArray(changedFiles) ? changedFiles : [];
65
+ const diffText = (diff || '(empty diff)').trim();
66
+
67
+ const lines = [];
68
+ lines.push(PROMPT_HEADER);
69
+ lines.push('');
70
+ lines.push('---');
71
+ lines.push('## Task');
72
+ lines.push('');
73
+ lines.push(`**ID:** ${t.id ?? '(unknown)'}`);
74
+ lines.push(`**Title:** ${title}`);
75
+ lines.push(`**Kind:** ${t.kind ?? 'unclassified'} · **Discipline:** ${t.discipline ?? 'unclassified'}`);
76
+ lines.push(`**Estimated effort:** ${t.est_minutes != null ? `${t.est_minutes} min` : '(not estimated)'} — use this to judge whether the diff size is proportionate to the work scoped.`);
77
+ if (t.version_id) lines.push(`**Version:** ${t.version_id}`);
78
+ lines.push('');
79
+ lines.push('**Description:**');
80
+ lines.push('');
81
+ lines.push(desc);
82
+ lines.push('');
83
+ lines.push('---');
84
+ lines.push('## Builder\'s value summary');
85
+ lines.push('');
86
+ lines.push(vs);
87
+ lines.push('');
88
+ lines.push('---');
89
+ lines.push(`## Changed files (${files.length})`);
90
+ lines.push('');
91
+ for (const f of files) lines.push(`- ${f}`);
92
+ if (files.length === 0) lines.push('(no files changed — this is suspicious)');
93
+ lines.push('');
94
+ lines.push('You can Read any of these files to see their post-merge state.');
95
+ lines.push('');
96
+ lines.push('---');
97
+ lines.push('## Diff size (for the economy axis)');
98
+ lines.push('');
99
+ if (diffStats && typeof diffStats === 'object') {
100
+ const fc = diffStats.files_changed ?? '?';
101
+ const la = diffStats.lines_added ?? '?';
102
+ const lr = diffStats.lines_removed ?? '?';
103
+ lines.push(`- files changed: ${fc}`);
104
+ lines.push(`- lines added: ${la}`);
105
+ lines.push(`- lines removed: ${lr}`);
106
+ lines.push('');
107
+ lines.push('Weigh this against the task description, kind, and estimated effort above when scoring economy. A diff much larger than the scoped work warrants — or one padded with speculative abstraction, duplication, or dead code — should score low on economy.');
108
+ } else {
109
+ lines.push('(diff stats unavailable — score economy from the diff itself.)');
110
+ }
111
+ lines.push('');
112
+ lines.push('---');
113
+ lines.push('## Unified diff');
114
+ lines.push('');
115
+ lines.push('```diff');
116
+ lines.push(diffText);
117
+ lines.push('```');
118
+ lines.push('', ...(prov ? [generatedProvenance.provenanceSection(prov)].filter(Boolean) : [])); // task 1002795 — the trivial path is where a docs-only ship lands
119
+ lines.push('Now grade. Default-fail unless your read of the diff and post-merge files turns up no material problems. Output ONLY the JSON object — nothing before or after it.');
120
+ return lines.join('\n');
121
+ }
122
+
123
+ // ---------- v3: per-shot validator (same as v2 logic) ----------
124
+
125
+ // Tries to extract a JSON object from a subagent response. The subagent is
126
+ // instructed to return ONLY JSON, but in practice models sometimes wrap it
127
+ // in markdown fences or add a line of prose. Be tolerant.
128
+ // #79 (#380, completed #493): robust JSON extraction now lives in the shared
129
+ // leaf module so grader.js AND every worker module use ONE implementation.
130
+ // Re-exported here for back-compat (validateAndScore + tests use
131
+ // grader.extractJson).
132
+ const { extractJson } = require('./grader-workers/extract-json');
133
+
134
+
135
+ module.exports = {
136
+ PROMPT_HEADER,
137
+ buildPrompt,
138
+ extractJson,
139
+ };