@bongos/core 1.19.74 → 1.19.76
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 +20 -15
- package/docs/design/projects-surfaces-redesign-direction.md +157 -0
- package/modules/lifecycle/github-push.js +6 -1
- package/modules/lifecycle/publish-reconciler.js +93 -17
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship.js +11 -0
- package/src/module-api.js +5 -1
- package/tests/github_push_land_proof.mjs +20 -0
- package/tests/publish_reconciler.mjs +87 -1
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.76",
|
|
6
|
+
"core_contract": "1.19.76",
|
|
7
|
+
"source_commit": "c120e5f2382927ea4eeb8b49b3a8f14bb5767fed",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-12T20:27:52.529Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 295,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
14
|
"functional_verbatim": 1225,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1541,
|
|
20
|
+
"tree_sha256": "256191beb3508d344b1cd505975827c265ec5fe89158c0bcbb92e5e7f0a0c030",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1384,6 +1384,11 @@
|
|
|
1384
1384
|
"mode": "0000644",
|
|
1385
1385
|
"sha256": "8a8430af4bbf082d3bab28d5b82b353a6921782014c2852a87eb1056ba101722"
|
|
1386
1386
|
},
|
|
1387
|
+
{
|
|
1388
|
+
"path": "docs/design/projects-surfaces-redesign-direction.md",
|
|
1389
|
+
"mode": "0000644",
|
|
1390
|
+
"sha256": "90babf214f3ef90e44943af2dcb129aa2b8cfb9326d1f8cfdd19768db3fd6715"
|
|
1391
|
+
},
|
|
1387
1392
|
{
|
|
1388
1393
|
"path": "docs/design/vanilla-hall-ui-redesign-scope.md",
|
|
1389
1394
|
"mode": "0000644",
|
|
@@ -3407,7 +3412,7 @@
|
|
|
3407
3412
|
{
|
|
3408
3413
|
"path": "modules/lifecycle/github-push.js",
|
|
3409
3414
|
"mode": "0000644",
|
|
3410
|
-
"sha256": "
|
|
3415
|
+
"sha256": "f090fbe484a1c9e8bf40d2871a150cef8ce73199455e54e44d117ce1ebc21da5"
|
|
3411
3416
|
},
|
|
3412
3417
|
{
|
|
3413
3418
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -3462,7 +3467,7 @@
|
|
|
3462
3467
|
{
|
|
3463
3468
|
"path": "modules/lifecycle/publish-reconciler.js",
|
|
3464
3469
|
"mode": "0000644",
|
|
3465
|
-
"sha256": "
|
|
3470
|
+
"sha256": "54fd7ba3024169f5887fe58182ba03519e66acbbf28c1b753712dbd36f91da01"
|
|
3466
3471
|
},
|
|
3467
3472
|
{
|
|
3468
3473
|
"path": "modules/lifecycle/routes/analytics.js",
|
|
@@ -4052,12 +4057,12 @@
|
|
|
4052
4057
|
{
|
|
4053
4058
|
"path": "package-lock.json",
|
|
4054
4059
|
"mode": "0000644",
|
|
4055
|
-
"sha256": "
|
|
4060
|
+
"sha256": "599fff93b9ec8c3f6c83cd9a182d9338bea5c289ba1305bb66a69c2818aee8cf"
|
|
4056
4061
|
},
|
|
4057
4062
|
{
|
|
4058
4063
|
"path": "package.json",
|
|
4059
4064
|
"mode": "0000644",
|
|
4060
|
-
"sha256": "
|
|
4065
|
+
"sha256": "3e61cb13243d69633f0b8ba4f1b48492f3c16077ada93e1ce3105f16285c3b51"
|
|
4061
4066
|
},
|
|
4062
4067
|
{
|
|
4063
4068
|
"path": "public-docs/index.html",
|
|
@@ -5252,7 +5257,7 @@
|
|
|
5252
5257
|
{
|
|
5253
5258
|
"path": "scripts/gds/ship.js",
|
|
5254
5259
|
"mode": "0000644",
|
|
5255
|
-
"sha256": "
|
|
5260
|
+
"sha256": "be7d4d35f8f19e4687e9a8444ca36c222895805a7ec35952f92a27ef567ed2b5"
|
|
5256
5261
|
},
|
|
5257
5262
|
{
|
|
5258
5263
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -5682,7 +5687,7 @@
|
|
|
5682
5687
|
{
|
|
5683
5688
|
"path": "src/module-api.js",
|
|
5684
5689
|
"mode": "0000644",
|
|
5685
|
-
"sha256": "
|
|
5690
|
+
"sha256": "b89d3c0f58693902c0632ef681849e5c0981a62d3eea34ae6096182c998419bf"
|
|
5686
5691
|
},
|
|
5687
5692
|
{
|
|
5688
5693
|
"path": "src/module-loader/loader.js",
|
|
@@ -6467,7 +6472,7 @@
|
|
|
6467
6472
|
{
|
|
6468
6473
|
"path": "tests/github_push_land_proof.mjs",
|
|
6469
6474
|
"mode": "0000644",
|
|
6470
|
-
"sha256": "
|
|
6475
|
+
"sha256": "3774072a50fb6ffdadfe2c890e8828473b72456f1c213f812c14eb08c74401a4"
|
|
6471
6476
|
},
|
|
6472
6477
|
{
|
|
6473
6478
|
"path": "tests/github_push_merge_driver.mjs",
|
|
@@ -7227,7 +7232,7 @@
|
|
|
7227
7232
|
{
|
|
7228
7233
|
"path": "tests/publish_reconciler.mjs",
|
|
7229
7234
|
"mode": "0000644",
|
|
7230
|
-
"sha256": "
|
|
7235
|
+
"sha256": "fe4b594292992429fe5e077491fa2916028a2f1815414f94434f83b4186b1138"
|
|
7231
7236
|
},
|
|
7232
7237
|
{
|
|
7233
7238
|
"path": "tests/rank_gate.mjs",
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Projects Surfaces — Redesign Direction
|
|
2
|
+
|
|
3
|
+
> **Task 1002695** (goal 1000059 "Projects surface revamp", R06) · direction decided with the owner 2026-08-12.
|
|
4
|
+
> Feeds **task 1002696** (R07 — the implementation carrier) and the re-walk (task 1002697, with task 1002715 folded in).
|
|
5
|
+
> Findings feed: the QA/UAT checkpoint walk (task 1002702, [session log](../session-logs/<redacted>.md)) and the truthfulness cluster (task 1002720, findings F15–F23).
|
|
6
|
+
|
|
7
|
+
Scope: the three owner-facing project surfaces — the **create wizard** (`public-landing/create.html`), the **done panel** (`panel-done` in the same file), and the **manage view** (`public-landing/projects.html`, served at `/projects`). **All three live on the apex host — `cloudbongos.com` — and stay there.** Project creation and management are not builders-hall surfaces; the hall (builders subdomain) may link *out* to them, nothing more. The gate catalog stays with goal 1000031; the hall's federation my-projects page is a different surface and is out of scope.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 0. Starting point — what exists today
|
|
12
|
+
|
|
13
|
+
- The flows **work end-to-end** (verified live on both venues by the checkpoint walk): create → address → GitHub sign-in → manage → teardown. What's broken is the *trust surface*, not the plumbing.
|
|
14
|
+
- Both pages are self-contained orphans: one inline `<style>` each, hand-rolled `:root`, no shared components, ~half the palette hardcoded past the branding contract's reach. The repo holds three unrelated design systems (sunrise landing, indigo hall kit, marble status page); these surfaces belong to the sunrise family but share no code with anything.
|
|
15
|
+
- Lifecycle truth lives in `provisioning_instances.status` — six runner-written states: `requested · provisioning · active · tearing_down · torn_down · error`. The UI maps them to chips (`queued / standing up / live / snagged / tearing down / torn down`).
|
|
16
|
+
- The three failure themes the walk proved (details in §§2–5): the platform asserts states it never verified, in both directions; the copy speaks operator; and the first-run paths (self-host, first admin, newborn instance) were never designed.
|
|
17
|
+
|
|
18
|
+
## 1. Decisions (owner, 2026-08-12)
|
|
19
|
+
|
|
20
|
+
1. **Truth model: heartbeat + honest chips.** The platform re-checks liveness on a background cadence and the UI shows verification recency. "Live" becomes a verified claim.
|
|
21
|
+
2. **Visual system: sunrise, tokenized — refined through design iterations.** Keep the sunrise landing brand; extract a shared token + component layer; the final look is nailed down through owner-review loops during implementation (§10), not invented in this doc.
|
|
22
|
+
3. **First-run: designed now, delivered split.** This direction defines the shape-aware surfaces; task 1002696 implements the surface parts, and runner/infra hardening stays in its parallel lane (tasks 1002717–1002722).
|
|
23
|
+
4. **Deliverable: this direction doc.** Owner sign-off on this doc is the task's done condition.
|
|
24
|
+
|
|
25
|
+
Also folded in, decided earlier the same day: **task 1002704 — auto-assign `<slug>.cloudbongos.com` at creation, opt-out.** The address step collapses from a form into a confirmation (§6).
|
|
26
|
+
|
|
27
|
+
## 2. Principles
|
|
28
|
+
|
|
29
|
+
1. **The UI only asserts what the platform verified — and says when.** Every liveness claim carries recency. Unverified is stated as unverified, never dressed as fact.
|
|
30
|
+
2. **Copy speaks owner, not operator.** No raw tool output on a card. No wall-clock promises. No process jargon. What happened, what it means, what (if anything) the owner can do.
|
|
31
|
+
3. **Deliberate end-states are calm.** A teardown the owner asked for is not an error and is never styled as one.
|
|
32
|
+
4. **Every flow owns its vocabulary.** Stage labels are never reused across flows — a card being torn down must not render a stage named "Live".
|
|
33
|
+
5. **Surfaces are honest about hosting shape.** A step renders only if somebody real will actually perform it on this instance's shape. Dead copy is a broken promise.
|
|
34
|
+
6. **One themeable token layer.** Everything an instance can't re-theme through the branding contract is a bug.
|
|
35
|
+
|
|
36
|
+
## 3. Truth model — the verification overlay
|
|
37
|
+
|
|
38
|
+
Verification is an **overlay on lifecycle, not a new lifecycle state**. The runner's six statuses stay untouched; verification adds recorded observations about whether the instance actually answers.
|
|
39
|
+
|
|
40
|
+
**Server (small piece, ships inside task 1002696):**
|
|
41
|
+
- `provisioning_instances` gains `last_verified_at` (timestamptz) and `last_verified_up` (boolean); both nullable.
|
|
42
|
+
- A heartbeat sweep in the provisioning module (in-process interval on the control plane, ~5-minute cadence with jitter) probes every `active` instance: `GET https://<domain>/healthz` when addressed, the recorded local port otherwise, short timeout. It writes the two columns and never flips `status`.
|
|
43
|
+
- `GET /provisioning/instances` and `/:id` expose both fields.
|
|
44
|
+
|
|
45
|
+
**Display contract (chips on both surfaces):**
|
|
46
|
+
|
|
47
|
+
| Condition | Chip |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `active`, last probe up, fresh (≤ 2 cadences) | **live** · checked 2m ago (green) |
|
|
50
|
+
| `active`, last probe failed | **unreachable** · since 14:02 (amber/red) |
|
|
51
|
+
| `active`, never probed or stale probe | **live?** · last seen 3h ago (amber) |
|
|
52
|
+
|
|
53
|
+
- The manage view refreshes settled cards on a slow interval (~60s) and on tab focus, so the overlay stays current while someone is looking. (Today an `active` card never polls again and can never self-correct.)
|
|
54
|
+
- Provision-time gating (walk finding F15 — the runner declared `active` after a failed probe) is **task 1002720's** server work; this direction sets its display contract: the runner either gates `active` on a passing probe or writes `last_verified_up=false` that the UI must render.
|
|
55
|
+
- The inverse direction (F23 — a *successful* GitHub-App setup recorded as failed after a restart race, then a single-use manifest code retried forever) is also task 1002720's; the display contract here is that the done panel's sign-in step **waits and re-verifies** before declaring failure, and never claims failure for a setup it hasn't conclusively observed failing.
|
|
56
|
+
- Address attach (F16): "answering at `<domain>`" is claimed only after a probe **against the domain** succeeds. If DNS wiring was skipped (no hook), the card says exactly what's left: "point your domain's A record at `<ip>` — we'll confirm once it answers."
|
|
57
|
+
|
|
58
|
+
## 4. Vocabulary — states, chips, strips
|
|
59
|
+
|
|
60
|
+
**Chips** (one set, both surfaces): `queued · standing up · live (·recency) · live? · unreachable · snagged · tearing down · torn down`. `torn down` renders neutral (faint ink, no color), never red.
|
|
61
|
+
|
|
62
|
+
**Progress strips are per-flow** — distinct labels, chosen by the open intent on the card (F22), not by lifecycle status alone:
|
|
63
|
+
|
|
64
|
+
| Flow | Stages |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Standup | Request filed → Standing it up → Live |
|
|
67
|
+
| Teardown | Teardown queued → Winding down → Torn down |
|
|
68
|
+
| Revive | Revive queued → Standing it up → Back live |
|
|
69
|
+
| Address | Address queued → Wiring the address → Answering at `<domain>` |
|
|
70
|
+
|
|
71
|
+
- A torn-down card with a pending revive intent shows the **revive strip** — never "you can start another project below" (F22's actively-wrong guidance).
|
|
72
|
+
- A strip's final stage lights only when the truth model confirms it (probe answered), not when the runner finishes its script.
|
|
73
|
+
|
|
74
|
+
## 5. Copy rules — owner voice
|
|
75
|
+
|
|
76
|
+
1. **Errors are a plain sentence + a disclosure.** Map the failed runner step to an owner-facing category — "We couldn't reach the project's repository," "The server didn't come up," "The address didn't wire up." The raw `error_note` (git stderr, ssh noise) moves into a `<details>` labeled "Technical details" — present for whoever helps, never the headline.
|
|
77
|
+
2. **No wall-clock promises.** Kill "usually under a minute" and friends. Describe what's happening and what happens next; never how long.
|
|
78
|
+
3. **Retry honesty (F19).** Intents retire after 3 attempts and nothing auto-retries. The snagged card says so — "Stopped after 3 tries" — and gains an owner-facing **Try again** action that re-enqueues (the enqueue API already accepts re-provisioning; wire it). "The platform keeps retrying" copy dies.
|
|
79
|
+
4. **Teardown truth (F17) — decided: the database is not dropped.** (Owner, 2026-08-12: no database drop unless we find otherwise — retention is policy, not an accident.) The confirm and the torn-down card say so plainly: the server goes away, the repository stays the owner's, the database is kept and recoverable by the operator. Copy always states exactly what the runner does.
|
|
80
|
+
5. **Repo visibility is stated, always (task 1002721).** Every path that creates or adopts a repo says whether it will be public or private, before the owner commits — GitHub's own UI must never be the first place visibility appears.
|
|
81
|
+
6. **Newborn welcome (idea 1000701).** A freshly-provisioned instance greets its founding owner with a welcome moment, and account nags (billing, art-key coverage) are suppressed until first-run completes.
|
|
82
|
+
|
|
83
|
+
## 6. Surface · Create wizard
|
|
84
|
+
|
|
85
|
+
Beats stay **Name · Home · Address · Create** — the flow shape survives; the address beat changes meaning and everything gets the copy + token pass.
|
|
86
|
+
|
|
87
|
+
- **Gate panel:** unchanged in flow (GitHub sign-in, AGPL trust link); copy pass only.
|
|
88
|
+
- **Name panel:** unchanged in flow; the live slug hint becomes the seed of the address confirmation.
|
|
89
|
+
- **Home panel:** the three-way progressive branch (picker / manual / fresh) survives; the manual path and the fresh path both state repo visibility explicitly (§5.5).
|
|
90
|
+
- **Address panel — collapses to a confirmation (task 1002704):** headline shows the assigned address — "`<slug>.cloudbongos.com` — yours from the moment it stands up." The slug stays editable inline (revalidating availability). A disclosure "Use a domain I own" holds the custom-domain input. Starting without an address remains possible only inside that disclosure, with the honest consequence stated: "sign-in on your instance can't be set up until it has an address."
|
|
91
|
+
- **Review panel:** the Domain row becomes the Address row and always shows the resolved address.
|
|
92
|
+
|
|
93
|
+
## 7. Surface · Done panel
|
|
94
|
+
|
|
95
|
+
- **Keep** the canonical-plan rendering (task 1002694's work: checklist from `GET /provisioning/onboard-plan`, manifest-driven sign-in leg, live tick states).
|
|
96
|
+
- **Shape-awareness:** the plan endpoint tags each step with the hosting shapes it applies to; the panel renders only steps real for this instance's shape. On shapes with no control-plane runner, `auto` steps either disappear or convert to explicit owner-instructions — no promise a runner will do what no runner exists to do.
|
|
97
|
+
- **The progress strip obeys the truth model:** "Live" lights on a verified answer, not on script completion; a lost verification race shows "confirming…" rather than a false ✖ (F23 display contract).
|
|
98
|
+
- **A door out:** the panel ends with a link to `/projects` — "watch it any time from Your projects."
|
|
99
|
+
|
|
100
|
+
## 8. Surface · /projects (manage view)
|
|
101
|
+
|
|
102
|
+
- **Card anatomy stays** (name + chip → meta → address → strip → message → actions) with the §3 chip overlay, §4 per-flow strips, and §5 message rules.
|
|
103
|
+
- **New actions:**
|
|
104
|
+
- **Bring it back** on `torn_down` cards — the enqueue API already revives a torn-down slug; the walk had to do it raw. Button + confirm, revive strip while it runs.
|
|
105
|
+
- **Try again** on `snagged` cards (§5.3).
|
|
106
|
+
- **Address row** shows the auto-assigned domain post-1002704; attach/change stays for custom domains, claiming "answering" only per §3.
|
|
107
|
+
- **Navigation doors (the page is currently orphaned):** the gate gets a signed-in "Your projects" door; the done panel links out (§7); the hall gets a cross-host link out to `cloudbongos.com/projects` (a door only — the surface itself never moves to the hall host). At least the gate and done-panel doors ship with task 1002696.
|
|
108
|
+
- **Refresh model:** slow interval + on-focus refetch for settled cards (§3); active-intent cards keep today's 5s poll.
|
|
109
|
+
|
|
110
|
+
## 9. First-run — designed now, delivered split
|
|
111
|
+
|
|
112
|
+
| First-run gap (walk source) | Design (this direction) | Delivery |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| Done panel promises a runner self-hosts don't have (task 1002717) | Shape-aware plan rendering (§7) | **task 1002696** |
|
|
115
|
+
| `FIRST_ADMIN` invisible — first user bounced to a nonexistent Owner (task 1002717) | A virgin instance's sign-in surface says plainly: "No one is admitted yet — the instance operator names the first admin," and points at the one-line fix (env var / branding field) | copy surface with task 1002696 where it lives in core UI; self-host docs + recipe with the **1002717 lane** |
|
|
116
|
+
| Newborn instance greets its founder with "your art coverage has ended" (idea 1000701) | §5.6 welcome + nag suppression until first-run completes | **task 1002696** if it stays a small `needs`-gating change; otherwise spun out as its own task |
|
|
117
|
+
| `/create` + `/projects` unreachable on single-origin self-hosts (task 1002717) | Out of this direction's hands — a mounting/architecture question | **1002717 lane** |
|
|
118
|
+
| Runner assumes the founder's box six ways; founder-identity hardcodes (tasks 1002718, 1002719) | Not a surface concern | **hardening lane** |
|
|
119
|
+
| Refuse-unknown dispatch + runner-drift gate (task 1002722) | Not a surface concern | **hardening lane** |
|
|
120
|
+
|
|
121
|
+
## 10. Visual system — sunrise, tokenized, iterated
|
|
122
|
+
|
|
123
|
+
**Base:** the sunrise landing brand stays — these surfaces are continuous with the gate, not the hall.
|
|
124
|
+
|
|
125
|
+
**Token + component layer** (new, shared by the three surfaces — e.g. `public-landing/assets/sunrise.css`):
|
|
126
|
+
- Tokens aligned to the branding contract's themeable set. The currently-hardcoded semantics (`--card`, `--line`, `--ok`, `--err`, chip fills, strip states, sky gradients) become tokens **derived from contract vars via `color-mix`** where that reads well; the contract's `COLOR_VARS` set is extended only where derivation fails aesthetically. End state: an instance pack re-themes the whole page, not half of it.
|
|
127
|
+
- Component set: page shell (sky header), card, chip, progress strip, step list, choice card, field row, disclosure, confirm dialog. Types and states defined once, used by all three surfaces.
|
|
128
|
+
|
|
129
|
+
**Iteration loops (owner-requested — the look is nailed by review, not by this doc):**
|
|
130
|
+
1. **Loop 1 — component sheet.** A static mock page showing the token palette and every component in every state (all chips, all strips, a card in each lifecycle). Owner reviews and directs before any surface is rebuilt.
|
|
131
|
+
2. **Loop 2 — applied surfaces.** The three rebuilt surfaces staged for an owner walk (stage preview or static) before task 1002696 ships.
|
|
132
|
+
3. **Final gate:** the re-walk (task 1002697, with the true-stranger pass task 1002715) is the acceptance test for the whole act.
|
|
133
|
+
|
|
134
|
+
## 11. Scope boundaries
|
|
135
|
+
|
|
136
|
+
**In (task 1002696 implements):** the three surfaces rebuilt on the shared token/component layer; heartbeat sweep + two columns + API exposure; chip overlay + per-flow strips; owner-voice copy pass incl. error categories + disclosures; address-step collapse (task 1002704); repo-visibility statements; Bring-it-back + Try-again actions; nav doors (gate, done panel); shape-aware done panel; newborn welcome/nag suppression (if small).
|
|
137
|
+
|
|
138
|
+
**Parallel (hardening lane, already seeded):** provision-time healthz gating, no-hook attach honesty, teardown-vs-dropdb reality, manifest race + spent-code fix (task 1002720); self-host reachability + FIRST_ADMIN recipe (task 1002717); runner assumptions (task 1002718); founder-identity hardcodes (task 1002719); dispatch/drift gates (task 1002722).
|
|
139
|
+
|
|
140
|
+
**Out:** the gate catalog (goal 1000031); the hall's federation my-projects surface; unreachable-instance Discord alerts (owner wants them — captured as idea 1000703, a follow-up on top of the heartbeat, deliberately not in task 1002696).
|
|
141
|
+
|
|
142
|
+
## 12. Implementation seams — what task 1002696 must not break
|
|
143
|
+
|
|
144
|
+
- The canonical onboard-plan contract (`GET /provisioning/onboard-plan`) and its consumers — the done panel renders *from* it; shape-awareness extends the plan, it doesn't fork it.
|
|
145
|
+
- The GitHub-App manifest flow (`startManifest` → public status/callback routes) — the one-click sign-in leg keeps working mid-redesign.
|
|
146
|
+
- The `sessionStorage` create-draft resume behavior.
|
|
147
|
+
- Branding injection: `landingPage()` string tokens (`{{productName}}` etc.) and `themeMarkup()`'s `:root` override — the new token layer must sit *under* it, not fight it.
|
|
148
|
+
- Apex-host mounting (`isApexHost`) — moving these pages off the control plane is 1002717-lane work, not this task.
|
|
149
|
+
- The provisioning API surface consumed by other callers; additions only, no breaking changes.
|
|
150
|
+
- `tests/onboard.mjs` and the walk's verified legs — the re-walk (task 1002697) must pass everything the checkpoint walk passed.
|
|
151
|
+
|
|
152
|
+
## 13. Sign-off dispositions (owner, 2026-08-12)
|
|
153
|
+
|
|
154
|
+
No open questions remain — both were decided at sign-off:
|
|
155
|
+
|
|
156
|
+
1. **Teardown does not drop the database** — retention is deliberate policy unless we find otherwise (§5.4). Any future change starts with the runner, and the copy follows it.
|
|
157
|
+
2. **Unreachable-instance Discord alerts: yes** — captured as idea 1000703, a follow-up on top of the heartbeat, outside task 1002696's scope.
|
|
@@ -497,7 +497,12 @@ async function findTaskPullRequest({ taskId }, deps = {}) {
|
|
|
497
497
|
if (!res.ok) throw err('STATUS_FETCH_FAILED', { status: res.status });
|
|
498
498
|
const prs = await res.json();
|
|
499
499
|
if (!Array.isArray(prs)) return null;
|
|
500
|
-
|
|
500
|
+
// task 1002723: real ships also title PRs with the COMMIT SUBJECT carrying a
|
|
501
|
+
// "(task <id>)" suffix (PR #89 — "docs: session log … (task 1002702)"), which the
|
|
502
|
+
// prefix-only match missed, stranding the task. A parenthesized exact id in the
|
|
503
|
+
// TITLE is still a title-level bind — body mentions stay excluded (see above).
|
|
504
|
+
const id = Number(taskId);
|
|
505
|
+
const titleRe = new RegExp(`(?:^task\\s+#?${id}\\b)|(?:\\(task\\s+#?${id}\\))`, 'i');
|
|
501
506
|
for (const pr of prs) {
|
|
502
507
|
if (!pr || !titleRe.test(String(pr.title || ''))) continue;
|
|
503
508
|
const head = pr.head || {};
|
|
@@ -78,8 +78,13 @@ const seams = require('../../src/module-api');
|
|
|
78
78
|
const conflictResolve = require('./conflict-resolve');
|
|
79
79
|
|
|
80
80
|
// 5-min cadence: frequent enough that a strand heals within minutes of CI
|
|
81
|
-
// finishing, cheap enough that the GitHub API cost is negligible
|
|
82
|
-
// confirmed task per sweep
|
|
81
|
+
// finishing, cheap enough that the GitHub API cost is negligible: steady state
|
|
82
|
+
// is 2 status-path calls per confirmed task per sweep (+1 title-search when no
|
|
83
|
+
// PR is known). A task whose convention is not yet known walks up to three
|
|
84
|
+
// candidates ONCE (≤6 calls); the winning branch — or, when nothing is found,
|
|
85
|
+
// the conventional first candidate — is memoized per worktree, so every later
|
|
86
|
+
// sweep probes one branch and leaves discovery to the title search. Override
|
|
87
|
+
// for tests/ops.
|
|
83
88
|
const INTERVAL_MS = Number(process.env.PUBLISH_RECONCILE_MS) || 5 * 60_000;
|
|
84
89
|
// A first sweep shortly after boot so a strand left by the previous process (e.g.
|
|
85
90
|
// a deploy that restarted the server mid-poll) heals fast, not on the next tick.
|
|
@@ -97,6 +102,10 @@ const STALE_CONFIRMED_MS = Number(process.env.PUBLISH_RECONCILE_STALE_MS) || 30
|
|
|
97
102
|
// a persistently-red branch surfaces via the STALE strand WARN above).
|
|
98
103
|
const MAX_STALE_HEAL_ATTEMPTS = Number(process.env.PUBLISH_RECONCILE_MAX_HEAL) || 3;
|
|
99
104
|
const healAttempts = new Map(); // branch -> regen-heal attempts this process
|
|
105
|
+
// task 1002723 (round 2): worktree_name -> the branch convention that owned its PR
|
|
106
|
+
// last sweep, so steady-state sweeps probe ONE branch (2 calls) instead of walking
|
|
107
|
+
// all three candidates every tick. In-memory like healAttempts; pruned per sweep.
|
|
108
|
+
const branchHomes = new Map();
|
|
100
109
|
|
|
101
110
|
let intervalTimer = null;
|
|
102
111
|
let firstTimer = null;
|
|
@@ -186,14 +195,15 @@ async function runSweep(deps = {}) {
|
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
// Safety: a worktree_name can be reused across sequential (released) claims, so
|
|
189
|
-
// two confirmed tasks could derive the SAME branch. We can't tell which task
|
|
190
|
-
// merged PR belongs to, so we skip ambiguous
|
|
198
|
+
// two confirmed tasks could derive the SAME branch set. We can't tell which task
|
|
199
|
+
// a merged PR belongs to, so we skip ambiguous worktrees (logged) and leave them
|
|
191
200
|
// for manual handling rather than risk shipping the wrong task's row.
|
|
192
|
-
|
|
201
|
+
// task 1002723: keyed on the worktree_name itself — every branch candidate is
|
|
202
|
+
// derived from it, so one ambiguous wt poisons all its candidates equally.
|
|
203
|
+
const wtCount = new Map();
|
|
193
204
|
for (const r of rows) {
|
|
194
205
|
if (!r.worktree_name) continue;
|
|
195
|
-
|
|
196
|
-
branchCount.set(b, (branchCount.get(b) || 0) + 1);
|
|
206
|
+
wtCount.set(r.worktree_name, (wtCount.get(r.worktree_name) || 0) + 1);
|
|
197
207
|
}
|
|
198
208
|
|
|
199
209
|
let reconciled = 0;
|
|
@@ -205,8 +215,15 @@ async function runSweep(deps = {}) {
|
|
|
205
215
|
let unprovable = 0; // confirmed tasks the sweep could not prove landed this pass
|
|
206
216
|
let staleStrands = 0; // of those, the ones older than STALE_CONFIRMED_MS (WARN'd)
|
|
207
217
|
let strandsFlagged = 0; // task 1002606: needs_rebase flags NEWLY set this sweep (any site)
|
|
218
|
+
// task 1002723: one entry per confirmed task per sweep ("<id>:<disposition>").
|
|
219
|
+
// The 1002702/1002716 strands sat through 5+ sweeps with ZERO journal evidence
|
|
220
|
+
// the sweep even saw them — young unprovables were a silent `continue`. Silence
|
|
221
|
+
// must not masquerade as health; this feeds the always-on line below.
|
|
222
|
+
const dispositions = [];
|
|
208
223
|
const attempts = deps.healAttempts || healAttempts; // injectable for deterministic tests
|
|
224
|
+
const homes = deps.branchHome || branchHomes; // task 1002723: wt -> winning branch (injectable)
|
|
209
225
|
const branchesSeen = new Set(); // to prune the attempts map post-sweep
|
|
226
|
+
const wtSeen = new Set(); // to prune the branchHomes map post-sweep
|
|
210
227
|
const nowMs = Date.now();
|
|
211
228
|
|
|
212
229
|
// task 1002606 — the ONE exit for every strand signal: flag needs_rebase
|
|
@@ -430,23 +447,62 @@ async function runSweep(deps = {}) {
|
|
|
430
447
|
};
|
|
431
448
|
|
|
432
449
|
for (const r of rows) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
450
|
+
// task 1002723: claude/<wt> is only ONE of the branch names real ships push —
|
|
451
|
+
// a worktree/laptop ship pushes <wt> itself (the task-1002702 strand: PR #89 on
|
|
452
|
+
// branch task-1002702, invisible to claude/task-1002702) or worktree-<wt> (the
|
|
453
|
+
// task-1002645 wave). Try each convention until one has a PR, so the merge
|
|
454
|
+
// nudge / heals / land proof engage regardless of which client shipped.
|
|
455
|
+
const wt = r.worktree_name || null;
|
|
456
|
+
if (wt) wtSeen.add(wt);
|
|
457
|
+
let candidates = wt
|
|
458
|
+
? [`claude/${wt}`, wt, `worktree-${wt}`].filter((b) => push.isSafeBranch(b))
|
|
459
|
+
: [];
|
|
460
|
+
// Cross-sweep memoization: once a sweep has found which convention owns the
|
|
461
|
+
// task's PR, later sweeps probe that branch first and break immediately —
|
|
462
|
+
// steady-state cost stays at the documented 2 calls per confirmed task.
|
|
463
|
+
// The home may also be an ADOPTED foreign branch name (title-search result) —
|
|
464
|
+
// it entered the memo through an isSafeBranch-verified path either way.
|
|
465
|
+
const home = wt ? homes.get(wt) : null;
|
|
466
|
+
if (home) {
|
|
467
|
+
candidates = [home, ...candidates.filter((b) => b !== home)];
|
|
468
|
+
}
|
|
469
|
+
const wtUsable = !!wt && wtCount.get(wt) === 1;
|
|
470
|
+
if (wt && wtCount.get(wt) > 1) {
|
|
471
|
+
// A reused worktree_name maps its branch set to >1 confirmed task, so a merged
|
|
472
|
+
// PR is ambiguous — but each task's OWN head_sha is not, so we skip only the
|
|
473
|
+
// branch/PR path and let the head_sha ancestry land-check below resolve it.
|
|
474
|
+
logFn(`[publish-reconciler] task ${r.task_id}: worktree ${wt} maps to >1 confirmed task — using head_sha land-check only`);
|
|
441
475
|
}
|
|
442
476
|
|
|
477
|
+
let branch = null;
|
|
443
478
|
let landed = false;
|
|
444
479
|
let landNote = '';
|
|
445
480
|
let branchStatus = null;
|
|
446
481
|
let prNotLanded = false;
|
|
447
482
|
|
|
448
|
-
if (
|
|
449
|
-
|
|
483
|
+
if (wtUsable) {
|
|
484
|
+
const homeKnown = !!home;
|
|
485
|
+
for (const cand of candidates) {
|
|
486
|
+
branchesSeen.add(cand);
|
|
487
|
+
branch = cand;
|
|
488
|
+
({ status: branchStatus, prNotLanded } = await sweepBranchPath(r, cand));
|
|
489
|
+
if (branchStatus && branchStatus.pr_state !== 'none') {
|
|
490
|
+
homes.set(wt, cand); // remember the PR home for the next sweep
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
// A remembered home with no PR means the full walk already happened on an
|
|
494
|
+
// earlier sweep and found nothing — don't re-walk the other candidates
|
|
495
|
+
// every 5 minutes. The title-search fallback below still runs each sweep,
|
|
496
|
+
// so a late-appearing PR on ANY branch name is discovered there (and the
|
|
497
|
+
// adoption path re-homes it).
|
|
498
|
+
if (homeKnown) break;
|
|
499
|
+
}
|
|
500
|
+
// First full walk found no PR anywhere: pin the conventional candidate as
|
|
501
|
+
// home so later sweeps probe ONE branch, not three, for this class (the
|
|
502
|
+
// never-pushed / allow-empty shapes, where a PR essentially never appears).
|
|
503
|
+
if (!(branchStatus && branchStatus.pr_state !== 'none') && !homeKnown && candidates.length) {
|
|
504
|
+
homes.set(wt, candidates[0]);
|
|
505
|
+
}
|
|
450
506
|
}
|
|
451
507
|
|
|
452
508
|
// task 1002645 — the claude/<worktree_name> derivation is the DEV-BOX convention;
|
|
@@ -468,6 +524,8 @@ async function runSweep(deps = {}) {
|
|
|
468
524
|
branch = found.branch;
|
|
469
525
|
branchesSeen.add(branch);
|
|
470
526
|
({ status: branchStatus, prNotLanded } = await sweepBranchPath(r, branch));
|
|
527
|
+
// Memoize the adopted home too, so the next sweep skips the re-search.
|
|
528
|
+
if (wt && branchStatus && branchStatus.pr_state !== 'none') homes.set(wt, branch);
|
|
471
529
|
}
|
|
472
530
|
} catch (_) {
|
|
473
531
|
/* best-effort: a PR-search miss waits for the next sweep */
|
|
@@ -525,6 +583,7 @@ async function runSweep(deps = {}) {
|
|
|
525
583
|
: prNotLanded
|
|
526
584
|
? 'pr_not_landed'
|
|
527
585
|
: (!branch && !r.published_head_sha) ? 'no_branch_no_tip' : 'land_not_proven';
|
|
586
|
+
dispositions.push(`${r.task_id}:${reason}`);
|
|
528
587
|
const ageMs = r.confirmed_at ? (nowMs - new Date(r.confirmed_at).getTime()) : null;
|
|
529
588
|
if (ageMs != null && ageMs > STALE_CONFIRMED_MS) {
|
|
530
589
|
staleStrands++;
|
|
@@ -554,6 +613,7 @@ async function runSweep(deps = {}) {
|
|
|
554
613
|
try {
|
|
555
614
|
const shipped = await db.shipTask({ taskId: r.task_id, builderId: r.builder_id });
|
|
556
615
|
reconciled++;
|
|
616
|
+
dispositions.push(`${r.task_id}:shipped`);
|
|
557
617
|
logFn(
|
|
558
618
|
`[publish-reconciler] task ${r.task_id} (${landNote}) landed → shipped`
|
|
559
619
|
);
|
|
@@ -571,7 +631,10 @@ async function runSweep(deps = {}) {
|
|
|
571
631
|
// in the race window — expected + idempotent, NOT an error.
|
|
572
632
|
if (e && e.code !== 'TASK_NOT_CONFIRMED') {
|
|
573
633
|
errors++;
|
|
634
|
+
dispositions.push(`${r.task_id}:ship_failed`);
|
|
574
635
|
errFn(`[publish-reconciler] shipTask ${r.task_id} failed: ${e && e.message}`);
|
|
636
|
+
} else {
|
|
637
|
+
dispositions.push(`${r.task_id}:already_flipped`);
|
|
575
638
|
}
|
|
576
639
|
}
|
|
577
640
|
}
|
|
@@ -584,6 +647,11 @@ async function runSweep(deps = {}) {
|
|
|
584
647
|
const plain = b.startsWith('behind:') ? b.slice('behind:'.length) : b;
|
|
585
648
|
if (!branchesSeen.has(plain)) attempts.delete(b);
|
|
586
649
|
}
|
|
650
|
+
// Same leak guard for the branch-home memo: a worktree that left the confirmed
|
|
651
|
+
// set (landed or released) never returns under the same name.
|
|
652
|
+
for (const w of homes.keys()) {
|
|
653
|
+
if (!wtSeen.has(w)) homes.delete(w);
|
|
654
|
+
}
|
|
587
655
|
|
|
588
656
|
if (reconciled || merged || resolvedConflicts || refreshedStale || behindUpdated) {
|
|
589
657
|
logFn(
|
|
@@ -595,6 +663,14 @@ async function runSweep(deps = {}) {
|
|
|
595
663
|
`[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`
|
|
596
664
|
);
|
|
597
665
|
}
|
|
666
|
+
// task 1002723: the always-on per-sweep account. Every confirmed task the sweep
|
|
667
|
+
// saw gets a named disposition — so a strand is visible on its FIRST quiet sweep,
|
|
668
|
+
// not at the 30-min WARN (or never, as with the silent young-unprovable class).
|
|
669
|
+
if (dispositions.length) {
|
|
670
|
+
const shown = dispositions.slice(0, 20).join(' · ');
|
|
671
|
+
const more = dispositions.length > 20 ? ` (+${dispositions.length - 20} more)` : '';
|
|
672
|
+
logFn(`[publish-reconciler] dispositions: ${shown}${more}`);
|
|
673
|
+
}
|
|
598
674
|
return { checked: rows.length, reconciled, merged, resolvedConflicts, refreshedStale, behindUpdated, errors, unprovable, staleStrands, strandsFlagged };
|
|
599
675
|
}
|
|
600
676
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.76",
|
|
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.76",
|
|
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.76",
|
|
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/ship.js
CHANGED
|
@@ -2497,6 +2497,17 @@ function zeroCommitVerdict(commitsAhead, dbOnly) {
|
|
|
2497
2497
|
async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
2498
2498
|
const branch = currentBranch();
|
|
2499
2499
|
if (!branch || branch === 'main' || branch === 'HEAD') {
|
|
2500
|
+
// task 1002723: an --allow-empty / --db-only ship legitimately runs FROM the
|
|
2501
|
+
// main checkout (nothing was ever committed), but this bail fired BEFORE the
|
|
2502
|
+
// deliberate-empty landing below could — the task parked at confirmed with no
|
|
2503
|
+
// branch and no published tip, a shape the server sweep can never prove (the
|
|
2504
|
+
// task-1002716 strand). Land it here iff the checkout has zero commits ahead
|
|
2505
|
+
// of origin/main; anything else keeps the safe bail.
|
|
2506
|
+
if (dbOnly && shellOk('git fetch origin main')
|
|
2507
|
+
&& zeroCommitVerdict(gitOk('git rev-list origin/main..HEAD --count'), true) === 'db-only') {
|
|
2508
|
+
console.log(' auto-merge: deliberate empty ship (--db-only / --allow-empty) from the main checkout - nothing to merge; advancing to shipped without merge/deploy.');
|
|
2509
|
+
return landed();
|
|
2510
|
+
}
|
|
2500
2511
|
console.log(` auto-merge: skipped (current branch is ${branch})`);
|
|
2501
2512
|
// The merge source IS the current branch, so on main there is nothing to
|
|
2502
2513
|
// merge. Half of the single-checkout catch-22 this task documents: the other
|
package/src/module-api.js
CHANGED
|
@@ -575,8 +575,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
575
575
|
// landed since 1.19.72 with no explicit bump. run 31619275904. (task 1002620)
|
|
576
576
|
// 1.19.74 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
577
577
|
// landed since 1.19.73 with no explicit bump. run 31628379432. (task 1002620)
|
|
578
|
+
// 1.19.75 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
579
|
+
// landed since 1.19.74 with no explicit bump. run 31632769733. (task 1002620)
|
|
580
|
+
// 1.19.76 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
581
|
+
// landed since 1.19.75 with no explicit bump. run 31637719876. (task 1002620)
|
|
578
582
|
// ---------------------------------------------------------------------------
|
|
579
|
-
const CORE_VERSION = '1.19.
|
|
583
|
+
const CORE_VERSION = '1.19.76'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
580
584
|
|
|
581
585
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
582
586
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -187,6 +187,26 @@ test('findTaskPullRequest: "task <id>: …" title → returns the REAL head bran
|
|
|
187
187
|
assert.equal(r.pr_number, 36);
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
test('findTaskPullRequest 1002723: a commit-subject title with a "(task <id>)" suffix binds (the PR #89 miss)', async () => {
|
|
191
|
+
const r = await gh.findTaskPullRequest(
|
|
192
|
+
{ taskId: 1002702 },
|
|
193
|
+
depsWithPulls([
|
|
194
|
+
pull('docs: session log — QA walk, 23 findings (task 1002702)', 'task-1002702', { merged: true, number: 89 }),
|
|
195
|
+
])
|
|
196
|
+
);
|
|
197
|
+
assert.ok(r, 'the suffix convention must bind');
|
|
198
|
+
assert.equal(r.branch, 'task-1002702');
|
|
199
|
+
assert.equal(r.pr_number, 89);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('findTaskPullRequest 1002723: a bare UNPARENTHESIZED mid-title id still does NOT bind', async () => {
|
|
203
|
+
const r = await gh.findTaskPullRequest(
|
|
204
|
+
{ taskId: 55 },
|
|
205
|
+
depsWithPulls([pull('fix the thing near task 55 handling', 'other-branch')])
|
|
206
|
+
);
|
|
207
|
+
assert.equal(r, null, 'only the ^task-prefix or (task <id>) parenthesized forms are title-level binds');
|
|
208
|
+
});
|
|
209
|
+
|
|
190
210
|
test('findTaskPullRequest: a body-only mention must NOT bind (tighter than prReferencesTask)', async () => {
|
|
191
211
|
const r = await gh.findTaskPullRequest(
|
|
192
212
|
{ taskId: 55 },
|
|
@@ -59,6 +59,7 @@ function mkDeps({
|
|
|
59
59
|
refreshStaleImpl,
|
|
60
60
|
updateByBranch = {},
|
|
61
61
|
healAttempts = new Map(),
|
|
62
|
+
branchHome = new Map(),
|
|
62
63
|
} = {}) {
|
|
63
64
|
const calls = { ship: [], broadcast: [], status: [], merge: [], ancestry: [], search: [], resolve: [], refresh: [], update: [], flag: [], events: [], logs: [], errors: [] };
|
|
64
65
|
// isSafeBranch mirrors github-push's pattern enough for the test (rejects
|
|
@@ -189,6 +190,8 @@ function mkDeps({
|
|
|
189
190
|
// task 1915: per-branch treadmill guard, injected so the cap is deterministic and
|
|
190
191
|
// never leaks module state across tests.
|
|
191
192
|
healAttempts,
|
|
193
|
+
// task 1002723: per-worktree branch-home memo, injected for the same reason.
|
|
194
|
+
branchHome,
|
|
192
195
|
log: (m) => calls.logs.push(m),
|
|
193
196
|
error: (m) => calls.errors.push(m),
|
|
194
197
|
_calls: calls,
|
|
@@ -1194,7 +1197,10 @@ test('1002645 THE PIN: a worktree-named branch (laptop wave ship) lands UNATTEND
|
|
|
1194
1197
|
const r = await reconciler.runSweep(deps);
|
|
1195
1198
|
assert.equal(r.reconciled, 1, 'the wave-5 strand shape must land with NO human step (ADR 0162)');
|
|
1196
1199
|
assert.deepEqual(deps._calls.ship, [{ taskId: 2645, builderId: 95 }]);
|
|
1197
|
-
|
|
1200
|
+
// task 1002723: worktree-<wt> is now a first-class branch CANDIDATE, so this shape
|
|
1201
|
+
// resolves before the title fallback is ever needed (the fallback stays for PRs on
|
|
1202
|
+
// foreign branch names). The pin's intent — lands unattended — is what matters.
|
|
1203
|
+
assert.equal(deps._calls.search.length, 0, 'candidate branches resolve it without the title search');
|
|
1198
1204
|
assert.ok(deps._calls.merge.includes('worktree-task-2645'),
|
|
1199
1205
|
'the merge nudge engages on the REAL branch — the only merger when native auto-merge is disallowed');
|
|
1200
1206
|
});
|
|
@@ -1271,6 +1277,86 @@ test('1002645 the reuse guard still holds through the fallback: adopted PR merge
|
|
|
1271
1277
|
assert.equal(deps._calls.ship.length, 0);
|
|
1272
1278
|
});
|
|
1273
1279
|
|
|
1280
|
+
// --- task 1002723 — the 1002702/1002716 strand pair: bare-branch candidates +
|
|
1281
|
+
// the always-on disposition line (silence must not masquerade as health) --------
|
|
1282
|
+
|
|
1283
|
+
test('1002723 THE PIN: a PR on the BARE worktree branch (task-<id>, no prefix) lands unattended', async () => {
|
|
1284
|
+
// The 1002702 strand: ship pushed branch task-1002702 (== worktree_name), PR #89
|
|
1285
|
+
// merged, and the sweep saw pr_state:none on claude/task-1002702 forever while
|
|
1286
|
+
// the title search missed the commit-subject PR title. The bare name is now a
|
|
1287
|
+
// first-class candidate.
|
|
1288
|
+
const deps = mkDeps({
|
|
1289
|
+
rows: [{ task_id: 77, builder_id: 3, worktree_name: 'task-77', confirmed_at: new Date().toISOString() }],
|
|
1290
|
+
statusByBranch: { 'task-77': landedPrFor(77) },
|
|
1291
|
+
});
|
|
1292
|
+
const r = await reconciler.runSweep(deps);
|
|
1293
|
+
assert.equal(r.reconciled, 1, 'the merged-PR-on-bare-branch shape must land with no manual flip');
|
|
1294
|
+
assert.ok(deps._calls.status.includes('claude/task-77'), 'dev-box convention tried first');
|
|
1295
|
+
assert.ok(deps._calls.status.includes('task-77'), 'then the bare worktree branch');
|
|
1296
|
+
assert.deepEqual(deps._calls.ship, [{ taskId: 77, builderId: 3 }]);
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
test('1002723 every confirmed task gets a per-sweep disposition entry — including silent young unprovables', async () => {
|
|
1300
|
+
const deps = mkDeps({
|
|
1301
|
+
rows: [
|
|
1302
|
+
// the 1002716 shape: allow-empty ship — no branch, no tip, young
|
|
1303
|
+
{ task_id: 91, builder_id: 3, worktree_name: null, confirmed_at: new Date().toISOString() },
|
|
1304
|
+
// a young task whose branches have no PR anywhere yet
|
|
1305
|
+
{ task_id: 92, builder_id: 3, worktree_name: 'wt-92', confirmed_at: new Date().toISOString() },
|
|
1306
|
+
],
|
|
1307
|
+
});
|
|
1308
|
+
await reconciler.runSweep(deps);
|
|
1309
|
+
const line = deps._calls.logs.find((l) => typeof l === 'string' && l.includes('dispositions:'));
|
|
1310
|
+
assert.ok(line, 'a dispositions line is logged whenever confirmed tasks were checked');
|
|
1311
|
+
assert.ok(line.includes('91:no_branch_no_tip'), `task 91 named with its reason in: ${line}`);
|
|
1312
|
+
assert.ok(line.includes('92:land_not_proven'), `task 92 named with its reason in: ${line}`);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
test('1002723 the winning branch convention is MEMOIZED across sweeps (steady-state back to ~2 calls)', async () => {
|
|
1316
|
+
const branchHome = new Map();
|
|
1317
|
+
const row = { task_id: 78, builder_id: 3, worktree_name: 'task-78', confirmed_at: new Date().toISOString() };
|
|
1318
|
+
const first = mkDeps({ rows: [row], statusByBranch: { 'task-78': PENDING }, branchHome });
|
|
1319
|
+
await reconciler.runSweep(first);
|
|
1320
|
+
assert.ok(first._calls.status.includes('claude/task-78'), 'first sweep walks the candidates');
|
|
1321
|
+
const second = mkDeps({ rows: [row], statusByBranch: { 'task-78': PENDING }, branchHome });
|
|
1322
|
+
await reconciler.runSweep(second);
|
|
1323
|
+
assert.equal(second._calls.status[0], 'task-78', 'second sweep probes the memoized home first');
|
|
1324
|
+
assert.ok(!second._calls.status.includes('claude/task-78'), 'losing candidates are not re-probed');
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
test('1002723 a NO-PR worktree walks the candidates once, then probes only its pinned home (title search stays the discovery channel)', async () => {
|
|
1328
|
+
const branchHome = new Map();
|
|
1329
|
+
const row = { task_id: 80, builder_id: 3, worktree_name: 'task-80', confirmed_at: new Date().toISOString() };
|
|
1330
|
+
const first = mkDeps({ rows: [row], branchHome }); // no PR on any branch
|
|
1331
|
+
await reconciler.runSweep(first);
|
|
1332
|
+
assert.equal(first._calls.status.length, 3, 'first sweep walks all three candidates');
|
|
1333
|
+
assert.equal(branchHome.get('task-80'), 'claude/task-80', 'conventional candidate pinned as home');
|
|
1334
|
+
const second = mkDeps({ rows: [row], branchHome });
|
|
1335
|
+
await reconciler.runSweep(second);
|
|
1336
|
+
assert.deepEqual(second._calls.status, ['claude/task-80'], 'later sweeps probe ONE branch, not three');
|
|
1337
|
+
assert.ok(second._calls.search.includes(80), 'the title-search fallback still runs — late PRs are discoverable');
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
test('1002723 the branch-home memo is pruned when its worktree leaves the confirmed set', async () => {
|
|
1341
|
+
const branchHome = new Map();
|
|
1342
|
+
const row = { task_id: 79, builder_id: 3, worktree_name: 'task-79', confirmed_at: new Date().toISOString() };
|
|
1343
|
+
await reconciler.runSweep(mkDeps({ rows: [row], statusByBranch: { 'task-79': PENDING }, branchHome }));
|
|
1344
|
+
assert.ok(branchHome.has('task-79'), 'memoized while confirmed');
|
|
1345
|
+
await reconciler.runSweep(mkDeps({ rows: [], branchHome }));
|
|
1346
|
+
assert.equal(branchHome.size, 0, 'pruned once the worktree leaves the set');
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
test('1002723 a shipped task appears in the dispositions line as shipped', async () => {
|
|
1350
|
+
const deps = mkDeps({
|
|
1351
|
+
rows: [{ task_id: 93, builder_id: 3, worktree_name: 'wt-93', published_head_sha: ON_MAIN }],
|
|
1352
|
+
statusByBranch: { 'claude/wt-93': LANDED },
|
|
1353
|
+
ancestryBySha: { [ON_MAIN]: true },
|
|
1354
|
+
});
|
|
1355
|
+
await reconciler.runSweep(deps);
|
|
1356
|
+
const line = deps._calls.logs.find((l) => typeof l === 'string' && l.includes('dispositions:'));
|
|
1357
|
+
assert.ok(line && line.includes('93:shipped'), `shipped disposition in: ${line}`);
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1274
1360
|
// Drain the microtask queue, then report + exit with the right code.
|
|
1275
1361
|
setTimeout(() => {
|
|
1276
1362
|
console.log(`\npublish reconciler (task 1134 + 1915) tests: ${passed} passed, ${failed} failed`);
|