@bongos/core 1.19.355 → 1.19.357

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 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.355",
6
- "core_contract": "1.19.355",
7
- "source_commit": "a7ad49c28f305814b88df556ae4c1d18ceca1a01",
5
+ "core_version": "1.19.357",
6
+ "core_contract": "1.19.357",
7
+ "source_commit": "3c8f80201ef2dd9992ceea78af1081b7cd74524d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-25T13:24:07.447Z",
9
+ "built_at": "2026-08-25T16:20:35.425Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 329,
12
+ "docs_redacted": 330,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1506,
14
+ "functional_verbatim": 1511,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1857,
20
- "tree_sha256": "abdcd03b753d6da9681bddd073feee645787426d01b5d8ecd460cba64d4a6b73",
19
+ "file_count": 1863,
20
+ "tree_sha256": "03a3fad69d2b334568f719ebc41a4cdde66d922efaae81ac1857f85baa39f760",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -367,7 +367,7 @@
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "f32214ef80ab2e05d032e35da07b84e7300a490001dd39a7f7d0ff1997053836"
370
+ "sha256": "4229624f3aebb2f7335278e5c69b32d0d722616d38fc9a511d4c829a3c9d476e"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
@@ -1439,10 +1439,15 @@
1439
1439
  "mode": "0000644",
1440
1440
  "sha256": "3067e87c4a2a1d63cb7b16526314b643f1940c4333cafe36627cd9bb5a293c0c"
1441
1441
  },
1442
+ {
1443
+ "path": "docs/adr/0190-settings-apply-in-place-env-patch.md",
1444
+ "mode": "0000644",
1445
+ "sha256": "bbf7e4f1903e519952dc80515b9c590a9f62a51160394d9d6cb07b378f7748e5"
1446
+ },
1442
1447
  {
1443
1448
  "path": "docs/adr/README.md",
1444
1449
  "mode": "0000644",
1445
- "sha256": "a1fc3b27fff2453bc6d419b118f93cf34be562bb264441b44184176049f878eb"
1450
+ "sha256": "1794e5de943ef65de6c9c0db0e82732b704c5261fe8bd8bef460ef5cc760c749"
1446
1451
  },
