@bongos/core 1.19.204 → 1.19.206
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 +29 -29
- package/.claude/skills/new-project/SKILL.md +3 -1
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +2 -0
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -2
- package/docs/api/openapi.json +4 -2
- package/docs/recipes/ops-gotchas.md +8 -1
- package/modules/lifecycle/github-push.js +130 -18
- package/modules/lifecycle/publish-reconciler.js +44 -1
- package/modules/lifecycle/routes/tasks.js +21 -4
- package/modules/provisioning/onboard-plan.js +1 -0
- package/modules/provisioning/provisioning.js +18 -8
- package/modules/provisioning/routes/provisioning.js +4 -0
- package/modules/provisioning/tests/provisioning.mjs +5 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/projects.html +19 -2
- package/scripts/gds/onboard.js +11 -3
- package/scripts/gds/provision.js +27 -4
- package/src/module-api.js +5 -1
- package/tests/onboard.mjs +5 -1
- package/tests/provision.mjs +30 -2
- package/tests/publish_branch_route.mjs +153 -2
- package/tests/publish_reconciler.mjs +63 -0
- package/tests/tool_scan_engine.mjs +13 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.206",
|
|
6
|
+
"core_contract": "1.19.206",
|
|
7
|
+
"source_commit": "6ef49e744b96d37db9a338979fb85bb56fb6aa6f",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-17T20:15:14.217Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 310,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1627,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "f7017bc51e10af4dd65ab8df53ce22c7a0e611c7fb948ee6598ce8c4f7831a6a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
{
|
|
188
188
|
"path": ".claude/skills/new-project/SKILL.md",
|
|
189
189
|
"mode": "0000644",
|
|
190
|
-
"sha256": "
|
|
190
|
+
"sha256": "9b36d441850c335fa634d892088e4c15a2a87acaa9e52808b74a8b2e6e9af39f"
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
"path": ".claude/skills/otb-character-review/SKILL.md",
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "38ef88079d260b67161e680ea77fa6ddecb3cc74a1452796cbe28c0e3601c052"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -707,7 +707,7 @@
|
|
|
707
707
|
{
|
|
708
708
|
"path": "docs/adr/0055-server-mediated-branch-publish.md",
|
|
709
709
|
"mode": "0000644",
|
|
710
|
-
"sha256": "
|
|
710
|
+
"sha256": "11ee013554922e0f9dadb582373c22d4aaa6433ddcb14af28386c4843a39497f"
|
|
711
711
|
},
|
|
712
712
|
{
|
|
713
713
|
"path": "docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md",
|
|
@@ -1137,7 +1137,7 @@
|
|
|
1137
1137
|
{
|
|
1138
1138
|
"path": "docs/adr/0133-github-app-manifest-onboarding-signin.md",
|
|
1139
1139
|
"mode": "0000644",
|
|
1140
|
-
"sha256": "
|
|
1140
|
+
"sha256": "dbd5b30d75631ba2754f4e7da0d43c03b1ed998b4082e8df23de0d7e7d5c5305"
|
|
1141
1141
|
},
|
|
1142
1142
|
{
|
|
1143
1143
|
"path": "docs/adr/0134-private-first-npm-distribution.md",
|
|
@@ -1372,7 +1372,7 @@
|
|
|
1372
1372
|
{
|
|
1373
1373
|
"path": "docs/api/openapi.json",
|
|
1374
1374
|
"mode": "0000644",
|
|
1375
|
-
"sha256": "
|
|
1375
|
+
"sha256": "c11ec9dcb0726382e15bb7fc9e74086037ed158256ef16b5efcc81fe2db2d078"
|
|
1376
1376
|
},
|
|
1377
1377
|
{
|
|
1378
1378
|
"path": "docs/architecture.md",
|
|
@@ -1672,7 +1672,7 @@
|
|
|
1672
1672
|
{
|
|
1673
1673
|
"path": "docs/recipes/ops-gotchas.md",
|
|
1674
1674
|
"mode": "0000644",
|
|
1675
|
-
"sha256": "
|
|
1675
|
+
"sha256": "d9956abefe8496cbbb8bbc94361431d40823c3a9d1fa8f4145e8b48999b54672"
|
|
1676
1676
|
},
|
|
1677
1677
|
{
|
|
1678
1678
|
"path": "docs/recipes/overnight-builder.md",
|
|
@@ -3567,7 +3567,7 @@
|
|
|
3567
3567
|
{
|
|
3568
3568
|
"path": "modules/lifecycle/github-push.js",
|
|
3569
3569
|
"mode": "0000644",
|
|
3570
|
-
"sha256": "
|
|
3570
|
+
"sha256": "127b6666bfbc4b85baebe85b0bf5501ad73b517faf4dafbc1a76a5535ac04ebc"
|
|
3571
3571
|
},
|
|
3572
3572
|
{
|
|
3573
3573
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -3622,7 +3622,7 @@
|
|
|
3622
3622
|
{
|
|
3623
3623
|
"path": "modules/lifecycle/publish-reconciler.js",
|
|
3624
3624
|
"mode": "0000644",
|
|
3625
|
-
"sha256": "
|
|
3625
|
+
"sha256": "1cca02e8eff2b0a7fdc1fa06ad6be2f633e25a29bdd10dddf6147610fdae087a"
|
|
3626
3626
|
},
|
|
3627
3627
|
{
|
|
3628
3628
|
"path": "modules/lifecycle/routes/analytics.js",
|
|
@@ -3662,7 +3662,7 @@
|
|
|
3662
3662
|
{
|
|
3663
3663
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
3664
3664
|
"mode": "0000644",
|
|
3665
|
-
"sha256": "
|
|
3665
|
+
"sha256": "95193b64dcf1121f10fbc871a07cb7997a27a64d3f7b2176957abdae28979729"
|
|
3666
3666
|
},
|
|
3667
3667
|
{
|
|
3668
3668
|
"path": "modules/lifecycle/routes/versions.js",
|
|
@@ -3957,7 +3957,7 @@
|
|
|
3957
3957
|
{
|
|
3958
3958
|
"path": "modules/provisioning/onboard-plan.js",
|
|
3959
3959
|
"mode": "0000644",
|
|
3960
|
-
"sha256": "
|
|
3960
|
+
"sha256": "de7927d2a3c1aac0d40ec5b1ac6c2e98e87cb593c4d3d2bd6ddfe3696371bff6"
|
|
3961
3961
|
},
|
|
3962
3962
|
{
|
|
3963
3963
|
"path": "modules/provisioning/pollers/liveness-sweep.js",
|
|
@@ -3967,7 +3967,7 @@
|
|
|
3967
3967
|
{
|
|
3968
3968
|
"path": "modules/provisioning/provisioning.js",
|
|
3969
3969
|
"mode": "0000644",
|
|
3970
|
-
"sha256": "
|
|
3970
|
+
"sha256": "e43977d0c5458de4fde91e6ccb784b112187545fdf03ceed2c352b605686a57e"
|
|
3971
3971
|
},
|
|
3972
3972
|
{
|
|
3973
3973
|
"path": "modules/provisioning/rate-limit.js",
|
|
@@ -3977,7 +3977,7 @@
|
|
|
3977
3977
|
{
|
|
3978
3978
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
3979
3979
|
"mode": "0000644",
|
|
3980
|
-
"sha256": "
|
|
3980
|
+
"sha256": "f7cf016e10587302437f5d8ab7b01d8b1be6694e05b9e8c47a6ccb2cdfda8ac4"
|
|
3981
3981
|
},
|
|
3982
3982
|
{
|
|
3983
3983
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -3987,7 +3987,7 @@
|
|
|
3987
3987
|
{
|
|
3988
3988
|
"path": "modules/provisioning/tests/provisioning.mjs",
|
|
3989
3989
|
"mode": "0000644",
|
|
3990
|
-
"sha256": "
|
|
3990
|
+
"sha256": "4c7bb37582485445e947e233c9fbb9c20c9ab55da9dd159c8062fffa4472a5d7"
|
|
3991
3991
|
},
|
|
3992
3992
|
{
|
|
3993
3993
|
"path": "modules/provisioning/ui/.gitkeep",
|
|
@@ -4277,12 +4277,12 @@
|
|
|
4277
4277
|
{
|
|
4278
4278
|
"path": "package-lock.json",
|
|
4279
4279
|
"mode": "0000644",
|
|
4280
|
-
"sha256": "
|
|
4280
|
+
"sha256": "8cece7080d6a67c93467f86ee1c02cff85f8b2dbd5e8219519e9bc9df034f895"
|
|
4281
4281
|
},
|
|
4282
4282
|
{
|
|
4283
4283
|
"path": "package.json",
|
|
4284
4284
|
"mode": "0000644",
|
|
4285
|
-
"sha256": "
|
|
4285
|
+
"sha256": "05a1768a85ccd224b9caa4e388b83f5a1d933b8b46761ebe7ce6f5a06f3c6d6a"
|
|
4286
4286
|
},
|
|
4287
4287
|
{
|
|
4288
4288
|
"path": "public-docs/index.html",
|
|
@@ -4542,7 +4542,7 @@
|
|
|
4542
4542
|
{
|
|
4543
4543
|
"path": "public-landing/projects.html",
|
|
4544
4544
|
"mode": "0000644",
|
|
4545
|
-
"sha256": "
|
|
4545
|
+
"sha256": "494d1aa1a6ff006a8e8f239c29d90c052f735919937c9dc6fb6b8eeff4902e91"
|
|
4546
4546
|
},
|
|
4547
4547
|
{
|
|
4548
4548
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5152,7 +5152,7 @@
|
|
|
5152
5152
|
{
|
|
5153
5153
|
"path": "scripts/gds/onboard.js",
|
|
5154
5154
|
"mode": "0000644",
|
|
5155
|
-
"sha256": "
|
|
5155
|
+
"sha256": "71ee037f1e893a1bcbcaca95e6bdc80c5eb4f5d3e613b59b95e1272d673d3d12"
|
|
5156
5156
|
},
|
|
5157
5157
|
{
|
|
5158
5158
|
"path": "scripts/gds/onboarding-analyze.js",
|
|
@@ -5197,7 +5197,7 @@
|
|
|
5197
5197
|
{
|
|
5198
5198
|
"path": "scripts/gds/provision.js",
|
|
5199
5199
|
"mode": "0000644",
|
|
5200
|
-
"sha256": "
|
|
5200
|
+
"sha256": "242f51c3e2ae991dc8a02d7285c829d3cd7b2a4022c516b218d259cac14b855f"
|
|
5201
5201
|
},
|
|
5202
5202
|
{
|
|
5203
5203
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -5892,7 +5892,7 @@
|
|
|
5892
5892
|
{
|
|
5893
5893
|
"path": "src/module-api.js",
|
|
5894
5894
|
"mode": "0000644",
|
|
5895
|
-
"sha256": "
|
|
5895
|
+
"sha256": "84441072e12b08bf8c5dde09972dc91dd530b586f30f287016d830589476fb5d"
|
|
5896
5896
|
},
|
|
5897
5897
|
{
|
|
5898
5898
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7377,7 +7377,7 @@
|
|
|
7377
7377
|
{
|
|
7378
7378
|
"path": "tests/onboard.mjs",
|
|
7379
7379
|
"mode": "0000644",
|
|
7380
|
-
"sha256": "
|
|
7380
|
+
"sha256": "c6823a6f0f81bcb4f01ccbd2364d6dcf76eb21e3ff10972738f792438939dee0"
|
|
7381
7381
|
},
|
|
7382
7382
|
{
|
|
7383
7383
|
"path": "tests/onboarding_gate.mjs",
|
|
@@ -7517,7 +7517,7 @@
|
|
|
7517
7517
|
{
|
|
7518
7518
|
"path": "tests/provision.mjs",
|
|
7519
7519
|
"mode": "0000644",
|
|
7520
|
-
"sha256": "
|
|
7520
|
+
"sha256": "30b55efaa97e695603875ec057901d5333977d41705fc862fc831a424c4e2674"
|
|
7521
7521
|
},
|
|
7522
7522
|
{
|
|
7523
7523
|
"path": "tests/provisioning_default_domain.mjs",
|
|
@@ -7572,7 +7572,7 @@
|
|
|
7572
7572
|
{
|
|
7573
7573
|
"path": "tests/publish_branch_route.mjs",
|
|
7574
7574
|
"mode": "0000644",
|
|
7575
|
-
"sha256": "
|
|
7575
|
+
"sha256": "485b2b965bcf44a515571de43851c42a32f278dcdd02510f0d3cd9f6ac53e61c"
|
|
7576
7576
|
},
|
|
7577
7577
|
{
|
|
7578
7578
|
"path": "tests/publish_manifest.mjs",
|
|
@@ -7582,7 +7582,7 @@
|
|
|
7582
7582
|
{
|
|
7583
7583
|
"path": "tests/publish_reconciler.mjs",
|
|
7584
7584
|
"mode": "0000644",
|
|
7585
|
-
"sha256": "
|
|
7585
|
+
"sha256": "1dfca44ba378b69041894371c1a3765eba40edbdec18570d7b67cc849bcb9e8a"
|
|
7586
7586
|
},
|
|
7587
7587
|
{
|
|
7588
7588
|
"path": "tests/rank_gate.mjs",
|
|
@@ -8077,7 +8077,7 @@
|
|
|
8077
8077
|
{
|
|
8078
8078
|
"path": "tests/tool_scan_engine.mjs",
|
|
8079
8079
|
"mode": "0000644",
|
|
8080
|
-
"sha256": "
|
|
8080
|
+
"sha256": "10a6a41cafcba32406be1aa79e0be1ace1cde2c4163bf0b1a0efd1e5e8f8c403"
|
|
8081
8081
|
},
|
|
8082
8082
|
{
|
|
8083
8083
|
"path": "tests/touches_overlap.mjs",
|
|
@@ -9,7 +9,7 @@ You are guiding a new owner from **zero to a live, owned Cloud Bongos instance**
|
|
|
9
9
|
|
|
10
10
|
A new instance is **standalone** (ADR 0108): it runs from its **own git repo** with `@cloudbongos/core` installed as a **pinned npm dependency**, not a fork of this monorepo. Central updates are a `bongos upgrade` version-pin bump, not a merge. Every step below assumes this shape (`hosting_shape=standalone`).
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Five steps are **irreducibly human** and must be rendered as **precise, UI-first, copy-paste-safe prompts** (a non-technical owner does them with browser clicks, no terminal guessing): (1) create the GitHub repo, (2) approve the pre-filled **GitHub App** for sign-in (ONE click — the App Manifest flow, task 2080; no secret to paste), (3) install that App on the repo (ONE click — the publish credential, task 1003049), (4) own a domain, (5) the first sign-in. Everything else is automated. Never ask the owner to do in a terminal what they can do with a click.
|
|
13
13
|
|
|
14
14
|
## The canonical sequence
|
|
15
15
|
|
|
@@ -85,6 +85,8 @@ node scripts/gds/oauth-secret.js place <slug> --client-id <id> --client-secret <
|
|
|
85
85
|
|
|
86
86
|
> **Guidance the standup learned:** do **NOT** hand-edit `web.env` with a shell `read -s` — during the Emersonian standup that silently left a 26-char truncated secret that broke sign-in with no obvious cause. `oauth-secret` validates the length/format (catching that truncation), rewrites the env line via Node, restarts the service, then **probes `/healthz` for `auth_configured:true`** before it reports success. If it refuses, the creds are wrong — recheck them; do not proceed.
|
|
87
87
|
|
|
88
|
+
**Human-only — install the GitHub App on the repo (task 1003049, ADR 0055).** The one-click App doubles as the instance's **publish credential**: the manifest requests Contents rw + PR rw + Actions read, the runner places `GITHUB_APP_ID` + `GITHUB_APP_PRIVATE_KEY` in `web.env`, and the server discovers the installation id on its own — but only an **installed** App can mint push tokens. One click: the wizard's **Install the app on your repo** button (`github.com/apps/<slug>/installations/new`), or GitHub → Settings → Developer settings → GitHub Apps → your app → **Install App** → the project repo. Skip it and every ship strands at `confirmed` behind a manual merge. (Manual OAuth-app path instead? Set a fine-grained `GITHUB_PUSH_TOKEN` in `web.env` — ADR 0055.)
|
|
89
|
+
|
|
88
90
|
**Human-only — first sign-in.** The owner opens `https://<domain>` and signs in with GitHub **once**. This seats the **founding Archon** — config alone seats no one, so this step is inherent and cannot be automated.
|
|
89
91
|
|
|
90
92
|
### 7. Verify
|
|
@@ -72,7 +72,7 @@ export interface GetProfilesHandleResponse { account: unknown; cross_project: un
|
|
|
72
72
|
export interface GetProjectsFeaturedResponse { projects: unknown }
|
|
73
73
|
export interface GetProjectsResponse { projects: unknown }
|
|
74
74
|
export interface GetProvisioningFleetResponse { instances: unknown }
|
|
75
|
-
export interface GetProvisioningGithubAppStatusResponse { ok: boolean; status: unknown; ready: unknown; terminal: unknown; domain: unknown; instance_status: unknown }
|
|
75
|
+
export interface GetProvisioningGithubAppStatusResponse { ok: boolean; status: unknown; ready: unknown; terminal: unknown; domain: unknown; instance_status: unknown; app_slug: unknown }
|
|
76
76
|
export interface GetProvisioningHealthResponse { module: unknown; ok: boolean; coreVersion: unknown }
|
|
77
77
|
export interface GetProvisioningInstancesIdResponse { instance: unknown; open_intent: unknown }
|
|
78
78
|
export interface GetProvisioningInstancesResponse { instances: unknown }
|
|
@@ -42,6 +42,8 @@ A **fine-grained GitHub PAT** scoped to the single repo: `Contents: read/write`
|
|
|
42
42
|
>
|
|
43
43
|
> Check it with `node scripts/gds/publish-credential-check.js` on the host — it lists the repo's workflows as a control call first, so `blind` and `readable_no_workflow` are distinguishable. Task 1002724 makes a blind probe non-fatal (the reconciler falls back to the git-history land proof), but a blind credential means the deploy gate is **not being enforced**, only bypassed.
|
|
44
44
|
|
|
45
|
+
> **Update (2026-08-17, task 1003049 — fresh instances get the credential from the manifest App; installation id auto-discovered):** a wizard-born instance had NO publish credential at all — the ADR 0133 manifest requested zero permissions and the conversion's `pem` was discarded — so on every new project the auto-merge + reconciler were dead on arrival and each ship stranded at `confirmed` behind manual archon surgery (gap-list W7, proven live on Spike Harbor). Now: the manifest requests exactly this ADR's scopes (Contents rw + PR rw + Actions read), the control-plane runner places `GITHUB_APP_ID` + `GITHUB_APP_PRIVATE_KEY` into the instance's web.env alongside the OAuth creds, and `github-push.js` **discovers the installation id at runtime** (`GET /repos/{owner}/{repo}/installation` with the App JWT; the env var still wins when set) — so `GITHUB_APP_INSTALLATION_ID` is no longer required. The one human step is installing the App on the repo (the onboarding `app-install` step; the wizard renders the one-click `github.com/apps/<slug>/installations/new` link). <redacted> is a first-class loud state: routes answer a plain-sentence 503 naming the install URL, and the publish-reconciler warns once per streak instead of silently parking ships. **Security trade-off (accepted):** the App is public (the W6 sign-in fix), so third parties can install it on their own repos — those installs sit inside a leaked App key's blast radius even though this server only ever uses its own repo's installation. Mitigation: the reconciler's foreign-install watch (`github-push.foreignInstallations`) warns loudly whenever the App is installed anywhere beyond the repo owner. Pre-existing instances: see the ops-gotchas entry (grant the permissions + generate a key by hand, or use a PAT).
|
|
46
|
+
|
|
45
47
|
## Consequences
|
|
46
48
|
|
|
47
49
|
- A dev-box builder ships end-to-end: `/builder-reauth` → claim → edit → `/builder-stage` ([ADR 0046](<redacted>.md)) → `/builder-ship`, with the GDS doing the push. The box holds no GitHub credential, so the [ADR 0053](0053-scoped-dev-box-session.md) blast-radius argument still holds.
|
|
@@ -31,9 +31,9 @@ The conversion endpoint is unauthenticated (the code is the auth), so the web ti
|
|
|
31
31
|
|
|
32
32
|
### Manifest shape
|
|
33
33
|
|
|
34
|
-
`redirect_url` = the **mothership** callback (the instance can't do the exchange). `callback_urls` = the **instance's** own `/api/gds/auth/web/callback` (what `auth.js` actually builds) **and** the `/api/bongos/...` alias, so sign-in works regardless of which prefix a build emits. `default_permissions: {}`
|
|
34
|
+
`redirect_url` = the **mothership** callback (the instance can't do the exchange). `callback_urls` = the **instance's** own `/api/gds/auth/web/callback` (what `auth.js` actually builds) **and** the `/api/bongos/...` alias, so sign-in works regardless of which prefix a build emits. `default_permissions: { contents: 'write', pull_requests: 'write', actions: 'read' }` — sign-in itself needs none, but the App doubles as the instance's ADR 0055 **publish credential** (task 1003049): the conversion's `pem` + app id land in web.env, and once the owner installs the App on the repo the server mints installation tokens to land shipped work itself. `hook_attributes.active:false`, `public:true`. Personal vs org differs only in the POST URL (derived from the repo owner); the App works for sign-in either way.
|
|
35
35
|
|
|
36
|
-
> **Amended 2026-08-17 (task 1003048, gap-list W6):** the manifest originally set `public:false`. That made every wizard-born project structurally single-builder on the web — GitHub answers a private App's authorize URL for any non-owner account with a bare 404, before the instance is ever involved (reproduced live on Spike Harbor). The App
|
|
36
|
+
> **Amended 2026-08-17 (task 1003048, gap-list W6):** the manifest originally set `public:false`. That made every wizard-born project structurally single-builder on the web — GitHub answers a private App's authorize URL for any non-owner account with a bare 404, before the instance is ever involved (reproduced live on Spike Harbor). The App requested zero permissions at the time, so public cost nothing; the manifest now sets `public:true`. (Task 1003049 later added the ADR 0055 repo scopes, which changes the calculus: a public App with write scopes **can** be installed by third parties on their own repos, and a leaked App private key's blast radius includes those installs — the server itself only ever uses its own repo's installation, but the credential could reach further if compromised. Accepted to keep this fix; mitigated by the publish-reconciler's **foreign-install watch**, which warns loudly whenever the App is installed anywhere beyond the repo owner.) Apps minted before the fix must be flipped by hand (GitHub App settings → Advanced → Make public) — see the ops-gotchas entry.
|
|
37
37
|
|
|
38
38
|
**App name (tasks 2093 → 2095).** GitHub App names are globally unique across all of GitHub, so a bare project slug (e.g. `demo`) is almost always already taken. The auto name is the **instance domain** (e.g. `demo.cloudbongos.com`): a domain is inherently unique (so it never collides) and it reads perfectly on GitHub's sign-in consent screen, where the name is shown as *"`<name>` by `<owner>` … will redirect to `https://<domain>`"* — the name IS the destination. GitHub keeps the dots in the display name (only the internal slug is transformed) and caps it at 34 chars. **Fallback** (no domain, or a domain over 34 chars): brand-qualify the project — `<mothership brand> - <project>`, e.g. `Cloud Bongos - demo`, from the mothership's own `branding().identity.productName` (portable, not hardcoded) — still far less collision-prone than a bare slug. Either way it is only a default the owner can rename on GitHub's confirmation screen.
|
|
39
39
|
|
package/docs/api/openapi.json
CHANGED
|
@@ -13402,7 +13402,8 @@
|
|
|
13402
13402
|
"ready": {},
|
|
13403
13403
|
"terminal": {},
|
|
13404
13404
|
"domain": {},
|
|
13405
|
-
"instance_status": {}
|
|
13405
|
+
"instance_status": {},
|
|
13406
|
+
"app_slug": {}
|
|
13406
13407
|
},
|
|
13407
13408
|
"required": [
|
|
13408
13409
|
"ok",
|
|
@@ -13410,7 +13411,8 @@
|
|
|
13410
13411
|
"ready",
|
|
13411
13412
|
"terminal",
|
|
13412
13413
|
"domain",
|
|
13413
|
-
"instance_status"
|
|
13414
|
+
"instance_status",
|
|
13415
|
+
"app_slug"
|
|
13414
13416
|
]
|
|
13415
13417
|
},
|
|
13416
13418
|
"GetProvisioningHealthResponse": {
|
|
@@ -131,9 +131,16 @@ Organized by topic, not chronology. New entries go in the matching section; crea
|
|
|
131
131
|
|
|
132
132
|
- **Symptom:** On a wizard-created instance, any GitHub account other than the App's owner clicks "Sign in with GitHub" and gets a bare GitHub 404 — before the instance is ever involved. The project is effectively single-builder on the web. (Device Flow sign-in for the same non-owner works, which makes the failure look like anything but an App setting.)
|
|
133
133
|
- **Root cause:** the ADR 0133 manifest flow used to mint the GitHub App with `public: false`, and GitHub 404s a private App's authorize URL for every non-owner. Fixed for newly minted Apps (`buildGithubAppManifest` now sets `public: true` — gap-list W6, task 1003048), but a manifest can't be re-applied to an existing App.
|
|
134
|
-
- **Fix for pre-existing instances:** the App owner flips it by hand — github.com → Settings → Developer settings → GitHub Apps → (the instance's App) → **Advanced** → **Make public**. One click, no permission changes
|
|
134
|
+
- **Fix for pre-existing instances:** the App owner flips it by hand — github.com → Settings → Developer settings → GitHub Apps → (the instance's App) → **Advanced** → **Make public**. One click, no permission changes needed for sign-in.
|
|
135
135
|
- **Live evidence:** reproduced twice against Spike Harbor (instance 18) with a fresh verified-email account, 2026-08-16 — see `docs/audits/project-creation-gap-list.md` W6.
|
|
136
136
|
|
|
137
|
+
### Manifest-born GitHub Apps created before the task-1003049 fix are NOT a publish credential — every ship strands at `confirmed`
|
|
138
|
+
|
|
139
|
+
- **Symptom:** on a fresh instance every `/builder-ship` parks at `confirmed`; `GET /tasks/:id/publish-status` answers 503 `push_unconfigured`; the archon has to merge PRs and `POST /tasks/:id/ship` by hand. Silent — work just piles up (gap-list W7, proven live on Spike Harbor task 1).
|
|
140
|
+
- **Root cause:** the ADR 0133 manifest used to request **zero** repo permissions and the conversion's App private key (`pem`) was discarded, so no publish credential (ADR 0055) ever existed on a wizard-born instance. Fixed for newly minted Apps (task 1003049): the manifest now requests `contents: write` + `pull_requests: write` + `actions: read`, `placeManifestCreds` writes `GITHUB_APP_ID` + `GITHUB_APP_PRIVATE_KEY` into web.env, and the server discovers the installation id at runtime — the one human step left is **installing the App on the repo** (the onboarding `app-install` step).
|
|
141
|
+
- **Fix for pre-existing instances:** in the App's GitHub settings — (1) **Permissions & events**: grant Contents read/write, Pull requests read/write, Actions read-only; (2) **General → Private keys**: generate a key; (3) put `GITHUB_APP_ID=<id>` and `GITHUB_APP_PRIVATE_KEY=<pem as one line with literal \n>` in `/etc/<slug>/web.env` and restart the unit; (4) **Install App** → pick the project repo (owners must re-approve the new permissions on an already-installed App). Alternative: a fine-grained `GITHUB_PUSH_TOKEN` PAT with the same three scopes (ADR 0055).
|
|
142
|
+
- **How you know it worked:** `publish-status` stops 503ing, and the publish-reconciler log line `WARN … not installed` (if the App wasn't installed yet) goes quiet after the install.
|
|
143
|
+
|
|
137
144
|
---
|
|
138
145
|
|
|
139
146
|
## Mac / local dev
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
// THE SERVER CREDENTIAL (task 1028 / ADR 0055 hardening). Preferred: a GitHub
|
|
21
21
|
// APP — the server holds ONLY the App private key and mints a SHORT-LIVED,
|
|
22
22
|
// repo-scoped INSTALLATION token (1h) per push window, so there's no standing
|
|
23
|
-
// push secret at rest. Set GITHUB_APP_ID +
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
23
|
+
// push secret at rest. Set GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY (the
|
|
24
|
+
// installation id is discovered from the repo at runtime; GITHUB_APP_INSTALLATION_ID
|
|
25
|
+
// still wins when set — task 1003049). Fallback (v1, kept for cutover): a standing
|
|
26
|
+
// fine-grained PAT in GITHUB_PUSH_TOKEN. resolveToken() prefers the App, then the
|
|
27
|
+
// PAT; the App JWT is RS256-signed with node:crypto (no jsonwebtoken / Octokit
|
|
28
|
+
// dependency).
|
|
27
29
|
//
|
|
28
30
|
// This module is the server seam: push a bundle, open/find the PR, enable auto-
|
|
29
31
|
// merge, report status. All git runs through execFile arg-arrays (shell:false,
|
|
@@ -58,12 +60,12 @@ const SAFE_SHA_RE = /^[0-9a-f]{7,64}$/;
|
|
|
58
60
|
|
|
59
61
|
// Configured if EITHER credential is present: a GitHub App (preferred) or the
|
|
60
62
|
// v1 PAT fallback. The route 503s when neither is set (task 1028).
|
|
63
|
+
// GITHUB_APP_INSTALLATION_ID is OPTIONAL (task 1003049): a fresh instance's
|
|
64
|
+
// manifest-born App has no known installation id at cred-placement time, so the
|
|
65
|
+
// server discovers it at runtime from GET /repos/{owner}/{repo}/installation —
|
|
66
|
+
// the one human onboarding step left is installing the App on the repo.
|
|
61
67
|
function appConfigured() {
|
|
62
|
-
return !!(
|
|
63
|
-
process.env.GITHUB_APP_ID &&
|
|
64
|
-
process.env.GITHUB_APP_INSTALLATION_ID &&
|
|
65
|
-
process.env.GITHUB_APP_PRIVATE_KEY
|
|
66
|
-
);
|
|
68
|
+
return !!(process.env.GITHUB_APP_ID && process.env.GITHUB_APP_PRIVATE_KEY);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
function isConfigured() {
|
|
@@ -200,10 +202,76 @@ function mintAppJwt({ appId, privateKeyPem, nowSec }) {
|
|
|
200
202
|
|
|
201
203
|
// Module-level cache of the current installation token, reused until ~60s before
|
|
202
204
|
// expiry. Installation tokens are short-lived by construction (1h) and repo-
|
|
203
|
-
// scoped to the single repo this server serves.
|
|
205
|
+
// scoped to the single repo this server serves. The discovered installation id
|
|
206
|
+
// is cached separately — it only changes if the App is uninstalled/reinstalled,
|
|
207
|
+
// and a stale id fails the token exchange loudly (APP_TOKEN_FAILED), never silently.
|
|
204
208
|
let _instCache = { token: null, expEpoch: 0 };
|
|
209
|
+
let _instIdCache = null;
|
|
210
|
+
// Negative cache for APP_NOT_INSTALLED: during an active ship the publish-status
|
|
211
|
+
// poll fires every few seconds, and re-discovering a known-absent installation
|
|
212
|
+
// each time re-mints a JWT + two GitHub calls for the same answer.
|
|
213
|
+
const NOT_INSTALLED_TTL_MS = 60_000;
|
|
214
|
+
let _notInstalled = { untilMs: 0, err: null };
|
|
205
215
|
function _resetAppTokenCacheForTests() {
|
|
206
216
|
_instCache = { token: null, expEpoch: 0 };
|
|
217
|
+
_instIdCache = null;
|
|
218
|
+
_notInstalled = { untilMs: 0, err: null };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function appJwtHeaders(jwt) {
|
|
222
|
+
return {
|
|
223
|
+
Authorization: `Bearer ${jwt}`,
|
|
224
|
+
Accept: 'application/vnd.github+json',
|
|
225
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
226
|
+
'User-Agent': userAgent(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Discover the App's installation id on THIS repo (task 1003049). A manifest-born
|
|
231
|
+
// instance (ADR 0133) knows its App id + private key at cred-placement time but not
|
|
232
|
+
// the installation id — the owner installs the App as a separate onboarding step —
|
|
233
|
+
// so the server derives it live instead of requiring a third env var. Throws
|
|
234
|
+
// APP_NOT_INSTALLED (with a best-effort install_url) when the App exists but nobody
|
|
235
|
+
// has installed it on the repo yet: that is the one remaining human step, and the
|
|
236
|
+
// refusal must say so in a sentence, not a bare code.
|
|
237
|
+
async function discoverInstallationId(jwt, deps = {}) {
|
|
238
|
+
if (_instIdCache) return _instIdCache;
|
|
239
|
+
const nowMs = deps.now ? deps.now() : Date.now();
|
|
240
|
+
if (_notInstalled.err && nowMs < _notInstalled.untilMs) throw _notInstalled.err;
|
|
241
|
+
const info = deps.repoInfo || loadRepoInfo();
|
|
242
|
+
if (!info || info.error) throw err('NO_REPO_INFO');
|
|
243
|
+
const { owner, repo } = info;
|
|
244
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
245
|
+
const res = await doFetch(`https://api.github.com/repos/${owner}/${repo}/installation`, {
|
|
246
|
+
headers: appJwtHeaders(jwt),
|
|
247
|
+
});
|
|
248
|
+
if (res.status === 404) {
|
|
249
|
+
// The App is real (the JWT authenticated) but not installed on the repo.
|
|
250
|
+
// Fetch the App's own page for a clickable install URL (best-effort — the
|
|
251
|
+
// guidance sentence works without it).
|
|
252
|
+
let installUrl = null;
|
|
253
|
+
try {
|
|
254
|
+
const appRes = await doFetch('https://api.github.com/app', { headers: appJwtHeaders(jwt) });
|
|
255
|
+
if (appRes.ok) {
|
|
256
|
+
const app = await appRes.json();
|
|
257
|
+
if (app && app.html_url) installUrl = `${app.html_url}/installations/new`;
|
|
258
|
+
}
|
|
259
|
+
} catch (_) { /* best-effort */ }
|
|
260
|
+
const e = err('APP_NOT_INSTALLED', { owner, repo, install_url: installUrl });
|
|
261
|
+
_notInstalled = { untilMs: nowMs + NOT_INSTALLED_TTL_MS, err: e };
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
if (!res.ok) {
|
|
265
|
+
const detail = await res.text().catch(() => '');
|
|
266
|
+
throw err('APP_TOKEN_FAILED', { status: res.status, detail: detail.slice(0, 200) });
|
|
267
|
+
}
|
|
268
|
+
const data = await res.json();
|
|
269
|
+
if (!data || data.id == null) {
|
|
270
|
+
throw err('APP_TOKEN_FAILED', { status: res.status, detail: 'installation lookup returned no id' });
|
|
271
|
+
}
|
|
272
|
+
_notInstalled = { untilMs: 0, err: null };
|
|
273
|
+
_instIdCache = String(data.id);
|
|
274
|
+
return _instIdCache;
|
|
207
275
|
}
|
|
208
276
|
|
|
209
277
|
// Returns a fresh/cached App installation token, or null if the App isn't
|
|
@@ -219,20 +287,22 @@ async function getInstallationToken(deps = {}) {
|
|
|
219
287
|
privateKeyPem: process.env.GITHUB_APP_PRIVATE_KEY,
|
|
220
288
|
nowSec,
|
|
221
289
|
});
|
|
290
|
+
// An explicit env id wins (the ADR 0055 v1 shape); otherwise discover it from
|
|
291
|
+
// the repo (task 1003049 — fresh instances never know it up front).
|
|
292
|
+
const instId = process.env.GITHUB_APP_INSTALLATION_ID || (await discoverInstallationId(jwt, deps));
|
|
222
293
|
const doFetch = deps.fetchImpl || fetch;
|
|
223
294
|
const res = await doFetch(
|
|
224
|
-
`https://api.github.com/app/installations/${
|
|
295
|
+
`https://api.github.com/app/installations/${instId}/access_tokens`,
|
|
225
296
|
{
|
|
226
297
|
method: 'POST',
|
|
227
|
-
headers:
|
|
228
|
-
Authorization: `Bearer ${jwt}`,
|
|
229
|
-
Accept: 'application/vnd.github+json',
|
|
230
|
-
'X-GitHub-Api-Version': '2022-11-28',
|
|
231
|
-
'User-Agent': userAgent(),
|
|
232
|
-
},
|
|
298
|
+
headers: appJwtHeaders(jwt),
|
|
233
299
|
}
|
|
234
300
|
);
|
|
235
301
|
if (!res.ok) {
|
|
302
|
+
// A DISCOVERED id can go stale (App uninstalled + reinstalled gets a new
|
|
303
|
+
// installation id). Drop it so the next attempt re-discovers instead of
|
|
304
|
+
// failing until a server restart. An env-pinned id is the operator's to fix.
|
|
305
|
+
if (!process.env.GITHUB_APP_INSTALLATION_ID) _instIdCache = null;
|
|
236
306
|
const detail = await res.text().catch(() => '');
|
|
237
307
|
throw err('APP_TOKEN_FAILED', { status: res.status, detail: detail.slice(0, 200) });
|
|
238
308
|
}
|
|
@@ -244,6 +314,42 @@ async function getInstallationToken(deps = {}) {
|
|
|
244
314
|
return data.token;
|
|
245
315
|
}
|
|
246
316
|
|
|
317
|
+
// Installations of this App on accounts OTHER than this repo's owner (task
|
|
318
|
+
// 1003049 grade, hacker finding). The App is public (W6) and now carries the
|
|
319
|
+
// ADR 0055 write scopes, so any third party CAN install it on their own repos —
|
|
320
|
+
// this server never uses those grants, but a leaked App private key could, so an
|
|
321
|
+
// unexpected install must be visible, never silent. The publish-reconciler calls
|
|
322
|
+
// this once per sweep and WARNs on a non-empty answer. Best-effort by design:
|
|
323
|
+
// returns null when it cannot tell (App unconfigured, repo info missing, API
|
|
324
|
+
// miss) — a watch must never break the sweep it rides on.
|
|
325
|
+
async function foreignInstallations(deps = {}) {
|
|
326
|
+
if (!appConfigured()) return null;
|
|
327
|
+
const info = deps.repoInfo || loadRepoInfo();
|
|
328
|
+
if (!info || info.error) return null;
|
|
329
|
+
const nowSec = Math.floor((deps.now ? deps.now() : Date.now()) / 1000);
|
|
330
|
+
const jwt = mintAppJwt({
|
|
331
|
+
appId: process.env.GITHUB_APP_ID,
|
|
332
|
+
privateKeyPem: process.env.GITHUB_APP_PRIVATE_KEY,
|
|
333
|
+
nowSec,
|
|
334
|
+
});
|
|
335
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
336
|
+
let data;
|
|
337
|
+
try {
|
|
338
|
+
const res = await doFetch('https://api.github.com/app/installations?per_page=100', {
|
|
339
|
+
headers: appJwtHeaders(jwt),
|
|
340
|
+
});
|
|
341
|
+
if (!res.ok) return null;
|
|
342
|
+
data = await res.json();
|
|
343
|
+
} catch (_) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
if (!Array.isArray(data)) return null;
|
|
347
|
+
const owner = String(info.owner || '').toLowerCase();
|
|
348
|
+
return data
|
|
349
|
+
.map((i) => ({ id: i && i.id, account: (i && i.account && i.account.login) || null }))
|
|
350
|
+
.filter((i) => String(i.account || '').toLowerCase() !== owner);
|
|
351
|
+
}
|
|
352
|
+
|
|
247
353
|
// Resolve the credential for git push + GitHub API: an explicitly-injected token
|
|
248
354
|
// (tests) first, then a GitHub App installation token, then the v1 PAT, else null.
|
|
249
355
|
async function resolveToken(deps = {}) {
|
|
@@ -863,7 +969,11 @@ async function updateBranchIfBehind({ branch }, deps = {}) {
|
|
|
863
969
|
// returns false on any failure, in which case the gate-review check sees no attestation
|
|
864
970
|
// and escalates — the SAFE direction (no rank bypass without a real server signal).
|
|
865
971
|
async function setCommitStatus({ sha, state, context, description, targetUrl }, deps = {}) {
|
|
866
|
-
|
|
972
|
+
// resolveToken can THROW (APP_TOKEN_FAILED / APP_NOT_INSTALLED); this helper's
|
|
973
|
+
// contract is "false on any failure" — the gate-review check then escalates,
|
|
974
|
+
// which is the safe direction.
|
|
975
|
+
let token;
|
|
976
|
+
try { token = await resolveToken(deps); } catch (_) { return false; }
|
|
867
977
|
if (!token) return false;
|
|
868
978
|
if (!SAFE_SHA_RE.test(String(sha || ''))) return false;
|
|
869
979
|
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
@@ -1253,6 +1363,8 @@ module.exports = {
|
|
|
1253
1363
|
isAncestorOfDefaultBranch,
|
|
1254
1364
|
resolveToken,
|
|
1255
1365
|
getInstallationToken,
|
|
1366
|
+
discoverInstallationId,
|
|
1367
|
+
foreignInstallations,
|
|
1256
1368
|
// exported for unit tests
|
|
1257
1369
|
buildAuthUrl,
|
|
1258
1370
|
mintAppJwt,
|
|
@@ -191,13 +191,56 @@ async function runSweep(deps = {}) {
|
|
|
191
191
|
if (!warn.unconfigured) {
|
|
192
192
|
warn.unconfigured = true;
|
|
193
193
|
errFn(
|
|
194
|
-
`[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} — confirmed→shipped can NEVER land until a credential is set (GITHUB_APP_ID +
|
|
194
|
+
`[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} — confirmed→shipped can NEVER land until a credential is set (GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY — installation id is auto-discovered — or GITHUB_PUSH_TOKEN). Subsequent sweeps stay quiet until this changes.`
|
|
195
195
|
);
|
|
196
196
|
}
|
|
197
197
|
return { skipped: 'unconfigured', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 0, strandsFlagged: 0 };
|
|
198
198
|
}
|
|
199
199
|
warn.unconfigured = false; // re-arm: a later credential LOSS must warn again
|
|
200
200
|
|
|
201
|
+
// task 1003049 — an App that is configured but NOT INSTALLED on the repo would
|
|
202
|
+
// pass the gate above and then throw APP_NOT_INSTALLED inside every per-branch
|
|
203
|
+
// call, where the best-effort catches swallow it — silently re-creating the
|
|
204
|
+
// stranding this sweep exists to prevent. Probe the token once (cached, so a
|
|
205
|
+
// healthy steady state costs nothing) and WARN once per streak instead.
|
|
206
|
+
if (typeof push.getInstallationToken === 'function' && typeof push.appConfigured === 'function' && push.appConfigured()) {
|
|
207
|
+
try {
|
|
208
|
+
await push.getInstallationToken();
|
|
209
|
+
warn.appNotInstalled = false; // re-arm: a later uninstall must warn again
|
|
210
|
+
// Foreign-install watch (task 1003049 grade): the App is public (W6) and
|
|
211
|
+
// carries write scopes, so a stranger CAN install it on their own repo.
|
|
212
|
+
// This server never uses that grant, but a leaked App key could — an
|
|
213
|
+
// unexpected install is surfaced loudly, once per streak, never silently.
|
|
214
|
+
if (typeof push.foreignInstallations === 'function') {
|
|
215
|
+
try {
|
|
216
|
+
const foreign = await push.foreignInstallations();
|
|
217
|
+
if (foreign && foreign.length) {
|
|
218
|
+
if (!warn.foreignInstalls) {
|
|
219
|
+
warn.foreignInstalls = true;
|
|
220
|
+
errFn(
|
|
221
|
+
`[publish-reconciler] WARN this project's GitHub App is installed on ${foreign.length} account(s) beyond the repo owner (${foreign.map((f) => f.account).filter(Boolean).join(', ')}). The server never uses those grants, but the App key's blast radius includes them — uninstall the App there (or rotate the key) if this is unexpected.`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
} else if (foreign) {
|
|
225
|
+
warn.foreignInstalls = false; // re-arm once the roster is clean again
|
|
226
|
+
}
|
|
227
|
+
} catch (_) { /* best-effort watch — never breaks the sweep */ }
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (e && e.code === 'APP_NOT_INSTALLED') {
|
|
231
|
+
if (!warn.appNotInstalled) {
|
|
232
|
+
warn.appNotInstalled = true;
|
|
233
|
+
errFn(
|
|
234
|
+
`[publish-reconciler] WARN the project's GitHub App is not installed on ${e.owner}/${e.repo} — confirmed→shipped can NEVER land until someone installs it${e.install_url ? ` (${e.install_url})` : ''}. Subsequent sweeps stay quiet until this changes.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return { skipped: 'app_not_installed', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 0, strandsFlagged: 0 };
|
|
238
|
+
}
|
|
239
|
+
// Any other failure (APP_TOKEN_FAILED, network) is transient territory — let
|
|
240
|
+
// the sweep run; the per-branch calls handle their own misses.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
201
244
|
let rows;
|
|
202
245
|
try {
|
|
203
246
|
rows = await db.listConfirmedTasksForReconcile({ limit: deps.limit || SWEEP_LIMIT });
|