@bongos/core 1.19.49 → 1.19.51

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 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.49",
6
- "core_contract": "1.19.49",
7
- "source_commit": "fd920fff3c9a5107c7e7d198f6e09d04c5f66f5b",
5
+ "core_version": "1.19.51",
6
+ "core_contract": "1.19.51",
7
+ "source_commit": "fe0b1449ce7c972b972778e74945261c35565c48",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-11T18:36:50.229Z",
9
+ "built_at": "2026-08-11T18:53:46.990Z",
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": "94afa19b71a022c6b15c9ce95d71b3c76fe6ed87758e5b38dbfa259e7215ca5b",
20
+ "tree_sha256": "c3aec243f4a401c529edd9c426192e56b1264abfa615e29045ac5cf054e33d57",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -367,7 +367,7 @@
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "649065862779b6a9b871607debfee5ca18bde8cccbde4d00ea42b0d175938c9d"
370
+ "sha256": "a5b9642b383c8c415158e091785ff40f28490d8341b4010f4c051b1a68942daf"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
@@ -1307,12 +1307,12 @@
1307
1307
  {
1308
1308
  "path": "docs/api-reference.md",
1309
1309
  "mode": "0000644",
1310
- "sha256": "8f83ea78edb348688196414bf2fb7352df3de0ded6abbbe44f2371c099431016"
1310
+ "sha256": "27e9c0ab46db5386833b9e03bcff0dae31f4606882e845e7e830f9ef6f2550b5"
1311
1311
  },
1312
1312
  {
1313
1313
  "path": "docs/api/openapi.json",
1314
1314
  "mode": "0000644",
1315
- "sha256": "27cf9c2b8548168f13dd2add1633bc9b3ca57f2fd5aa3b42ac8dbfb5cf60c41f"
1315
+ "sha256": "612c7a81756f61f2c18d4b35540379cf63f32a4caa9e8d84a64b247d497ad7a3"
1316
1316
  },
