mandrel 2.65.0 → 2.66.0

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.
Files changed (56) hide show
  1. package/.agents/agents/acceptance-critic.md +5 -5
  2. package/.agents/agents/auditor.md +17 -18
  3. package/.agents/agents/plan-critic.md +5 -5
  4. package/.agents/agents/story-worker.md +5 -5
  5. package/.agents/docs/execution-reference.md +27 -5
  6. package/.agents/instructions.md +10 -12
  7. package/.agents/rules/ci-remediation.md +3 -3
  8. package/.agents/rules/gherkin-standards.md +3 -2
  9. package/.agents/rules/git-conventions-reference.md +12 -3
  10. package/.agents/rules/git-conventions.md +9 -7
  11. package/.agents/rules/testing-standards.md +8 -7
  12. package/.agents/runtime-deps.json +1 -1
  13. package/.agents/scripts/bootstrap.js +94 -89
  14. package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
  15. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
  16. package/.agents/scripts/lib/cli/standard-args.js +60 -76
  17. package/.agents/scripts/lib/cli-args.js +26 -0
  18. package/.agents/scripts/lib/config/gates/shared.js +3 -3
  19. package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
  20. package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
  21. package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
  22. package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
  23. package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
  24. package/.agents/scripts/lib/observability/signal-validator.js +17 -5
  25. package/.agents/scripts/lib/orchestration/code-review.js +22 -0
  26. package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
  27. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
  28. package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
  29. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
  30. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
  31. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
  32. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
  33. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
  34. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
  35. package/.agents/scripts/lib/signals/detectors/common.js +63 -51
  36. package/.agents/scripts/lib/transpile.js +28 -3
  37. package/.agents/scripts/single-story-close.js +10 -2
  38. package/.agents/scripts/single-story-confirm-merge.js +267 -238
  39. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  40. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
  41. package/.agents/workflows/audit-architecture.md +5 -4
  42. package/.agents/workflows/audit-documentation.md +5 -5
  43. package/.agents/workflows/audit-performance.md +10 -10
  44. package/.agents/workflows/helpers/acceptance-self-eval.md +8 -8
  45. package/.agents/workflows/helpers/audit-lens-core.md +30 -57
  46. package/.agents/workflows/helpers/deliver-digest.md +2 -2
  47. package/.agents/workflows/helpers/deliver-reference.md +3 -1
  48. package/.agents/workflows/helpers/deliver-story.md +6 -1
  49. package/.agents/workflows/helpers/parallel-tooling.md +16 -18
  50. package/.agents/workflows/mandrel-deliver.md +1 -1
  51. package/.agents/workflows/mandrel-plan.md +6 -5
  52. package/docs/CHANGELOG.md +26 -0
  53. package/lib/cli/guarded-sync.js +87 -0
  54. package/lib/cli/sync-agents.js +9 -92
  55. package/lib/cli/sync-commands.js +9 -101
  56. package/package.json +2 -2
@@ -12,8 +12,8 @@ effort: medium
12
12
 
13
13
  <!--
14
14
  Shared common core — byte-identical across every `.agents/agents/*.md` role
15
- context, ordered FIRST so all role boots share one prompt-cache prefix
16
- (prompt-cache is keyed on the exact byte prefix; the role delta comes last).
15
+ context, ordered FIRST so every role binds the same baseline rules (the
16
+ roles pin different effort levels and share no cache prefix; delta last).
17
17
  Edit it in every role file at once —
18
18
  tests/bootstrap/agent-shared-prefix.test.js fails on any divergence.
19
19
  security-baseline stays inviolable and single-sourced — @-import it, never
@@ -31,9 +31,9 @@ role-delta marker below; the workflow prose your caller hands you supplies
31
31
  the step-by-step. This shared core binds every role:
32
32
 
33
33
  - **Non-interactive.** You have no input channel mid-run. Never ask
34
- clarifying questions — pick the narrowest reasonable interpretation of
35
- your charter, and when you cannot proceed, take your role's
36
- blocked/failure path instead of stalling.
34
+ clarifying questions — take the reading your charter most directly
35
+ supports, name it in your return, and when you cannot proceed, take
36
+ your role's blocked/failure path instead of stalling.
37
37
  - **Absolute paths only.** Your shell's working directory is not guaranteed