1447
1452
  {
1448
1453
  "path": "docs/api-reference.md",
@@ -1452,7 +1457,7 @@
1452
1457
  {
1453
1458
  "path": "docs/api/openapi.json",
1454
1459
  "mode": "0000644",
1455
- "sha256": "03a8b40f27b4ad5bd8219d4a755eb18c7b925feee465c6d97d3cb1b8c372651f"
1460
+ "sha256": "ef200cfe7325260763ca25e1af1b506579d69c0c78bc8d9fa276ac2900486ac9"
1456
1461
  },
1457
1462
  {
1458
1463
  "path": "docs/architecture.md",
@@ -4207,7 +4212,12 @@
4207
4212
  {
4208
4213
  "path": "modules/memory/module.json",
4209
4214
  "mode": "0000644",
4210
- "sha256": "ace721e3ae257aee7f9b13bfbb60b6959f9d72e11059c316d22701754db5b911"
4215
+ "sha256": "c697548692393d65c6beb8092f70f8864acb53d568548f123213023ca10032f8"
4216
+ },
4217
+ {
4218
+ "path": "modules/memory/pollers/search-index.js",
4219
+ "mode": "0000644",
4220
+ "sha256": "4950c01fc2b895a7eb8f5fc929826c1bea44d9764f5ffa4c8378de73be1446e9"
4211
4221
  },
4212
4222
  {
4213
4223
  "path": "modules/memory/routes/learnings.js",
@@ -4459,6 +4469,11 @@
4459
4469
  "mode": "0000644",
4460
4470
  "sha256": "de17cfb74f8684044abfb03e56698f91fa5aa813608f3a19d2ddf81d113e897e"
4461
4471
  },
4472
+ {
4473
+ "path": "modules/provisioning/migrations/provisioning_010_settings_apply.sql",
4474
+ "mode": "0000644",
4475
+ "sha256": "416296ce60ce307628909d1847e52d024fd09fb8f882c0afe9db42331d06dc21"
4476
+ },
4462
4477
  {
4463
4478
  "path": "modules/provisioning/module.json",
4464
4479
  "mode": "0000644",
@@ -4477,7 +4492,7 @@
4477
4492
  {
4478
4493
  "path": "modules/provisioning/provisioning.js",
4479
4494
  "mode": "0000644",
4480
- "sha256": "ff156c4c0252240026a5ce98547efa76d585d6f0fd88c5b0b95c5598901865ab"
4495
+ "sha256": "e6e3bcd6968be6ad16de989b2de301de7175c9040bb582529527163f75a05ff6"
4481
4496
  },
4482
4497
  {
4483
4498
  "path": "modules/provisioning/rate-limit.js",
@@ -4487,7 +4502,7 @@
4487
4502
  {
4488
4503
  "path": "modules/provisioning/routes/provisioning.js",
4489
4504
  "mode": "0000644",
4490
- "sha256": "026234f2c2c212a7ecc3808380bd3ea0cca81e2efc39c619968c505099830e5c"
4505
+ "sha256": "dc701815b1aa58895cedb1897bc5103f3250291b4ff4b507051ccda5310f7c5e"
4491
4506
  },
4492
4507
  {
4493
4508
  "path": "modules/provisioning/tests/liveness.mjs",
@@ -4787,12 +4802,12 @@
4787
4802
  {
4788
4803
  "path": "package-lock.json",
4789
4804
  "mode": "0000644",
4790
- "sha256": "2e354a8f810ac30ec70aab86c458d275f4d598788d8dbbfb1fc2a91a9f035ff1"
4805
+ "sha256": "b1353851e7bd50f58b47d0191076eacc249bdca62553d0bb9a6ee767aed9c712"
4791
4806
  },
4792
4807
  {
4793
4808
  "path": "package.json",
4794
4809
  "mode": "0000644",
4795
- "sha256": "7dc6f776203244cb6e6b909f1a32c93948d04dc243bfd714d281bf1b8ee8fcdb"
4810
+ "sha256": "632ea5fa7541ae2eb15f7afdc97f5414b65e1cadcf627e03ed7aee1f1443a0c0"
4796
4811
  },
4797
4812
  {
4798
4813
  "path": "public-docs/index.html",
@@ -5052,7 +5067,7 @@
5052
5067
  {
5053
5068
  "path": "public-landing/projects.html",
5054
5069
  "mode": "0000644",
5055
- "sha256": "42ddc3ecf96a31919f0e700f2cd0092d8225858c78ef4d231e4b3ac301b5b318"
5070
+ "sha256": "50cecb3a7b9cbae9ec321fa4eb2da1437c6f226037406a9c6bc5fe9e18813801"
5056
5071
  },
5057
5072
  {
5058
5073
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -5802,12 +5817,12 @@
5802
5817
  {
5803
5818
  "path": "scripts/gds/provision-units.js",
5804
5819
  "mode": "0000644",
5805
- "sha256": "e8a44143883403c2ff8e20107250d63f6d19a14050279e8d675480582e07c555"
5820
+ "sha256": "5e65be1f313f387cbf97116c678b54cbf4616e0580a42afc98860982f4f14bfe"
5806
5821
  },
5807
5822
  {
5808
5823
  "path": "scripts/gds/provision.js",
5809
5824
  "mode": "0000644",
5810
- "sha256": "5b4f5edce415dd46db830104eb196de678214730d736ebf3ae26a7671b718c77"
5825
+ "sha256": "64d0869afb99745ce08b311bb973d183746f9ed04c65bc112e5610740746eda3"
5811
5826
  },
5812
5827
  {
5813
5828
  "path": "scripts/gds/publish-credential-check.js",
@@ -6632,7 +6647,7 @@
6632
6647
  {
6633
6648
  "path": "src/branding.js",
6634
6649
  "mode": "0000644",
6635
- "sha256": "04c7bd3146abc16cb51d81fa6550520cdd169cd527fbe2de13dffd2c6fcc8ef5"
6650
+ "sha256": "239e2af1c637d0f72e8a61dc2ef5450ca0821477617941399253330e9e4afa25"
6636
6651
  },
6637
6652
  {
6638
6653
  "path": "src/build-info.js",
@@ -6652,7 +6667,7 @@
6652
6667
  {
6653
6668
  "path": "src/module-api.js",
6654
6669
  "mode": "0000644",
6655
- "sha256": "43ca06d2e5757257056bcaac608518df57c750d552ad667ec751a6ca28a0c00a"
6670
+ "sha256": "2376b2c1c18430faa5dc61183b3dace18e5d991515aa508fcb3d739ba9342701"
6656
6671
  },
6657
6672
  {
6658
6673
  "path": "src/module-loader/catalog.js",
@@ -7282,7 +7297,7 @@
7282
7297
  {
7283
7298
  "path": "tests/currency_label.mjs",
7284
7299
  "mode": "0000644",
7285
- "sha256": "dd3a232a6bcb8fb26abb88fa98662919a474f61527d88484c14a3bb3c98bf3c8"
7300
+ "sha256": "e6e6f835296328e1055b048c6610c2c69853d311252365f904c6a3de4a2c4d59"
7286
7301
  },
7287
7302
  {
7288
7303
  "path": "tests/cutover_codemod.mjs",
@@ -8107,7 +8122,7 @@
8107
8122
  {
8108
8123
  "path": "tests/instance_manifest.mjs",
8109
8124
  "mode": "0000644",
8110
- "sha256": "11bd1d3fe5050dd8f561499d432c788ab7c786ffaa8e39d9a24d78b9317e3143"
8125
+ "sha256": "52265f1f4ec43afc1f07467692c40ce608eaea74f2752ef1fe4ff1a7bb187387"
8111
8126
  },
8112
8127
  {
8113
8128
  "path": "tests/instance_mismatch_guard.mjs",
@@ -8542,7 +8557,7 @@
8542
8557
  {
8543
8558
  "path": "tests/projects_hub_pre_uat.mjs",
8544
8559
  "mode": "0000644",
8545
- "sha256": "919caac313f78c8c88e7ecca1a49ff2f1da53e443acda434f0648a9b5215f24d"
8560
+ "sha256": "aff31f49d05b6c0d0249ac44e6c44faa7d0d622f1a065a58d8a22a989122a3f2"
8546
8561
  },
8547
8562
  {
8548
8563
  "path": "tests/promote_goal_id.mjs",
@@ -8559,6 +8574,11 @@
8559
8574
  "mode": "0000644",
8560
8575
  "sha256": "0fc6c6626b43458c2e065322bec5a2ecd87e50a44abd2838d81ff0f0b6abeae7"
8561
8576
  },
8577
+ {
8578
+ "path": "tests/provision_settings_apply.mjs",
8579
+ "mode": "0000644",
8580
+ "sha256": "9666ed868774ebe49a76b15b9de6db17ac93af23a740c3384e25b73b4536cc58"
8581
+ },
8562
8582
  {
8563
8583
  "path": "tests/provisioning_default_domain.mjs",
8564
8584
  "mode": "0000644",
@@ -8582,12 +8602,17 @@
8582
8602
  {
8583
8603
  "path": "tests/provisioning_settings.mjs",
8584
8604
  "mode": "0000644",
8585
- "sha256": "7b6aa958f626054746e5272113bb314a118073507d5baed65f52cafdc25f7085"
8605
+ "sha256": "662d193ac3567e984da03e13ec4d41950cc29b7687fd195513e0291cff37c1c4"
8606
+ },
8607
+ {
8608
+ "path": "tests/provisioning_settings_apply.mjs",
8609
+ "mode": "0000644",
8610
+ "sha256": "916211a242794f56d4c4d65b492d46c168a89acb3985f64e40533221402caf53"
8586
8611
  },
8587
8612
  {
8588
8613
  "path": "tests/provisioning_settings_env.mjs",
8589
8614
  "mode": "0000644",
8590
- "sha256": "ee73d6af28f68bc6c197aae847663e9db2afb45e5073e2886a28c2a15925ad53"
8615
+ "sha256": "08770277cad7e16507a7c6c074732711fb9aad2025531ad8ec9de12c09c2d2d6"
8591
8616
  },
8592
8617
  {
8593
8618
  "path": "tests/provisioning_slug_available.mjs",
@@ -8597,7 +8622,7 @@
8597
8622
  {
8598
8623
  "path": "tests/provisioning_teardown_intent.mjs",
8599
8624
  "mode": "0000644",
8600
- "sha256": "68d328b69cd8b981e8454d679c00a757abeeafb4e083249489eec744cdd4898d"
8625
+ "sha256": "cfc6e239d42be33dad3e030333d51734114b7c969d1e42079e77d3c468c5728d"
8601
8626
  },
8602
8627
  {
8603
8628
  "path": "tests/provisioning_teardown_states.mjs",
@@ -8784,6 +8809,11 @@
8784
8809
  "mode": "0000644",
8785
8810
  "sha256": "1034ab278e7eb005ba0286b8bcecd7fafdec390418ae94e3cbacf81ffb9fc88c"
8786
8811
  },
8812
+ {
8813
+ "path": "tests/search_index_poller.mjs",
8814
+ "mode": "0000644",
8815
+ "sha256": "f665d4f0a9ccfb5b92564dfda39d13d6d8ed3146cde3fe2e05bb9f9aec029558"
8816
+ },
8787
8817
  {
8788
8818
  "path": "tests/search_ingest.mjs",
8789
8819
  "mode": "0000644",
@@ -153,7 +153,7 @@ export interface PatchMeSoundPrefsResponse { prefs: unknown; effective: unknown;
153
153
  export interface PatchProjectsIdFeaturedRequest { featured: boolean }
154
154
  export interface PatchProjectsIdFeaturedResponse { project: unknown }
155
155
  export interface PatchProvisioningInstancesIdSettingsRequest { platform_visibility?: string; joinability?: string }
156
- export interface PatchProvisioningInstancesIdSettingsResponse { ok: boolean; settings: unknown }
156
+ export interface PatchProvisioningInstancesIdSettingsResponse { ok: boolean; saved: unknown; push: unknown }
157
157
  export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; description?: string; status?: unknown }
158
158
  export interface PostAccessRequestsRequest { github_login: string; display_name?: string; note?: string }
159
159
  export interface PostAccessRequestsResponse { ok: boolean; request?: unknown; already_pending?: boolean }
@@ -0,0 +1,59 @@
1
+ # ADR 0190 — A changed project setting reaches a running instance by an in-place env patch and a restart, never a template re-write
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-08-25
5
+ - **Task:** [task 1003140](https://cloudbongos.com/builders#/task/1003140) (goal 1000058 — the non-cloudbongos project lifecycle)
6
+ - **Relates to:** ADR 0111 (provisioning trust boundary), ADR 0128 (runner privilege model), ADR 0133 (GitHub App Manifest cred placement), task 1003139 (the standup leg), task 1002692 (domain-attach), task 1002691 (the open-intent slot), task 1002720 (F16/F19/F23 truthfulness)
7
+
8
+ ## Context
9
+
10
+ Task 1003139 built the *standup* half of the platform→instance settings channel: a newly-provisioned instance reads `platform_visibility` / `joinability` from the `/etc/<slug>/web.env` the control-plane runner writes, through `src/branding.js` `ENV_OVERRIDES` onto `project.*`. That closed the "write-only state" gap for new instances only. A **running** instance loads its branding pack once — `branding()` is memoized and deep-frozen for the process lifetime — so a value changed on the platform afterwards could not reach it by any path. `PATCH /provisioning/instances/:id/settings` wrote the jsonb and pushed nothing.
11
+
12
+ The owner decided on 2026-08-19 that a restart is an acceptable cost of applying a change, and that the surface must **say so** rather than restart silently; and that a PATCH is a *request* until the intent settles, so no surface may claim the new state on PATCH return.
13
+
14
+ The natural implementation is the `domain-attach` shape (task 1002692) minus the DNS legs: re-write web.env, restart the service, verify healthz. That shape has a hazard that only shows on an instance the owner has actually used.
15
+
16
+ ## Decision
17
+
18
+ 1. **A `settings-apply` intent** (migration `provisioning_010`, `INTENT_ACTIONS` in lockstep). `PATCH /settings` writes the platform row **first**, then — for an instance in `SETTINGS_APPLY_FROM = {active}` on a shape in `SETTINGS_APPLY_SHAPES = {co-tenant, standalone}` — enqueues the intent via `enqueueSettingsApply`. The web tier still only enqueues; the runner remains the sole token-holder (ADR 0111 §2).
19
+
20
+ 2. **The runner patches the file in place, and every expected failure returns `ok:false`.** `settingsApplyInstance` reads the instance's web.env as it stands (`sudo cat` under the privileged model, ADR 0128), upserts **only** the two settings lines with the pure `upsertEnvVars` (replace where present, append where absent), writes it back through the same temp-file + `sudo install -m 600` path every `/etc` write uses, and restarts the unit. The write and the restart are guarded: a refused install or a non-zero `systemctl restart` returns `{ ok:false, error }` so the drain loop re-pends the intent (retrying up to the attempt cap) and leaves the instance's status alone — it **never** throws, because the drain loop's catch flips an instance to `error`, and a healthy active project marked broken over a settings push would stop being probed, fall off the push path, and need a re-provision to recover. Dry-run *plans* the patch ("would patch `<keys>` in `<path>` in place") rather than writing a synthetic two-line body that would read as the exact overwrite this leg exists to avoid.
21
+
22
+ 3. **The verify asks the instance whether it *read* the values, and records only what it earned.** After the restart the runner polls `/healthz` with the manifest probe's patience (`MANIFEST_VERIFY_TRIES × MANIFEST_VERIFY_INTERVAL_MS`, the F23 lesson — a false "down" here is not cosmetic, it arms the owner's re-kick paths), then fetches the instance's own public `GET /api/gds/instance` and compares `brand.project` against the pushed values (`settingsConsumed`): **true** → "restarted and reads the new values"; **false** → "restarted, but still reports the OLD values — its core may predate the settings channel"; **null** → "its core does not publish its settings yet, so whether it reads them is unknown". Answering `/healthz` proves the process, not that the setting took; an instance whose pinned core predates task 1003139 restarts fine and ignores the lines, and the event must not call that "applied".
23
+
24
+ 4. **The state gate is `{active}` only.** A pre-active instance gets the same values at standup from the same `effectiveSettings()` row; a torn-down one has nothing to restart. The provision intent cannot stand in for the push — `PROVISION_ENQUEUE_FROM` deliberately excludes `active` (task 1002691).
25
+
26
+ 5. **A dedicated droplet is refused honestly, at both ends.** It is stood up by cloud-init and the runner has no post-hoc exec path to it (the `placeManifestCreds` precedent). The route stores the value and answers `push.reachable:false` — *"This project runs on a machine the platform doesn't manage, so it can't be restarted from here — whoever runs that machine has to apply it there."* (not "its own server", which is also the hub's label for a *standalone* project, which **is** pushable); the runner, should such an intent ever arrive, returns `ok:false` with the exact lines to set — never a reported push that did not land.
27
+
28
+ 6. **A save that changes nothing neither writes nor restarts.** The route compares the request against `effectiveSettings(inst)`; an unchanged save answers *"No change — this project already has these settings."* with no row write, no event and no intent, so a whole-form save or a retried request cannot bounce a live project. The one exception is the owner re-kick (the domain route's F19 shape): the same values re-sent while a retired push left its `error_note` re-queue the push.
29
+
30
+ 7. **The response is a receipt, not a claim.** `{ ok, saved, push }` — `saved` is what the platform now stores (deliberately not `settings`, the field a form would render as the live value), and `push: { queued, restart, reachable, message }` is one shape on every branch: `restart` = a restart is what applies this, `reachable` = the platform can perform that restart. Sentences are state-only, with no wall-clock **or duration** promises ("briefly" is a claim about downtime; the runner's own failure branch contradicts it). An open slot held by other work (or by a push already *running*, which may have read the row before this write) answers `409 intent_conflict` with `details.recorded:true`; an identical *pending* push is idempotent because the runner reads the row's current values when it runs; an enqueue that throws still answers `200` *"Saved, but the restart could not be queued just now — send it again to apply it."* — a `500` would deny a durable write. A snagged (`error`) project is told its standup stopped partway and the change takes effect the next time it is stood up, not that it "finishes standing up". The archon override stamps `api:archon:<id>` on the event and the intent (the force-teardown convention).
31
+
32
+ 8. **A pending push yields to a teardown.** `enqueueTeardown` now supersedes a *pending* `settings-apply` exactly as it supersedes a pending `provision` (task 1002691): a push into a project about to be torn down is dead work, and the single open-intent slot must not hold the delete hostage across retry ticks. Running intents are never yanked.
33
+
34
+ 9. **The read-back is the instance's own manifest.** `clientBranding` now projects `project: { platformVisibility, joinability }` — what the instance *resolved* from its environment, env override included — so `GET /api/gds/instance` `brand.project` is where a manage page learns what the instance runs. Resolved, not enforced: enforcement lands with tasks 1003043 (visibility) and 1003044 (joinability). The allow-list ledger in `tests/currency_label.mjs` was widened deliberately; the policy is public by nature (a gated project's gate says "sign in"; an invite-only door says so).
35
+
36
+ 10. **The secret never reaches the journal.** A remote (ADR 0130) file placement embeds the base64 body in the ssh command; `makeExec` now takes a `shown` label so the runner logs "write `<path>` (N bytes)" and never the payload — this leg is the first to feed a *credentialed* file back through that path, but the fix covers every remote web.env write. The locally staged temp file is created `0600`.
37
+
38
+ ## Why the in-place patch is load-bearing
39
+
40
+ `domainAttachInstance` re-writes web.env by re-running `webEnvBody(inst, { federation: null })`. That regenerates the file **from the row** — and the row does not know the OAuth client id/secret that `placeManifestCreds` fetched from GitHub and placed later, nor the App private key (the ADR 0055 publish credential), nor the federation block. A template re-write therefore writes those lines back **blank**. On the domain-attach path this is masked by wizard order (address first, GitHub App second) — except on the owner re-kick (task 1002720 F19), which can run after the creds are placed; a settings push always happens on an instance the owner is signed in to, so the same move would break sign-in on the exact project they just touched. The fix is not to teach `webEnvBody` the secrets (the row must never hold them) but to change **only the lines this leg owns**. An unreadable or empty file is refused rather than "patched" — writing two lines over a missing `PORT`/`PGDATABASE` would take the instance down.
41
+
42
+ The domain-attach hazard itself is pre-existing and out of this task's scope; it is captured as idea 1000842 for its own task, with this leg's in-place shape as the fix.
43
+
44
+ ## Consequences
45
+
46
+ - Flipping a setting on a live co-tenant or standalone project restarts it within one runner tick; the owner is told so on every surface that carries the receipt, and told "no change" when nothing changed.
47
+ - The hub's card strip and the done panel label the intent by state — *"A settings change is queued — this card updates when it starts."* while pending, *"Applying a settings change — the project restarts to pick it up."* while running — instead of the generic "a settings-apply run is queued".
48
+ - An instance whose pinned core predates task 1003139 receives the env lines and the restart but does not *consume* them until its own core is upgraded; the runner's event says so ("does not publish its settings yet" / "still reports the OLD values") rather than "applied". The live test project (Spike Harbor) ran core 1.19.2 when this landed, so its manifest read-back waits on its own core pin. The live leg is witnessed only once this code is the control plane's pinned core (the runner executes the pinned package), so the 2026-08-25 session log records the baseline and the post-pin procedure, and the result lands as a learning on the task. Surfacing "not consumed" on the owner's card is follow-up work.
49
+ - `provision.js` grew by ~110 lines and stays well under the 1,500-line ratchet (the prerequisite shrink landed in task 1003206).
50
+
51
+ ## Rejected
52
+
53
+ - **The template re-write** (`webEnvBody`) — blanks placed creds; see above.
54
+ - **Re-provisioning as the push** — `provisionInstance` is a no-op on an active instance by design, and a full standup is the wrong tool for two lines.
55
+ - **Skipping the restart** (hot-reload the pack) — the pack is memoized and deep-frozen on purpose; per-request re-reads would reopen the config-drift class of bug the freeze exists to prevent.
56
+ - **Claiming the new state on PATCH return** — contradicts the owner's honesty rule and `last_verified_up`'s whole discipline.
57
+ - **Queueing for a dedicated droplet anyway** — a push that cannot land, reported as queued, is exactly the F16 lie.
58
+ - **Treating `/healthz` as proof the setting took** — proves the process only; the manifest read-back is the witness.
59
+ - **Restarting on every save** — the owner accepted a restart as the cost of *applying a change*, not per Save click.
@@ -265,5 +265,6 @@ This keeps the decision history honest and traceable.
265
265
  | 0187 | [**The OAuth web-handshake cookies live 30 minutes, and move together** ([task 1003234](https://cloudbongos.com/builders#/task/1003234), goal 1000058). The three cookies `/auth/web/start` sets — the CSRF state, the validated return target, the repo-scope opt-in — were 10 minutes each, a hall builder's timescale; a founder's first sign-in can include a GitHub SIGNUP with email verification and came back past ten minutes to "State mismatch" with the return + repo scope gone, and the page's retry door was a bare start. Decision: one `HANDSHAKE_COOKIE_MS = 30 min` for all three (they MUST move together — a state outliving its return cookie lands on the default landing with no scope, a return outliving its state lands on State mismatch), and every OAuth error page plus the access-pending auto-walk-in rebuilds its door from whatever cookies are still valid (`recoveryStartHref`, through the same open-redirect allowlist). What did NOT change: the state is still single-use, still `httpOnly`/`sameSite=lax`/apex-scoped, the code still the bearer — only the window for a legitimate handshake grew. Rejected: lengthening only return + repos (keeps the dead end and breaks the one-lifetime invariant the other way), a server-side state store, sixty minutes. The Discord-link state cookie is a different flow and keeps 10.](<redacted>.md) | auth / security posture |
266
266
  | 0188 | [**The weekly patrol runs on CI, and a declared cadence must have something that fires it** ([task 1003209](https://cloudbongos.com/builders#/task/1003209), goal 1000079). Goal 1000076 shipped a weekly red-team patrol, a routine describing how to run it, a recipe, and a manifest row — and nothing ever ran it; the follow-up audit §7 named all three halves (no committed scheduler, no cadence for a scheduler to read, and a manifest green anyway because its wiring predicate was satisfied by the SKILL.md mentioning its own scanner). The symmetric fix — an in-process timer beside `peer-vote-tally` in `routine-timers.js` — is **wrong on correctness, not cost**: all three patrol checks read things only the REPO has (full git history, `.github/workflows/`, an auditable lockfile), and a deployed instance runs a VENDORED core out of `node_modules/@bongos/core` that has none of them, so the timer would have scanned nothing and reported clean — worse than unscheduled. The runner is therefore CI: a weekly Actions lane with `fetch-depth: 0` and a pinned, checksum-verified gitleaks (the missing binary the audit listed separately), invoking `run-routine.js redteam-weekly` so the SKILL.md's steps stay the single source of truth. `cadence:` becomes a real frontmatter field — declarative only, scheduler-agnostic. The load-bearing piece is `routine-schedule.js` (fitness Check 25), whose predicate is **relational, not existential**: it classifies the cron into the period it actually realizes and fails when that disagrees with the declared cadence — the one rot mode no substring or existence check can EVER see, since editing weekly→monthly leaves every "is it wired?" predicate green. The manifest row was re-pointed to match ("wired" now means the SCHEDULER names the routine). Finally the exit code says **a finding was LOST, not that a finding exists**: findings are advisory (ADR 0162), so filing three is a SUCCESS and red is reserved for findings that cannot be recorded — failing on findings would have gone red on run one and stayed there (the unrotated staging credential in `092e473` is a standing history hit), and an always-red weekly job is one nobody reads. Filing is credential-gated but OPTIONAL — with no session secret the patrol still runs and writes its findings to the job summary, which matters because this goal's C1 is about eliminating long-lived tokens, not adding one. Rejected: the in-process timer; teaching `routine-timers.js` generic cadence discovery (a scheduler built for nobody — its only candidate user cannot run there); failing whenever findings exist; requiring the secret; a deeper manifest predicate instead of a new check.](<redacted>.md) | security / enforcement durability |
267
267
  | 0189 | [**A frozen baseline must justify itself: declared floors and the freeze-forever detector** ([task 1003210](https://cloudbongos.com/builders#/task/1003210), goal 1000079). The shrink-only ratchet makes a quality metric unable to REGRESS and, by the same design, unable to sit still noticeably: the follow-up audit measured the whole file at "every metric equal to baseline, 0 reduced" and the ratchet reported a clean PASS, because one green covers both actively-paying-down and nobody-has-looked-in-a-year. The ratchet has no concept of time; this adds it. **Age comes from git, never a stored `since:` field** — a timestamp beside each metric would be faster and hermetic and would also be the field that silently stops being updated, leaving old debt permanently fresh-looking, which is worse than no check because it produces confident green; an unavailable history reports UNKNOWN, not fresh (the `--require-roster` doctrine). A metric's age is the date of the OLDEST commit still carrying its current value, so a baseline recorded once and never revisited dates to its introduction — exactly the case being hunted. **The half that matters is the declared floor.** The naive "flag anything unmoved for N weeks" fires forever on metrics that are FINISHED (`require_cycle_count`'s four deliberate lazy requires; the ~28 duplicate windows ADR 0091 §2 forces because a module may not import core), and a check that cries wolf weekly is one everyone learns to skip — the same alert-fatigue failure ADR 0188 §2.4 avoids. So a metric may declare `floors: {name: {value, why}}` and a metric AT its floor is done, not stale; the `why` is mandatory and length-checked, because a floor exempts a metric from the clock permanently and must therefore be a written claim someone can disagree with — **silence was the problem**, and a floor without a reason is just a mute button. A floor exempts only DOWN TO the floor, so `duplicate_window_count`'s 59-vs-28 keeps its ~31 reducible windows on the clock. **Two severities, split by whose fault it is:** declaration rot (a floor naming a dead metric, a floor with no reason, a floor the metric already dropped below) is hermetic and caused by the diff at hand, so it HARD-FAILS the merge gate; staleness is nobody's diff and would punish whoever pushed next, so it warns in fitness and is FILED weekly by the patrol — the "repeatable, scheduled version" the audit asked for. Also fixed a latent eater: `--update-baselines` wrote a fixed object literal, so one refresh destroyed the accumulated `_comment` and (once floors landed) every floor, deleting a finished metric's justification from the repo while turning it into permanent weekly noise. Nothing is flagged today — the file is six days old — so the detector is verified by shrinking the window, not by waiting. Rejected: a stored `since:`; no exemptions; a floor with no required reason; hard-failing on staleness; patrol-only placement.](<redacted>.md) | quality ratchets / debt governance |
268
+ | 0190 | [**A changed project setting reaches a running instance by an in-place env patch and a restart — never a template re-write** ([task 1003140](https://cloudbongos.com/builders#/task/1003140), goal 1000058). Task 1003139 made a NEW instance read `platform_visibility` / `joinability` from the web.env the runner writes; a RUNNING instance loads its branding pack once, memoized and deep-frozen for the process lifetime, so a changed value could not reach it at all — the settings were write-only state on the platform. Decision: `PATCH /provisioning/instances/:id/settings` writes the platform row first, then for an ACTIVE instance on a shape the runner can reach enqueues a `settings-apply` intent (migration provisioning_010); the control-plane runner reads the instance's web.env as it stands, upserts ONLY the two settings lines (`upsertEnvVars`), and RESTARTS the service. The in-place patch is the load-bearing choice: the natural move — re-running `webEnvBody` as domain-attach does — regenerates the file from the row, and the row does not know the OAuth client secret or the App private key that `placeManifestCreds` placed later, so a template re-write silently blanks sign-in on the very instance the owner just touched. `SETTINGS_APPLY_FROM = {active}` because a pre-active instance gets the same values at standup and a torn-down one has nothing to restart (the provision intent cannot stand in — it excludes `active`, task 1002691); `SETTINGS_APPLY_SHAPES = {co-tenant, standalone}` because a dedicated droplet has no post-hoc exec path, and a push that cannot land is refused at the route AND the runner rather than reported. The response is a receipt, not a claim: it says the restart is coming and that the instance keeps its current setting until then, and the read-back lives on the instance's own public `GET /instance` manifest, whose `brand.project` now carries the resolved policy (`clientBranding` allow-list widened deliberately). Rejected: the template re-write; re-provisioning as the push; skipping the restart (impossible while the pack is memoized); claiming the new state on PATCH return.](<redacted>.md) | provisioning / settings channel |
268
269
 
269
270
  > ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
@@ -10204,7 +10204,7 @@
10204
10204
  "provisioning"
10205
10205
  ],
10206
10206
  "summary": "PATCH /provisioning/instances/:id/settings",
10207
- "description": "PATCH /provisioning/instances/:id/settings — change a project's settings. Body: any subset of the known keys; strict validation refuses unknown keys and off-vocabulary values. rank: any authenticated builder (own resource; archon may change any — the override is audited via the event's actor).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
10207
+ "description": "PATCH /provisioning/instances/:id/settings — change a project's settings. Body: any subset of the known keys; strict validation refuses unknown keys and off-vocabulary values. rank: any authenticated builder (own resource; archon may change any — the override is audited via the event's actor). The platform's row is updated first; then, for an ACTIVE instance on a shape the runner can reach, a 'settings-apply' intent is enqueued (task 1003140): the control-plane runner patches the instance's web.env in place and RESTARTS it, because the branding pack is memoized for the process lifetime. A save that changes nothing neither writes nor restarts. The response is a request receipt — `saved` is what the platform now stores, and `push` { queued, restart, reachable, message } says whether a restart was queued and when the value takes effect — never a claim the instance already runs it; a client reads that back from the instance's own public GET /instance manifest (brand.project). 409 intent_conflict (details.recorded: true) when the open slot is held by other work — the change IS saved.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
10208
10208
  "x-rank": "any-builder",
10209
10209
  "x-source": "modules/provisioning/routes/provisioning.js",
10210
10210
  "parameters": [
@@ -15916,11 +15916,13 @@
15916
15916
  "ok": {
15917
15917
  "type": "boolean"
15918
15918
  },
15919
- "settings": {}
15919
+ "saved": {},
15920
+ "push": {}
15920
15921
  },
15921
15922
  "required": [
15922
15923
  "ok",
15923
- "settings"
15924
+ "saved",
15925
+ "push"
15924
15926
  ]
15925
15927
  },
15926
15928
  "PatchTasksIdRequest": {
@@ -4,9 +4,18 @@
4
4
  "description": "Per-builder session memory, the lexical recall/search retrieval layer (ADR 0060), and durable learnings. A core-domain module carved out of the GDS core in BV1.R72 (ADR 0091); default-on because every instance needs the build system's memory.",
5
5
  "coreVersion": "^1.5.0",
6
6
  "default": true,
7
- "maintenance": { "status": "core-maintained" },
7
+ "maintenance": {
8
+ "status": "core-maintained"
9
+ },
8
10
  "contributes": {
9
- "routes": ["memory", "search", "learnings"]
11
+ "routes": [
12
+ "memory",
13
+ "search",
14
+ "learnings"
15
+ ],
16
+ "pollers": [
17
+ "search-index"
18
+ ]
10
19
  },
11
20
  "provides": [],
12
21
  "consumes": []
@@ -0,0 +1,201 @@
1
+ 'use strict';
2
+
3
+ // modules/memory/pollers/search-index.js — the recall index keeps itself, and says
4
+ // so out loud (task 1003162, goal 1000027).
5
+ //
6
+ // THE INCIDENT. `/recall` on live returned the `index_empty` outage state for words
7
+ // that demonstrably exist in the corpus — an entire goal's worth of ADRs, session
8
+ // logs and task rows. The index genuinely held nothing, and had held nothing for as
9
+ // long as anyone could tell, because NOTHING RAN THE INGESTERS.
10
+ //
11
+ // ADR 0060 says repo markdown is ingested by "a deploy-hook step" and DB prose by
12
+ // "a nightly timer". Both are BOX-SIDE artifacts: `grep -rn search-ingest` across
13
+ // the core tree finds the two ingesters, some prose about them, and nothing that
14
+ // executes either. Whether the box's deploy.sh ever called them is unknowable from
15
+ // here — and that unknowability IS the bug. An index that depends on an unwatched
16
+ // host script fails exactly the way this one did: silently, indefinitely, and only
17
+ // noticed when a human happens to search for something they know is there.
18
+ //
19
+ // WHY IN-PROCESS, and not a cron or a CI workflow. The ingesters need the database.
20
+ // A GitHub Actions schedule cannot reach it; the box's own timer is the thing that
21
+ // stopped. The web process already holds a pool and already runs this exact shape
22
+ // of work (the publish reconciler, the board-expiry sweep), so putting the sweep
23
+ // here makes it start whenever the app starts and stop only when the app stops.
24
+ // There is no separate thing left to forget to arm.
25
+ //
26
+ // THE SHAPE follows the publish reconciler, the in-tree precedent:
27
+ // * a DISPOSITION LINE every sweep — the reconciler's lesson is that a sweep
28
+ // which only speaks on failure is indistinguishable from a sweep that is not
29
+ // running. "held 4,812 chunks, nothing to do" is the sentence whose ABSENCE is
30
+ // the alarm.
31
+ // * failure isolation — the md ingest failing must not skip the db ingest.
32
+ // * bounded and self-gating — SEARCH_INDEX_SWEEP_DISABLED turns it off, an unref'd
33
+ // timer never holds the process open, and a sweep already running is not
34
+ // re-entered.
35
+ //
36
+ // AND IT CHECKS EXIT CODES, which is the half the task named explicitly. Task
37
+ // 1002859 made a partial ingest exit non-zero and NAME every skipped source; that
38
+ // only helps if someone reads the code. Spawning the CLIs (rather than importing
39
+ // their main()) is what preserves it — the exit code is the contract, and running
40
+ // them the documented way keeps this poller honest about what an operator would see.
41
+
42
+ const path = require('node:path');
43
+ const { execFile } = require('node:child_process');
44
+
45
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
46
+ const MD_INGESTER = path.join(REPO_ROOT, 'scripts', 'gds', 'search-ingest-md.js');
47
+ const DB_INGESTER = path.join(REPO_ROOT, 'scripts', 'gds', 'search-ingest-db.js');
48
+
49
+ // Nightly-ish. DB prose changes slowly and the md corpus only moves on deploy, so
50
+ // a long period is right; the value is that it recurs at all, not that it is tight.
51
+ const INTERVAL_MS = Number(process.env.SEARCH_INDEX_SWEEP_MS) || 6 * 60 * 60 * 1000;
52
+ // An EMPTY index is an outage, not a slow day: recall answers nothing at all until
53
+ // it is fixed. Sweep early after boot so a deploy that lands on an empty table
54
+ // repairs it in minutes rather than at the next six-hour tick.
55
+ const FIRST_SWEEP_DELAY_MS = Number(process.env.SEARCH_INDEX_FIRST_MS) || 90_000;
56
+ // The md ingest walks a few hundred files; the db ingest reads prose tables. Both
57
+ // are minutes at worst. The cap exists so a wedged child can never pin the sweep.
58
+ const INGEST_TIMEOUT_MS = Number(process.env.SEARCH_INDEX_INGEST_TIMEOUT_MS) || 10 * 60 * 1000;
59
+
60
+ let intervalTimer = null;
61
+ let firstTimer = null;
62
+ let sweeping = false;
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Pure decisions — exported and unit-tested. No DB, no spawn, no clock.
66
+ // ---------------------------------------------------------------------------
67
+
68
+ // What does a chunk count MEAN? Separated from the doing so the judgement can be
69
+ // tested without a database, and so the wording lives in one place.
70
+ //
71
+ // `empty` is deliberately its own state rather than "very stale": it is the only
72
+ // one where recall is not degraded but DEAD, it is rank-independent (so it can
73
+ // never be "nothing visible to YOU"), and it is the one that justifies spending an
74
+ // ingest immediately on boot.
75
+ function describeIndex({ chunks, lastIngestedAt = null, now = Date.now() } = {}) {
76
+ // typeof FIRST: Number(null) is 0 and Number('') is 0, so a missing column or an
77
+ // absent row would coerce into a confident "the index is empty" — a wrong
78
+ // diagnosis that also spends an ingest. Only an actual number can be a count.
79
+ const n = typeof chunks === 'number' ? chunks : NaN;
80
+ if (!Number.isFinite(n) || n < 0) {
81
+ return { level: 'alarm', state: 'unreadable', chunks: null,
82
+ message: 'search-index: could not read search_chunks — recall health is UNKNOWN, which is not the same as healthy.' };
83
+ }
84
+ if (n === 0) {
85
+ return { level: 'alarm', state: 'empty', chunks: 0,
86
+ message: 'search-index: search_chunks is EMPTY — /recall can return nothing at all, for any query, at any rank. Ingesting now.' };
87
+ }
88
+ const ageMs = lastIngestedAt ? Math.max(0, now - new Date(lastIngestedAt).getTime()) : null;
89
+ const ageDays = ageMs == null ? null : Math.floor(ageMs / 86_400_000);
90
+ if (ageDays != null && ageDays >= 7) {
91
+ return { level: 'warn', state: 'stale', chunks: n, age_days: ageDays,
92
+ message: `search-index: ${n} chunk(s), but the newest is ${ageDays} day(s) old — the sweep may have stopped.` };
93
+ }
94
+ return { level: 'ok', state: 'ok', chunks: n, age_days: ageDays,
95
+ message: `search-index: ${n} chunk(s) indexed${ageDays == null ? '' : `, newest ${ageDays} day(s) old`}.` };
96
+ }
97
+
98
+ // One ingester's outcome, in the words an operator needs. A non-zero exit is the
99
+ // signal task 1002859 built and nothing was reading.
100
+ function describeIngest({ name, code, signal = null, stderr = '' } = {}) {
101
+ if (signal) {
102
+ return { level: 'alarm', ok: false,
103
+ message: `search-index: ${name} was KILLED by ${signal} (timeout or OOM) — the index is partially written at best.` };
104
+ }
105
+ if (code === 0) return { level: 'ok', ok: true, message: `search-index: ${name} ok.` };
106
+ // The tail, not the head: these CLIs name what they skipped at the END.
107
+ const tail = String(stderr || '').trim().split('\n').slice(-6).join('\n');
108
+ return { level: 'alarm', ok: false,
109
+ message: `search-index: ${name} exited ${code} — it names every source it skipped:\n${tail || '(no stderr captured)'}` };
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Doing
114
+ // ---------------------------------------------------------------------------
115
+
116
+ function runIngester(file, { execFileImpl = execFile } = {}) {
117
+ return new Promise((resolve) => {
118
+ execFileImpl(process.execPath, [file], {
119
+ cwd: REPO_ROOT, timeout: INGEST_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024,
120
+ env: process.env, shell: false,
121
+ }, (err, _stdout, stderr) => {
122
+ resolve({
123
+ code: err ? (typeof err.code === 'number' ? err.code : 1) : 0,
124
+ signal: (err && err.signal) || null,
125
+ stderr: stderr || (err && err.message) || '',
126
+ });
127
+ });
128
+ });
129
+ }
130
+
131
+ async function readChunkStats(pool) {
132
+ const r = await pool.query(
133
+ 'SELECT count(*)::int AS chunks, max(updated_at) AS last FROM search_chunks'
134
+ );
135
+ const row = (r && r.rows && r.rows[0]) || {};
136
+ return { chunks: row.chunks, lastIngestedAt: row.last || null };
137
+ }
138
+
139
+ // One sweep. Never throws — a poller that can take the process down is worse than
140
+ // the outage it watches.
141
+ async function sweep({ pool, log = console, execFileImpl = execFile, now = Date.now() } = {}) {
142
+ let health;
143
+ try {
144
+ health = describeIndex({ ...(await readChunkStats(pool)), now });
145
+ } catch (e) {
146
+ health = describeIndex({ chunks: NaN, now });
147
+ health.error = (e && e.message) || String(e);
148
+ }
149
+ (health.level === 'ok' ? log.log : log.error).call(log, health.message);
150
+
151
+ // Ingest when the index is empty or unreadable-but-recoverable; a populated
152
+ // index is refreshed on the ordinary tick, not re-walked because it is stale by
153
+ // an hour. `empty` is the case that cannot wait.
154
+ const results = [];
155
+ for (const [name, file] of [['search-ingest-md', MD_INGESTER], ['search-ingest-db', DB_INGESTER]]) {
156
+ const r = await runIngester(file, { execFileImpl });
157
+ const d = describeIngest({ name, ...r });
158
+ results.push(d);
159
+ (d.ok ? log.log : log.error).call(log, d.message);
160
+ }
161
+
162
+ // The after-picture. Without it a run that exited 0 while writing nothing still
163
+ // reads as success — the exact shape of failure this task exists to end.
164
+ let after = null;
165
+ try {
166
+ after = describeIndex({ ...(await readChunkStats(pool)), now });
167
+ (after.level === 'ok' ? log.log : log.error).call(log, `search-index: after sweep — ${after.message}`);
168
+ } catch (_) { /* the before-picture already reported; never throw from a sweep */ }
169
+
170
+ return { health, results, after };
171
+ }
172
+
173
+ function start(deps = {}) {
174
+ // Same injectable-log shape as sweep() above (default console), so the two
175
+ // halves of this poller log through one seam and neither hard-codes console.
176
+ const log = deps.log || console;
177
+ if (process.env.SEARCH_INDEX_SWEEP_DISABLED === 'true') {
178
+ log.log('search-index: sweep disabled by SEARCH_INDEX_SWEEP_DISABLED.');
179
+ return { stop };
180
+ }
181
+ const pool = deps.pool || require('../../../src/module-api.js').pool;
182
+ const tick = async () => {
183
+ if (sweeping) return; // never re-enter; a long ingest must not stack
184
+ sweeping = true;
185
+ try { await sweep({ pool, log }); }
186
+ catch (e) { log.error('search-index: sweep failed:', (e && e.message) || e); }
187
+ finally { sweeping = false; }
188
+ };
189
+ firstTimer = setTimeout(tick, FIRST_SWEEP_DELAY_MS);
190
+ intervalTimer = setInterval(tick, INTERVAL_MS);
191
+ if (firstTimer.unref) firstTimer.unref();
192
+ if (intervalTimer.unref) intervalTimer.unref();
193
+ return { stop };
194
+ }
195
+
196
+ function stop() {
197
+ if (firstTimer) { clearTimeout(firstTimer); firstTimer = null; }
198
+ if (intervalTimer) { clearInterval(intervalTimer); intervalTimer = null; }
199
+ }
200
+
201
+ module.exports = { start, stop, sweep, describeIndex, describeIngest, INTERVAL_MS, FIRST_SWEEP_DELAY_MS };
@@ -0,0 +1,24 @@
1
+ -- provisioning_010_settings_apply.sql — the settings-apply intent action
2
+ -- (task 1003140, goal 1000058: the live-push half of the platform→instance
3
+ -- settings channel).
4
+ --
5
+ -- WHY. Task 1003139 built the standup leg: a NEW instance reads
6
+ -- platform_visibility / joinability from the web.env the runner writes. But a
7
+ -- RUNNING instance loads its branding pack once — memoized + deep-frozen for the
8
+ -- process lifetime — so a changed setting could not reach it without re-writing
9
+ -- /etc/<slug>/web.env and RESTARTING the service: the domain-attach shape minus
10
+ -- the DNS legs. PATCH /provisioning/instances/:id/settings now enqueues a
11
+ -- 'settings-apply' intent for an ACTIVE instance; the control-plane runner (the
12
+ -- sole token-holder, ADR 0111 §2) patches the settings lines in place and
13
+ -- restarts. The web tier still only ENQUEUES — the trust boundary is unchanged.
14
+ --
15
+ -- Widening a CHECK enum is additive (permits MORE values; no row changes) — the
16
+ -- provisioning_003/005 precedent. Applied only when the provisioning module is
17
+ -- enabled. Safe to re-run. Keep INTENT_ACTIONS in provisioning.js in lockstep.
18
+
19
+ ALTER TABLE provisioning_intents DROP CONSTRAINT IF EXISTS provisioning_intents_action_check;
20
+ ALTER TABLE provisioning_intents ADD CONSTRAINT provisioning_intents_action_check
21
+ CHECK (action IN ('provision', 'teardown', 'oauth-manifest', 'domain-attach', 'settings-apply'));
22
+
23
+ INSERT INTO schema_migrations (version) VALUES ('provisioning_010_settings_apply')
24
+ ON CONFLICT (version) DO NOTHING;