mandrel 2.54.0 → 2.56.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/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +8 -2
- package/.agents/docs/configuration.md +5 -0
- package/.agents/rules/ci-remediation.md +39 -21
- package/.agents/schemas/agentrc.schema.json +34 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +374 -76
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/file-ci-gap.js +306 -0
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +80 -29
- package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
- package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +61 -115
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +52 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
- package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
- package/.agents/scripts/lib/findings/route-finding.js +42 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/github/framework-repo.js +148 -2
- package/.agents/scripts/lib/label-constants.js +6 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +233 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -0
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +63 -42
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +30 -0
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/pr-watch-with-update.js +3 -2
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/audit-to-stories.md +63 -27
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +35 -14
- package/.agents/workflows/helpers/deliver-story-reference.md +26 -8
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +30 -0
- package/.agents/workflows/mandrel-plan.md +10 -13
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +37 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -15,6 +15,7 @@ import { createProvider } from '../../provider-factory.js';
|
|
|
15
15
|
import { flipLabelAndNotify } from '../../single-story/story-merged-notify.js';
|
|
16
16
|
import { WorktreeManager } from '../../worktree-manager.js';
|
|
17
17
|
import { runCodeReview as runCodeReviewDefault } from '../code-review.js';
|
|
18
|
+
import { MERGED_FLIP_FAILED_BLOCK_CLASS } from '../lifecycle/emit-merge-flip-failed.js';
|
|
18
19
|
import { resolveRunScopedConfig } from '../run-scoped-config.js';
|
|
19
20
|
import { releaseStoryLease } from '../single-story-lease-guard.js';
|
|
20
21
|
import {
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
NEXT_COMMANDS,
|
|
24
25
|
terminalFromWaitOutcome,
|
|
25
26
|
} from '../story-deliver-terminal.js';
|
|
27
|
+
import { deriveCloseNote } from './close-note.js';
|
|
26
28
|
import { runAutoMergePhase } from './phases/auto-merge.js';
|
|
27
29
|
import { runBaseSyncPhase } from './phases/base-sync.js';
|
|
28
30
|
import { runCloseValidationPhase } from './phases/close-validation.js';
|
|
@@ -203,6 +205,7 @@ async function runPrePushPhases({
|
|
|
203
205
|
injectedSync,
|
|
204
206
|
injectedGitSpawn,
|
|
205
207
|
setPhase = () => {},
|
|
208
|
+
setObservedGates = () => {},
|
|
206
209
|
}) {
|
|
207
210
|
setPhase('wrong-tree-guard');
|
|
208
211
|
await runWrongTreeGuardPhase({
|
|
@@ -235,18 +238,34 @@ async function runPrePushPhases({
|
|
|
235
238
|
return { validationGates: null };
|
|
236
239
|
}
|
|
237
240
|
setPhase('close-validation');
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
241
|
+
let validation;
|
|
242
|
+
try {
|
|
243
|
+
validation = await runCloseValidationPhase({
|
|
244
|
+
cwd,
|
|
245
|
+
worktreePath,
|
|
246
|
+
config,
|
|
247
|
+
baseBranch,
|
|
248
|
+
storyBranch,
|
|
249
|
+
storyId,
|
|
250
|
+
progress,
|
|
251
|
+
runCloseValidation,
|
|
252
|
+
buildDefaultGates,
|
|
253
|
+
});
|
|
254
|
+
} catch (err) {
|
|
255
|
+
// Story #5279 — the failed terminal REPORTS gates rather than
|
|
256
|
+
// reconstructing them, so hand it what this run observed. The phase tags
|
|
257
|
+
// `closeGate` with the entry that died; that one name is the whole of
|
|
258
|
+
// what the run observed about gate outcomes, and therefore the whole of
|
|
259
|
+
// what the envelope may claim. Anything else it might have named is a
|
|
260
|
+
// gate the run never proved ran at all.
|
|
261
|
+
if (typeof err?.closeGate === 'string') {
|
|
262
|
+
setObservedGates({ [err.closeGate]: 'failed' });
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
const gates = validation?.gates ?? null;
|
|
267
|
+
setObservedGates(gates);
|
|
268
|
+
return { validationGates: gates };
|
|
250
269
|
}
|
|
251
270
|
|
|
252
271
|
async function openAndReviewPr({
|
|
@@ -402,6 +421,38 @@ async function releaseLeaseOnBlock(run, leaseArgs) {
|
|
|
402
421
|
}
|
|
403
422
|
}
|
|
404
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Did this run OBSERVE the PR merge? (Story #5279)
|
|
426
|
+
*
|
|
427
|
+
* `merged` used to be whatever the caller happened to pass, and the two
|
|
428
|
+
* finishers passed different halves of the truth. Three observations say a
|
|
429
|
+
* merge happened, and each of them reported `merged: false` on at least one
|
|
430
|
+
* path:
|
|
431
|
+
*
|
|
432
|
+
* - the confirm phase watched it land (`waitOutcome.confirmed`);
|
|
433
|
+
* - it landed and only the `agent::done` label write failed
|
|
434
|
+
* (`merged-flip-failed`) — the terminal envelope for that same run
|
|
435
|
+
* already reports `pr.state: MERGED` from the probe, so a result denying
|
|
436
|
+
* the merge made the two halves of one run contradict each other;
|
|
437
|
+
* - the arm phase direct-squash-merged it synchronously because the
|
|
438
|
+
* repository has no native auto-merge (`directMerged`), which the
|
|
439
|
+
* no-wait finisher never consulted at all.
|
|
440
|
+
*
|
|
441
|
+
* Deliberately observation-only: it reads what the run saw, never what the
|
|
442
|
+
* run intended. A `null`/absent `waitOutcome` is the no-wait finisher, whose
|
|
443
|
+
* sole observation is the direct merge.
|
|
444
|
+
*
|
|
445
|
+
* @param {{ waitOutcome?: object|null, directMerged?: boolean }} args
|
|
446
|
+
* @returns {boolean}
|
|
447
|
+
*/
|
|
448
|
+
function deriveObservedMerge({ waitOutcome = null, directMerged = false }) {
|
|
449
|
+
return (
|
|
450
|
+
waitOutcome?.confirmed === true ||
|
|
451
|
+
waitOutcome?.blockClass === MERGED_FLIP_FAILED_BLOCK_CLASS ||
|
|
452
|
+
directMerged === true
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
405
456
|
function closeResult({
|
|
406
457
|
storyId,
|
|
407
458
|
storyBranch,
|
|
@@ -416,6 +467,7 @@ function closeResult({
|
|
|
416
467
|
directMerged = false,
|
|
417
468
|
waitedForMerge = false,
|
|
418
469
|
merged = false,
|
|
470
|
+
landCompleted = merged,
|
|
419
471
|
}) {
|
|
420
472
|
return {
|
|
421
473
|
storyId,
|
|
@@ -440,11 +492,20 @@ function closeResult({
|
|
|
440
492
|
directMerged,
|
|
441
493
|
waitedForMerge,
|
|
442
494
|
merged,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
495
|
+
// Story #5266 — derived from `merged` / `directMerged` / `autoMergeEnabled`,
|
|
496
|
+
// never from `waitedForMerge`. A wait that expired unmerged used to write
|
|
497
|
+
// "PR merge confirmed" beside `merged: false`, and this log is what the
|
|
498
|
+
// operator reads first. See close-note.js for the invariant.
|
|
499
|
+
// Story #5279 — `landCompleted` keeps that invariant intact now that
|
|
500
|
+
// `merged: true` no longer implies the flip and the tail ran: a direct
|
|
501
|
+
// merge under `--no-wait-merge`, and a `merged-flip-failed` block, are
|
|
502
|
+
// both merged WITHOUT a completed land.
|
|
503
|
+
note: deriveCloseNote({
|
|
504
|
+
merged,
|
|
505
|
+
directMerged,
|
|
506
|
+
autoMergeEnabled,
|
|
507
|
+
landCompleted,
|
|
508
|
+
}),
|
|
448
509
|
};
|
|
449
510
|
}
|
|
450
511
|
|
|
@@ -458,6 +519,7 @@ export async function runSingleStoryClose({
|
|
|
458
519
|
noWaitForMerge: noWaitForMergeParam,
|
|
459
520
|
maxWaitSeconds: maxWaitSecondsParam,
|
|
460
521
|
mergeWatchMode: mergeWatchModeParam,
|
|
522
|
+
rerunAdvisory: rerunAdvisoryParam,
|
|
461
523
|
overrideReviewBlock: overrideReviewBlockParam,
|
|
462
524
|
injectedProvider,
|
|
463
525
|
injectedConfig,
|
|
@@ -478,11 +540,12 @@ export async function runSingleStoryClose({
|
|
|
478
540
|
noWaitForMergeParam,
|
|
479
541
|
maxWaitSecondsParam,
|
|
480
542
|
mergeWatchModeParam,
|
|
543
|
+
rerunAdvisoryParam,
|
|
481
544
|
overrideReviewBlockParam,
|
|
482
545
|
});
|
|
483
546
|
if (!options.storyId) {
|
|
484
547
|
throw new Error(
|
|
485
|
-
'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>] [--override-review-block <reason>]',
|
|
548
|
+
'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>] [--rerun-advisory <n>] [--override-review-block <reason>]',
|
|
486
549
|
);
|
|
487
550
|
}
|
|
488
551
|
|
|
@@ -493,13 +556,23 @@ export async function runSingleStoryClose({
|
|
|
493
556
|
// the `failed` terminal envelope. Tagging rather than swallowing is what
|
|
494
557
|
// lets `failed` name its phase without inventing a second success path.
|
|
495
558
|
let phase = 'init';
|
|
559
|
+
// Story #5279 — the per-gate outcomes this run OBSERVED, tagged onto a
|
|
560
|
+
// throwing error alongside the phase so `failedTerminalFor` reports gates
|
|
561
|
+
// instead of reconstructing which ones "must have" run. Stays null until
|
|
562
|
+
// close-validation reports, so a run that died before it claims no gates at
|
|
563
|
+
// all rather than inventing names for gates that were never registered.
|
|
564
|
+
let observedGates = null;
|
|
496
565
|
const setPhase = (next) => {
|
|
497
566
|
phase = next;
|
|
498
567
|
};
|
|
568
|
+
const setObservedGates = (gates) => {
|
|
569
|
+
observedGates = gates;
|
|
570
|
+
};
|
|
499
571
|
try {
|
|
500
572
|
return await runClosePipeline({
|
|
501
573
|
options,
|
|
502
574
|
setPhase,
|
|
575
|
+
setObservedGates,
|
|
503
576
|
injectedProvider,
|
|
504
577
|
injectedConfig,
|
|
505
578
|
injectedNotify,
|
|
@@ -510,8 +583,9 @@ export async function runSingleStoryClose({
|
|
|
510
583
|
injectedReleaseLease,
|
|
511
584
|
});
|
|
512
585
|
} catch (err) {
|
|
513
|
-
if (err && typeof err === 'object'
|
|
514
|
-
err.closePhase = phase;
|
|
586
|
+
if (err && typeof err === 'object') {
|
|
587
|
+
if (!err.closePhase) err.closePhase = phase;
|
|
588
|
+
if (!err.closeGates && observedGates) err.closeGates = observedGates;
|
|
515
589
|
}
|
|
516
590
|
throw err;
|
|
517
591
|
}
|
|
@@ -596,6 +670,7 @@ async function finishWithMergeWait(prCtx, deps) {
|
|
|
596
670
|
config: prCtx.config,
|
|
597
671
|
maxWaitSeconds: deps.maxWaitSeconds,
|
|
598
672
|
mergeWatchMode: deps.mergeWatchMode,
|
|
673
|
+
rerunAdvisory: deps.rerunAdvisory,
|
|
599
674
|
progress,
|
|
600
675
|
injectedGh: deps.injectedGh,
|
|
601
676
|
injectedNotify: deps.injectedNotify,
|
|
@@ -628,7 +703,15 @@ async function finishWithMergeWait(prCtx, deps) {
|
|
|
628
703
|
localCleanupDeferred: prCtx.localCleanupDeferred,
|
|
629
704
|
directMerged: prCtx.directMerged,
|
|
630
705
|
waitedForMerge: true,
|
|
631
|
-
|
|
706
|
+
// Story #5279 — `confirmed` alone denied the merge behind a
|
|
707
|
+
// `merged-flip-failed` block, whose own envelope (above) reports
|
|
708
|
+
// `pr.state: MERGED` off the probe.
|
|
709
|
+
merged: deriveObservedMerge({
|
|
710
|
+
waitOutcome,
|
|
711
|
+
directMerged: prCtx.directMerged,
|
|
712
|
+
}),
|
|
713
|
+
// Only the confirmed ending runs the flip, the issue close and the tail.
|
|
714
|
+
landCompleted: waitOutcome.confirmed === true,
|
|
632
715
|
});
|
|
633
716
|
await emitTerminal({ terminal, result, config: prCtx.config });
|
|
634
717
|
reportWaitTerminal(terminal, { storyId: prCtx.storyId, prUrl: prCtx.prUrl });
|
|
@@ -646,6 +729,11 @@ async function finishWithMergeWait(prCtx, deps) {
|
|
|
646
729
|
* @returns {Promise<{ success: boolean, result: object, terminal: object }>}
|
|
647
730
|
*/
|
|
648
731
|
async function finishWithoutMergeWait(prCtx, waitForMergeReason) {
|
|
732
|
+
// Story #5279 — a repository with no native auto-merge is direct
|
|
733
|
+
// squash-merged synchronously by the arm phase, so this ending can and does
|
|
734
|
+
// hold an OBSERVED merge. It used to report `merged: false` and hard-code
|
|
735
|
+
// `pr.state: 'OPEN'` for a PR the same run had just merged.
|
|
736
|
+
const merged = deriveObservedMerge({ directMerged: prCtx.directMerged });
|
|
649
737
|
const result = closeResult({
|
|
650
738
|
storyId: prCtx.storyId,
|
|
651
739
|
storyBranch: prCtx.storyBranch,
|
|
@@ -661,6 +749,10 @@ async function finishWithoutMergeWait(prCtx, waitForMergeReason) {
|
|
|
661
749
|
leaseReleased: false,
|
|
662
750
|
localCleanupDeferred: prCtx.localCleanupDeferred,
|
|
663
751
|
directMerged: prCtx.directMerged,
|
|
752
|
+
merged,
|
|
753
|
+
// Whatever the merge state, this ending never flips `agent::done`, never
|
|
754
|
+
// closes the issue and never runs the tail — `nextCommand` does.
|
|
755
|
+
landCompleted: false,
|
|
664
756
|
});
|
|
665
757
|
const terminal = buildTerminalEnvelope({
|
|
666
758
|
storyId: prCtx.storyId,
|
|
@@ -671,7 +763,7 @@ async function finishWithoutMergeWait(prCtx, waitForMergeReason) {
|
|
|
671
763
|
pr: {
|
|
672
764
|
number: prCtx.prNumber,
|
|
673
765
|
url: prCtx.prUrl ?? null,
|
|
674
|
-
state: 'OPEN',
|
|
766
|
+
state: merged ? 'MERGED' : 'OPEN',
|
|
675
767
|
autoMergeEnabled: Boolean(prCtx.autoMergeEnabled),
|
|
676
768
|
},
|
|
677
769
|
gates: prCtx.gates,
|
|
@@ -731,6 +823,7 @@ function reportOperatorMergeSkip({
|
|
|
731
823
|
async function runClosePipeline({
|
|
732
824
|
options,
|
|
733
825
|
setPhase,
|
|
826
|
+
setObservedGates,
|
|
734
827
|
injectedProvider,
|
|
735
828
|
injectedConfig,
|
|
736
829
|
injectedNotify,
|
|
@@ -805,6 +898,7 @@ async function runClosePipeline({
|
|
|
805
898
|
injectedSync,
|
|
806
899
|
injectedGitSpawn,
|
|
807
900
|
setPhase,
|
|
901
|
+
setObservedGates,
|
|
808
902
|
}),
|
|
809
903
|
leaseArgs,
|
|
810
904
|
);
|
|
@@ -947,6 +1041,7 @@ async function runClosePipeline({
|
|
|
947
1041
|
provider,
|
|
948
1042
|
maxWaitSeconds: options.maxWaitSeconds,
|
|
949
1043
|
mergeWatchMode: options.mergeWatchMode,
|
|
1044
|
+
rerunAdvisory: options.rerunAdvisory,
|
|
950
1045
|
setPhase,
|
|
951
1046
|
injectedGh,
|
|
952
1047
|
injectedNotify,
|
|
@@ -64,7 +64,11 @@ import {
|
|
|
64
64
|
import { getQuality } from '../../config-resolver.js';
|
|
65
65
|
import { gitSync as defaultGitSync } from '../../git-utils.js';
|
|
66
66
|
import { Logger as DefaultLogger } from '../../Logger.js';
|
|
67
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
currentBranch,
|
|
69
|
+
listChangedFiles,
|
|
70
|
+
listDirtyPaths,
|
|
71
|
+
} from './format-autofix.js';
|
|
68
72
|
|
|
69
73
|
const TAG = '[baseline-writeback]';
|
|
70
74
|
|
|
@@ -92,6 +96,7 @@ const GUARD_REASONS = new Set([
|
|
|
92
96
|
'gate-disabled',
|
|
93
97
|
'no-changed-files',
|
|
94
98
|
'wrong-branch',
|
|
99
|
+
'dirty-tree',
|
|
95
100
|
'no-baseline',
|
|
96
101
|
'no-scorer',
|
|
97
102
|
]);
|
|
@@ -267,7 +272,14 @@ function commitBaseline({ cwd, git, relPath, subject, body }) {
|
|
|
267
272
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
268
273
|
});
|
|
269
274
|
} catch (err) {
|
|
270
|
-
git
|
|
275
|
+
// `git checkout -- <path>` restores the WORKTREE from the index — and the
|
|
276
|
+
// index is exactly what the `git add` above just overwrote, so on a
|
|
277
|
+
// rejected commit it restored the file to the value it was meant to be
|
|
278
|
+
// rolled back FROM. The rollback was a no-op that looked like one, and the
|
|
279
|
+
// rewritten row survived as a staged edit into whatever the gates scored
|
|
280
|
+
// next. `restore --staged --worktree` resets both to HEAD, which is what
|
|
281
|
+
// "leave the tree as the close found it" actually means.
|
|
282
|
+
git(['restore', '--staged', '--worktree', '--', relPath], {
|
|
271
283
|
cwd,
|
|
272
284
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
273
285
|
});
|
|
@@ -289,24 +301,84 @@ function commitBaseline({ cwd, git, relPath, subject, body }) {
|
|
|
289
301
|
* so a mis-wired `worktreePath` can never leave a modified baseline in a tree
|
|
290
302
|
* whose history we then refuse to touch.
|
|
291
303
|
*
|
|
292
|
-
*
|
|
304
|
+
* The dirty-tree guard is the same refusal `runScopedFormatAutofix` makes and
|
|
305
|
+
* for the same reason, sharpened to one path: this step's only write target is
|
|
306
|
+
* the baseline file, and it commits that file by name. An uncommitted edit
|
|
307
|
+
* sitting on it — a hand-run `maintainability:reanchor`, a half-resolved merge,
|
|
308
|
+
* an operator mid-edit — would be swept into a `baseline-refresh:` commit
|
|
309
|
+
* authored by close and attributed to rows this branch improved. That commit is
|
|
310
|
+
* the one `refresh-ack.js` VOUCHES for, so an absorbed edit is not merely
|
|
311
|
+
* unrelated: it arrives pre-acknowledged, which is the precise laundering this
|
|
312
|
+
* whole Story exists to close.
|
|
313
|
+
*
|
|
314
|
+
* @param {{ gate: object|undefined, workTree: string, storyBranch: string, git: Function, changed: string[], relPath: string }} ctx
|
|
293
315
|
* @returns {string|null}
|
|
294
316
|
*/
|
|
295
|
-
function precheck({ gate, workTree, storyBranch, git, changed }) {
|
|
317
|
+
function precheck({ gate, workTree, storyBranch, git, changed, relPath }) {
|
|
296
318
|
if (gate?.enabled === false) return 'gate-disabled';
|
|
297
319
|
if (changed.length === 0) return 'no-changed-files';
|
|
298
320
|
const onBranch = currentBranch(workTree, git);
|
|
299
321
|
if (onBranch !== storyBranch) return 'wrong-branch';
|
|
322
|
+
if (isDirty({ workTree, git, relPath })) return 'dirty-tree';
|
|
300
323
|
return null;
|
|
301
324
|
}
|
|
302
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Is the baseline file already modified in the worktree or the index?
|
|
328
|
+
*
|
|
329
|
+
* Fails CLOSED on an unreadable status: a step that cannot tell whether it is
|
|
330
|
+
* about to absorb someone else's edit must not proceed, and skipping costs
|
|
331
|
+
* only a stale upward row that the nightly full-scope re-score still catches.
|
|
332
|
+
*
|
|
333
|
+
* @param {{ workTree: string, git: Function, relPath: string }} ctx
|
|
334
|
+
* @returns {boolean}
|
|
335
|
+
*/
|
|
336
|
+
function isDirty({ workTree, git, relPath }) {
|
|
337
|
+
try {
|
|
338
|
+
return listDirtyPaths(workTree, git).includes(relPath);
|
|
339
|
+
} catch {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The ref the branch's changed-file set is measured against.
|
|
346
|
+
*
|
|
347
|
+
* `origin/<baseBranch>` when the remote-tracking ref exists, the local branch
|
|
348
|
+
* otherwise. A local `main` in a long-lived checkout — and in every Story
|
|
349
|
+
* worktree, which is seeded once and never pulled again — drifts behind the
|
|
350
|
+
* remote, and a stale base widens the three-dot range to include commits that
|
|
351
|
+
* landed on the base after the branch forked. Every file in that widening is
|
|
352
|
+
* then scored and written back by whichever Story happens to close next, which
|
|
353
|
+
* is precisely the "absorb unrelated drift into the next PR" failure
|
|
354
|
+
* constraint 3 in the preamble forbids.
|
|
355
|
+
*
|
|
356
|
+
* @param {{ workTree: string, git: Function, baseBranch: string }} ctx
|
|
357
|
+
* @returns {string}
|
|
358
|
+
*/
|
|
359
|
+
function resolveScopeBase({ workTree, git, baseBranch }) {
|
|
360
|
+
try {
|
|
361
|
+
git(
|
|
362
|
+
['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${baseBranch}`],
|
|
363
|
+
{
|
|
364
|
+
cwd: workTree,
|
|
365
|
+
encoding: 'utf8',
|
|
366
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
return `origin/${baseBranch}`;
|
|
370
|
+
} catch {
|
|
371
|
+
return baseBranch;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
303
375
|
/**
|
|
304
376
|
* Persist improved maintainability rows for the files this branch changed, and
|
|
305
377
|
* fold them into one `baseline-refresh:` commit on the Story branch.
|
|
306
378
|
*
|
|
307
379
|
* Every no-op is reported by name rather than silently: `gate-disabled`,
|
|
308
|
-
* `no-changed-files`, `wrong-branch`, `
|
|
309
|
-
* `no-improvements`, `unchanged`. The caller logs the reason and proceeds —
|
|
380
|
+
* `no-changed-files`, `wrong-branch`, `dirty-tree`, `no-baseline`,
|
|
381
|
+
* `no-scored-rows`, `no-improvements`, `unchanged`. The caller logs the reason and proceeds —
|
|
310
382
|
* this step is never allowed to fail a close, because `check-baselines` is
|
|
311
383
|
* still the gate and this is only the refresh half of the loop.
|
|
312
384
|
*
|
|
@@ -364,12 +436,22 @@ export async function runBaselineUpwardWriteback({
|
|
|
364
436
|
|
|
365
437
|
const changed = listChangedFiles({
|
|
366
438
|
cwd: workTree,
|
|
367
|
-
baseBranch,
|
|
439
|
+
baseBranch: resolveScopeBase({ workTree, git, baseBranch }),
|
|
368
440
|
storyBranch,
|
|
369
441
|
git,
|
|
370
442
|
}).filter((file) => SCORABLE.test(file));
|
|
371
443
|
|
|
372
|
-
const
|
|
444
|
+
const writePath = resolveWritePath({ cwd: workTree });
|
|
445
|
+
const relPath = path.relative(workTree, writePath).split(path.sep).join('/');
|
|
446
|
+
|
|
447
|
+
const blocked = precheck({
|
|
448
|
+
gate,
|
|
449
|
+
workTree,
|
|
450
|
+
storyBranch,
|
|
451
|
+
git,
|
|
452
|
+
changed,
|
|
453
|
+
relPath,
|
|
454
|
+
});
|
|
373
455
|
if (blocked) return skip(logger, blocked);
|
|
374
456
|
|
|
375
457
|
const baselineRows = loadBaselineRows({ cwd: workTree });
|
|
@@ -399,7 +481,8 @@ export async function runBaselineUpwardWriteback({
|
|
|
399
481
|
storyId,
|
|
400
482
|
logger,
|
|
401
483
|
refreshBaseline,
|
|
402
|
-
|
|
484
|
+
writePath,
|
|
485
|
+
relPath,
|
|
403
486
|
});
|
|
404
487
|
}
|
|
405
488
|
|
|
@@ -420,10 +503,10 @@ async function persist({
|
|
|
420
503
|
storyId,
|
|
421
504
|
logger,
|
|
422
505
|
refreshBaseline,
|
|
423
|
-
|
|
506
|
+
writePath,
|
|
507
|
+
relPath,
|
|
424
508
|
}) {
|
|
425
509
|
const improvedPaths = improved.map((row) => row.path);
|
|
426
|
-
const writePath = resolveWritePath({ cwd: workTree });
|
|
427
510
|
const { wrote } = await refreshBaseline({
|
|
428
511
|
kind: KIND,
|
|
429
512
|
cwd: workTree,
|
|
@@ -436,7 +519,6 @@ async function persist({
|
|
|
436
519
|
// nothing worth a log line above debug.
|
|
437
520
|
if (!wrote) return skip(logger, 'unchanged');
|
|
438
521
|
|
|
439
|
-
const relPath = path.relative(workTree, writePath).split(path.sep).join('/');
|
|
440
522
|
const { sha } = commitBaseline({
|
|
441
523
|
cwd: workTree,
|
|
442
524
|
git,
|
|
@@ -59,11 +59,16 @@ export const FORMAT_AUTOFIX_TIMEOUT_EXIT_CODE = 124;
|
|
|
59
59
|
* (e.g. ` M file` for unstaged-modified) so we slice a fixed 3 chars off
|
|
60
60
|
* the front rather than trimming.
|
|
61
61
|
*
|
|
62
|
+
* Exported since Story #5277: `baseline-upward-writeback.js` needs the same
|
|
63
|
+
* "is this path already dirty?" test before it rewrites a baseline row, and a
|
|
64
|
+
* second porcelain parser is exactly the near-duplicate the duplication gate
|
|
65
|
+
* exists to refuse.
|
|
66
|
+
*
|
|
62
67
|
* @param {string} cwd
|
|
63
68
|
* @param {(args: string[], opts: object) => string} git
|
|
64
69
|
* @returns {string[]}
|
|
65
70
|
*/
|
|
66
|
-
function listDirtyPaths(cwd, git) {
|
|
71
|
+
export function listDirtyPaths(cwd, git) {
|
|
67
72
|
const out = git(['status', '--porcelain'], {
|
|
68
73
|
cwd,
|
|
69
74
|
encoding: 'utf8',
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import { signalsFile } from '../config/temp-paths.js';
|
|
13
13
|
import { graduateRetroProposals } from '../feedback-loop/retro-proposals-graduator.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_FRAMEWORK_REPO,
|
|
16
|
+
formatRepoSlug,
|
|
17
|
+
resolveOwnershipRepos,
|
|
18
|
+
} from '../github/framework-repo.js';
|
|
15
19
|
import { Logger } from '../Logger.js';
|
|
16
20
|
import { normalizeGatheredSignal } from '../observability/runtime-friction.js';
|
|
17
21
|
import {
|
|
@@ -54,28 +58,36 @@ function resolveFrictionWindowDays(config) {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/**
|
|
61
|
+
* Resolve the follow-up ownership buckets for the retro composer and the
|
|
62
|
+
* graduator walk.
|
|
63
|
+
*
|
|
64
|
+
* Routing itself lives in `github/framework-repo.js` — this is the slug-shaped
|
|
65
|
+
* adapter the composer wants (`composeRoutedProposals` renders slugs in prose)
|
|
66
|
+
* plus the `{owner, repo}` map `graduate()` routes on. The `platform` bucket
|
|
67
|
+
* has **no default**: an unconfigured shared-infra repo stays `null` so a
|
|
68
|
+
* caller reports it rather than filing platform-owned work somewhere plausible.
|
|
69
|
+
*
|
|
57
70
|
* @param {object} [config]
|
|
58
|
-
* @returns {{ frameworkRepo: string, consumerRepo: string, currentRepo: { owner: string, repo: string } }}
|
|
71
|
+
* @returns {{ frameworkRepo: string, consumerRepo: string, platform: (string|null), currentRepo: { owner: string, repo: string }, repos: { consumer: object|null, framework: object|null, platform: object|null } }}
|
|
59
72
|
*/
|
|
60
73
|
export function resolveFollowUpRepos(config) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const frameworkRepo =
|
|
68
|
-
typeof config?.github?.frameworkRepo === 'string' &&
|
|
69
|
-
config.github.frameworkRepo.trim()
|
|
70
|
-
? config.github.frameworkRepo.trim()
|
|
71
|
-
: DEFAULT_FRAMEWORK_REPO;
|
|
74
|
+
const repos = resolveOwnershipRepos(config);
|
|
75
|
+
const consumerRepo = formatRepoSlug(repos.consumer) ?? DEFAULT_FRAMEWORK_REPO;
|
|
76
|
+
// `resolveOwnershipRepos` always resolves the framework bucket (it defaults
|
|
77
|
+
// to the mirror constant), and both slugs above are well-formed by
|
|
78
|
+
// construction — so neither the render nor the split can yield a blank half.
|
|
79
|
+
const frameworkRepo = formatRepoSlug(repos.framework);
|
|
72
80
|
const [cOwner, cRepo] = consumerRepo.split('/');
|
|
81
|
+
const currentRepo = { owner: cOwner, repo: cRepo };
|
|
73
82
|
return {
|
|
74
83
|
frameworkRepo,
|
|
75
84
|
consumerRepo,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
platform: formatRepoSlug(repos.platform),
|
|
86
|
+
currentRepo,
|
|
87
|
+
repos: {
|
|
88
|
+
consumer: currentRepo,
|
|
89
|
+
framework: repos.framework,
|
|
90
|
+
platform: repos.platform,
|
|
79
91
|
},
|
|
80
92
|
};
|
|
81
93
|
}
|
|
@@ -749,10 +761,10 @@ export async function captureStoryFollowUps({
|
|
|
749
761
|
provider,
|
|
750
762
|
config,
|
|
751
763
|
currentRepo: repos.currentRepo,
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
764
|
+
// The resolved bucket object, not a re-split of the slug: routing is
|
|
765
|
+
// decided once in `github/framework-repo.js`.
|
|
766
|
+
frameworkRepo: repos.repos.framework,
|
|
767
|
+
platformRepo: repos.repos.platform,
|
|
756
768
|
routedProposals: proposals,
|
|
757
769
|
cwd,
|
|
758
770
|
});
|
|
@@ -601,19 +601,28 @@ function assertAcyclic(slugAdjacency) {
|
|
|
601
601
|
}
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
function attachFindingsAndErrors(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
604
|
+
function attachFindingsAndErrors(
|
|
605
|
+
tickets,
|
|
606
|
+
findings,
|
|
607
|
+
errors,
|
|
608
|
+
normalizations = [],
|
|
609
|
+
) {
|
|
610
|
+
for (const [key, value] of [
|
|
611
|
+
['findings', findings],
|
|
612
|
+
['errors', errors],
|
|
613
|
+
// Story #5265: the auto-normalizations the assumption gate applied. They
|
|
614
|
+
// used to end at a `Logger.warn` and die with the process, so persist's
|
|
615
|
+
// emitted result reported a plan whose declarations it had silently
|
|
616
|
+
// rewritten as if nothing had been rewritten.
|
|
617
|
+
['normalizations', normalizations],
|
|
618
|
+
]) {
|
|
619
|
+
Object.defineProperty(tickets, key, {
|
|
620
|
+
value,
|
|
621
|
+
enumerable: false,
|
|
622
|
+
configurable: true,
|
|
623
|
+
writable: true,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
617
626
|
}
|
|
618
627
|
|
|
619
628
|
export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
@@ -671,6 +680,7 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
671
680
|
// unit tests keep their semantics; production call-sites always pass
|
|
672
681
|
// it.
|
|
673
682
|
let assumptionErrors = [];
|
|
683
|
+
let assumptionNormalizations = [];
|
|
674
684
|
if (opts.baseBranchRef) {
|
|
675
685
|
const assumptionReport = validateStoryFileAssumptions({
|
|
676
686
|
tickets,
|
|
@@ -699,6 +709,7 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
699
709
|
);
|
|
700
710
|
}
|
|
701
711
|
assumptionErrors = assumptionReport.errors;
|
|
712
|
+
assumptionNormalizations = assumptionReport.normalizations ?? [];
|
|
702
713
|
}
|
|
703
714
|
|
|
704
715
|
const sizingFindings = computeSizingFindings({
|
|
@@ -741,7 +752,7 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
741
752
|
errors.push(`File assumption mismatch: ${e}`);
|
|
742
753
|
}
|
|
743
754
|
|
|
744
|
-
attachFindingsAndErrors(tickets, findings, errors);
|
|
755
|
+
attachFindingsAndErrors(tickets, findings, errors, assumptionNormalizations);
|
|
745
756
|
return tickets;
|
|
746
757
|
}
|
|
747
758
|
|
|
@@ -469,6 +469,36 @@ export function deriveParentState(siblings) {
|
|
|
469
469
|
return null;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Did any of these children actually **land**?
|
|
474
|
+
*
|
|
475
|
+
* `deriveParentState` answers `agent::done` for a child set in which every
|
|
476
|
+
* child is done *or closed*, and it is right to: a closed child is finished
|
|
477
|
+
* work as far as the parent's lifecycle goes. But "finished" and "landed" are
|
|
478
|
+
* different claims, and the close path spends the difference. A cohort
|
|
479
|
+
* re-planned out of existence closes every child as superseded, carrying no
|
|
480
|
+
* `agent::done` and having merged nothing — and the container above it then
|
|
481
|
+
* closed as `completed`, over a log line claiming every child Story landed.
|
|
482
|
+
* Both the state reason and the sentence were false.
|
|
483
|
+
*
|
|
484
|
+
* So the two questions are asked separately: `deriveParentState` decides
|
|
485
|
+
* *whether* the parent is finished, this decides *how* it finished. A single
|
|
486
|
+
* landed child is enough — a container that delivered some of its work and
|
|
487
|
+
* superseded the rest completed, partially, and `not_planned` is reserved for
|
|
488
|
+
* the case where nothing was delivered at all.
|
|
489
|
+
*
|
|
490
|
+
* @param {Array<{ labels?: string[] }>} children
|
|
491
|
+
* @returns {boolean} True when at least one child carries `agent::done`.
|
|
492
|
+
*/
|
|
493
|
+
export function anyChildLanded(children) {
|
|
494
|
+
if (!Array.isArray(children)) return false;
|
|
495
|
+
return children.some((child) =>
|
|
496
|
+
(Array.isArray(child?.labels) ? child.labels : []).includes(
|
|
497
|
+
STATE_LABELS.DONE,
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
472
502
|
/**
|
|
473
503
|
* Parent-state cascade for non-terminal transitions. Story #2676.
|
|
474
504
|
*
|