@bongos/core 1.19.36 → 1.19.38
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 +32 -17
- package/.claude/skills/grader-health/SKILL.md +61 -0
- package/docs/file-map.md +1 -0
- package/modules/lifecycle/db.js +38 -0
- package/modules/lifecycle/routes/claims.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claim.js +7 -0
- package/scripts/gds/run-unit-tests.js +1 -1
- package/src/module-api.js +5 -1
- package/tests/claim_error_surface.mjs +12 -0
- package/tests/claim_gate_open_blocker.mjs +93 -0
- package/tests/skill_grader_health.mjs +48 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.38",
|
|
6
|
+
"core_contract": "1.19.38",
|
|
7
|
+
"source_commit": "ba6a8f6285476ab3cd4e33f9278b333ac0fc3a29",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-11T16:17:20.571Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 288,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1212,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1521,
|
|
20
|
+
"tree_sha256": "9d55773209aeb85fef771e85aa30e901c52e54e155601030e44cd16fd5c46bda",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -159,6 +159,11 @@
|
|
|
159
159
|
"mode": "0000644",
|
|
160
160
|
"sha256": "e0bf90fa006499aee0a7d75f10fe4b5972c9501129b1e432e7d6e192b367a54c"
|
|
161
161
|
},
|
|
162
|
+
{
|
|
163
|
+
"path": ".claude/skills/grader-health/SKILL.md",
|
|
164
|
+
"mode": "0000644",
|
|
165
|
+
"sha256": "b9823a6a14b4197f7dae90c8ad6713fcc5b3ce672d03d7b59c8e868d5d6ba70e"
|
|
166
|
+
},
|
|
162
167
|
{
|
|
163
168
|
"path": ".claude/skills/idea-triage/SKILL.md",
|
|
164
169
|
"mode": "0000644",
|
|
@@ -1357,7 +1362,7 @@
|
|
|
1357
1362
|
{
|
|
1358
1363
|
"path": "docs/file-map.md",
|
|
1359
1364
|
"mode": "0000644",
|
|
1360
|
-
"sha256": "
|
|
1365
|
+
"sha256": "f51d3978f50ec9b2a34dc12b8fa3b9e8874543e5ab3503f72a09d7d2b06c565b"
|
|
1361
1366
|
},
|
|
1362
1367
|
{
|
|
1363
1368
|
"path": "docs/handoff-template.md",
|
|
@@ -3367,7 +3372,7 @@
|
|
|
3367
3372
|
{
|
|
3368
3373
|
"path": "modules/lifecycle/db.js",
|
|
3369
3374
|
"mode": "0000644",
|
|
3370
|
-
"sha256": "
|
|
3375
|
+
"sha256": "c7dfc49df888857c8975077a069e8730d16926d8e92776f09489f69a40068f31"
|
|
3371
3376
|
},
|
|
3372
3377
|
{
|
|
3373
3378
|
"path": "modules/lifecycle/done-when.js",
|
|
@@ -3442,7 +3447,7 @@
|
|
|
3442
3447
|
{
|
|
3443
3448
|
"path": "modules/lifecycle/routes/claims.js",
|
|
3444
3449
|
"mode": "0000644",
|
|
3445
|
-
"sha256": "
|
|
3450
|
+
"sha256": "92065e0a5cfd2a5d76bbf04f4efafe0e2b3bc31cf616136963d4c07fad25047e"
|
|
3446
3451
|
},
|
|
3447
3452
|
{
|
|
3448
3453
|
"path": "modules/lifecycle/routes/dependencies.js",
|
|
@@ -4017,12 +4022,12 @@
|
|
|
4017
4022
|
{
|
|
4018
4023
|
"path": "package-lock.json",
|
|
4019
4024
|
"mode": "0000644",
|
|
4020
|
-
"sha256": "
|
|
4025
|
+
"sha256": "413f575fce07ba5b167f6492a4f1aed1f7eacb4f4351457303d3875f04557787"
|
|
4021
4026
|
},
|
|
4022
4027
|
{
|
|
4023
4028
|
"path": "package.json",
|
|
4024
4029
|
"mode": "0000644",
|
|
4025
|
-
"sha256": "
|
|
4030
|
+
"sha256": "d0d31ac616a949591bca2975236bbf6d98437e969d1618bd68811620c2a130c3"
|
|
4026
4031
|
},
|
|
4027
4032
|
{
|
|
4028
4033
|
"path": "public-docs/index.html",
|
|
@@ -4547,7 +4552,7 @@
|
|
|
4547
4552
|
{
|
|
4548
4553
|
"path": "scripts/gds/claim.js",
|
|
4549
4554
|
"mode": "0000644",
|
|
4550
|
-
"sha256": "
|
|
4555
|
+
"sha256": "6f09d9344f57039c07d2de943dabb48dbf717226aa2667f4163dab47fb74428a"
|
|
4551
4556
|
},
|
|
4552
4557
|
{
|
|
4553
4558
|
"path": "scripts/gds/classify-discipline.js",
|
|
@@ -5057,7 +5062,7 @@
|
|
|
5057
5062
|
{
|
|
5058
5063
|
"path": "scripts/gds/run-unit-tests.js",
|
|
5059
5064
|
"mode": "0000644",
|
|
5060
|
-
"sha256": "
|
|
5065
|
+
"sha256": "ce8856194aa805d1f62d47d4b04bac05168c0c748f869a094476a28b9db2a30a"
|
|
5061
5066
|
},
|
|
5062
5067
|
{
|
|
5063
5068
|
"path": "scripts/gds/sandbox-stage.js",
|
|
@@ -5642,7 +5647,7 @@
|
|
|
5642
5647
|
{
|
|
5643
5648
|
"path": "src/module-api.js",
|
|
5644
5649
|
"mode": "0000644",
|
|
5645
|
-
"sha256": "
|
|
5650
|
+
"sha256": "2dfd2867ab83689497aff7a3b7f213e756c182e047ef8da754e098a945491ce7"
|
|
5646
5651
|
},
|
|
5647
5652
|
{
|
|
5648
5653
|
"path": "src/module-loader/loader.js",
|
|
@@ -6042,7 +6047,12 @@
|
|
|
6042
6047
|
{
|
|
6043
6048
|
"path": "tests/claim_error_surface.mjs",
|
|
6044
6049
|
"mode": "0000644",
|
|
6045
|
-
"sha256": "
|
|
6050
|
+
"sha256": "365f498d290298e0cd74888aff080cfbf3439e5e0cf2dd883291f39dface27e7"
|
|
6051
|
+
},
|
|
6052
|
+
{
|
|
6053
|
+
"path": "tests/claim_gate_open_blocker.mjs",
|
|
6054
|
+
"mode": "0000644",
|
|
6055
|
+
"sha256": "e62324e8e504e4f2788355b1ed1ffc5b1d434ef0ef0e72c18751236436e10c49"
|
|
6046
6056
|
},
|
|
6047
6057
|
{
|
|
6048
6058
|
"path": "tests/claim_gate_rebase.mjs",
|
|
@@ -7459,6 +7469,11 @@
|
|
|
7459
7469
|
"mode": "0000644",
|
|
7460
7470
|
"sha256": "76bed8e6fae3bd77d4b45be14d9f80bda71b2f92323fa5697a7e4f07b65d3ba2"
|
|
7461
7471
|
},
|
|
7472
|
+
{
|
|
7473
|
+
"path": "tests/skill_grader_health.mjs",
|
|
7474
|
+
"mode": "0000644",
|
|
7475
|
+
"sha256": "4557c934704a009612ed8858c0f59c5b53facb5d92fa2c0740b1aecb079aa5c9"
|
|
7476
|
+
},
|
|
7462
7477
|
{
|
|
7463
7478
|
"path": "tests/skill_prefs.mjs",
|
|
7464
7479
|
"mode": "0000644",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grader-health
|
|
3
|
+
description: Read-only health check over recent grades — consecutive outage runs, pass-rate drift vs baseline, per-grade cost anomalies, and stale model pins. The routine that would have caught the 21-outage 0.0-score streak on day one. Triggers when the user says "/grader-health", "check the grader", "is the grader healthy", "grader health check", "why are grades failing lately", or as part of a periodic ops sweep. Reports and emits exact remediation lines; changes nothing (ADR 0162 — no new gates, describe only). Optional --probe runs a live paid calibration (Metic+).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are running a health check over the grading panel. Everything here is **read-only**: you read live surfaces, compare against known baselines, and emit findings + exact remediation commands. You never confirm, flip, or re-grade anything yourself — the builder (or /grade-recover) acts on what you report.
|
|
7
|
+
|
|
8
|
+
## The four axes
|
|
9
|
+
|
|
10
|
+
### 1 — Outage runs (the pattern this skill exists to catch)
|
|
11
|
+
|
|
12
|
+
A grader outage used to be recorded as a 0.0 "quality failure": one builder accumulated **21 zero-score rows** over a month before anyone noticed, and the public tile showed a phantom 5-day 0%-pass crater. Since the outage-as-zero fix, aggregates exclude outage rounds and bucket them as `n_unavailable` — so the streak is now directly readable.
|
|
13
|
+
|
|
14
|
+
1. Read the public trend: `node scripts/gds/api.js GET "/api/gds/public/grades?days=30"` — scan `trend[]` for days where the average is 0 (or null) with passes 0 and n ≥ 2, and for any nonzero `n_unavailable` streak.
|
|
15
|
+
2. (Archon) Read the per-builder split: `node scripts/gds/api.js GET "/api/gds/grades/by-builder?days=30"` — a single builder with pass_rate 0.000 or a large `n_unavailable` bucket is the 21×0.0 shape localized to one machine or one environment.
|
|
16
|
+
3. **Confirm outage vs rejection before alarming anyone.** For each task in a suspect window: `node scripts/gds/api.js GET "/api/gds/tasks/<id>?include=grade"` — `signals.panel_outcome='unavailable'` (with `grader_unavailable_reason` and per-worker errors in `signals.workers`) is an outage, **not a quality rejection**; a real FAIL carries findings in `issues[]`.
|
|
17
|
+
4. For every task parked at `completed` by a confirmed outage, emit the exact remediation line, one per task:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node scripts/gds/ship.js <id> --regrade
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
5. Check the local spawn leg: the panel spawns `claude -p` subprocesses, so verify the CLI is present and authenticated, and run `node scripts/gds/doctor.js` for the hooks/worktree leg.
|
|
24
|
+
|
|
25
|
+
### 2 — Pass-rate drift
|
|
26
|
+
|
|
27
|
+
Baselines: the 14-agent evaluation window measured a **90.6%** pass rate, already above the 87% that ADR 0024 called "suspiciously high" and built the adversarial panel to fix. From `/api/gds/public/grades` compute the rolling pass rate and compare:
|
|
28
|
+
|
|
29
|
+
- **Creeping up** past the baseline → grade inflation; the panel is rubber-stamping (report; candidate causes: score compression, reused grades, trivial-path overuse).
|
|
30
|
+
- **Collapsing** far below it → either a real quality regression or an unconfirmed outage window — axis 1 disambiguates.
|
|
31
|
+
|
|
32
|
+
### 3 — Cost anomalies
|
|
33
|
+
|
|
34
|
+
Benchmark: mean panel cost **$2.14** per grade, and ~90% of it is **fixed** re-exploration cost, independent of diff size. Read grader spend: `node scripts/gds/api.js GET "/api/gds/public/cost-summary?source=grader&days=30"` and divide by the grade count from axis 2's window.
|
|
35
|
+
|
|
36
|
+
- Mean drifting well above the benchmark → panel re-rolls/retries or timeout-and-retry loops (not "big diffs" — the fixed-cost share means diff size barely moves the mean).
|
|
37
|
+
- Near-zero spend with grades still recording → the trivial path or grade reuse dominating; spot-check that gating lenses are actually running.
|
|
38
|
+
|
|
39
|
+
### 4 — Model pins
|
|
40
|
+
|
|
41
|
+
The evaluation found every grade ran on a generation-stale pin. Read the live pin from the repo: `DEFAULT_GRADER_MODEL` and `selectGraderModel` in `modules/grading/grader.js`, plus the rubric version in `modules/grading/grader-rubric.json`. Report when the pinned family is a generation behind the current Claude family, or when `selectGraderModel` would throw on a current builder model id (it throws on ids lacking opus/sonnet/haiku — a stranded-ship risk).
|
|
42
|
+
|
|
43
|
+
## Optional: `--probe` (live calibration, spends real money)
|
|
44
|
+
|
|
45
|
+
With `--probe`, run the live panel calibration: `node scripts/gds/grade-smoke.js`. It grades known-good and known-bad fixtures against the real panel and reports miscalibration. **Costs ~$0.30–0.90 per run; Metic+ only.** Never run it implicitly — only when the user passed `--probe` or asked for a live calibration.
|
|
46
|
+
|
|
47
|
+
## Report shape
|
|
48
|
+
|
|
49
|
+
End with a compact findings list, most severe first: axis, evidence (the numbers you read), verdict (outage / drift / anomaly / stale-pin / healthy), and the exact remediation command or the skill to hand off to (`/grade-recover` for a single stuck task; a GDS task via `capture.js` for systemic fixes). A clean bill of health is one line per axis.
|
|
50
|
+
|
|
51
|
+
## Constraints
|
|
52
|
+
|
|
53
|
+
- **Read-only, describe-only (ADR 0162).** No confirms, no regrades run by this skill, no status flips, no new gates. The remediation lines are FOR the builder to run.
|
|
54
|
+
- **Confirm before alarming.** Axis 1's outage-vs-rejection check comes before any "the grader is down" conclusion — a real FAIL streak on one surface is signal, not outage.
|
|
55
|
+
- **`--probe` spends money** — explicit opt-in only, Metic+.
|
|
56
|
+
|
|
57
|
+
## Files this skill touches
|
|
58
|
+
|
|
59
|
+
- Reads: `GET /api/gds/public/grades`, `GET /api/gds/grades/by-builder` (Archon), `GET /api/gds/public/cost-summary?source=grader`, `GET /api/gds/tasks/:id?include=grade`, `modules/grading/grader.js`, `modules/grading/grader-rubric.json`.
|
|
60
|
+
- Runs (read-only): `node scripts/gds/doctor.js`; with `--probe` only: `node scripts/gds/grade-smoke.js`.
|
|
61
|
+
- Writes: nothing.
|
package/docs/file-map.md
CHANGED
|
@@ -351,6 +351,7 @@ tests/
|
|
|
351
351
|
├── goal-create/SKILL.md ← /goal-create — plan and create ONE goal (the module-scoped workspace between version and criteria, ADR 0086) in one session: <redacted>, scope wall, 2-4 criteria, seed tasks; also the auto-pickup when a task is created with no goal to live in (goal_advisory); Metic+ (Archon for a protected-module scope)
|
|
352
352
|
├── goal-review/SKILL.md ← /goal-review — walk the criterion review queue: each auto-flagged 'met — pending review' criterion gets confirm / reject / defer; confirming a goal's last criterion auto-achieves it (ADR 0086 §6); Metic+
|
|
353
353
|
├── grade-recover/SKILL.md ← /grade-recover N — the 'my grade failed, now what' loop: diagnose outage vs fabricated fail vs genuine fail from the persisted grade, then run the right recovery (--regrade or the auditable override-request); adds no ship-path gate (ADR 0162, task 1002655)
|
|
354
|
+
├── grader-health/SKILL.md ← /grader-health — read-only grader health check: outage runs (n_unavailable streaks, outage-vs-rejection confirmation), pass-rate drift vs baseline, per-grade cost anomalies, stale model pins; emits exact --regrade remediation lines; optional --probe live calibration (Metic+, paid) (task 1002656)
|
|
354
355
|
├── idea-triage/SKILL.md ← /idea-triage — daily walk through the GDS idea_inbox (promote / discard / merge); Metic+
|
|
355
356
|
├── ideate/SKILL.md ← /ideate — ideation-session playbook for ideator-discipline tasks; inverts the loop (human drives, Claude is the sounding board) ([#1267](https://example.com/builders#/task/1267))
|
|
356
357
|
├── merge-mode/SKILL.md ← /merge-mode — manual fallback for the auto-merge: land every confirmed task (merge + smoke + deploy)
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -2586,6 +2586,23 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
2586
2586
|
throw { code: 'TASK_NOT_READY', currentStatus: task.status };
|
|
2587
2587
|
}
|
|
2588
2588
|
|
|
2589
|
+
// task 1002650 (idea 1000644): the claimable FEED filters out ready tasks
|
|
2590
|
+
// with an open blocker link, but the direct claim-by-id path never checked —
|
|
2591
|
+
// the feed hid the trap and this path walked into it. Mirror migration 176's
|
|
2592
|
+
// task_is_fully_unblocked clause so both gates enforce the same rule.
|
|
2593
|
+
const { rows: openBlockers } = await client.query(
|
|
2594
|
+
`SELECT b.id, b.title
|
|
2595
|
+
FROM task_blockers tb
|
|
2596
|
+
JOIN blockers b ON b.id = tb.blocker_id
|
|
2597
|
+
WHERE tb.task_id = $1
|
|
2598
|
+
AND b.status = 'open'
|
|
2599
|
+
ORDER BY b.id`,
|
|
2600
|
+
[taskId]
|
|
2601
|
+
);
|
|
2602
|
+
if (openBlockers.length > 0) {
|
|
2603
|
+
throw { code: 'OPEN_BLOCKER', blockers: openBlockers };
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2589
2606
|
// Three-rank model (task #360 / ADR 0018; V3.R15 #239): a Xenos is the
|
|
2590
2607
|
// sandboxed entry rank and may claim ONLY tasks explicitly opened to them
|
|
2591
2608
|
// (newcomer_friendly). Metic+ (the approved working rank) and Archon claim
|
|
@@ -2893,6 +2910,27 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
|
|
|
2893
2910
|
}
|
|
2894
2911
|
}
|
|
2895
2912
|
|
|
2913
|
+
// task 1002650 (idea 1000644): same open-blocker rule as claimTask — the
|
|
2914
|
+
// batch path must not sidestep the gate the feed and single claim enforce.
|
|
2915
|
+
const { rows: blockedRows } = await client.query(
|
|
2916
|
+
`SELECT tb.task_id, b.id, b.title
|
|
2917
|
+
FROM task_blockers tb
|
|
2918
|
+
JOIN blockers b ON b.id = tb.blocker_id
|
|
2919
|
+
WHERE tb.task_id = ANY($1::bigint[])
|
|
2920
|
+
AND b.status = 'open'
|
|
2921
|
+
ORDER BY b.id`,
|
|
2922
|
+
[uniqIds]
|
|
2923
|
+
);
|
|
2924
|
+
const blockersByTask = new Map();
|
|
2925
|
+
for (const r of blockedRows) {
|
|
2926
|
+
const list = blockersByTask.get(Number(r.task_id)) || [];
|
|
2927
|
+
list.push({ id: r.id, title: r.title });
|
|
2928
|
+
blockersByTask.set(Number(r.task_id), list);
|
|
2929
|
+
}
|
|
2930
|
+
for (const [blockedTaskId, blockers] of blockersByTask) {
|
|
2931
|
+
failures.push({ task_id: blockedTaskId, reason: 'OPEN_BLOCKER', blockers });
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2896
2934
|
// Three-rank model (task #360 / ADR 0018; V3.R15 #239): a Xenos may
|
|
2897
2935
|
// batch-claim only tasks explicitly opened to them (newcomer_friendly).
|
|
2898
2936
|
// Mirrors the single claimTask gate so the batch path can't be used to
|
|
@@ -250,6 +250,10 @@ module.exports = function buildClaimsRouter() {
|
|
|
250
250
|
err.code === 'TOUCHES_CONFLICT' ? 409 :
|
|
251
251
|
err.code === 'MODULE_CONFLICT' ? 409 :
|
|
252
252
|
err.code === 'DEPS_NOT_SHIPPED' ? 409 :
|
|
253
|
+
// task 1002650 (idea 1000644): a ready task with an open blocker link is
|
|
254
|
+
// hidden from the claimable feed; the direct claim-by-id gate now
|
|
255
|
+
// enforces the same rule instead of letting the claim walk into the trap.
|
|
256
|
+
err.code === 'OPEN_BLOCKER' ? 409 :
|
|
253
257
|
// ADR 0120 part 3: the builder owes a rebase on an un-landed confirmed
|
|
254
258
|
// task (reconciler-flagged needs_rebase); NEW claims are gated until
|
|
255
259
|
// they land or release it. Existing claims are unaffected.
|
|
@@ -285,6 +289,12 @@ module.exports = function buildClaimsRouter() {
|
|
|
285
289
|
const ids = (err.rebaseTasks || []).map((t) => `task ${t.id}`);
|
|
286
290
|
err.hint = `Your queue is gated: ${ids.length} confirmed task(s) await your rebase (${ids.join(', ') || 'unknown'}). Land each (rebase its branch + re-ship, or /merge-mode) — or /builder-release it — then claim new work.`;
|
|
287
291
|
}
|
|
292
|
+
// task 1002650: name the blocker(s) and the way forward — the same rule
|
|
293
|
+
// the claimable feed enforces, spoken at the gate instead of a bare 409.
|
|
294
|
+
if (err.code === 'OPEN_BLOCKER') {
|
|
295
|
+
const names = (err.blockers || []).map((b) => `blocker ${b.id} ("${b.title}")`);
|
|
296
|
+
err.hint = `This task is waiting on ${names.join(', ') || 'an open blocker'}. Resolve it first (/blocker-solve N drives one to done; resolving auto-promotes its linked tasks) — or pick another task from /builder-start.`;
|
|
297
|
+
}
|
|
288
298
|
return res.fail(err.code, status, { ...err });
|
|
289
299
|
}
|
|
290
300
|
console.error('[gds] POST /claims', err);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.38",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.38",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.38",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/claim.js
CHANGED
|
@@ -256,6 +256,13 @@ function formatClaimFailure(r, taskId) {
|
|
|
256
256
|
lines.push('Ship those first (or release them) and try again.');
|
|
257
257
|
return { lines, exit: 1 };
|
|
258
258
|
}
|
|
259
|
+
if (code === 'OPEN_BLOCKER') {
|
|
260
|
+
// task 1002650: the same rule the claimable feed enforces, spoken at the gate.
|
|
261
|
+
lines.push(`Task #${taskId} is waiting on an open blocker:`);
|
|
262
|
+
for (const b of d.blockers || []) lines.push(` - blocker ${b.id} ${b.title}`);
|
|
263
|
+
lines.push('Resolve it first (/blocker-solve N drives one to done; resolving auto-promotes its linked tasks) — or pick another task from /builder-start.');
|
|
264
|
+
return { lines, exit: 1 };
|
|
265
|
+
}
|
|
259
266
|
// Fallback — still readable: the message + code, never a raw [object Object].
|
|
260
267
|
lines.push(`Claim failed (${r ? r.status : '?'})${code ? ` [${code}]` : ''}${message ? `: ${message}` : ''}.`);
|
|
261
268
|
return { lines, exit: 1 };
|
|
@@ -73,7 +73,7 @@ const INTEGRATION = new Set([
|
|
|
73
73
|
// platform-server — network + package build + a spawned server, run by its own
|
|
74
74
|
// .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
|
|
75
75
|
'consumer_layout_boot',
|
|
76
|
-
'claim_gate_rebase', 'clawback_confirmed_backlog', 'discord_autojoin', 'discord_oauth',
|
|
76
|
+
'claim_gate_open_blocker', 'claim_gate_rebase', 'clawback_confirmed_backlog', 'discord_autojoin', 'discord_oauth',
|
|
77
77
|
'governance_fold_bench', 'governance_live_proof', 'linkify_refs', 'memory_isolation',
|
|
78
78
|
'memory_pull', 'needs_rebase', 'parallel-load', 'reward_mode_cost_plus_only',
|
|
79
79
|
'session_records', 'setup_smoke',
|
package/src/module-api.js
CHANGED
|
@@ -499,8 +499,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
499
499
|
// landed since 1.19.34 with no explicit bump. run 31507293725. (task 1002620)
|
|
500
500
|
// 1.19.36 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
501
501
|
// landed since 1.19.35 with no explicit bump. run 31509715311. (task 1002620)
|
|
502
|
+
// 1.19.37 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
503
|
+
// landed since 1.19.36 with no explicit bump. run 31510590459. (task 1002620)
|
|
504
|
+
// 1.19.38 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
505
|
+
// landed since 1.19.37 with no explicit bump. run 31511537558. (task 1002620)
|
|
502
506
|
// ---------------------------------------------------------------------------
|
|
503
|
-
const CORE_VERSION = '1.19.
|
|
507
|
+
const CORE_VERSION = '1.19.38'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
504
508
|
|
|
505
509
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
506
510
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -36,6 +36,7 @@ t('never dumps [object Object] for any known code', () => {
|
|
|
36
36
|
'session_already_has_bound_claim',
|
|
37
37
|
'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
|
|
38
38
|
'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
|
|
39
|
+
'OPEN_BLOCKER',
|
|
39
40
|
'some_unmapped_future_code',
|
|
40
41
|
];
|
|
41
42
|
for (const c of codes) {
|
|
@@ -87,6 +88,17 @@ t('DEPS_NOT_SHIPPED: lists each blocking dependency', () => {
|
|
|
87
88
|
assert.match(out, /Dep A/);
|
|
88
89
|
});
|
|
89
90
|
|
|
91
|
+
// task 1002650: the wave-5 speech standard — the refusal names the blocker
|
|
92
|
+
// (id + title) and the remedy, not a bare 409.
|
|
93
|
+
t('OPEN_BLOCKER: names the blocker and the way forward', () => {
|
|
94
|
+
const out = text(formatClaimFailure(
|
|
95
|
+
envelope('OPEN_BLOCKER', { blockers: [{ id: 1000094, title: 'Owner must verify the installer' }] }), 9));
|
|
96
|
+
assert.match(out, /blocker 1000094/);
|
|
97
|
+
assert.match(out, /Owner must verify the installer/);
|
|
98
|
+
assert.match(out, /blocker-solve/);
|
|
99
|
+
assert.match(out, /pick another task/);
|
|
100
|
+
});
|
|
101
|
+
|
|
90
102
|
t('MODULE_CONFLICT + TOUCHES_CONFLICT: list conflicting task titles', () => {
|
|
91
103
|
const mc = text(formatClaimFailure(
|
|
92
104
|
envelope('MODULE_CONFLICT', { module_key: 'economy', conflictTaskTitles: ['Bar'] }), 4));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// tests/claim_gate_open_blocker.mjs — task 1002650 (idea 1000644, real DB).
|
|
2
|
+
//
|
|
3
|
+
// The open-blocker claim gate: a ready task linked to an OPEN blocker is
|
|
4
|
+
// hidden from the claimable feed (CLAIMABLE_SQL), but the direct claim-by-id
|
|
5
|
+
// path never checked — POST /claims walked straight into the trap the feed was
|
|
6
|
+
// hiding. claimTask and claimTasksBatch now refuse with OPEN_BLOCKER, naming
|
|
7
|
+
// the blocker, mirroring migration 176's task_is_fully_unblocked clause.
|
|
8
|
+
// Real-DB integration test (self-skips without a reachable Postgres; in the
|
|
9
|
+
// INTEGRATION set so the default DB-free lane never runs it).
|
|
10
|
+
//
|
|
11
|
+
// Run: DATABASE_URL=... node tests/claim_gate_open_blocker.mjs
|
|
12
|
+
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { Pool } = require('pg');
|
|
17
|
+
|
|
18
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
19
|
+
try {
|
|
20
|
+
await pool.query('SELECT 1');
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.log(`claim_gate_open_blocker.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
23
|
+
await pool.end().catch(() => {});
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const seams = require('../src/module-seams.js');
|
|
28
|
+
if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
|
|
29
|
+
const db = require('../modules/lifecycle/db.js');
|
|
30
|
+
|
|
31
|
+
const VER = 'ZZZ-OB', WORKER = 90000301, TRAP = 90000302, CONTROL = 90000303, BLOCKER = 90000304;
|
|
32
|
+
const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
|
|
33
|
+
|
|
34
|
+
async function cleanup() {
|
|
35
|
+
await q(`DELETE FROM builder_achievements WHERE builder_id=$1`, [WORKER]).catch(() => {});
|
|
36
|
+
await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id = ANY($2::bigint[])`, [WORKER, [TRAP, CONTROL]]).catch(() => {});
|
|
37
|
+
await q(`DELETE FROM claims WHERE task_id = ANY($1::bigint[])`, [[TRAP, CONTROL]]).catch(() => {});
|
|
38
|
+
await q(`DELETE FROM task_blockers WHERE task_id = ANY($1::bigint[])`, [[TRAP, CONTROL]]).catch(() => {});
|
|
39
|
+
await q(`DELETE FROM blockers WHERE id=$1`, [BLOCKER]).catch(() => {});
|
|
40
|
+
await q(`DELETE FROM tasks WHERE id = ANY($1::bigint[])`, [[TRAP, CONTROL]]).catch(() => {});
|
|
41
|
+
await q(`DELETE FROM builders WHERE id=$1`, [WORKER]).catch(() => {});
|
|
42
|
+
await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let pass = 0;
|
|
46
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await cleanup();
|
|
50
|
+
await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'ob','planning','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
|
|
51
|
+
// Archon so no rank/newcomer gate interferes; the gate under test is OPEN_BLOCKER.
|
|
52
|
+
await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'ob-w','ob-worker','OB Worker','archon')`, [WORKER]);
|
|
53
|
+
// TRAP: ready (feed-hidden) with an open blocker linked. CONTROL: ready, clean.
|
|
54
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind) VALUES ($1,$2,'trap','ready',10,'feature')`, [TRAP, VER]);
|
|
55
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind) VALUES ($1,$2,'control','ready',10,'feature')`, [CONTROL, VER]);
|
|
56
|
+
await q(`INSERT INTO blockers (id,title,status) VALUES ($1,'Owner must flip the switch','open')`, [BLOCKER]);
|
|
57
|
+
await q(`INSERT INTO task_blockers (task_id,blocker_id) VALUES ($1,$2)`, [TRAP, BLOCKER]);
|
|
58
|
+
|
|
59
|
+
// 1) Direct claim-by-id is refused with OPEN_BLOCKER, naming the blocker.
|
|
60
|
+
let threw = null;
|
|
61
|
+
try {
|
|
62
|
+
await db.claimTask({ taskId: String(TRAP), builderId: String(WORKER), worktreeName: 'ob-trap' });
|
|
63
|
+
} catch (e) { threw = e; }
|
|
64
|
+
ok(threw && threw.code === 'OPEN_BLOCKER', `claim refused with OPEN_BLOCKER (got ${threw && (threw.code || threw.message)})`);
|
|
65
|
+
ok(Array.isArray(threw.blockers) && threw.blockers.some((b) => Number(b.id) === BLOCKER),
|
|
66
|
+
'the 409 names the blocker id');
|
|
67
|
+
ok(threw.blockers.every((b) => typeof b.title === 'string' && b.title.length > 0),
|
|
68
|
+
'each named blocker carries its title (the wave-5 speech payload)');
|
|
69
|
+
const trapStatus = (await q(`SELECT status FROM tasks WHERE id=$1`, [TRAP]))[0].status;
|
|
70
|
+
ok(trapStatus === 'ready', 'the refused task stays ready (no partial claim)');
|
|
71
|
+
|
|
72
|
+
// 2) The batch path enforces the same rule — no sidestep via /builder-sequence.
|
|
73
|
+
let batchThrew = null;
|
|
74
|
+
try {
|
|
75
|
+
await db.claimTasksBatch({ taskIds: [TRAP, CONTROL], builderId: String(WORKER), worktreePrefix: 'ob-batch' });
|
|
76
|
+
} catch (e) { batchThrew = e; }
|
|
77
|
+
ok(batchThrew && batchThrew.code === 'BATCH_FAILED', `batch claim refused (got ${batchThrew && (batchThrew.code || batchThrew.message)})`);
|
|
78
|
+
ok((batchThrew.failures || []).some((f) => Number(f.task_id) === TRAP && f.reason === 'OPEN_BLOCKER'),
|
|
79
|
+
'the batch failure names the trapped task with reason OPEN_BLOCKER');
|
|
80
|
+
|
|
81
|
+
// 3) Resolving the blocker opens the gate.
|
|
82
|
+
await q(`UPDATE blockers SET status='resolved', resolved_at=now() WHERE id=$1`, [BLOCKER]);
|
|
83
|
+
const claim = await db.claimTask({ taskId: String(TRAP), builderId: String(WORKER), worktreeName: 'ob-trap' });
|
|
84
|
+
ok(claim && claim.id && Number(claim.task_id) === TRAP, 'claim succeeds once the blocker is resolved');
|
|
85
|
+
|
|
86
|
+
console.log(`\nclaim_gate_open_blocker (task 1002650): ${pass} passed, 0 failed`);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error('claim_gate_open_blocker.mjs FAILED:', e.stack || e.message);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
} finally {
|
|
91
|
+
await cleanup();
|
|
92
|
+
await pool.end();
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// tests/skill_grader_health.mjs — the /grader-health skill exists and covers
|
|
2
|
+
// the four health axes from the grader-hardening evaluation §6 (task 1002656):
|
|
3
|
+
// outage runs, pass-rate drift, cost anomalies, and stale model pins — reading
|
|
4
|
+
// LIVE data surfaces, emitting exact --regrade remediation lines, and staying
|
|
5
|
+
// read-only (describe, never confirm/flip — ADR 0162).
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const SKILL = path.join(ROOT, '.claude', 'skills', 'grader-health', 'SKILL.md');
|
|
14
|
+
|
|
15
|
+
// .claude/skills is INSTANCE-materialized; a neutral core checkout carries none
|
|
16
|
+
// (task 1002470) — skip there, same guard shape as file_map.mjs.
|
|
17
|
+
const skillsDirExists = existsSync(path.join(ROOT, '.claude', 'skills'));
|
|
18
|
+
|
|
19
|
+
test('/grader-health exists and covers the four health axes on live data', (t) => {
|
|
20
|
+
if (!skillsDirExists) return t.skip('.claude/skills not materialized in this checkout');
|
|
21
|
+
assert.ok(existsSync(SKILL), '.claude/skills/grader-health/SKILL.md must exist (task 1002656)');
|
|
22
|
+
const md = readFileSync(SKILL, 'utf8');
|
|
23
|
+
|
|
24
|
+
// Live data surfaces, named concretely.
|
|
25
|
+
assert.match(md, /\/api\/gds\/public\/grades/, 'reads the public grade trend');
|
|
26
|
+
assert.match(md, /\/api\/gds\/grades\/by-builder/, 'reads the per-builder split (Archon)');
|
|
27
|
+
assert.match(md, /\/api\/gds\/public\/cost-summary/, 'reads grader spend');
|
|
28
|
+
|
|
29
|
+
// Axis 1 — outage runs: the 21×0.0 pattern and outage-vs-rejection confirmation.
|
|
30
|
+
assert.match(md, /n_unavailable/, 'names the outage bucket the aggregates expose');
|
|
31
|
+
assert.match(md, /panel_outcome='unavailable'/, 'confirms outage vs rejection per task');
|
|
32
|
+
|
|
33
|
+
// Axis 2 — pass-rate drift vs the evaluation baseline.
|
|
34
|
+
assert.match(md, /90\.6%/, 'names the eval-window pass-rate baseline');
|
|
35
|
+
|
|
36
|
+
// Axis 3 — cost anomalies vs the fixed-cost benchmark.
|
|
37
|
+
assert.match(md, /\$2\.14/, 'names the mean per-grade cost benchmark');
|
|
38
|
+
|
|
39
|
+
// Axis 4 — model pins.
|
|
40
|
+
assert.match(md, /DEFAULT_GRADER_MODEL|selectGraderModel/, 'checks the live model pin');
|
|
41
|
+
|
|
42
|
+
// Remediation + probe + the read-only contract.
|
|
43
|
+
assert.match(md, /node scripts\/gds\/ship\.js <(?:task-)?id> --regrade/, 'emits the exact remediation line');
|
|
44
|
+
assert.match(md, /--probe/, 'offers the optional live calibration probe');
|
|
45
|
+
assert.match(md, /grade-smoke\.js/, 'names the probe script');
|
|
46
|
+
assert.match(md, /read-only/i, 'states the read-only contract');
|
|
47
|
+
assert.match(md, /ADR 0162/, 'anchors the no-new-gates constraint');
|
|
48
|
+
});
|