38
38
  to persist between calls; pass absolute paths for every file and script.
39
39
  - **Anti-thrashing.** When the same error class recurs despite the same fix,
@@ -12,8 +12,8 @@ effort: medium
12
12
 
13
13
  <!--
14
14
  Shared common core — byte-identical across every `.agents/agents/*.md` role
15
- context, ordered FIRST so all role boots share one prompt-cache prefix
16
- (prompt-cache is keyed on the exact byte prefix; the role delta comes last).
15
+ context, ordered FIRST so every role binds the same baseline rules (the
16
+ roles pin different effort levels and share no cache prefix; delta last).
17
17
  Edit it in every role file at once —
18
18
  tests/bootstrap/agent-shared-prefix.test.js fails on any divergence.
19
19
  security-baseline stays inviolable and single-sourced — @-import it, never
@@ -31,9 +31,9 @@ role-delta marker below; the workflow prose your caller hands you supplies
31
31
  the step-by-step. This shared core binds every role:
32
32
 
33
33
  - **Non-interactive.** You have no input channel mid-run. Never ask
34
- clarifying questions — pick the narrowest reasonable interpretation of
35
- your charter, and when you cannot proceed, take your role's
36
- blocked/failure path instead of stalling.
34
+ clarifying questions — take the reading your charter most directly
35
+ supports, name it in your return, and when you cannot proceed, take
36
+ your role's blocked/failure path instead of stalling.
37
37
  - **Absolute paths only.** Your shell's working directory is not guaranteed
38
38
  to persist between calls; pass absolute paths for every file and script.
39
39
  - **Anti-thrashing.** When the same error class recurs despite the same fix,
@@ -116,13 +116,12 @@ and a surviving **Critical** halts the delivery gate:
116
116
  - **Info** — the floor: a grounded observation asking for no scheduled work
117
117
  (accepts `Informational`). Never a home for findings that fail the bar below.
118
118
 
119
- ## Self-cross-check bar (mandatory before you write the report)
119
+ ## Self-cross-check bar
120
120
 
121
- You are your own adversarial reviewer. After drafting the Detailed Findings and
122
- **before** writing the artifact, re-open every finding and keep it only when
123
- **all** hold: a **grounded** `path:line` you actually read; **reproducible
124
- evidence** (a tool reading, a quoted snippet, or a specific standard it
125
- violates) — never "this looks wrong"; **in-scope** under the scope filter; and
121
+ A finding goes in the report only when **all** hold: a **grounded**
122
+ `path:line` you actually read; **reproducible evidence** (a tool reading, a
123
+ quoted snippet, or a specific standard it violates) — never "this looks
124
+ wrong"; **in-scope** under the scope filter; and
126
125
  an **actionable** recommendation. Drop anything resting on a sanctioned test
127
126
  seam, an entry point / public API surface, dynamic/framework reachability, an
128
127
  intentional documented deviation, or a formatter-governed style nit.
@@ -136,14 +135,14 @@ Beside it, carry one machine-readable tally of the findings you kept —
136
135
  included, `Info` never counted. `audit-to-stories` cross-checks that line
137
136
  against its parse and refuses a report whose tally is missing or wrong.
138
137
 
139
- ## Fan-out (heavyweight lenses)
138
+ ## Fan-out (operator-requested only)
140
139
 
141
- When your caller dispatches you for a single dimension of a heavyweight lens
142
- (`audit-architecture`, `audit-performance`, `audit-documentation`), audit only
143
- that dimension and return its findings; the parent merges the per-dimension
144
- results under this self-cross-check bar. Within the supported nesting-depth
145
- budget you may apply `parallel-tooling.md` Rule 3 to your own independent
146
- sub-units.
140
+ By default you audit the whole lens. When your caller dispatches you for a
141
+ single dimension — which it does only on an explicit operator request for
142
+ per-dimension fan-out — audit only that dimension and return its findings;
143
+ the caller merges the per-dimension results under this self-cross-check bar.
144
+ You never dispatch sub-agents of your own: no nested fan-out, whatever the
145
+ lens's size.
147
146
 
148
147
  ## Return contract
149
148
 
@@ -12,8 +12,8 @@ effort: medium
12
12
 
13
13
  <!--
14
14
  Shared common core — byte-identical across every `.agents/agents/*.md` role
