@bongos/core 1.19.37 → 1.19.39
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 +26 -16
- package/.claude/skills/grader-health/SKILL.md +61 -0
- package/docs/file-map.md +1 -0
- package/modules/lifecycle/github-push.js +35 -0
- package/modules/lifecycle/publish-reconciler.js +41 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/github_push_merge_driver.mjs +58 -0
- package/tests/publish_reconciler.mjs +84 -1
- 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.39",
|
|
6
|
+
"core_contract": "1.19.39",
|
|
7
|
+
"source_commit": "d61c48b44f6f117383794f20d49b5bd7b83d35d7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-11T16:
|
|
9
|
+
"built_at": "2026-08-11T16:25:51.076Z",
|
|
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": "14c889242599811c768effd9bc7ef844e4a6ee4ef0a041c74ff1629fda6f8907",
|
|
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",
|
|
@@ -3377,7 +3382,7 @@
|
|
|
3377
3382
|
{
|
|
3378
3383
|
"path": "modules/lifecycle/github-push.js",
|
|
3379
3384
|
"mode": "0000644",
|
|
3380
|
-
"sha256": "
|
|
3385
|
+
"sha256": "9658ccf0dd332d6c4ce1e269c2966ebe2e2bed5f909d5d503722d8e8cfb1ebbc"
|
|
3381
3386
|
},
|
|
3382
3387
|
{
|
|
3383
3388
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -3432,7 +3437,7 @@
|
|
|
3432
3437
|
{
|
|
3433
3438
|
"path": "modules/lifecycle/publish-reconciler.js",
|
|
3434
3439
|
"mode": "0000644",
|
|
3435
|
-
"sha256": "
|
|
3440
|
+
"sha256": "ad57ad242956c6ee3dc4aeddd66e980af48737fab644765aa3c400720f4a9668"
|
|
3436
3441
|
},
|
|
3437
3442
|
{
|
|
3438
3443
|
"path": "modules/lifecycle/routes/analytics.js",
|
|
@@ -4017,12 +4022,12 @@
|
|
|
4017
4022
|
{
|
|
4018
4023
|
"path": "package-lock.json",
|
|
4019
4024
|
"mode": "0000644",
|
|
4020
|
-
"sha256": "
|
|
4025
|
+
"sha256": "12291ed6b1927973c8d8c42c5384d7ba2368779b9a39c566336253069214cfb2"
|
|
4021
4026
|
},
|
|
4022
4027
|
{
|
|
4023
4028
|
"path": "package.json",
|
|
4024
4029
|
"mode": "0000644",
|
|
4025
|
-
"sha256": "
|
|
4030
|
+
"sha256": "18139931183bcb89c64704ae5167ab99d1e8ae9841728870b277959af8523069"
|
|
4026
4031
|
},
|
|
4027
4032
|
{
|
|
4028
4033
|
"path": "public-docs/index.html",
|
|
@@ -5642,7 +5647,7 @@
|
|
|
5642
5647
|
{
|
|
5643
5648
|
"path": "src/module-api.js",
|
|
5644
5649
|
"mode": "0000644",
|
|
5645
|
-
"sha256": "
|
|
5650
|
+
"sha256": "5f51d5f403c88eedf838dab85e4a9ddfa8ff575ec07b5f1189e12999001a335a"
|
|
5646
5651
|
},
|
|
5647
5652
|
{
|
|
5648
5653
|
"path": "src/module-loader/loader.js",
|
|
@@ -6412,7 +6417,7 @@
|
|
|
6412
6417
|
{
|
|
6413
6418
|
"path": "tests/github_push_merge_driver.mjs",
|
|
6414
6419
|
"mode": "0000644",
|
|
6415
|
-
"sha256": "
|
|
6420
|
+
"sha256": "850113ed91babc9eb54dc22231f194d283fd3f94cee9deef1e6e0bf6f3a969c2"
|
|
6416
6421
|
},
|
|
6417
6422
|
{
|
|
6418
6423
|
"path": "tests/github_repos_helpers.mjs",
|
|
@@ -7142,7 +7147,7 @@
|
|
|
7142
7147
|
{
|
|
7143
7148
|
"path": "tests/publish_reconciler.mjs",
|
|
7144
7149
|
"mode": "0000644",
|
|
7145
|
-
"sha256": "
|
|
7150
|
+
"sha256": "1aeb87a14e411160784c94ee5392f323a2ad0a373bd9acf91337289041e8d2bc"
|
|
7146
7151
|
},
|
|
7147
7152
|
{
|
|
7148
7153
|
"path": "tests/rank_gate.mjs",
|
|
@@ -7464,6 +7469,11 @@
|
|
|
7464
7469
|
"mode": "0000644",
|
|
7465
7470
|
"sha256": "76bed8e6fae3bd77d4b45be14d9f80bda71b2f92323fa5697a7e4f07b65d3ba2"
|
|
7466
7471
|
},
|
|
7472
|
+
{
|
|
7473
|
+
"path": "tests/skill_grader_health.mjs",
|
|
7474
|
+
"mode": "0000644",
|
|
7475
|
+
"sha256": "4557c934704a009612ed8858c0f59c5b53facb5d92fa2c0740b1aecb079aa5c9"
|
|
7476
|
+
},
|
|
7467
7477
|
{
|
|
7468
7478
|
"path": "tests/skill_prefs.mjs",
|
|
7469
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)
|
|
@@ -675,6 +675,40 @@ async function mergeIfGreen({ branch }, deps = {}) {
|
|
|
675
675
|
return { ...base, reason: `merge_api_${res.status}`, detail: detail.slice(0, 160) };
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
// task 1002666 — the carrier race. Publish-on-merge (ADR 0161) pushes a carrier
|
|
679
|
+
// commit to main after EVERY land, so under an up-to-date branch-protection rule
|
|
680
|
+
// a PR whose checks report after the previous merge's carrier goes BEHIND:
|
|
681
|
+
// green, mergeable, and permanently stalled (GitHub's update-branch also disarms
|
|
682
|
+
// auto-merge, so re-arming alone cannot recover it — observed on PR #48,
|
|
683
|
+
// 2026-08-11). Server-side remedy: update the branch; checks re-run on the fresh
|
|
684
|
+
// head and the next sweep's mergeIfGreen lands it. Fires ONLY on the definitive
|
|
685
|
+
// BEHIND shape — GitHub reports the strict-up-to-date green-PR stall as BEHIND
|
|
686
|
+
// once checks have reported; BLOCKED means checks are pending/missing and stays
|
|
687
|
+
// untouched (updating there would loop on PRs that can never go green).
|
|
688
|
+
// The state is re-read here even though the sweep's mergeIfGreen just read it:
|
|
689
|
+
// update-branch is a WRITE, and the re-read guards against acting on a state
|
|
690
|
+
// that changed between the two calls (e.g. the PR merged meanwhile) — the same
|
|
691
|
+
// read-before-write discipline mergeIfGreen itself applies.
|
|
692
|
+
async function updateBranchIfBehind({ branch }, deps = {}) {
|
|
693
|
+
const s = await getMergeState({ branch }, deps);
|
|
694
|
+
const base = { number: s.number, updated_now: false, merge_state: s.merge_state, reason: null };
|
|
695
|
+
if (s.merged) return { ...base, reason: 'already_merged' };
|
|
696
|
+
if (s.pr_state !== 'open') return { ...base, reason: 'no_open_pr' };
|
|
697
|
+
if (s.merge_state !== 'BEHIND') return { ...base, reason: mergeBlockReason(s) };
|
|
698
|
+
const token = await resolveToken(deps);
|
|
699
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
700
|
+
const { owner, repo } = repoInfo;
|
|
701
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
702
|
+
const res = await doFetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${s.number}/update-branch`, {
|
|
703
|
+
method: 'PUT',
|
|
704
|
+
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
705
|
+
body: JSON.stringify(s.head_sha ? { expected_head_sha: s.head_sha } : {}),
|
|
706
|
+
});
|
|
707
|
+
if (res.ok) return { ...base, updated_now: true };
|
|
708
|
+
const detail = await res.text().catch(() => '');
|
|
709
|
+
return { ...base, reason: `update_api_${res.status}`, detail: detail.slice(0, 160) };
|
|
710
|
+
}
|
|
711
|
+
|
|
678
712
|
// task 1139 (ADR 0058): server-set commit status — the TRUSTLESS rank attestation the
|
|
679
713
|
// gate-review CI check reads. Only the server (holding the App/PAT credential AND the
|
|
680
714
|
// authenticated builder's server-resolved rank) can set it, so CI can trust it where it
|
|
@@ -1043,6 +1077,7 @@ module.exports = {
|
|
|
1043
1077
|
getPublishStatus,
|
|
1044
1078
|
getMergeState,
|
|
1045
1079
|
mergeIfGreen,
|
|
1080
|
+
updateBranchIfBehind,
|
|
1046
1081
|
setCommitStatus,
|
|
1047
1082
|
setGateOwnerApproved,
|
|
1048
1083
|
listOpenPullRequests,
|
|
@@ -200,6 +200,7 @@ async function runSweep(deps = {}) {
|
|
|
200
200
|
let merged = 0;
|
|
201
201
|
let resolvedConflicts = 0;
|
|
202
202
|
let refreshedStale = 0; // task 1915: PRs whose stale generated indexes we regen+pushed
|
|
203
|
+
let behindUpdated = 0; // task 1002666: green PRs behind main (carrier race) whose branch we updated
|
|
203
204
|
let errors = 0;
|
|
204
205
|
let unprovable = 0; // confirmed tasks the sweep could not prove landed this pass
|
|
205
206
|
let staleStrands = 0; // of those, the ones older than STALE_CONFIRMED_MS (WARN'd)
|
|
@@ -305,6 +306,38 @@ async function runSweep(deps = {}) {
|
|
|
305
306
|
} catch (e) {
|
|
306
307
|
errFn(`[publish-reconciler] conflict-resolve for task ${r.task_id} (non-blocking): ${e && e.message}`);
|
|
307
308
|
}
|
|
309
|
+
} else if (
|
|
310
|
+
m && !m.merged && m.reason === 'branch_behind_base' &&
|
|
311
|
+
typeof push.updateBranchIfBehind === 'function'
|
|
312
|
+
) {
|
|
313
|
+
// task 1002666: the ADR 0161 carrier race — every land pushes a
|
|
314
|
+
// carrier commit to main, so a green PR whose checks reported after
|
|
315
|
+
// the previous merge sits BEHIND forever (GitHub's update-branch
|
|
316
|
+
// disarms auto-merge, so it cannot recover on its own). Update the
|
|
317
|
+
// branch server-side; checks re-run on the fresh head and the NEXT
|
|
318
|
+
// sweep's mergeIfGreen lands it. Attempt-capped per branch on the
|
|
319
|
+
// shared heal map — on a hot fleet every update re-opens the race
|
|
320
|
+
// window, and a treadmill must surface, not spin.
|
|
321
|
+
const bKey = `behind:${branch}`;
|
|
322
|
+
const bn = attempts.get(bKey) || 0;
|
|
323
|
+
if (bn >= MAX_STALE_HEAL_ATTEMPTS) {
|
|
324
|
+
logFn(
|
|
325
|
+
`[publish-reconciler] task ${r.task_id} (${branch}) PR #${m.number} still behind main after ${bn} update-branch attempt(s) — capped (merge traffic is outracing this PR); left for a human`
|
|
326
|
+
);
|
|
327
|
+
} else {
|
|
328
|
+
attempts.set(bKey, bn + 1);
|
|
329
|
+
try {
|
|
330
|
+
const u = await push.updateBranchIfBehind({ branch });
|
|
331
|
+
if (u && u.updated_now) {
|
|
332
|
+
behindUpdated++;
|
|
333
|
+
logFn(
|
|
334
|
+
`[publish-reconciler] task ${r.task_id} (${branch}) PR #${u.number} was green but behind main (carrier race) — server-updated the branch; checks re-run, lands next sweep`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
} catch (e) {
|
|
338
|
+
errFn(`[publish-reconciler] update-branch for task ${r.task_id} (non-blocking): ${e && e.message}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
308
341
|
} else if (
|
|
309
342
|
m && couldBeStaleGenerated(m) &&
|
|
310
343
|
typeof resolver.refreshStaleGenerated === 'function'
|
|
@@ -544,14 +577,17 @@ async function runSweep(deps = {}) {
|
|
|
544
577
|
}
|
|
545
578
|
|
|
546
579
|
// Prune the treadmill guard so it can't leak: a branch that left the confirmed set
|
|
547
|
-
// (landed, or released) never returns under the same name, so drop it.
|
|
580
|
+
// (landed, or released) never returns under the same name, so drop it. Keys may be
|
|
581
|
+
// namespaced ('behind:<branch>', task 1002666) — compare on the branch itself so a
|
|
582
|
+
// live branch's cap survives across sweeps and a departed branch's keys still prune.
|
|
548
583
|
for (const b of attempts.keys()) {
|
|
549
|
-
|
|
584
|
+
const plain = b.startsWith('behind:') ? b.slice('behind:'.length) : b;
|
|
585
|
+
if (!branchesSeen.has(plain)) attempts.delete(b);
|
|
550
586
|
}
|
|
551
587
|
|
|
552
|
-
if (reconciled || merged || resolvedConflicts || refreshedStale) {
|
|
588
|
+
if (reconciled || merged || resolvedConflicts || refreshedStale || behindUpdated) {
|
|
553
589
|
logFn(
|
|
554
|
-
`[publish-reconciler] swept ${rows.length} confirmed task(s); merged ${merged} stalled PR(s); auto-resolved ${resolvedConflicts} generated/index conflict(s); regen-healed ${refreshedStale} stale-generated PR(s); reconciled ${reconciled} → shipped`
|
|
590
|
+
`[publish-reconciler] swept ${rows.length} confirmed task(s); merged ${merged} stalled PR(s); auto-resolved ${resolvedConflicts} generated/index conflict(s); regen-healed ${refreshedStale} stale-generated PR(s); branch-updated ${behindUpdated} behind-main PR(s); reconciled ${reconciled} → shipped`
|
|
555
591
|
);
|
|
556
592
|
}
|
|
557
593
|
if (staleStrands) {
|
|
@@ -559,7 +595,7 @@ async function runSweep(deps = {}) {
|
|
|
559
595
|
`[publish-reconciler] ${staleStrands} stale strand(s): confirmed >${Math.round(STALE_CONFIRMED_MS / 60_000)}min with no provable land — credited but unlanded; see STRAND lines above`
|
|
560
596
|
);
|
|
561
597
|
}
|
|
562
|
-
return { checked: rows.length, reconciled, merged, resolvedConflicts, refreshedStale, errors, unprovable, staleStrands, strandsFlagged };
|
|
598
|
+
return { checked: rows.length, reconciled, merged, resolvedConflicts, refreshedStale, behindUpdated, errors, unprovable, staleStrands, strandsFlagged };
|
|
563
599
|
}
|
|
564
600
|
|
|
565
601
|
// task 1366 — server-side session-cost reconcile, piggybacked on the same 5-min
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.39",
|
|
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.39",
|
|
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.39",
|
|
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
|
@@ -501,8 +501,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
501
501
|
// landed since 1.19.35 with no explicit bump. run 31509715311. (task 1002620)
|
|
502
502
|
// 1.19.37 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
503
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)
|
|
506
|
+
// 1.19.39 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
507
|
+
// landed since 1.19.38 with no explicit bump. run 31512284325. (task 1002620)
|
|
504
508
|
// ---------------------------------------------------------------------------
|
|
505
|
-
const CORE_VERSION = '1.19.
|
|
509
|
+
const CORE_VERSION = '1.19.39'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
506
510
|
|
|
507
511
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
508
512
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -169,6 +169,64 @@ test('mergeBlockReason maps the common blocker states', () => {
|
|
|
169
169
|
assert.equal(gh.mergeBlockReason({ ...open, merged: true }), null);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
// --- task 1002666: updateBranchIfBehind (the carrier-race branch updater) ---
|
|
173
|
+
|
|
174
|
+
// fetchImpl serving graphql + recording the update-branch PUT.
|
|
175
|
+
function mkUpdateFetch({ node, updateStatus = 202 } = {}) {
|
|
176
|
+
const calls = { graphql: 0, update: 0, updateArgs: null };
|
|
177
|
+
const fetchImpl = async (url, opts = {}) => {
|
|
178
|
+
if (String(url).includes('/graphql')) {
|
|
179
|
+
calls.graphql++;
|
|
180
|
+
return {
|
|
181
|
+
ok: true, status: 200,
|
|
182
|
+
json: async () => ({ data: { repository: { pullRequests: { nodes: node ? [node] : [] } } } }),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (String(url).includes('/update-branch')) {
|
|
186
|
+
calls.update++;
|
|
187
|
+
calls.updateArgs = { url, body: JSON.parse(opts.body || '{}') };
|
|
188
|
+
return {
|
|
189
|
+
ok: updateStatus >= 200 && updateStatus < 300,
|
|
190
|
+
status: updateStatus,
|
|
191
|
+
text: async () => '{}',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
throw new Error(`unexpected fetch: ${url}`);
|
|
195
|
+
};
|
|
196
|
+
return { fetchImpl, calls };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
test('updateBranchIfBehind updates a BEHIND PR, pinning the expected head sha', async () => {
|
|
200
|
+
const { fetchImpl, calls } = mkUpdateFetch({ node: prNode({ mergeStateStatus: 'BEHIND', oid: 'headsha' }) });
|
|
201
|
+
const u = await gh.updateBranchIfBehind({ branch: 'claude/wt' }, DEPS(fetchImpl));
|
|
202
|
+
assert.equal(u.updated_now, true);
|
|
203
|
+
assert.equal(calls.update, 1);
|
|
204
|
+
assert.match(String(calls.updateArgs.url), /\/pulls\/125\/update-branch$/);
|
|
205
|
+
assert.equal(calls.updateArgs.body.expected_head_sha, 'headsha', 'a raced head 422s instead of updating blind');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('updateBranchIfBehind refuses every non-BEHIND shape without calling the API', async () => {
|
|
209
|
+
for (const [node, reason] of [
|
|
210
|
+
[prNode({ mergeStateStatus: 'CLEAN' }), 'clean'],
|
|
211
|
+
[prNode({ merged: true }), 'already_merged'],
|
|
212
|
+
[prNode({ state: 'CLOSED' }), 'no_open_pr'],
|
|
213
|
+
[prNode({ mergeStateStatus: 'BLOCKED' }), 'blocked_required_review_or_pending_check'],
|
|
214
|
+
]) {
|
|
215
|
+
const { fetchImpl, calls } = mkUpdateFetch({ node });
|
|
216
|
+
const u = await gh.updateBranchIfBehind({ branch: 'claude/wt' }, DEPS(fetchImpl));
|
|
217
|
+
assert.equal(u.updated_now, false);
|
|
218
|
+
assert.equal(calls.update, 0, 'no update PUT for a non-BEHIND PR');
|
|
219
|
+
if (reason) assert.equal(u.reason, reason);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('updateBranchIfBehind surfaces an API refusal as update_api_<status>', async () => {
|
|
224
|
+
const { fetchImpl } = mkUpdateFetch({ node: prNode({ mergeStateStatus: 'BEHIND' }), updateStatus: 403 });
|
|
225
|
+
const u = await gh.updateBranchIfBehind({ branch: 'claude/wt' }, DEPS(fetchImpl));
|
|
226
|
+
assert.equal(u.updated_now, false);
|
|
227
|
+
assert.equal(u.reason, 'update_api_403');
|
|
228
|
+
});
|
|
229
|
+
|
|
172
230
|
setTimeout(() => {
|
|
173
231
|
console.log(`\ngithub-push merge driver (task 1162) tests: ${passed} passed, ${failed} failed`);
|
|
174
232
|
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -57,9 +57,10 @@ function mkDeps({
|
|
|
57
57
|
broadcastImpl,
|
|
58
58
|
conflictResolveImpl,
|
|
59
59
|
refreshStaleImpl,
|
|
60
|
+
updateByBranch = {},
|
|
60
61
|
healAttempts = new Map(),
|
|
61
62
|
} = {}) {
|
|
62
|
-
const calls = { ship: [], broadcast: [], status: [], merge: [], ancestry: [], search: [], resolve: [], refresh: [], flag: [], events: [], logs: [], errors: [] };
|
|
63
|
+
const calls = { ship: [], broadcast: [], status: [], merge: [], ancestry: [], search: [], resolve: [], refresh: [], update: [], flag: [], events: [], logs: [], errors: [] };
|
|
63
64
|
// isSafeBranch mirrors github-push's pattern enough for the test (rejects
|
|
64
65
|
// option-like / traversal refs); we use the REAL module so the contract can't drift.
|
|
65
66
|
const githubPush = require('../modules/lifecycle/github-push.js');
|
|
@@ -110,6 +111,15 @@ function mkDeps({
|
|
|
110
111
|
if (f instanceof Error) throw f;
|
|
111
112
|
return f || null;
|
|
112
113
|
},
|
|
114
|
+
// task 1002666: the carrier-race branch updater. Default: no open PR (a
|
|
115
|
+
// no-op), so pre-1002666 tests keep their exact behavior. Script per
|
|
116
|
+
// branch via updateByBranch; an Error value throws (swallowed, non-blocking).
|
|
117
|
+
updateBranchIfBehind: async ({ branch }) => {
|
|
118
|
+
calls.update.push(branch);
|
|
119
|
+
const u = updateByBranch[branch];
|
|
120
|
+
if (u instanceof Error) throw u;
|
|
121
|
+
return u || { updated_now: false, reason: 'no_open_pr' };
|
|
122
|
+
},
|
|
113
123
|
// task 1313 head_sha land-check. Default: not on main. An Error value throws
|
|
114
124
|
// (to prove a transient compare-API miss is swallowed, leaving it for next sweep).
|
|
115
125
|
isAncestorOfDefaultBranch: async ({ sha }) => {
|
|
@@ -790,6 +800,79 @@ test('1915 mergeable-but-red PR → regen-heal fires on the right branch; refres
|
|
|
790
800
|
assert.equal(deps._calls.ship.length, 0, 'the heal never ships (no phantom-ship — land still needs the tip proof)');
|
|
791
801
|
});
|
|
792
802
|
|
|
803
|
+
// --- task 1002666: the carrier race — green PR behind main gets its branch updated ---
|
|
804
|
+
|
|
805
|
+
// mergeIfGreen shape for a green, mergeable PR that fell behind main (the ADR 0161
|
|
806
|
+
// carrier landed after this PR's checks reported).
|
|
807
|
+
const behindGreen = (number = 48) => ({
|
|
808
|
+
pr_state: 'open', number, merged: false, merged_now: false, mergeable: 'MERGEABLE', reason: 'branch_behind_base',
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
test('1002666 green-but-behind PR (carrier race) → server updates the branch; NOT shipped this sweep', async () => {
|
|
812
|
+
const deps = mkDeps({
|
|
813
|
+
rows: [{ task_id: 500, builder_id: 5, worktree_name: 'wt-behind' }],
|
|
814
|
+
statusByBranch: { 'claude/wt-behind': PENDING },
|
|
815
|
+
mergeByBranch: { 'claude/wt-behind': behindGreen() },
|
|
816
|
+
updateByBranch: { 'claude/wt-behind': { number: 48, updated_now: true, merge_state: 'BEHIND', reason: null } },
|
|
817
|
+
});
|
|
818
|
+
const r = await reconciler.runSweep(deps);
|
|
819
|
+
assert.equal(r.behindUpdated, 1, 'the behind PR was branch-updated');
|
|
820
|
+
assert.deepEqual(deps._calls.update, ['claude/wt-behind'], 'the update is invoked on exactly this task\'s branch');
|
|
821
|
+
assert.equal(r.reconciled, 0, 'no ship this sweep — checks re-run on the new head, lands next sweep');
|
|
822
|
+
assert.equal(deps._calls.ship.length, 0, 'the update never ships (land still needs the tip proof)');
|
|
823
|
+
assert.ok(deps._calls.logs.some((l) => /carrier race/.test(l)), 'the log names the carrier race');
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test('1002666 update-branch attempts are capped on the shared heal map (a hot fleet must surface, not spin)', async () => {
|
|
827
|
+
const healAttempts = new Map([['behind:claude/wt-behind', 3]]);
|
|
828
|
+
const deps = mkDeps({
|
|
829
|
+
rows: [{ task_id: 501, builder_id: 5, worktree_name: 'wt-behind' }],
|
|
830
|
+
statusByBranch: { 'claude/wt-behind': PENDING },
|
|
831
|
+
mergeByBranch: { 'claude/wt-behind': behindGreen() },
|
|
832
|
+
updateByBranch: { 'claude/wt-behind': { number: 48, updated_now: true } },
|
|
833
|
+
healAttempts,
|
|
834
|
+
});
|
|
835
|
+
const r = await reconciler.runSweep(deps);
|
|
836
|
+
assert.equal(deps._calls.update.length, 0, 'at the cap the updater is not invoked');
|
|
837
|
+
assert.equal(r.behindUpdated, 0);
|
|
838
|
+
assert.ok(deps._calls.logs.some((l) => /capped/.test(l)), 'the cap is logged, not silent');
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
test('1002666 the behind-cap ACCUMULATES across sweeps for a live branch (the prune must not reset it)', async () => {
|
|
842
|
+
const healAttempts = new Map();
|
|
843
|
+
const mk = () => mkDeps({
|
|
844
|
+
rows: [{ task_id: 503, builder_id: 5, worktree_name: 'wt-behind' }],
|
|
845
|
+
statusByBranch: { 'claude/wt-behind': PENDING },
|
|
846
|
+
mergeByBranch: { 'claude/wt-behind': behindGreen() },
|
|
847
|
+
updateByBranch: { 'claude/wt-behind': { number: 48, updated_now: true } },
|
|
848
|
+
healAttempts,
|
|
849
|
+
});
|
|
850
|
+
for (let i = 1; i <= 3; i++) {
|
|
851
|
+
await reconciler.runSweep(mk());
|
|
852
|
+
assert.equal(healAttempts.get('behind:claude/wt-behind'), i, `attempt ${i} is remembered after the sweep's prune`);
|
|
853
|
+
}
|
|
854
|
+
const fourth = mk();
|
|
855
|
+
const r = await reconciler.runSweep(fourth);
|
|
856
|
+
assert.equal(fourth._calls.update.length, 0, 'the 4th sweep is capped — a permanently-behind PR surfaces instead of spinning');
|
|
857
|
+
assert.equal(r.behindUpdated, 0);
|
|
858
|
+
// A branch that LEAVES the confirmed set prunes its namespaced key too (no leak).
|
|
859
|
+
const empty = mkDeps({ rows: [], healAttempts });
|
|
860
|
+
await reconciler.runSweep(empty);
|
|
861
|
+
assert.equal(healAttempts.has('behind:claude/wt-behind'), false, 'departed branch prunes its behind: key');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test('1002666 an update-branch API failure is swallowed (non-blocking) and the sweep continues', async () => {
|
|
865
|
+
const deps = mkDeps({
|
|
866
|
+
rows: [{ task_id: 502, builder_id: 5, worktree_name: 'wt-behind' }],
|
|
867
|
+
statusByBranch: { 'claude/wt-behind': PENDING },
|
|
868
|
+
mergeByBranch: { 'claude/wt-behind': behindGreen() },
|
|
869
|
+
updateByBranch: { 'claude/wt-behind': new Error('update-branch 403') },
|
|
870
|
+
});
|
|
871
|
+
const r = await reconciler.runSweep(deps);
|
|
872
|
+
assert.equal(r.behindUpdated, 0);
|
|
873
|
+
assert.ok(deps._calls.errors.some((l) => /update-branch/.test(l)), 'the failure is logged to errFn');
|
|
874
|
+
});
|
|
875
|
+
|
|
793
876
|
test('1915 mergeable-but-red PR where regen yields NO diff → not healed, logged as a real check failure', async () => {
|
|
794
877
|
const deps = mkDeps({
|
|
795
878
|
rows: [{ task_id: 401, builder_id: 5, worktree_name: 'wt-realfail' }],
|
|
@@ -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
|
+
});
|