mandrel 1.67.0 → 1.68.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.
@@ -300,6 +300,22 @@ const ACCEPTANCE_EVAL_SCHEMA = {
300
300
  additionalProperties: false,
301
301
  };
302
302
 
303
+ /**
304
+ * `delivery.feedbackLoop` — opt-out toggles consumed by the Epic finalize
305
+ * listener's auto-file graduators (`lib/feedback-loop/*-graduator.js`, read
306
+ * via `graduator-core.js#makeIsAutoFileEnabled`). Both default to `true`
307
+ * (auto-file on); set either to `false` to suppress auto-filing the
308
+ * corresponding non-blocking findings as follow-up issues.
309
+ */
310
+ const FEEDBACK_LOOP_SCHEMA = {
311
+ type: 'object',
312
+ properties: {
313
+ codeReviewAutoFile: { type: 'boolean' },
314
+ auditResultsAutoFile: { type: 'boolean' },
315
+ },
316
+ additionalProperties: false,
317
+ };
318
+
303
319
  export const DELIVERY_SCHEMA = {
304
320
  type: 'object',
305
321
  properties: {
@@ -318,6 +334,7 @@ export const DELIVERY_SCHEMA = {
318
334
  retro: RETRO_SCHEMA,
319
335
  refactorStage: REFACTOR_STAGE_SCHEMA,
320
336
  acceptanceEval: ACCEPTANCE_EVAL_SCHEMA,
337
+ feedbackLoop: FEEDBACK_LOOP_SCHEMA,
321
338
  ci: CI_DELIVERY_SCHEMA,
322
339
  preflight: PREFLIGHT_SCHEMA,
323
340
  // Cross-Story concurrency-hazard gate (Story #2297). When true,
@@ -282,6 +282,17 @@ const PLANNING_SCHEMA = {
282
282
  // decompose loop's re-prompt gate.
283
283
  failOnSharedEditors: { type: 'boolean' },
284
284
  requireExplicitCrossStoryDeps: { type: 'boolean' },
285
+ // Cross-cutting registry conflict knobs consumed by
286
+ // `ticket-validator-conflicts.js` (wired through
287
+ // `epic-plan-decompose/phases/planning-artifacts.js`).
288
+ // `crossCuttingRegistries` names the registry paths whose concurrent
289
+ // edits are flagged; `failOnRegistryConflicts` upgrades that finding to
290
+ // `'hard'`. `failOnLargeFanOut` / `largeFanOutThreshold` gate the
291
+ // single-Story fan-out finding.
292
+ crossCuttingRegistries: LIST_OR_EXTENDER_OF_STRINGS,
293
+ failOnRegistryConflicts: { type: 'boolean' },
294
+ failOnLargeFanOut: { type: 'boolean' },
295
+ largeFanOutThreshold: { type: 'integer', minimum: 0 },
285
296
  // Navigability-reachability config consumed by the epic-plan-healthcheck
286
297
  // --paranoid reachability check (Epic #4131, F7). Opt-in: absent or empty
287
298
  // routeGlobs degrades to a silent no-op.
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import path from 'node:path';
10
+ import { verifyBddRunnerPendingTag } from '../../../bdd-runner-detect.js';
10
11
  import { Logger } from '../../../Logger.js';
11
12
  import { AGENT_LABELS, TYPE_LABELS } from '../../../label-constants.js';
12
13
  import { cleanupPhaseTempFiles } from '../../../plan-phase-cleanup.js';
@@ -45,12 +46,20 @@ function buildRiskVerdictCommentBody({ epicId, riskVerdict, planningRisk }) {
45
46
  verdict: riskVerdict,
46
47
  planningRisk,
47
48
  };
49
+ // Story #4145 — when the disposition was forced to not-applicable because
50
+ // no BDD runner exists, make the waiver operator-visible in the rendered
51
+ // comment (not just the fenced JSON record) so a reviewer sees why an
52
+ // otherwise-required AC table was waived.
53
+ const waiverNote = planningRisk.acceptanceWaivedReason
54
+ ? ['', `> ⚠️ **Acceptance waived** — ${planningRisk.acceptanceWaivedReason}`]
55
+ : [];
48
56
  return [
49
57
  `### 🧭 Planning Risk Verdict — ${planningRisk.overallLevel} · ${planningRisk.gateDecision}`,
50
58
  '',
51
59
  riskVerdict.summary,
52
60
  '',
53
61
  ...axisTable,
62
+ ...waiverNote,
54
63
  '',
55
64
  '```json',
56
65
  JSON.stringify(record, null, 2),
@@ -99,7 +108,29 @@ export async function runSpecPhase(
99
108
  '[epic-plan-spec] risk verdict is required — author risk-verdict.json via the epic-plan-spec-author Skill and pass it with --risk-verdict.',
100
109
  );
101
110
  }
102
- const planningRisk = deriveRiskEnvelope(riskVerdict);
111
+
112
+ // Story #4145 — probe the project's BDD runner. When none is detected
113
+ // (`fallback === true`, e.g. a node:test repo with no tests/features/**),
114
+ // the acceptance disposition is forced to not-applicable inside
115
+ // deriveRiskEnvelope: an authored AC table could never be reconciled by
116
+ // `@epic-<id>-ac-*` feature tags, so /deliver finalize would otherwise
117
+ // abort and require a manual `acceptance::n-a`. The probe is static and
118
+ // best-effort — a detection failure degrades to "runner present" (no
119
+ // forced waiver), preserving the BDD-repo path, and never blocks Phase 7.
120
+ let bddRunner = null;
121
+ try {
122
+ bddRunner = await verifyBddRunnerPendingTag({ cwd: PROJECT_ROOT });
123
+ } catch (err) {
124
+ Logger.warn(
125
+ `[epic-plan-spec] BDD runner probe skipped (${err.message}); acceptance disposition derived from risk axes only.`,
126
+ );
127
+ }
128
+ const planningRisk = deriveRiskEnvelope(riskVerdict, { bddRunner });
129
+ if (planningRisk.acceptanceWaivedReason) {
130
+ Logger.info(
131
+ `[epic-plan-spec] Acceptance disposition forced to not-applicable for Epic #${epicId}: ${planningRisk.acceptanceWaivedReason}`,
132
+ );
133
+ }
103
134
 
104
135
  const epic = await provider.getEpic(epicId);
105
136
  if (!epic) {
@@ -358,10 +358,15 @@ export async function pollUntilTerminal({
358
358
  * @param {number} opts.maxPolls Hard cap on total poll iterations.
359
359
  * @param {number} opts.maxUpdates Cap on `gh pr update-branch` recovery calls.
360
360
  * @param {number} opts.pollIntervalMs Delay between poll ticks.
361
- * @param {Function} opts.ghPrChecksFn
362
- * @param {Function} opts.ghPrViewFn
363
- * @param {Function} opts.ghPrUpdateBranchFn
364
- * @param {Function} opts.sleepFn
361
+ * @param {Function} [opts.ghPrChecksFn] `gh pr checks` invoker. Defaults
362
+ * to the real `gh pr checks` spawn so the CLI path (which injects no
363
+ * port) works; tests override it with a stub. Story #4144.
364
+ * @param {Function} [opts.ghPrViewFn] `gh pr view` invoker. Defaults
365
+ * to the real spawn; tests override.
366
+ * @param {Function} [opts.ghPrUpdateBranchFn] `gh pr update-branch`
367
+ * invoker. Defaults to the real spawn; tests override.
368
+ * @param {Function} [opts.sleepFn] Poll-tick delay. Defaults to a
369
+ * real `setTimeout`-backed sleep; tests override with a no-op.
365
370
  * @param {{ info?: Function, warn?: Function, debug?: Function }} opts.logger
366
371
  * @param {{status:number,stdout:string,stderr:string}} [opts.firstProbe]
367
372
  * Optional already-issued `gh pr checks` result. When the caller (the
@@ -388,10 +393,10 @@ export async function watchPrToTerminal({
388
393
  maxPolls,
389
394
  maxUpdates,
390
395
  pollIntervalMs,
391
- ghPrChecksFn,
392
- ghPrViewFn,
393
- ghPrUpdateBranchFn,
394
- sleepFn,
396
+ ghPrChecksFn = ghPrChecks,
397
+ ghPrViewFn = ghPrView,
398
+ ghPrUpdateBranchFn = ghPrUpdateBranch,
399
+ sleepFn = defaultSleep,
395
400
  logger,
396
401
  firstProbe,
397
402
  }) {
@@ -37,6 +37,18 @@
37
37
  * @property {boolean} requiresReview
38
38
  * @property {AcceptanceDisposition} acceptanceDisposition
39
39
  * @property {GateDecision} gateDecision
40
+ * @property {string} [acceptanceWaivedReason] Present only when the
41
+ * acceptance disposition was forced to `not-applicable` by a non-axis
42
+ * signal (currently: no BDD runner detected). An operator-visible
43
+ * rationale so the override is never silent (Story #4145).
44
+ */
45
+
46
+ /**
47
+ * @typedef {Object} BddRunnerProbe
48
+ * @property {string|null} runner
49
+ * @property {boolean} fallback `true` when no supported BDD runner was
50
+ * detected in the project (`verifyBddRunnerPendingTag`).
51
+ * @property {string} [reason]
40
52
  */
41
53
 
42
54
  const LEVEL_RANK = Object.freeze({ low: 0, medium: 1, high: 2 });
@@ -129,27 +141,54 @@ function resolveRequiresReview(overallLevel, axes) {
129
141
  * (`epic-plan-spec.js`), never here, so a malformed verdict fails closed
130
142
  * before this function runs.
131
143
  *
144
+ * **No-BDD-runner waiver (Story #4145).** The acceptance disposition the risk
145
+ * axes derive presumes a BDD runner exists to satisfy an authored AC table.
146
+ * When `opts.bddRunner.fallback === true` (no supported runner detected — e.g.
147
+ * a `node:test` repo with no `tests/features/**`), an authored AC table can
148
+ * never be reconciled by `@epic-<id>-ac-*` feature tags, so `/deliver`
149
+ * finalize would abort. In that case the disposition is **forced** to
150
+ * `not-applicable` regardless of the risk axes, and `acceptanceWaivedReason`
151
+ * records the override so it is operator-visible, not silent. The
152
+ * `requiresReview` / `gateDecision` outputs are unaffected — a high-risk
153
+ * Epic still routes to review; only the acceptance-spec requirement is
154
+ * waived. Repos that ship a BDD runner (`fallback !== true`) are unaffected.
155
+ *
132
156
  * @param {RiskVerdict} [verdict]
157
+ * @param {{ bddRunner?: BddRunnerProbe|null }} [opts]
133
158
  * @returns {PlanningRiskEnvelope}
134
159
  */
135
- export function deriveRiskEnvelope(verdict = {}) {
160
+ export function deriveRiskEnvelope(verdict = {}, { bddRunner = null } = {}) {
136
161
  const axes = (Array.isArray(verdict.axes) ? verdict.axes : []).map(
137
162
  ({ axis, level, rationale }) => ({ axis, level, rationale }),
138
163
  );
139
164
 
140
165
  const overallLevel = resolveOverallLevel(axes);
141
- const acceptanceDisposition = resolveAcceptanceDisposition(
142
- axes,
143
- overallLevel,
144
- );
166
+ const axisDisposition = resolveAcceptanceDisposition(axes, overallLevel);
145
167
  const requiresReview = resolveRequiresReview(overallLevel, axes);
146
168
  const gateDecision = requiresReview ? 'review-required' : 'auto-proceed';
147
169
 
148
- return {
170
+ const noBddRunner = bddRunner?.fallback === true;
171
+ // Force the waiver only when the axes would otherwise have required (or
172
+ // recommended) an AC table; if the disposition is already not-applicable
173
+ // there is nothing to override and no waiver rationale to surface.
174
+ const forceWaiver = noBddRunner && axisDisposition !== 'not-applicable';
175
+ const acceptanceDisposition = forceWaiver
176
+ ? 'not-applicable'
177
+ : axisDisposition;
178
+
179
+ /** @type {PlanningRiskEnvelope} */
180
+ const envelope = {
149
181
  axes,
150
182
  overallLevel,
151
183
  requiresReview,
152
184
  acceptanceDisposition,
153
185
  gateDecision,
154
186
  };
187
+ if (forceWaiver) {
188
+ envelope.acceptanceWaivedReason =
189
+ `no BDD runner detected (${bddRunner?.reason ?? 'no-bdd-runner-detected'}) — ` +
190
+ `an authored acceptance-spec AC table cannot be reconciled by feature tags, ` +
191
+ `so the acceptance disposition is waived to not-applicable (was ${axisDisposition}).`;
192
+ }
193
+ return envelope;
155
194
  }
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.68.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.67.0...mandrel-v1.68.0) (2026-06-15)
6
+
7
+
8
+ ### Added
9
+
10
+ * **plan:** waive acceptance disposition when no BDD runner detected (refs [#4145](https://github.com/dsj1984/mandrel/issues/4145)) ([#4147](https://github.com/dsj1984/mandrel/issues/4147)) ([f119919](https://github.com/dsj1984/mandrel/commit/f119919c4e69b4a583eeec7c8fe860116dcdf87c))
11
+
12
+
13
+ ### Fixed
14
+
15
+ * **watcher:** default gh ports in watchPrToTerminal so the CLI path runs (refs [#4144](https://github.com/dsj1984/mandrel/issues/4144)) ([#4148](https://github.com/dsj1984/mandrel/issues/4148)) ([764cc05](https://github.com/dsj1984/mandrel/commit/764cc05b4414f2dff0fb1247b647460df32d037d))
16
+
5
17
  ## [1.67.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.66.0...mandrel-v1.67.0) (2026-06-15)
6
18
 
7
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "1.67.0",
3
+ "version": "1.68.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/",