1317
1317
  {
1318
1318
  "path": "docs/architecture.md",
@@ -1572,7 +1572,7 @@
1572
1572
  {
1573
1573
  "path": "docs/recipes/private-npm-distribution.md",
1574
1574
  "mode": "0000644",
1575
- "sha256": "1aafe7dafea94084c381e7ea1e91fad533ca379f9560da6f9f9f4a2cf4db4fc5"
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": "2e080d8d50383a24d77f9cf166f6e93e05e7bccc8775d14bbf68f2d6733eecd4"
2865
+ "sha256": "6a0a2205fee2a4b0efc422c884826a9fd2af3d8785b393d5f7123e1758f1ebfc"
2866
2866
  },
2867
2867
  {
2868
2868
  "path": "modules/grading/grader-workers/efficiency.js",
2869
2869
  "mode": "0000644",
2870
- "sha256": "3e3721289ceb8c9c14e7aff7dc9569cbf01be3f2363bcaedc04aaee01d3fca3a"
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": "650914c1f726c02ee19c80f72676c2bf86d99b9aded6cff1210905db52cb48d8"
2885
+ "sha256": "c7177625382e2ea61ee706c6b9b8669d9d452cdc0a92235695438b249c1ab6fc"
2886
2886
  },
2887
2887
  {
2888
2888
  "path": "modules/grading/grader-workers/index.js",
2889
2889
  "mode": "0000644",
2890
- "sha256": "232dc6b3c2d97282cce8032a1c671c89bb8e1e8936b4bb0acbd3ec4471e74f0a"
2890
+ "sha256": "8ec5e91341ec5247ee643c6ce75fb5ec21a62b3bba176fdde060cdb352edb808"
2891
2891
  },
2892
2892
  {
2893
2893
  "path": "modules/grading/grader-workers/narc.js",
2894
2894
  "mode": "0000644",
2895
- "sha256": "eeb9545d638d58958049c7889401f1cd49a2fb03607c25bfbef3625a0c7dd9fd"
2895
+ "sha256": "f35c3fce7feb6198df0011a9d22533038ca0766733c97a31841a184251800d39"
2896
2896
  },
2897
2897
  {
2898
2898
  "path": "modules/grading/grader-workers/quality.js",
2899
2899
  "mode": "0000644",
2900
- "sha256": "fa73a1d68c0126a2c109e9b7ef8692df29cbb41bbb800ab64e127f20e0a5865e"
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": "9821a36e9a11be805cf806d83edd4be8cf8c0b5d40fe63276b50f18d8eff8a74"
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": "f6b80df54bb7634a9effb1cbe51b3ff99b05b0b7912be5a06690f63d237689f9"
2920
+ "sha256": "edf6ecc725b37fe2c515174e6b06025f82fae10172e6f13d8fcfbb060c11474a"
2921
2921
  },
2922
2922
  {
2923
2923
  "path": "modules/grading/module.json",
@@ -3387,7 +3387,7 @@
3387
3387
  {
3388
3388
  "path": "modules/lifecycle/db.js",
3389
3389
  "mode": "0000644",
3390
- "sha256": "57c88852ff0aa9f0b3673fa62b35f352d92cc9070e7872137003e395c42eb026"
3390
+ "sha256": "8e67cd1325da44cf95d5d8c34427222e555e0458bb17f23f33c1055b1ae7e211"
3391
3391
  },
3392
3392
  {
3393
3393
  "path": "modules/lifecycle/done-when.js",
@@ -3492,7 +3492,7 @@
3492
3492
  {
3493
3493
  "path": "modules/lifecycle/routes/tasks.js",
3494
3494
  "mode": "0000644",
3495
- "sha256": "8a6ecd6c7ecadcbec79483a589fef11957d13e2c94b569a9ff9da97f2908b713"
3495
+ "sha256": "68b4f47e3a5016645893c60eca2fe11a242012abf6625c99d6a774356d82d1ce"
3496
3496
  },
3497
3497
  {
3498
3498
  "path": "modules/lifecycle/routes/versions.js",
@@ -4037,12 +4037,12 @@
4037
4037
  {
4038
4038
  "path": "package-lock.json",
4039
4039
  "mode": "0000644",
4040
- "sha256": "7124698435e257f65256f4d3ab34277211b216cece5849b932368eccde73330f"
4040
+ "sha256": "43bd308850fdf28f3fa803c778af55a4a86e72edb62ccd8dc2d3297da95efbe2"
4041
4041
  },
4042
4042
  {
4043
4043
  "path": "package.json",
4044
4044
  "mode": "0000644",
4045
- "sha256": "16fecf0cf1d4f688f452ad27d37ba05776dd028444cd6cc9a2ab68deba3bb693"
4045
+ "sha256": "a402f396d0bfd5b10f24685a15ac36f9022c182db018787fbdd96b296e26c5db"
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": "115af471c497515927528927da78793400e7181476c23c724479bccd079baa3c"
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": "94f343beba33727cb83367555c35a699f53c8efa5d59e3b65d3f6a1cf3e0e3b0"
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": "902071a6283cfb63ca5cc90c8ca201cd97e14490a380b4e4a51c231a89b02e3f"
5665
+ "sha256": "3c2f62e446506de3fa855989e8606aad486566e37eed47b61c3a18b835a404f4"
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": "a661c482da391651dbf85fe92fe2a588802f8855560251cdc2074ae1b46d4425"
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": "227d71aaab363d9346c3034f41cfb518e866b49bb460802b32626f288ecdcd76"
6650
+ "sha256": "31557b08f8886f5d76201b5afbe0332e834504b67a391699798170aede9541e8"
6651
6651
  },
6652
6652
  {
6653
6653
  "path": "tests/grader_bypass.mjs",
@@ -6882,7 +6882,7 @@
6882
6882
  {
6883
6883
  "path": "tests/migration_allocation.mjs",
6884
6884
  "mode": "0000644",
6885
- "sha256": "08db201ebec44ea5a711e3c86e36dda63ec22881a400e910dd7c64961c80f6e9"
6885
+ "sha256": "6755c9568cd6cdb7b8600ae2c6651856d63c053684dcb8b765f7329590a9fc08"
6886
6886
  },
6887
6887
  {
6888
6888
  "path": "tests/migration_namespace.mjs",
@@ -131,7 +131,7 @@ export interface PatchMeSkillPrefsResponse { prefs: unknown; effective: unknown;
131
131
  export interface PatchMeSoundPrefsResponse { prefs: unknown; effective: unknown; effective_map: unknown; volume: unknown; defaults: unknown; known_sounds: unknown }
132
132
  export interface PatchProjectsIdFeaturedRequest { featured: boolean }
133
133
  export interface PatchProjectsIdFeaturedResponse { project: unknown }
134
- export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; status?: unknown }
134
+ export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; description?: string; status?: unknown }
135
135
  export interface PostAccessRequestsRequest { github_login: string; display_name?: string; note?: string }
136
136
  export interface PostAccessRequestsResponse { ok: boolean; request?: unknown; already_pending?: boolean }
137
137
  export interface PostAuthAppPairApproveRequest { pair_code: string }
@@ -13792,6 +13792,11 @@
13792
13792
  "needs_migration": {
13793
13793
  "type": "boolean"
13794
13794
  },
13795
+ "description": {
13796
+ "type": "string",
13797
+ "minLength": 1,
13798
+ "maxLength": 50000
13799
+ },
13795
13800
  "status": {}
13796
13801
  },
13797
13802
  "additionalProperties": false
@@ -463,7 +463,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
463
463
  | GET | `/api/bongos/tasks` | `any-builder` | — | reads (filtered task list); preserved per CRITICAL §4. |
464
464
  | POST | `/api/bongos/tasks` | `metic+archon` | `version_id`, `title`, `description`, `status`, `touches`, `est_minutes`, `est_cost_usd`, `manual_degree`, `priority`, `automation_tag`, `credits_reward`, `source`, `source_ref`, `value_summary`, `kind`, `discipline`, `parallel_safe`, `newcomer_friendly`, `security_sensitive`, `requires_rank`, `auto_detect_touches`, `criterion_ids`, `module_key`, `goal_id` | Task create/promote/edit (plan/scope mutation) is Metic+ as of ADR 0090 (owner decision 2026-06-26): a Metic who can run idea-triage may … |
465
465
  | GET | `/api/bongos/tasks/:id` | `any-builder` | — | single-task read; preserved per CRITICAL §4 (supervisor hot path). |
466
- | PATCH | `/api/bongos/tasks/:id` | `metic+archon` | `kind`, `discipline`, `requires_rank`, `module_key`, `goal_id`, `parent_task_id`, `parallel_safe`, `newcomer_friendly`, `xenos_claimable`, `security_sensitive`, `automation_tag`, `needs_migration`, `status` | PATCH /tasks/:id — supports body.parent_task_id (V2 C2), body.kind (V2 B1), body.discipline (mig 014), body.parallel_safe (mig 028), and … |
466
+ | PATCH | `/api/bongos/tasks/:id` | `metic+archon` | `kind`, `discipline`, `requires_rank`, `module_key`, `goal_id`, `parent_task_id`, `parallel_safe`, `newcomer_friendly`, `xenos_claimable`, `security_sensitive`, `automation_tag`, `needs_migration`, `description`, `status` | PATCH /tasks/:id — supports body.parent_task_id (V2 C2), body.kind (V2 B1), body.discipline (mig 014), body.parallel_safe (mig 028), and … |
467
467
  | POST | `/api/bongos/tasks/:id/abandon` | `metic+archon` | `reason` | POST /tasks/:id/abandon — terminal "retire this task" transition (task 987). |
468
468
  | POST | `/api/bongos/tasks/:id/attest-gate` | `any-builder` | `head_sha` | POST /tasks/:id/attest-gate — body: { head_sha }. |
469
469
  | POST | `/api/bongos/tasks/:id/confirm` | `any-builder` | `verified` | |
@@ -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* → **Classic → Automation** (bypasses 2FA, which npm requires to publish), with **read-write** on the `@bongos` packages — a **read-only** token is enough for machines that only install. Store it as an environment variable, never in a committed file:
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 Classic Automation token
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 Classic **Automation** token — read-only — from the paid `bongos` org). 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).
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
- const prompt = worker.buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, deterministicFindings, priorRound });
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
- model: graderModel,
1749
+ // task 1002660: rubric tier override per worker (cross-model-guarded).
1750
+ model: workerModelFor(worker.KIND, graderModel, builderModel),
1663
1751
  runOpts,
