@bongos/core 1.21.108 → 1.21.109

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.108",
6
- "core_contract": "1.21.108",
7
- "source_commit": "4744afee212fe2a5c5bac890ee578bbaae7e3c5b",
5
+ "core_version": "1.21.109",
6
+ "core_contract": "1.21.109",
7
+ "source_commit": "99f3c48f4295f02d3a12c5f222ca33ca04453406",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-09T16:36:02.296Z",
9
+ "built_at": "2026-10-09T16:52:03.016Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 592,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 3027,
14
+ "functional_verbatim": 3028,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3648,
20
- "tree_sha256": "62c2521f5de99bd666286fed934e1c0e7269d9b22a9953b772b54b1fa3a738e2",
19
+ "file_count": 3649,
20
+ "tree_sha256": "92bfdb3234394f28d268e4115634d136414a6c4b45fab928376b1ee741b62fcc",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -152,7 +152,7 @@
152
152
  {
153
153
  "path": ".claude/skills/goal-create/SKILL.md",
154
154
  "mode": "0000644",
155
- "sha256": "5fb0e00c6d6d8f1d1cc87136dd0a42cfc9379ffc55b8b5bdbbd736541e50c299"
155
+ "sha256": "11f9e820c5e37818de14b701945f8606562572c4381ba90ded31364005edf0fb"
156
156
  },
