@bongos/core 1.19.30 → 1.19.32

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,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.30",
6
- "core_contract": "1.19.30",
7
- "source_commit": "103b0c420b448f90a8f42f923f4e67891c45b210",
5
+ "core_version": "1.19.32",
6
+ "core_contract": "1.19.32",
7
+ "source_commit": "a4f8a4de7d7d7ba6d7799a4d20c54e83abf3e5fe",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-11T14:41:56.292Z",
9
+ "built_at": "2026-08-11T15:03:03.893Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 286,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1206,
14
+ "functional_verbatim": 1207,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1513,
20
- "tree_sha256": "66f1b7a904b38f7980ead45fe34e98228990e4be951b312f807251687b13d098",
19
+ "file_count": 1514,
20
+ "tree_sha256": "e1a52404d82bbbff5229cd5f74279ae8499982944367cf7102600a9169f2faba",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -352,7 +352,7 @@
352
352
  {
353
353
  "path": "clients/bongos-client/index.d.ts",
354
354
  "mode": "0000644",
355
- "sha256": "7e6732aedaf7aaa3c657fc05e2dfd1dcc63e0fb06586d4077248d79d6198297e"
355
+ "sha256": "649065862779b6a9b871607debfee5ca18bde8cccbde4d00ea42b0d175938c9d"
356
356
  },
