@bongos/core 1.19.38 → 1.19.40
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/grade-audit/SKILL.md +77 -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_grade_audit.mjs +50 -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.40",
|
|
6
|
+
"core_contract": "1.19.40",
|
|
7
|
+
"source_commit": "512e419a012e048b5e7d5904053cbc45df054a09",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-11T16:
|
|
9
|
+
"built_at": "2026-08-11T16:35:25.688Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 289,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1213,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1523,
|
|
20
|
+
"tree_sha256": "39b4dbb47e0100a520bddafb358c1c89d7a74bc6975261774d9dd12059d544dc",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -154,6 +154,11 @@
|
|
|
154
154
|
"mode": "0000644",
|
|
155
155
|
"sha256": "e789b9157c9817bac354eaeed8152b53f3013f677934b062bca1c8e07f1f2dc8"
|
|
156
156
|
},
|
|
157
|
+
{
|
|
158
|
+
"path": ".claude/skills/grade-audit/SKILL.md",
|
|
159
|
+
"mode": "0000644",
|
|
160
|
+
"sha256": "2b4d33396291f58f1c0d7c01d173b64d587b2a0a483b17328e57eb602a8df9fe"
|
|
161
|
+
},
|
|
157
162
|
{
|
|
158
163
|
"path": ".claude/skills/grade-recover/SKILL.md",
|
|
159
164
|
"mode": "0000644",
|
|
@@ -1362,7 +1367,7 @@
|
|
|
1362
1367
|
{
|
|
1363
1368
|
"path": "docs/file-map.md",
|
|
1364
1369
|
"mode": "0000644",
|
|
1365
|
-
"sha256": "
|
|
1370
|
+
"sha256": "15801d1c51e7e46ccf4e52dad5d3d34e4adc96b72eed60e3ef2c9ccccf94d1da"
|
|
1366
1371
|
},
|
|
1367
1372
|
{
|
|
1368
1373
|
"path": "docs/handoff-template.md",
|
|
@@ -3382,7 +3387,7 @@
|
|
|
3382
3387
|
{
|
|
3383
3388
|
"path": "modules/lifecycle/github-push.js",
|
|
3384
3389
|
"mode": "0000644",
|
|
3385
|
-
"sha256": "
|
|
3390
|
+
"sha256": "9658ccf0dd332d6c4ce1e269c2966ebe2e2bed5f909d5d503722d8e8cfb1ebbc"
|
|
3386
3391
|
},
|
|
3387
3392
|
{
|
|
3388
3393
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -3437,7 +3442,7 @@
|
|
|
3437
3442
|
{
|
|
3438
3443
|
"path": "modules/lifecycle/publish-reconciler.js",
|
|
3439
3444
|
"mode": "0000644",
|
|
3440
|
-
"sha256": "
|
|
3445
|
+
"sha256": "ad57ad242956c6ee3dc4aeddd66e980af48737fab644765aa3c400720f4a9668"
|
|
3441
3446
|
},
|
|
3442
3447
|
{
|
|
3443
3448
|
"path": "modules/lifecycle/routes/analytics.js",
|
|
@@ -4022,12 +4027,12 @@
|
|
|
4022
4027
|
{
|
|
4023
4028
|
"path": "package-lock.json",
|
|
4024
4029
|
"mode": "0000644",
|
|
4025
|
-
"sha256": "
|
|
4030
|
+
"sha256": "290c88cdeae475bf124eb3e74292108c669006b69414d258ae3d961b9e80f743"
|
|
4026
4031
|
},
|
|
4027
4032
|
{
|
|
4028
4033
|
"path": "package.json",
|
|
4029
4034
|
"mode": "0000644",
|
|
4030
|
-
"sha256": "
|
|
4035
|
+
"sha256": "316768e98e388920e8710c944cd4fbfd7c2bf947a742a3aa6698d66b35e838e6"
|
|
4031
4036
|
},
|
|
4032
4037
|
{
|
|
4033
4038
|
"path": "public-docs/index.html",
|
|
@@ -5647,7 +5652,7 @@
|
|
|
5647
5652
|
{
|
|
5648
5653
|
"path": "src/module-api.js",
|
|
5649
5654
|
"mode": "0000644",
|
|
5650
|
-
"sha256": "
|
|
5655
|
+
"sha256": "a122aca901bb83b94b1d475a651eae90c7a7f72251703ce3a81cd14dcf39fe66"
|
|
5651
5656
|
},
|
|
5652
5657
|
{
|
|
5653
5658
|
"path": "src/module-loader/loader.js",
|
|
@@ -6417,7 +6422,7 @@
|
|
|
6417
6422
|
{
|
|
6418
6423
|
"path": "tests/github_push_merge_driver.mjs",
|
|
6419
6424
|
"mode": "0000644",
|
|
6420
|
-
"sha256": "
|
|
6425
|
+
"sha256": "850113ed91babc9eb54dc22231f194d283fd3f94cee9deef1e6e0bf6f3a969c2"
|
|
6421
6426
|
},
|
|
6422
6427
|
{
|
|
6423
6428
|
"path": "tests/github_repos_helpers.mjs",
|
|
@@ -7147,7 +7152,7 @@
|
|
|
7147
7152
|
{
|
|
7148
7153
|
"path": "tests/publish_reconciler.mjs",
|
|
7149
7154
|
"mode": "0000644",
|
|
7150
|
-
"sha256": "
|
|
7155
|
+
"sha256": "1aeb87a14e411160784c94ee5392f323a2ad0a373bd9acf91337289041e8d2bc"
|
|
7151
7156
|
},
|
|
7152
7157
|
{
|
|
7153
7158
|
"path": "tests/rank_gate.mjs",
|
|
@@ -7464,6 +7469,11 @@
|
|
|
7464
7469
|
"mode": "0000644",
|
|
7465
7470
|
"sha256": "0eeccae9320f419667cf9ede3c13671a5b03571f7f2f17067bff1857234ddf91"
|
|
7466
7471
|
},
|
|
7472
|
+
{
|
|
7473
|
+
"path": "tests/skill_grade_audit.mjs",
|
|
7474
|
+
"mode": "0000644",
|
|
7475
|
+
"sha256": "75ad6cd89f6e84e88764a99350e093df31a525eff392e3f84ce99fa32cb952c4"
|
|
7476
|
+
},
|
|
7467
7477
|
{
|
|
7468
7478
|
"path": "tests/skill_grade_recover.mjs",
|
|
7469
7479
|
"mode": "0000644",
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grade-audit
|
|
3
|
+
description: Manual-cadence accountability sweep over recent grades — the override ledger (what shipped despite a failing grade, and was it accounted for), dropped blocker/major findings on passing grades, a false-pass spot-check for runtime-behavior blindness, and an outage roll-up. Triggers when the user says "/grade-audit", "audit recent grades", "run the grade audit", "what shipped past the grader", or "weekly grader accountability sweep". Describes and queues follow-up work ONLY — it NEVER confirms a task, flips a status, or re-grades anything (ADR 0162; verdicts are advisory, never self-executing). Manually invoked; do not depend on scheduled routines.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are auditing the last week of grades for accountability gaps: work that shipped past a failing grade unaccounted for, real findings that rode passing grades into production and evaporated, and passes the panel structurally could not have judged. You **describe and queue**. You never confirm, flip, re-grade, or add a gate — every follow-up lands as a queued idea or a handoff to another skill.
|
|
7
|
+
|
|
8
|
+
## The sweep window
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node scripts/gds/api.js GET "/api/gds/public/recent-shipped?days=7"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For each shipped task in the window, read its persisted grade:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node scripts/gds/api.js GET "/api/gds/tasks/<id>?include=grade"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Bucket by grade shape: `passed`, failed, `signals.panel_outcome='unavailable'` (outage — no quality signal existed), or no grade row at all.
|
|
21
|
+
|
|
22
|
+
## Leg 1 — the override ledger
|
|
23
|
+
|
|
24
|
+
For every shipped task whose grade **failed** (or is absent), the land needed an override. Cross-reference the ledger (Archon — `override_request.decide`):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node scripts/gds/api.js GET "/api/gds/override-requests?status=approved"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Split **infra vs real** first: a fail that is actually `panel_outcome='unavailable'` (or the errored-worker zero-issue shape) is an outage artifact, not an overridden quality verdict — route those to the Leg 4 roll-up.
|
|
31
|
+
- A shipped task with a real failing grade and **no approved override-request row** landed via the raw confirm — the un-audited path. Name it in the report and print its unaddressed `issues[]` verbatim. That set is the ledger's debt.
|
|
32
|
+
- For each such task, queue the debt (see the queueing rule below) — do not chase the lander, do not un-ship anything.
|
|
33
|
+
|
|
34
|
+
## Leg 2 — dropped findings on passing grades
|
|
35
|
+
|
|
36
|
+
A pass with `blocker`/`major` entries in `issues[]` shipped real findings that no process ever picks up (the evaluation counted 274 findings on passing ships with zero conversion paths). For each:
|
|
37
|
+
|
|
38
|
+
- **Worker-attribution filter:** findings attributed to the advisory **Narc** are the known noise class — triage them by hand (read the finding, decide), never auto-trust them into the queue. Findings from the gating workers (Quality, Hacker, Efficiency) or the deterministic pre-passes are higher-confidence — queue them unless plainly stale.
|
|
39
|
+
- **Queueing rule (idempotent):** first read the open inbox (`node scripts/gds/api.js GET /api/gds/inbox`) and skip any finding already queued — the marker is the deterministic title prefix. Then:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node scripts/gds/capture.js "[grade-audit] task <id>: <finding summary>" --kind bug
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
One idea per surviving finding, titled exactly `[grade-audit] task <id>: …` so a re-run of this audit files nothing twice.
|
|
46
|
+
|
|
47
|
+
## Leg 3 — false-pass spot-check (runtime-behavior blindness)
|
|
48
|
+
|
|
49
|
+
The re-review found 2/8 material misses, both the same root cause: **the panel grades diff text and never reasons about runtime** — a CI workflow whose default shallow checkout broke on first run scored functional_fidelity 10/10, and an SSRF guard bypassable via HTTP redirect passed at 9.5. For the riskiest ships in the window — security surfaces, CI/workflow files, the largest diffs, and 9.5s with suspiciously few findings — re-read the merged diff asking one question: *what does this code do at runtime that the diff text does not show?* (defaults it inherits, redirects it follows, first-run behavior, tool/CI defaults). A suspicion is queued via the Leg 2 rule, prefixed the same way — never a re-grade, never a flipped verdict.
|
|
50
|
+
|
|
51
|
+
## Leg 4 — outage roll-up
|
|
52
|
+
|
|
53
|
+
Count the window's `panel_outcome='unavailable'` rows. One-off outages just get the `--regrade` pointer in the report; a streak (2+ consecutive, or clustered on one builder) is a health problem — hand off to **/grader-health**, which owns outage diagnosis. Do not diagnose here.
|
|
54
|
+
|
|
55
|
+
## Report shape
|
|
56
|
+
|
|
57
|
+
End with the ledger, most severe first:
|
|
58
|
+
|
|
59
|
+
1. **Un-accounted overrides** — task, findings printed verbatim, queued-idea ref. A task that landed with no approved override-request row is the headline.
|
|
60
|
+
2. **Queued findings** — what was filed this run, what was skipped as already-queued, what was held back by the Narc filter (and why).
|
|
61
|
+
3. **Spot-check suspicions** — task, the runtime question the panel couldn't answer, queued-idea ref.
|
|
62
|
+
4. **Outage roll-up** — count + the /grader-health handoff if a streak.
|
|
63
|
+
|
|
64
|
+
A clean week is four one-liners.
|
|
65
|
+
|
|
66
|
+
## Constraints
|
|
67
|
+
|
|
68
|
+
- **Never confirm, never flip, never re-grade (ADR 0162).** Verdicts are advisory and never self-executing here — this skill's entire output is a report plus queued `idea_inbox` rows. If a leg tempts you to "just fix it", the fix is a queued idea or a claimed task, not an action inside the audit.
|
|
69
|
+
- **Manual cadence.** Invoke by hand (weekly is the intended rhythm). Do not wire it to cron or depend on `.claude/scheduled-tasks/`.
|
|
70
|
+
- **Idempotent re-runs.** The `[grade-audit] task <id>:` title marker + the inbox pre-check are what make running it twice harmless — keep both.
|
|
71
|
+
- **Archon surfaces degrade gracefully.** Without `override_request.decide`, Leg 1 can only report "ledger unreadable at this rank" — say so rather than skipping silently.
|
|
72
|
+
|
|
73
|
+
## Files this skill touches
|
|
74
|
+
|
|
75
|
+
- Reads: `GET /api/gds/public/recent-shipped`, `GET /api/gds/tasks/:id?include=grade`, `GET /api/gds/override-requests?status=approved` (Archon), `GET /api/gds/inbox`.
|
|
76
|
+
- Writes: `idea_inbox` rows via `node scripts/gds/capture.js` (queued follow-ups only).
|
|
77
|
+
- Never calls: any confirm, status, grade, or regrade endpoint.
|
package/docs/file-map.md
CHANGED
|
@@ -350,6 +350,7 @@ tests/
|
|
|
350
350
|
├── fix-task/SKILL.md ← /fix-task N — claim, fix (test-first), verify, and ship a single GDS task by id end to end; defers to /builder-claim + /builder-ship for their own mechanics
|
|
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
|
+
├── grade-audit/SKILL.md ← /grade-audit — manual-cadence accountability sweep: override ledger (shipped-despite-fail, infra vs real, un-requested confirms named), dropped blocker/major findings on passes queued via capture.js with the advisory-Narc filter, runtime-blindness false-pass spot-check, outage roll-up → /grader-health; describes and queues only, never confirms (ADR 0162, task 1002657)
|
|
353
354
|
├── 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
355
|
├── 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)
|
|
355
356
|
├── idea-triage/SKILL.md ← /idea-triage — daily walk through the GDS idea_inbox (promote / discard / merge); Metic+
|
|
@@ -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.40",
|
|
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.40",
|
|
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.40",
|
|
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
|
@@ -503,8 +503,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
503
503
|
// landed since 1.19.36 with no explicit bump. run 31510590459. (task 1002620)
|
|
504
504
|
// 1.19.38 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
505
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)
|
|
508
|
+
// 1.19.40 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
509
|
+
// landed since 1.19.39 with no explicit bump. run 31513136724. (task 1002620)
|
|
506
510
|
// ---------------------------------------------------------------------------
|
|
507
|
-
const CORE_VERSION = '1.19.
|
|
511
|
+
const CORE_VERSION = '1.19.40'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
508
512
|
|
|
509
513
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
510
514
|
// 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,50 @@
|
|
|
1
|
+
// tests/skill_grade_audit.mjs — the /grade-audit skill exists and covers the
|
|
2
|
+
// accountability sweep from the grader-hardening evaluation §6 (task 1002657):
|
|
3
|
+
// the override ledger, dropped blocker/major findings on passing grades (with
|
|
4
|
+
// the advisory-Narc attribution filter), the false-pass spot-check, and the
|
|
5
|
+
// outage roll-up handoff — describing and queueing ONLY (ADR 0162/0158: it
|
|
6
|
+
// never confirms a task or flips status).
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const SKILL = path.join(ROOT, '.claude', 'skills', 'grade-audit', 'SKILL.md');
|
|
15
|
+
|
|
16
|
+
// .claude/skills is INSTANCE-materialized; a neutral core checkout carries none
|
|
17
|
+
// (task 1002470) — skip there, same guard shape as file_map.mjs.
|
|
18
|
+
const skillsDirExists = existsSync(path.join(ROOT, '.claude', 'skills'));
|
|
19
|
+
|
|
20
|
+
test('/grade-audit exists, covers the sweep legs, and stays describe-and-queue only', (t) => {
|
|
21
|
+
if (!skillsDirExists) return t.skip('.claude/skills not materialized in this checkout');
|
|
22
|
+
assert.ok(existsSync(SKILL), '.claude/skills/grade-audit/SKILL.md must exist (task 1002657)');
|
|
23
|
+
const md = readFileSync(SKILL, 'utf8');
|
|
24
|
+
|
|
25
|
+
// Data surfaces.
|
|
26
|
+
assert.match(md, /\/api\/gds\/public\/recent-shipped/, 'sweeps the recent-shipped window');
|
|
27
|
+
assert.match(md, /include=grade/, 'reads each task\'s persisted grade');
|
|
28
|
+
assert.match(md, /\/api\/gds\/override-requests/, 'cross-references the override ledger');
|
|
29
|
+
|
|
30
|
+
// Leg 1 — override ledger: shipped-with-failing-grade, infra vs real split.
|
|
31
|
+
assert.match(md, /panel_outcome='unavailable'/, 'splits infra outage from a real fail');
|
|
32
|
+
assert.match(md, /no approved (override-)?request/i, 'names tasks that landed with no request row');
|
|
33
|
+
|
|
34
|
+
// Leg 2 — dropped findings with the worker-attribution filter.
|
|
35
|
+
assert.match(md, /blocker|major/, 'targets blocker/major findings on passing grades');
|
|
36
|
+
assert.match(md, /Narc/i, 'carries the advisory-Narc attribution filter');
|
|
37
|
+
assert.match(md, /capture\.js/, 'queues findings via capture.js');
|
|
38
|
+
assert.match(md, /\[grade-audit\]/, 'uses the deterministic title marker for idempotency');
|
|
39
|
+
|
|
40
|
+
// Leg 3 — false-pass spot-check (the 2/8 runtime-blindness classes).
|
|
41
|
+
assert.match(md, /runtime/i, 'spot-checks for runtime-behavior blindness');
|
|
42
|
+
|
|
43
|
+
// Leg 4 — outage roll-up handoff.
|
|
44
|
+
assert.match(md, /\/grader-health/, 'hands outage windows to /grader-health');
|
|
45
|
+
|
|
46
|
+
// The hard constraint.
|
|
47
|
+
assert.match(md, /never confirm/i, 'states it never confirms');
|
|
48
|
+
assert.match(md, /ADR 0162/, 'anchors the no-new-gates constraint');
|
|
49
|
+
assert.ok(!/curl[^\n]*\/confirm/.test(md), 'carries no ready-to-paste confirm curl');
|
|
50
|
+
});
|