mandrel 1.91.0 → 1.93.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/README.md +5 -5
- package/.agents/docs/SDLC.md +75 -107
- package/.agents/docs/configuration.md +1 -1
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/docs/workflows.md +4 -10
- package/.agents/instructions.md +2 -2
- package/.agents/schemas/agentrc.schema.json +1 -1
- package/.agents/schemas/lifecycle/README.md +2 -1
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/risk-verdict.schema.json +14 -1
- package/.agents/scripts/README.md +0 -46
- package/.agents/scripts/analyze-execution.js +76 -1
- package/.agents/scripts/check-doc-links.js +3 -0
- package/.agents/scripts/check-workflow-cli-lint.js +208 -0
- package/.agents/scripts/epic-audit-recheck.js +1 -1
- package/.agents/scripts/epic-plan-clarity.js +16 -6
- package/.agents/scripts/epic-plan-decompose.js +37 -59
- package/.agents/scripts/epic-plan-healthcheck.js +39 -6
- package/.agents/scripts/epic-plan-spec.js +41 -168
- package/.agents/scripts/epic-reconcile.js +7 -1
- package/.agents/scripts/lib/Logger.js +4 -4
- package/.agents/scripts/lib/audit-suite/index.js +4 -5
- package/.agents/scripts/lib/audit-suite/runner.js +6 -4
- package/.agents/scripts/lib/audit-suite/selector.js +2 -3
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/command-header.js +20 -0
- package/.agents/scripts/lib/config/github.js +1 -3
- package/.agents/scripts/lib/config-settings-schema.js +4 -3
- package/.agents/scripts/lib/degraded-mode.js +1 -1
- package/.agents/scripts/lib/label-constants.js +18 -3
- package/.agents/scripts/lib/label-taxonomy.js +14 -3
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
- package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
- package/.agents/scripts/lib/orchestration/file-assumptions.js +68 -7
- package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-context.js +696 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +99 -0
- package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
- package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
- package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +1015 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +206 -0
- package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
- package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -1
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
- package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
- package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
- package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
- package/.agents/scripts/plan-context.js +204 -0
- package/.agents/scripts/plan-critics.js +199 -0
- package/.agents/scripts/plan-persist.js +383 -0
- package/.agents/scripts/pr-watch-with-update.js +8 -7
- package/.agents/scripts/run-lint.js +10 -11
- package/.agents/scripts/story-plan.js +19 -7
- package/.agents/scripts/sync-claude-commands.js +10 -5
- package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
- package/.agents/workflows/audit-documentation.md +5 -7
- package/.agents/workflows/audit-lighthouse.md +1 -0
- package/.agents/workflows/audit-security.md +1 -0
- package/.agents/workflows/audit-to-stories.md +16 -5
- package/.agents/workflows/git-deliver.md +76 -243
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/code-review.md +7 -6
- package/.agents/workflows/helpers/deliver-epic.md +5 -4
- package/.agents/workflows/helpers/plan-epic-reference.md +120 -96
- package/.agents/workflows/helpers/plan-epic.md +307 -994
- package/.agents/workflows/helpers/plan-story.md +14 -14
- package/.agents/workflows/helpers/scope-triage-gate.md +15 -4
- package/.agents/workflows/helpers/single-story-deliver.md +5 -4
- package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
- package/.agents/workflows/mandrel-update.md +144 -466
- package/.agents/workflows/plan.md +87 -145
- package/docs/CHANGELOG.md +20 -0
- package/lib/cli/registry.js +21 -3
- package/package.json +1 -1
- package/.agents/schemas/loop-unit.schema.json +0 -70
- package/.agents/scripts/assert-branch.js +0 -81
- package/.agents/scripts/check-loop-units.js +0 -204
- package/.agents/scripts/detect-merges.js +0 -111
- package/.agents/scripts/git-pr-quality-gate.js +0 -205
- package/.agents/scripts/git-rebase-and-resolve.js +0 -234
- package/.agents/scripts/hierarchy-gate.js +0 -192
- package/.agents/scripts/hydrate-context.js +0 -179
- package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
- package/.agents/scripts/loc-delta.js +0 -205
- package/.agents/scripts/render-manifest.js +0 -143
- package/.agents/scripts/run-audit-suite.js +0 -97
- package/.agents/scripts/select-audits.js +0 -155
- package/.agents/scripts/update-mutation-baseline.js +0 -189
- package/.agents/workflows/explain.md +0 -118
- package/.agents/workflows/git-merge-pr.md +0 -377
- package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
- package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
- package/.agents/workflows/loops/README.md +0 -65
- package/.agents/workflows/loops/fix-failing-tests.md +0 -74
- package/.agents/workflows/loops/nightly-audit.md +0 -81
- package/.agents/workflows/loops/watch-ci.md +0 -68
|
@@ -240,13 +240,17 @@ export async function runDecomposePhase(
|
|
|
240
240
|
* @param {typeof defaultRunPlanHealthcheck} args.runHealthcheckFn
|
|
241
241
|
* @returns {Promise<{ ok: boolean, waived?: boolean, reason?: string|null }>}
|
|
242
242
|
*/
|
|
243
|
-
function enforceFanOutGate(
|
|
243
|
+
export function enforceFanOutGate(
|
|
244
|
+
findings,
|
|
245
|
+
allowLargeFanOut,
|
|
246
|
+
tag = 'epic-plan-decompose',
|
|
247
|
+
) {
|
|
244
248
|
const fanOut = (findings ?? []).filter((f) => f.kind === 'fan-out-warning');
|
|
245
249
|
if (fanOut.length === 0) return;
|
|
246
250
|
if (allowLargeFanOut) {
|
|
247
251
|
for (const f of fanOut) {
|
|
248
252
|
Logger.warn(
|
|
249
|
-
`[
|
|
253
|
+
`[${tag}] Persisting a large-fan-out deletion: ` +
|
|
250
254
|
`Task "${f.taskSlug}" deletes "${f.path}" with ${f.callSiteCount} ` +
|
|
251
255
|
`call site(s) (threshold ${f.threshold}). Operator override --allow-large-fan-out.`,
|
|
252
256
|
);
|
|
@@ -260,7 +264,7 @@ function enforceFanOutGate(findings, allowLargeFanOut) {
|
|
|
260
264
|
)
|
|
261
265
|
.join('\n');
|
|
262
266
|
throw new Error(
|
|
263
|
-
`[
|
|
267
|
+
`[${tag}] ${fanOut.length} Task(s) declare large-fan-out deletions:\n${lines}\n\n` +
|
|
264
268
|
`Split each deletion into a subsystem-by-subsystem migration across multiple Stories, ` +
|
|
265
269
|
`or rerun --allow-large-fan-out after confirming the deletion is intentional.`,
|
|
266
270
|
);
|
|
@@ -284,24 +288,30 @@ function enforceFanOutGate(findings, allowLargeFanOut) {
|
|
|
284
288
|
*
|
|
285
289
|
* @param {object[]} findings
|
|
286
290
|
*/
|
|
287
|
-
function surfaceSoftConflictFindings(
|
|
291
|
+
export function surfaceSoftConflictFindings(
|
|
292
|
+
findings,
|
|
293
|
+
tag = 'epic-plan-decompose',
|
|
294
|
+
) {
|
|
288
295
|
const soft = (findings ?? []).filter(
|
|
289
296
|
(f) => f?.severity === 'soft' && f?.kind !== 'fan-out-warning',
|
|
290
297
|
);
|
|
291
298
|
if (soft.length === 0) return;
|
|
292
299
|
Logger.warn(
|
|
293
|
-
`[
|
|
300
|
+
`[${tag}] ${soft.length} soft cross-Story conflict finding(s) — review before approving the plan:`,
|
|
294
301
|
);
|
|
295
302
|
for (const finding of soft) {
|
|
296
|
-
Logger.warn(
|
|
297
|
-
`[epic-plan-decompose] soft conflict: ${renderHardConflictError(finding)}`,
|
|
298
|
-
);
|
|
303
|
+
Logger.warn(`[${tag}] soft conflict: ${renderHardConflictError(finding)}`);
|
|
299
304
|
}
|
|
300
305
|
}
|
|
301
306
|
|
|
302
|
-
async function runHealthcheckGate({
|
|
307
|
+
export async function runHealthcheckGate({
|
|
308
|
+
epicId,
|
|
309
|
+
epic,
|
|
310
|
+
runHealthcheckFn,
|
|
311
|
+
tag = 'epic-plan-decompose',
|
|
312
|
+
}) {
|
|
303
313
|
Logger.info(
|
|
304
|
-
`[
|
|
314
|
+
`[${tag}] Running post-plan readiness healthcheck for Epic #${epicId}...`,
|
|
305
315
|
);
|
|
306
316
|
let result;
|
|
307
317
|
try {
|
|
@@ -321,13 +331,13 @@ async function runHealthcheckGate({ epicId, epic, runHealthcheckFn }) {
|
|
|
321
331
|
const waived = labels.includes(PLANNING_HEALTHCHECK_WAIVED);
|
|
322
332
|
if (waived) {
|
|
323
333
|
Logger.warn(
|
|
324
|
-
`[
|
|
334
|
+
`[${tag}] Healthcheck failed for Epic #${epicId} but '${PLANNING_HEALTHCHECK_WAIVED}' is applied — proceeding with agent::ready handoff. Reason: ${result?.reason ?? '(no reason reported)'}`,
|
|
325
335
|
);
|
|
326
336
|
return { ok: false, waived: true, reason: result?.reason ?? null };
|
|
327
337
|
}
|
|
328
338
|
|
|
329
339
|
throw new Error(
|
|
330
|
-
`[
|
|
340
|
+
`[${tag}] Refusing agent::ready handoff for Epic #${epicId}: ` +
|
|
331
341
|
`post-plan healthcheck failed (${result?.reason ?? '(no reason reported)'}). ` +
|
|
332
342
|
`Resolve the failing check(s), or apply the '${PLANNING_HEALTHCHECK_WAIVED}' ` +
|
|
333
343
|
`label to the Epic to override and rerun the persist phase.`,
|
|
@@ -113,7 +113,12 @@ export async function buildAuthoringContext(
|
|
|
113
113
|
settings = {},
|
|
114
114
|
opts = {},
|
|
115
115
|
) {
|
|
116
|
-
|
|
116
|
+
// Epic #4474 (M3 PR2): `opts.epic` is an optional prefetched Epic object
|
|
117
|
+
// so a caller that already holds the issue (the folded `plan-context.js`
|
|
118
|
+
// envelope build, which also needs the raw body for clarity scoring and
|
|
119
|
+
// re-plan detection) does not pay a second provider fetch. Absent, the
|
|
120
|
+
// fetch behaviour is unchanged.
|
|
121
|
+
const epic = opts.epic ?? (await provider.getEpic(epicId));
|
|
117
122
|
if (!epic) {
|
|
118
123
|
throw new Error(`Epic #${epicId} not found.`);
|
|
119
124
|
}
|
|
@@ -568,7 +568,20 @@ export function diff({ spec, state, ghState } = {}) {
|
|
|
568
568
|
|
|
569
569
|
for (const entity of flattenSpec(spec)) {
|
|
570
570
|
seenSpecSlugs.add(entity.slug);
|
|
571
|
-
|
|
571
|
+
// Epic #4474 (PR3) — a mapping entry without a numeric `issueNumber`
|
|
572
|
+
// is a partial-failure tombstone: the apply's state writer projects an
|
|
573
|
+
// entry per spec slug, but only completed creations carry a number
|
|
574
|
+
// (`buildState` seeds new slugs with `issueNumber: null`). Treating
|
|
575
|
+
// such an entry as "mapped" made a `--resume` after a rate-limit crash
|
|
576
|
+
// silently skip every failed creation (no Create emitted; the content
|
|
577
|
+
// hash matches, so no Update either — a lossy resume). A numberless
|
|
578
|
+
// entry is therefore unmapped here → Create. No duplicate risk: any
|
|
579
|
+
// live same-title issue was already reseeded onto the slug (with its
|
|
580
|
+
// number) by `reseedMappingFromGh` before the diff runs.
|
|
581
|
+
const mappedEntry = mapping[entity.slug];
|
|
582
|
+
const mapped = Number.isInteger(mappedEntry?.issueNumber)
|
|
583
|
+
? mappedEntry
|
|
584
|
+
: undefined;
|
|
572
585
|
|
|
573
586
|
if (!mapped) {
|
|
574
587
|
plan.creates.push(
|
|
@@ -630,9 +643,12 @@ export function diff({ spec, state, ghState } = {}) {
|
|
|
630
643
|
}
|
|
631
644
|
}
|
|
632
645
|
|
|
633
|
-
// Closes — anything in mapping not seen in spec.
|
|
646
|
+
// Closes — anything in mapping not seen in spec. A numberless
|
|
647
|
+
// partial-failure tombstone (see above) has no live issue to close;
|
|
648
|
+
// dropping it from the spec simply drops the entry.
|
|
634
649
|
for (const [slug, mapped] of Object.entries(mapping)) {
|
|
635
650
|
if (seenSpecSlugs.has(slug)) continue;
|
|
651
|
+
if (!Number.isInteger(mapped?.issueNumber)) continue;
|
|
636
652
|
plan.closes.push(
|
|
637
653
|
closeOp({
|
|
638
654
|
slug,
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Rules (one error per mismatched path):
|
|
17
17
|
* - `creates` + path **exists** → error (Story would clobber).
|
|
18
|
-
* - `refactors-existing` + path **absent** →
|
|
18
|
+
* - `refactors-existing` (via `changes`) + path **absent** →
|
|
19
|
+
* auto-normalized to `creates` with a logged warning (#4496 fix 5):
|
|
20
|
+
* a refactor declaration against a base-untracked path is
|
|
21
|
+
* deterministically a create, so rejecting it only forces a
|
|
22
|
+
* reject→amend→re-persist cycle for a mechanical rewrite. Genuine
|
|
23
|
+
* mismatches keep failing — a `references`-sourced `refactors-existing`
|
|
24
|
+
* on an absent path is a missing read dependency and stays an error.
|
|
19
25
|
* - `exists` + path **absent** → error (read dependency missing).
|
|
20
26
|
* - `deletes` + path **absent** → error (nothing to delete).
|
|
21
27
|
*
|
|
@@ -185,6 +191,18 @@ function renderMismatch({
|
|
|
185
191
|
return `"${slug}" → body.${source} declares assumption="${assumption}" for ${path} but the path already exists at the base branch.`;
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Render an auto-normalization (#4496 fix 5) into a stable warning string.
|
|
196
|
+
* Kept pure and exported through the report so callers log a
|
|
197
|
+
* self-explanatory line rather than re-deriving the rationale.
|
|
198
|
+
*
|
|
199
|
+
* @param {{ slug: string, source: string, path: string, assumption: string }} normalization
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
function renderNormalization({ slug, source, path, assumption }) {
|
|
203
|
+
return `"${slug}" → body.${source} declares assumption="${assumption}" for ${path} but the path is untracked at the base branch — auto-normalized to "creates" (a refactor of a base-untracked path is deterministically a create). Declare assumption="creates" in the plan to silence this warning.`;
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
/**
|
|
189
207
|
* Index, across every Story, which Stories declare a `creates` (and which
|
|
190
208
|
* declare a `deletes`) for each `changes`-sourced path. The maps drive the
|
|
@@ -251,8 +269,11 @@ function predecessorMutator(index, path, predecessors) {
|
|
|
251
269
|
*
|
|
252
270
|
* {
|
|
253
271
|
* errors: string[] // one entry per mismatch, batched per Story
|
|
254
|
-
* warnings: string[] // legacy/no-assumption deprecation nudges
|
|
272
|
+
* warnings: string[] // legacy/no-assumption deprecation nudges +
|
|
273
|
+
* // auto-normalization notices (#4496 fix 5)
|
|
255
274
|
* mismatches: object[] // structured payload for downstream tooling
|
|
275
|
+
* normalizations: object[] // `refactors-existing`→`creates`
|
|
276
|
+
* // auto-normalizations on base-untracked paths
|
|
256
277
|
* }
|
|
257
278
|
*
|
|
258
279
|
* Under the 2-tier hierarchy the Story is the implementation unit, so the
|
|
@@ -283,6 +304,7 @@ export function validateStoryFileAssumptions(opts) {
|
|
|
283
304
|
const errors = [];
|
|
284
305
|
const warnings = [];
|
|
285
306
|
const mismatches = [];
|
|
307
|
+
const normalizations = [];
|
|
286
308
|
const probeCache = new Map();
|
|
287
309
|
|
|
288
310
|
// Wave-aware setup (Story #3960): transitive predecessor sets over the
|
|
@@ -350,6 +372,14 @@ export function validateStoryFileAssumptions(opts) {
|
|
|
350
372
|
predecessorCreator,
|
|
351
373
|
});
|
|
352
374
|
if (mismatch !== null) {
|
|
375
|
+
// Auto-normalization (#4496 fix 5): a deterministic
|
|
376
|
+
// `refactors-existing`→`creates` rewrite is a warning, never a
|
|
377
|
+
// rejection — genuine mismatches keep flowing to `errors`.
|
|
378
|
+
if (mismatch.normalizedTo === 'creates') {
|
|
379
|
+
normalizations.push(mismatch);
|
|
380
|
+
warnings.push(renderNormalization(mismatch));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
353
383
|
mismatches.push(mismatch);
|
|
354
384
|
errors.push(renderMismatch(mismatch));
|
|
355
385
|
continue;
|
|
@@ -384,7 +414,7 @@ export function validateStoryFileAssumptions(opts) {
|
|
|
384
414
|
}
|
|
385
415
|
}
|
|
386
416
|
}
|
|
387
|
-
return { errors, warnings, mismatches };
|
|
417
|
+
return { errors, warnings, mismatches, normalizations };
|
|
388
418
|
}
|
|
389
419
|
|
|
390
420
|
/**
|
|
@@ -468,12 +498,43 @@ function checkAssumption({
|
|
|
468
498
|
}
|
|
469
499
|
return null;
|
|
470
500
|
case 'refactors-existing':
|
|
501
|
+
// Validate against the simulated tree: a predecessor `creates` makes
|
|
502
|
+
// an otherwise-absent base path present, so `refactors-existing`
|
|
503
|
+
// against it is no longer a false-positive mismatch (Story #3960).
|
|
504
|
+
if (!simulatedExists) {
|
|
505
|
+
// Auto-normalization (#4496 fix 5): a `changes`-sourced refactor
|
|
506
|
+
// declaration on a path untracked at the base branch (and not
|
|
507
|
+
// produced by any predecessor) is deterministically a create —
|
|
508
|
+
// downgrade to a normalization warning instead of rejecting. Two
|
|
509
|
+
// genuine mismatches stay hard errors: a `references`-sourced entry
|
|
510
|
+
// (a read dependency this Story does not author cannot be "created"
|
|
511
|
+
// here), and a base-TRACKED path a predecessor deletes (the absence
|
|
512
|
+
// is a plan-shape conflict, not an untracked-path misdeclaration).
|
|
513
|
+
if (source === 'changes' && !baseExists) {
|
|
514
|
+
return {
|
|
515
|
+
slug,
|
|
516
|
+
source,
|
|
517
|
+
path,
|
|
518
|
+
assumption,
|
|
519
|
+
expected: 'creates',
|
|
520
|
+
actual: 'absent',
|
|
521
|
+
normalizedTo: 'creates',
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
slug,
|
|
526
|
+
source,
|
|
527
|
+
path,
|
|
528
|
+
assumption,
|
|
529
|
+
expected: 'present',
|
|
530
|
+
actual: 'absent',
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
471
534
|
case 'exists':
|
|
472
535
|
case 'deletes':
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// `exists` / `deletes` against it is no longer a false-positive
|
|
476
|
-
// mismatch (Story #3960).
|
|
536
|
+
// Same simulated-tree overlay as above (Story #3960); an absent path
|
|
537
|
+
// remains a genuine mismatch for both assumptions.
|
|
477
538
|
if (!simulatedExists) {
|
|
478
539
|
return {
|
|
479
540
|
slug,
|
|
@@ -1,35 +1,22 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* node:coverage ignore file */
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* ledger-diff.js — structural-diff and ordering-assertion helpers over a
|
|
3
|
+
* lifecycle ledger (`temp/epic-<id>/lifecycle.ndjson`) for the
|
|
4
|
+
* repeatability invariants.
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* by epic.merge.ready (same seqId
|
|
14
|
-
* chain; armed.seqId > ready.seqId).
|
|
15
|
-
* reconcile-ordering — pr.created must be preceded by
|
|
16
|
-
* acceptance.reconcile.ok.
|
|
17
|
-
* — exits 0 on pass; exits 1 with a structured message on fail.
|
|
6
|
+
* Relocated from the retired `.agents/scripts/lifecycle-diff.js` CLI
|
|
7
|
+
* (#4482): the assertion helpers are the live surface — they pin the
|
|
8
|
+
* cross-listener ordering contracts in the lifecycle invariant tests —
|
|
9
|
+
* while the CLI wrapper had no workflow, script, or CI consumer.
|
|
18
10
|
*
|
|
19
11
|
* Invariants are derived from Tech Spec #2189 § Repeatability Acceptance
|
|
20
12
|
* Criteria.
|
|
21
13
|
*/
|
|
22
14
|
|
|
23
|
-
import { readFileSync } from 'node:fs';
|
|
24
|
-
import { parseArgs } from 'node:util';
|
|
25
|
-
|
|
26
|
-
import { runAsCli } from './lib/cli-utils.js';
|
|
27
|
-
|
|
28
15
|
/**
|
|
29
16
|
* Parse an NDJSON lifecycle ledger into an array of records. Blank
|
|
30
17
|
* lines tolerated; malformed JSON throws with line number. Duplicated
|
|
31
|
-
* from `
|
|
32
|
-
*
|
|
18
|
+
* from `trace-logger.js` to keep the diff surface decoupled from the
|
|
19
|
+
* listener surface.
|
|
33
20
|
*/
|
|
34
21
|
export function parseLedgerText(text) {
|
|
35
22
|
const out = [];
|
|
@@ -41,7 +28,7 @@ export function parseLedgerText(text) {
|
|
|
41
28
|
out.push(JSON.parse(line));
|
|
42
29
|
} catch (_err) {
|
|
43
30
|
throw new Error(
|
|
44
|
-
`
|
|
31
|
+
`ledger-diff: malformed JSON in ledger on line ${i + 1}: ${line.slice(0, 80)}`,
|
|
45
32
|
);
|
|
46
33
|
}
|
|
47
34
|
}
|
|
@@ -151,56 +138,3 @@ export function assertReconcileOrdering(records) {
|
|
|
151
138
|
}
|
|
152
139
|
return { ok: true };
|
|
153
140
|
}
|
|
154
|
-
|
|
155
|
-
const ASSERTIONS = new Map([
|
|
156
|
-
['merge-gate-ordering', assertMergeGateOrdering],
|
|
157
|
-
['reconcile-ordering', assertReconcileOrdering],
|
|
158
|
-
]);
|
|
159
|
-
|
|
160
|
-
function loadLedger(p) {
|
|
161
|
-
return parseLedgerText(readFileSync(p, 'utf8'));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function main() {
|
|
165
|
-
const { values, positionals } = parseArgs({
|
|
166
|
-
options: {
|
|
167
|
-
assert: { type: 'string' },
|
|
168
|
-
},
|
|
169
|
-
allowPositionals: true,
|
|
170
|
-
args: process.argv.slice(2),
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// --assert <mode> <ledger>
|
|
174
|
-
if (!values.assert) {
|
|
175
|
-
process.stderr.write('Usage: lifecycle-diff --assert <mode> <ledger>\n');
|
|
176
|
-
return 2;
|
|
177
|
-
}
|
|
178
|
-
if (positionals.length !== 1) {
|
|
179
|
-
process.stderr.write(
|
|
180
|
-
'lifecycle-diff --assert <mode> requires exactly one positional ledger path\n',
|
|
181
|
-
);
|
|
182
|
-
return 2;
|
|
183
|
-
}
|
|
184
|
-
const assertion = ASSERTIONS.get(values.assert);
|
|
185
|
-
if (!assertion) {
|
|
186
|
-
process.stderr.write(
|
|
187
|
-
`lifecycle-diff: unknown --assert mode "${values.assert}". Valid: ${[...ASSERTIONS.keys()].join(', ')}\n`,
|
|
188
|
-
);
|
|
189
|
-
return 2;
|
|
190
|
-
}
|
|
191
|
-
const records = loadLedger(positionals[0]);
|
|
192
|
-
const result = assertion(records);
|
|
193
|
-
if (result.ok) {
|
|
194
|
-
process.stdout.write(`[lifecycle-diff] PASS ${values.assert}\n`);
|
|
195
|
-
return 0;
|
|
196
|
-
}
|
|
197
|
-
process.stderr.write(
|
|
198
|
-
`[lifecycle-diff] FAIL ${values.assert}: ${result.reason}\n`,
|
|
199
|
-
);
|
|
200
|
-
return 1;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
await runAsCli(import.meta.url, main, {
|
|
204
|
-
source: 'lifecycle-diff',
|
|
205
|
-
propagateExitCode: true,
|
|
206
|
-
});
|
|
@@ -199,7 +199,7 @@ export class AcceptanceReconciler {
|
|
|
199
199
|
? epicIdFromPayload
|
|
200
200
|
: this.epicId;
|
|
201
201
|
|
|
202
|
-
// 1. Start emit. The downstream
|
|
202
|
+
// 1. Start emit. The downstream ledger-diff `reconcile-ordering`
|
|
203
203
|
// assert checks for `acceptance.reconcile.ok` before `pr.created`,
|
|
204
204
|
// but operators want to see the `.start` marker in the trace too.
|
|
205
205
|
try {
|