mandrel 1.64.0 → 1.65.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.
@@ -26,6 +26,28 @@ export const INTEGRATION_INCLUDE = [
26
26
 
27
27
  const matchesIntegration = picomatch(INTEGRATION_INCLUDE, { dot: true });
28
28
 
29
+ /**
30
+ * Repo-relative roots the tier walker scans for test files (names ending in
31
+ * `.test.js`).
32
+ *
33
+ * `tests` holds the framework's suite tree; `lib` holds the published CLI
34
+ * (under `lib/cli` and `lib/migrations`) whose tests are colocated in
35
+ * `__tests__` directories per the unit-tier convention in
36
+ * `rules/testing-standards.md`. Without `lib` here, both the quick /
37
+ * integration walk and the full-tier glob set miss the colocated CLI tests,
38
+ * leaving that coverage dark in `npm test`. The matching full-tier globs
39
+ * live in `FULL_TIER_GLOBS`.
40
+ */
41
+ const TEST_WALK_ROOTS = ['tests', 'lib'];
42
+
43
+ /**
44
+ * Glob targets for the `full` tier — one per walk root in `TEST_WALK_ROOTS`.
45
+ * The `tests` glob is a flat recursive sweep; the `lib` glob is scoped to
46
+ * `__tests__` subtrees so it only matches colocated tests, never the shipped
47
+ * source modules themselves.
48
+ */
49
+ const FULL_TIER_GLOBS = ['tests/**/*.test.js', 'lib/**/__tests__/**/*.test.js'];
50
+
29
51
  /**
30
52
  * @param {string} dir
31
53
  * @param {string} prefix
@@ -56,13 +78,11 @@ function walkTestFiles(dir, prefix, fsLike) {
56
78
  * @returns {string[]}
57
79
  */
58
80
  export function listTestFilesForTier(tier, repoRoot, fsLike = fs) {
59
- const all = walkTestFiles(
60
- path.join(repoRoot, 'tests'),
61
- 'tests',
62
- fsLike,
81
+ const all = TEST_WALK_ROOTS.flatMap((root) =>
82
+ walkTestFiles(path.join(repoRoot, root), root, fsLike),
63
83
  ).sort();
64
84
  if (tier === 'full') {
65
- return ['tests/**/*.test.js'];
85
+ return [...FULL_TIER_GLOBS];
66
86
  }
67
87
  const integration = all.filter((file) => matchesIntegration(file));
68
88
  if (tier === 'integration') {
@@ -239,24 +239,61 @@ and optionally route or promote it — with the operator deciding each write.
239
239
  `regression-of-closed`. Stamp the `fingerprintFooter(sha)` marker into any
240
240
  Issue body so future runs dedup against it.
241
241
 
242
- 3. **Optionally promote** the finding to a follow-up ticket via
242
+ 3. **Promote `file`-dispositioned findings through `/plan`** (never a raw
243
+ GitHub Issue) via
243
244
  [`promote-finding.js`](../scripts/lib/findings/promote-finding.js), which
244
- clusters, routes, and files through the same ports — never hand-roll the
245
- promotion:
245
+ clusters, sizes, routes, and files through the same ports `/qa-explore` and
246
+ `/audit-to-stories` consume — never hand-roll the promotion, the clustering,
247
+ or the sizing:
246
248
 
247
249
  ```js
248
250
  import { promoteFindings } from '../scripts/lib/findings/promote-finding.js';
249
- const promotions = await promoteFindings(ledgerItems, { searchIssues, createStory });
251
+ const { promotions } = await promoteFindings(ledgerItems, {
252
+ searchIssues, // GitHub provider, open + closed
253
+ createStory, // tight cluster (≤2 surfaces): render seed → /plan --from-notes
254
+ createEpic, // broad cluster (>2 surfaces): render seed → /plan --idea
255
+ });
250
256
  ```
251
257
 
252
- 4. **Gate:** any ledger append, ticket-filing, or label mutation is a write —
253
- confirm **each one** with the operator before it happens. Redaction has
254
- already run, so nothing unredacted reaches disk or GitHub.
258
+ - **Sizing is delegated, not decided in prose.** `promoteFindings` runs
259
+ `clusterLedgerItems` + `targetForCluster`: a cluster spanning **≤2**
260
+ distinct coverage surfaces routes to `createStory`; **>2** routes to
261
+ `createEpic`. Do not re-cluster, re-size, or re-dedup in the workflow —
262
+ [`route-finding.js`](../scripts/lib/findings/route-finding.js) /
263
+ [`promote-finding.js`](../scripts/lib/findings/promote-finding.js) are the
264
+ single implementation.
265
+ - **`createStory` (`/plan --from-notes`)** — render a **redacted**
266
+ `--from-notes` seed from the cluster (reuse the `/audit-to-stories`
267
+ Phase 5b notes shape; redaction already ran in Phase 2), **stamp the
268
+ cluster's `fingerprintFooter(sha)` verbatim into the seed body**, then
269
+ chain `/plan --from-notes <seed>`. The footer must survive into the issue
270
+ body the Story create path writes — it round-trips through
271
+ `story-plan.js --body <file> --dry-run` unchanged (asserted by the
272
+ deterministic round-trip test under `tests/`) so a later `routeFinding`
273
+ dedups the same finding instead of re-filing it.
274
+ - **`createEpic` (`/plan --idea`)** — carry the cluster's
275
+ `fingerprintFooter(sha)` into the `/plan --idea` seed, then chain
276
+ `/plan --idea <seed>`. **Known limitation (not solved here):**
277
+ per-child-Story fingerprint propagation through full Epic decomposition is
278
+ *not* guaranteed — the fingerprint is carried in the Epic seed only; the
279
+ child Stories `/plan` spawns from that seed are not individually
280
+ footer-stamped.
281
+ - **A `file` disposition never opens a raw GitHub Issue.** Every `file`
282
+ finding flows through `promoteFindings` → `/plan`; only `defer` (carry
283
+ forward as backlog) and `dismiss` (non-actionable) skip the `/plan`
284
+ handoff.
285
+
286
+ 4. **Gate:** any ledger append, seed write, `/plan` invocation, ticket-filing,
287
+ or label mutation is a write — confirm **each one** with the operator before
288
+ it happens. The plan→deliver hard stop is preserved: each `/plan` chain
289
+ pauses at its own HITL gates and never auto-delivers. Redaction has already
290
+ run, so nothing unredacted reaches disk or GitHub.
255
291
 
256
292
  After recording, summarize: the finding recorded, its coverage verdict and
257
293
  `missingTest`, any route/promotion decision
258
- (`new`/`update-existing`/`duplicate`/`regression-of-closed`), and the rolling
259
- backlog a resumed session will pick up.
294
+ (`new`/`update-existing`/`duplicate`/`regression-of-closed`) and whether it was
295
+ promoted to a Story (`/plan --from-notes`) or Epic (`/plan --idea`), and the
296
+ rolling backlog a resumed session will pick up.
260
297
 
261
298
  ---
262
299
 
@@ -291,3 +328,24 @@ backlog a resumed session will pick up.
291
328
  promotion ([`promote-finding.js`](../scripts/lib/findings/promote-finding.js)),
292
329
  and session resolution ([`qa-session.js`](../scripts/lib/qa/qa-session.js))
293
330
  are deterministic — never re-derive them in prose.
331
+ - **Promote through `/plan`, never a raw Issue.** A `file`-dispositioned
332
+ finding is promoted via `promoteFindings`, which chains into
333
+ [`/plan`](plan.md) (`--from-notes` for a tight cluster, `--idea` for a broad
334
+ one) — mirroring [`/audit-to-stories`](audit-to-stories.md). `/qa-assist`
335
+ never opens a bare GitHub Issue for a `file` finding. The cluster's
336
+ `fingerprintFooter(sha)` is stamped verbatim into the seed so a future
337
+ `routeFinding` dedups it.
338
+
339
+ ## See also
340
+
341
+ - [`/plan`](plan.md) — the planning pipeline `/qa-assist` chains into when an
342
+ operator dispositions a finding `file` (`--from-notes` for a Story, `--idea`
343
+ for an Epic). The plan→deliver hard stop is preserved across the handoff.
344
+ - [`/qa-explore`](qa-explore.md) — the agent-led sibling that drives a named
345
+ surface and triages through the same `/plan` handoff.
346
+ - [`/audit-to-stories`](audit-to-stories.md) — the precedent for the
347
+ findings → `/plan` handoff and the shared fingerprint-footer dedup contract.
348
+ - [`promote-finding.js`](../scripts/lib/findings/promote-finding.js) /
349
+ [`route-finding.js`](../scripts/lib/findings/route-finding.js) — the shared
350
+ cluster/size/promote and dedup/route/fingerprint-footer helpers. There is no
351
+ second clustering, sizing, or dedup implementation.
@@ -296,19 +296,62 @@ For each untriaged ledger item:
296
296
  `searchIssues` port to the GitHub provider (querying both open and closed
297
297
  Issues).
298
298
 
299
- 3. **Decide the disposition** with the operator: `file` (promote to a
300
- follow-up ticket with the classified labels + fingerprint footer), `defer`
301
- (carry forward to a later session as backlog), or `dismiss` (non-actionable).
302
- Record the chosen `disposition` back onto the ledger item.
299
+ 3. **Decide the disposition** with the operator: `file` (promote through
300
+ `/plan` never a raw GitHub Issue), `defer` (carry forward to a later
301
+ session as backlog), or `dismiss` (non-actionable). Record the chosen
302
+ `disposition` back onto the ledger item.
303
303
 
304
- 4. **Gate:** any ticket-filing or label mutation is a write — confirm each one
305
- with the operator before it happens. Capture stayed read-only precisely so
306
- that every state change lands here, deliberately and confirmed.
304
+ 4. **Promote the `file`-dispositioned findings through `/plan`** via
305
+ [`promote-finding.js`](../scripts/lib/findings/promote-finding.js) the
306
+ same cluster/size/route/file path `/qa-assist` and `/audit-to-stories`
307
+ consume. Never hand-roll the clustering, sizing, or promotion in prose:
308
+
309
+ ```js
310
+ import { promoteFindings } from '../scripts/lib/findings/promote-finding.js';
311
+ const { promotions } = await promoteFindings(ledgerItems, {
312
+ searchIssues, // GitHub provider, open + closed
313
+ createStory, // tight cluster (≤2 surfaces): render seed → /plan --from-notes
314
+ createEpic, // broad cluster (>2 surfaces): render seed → /plan --idea
315
+ });
316
+ ```
317
+
318
+ - **Sizing is delegated, not decided in prose.** `promoteFindings` runs
319
+ `clusterLedgerItems` + `targetForCluster`: a cluster spanning **≤2**
320
+ distinct coverage surfaces routes to `createStory`; **>2** routes to
321
+ `createEpic`. The workflow introduces no new sizing, clustering, or dedup
322
+ logic — `route-finding.js` / `promote-finding.js` remain the single
323
+ implementation.
324
+ - **`createStory` (`/plan --from-notes`)** — render a **redacted**
325
+ `--from-notes` seed from the cluster (reuse the `/audit-to-stories`
326
+ Phase 5b notes shape; redaction already ran in Capture), **stamp the
327
+ cluster's `fingerprintFooter(sha)` verbatim into the seed body**, then
328
+ chain `/plan --from-notes <seed>`. The footer must survive into the issue
329
+ body the Story create path writes — it round-trips through
330
+ `story-plan.js --body <file> --dry-run` unchanged (asserted by the
331
+ deterministic round-trip test under `tests/`) so a later `routeFinding`
332
+ dedups the same finding instead of re-filing it.
333
+ - **`createEpic` (`/plan --idea`)** — carry the cluster's
334
+ `fingerprintFooter(sha)` into the `/plan --idea` seed, then chain
335
+ `/plan --idea <seed>`. **Known limitation (not solved here):**
336
+ per-child-Story fingerprint propagation through full Epic decomposition is
337
+ *not* guaranteed — the fingerprint is carried in the Epic seed only; the
338
+ child Stories `/plan` spawns from that seed are not individually
339
+ footer-stamped.
340
+ - **A `file` disposition never opens a raw GitHub Issue.** Every `file`
341
+ finding flows through `promoteFindings` → `/plan`; only `defer` and
342
+ `dismiss` skip the `/plan` handoff.
343
+
344
+ 5. **Gate:** any ticket-filing, seed write, `/plan` invocation, or label
345
+ mutation is a write — confirm each one with the operator before it happens.
346
+ Capture stayed read-only precisely so that every state change lands here,
347
+ deliberately and confirmed. The plan→deliver hard stop is preserved: each
348
+ `/plan` chain pauses at its own HITL gates and never auto-delivers.
307
349
 
308
350
  After triage, write the updated dispositions back to the ledger (still under
309
351
  `temp/qa/`), and summarize: items captured, the driving method used, classes,
310
- routes (`new`/`update-existing`/`duplicate`/`regression-of-closed`), filed
311
- tickets, and the deferred rolling backlog that a resumed session will pick up.
352
+ routes (`new`/`update-existing`/`duplicate`/`regression-of-closed`), the
353
+ Stories (`/plan --from-notes`) and Epics (`/plan --idea`) promoted, and the
354
+ deferred rolling backlog that a resumed session will pick up.
312
355
 
313
356
  ---
314
357
 
@@ -343,8 +386,31 @@ tickets, and the deferred rolling backlog that a resumed session will pick up.
343
386
  ([`coverage-verdict.js`](../scripts/lib/qa/coverage-verdict.js)),
344
387
  missing-test ([`propose-missing-test.js`](../scripts/lib/qa/propose-missing-test.js)),
345
388
  classification ([`classify-finding.js`](../scripts/lib/findings/classify-finding.js)),
346
- and dedup/route ([`route-finding.js`](../scripts/lib/findings/route-finding.js))
347
- are deterministic — never re-derive them in prose.
389
+ dedup/route ([`route-finding.js`](../scripts/lib/findings/route-finding.js)),
390
+ and cluster/size/promote
391
+ ([`promote-finding.js`](../scripts/lib/findings/promote-finding.js)) are
392
+ deterministic — never re-derive them in prose.
393
+ - **Promote through `/plan`, never a raw Issue.** A `file`-dispositioned
394
+ finding is promoted via `promoteFindings`, which chains into
395
+ [`/plan`](plan.md) (`--from-notes` for a tight cluster, `--idea` for a broad
396
+ one) — mirroring [`/audit-to-stories`](audit-to-stories.md). `/qa-explore`
397
+ never opens a bare GitHub Issue for a `file` finding. The cluster's
398
+ `fingerprintFooter(sha)` is stamped verbatim into the seed so a future
399
+ `routeFinding` dedups it.
348
400
  - **Resume safely.** A reused session appends and carries the un-triaged
349
401
  backlog forward via [`qa-session.js`](../scripts/lib/qa/qa-session.js); it
350
402
  never overwrites a prior ledger.
403
+
404
+ ## See also
405
+
406
+ - [`/plan`](plan.md) — the planning pipeline `/qa-explore` Triage chains into
407
+ for a `file`-dispositioned finding (`--from-notes` for a Story, `--idea` for
408
+ an Epic). The plan→deliver hard stop is preserved across the handoff.
409
+ - [`/qa-assist`](qa-assist.md) — the human-led sibling that enriches a single
410
+ operator observation and triages through the same `/plan` handoff.
411
+ - [`/audit-to-stories`](audit-to-stories.md) — the precedent for the
412
+ findings → `/plan` handoff and the shared fingerprint-footer dedup contract.
413
+ - [`promote-finding.js`](../scripts/lib/findings/promote-finding.js) /
414
+ [`route-finding.js`](../scripts/lib/findings/route-finding.js) — the shared
415
+ cluster/size/promote and dedup/route/fingerprint-footer helpers. There is no
416
+ second clustering, sizing, or dedup implementation.
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.65.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.64.0...mandrel-v1.65.0) (2026-06-14)
6
+
7
+
8
+ ### Added
9
+
10
+ * Epic [#4118](https://github.com/dsj1984/mandrel/issues/4118) ([#4127](https://github.com/dsj1984/mandrel/issues/4127)) ([d24a1f7](https://github.com/dsj1984/mandrel/commit/d24a1f7d3fc36d20015890fbadd7d08caa1d506b))
11
+ * **qa:** route /qa-assist and /qa-explore triage into /plan (refs [#4115](https://github.com/dsj1984/mandrel/issues/4115)) ([#4116](https://github.com/dsj1984/mandrel/issues/4116)) ([de2a211](https://github.com/dsj1984/mandrel/commit/de2a211089104edd1cb76f77d668b0219a041c3e))
12
+
5
13
  ## [1.64.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.63.0...mandrel-v1.64.0) (2026-06-14)
6
14
 
7
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "1.64.0",
3
+ "version": "1.65.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, personas, skills, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",
@@ -85,6 +85,7 @@
85
85
  "lint-staged": "^17.0.4",
86
86
  "markdownlint-cli2": "^0.18.1",
87
87
  "memfs": "^4.57.2",
88
+ "node-pty": "^1.0.0",
88
89
  "typescript": ">=5.0.0"
89
90
  },
90
91
  "dependencies": {