@bongos/core 1.19.1030 → 1.19.1032
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 +23 -18
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +26 -0
- package/docs/adr/0338-modules-travel-through-a-bongos-hosted-store.md +119 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/db-claim-reads.js +8 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-run.js +51 -3
- package/scripts/gds/autobongos-service.ps1 +7 -2
- package/src/module-api.js +1 -1
- package/tests/autobongos_loop.mjs +91 -0
- package/tests/grade_diff_hash.mjs +77 -2
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.1032",
|
|
6
|
+
"core_contract": "1.19.1032",
|
|
7
|
+
"source_commit": "016ed2163285ec64cc802cfe18a8990f9549c8d6",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-25T21:
|
|
9
|
+
"built_at": "2026-09-25T21:42:52.123Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 530,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
14
|
"functional_verbatim": 2459,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3015,
|
|
20
|
+
"tree_sha256": "dda9c12ae3e87decfa28f7379633ebbc1c679833d2aba34708a70e4898fa7d49",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -1027,7 +1027,7 @@
|
|
|
1027
1027
|
{
|
|
1028
1028
|
"path": "docs/adr/0110-untrack-whole-file-generated-nav-docs.md",
|
|
1029
1029
|
"mode": "0000644",
|
|
1030
|
-
"sha256": "
|
|
1030
|
+
"sha256": "ab6173c6ca1d2f37e87761dab79f011865409521bee5cd94107c33c26148677b"
|
|
1031
1031
|
},
|
|
1032
1032
|
{
|
|
1033
1033
|
"path": "docs/adr/0111-instance-hosting-provisioning-module.md",
|
|
@@ -2184,10 +2184,15 @@
|
|
|
2184
2184
|
"mode": "0000644",
|
|
2185
2185
|
"sha256": "b64fbfec7c6bda4ec654cb89a31878d401ab4c9ea5401d3129b74cc5627edecd"
|
|
2186
2186
|
},
|
|
2187
|
+
{
|
|
2188
|
+
"path": "docs/adr/0338-modules-travel-through-a-bongos-hosted-store.md",
|
|
2189
|
+
"mode": "0000644",
|
|
2190
|
+
"sha256": "5acb8ac6e656a909ef6392f845f7a479d44d7b7619197e9f66b4879e16a6971c"
|
|
2191
|
+
},
|
|
2187
2192
|
{
|
|
2188
2193
|
"path": "docs/adr/README.md",
|
|
2189
2194
|
"mode": "0000644",
|
|
2190
|
-
"sha256": "
|
|
2195
|
+
"sha256": "c21920e3e14d6d4d26bf7bdf830d4529686c0ec41bf0c41cdae17971fd7cfba7"
|
|
2191
2196
|
},
|
|
2192
2197
|
{
|
|
2193
2198
|
"path": "docs/api-reference.md",
|
|
@@ -2757,7 +2762,7 @@
|
|
|
2757
2762
|
{
|
|
2758
2763
|
"path": "docs/module-api-changelog.md",
|
|
2759
2764
|
"mode": "0000644",
|
|
2760
|
-
"sha256": "
|
|
2765
|
+
"sha256": "02e3773cd062329373089c27d7878ecf9134914829c464adcb72499b5dfa7474"
|
|
2761
2766
|
},
|
|
2762
2767
|
{
|
|
2763
2768
|
"path": "docs/modules-contract.md",
|
|
@@ -5862,7 +5867,7 @@
|
|
|
5862
5867
|
{
|
|
5863
5868
|
"path": "modules/lifecycle/db-claim-reads.js",
|
|
5864
5869
|
"mode": "0000644",
|
|
5865
|
-
"sha256": "
|
|
5870
|
+
"sha256": "2f51bbe36717c80645ed263502fa26109767d3b0716f233db169cfe9ef8dfd92"
|
|
5866
5871
|
},
|
|
5867
5872
|
{
|
|
5868
5873
|
"path": "modules/lifecycle/db-claims.js",
|
|
@@ -8362,12 +8367,12 @@
|
|
|
8362
8367
|
{
|
|
8363
8368
|
"path": "package-lock.json",
|
|
8364
8369
|
"mode": "0000644",
|
|
8365
|
-
"sha256": "
|
|
8370
|
+
"sha256": "a101d5c7db8bf74c23437aeb893c14774bf675250489aa0f8da691c840f141d5"
|
|
8366
8371
|
},
|
|
8367
8372
|
{
|
|
8368
8373
|
"path": "package.json",
|
|
8369
8374
|
"mode": "0000644",
|
|
8370
|
-
"sha256": "
|
|
8375
|
+
"sha256": "3267957ac4cec25809715c70027bb2c0b08cea10468cdd49dfbd0dd13f3d6d1d"
|
|
8371
8376
|
},
|
|
8372
8377
|
{
|
|
8373
8378
|
"path": "public-docs/index.html",
|
|
@@ -8542,7 +8547,7 @@
|
|
|
8542
8547
|
{
|
|
8543
8548
|
"path": "scripts/gds/autobongos-run.js",
|
|
8544
8549
|
"mode": "0000644",
|
|
8545
|
-
"sha256": "
|
|
8550
|
+
"sha256": "9fe8be8bc38039f700b6aab8b52508c162252513cc6b5b87aacb7dc48648937a"
|
|
8546
8551
|
},
|
|
8547
8552
|
{
|
|
8548
8553
|
"path": "scripts/gds/autobongos-service.cmd",
|
|
@@ -8552,7 +8557,7 @@
|
|
|
8552
8557
|
{
|
|
8553
8558
|
"path": "scripts/gds/autobongos-service.ps1",
|
|
8554
8559
|
"mode": "0000644",
|
|
8555
|
-
"sha256": "
|
|
8560
|
+
"sha256": "ed95f836655535827c14a32caf197fee7c238e15d8e8bf7a9e987b78ab266887"
|
|
8556
8561
|
},
|
|
8557
8562
|
{
|
|
8558
8563
|
"path": "scripts/gds/autobongos-verify.js",
|
|
@@ -10412,7 +10417,7 @@
|
|
|
10412
10417
|
{
|
|
10413
10418
|
"path": "src/module-api.js",
|
|
10414
10419
|
"mode": "0000644",
|
|
10415
|
-
"sha256": "
|
|
10420
|
+
"sha256": "42f07ce15936b5d07b52f4cb0d582af2f0b2c248bf96240bec8ba224d18af95b"
|
|
10416
10421
|
},
|
|
10417
10422
|
{
|
|
10418
10423
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10847,7 +10852,7 @@
|
|
|
10847
10852
|
{
|
|
10848
10853
|
"path": "tests/autobongos_loop.mjs",
|
|
10849
10854
|
"mode": "0000644",
|
|
10850
|
-
"sha256": "
|
|
10855
|
+
"sha256": "8ba5a6b492ae5afabb13f9e44d2f82cb873a094a659425554f0d105bd979aff4"
|
|
10851
10856
|
},
|
|
10852
10857
|
{
|
|
10853
10858
|
"path": "tests/autobongos_verify.mjs",
|
|
@@ -12272,7 +12277,7 @@
|
|
|
12272
12277
|
{
|
|
12273
12278
|
"path": "tests/grade_diff_hash.mjs",
|
|
12274
12279
|
"mode": "0000644",
|
|
12275
|
-
"sha256": "
|
|
12280
|
+
"sha256": "7dc3ce65bada73216c06eda8ac2b10c248f1909e0953a4737584a8d851b743e4"
|
|
12276
12281
|
},
|
|
12277
12282
|
{
|
|
12278
12283
|
"path": "tests/grade_server_authoritative.mjs",
|
|
@@ -47,3 +47,29 @@ Apply the atlas pattern to the two **whole-file navigation** artifacts — `docs
|
|
|
47
47
|
- **Post-main regen bot / cron that re-commits the files** (the owner's two original ideas) — rejected as the *primary* mechanism: both keep the files in git, weaken the freshness gate for every content PR, burn CI on regen PRs, and (there is no GitHub webhook in this codebase) can only poll anyway. "Don't commit the file" is strictly simpler than "automate re-committing it." The bounded bot survives only for the API spec (a follow-up task), which has external consumers.
|
|
48
48
|
- **Strengthen the reactive heal only** (ADR 0082++) — rejected: makes conflicts invisible, not absent; the per-branch bookkeeping commits and the heal latency remain, and the cost grows with parallelism.
|
|
49
49
|
- **Trim `docs/repo-map.md` from `REGENERABLE`/`.gitattributes`** — deferred: it would force a rewrite of `tests/git_merge_regen.mjs` (which uses repo-map as its canonical whole-file heal fixture) for no real gain, since a gitignored file never reaches the driver anyway. Leaving the entries in place is the lower-risk transition posture.
|
|
50
|
+
|
|
51
|
+
## Amendment — 2026-09-25 (task 1001929): `docs/file-map.md` stays tracked; the reactive heal is enough
|
|
52
|
+
|
|
53
|
+
**The open question (pt.4).** `docs/file-map.md` is hybrid — hand-written prose plus two generated blocks (`.claude/skills/`, `.claude/scheduled-tasks/`, [ADR 0066](0066-generated-file-map-sections.md)) — so it cannot be gitignored like the two whole files above. What was left to decide: does its generated churn cause enough parallel-branch conflicts to justify the repo-map treatment (move the blocks into a gitignored companion file and split the gate into `--check-block`/`--check-whole`), or does the `otb-regen` heal it already has (`.gitattributes` + `REGENERABLE`, [ADR 0082](0082-server-side-merge-conflict-auto-resolution.md)) cope?
|
|
54
|
+
|
|
55
|
+
**Measured, not estimated.** Every merge commit reachable from `origin/main` at `331569b4` (2026-09-25) — the whole history since the 2026-07-03 core extraction, PR landings and the branch-side "merge origin/main into task-N" freshness merges alike — was replayed with `git merge-tree --write-tree --name-only <parent1> <parent2>` in a mirror clone (`git clone --mirror --shared`) that has **no** `merge.otb-regen.driver` registered. Git then falls back to its plain 3-way text merge, so the replay shows what each merge would have hit with no heal at all. For each `file-map.md` conflict, the conflict markers in the written tree were located against the `BEGIN`/`END GENERATED FILE-MAP` markers.
|
|
56
|
+
|
|
57
|
+
| Replay result | Count |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Merges replayed | 2,287 |
|
|
60
|
+
| Merges that would have conflicted on any file | 248 |
|
|
61
|
+
| … on `docs/file-map.md` | **3** — all in hand-written prose, **0** inside a generated block |
|
|
62
|
+
| … on `docs/file-map.notes.json` (the hand-written notes sidecar) | 0 |
|
|
63
|
+
| Non-merge commits touching `docs/file-map.md` | 129 — 89 prose only, 30 generated block only, 9 both |
|
|
64
|
+
|
|
65
|
+
For scale, the same replay counted 78 conflicts on `docs/copy-inventory.md`, 77 on `CLAUDE.md`, 70 on `docs/copy-registry.json`, and 21 each on `docs/api/openapi.json` and `docs/api-reference.md`.
|
|
66
|
+
|
|
67
|
+
The three `file-map.md` conflicts all fell on 2026-08-29 (the freshness merges for tasks 1002968, 1002290 and 1002967), and all have one shape: two sessions each hand-appended a line for a new test file at the same end of the `tests/` listing. There have been none since. The generated blocks do churn (39 commits changed one) but never conflicted, even with the driver switched off: each block lists one directory per line in sorted order, so two branches adding different skills usually edit different lines. If they ever do meet, the driver already re-renders both blocks from the merged tree.
|
|
68
|
+
|
|
69
|
+
**Decision: no split.**
|
|
70
|
+
|
|
71
|
+
1. The split targets the one part of the file that has never conflicted. Moving the blocks out would have prevented none of the three conflicts on record — all were prose, and the prose stays in git whatever happens to the blocks.
|
|
72
|
+
2. It would cost real machinery for no measured gain: a new gitignored file with its own regeneration on every refresh path (the recipe in Consequences above, plus the standalone path, [ADR 0147](0147-standalone-live-docs-regen.md)), a gate split, and relinking the map to the companion file.
|
|
73
|
+
3. The file-map gate is unchanged: plain `gen-file-map.js --check`, hard, in `fitness.js`. It also enforces the note-per-skill contract and module coverage (task 1004066), both of which assume the file is tracked.
|
|
74
|
+
|
|
75
|
+
**Revisit if** a re-run of the same replay shows conflicts *inside* a generated block, or prose conflicts become routine. The prose case would need a different fix (for example, a per-area file for the hand-written tree); the split considered here cannot reach it.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# ADR 0338 — Modules travel through a Bongos-hosted store, and an acquired module keeps working forever
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-25
|
|
5
|
+
- **Task:** [task 1003782](https://cloudbongos.com/builders#/task/1003782) (goal 1000091 — working area 5, Module distribution & economy)
|
|
6
|
+
- **Deciders:** Will (owner of working area 5) chose the transport and the delist semantics; Claude researched the options and wrote the record.
|
|
7
|
+
- **Related:** [ADR 0264](0264-the-ten-working-areas.md) (area 5's state) · [ADR 0107](0107-module-upstreaming-contribution-to-core-catalog.md) §6 (modules ship inside core today) · [ADR 0135](0135-module-upstream-submission-interim-queue.md) (the interim submit queue) · [ADR 0138](0138-bundled-module-deps-declared-per-module.md) (module npm deps composed at init) · [ADR 0161](0161-publish-on-merge.md) / [ADR 0283](0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md) (how the core itself is distributed) · [ADR 0324](0324-the-public-npm-cli-channel-is-retired.md) (the owner retiring a public npm channel) · [ADR 0098](0098-public-mirror-publish-manifest.md) (`isPublishable()`)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
A module has no way to travel on its own. `bongos module list` enumerates what is on disk,
|
|
12
|
+
`bongos module submit` pushes *into* core (ADR 0135), and nothing installs, fetches or
|
|
13
|
+
downloads one. A module reaches an instance only by shipping inside the core package
|
|
14
|
+
(`default:false`, ADR 0107 §6) or by living in the instance's own repo. A module also has no
|
|
15
|
+
version of its own — `module.json` carries only a `coreVersion` range (task 1003781 adds one).
|
|
16
|
+
|
|
17
|
+
Roughly a third of area 5 hangs off how a module travels: install (task 1003785), update
|
|
18
|
+
(task 1003786), upgrade reporting (task 1003787), the registry and entitlement tables
|
|
19
|
+
(tasks 1003812, 1003813), release channels (task 1003815) and delist (task 1003797). Whatever
|
|
20
|
+
is chosen has to leave room for four things the area's criteria already require:
|
|
21
|
+
|
|
22
|
+
1. an **entitlement check before install** — a paid module is not handed to someone who has not paid;
|
|
23
|
+
2. a **per-version quality score**;
|
|
24
|
+
3. a **tester channel** that sees a version before general release;
|
|
25
|
+
4. a **delist** that actually stops distribution.
|
|
26
|
+
|
|
27
|
+
Three transports were weighed:
|
|
28
|
+
|
|
29
|
+
| | npm packages | Bongos-hosted store | Git refs |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| Added monthly cost | $0 | ≈ $0 at first (existing control plane + the $5/mo DO Spaces bucket already paid for backups) | $0 |
|
|
32
|
+
| Entitlement before install | **No** — a public package is downloadable by anyone; a private one needs a paid npm seat per buyer | **Yes** — the server checks before it serves | Only by per-repo access management |
|
|
33
|
+
| Delist stops distribution | **No** — npm restricts unpublish, and published versions persist | **Yes** — the server stops serving | **No** — clones and forks persist |
|
|
34
|
+
| Tester channel | dist-tags, awkwardly | native | branches, awkwardly |
|
|
35
|
+
| Integrity | built in | reuse the core's `tree_sha256` manifest | weak |
|
|
36
|
+
| Upkeep | another public channel (ADR 0324 retired the last one on the owner's judgement) | we run upload/download | low |
|
|
37
|
+
|
|
38
|
+
npm and git refs both fail requirements 1 and 4 by construction, and those two are the
|
|
39
|
+
economy: a store that cannot withhold a paid module or stop distributing a removed one is not
|
|
40
|
+
a store. That is the point of this working area, and the owner said so directly.
|
|
41
|
+
|
|
42
|
+
## Decision
|
|
43
|
+
|
|
44
|
+
### D1 — The transport is a Bongos-hosted module store
|
|
45
|
+
|
|
46
|
+
Cloud Bongos hosts the store. An author publishes a packaged module version to it; an
|
|
47
|
+
instance acquires a module from it; people pay for modules through it (the money movement
|
|
48
|
+
itself is area 8's, per the area-5 criteria — this ADR decides the channel, not the payment).
|
|
49
|
+
|
|
50
|
+
The shape the implementing tasks build to:
|
|
51
|
+
|
|
52
|
+
- **Artifact.** A module version is a tarball plus a manifest in the same format
|
|
53
|
+
`package-core.js` already produces for the core: per-file hashes and a
|
|
54
|
+
compression-independent `tree_sha256`, built through the same `isPublishable()`
|
|
55
|
+
default-deny gate (ADR 0098). No second packaging format.
|
|
56
|
+
- **Where it lives.** The registry rows (module, version, price, channel, score) are tables on
|
|
57
|
+
the control-plane database (tasks 1003812, 1003814, 1003815). The tarballs live in object
|
|
58
|
+
storage — the DO Spaces bucket the platform already pays for — or on the control plane's
|
|
59
|
+
disk while volume is small. The store is served by the control plane.
|
|
60
|
+
- **Acquire = check, then serve.** Download is an authenticated request; the server checks the
|
|
61
|
+
entitlement (task 1003813) and the channel the instance is on before it hands over the
|
|
62
|
+
artifact. A free module still goes through the same path with a zero price, so free and paid
|
|
63
|
+
are one code path (the owner's "assessed identically" rule applies to distribution too).
|
|
64
|
+
- **Verify on arrival.** `bongos module install` / `update` (tasks 1003785, 1003786) verify
|
|
65
|
+
the tarball against its manifest hashes before placing it under the loader's discovery root,
|
|
66
|
+
and record the exact version acquired.
|
|
67
|
+
- **Modules' own npm dependencies** keep travelling through npm as ADR 0138 already composes
|
|
68
|
+
them. The store carries the module's own code, not its third-party libraries.
|
|
69
|
+
|
|
70
|
+
**Cost.** This adds no new fixed cost today. If download volume outgrows the Spaces tier's
|
|
71
|
+
included transfer, that is the point to revisit, with serving from the control plane's disk as
|
|
72
|
+
the cheaper fallback (CLAUDE.md §4).
|
|
73
|
+
|
|
74
|
+
### D2 — An acquired module keeps working, forever; removal only stops what comes next
|
|
75
|
+
|
|
76
|
+
When a module is **delisted** by Metic+ (task 1003797), **withdrawn by its author**, or its
|
|
77
|
+
store entry is otherwise deleted:
|
|
78
|
+
|
|
79
|
+
- it **leaves the catalog** and **cannot be newly acquired**;
|
|
80
|
+
- **no further versions or updates** are served for it;
|
|
81
|
+
- **every instance that already acquired it keeps it, and it keeps working** — the store never
|
|
82
|
+
reaches into an instance to disable, remove or expire an acquired module, and an entitlement
|
|
83
|
+
is never revoked by a delist;
|
|
84
|
+
- the instance is **told** — `bongos upgrade` / the module update check (task 1003787) and the
|
|
85
|
+
hall's module view say plainly that this module will receive no more updates.
|
|
86
|
+
|
|
87
|
+
The artifact an instance acquired is already on that instance, so this costs the store nothing
|
|
88
|
+
to honour: it simply never deletes or disables what it has handed over.
|
|
89
|
+
|
|
90
|
+
The refund path in task 1003789 is the one place an entitlement can end, and it is a separate,
|
|
91
|
+
buyer-initiated act — a delist is not a refund.
|
|
92
|
+
|
|
93
|
+
## Consequences
|
|
94
|
+
|
|
95
|
+
- **Install, update and upgrade (1003785–1003787) are unblocked** against a concrete target:
|
|
96
|
+
an authenticated control-plane download of a hash-manifested tarball.
|
|
97
|
+
- **Delist (1003797) is a catalog and serving change only.** It must not touch installed
|
|
98
|
+
copies; a test should pin that a delisted module still loads on an instance that holds it.
|
|
99
|
+
- **Security issues are not a special case under D2.** The owner's rule is "always works; tell
|
|
100
|
+
the user". A module with a vulnerability is delisted and the holder is warned; the store does
|
|
101
|
+
not remotely disable it. If a remote kill switch is ever wanted, that needs a new ADR that
|
|
102
|
+
supersedes D2 — it is not implied here.
|
|
103
|
+
- **The store becomes something we run.** Upload, download, storage and its availability are
|
|
104
|
+
now platform responsibilities on the control plane. An outage blocks new installs and
|
|
105
|
+
updates; it never affects an already-installed module, by D2.
|
|
106
|
+
- **ADR 0135's per-instance submission queue** remains the path for proposing a module
|
|
107
|
+
*into core*. Publishing to the store is a different act (task scope for the publish command
|
|
108
|
+
is not yet filed and should be, alongside 1003785).
|
|
109
|
+
- **No cryptographic signing yet.** Integrity is the manifest hashes, delivered over TLS from
|
|
110
|
+
a server we control. Author signing can be added later without changing the transport.
|
|
111
|
+
|
|
112
|
+
## Rejected
|
|
113
|
+
|
|
114
|
+
- **npm packages** — public packages cannot withhold a paid module or be truly withdrawn, and
|
|
115
|
+
the owner has already retired one public npm channel (ADR 0324) on the judgement of what the
|
|
116
|
+
project wants to maintain.
|
|
117
|
+
- **Git refs** — cheapest to start, but no integrity story, no ranking hook, and a removal
|
|
118
|
+
cannot stop copies already cloned.
|
|
119
|
+
- **Remote disable on delist** — rejected by the owner: getting a module means keeping it.
|
package/docs/adr/README.md
CHANGED
|
@@ -429,3 +429,4 @@ This keeps the decision history honest and traceable.
|
|
|
429
429
|
| 0335 | [**An invite is a pre-approved row on the project's own instance, and `project.recruit` says who may send one** ([task 1002813](https://cloudbongos.com/builders#/task/1002813), R1 of the project admin console wave, goal 1000110 — BONGOS-V2). Supersedes ADR 0141 §5's "Invitations (deferred slice)" paragraph only. Formalises spike task 1002786's three decisions and reconciles them with six weeks of shipping: the instance invite route (task 1003044), the PATCH rescind (ADR 0201 D4.2) and the composed join door (ADR 0247) already exist, and the hub invite's dead end is now reachable from the creation wizard. **Decisions:** (D1) authority lives on the project's own instance, and the instance IS the project; the hub is identity, relay, echo, notification and links, and never writes a project's admission row; (D2) a recruit invite is an `access_requests` row with `status='invited'`, so signing in is the acceptance act and the gate is untouched; the row gains `kind ∈ (application, invite)` and `created_by` (the inviter, which a rescind would otherwise overwrite), reuses `note`, backfills exactly (`created_at = resolved_at`), and neither column ever enters an admission read; (D3) one new grantable atom, `project.recruit` (`system:false`, floor archon, widened per instance through the existing governance tab), guards the invite, the outstanding list, the invite rescind and the queue read, while `access_request.review` keeps the verdict; because inviting is admitting, a recruiter without the review atom may not approve or re-admit an APPLICANT through the invite route (409 to the queue); the hub sliver moves onto the atom, and the hub directory and hub invite KEEP `project.curate`, revising ADR 0210/0215/0248, since swapping the directory would raise a gate on a viewing surface (ADR 0217). **Task map:** R2 (task 1002814) = the migration, the invite writes and the sender list, still on `access_request.review`; R3 (task 1002815) = mint the atom, move the gates, the 409 clause, the sliver swap and the pins, and it lands after R2; R5 (task 1002817) is already delivered, so close it into task 1002331; task 1003362 folds into R3. Rejected: a new invitations table; the hub pending row as the invite; widening `access_request.review` instead of minting an atom; a per-project settings row for the floor; letting a recruiter's invite approve applications; `system:true`; swapping `project.curate` on the hub directory.](0335-an-invite-is-a-pre-approved-row-on-the-projects-own-instance.md) | governance / onboarding / platform-identity |
|
|
430
430
|
| 0336 | [**A guild is a hub-side group whose members each consent twice, and its collective record is only ever the sum of what those members already show** ([task 1002299](https://cloudbongos.com/builders#/task/1002299), goal 1000110 — the R23 design spike for goal 1000045's C7/C8/C9). No platform guild existed (every "guild" in the tree is Discord's). Decisions: (1) three hub tables in `platform-identity`, in their own `guilds.js` (the 1,500-line ratchet), distinct from instance-local `goal_members`: guilds (slug, name, `visibility ∈ public|unlisted` with **no default**), members (`membership_kind ∈ owner|member`, never `role` per ADR 0174; a partial unique index holds exactly one owner; a per-member `shown_publicly`), and requests; (2) two consents to join, no self-join, through ADR 0202's connections lifecycle verbatim (silent decline, 90-day swallowed re-ask, 30-day read-time expiry); `accountActiveSql` floors the feature; an **unlisted guild 404s to everyone on the public read, members included**, and takes invites only, because a slug is guessable; (3) the public roster is `shown_publicly AND accountVisibleSql`, and widening a guild to public changes no member's flag but the flipping owner's, so consent given to a small audience is never reused for a large one; (4) **the privacy resolution — NOT ratified; superseded by [ADR 0337](0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md) D4.4:** it proposed that a member is counted iff shown AND `rollupVisibleSql`, read-time, `no-store`, never stored, and N and M are both lengths of lists the reader can see. The guild record is therefore always a sum of numbers already on members' own public profiles, so nothing can be subtracted out. All-or-nothing is rejected (one member blanks everyone and hiding becomes costly), as is per-member consent to count hidden numbers (it breaks "hide_stats beats everything"). The trade-off: guilds of privacy-minded builders look smaller and rank lower. A union count of all members' projects is refused because it reveals shared private projects; (5) the namespace is `/g/<slug>`, `g` is reserved in the SEO spec's project-slug list, the slug is fixed in V1, and `tests/guild_namespace_reserved.mjs` pins the two documents together; (6) a guild engages a project only through the individual door: a guild application is each member's own vouched application, labelled through ADR 0260's live applicant-profile read (no new instance field); inviting a guild fans out ordinary individual invites to its public roster at that moment, **never a guild-level pre-approval** (that would let a guild owner decide admission to someone else's project); (7) the map ranks public guilds by size and counted works shipped, read-time, with a slug tiebreak, and R29 narrows to the visual. Scope changes for R24–R37 are tabled in the ADR, including R28's dependency on the abandoned R11 and R29/R31's archived orb goals.](0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md) | platform identity / privacy / social graph |
|
|
431
431
|
| 0337 | [**The hub account is the person, a hall account is their seat in one project, so anything that spans projects lives on the hub** ([task 1004255](https://cloudbongos.com/builders#/task/1004255), goal 1000110 — the owner's hub/hall re-scope, 2026-09-24/25). Extends [ADR 0141](<redacted>.md) from identity to placement: (1) a surface about the person across projects, or about more than one project, goes on the HUB (`public-landing`, `platform-identity`): people directory, community, guilds, connections, recruiting, platform-account settings; one about this project goes in the HALL (`hall-ui`); "Builders you may know" is on both; `status-ui` is the status dashboard, not the hall; (2) no authority moves, and hub actions still land as the project's own rows; (3) hall `/people` retires, ADR 0334's recruiting directory moves unchanged to the hub community page, `/roster` is the in-project directory; (4) owner calls: the hidden-builder recruiter panel is recruiter-only while public search is unchanged (amends community-tab spec D8); platform-account settings get a new hub page; **guild totals count hidden and private members by default, with a notice on the join card and a per-guild off switch (`counted_in_totals`), rounded to a 1–2–5 band, only once a guild has at least 3 of them, refreshed weekly; size stays the public roster length** (supersedes ADR 0336 D5; relaxes the privacy spec's "`hide_stats` beats everything" for guild totals only, for a member who was told), with the fake-account padding limit stated and accepted.](0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md) | platform identity / placement / privacy |
|
|
432
|
+
| 0338 | [**Modules travel through a Bongos-hosted store, and an acquired module keeps working forever** ([task 1003782](https://cloudbongos.com/builders#/task/1003782), goal 1000091 — working area 5, owner Will). A module had no transport of its own: it reached an instance only inside the core (ADR 0107 §6) or in the instance's repo. Weighed npm packages, a Bongos-hosted store and git refs against the four things area 5's criteria require — entitlement before install, a per-version score, a tester channel, and a delist that stops distribution. npm and git refs fail entitlement and delist by construction. **D1:** Cloud Bongos hosts the store; a module version is a tarball + hash manifest in the same format `package-core.js` already emits (same `isPublishable()` gate), registry rows on the control-plane DB, artifacts in the already-paid DO Spaces bucket, served by the control plane only after an entitlement + channel check; free modules take the same path at price zero; install/update verify hashes on arrival; modules' own npm deps still travel via npm (ADR 0138). No new fixed cost. **D2 (owner):** delisting, author withdrawal or deletion removes a module from the catalog and stops new acquisitions and updates, but **an acquired module keeps working forever** — the store never disables or removes an installed copy and a delist never revokes an entitlement; the holder is told no more updates will come. Security issues are not a special case; a remote kill switch would need a superseding ADR. Rejected: npm, git refs, remote disable on delist.](0338-modules-travel-through-a-bongos-hosted-store.md) | modules / distribution / economy |
|
|
@@ -2539,5 +2539,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2539
2539
|
landed since 1.19.1028 with no explicit bump. run 36189693139. (task 1002620)
|
|
2540
2540
|
1.19.1030 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2541
2541
|
landed since 1.19.1029 with no explicit bump. run 36190779629. (task 1002620)
|
|
2542
|
+
1.19.1031 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2543
|
+
landed since 1.19.1030 with no explicit bump. run 36192093201. (task 1002620)
|
|
2544
|
+
1.19.1032 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2545
|
+
landed since 1.19.1031 with no explicit bump. run 36192968150. (task 1002620)
|
|
2542
2546
|
---------------------------------------------------------------------------
|
|
2543
2547
|
```
|
|
@@ -157,8 +157,14 @@ async function getLastClaimHolder(taskId) {
|
|
|
157
157
|
// /grade-recover reads a failed grade as having no findings at all. Lifting is
|
|
158
158
|
// additive: `signals.issues` stays exactly where it is for anything already
|
|
159
159
|
// reading it.
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
//
|
|
161
|
+
// `deps.pool` is the family's test-only pool seam (task 538), honoured ONLY under
|
|
162
|
+
// NODE_ENV==='test' (the same line moduleScopeForActiveClaims uses above), so
|
|
163
|
+
// tests/grade_diff_hash.mjs can drive the lift with a fake row instead of a
|
|
164
|
+
// real Postgres (task 1003490). Production callers pass the task id alone.
|
|
165
|
+
async function getTaskGrade(taskId, deps = {}) {
|
|
166
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
167
|
+
const { rows } = await activePool.query(
|
|
162
168
|
`SELECT diff_hash, passed, score, threshold, grader_kind, grader_version,
|
|
163
169
|
rubric_scores, signals, notes_md
|
|
164
170
|
FROM task_grades
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1032",
|
|
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.1032",
|
|
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.1032",
|
|
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",
|
|
@@ -162,6 +162,20 @@ const WORKER_ENV_ALLOW = [
|
|
|
162
162
|
'BONGOS_API_BASE', 'CLOUDBONGOS_API_BASE',
|
|
163
163
|
'OTB_CLAUDE_BIN', 'CLAUDE_BIN',
|
|
164
164
|
'XDG_CONFIG_HOME',
|
|
165
|
+
// THE WORKER'S OWN CREDENTIAL, and it has to be here (task 1003907, found by
|
|
166
|
+
// running it). On a desktop, the claude CLI authenticates from the credential
|
|
167
|
+
// store under USERPROFILE/HOME, which are already allowed - so this list looked
|
|
168
|
+
// complete. On a HEADLESS host there is no interactive login to have written
|
|
169
|
+
// one, and setup.js's own preflight says to set CLAUDE_CODE_OAUTH_TOKEN there
|
|
170
|
+
// instead. Stripping it meant every worker on the machine this was built for
|
|
171
|
+
// started unauthenticated, exited in ~2s, and the run recorded only "worker
|
|
172
|
+
// output was not JSON".
|
|
173
|
+
//
|
|
174
|
+
// This is not the leak the list exists to prevent. The worker IS a Claude
|
|
175
|
+
// session; its own auth is the one credential it cannot do its job without.
|
|
176
|
+
// Everything the comment above refuses - DB URLs, git and npm tokens - stays
|
|
177
|
+
// refused.
|
|
178
|
+
'CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY',
|
|
165
179
|
];
|
|
166
180
|
|
|
167
181
|
function workerEnv(source = process.env, extra = {}) {
|
|
@@ -175,12 +189,46 @@ function workerEnv(source = process.env, extra = {}) {
|
|
|
175
189
|
|
|
176
190
|
const MAX_WORKER_OUTPUT = 8 * 1024 * 1024; // a misbehaving worker must not grow memory until the timeout
|
|
177
191
|
|
|
192
|
+
// resolveWorkerBin — WHICH `claude`, and whether it needs a shell.
|
|
193
|
+
//
|
|
194
|
+
// THE BUG THIS EXISTS FOR (task 1003907, found by running it on the real Windows
|
|
195
|
+
// host). On Windows `claude` on PATH is a .cmd shim, and Node has refused to
|
|
196
|
+
// spawn .cmd/.bat without a shell since CVE-2024-27980 — so this used to pass
|
|
197
|
+
// `shell: process.platform === 'win32'`. But `shell: true` does not pass argv
|
|
198
|
+
// through: it CONCATENATES it into one command line that cmd.exe re-parses,
|
|
199
|
+
// which destroys every embedded quote. `--json-schema` is a JSON blob made
|
|
200
|
+
// almost entirely of quotes, so claude received a mangled argument and exited in
|
|
201
|
+
// ~2s with `--json-schema is not valid JSON`. Every worker. Every run. Forever —
|
|
202
|
+
// and the cadence classes that as `quiet`, so it never trips the failure ladder.
|
|
203
|
+
//
|
|
204
|
+
// The npm package ships a REAL claude.exe beside the shim, so prefer it: a true
|
|
205
|
+
// executable spawns with shell:false and exact argv, and nothing re-parses.
|
|
206
|
+
// Falling back to the old behaviour is deliberate — a host with a differently
|
|
207
|
+
// laid-out install should still run, and a corrupted schema surfaces as a failed
|
|
208
|
+
// worker rather than as a refusal to start at all.
|
|
209
|
+
function resolveWorkerBin(env = process.env, platform = process.platform) {
|
|
210
|
+
const override = env.OTB_CLAUDE_BIN || env.CLAUDE_BIN;
|
|
211
|
+
if (override) return { bin: override, shell: platform === 'win32' && /\.(cmd|bat)$/i.test(override) };
|
|
212
|
+
if (platform !== 'win32') return { bin: 'claude', shell: false };
|
|
213
|
+
for (const dir of String(env.PATH || env.Path || '').split(path.delimiter)) {
|
|
214
|
+
if (!dir) continue;
|
|
215
|
+
const candidates = [
|
|
216
|
+
path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
|
|
217
|
+
path.join(dir, 'claude.exe'),
|
|
218
|
+
];
|
|
219
|
+
for (const c of candidates) {
|
|
220
|
+
try { if (fs.existsSync(c)) return { bin: c, shell: false }; } catch (_) {}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { bin: 'claude', shell: true };
|
|
224
|
+
}
|
|
225
|
+
|
|
178
226
|
// spawnWorker — one headless session, bounded by WALL CLOCK, in the task's own
|
|
179
227
|
// working tree.
|
|
180
228
|
async function spawnWorker({ task, model, timeoutMs, cwd, deps = {} }) {
|
|
181
229
|
const spawnFn = deps.spawn || spawn;
|
|
182
230
|
const sessionId = deps.sessionId || crypto.randomUUID(); // pinned, so the session can be resumed
|
|
183
|
-
const bin =
|
|
231
|
+
const { bin, shell: workerShell } = deps.resolveBin ? deps.resolveBin() : resolveWorkerBin();
|
|
184
232
|
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
185
233
|
const args = buildWorkerArgs({ sessionId, model, isRoot });
|
|
186
234
|
const prompt = buildWorkerPrompt({ task });
|
|
@@ -194,7 +242,7 @@ async function spawnWorker({ task, model, timeoutMs, cwd, deps = {} }) {
|
|
|
194
242
|
// `claude -p` with an open inherited stdin waits seconds for input that
|
|
195
243
|
// never comes, on every single spawn.
|
|
196
244
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
197
|
-
shell:
|
|
245
|
+
shell: workerShell,
|
|
198
246
|
windowsHide: true,
|
|
199
247
|
env: workerEnv(),
|
|
200
248
|
});
|
|
@@ -749,7 +797,7 @@ if (require.main === module) {
|
|
|
749
797
|
}
|
|
750
798
|
|
|
751
799
|
module.exports = {
|
|
752
|
-
readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW,
|
|
800
|
+
readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW, resolveWorkerBin,
|
|
753
801
|
worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX,
|
|
754
802
|
heartbeatPath, writeHeartbeat, readHeartbeat, pidAlive, anotherRunnerIsAlive, HEARTBEAT_STALE_MS,
|
|
755
803
|
isRunnerTree, RUNNER_TREE_RE, postHeartbeat, RUNNER_STARTED_AT,
|
|
@@ -89,7 +89,12 @@ switch ($Action) {
|
|
|
89
89
|
Assert-Prereqs
|
|
90
90
|
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
|
91
91
|
|
|
92
|
-
$action
|
|
92
|
+
# NOT $action: PowerShell variable names are CASE-INSENSITIVE, so $action IS the
|
|
93
|
+
# $Action parameter above, and its [ValidateSet]/[string] attributes stay attached
|
|
94
|
+
# to the variable. Assigning a task-action object to it throws "the value
|
|
95
|
+
# MSFT_TaskExecAction is not a valid value for the Action variable" and install
|
|
96
|
+
# fails every time, on every machine. Same trap as the `run` shadow in task 1003908.
|
|
97
|
+
$taskAction = New-ScheduledTaskAction -Execute $Wrapper -WorkingDirectory $RepoRoot
|
|
93
98
|
# AtStartup covers the cold reboot the done-when actually names. AtLogOn is
|
|
94
99
|
# NOT used: it would wait for someone to sign in, which is the one thing that
|
|
95
100
|
# must not be required.
|
|
@@ -107,7 +112,7 @@ switch ($Action) {
|
|
|
107
112
|
# MultipleInstances IgnoreNew = the scheduler will not start a second copy
|
|
108
113
|
# over a live one; the heartbeat file is the second guard, inside the runner.
|
|
109
114
|
|
|
110
|
-
Register-ScheduledTask -TaskName $TaskName -Action $
|
|
115
|
+
Register-ScheduledTask -TaskName $TaskName -Action $taskAction -Trigger $trigger `
|
|
111
116
|
-Principal $principal -Settings $settings -Force | Out-Null
|
|
112
117
|
|
|
113
118
|
Write-Host "installed '$TaskName' -- starts at boot, restarts on crash, logs to $LogFile"
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.1032'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -563,3 +563,94 @@ test('the module-level exec helper is reachable from inside iteration()', async
|
|
|
563
563
|
assert.equal(picked, true, 'iteration must reach the picker without touching an un-injected binding');
|
|
564
564
|
assert.equal(row.event, 'nothing_claimable');
|
|
565
565
|
});
|
|
566
|
+
|
|
567
|
+
// --- resolveWorkerBin: the Windows spawn that corrupted every worker ---------
|
|
568
|
+
//
|
|
569
|
+
// Found by task 1003907 running the thing rather than testing it. `shell: true`
|
|
570
|
+
// does not pass argv through on Windows: it CONCATENATES it into one command
|
|
571
|
+
// line that cmd.exe re-parses, destroying the quotes in --json-schema. claude
|
|
572
|
+
// then exited in ~2s with "--json-schema is not valid JSON" on EVERY worker, and
|
|
573
|
+
// cadence.js classes that outcome as `quiet`, which neither trips nor forgives
|
|
574
|
+
// the failure count — so a totally broken runner never escalated and looked
|
|
575
|
+
// healthy in the hall. The invariant: on win32, never ask for a shell when a
|
|
576
|
+
// real executable is available.
|
|
577
|
+
//
|
|
578
|
+
// Paths here use forward slashes deliberately. The resolver only string-matches
|
|
579
|
+
// the extension on an override, and backslash-heavy literals are their own
|
|
580
|
+
// source of bugs in a test that exists because of quoting.
|
|
581
|
+
|
|
582
|
+
test('resolveWorkerBin: a .cmd override still gets its shell', () => {
|
|
583
|
+
const got = runner.resolveWorkerBin({ CLAUDE_BIN: 'C:/x/claude.cmd' }, 'win32');
|
|
584
|
+
assert.equal(got.bin, 'C:/x/claude.cmd');
|
|
585
|
+
assert.equal(got.shell, true, 'Node cannot spawn a .cmd without a shell (CVE-2024-27980)');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('resolveWorkerBin: a real .exe override is never re-parsed by cmd.exe', () => {
|
|
589
|
+
const got = runner.resolveWorkerBin({ OTB_CLAUDE_BIN: 'C:/x/claude.exe' }, 'win32');
|
|
590
|
+
assert.equal(got.shell, false);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test('resolveWorkerBin: win32 with nothing on PATH falls back rather than refusing', () => {
|
|
594
|
+
const got = runner.resolveWorkerBin({ PATH: 'C:/nowhere-at-all' }, 'win32');
|
|
595
|
+
assert.deepEqual(got, { bin: 'claude', shell: true },
|
|
596
|
+
'a differently-laid-out install must still start; a mangled schema surfaces as a failed worker');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('resolveWorkerBin: non-windows never asks for a shell', () => {
|
|
600
|
+
assert.deepEqual(runner.resolveWorkerBin({ PATH: '/usr/bin' }, 'linux'), { bin: 'claude', shell: false });
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test('the schema argument is the one that cannot survive concatenation', () => {
|
|
604
|
+
const args = buildWorkerArgs({ sessionId: 'abc' });
|
|
605
|
+
const schema = args[args.indexOf('--json-schema') + 1];
|
|
606
|
+
assert.ok(schema.includes('"'), 'the regression only bites because the schema carries quotes');
|
|
607
|
+
assert.ok(schema.length > 50, 'and because it must arrive as one intact argv slot');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// The test that would actually have caught the shipped bug: the resolver's
|
|
611
|
+
// answer has to REACH the spawn options. Testing resolveWorkerBin alone leaves
|
|
612
|
+
// `shell: process.platform === 'win32'` free to reappear at the call site.
|
|
613
|
+
test('spawnWorker passes the resolver’s shell verdict straight to spawn', async () => {
|
|
614
|
+
const { EventEmitter } = require('node:events');
|
|
615
|
+
let opts = null; let binSeen = null; let argvSeen = null;
|
|
616
|
+
const fakeSpawn = (bin, args, o) => {
|
|
617
|
+
binSeen = bin; argvSeen = args; opts = o;
|
|
618
|
+
const child = new EventEmitter();
|
|
619
|
+
child.stdout = new EventEmitter();
|
|
620
|
+
child.stderr = new EventEmitter();
|
|
621
|
+
child.stdin = { end: () => setImmediate(() => child.emit('close', 0)) };
|
|
622
|
+
child.kill = () => {};
|
|
623
|
+
return child;
|
|
624
|
+
};
|
|
625
|
+
await runner.spawnWorker({
|
|
626
|
+
task: { id: 7, title: 't', description: 'd' },
|
|
627
|
+
timeoutMs: 1000,
|
|
628
|
+
cwd: '.',
|
|
629
|
+
deps: { spawn: fakeSpawn, resolveBin: () => ({ bin: 'C:/x/claude.exe', shell: false }) },
|
|
630
|
+
});
|
|
631
|
+
assert.equal(opts.shell, false, 'a resolved .exe must spawn WITHOUT a shell — this is the whole fix');
|
|
632
|
+
assert.equal(binSeen, 'C:/x/claude.exe');
|
|
633
|
+
const schema = argvSeen[argvSeen.indexOf('--json-schema') + 1];
|
|
634
|
+
assert.equal(schema, JSON.stringify(WORKER_VERDICT_SCHEMA), 'the schema must arrive as one intact argv slot');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// The worker's own credential must survive the env allowlist (task 1003907).
|
|
638
|
+
// Stripping it meant every worker on a headless host started unauthenticated and
|
|
639
|
+
// exited in ~2s with no usable output — while the supervisor recorded only
|
|
640
|
+
// "worker output was not JSON" and the cadence called that quiet.
|
|
641
|
+
test('workerEnv passes Claude auth through, and still refuses the rest', () => {
|
|
642
|
+
const src = {
|
|
643
|
+
PATH: '/usr/bin',
|
|
644
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'tok',
|
|
645
|
+
ANTHROPIC_API_KEY: 'key',
|
|
646
|
+
DATABASE_URL: 'postgres://nope',
|
|
647
|
+
GITHUB_TOKEN: 'ghp_nope',
|
|
648
|
+
NPM_TOKEN: 'npm_nope',
|
|
649
|
+
};
|
|
650
|
+
const env = runner.workerEnv(src);
|
|
651
|
+
assert.equal(env.CLAUDE_CODE_OAUTH_TOKEN, 'tok', 'a worker that cannot authenticate cannot work');
|
|
652
|
+
assert.equal(env.ANTHROPIC_API_KEY, 'key');
|
|
653
|
+
for (const leak of ['DATABASE_URL', 'GITHUB_TOKEN', 'NPM_TOKEN']) {
|
|
654
|
+
assert.equal(env[leak], undefined, `${leak} must never reach a bypassPermissions worker`);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// Covers:
|
|
6
6
|
// 1. diffHash / shouldReuseGrade — pure helpers in ship.js (no git/network)
|
|
7
7
|
// 2. db.applyGrade stores diff_hash — SQL-aware fakePool (NODE_ENV=test seam)
|
|
8
|
-
// 3. db.getTaskGrade
|
|
9
|
-
//
|
|
8
|
+
// 3. db.getTaskGrade — exported, returns null for a task with no grade, and
|
|
9
|
+
// lifts signals.issues to the top level without removing the nested key
|
|
10
|
+
// (task 1003490; driven through the NODE_ENV=test deps.pool seam)
|
|
10
11
|
//
|
|
11
12
|
// Run: node tests/grade_diff_hash.mjs
|
|
12
13
|
|
|
@@ -210,6 +211,80 @@ await test('getTaskGrade is exported from lifecycle/db.js', () => {
|
|
|
210
211
|
assert.equal(typeof db.getTaskGrade, 'function', 'getTaskGrade must be exported');
|
|
211
212
|
});
|
|
212
213
|
|
|
214
|
+
// task 1003490 — the issues LIFT (task 1003488's second half). task_grades has
|
|
215
|
+
// no `issues` column, so applyGrade stores the findings under signals.issues;
|
|
216
|
+
// getTaskGrade copies them to the top level so a reader that does not know that
|
|
217
|
+
// storage detail still sees them. These drive the real function through the
|
|
218
|
+
// deps.pool seam (honoured because NODE_ENV === 'test' above) with a fake row,
|
|
219
|
+
// so dropping or inverting the lift turns this file red.
|
|
220
|
+
function gradeRowPool(row) {
|
|
221
|
+
const calls = [];
|
|
222
|
+
return {
|
|
223
|
+
calls,
|
|
224
|
+
async query(sql, params) {
|
|
225
|
+
calls.push({ sql, params });
|
|
226
|
+
return { rows: row === undefined ? [] : [row] };
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function storedGradeRow(signals) {
|
|
231
|
+
return {
|
|
232
|
+
diff_hash: 'abc123', passed: false, score: 4.5, threshold: 6,
|
|
233
|
+
grader_kind: 'subagent-panel', grader_version: 'v4.0',
|
|
234
|
+
rubric_scores: { functional_fidelity: 9, code_quality: 2 },
|
|
235
|
+
signals, notes_md: 'FAIL',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await test('getTaskGrade lifts signals.issues onto the returned row', async () => {
|
|
240
|
+
const stored = [
|
|
241
|
+
{ severity: 'blocker', file: 'a.js', summary: 'breaks the build', worker: 'quality' },
|
|
242
|
+
{ severity: 'nit', file: 'b.js', summary: 'naming', worker: 'narc' },
|
|
243
|
+
];
|
|
244
|
+
const pool = gradeRowPool(storedGradeRow({ panel_outcome: 'fail', issues: stored }));
|
|
245
|
+
const grade = await db.getTaskGrade(42, { pool });
|
|
246
|
+
assert.deepEqual(grade.issues, stored, 'the stored findings appear at the top level');
|
|
247
|
+
assert.equal(pool.calls.length, 1, 'one read');
|
|
248
|
+
assert.match(pool.calls[0].sql, /FROM task_grades/);
|
|
249
|
+
assert.deepEqual(pool.calls[0].params, [42], 'keyed on the task id');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await test('getTaskGrade leaves signals.issues in place (the lift is additive)', async () => {
|
|
253
|
+
const stored = [{ severity: 'major', file: 'c.js', summary: 'unchecked input' }];
|
|
254
|
+
const grade = await db.getTaskGrade(42, {
|
|
255
|
+
pool: gradeRowPool(storedGradeRow({ panel_outcome: 'fail', issue_count: 1, issues: stored })),
|
|
256
|
+
});
|
|
257
|
+
assert.deepEqual(grade.signals.issues, stored, 'signals.issues is still there for readers of the nested key');
|
|
258
|
+
assert.equal(grade.signals.panel_outcome, 'fail', 'the rest of signals is untouched');
|
|
259
|
+
assert.equal(grade.signals.issue_count, 1);
|
|
260
|
+
assert.equal(grade.score, 4.5, 'the row columns ride through unchanged');
|
|
261
|
+
assert.deepEqual(grade.rubric_scores, { functional_fidelity: 9, code_quality: 2 });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await test('getTaskGrade yields issues: [] (not undefined) when signals carries no issues', async () => {
|
|
265
|
+
const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow({ panel_outcome: 'pass' })) });
|
|
266
|
+
assert.ok(Array.isArray(grade.issues), `issues must be an array, got ${typeof grade.issues}`);
|
|
267
|
+
assert.deepEqual(grade.issues, []);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await test('getTaskGrade yields issues: [] when signals itself is null', async () => {
|
|
271
|
+
const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow(null)) });
|
|
272
|
+
assert.deepEqual(grade.issues, []);
|
|
273
|
+
assert.equal(grade.signals, null, 'a null signals column is passed through, not invented');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
for (const [label, malformed] of [['a string', 'blocker: it is broken'], ['null', null], ['an object', { severity: 'blocker' }]]) {
|
|
277
|
+
await test(`getTaskGrade yields issues: [] when signals.issues is ${label}`, async () => {
|
|
278
|
+
const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow({ issues: malformed })) });
|
|
279
|
+
assert.deepEqual(grade.issues, [], 'a malformed stored value never reaches the top level');
|
|
280
|
+
assert.deepEqual(grade.signals.issues, malformed, 'and the nested key is still left exactly as stored');
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await test('getTaskGrade returns null when the task has no grade row', async () => {
|
|
285
|
+
assert.equal(await db.getTaskGrade(42, { pool: gradeRowPool(undefined) }), null);
|
|
286
|
+
});
|
|
287
|
+
|
|
213
288
|
// ======================================================================
|
|
214
289
|
// Summary
|
|
215
290
|
// ======================================================================
|