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.
- package/.agents/scripts/lib/config-settings-schema-delivery.js +17 -0
- package/.agents/scripts/lib/config-settings-schema.js +11 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +32 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +13 -8
- package/.agents/scripts/lib/orchestration/planning-risk.js +45 -6
- package/docs/CHANGELOG.md +12 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* @param {Function} opts.
|
|
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
|
|
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
|
-
|
|
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