15
- context, ordered FIRST so all role boots share one prompt-cache prefix
16
- (prompt-cache is keyed on the exact byte prefix; the role delta comes last).
15
+ context, ordered FIRST so every role binds the same baseline rules (the
16
+ roles pin different effort levels and share no cache prefix; delta last).
17
17
  Edit it in every role file at once —
18
18
  tests/bootstrap/agent-shared-prefix.test.js fails on any divergence.
19
19
  security-baseline stays inviolable and single-sourced — @-import it, never
@@ -31,9 +31,9 @@ role-delta marker below; the workflow prose your caller hands you supplies
31
31
  the step-by-step. This shared core binds every role:
32
32
 
33
33
  - **Non-interactive.** You have no input channel mid-run. Never ask
34
- clarifying questions — pick the narrowest reasonable interpretation of
35
- your charter, and when you cannot proceed, take your role's
36
- blocked/failure path instead of stalling.
34
+ clarifying questions — take the reading your charter most directly
35
+ supports, name it in your return, and when you cannot proceed, take
36
+ your role's blocked/failure path instead of stalling.
37
37
  - **Absolute paths only.** Your shell's working directory is not guaranteed
38
38
  to persist between calls; pass absolute paths for every file and script.
39
39
  - **Anti-thrashing.** When the same error class recurs despite the same fix,
@@ -9,8 +9,8 @@ description: >-
9
9
 
10
10
  <!--
11
11
  Shared common core — byte-identical across every `.agents/agents/*.md` role
12
- context, ordered FIRST so all role boots share one prompt-cache prefix
13
- (prompt-cache is keyed on the exact byte prefix; the role delta comes last).
12
+ context, ordered FIRST so every role binds the same baseline rules (the
13
+ roles pin different effort levels and share no cache prefix; delta last).
14
14
  Edit it in every role file at once —
15
15
  tests/bootstrap/agent-shared-prefix.test.js fails on any divergence.
16
16
  security-baseline stays inviolable and single-sourced — @-import it, never
@@ -28,9 +28,9 @@ role-delta marker below; the workflow prose your caller hands you supplies
28
28
  the step-by-step. This shared core binds every role:
29
29
 
30
30
  - **Non-interactive.** You have no input channel mid-run. Never ask
31
- clarifying questions — pick the narrowest reasonable interpretation of
32
- your charter, and when you cannot proceed, take your role's
33
- blocked/failure path instead of stalling.
31
+ clarifying questions — take the reading your charter most directly
32
+ supports, name it in your return, and when you cannot proceed, take
33
+ your role's blocked/failure path instead of stalling.
34
34
  - **Absolute paths only.** Your shell's working directory is not guaranteed
35
35
  to persist between calls; pass absolute paths for every file and script.
36
36
  - **Anti-thrashing.** When the same error class recurs despite the same fix,