357
357
  {
358
358
  "path": "clients/bongos-client/index.mjs",
@@ -1282,12 +1282,12 @@
1282
1282
  {
1283
1283
  "path": "docs/api-reference.md",
1284
1284
  "mode": "0000644",
1285
- "sha256": "e28575936d1cb0cce684b20723743fd0d801ccd0a7ec58fff5360e160b23b5e7"
1285
+ "sha256": "8f83ea78edb348688196414bf2fb7352df3de0ded6abbbe44f2371c099431016"
1286
1286
  },
1287
1287
  {
1288
1288
  "path": "docs/api/openapi.json",
1289
1289
  "mode": "0000644",
1290
- "sha256": "b232ca69ae97842be312b76f6ac49ca66090c5c8fef392f609bb857e5f5c2979"
1290
+ "sha256": "27cf9c2b8548168f13dd2add1633bc9b3ca57f2fd5aa3b42ac8dbfb5cf60c41f"
1291
1291
  },
1292
1292
  {
1293
1293
  "path": "docs/architecture.md",
@@ -2837,12 +2837,12 @@
2837
2837
  {
2838
2838
  "path": "modules/grading/grader-rubric.json",
2839
2839
  "mode": "0000644",
2840
- "sha256": "52dfa2cde50170d982cc816da444fe1d1cf79814104cc9452aee1d658474556e"
2840
+ "sha256": "6917875f2be5c39248b44caf7bf8d50f68cbe8effbc2bfdef3ba11e5ac17fc6c"
2841
2841
  },
2842
2842
  {
2843
2843
  "path": "modules/grading/grader-workers/efficiency.js",
2844
2844
  "mode": "0000644",
2845
- "sha256": "6163aa89c558269267decf1ed91d6fc65939f27a34baf67364af01f8e6204cc1"
2845
+ "sha256": "9b8bbb51c5d8641d1c145d90ad8d0d96d19466abd8777bd10d5819630a6b7066"
2846
2846
  },
2847
2847
  {
2848
2848
  "path": "modules/grading/grader-workers/extract-json.js",
@@ -2857,7 +2857,7 @@
2857
2857
  {
2858
2858
  "path": "modules/grading/grader-workers/hacker.js",
2859
2859
  "mode": "0000644",
2860
- "sha256": "be6e82997286141f1c561c496ce1a717591d2b2bed9d5cee1f7af73770bc526c"
2860
+ "sha256": "99f64187efc56c2edf5a41626f87ee4083e83da43f1a00628d3506929d42e3c2"
2861
2861
  },
2862
2862
  {
2863
2863
  "path": "modules/grading/grader-workers/index.js",
@@ -2867,12 +2867,12 @@
2867
2867
  {
2868
2868
  "path": "modules/grading/grader-workers/narc.js",
2869
2869
  "mode": "0000644",
2870
- "sha256": "c034baa7e0973cbbf09f54305ed1ee8cb90c9642088edd6b45b63504ba267435"
2870
+ "sha256": "4e881e6d04bafcf6093be0e1c653979d00c174b6f7966fb9cd3d833a994f48d3"
2871
2871
  },
2872
2872
  {
2873
2873
  "path": "modules/grading/grader-workers/quality.js",
2874
2874
  "mode": "0000644",
2875
- "sha256": "7a64a9c4bb97b791be3e7ca8fe17164b80ae30aca968ac05a0229a6e4926b05e"
2875
+ "sha256": "de6003eb2af2abca2e6d68ebab729c8a3e18f0b8514d9f14e65b9442c21df253"
2876
2876
  },
2877
2877
  {
2878
2878
  "path": "modules/grading/grader-workers/rubric.js",
@@ -2882,12 +2882,12 @@
2882
2882
  {
2883
2883
  "path": "modules/grading/grader-workers/worker-prompt.js",
2884
2884
  "mode": "0000644",
2885
- "sha256": "017536ca8d35b1bb9b893a4fa7402d1f13f36c2e4c12585b9f296971080fa19c"
2885
+ "sha256": "f691b0a42984a9e768477ab694bf7d7bc92264d4bb6d5f50ea52fc0e79abbfb1"
2886
2886
  },
2887
2887
  {
2888
2888
  "path": "modules/grading/grader-workers/worker-verdict.js",
2889
2889
  "mode": "0000644",
2890
- "sha256": "8185aa753112fe66a13c2ce650ef21b9a832413e70fbebc8ff048aa8e7aae5d1"
2890
+ "sha256": "9c3abb069cfdee124b4fd5c521fe1c11b44177f7af941c9ab7bbb2a4a3718bab"
2891
2891
  },
2892
2892
  {
2893
2893
  "path": "modules/grading/grader.js",
@@ -3362,7 +3362,7 @@
3362
3362
  {
3363
3363
  "path": "modules/lifecycle/db.js",
3364
3364
  "mode": "0000644",
3365
- "sha256": "85c856016e548399538734098d2f08d3920fb52814759ffa314ed26d98bf32eb"
3365
+ "sha256": "b2927d0be39718b274dda102c0d9d9035d323d1c60937ea7cd7a216315d5dc64"
3366
3366
  },
3367
3367
  {
3368
3368
  "path": "modules/lifecycle/done-when.js",
@@ -3467,7 +3467,7 @@
3467
3467
  {
3468
3468
  "path": "modules/lifecycle/routes/tasks.js",
3469
3469
  "mode": "0000644",
3470
- "sha256": "aa179ab79508a002e8f580fb839767d8aaa8b6f02a46d021bb61633dd9f8f9a4"
3470
+ "sha256": "8a6ecd6c7ecadcbec79483a589fef11957d13e2c94b569a9ff9da97f2908b713"
3471
3471
  },
3472
3472
  {
3473
3473
  "path": "modules/lifecycle/routes/versions.js",
@@ -4012,12 +4012,12 @@
4012
4012
  {
4013
4013
  "path": "package-lock.json",
4014
4014
  "mode": "0000644",
4015
- "sha256": "f1d885c73d1c6d6c7a1b5762e022f575045e320aa8e0fcfcaf3e9f00153fe8d5"
4015
+ "sha256": "ef48567990e07bdcd64ae5199d809b152d43b9bcae316adc4ddf6f9358f02d18"
4016
4016
  },
4017
4017
  {
4018
4018
  "path": "package.json",
4019
4019
  "mode": "0000644",
4020
- "sha256": "ef7a211fb70fc0316e3146e7a35a09617ea7fc2e275e8b06af61da04da94db51"
4020
+ "sha256": "d9078dd247c8d55e21bf2fb79a2b406a9d4901498284713e56954610b2b45e58"
4021
4021
  },
4022
4022
  {
4023
4023
  "path": "public-docs/index.html",
@@ -5637,7 +5637,7 @@
5637
5637
  {
5638
5638
  "path": "src/module-api.js",
5639
5639
  "mode": "0000644",
5640
- "sha256": "696cdcbb043dc4b0792302807c0130d348e83d7d2e8b2fc992805f31a4a3d677"
5640
+ "sha256": "b5517fa6cc30948cc76d4acecb2171d2f5a9854f557c4c6ee41e1e2315db643e"
5641
5641
  },
5642
5642
  {
5643
5643
  "path": "src/module-loader/loader.js",
@@ -6607,7 +6607,7 @@
6607
6607
  {
6608
6608
  "path": "tests/grader.mjs",
6609
6609
  "mode": "0000644",
6610
- "sha256": "4832bcffc7385cdd8deb0c1ff9bb44542190a3da106d2f00ccacd823c90103ec"
6610
+ "sha256": "9c8525b85be898be30d6034902b34fcdedb16cf4b37fa8d79fbd5d1fb89898cb"
6611
6611
  },
6612
6612
  {
6613
6613
  "path": "tests/grader_bypass.mjs",
@@ -6624,6 +6624,11 @@
6624
6624
  "mode": "0000644",
6625
6625
  "sha256": "fa155f3c5faa3763d1a51d56b7746ad870e826e3249f5e9b08108a3cfbfcbd72"
6626
6626
  },
6627
+ {
6628
+ "path": "tests/grader_severity_gate.mjs",
6629
+ "mode": "0000644",
6630
+ "sha256": "956384c2334742858dba987fe042b6c26581875ed5981a7565d43e46b55dfc49"
6631
+ },
6627
6632
  {
6628
6633
  "path": "tests/hall_audit.mjs",
6629
6634
  "mode": "0000644",
@@ -6827,7 +6832,7 @@
6827
6832
  {
6828
6833
  "path": "tests/migration_allocation.mjs",
6829
6834
  "mode": "0000644",
6830
- "sha256": "f014dee000483e41d01d41bc53361b81deba8ff1b21e5810aaf9200d65b8f366"
6835
+ "sha256": "10b1faf40cef2a3872c44109b63680e79922446b573bb476c79540d44a5622ac"
6831
6836
  },
6832
6837
  {
6833
6838
  "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; 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; 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 }
@@ -13789,6 +13789,9 @@
13789
13789
  "type": "string",
13790
13790
  "maxLength": 64
13791
13791
  },
13792
+ "needs_migration": {
13793
+ "type": "boolean"
13794
+ },
13792
13795
  "status": {}
13793
13796
  },
13794
13797
  "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`, `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`, `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` | |
@@ -32,17 +32,18 @@
32
32
  "concerns": "ANY worker.verdict='concerns' (and no fails) → overall passed=false, forwarded confidence = fallthrough_confidence (manual-confirm available).",
33
33
  "pass": "ALL workers.verdict='pass' → overall passed=true, advance to confirmed."
34
34
  },
35
+ "severity_gate_policy": "Task 1002663: style findings (comment style/length/placement, naming conventions, doc placement, test-name conventions) are ADVISORY — severity nit, recorded in issues[] for the audit trail, never a verdict driver. Enforced deterministically in grader-workers/worker-verdict.js applySeverityGatePolicy for narc/quality/efficiency: a worker 'fail' whose only basis is a sub-threshold score with NO major/blocker finding is an inconsistent self-report and downgrades to 'concerns' (manual-confirm pause) — a worker whose findings are all nit/minor can never verdict fail. The HACKER is deliberately EXEMPT: the security lens stays fail-closed on an unbacked sub-threshold score (a real finding mislabeled minor must still hard-fail). Untouched everywhere: blocker-driven fails, non-score fail triggers (Narc missing-AC), error short-circuits. Every worker prompt carries the matching STYLE_ADVISORY paragraph (worker-prompt.js).",
35
36
  "active_workers": {
36
37
  "narc": {
37
38
  "specialty": "semantic completion (description vs diff, line-level)",
38
39
  "scores_reported": ["functional_fidelity"],
39
- "verdict_rules": "fail if any blocker issue, any AC marked 'missing', or ff < 6; concerns if any AC 'partial' OR confidence < 0.4; pass otherwise",
40
+ "verdict_rules": "fail if any blocker issue, any AC marked 'missing', or ff < 6; concerns if any AC 'partial' OR confidence < 0.4; pass otherwise; severity_gate_policy: an ff < 6 with no major/blocker finding downgrades fail → concerns (style findings are advisory)",
40
41
  "prompt_lives_in": "modules/grading/grader-workers/narc.js"
41
42
  },
42
43
  "quality": {
43
44
  "specialty": "code quality (idiom, dead code, tests, invariants) + economy (advisory)",
44
45
  "scores_reported": ["code_quality", "economy"],
45
- "verdict_rules": "fail if any blocker issue or cq < 6; concerns if confidence < 0.4, economy < 6 (advisory nudge), or any major issue; pass otherwise",
46
+ "verdict_rules": "fail if any blocker issue or cq < 6; concerns if confidence < 0.4, economy < 6 (advisory nudge), or any major issue; pass otherwise; severity_gate_policy: a cq < 6 with no major/blocker finding downgrades fail → concerns (style findings are advisory)",
46
47
  "prompt_lives_in": "modules/grading/grader-workers/quality.js"
47
48
  },
48
49
  "hacker": {
@@ -55,7 +56,7 @@
55
56
  "efficiency": {
56
57
  "specialty": "runtime cost / hot-path performance (N+1, O(n²), sync I/O, unbounded queries)",
57
58
  "scores_reported": ["efficiency_score"],
58
- "verdict_rules": "fail if any blocker (critical/high) runtime-cost issue or efficiency_score < 6; concerns if confidence < 0.4 or any major (medium-severity) finding; pass otherwise",
59
+ "verdict_rules": "fail if any blocker (critical/high) runtime-cost issue or efficiency_score < 6; concerns if confidence < 0.4 or any major (medium-severity) finding; pass otherwise; severity_gate_policy: an efficiency_score < 6 with no major/blocker finding downgrades fail → concerns (style findings are advisory)",
59
60
  "distinct_from_economy": "Quality's `economy` axis measures DIFF SIZE (was this change bigger than the task needed?). Efficiency Monger's `efficiency_score` measures RUNTIME COST (will this code be expensive when it runs?). Both axes can be high (small AND fast); either can be low (small but slow, or large but fast). The panel records both separately.",
60
61
  "prompt_lives_in": "modules/grading/grader-workers/efficiency.js"
61
62
  }
@@ -17,7 +17,7 @@ const { THRESHOLD, LOW_CONF } = require('./rubric');
17
17
  // SR-15 / G1 (#995): scrub + fence all builder-controlled inputs. The shared
18
18
  // scaffold (worker-prompt) owns fenceBlock/scrubUntrusted/NOFOLLOW_PREAMBLE.
19
19
  const { buildWorkerPrompt, LLM_PROJECT } = require('./worker-prompt');
20
- const { failResult, parseCommon, finalizeVerdict } = require('./worker-verdict');
20
+ const { failResult, parseCommon, finalizeVerdict, applySeverityGatePolicy } = require('./worker-verdict');
21
21
 
22
22
  const KIND = 'efficiency';
23
23
  const NAME = 'Efficiency Monger';
@@ -191,7 +191,7 @@ function computeVerdict(parsed, meta = {}) {
191
191
  notesPrefix = `Efficiency Monger — PASS (eff=${effR}/10, ${patternsChecked.length} pattern${patternsChecked.length === 1 ? '' : 's'} checked, conf=${conf ?? '?'})`;
192
192
  }
193
193
 
194
- return finalizeVerdict({
194
+ return applySeverityGatePolicy(finalizeVerdict({
195
195
  verdict,
196
196
  scores: { efficiency_score: effR },
197
197
  issues,
@@ -199,7 +199,7 @@ function computeVerdict(parsed, meta = {}) {
199
199
  notesPrefix,
200
200
  notesFromSubagent,
201
201
  confidence: conf,
202
- });
202
+ }), { threshold: THRESHOLD });
203
203
  }
204
204
 
205
205
  module.exports = {
@@ -217,6 +217,11 @@ function computeVerdict(parsed, meta = {}) {
217
217
  notesPrefix = `Hacker — PASS (sec=${secR}/10, ${surfacesChecked.length} surface${surfacesChecked.length === 1 ? '' : 's'} checked, conf=${conf ?? '?'})`;
218
218
  }
219
219
 
220
+ // #1002663: the Hacker is deliberately EXEMPT from applySeverityGatePolicy.
221
+ // The security lens stays fail-closed on an unbacked sub-threshold score —
222
+ // a real finding the model mislabels as minor must still hard-fail, and the
223
+ // nit-churn evidence that motivated the policy was style/Quality-side, never
224
+ // security-score-driven.
220
225
  return finalizeVerdict({
221
226
  verdict,
222
227
  scores: { security_score: secR },
@@ -16,7 +16,7 @@ const { THRESHOLD, LOW_CONF } = require('./rubric');
16
16
  // block the model is told to treat as DATA, never instructions. The shared
17
17
  // scaffold (worker-prompt) owns fenceBlock/scrubUntrusted/NOFOLLOW_PREAMBLE.
18
18
  const { buildWorkerPrompt, LLM_PROJECT } = require('./worker-prompt');
19
- const { failResult, parseCommon, finalizeVerdict } = require('./worker-verdict');
19
+ const { failResult, parseCommon, finalizeVerdict, applySeverityGatePolicy } = require('./worker-verdict');
20
20
 
21
21
  const KIND = 'narc';
22
22
  const NAME = 'Narc';
@@ -185,7 +185,7 @@ function computeVerdict(parsed, meta = {}) {
185
185
  notesPrefix = `Narc — PASS (ff=${ffR}/10, ${verifiedStr}, conf=${conf ?? '?'})`;
186
186
  }
187
187
 
188
- return finalizeVerdict({
188
+ return applySeverityGatePolicy(finalizeVerdict({
189
189
  verdict,
190
190
  scores: { functional_fidelity: ffR },
191
191
  issues,
@@ -193,7 +193,9 @@ function computeVerdict(parsed, meta = {}) {
193
193
  notesPrefix,
194
194
  notesFromSubagent,
195
195
  confidence: conf,
196
- });
196
+ // A missing AC is a substantive non-score fail basis — it routinely
197
+ // co-occurs with a sub-threshold ff and must never be downgraded.
198
+ }), { threshold: THRESHOLD, substantiveFail: missingCount > 0 });
197
199
  }
198
200
 
199
201
  module.exports = {
@@ -14,7 +14,7 @@ const { THRESHOLD, LOW_CONF } = require('./rubric');
14
14
  // SR-15 / G1 (#995): scrub + fence all builder-controlled inputs. The shared
15
15
  // scaffold (worker-prompt) owns fenceBlock/scrubUntrusted/NOFOLLOW_PREAMBLE.
16
16
  const { buildWorkerPrompt, LLM_PROJECT } = require('./worker-prompt');
17
- const { failResult, parseCommon, finalizeVerdict } = require('./worker-verdict');
17
+ const { failResult, parseCommon, finalizeVerdict, applySeverityGatePolicy } = require('./worker-verdict');
18
18
 
19
19
  const KIND = 'quality';
20
20
  const NAME = 'Quality';
@@ -174,7 +174,7 @@ function computeVerdict(parsed, meta = {}) {
174
174
  + `, conf=${conf ?? '?'})`;
175
175
  }
176
176
 
177
- return finalizeVerdict({
177
+ return applySeverityGatePolicy(finalizeVerdict({
178
178
  verdict,
179
179
  scores: {
180
180
  code_quality: cqR,
@@ -184,7 +184,7 @@ function computeVerdict(parsed, meta = {}) {
184
184
  notesPrefix,
185
185
  notesFromSubagent,
186
186
  confidence: conf,
187
- });
187
+ }), { threshold: THRESHOLD });
188
188
  }
189
189
 
190
190
  module.exports = {
@@ -29,6 +29,18 @@ const LLM_PROJECT = (() => {
29
29
  catch { return 'this project'; }
30
30
  })();
31
31
 
32
+ // #1002663 severity-gate policy, stated to every worker: style findings are
33
+ // advisory and must never drive the verdict. Enforced deterministically in
34
+ // worker-verdict.js applySeverityGatePolicy — this paragraph keeps the model's
35
+ // self-reports consistent with that gate so downgrades stay rare.
36
+ const STYLE_ADVISORY = [
37
+ '## Severity policy — style findings are ADVISORY',
38
+ '',
39
+ 'Comment style/length/placement, naming conventions, documentation placement, and test-name conventions are severity `nit`, always. Record them in `issues[]` for the audit trail, but:',
40
+ '- never report a style finding at `major` or `blocker`;',
41
+ '- never let style concerns depress a numeric score below the pass threshold — a sub-threshold score MUST be backed by at least one `major` or `blocker` finding, or the verdict machinery downgrades your fail to concerns as an inconsistent self-report.',
42
+ ].join('\n');
43
+
32
44
  function buildWorkerPrompt({
33
45
  header, task, valueSummary, diff, changedFiles,
34
46
  taskHeader, descLabel, emptyFilesText, readFilesHint, finalInstruction,
@@ -82,6 +94,8 @@ function buildWorkerPrompt({
82
94
  lines.push('');
83
95
  lines.push(...fenceBlock('UNIFIED DIFF', diffText));
84
96
  lines.push('');
97
+ lines.push(STYLE_ADVISORY);
98
+ lines.push('');
85
99
  lines.push(finalInstruction);
86
100
  return lines.join('\n');
87
101
  }
@@ -47,4 +47,33 @@ function finalizeVerdict({ verdict, scores, issues, extra = {}, notesPrefix, not
47
47
  };
48
48
  }
49
49
 
50
- module.exports = { failResult, parseCommon, finalizeVerdict };
50
+ // #1002663 severity-gate policy: style findings are advisory, and a
51
+ // sub-threshold score must be BACKED by a substantive finding. Every worker
52
+ // prompt already demands "a score below threshold must be accompanied by a
53
+ // major-or-blocker issue"; this makes that contract deterministic instead of
54
+ // advisory prose the model can ignore. A 'fail' whose only basis is a
55
+ // sub-threshold score with NO major/blocker finding is an inconsistent
56
+ // self-report (style gripes leaking into the score channel — the task-1002653
57
+ // regrade-churn class) and downgrades to 'concerns', which routes to the
58
+ // manual-confirm pause rather than a hard panel FAIL. Deliberately untouched:
59
+ // blocker-driven fails, non-score fail triggers (e.g. Narc's missing-AC),
60
+ // and the error short-circuits — no per-worker security trigger is flattened.
61
+ // `substantiveFail`: a worker whose fail has a NON-SCORE basis the policy
62
+ // cannot see in issues[] (Narc's missing-AC — which routinely CO-OCCURS with a
63
+ // sub-threshold ff) passes true so the downgrade never touches it. Without
64
+ // this, missing-AC + low ff + no major issue read as an unbacked score fail.
65
+ function applySeverityGatePolicy(result, { threshold, substantiveFail = false }) {
66
+ if (!result || result.error || result.verdict !== 'fail' || substantiveFail) return result;
67
+ const issues = Array.isArray(result.issues) ? result.issues : [];
68
+ if (issues.some((i) => i && (i.severity === 'blocker' || i.severity === 'major'))) return result;
69
+ const reported = Object.values(result.scores || {}).filter((v) => Number.isFinite(v));
70
+ if (!reported.some((v) => v < threshold)) return result;
71
+ return {
72
+ ...result,
73
+ verdict: 'concerns',
74
+ policy_downgrade: 'subthreshold_score_without_substantive_finding',
75
+ notes_md: `${result.notes_md}\n[severity-gate] sub-threshold score with no major/blocker finding — fail downgraded to concerns (style findings are advisory)`.trim(),
76
+ };
77
+ }
78
+
79
+ module.exports = { failResult, parseCommon, finalizeVerdict, applySeverityGatePolicy };
@@ -1686,6 +1686,11 @@ async function updateTaskParallelSafe(id, parallelSafe) { return updateTaskColum
1686
1686
 
1687
1687
  async function updateTaskAutomationTag(id, automationTag) { return updateTaskColumn(id, 'automation_tag', automationTag); }
1688
1688
 
1689
+ // task 1002648: correct the create-time migration heuristic's verdict — a false
1690
+ // flag makes claiming reserve a migration number that goes unused (task 1002614
1691
+ // burned 213 that way). Boolean only; the route validates.
1692
+ async function updateTaskNeedsMigration(id, needsMigration) { return updateTaskColumn(id, 'needs_migration', needsMigration === true); }
1693
+
1689
1694
  // BV1.R53 (task 1422): assign (or clear) the module_key that declares which
1690
1695
  // module this task's work belongs to. Drives module-disjointness at claim time
1691
1696
  // and diff⊆module enforcement at ship time. Accepts any key in MODULE_SCOPE_MAP,
@@ -5441,6 +5446,7 @@ module.exports = {
5441
5446
  updateTaskParallelSafe,
5442
5447
  updateTaskSecuritySensitive,
5443
5448
  updateTaskAutomationTag,
5449
+ updateTaskNeedsMigration,
5444
5450
  updateTaskModuleKey,
5445
5451
  listClaimableTasks,
5446
5452
  capNewcomerTasks,
@@ -69,8 +69,27 @@ const VALID_FITS = new Set(['autonomous', 'parallel']);
69
69
  const MIGRATION_KEYWORD_RE =
70
70
  /\bmigrations?\b|\bschema\b|\balter\s+table\b|\bcreate\s+table\b/i;
71
71
 
72
+ // task 1002648 (idea 1000642): compounds where the keyword is qualified by a
73
+ // NON-database noun are refactor/API language, not schema work, and are blanked
74
+ // before the keyword test. Two families surfaced in the wild: "gate/route/code
75
+ // migration" (code-migration phrasing) and "request/PATCH/:NNN schema"
76
+ // (API-shape phrasing — the task-1002614 false positive, which reserved
77
+ // migration 213 for zero schema change). Bare "migration"/"schema" and every
78
+ // DB-shaped compound ("schema migration", "DB schema") still flag.
79
+ const NON_DB_COMPOUND_RE = new RegExp(
80
+ [
81
+ /\b(?:gate|route|code)s?[-\s]+migrations?\b/.source,
82
+ /\bmigrations?\s+of\s+(?:gate|route|code)s?\b/.source,
83
+ /\b(?:request|response|body|validation|patch|post|put|delete|json|openapi|api|error)[-\s]+schemas?\b/.source,
84
+ /:\d+[-\s]+schemas?\b/.source,
85
+ ].join('|'),
86
+ 'gi'
87
+ );
88
+
72
89
  function detectsMigration(...texts) {
73
- return texts.some((t) => typeof t === 'string' && MIGRATION_KEYWORD_RE.test(t));
90
+ return texts.some(
91
+ (t) => typeof t === 'string' && MIGRATION_KEYWORD_RE.test(t.replace(NON_DB_COMPOUND_RE, ' '))
92
+ );
74
93
  }
75
94
 
76
95
  // ---- peer votes (V3.R25 #245) ---------------------------------------------
@@ -773,6 +792,7 @@ module.exports = function buildTasksRouter() {
773
792
  xenos_claimable: { type: 'boolean' }, // pre-rename alias for newcomer_friendly
774
793
  security_sensitive: { type: 'boolean' }, // task 990 — Archon-only ship broadcast
775
794
  automation_tag: { type: 'string', maxLength: 64 },
795
+ needs_migration: { type: 'boolean' }, // task 1002648 — correct the create-time heuristic's flag (the claim-time allocator keys off it)
776
796
  // task 1002614 (map D-18): DECLARED so strict-mode admits the request far
777
797
  // enough to reach the deliberate status_not_patchable hint below (#514) —
778
798
  // the hint had been unreachable dead code behind unknown_field since the
@@ -794,6 +814,7 @@ module.exports = function buildTasksRouter() {
794
814
  const hasParallelSafe = Object.prototype.hasOwnProperty.call(body, 'parallel_safe');
795
815
  const hasSecuritySensitive = Object.prototype.hasOwnProperty.call(body, 'security_sensitive');
796
816
  const hasAutomationTag = Object.prototype.hasOwnProperty.call(body, 'automation_tag');
817
+ const hasNeedsMigration = Object.prototype.hasOwnProperty.call(body, 'needs_migration');
797
818
  const hasRequiresRank = Object.prototype.hasOwnProperty.call(body, 'requires_rank');
798
819
  const hasModuleKey = Object.prototype.hasOwnProperty.call(body, 'module_key');
799
820
  const hasGoalId = Object.prototype.hasOwnProperty.call(body, 'goal_id');
@@ -808,8 +829,8 @@ module.exports = function buildTasksRouter() {
808
829
  if (Object.prototype.hasOwnProperty.call(body, 'status')) {
809
830
  return res.fail('status_not_patchable', { status: 400, message: STATUS_HINT });
810
831
  }
811
- if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasRequiresRank && !hasModuleKey && !hasGoalId) {
812
- return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, parallel_safe, security_sensitive, automation_tag, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
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 });
813
834
  }
814
835
  // ---- task 1002609 (G-22): settle EVERY gate and validation before the ----
815
836
  // ---- FIRST write. The previous shape validated-and-wrote per field in ----
@@ -937,6 +958,15 @@ module.exports = function buildTasksRouter() {
937
958
  return res.fail('patch_failed', { status: 500, message: 'internal error' });
938
959
  }
939
960
  }
961
+ if (hasNeedsMigration) {
962
+ try {
963
+ updated = await db.updateTaskNeedsMigration(id, body.needs_migration);
964
+ if (!updated) return res.fail('task_not_found', 404);
965
+ } catch (err) {
966
+ console.error('[gds] PATCH /tasks/:id needs_migration', err);
967
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
968
+ }
969
+ }
940
970
  if (hasModuleKey) {
941
971
  try {
942
972
  updated = await db.updateTaskModuleKey(id, body.module_key ?? null);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.30",
3
+ "version": "1.19.32",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.30",
9
+ "version": "1.19.32",
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.30",
3
+ "version": "1.19.32",
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/src/module-api.js CHANGED
@@ -487,8 +487,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
487
487
  // landed since 1.19.28 with no explicit bump. run 31500810303. (task 1002620)
488
488
  // 1.19.30 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
489
489
  // landed since 1.19.29 with no explicit bump. run 31502880105. (task 1002620)
490
+ // 1.19.31 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
491
+ // landed since 1.19.30 with no explicit bump. run 31503461332. (task 1002620)
492
+ // 1.19.32 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
493
+ // landed since 1.19.31 with no explicit bump. run 31504858325. (task 1002620)
490
494
  // ---------------------------------------------------------------------------
491
- const CORE_VERSION = '1.19.30'; // CI auto-patch carrier (ADR 0161); see changelog above
495
+ const CORE_VERSION = '1.19.32'; // CI auto-patch carrier (ADR 0161); see changelog above
492
496
 
493
497
  // A namespaced logger so a module's log lines are attributable + consistent.
494
498
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
package/tests/grader.mjs CHANGED
@@ -1267,12 +1267,15 @@ test('v4 narc: any partial AC → concerns', () => {
1267
1267
  assert.equal(v.verdict, 'concerns');
1268
1268
  });
1269
1269
 
1270
- test('v4 narc: ff < threshold → fail', () => {
1270
+ // #1002663 severity-gate policy: a sub-threshold score must be BACKED by a
1271
+ // major/blocker finding to hard-fail; unbacked it downgrades to concerns
1272
+ // (tests/grader_severity_gate.mjs covers the downgrade shapes).
1273
+ test('v4 narc: ff < threshold backed by a major finding → fail', () => {
1271
1274
  const v = grader.WORKERS.narc.computeVerdict({
1272
1275
  functional_fidelity: 4,
1273
1276
  confidence: 0.9,
1274
1277
  ac_checklist: [{ ac: 'A', status: 'satisfied' }],
1275
- issues: [],
1278
+ issues: [{ severity: 'major', file: 'x.js', summary: 'scope gap' }],
1276
1279
  notes_md: 'low score',
1277
1280
  }, {});
1278
1281
  assert.equal(v.verdict, 'fail');
@@ -1316,9 +1319,10 @@ test('v4 quality: blocker issue → fail', () => {
1316
1319
  assert.equal(v.verdict, 'fail');
1317
1320
  });
1318
1321
 
1319
- test('v4 quality: cq < threshold → fail', () => {
1322
+ test('v4 quality: cq < threshold backed by a major issue → fail (#1002663 policy)', () => {
1320
1323
  const v = grader.WORKERS.quality.computeVerdict({
1321
- code_quality: 4, economy: 7, confidence: 0.9, issues: [], notes_md: '',
1324
+ code_quality: 4, economy: 7, confidence: 0.9,
1325
+ issues: [{ severity: 'major', file: 'x.js', summary: 'dead code path' }], notes_md: '',
1322
1326
  }, {});
1323
1327
  assert.equal(v.verdict, 'fail');
1324
1328
  });
@@ -1685,7 +1689,7 @@ test('v4 hacker: blocker (critical/high) issue → fail', () => {
1685
1689
  assert.equal(v.verdict, 'fail');
1686
1690
  });
1687
1691
 
1688
- test('v4 hacker: security_score below threshold → fail', () => {
1692
+ test('v4 hacker: security_score below threshold → fail (EXEMPT from the #1002663 downgrade — fail-closed)', () => {
1689
1693
  const v = grader.WORKERS.hacker.computeVerdict({
1690
1694
  security_score: 4, confidence: 0.9, surfaces_checked: [], issues: [], notes_md: '',
1691
1695
  }, {});
@@ -1821,9 +1825,10 @@ test('v4 efficiency: blocker (N+1, O(n²), sync-I/O-on-hot-path) → fail', () =
1821
1825
  assert.equal(v.verdict, 'fail');
1822
1826
  });
1823
1827
 
1824
- test('v4 efficiency: efficiency_score below threshold → fail', () => {
1828
+ test('v4 efficiency: efficiency_score below threshold backed by a major finding → fail (#1002663 policy)', () => {
1825
1829
  const v = grader.WORKERS.efficiency.computeVerdict({
1826
- efficiency_score: 4, confidence: 0.9, expensive_patterns_checked: [], issues: [], notes_md: '',
1830
+ efficiency_score: 4, confidence: 0.9, expensive_patterns_checked: [],
1831
+ issues: [{ severity: 'major', file: 'x.js', summary: 'N+1 in list view' }], notes_md: '',
1827
1832
  }, {});
1828
1833
  assert.equal(v.verdict, 'fail');
1829
1834
  });
@@ -0,0 +1,156 @@
1
+ // tests/grader_severity_gate.mjs
2
+ //
3
+ // Unit tests for task 1002663 — the severity-gate policy: style findings are
4
+ // ADVISORY, and a sub-threshold score must be BACKED by a major/blocker
5
+ // finding. Evidence class (task 1002653, 2026-08-11): panel rounds hung on
6
+ // comment-style nits, and the prompt line "a score below 6 must be accompanied
7
+ // by a major-or-blocker issue" was advisory prose the model could ignore.
8
+ // applySeverityGatePolicy (grader-workers/worker-verdict.js) makes it
9
+ // deterministic: an unbacked sub-threshold 'fail' downgrades to 'concerns'
10
+ // (manual-confirm pause), never a hard panel FAIL — while blocker-driven
11
+ // fails, non-score triggers (Narc's missing-AC), and error short-circuits
12
+ // stay exactly as they were.
13
+ //
14
+ // Run: node tests/grader_severity_gate.mjs
15
+
16
+ import { strict as assert } from 'node:assert';
17
+ import { createRequire } from 'node:module';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const { applySeverityGatePolicy, failResult } = require('../modules/grading/grader-workers/worker-verdict.js');
21
+ const narc = require('../modules/grading/grader-workers/narc.js');
22
+ const quality = require('../modules/grading/grader-workers/quality.js');
23
+ const hacker = require('../modules/grading/grader-workers/hacker.js');
24
+ const efficiency = require('../modules/grading/grader-workers/efficiency.js');
25
+
26
+ const THRESHOLD = 6;
27
+
28
+ let passed = 0;
29
+ let failed = 0;
30
+ function test(name, fn) {
31
+ try { fn(); passed++; console.log(` ok ${name}`); }
32
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
33
+ }
34
+
35
+ const nit = { severity: 'nit', file: 'x.js', summary: 'comment style' };
36
+ const minor = { severity: 'minor', file: 'x.js', summary: 'small thing' };
37
+ const major = { severity: 'major', file: 'x.js', summary: 'real problem' };
38
+ const blocker = { severity: 'blocker', file: 'x.js', summary: 'must fix' };
39
+
40
+ // ---- applySeverityGatePolicy unit ----------------------------------------
41
+
42
+ test('unbacked sub-threshold fail (only nits/minors) → concerns with policy_downgrade', () => {
43
+ const r = applySeverityGatePolicy(
44
+ { verdict: 'fail', scores: { code_quality: 5 }, issues: [nit, minor], notes_md: 'n', confidence: 0.9, error: null },
45
+ { threshold: THRESHOLD }
46
+ );
47
+ assert.equal(r.verdict, 'concerns');
48
+ assert.equal(r.policy_downgrade, 'subthreshold_score_without_substantive_finding');
49
+ assert.match(r.notes_md, /severity-gate/);
50
+ });
51
+
52
+ test('sub-threshold fail BACKED by a major stays fail', () => {
53
+ const r = applySeverityGatePolicy(
54
+ { verdict: 'fail', scores: { code_quality: 5 }, issues: [major], notes_md: 'n', confidence: 0.9, error: null },
55
+ { threshold: THRESHOLD }
56
+ );
57
+ assert.equal(r.verdict, 'fail');
58
+ assert.equal(r.policy_downgrade, undefined);
59
+ });
60
+
61
+ test('blocker-driven fail with passing scores stays fail (no security trigger flattened)', () => {
62
+ const r = applySeverityGatePolicy(
63
+ { verdict: 'fail', scores: { security_score: 9 }, issues: [blocker], notes_md: 'n', confidence: 0.9, error: null },
64
+ { threshold: THRESHOLD }
65
+ );
66
+ assert.equal(r.verdict, 'fail');
67
+ });
68
+
69
+ test('non-score fail trigger (no sub-threshold score, no issues) stays fail — the Narc missing-AC shape', () => {
70
+ const r = applySeverityGatePolicy(
71
+ { verdict: 'fail', scores: { functional_fidelity: 7 }, issues: [], notes_md: 'n', confidence: 0.9, error: null },
72
+ { threshold: THRESHOLD }
73
+ );
74
+ assert.equal(r.verdict, 'fail');
75
+ });
76
+
77
+ test('error short-circuits are untouched', () => {
78
+ const r = applySeverityGatePolicy(failResult({ notesMd: 'boom', error: 'subprocess_error' }), { threshold: THRESHOLD });
79
+ assert.equal(r.verdict, 'fail');
80
+ assert.equal(r.error, 'subprocess_error');
81
+ });
82
+
83
+ test('pass and concerns verdicts are untouched', () => {
84
+ for (const verdict of ['pass', 'concerns']) {
85
+ const r = applySeverityGatePolicy(
86
+ { verdict, scores: { code_quality: 5 }, issues: [nit], notes_md: 'n', confidence: 0.9, error: null },
87
+ { threshold: THRESHOLD }
88
+ );
89
+ assert.equal(r.verdict, verdict);
90
+ }
91
+ });
92
+
93
+ // ---- worker integration ---------------------------------------------------
94
+
95
+ test('quality: cq=5 with only nits → concerns (not fail)', () => {
96
+ const r = quality.computeVerdict({ code_quality: 5, economy: 8, confidence: 0.9, issues: [nit], notes_md: '' });
97
+ assert.equal(r.verdict, 'concerns');
98
+ assert.equal(r.policy_downgrade, 'subthreshold_score_without_substantive_finding');
99
+ });
100
+
101
+ test('quality: cq=5 with a major → fail stays', () => {
102
+ const r = quality.computeVerdict({ code_quality: 5, economy: 8, confidence: 0.9, issues: [major], notes_md: '' });
103
+ assert.equal(r.verdict, 'fail');
104
+ });
105
+
106
+ test('quality: blocker with cq=9 → fail stays', () => {
107
+ const r = quality.computeVerdict({ code_quality: 9, economy: 8, confidence: 0.9, issues: [blocker], notes_md: '' });
108
+ assert.equal(r.verdict, 'fail');
109
+ });
110
+
111
+ test('hacker is EXEMPT: security_score=5 with only a nit stays a hard fail (security lens fail-closed)', () => {
112
+ const r = hacker.computeVerdict({ security_score: 5, confidence: 0.9, issues: [nit], notes_md: '', surfaces_checked: [] });
113
+ assert.equal(r.verdict, 'fail');
114
+ assert.equal(r.policy_downgrade, undefined);
115
+ });
116
+
117
+ test('efficiency: eff=5 with only a minor → concerns (not fail)', () => {
118
+ const r = efficiency.computeVerdict({ efficiency_score: 5, confidence: 0.9, issues: [minor], notes_md: '', expensive_patterns_checked: [] });
119
+ assert.equal(r.verdict, 'concerns');
120
+ });
121
+
122
+ test('narc: missing AC with ff=7 and no issues → fail stays (non-score trigger untouched)', () => {
123
+ const r = narc.computeVerdict({
124
+ functional_fidelity: 7, confidence: 0.9, issues: [], notes_md: '',
125
+ ac_checklist: [{ ac: 'does the thing', status: 'missing' }],
126
+ });
127
+ assert.equal(r.verdict, 'fail');
128
+ });
129
+
130
+ test('narc: missing AC CO-OCCURRING with sub-threshold ff and no issues → fail stays (the common shape)', () => {
131
+ const r = narc.computeVerdict({
132
+ functional_fidelity: 4, confidence: 0.9, issues: [], notes_md: '',
133
+ ac_checklist: [{ ac: 'does the thing', status: 'missing' }],
134
+ });
135
+ assert.equal(r.verdict, 'fail');
136
+ assert.equal(r.policy_downgrade, undefined);
137
+ });
138
+
139
+ test('narc: ff=5 with only nits → concerns (not fail)', () => {
140
+ const r = narc.computeVerdict({ functional_fidelity: 5, confidence: 0.9, issues: [nit], notes_md: '', ac_checklist: [] });
141
+ assert.equal(r.verdict, 'concerns');
142
+ });
143
+
144
+ // ---- prompt carries the policy -------------------------------------------
145
+
146
+ test('every worker prompt carries the STYLE_ADVISORY paragraph', () => {
147
+ const args = { task: { id: 1, title: 't', description: 'd', kind: 'bug', discipline: 'engineer' }, valueSummary: 'v', diff: '+ x', changedFiles: ['x.js'] };
148
+ for (const w of [narc, quality, hacker, efficiency]) {
149
+ const p = w.buildPrompt(args);
150
+ assert.match(p, /Severity policy — style findings are ADVISORY/, `${w.KIND || 'worker'} prompt must carry the policy`);
151
+ assert.match(p, /never report a style finding at `major`/);
152
+ }
153
+ });
154
+
155
+ console.log(`\n${passed} passed, ${failed} failed`);
156
+ process.exit(failed === 0 ? 0 : 1);
@@ -27,6 +27,7 @@ import { strict as assert } from 'node:assert';
27
27
  import { readFileSync } from 'node:fs';
28
28
  import { fileURLToPath } from 'node:url';
29
29
  import { dirname, join } from 'node:path';
30
+ import { createRequire } from 'node:module';
30
31
 
31
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
32
33
  const repoRoot = join(__dirname, '..');
@@ -141,6 +142,56 @@ function shouldReserveMigrationNumber(task) {
141
142
  );
142
143
  });
143
144
 
145
+ // ---- task 1002648 (idea 1000642): the classifier must not flag non-DB ----
146
+ // "migration"/"schema" compounds, and a wrong verdict must be correctable
147
+ // via PATCH. Behavioral tests use the real exported detectsMigration.
148
+ const require = createRequire(import.meta.url);
149
+ const { detectsMigration } = require('../modules/lifecycle/routes/tasks.js');
150
+
151
+ await test('classifier ignores code-migration and API-schema phrasing (task 1002648)', () => {
152
+ const falsePositives = [
153
+ 'gate migration cleanup across the router',
154
+ 'finish the route-migration started in R86',
155
+ 'the migration of gates to the new catalog',
156
+ // the task-1002614 repro — reserved migration 213 for zero schema change:
157
+ 'the PATCH schema declares 11 keys and description is not among them',
158
+ 'status is undeclared in the :751 schema, so strict-mode validation returns unknown_field first',
159
+ 'align the request schema with the response schema',
160
+ ];
161
+ for (const text of falsePositives) {
162
+ assert.equal(detectsMigration(text), false, `must NOT flag: ${JSON.stringify(text)}`);
163
+ }
164
+ });
165
+
166
+ await test('classifier still flags real schema work (regression)', () => {
167
+ const truePositives = [
168
+ 'add a migration to backfill the marker',
169
+ 'ALTER TABLE builders add a column',
170
+ 'create table for the new ledger',
171
+ 'schema change on tasks',
172
+ 'a schema migration for the counter floor',
173
+ 'apply migrations/ before deploy',
174
+ ];
175
+ for (const text of truePositives) {
176
+ assert.equal(detectsMigration(text), true, `must flag: ${JSON.stringify(text)}`);
177
+ }
178
+ });
179
+
180
+ await test('PATCH /tasks/:id accepts needs_migration so a wrong flag is correctable', () => {
181
+ assert.ok(
182
+ /needs_migration: \{ type: 'boolean' \}/.test(routesSrc),
183
+ 'the PATCH schema must declare needs_migration as a boolean'
184
+ );
185
+ assert.ok(
186
+ /db\.updateTaskNeedsMigration\(id, body\.needs_migration\)/.test(routesSrc),
187
+ 'the PATCH handler must write via db.updateTaskNeedsMigration'
188
+ );
189
+ assert.ok(
190
+ /async function updateTaskNeedsMigration/.test(dbSrc) && /updateTaskNeedsMigration,/.test(dbSrc),
191
+ 'db.js must define and export updateTaskNeedsMigration'
192
+ );
193
+ });
194
+
144
195
  console.log(`\n${passed} passed, ${failed} failed`);
145
196
  if (failed > 0) process.exit(1);
146
197
  })();