mandrel 2.25.0 → 2.26.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/agents/acceptance-critic.md +10 -6
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +3 -1
- package/.agents/instructions.md +1 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +8 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +89 -6
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +38 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +26 -4
- package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -11
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/crap-engine.js +32 -13
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +13 -0
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +209 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/wave-runner/live-probe.js +24 -14
- package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
- package/.agents/scripts/lib/workers/crap-worker.js +2 -10
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +112 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +26 -36
- package/.agents/scripts/single-story-close.js +28 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/stories-wave-tick.js +214 -38
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/deliver.md +13 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +77 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
- package/.agents/workflows/helpers/plan-reference.md +15 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +3 -0
- package/README.md +13 -6
- package/docs/CHANGELOG.md +64 -0
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -377,6 +377,7 @@ export async function runSingleStoryClose({
|
|
|
377
377
|
waitForMerge: waitForMergeParam,
|
|
378
378
|
noWaitForMerge: noWaitForMergeParam,
|
|
379
379
|
maxWaitSeconds: maxWaitSecondsParam,
|
|
380
|
+
mergeWatchMode: mergeWatchModeParam,
|
|
380
381
|
injectedProvider,
|
|
381
382
|
injectedConfig,
|
|
382
383
|
injectedNotify,
|
|
@@ -395,10 +396,11 @@ export async function runSingleStoryClose({
|
|
|
395
396
|
waitForMergeParam,
|
|
396
397
|
noWaitForMergeParam,
|
|
397
398
|
maxWaitSecondsParam,
|
|
399
|
+
mergeWatchModeParam,
|
|
398
400
|
});
|
|
399
401
|
if (!options.storyId) {
|
|
400
402
|
throw new Error(
|
|
401
|
-
'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--wait-merge|--no-wait-merge] [--max-wait-seconds <n>]',
|
|
403
|
+
'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--wait-merge|--no-wait-merge] [--max-wait-seconds <n>] [--merge-watch-mode <sync|async>]',
|
|
402
404
|
);
|
|
403
405
|
}
|
|
404
406
|
|
|
@@ -433,6 +435,210 @@ export async function runSingleStoryClose({
|
|
|
433
435
|
}
|
|
434
436
|
}
|
|
435
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Resolve the arm outcome. An already-merged head PR (Story #4873) has nothing
|
|
440
|
+
* to arm — `gh pr merge` against it fails, which would report the arm as a
|
|
441
|
+
* fault and block a Story whose work is already on the base branch. Skip the
|
|
442
|
+
* arm and let the confirm phase observe the merge that happened.
|
|
443
|
+
*
|
|
444
|
+
* @param {object} args
|
|
445
|
+
* @returns {Promise<{ autoMergeEnabled: boolean, autoMergeReason: string|null,
|
|
446
|
+
* localCleanupDeferred: boolean, directMerged: boolean }>}
|
|
447
|
+
*/
|
|
448
|
+
async function resolveAutoMergeOutcome({ alreadyMerged, ...phaseArgs }) {
|
|
449
|
+
if (alreadyMerged) {
|
|
450
|
+
return {
|
|
451
|
+
autoMergeEnabled: true,
|
|
452
|
+
autoMergeReason: null,
|
|
453
|
+
localCleanupDeferred: false,
|
|
454
|
+
directMerged: false,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return await runAutoMergePhase(phaseArgs);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Report the merge-wait terminal on the progress channel. Three endings, each
|
|
462
|
+
* with its own operator-facing shape — `landed` is done, `pending` is
|
|
463
|
+
* resumable and NOT a failure, anything else is a block naming its class.
|
|
464
|
+
*
|
|
465
|
+
* @param {{ status: string, nextCommand: string, blocked?: {blockClass?: string} }} terminal
|
|
466
|
+
* @param {{ storyId: number, prUrl: string|null }} ctx
|
|
467
|
+
* @returns {void}
|
|
468
|
+
*/
|
|
469
|
+
function reportWaitTerminal(terminal, { storyId, prUrl }) {
|
|
470
|
+
if (terminal.status === 'landed') {
|
|
471
|
+
progress('DONE', `✅ Story #${storyId}: PR merged → ${prUrl}`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (terminal.status === 'pending') {
|
|
475
|
+
// NOT a failure and NOT a block — the wait reached the edge of its
|
|
476
|
+
// host slot with the PR healthy and in flight. The CLI maps this to
|
|
477
|
+
// its own exit code so a caller can resume without classifying.
|
|
478
|
+
progress(
|
|
479
|
+
'PENDING',
|
|
480
|
+
`⏸ Story #${storyId}: PR ${prUrl} still in flight — resume with: ${terminal.nextCommand}`,
|
|
481
|
+
);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
progress(
|
|
485
|
+
'BLOCKED',
|
|
486
|
+
`🛑 Story #${storyId}: PR ${prUrl} did not land ` +
|
|
487
|
+
`(blockClass=${terminal.blocked?.blockClass}). Story is at agent::blocked. ` +
|
|
488
|
+
`Next: ${terminal.nextCommand}`,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The close-and-land ending (Story #4428): poll the just-armed PR to merge
|
|
494
|
+
* confirmation and emit the terminal the outcome names.
|
|
495
|
+
*
|
|
496
|
+
* @param {object} prCtx The shared PR/gate context built by the pipeline.
|
|
497
|
+
* @param {object} deps Runtime seams the confirm phase needs.
|
|
498
|
+
* @returns {Promise<{ success: boolean, result: object, terminal: object }>}
|
|
499
|
+
*/
|
|
500
|
+
async function finishWithMergeWait(prCtx, deps) {
|
|
501
|
+
deps.setPhase('confirm-merge');
|
|
502
|
+
const waitOutcome = await runConfirmMergePhase({
|
|
503
|
+
cwd: deps.cwd,
|
|
504
|
+
storyId: prCtx.storyId,
|
|
505
|
+
storyBranch: prCtx.storyBranch,
|
|
506
|
+
baseBranch: prCtx.baseBranch,
|
|
507
|
+
prNumber: prCtx.prNumber,
|
|
508
|
+
prUrl: prCtx.prUrl,
|
|
509
|
+
autoMergeEnabled: prCtx.autoMergeEnabled,
|
|
510
|
+
autoMergeReason: prCtx.autoMergeReason,
|
|
511
|
+
provider: deps.provider,
|
|
512
|
+
config: prCtx.config,
|
|
513
|
+
maxWaitSeconds: deps.maxWaitSeconds,
|
|
514
|
+
mergeWatchMode: deps.mergeWatchMode,
|
|
515
|
+
progress,
|
|
516
|
+
injectedGh: deps.injectedGh,
|
|
517
|
+
injectedNotify: deps.injectedNotify,
|
|
518
|
+
});
|
|
519
|
+
const terminal = terminalFromWaitOutcome({
|
|
520
|
+
waitOutcome,
|
|
521
|
+
storyId: prCtx.storyId,
|
|
522
|
+
storyBranch: prCtx.storyBranch,
|
|
523
|
+
baseBranch: prCtx.baseBranch,
|
|
524
|
+
prNumber: prCtx.prNumber,
|
|
525
|
+
prUrl: prCtx.prUrl,
|
|
526
|
+
autoMergeEnabled: prCtx.autoMergeEnabled,
|
|
527
|
+
gates: prCtx.gates,
|
|
528
|
+
elapsedSeconds: elapsedSecondsSince(prCtx.startedAtMs),
|
|
529
|
+
});
|
|
530
|
+
const result = closeResult({
|
|
531
|
+
storyId: prCtx.storyId,
|
|
532
|
+
storyBranch: prCtx.storyBranch,
|
|
533
|
+
baseBranch: prCtx.baseBranch,
|
|
534
|
+
prUrl: prCtx.prUrl,
|
|
535
|
+
prNumber: prCtx.prNumber,
|
|
536
|
+
autoMergeEnabled: prCtx.autoMergeEnabled,
|
|
537
|
+
autoMergeReason: prCtx.autoMergeReason,
|
|
538
|
+
worktreeReaped: prCtx.worktreeReaped,
|
|
539
|
+
// Story #4860 — the release is a post-land tail step now, so the tail's
|
|
540
|
+
// own per-step boolean IS the answer. A wait that ended anything other
|
|
541
|
+
// than landed never ran the tail, and correctly reports `false`: the
|
|
542
|
+
// claim is still held, by design.
|
|
543
|
+
leaseReleased: waitOutcome.tail?.leaseRelease === true,
|
|
544
|
+
localCleanupDeferred: prCtx.localCleanupDeferred,
|
|
545
|
+
directMerged: prCtx.directMerged,
|
|
546
|
+
waitedForMerge: true,
|
|
547
|
+
merged: waitOutcome.confirmed === true,
|
|
548
|
+
});
|
|
549
|
+
await emitTerminal({ terminal, result, config: prCtx.config });
|
|
550
|
+
reportWaitTerminal(terminal, { storyId: prCtx.storyId, prUrl: prCtx.prUrl });
|
|
551
|
+
return { success: terminal.status === 'landed', result, terminal };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* The no-wait ending — `--no-wait-merge` / operator-merge. The PR is open and
|
|
556
|
+
* a human owns the land. That is a `pending` terminal by definition: the work
|
|
557
|
+
* is not done, nothing is broken, and one named command finishes it — rather
|
|
558
|
+
* than a fourth status invented for this one case.
|
|
559
|
+
*
|
|
560
|
+
* @param {object} prCtx
|
|
561
|
+
* @param {string} waitForMergeReason
|
|
562
|
+
* @returns {Promise<{ success: boolean, result: object, terminal: object }>}
|
|
563
|
+
*/
|
|
564
|
+
async function finishWithoutMergeWait(prCtx, waitForMergeReason) {
|
|
565
|
+
const result = closeResult({
|
|
566
|
+
storyId: prCtx.storyId,
|
|
567
|
+
storyBranch: prCtx.storyBranch,
|
|
568
|
+
baseBranch: prCtx.baseBranch,
|
|
569
|
+
prUrl: prCtx.prUrl,
|
|
570
|
+
prNumber: prCtx.prNumber,
|
|
571
|
+
autoMergeEnabled: prCtx.autoMergeEnabled,
|
|
572
|
+
autoMergeReason: prCtx.autoMergeReason,
|
|
573
|
+
worktreeReaped: prCtx.worktreeReaped,
|
|
574
|
+
// Story #4860 — this is the no-wait ending: the PR is open and a human
|
|
575
|
+
// owns the merge, so the Story stays assigned until the confirm-merge
|
|
576
|
+
// surface lands it and runs the tail.
|
|
577
|
+
leaseReleased: false,
|
|
578
|
+
localCleanupDeferred: prCtx.localCleanupDeferred,
|
|
579
|
+
directMerged: prCtx.directMerged,
|
|
580
|
+
});
|
|
581
|
+
const terminal = buildTerminalEnvelope({
|
|
582
|
+
storyId: prCtx.storyId,
|
|
583
|
+
status: 'pending',
|
|
584
|
+
phase: 'auto-merge',
|
|
585
|
+
storyBranch: prCtx.storyBranch,
|
|
586
|
+
baseBranch: prCtx.baseBranch,
|
|
587
|
+
pr: {
|
|
588
|
+
number: prCtx.prNumber,
|
|
589
|
+
url: prCtx.prUrl ?? null,
|
|
590
|
+
state: 'OPEN',
|
|
591
|
+
autoMergeEnabled: Boolean(prCtx.autoMergeEnabled),
|
|
592
|
+
},
|
|
593
|
+
gates: prCtx.gates,
|
|
594
|
+
nextCommand: NEXT_COMMANDS.confirmMerge(prCtx.storyId),
|
|
595
|
+
elapsedSeconds: elapsedSecondsSince(prCtx.startedAtMs),
|
|
596
|
+
});
|
|
597
|
+
await emitTerminal({ terminal, result, config: prCtx.config });
|
|
598
|
+
progress(
|
|
599
|
+
'DONE',
|
|
600
|
+
`✅ Story #${prCtx.storyId}: PR ready → ${prCtx.prUrl} (${waitForMergeReason})`,
|
|
601
|
+
);
|
|
602
|
+
return { success: true, result, terminal };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Wall-clock seconds since the close started, rounded — the terminal
|
|
607
|
+
* envelope's `elapsedSeconds`.
|
|
608
|
+
*
|
|
609
|
+
* @param {number} startedAtMs
|
|
610
|
+
* @returns {number}
|
|
611
|
+
*/
|
|
612
|
+
function elapsedSecondsSince(startedAtMs) {
|
|
613
|
+
return Math.round((Date.now() - startedAtMs) / 1000);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Announce the operator-merge skip: the PR was deliberately left un-armed, so
|
|
618
|
+
* nothing here can land it and the Story rests at `agent::closing`. No-op on
|
|
619
|
+
* every other wait reason.
|
|
620
|
+
*
|
|
621
|
+
* @param {{ waitForMergeReason: string, autoMergeReason: string|null,
|
|
622
|
+
* storyId: number, waitForMergeExplicit?: boolean }} args
|
|
623
|
+
* @returns {void}
|
|
624
|
+
*/
|
|
625
|
+
function reportOperatorMergeSkip({
|
|
626
|
+
waitForMergeReason,
|
|
627
|
+
autoMergeReason,
|
|
628
|
+
storyId,
|
|
629
|
+
waitForMergeExplicit,
|
|
630
|
+
}) {
|
|
631
|
+
if (waitForMergeReason !== 'operator-merge') return;
|
|
632
|
+
progress(
|
|
633
|
+
'MERGE',
|
|
634
|
+
`⏭ Not waiting for merge (${autoMergeReason}) — the operator owns this merge; ` +
|
|
635
|
+
`Story #${storyId} rests at agent::closing.` +
|
|
636
|
+
(waitForMergeExplicit === true
|
|
637
|
+
? ' --wait-merge cannot land a PR that was deliberately left un-armed.'
|
|
638
|
+
: ''),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
436
642
|
/**
|
|
437
643
|
* The close pipeline proper. Split out of `runSingleStoryClose` so the
|
|
438
644
|
* phase-tagging wrapper above stays a thin, obviously-correct boundary rather
|
|
@@ -553,31 +759,21 @@ async function runClosePipeline({
|
|
|
553
759
|
WorktreeManager,
|
|
554
760
|
});
|
|
555
761
|
setPhase('auto-merge');
|
|
556
|
-
// An already-merged head PR (Story #4873) has nothing to arm — `gh pr merge`
|
|
557
|
-
// against it fails, which would report the arm as a fault and block a Story
|
|
558
|
-
// whose work is already on the base branch. Skip the arm and let the confirm
|
|
559
|
-
// phase observe the merge that happened.
|
|
560
762
|
const {
|
|
561
763
|
autoMergeEnabled,
|
|
562
764
|
autoMergeReason,
|
|
563
765
|
localCleanupDeferred,
|
|
564
766
|
directMerged,
|
|
565
|
-
} =
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
prUrl,
|
|
576
|
-
noAutoMerge: options.noAutoMerge,
|
|
577
|
-
autoMergePolicy: getCiDelivery(config).autoMerge,
|
|
578
|
-
gh: injectedGh,
|
|
579
|
-
progress,
|
|
580
|
-
});
|
|
767
|
+
} = await resolveAutoMergeOutcome({
|
|
768
|
+
alreadyMerged,
|
|
769
|
+
cwd: options.cwd,
|
|
770
|
+
prNumber,
|
|
771
|
+
prUrl,
|
|
772
|
+
noAutoMerge: options.noAutoMerge,
|
|
773
|
+
autoMergePolicy: getCiDelivery(config).autoMerge,
|
|
774
|
+
gh: injectedGh,
|
|
775
|
+
progress,
|
|
776
|
+
});
|
|
581
777
|
await flipLabelAndNotify({
|
|
582
778
|
provider,
|
|
583
779
|
notifyFn: injectedNotify,
|
|
@@ -619,133 +815,42 @@ async function runClosePipeline({
|
|
|
619
815
|
config,
|
|
620
816
|
autoMergeReason,
|
|
621
817
|
});
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
: ''),
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
const gates = {
|
|
633
|
-
validation: options.skipValidation ? 'skipped' : 'passed',
|
|
634
|
-
baseSync: options.skipSync ? 'skipped' : 'passed',
|
|
635
|
-
codeReview: 'passed',
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
if (waitForMerge) {
|
|
639
|
-
setPhase('confirm-merge');
|
|
640
|
-
const waitOutcome = await runConfirmMergePhase({
|
|
641
|
-
cwd: options.cwd,
|
|
642
|
-
storyId: options.storyId,
|
|
643
|
-
storyBranch,
|
|
644
|
-
baseBranch,
|
|
645
|
-
prNumber,
|
|
646
|
-
prUrl,
|
|
647
|
-
autoMergeEnabled,
|
|
648
|
-
autoMergeReason,
|
|
649
|
-
provider,
|
|
650
|
-
config,
|
|
651
|
-
maxWaitSeconds: options.maxWaitSeconds,
|
|
652
|
-
progress,
|
|
653
|
-
injectedGh,
|
|
654
|
-
injectedNotify,
|
|
655
|
-
});
|
|
656
|
-
const terminal = terminalFromWaitOutcome({
|
|
657
|
-
waitOutcome,
|
|
658
|
-
storyId: options.storyId,
|
|
659
|
-
storyBranch,
|
|
660
|
-
baseBranch,
|
|
661
|
-
prNumber,
|
|
662
|
-
prUrl,
|
|
663
|
-
autoMergeEnabled,
|
|
664
|
-
gates,
|
|
665
|
-
elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1000),
|
|
666
|
-
});
|
|
667
|
-
const result = closeResult({
|
|
668
|
-
storyId: options.storyId,
|
|
669
|
-
storyBranch,
|
|
670
|
-
baseBranch,
|
|
671
|
-
prUrl,
|
|
672
|
-
prNumber,
|
|
673
|
-
autoMergeEnabled,
|
|
674
|
-
autoMergeReason,
|
|
675
|
-
worktreeReaped,
|
|
676
|
-
// Story #4860 — the release is a post-land tail step now, so the tail's
|
|
677
|
-
// own per-step boolean IS the answer. A wait that ended anything other
|
|
678
|
-
// than landed never ran the tail, and correctly reports `false`: the
|
|
679
|
-
// claim is still held, by design.
|
|
680
|
-
leaseReleased: waitOutcome.tail?.leaseRelease === true,
|
|
681
|
-
localCleanupDeferred,
|
|
682
|
-
directMerged,
|
|
683
|
-
waitedForMerge: true,
|
|
684
|
-
merged: waitOutcome.confirmed === true,
|
|
685
|
-
});
|
|
686
|
-
await emitTerminal({ terminal, result, config });
|
|
687
|
-
|
|
688
|
-
if (terminal.status === 'landed') {
|
|
689
|
-
progress('DONE', `✅ Story #${options.storyId}: PR merged → ${prUrl}`);
|
|
690
|
-
} else if (terminal.status === 'pending') {
|
|
691
|
-
// NOT a failure and NOT a block — the wait reached the edge of its
|
|
692
|
-
// host slot with the PR healthy and in flight. The CLI maps this to
|
|
693
|
-
// its own exit code so a caller can resume without classifying.
|
|
694
|
-
progress(
|
|
695
|
-
'PENDING',
|
|
696
|
-
`⏸ Story #${options.storyId}: PR ${prUrl} still in flight — resume with: ${terminal.nextCommand}`,
|
|
697
|
-
);
|
|
698
|
-
} else {
|
|
699
|
-
progress(
|
|
700
|
-
'BLOCKED',
|
|
701
|
-
`🛑 Story #${options.storyId}: PR ${prUrl} did not land ` +
|
|
702
|
-
`(blockClass=${terminal.blocked?.blockClass}). Story is at agent::blocked. ` +
|
|
703
|
-
`Next: ${terminal.nextCommand}`,
|
|
704
|
-
);
|
|
705
|
-
}
|
|
706
|
-
return { success: terminal.status === 'landed', result, terminal };
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const result = closeResult({
|
|
818
|
+
reportOperatorMergeSkip({
|
|
819
|
+
waitForMergeReason,
|
|
820
|
+
autoMergeReason,
|
|
821
|
+
storyId: options.storyId,
|
|
822
|
+
waitForMergeExplicit: options.waitForMergeExplicit,
|
|
823
|
+
});
|
|
824
|
+
const prCtx = {
|
|
710
825
|
storyId: options.storyId,
|
|
711
826
|
storyBranch,
|
|
712
827
|
baseBranch,
|
|
713
|
-
prUrl,
|
|
714
828
|
prNumber,
|
|
829
|
+
prUrl,
|
|
715
830
|
autoMergeEnabled,
|
|
716
831
|
autoMergeReason,
|
|
717
832
|
worktreeReaped,
|
|
718
|
-
// Story #4860 — this is the no-wait ending: the PR is open and a human
|
|
719
|
-
// owns the merge, so the Story stays assigned until the confirm-merge
|
|
720
|
-
// surface lands it and runs the tail.
|
|
721
|
-
leaseReleased: false,
|
|
722
833
|
localCleanupDeferred,
|
|
723
834
|
directMerged,
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
storyId: options.storyId,
|
|
731
|
-
status: 'pending',
|
|
732
|
-
phase: 'auto-merge',
|
|
733
|
-
storyBranch,
|
|
734
|
-
baseBranch,
|
|
735
|
-
pr: {
|
|
736
|
-
number: prNumber,
|
|
737
|
-
url: prUrl ?? null,
|
|
738
|
-
state: 'OPEN',
|
|
739
|
-
autoMergeEnabled: Boolean(autoMergeEnabled),
|
|
835
|
+
config,
|
|
836
|
+
startedAtMs,
|
|
837
|
+
gates: {
|
|
838
|
+
validation: options.skipValidation ? 'skipped' : 'passed',
|
|
839
|
+
baseSync: options.skipSync ? 'skipped' : 'passed',
|
|
840
|
+
codeReview: 'passed',
|
|
740
841
|
},
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
if (waitForMerge) {
|
|
845
|
+
return await finishWithMergeWait(prCtx, {
|
|
846
|
+
cwd: options.cwd,
|
|
847
|
+
provider,
|
|
848
|
+
maxWaitSeconds: options.maxWaitSeconds,
|
|
849
|
+
mergeWatchMode: options.mergeWatchMode,
|
|
850
|
+
setPhase,
|
|
851
|
+
injectedGh,
|
|
852
|
+
injectedNotify,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
return await finishWithoutMergeWait(prCtx, waitForMergeReason);
|
|
751
856
|
}
|
|
@@ -9,13 +9,24 @@
|
|
|
9
9
|
import { parse as parseStoryBody } from '../story-body/story-body.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Warn threshold (words) for a Story's inline `## Spec` (Story #4723,
|
|
13
|
+
* retuned by Story #4907). The budget carries **two distinct numbers**, and
|
|
14
|
+
* both belong in any surface that documents it:
|
|
15
|
+
*
|
|
16
|
+
* - **~250 words — the authoring target.** The #4707 contract-level-prose
|
|
17
|
+
* aim: interfaces, invariants, and load-bearing constraints, not
|
|
18
|
+
* route-by-route behavior narration. It lives in the story-author template
|
|
19
|
+
* placeholder (`plan-context.js`), which is what an author actually reads.
|
|
20
|
+
* - **350 words — this warn threshold.** Deliberately slack against the
|
|
21
|
+
* target so the warning marks a real outlier rather than ordinary
|
|
22
|
+
* authoring variance. Measured over the 45 most recent Stories carrying a
|
|
23
|
+
* Spec, a 250-word threshold fired on 22% of *accepted* work (median 233,
|
|
24
|
+
* p75 249) — a warning that common trains its readers to ignore it.
|
|
25
|
+
*
|
|
26
|
+
* Distinct again from the hard ~1500-token fail-closed ceiling in
|
|
16
27
|
* `spec-spill.js`: this budget only warns; it never fails the persist.
|
|
17
28
|
*/
|
|
18
|
-
export const SPEC_SOFT_WORD_BUDGET =
|
|
29
|
+
export const SPEC_SOFT_WORD_BUDGET = 350;
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
32
|
* Resolve a Story's Spec prose across both authoring shapes: the canonical
|