@@ -72,11 +72,12 @@ and schema mechanics are in [§ Friction telemetry](#friction-telemetry) above.
72
72
  ## FinOps & token budgeting (economic guardrails)
73
73
 
74
74
  Mandrel does **not** enforce live LLM spend from response metadata. It bounds
75
- two things, both **fixed framework constants** rather than operator knobs, and
76
- both **fail closed**: the assembled `/mandrel-plan` context envelope, and plan-time
77
- Story sizing. Your host runtime (editor / CLI) owns session quota and hard
78
- stops. Consult this section when reasoning about why `/mandrel-plan` refused an
79
- over-ceiling envelope or an over-budget Story count.
75
+ one thing, a **fixed framework constant** rather than an operator knob that
76
+ **fails closed**: the assembled `/mandrel-plan` context envelope. Plan-time
77
+ Story sizing is not bounded (retired by Story #5312). Your host runtime
78
+ (editor / CLI) owns session quota and hard stops. Consult this section when
79
+ reasoning about why `/mandrel-plan` refused an over-ceiling envelope, or when
80
+ choosing the session effort and model for a command.
80
81
 
81
82
  > **There is no configurable context budget.** `planning.context.maxBytes` /
82
83
  > `summaryMode` were removed outright in Story #4541, along with the
@@ -126,3 +127,24 @@ over-ceiling envelope or an over-budget Story count.
126
127
  nothing at plan time scores its authored mass.
127
128
  - **Host runtime**: session billing, quota exhaustion, and operator overrides
128
129
  are enforced by your provider (e.g. Claude Code), not by Mandrel scripts.
130
+
131
+ ### Session effort and model
132
+
133
+ Effort is the operator's dial, set once per session. Pick it deliberately up
134
+ front, because changing effort mid-session invalidates the prompt cache for
135
+ everything that follows.
136
+
137
+ - **Recommended session effort.** `medium` for `/mandrel-deliver`: Stories
138
+ are well-scoped by construction, so delivery rarely needs more. `high` for
139
+ `/mandrel-plan`: a Spec defect costs a redraft or a blocked Story
140
+ downstream, which is dearer than the planning turn.
141
+ - **Role agents.** `story-worker` declares no effort and inherits the
142
+ session's (Story #5426). The evaluator roles (`acceptance-critic`,
143
+ `plan-critic`, `auditor`) pin `medium`.
144
+ - **Where pins live.** Effort and model pins belong only on role agents under
145
+ `.agents/agents/`, never in workflow or command frontmatter: a command that
146
+ pinned its own effort would change effort mid-session and break the prompt
147
+ cache.
148
+ - **Escalation is an operator step.** The Agent tool takes no per-call
149
+ effort, so no workflow can escalate on its own. Before resuming a blocked
150
+ Story, raise session effort one step; raise effort before switching models.
@@ -118,7 +118,7 @@ truncates with a note naming what was cut:
118
118
 
119
119
  ## 3. Core Philosophy
120
120
 
121
- 1. **Context First.** **Digest-first reading (Story #4433):** never
121
+ 1. **Context First.** **Digest-first reading:** never
122
122
  ingest the whole `project.docsContextFiles` set up front — read the
123
123
  docs digest and pull files on demand at the section it names. No
124
124
  digest (ad hoc task, `docsContextFiles` unset, null `docsDigestPath`)
@@ -127,8 +127,10 @@ truncates with a note naming what was cut:
127
127
  present. Always read the current Story's body (`## Spec` +
128
128
  `acceptance[]` / `verify[]`); prefer targeted retrieval over broad
129
129
  reads.
130
- 2. **Plan First.** For non-trivial tasks (3+ steps or architectural
131
- decisions), update the Story's `## Spec` via `/mandrel-plan` before code.
130
+ 2. **Plan First.** Planned work carries its plan in the Story's
131
+ `## Spec`, authored via `/mandrel-plan`; an unplanned prompt takes
132
+ `/mandrel-deliver`'s light path, which escalates to `/mandrel-plan`
133
+ when its gate trips.
132
134
  3. **Artifacts over Chat.** Write test/build/debug output to log
133
135
  files, not into chat.
134
136
  4. **Idempotency.** Scripts must be safe to run repeatedly.
@@ -137,16 +139,13 @@ truncates with a note naming what was cut:
137
139
 
138
140
  ## 4. Execution & Quality Discipline
139
141
 
140
- - **Re-Plan on Failure.** If a strategy fails, STOP and re-plan.
141
142
  - **Subagent Strategy.** Each spawn re-pays the full always-loaded
142
143
  context — a cost decision. Prefer inline search for small lookups;
143
144
  spawn only when the work justifies replicating context. One objective
144
145
  per subagent; depth compounds the cost (every nested level re-pays).
145
- - **Anti-Laziness / No Dead Code.** NEVER use placeholder comments like
146
- `// ... existing code ...`; every edit must leave complete, runnable code.
147
- Remove unused imports, commented-out code, and dead branches before
146
+ - **No Dead Code.** Every edit leaves complete, runnable code. Remove
147
+ unused imports, commented-out code, and dead branches before
148
148
  finalizing.
149
- - **Verification.** Include explicit verification steps in every plan.
150
149
 
151
150
  ---
152
151
 
@@ -191,7 +190,6 @@ anything under it.
191
190
  `/mandrel-plan` sizes each Story as a **capability slice a frontier model
192
191
  delivers and self-verifies in one pass** — a broad footprint is normal
193
192
  when the change is cohesive, and no plan-time ceiling scores it; do not
194
- re-slice it into per-module fragments. On an out-of-scope task: **plan
195
- first** in numbered cohesive sub-steps, **commit incrementally** per
196
- sub-step, and **fail fast** — STOP and report if any sub-step fails
197
- validation.
193
+ re-slice it into per-module fragments. On an out-of-scope task,
194
+ **commit incrementally** per cohesive sub-step, and when a sub-step
195
+ fails validation, stop and apply § 1.I (re-plan or yield).
@@ -137,9 +137,9 @@ run — a blind fix to a suite nobody exercised is how the gap compounds.
137
137
 
138
138
  `capacity` and `unreproducible-tier` name failures that are proven properties
139
139
  of the **environment**: no commit on the branch can move the head SHA to clear
140
- them, so the no-rerun rule used to strand a correct delivery until a human
141
- cleared it by hand. Those two verdicts — and only those two — now buy exactly
142
- one rerun:
140
+ them, so without an allowance the no-rerun rule would strand a correct
141
+ delivery until a human cleared it by hand. Those two verdicts — and only those
142
+ two — buy exactly one rerun:
143
143
 
144
144
  1. Reach the verdict with its required readings (§ above). A green on re-run is
145
145
  never one of those readings.
@@ -143,8 +143,9 @@ authored:
143
143
  widen the regex, updating every call site in the same PR.
144
144
  4. **Add a new definition only when no reasonable match exists**, in the
145
145
  correct domain directory. Never copy-paste a step implementation to support
146
- a paraphrased scenario, and never author new step definitions during
147
- scenario authoring — record the missing step as a named gap instead.
146
+ a paraphrased scenario. A prose-only authoring pass (one whose scope
147
+ excludes step-definition code) records the missing step as a named gap
148
+ instead of writing it.
148
149
 
149
150
  When a step is superseded, mark it deprecated and migrate every call site in
150
151
  the same PR; do not leave two near-identical steps live.
@@ -15,7 +15,9 @@ Mandrel ships as the `mandrel` npm package, whose consumers pin an
15
15
  exact lockfile version; they opt into breaks at upgrade time. Operator policy
16
16
  for any contract change (config shape, baseline shape, schema, lifecycle
17
17
  payload, ticket label, dispatch artifact, public API of a script) is
18
- therefore:
18
+ therefore as follows. It governs Mandrel's own framework contracts; a
19
+ consumer's product API keeps the expand–contract rule in
20
+ [`api-conventions.md`](api-conventions.md).
19
21
 
20
22
  1. **Hard cutovers only.** Contract changes ship as a single in-tree
21
23
  migration of every producer and consumer. There is no parallel
@@ -154,10 +156,10 @@ different hazards:
154
156
 
155
157
  ## Meta Labels (Retrospective Signal Routing)
156
158
 
157
- Two `meta::*` labels route retrospective signals into durable substrates so
159
+ Three `meta::*` labels route retrospective signals into durable substrates so
158
160
  the `/mandrel-plan` Phase 0 fetcher (see
159
161
  [`prior-feedback-fetcher.js`](../scripts/lib/feedback-loop/prior-feedback-fetcher.js))
160
- can surface open feedback issues to the planner. Both labels live in
162
+ can surface open feedback issues to the planner. All three live in
161
163
  [`label-constants.js`](../scripts/lib/label-constants.js) under the
162
164
  `META_LABELS` export — reference them by symbol from scripts rather than
163
165
  hard-coding the string.
@@ -180,3 +182,10 @@ project-local automation). The work is scoped to the consumer's
180
182
  framework changes. Issues that span both axes should carry both labels —
