@bongos/core 1.21.99 → 1.21.101

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.21.99",
6
- "core_contract": "1.21.99",
7
- "source_commit": "262e039f7737aee04e24db9559e593544fab5532",
5
+ "core_version": "1.21.101",
6
+ "core_contract": "1.21.101",
7
+ "source_commit": "7858ffa1199248ca5c40837b5f21f0e8ef19d47f",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-09T12:05:36.533Z",
9
+ "built_at": "2026-10-09T13:00:03.202Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 591,
12
+ "docs_redacted": 592,
13
13
  "agent_docs_stubbed": 28,
14
14
  "functional_verbatim": 3019,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3639,
20
- "tree_sha256": "6627907aa4a62c0516525dd3273bf188497dd8914388d4fcd5435d4563149bbf",
19
+ "file_count": 3640,
20
+ "tree_sha256": "02e0d13b4633dbacbe287ad2057cb25994404d346b87f7f87f013d0f367ee8c2",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -294,6 +294,11 @@
294
294
  "mode": "0000644",
295
295
  "sha256": "b151b43139e62a6b71efc49edd447ee0c4b6a95deb42d7a840b851ab79ec4782"
296
296
  },
297
+ {
298
+ "path": "GOVERNMENT.md",
299
+ "mode": "0000644",
300
+ "sha256": "a1e334f7814b8294c460be15ad7f3bad0b7ea5be8e2463c1d7f368a149e5f5c0"
301
+ },
297
302
  {
298
303
  "path": "LICENSE",
299
304
  "mode": "0000644",
@@ -302,7 +307,7 @@
302
307
  {
303
308
  "path": "README.md",
304
309
  "mode": "0000644",
305
- "sha256": "9c9e99127efafe70d60e1d464f07815397e436e69dc0b82c2e67f41dbd8dc8d6"
310
+ "sha256": "abbf022b58c07144e4ae1d57cde3a232237099c1f37474310d56abae1992c141"
306
311
  },
307
312
  {
308
313
  "path": "bin/bongos.js",
@@ -2867,7 +2872,7 @@
2867
2872
  {
2868
2873
  "path": "docs/module-api-changelog.md",
2869
2874
  "mode": "0000644",
2870
- "sha256": "d43b9ee9d1bae22277b8e9b9d425aafef3d1c2c5298814fd6a1a319e632917b7"
2875
+ "sha256": "cf3cee3fb9bea331857c69de667438a0c83137849f262712af3de7172e701e26"
2871
2876
  },
2872
2877
  {
2873
2878
  "path": "docs/modules-contract.md",
@@ -2927,7 +2932,7 @@
2927
2932
  {
2928
2933
  "path": "docs/onboarding/primer.md",
2929
2934
  "mode": "0000644",
2930
- "sha256": "f347c1355e2f1a71f3170a260a9c7cec719624c47b24ba89a6db371868caefdc"
2935
+ "sha256": "bed8d38e81c9b6cf0b67d589f7e6ab4362034776c651c1a3743d6d98d6f6c8eb"
2931
2936
  },
2932
2937
  {
2933
2938
  "path": "docs/onboarding/slash-commands.md",
@@ -10127,12 +10132,12 @@
10127
10132
  {
10128
10133
  "path": "package-lock.json",
10129
10134
  "mode": "0000644",
10130
- "sha256": "743e74b9cb354d11b19bb40b81f9640932ac68730487816569ee5796875392a4"
10135
+ "sha256": "dd270ec93485ddb27786d87ba9eb34a588c39492310114df6c22efb4a99ee447"
10131
10136
  },
10132
10137
  {
10133
10138
  "path": "package.json",
10134
10139
  "mode": "0000644",
10135
- "sha256": "e74953a3c03fb015d7f54229580414d430cff7159d5361dca7754eb28d852310"
10140
+ "sha256": "888c5c97454286a61cc39bbe0c14547d92765fa057195303f3f3352410edcfa6"
10136
10141
  },
10137
10142
  {
10138
10143
  "path": "public-docs/index.html",
@@ -10152,7 +10157,7 @@
10152
10157
  {
10153
10158
  "path": "release-notes.json",
10154
10159
  "mode": "0000644",
10155
- "sha256": "76a1303dec3ba5b72f9a7eb03de1896d6dafc040c8c9260af6c7b8fe2a6e58cf"
10160
+ "sha256": "d1156f6a018933ff1c6f4f4481a4089ac754815318468fa5ce6bdd880105bbdb"
10156
10161
  },
10157
10162
  {
10158
10163
  "path": "scripts/cron/style_guide_audit.sh",
@@ -11237,7 +11242,7 @@
11237
11242
  {
11238
11243
  "path": "scripts/gds/publish-manifest.js",
11239
11244
  "mode": "0000644",
11240
- "sha256": "e698374e1c0709b343202ccd3b336c4fc8b7b9523f7f15291f99cff0f43fd759"
11245
+ "sha256": "d059af3ab3d980229ea02073b69d8281c7dd8526d8a3a1445e89be51cd36fb50"
11241
11246
  },
11242
11247
  {
11243
11248
  "path": "scripts/gds/publish-watch.js",
@@ -11927,7 +11932,7 @@
11927
11932
  {
11928
11933
  "path": "scripts/gds/worktree.js",
11929
11934
  "mode": "0000644",
11930
- "sha256": "69f167d079c1900568f740d8d4a1a2288f5ff47146634209f8a1a9350a3aa5e2"
11935
+ "sha256": "40aa204dd081022580bbd016b1f2741ebfc4eb9324d4f60a2ac7da9636d813f1"
11931
11936
  },
11932
11937
  {
11933
11938
  "path": "scripts/hall-preview/README.md",
@@ -12187,7 +12192,7 @@
12187
12192
  {
12188
12193
  "path": "src/bongos/module-scope-map.js",
12189
12194
  "mode": "0000644",
12190
- "sha256": "4a42901ad09df965b0b42ce2cb75c0993adfd49b560b050d99419cba1ccdcaa4"
12195
+ "sha256": "67711bf6dd6becd1edeb6866279a788fd6360a51d013eed77f2592be0aaab01b"
12191
12196
  },
12192
12197
  {
12193
12198
  "path": "src/bongos/module-store.js",
@@ -12427,7 +12432,7 @@
12427
12432
  {
12428
12433
  "path": "src/module-api.js",
12429
12434
  "mode": "0000644",
12430
- "sha256": "fe5b5772fd9a33f1aa10b93689e32179c6c81d9dbf9a3a33ee4af2383a3bdb02"
12435
+ "sha256": "515c7e51fc93bf9b5fe62e46252650d733818c4e9fd6e7ab3e310fbd6593564a"
12431
12436
  },
12432
12437
  {
12433
12438
  "path": "src/module-loader/catalog.js",
@@ -18202,7 +18207,7 @@
18202
18207
  {
18203
18208
  "path": "tests/worktree_helper.mjs",
18204
18209
  "mode": "0000644",
18205
- "sha256": "cac88e36d1e2ef2cbe2799275186f0787431b5b636fc52d1325b5dbbfcd56368"
18210
+ "sha256": "0dea42cbad0d17eb63e516a6812050b239c51b0f2f2d13c3a949eff27ec44657"
18206
18211
  },
18207
18212
  {
18208
18213
  "path": "tests/world_tokens.mjs",
package/GOVERNMENT.md ADDED
@@ -0,0 +1,145 @@
1
+ # How government works
2
+
3
+ > This is about **your project's** government. How Cloud Bongos itself is run is a different file: [`GOVERNANCE.md`](GOVERNANCE.md).
4
+
5
+ Every project on Cloud Bongos has a government. It answers one question: when something needs deciding, who decides, and how? It's not a theme or a label on a page. It's the rules the server actually checks every time someone tries to do something, so the government you pick is the way your project gets steered. Pick it at the start, change it later by vote, and every change is on the record.
6
+
7
+ A few words you'll see a lot:
8
+
9
+ - **Rank**: your standing on the project. There are four built-in ranks. **Archon** is the founder. **Metic** is a trusted builder, a rank you earn by shipping. **Thetes** and **Xenos** are newer builders, with Xenos the newest. You can also make your own ranks, like a council.
10
+ - **Board**: the people who get a vote.
11
+ - **Sitting**: one vote on one thing. It stays open until it closes as **passed** or **returned** (sent back).
12
+ - **Amendment**: a proposal to change the rules. The government you have now decides it, not the one you're proposing.
13
+
14
+ ## The four forms you can pick today
15
+
16
+ A form is just two settings together: who votes, and how many have to agree.
17
+
18
+ | Form | Who votes | What passes | Good for |
19
+ |---|---|---|---|
20
+ | **Monarchy / BDFL** ("benevolent dictator for life") | Only the founder | The founder's one yes | A solo start, when there's nobody else to ask. Every project is born one. |
21
+ | **Timocratic Republic** | Trusted builders (Metic) and the founder | Passes unless someone objects | A small team where people earned their say by shipping. |
22
+ | **Direct Democracy** | Every builder | Passes unless someone objects | A flat team where everyone in the room counts. |
23
+ | **Foundation** | A council you name | Passes unless someone objects | A bigger project that wants a small group of trustees looking after it. |
24
+
25
+ For a Foundation, the council is a rank you create and fill yourself. Make that rank first, then switch.
26
+
27
+ You can also mix your own settings, like "every builder, more than half". If your mix doesn't match a named form it reads as **Custom**. That's fine. Custom is still the real rules, not a guess at the nearest name.
28
+
29
+ Three more forms are on the list but **not available yet**: **Presidential** (needs elections), **Cooperative** (needs member shares) and **Federation** (for projects made of many teams).
30
+
31
+ Not sure? Start as a Monarchy and switch once there's someone to share the calls with.
32
+
33
+ ## The dials
34
+
35
+ ### Who votes
36
+
37
+ The founder only, trusted builders and up, every builder, or a council you name. Whoever holds the rank gets the vote, and nobody keeps it after they lose the rank. A builder who leaves, or gets moved down, is off the board on their very next request.
38
+
39
+ ### How many must agree
40
+
41
+ | Rule | What it means | If nobody votes |
42
+ |---|---|---|
43
+ | **One yes is enough** | The first yes decides it. | it passes |
44
+ | **Passes unless someone objects** | A yes doesn't end it. It passes once everyone has voted with no objection. One objection sends it back. | it passes |
45
+ | **More than half** | More than half of **everyone on the board** says yes, not just the people who showed up. 3 of 5 carries it, and a tie isn't a majority. | it's returned |
46
+ | **Everyone agrees** | Everyone who votes says yes, and at least one person does. One objection sends it back straight away. People who don't vote don't block it. | it's returned |
47
+
48
+ An objection has to come with a reason, so whoever proposed it knows what to fix.
49
+
50
+ ### The voting clock
51
+
52
+ Your constitution (the written-down settings of your government) can give a sitting a time limit, and a sitting closes early once everyone has voted. Look at the last column above: under the first two rules silence means yes. So a building's vote only gets a clock under "more than half" or "everyone agrees". We won't let a deadline pass something nobody read. Out of the box there's no clock.
53
+
54
+ Honest note: the clock is built, but the walk below doesn't test a clock running out yet. Every sitting it closes, closes on votes.
55
+
56
+ ### Who decides in each building
57
+
58
+ Your project's decisions are split into nine **buildings**, one per kind of decision. You'll find them on the street in the hall (`/street`).
59
+
60
+ | Building | What it covers | Who decides as today |
61
+ |---|---|---|
62
+ | **Charter** | The government itself | Always the board, under the rules in force |
63
+ | **Rules** | Project settings (when work counts as stale, ship mode) and editing work categories | Whoever may change project settings |
64
+ | **Disputes** | Security reports: confirm, dispute, mark fixed | Whoever may judge reports |
65
+ | **People** | Giving and taking custom ranks, access requests, invites | Whoever may manage ranks |
66
+ | **Planning** | Creating and ordering goals, new work categories, the focus version | Whoever may create goals |
67
+ | **Automation** | Unattended building (AI agents working with nobody watching): on or off, which goals, priority, pausing a runner | Whoever may switch it on and off |
68
+ | **Review** | Marking a goal's done-when check as met or not met | Whoever may mark work as meeting its checks |
69
+ | **Release** | Clearing a PR's gate, override requests, putting a new version live, releases and deploys | Whoever may put a release live |
70
+ | **Treasury** | Builders' monthly spending limits | Whoever may set spending limits |
71
+
72
+ Every building except the Charter is in one of three states:
73
+
74
+ - **As today.** Whoever holds that building's permission acts alone, from its own page. Every building starts here.
75
+ - **By vote.** You pick who sits (a rank, a rank and everyone above it, or a list of ranks) and how they decide: first yes wins, passes unless someone objects, more than half, everyone agrees, or signed off by someone who didn't build it. Once a building is by vote, nobody can act alone there. Try it and you get refused and pointed at the vote instead.
76
+ - **Empty.** Nobody holds it yet, so its matters wait.
77
+
78
+ The walk put every building to a vote, tried to act alone on all 28 of their direct actions (every one was refused), then set them back to as today and checked all 28 work again. Two things are built but **not walked yet**: the empty state, and a "decides alone" rule where one seated member can still act by themselves.
79
+
80
+ ## What a vote can actually change
81
+
82
+ This is the part that matters. A passed vote doesn't just get recorded, it **does the thing**. The vote is the authority, so it acts on its own, not through any one person.
83
+
84
+ | Building | What a passed vote carries out |
85
+ |---|---|
86
+ | **People** | Gives someone a custom rank, or takes it away |
87
+ | **Automation** | Switches unattended building on or off, or allows a goal to be built unattended |
88
+ | **Release** | Approves an override request, or clears the gate on a PR |
89
+ | **Review** | Signs off a done-when check |
90
+ | **Treasury** | Sets a builder's monthly spending limit |
91
+
92
+ Two more things the walk proved: a returned vote changes nothing, and someone the building doesn't seat can't vote on its matters.
93
+
94
+ At **Rules, Disputes and Planning** a vote stops people acting alone, but a passed vote there can't carry out an action by itself yet. That's **later**.
95
+
96
+ ## How to change your government
97
+
98
+ **1. When you found the project.** Every project is born a Monarchy. At the decide step of founding you pick your form. That pick is filed as your first amendment, and your one yes as founder ratifies it. After that, the five founding stages (scope, strategize, charter, legalize, legislation) are each closed by a vote of the government you picked. Pick a Timocratic Republic and you can't close a stage alone: your trusted builders have to say yes too. A stage with unfinished tasks can't be put to a vote at all. Once all five pass the project comes alive, still governed by your pick.
99
+
100
+ **2. Any time after: the Charter building.** Open the Charter on the street and hit "change how this works". The forms are one tap each. A Foundation needs its council rank first, and the forms that aren't ready yet are shown as later. Or pick who decides and how by hand. Add a reason and file it. That's an amendment, and your current government votes on it.
101
+
102
+ **3. Any building, the same way.** "Change how this works" on any building drafts a change to who decides there.
103
+
104
+ (The picker screens are built. The walk tests the amendments they file, not the screens themselves.)
105
+
106
+ The rules every amendment follows:
107
+
108
+ - Your current government decides it.
109
+ - There's no settings page. A passed amendment is the only way the rules change. Even editing the database by hand gets refused.
110
+ - If your change would take away your own vote, it stops and asks you to confirm first.
111
+ - Every attempt, passed or returned, stays on the record.
112
+
113
+ ## What can never be changed, and why
114
+
115
+ Some things no vote can move, and that's on purpose.
116
+
117
+ - **The Archon rank.** You can't edit it from the government page, so nobody can click the founder out of their own project.
118
+ - **Pushing to main, touching protected files, and server access.** These are enforced outside the app, by git hooks and audits, not by the database. So no rank can ever be given them, not even by vote. A switch that looked like it granted them would be a switch that does nothing, so we don't show one.
119
+ - **A file can't give anyone power.** Editing `CLAUDE.md`, a config file, your settings or your hooks gets you nothing. The server checks your rank against the live database on every request, with no caching.
120
+ - **Nothing outside a vote changes the constitution.** Not an environment variable and not a hand edit. The only way in is an amendment.
121
+
122
+ These walls are proven by their own tests. The government walk below doesn't cover them.
123
+
124
+ ## Today vs later
125
+
126
+ | | Today | Later |
127
+ |---|---|---|
128
+ | **Forms** | Monarchy / BDFL, Timocratic Republic, Direct Democracy, Foundation, and your own Custom mix | Presidential, Cooperative, Federation |
129
+ | **Dials** | Who votes, how many must agree, the voting clock | Veto points (how many separate approvals a big change needs), whether the team can replace its leader by vote, judicial review (who can cancel a decision that breaks the charter), a government that changes as you grow |
130
+ | **What a vote does** | People, Automation, Release, Review, Treasury carry out their actions | An action at Rules, Disputes and Planning; grade appeals; standing rules; votes on treasury spending |
131
+ | **Proof** | The walk, in memory, re-run in CI | The same walk against a real database |
132
+
133
+ ## How we know it works
134
+
135
+ We don't take our own word for it. There's a walk: one repeatable run over a throwaway test project with six people. It founds the project, closes all five stages, switches through every form, puts every building to a vote and tries to act alone everywhere, then passes every kind of matter and checks the thing actually happened.
136
+
137
+ **119 checks, 119 pass.** Every request goes through the real code that makes every decision. One honest catch: the storage underneath is an in-memory stand-in for the database. A real-database run is later.
138
+
139
+ Run it yourself:
140
+
141
+ ```sh
142
+ node scripts/gds/government-walk.js
143
+ ```
144
+
145
+ The full pass/fail table is in [`docs/governance/government-walk.md`](docs/governance/government-walk.md). CI re-runs the walk on every change, so if any row flips, the build goes red.
package/README.md CHANGED
@@ -6,6 +6,8 @@ Cloud Bongos is free software under the **GNU AGPL-3.0** (see [`LICENSE`](./LICE
6
6
 
7
7
  > **Provenance.** This core was extracted with fresh history from **Example** — the reference instance, built by Hermeslines, and the first project to run on Cloud Bongos. Example now consumes this core as a pinned dependency, the same shape as any other instance. The extraction is a redacted subtree (no instance secrets, IPs, or private identity); the exact pin — `core_version`, `source_commit`, `tree_sha256` — is recorded in [`.cloudbongos-core.json`](./.cloudbongos-core.json).
8
8
 
9
+ **How your project decides things** — who votes, what a vote changes, and how to change your government — is in [`GOVERNMENT.md`](./GOVERNMENT.md).
10
+
9
11
  The durable framing for AI agents — methodology, working rules, session protocol, architecture — lives in [`CLAUDE.md`](./CLAUDE.md). Read it before editing code.
10
12
 
11
13
  ## What's in here
@@ -3035,5 +3035,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
3035
3035
  landed since 1.21.97 with no explicit bump. run 37925528587. (task 1002620)
3036
3036
  1.21.99 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3037
3037
  landed since 1.21.98 with no explicit bump. run 37927704012. (task 1002620)
3038
+ 1.21.100 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3039
+ landed since 1.21.99 with no explicit bump. run 37931583687. (task 1002620)
3040
+ 1.21.101 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3041
+ landed since 1.21.100 with no explicit bump. run 37933684016. (task 1002620)
3038
3042
  ---------------------------------------------------------------------------
3039
3043
  ```
@@ -116,6 +116,7 @@ Other depth, in roughly the order you'll need it:
116
116
  - [`docs/architecture.md`](../architecture.md) — live system: droplet, stack, schema, API surface
117
117
  - [`docs/modules-contract.md`](../modules-contract.md) — the product is built as a small core plus swappable `modules/<key>/` (the game, Discord, art, and more); this is the live roster and how a task lands inside one
118
118
  - [`docs/canonical-permissions.md`](../canonical-permissions.md) — who can do what (rank gates)
119
+ - [`GOVERNMENT.md`](../../GOVERNMENT.md) — how your project's government works and how to change it, in plain words
119
120
  - [`docs/file-map.md`](../file-map.md) — where every folder is and what it contains
120
121
  - [`docs/recipes/`](../recipes/) — long-form ops gotchas (read before deploying)
121
122
  - [`docs/onboarding/diagrams/`](diagrams/) — pan-and-zoom maps of the task lifecycle, rank ladder, the credit economy, the complete architecture (where your work runs and how it reaches production), and versioning
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.99",
3
+ "version": "1.21.101",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.99",
9
+ "version": "1.21.101",
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.21.99",
3
+ "version": "1.21.101",
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",
@@ -9432,5 +9432,17 @@
9432
9432
  "id": "1004658",
9433
9433
  "text": "There is now one walk anyone can re-run that tests a project's whole government from start to finish, and a table showing the result. It founds a test project with a chosen government and checks that this government, not the f"
9434
9434
  }
9435
+ ],
9436
+ "1.21.100": [
9437
+ {
9438
+ "id": "1004659",
9439
+ "text": "There is now a plain-language guide to how a project's government works: GOVERNMENT.md, linked from the README and the hall primer. It explains the four forms you can pick today, who votes and how many must agree, who decides"
9440
+ }
9441
+ ],
9442
+ "1.21.101": [
9443
+ {
9444
+ "id": "1004582",
9445
+ "text": "Cleaning up a finished worktree no longer crashes on Linux and Mac machines. It used to stop halfway and leave the old workspace registered; now it removes the shortcut to the shared packages safely and carries on."
9446
+ }
9435
9447
  ]
9436
9448
  }
@@ -90,6 +90,7 @@ const PUBLISH_ALLOWLIST = [
90
90
  'README.md',
91
91
  'LICENSE',
92
92
  'GOVERNANCE.md',
93
+ 'GOVERNMENT.md', // the plain-language guide to a project's own government (task 1004659)
93
94
  'CONTRIBUTING.md',
94
95
  'CODE_OF_CONDUCT.md',
95
96
  'docs/adr/', // sanitized — ADR 0065 §4 "docs/ADRs (sanitized)"
@@ -173,6 +173,24 @@ function nodeModulesState(wtPath) {
173
173
  return st.isSymbolicLink() ? 'junction' : 'real-dir';
174
174
  }
175
175
 
176
+ // Remove a node_modules LINK without ever following it (task 1004582). rmdir is
177
+ // only right for a Windows junction: on Linux/macOS (cloud containers) the link
178
+ // is a plain symlink and rmdir throws ENOTDIR, leaving the worktree registered.
179
+ // unlink removes the link itself on every platform — libuv unlinks a junction
180
+ // too — and refuses a real directory outright, so it can never delete contents.
181
+ // The win32 rmdir fallback covers a reparse point unlink will not take.
182
+ function unlinkNodeModulesLink(linkPath, { platform = process.platform, fsImpl = fs } = {}) {
183
+ if (!fsImpl.lstatSync(linkPath).isSymbolicLink()) {
184
+ throw new Error(`${linkPath} is not a link — refusing to remove a real directory`);
185
+ }
186
+ try {
187
+ fsImpl.unlinkSync(linkPath);
188
+ } catch (e) {
189
+ if (platform !== 'win32' || (e.code !== 'EPERM' && e.code !== 'EISDIR')) throw e;
190
+ fsImpl.rmdirSync(linkPath); // on a junction, rmdir removes the LINK only
191
+ }
192
+ }
193
+
176
194
  function resolveWorktreePath(root, nameOrPath) {
177
195
  const all = listWorktrees(root);
178
196
  const norm = (p) => path.resolve(p).replace(/\\/g, '/').toLowerCase();
@@ -296,8 +314,8 @@ function cmdRemove(root, argv) {
296
314
  const state = nodeModulesState(wtPath);
297
315
  for (const step of removalPlan({ nodeModulesState: state })) {
298
316
  if (step === 'unlink-junction') {
299
- // rmdir on a junction removes the LINK only, never the target's contents.
300
- fs.rmdirSync(path.join(wtPath, 'node_modules'));
317
+ // Removes the LINK only, never the target's contents — junction or symlink.
318
+ unlinkNodeModulesLink(path.join(wtPath, 'node_modules'));
301
319
  console.log(' removed node_modules junction (link only — the shared node_modules is untouched)');
302
320
  } else {
303
321
  const args = ['worktree', 'remove', ...(hasFlag('--force', argv) ? ['--force'] : []), wtPath];
@@ -427,4 +445,5 @@ if (require.main === module) main();
427
445
 
428
446
  module.exports = {
429
447
  worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules, huskDisposition,
448
+ unlinkNodeModulesLink,
430
449
  };
@@ -596,6 +596,7 @@ const UNOWNED_ROOTS = Object.freeze({
596
596
  'CODE_OF_CONDUCT.md': 'project governance text',
597
597
  'CONTRIBUTING.md': 'project governance text',
598
598
  'GOVERNANCE.md': 'project governance text',
599
+ 'GOVERNMENT.md': 'project governance text',
599
600
  // Top-level product/design statements, read by humans before any module matters.
600
601
  'README.md': 'repo front door',
601
602
  'DESIGN.md': 'top-level design statement',
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.99'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.101'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -27,7 +27,7 @@ import os from 'node:os';
27
27
  import path from 'node:path';
28
28
 
29
29
  const require = createRequire(import.meta.url);
30
- const { worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules, huskDisposition } = require('../scripts/gds/worktree.js');
30
+ const { worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules, huskDisposition, unlinkNodeModulesLink } = require('../scripts/gds/worktree.js');
31
31
 
32
32
  const HOUR = 60 * 60 * 1000;
33
33
 
@@ -118,16 +118,75 @@ test('nodeModulesState: absent / real-dir / junction classified from disk', () =
118
118
  fs.symlinkSync(target, link, 'junction');
119
119
  eq(nodeModulesState(wtJct), 'junction');
120
120
 
121
- // Removing the link (the helper's unlink step) must not touch the target.
122
- // win32 junctions need rmdir; a posix dir-symlink needs unlink — cover both
123
- // so this file passes on Linux CI and the real Windows box alike.
124
- try { fs.rmdirSync(link); } catch (_) { fs.unlinkSync(link); }
121
+ // Removing the link (the helper's own unlink step) must not touch the target.
122
+ // On Linux CI this is a plain dir-symlink — the case rmdir crashed on with
123
+ // ENOTDIR (task 1004582); on Windows it is a real junction.
124
+ unlinkNodeModulesLink(link);
125
+ eq(fs.existsSync(link), false, 'the link itself is gone');
125
126
  eq(fs.existsSync(path.join(target, 'canary.txt')), true, 'target survives link removal');
126
127
  } finally {
127
128
  fs.rmSync(base, { recursive: true, force: true });
128
129
  }
129
130
  });
130
131
 
132
+ // The win32 rmdir fallback, driven through a fake fs so every platform runs it.
133
+ function fakeFs(unlinkCode) {
134
+ const calls = [];
135
+ return {
136
+ calls,
137
+ lstatSync: () => ({ isSymbolicLink: () => true }),
138
+ unlinkSync: (p) => {
139
+ calls.push(['unlink', p]);
140
+ if (unlinkCode) { const e = new Error(unlinkCode); e.code = unlinkCode; throw e; }
141
+ },
142
+ rmdirSync: (p) => { calls.push(['rmdir', p]); },
143
+ };
144
+ }
145
+
146
+ test('unlinkNodeModulesLink: unlink succeeds → rmdir is never called', () => {
147
+ const f = fakeFs(null);
148
+ unlinkNodeModulesLink('L', { platform: 'linux', fsImpl: f });
149
+ eqArr(f.calls, [['unlink', 'L']]);
150
+ });
151
+
152
+ for (const code of ['EPERM', 'EISDIR']) {
153
+ test(`unlinkNodeModulesLink: win32 + ${code} falls back to rmdir`, () => {
154
+ const f = fakeFs(code);
155
+ unlinkNodeModulesLink('L', { platform: 'win32', fsImpl: f });
156
+ eqArr(f.calls, [['unlink', 'L'], ['rmdir', 'L']]);
157
+ });
158
+ test(`unlinkNodeModulesLink: non-win32 + ${code} rethrows, no rmdir`, () => {
159
+ const f = fakeFs(code);
160
+ let thrown = null;
161
+ try { unlinkNodeModulesLink('L', { platform: 'linux', fsImpl: f }); } catch (e) { thrown = e.code; }
162
+ eq(thrown, code);
163
+ eqArr(f.calls, [['unlink', 'L']]);
164
+ });
165
+ }
166
+
167
+ test('unlinkNodeModulesLink: win32 + any other code rethrows, no rmdir', () => {
168
+ const f = fakeFs('EBUSY');
169
+ let thrown = null;
170
+ try { unlinkNodeModulesLink('L', { platform: 'win32', fsImpl: f }); } catch (e) { thrown = e.code; }
171
+ eq(thrown, 'EBUSY');
172
+ eqArr(f.calls, [['unlink', 'L']]);
173
+ });
174
+
175
+ test('unlinkNodeModulesLink refuses a REAL directory and leaves it intact', () => {
176
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'wt-helper-test-'));
177
+ try {
178
+ const real = path.join(base, 'node_modules');
179
+ fs.mkdirSync(real);
180
+ fs.writeFileSync(path.join(real, 'canary.txt'), 'x');
181
+ let threw = false;
182
+ try { unlinkNodeModulesLink(real); } catch (_) { threw = true; }
183
+ eq(threw, true, 'a real dir is refused');
184
+ eq(fs.existsSync(path.join(real, 'canary.txt')), true, 'its contents survive');
185
+ } finally {
186
+ fs.rmSync(base, { recursive: true, force: true });
187
+ }
188
+ });
189
+
131
190
  // ---- 3. removalPlan — junction-first ordering is the whole point ----
132
191
 
133
192
  test('junction node_modules: unlink FIRST, then git worktree remove', () => {