1664
- runSubagentFn: runSubagent,
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,
@@ -1477,7 +1477,7 @@ async function findTaskBySourceRef({ source, sourceRef } = {}, deps = {}) {
1477
1477
 
1478
1478
  const TASK_SETTER_COLUMNS = new Set([
1479
1479
  'kind', 'discipline', 'newcomer_friendly', 'security_sensitive', 'parallel_safe', 'automation_tag',
1480
- 'needs_migration', 'module_key',
1480
+ 'needs_migration', 'module_key', 'description',
1481
1481
  ]);
1482
1482
 
1483
1483
  async function updateTaskColumn(id, column, value) {
@@ -1532,6 +1532,12 @@ async function updateTaskGoal(id, goalId) {
1532
1532
 
1533
1533
  async function updateTaskKind(id, kind) { return updateTaskColumn(id, 'kind', kind); }
1534
1534
 
1535
+ // task 1002647 (choke-point map s4.3): the task body is a load-bearing prompt —
1536
+ // it was settable only at create, un-editable at every rank. Editable now under
1537
+ // the same task.edit permission as the other planning fields; the global
1538
+ // write-audit middleware captures each edit's new body in audit_log.
1539
+ async function updateTaskDescription(id, description) { return updateTaskColumn(id, 'description', description); }
1540
+
1535
1541
 
1536
1542
  async function updateTaskDiscipline(id, discipline) { return updateTaskColumn(id, 'discipline', discipline); }
1537
1543
 
@@ -5457,6 +5463,7 @@ module.exports = {
5457
5463
  updateTaskGoal,
5458
5464
  createCriterion,
5459
5465
  updateTaskKind,
5466
+ updateTaskDescription,
5460
5467
  updateTaskDiscipline,
5461
5468
  updateTaskNewcomerFriendly,
5462
5469
  updateTaskRequiresRank,
@@ -793,6 +793,12 @@ module.exports = function buildTasksRouter() {
793
793
  security_sensitive: { type: 'boolean' }, // task 990 — Archon-only ship broadcast
794
794
  automation_tag: { type: 'string', maxLength: 64 },
795
795
  needs_migration: { type: 'boolean' }, // task 1002648 — correct the create-time heuristic's flag (the claim-time allocator keys off it)
796
+ // task 1002647 (map s4.3): the task body is a load-bearing builder prompt
797
+ // and was settable only at create — no route edited it at any rank. Now
798
+ // editable under this route's task.edit permission. minLength 1: a body
799
+ // can be rewritten, never blanked (and null is refused by the type check).
800
+ // Each edit's new body lands in audit_log via the write-audit middleware.
801
+ description: { type: 'string', minLength: 1, maxLength: LIMITS.DESCRIPTION },
796
802
  // task 1002614 (map D-18): DECLARED so strict-mode admits the request far
797
803
  // enough to reach the deliberate status_not_patchable hint below (#514) —
798
804
  // the hint had been unreachable dead code behind unknown_field since the
@@ -818,6 +824,7 @@ module.exports = function buildTasksRouter() {
818
824
  const hasRequiresRank = Object.prototype.hasOwnProperty.call(body, 'requires_rank');
819
825
  const hasModuleKey = Object.prototype.hasOwnProperty.call(body, 'module_key');
820
826
  const hasGoalId = Object.prototype.hasOwnProperty.call(body, 'goal_id');
827
+ const hasDescription = Object.prototype.hasOwnProperty.call(body, 'description');
821
828
  // #514: status is a lifecycle field, not a PATCH-able column — it moves only
822
829
  // through the dedicated transition routes. The #506 session tried
823
830
  // `PATCH status=ready`, got the generic no_supported_fields error, and had
@@ -829,8 +836,8 @@ module.exports = function buildTasksRouter() {
829
836
  if (Object.prototype.hasOwnProperty.call(body, 'status')) {
830
837
  return res.fail('status_not_patchable', { status: 400, message: STATUS_HINT });
831
838
  }
832
- if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasNeedsMigration && !hasRequiresRank && !hasModuleKey && !hasGoalId) {
833
- return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, parallel_safe, security_sensitive, automation_tag, needs_migration, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
839
+ if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasNeedsMigration && !hasRequiresRank && !hasModuleKey && !hasGoalId && !hasDescription) {
840
+ return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, description, parallel_safe, security_sensitive, automation_tag, needs_migration, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
834
841
  }
835
842
  // ---- task 1002609 (G-22): settle EVERY gate and validation before the ----
836
843
  // ---- FIRST write. The previous shape validated-and-wrote per field in ----
@@ -976,6 +983,15 @@ module.exports = function buildTasksRouter() {
976
983
  return res.fail('patch_failed', { status: 500, message: 'internal error' });
977
984
  }
978
985
  }
986
+ if (hasDescription) {
987
+ try {
988
+ updated = await db.updateTaskDescription(id, body.description);
989
+ if (!updated) return res.fail('task_not_found', 404);
990
+ } catch (err) {
991
+ console.error('[gds] PATCH /tasks/:id description', err);
992
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
993
+ }
994
+ }
979
995
  if (hasGoalId) {
980
996
  try {
981
997
  updated = await db.updateTaskGoal(id, body.goal_id);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.49",
3
+ "version": "1.19.51",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.49",
9
+ "version": "1.19.51",
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.49",
3
+ "version": "1.19.51",
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",
@@ -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 automation token from the paid \`bongos\` org);`,
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}}`,
@@ -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
- async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles, priorRound = null }) {
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
- // Re-POST the stored result with a reused marker so the audit trail is
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
- const payload = { ...buildGradePayload(reusedResult, committedFiles), diff_hash: currentHash };
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
- const payload = { ...buildGradePayload(gradeResult, committedFiles), diff_hash: currentHash ?? undefined };
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
- const smoke = runSmoke();
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 runs the
3990
- // subagent grader, POSTs the result, and logs the panel cost — the same unit
3991
- // the --regrade path reuses.
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: fullTask,
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,12 @@ 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)
530
+ // 1.19.51 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
531
+ // landed since 1.19.50 with no explicit bump. run 31525032273. (task 1002620)
528
532
  // ---------------------------------------------------------------------------
529
- const CORE_VERSION = '1.19.49'; // CI auto-patch carrier (ADR 0161); see changelog above
533
+ const CORE_VERSION = '1.19.51'; // CI auto-patch carrier (ADR 0161); see changelog above
530
534
 
531
535
  // A namespaced logger so a module's log lines are attributable + consistent.
532
536
  // 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.
@@ -197,6 +197,7 @@ function shouldReserveMigrationNumber(task) {
197
197
  [() => db.updateTaskAutomationTag(1, 'live-only'), 'automation_tag'],
198
198
  [() => db.updateTaskNeedsMigration(1, false), 'needs_migration'],
199
199
  [() => db.updateTaskModuleKey(1, null), 'module_key'],
200
+ [() => db.updateTaskDescription(1, 'edited body'), 'description'], // task 1002647
200
201
  ];
201
202
  for (const [call, column] of setters) {
202
203
  const before = seen.length;