181
183
  `fetchPriorFeedback` dedupes by issue number so a dual-labeled issue
182
184
  appears exactly once in the planner context.
185
+
186
+ ### `meta::platform-gap`
187
+
188
+ Apply this label to a GitHub issue whose fault lies in a shared base
189
+ config, runner fleet, or cross-repo toolchain that neither the framework
190
+ nor the consumer owns — the `--owner platform` bucket of
191
+ [`ci-remediation.md`](ci-remediation.md).
@@ -16,9 +16,8 @@ Every Story lands on a dedicated **Story branch** named
16
16
  `story-<storyId>`, seeded from `project.baseBranch` (`main` by default),
17
17
  isolated in its own worktree at `.worktrees/story-<id>/`. The runtime
18
18
  owns both via `single-story-init.js`; agents commit there only. Close
19
- opens a PR against `main` (squash + required checks). No `epic/<id>`
20
- integration branch, no `--no-ff` wave merge, no child tickets: commits
21
- land on `story-<storyId>` directly, the
19
+ opens a PR against `main` (squash + required checks). Commits land on
20
+ `story-<storyId>` directly, the
22
21
  subject referencing the Story via `(refs #<storyId>)` — see
23
22
  [`.agents/instructions.md` § 5.B](../instructions.md).
24
23
 