157
157
  {
158
158
  "path": ".claude/skills/goal-review/SKILL.md",
@@ -207,7 +207,7 @@
207
207
  {
208
208
  "path": ".claude/skills/planning-session/SKILL.md",
209
209
  "mode": "0000644",
210
- "sha256": "07383a95bda58d86c833d963c421a9792b5fb879fa7fc75aba7b18b318e59b6e"
210
+ "sha256": "0d3ad6a9a7f3d0ee4bbdc0a8539705bdc5e9ab492151f3886120c15e102758a6"
211
211
  },
212
212
  {
213
213
  "path": ".claude/skills/priority-session/SKILL.md",
@@ -2872,7 +2872,7 @@
2872
2872
  {
2873
2873
  "path": "docs/module-api-changelog.md",
2874
2874
  "mode": "0000644",
2875
- "sha256": "591ca6f09789a658cec2c0442d76fff5b80c3af4c620fb6b197499aa276d21fc"
2875
+ "sha256": "a35307c4313c8d7e32ad065d3713213ecb0a99990d5996f1e1221a0ea05b24fa"
2876
2876
  },
2877
2877
  {
2878
2878
  "path": "docs/modules-contract.md",
@@ -10157,12 +10157,12 @@
10157
10157
  {
10158
10158
  "path": "package-lock.json",
10159
10159
  "mode": "0000644",
10160
- "sha256": "6a7262e960db14a5a541c0735d0f41d9e5f436e0c434a6fd77dd01e177e6fb34"
10160
+ "sha256": "b52f46812f60f0c96bead621c93cd5d916561ab4f37ea94ce28e7c46198ba985"
10161
10161
  },
10162
10162
  {
10163
10163
  "path": "package.json",
10164
10164
  "mode": "0000644",
10165
- "sha256": "bd46dfa1e2c3169155d64d561b31e229137c85c66271ac2f0003ab47bedffdcd"
10165
+ "sha256": "9c36d2b1a974a7a7ef2af11f87f498e6fc26f4d6b40056952514db8252689769"
10166
10166
  },
10167
10167
  {
10168
10168
  "path": "public-docs/index.html",
@@ -10182,7 +10182,7 @@
10182
10182
  {
10183
10183
  "path": "release-notes.json",
10184
10184
  "mode": "0000644",
10185
- "sha256": "71cdec4456ab1476dce44818b9d3f10cee7c0a8a8d67ad70c1adfc96debcf47b"
10185
+ "sha256": "3dbd02da56539f0d9fa96b2bc3ec5c586bde41a3242f2d1d86cc0fd644fb8592"
10186
10186
  },
10187
10187
  {
10188
10188
  "path": "scripts/cron/style_guide_audit.sh",
@@ -11982,7 +11982,7 @@
11982
11982
  {
11983
11983
  "path": "scripts/hall-preview/server.js",
11984
11984
  "mode": "0000644",
11985
- "sha256": "73f90a45a8dbe04de03bdca1ad42f61c6dfe6e75982c3df8f311c8f1fa9eeed9"
11985
+ "sha256": "5819bcbb67a9940a05ee7d874d29edbad366d2bdae64f40d0f9d5a4ea8e32376"
11986
11986
  },
11987
11987
  {
11988
11988
  "path": "scripts/hall-preview/task-visual.js",
@@ -12457,7 +12457,7 @@
12457
12457
  {
12458
12458
  "path": "src/module-api.js",
12459
12459
  "mode": "0000644",
12460
- "sha256": "f208a278069baf39f28e39ddc85193af5cda7d17095dfe1edbbb89f2e7568350"
12460
+ "sha256": "d741e7462f49f3e622275d1901613d0e716de10361aa9e8ee1bc0b195fe134df"
12461
12461
  },
12462
12462
  {
12463
12463
  "path": "src/module-loader/catalog.js",
@@ -14894,6 +14894,11 @@
14894
14894
  "mode": "0000644",
14895
14895
  "sha256": "14e0bfda70dac75dc29bd784739ade11fe2cb986214bd0a1e8859f398b52dafb"
14896
14896
  },
14897
+ {
14898
+ "path": "tests/hall_preview_encoded_paths.mjs",
14899
+ "mode": "0000644",
14900
+ "sha256": "a4a0141c0743557d2344e2fbb0c573112b6c787e41332fdd04d5b656b17b65ba"
14901
+ },
14897
14902
  {
14898
14903
  "path": "tests/hall_preview_governor_fixtures.mjs",
14899
14904
  "mode": "0000644",
@@ -16252,7 +16257,7 @@
16252
16257
  {
16253
16258
  "path": "tests/planning_session_skill.mjs",
16254
16259
  "mode": "0000644",
16255
- "sha256": "05bf5a11c0cb334d5da1b14f976f9e8ed8459ffec609f418c752ad72d87b7345"
16260
+ "sha256": "5fa3820f0c2a9b718cc3bcb513264ffae24c25c91d1a7039f0459a2befb36a6b"
16256
16261
  },
16257
16262
  {
16258
16263
  "path": "tests/platform_boot.mjs",
@@ -12,7 +12,7 @@ cost: >-
12
12
 
13
13
  You are running the **goal-create** session for this Cloud Bongos instance. You produce **one goal**: a title, a scope wall, two to four done-when criteria, and a handful of seed tasks — written to Bongos in one sitting.
14
14
 
15
- > **Not `/planning-session`, not `/goal-review`.** `/planning-session` scopes a whole **version**: 5–9 criteria, a written spec under `docs/specs/`, a multi-round interview, a seed script, a migration. This skill produces **one goal**, with no spec file and no migration. `/goal-review` is the other end of the same lifecycle — it *confirms* criteria and auto-achieves goals. If the ask is "plan the next version", stop and run `/planning-session` instead.
15
+ > **Not `/planning-session`, not `/goal-review`.** `/planning-session` scopes a whole **version**: 5–9 criteria, a written spec under `docs/specs/`, a multi-round interview, a seed script, a migration. This skill produces **one goal**, with no spec file and no migration. `/goal-review` is the other end of the same lifecycle — it *confirms* criteria and auto-achieves goals. If the ask is "plan the next version", stop and run `/planning-session` instead. **The same goes for a big design-driven ask** — the owner has a design in mind (a mockup, a named model, "it should feel like…") and it would take several tasks to build: route it to `/planning-session` before seeding anything, because only there is each criterion tied to the agreed design and each task told where that design lives (task 1004675). Seeding it here skips that binding, and the grader — which never reads the design — cannot notice the build drifting from it.
16
16
 
17
17
  ## Rank gate — Metic+ only
18
18
 
@@ -21,6 +21,7 @@ The first planning session — V3 Planning Session #1, 2026-05-12 — is the tem
21
21
  - The user says "let's plan V4" / "plan the next version" / "open a planning session"
22
22
  - A version is in `status='planning'` and needs scope locked before tasks start landing in earnest
23
23
  - A previous planning session left proposals in `idea_inbox` tagged `kind='criterion-proposal'` that need ratification
24
+ - **A big design-driven ask arrives — route it here BEFORE any task is seeded (task 1004675).** The tell: the owner has a design in mind (a mockup, a named model, "it should feel like…"), and the work would span several tasks. Seeding those tasks straight from chat — through `/goal-create`, `/idea-triage`, or a hand-written `POST /tasks` — skips the spec, the interview and the design binding in Phase 2, which is exactly how a build drifts from what was agreed. A single small task, or a goal whose shape is already settled and has no design to honour, does not need this.
24
25
 
25
26
  **Run an ideation pass first when the goal's SHAPE is not yet settled.** An ideator session (the [Ideator pack](../../../docs/packs/ideator.md)) inverts this skill's stance: there the human drives and you are the sounding board, so *their* framing sets the shape of the thing. Carry its output into Phase 1 and the Phase-4 interview is left with genuine residue instead of first-pass design.
26
27
 
@@ -106,12 +107,14 @@ This is where the user sees what they're shaping. Do not skip this — criteria
106
107
 
107
108
  **A row WITHOUT the pointer still needs its description read**, because the marker only exists from the day someone used merge instead of abandon-with-a-note, and the older rows are exactly the ones a rescope trips over. **Verify against the repo, not the row.** On 2026-08-16 a session read task 1003005 ("Rename modules/governance to modules/government") as `abandoned` and nearly put a false edge in a new goal's graph — the folder move had shipped under task 1003004, and only the prose said so. One `git log` over the paths the task named would have settled it.
108
109
 
109
- ### Phase 1.5 — Map the mechanism (process-change sessions only) — task 1003075
110
+ ### Phase 1.5 — Map the mechanism (the default whenever an existing surface is touched) — task 1003075, task 1004675
110
111
 
111
- **Run this phase when the session is scoped to CHANGING how an existing mechanism already works** (a redesign, not a net-new feature with nothing live to replace) — skip it for a purely additive version with no prior mechanism in play. The precedent is the 2026-08-17 idea-routing session (goal 1000071): a subagent mapping the LIVE capture→triage→promote path before Phase 2 turned "route bugs to goals" into the capture-time-promotion design and resolved the bugs-vs-features question almost by itself — the session's own retro named it the highest-value step, and until now the skill only implied it.
112
+ **Run this phase by default for any session that touches an existing surface** — a process-change redesign, an extension of a live feature, a new view over data that already exists. **Skipping it is the thing that needs a stated reason**, written into the spec's *Design decisions*: e.g. "purely additive, no live mechanism in play — nothing to map". Do not skip it because the owner arrived with a confident model; that is exactly the case where an unmapped assumption costs the most.
113
+
114
+ The precedent is the 2026-08-17 idea-routing session (goal 1000071): a subagent mapping the LIVE capture→triage→promote path before Phase 2 turned "route bugs to goals" into the capture-time-promotion design and resolved the bugs-vs-features question almost by itself — the session's own retro named it the highest-value step. The 2026-09-19 ideator-surface session (meta task 1004076) was the second in a row: mapping moved the scope three times before any criterion was written — the owner's "size = karma" was not computable (karma rows carry no task or idea key), task parentage could not carry a descent (one level, no cross-version parents), and nothing recorded that shipped work was later removed, so half the design rested on a signal that did not exist.
112
115
 
113
116
  1. **Input.** Name the subsystem(s) the version is about to change, and the entry points you already know (a route file, a CLI script, a module's public function) as the subagent's starting point.
114
- 2. **Spawn a read-only mapping subagent** (`Explore` or `general-purpose`, `model: sonnet` or higher per CLAUDE.md's subagent routing — this feeds a design decision, not plumbing) with a prompt asking it to trace the CURRENT live mechanism end-to-end: every entry point that touches it, the chokepoint(s) data already funnels through, and any function/route already doing part of what the new design needs. Ask for file:line references, not prose summaries — the output has to be citable in the spec.
117
+ 2. **Spawn a read-only mapping subagent** (`Explore` or `general-purpose`, `model: sonnet` or higher per CLAUDE.md's subagent routing — this feeds a design decision, not plumbing) with a prompt asking it to trace the CURRENT live mechanism end-to-end: every entry point that touches it, the chokepoint(s) data already funnels through, and any function/route already doing part of what the new design needs. Ask for file:line references, not prose summaries — the output has to be citable in the spec. **Always ask it one standard question as well: "Name any quantity this design assumes is computable, and say whether it actually is — which table or column carries it, or why nothing does."**
115
118
  3. **Output artifact — the mechanics map.** A short structured list: each chokepoint or reusable function/route, with a file:line and a one-line note on what it does today. Write it down (in the session, or straight into the spec draft) rather than letting it live only in the subagent's reply.
116
119
  4. **Consumed by:**
117
120
  - **Phase 2 (Criteria)** — a chokepoint the map surfaces (e.g. "every capture vector funnels through one function") often turns directly into a criterion, or reframes one that would otherwise be written as a task list.
@@ -129,6 +132,12 @@ For each criterion:
129
132
 
130
133
  **Apply the strength test BEFORE presenting each criterion.** A criterion passes the test if it plausibly anchors at least 5 tasks across multiple `kind` values (feature + bug + infra, not just feature). If it only anchors 1-3 specific tasks, or reads like an implementation step, rewrite it as the resulting outcome before showing it to the user.
131
134
 
135
+ **Tie every criterion to the agreed design — outcomes alone let the build stray (task 1004675).** An outcome criterion is right, but it is only half the job. The grader reads the **task description and the diff**; it does **not** read `docs/specs/`. So an outcome criterion ("a thought is never lost") plus a vague task ("make the outcome persist") can be satisfied by a build that has nothing to do with the design the owner agreed, and nothing in the pipeline objects — the spec is the design of record and sits outside the loop that judges the work. The owner caught this in the 2026-09-19 ideator-surface session: "none of this criteria is easily indicative of the initial scope and designs we made". Close the gap three ways:
136
+
137
+ 1. **Split the settled design into MODEL and RENDERING.** The model is the durable decision — the named states, the independent channels, the rules (that session: five named states, two independent channels, a decay rule). It goes **into the criterion text**. The rendering — how it is drawn (that session: as a sky) — stays in the spec, free to change. A criterion that names the rendering breaks the day the look changes; a criterion that omits the model lets any build pass.
138
+ 2. **Give each criterion a proof whose scenario a wrong build fails.** Not "the page renders" but "a thought that caused 180 credits of work, since removed, displays at full size and zero brightness". Write that scenario into the proof task's **description** (Phase 3), because the description is where the grader actually looks.
139
+ 3. **Cite the design of record in every task description, and make sure the builder can open it.** Name the spec path (`docs/specs/<ver>-<goal-slug>.md`, committed in Phase 5) and the section the task implements. A private artifact link or a chat-only mockup as the design of record is decorative — whoever builds cannot open it.
140
+
132
141
  **Number every criterion** (`1.`, `2.`, …) in the presented list. The user will reference them by number when editing — e.g. "edit 4, remove 5, add a new one about X" — so they don't have to copy/paste the criterion titles.
133
142
 
134
143
  **Ask "what's missing?" before finalizing.** Once you've presented N criteria and worked through edits, explicitly prompt: "What concerns you that I haven't named? What's missing from this list?"
@@ -179,9 +188,9 @@ Don't tunnel-vision. Surface lateral options the user might not have thought of:
179
188
 
180
189
  **Proactively propose needs-exploration tasks** for any criterion where the implementation path isn't clear. Don't wait for the user to flag it. Frame them as "this one needs an architecture pass first — output an ADR + sub-task list before we build."
181
190
 
182
- **For each criterion, explicitly propose a proof-task** — the single live demonstration that proves the criterion is met. Examples: "Builder #2 onboards in ≤2h" for an onboarding criterion, "five concurrent builders shipping without collisions on staging" for a parallel-safety criterion. Without a proof-task, the criterion may technically ship without anyone knowing it works.
191
+ **For each criterion, explicitly propose a proof-task** — the single live demonstration that proves the criterion is met. Examples: "Builder #2 onboards in ≤2h" for an onboarding criterion, "five concurrent builders shipping without collisions on staging" for a parallel-safety criterion. Without a proof-task, the criterion may technically ship without anyone knowing it works. **The proof's scenario must be one a wrong build fails** — state the concrete input and the exact observable result the agreed MODEL implies (Phase 2), so a build that ignored the design cannot pass it.
183
192
 
184
- **Encourage short task descriptions with cross-refs.** Each task's description should focus on done-when + touches; reference dependency tasks by their R## rather than restating context. Long standalone descriptions are content-debt at scale (80 tasks × 200 words = a lot of redundant context).
193
+ **Encourage short task descriptions with cross-refs.** Each task's description should focus on done-when + touches; reference dependency tasks by their R## rather than restating context. **But every description cites the design of record** — the spec path and section it implements (Phase 2, "Tie every criterion to the agreed design") — because the grader never opens the spec on its own. Long standalone descriptions are content-debt at scale (80 tasks × 200 words = a lot of redundant context).
185
194
 
186
195
  > **`touches[]` is an advisory hint, not a hard fence ([ADR 0049](../../../docs/adr/0049-split-parallel-safety-contract.md)).** Parallel-safety is a *split contract*: for interactive single-human work, `git merge` is the authoritative collision detector and the claim-time overlap signal only warns; only the autonomous `--parallel` runner treats footprint overlap as a hard gate. So don't agonize over predicting every file at planning time — a rough blast-radius is enough, and a wrong prediction is no longer ship-blocking. (Step 7/8 — task [#882](https://example.com/builders#/task/882) — makes the per-task `touches` field optional and backfills it from the committed diff at ship.)
187
196
 
@@ -213,6 +222,8 @@ Tell the user the spec is written and where, then move straight to the interview
213
222
 
214
223
  **Ask only NON-OBVIOUS questions.** A question earns its place only if: the spec is genuinely underspecified on it, reasonable experts would disagree, AND the answer changes what gets built or how it's broken into tasks. Span dimensions across rounds — **data model & schema, API surface & contracts, UI/UX & information architecture, permissions/authz, privacy/consent, performance & scale, failure modes & edge cases, migration/rollout/backfill, naming the user will see, and scope boundaries.**
215
224
 
225
+ **Naming questions: show the options side by side before recommending one (task 1004675).** A name the user will see — a page, a state, a role, a button — is a question where a lone recommendation anchors the owner before they have compared anything. Lay out three or four candidates together, each with a one-line note on how it reads to the person who will see it, and only then say which you would pick and why. In an AskUserQuestion round, that means each candidate is its own option (use the option `preview` for the side-by-side), with your pick listed first and marked recommended — never one name with "or something else?".
226
+
216
227
  **Do NOT ask** anything answerable by reading the codebase, anything the criteria already settle, yes/no rubber-stamps, or "do you want it to be good?" filler. If you catch yourself about to ask something with one obvious answer, decide it yourself and record it in *Design decisions* instead.
217
228
 
218
229
  > **This interview is the deliberate exception to the usual "decide implementation details yourself" default (see Tone, and CLAUDE.md §2).** The owner has explicitly asked to be consulted in depth here — so probe technical implementation, UX, tradeoffs, and concerns, not just outcomes.
@@ -3053,5 +3053,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3053
3053
  landed since 1.21.106 with no explicit bump. run 37957919746. (task 1002620)
3054
3054
  1.21.108 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3055
3055
  landed since 1.21.107 with no explicit bump. run 37960186494. (task 1002620)
3056
+ 1.21.109 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3057
+ landed since 1.21.108 with no explicit bump. run 37962089344. (task 1002620)
3056
3058
  ---------------------------------------------------------------------------
3057
3059
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.108",
3
+ "version": "1.21.109",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.108",
9
+ "version": "1.21.109",
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.108",
3
+ "version": "1.21.109",
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",
@@ -9498,5 +9498,15 @@
9498
9498
  "id": "1004696",
9499
9499
  "text": "Artists can now recommend removing a line while editing a page's words: mark it with \"Remove this line\" or just delete its words, see it crossed out, keep it after all, and send it with their rewrites. Each removal goes to an"
9500
9500
  }
9501
+ ],
9502
+ "1.21.109": [
9503
+ {
9504
+ "id": "1004700",
9505
+ "text": "The \"Edit words\" paintbrush now works in the local hall preview instead of saying the page could not be taken."
9506
+ },
9507
+ {
9508
+ "id": "1004675",
9509
+ "text": "Planning sessions now keep the work tied to the design the owner agreed. Each goal's done-when line must carry the agreed model (the named states and rules), and each proof task must describe a scenario a wrong build would fai"
9510
+ }
9501
9511
  ]
9502
9512
  }
@@ -271,7 +271,18 @@ const app = express();
271
271
  app.use((req, res, next) => {
272
272
  const m = req.path.match(/^\/api\/(?:gds|bongos)(?:\/v1)?\/(.+)$/);
273
273
  if (!m) return next();
274
- const apiPath = m[1].replace(/\/$/, '');
274
+ // Decoded before any matching (task 1004700): the paintbrush sends its page id
275
+ // through encodeURIComponent, so builders:studio arrives as builders%3Astudio
276
+ // and every copy-desk fixture and canned write missed. A malformed escape keeps
277
+ // the raw path, which then simply finds no fixture.
278
+ let apiPath = m[1].replace(/\/$/, '');
279
+ try { apiPath = decodeURIComponent(apiPath); } catch { /* keep raw */ }
280
+ // Decoding can mint a backslash, NUL or `..` segment that the fixture-name
281
+ // mapper does not neutralise (path.join honours `\` on Windows), so a decoded
282
+ // path that could leave the fixtures dir is refused outright.
283
+ if (/[\\\0]|(^|\/)\.\.(\/|$)/.test(apiPath)) {
284
+ return res.status(400).json({ error: { code: 'bad_path', message: 'path escapes the fixtures' } });
285
+ }
275
286
 
276
287
  // SSE live channel: emit one nudge (so "live refresh" paths run once), then
277
288
  // keep-alive comments. Pages treat this exactly like prod's stream.
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.108'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.109'; // 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');
@@ -0,0 +1,81 @@
1
+ // tests/hall_preview_encoded_paths.mjs — the hall preview decodes the API path
2
+ // before matching fixtures and canned writes (task 1004700).
3
+ //
4
+ // The paintbrush sends its page id through encodeURIComponent, so builders:studio
5
+ // arrives as builders%3Astudio. Matched raw, every copy-desk read and canned write
6
+ // 404'd and the bar said "That page could not be taken just now". This boots the
7
+ // real harness and asks for both spellings.
8
+ //
9
+ // Run: node --test tests/hall_preview_encoded_paths.mjs
10
+
11
+ import { test, before, after } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { spawn } from 'node:child_process';
14
+ import net from 'node:net';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
+ let child;
20
+ let base;
21
+
22
+ function freePort() {
23
+ return new Promise((resolve, reject) => {
24
+ const s = net.createServer();
25
+ s.once('error', reject);
26
+ s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); });
27
+ });
28
+ }
29
+
30
+ before(async () => {
31
+ const port = await freePort();
32
+ base = `http://127.0.0.1:${port}/api/bongos/copy-desk/pages`;
33
+ child = spawn(process.execPath, ['scripts/hall-preview/server.js', '--port', String(port), '--fixture-me'], { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] });
34
+ await new Promise((resolve, reject) => {
35
+ const t = setTimeout(() => reject(new Error('hall-preview did not start')), 60000);
36
+ child.stdout.on('data', (b) => { if (String(b).includes('hall-preview:')) { clearTimeout(t); resolve(); } });
37
+ child.once('exit', (code) => { clearTimeout(t); reject(new Error(`hall-preview exited ${code}`)); });
38
+ });
39
+ });
40
+
41
+ after(() => { if (child) child.kill(); });
42
+
43
+ async function call(method, id, tail) {
44
+ const r = await fetch(`${base}/${id}/${tail}`, { method, headers: { 'content-type': 'application/json' }, body: method === 'GET' ? undefined : '{}' });
45
+ return { status: r.status, body: await r.json() };
46
+ }
47
+
48
+ test('GET draft: an encoded page id gets the same fixture as the plain one', async () => {
49
+ const plain = await call('GET', 'builders:studio', 'draft');
50
+ const encoded = await call('GET', encodeURIComponent('builders:studio'), 'draft');
51
+ assert.equal(plain.status, 200);
52
+ assert.equal(encoded.status, 200);
53
+ assert.deepEqual(encoded.body, plain.body);
54
+ // and the catch-all, for a page with no fixture of its own
55
+ const other = await call('GET', encodeURIComponent('builders:work'), 'draft');
56
+ assert.equal(other.status, 200);
57
+ assert.deepEqual(other.body, (await call('GET', 'builders:work', 'draft')).body);
58
+ });
59
+
60
+ test('canned PUT draft: an encoded page id gets the same canned reply as the plain one', async () => {
61
+ const plain = await call('PUT', 'builders:studio', 'draft');
62
+ const encoded = await call('PUT', encodeURIComponent('builders:studio'), 'draft');
63
+ assert.equal(plain.status, 200);
64
+ assert.equal(encoded.status, 200);
65
+ assert.deepEqual(encoded.body, plain.body);
66
+ });
67
+
68
+ test('a decoded backslash or .. segment cannot walk out of the fixtures dir', async () => {
69
+ for (const id of ['..%5c..%5c..%5cpackage', '..%2F..%2Fpackage', 'a%00b']) {
70
+ const r = await call('GET', id, 'draft');
71
+ assert.equal(r.status, 400, id);
72
+ assert.equal(r.body.error.code, 'bad_path', id);
73
+ }
74
+ // the PUT path is checked before CANNED_WRITES too
75
+ assert.equal((await call('PUT', '..%5cx', 'draft')).status, 400);
76
+ });
77
+
78
+ test('a malformed escape does not crash the harness', async () => {
79
+ const r = await call('GET', 'builders%E0%A4%A', 'draft');
80
+ assert.equal(r.status, 404);
81
+ });
@@ -83,12 +83,70 @@ test('the mechanics-map step names the subagent mechanism and the retro preceden
83
83
  assert.ok(section.includes('task 1003075'));
84
84
  });
85
85
 
86
- test('the step is scoped to process-change sessions, not every planning session', () => {
86
+ // task 1004675 (idea 1001074): the step used to read as an opt-in for
87
+ // process-change sessions only, so the sessions with a confident owner model
88
+ // skipped it. It is now the default; skipping is what needs a stated reason.
89
+ function phase15() {
87
90
  const start = NORM.indexOf('Phase 1.5 — Map the mechanism');
88
91
  const end = NORM.indexOf('Phase 2 — Criteria', start);
89
- const section = NORM.slice(start, end === -1 ? undefined : end);
90
- assert.ok(/process-change/i.test(section));
91
- assert.ok(/skip/i.test(section), 'says explicitly when to skip it');
92
+ return NORM.slice(start, end === -1 ? undefined : end);
93
+ }
94
+
95
+ test('the mapping step is the default for any existing surface, and skipping it needs a stated reason', () => {
96
+ const section = phase15();
97
+ assert.ok(!/process-change sessions only/i.test(section), 'no longer framed as opt-in for process-change sessions');
98
+ assert.match(section, /by default for any session that touches an existing surface/);
99
+ assert.match(section, /Skipping it is the thing that needs a stated reason/);
100
+ assert.match(section, /confident model/, 'names the confident-owner case as the one most likely to skip');
101
+ });
102
+
103
+ test('the mapping subagent is always asked the computable-quantity question', () => {
104
+ assert.match(phase15(), /Name any quantity this design assumes is computable, and say whether it actually is/);
105
+ });
106
+
107
+ // --- task 1004675: tie criteria to the agreed design --------------------------
108
+
109
+ function criteriaPhase() {
110
+ const start = NORM.indexOf('### Phase 2 — Criteria');
111
+ const end = NORM.indexOf('### Phase 2.5', start);
112
+ return NORM.slice(start, end);
113
+ }
114
+
115
+ test('Phase 2 says the grader reads the description and diff, not the spec', () => {
116
+ const s = criteriaPhase();
117
+ assert.match(s, /grader reads the \*\*task description and the diff\*\*/);
118
+ assert.match(s, /does \*\*not\*\* read `docs\/specs\/`/);
119
+ });
120
+
121
+ test('Phase 2 names the three bindings: model vs rendering, a proof a wrong build fails, cite the design of record', () => {
122
+ const s = criteriaPhase();
123
+ assert.match(s, /Split the settled design into MODEL and RENDERING/);
124
+ assert.match(s, /goes \*\*into the criterion text\*\*/, 'the model goes in the criterion');
125
+ assert.match(s, /stays in the spec, free to change/, 'the rendering stays in the spec');
126
+ assert.match(s, /proof whose scenario a wrong build fails/);
127
+ assert.match(s, /Cite the design of record in every task description, and make sure the builder can open it/);
128
+ assert.match(s, /private artifact link/, 'warns that an unopenable design of record is decorative');
129
+ });
130
+
131
+ test('Phase 3 carries the binding into the proof task and every task description', () => {
132
+ const start = NORM.indexOf('### Phase 3');
133
+ const s = NORM.slice(start, NORM.indexOf('### Phase 4', start));
134
+ assert.match(s, /The proof's scenario must be one a wrong build fails/);
135
+ assert.match(s, /every description cites the design of record/);
136
+ });
137
+
138
+ test('Phase 4 lays naming options side by side before recommending one', () => {
139
+ const start = NORM.indexOf('### Phase 4');
140
+ const s = NORM.slice(start, NORM.indexOf('### Phase 5', start));
141
+ assert.match(s, /show the options side by side before recommending one/);
142
+ assert.match(s, /never one name with "or something else\?"/);
143
+ });
144
+
145
+ test('big design-driven asks are routed to /planning-session before seeding, from both playbooks', () => {
146
+ const when = NORM.slice(NORM.indexOf('## When to invoke'), NORM.indexOf('## When NOT to invoke'));
147
+ assert.match(when, /big design-driven ask arrives — route it here BEFORE any task is seeded/);
148
+ const goal = fs.readFileSync(path.join(ROOT, '.claude/skills/goal-create/SKILL.md'), 'utf8').replace(/\s+/g, ' ');
149
+ assert.match(goal, /big design-driven ask\*\* .* route it to `\/planning-session` before seeding anything/);
92
150
  });
93
151
 
94
152
  // --- task 1001438: the Phase 5 promote must be scoped to the seeded set -----