@bongos/core 1.19.48 → 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 +30 -25
- package/docs/adr/0165-instances-hookless-by-default.md +40 -0
- package/docs/adr/README.md +1 -0
- package/docs/canonical-permissions.md +3 -1
- 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 +5 -1
- package/tests/grade_diff_hash.mjs +29 -1
- package/tests/grader.mjs +122 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
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
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 291,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
14
|
"functional_verbatim": 1217,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1529,
|
|
20
|
+
"tree_sha256": "e96a0d29e2d6adc516aaa0d095dbb1c4d543dbec0508228b2cbcf16a1b400dbd",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1294,10 +1294,15 @@
|
|
|
1294
1294
|
"mode": "0000644",
|
|
1295
1295
|
"sha256": "64fb6e129cf5484266e1b6a9cddd8ba88ff7438b9646c3f4d9ffd0cda3cce642"
|
|
1296
1296
|
},
|
|
1297
|
+
{
|
|
1298
|
+
"path": "docs/adr/0165-instances-hookless-by-default.md",
|
|
1299
|
+
"mode": "0000644",
|
|
1300
|
+
"sha256": "d422e10adc391127a7f39b9eb8b5927c149ee455402bc5e4641f55e8b31b2d15"
|
|
1301
|
+
},
|
|
1297
1302
|
{
|
|
1298
1303
|
"path": "docs/adr/README.md",
|
|
1299
1304
|
"mode": "0000644",
|
|
1300
|
-
"sha256": "
|
|
1305
|
+
"sha256": "09a7ba734571fbb304c60a488fe9ccdb6ca06d01127cfb87f07b1cbb54d69408"
|
|
1301
1306
|
},
|
|
1302
1307
|
{
|
|
1303
1308
|
"path": "docs/api-reference.md",
|
|
@@ -1327,7 +1332,7 @@
|
|
|
1327
1332
|
{
|
|
1328
1333
|
"path": "docs/canonical-permissions.md",
|
|
1329
1334
|
"mode": "0000644",
|
|
1330
|
-
"sha256": "
|
|
1335
|
+
"sha256": "b7f66c6f1fbed274e5ba3295c06db8fc8edb84edd44f6ce92f6734e18267e3d9"
|
|
1331
1336
|
},
|
|
1332
1337
|
{
|
|
1333
1338
|
"path": "docs/design-contract.md",
|
|
@@ -1567,7 +1572,7 @@
|
|
|
1567
1572
|
{
|
|
1568
1573
|
"path": "docs/recipes/private-npm-distribution.md",
|
|
1569
1574
|
"mode": "0000644",
|
|
1570
|
-
"sha256": "
|
|
1575
|
+
"sha256": "5bfca9628859f1cdd79d741bdd50f3fe415430ee3380a2ac7f46b97ebe082d0e"
|
|
1571
1576
|
},
|
|
1572
1577
|
{
|
|
1573
1578
|
"path": "docs/recipes/search-index-ingestion.md",
|
|
@@ -2857,12 +2862,12 @@
|
|
|
2857
2862
|
{
|
|
2858
2863
|
"path": "modules/grading/grader-rubric.json",
|
|
2859
2864
|
"mode": "0000644",
|
|
2860
|
-
"sha256": "
|
|
2865
|
+
"sha256": "6a0a2205fee2a4b0efc422c884826a9fd2af3d8785b393d5f7123e1758f1ebfc"
|
|
2861
2866
|
},
|
|
2862
2867
|
{
|
|
2863
2868
|
"path": "modules/grading/grader-workers/efficiency.js",
|
|
2864
2869
|
"mode": "0000644",
|
|
2865
|
-
"sha256": "
|
|
2870
|
+
"sha256": "ab85c9c7b167e710b07af1d4beb1579a593aa617e1c1990238d329fdb0362035"
|
|
2866
2871
|
},
|
|
2867
2872
|
{
|
|
2868
2873
|
"path": "modules/grading/grader-workers/extract-json.js",
|
|
@@ -2877,22 +2882,22 @@
|
|
|
2877
2882
|
{
|
|
2878
2883
|
"path": "modules/grading/grader-workers/hacker.js",
|
|
2879
2884
|
"mode": "0000644",
|
|
2880
|
-
"sha256": "
|
|
2885
|
+
"sha256": "c7177625382e2ea61ee706c6b9b8669d9d452cdc0a92235695438b249c1ab6fc"
|
|
2881
2886
|
},
|
|
2882
2887
|
{
|
|
2883
2888
|
"path": "modules/grading/grader-workers/index.js",
|
|
2884
2889
|
"mode": "0000644",
|
|
2885
|
-
"sha256": "
|
|
2890
|
+
"sha256": "8ec5e91341ec5247ee643c6ce75fb5ec21a62b3bba176fdde060cdb352edb808"
|
|
2886
2891
|
},
|
|
2887
2892
|
{
|
|
2888
2893
|
"path": "modules/grading/grader-workers/narc.js",
|
|
2889
2894
|
"mode": "0000644",
|
|
2890
|
-
"sha256": "
|
|
2895
|
+
"sha256": "f35c3fce7feb6198df0011a9d22533038ca0766733c97a31841a184251800d39"
|
|
2891
2896
|
},
|
|
2892
2897
|
{
|
|
2893
2898
|
"path": "modules/grading/grader-workers/quality.js",
|
|
2894
2899
|
"mode": "0000644",
|
|
2895
|
-
"sha256": "
|
|
2900
|
+
"sha256": "003b51b4a5eab34bb634ceaad5ae9c1ae6f2566423fe0d15cef79b936dc6756b"
|
|
2896
2901
|
},
|
|
2897
2902
|
{
|
|
2898
2903
|
"path": "modules/grading/grader-workers/rubric.js",
|
|
@@ -2902,7 +2907,7 @@
|
|
|
2902
2907
|
{
|
|
2903
2908
|
"path": "modules/grading/grader-workers/worker-prompt.js",
|
|
2904
2909
|
"mode": "0000644",
|
|
2905
|
-
"sha256": "
|
|
2910
|
+
"sha256": "5b614e96ed951c5eebb5c40c93612f59ff79da768055233b59790599421a70ec"
|
|
2906
2911
|
},
|
|
2907
2912
|
{
|
|
2908
2913
|
"path": "modules/grading/grader-workers/worker-verdict.js",
|
|
@@ -2912,7 +2917,7 @@
|
|
|
2912
2917
|
{
|
|
2913
2918
|
"path": "modules/grading/grader.js",
|
|
2914
2919
|
"mode": "0000644",
|
|
2915
|
-
"sha256": "
|
|
2920
|
+
"sha256": "edf6ecc725b37fe2c515174e6b06025f82fae10172e6f13d8fcfbb060c11474a"
|
|
2916
2921
|
},
|
|
2917
2922
|
{
|
|
2918
2923
|
"path": "modules/grading/module.json",
|
|
@@ -4032,12 +4037,12 @@
|
|
|
4032
4037
|
{
|
|
4033
4038
|
"path": "package-lock.json",
|
|
4034
4039
|
"mode": "0000644",
|
|
4035
|
-
"sha256": "
|
|
4040
|
+
"sha256": "ffd1f91d40f2098324c3054cd3accdfdf210a668972dcb8702104cd11ed6c4dc"
|
|
4036
4041
|
},
|
|
4037
4042
|
{
|
|
4038
4043
|
"path": "package.json",
|
|
4039
4044
|
"mode": "0000644",
|
|
4040
|
-
"sha256": "
|
|
4045
|
+
"sha256": "cd444deb3940b96b8afedb89f5ece513d842d4a9148b3a50304d8d77866dd904"
|
|
4041
4046
|
},
|
|
4042
4047
|
{
|
|
4043
4048
|
"path": "public-docs/index.html",
|
|
@@ -4952,7 +4957,7 @@
|
|
|
4952
4957
|
{
|
|
4953
4958
|
"path": "scripts/gds/npmrc.js",
|
|
4954
4959
|
"mode": "0000644",
|
|
4955
|
-
"sha256": "
|
|
4960
|
+
"sha256": "9dfd27e75def392a73d7de7fd23d462afc97b17a8199acfe6563a80692570026"
|
|
4956
4961
|
},
|
|
4957
4962
|
{
|
|
4958
4963
|
"path": "scripts/gds/oauth-secret.js",
|
|
@@ -5227,7 +5232,7 @@
|
|
|
5227
5232
|
{
|
|
5228
5233
|
"path": "scripts/gds/ship.js",
|
|
5229
5234
|
"mode": "0000644",
|
|
5230
|
-
"sha256": "
|
|
5235
|
+
"sha256": "aa77aef41188c5ef596c9c5e7e89f54f97c93b6e889e1c776b8fe34c8af66cce"
|
|
5231
5236
|
},
|
|
5232
5237
|
{
|
|
5233
5238
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -5657,7 +5662,7 @@
|
|
|
5657
5662
|
{
|
|
5658
5663
|
"path": "src/module-api.js",
|
|
5659
5664
|
"mode": "0000644",
|
|
5660
|
-
"sha256": "
|
|
5665
|
+
"sha256": "2d8b78d34893b1e4c25d081c343ad1b0d1deb2a675cae9a3c4e96500c6cea821"
|
|
5661
5666
|
},
|
|
5662
5667
|
{
|
|
5663
5668
|
"path": "src/module-loader/loader.js",
|
|
@@ -6627,7 +6632,7 @@
|
|
|
6627
6632
|
{
|
|
6628
6633
|
"path": "tests/grade_diff_hash.mjs",
|
|
6629
6634
|
"mode": "0000644",
|
|
6630
|
-
"sha256": "
|
|
6635
|
+
"sha256": "170b816aa9032e6eccba415f67ce60c10da5f2f706a1b1ee458a5d8578679535"
|
|
6631
6636
|
},
|
|
6632
6637
|
{
|
|
6633
6638
|
"path": "tests/grade_server_authoritative.mjs",
|
|
@@ -6642,7 +6647,7 @@
|
|
|
6642
6647
|
{
|
|
6643
6648
|
"path": "tests/grader.mjs",
|
|
6644
6649
|
"mode": "0000644",
|
|
6645
|
-
"sha256": "
|
|
6650
|
+
"sha256": "31557b08f8886f5d76201b5afbe0332e834504b67a391699798170aede9541e8"
|
|
6646
6651
|
},
|
|
6647
6652
|
{
|
|
6648
6653
|
"path": "tests/grader_bypass.mjs",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 0165 — Instances are hookless by default: no scaffolded `.husky/`; local hooks stay an instance-owned opt-in
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-11
|
|
5
|
+
- **Deciders:** Claude (jaxri session), under the bug-wave mandate — flagged owner-adjacent; the ADR is the revisit point
|
|
6
|
+
|
|
7
|
+
**Track:** `internal` (provisioning / trust boundary). Task [1002641](https://cloudbongos.com/builders#/task/1002641), from idea 1000655. Relates to [ADR 0043](<redacted>.md) (Layer 1), [ADR 0055](<redacted>.md) (server-mediated publish), [ADR 0162](<redacted>.md) (CI-only gates).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
Task 1002608 delivered `.husky/pre-push` — the ADR 0043 **Layer 1** local permission gate — for **this core repo**, and `scripts/gds/install-git-hooks.js` (run by `/builder-setup`) activates whatever hooks a clone's `.husky/` tracks by setting `core.hooksPath`.
|
|
14
|
+
|
|
15
|
+
But nothing delivers Layer 1 to a **scaffolded standalone instance**: `init.js` / `provision.js` / `onboard.js` materialize no `.husky/`, and `publish-manifest.js` excludes `.husky/` from the published core artifact **by design** (`PROTECTED_TOPOLOGY_EXCLUDE` — a core update must never silently rewrite an instance's local gate topology). So instances run with no pre-push permission gate and no pre-commit secrets hook. Task 1002641 asks: is that a gap to fill, or the accepted posture?
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
**Hookless-by-default is the accepted posture for instances. Provisioning scaffolds no `.husky/`. Local hooks are an instance-owned opt-in: an instance that wants them commits its own `.husky/`, and the core's existing `install-git-hooks.js` activates them on every builder's machine.**
|
|
20
|
+
|
|
21
|
+
Four structural reasons:
|
|
22
|
+
|
|
23
|
+
1. **The population Layer 1 constrains holds no push credential on an instance.** Sub-Metic builders work dev boxes where the git origin is credential-less and landing goes through **server-mediated publish** (ADR 0053/0055): the box uploads a bundle, the *server* pushes and opens the PR. A client-side pre-push hook on the box guards a push that cannot happen. Metic+ builders with real push access are inside the trust boundary, and their pushes meet the server-side gates that actually enforce: branch protection, the required `unit` check (fitness, gate-review tests, the authorship gate — task 1002640), and the every-push `main-audit`.
|
|
24
|
+
2. **A client hook is not a security boundary** (ADR 0016: the DB and server enforce; local edits grant nothing). On the core repo, `.husky/pre-push` is defense-in-depth for contributors with direct push access — a materially different population than instance builders. Scaffolding it onto instances would *look* like a control while a hostile clone deletes it in one command.
|
|
25
|
+
3. **The core cannot maintain what it deliberately does not ship.** `.husky/` is `PROTECTED_TOPOLOGY_EXCLUDE`d from the artifact, so scaffolded hooks would be born orphans — no upgrade path, rotting silently until they misfire. A written control that no longer runs teaches builders to ignore controls (the audit-authorship lesson, task 1002640).
|
|
26
|
+
4. **ADR 0162 (owner directive): gates live in CI, rank-consistent and hands-free.** Instance-side client gates re-introduce the blocked-builder friction class that ADR retired — a broken hook strands a legitimate land until a human intervenes.
|
|
27
|
+
|
|
28
|
+
**Secrets hooks follow the same rule.** A pre-commit secrets guard needs per-machine tooling and gives false local confidence; the PR-time CI secret scan is the enforcing control on every land.
|
|
29
|
+
|
|
30
|
+
**The opt-in stays real, and stays instance-owned.** `install-git-hooks.js` ships in the core and already runs at `/builder-setup` on every instance; it activates whatever `.husky/` the *instance's own repo* tracks. Policy (which hooks, if any) is the instance's; the activation mechanism is the core's. That split mirrors the branding contract (ADR 0062 §7).
|
|
31
|
+
|
|
32
|
+
## Rejected
|
|
33
|
+
|
|
34
|
+
- **Scaffold `.husky/` wrappers at `init`/provision** — orphaned gate topology the core can't update (reason 3), guarding a push path instance builders don't have (reason 1).
|
|
35
|
+
- **An `init --with-hooks` flag** — speculative surface; no instance has asked. If one does, it composes trivially on the opt-in path above.
|
|
36
|
+
- **Treating hooklessness as a security regression to hotfix** — the enforcing controls on instances are server-side and already in place; Layer 1 was always the advisory outermost layer, not the wall.
|
|
37
|
+
|
|
38
|
+
## When to revisit
|
|
39
|
+
|
|
40
|
+
An instance model where builders routinely push directly with their own credentials (no server-mediated publish, no branch protection) would remove reasons 1–2; revisit then.
|
package/docs/adr/README.md
CHANGED
|
@@ -237,5 +237,6 @@ This keeps the decision history honest and traceable.
|
|
|
237
237
|
|
|
238
238
|
| 0163 | [**Pre-197 display names: a transitional 'legacy' source, resolved at first sign-in** ([task 1002643](https://cloudbongos.com/builders#/task/1002643); idea 1000659). Amends [ADR 0156](<redacted>.md), whose migration defaulted every pre-existing row to `'github'` — false for builders who renamed after their last sign-in but before 197 landed, so their names kept reverting. No audit trail exists to backfill precisely, so `core_214` marks every pre-197 `'github'` row `'legacy'` (scoped by 197's `applied_at` on the instance — portable) and the upsert resolves each at its first sign-in: stored = incoming GitHub name → provably GitHub-derived → `'github'`; differs → treated as builder-set → kept, stamped `'user'`. Accepted misclassification: a GitHub-side rename in the window freezes a stale label (visible, self-fixable in Settings) — preferred over silently destroying deliberate renames. Rejected: `display_name <> github_login` heuristic (freezes nearly everyone), comparing forever on all `'github'` rows (turns every future GitHub rename into a frozen name).](<redacted>.md) | GDS / builder experience |
|
|
239
239
|
| 0164 | [**The implausible-pass guard reads substance + evidence; the Metic+ honor-system residual is explicit** ([task 1002658](https://cloudbongos.com/builders#/task/1002658); grader-hardening goal 1000056). The guard fired only on `issues.length===0` — one fabricated `{}` defeated it, its magnitude inputs are client-supplied, and every in-sample firing (eval 2-for-2, tasks 1002643 ×2, 1002663) was a false positive on an honest unanimous zero-issue panel PASS. Redesign: count only SUBSTANTIVE issues (known severity + non-empty summary — padding trips the guard regardless), and waive the zero-issue pass when `signals.workers` carries the full ≥3-worker panel shape, stamped `implausible_pass_waived` for /grade-audit. The residual is named, not hidden: every grade-time input is client-authored, so plausibility at grade time is honest-mistake hygiene; the binding authorities on the real diff are land-time (CI unit, gate-review, main audit) — Metic+ grade-time honesty is accepted honor-system per this ADR. The ADR-0042 CI grade-gate (land it vs record permanent deferral) goes to the owner as a blocker, not decided here. No approval step added or removed (ADR 0162).](<redacted>.md) | GDS / grading |
|
|
240
|
+
| 0165 | [**Instances are hookless by default: no scaffolded `.husky/`; local hooks stay an instance-owned opt-in** ([task 1002641](https://cloudbongos.com/builders#/task/1002641); idea 1000655). Task 1002608 delivered the ADR 0043 Layer 1 pre-push gate for the CORE repo, but nothing materializes `.husky/` on a scaffolded standalone instance (`publish-manifest.js` excludes it by design). Decision: that is the accepted posture, not a gap. Sub-Metic instance builders hold no push credential at all (server-mediated publish, ADR 0053/0055) — a client pre-push hook guards a push that cannot happen; a client hook is not a security boundary (ADR 0016); the core cannot maintain hooks it deliberately does not ship (scaffolded hooks would be born orphans — the audit-authorship rot lesson, task 1002640); and ADR 0162 puts gates in CI, rank-consistent and hands-free. Secrets hooks follow the same rule (the PR-time CI secret scan enforces). The opt-in stays real and instance-owned: an instance commits its own `.husky/` and the core's `install-git-hooks.js` (already run by `/builder-setup`) activates it. Rejected: scaffolding wrappers at init, an `--with-hooks` flag (speculative), treating hooklessness as a regression. Revisit if an instance model has builders pushing directly with their own credentials.](<redacted>.md) | provisioning / trust boundary |
|
|
240
241
|
|
|
241
242
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
|
@@ -265,7 +265,9 @@ pre-merge gate misses, independent of the builder's cooperation:
|
|
|
265
265
|
[`src/bongos/permission-path-check.js`](../src/bongos/permission-path-check.js) (source of truth).
|
|
266
266
|
- **Nobody pushes `main` directly.** The pre-push hook (`.husky/pre-push`, tracked in this repo and
|
|
267
267
|
activated per-clone by `/builder-setup` via `install-git-hooks.js` — inert on a clone that never
|
|
268
|
-
activated it; ADR 0043 §"G-2/G-17 resolution") blocks it
|
|
268
|
+
activated it; ADR 0043 §"G-2/G-17 resolution") blocks it. This hook is a CORE-repo control:
|
|
269
|
+
scaffolded standalone instances are hookless by default — deliberate, not a gap
|
|
270
|
+
([ADR 0165](adr/<redacted>.md)); the graded
|
|
269
271
|
pipeline pushes only via `scripts/gds/push-main.js` (which carries the `OTB_ALLOW_MAIN_PUSH`
|
|
270
272
|
override). A raw push is caught and reverted by the **post-push main audit**
|
|
271
273
|
(`scripts/gds/main-audit.js`) — nightly and as the `~/deploy.sh` deploy gate — which is the
|
|
@@ -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
|
@@ -523,8 +523,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
523
523
|
// landed since 1.19.46 with no explicit bump. run 31521404176. (task 1002620)
|
|
524
524
|
// 1.19.48 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
525
525
|
// landed since 1.19.47 with no explicit bump. run 31522865111. (task 1002620)
|
|
526
|
+
// 1.19.49 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
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)
|
|
526
530
|
// ---------------------------------------------------------------------------
|
|
527
|
-
const CORE_VERSION = '1.19.
|
|
531
|
+
const CORE_VERSION = '1.19.50'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
528
532
|
|
|
529
533
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
530
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.
|