@@ -38,15 +37,18 @@ subject referencing the Story via `(refs #<storyId>)` — see
38
37
 
39
38
  ## Push Validation & Reliability (MUSTs)
40
39
 
41
- 1. Run the configured validation commands locally **before** `git push`.
40
+ 1. Validate locally **before** `git push`. On a Story branch that is the
41
+ one credited suite run (`deliver-digest.md` § 5); close runs every
42
+ other gate, so do not pre-run them. Elsewhere, run the configured
43
+ validation commands.
42
44
  2. Do NOT assume a push succeeded unless the output confirms the remote
43
45
  ref was updated (`[new branch]`, `[up to date]`, `... -> ...`).
44
46
  3. If a `pre-push` hook rejects, fix the cause and create a NEW follow-up
45
47
  commit — never amend the rejected commit.
46
48
  4. **Never bypass hooks** (`--no-verify`, `--no-gpg-sign`, …) without
47
- explicit operator authorization. The one recognized exception — a
48
- Biome zero-match failure under a harness-managed worktree path — is a
49
- consumer-tooling gap, **not** authorization; see
49
+ explicit operator authorization. A Biome zero-match failure under a
50
+ harness-managed worktree path is a known false negative — a
51
+ consumer-tooling gap, **not** authorization to bypass; see
50
52
  [`git-conventions-reference.md` § Push Validation](git-conventions-reference.md).
51
53
 
52
54
  ## Local checkout hygiene
@@ -156,13 +156,14 @@ behaviour (`sets status to completed`, not `works`).
156
156
 
157
157
  A file that passes alone but fails inside the full `npm test` suite is **test
158
158
  pollution** — one test leaks shared state (env vars, temp files, the
159
- mock-module registry, global singletons) and a later test trips on it. Reach
160
- for `npm run test:isolate` before manually bisecting: it runs every matching
161
- file individually under `--test-concurrency=1`, then all together, flags files
162
- that pass alone but fail in the suite (**flippers**), binary-bisects the
163
- smallest reproducing subset, and reports any file that exited with leftover
164
- `process.env` mutations. The fix is almost always missing teardown — wrap the
165
- mutation in a `t.before` / `t.after` pair, or restore the prior value in
159
+ mock-module registry, global singletons) and a later test trips on it. Run
160
+ every matching file alone, then all together; the files that pass alone but
161
+ fail in the suite (**flippers**) bound the search, and bisecting them finds
162
+ the smallest reproducing subset. Mandrel's own repository automates this as
163
+ `npm run test:isolate`, which also reports leftover `process.env`
164
+ mutations; a consumer uses its runner's equivalent. The fix is almost
165
+ always missing teardown — wrap the mutation in a `t.before` / `t.after`
166
+ pair, or restore the prior value in
166
167
  `try` / `finally`.
167
168
 
168
169
  For browser-based changes, pair the cycle with runtime verification via Chrome
@@ -18,6 +18,6 @@
18
18
  "chokidar": "^5.0.0",
19
19
  "jscpd": "^4.0.0",
20
20
  "knip": "^6.17.1",
21
- "typescript": ">=5.0.0"
21
+ "typescript": ">=5.0.0 <7"
22
22
  }
23
23
  }
@@ -716,6 +716,13 @@ async function detectCreation(answers, skipGithub) {
716
716
  return creation;
717
717
  }
718
718
 
