mandrel 2.24.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 +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -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 +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- 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-context-budget.js +95 -14
- 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 +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -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-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -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 +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- 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 -7
- 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/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- 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/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -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/acceptance-eval-decision.js +110 -17
- 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/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- 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/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- 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 +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- 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/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- 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/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- 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/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- 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/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- 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/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -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 +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- 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/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- 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 +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- 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
|
@@ -12,6 +12,7 @@ import { createProvider } from '../../provider-factory.js';
|
|
|
12
12
|
import { flipLabelAndNotify } from '../../single-story/story-merged-notify.js';
|
|
13
13
|
import { WorktreeManager } from '../../worktree-manager.js';
|
|
14
14
|
import { runCodeReview as runCodeReviewDefault } from '../code-review.js';
|
|
15
|
+
import { resolveRunScopedConfig } from '../run-scoped-config.js';
|
|
15
16
|
import { releaseStoryLease } from '../single-story-lease-guard.js';
|
|
16
17
|
import {
|
|
17
18
|
buildTerminalEnvelope,
|
|
@@ -144,6 +145,7 @@ async function runPrePushPhases({
|
|
|
144
145
|
worktreePath,
|
|
145
146
|
config,
|
|
146
147
|
baseBranch,
|
|
148
|
+
baseConfirmed,
|
|
147
149
|
storyBranch,
|
|
148
150
|
storyId,
|
|
149
151
|
provider,
|
|
@@ -185,6 +187,7 @@ async function runPrePushPhases({
|
|
|
185
187
|
cwd,
|
|
186
188
|
worktreePath,
|
|
187
189
|
baseBranch,
|
|
190
|
+
baseConfirmed,
|
|
188
191
|
storyBranch,
|
|
189
192
|
storyId,
|
|
190
193
|
provider,
|
|
@@ -210,7 +213,7 @@ async function openAndReviewPr({
|
|
|
210
213
|
setPhase('push');
|
|
211
214
|
pushStoryBranch({ cwd, storyBranch, gitSync, progress });
|
|
212
215
|
setPhase('pull-request');
|
|
213
|
-
const prUrl = await ensurePullRequestWith({
|
|
216
|
+
const { url: prUrl, alreadyMerged } = await ensurePullRequestWith({
|
|
214
217
|
cwd,
|
|
215
218
|
storyId,
|
|
216
219
|
storyTitle: story.title,
|
|
@@ -220,6 +223,13 @@ async function openAndReviewPr({
|
|
|
220
223
|
progress,
|
|
221
224
|
});
|
|
222
225
|
const prNumber = parsePrNumber(prUrl);
|
|
226
|
+
// Story #4873 — the head's PR already merged (an armed PR that landed while
|
|
227
|
+
// a previous close invocation was between phases). There is nothing left to
|
|
228
|
+
// review and nothing left to arm; the confirm phase probes the PR, observes
|
|
229
|
+
// MERGED, and lands the Story on the merge that actually happened.
|
|
230
|
+
if (alreadyMerged) {
|
|
231
|
+
return { prUrl, prNumber, alreadyMerged: true };
|
|
232
|
+
}
|
|
223
233
|
setPhase('code-review');
|
|
224
234
|
const reviewOutcome = await runStoryScopeReview({
|
|
225
235
|
cwd,
|
|
@@ -245,7 +255,7 @@ async function openAndReviewPr({
|
|
|
245
255
|
'Auto-merge was not enabled. Remediate the findings posted to the PR and re-run `/deliver`.',
|
|
246
256
|
);
|
|
247
257
|
}
|
|
248
|
-
return { prUrl, prNumber };
|
|
258
|
+
return { prUrl, prNumber, alreadyMerged: false };
|
|
249
259
|
}
|
|
250
260
|
|
|
251
261
|
async function releaseLease({
|
|
@@ -367,6 +377,7 @@ export async function runSingleStoryClose({
|
|
|
367
377
|
waitForMerge: waitForMergeParam,
|
|
368
378
|
noWaitForMerge: noWaitForMergeParam,
|
|
369
379
|
maxWaitSeconds: maxWaitSecondsParam,
|
|
380
|
+
mergeWatchMode: mergeWatchModeParam,
|
|
370
381
|
injectedProvider,
|
|
371
382
|
injectedConfig,
|
|
372
383
|
injectedNotify,
|
|
@@ -385,10 +396,11 @@ export async function runSingleStoryClose({
|
|
|
385
396
|
waitForMergeParam,
|
|
386
397
|
noWaitForMergeParam,
|
|
387
398
|
maxWaitSecondsParam,
|
|
399
|
+
mergeWatchModeParam,
|
|
388
400
|
});
|
|
389
401
|
if (!options.storyId) {
|
|
390
402
|
throw new Error(
|
|
391
|
-
'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>]',
|
|
392
404
|
);
|
|
393
405
|
}
|
|
394
406
|
|
|
@@ -423,6 +435,210 @@ export async function runSingleStoryClose({
|
|
|
423
435
|
}
|
|
424
436
|
}
|
|
425
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
|
+
|
|
426
642
|
/**
|
|
427
643
|
* The close pipeline proper. Split out of `runSingleStoryClose` so the
|
|
428
644
|
* phase-tagging wrapper above stays a thin, obviously-correct boundary rather
|
|
@@ -443,7 +659,6 @@ async function runClosePipeline({
|
|
|
443
659
|
const startedAtMs = Date.now();
|
|
444
660
|
const config = injectedConfig || resolveConfig({ cwd: options.cwd });
|
|
445
661
|
const provider = injectedProvider || createProvider(config);
|
|
446
|
-
const baseBranch = config.project?.baseBranch ?? 'main';
|
|
447
662
|
const storyBranch = getStoryBranch(options.storyId);
|
|
448
663
|
|
|
449
664
|
progress('INIT', `Closing standalone Story #${options.storyId}...`);
|
|
@@ -462,6 +677,22 @@ async function runClosePipeline({
|
|
|
462
677
|
);
|
|
463
678
|
}
|
|
464
679
|
|
|
680
|
+
// Story #4891 — the base branch this run was SEEDED from, read back off the
|
|
681
|
+
// run's `story-init` receipt rather than re-resolved from a config file that
|
|
682
|
+
// may have changed during the whole implementation window. Throws (fail
|
|
683
|
+
// closed, naming both values) when the pin and current config disagree —
|
|
684
|
+
// deliberately here, before the gate chain, format-autofix and base-sync,
|
|
685
|
+
// so a wrong base is never merged into the Story branch. `baseConfirmed`
|
|
686
|
+
// gates the base-merge remediation advice further down.
|
|
687
|
+
const { values: runScoped, confirmed: baseConfirmed } =
|
|
688
|
+
await resolveRunScopedConfig({
|
|
689
|
+
provider,
|
|
690
|
+
storyId: options.storyId,
|
|
691
|
+
config,
|
|
692
|
+
progress,
|
|
693
|
+
});
|
|
694
|
+
const baseBranch = runScoped.baseBranch;
|
|
695
|
+
|
|
465
696
|
const worktreePath = resolveWorktreePath({
|
|
466
697
|
cwd: options.cwd,
|
|
467
698
|
config,
|
|
@@ -483,6 +714,7 @@ async function runClosePipeline({
|
|
|
483
714
|
...options,
|
|
484
715
|
config,
|
|
485
716
|
baseBranch,
|
|
717
|
+
baseConfirmed,
|
|
486
718
|
storyBranch,
|
|
487
719
|
provider,
|
|
488
720
|
worktreePath,
|
|
@@ -493,7 +725,7 @@ async function runClosePipeline({
|
|
|
493
725
|
leaseArgs,
|
|
494
726
|
);
|
|
495
727
|
|
|
496
|
-
const { prUrl, prNumber } = await releaseLeaseOnBlock(
|
|
728
|
+
const { prUrl, prNumber, alreadyMerged } = await releaseLeaseOnBlock(
|
|
497
729
|
() =>
|
|
498
730
|
openAndReviewPr({
|
|
499
731
|
cwd: options.cwd,
|
|
@@ -532,7 +764,8 @@ async function runClosePipeline({
|
|
|
532
764
|
autoMergeReason,
|
|
533
765
|
localCleanupDeferred,
|
|
534
766
|
directMerged,
|
|
535
|
-
} = await
|
|
767
|
+
} = await resolveAutoMergeOutcome({
|
|
768
|
+
alreadyMerged,
|
|
536
769
|
cwd: options.cwd,
|
|
537
770
|
prNumber,
|
|
538
771
|
prUrl,
|
|
@@ -582,133 +815,42 @@ async function runClosePipeline({
|
|
|
582
815
|
config,
|
|
583
816
|
autoMergeReason,
|
|
584
817
|
});
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
: ''),
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
const gates = {
|
|
596
|
-
validation: options.skipValidation ? 'skipped' : 'passed',
|
|
597
|
-
baseSync: options.skipSync ? 'skipped' : 'passed',
|
|
598
|
-
codeReview: 'passed',
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
if (waitForMerge) {
|
|
602
|
-
setPhase('confirm-merge');
|
|
603
|
-
const waitOutcome = await runConfirmMergePhase({
|
|
604
|
-
cwd: options.cwd,
|
|
605
|
-
storyId: options.storyId,
|
|
606
|
-
storyBranch,
|
|
607
|
-
baseBranch,
|
|
608
|
-
prNumber,
|
|
609
|
-
prUrl,
|
|
610
|
-
autoMergeEnabled,
|
|
611
|
-
autoMergeReason,
|
|
612
|
-
provider,
|
|
613
|
-
config,
|
|
614
|
-
maxWaitSeconds: options.maxWaitSeconds,
|
|
615
|
-
progress,
|
|
616
|
-
injectedGh,
|
|
617
|
-
injectedNotify,
|
|
618
|
-
});
|
|
619
|
-
const terminal = terminalFromWaitOutcome({
|
|
620
|
-
waitOutcome,
|
|
621
|
-
storyId: options.storyId,
|
|
622
|
-
storyBranch,
|
|
623
|
-
baseBranch,
|
|
624
|
-
prNumber,
|
|
625
|
-
prUrl,
|
|
626
|
-
autoMergeEnabled,
|
|
627
|
-
gates,
|
|
628
|
-
elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1000),
|
|
629
|
-
});
|
|
630
|
-
const result = closeResult({
|
|
631
|
-
storyId: options.storyId,
|
|
632
|
-
storyBranch,
|
|
633
|
-
baseBranch,
|
|
634
|
-
prUrl,
|
|
635
|
-
prNumber,
|
|
636
|
-
autoMergeEnabled,
|
|
637
|
-
autoMergeReason,
|
|
638
|
-
worktreeReaped,
|
|
639
|
-
// Story #4860 — the release is a post-land tail step now, so the tail's
|
|
640
|
-
// own per-step boolean IS the answer. A wait that ended anything other
|
|
641
|
-
// than landed never ran the tail, and correctly reports `false`: the
|
|
642
|
-
// claim is still held, by design.
|
|
643
|
-
leaseReleased: waitOutcome.tail?.leaseRelease === true,
|
|
644
|
-
localCleanupDeferred,
|
|
645
|
-
directMerged,
|
|
646
|
-
waitedForMerge: true,
|
|
647
|
-
merged: waitOutcome.confirmed === true,
|
|
648
|
-
});
|
|
649
|
-
await emitTerminal({ terminal, result, config });
|
|
650
|
-
|
|
651
|
-
if (terminal.status === 'landed') {
|
|
652
|
-
progress('DONE', `✅ Story #${options.storyId}: PR merged → ${prUrl}`);
|
|
653
|
-
} else if (terminal.status === 'pending') {
|
|
654
|
-
// NOT a failure and NOT a block — the wait reached the edge of its
|
|
655
|
-
// host slot with the PR healthy and in flight. The CLI maps this to
|
|
656
|
-
// its own exit code so a caller can resume without classifying.
|
|
657
|
-
progress(
|
|
658
|
-
'PENDING',
|
|
659
|
-
`⏸ Story #${options.storyId}: PR ${prUrl} still in flight — resume with: ${terminal.nextCommand}`,
|
|
660
|
-
);
|
|
661
|
-
} else {
|
|
662
|
-
progress(
|
|
663
|
-
'BLOCKED',
|
|
664
|
-
`🛑 Story #${options.storyId}: PR ${prUrl} did not land ` +
|
|
665
|
-
`(blockClass=${terminal.blocked?.blockClass}). Story is at agent::blocked. ` +
|
|
666
|
-
`Next: ${terminal.nextCommand}`,
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
return { success: terminal.status === 'landed', result, terminal };
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const result = closeResult({
|
|
818
|
+
reportOperatorMergeSkip({
|
|
819
|
+
waitForMergeReason,
|
|
820
|
+
autoMergeReason,
|
|
821
|
+
storyId: options.storyId,
|
|
822
|
+
waitForMergeExplicit: options.waitForMergeExplicit,
|
|
823
|
+
});
|
|
824
|
+
const prCtx = {
|
|
673
825
|
storyId: options.storyId,
|
|
674
826
|
storyBranch,
|
|
675
827
|
baseBranch,
|
|
676
|
-
prUrl,
|
|
677
828
|
prNumber,
|
|
829
|
+
prUrl,
|
|
678
830
|
autoMergeEnabled,
|
|
679
831
|
autoMergeReason,
|
|
680
832
|
worktreeReaped,
|
|
681
|
-
// Story #4860 — this is the no-wait ending: the PR is open and a human
|
|
682
|
-
// owns the merge, so the Story stays assigned until the confirm-merge
|
|
683
|
-
// surface lands it and runs the tail.
|
|
684
|
-
leaseReleased: false,
|
|
685
833
|
localCleanupDeferred,
|
|
686
834
|
directMerged,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
storyId: options.storyId,
|
|
694
|
-
status: 'pending',
|
|
695
|
-
phase: 'auto-merge',
|
|
696
|
-
storyBranch,
|
|
697
|
-
baseBranch,
|
|
698
|
-
pr: {
|
|
699
|
-
number: prNumber,
|
|
700
|
-
url: prUrl ?? null,
|
|
701
|
-
state: 'OPEN',
|
|
702
|
-
autoMergeEnabled: Boolean(autoMergeEnabled),
|
|
835
|
+
config,
|
|
836
|
+
startedAtMs,
|
|
837
|
+
gates: {
|
|
838
|
+
validation: options.skipValidation ? 'skipped' : 'passed',
|
|
839
|
+
baseSync: options.skipSync ? 'skipped' : 'passed',
|
|
840
|
+
codeReview: 'passed',
|
|
703
841
|
},
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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);
|
|
714
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
|