@bongos/core 1.19.49 → 1.19.50
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 +21 -21
- package/docs/recipes/private-npm-distribution.md +3 -3
- package/modules/grading/grader-rubric.json +2 -0
- package/modules/grading/grader-workers/efficiency.js +2 -1
- package/modules/grading/grader-workers/hacker.js +2 -1
- package/modules/grading/grader-workers/index.js +4 -3
- package/modules/grading/grader-workers/narc.js +2 -1
- package/modules/grading/grader-workers/quality.js +2 -1
- package/modules/grading/grader-workers/worker-prompt.js +24 -1
- package/modules/grading/grader.js +93 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/npmrc.js +1 -1
- package/scripts/gds/ship.js +109 -18
- package/src/module-api.js +3 -1
- package/tests/grade_diff_hash.mjs +29 -1
- package/tests/grader.mjs +122 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.50",
|
|
6
|
+
"core_contract": "1.19.50",
|
|
7
|
+
"source_commit": "1a7b11b4377e9fefb36560c0cda9d4b1030a05c9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-11T18:
|
|
9
|
+
"built_at": "2026-08-11T18:44:57.114Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 291,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1529,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "e96a0d29e2d6adc516aaa0d095dbb1c4d543dbec0508228b2cbcf16a1b400dbd",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1572,7 +1572,7 @@
|
|
|
1572
1572
|
{
|
|
1573
1573
|
"path": "docs/recipes/private-npm-distribution.md",
|
|
1574
1574
|
"mode": "0000644",
|
|
1575
|
-
"sha256": "
|
|
1575
|
+
"sha256": "5bfca9628859f1cdd79d741bdd50f3fe415430ee3380a2ac7f46b97ebe082d0e"
|
|
1576
1576
|
},
|
|
1577
1577
|
{
|
|
1578
1578
|
"path": "docs/recipes/search-index-ingestion.md",
|
|
@@ -2862,12 +2862,12 @@
|
|
|
2862
2862
|
{
|
|
2863
2863
|
"path": "modules/grading/grader-rubric.json",
|
|
2864
2864
|
"mode": "0000644",
|
|
2865
|
-
"sha256": "
|
|
2865
|
+
"sha256": "6a0a2205fee2a4b0efc422c884826a9fd2af3d8785b393d5f7123e1758f1ebfc"
|
|
2866
2866
|
},
|
|
2867
2867
|
{
|
|
2868
2868
|
"path": "modules/grading/grader-workers/efficiency.js",
|
|
2869
2869
|
"mode": "0000644",
|
|
2870
|
-
"sha256": "
|
|
2870
|
+
"sha256": "ab85c9c7b167e710b07af1d4beb1579a593aa617e1c1990238d329fdb0362035"
|
|
2871
2871
|
},
|
|
2872
2872
|
{
|
|
2873
2873
|
"path": "modules/grading/grader-workers/extract-json.js",
|
|
@@ -2882,22 +2882,22 @@
|
|
|
2882
2882
|
{
|
|
2883
2883
|
"path": "modules/grading/grader-workers/hacker.js",
|
|
2884
2884
|
"mode": "0000644",
|
|
2885
|
-
"sha256": "
|
|
2885
|
+
"sha256": "c7177625382e2ea61ee706c6b9b8669d9d452cdc0a92235695438b249c1ab6fc"
|
|
2886
2886
|
},
|
|
2887
2887
|
{
|
|
2888
2888
|
"path": "modules/grading/grader-workers/index.js",
|
|
2889
2889
|
"mode": "0000644",
|
|
2890
|
-
"sha256": "
|
|
2890
|
+
"sha256": "8ec5e91341ec5247ee643c6ce75fb5ec21a62b3bba176fdde060cdb352edb808"
|
|
2891
2891
|
},
|
|
2892
2892
|
{
|
|
2893
2893
|
"path": "modules/grading/grader-workers/narc.js",
|
|
2894
2894
|
"mode": "0000644",
|
|
2895
|
-
"sha256": "
|
|
2895
|
+
"sha256": "f35c3fce7feb6198df0011a9d22533038ca0766733c97a31841a184251800d39"
|
|
2896
2896
|
},
|
|
2897
2897
|
{
|
|
2898
2898
|
"path": "modules/grading/grader-workers/quality.js",
|
|
2899
2899
|
"mode": "0000644",
|
|
2900
|
-
"sha256": "
|
|
2900
|
+
"sha256": "003b51b4a5eab34bb634ceaad5ae9c1ae6f2566423fe0d15cef79b936dc6756b"
|
|
2901
2901
|
},
|
|
2902
2902
|
{
|
|
2903
2903
|
"path": "modules/grading/grader-workers/rubric.js",
|
|
@@ -2907,7 +2907,7 @@
|
|
|
2907
2907
|
{
|
|
2908
2908
|
"path": "modules/grading/grader-workers/worker-prompt.js",
|
|
2909
2909
|
"mode": "0000644",
|
|
2910
|
-
"sha256": "
|
|
2910
|
+
"sha256": "5b614e96ed951c5eebb5c40c93612f59ff79da768055233b59790599421a70ec"
|
|
2911
2911
|
},
|
|
2912
2912
|
{
|
|
2913
2913
|
"path": "modules/grading/grader-workers/worker-verdict.js",
|
|
@@ -2917,7 +2917,7 @@
|
|
|
2917
2917
|
{
|
|
2918
2918
|
"path": "modules/grading/grader.js",
|
|
2919
2919
|
"mode": "0000644",
|
|
2920
|
-
"sha256": "
|
|
2920
|
+
"sha256": "edf6ecc725b37fe2c515174e6b06025f82fae10172e6f13d8fcfbb060c11474a"
|
|
2921
2921
|
},
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "modules/grading/module.json",
|
|
@@ -4037,12 +4037,12 @@
|
|
|
4037
4037
|
{
|
|
4038
4038
|
"path": "package-lock.json",
|
|
4039
4039
|
"mode": "0000644",
|
|
4040
|
-
"sha256": "
|
|
4040
|
+
"sha256": "ffd1f91d40f2098324c3054cd3accdfdf210a668972dcb8702104cd11ed6c4dc"
|
|
4041
4041
|
},
|
|
4042
4042
|
{
|
|
4043
4043
|
"path": "package.json",
|
|
4044
4044
|
"mode": "0000644",
|
|
4045
|
-
"sha256": "
|
|
4045
|
+
"sha256": "cd444deb3940b96b8afedb89f5ece513d842d4a9148b3a50304d8d77866dd904"
|
|
4046
4046
|
},
|
|
4047
4047
|
{
|
|
4048
4048
|
"path": "public-docs/index.html",
|
|
@@ -4957,7 +4957,7 @@
|
|
|
4957
4957
|
{
|
|
4958
4958
|
"path": "scripts/gds/npmrc.js",
|
|
4959
4959
|
"mode": "0000644",
|
|
4960
|
-
"sha256": "
|
|
4960
|
+
"sha256": "9dfd27e75def392a73d7de7fd23d462afc97b17a8199acfe6563a80692570026"
|
|
4961
4961
|
},
|
|
4962
4962
|
{
|
|
4963
4963
|
"path": "scripts/gds/oauth-secret.js",
|
|
@@ -5232,7 +5232,7 @@
|
|
|
5232
5232
|
{
|
|
5233
5233
|
"path": "scripts/gds/ship.js",
|
|
5234
5234
|
"mode": "0000644",
|
|
5235
|
-
"sha256": "
|
|
5235
|
+
"sha256": "aa77aef41188c5ef596c9c5e7e89f54f97c93b6e889e1c776b8fe34c8af66cce"
|
|
5236
5236
|
},
|
|
5237
5237
|
{
|
|
5238
5238
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -5662,7 +5662,7 @@
|
|
|
5662
5662
|
{
|
|
5663
5663
|
"path": "src/module-api.js",
|
|
5664
5664
|
"mode": "0000644",
|
|
5665
|
-
"sha256": "
|
|
5665
|
+
"sha256": "2d8b78d34893b1e4c25d081c343ad1b0d1deb2a675cae9a3c4e96500c6cea821"
|
|
5666
5666
|
},
|
|
5667
5667
|
{
|
|
5668
5668
|
"path": "src/module-loader/loader.js",
|
|
@@ -6632,7 +6632,7 @@
|
|
|
6632
6632
|
{
|
|
6633
6633
|
"path": "tests/grade_diff_hash.mjs",
|
|
6634
6634
|
"mode": "0000644",
|
|
6635
|
-
"sha256": "
|
|
6635
|
+
"sha256": "170b816aa9032e6eccba415f67ce60c10da5f2f706a1b1ee458a5d8578679535"
|
|
6636
6636
|
},
|
|
6637
6637
|
{
|
|
6638
6638
|
"path": "tests/grade_server_authoritative.mjs",
|
|
@@ -6647,7 +6647,7 @@
|
|
|
6647
6647
|
{
|
|
6648
6648
|
"path": "tests/grader.mjs",
|
|
6649
6649
|
"mode": "0000644",
|
|
6650
|
-
"sha256": "
|
|
6650
|
+
"sha256": "31557b08f8886f5d76201b5afbe0332e834504b67a391699798170aede9541e8"
|
|
6651
6651
|
},
|
|
6652
6652
|
{
|
|
6653
6653
|
"path": "tests/grader_bypass.mjs",
|
|
@@ -20,7 +20,7 @@ Both carry `publishConfig: { access: "restricted" }`, so **publishing them is pr
|
|
|
20
20
|
These involve payment and permanent, account-level actions. They are **not** done inside a build session.
|
|
21
21
|
|
|
22
22
|
1. **Put the `bongos` npm org on a paid plan.** Private scoped publishing requires it. On npmjs.com → your org → Billing → upgrade to a plan that allows private packages (npm Teams, ~$7/member/month). Add as members only the people who need to publish or install during pre-launch — each is a billed seat.
|
|
23
|
-
2. **Create an access token.** npmjs.com → Access Tokens → *Generate New Token* → **
|
|
23
|
+
2. **Create an access token.** npm retired classic (Automation/Publish) tokens — use a **granular access token**: npmjs.com → Access Tokens → *Generate New Token* → **Granular Access Token**, scoped to the `@bongos` packages with **Read and write** permission, and — for a token that publishes — enable **"Bypass two-factor authentication"** (npm otherwise requires interactive 2FA at publish). A **Read-only** granular token is enough for machines that only install. Granular tokens **expire** (pick the longest expiry the policy allows and calendar the rotation — a silently expired token is a 404 on the private package, the same symptom as a missing one). Store it as an environment variable, never in a committed file:
|
|
24
24
|
```sh
|
|
25
25
|
export NPM_TOKEN=<redacted>
|
|
26
26
|
```
|
|
@@ -59,7 +59,7 @@ The steps below remain as the **manual fallback** (lane disarmed, npm outage, or
|
|
|
59
59
|
2. **Publish** (owner, from a clean checkout of the released commit):
|
|
60
60
|
```sh
|
|
61
61
|
node scripts/gds/package-core.js --publish # DRY-RUN: runs the fail-closed no-leak gate, previews @bongos/core@<version>
|
|
62
|
-
npm login # once per machine (+ 2FA), or use a
|
|
62
|
+
npm login # once per machine (+ 2FA), or use a granular token with 2FA bypass
|
|
63
63
|
node scripts/gds/package-core.js --publish --live # permanent; the no-leak gate must pass or nothing is built/uploaded
|
|
64
64
|
```
|
|
65
65
|
The no-leak / mirror-redact gate is **built into the build** — a leaking artifact is never produced, so `--publish` cannot upload one.
|
|
@@ -89,7 +89,7 @@ Without a valid token + org membership the install 404s — that is the private
|
|
|
89
89
|
|
|
90
90
|
Provisioning defaults to installing the core from a **vendored tarball** committed into each instance repo (task 2053) — offline, needs no token, and stays the default/fallback until the registry path is proven on a real greenfield standup. The registry pull is **opt-in and fail-soft**:
|
|
91
91
|
|
|
92
|
-
- **Turn it on** by setting `PROVISION_CORE_FROM_REGISTRY=1` in the control-plane runner's environment (e.g. `/etc/cloudbongos/box.env`) **and** providing a read-only `NPM_TOKEN` (a
|
|
92
|
+
- **Turn it on** by setting `PROVISION_CORE_FROM_REGISTRY=1` in the control-plane runner's environment (e.g. `/etc/cloudbongos/box.env`) **and** providing a read-only `NPM_TOKEN` (a **granular access token** — Read-only on the `@bongos` packages — from the paid `bongos` org; mind its expiry). With both present, the standalone scaffold pins `@bongos/core@<version>` from the registry and writes an env-fed `.npmrc` (`_authToken=<redacted> instead of vendoring a tarball; `npm ci` then pulls the private package (the token is inherited from the runner env — never committed).
|
|
93
93
|
- **Fail-soft**: if the flag is set but `NPM_TOKEN` is absent, provisioning logs a notice and **falls back to the vendored tarball**, so a tokenless standup still works.
|
|
94
94
|
- **Directly**: `bongos init --registry-core` scaffolds the registry shape (caret pin + `.npmrc`; generates the lockfile when `NPM_TOKEN` is set). `bongos upgrade --to <v> --registry` bumps a consumer to a registry version. Both are mutually exclusive with the vendored `--vendor-core` / `--from` inputs.
|
|
95
95
|
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"assumed_builder_model": "claude-fable-5",
|
|
13
13
|
"model_refresh_note": "task 1002659 (2026-08-11): pins refreshed to the Claude 5 family. Assumed builder = claude-fable-5 (the owner-top-model default per CLAUDE.md §2); default grader = claude-sonnet-5 (cross-model per ADR 0024, cost-parity with the prior sonnet-4-6 posture — opus-5 grades the sonnet/haiku builders where the grader should be the stronger model). selectGraderModel no longer throws on an unknown builder id: it warns loudly and falls back to default_model (the #117 throw stranded ships whose builder id lacked an opus/sonnet/haiku substring — exactly what claude-fable-5 is).",
|
|
14
14
|
"active_panel": ["narc", "quality", "hacker", "efficiency"],
|
|
15
|
+
"worker_model_tiers": { "quality": "claude-haiku-4-5-20251001" },
|
|
16
|
+
"worker_model_tiers_note": "task 1002660: per-worker cost tiering. Quality was 41% of panel spend with 75% nit findings — its cq/economy lens runs on the haiku tier. workerModelFor refuses an override that lands on the builder's own model family (the ADR 0024 cross-model guarantee outranks the cost lever); gate semantics unchanged — Quality's verdict still gates identically.",
|
|
15
17
|
"fallthrough_confidence": 0.3,
|
|
16
18
|
"low_confidence_threshold": 0.4,
|
|
17
19
|
"rationale": "GDS-V3 task #410 (MAS Phase 2.0), 2026-05-29. Supersedes v3 (single-prompt 3-shot generalist). Phase 1 calibration showed minimal between-shot disagreement on the same prompt — the strict gate fired correctly but the diversity signal was weak. v4 trades sampling-variance for lens-specialization: replace 3 identical-prompt shots with N workers (each a distinct prompt focused on one axis). Phase 2.0 ships 2 of the 3 planned workers (narc for completion, quality for cq + advisory economy) — keeps the gate at the same strictness while the framework calibrates. Phase 2.1 adds hacker (security; wraps /builder-redteam as a CI gate). Phase 2.2 adds efficiency_monger.",
|
|
@@ -107,7 +107,7 @@ No prose, no markdown fences:
|
|
|
107
107
|
|
|
108
108
|
You have a Read tool with a hard budget of 5 calls. Do not run tests, do not edit files, do not call the network.`;
|
|
109
109
|
|
|
110
|
-
function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, priorRound }) {
|
|
110
|
+
function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, priorRound, contextBundle }) {
|
|
111
111
|
const t = task || {};
|
|
112
112
|
|
|
113
113
|
// Efficiency Monger shows diff size as informational context (its axis is
|
|
@@ -127,6 +127,7 @@ function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, priorR
|
|
|
127
127
|
|
|
128
128
|
return buildWorkerPrompt({
|
|
129
129
|
priorRound,
|
|
130
|
+
contextBundle, // task 1002660: shared post-change file contents
|
|
130
131
|
header: PROMPT_HEADER,
|
|
131
132
|
task: t,
|
|
132
133
|
valueSummary,
|
|
@@ -117,7 +117,7 @@ No prose, no markdown fences:
|
|
|
117
117
|
|
|
118
118
|
You have a Read tool with a hard budget of 5 calls. Do not run tests, do not edit files, do not call the network.`;
|
|
119
119
|
|
|
120
|
-
function buildPrompt({ task, valueSummary, diff, changedFiles, deterministicFindings, priorRound }) {
|
|
120
|
+
function buildPrompt({ task, valueSummary, diff, changedFiles, deterministicFindings, priorRound, contextBundle }) {
|
|
121
121
|
const t = task || {};
|
|
122
122
|
const findings = Array.isArray(deterministicFindings) ? deterministicFindings : [];
|
|
123
123
|
|
|
@@ -155,6 +155,7 @@ function buildPrompt({ task, valueSummary, diff, changedFiles, deterministicFind
|
|
|
155
155
|
|
|
156
156
|
return buildWorkerPrompt({
|
|
157
157
|
priorRound,
|
|
158
|
+
contextBundle, // task 1002660: shared post-change file contents
|
|
158
159
|
header: PROMPT_HEADER,
|
|
159
160
|
task: t,
|
|
160
161
|
valueSummary,
|
|
@@ -108,14 +108,15 @@ function resolvePanel(kinds) {
|
|
|
108
108
|
// without knowing about the runSubagent internals. ("shot" is v3 multi-shot
|
|
109
109
|
// terminology; v4 calls these workers — but the subprocess plumbing is the
|
|
110
110
|
// same shape underneath.)
|
|
111
|
-
async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffStats, model, runOpts, runSubagentFn, extractAssistantResultFn, deterministicFindings, priorRound }) {
|
|
111
|
+
async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffStats, model, runOpts, runSubagentFn, extractAssistantResultFn, deterministicFindings, priorRound, contextBundle }) {
|
|
112
112
|
// deterministicFindings (route-rank pre-pass, #475) is forwarded to every
|
|
113
113
|
// worker's buildPrompt; only the Hacker reads it — the others ignore the
|
|
114
114
|
// extra field, so this is a no-op for narc/quality/efficiency.
|
|
115
115
|
// priorRound (task 1002662) rides the same way: the prior grade's findings +
|
|
116
116
|
// builder response for a delta-aware re-grade; every worker renders it via
|
|
117
|
-
// the shared scaffold.
|
|
118
|
-
|
|
117
|
+
// the shared scaffold. contextBundle (task 1002660) likewise: the post-change
|
|
118
|
+
// file contents read once by the caller, rendered by the shared scaffold.
|
|
119
|
+
const prompt = worker.buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, deterministicFindings, priorRound, contextBundle });
|
|
119
120
|
|
|
120
121
|
// One subagent attempt → verdict. Pulled out so #465 can re-run it once on a
|
|
121
122
|
// transient error without duplicating the spawn/parse/verdict plumbing.
|
|
@@ -91,7 +91,7 @@ No prose before or after, no markdown fences:
|
|
|
91
91
|
|
|
92
92
|
You have a Read tool with a hard budget of 5 calls. Do not run tests, do not edit files, do not call the network.`;
|
|
93
93
|
|
|
94
|
-
function buildPrompt({ task, valueSummary, diff, changedFiles, priorRound }) {
|
|
94
|
+
function buildPrompt({ task, valueSummary, diff, changedFiles, priorRound, contextBundle }) {
|
|
95
95
|
const t = task || {};
|
|
96
96
|
// Narc's description fallback is uniquely "(no description — set confidence
|
|
97
97
|
// LOW)" (the others use "(no description)"); the shared scaffold defaults to
|
|
@@ -100,6 +100,7 @@ function buildPrompt({ task, valueSummary, diff, changedFiles, priorRound }) {
|
|
|
100
100
|
// stays byte-identical to the inlined version.
|
|
101
101
|
return buildWorkerPrompt({
|
|
102
102
|
priorRound,
|
|
103
|
+
contextBundle, // task 1002660: shared post-change file contents
|
|
103
104
|
header: PROMPT_HEADER,
|
|
104
105
|
task: { ...t, description: t.description || '(no description — set confidence LOW)' },
|
|
105
106
|
valueSummary,
|
|
@@ -80,7 +80,7 @@ No prose, no markdown fences:
|
|
|
80
80
|
|
|
81
81
|
You have a Read tool. Spend reads checking that removed code is unreferenced elsewhere, that new helpers match surrounding style, and that invariants the diff touches are still preserved in the post-merge state. Do not run tests, do not edit files, do not call the network.`;
|
|
82
82
|
|
|
83
|
-
function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, priorRound }) {
|
|
83
|
+
function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, priorRound, contextBundle }) {
|
|
84
84
|
const t = task || {};
|
|
85
85
|
|
|
86
86
|
// Quality owns a diff-size section (the economy axis) before the diff.
|
|
@@ -101,6 +101,7 @@ function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, priorR
|
|
|
101
101
|
|
|
102
102
|
return buildWorkerPrompt({
|
|
103
103
|
priorRound,
|
|
104
|
+
contextBundle, // task 1002660: shared post-change file contents
|
|
104
105
|
header: PROMPT_HEADER,
|
|
105
106
|
task: t,
|
|
106
107
|
valueSummary,
|
|
@@ -62,7 +62,7 @@ const REGRADE_DELTA_POLICY = [
|
|
|
62
62
|
function buildWorkerPrompt({
|
|
63
63
|
header, task, valueSummary, diff, changedFiles,
|
|
64
64
|
taskHeader, descLabel, emptyFilesText, readFilesHint, finalInstruction,
|
|
65
|
-
afterKindLines = [], beforeDiffLines = [], priorRound = null,
|
|
65
|
+
afterKindLines = [], beforeDiffLines = [], priorRound = null, contextBundle = null,
|
|
66
66
|
}) {
|
|
67
67
|
const t = task || {};
|
|
68
68
|
const desc = (t.description || '(no description)').trim();
|
|
@@ -106,6 +106,29 @@ function buildWorkerPrompt({
|
|
|
106
106
|
lines.push('');
|
|
107
107
|
lines.push(readFilesHint);
|
|
108
108
|
lines.push('');
|
|
109
|
+
// task 1002660: the shared context bundle — post-change file contents read
|
|
110
|
+
// ONCE by the caller so four workers don't each re-explore the repo (the
|
|
111
|
+
// dominant panel cost). Untrusted builder-authored content → fenced, one
|
|
112
|
+
// block per file. The trusted lead line tells the worker Read is only
|
|
113
|
+
// needed for files NOT bundled.
|
|
114
|
+
if (contextBundle && Array.isArray(contextBundle.files) && contextBundle.files.length > 0) {
|
|
115
|
+
lines.push('---');
|
|
116
|
+
lines.push(`## Post-change file contents (${contextBundle.files.length} of ${(changedFiles || []).length} changed files, bundled)`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push('The post-change contents of the changed files below are included inline — use your Read tool ONLY for files not bundled here (listed after the blocks) or for unchanged context files.');
|
|
119
|
+
lines.push('');
|
|
120
|
+
for (const f of contextBundle.files) {
|
|
121
|
+
// The filename is builder-controlled — scrub it like every other
|
|
122
|
+
// untrusted string that lands OUTSIDE a fence (panel hacker finding).
|
|
123
|
+
const label = `FILE: ${scrubUntrusted(String(f.file))}${f.truncated ? ' (truncated)' : ''}`;
|
|
124
|
+
lines.push(...fenceBlock(label, f.content));
|
|
125
|
+
lines.push('');
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(contextBundle.skipped) && contextBundle.skipped.length > 0) {
|
|
128
|
+
lines.push(`Not bundled (deleted, binary, or over the size cap — Read them if needed): ${contextBundle.skipped.map((s) => scrubUntrusted(String(s))).join(', ')}`);
|
|
129
|
+
lines.push('');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
109
132
|
// task 1002662: prior panel round context for a delta-aware re-grade. The
|
|
110
133
|
// findings/response/interim summary are builder-adjacent data → fenced; the
|
|
111
134
|
// policy that governs them is trusted text outside the fences.
|
|
@@ -102,6 +102,11 @@ const DEFAULT_SHOTS = V3.shots || 3;
|
|
|
102
102
|
const ASSUMED_BUILDER_MODEL = V4.assumed_builder_model || 'claude-fable-5';
|
|
103
103
|
const DEFAULT_GRADER_MODEL = V4.default_model || 'claude-sonnet-5';
|
|
104
104
|
const FALLTHROUGH_CONFIDENCE = V4.fallthrough_confidence ?? 0.3;
|
|
105
|
+
// task 1002660: per-worker model tier overrides (rubric-configured). The
|
|
106
|
+
// Quality worker was 41% of panel spend with 75% nit findings — its cq/economy
|
|
107
|
+
// judgment doesn't need the full cross-model tier. Keys are worker kinds;
|
|
108
|
+
// values are model ids. workerModelFor guards the cross-model invariant.
|
|
109
|
+
const WORKER_MODEL_TIERS = V4.worker_model_tiers || {};
|
|
105
110
|
const workers = require('./grader-workers');
|
|
106
111
|
// Carved into modules/grading/ (BV1.R73 / ADR 0091 §1+§4): the two deterministic
|
|
107
112
|
// trust classifiers are KERNEL (KERNEL_FILES, R70) — reached through the doorway,
|
|
@@ -235,6 +240,20 @@ function selectGraderModel({ builderModel } = {}) {
|
|
|
235
240
|
return DEFAULT_GRADER_MODEL;
|
|
236
241
|
}
|
|
237
242
|
|
|
243
|
+
// task 1002660: resolve one worker's model — the rubric tier override when one
|
|
244
|
+
// exists, else the panel's cross-model pick. The override is refused (falls
|
|
245
|
+
// back to graderModel) when it lands on the BUILDER's own family: tiering is
|
|
246
|
+
// a cost lever, never a hole in the ADR 0024 cross-model guarantee.
|
|
247
|
+
const MODEL_FAMILY_TOKENS = ['fable', 'mythos', 'opus', 'sonnet', 'haiku'];
|
|
248
|
+
function workerModelFor(kind, graderModel, builderModel) {
|
|
249
|
+
const tier = WORKER_MODEL_TIERS[kind];
|
|
250
|
+
if (!tier) return graderModel;
|
|
251
|
+
const bm = String(builderModel || ASSUMED_BUILDER_MODEL).toLowerCase();
|
|
252
|
+
const tierFamily = MODEL_FAMILY_TOKENS.find((t) => String(tier).toLowerCase().includes(t));
|
|
253
|
+
if (tierFamily && bm.includes(tierFamily)) return graderModel;
|
|
254
|
+
return tier;
|
|
255
|
+
}
|
|
256
|
+
|
|
238
257
|
// ---------- v3: prompt builder (default-fail framing) ----------
|
|
239
258
|
|
|
240
259
|
// The instance's project name, injected into the grader's identity line so a
|
|
@@ -1135,6 +1154,60 @@ function runSubagent({ prompt, opts = {} }) {
|
|
|
1135
1154
|
});
|
|
1136
1155
|
}
|
|
1137
1156
|
|
|
1157
|
+
// ---------- task 1002660: shared context bundle ----------
|
|
1158
|
+
//
|
|
1159
|
+
// ~90% of panel cost was fixed re-exploration: four subprocesses each spending
|
|
1160
|
+
// agentic Read turns re-opening the same changed files. Read them ONCE here
|
|
1161
|
+
// and hand every worker the post-change contents inline (fenced, untrusted),
|
|
1162
|
+
// so a worker's Read tool is only needed for files beyond the bundle.
|
|
1163
|
+
const CONTEXT_BUNDLE_MAX_FILES = 24;
|
|
1164
|
+
const CONTEXT_BUNDLE_MAX_TOTAL_CHARS = 120_000;
|
|
1165
|
+
const CONTEXT_BUNDLE_MAX_FILE_CHARS = 16_000;
|
|
1166
|
+
// Binary-ish extensions never worth inlining (the diff already names them).
|
|
1167
|
+
const CONTEXT_BUNDLE_SKIP_RE = /\.(?:png|jpe?g|gif|webp|ico|svgz|woff2?|ttf|eot|zip|gz|tgz|tar|pdf|mp[34]|wav|ogg|bin|exe|dll|node)$/i;
|
|
1168
|
+
|
|
1169
|
+
// Pure-ish (fs reads only, injectable): returns { files: [{file, content,
|
|
1170
|
+
// truncated}], skipped: [file...], total_chars } for the post-change contents
|
|
1171
|
+
// of changedFiles under cwd. Deleted/unreadable files land in skipped —
|
|
1172
|
+
// their absence from the bundle is visible, never silent.
|
|
1173
|
+
//
|
|
1174
|
+
// Security (panel hacker finding, this task): changedFiles is builder-
|
|
1175
|
+
// controlled and git commits symlinks, so a naive readFileSync would follow a
|
|
1176
|
+
// committed link OUT of the repo (SSH keys, session tokens) and inject the
|
|
1177
|
+
// target into worker prompts + the stored grade. lstat-refuse symlinks and
|
|
1178
|
+
// non-files, and require the resolved path to stay under cwd (containment) —
|
|
1179
|
+
// a refused entry lands in skipped, visible like every other exclusion.
|
|
1180
|
+
function buildContextBundle({ changedFiles, cwd, fsImpl = fs } = {}) {
|
|
1181
|
+
const files = [];
|
|
1182
|
+
const skipped = [];
|
|
1183
|
+
let total = 0;
|
|
1184
|
+
const root = path.resolve(cwd || process.cwd());
|
|
1185
|
+
for (const rel of (Array.isArray(changedFiles) ? changedFiles : [])) {
|
|
1186
|
+
if (files.length >= CONTEXT_BUNDLE_MAX_FILES || total >= CONTEXT_BUNDLE_MAX_TOTAL_CHARS) {
|
|
1187
|
+
skipped.push(rel);
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (CONTEXT_BUNDLE_SKIP_RE.test(rel)) { skipped.push(rel); continue; }
|
|
1191
|
+
let content;
|
|
1192
|
+
try {
|
|
1193
|
+
const abs = path.resolve(root, rel);
|
|
1194
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) { skipped.push(rel); continue; }
|
|
1195
|
+
const st = fsImpl.lstatSync(abs);
|
|
1196
|
+
if (!st.isFile() || st.isSymbolicLink()) { skipped.push(rel); continue; }
|
|
1197
|
+
content = fsImpl.readFileSync(abs, 'utf8');
|
|
1198
|
+
} catch (_) { skipped.push(rel); continue; } // deleted by the diff, or unreadable
|
|
1199
|
+
let truncated = false;
|
|
1200
|
+
if (content.length > CONTEXT_BUNDLE_MAX_FILE_CHARS) {
|
|
1201
|
+
content = content.slice(0, CONTEXT_BUNDLE_MAX_FILE_CHARS);
|
|
1202
|
+
truncated = true;
|
|
1203
|
+
}
|
|
1204
|
+
if (total + content.length > CONTEXT_BUNDLE_MAX_TOTAL_CHARS) { skipped.push(rel); continue; }
|
|
1205
|
+
total += content.length;
|
|
1206
|
+
files.push({ file: rel, content, truncated });
|
|
1207
|
+
}
|
|
1208
|
+
return { files, skipped, total_chars: total };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1138
1211
|
// System 5 (task 1359): an OPT-IN cached path around runSubagent. When the caller
|
|
1139
1212
|
// supplies a `cache` spec (from llm-cache.makeCacheSpec), a hit replays the stored
|
|
1140
1213
|
// result at cost_usd:0 (so $0 flows naturally into extractCostUsd → session-cost);
|
|
@@ -1652,6 +1725,20 @@ async function gradeSubagent({ task, valueSummary, diff, changedFiles, diffStats
|
|
|
1652
1725
|
...(permPath ? permPath.findings : []),
|
|
1653
1726
|
];
|
|
1654
1727
|
|
|
1728
|
+
// task 1002660: one read of the changed files, shared by all four workers —
|
|
1729
|
+
// kills the per-worker re-exploration that dominated panel cost.
|
|
1730
|
+
const contextBundle = buildContextBundle({ changedFiles, cwd: runOpts.cwd });
|
|
1731
|
+
// task 1002660: cache-wrapped spawns. Same prompt + model replays at $0
|
|
1732
|
+
// (llm-cache, System 5) — the prompt embeds the diff + bundle, so the key
|
|
1733
|
+
// needs no separate scope sha. Absent llm-cache → plain runSubagent.
|
|
1734
|
+
const runSubagentPanelCached = ({ prompt, opts }) => runSubagentCached({
|
|
1735
|
+
prompt,
|
|
1736
|
+
opts,
|
|
1737
|
+
cache: api.llmCache
|
|
1738
|
+
? api.llmCache.makeCacheSpec({ prompt, model: opts?.model, task_type: 'grade_worker' })
|
|
1739
|
+
: null,
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1655
1742
|
const startedAt = Date.now();
|
|
1656
1743
|
const workerPromises = panel.map((worker) => workers.runWorker(worker, {
|
|
1657
1744
|
task,
|
|
@@ -1659,12 +1746,14 @@ async function gradeSubagent({ task, valueSummary, diff, changedFiles, diffStats
|
|
|
1659
1746
|
diff,
|
|
1660
1747
|
changedFiles,
|
|
1661
1748
|
diffStats,
|
|
1662
|
-
|
|
1749
|
+
// task 1002660: rubric tier override per worker (cross-model-guarded).
|
|
1750
|
+
model: workerModelFor(worker.KIND, graderModel, builderModel),
|
|
1663
1751
|
runOpts,
|
|
1664
|
-
runSubagentFn:
|
|
1752
|
+
runSubagentFn: runSubagentPanelCached,
|
|
1665
1753
|
extractAssistantResultFn: extractAssistantResult,
|
|
1666
1754
|
deterministicFindings: deterministicFindings.length ? deterministicFindings : null,
|
|
1667
1755
|
priorRound: priorRound || null, // task 1002662: delta-aware re-grade context
|
|
1756
|
+
contextBundle, // task 1002660: post-change file contents, read once
|
|
1668
1757
|
}));
|
|
1669
1758
|
const workerResults = await Promise.all(workerPromises);
|
|
1670
1759
|
// Append the deterministic checks as pseudo-workers so the any-fail gate
|
|
@@ -1906,6 +1995,8 @@ module.exports = {
|
|
|
1906
1995
|
// v3 surface (kept exported for diagnostics + tests)
|
|
1907
1996
|
buildPrompt,
|
|
1908
1997
|
selectGraderModel,
|
|
1998
|
+
workerModelFor, // task 1002660: per-worker tier override (cross-model-guarded)
|
|
1999
|
+
buildContextBundle, // task 1002660: shared post-change file contents, read once per panel
|
|
1909
2000
|
validateAndScore,
|
|
1910
2001
|
aggregateShots,
|
|
1911
2002
|
runSubagent,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.50",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.50",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.50",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/npmrc.js
CHANGED
|
@@ -33,7 +33,7 @@ function npmrcContent({ scope = CORE_SCOPE, host = REGISTRY_HOST, tokenEnv = TOK
|
|
|
33
33
|
return [
|
|
34
34
|
`; Cloud Bongos — env-fed auth for the PRIVATE ${scope}/* packages (ADR 0134, task 2090).`,
|
|
35
35
|
`; The token EXPANDS FROM THE ENVIRONMENT at install time; nothing secret is committed here.`,
|
|
36
|
-
`; \`npm ci\` needs ${tokenEnv} set (a read-only
|
|
36
|
+
`; \`npm ci\` needs ${tokenEnv} set (a read-only npm access token from the paid \`bongos\` org);`,
|
|
37
37
|
`; without it the private package 404s — that is the expected tokenless outcome.`,
|
|
38
38
|
`${scope}:registry=https://${host}/`,
|
|
39
39
|
`//${host}/:_authToken=\${${tokenEnv}}`,
|
package/scripts/gds/ship.js
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
// # staged-fresh (a re-run); it can't pre-approve.
|
|
57
57
|
|
|
58
58
|
const crypto = require('node:crypto');
|
|
59
|
-
const { execSync, execFileSync, spawnSync } = require('node:child_process');
|
|
59
|
+
const { execSync, execFileSync, spawnSync, spawn } = require('node:child_process');
|
|
60
60
|
const fs = require('node:fs');
|
|
61
61
|
const os = require('node:os');
|
|
62
62
|
const path = require('node:path');
|
|
@@ -2066,6 +2066,28 @@ function shouldReuseGrade(storedGrade, currentDiffHash) {
|
|
|
2066
2066
|
return storedGrade.diff_hash === currentDiffHash;
|
|
2067
2067
|
}
|
|
2068
2068
|
|
|
2069
|
+
// task 1002660: task_grades has no issues column — the panel's deduped
|
|
2070
|
+
// issues[] survive only inside signals.workers[].issues. Rebuild them (same
|
|
2071
|
+
// (file, summary) dedupe as aggregatePanel) so the reuse re-POST carries the
|
|
2072
|
+
// substance the original PASS carried. Before this, the reused body posted
|
|
2073
|
+
// issues: undefined, so every reused PASS with findings read as a zero-issue
|
|
2074
|
+
// pass server-side and tripped the SR-9 plausibility guard into
|
|
2075
|
+
// manual-confirm — 0/106 successful uses, the feature defeated itself.
|
|
2076
|
+
function reconstructPanelIssues(signals) {
|
|
2077
|
+
const out = [];
|
|
2078
|
+
const seen = new Set();
|
|
2079
|
+
for (const w of (Array.isArray(signals?.workers) ? signals.workers : [])) {
|
|
2080
|
+
for (const it of (Array.isArray(w?.issues) ? w.issues : [])) {
|
|
2081
|
+
if (!it || typeof it !== 'object') continue;
|
|
2082
|
+
const key = `${it.file || ''}|${it.summary || ''}`;
|
|
2083
|
+
if (seen.has(key)) continue;
|
|
2084
|
+
seen.add(key);
|
|
2085
|
+
out.push(it);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return out;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2069
2091
|
// --- Subagent grader runner ---
|
|
2070
2092
|
// Wraps grader.gradeSubagent so ship.js callers see a single function with
|
|
2071
2093
|
// progress logging. MAS Phase 2.0 #410: gradeSubagent now runs the WORKER
|
|
@@ -2118,6 +2140,15 @@ async function runSubagentGrader({ task, valueSummary, baseline, changedFiles: p
|
|
|
2118
2140
|
builderModel, // task 1002659: real session model → cross-model grader pick
|
|
2119
2141
|
runOpts,
|
|
2120
2142
|
});
|
|
2143
|
+
// task 1002660: the single generalist shot has no Hacker or Efficiency
|
|
2144
|
+
// lens — acceptable for genuinely inert copy/docs files, but never
|
|
2145
|
+
// silently. The marker makes the dropped lenses queryable by /grade-audit
|
|
2146
|
+
// (same pattern as implausible_pass_waived, ADR 0164).
|
|
2147
|
+
result.signals = {
|
|
2148
|
+
...(result.signals || {}),
|
|
2149
|
+
trivial_path: true,
|
|
2150
|
+
lenses_dropped: ['hacker', 'efficiency'],
|
|
2151
|
+
};
|
|
2121
2152
|
} else {
|
|
2122
2153
|
const panel = grader.WORKERS.DEFAULT_PANEL;
|
|
2123
2154
|
console.log(` subagent grader ${grader.VERSION}: panel of ${panel.length} workers (${panel.join(', ')}), model=${grader.selectGraderModel({ builderModel })} (builder ${builderModel || `${grader.ASSUMED_BUILDER_MODEL} assumed`}), changed_files=${changedFiles.length}, diff_chars=${diff.length}, per-worker timeout=${GRADER_TIMEOUT_MS}ms`);
|
|
@@ -2160,6 +2191,21 @@ function runSmoke() {
|
|
|
2160
2191
|
return { ran: true, clean: okPms };
|
|
2161
2192
|
}
|
|
2162
2193
|
|
|
2194
|
+
// task 1002660: the async twin of runSmoke for the PRE-RESOLVE gate only. The
|
|
2195
|
+
// sync runSmoke blocks the event loop (execSync), which would freeze the
|
|
2196
|
+
// overlapped grade-panel promise mid-flight; awaiting this one keeps the loop
|
|
2197
|
+
// free so the panel's subprocess callbacks progress while smoke runs. The
|
|
2198
|
+
// post-merge smoke call keeps the sync form (nothing overlaps there).
|
|
2199
|
+
function runSmokeAsync() {
|
|
2200
|
+
return new Promise((resolve) => {
|
|
2201
|
+
const child = spawn(process.execPath, ['scripts/gds/smoke-gds.js'], {
|
|
2202
|
+
cwd: REPO_ROOT, stdio: 'inherit', windowsHide: true,
|
|
2203
|
+
});
|
|
2204
|
+
child.on('error', () => resolve({ ran: true, clean: false }));
|
|
2205
|
+
child.on('close', (code) => resolve({ ran: true, clean: code === 0 }));
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2163
2209
|
// task 1002605 (G-18): the pre-resolve smoke gate's two message sets, pure so the
|
|
2164
2210
|
// wording is pinned by a DB-free unit test (tests/ship_smoke_gate.mjs). A smoke
|
|
2165
2211
|
// FAIL used to fall silently through into the claim resolve — claim consumed,
|
|
@@ -2827,7 +2873,11 @@ function buildPriorRoundContext({ storedGrade, responseText = '', interimCommits
|
|
|
2827
2873
|
// and check whether a stored PASS grade exists for the same hash. On a match
|
|
2828
2874
|
// we re-POST the stored grade (marking it reused in signals) and skip the
|
|
2829
2875
|
// panel entirely — cheap re-ships with an unchanged diff stay fast.
|
|
2830
|
-
|
|
2876
|
+
// task 1002660: the COMPUTE half — the reuse lookup + the panel run. No server
|
|
2877
|
+
// writes, so shipMain can start it before the smoke gate and overlap the
|
|
2878
|
+
// multi-minute panel with smoke + the claim resolve. Returns
|
|
2879
|
+
// { kind: 'reuse', currentHash, reusedResult } | { kind: 'fresh', currentHash, gradeResult }.
|
|
2880
|
+
async function computeGradeResult({ taskId, task, summary, baseline, committedFiles, priorRound = null }) {
|
|
2831
2881
|
// Compute the fingerprint of the raw (untruncated) diff so we can compare
|
|
2832
2882
|
// against any stored grade, independent of the display-truncation cap.
|
|
2833
2883
|
const currentHash = computeRawDiffHash(baseline);
|
|
@@ -2838,7 +2888,7 @@ async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles,
|
|
|
2838
2888
|
|
|
2839
2889
|
if (shouldReuseGrade(storedGrade, currentHash)) {
|
|
2840
2890
|
console.log(' Grade reuse: stored PASS grade matches current diff hash — skipping panel.');
|
|
2841
|
-
//
|
|
2891
|
+
// Reuse the stored result with a reused marker so the audit trail is
|
|
2842
2892
|
// complete. We keep the original grader_kind and inject reuse metadata
|
|
2843
2893
|
// into signals so a bare table scan can spot reused grades.
|
|
2844
2894
|
const reusedSignals = {
|
|
@@ -2855,10 +2905,12 @@ async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles,
|
|
|
2855
2905
|
rubric_scores: storedGrade.rubric_scores,
|
|
2856
2906
|
notes_md: storedGrade.notes_md,
|
|
2857
2907
|
signals: reusedSignals,
|
|
2908
|
+
// task 1002660: carry the original panel's issues (rebuilt from
|
|
2909
|
+
// signals.workers[].issues — the row stores them nowhere else) so the
|
|
2910
|
+
// reused PASS stands on the same substance/evidence it passed with.
|
|
2911
|
+
issues: reconstructPanelIssues(storedGrade.signals),
|
|
2858
2912
|
};
|
|
2859
|
-
|
|
2860
|
-
return (await cliClient()).tasks.postTasksIdGrade({ id: taskId, body: payload });
|
|
2861
|
-
// No logGraderPanelCost — the panel didn't run, there's no cost to log.
|
|
2913
|
+
return { kind: 'reuse', currentHash, reusedResult };
|
|
2862
2914
|
}
|
|
2863
2915
|
}
|
|
2864
2916
|
|
|
@@ -2875,7 +2927,25 @@ async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles,
|
|
|
2875
2927
|
if (priorRound?.audit) {
|
|
2876
2928
|
gradeResult.signals = { ...(gradeResult.signals || {}), regrade: priorRound.audit };
|
|
2877
2929
|
}
|
|
2878
|
-
|
|
2930
|
+
return { kind: 'fresh', currentHash, gradeResult };
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// The RECORD half: POST the computed grade + log the panel cost. `precomputed`
|
|
2934
|
+
// (a promise from computeGradeResult, task 1002660) lets the ship path hand in
|
|
2935
|
+
// the overlapped computation; the --regrade path passes nothing and computes
|
|
2936
|
+
// inline — behavior identical to the pre-split gradeAndRecord either way.
|
|
2937
|
+
async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles, priorRound = null, precomputed = null }) {
|
|
2938
|
+
const computed = await (precomputed
|
|
2939
|
+
|| computeGradeResult({ taskId, task, summary, baseline, committedFiles, priorRound }));
|
|
2940
|
+
|
|
2941
|
+
if (computed.kind === 'reuse') {
|
|
2942
|
+
const payload = { ...buildGradePayload(computed.reusedResult, committedFiles), diff_hash: computed.currentHash };
|
|
2943
|
+
return (await cliClient()).tasks.postTasksIdGrade({ id: taskId, body: payload });
|
|
2944
|
+
// No logGraderPanelCost — the panel didn't run, there's no cost to log.
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
const { gradeResult } = computed;
|
|
2948
|
+
const payload = { ...buildGradePayload(gradeResult, committedFiles), diff_hash: computed.currentHash ?? undefined };
|
|
2879
2949
|
const gradePost = await (await cliClient()).tasks.postTasksIdGrade({ id: taskId, body: payload });
|
|
2880
2950
|
// Log the panel's cost once the grade is recorded — independent of the
|
|
2881
2951
|
// pass/fail VERDICT (the panel ran, and cost money, either way), but only
|
|
@@ -3844,8 +3914,30 @@ async function shipMain() {
|
|
|
3844
3914
|
// on the actual ship: the --approved re-run, or a laptop / non-game ship the
|
|
3845
3915
|
// gate waved straight through. (#1061 — the gate is a no-op for those, so this
|
|
3846
3916
|
// is the same single smoke run as before, just sequenced after it.)
|
|
3917
|
+
|
|
3918
|
+
// task 1002660: start the grade COMPUTATION now, so the multi-minute panel
|
|
3919
|
+
// overlaps the smoke run + claim resolve instead of serializing behind them.
|
|
3920
|
+
// computeGradeResult performs no server writes — the grade POST stays in the
|
|
3921
|
+
// grade step below, after the resolve lands the task at 'completed'. On a
|
|
3922
|
+
// smoke abort the in-flight panel is discarded (its partial spend is lost —
|
|
3923
|
+
// rare, and cheaper than serializing every ship behind smoke).
|
|
3924
|
+
let gradeCompute = null;
|
|
3925
|
+
let fullTaskForGrade = null;
|
|
3926
|
+
if (!skipGrade) {
|
|
3927
|
+
const tFetch = await (await cliClient()).tasks.getTasksId({ id: taskId });
|
|
3928
|
+
fullTaskForGrade = tFetch.ok ? tFetch.data.task : { id: taskId, title: ac.title };
|
|
3929
|
+
console.log('Starting the quality grader panel (overlaps the smoke run — task 1002660)...');
|
|
3930
|
+
gradeCompute = computeGradeResult({
|
|
3931
|
+
taskId, task: fullTaskForGrade, summary,
|
|
3932
|
+
baseline: graderBaseline, committedFiles,
|
|
3933
|
+
});
|
|
3934
|
+
gradeCompute.catch(() => {}); // an abort path must not die on an unhandled rejection
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3847
3937
|
console.log('Running smoke tests against production...');
|
|
3848
|
-
|
|
3938
|
+
// task 1002660: the async twin — awaiting keeps the event loop free so the
|
|
3939
|
+
// overlapped panel's subprocess callbacks progress while smoke runs.
|
|
3940
|
+
const smoke = await runSmokeAsync();
|
|
3849
3941
|
const passed = smoke.clean;
|
|
3850
3942
|
console.log('');
|
|
3851
3943
|
console.log(`Verification: smoke=${smoke.clean ? 'clean' : 'FAIL'}, overall=${passed ? 'PASS' : 'FAIL'}`);
|
|
@@ -3977,24 +4069,22 @@ async function shipMain() {
|
|
|
3977
4069
|
console.log('');
|
|
3978
4070
|
console.log('Running subagent quality grader...');
|
|
3979
4071
|
|
|
3980
|
-
// Fetch the full task (description, kind, discipline, etc.) so the
|
|
3981
|
-
// grader prompt has the intent the subagent is grading against.
|
|
3982
|
-
const tFetch = await (await cliClient()).tasks.getTasksId({ id: taskId });
|
|
3983
|
-
const fullTask = tFetch.ok ? tFetch.data.task : { id: taskId, title: ac.title };
|
|
3984
|
-
|
|
3985
4072
|
// #513 + #536: the grader reads COMMITTED history (baseline...HEAD), not
|
|
3986
4073
|
// the working tree. The empty-committed-diff guard now runs earlier — BEFORE
|
|
3987
4074
|
// the claim is resolved (see above) — so by the time we reach here the diff
|
|
3988
4075
|
// is guaranteed non-empty on the grader path. Reuse the baseline + file list
|
|
3989
|
-
// computed there so we don't diff twice. #678: gradeAndRecord
|
|
3990
|
-
//
|
|
3991
|
-
// the
|
|
4076
|
+
// computed there so we don't diff twice. #678: gradeAndRecord POSTs the
|
|
4077
|
+
// result and logs the panel cost — the same unit the --regrade path reuses.
|
|
4078
|
+
// task 1002660: the panel computation was started BEFORE the smoke gate
|
|
4079
|
+
// (gradeCompute above) — hand it in so this step only waits out whatever
|
|
4080
|
+
// panel time smoke + resolve didn't already absorb.
|
|
3992
4081
|
const gradePost = await gradeAndRecord({
|
|
3993
4082
|
taskId,
|
|
3994
|
-
task:
|
|
4083
|
+
task: fullTaskForGrade || { id: taskId, title: ac.title },
|
|
3995
4084
|
summary,
|
|
3996
4085
|
baseline: graderBaseline,
|
|
3997
4086
|
committedFiles,
|
|
4087
|
+
precomputed: gradeCompute,
|
|
3998
4088
|
});
|
|
3999
4089
|
|
|
4000
4090
|
if (!gradePost.ok) {
|
|
@@ -4281,7 +4371,8 @@ module.exports = {
|
|
|
4281
4371
|
// V4.R08: diff fingerprint + grade-reuse helpers — exported pure so the
|
|
4282
4372
|
// truth table (same hash+pass→reuse, fail→no reuse, mismatch→re-grade,
|
|
4283
4373
|
// null→re-grade) is unit-tested without git/network.
|
|
4284
|
-
diffHash, computeRawDiffHash, shouldReuseGrade, gradeAndRecord,
|
|
4374
|
+
diffHash, computeRawDiffHash, shouldReuseGrade, reconstructPanelIssues, gradeAndRecord,
|
|
4375
|
+
computeGradeResult, // task 1002660: the no-server-write compute half, overlapped with smoke
|
|
4285
4376
|
// task 1002662: delta-aware re-grade — the prior-round payload builder,
|
|
4286
4377
|
// exported pure so the panel/audit shapes are pinned without git/network.
|
|
4287
4378
|
buildPriorRoundContext,
|
package/src/module-api.js
CHANGED
|
@@ -525,8 +525,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
525
525
|
// landed since 1.19.47 with no explicit bump. run 31522865111. (task 1002620)
|
|
526
526
|
// 1.19.49 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
527
527
|
// landed since 1.19.48 with no explicit bump. run 31523559509. (task 1002620)
|
|
528
|
+
// 1.19.50 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
529
|
+
// landed since 1.19.49 with no explicit bump. run 31524280584. (task 1002620)
|
|
528
530
|
// ---------------------------------------------------------------------------
|
|
529
|
-
const CORE_VERSION = '1.19.
|
|
531
|
+
const CORE_VERSION = '1.19.50'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
530
532
|
|
|
531
533
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
532
534
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -15,7 +15,7 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { makeSqlAwareClient } from './helpers.mjs';
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
|
-
const { diffHash, shouldReuseGrade } = require('../scripts/gds/ship.js');
|
|
18
|
+
const { diffHash, shouldReuseGrade, reconstructPanelIssues } = require('../scripts/gds/ship.js');
|
|
19
19
|
const db = require('../modules/lifecycle/db.js');
|
|
20
20
|
// applyGrade resolves the 'reward' kernel port; register it so calls in the
|
|
21
21
|
// passed=true path don't blow up on null (mirrors grade_attribution.mjs).
|
|
@@ -111,6 +111,34 @@ test('shouldReuseGrade → false when currentDiffHash is null (no baseline)', ()
|
|
|
111
111
|
assert.equal(shouldReuseGrade({ passed: true, diff_hash: HASH_A }, null), false);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
// ======================================================================
|
|
115
|
+
// 2b. reconstructPanelIssues — the reuse re-POST carries the original
|
|
116
|
+
// panel's issues, rebuilt from signals.workers[].issues (task 1002660)
|
|
117
|
+
// ======================================================================
|
|
118
|
+
|
|
119
|
+
test('reconstructPanelIssues rebuilds deduped issues from workers', () => {
|
|
120
|
+
const issues = reconstructPanelIssues({
|
|
121
|
+
workers: [
|
|
122
|
+
{ kind: 'narc', issues: [{ severity: 'nit', file: 'a.js', summary: 'style' }] },
|
|
123
|
+
{ kind: 'quality', issues: [
|
|
124
|
+
{ severity: 'nit', file: 'a.js', summary: 'style' }, // dupe of narc's
|
|
125
|
+
{ severity: 'minor', file: 'b.js', summary: 'dead code' },
|
|
126
|
+
] },
|
|
127
|
+
{ kind: 'hacker', issues: [] },
|
|
128
|
+
{ kind: 'efficiency' }, // no issues field
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
assert.equal(issues.length, 2, 'dupes collapse on (file, summary)');
|
|
132
|
+
assert.deepEqual(issues.map((i) => i.file), ['a.js', 'b.js']);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('reconstructPanelIssues → [] on missing/empty/junk signals', () => {
|
|
136
|
+
assert.deepEqual(reconstructPanelIssues(null), []);
|
|
137
|
+
assert.deepEqual(reconstructPanelIssues({}), []);
|
|
138
|
+
assert.deepEqual(reconstructPanelIssues({ workers: 'junk' }), []);
|
|
139
|
+
assert.deepEqual(reconstructPanelIssues({ workers: [null, { issues: [null, 'junk'] }] }), []);
|
|
140
|
+
});
|
|
141
|
+
|
|
114
142
|
// ======================================================================
|
|
115
143
|
// 3. db.applyGrade stores diff_hash (SQL-aware fake pool, NODE_ENV=test)
|
|
116
144
|
// ======================================================================
|
package/tests/grader.mjs
CHANGED
|
@@ -840,6 +840,128 @@ test('selectGraderModel: builder Haiku → grader Opus', () => {
|
|
|
840
840
|
assert.equal(grader.selectGraderModel({ builderModel: 'haiku' }), 'claude-opus-5');
|
|
841
841
|
});
|
|
842
842
|
|
|
843
|
+
// --- task 1002660: per-worker model tiering (cross-model-guarded) ---
|
|
844
|
+
|
|
845
|
+
test('workerModelFor: Quality tiers down to haiku; other workers keep the panel model', () => {
|
|
846
|
+
assert.equal(
|
|
847
|
+
grader.workerModelFor('quality', 'claude-sonnet-5', 'claude-fable-5'),
|
|
848
|
+
'claude-haiku-4-5-20251001'
|
|
849
|
+
);
|
|
850
|
+
for (const kind of ['narc', 'hacker', 'efficiency']) {
|
|
851
|
+
assert.equal(grader.workerModelFor(kind, 'claude-sonnet-5', 'claude-fable-5'), 'claude-sonnet-5');
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test('workerModelFor: a tier override landing on the BUILDER family is refused (cross-model guarantee)', () => {
|
|
856
|
+
// A haiku builder must not be quality-judged by haiku — fall back to the panel model.
|
|
857
|
+
assert.equal(
|
|
858
|
+
grader.workerModelFor('quality', 'claude-opus-5', 'claude-haiku-4-5-20251001'),
|
|
859
|
+
'claude-opus-5'
|
|
860
|
+
);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// --- task 1002660: shared context bundle ---
|
|
864
|
+
|
|
865
|
+
const plainFileStat = { isFile: () => true, isSymbolicLink: () => false };
|
|
866
|
+
function fakeFsFor(files, { symlinks = [] } = {}) {
|
|
867
|
+
const relOf = (p) => Object.keys(files).find((k) => String(p).replace(/\\/g, '/').endsWith(k));
|
|
868
|
+
return {
|
|
869
|
+
lstatSync: (p) => {
|
|
870
|
+
const rel = relOf(p);
|
|
871
|
+
if (!rel) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }
|
|
872
|
+
return symlinks.includes(rel)
|
|
873
|
+
? { isFile: () => false, isSymbolicLink: () => true }
|
|
874
|
+
: plainFileStat;
|
|
875
|
+
},
|
|
876
|
+
readFileSync: (p) => {
|
|
877
|
+
const rel = relOf(p);
|
|
878
|
+
if (!rel) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }
|
|
879
|
+
return files[rel];
|
|
880
|
+
},
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
test('buildContextBundle reads changed files once, skips binaries/missing, respects caps', () => {
|
|
885
|
+
const files = {
|
|
886
|
+
'src/a.js': 'const a = 1;\n',
|
|
887
|
+
'docs/b.md': '# b\n',
|
|
888
|
+
'art/pic.png': 'binarybytes',
|
|
889
|
+
};
|
|
890
|
+
const bundle = grader.buildContextBundle({
|
|
891
|
+
changedFiles: ['src/a.js', 'docs/b.md', 'art/pic.png', 'gone/deleted.js'],
|
|
892
|
+
cwd: '/repo',
|
|
893
|
+
fsImpl: fakeFsFor(files),
|
|
894
|
+
});
|
|
895
|
+
assert.deepEqual(bundle.files.map((f) => f.file), ['src/a.js', 'docs/b.md']);
|
|
896
|
+
assert.deepEqual(bundle.skipped, ['art/pic.png', 'gone/deleted.js'], 'binary + deleted → skipped, visibly');
|
|
897
|
+
assert.equal(bundle.files[0].content, 'const a = 1;\n');
|
|
898
|
+
assert.equal(bundle.total_chars, files['src/a.js'].length + files['docs/b.md'].length);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test('buildContextBundle truncates oversized files and marks them', () => {
|
|
902
|
+
const big = 'x'.repeat(20_000);
|
|
903
|
+
const bundle = grader.buildContextBundle({
|
|
904
|
+
changedFiles: ['big.js'], cwd: '/r',
|
|
905
|
+
fsImpl: { lstatSync: () => plainFileStat, readFileSync: () => big },
|
|
906
|
+
});
|
|
907
|
+
assert.equal(bundle.files[0].truncated, true);
|
|
908
|
+
assert.ok(bundle.files[0].content.length <= 16_000);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test('buildContextBundle REFUSES symlinks — a committed link cannot pull out-of-repo content into prompts (hacker finding)', () => {
|
|
912
|
+
const files = { 'src/a.js': 'ok', 'evil-link.js': 'should never be read' };
|
|
913
|
+
const bundle = grader.buildContextBundle({
|
|
914
|
+
changedFiles: ['src/a.js', 'evil-link.js'],
|
|
915
|
+
cwd: '/repo',
|
|
916
|
+
fsImpl: fakeFsFor(files, { symlinks: ['evil-link.js'] }),
|
|
917
|
+
});
|
|
918
|
+
assert.deepEqual(bundle.files.map((f) => f.file), ['src/a.js']);
|
|
919
|
+
assert.ok(bundle.skipped.includes('evil-link.js'), 'symlink refused, visibly');
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test('buildContextBundle REFUSES path escapes — a ../ entry cannot leave cwd (containment)', () => {
|
|
923
|
+
let readAttempted = false;
|
|
924
|
+
const bundle = grader.buildContextBundle({
|
|
925
|
+
changedFiles: ['../outside/secrets.txt'],
|
|
926
|
+
cwd: '/repo/inner',
|
|
927
|
+
fsImpl: {
|
|
928
|
+
lstatSync: () => plainFileStat,
|
|
929
|
+
readFileSync: () => { readAttempted = true; return 'secret'; },
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
assert.deepEqual(bundle.files, []);
|
|
933
|
+
assert.deepEqual(bundle.skipped, ['../outside/secrets.txt']);
|
|
934
|
+
assert.equal(readAttempted, false, 'the escape is refused before any read');
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test('worker prompts render the context bundle fenced, with the skipped list visible', () => {
|
|
938
|
+
const prompt = grader.WORKERS.narc.buildPrompt({
|
|
939
|
+
task: { id: 1, title: 't', description: 'd' },
|
|
940
|
+
valueSummary: 'v',
|
|
941
|
+
diff: 'diff --git a/x b/x',
|
|
942
|
+
changedFiles: ['src/a.js', 'art/pic.png'],
|
|
943
|
+
contextBundle: {
|
|
944
|
+
files: [{ file: 'src/a.js', content: 'const a = 1;', truncated: false }],
|
|
945
|
+
skipped: ['art/pic.png'],
|
|
946
|
+
total_chars: 12,
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
assert.ok(prompt.includes('Post-change file contents'), 'bundle section renders');
|
|
950
|
+
assert.ok(prompt.includes('const a = 1;'), 'bundled content is present');
|
|
951
|
+
assert.ok(prompt.includes('FILE: src/a.js'), 'per-file fence label present');
|
|
952
|
+
assert.ok(/Not bundled .*art\/pic\.png/.test(prompt), 'skipped files are named, never silent');
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test('worker prompts without a bundle are unchanged (no bundle section)', () => {
|
|
956
|
+
const prompt = grader.WORKERS.narc.buildPrompt({
|
|
957
|
+
task: { id: 1, title: 't', description: 'd' },
|
|
958
|
+
valueSummary: 'v',
|
|
959
|
+
diff: 'diff --git a/x b/x',
|
|
960
|
+
changedFiles: ['src/a.js'],
|
|
961
|
+
});
|
|
962
|
+
assert.ok(!prompt.includes('Post-change file contents'));
|
|
963
|
+
});
|
|
964
|
+
|
|
843
965
|
test('selectGraderModel: missing builder → default (assumed-Fable builder → Sonnet 5); unknown id FALLS BACK, never throws (task 1002659)', () => {
|
|
844
966
|
// No builderModel is the legitimate default path (the caller assumes the
|
|
845
967
|
// owner-top-model builder per CLAUDE.md §2), resolving to the default grader.
|