719
+ function projectNameFor(pn, projects) {
720
+ const match = projects.find((p) => p.value === pn);
721
+ if (!match) return '(unknown)';
722
+ const m = /^(.*)\s+\(#\d+\)$/.exec(match.label);
723
+ return m ? m[1] : match.label;
724
+ }
725
+
719
726
  /**
720
727
  * `{ name, number }` for the summary; the picker stores only the number, so
721
728
  * an existing project's name is looked up.
@@ -723,20 +730,78 @@ async function detectCreation(answers, skipGithub) {
723
730
  function resolveProjectDisplay(answers, skipGithub, projectsList) {
724
731
  const pn = answers.projectNumber;
725
732
  if (!pn) return { name: '(skip)', number: '(skip)' };
726
- if (/^\d+$/.test(pn)) {
727
- let name = '(unknown)';
728
- if (!skipGithub) {
729
- const projects =
730
- projectsList ?? safeList(() => listProjects({ owner: answers.owner }));
731
- const match = projects.find((p) => p.value === pn);
732
- if (match) {
733
- const m = /^(.*)\s+\(#\d+\)$/.exec(match.label);
734
- name = m ? m[1] : match.label;
735
- }
733
+ if (!/^\d+$/.test(pn)) return { name: pn, number: '(new)' };
734
+ if (skipGithub) return { name: '(unknown)', number: pn };
735
+ const projects =
736
+ projectsList ?? safeList(() => listProjects({ owner: answers.owner }));
737
+ return { name: projectNameFor(pn, projects), number: pn };
738
+ }
739
+
740
+ /** Opt-ins default off; dry-run resolves them without prompting. */
741
+ const OPT_INS = Object.freeze([
742
+ {
743
+ key: 'withProjectBoard',
744
+ flag: 'with-project-board',
745
+ prompt: 'Set up project board fields (Status, custom)?',
746
+ },
747
+ {
748
+ key: 'withIssueForms',
749
+ flag: 'with-issue-forms',
750
+ prompt: 'Generate GitHub Issue Form templates?',
751
+ },
752
+ {
753
+ key: 'withQuality',
754
+ flag: 'with-quality',
755
+ prompt:
756
+ 'Install local quality gates (pre-commit hook + quality:preview/watch scripts)?',
757
+ },
758
+ ]);
759
+
760
+ async function resolveOptIns(state) {
761
+ const optIns = {};
762
+ for (const { key, flag, prompt } of OPT_INS) {
763
+ let on = Boolean(state.flags[flag]);
764
+ if (!state.flags['dry-run'] && !on) {
765
+ on = await confirmYesNo(prompt, state.interactive, false);
736
766
  }
737
- return { name, number: pn };
767
+ optIns[key] = on;
738
768
  }
739
- return { name: pn, number: '(new)' };
769
+ return optIns;
770
+ }
771
+
772
+ async function approveCreation(state, creation) {
773
+ if (state.flags['dry-run'] || !(creation.newRepo || creation.newProject)) {
774
+ return true;
775
+ }
776
+ return confirmYesNo(
777
+ 'Create the new GitHub repo/project listed above?',
778
+ state.interactive,
779
+ );
780
+ }
781
+
782
+ async function confirmSummary(state, answers, creation, projectsList) {
783
+ const skipGithub = Boolean(state.flags['skip-github']);
784
+ const project = resolveProjectDisplay(answers, skipGithub, projectsList);
785
+ Logger.info(
786
+ renderAnswerSummary(
787
+ answers,
788
+ creation,
789
+ project,
790
+ state.gitInitialized,
791
+ resolveRepoVisibility(state.flags),
792
+ ),
793
+ );
794
+ return confirmYesNo('Is this correct?', state.interactive);
795
+ }
796
+
797
+ /** Owner repo/project lists, fetched once for pickers and summary. */
798
+ function fetchPickerLists(state, skipGithub) {
799
+ const owner = resolveOwnerForPicker(state.defaults, state.flags);
800
+ if (skipGithub || !owner) return { reposList: [], projectsList: [] };
801
+ return {
802
+ reposList: safeList(() => listRepos({ owner }).map(bareRepoName)),
803
+ projectsList: safeList(() => listProjects({ owner })),
804
+ };
740
805
  }
741
806
 
742
807
  /**
@@ -745,22 +810,16 @@ function resolveProjectDisplay(answers, skipGithub, projectsList) {
745
810
  */
746
811
  export async function collectAndConfirm(state) {
747
812
  const skipGithub = Boolean(state.flags['skip-github']);
748
- const owner = resolveOwnerForPicker(state.defaults, state.flags);
749
- // Fetched once for pickers and summary, so the name never needs a second call.
750
- const reposList =
751
- !skipGithub && owner
752
- ? safeList(() => listRepos({ owner }).map(bareRepoName))
753
- : [];
754
- const projectsList =
755
- !skipGithub && owner ? safeList(() => listProjects({ owner })) : [];
756
-
813
+ const lists = fetchPickerLists(state, skipGithub);
757
814
  let silentAccept = state.silentAccept;
758
815
  for (;;) {
759
816
  const { answers, missing } = await collectAnswers({
760
- questions: buildQuestions(state.defaults, state.flags, process.env, {
761
- reposList,
762
- projectsList,
763
- }),
817
+ questions: buildQuestions(
818
+ state.defaults,
819
+ state.flags,
820
+ process.env,
821
+ lists,
822
+ ),
764
823
  flags: state.flags,
765
824
  interactive: state.interactive,
766
825
  assumeYes: state.assumeYes,
@@ -775,78 +834,24 @@ export async function collectAndConfirm(state) {
775
834
  );
776
835
  return { ok: false, exit: 1 };
777
836
  }
778
- if (!answers.operatorHandle) answers.operatorHandle = answers.owner;
779
- answers.operatorHandle = normalizeHandleAnswer(answers.operatorHandle);
837
+ answers.operatorHandle = normalizeHandleAnswer(
838
+ answers.operatorHandle || answers.owner,
839
+ );
780
840
 
781
841
  const creation = await detectCreation(answers, skipGithub);
782
- const project = resolveProjectDisplay(answers, skipGithub, projectsList);
783
- Logger.info(
784
- renderAnswerSummary(
785
- answers,
786
- creation,
787
- project,
788
- state.gitInitialized,
789
- resolveRepoVisibility(state.flags),
790
- ),
791
- );
792
- const correct = await confirmYesNo('Is this correct?', state.interactive);
793
- if (!correct) {
842
+ if (!(await confirmSummary(state, answers, creation, lists.projectsList))) {
794
843
  Logger.info('[Bootstrap] Okay — let’s try again.');
795
844
  silentAccept = [];
796
845
  continue;
797
846
  }
798
-
799
- if (!state.flags['dry-run'] && (creation.newRepo || creation.newProject)) {
800
- const approved = await confirmYesNo(
801
- 'Create the new GitHub repo/project listed above?',
802
- state.interactive,
803
- );
804
- if (!approved) {
805
- Logger.error(
806
- '[Bootstrap] Creation declined — cannot continue without the repo/project. Exiting.',
807
- );
808
- return { ok: false, exit: 1 };
809
- }
810
- }
811
-
812
- // Opt-ins default off; dry-run resolves them without prompting.
813
- let withProjectBoard = Boolean(state.flags['with-project-board']);
814
- if (!state.flags['dry-run'] && !withProjectBoard) {
815
- withProjectBoard = await confirmYesNo(
816
- 'Set up project board fields (Status, custom)?',
817
- state.interactive,
818
- false,
819
- );
820
- }
821
-
822
- let withIssueForms = Boolean(state.flags['with-issue-forms']);
823
- if (!state.flags['dry-run'] && !withIssueForms) {
824
- withIssueForms = await confirmYesNo(
825
- 'Generate GitHub Issue Form templates?',
826
- state.interactive,
827
- false,
828
- );
829
- }
830
-
831
- let withQuality = Boolean(state.flags['with-quality']);
832
- if (!state.flags['dry-run'] && !withQuality) {
833
- withQuality = await confirmYesNo(
834
- 'Install local quality gates (pre-commit hook + quality:preview/watch scripts)?',
835
- state.interactive,
836
- false,
847
+ if (!(await approveCreation(state, creation))) {
848
+ Logger.error(
849
+ '[Bootstrap] Creation declined — cannot continue without the repo/project. Exiting.',
837
850
  );
851
+ return { ok: false, exit: 1 };
838
852
  }
839
-
840
- return {
841
- ok: true,
842
- payload: {
843
- answers,
844
- creation,
845
- withProjectBoard,
846
- withIssueForms,
847
- withQuality,
848
- },
849
- };
853
+ const optIns = await resolveOptIns(state);
854
+ return { ok: true, payload: { answers, creation, ...optIns } };
850
855
  }
851
856
  }
852
857