mandrel 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +59 -28
- package/.agents/agents/acceptance-critic.md +20 -9
- package/.agents/agents/story-worker.md +45 -48
- package/.agents/audit-checklists/performance.md +1 -1
- package/.agents/docs/SDLC.md +60 -46
- package/.agents/docs/agentrc-reference.json +8 -13
- package/.agents/docs/configuration.md +33 -57
- package/.agents/docs/execution-reference.md +39 -10
- package/.agents/docs/quality-gates.md +17 -19
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +64 -79
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +10 -0
- package/.agents/rules/git-conventions-reference.md +42 -51
- package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
- package/.agents/schemas/agentrc.schema.json +35 -46
- package/.agents/schemas/audit-rules.json +59 -1
- package/.agents/schemas/audit-rules.schema.json +33 -1
- package/.agents/schemas/lifecycle/README.md +1 -2
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
- package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
- package/.agents/schemas/signal-event.schema.json +3 -3
- package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +24 -68
- package/.agents/scripts/agents-bootstrap-github.js +1 -1
- package/.agents/scripts/bootstrap.js +3 -3
- package/.agents/scripts/check-dead-exports.js +43 -104
- package/.agents/scripts/check-doc-links.js +2 -2
- package/.agents/scripts/check-lifecycle-lint.js +1 -1
- package/.agents/scripts/check-workflow-cli-lint.js +91 -0
- package/.agents/scripts/deliver-recover.js +122 -0
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/evidence-gate.js +20 -50
- package/.agents/scripts/generate-skills-index.js +17 -1
- package/.agents/scripts/generate-workflows-doc.js +4 -4
- package/.agents/scripts/lib/ITicketingProvider.js +1 -19
- package/.agents/scripts/lib/audit-suite/selector.js +323 -23
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
- package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
- package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
- package/.agents/scripts/lib/checks/index.js +1 -1
- package/.agents/scripts/lib/checks/loop-health.js +12 -11
- package/.agents/scripts/lib/checks/state.js +17 -248
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
- package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
- package/.agents/scripts/lib/cli-args.js +23 -2
- package/.agents/scripts/lib/close-validation/gates.js +13 -13
- package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
- package/.agents/scripts/lib/close-validation/runner.js +13 -21
- package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
- package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config/delivery-routing.js +7 -6
- package/.agents/scripts/lib/config/explain.js +10 -16
- package/.agents/scripts/lib/config/github.js +7 -5
- package/.agents/scripts/lib/config/limits.js +15 -25
- package/.agents/scripts/lib/config/quality.js +11 -14
- package/.agents/scripts/lib/config/runners.js +8 -21
- package/.agents/scripts/lib/config/temp-paths.js +18 -56
- package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
- package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
- package/.agents/scripts/lib/config-settings-schema.js +48 -22
- package/.agents/scripts/lib/dead-exports-knip.js +105 -0
- package/.agents/scripts/lib/dead-exports-mode.js +51 -0
- package/.agents/scripts/lib/duplicate-search.js +38 -7
- package/.agents/scripts/lib/findings/promote-finding.js +23 -14
- package/.agents/scripts/lib/format-generated-json.js +97 -0
- package/.agents/scripts/lib/framework-version.js +19 -189
- package/.agents/scripts/lib/gh-exec.js +8 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
- package/.agents/scripts/lib/git-utils.js +0 -14
- package/.agents/scripts/lib/json-utils.js +1 -2
- package/.agents/scripts/lib/label-constants.js +0 -15
- package/.agents/scripts/lib/label-taxonomy.js +1 -12
- package/.agents/scripts/lib/observability/active-story-env.js +42 -163
- package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
- package/.agents/scripts/lib/observability/signal-validator.js +4 -4
- package/.agents/scripts/lib/observability/signals-writer.js +6 -82
- package/.agents/scripts/lib/observability/source-classifier.js +2 -2
- package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
- package/.agents/scripts/lib/orchestration/change-set.js +103 -0
- package/.agents/scripts/lib/orchestration/code-review.js +70 -191
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
- package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
- package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
- package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
- package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
- package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
- package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
- package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
- package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
- package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
- package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
- package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
- package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
- package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
- package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
- package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
- package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
- package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
- package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
- package/.agents/scripts/lib/planning-corpus.js +12 -286
- package/.agents/scripts/lib/preflight-runner.js +2 -2
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
- package/.agents/scripts/lib/signals/index.js +4 -17
- package/.agents/scripts/lib/signals/read.js +35 -35
- package/.agents/scripts/lib/signals/schema.js +8 -11
- package/.agents/scripts/lib/signals/span-tree.js +7 -7
- package/.agents/scripts/lib/signals/write.js +0 -1
- package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
- package/.agents/scripts/lib/skills/parse-skill.js +16 -3
- package/.agents/scripts/lib/story-adjacency.js +8 -7
- package/.agents/scripts/lib/story-body/story-body.js +81 -13
- package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
- package/.agents/scripts/lib/test-env.js +14 -1
- package/.agents/scripts/lib/test-tiers.js +0 -3
- package/.agents/scripts/lib/ticket-body-sections.js +0 -14
- package/.agents/scripts/lib/validation-evidence.js +31 -59
- package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
- package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
- package/.agents/scripts/plan-context.js +38 -7
- package/.agents/scripts/plan-critics.js +203 -0
- package/.agents/scripts/plan-persist.js +145 -35
- package/.agents/scripts/plan-run-epilogue.js +83 -38
- package/.agents/scripts/post-structured-comment.js +0 -38
- package/.agents/scripts/pr-watch-with-update.js +43 -22
- package/.agents/scripts/providers/github/compose.js +0 -1
- package/.agents/scripts/providers/github/errors.js +0 -19
- package/.agents/scripts/providers/github/issues.js +1 -11
- package/.agents/scripts/providers/github/mappers.js +5 -0
- package/.agents/scripts/providers/github/sub-issues.js +0 -47
- package/.agents/scripts/providers/github/tickets.js +33 -153
- package/.agents/scripts/providers/github.js +17 -6
- package/.agents/scripts/quality-preview.js +13 -6
- package/.agents/scripts/resolve-stories.js +236 -0
- package/.agents/scripts/run-coverage.js +4 -1
- package/.agents/scripts/run-lint.js +2 -2
- package/.agents/scripts/run-verify.js +31 -2
- package/.agents/scripts/signals-view.js +9 -10
- package/.agents/scripts/single-story-close.js +173 -18
- package/.agents/scripts/single-story-confirm-merge.js +288 -15
- package/.agents/scripts/single-story-init.js +6 -10
- package/.agents/scripts/stories-wave-tick.js +380 -53
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/update-ticket-state.js +8 -50
- package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
- package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
- package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
- package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
- package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
- package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
- package/.agents/skills/core/scope-triage/SKILL.md +3 -0
- package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
- package/.agents/skills/core/security-and-hardening/reference.md +375 -0
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
- package/.agents/workflows/audit-architecture.md +3 -4
- package/.agents/workflows/audit-clean-code.md +4 -4
- package/.agents/workflows/audit-documentation.md +4 -5
- package/.agents/workflows/audit-lighthouse.md +8 -0
- package/.agents/workflows/audit-navigability.md +10 -0
- package/.agents/workflows/audit-performance.md +2 -3
- package/.agents/workflows/audit-quality.md +8 -9
- package/.agents/workflows/audit-security.md +1 -2
- package/.agents/workflows/audit-seo.md +10 -0
- package/.agents/workflows/audit-ux-ui.md +7 -0
- package/.agents/workflows/deliver.md +133 -45
- package/.agents/workflows/git-cleanup.md +2 -2
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
- package/.agents/workflows/helpers/code-review.md +14 -12
- package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
- package/.agents/workflows/helpers/deliver-story.md +209 -118
- package/.agents/workflows/helpers/parallel-tooling.md +2 -2
- package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
- package/.agents/workflows/plan.md +239 -19
- package/.agents/workflows/qa-assist.md +6 -6
- package/.agents/workflows/qa-explore.md +3 -3
- package/.agents/workflows/qa-run.md +1 -5
- package/bin/mandrel.js +12 -1
- package/docs/CHANGELOG.md +62 -0
- package/lib/cli/registry.js +262 -19
- package/lib/cli/sync-agents.js +157 -0
- package/lib/cli/sync-commands.js +115 -6
- package/lib/cli/sync.js +168 -6
- package/lib/cli/update.js +105 -8
- package/lib/cli/version-helpers.js +131 -0
- package/lib/migrations/README.md +7 -5
- package/lib/migrations/index.js +17 -9
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
- package/package.json +2 -2
- package/.agents/schemas/epic-perf-report.schema.json +0 -89
- package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
- package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
- package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
- package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
- package/.agents/schemas/risk-verdict.schema.json +0 -53
- package/.agents/schemas/story-perf-summary.schema.json +0 -73
- package/.agents/scripts/analyze-execution.js +0 -444
- package/.agents/scripts/check-prepush-recovery.js +0 -90
- package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
- package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
- package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
- package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
- package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
- package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
- package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
- package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
- package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
- package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
- package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
- package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
- package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
- package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
- package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
- package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
- package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
- package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
- package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
- package/.agents/scripts/resolve-plan-run.js +0 -117
- package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
|
@@ -1,36 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* phases/confirm-merge.js —
|
|
3
|
-
*
|
|
2
|
+
* phases/confirm-merge.js — the close-and-land merge wait (Story #4428,
|
|
3
|
+
* reworked into a resumable, checks-aware wait by Story #4543).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* merge
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* This is the **default terminal step for every run** — attended and
|
|
6
|
+
* headless alike — because `waitForMerge` defaults from
|
|
7
|
+
* `delivery.routing.closeAndLand` (`true`); `--no-wait-merge` is the opt-out,
|
|
8
|
+
* and a PR the operator deliberately left un-armed (`--no-auto-merge` /
|
|
9
|
+
* `autoMerge: "strict"`) resolves to no-wait and rests at `agent::closing`
|
|
10
|
+
* for the human.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* from `phases/options.js`): instead of returning at `agent::closing`, it
|
|
14
|
-
* polls the armed PR to merge confirmation — reusing the SAME
|
|
15
|
-
* `confirmStoryMerged` flip logic `single-story-confirm-merge.js` calls
|
|
16
|
-
* (Story #4428 AC4: exactly one merged/`agent::done` implementation) — on
|
|
17
|
-
* the `delivery.mergeWatch.intervalSeconds` / `maxBudgetSeconds` cadence
|
|
18
|
-
* (mirroring `MergeWatcher`'s poll/budget shape rather than forking it).
|
|
12
|
+
* ## The timing model (Story #4543 — the load-bearing design decision)
|
|
19
13
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
14
|
+
* The original wait polled a single budget: `maxBudgetSeconds`, one hour.
|
|
15
|
+
* The host caps a single tool invocation at ~10 minutes, and the close gates
|
|
16
|
+
* burn minutes of that before the wait even starts. So a close-and-land
|
|
17
|
+
* whose CI took longer than roughly eight minutes was **killed mid-poll**
|
|
18
|
+
* with no terminal path taken — no `merge.unlanded` event, no `agent::blocked`
|
|
19
|
+
* flip, the Story parked at `agent::closing`: precisely the strand the
|
|
20
|
+
* must-land contract exists to eliminate.
|
|
21
|
+
*
|
|
22
|
+
* The fix splits the two timing domains that were conflated:
|
|
23
|
+
*
|
|
24
|
+
* - **`maxWaitSeconds`** bounds THIS invocation (default 300s, comfortably
|
|
25
|
+
* inside the host ceiling). On expiry the wait returns
|
|
26
|
+
* `terminal: 'pending'` — **no label mutation, no `merge.unlanded`
|
|
27
|
+
* event** — and the caller surfaces a resumable terminal with its own
|
|
28
|
+
* exit code. Merely shrinking `maxBudgetSeconds` instead would have been
|
|
29
|
+
* wrong: that path conflates slow CI with a hard block, so most runs
|
|
30
|
+
* would have been misfiled as blocked.
|
|
31
|
+
* - **`maxBudgetSeconds`** bounds the CUMULATIVE wait, anchored at the
|
|
32
|
+
* PR's `createdAt` rather than this invocation's start, so resumes do not
|
|
33
|
+
* restart the clock. Exhausting it is the genuine give-up: classify,
|
|
34
|
+
* emit, block. `agent::blocked` stays reserved for hard blocks.
|
|
35
|
+
*
|
|
36
|
+
* Backgrounding is not a workaround here and does not need to be: an
|
|
37
|
+
* interrupted poll is stateless and re-entrant by construction.
|
|
38
|
+
*
|
|
39
|
+
* ## The wait is not weaker than the watch it displaced
|
|
40
|
+
*
|
|
41
|
+
* The pre-#4543 poll read only `state` / `mergedAt`. A check that went red
|
|
42
|
+
* at minute one therefore burned the full hour and then classified as
|
|
43
|
+
* `branch-protection-human-required` (the exhaustion probe sees
|
|
44
|
+
* `mergeStateStatus: BLOCKED` with checks settled) — sending the operator to
|
|
45
|
+
* diagnose branch protection instead of their red check. This wait probes the
|
|
46
|
+
* checks every iteration: it fails fast on `checks-failed`, and runs a
|
|
47
|
+
* bounded `gh pr update-branch` on a BEHIND PR instead of waiting out the
|
|
48
|
+
* budget behind a base it could have caught up to.
|
|
49
|
+
*
|
|
50
|
+
* The per-iteration `provider.getTicket` is also gone. It was re-fetched
|
|
51
|
+
* every poll for an idempotence check whose answer cannot change mid-poll —
|
|
52
|
+
* ~240 reads per Story per hour. The loop now probes the PR only, and calls
|
|
53
|
+
* the shared `confirmStoryMerged` exactly once, after a merge is observed.
|
|
31
54
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
55
|
+
* Terminal outcomes:
|
|
56
|
+
* - `{ confirmed: true, action, tail }` — the PR merged; `confirmStoryMerged`
|
|
57
|
+
* flipped `agent::closing → agent::done` and closed the issue, and the
|
|
58
|
+
* shared post-land tail ran.
|
|
59
|
+
* - `{ confirmed: false, terminal: 'pending', waitBudget }` — this
|
|
60
|
+
* invocation's bound expired with the PR still in flight. Resumable;
|
|
61
|
+
* nothing was mutated.
|
|
62
|
+
* - `{ confirmed: false, terminal: 'blocked', blockClass, reason }` — the
|
|
63
|
+
* arm failed outright, the PR closed without merging, a required check
|
|
64
|
+
* went red, or the cumulative budget was exhausted. Classified via the
|
|
65
|
+
* shared `classifyMergeBlock`, emitted as `merge.unlanded`, friction
|
|
66
|
+
* posted, Story transitioned to `agent::blocked`.
|
|
67
|
+
* - `{ confirmed: false, terminal: 'blocked', blockClass: 'merged-flip-failed' }`
|
|
68
|
+
* — the PR merged but the `agent::done` label write failed. Its own
|
|
69
|
+
* `merge.flip-failed` event and friction wording (Story #4539): the merge
|
|
70
|
+
* landed, so attributing it to an unlanded merge would send the operator
|
|
71
|
+
* to diagnose branch protection instead of re-running the idempotent
|
|
72
|
+
* confirm.
|
|
34
73
|
*/
|
|
35
74
|
|
|
36
75
|
import { gh as defaultGh } from '../../../gh-exec.js';
|
|
@@ -38,42 +77,84 @@ import {
|
|
|
38
77
|
confirmStoryMerged as defaultConfirmStoryMerged,
|
|
39
78
|
readPrMergeState as defaultReadPrMergeState,
|
|
40
79
|
} from '../../../single-story/confirm-merge.js';
|
|
80
|
+
import {
|
|
81
|
+
emitMergeFlipFailed as defaultEmitMergeFlipFailed,
|
|
82
|
+
MERGED_FLIP_FAILED_BLOCK_CLASS,
|
|
83
|
+
} from '../../lifecycle/emit-merge-flip-failed.js';
|
|
41
84
|
import { emitMergeUnlanded as defaultEmitMergeUnlanded } from '../../lifecycle/emit-merge-unlanded.js';
|
|
85
|
+
import { classifyMergeBlock as defaultClassifyMergeBlock } from '../../merge-block-class.js';
|
|
42
86
|
import {
|
|
43
87
|
DEFAULT_INTERVAL_SECONDS,
|
|
44
88
|
DEFAULT_MAX_BUDGET_SECONDS,
|
|
45
89
|
deriveChecksStatus,
|
|
46
|
-
|
|
47
|
-
|
|
90
|
+
failingChecksBlockMerge,
|
|
91
|
+
} from '../../merge-poll.js';
|
|
92
|
+
import { NEXT_COMMANDS } from '../../story-deliver-terminal.js';
|
|
48
93
|
import {
|
|
49
94
|
postStructuredComment,
|
|
50
95
|
STATE_LABELS,
|
|
51
96
|
transitionTicketState,
|
|
52
97
|
} from '../../ticketing.js';
|
|
98
|
+
import { runPostLandTail as defaultRunPostLandTail } from './post-land.js';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Per-invocation merge-wait bound. 300s fits inside a single host tool
|
|
102
|
+
* invocation (~10 min ceiling) with room for the close gates that precede
|
|
103
|
+
* the wait. A headless caller with no such ceiling raises
|
|
104
|
+
* `delivery.mergeWatch.maxWaitSeconds` to keep single-block semantics.
|
|
105
|
+
*/
|
|
106
|
+
export const DEFAULT_MAX_WAIT_SECONDS = 300;
|
|
107
|
+
|
|
108
|
+
/** Bounded `gh pr update-branch` attempts for a BEHIND PR. */
|
|
109
|
+
export const DEFAULT_UPDATE_ATTEMPTS = 3;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Minimum polls before the CUMULATIVE budget may block.
|
|
113
|
+
*
|
|
114
|
+
* The cumulative clock is anchored at the PR's `createdAt` so resumes do not
|
|
115
|
+
* restart it — but that alone means a PR older than `maxBudgetSeconds` (1h by
|
|
116
|
+
* default) is already over budget on its very first probe. Resuming a Story
|
|
117
|
+
* the next morning, or landing a long-open PR, would then flip
|
|
118
|
+
* `agent::blocked` and emit `merge.unlanded` against a perfectly healthy PR
|
|
119
|
+
* that was seconds from merging, without ever having waited.
|
|
120
|
+
*
|
|
121
|
+
* The floor gives every invocation at least one real poll cycle before the
|
|
122
|
+
* cumulative bound can fire. A genuinely stuck PR still blocks within one
|
|
123
|
+
* interval (~30s), so the give-up bound keeps its meaning; a PR about to go
|
|
124
|
+
* green gets the chance it earned.
|
|
125
|
+
*/
|
|
126
|
+
export const MIN_POLLS_BEFORE_BUDGET_BLOCK = 2;
|
|
53
127
|
|
|
54
128
|
function defaultSleep(ms) {
|
|
55
129
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
130
|
}
|
|
57
131
|
|
|
58
132
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* `
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
133
|
+
* One probe per poll iteration, carrying every field the loop and the
|
|
134
|
+
* terminal classifier need: merge state, the checks rollup, the merge-state
|
|
135
|
+
* status (for BEHIND recovery and human-required classification), and
|
|
136
|
+
* `createdAt` (the cumulative-budget anchor).
|
|
137
|
+
*
|
|
138
|
+
* Returns a degraded `{ checksStatus: 'pending', error }` probe when the read
|
|
139
|
+
* itself fails, preserving the conservative classification on probe errors —
|
|
140
|
+
* a flaky API read must not be mistaken for a definitive verdict.
|
|
141
|
+
*
|
|
142
|
+
* @returns {Promise<object>}
|
|
68
143
|
*/
|
|
69
|
-
async function
|
|
144
|
+
export async function readPrWaitProbe({ prNumber, gh = defaultGh }) {
|
|
70
145
|
try {
|
|
71
146
|
const view = await gh.pr.view(prNumber, [
|
|
147
|
+
'state',
|
|
148
|
+
'mergedAt',
|
|
149
|
+
'createdAt',
|
|
72
150
|
'mergeStateStatus',
|
|
73
151
|
'reviewDecision',
|
|
74
152
|
'statusCheckRollup',
|
|
75
153
|
]);
|
|
76
154
|
return {
|
|
155
|
+
state: typeof view?.state === 'string' ? view.state : null,
|
|
156
|
+
mergedAt: typeof view?.mergedAt === 'string' ? view.mergedAt : null,
|
|
157
|
+
createdAt: typeof view?.createdAt === 'string' ? view.createdAt : null,
|
|
77
158
|
mergeStateStatus:
|
|
78
159
|
typeof view?.mergeStateStatus === 'string'
|
|
79
160
|
? view.mergeStateStatus
|
|
@@ -86,37 +167,77 @@ async function readPrClassificationProbe({ prNumber, gh = defaultGh }) {
|
|
|
86
167
|
};
|
|
87
168
|
} catch (err) {
|
|
88
169
|
return {
|
|
170
|
+
state: null,
|
|
171
|
+
mergedAt: null,
|
|
172
|
+
createdAt: null,
|
|
89
173
|
checksStatus: 'pending',
|
|
90
|
-
error: `
|
|
174
|
+
error: `PR probe failed: ${err?.message ?? err}`,
|
|
91
175
|
};
|
|
92
176
|
}
|
|
93
177
|
}
|
|
94
178
|
|
|
95
179
|
/**
|
|
96
|
-
* Resolve the
|
|
97
|
-
* the
|
|
180
|
+
* Resolve the wait cadence and both budgets from `delivery.mergeWatch.*`,
|
|
181
|
+
* falling back to the framework defaults when a key is absent or invalid.
|
|
182
|
+
*
|
|
183
|
+
* `maxWaitSecondsOverride` is the per-run `--max-wait-seconds` flag and wins
|
|
184
|
+
* over the config: a headless caller with no host tool-invocation ceiling
|
|
185
|
+
* raises the per-invocation bound to keep single-block semantics without
|
|
186
|
+
* editing the consumer's config.
|
|
98
187
|
*
|
|
99
188
|
* @param {object} [config]
|
|
100
|
-
* @
|
|
189
|
+
* @param {number} [maxWaitSecondsOverride]
|
|
190
|
+
* @returns {{ intervalSeconds: number, maxWaitSeconds: number, maxBudgetSeconds: number, updateAttempts: number }}
|
|
101
191
|
*/
|
|
102
|
-
function
|
|
192
|
+
export function resolveMergeWaitConfig(config, maxWaitSecondsOverride) {
|
|
103
193
|
const mergeWatch = config?.delivery?.mergeWatch ?? {};
|
|
104
|
-
const
|
|
105
|
-
Number.isInteger(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
194
|
+
const int = (value, fallback, min = 1) =>
|
|
195
|
+
Number.isInteger(value) && value >= min ? value : fallback;
|
|
196
|
+
const maxWaitSeconds = int(
|
|
197
|
+
maxWaitSecondsOverride,
|
|
198
|
+
int(mergeWatch.maxWaitSeconds, DEFAULT_MAX_WAIT_SECONDS),
|
|
199
|
+
);
|
|
200
|
+
// A poll interval longer than the wait bound is incoherent, and silently
|
|
201
|
+
// harmful: the pending check would fire on poll 1 every time, so the wait
|
|
202
|
+
// could never sleep, `polls` could never reach
|
|
203
|
+
// MIN_POLLS_BEFORE_BUDGET_BLOCK, and the cumulative budget would become
|
|
204
|
+
// unreachable across ANY number of resumes — a Story stuck in permanent
|
|
205
|
+
// `pending` that never escalates. Clamping the interval to the bound keeps
|
|
206
|
+
// at least one real poll cycle possible, which is what both the floor and
|
|
207
|
+
// the give-up bound depend on.
|
|
208
|
+
const intervalSeconds = Math.min(
|
|
209
|
+
int(mergeWatch.intervalSeconds, DEFAULT_INTERVAL_SECONDS),
|
|
210
|
+
maxWaitSeconds,
|
|
211
|
+
);
|
|
212
|
+
return {
|
|
213
|
+
intervalSeconds,
|
|
214
|
+
maxWaitSeconds,
|
|
215
|
+
maxBudgetSeconds: int(
|
|
216
|
+
mergeWatch.maxBudgetSeconds,
|
|
217
|
+
DEFAULT_MAX_BUDGET_SECONDS,
|
|
218
|
+
),
|
|
219
|
+
updateAttempts: int(mergeWatch.updateAttempts, DEFAULT_UPDATE_ATTEMPTS, 0),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Anchor the cumulative budget at the PR's `createdAt` so a resumed wait
|
|
225
|
+
* does not restart the clock. Falls back to this invocation's start when the
|
|
226
|
+
* probe carried no timestamp — a conservative degrade: the worst case is a
|
|
227
|
+
* resume getting a fresh cumulative budget, which is exactly the pre-#4543
|
|
228
|
+
* behaviour, never a premature block.
|
|
229
|
+
*
|
|
230
|
+
* @returns {number} epoch ms
|
|
231
|
+
*/
|
|
232
|
+
export function resolveBudgetAnchorMs({ createdAt, fallbackMs }) {
|
|
233
|
+
if (typeof createdAt !== 'string' || !createdAt) return fallbackMs;
|
|
234
|
+
const parsed = Date.parse(createdAt);
|
|
235
|
+
return Number.isFinite(parsed) ? parsed : fallbackMs;
|
|
115
236
|
}
|
|
116
237
|
|
|
117
238
|
/**
|
|
118
239
|
* Format the `friction` comment body posted alongside the `agent::blocked`
|
|
119
|
-
* transition when a
|
|
240
|
+
* transition when a landing attempt gives up without a confirmed merge.
|
|
120
241
|
*/
|
|
121
242
|
function formatUnlandedFriction({
|
|
122
243
|
storyId,
|
|
@@ -130,26 +251,165 @@ function formatUnlandedFriction({
|
|
|
130
251
|
Number.isInteger(prNumber) && prNumber > 0
|
|
131
252
|
? `PR #${prNumber}${prUrl ? ` (${prUrl})` : ''}`
|
|
132
253
|
: (prUrl ?? 'the PR');
|
|
254
|
+
const remedy =
|
|
255
|
+
blockClass === 'checks-failed'
|
|
256
|
+
? `A required check is **red**. Fix the failure and push a new commit on \`story-${storyId}\`; ` +
|
|
257
|
+
`auto-merge stays armed across retries. Watch the checks with:\n\n` +
|
|
258
|
+
`\`\`\`bash\n${NEXT_COMMANDS.watchCi(storyId, prNumber)}\n\`\`\``
|
|
259
|
+
: `Resolve the underlying condition (branch protection, required checks, ` +
|
|
260
|
+
`or a manual merge), then resume the land:\n\n` +
|
|
261
|
+
`\`\`\`bash\n${NEXT_COMMANDS.resumeLand(storyId)}\n\`\`\``;
|
|
133
262
|
return (
|
|
134
|
-
`###
|
|
135
|
-
`Story #${storyId}: the
|
|
136
|
-
`
|
|
137
|
-
`confirmed merge.\n\n` +
|
|
263
|
+
`### close-and-land: merge did not land\n\n` +
|
|
264
|
+
`Story #${storyId}: the close polled ${prLabel} for merge confirmation and ` +
|
|
265
|
+
`gave up after ${elapsedSeconds}s without observing a confirmed merge.\n\n` +
|
|
138
266
|
`**Block class:** \`${blockClass}\`\n\n` +
|
|
139
267
|
`**Reason:** ${reason}\n\n` +
|
|
140
|
-
`Story transitioned to \`agent::blocked
|
|
141
|
-
`condition (branch protection, required checks, or a manual merge), ` +
|
|
142
|
-
`then re-run \`single-story-confirm-merge.js\` or resume delivery.`
|
|
268
|
+
`Story transitioned to \`agent::blocked\`.\n\n${remedy}`
|
|
143
269
|
);
|
|
144
270
|
}
|
|
145
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Format the `friction` comment for a merge that **landed** while the
|
|
274
|
+
* `agent::done` label write failed. Deliberately not the unlanded wording:
|
|
275
|
+
* the merge is not in question, so pointing the operator at branch
|
|
276
|
+
* protection and required checks would send them to diagnose a fault that
|
|
277
|
+
* does not exist. Name the actual remedy instead.
|
|
278
|
+
*/
|
|
279
|
+
function formatFlipFailedFriction({
|
|
280
|
+
storyId,
|
|
281
|
+
prNumber,
|
|
282
|
+
prUrl,
|
|
283
|
+
reason,
|
|
284
|
+
elapsedSeconds,
|
|
285
|
+
}) {
|
|
286
|
+
const prLabel =
|
|
287
|
+
Number.isInteger(prNumber) && prNumber > 0
|
|
288
|
+
? `PR #${prNumber}${prUrl ? ` (${prUrl})` : ''}`
|
|
289
|
+
: (prUrl ?? 'the PR');
|
|
290
|
+
return (
|
|
291
|
+
`### merge landed; the agent::done flip failed\n\n` +
|
|
292
|
+
`Story #${storyId}: ${prLabel} **merged successfully** after ${elapsedSeconds}s, ` +
|
|
293
|
+
`but the \`agent::closing\` → \`agent::done\` label write failed. The code is ` +
|
|
294
|
+
`on the base branch — this is a label-write fault, not a merge fault, so ` +
|
|
295
|
+
`there is nothing to diagnose about branch protection or required checks.\n\n` +
|
|
296
|
+
`**Block class:** \`${MERGED_FLIP_FAILED_BLOCK_CLASS}\`\n\n` +
|
|
297
|
+
`**Reason:** ${reason}\n\n` +
|
|
298
|
+
`Story transitioned to \`agent::blocked\` so the merged-but-mislabelled ` +
|
|
299
|
+
`state is explicit rather than silently resting at \`agent::closing\`.\n\n` +
|
|
300
|
+
`**Remedy:** re-run the merge confirmation — it is idempotent and flips ` +
|
|
301
|
+
`the label from the already-merged PR:\n\n` +
|
|
302
|
+
`\`\`\`bash\n${NEXT_COMMANDS.confirmMerge(storyId)}\n\`\`\``
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Post a friction comment best-effort and return its id when the provider
|
|
308
|
+
* surfaces one. The id is the terminal envelope's `frictionCommentId`
|
|
309
|
+
* pointer, so a caller can link the operator straight at the remediation
|
|
310
|
+
* instead of telling them to go find it.
|
|
311
|
+
*
|
|
312
|
+
* @returns {Promise<string|null>}
|
|
313
|
+
*/
|
|
314
|
+
async function postFriction({ provider, storyId, body, progress }) {
|
|
315
|
+
try {
|
|
316
|
+
const posted = await postStructuredComment(
|
|
317
|
+
provider,
|
|
318
|
+
storyId,
|
|
319
|
+
'friction',
|
|
320
|
+
body,
|
|
321
|
+
);
|
|
322
|
+
const id = posted?.id ?? posted?.commentId ?? null;
|
|
323
|
+
return id == null ? null : String(id);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
progress?.(
|
|
326
|
+
'CONFIRM',
|
|
327
|
+
`⚠️ Failed to post friction comment: ${err?.message ?? err}`,
|
|
328
|
+
);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Terminal for a confirmed merge whose `agent::done` flip failed. Emits
|
|
335
|
+
* `merge.flip-failed` (NOT `merge.unlanded` — the merge landed), posts the
|
|
336
|
+
* flip-failed friction, and blocks explicitly. Best-effort throughout: the
|
|
337
|
+
* caller owns the non-zero exit.
|
|
338
|
+
*/
|
|
339
|
+
async function blockOnFlipFailed({
|
|
340
|
+
storyId,
|
|
341
|
+
prNumber,
|
|
342
|
+
prUrl,
|
|
343
|
+
reason,
|
|
344
|
+
elapsedSeconds,
|
|
345
|
+
provider,
|
|
346
|
+
progress,
|
|
347
|
+
emitMergeFlipFailedFn,
|
|
348
|
+
prProbe,
|
|
349
|
+
}) {
|
|
350
|
+
if (Number.isInteger(prNumber) && prNumber > 0) {
|
|
351
|
+
try {
|
|
352
|
+
emitMergeFlipFailedFn({
|
|
353
|
+
scope: 'story',
|
|
354
|
+
ticketId: storyId,
|
|
355
|
+
prNumber,
|
|
356
|
+
reason,
|
|
357
|
+
elapsedSeconds,
|
|
358
|
+
});
|
|
359
|
+
} catch (err) {
|
|
360
|
+
progress?.(
|
|
361
|
+
'CONFIRM',
|
|
362
|
+
`⚠️ merge.flip-failed emit failed (continuing): ${err?.message ?? err}`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const frictionCommentId = await postFriction({
|
|
368
|
+
provider,
|
|
369
|
+
storyId,
|
|
370
|
+
body: formatFlipFailedFriction({
|
|
371
|
+
storyId,
|
|
372
|
+
prNumber,
|
|
373
|
+
prUrl,
|
|
374
|
+
reason,
|
|
375
|
+
elapsedSeconds,
|
|
376
|
+
}),
|
|
377
|
+
progress,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
await transitionTicketState(provider, storyId, STATE_LABELS.BLOCKED, {});
|
|
382
|
+
progress?.(
|
|
383
|
+
'CONFIRM',
|
|
384
|
+
`🛑 Story #${storyId} → agent::blocked (${MERGED_FLIP_FAILED_BLOCK_CLASS}) — merge landed, label flip failed.`,
|
|
385
|
+
);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
progress?.(
|
|
388
|
+
'CONFIRM',
|
|
389
|
+
`⚠️ Failed to flip Story #${storyId} to agent::blocked: ${err?.message ?? err}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
confirmed: false,
|
|
395
|
+
terminal: 'blocked',
|
|
396
|
+
blockClass: MERGED_FLIP_FAILED_BLOCK_CLASS,
|
|
397
|
+
reason,
|
|
398
|
+
frictionCommentId,
|
|
399
|
+
elapsedSeconds,
|
|
400
|
+
// The merge is CONFIRMED here — only the label write failed — so the
|
|
401
|
+
// envelope must say MERGED even when the probe that got us here was read
|
|
402
|
+
// before the merge landed. Reporting the stale OPEN (or null) would tell
|
|
403
|
+
// the operator to chase a merge that already happened.
|
|
404
|
+
prProbe: { ...(prProbe ?? {}), state: 'MERGED' },
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
146
408
|
/**
|
|
147
409
|
* Classify the unlanded merge, emit `merge.unlanded`, post a `friction`
|
|
148
410
|
* comment, and transition the Story to `agent::blocked`. Every side effect
|
|
149
|
-
* is best-effort logged rather than thrown — the caller
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* @returns {Promise<{ confirmed: false, blockClass: string, reason: string, elapsedSeconds: number }>}
|
|
411
|
+
* is best-effort logged rather than thrown — the caller owns surfacing the
|
|
412
|
+
* non-zero exit once this returns.
|
|
153
413
|
*/
|
|
154
414
|
async function blockOnUnlanded({
|
|
155
415
|
storyId,
|
|
@@ -193,46 +453,183 @@ async function blockOnUnlanded({
|
|
|
193
453
|
);
|
|
194
454
|
}
|
|
195
455
|
|
|
196
|
-
const
|
|
456
|
+
const frictionCommentId = await postFriction({
|
|
457
|
+
provider,
|
|
197
458
|
storyId,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
459
|
+
body: formatUnlandedFriction({
|
|
460
|
+
storyId,
|
|
461
|
+
prNumber,
|
|
462
|
+
prUrl,
|
|
463
|
+
blockClass,
|
|
464
|
+
reason,
|
|
465
|
+
elapsedSeconds,
|
|
466
|
+
}),
|
|
467
|
+
progress,
|
|
203
468
|
});
|
|
469
|
+
|
|
204
470
|
try {
|
|
205
|
-
await
|
|
471
|
+
await transitionTicketState(provider, storyId, STATE_LABELS.BLOCKED, {});
|
|
472
|
+
progress?.(
|
|
473
|
+
'CONFIRM',
|
|
474
|
+
`🛑 Story #${storyId} → agent::blocked (${blockClass}).`,
|
|
475
|
+
);
|
|
206
476
|
} catch (err) {
|
|
207
477
|
progress?.(
|
|
208
478
|
'CONFIRM',
|
|
209
|
-
`⚠️ Failed to
|
|
479
|
+
`⚠️ Failed to flip Story #${storyId} to agent::blocked: ${err?.message ?? err}`,
|
|
210
480
|
);
|
|
211
481
|
}
|
|
212
482
|
|
|
483
|
+
return {
|
|
484
|
+
confirmed: false,
|
|
485
|
+
terminal: 'blocked',
|
|
486
|
+
blockClass,
|
|
487
|
+
reason,
|
|
488
|
+
frictionCommentId,
|
|
489
|
+
elapsedSeconds,
|
|
490
|
+
// The probe the classifier just read. The terminal envelope reports
|
|
491
|
+
// `pr.state` / `pr.checksStatus` from here; dropping it made every
|
|
492
|
+
// blocked envelope claim `null` for facts we had just observed — a
|
|
493
|
+
// `checks-failed` envelope reporting `checksStatus: null` contradicts
|
|
494
|
+
// itself. Schema wants "live PR facts as observed at terminal time".
|
|
495
|
+
prProbe,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Bring a BEHIND PR up to date, bounded by `updateAttempts`. Best-effort:
|
|
501
|
+
* a failed update is not itself a terminal — the next poll re-reads the
|
|
502
|
+
* real state and lets the normal classification decide.
|
|
503
|
+
*
|
|
504
|
+
* @returns {Promise<boolean>} whether an update was actually attempted.
|
|
505
|
+
*/
|
|
506
|
+
async function maybeUpdateBehindPr({
|
|
507
|
+
probe,
|
|
508
|
+
prNumber,
|
|
509
|
+
updatesUsed,
|
|
510
|
+
updateAttempts,
|
|
511
|
+
gh,
|
|
512
|
+
progress,
|
|
513
|
+
}) {
|
|
514
|
+
if (probe.mergeStateStatus !== 'BEHIND') return false;
|
|
515
|
+
if (updatesUsed >= updateAttempts) {
|
|
516
|
+
progress?.(
|
|
517
|
+
'CONFIRM',
|
|
518
|
+
`⚠️ PR #${prNumber} is BEHIND but the update budget (${updateAttempts}) is spent — not updating again.`,
|
|
519
|
+
);
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
213
522
|
try {
|
|
214
|
-
await
|
|
523
|
+
await (gh ?? defaultGh).pr.updateBranch(prNumber);
|
|
215
524
|
progress?.(
|
|
216
525
|
'CONFIRM',
|
|
217
|
-
|
|
526
|
+
`⏫ PR #${prNumber} was BEHIND its base — updated (attempt ${updatesUsed + 1}/${updateAttempts}).`,
|
|
218
527
|
);
|
|
219
528
|
} catch (err) {
|
|
220
529
|
progress?.(
|
|
221
530
|
'CONFIRM',
|
|
222
|
-
`⚠️
|
|
531
|
+
`⚠️ gh pr update-branch failed (continuing): ${err?.message ?? err}`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Handle an observed merge: run the shared `confirmStoryMerged` flip, then
|
|
539
|
+
* the shared post-land tail. Called at most once per wait — the loop probes
|
|
540
|
+
* the PR, not the ticket.
|
|
541
|
+
*/
|
|
542
|
+
async function onMergeObserved({
|
|
543
|
+
storyId,
|
|
544
|
+
storyBranch,
|
|
545
|
+
baseBranch,
|
|
546
|
+
prNumber,
|
|
547
|
+
prUrl,
|
|
548
|
+
cwd,
|
|
549
|
+
config,
|
|
550
|
+
provider,
|
|
551
|
+
progress,
|
|
552
|
+
injectedGh,
|
|
553
|
+
injectedNotify,
|
|
554
|
+
readPrMergeStateFn,
|
|
555
|
+
confirmStoryMergedFn,
|
|
556
|
+
runPostLandTailFn,
|
|
557
|
+
emitMergeFlipFailedFn,
|
|
558
|
+
prProbe,
|
|
559
|
+
elapsedSeconds,
|
|
560
|
+
}) {
|
|
561
|
+
const confirmation = await confirmStoryMergedFn({
|
|
562
|
+
provider,
|
|
563
|
+
storyId,
|
|
564
|
+
prNumber,
|
|
565
|
+
prUrl,
|
|
566
|
+
cwd,
|
|
567
|
+
config,
|
|
568
|
+
progress,
|
|
569
|
+
injectedGh,
|
|
570
|
+
injectedNotify,
|
|
571
|
+
readPrMergeStateFn,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
if (confirmation.merged && confirmation.action === 'flip-failed') {
|
|
575
|
+
// The PR merged but the agent::closing → agent::done label flip itself
|
|
576
|
+
// threw. Blocking explicitly is right — reporting confirmed:true would
|
|
577
|
+
// strand the Story at agent::closing with no notification. Reporting it
|
|
578
|
+
// as UNLANDED was not (Story #4539): the merge landed, so the
|
|
579
|
+
// merge.unlanded event would be false and its friction would send the
|
|
580
|
+
// operator to branch protection instead of the one-line remedy.
|
|
581
|
+
progress?.(
|
|
582
|
+
'CONFIRM',
|
|
583
|
+
`⚠️ Story #${storyId} merge confirmed but the agent::done flip failed — blocking explicitly.`,
|
|
223
584
|
);
|
|
585
|
+
return blockOnFlipFailed({
|
|
586
|
+
storyId,
|
|
587
|
+
prNumber,
|
|
588
|
+
prUrl,
|
|
589
|
+
reason:
|
|
590
|
+
confirmation.reason ??
|
|
591
|
+
'merge confirmed but the agent::done label write failed',
|
|
592
|
+
elapsedSeconds,
|
|
593
|
+
provider,
|
|
594
|
+
progress,
|
|
595
|
+
emitMergeFlipFailedFn,
|
|
596
|
+
prProbe,
|
|
597
|
+
});
|
|
224
598
|
}
|
|
225
599
|
|
|
226
|
-
|
|
600
|
+
progress?.('CONFIRM', `✅ Story #${storyId} merge confirmed — agent::done.`);
|
|
601
|
+
const tail = await runPostLandTailFn({
|
|
602
|
+
storyId,
|
|
603
|
+
storyBranch,
|
|
604
|
+
baseBranch,
|
|
605
|
+
cwd,
|
|
606
|
+
provider,
|
|
607
|
+
config,
|
|
608
|
+
progress,
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
confirmed: true,
|
|
612
|
+
terminal: 'landed',
|
|
613
|
+
action: confirmation.action,
|
|
614
|
+
tail,
|
|
615
|
+
// Carry the OBSERVED rollup rather than stamping 'success'. A merge landed
|
|
616
|
+
// by admin override, or with non-required checks red, must not be reported
|
|
617
|
+
// as a green run nobody actually saw — that is the same
|
|
618
|
+
// report-an-outcome-you-never-checked shape the land tail's per-step
|
|
619
|
+
// booleans exist to prevent.
|
|
620
|
+
prProbe,
|
|
621
|
+
};
|
|
227
622
|
}
|
|
228
623
|
|
|
229
624
|
/**
|
|
230
|
-
* Poll an armed
|
|
231
|
-
*
|
|
625
|
+
* Poll an armed Story PR to merge confirmation, a resumable `pending`
|
|
626
|
+
* expiry, or a classified `agent::blocked` terminal.
|
|
232
627
|
*
|
|
233
628
|
* @param {object} args
|
|
234
|
-
* @param {string} args.cwd
|
|
629
|
+
* @param {string} args.cwd The MAIN checkout.
|
|
235
630
|
* @param {number} args.storyId
|
|
631
|
+
* @param {string} [args.storyBranch]
|
|
632
|
+
* @param {string} [args.baseBranch]
|
|
236
633
|
* @param {number|null} args.prNumber
|
|
237
634
|
* @param {string} args.prUrl
|
|
238
635
|
* @param {boolean} args.autoMergeEnabled
|
|
@@ -245,42 +642,49 @@ async function blockOnUnlanded({
|
|
|
245
642
|
* @param {Function} [args.confirmStoryMergedFn] Test seam — defaults to the
|
|
246
643
|
* SAME `confirmStoryMerged` export `single-story-confirm-merge.js` calls
|
|
247
644
|
* (Story #4428 AC4: one merged/`agent::done` implementation).
|
|
248
|
-
* @param {Function} [args.
|
|
645
|
+
* @param {Function} [args.readPrWaitProbeFn] Test seam for the poll probe.
|
|
646
|
+
* @param {Function} [args.readPrMergeStateFn] Test seam for the PR-state reader.
|
|
249
647
|
* @param {Function} [args.classifyMergeBlockFn] Test seam for the classifier.
|
|
250
|
-
* @param {Function} [args.emitMergeUnlandedFn]
|
|
648
|
+
* @param {Function} [args.emitMergeUnlandedFn] Test seam for the emitter.
|
|
649
|
+
* @param {Function} [args.runPostLandTailFn] Test seam for the land tail.
|
|
251
650
|
* @param {(ms: number) => Promise<void>} [args.sleepFn] Test seam so the
|
|
252
651
|
* suite does not actually wait.
|
|
253
652
|
* @param {() => number} [args.nowMsFn] Test seam; returns epoch ms.
|
|
254
|
-
* @returns {Promise<
|
|
653
|
+
* @returns {Promise<object>}
|
|
255
654
|
*/
|
|
256
655
|
export async function runConfirmMergePhase({
|
|
257
656
|
cwd,
|
|
258
657
|
storyId,
|
|
658
|
+
storyBranch,
|
|
659
|
+
baseBranch,
|
|
259
660
|
prNumber,
|
|
260
661
|
prUrl,
|
|
261
662
|
autoMergeEnabled,
|
|
262
663
|
autoMergeReason,
|
|
263
664
|
provider,
|
|
264
665
|
config,
|
|
666
|
+
maxWaitSeconds: maxWaitSecondsOverride,
|
|
265
667
|
progress,
|
|
266
668
|
injectedGh,
|
|
267
669
|
injectedNotify,
|
|
268
670
|
confirmStoryMergedFn = defaultConfirmStoryMerged,
|
|
671
|
+
readPrWaitProbeFn = readPrWaitProbe,
|
|
269
672
|
readPrMergeStateFn = defaultReadPrMergeState,
|
|
270
|
-
readPrClassificationProbeFn = readPrClassificationProbe,
|
|
271
673
|
classifyMergeBlockFn = defaultClassifyMergeBlock,
|
|
272
674
|
emitMergeUnlandedFn = defaultEmitMergeUnlanded,
|
|
675
|
+
emitMergeFlipFailedFn = defaultEmitMergeFlipFailed,
|
|
676
|
+
runPostLandTailFn = defaultRunPostLandTail,
|
|
273
677
|
sleepFn = defaultSleep,
|
|
274
678
|
nowMsFn = Date.now,
|
|
275
679
|
}) {
|
|
276
680
|
// The arm itself never succeeded (gh failure, unparseable PR number, or a
|
|
277
681
|
// deliberate disablement) — there is no "armed but unconfirmed" PR to
|
|
278
|
-
// poll.
|
|
279
|
-
//
|
|
682
|
+
// poll. An explicit terminal state is still required, so classify and
|
|
683
|
+
// block immediately rather than resting silently.
|
|
280
684
|
if (!autoMergeEnabled) {
|
|
281
685
|
progress?.(
|
|
282
686
|
'CONFIRM',
|
|
283
|
-
`⚠️ Auto-merge not enabled (${autoMergeReason ?? 'unknown'}) —
|
|
687
|
+
`⚠️ Auto-merge not enabled (${autoMergeReason ?? 'unknown'}) — cannot wait for a merge that was never armed.`,
|
|
284
688
|
);
|
|
285
689
|
return blockOnUnlanded({
|
|
286
690
|
storyId,
|
|
@@ -295,61 +699,81 @@ export async function runConfirmMergePhase({
|
|
|
295
699
|
});
|
|
296
700
|
}
|
|
297
701
|
|
|
298
|
-
const { intervalSeconds, maxBudgetSeconds } =
|
|
299
|
-
|
|
702
|
+
const { intervalSeconds, maxWaitSeconds, maxBudgetSeconds, updateAttempts } =
|
|
703
|
+
resolveMergeWaitConfig(config, maxWaitSecondsOverride);
|
|
300
704
|
const intervalMs = intervalSeconds * 1000;
|
|
301
|
-
const budgetMs = maxBudgetSeconds * 1000;
|
|
302
705
|
const startedAtMs = nowMsFn();
|
|
706
|
+
let anchorMs = startedAtMs;
|
|
707
|
+
let updatesUsed = 0;
|
|
708
|
+
let polls = 0;
|
|
303
709
|
|
|
304
710
|
progress?.(
|
|
305
711
|
'CONFIRM',
|
|
306
|
-
`⏳
|
|
712
|
+
`⏳ Close-and-land: polling PR #${prNumber} for merge confirmation ` +
|
|
713
|
+
`(wait=${maxWaitSeconds}s this invocation, cumulative budget=${maxBudgetSeconds}s)...`,
|
|
307
714
|
);
|
|
308
715
|
|
|
309
716
|
while (true) {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
injectedGh,
|
|
319
|
-
injectedNotify,
|
|
320
|
-
readPrMergeStateFn,
|
|
717
|
+
const probe = await readPrWaitProbeFn({ prNumber, gh: injectedGh });
|
|
718
|
+
polls += 1;
|
|
719
|
+
|
|
720
|
+
// Anchor the cumulative budget at the PR's creation the first time we
|
|
721
|
+
// learn it, so a resumed wait continues the clock instead of restarting.
|
|
722
|
+
anchorMs = resolveBudgetAnchorMs({
|
|
723
|
+
createdAt: probe.createdAt,
|
|
724
|
+
fallbackMs: startedAtMs,
|
|
321
725
|
});
|
|
322
726
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
|
|
727
|
+
const waitedMs = nowMsFn() - startedAtMs;
|
|
728
|
+
const cumulativeMs = Math.max(nowMsFn() - anchorMs, waitedMs);
|
|
729
|
+
const waitBudget = {
|
|
730
|
+
maxWaitSeconds,
|
|
731
|
+
waitedSeconds: Math.round(waitedMs / 1000),
|
|
732
|
+
cumulativeSeconds: Math.round(cumulativeMs / 1000),
|
|
733
|
+
maxBudgetSeconds,
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
if (probe.state === 'MERGED' || probe.mergedAt) {
|
|
737
|
+
return onMergeObserved({
|
|
738
|
+
storyId,
|
|
739
|
+
storyBranch,
|
|
740
|
+
baseBranch,
|
|
741
|
+
prNumber,
|
|
742
|
+
prUrl,
|
|
743
|
+
cwd,
|
|
744
|
+
config,
|
|
745
|
+
provider,
|
|
746
|
+
progress,
|
|
747
|
+
injectedGh,
|
|
748
|
+
injectedNotify,
|
|
749
|
+
readPrMergeStateFn,
|
|
750
|
+
confirmStoryMergedFn,
|
|
751
|
+
runPostLandTailFn,
|
|
752
|
+
emitMergeFlipFailedFn,
|
|
753
|
+
prProbe: probe,
|
|
754
|
+
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
755
|
+
});
|
|
329
756
|
}
|
|
330
757
|
|
|
331
|
-
if (
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
progress?.(
|
|
340
|
-
'CONFIRM',
|
|
341
|
-
`⚠️ Story #${storyId} merge confirmed but the agent::done flip failed — blocking explicitly.`,
|
|
342
|
-
);
|
|
758
|
+
if (probe.state === 'CLOSED') {
|
|
759
|
+
// Closed without merging — a definitive terminal, not a "still
|
|
760
|
+
// pending" condition the budget should keep waiting on. checksStatus
|
|
761
|
+
// MUST be a non-pending, non-undefined value here: the classifier's
|
|
762
|
+
// budget-exhausted branch treats an undefined checksStatus as "still
|
|
763
|
+
// pending", which would misclassify this definitive case as
|
|
764
|
+
// checks-pending-timeout instead of reaching the api-race-other
|
|
765
|
+
// reason built from prProbe.error.
|
|
343
766
|
return blockOnUnlanded({
|
|
344
767
|
storyId,
|
|
345
768
|
prNumber,
|
|
346
769
|
prUrl,
|
|
347
770
|
prProbe: {
|
|
348
|
-
|
|
771
|
+
checksStatus: 'closed',
|
|
772
|
+
error: 'PR closed without merging (state=CLOSED)',
|
|
349
773
|
},
|
|
350
774
|
budget: {
|
|
351
775
|
exhausted: true,
|
|
352
|
-
elapsedSeconds: Math.round(
|
|
776
|
+
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
353
777
|
},
|
|
354
778
|
provider,
|
|
355
779
|
progress,
|
|
@@ -358,27 +782,26 @@ export async function runConfirmMergePhase({
|
|
|
358
782
|
});
|
|
359
783
|
}
|
|
360
784
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
785
|
+
// Fail fast on a red REQUIRED check. No remaining budget turns a failed
|
|
786
|
+
// check green, and waiting it out is what made the pre-#4543 wait report
|
|
787
|
+
// the operator's red test run as a branch-protection block.
|
|
788
|
+
//
|
|
789
|
+
// Gated on `failingChecksBlockMerge`, not on the raw rollup: a red
|
|
790
|
+
// OPTIONAL check does not stop native auto-merge, so failing fast on it
|
|
791
|
+
// would block the Story while the PR lands anyway.
|
|
792
|
+
if (failingChecksBlockMerge(probe)) {
|
|
793
|
+
progress?.(
|
|
794
|
+
'CONFIRM',
|
|
795
|
+
`🛑 PR #${prNumber}: a required check went red — failing fast rather than burning the budget.`,
|
|
796
|
+
);
|
|
371
797
|
return blockOnUnlanded({
|
|
372
798
|
storyId,
|
|
373
799
|
prNumber,
|
|
374
800
|
prUrl,
|
|
375
|
-
prProbe:
|
|
376
|
-
checksStatus: 'closed',
|
|
377
|
-
error: 'PR closed without merging (state=CLOSED)',
|
|
378
|
-
},
|
|
801
|
+
prProbe: probe,
|
|
379
802
|
budget: {
|
|
380
|
-
exhausted:
|
|
381
|
-
elapsedSeconds: Math.round(
|
|
803
|
+
exhausted: false,
|
|
804
|
+
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
382
805
|
},
|
|
383
806
|
provider,
|
|
384
807
|
progress,
|
|
@@ -387,25 +810,36 @@ export async function runConfirmMergePhase({
|
|
|
387
810
|
});
|
|
388
811
|
}
|
|
389
812
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
// of the fields classifyMergeBlock keys on, instead of stamping
|
|
394
|
-
// every timeout `checksStatus: 'pending'` (which made a
|
|
395
|
-
// review-required block undiagnosable). The probe degrades to the
|
|
396
|
-
// prior conservative pending stamp when the read itself fails.
|
|
397
|
-
const prProbe = await readPrClassificationProbeFn({
|
|
813
|
+
if (
|
|
814
|
+
await maybeUpdateBehindPr({
|
|
815
|
+
probe,
|
|
398
816
|
prNumber,
|
|
817
|
+
updatesUsed,
|
|
818
|
+
updateAttempts,
|
|
399
819
|
gh: injectedGh,
|
|
400
|
-
|
|
820
|
+
progress,
|
|
821
|
+
})
|
|
822
|
+
) {
|
|
823
|
+
updatesUsed += 1;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Cumulative budget exhausted → the genuine give-up. Classify from the
|
|
827
|
+
// probe we already hold. Gated behind the poll floor so an
|
|
828
|
+
// already-over-budget PR (anchored at a createdAt older than the budget —
|
|
829
|
+
// a resume the next day, or a long-open PR) still gets a real poll cycle
|
|
830
|
+
// instead of being blocked before this invocation waited at all.
|
|
831
|
+
if (
|
|
832
|
+
polls >= MIN_POLLS_BEFORE_BUDGET_BLOCK &&
|
|
833
|
+
cumulativeMs + intervalMs > maxBudgetSeconds * 1000
|
|
834
|
+
) {
|
|
401
835
|
return blockOnUnlanded({
|
|
402
836
|
storyId,
|
|
403
837
|
prNumber,
|
|
404
838
|
prUrl,
|
|
405
|
-
prProbe,
|
|
839
|
+
prProbe: probe,
|
|
406
840
|
budget: {
|
|
407
841
|
exhausted: true,
|
|
408
|
-
elapsedSeconds: Math.round(
|
|
842
|
+
elapsedSeconds: Math.round(cumulativeMs / 1000),
|
|
409
843
|
},
|
|
410
844
|
provider,
|
|
411
845
|
progress,
|
|
@@ -414,6 +848,28 @@ export async function runConfirmMergePhase({
|
|
|
414
848
|
});
|
|
415
849
|
}
|
|
416
850
|
|
|
851
|
+
// This invocation's bound expired → PENDING. Deliberately NOT a block:
|
|
852
|
+
// nothing is wrong, the run simply reached the edge of its host slot.
|
|
853
|
+
// No label mutation, no merge.unlanded event — the caller surfaces a
|
|
854
|
+
// resumable terminal and the next invocation continues the cumulative
|
|
855
|
+
// clock from the PR's createdAt.
|
|
856
|
+
if (waitedMs + intervalMs > maxWaitSeconds * 1000) {
|
|
857
|
+
progress?.(
|
|
858
|
+
'CONFIRM',
|
|
859
|
+
`⏸ Merge wait bound reached (${waitBudget.waitedSeconds}s of ${maxWaitSeconds}s this invocation; ` +
|
|
860
|
+
`${waitBudget.cumulativeSeconds}s of ${maxBudgetSeconds}s cumulative). PR #${prNumber} still in flight ` +
|
|
861
|
+
`(checks=${probe.checksStatus ?? 'unknown'}). Story stays at agent::closing — resumable.`,
|
|
862
|
+
);
|
|
863
|
+
return {
|
|
864
|
+
confirmed: false,
|
|
865
|
+
terminal: 'pending',
|
|
866
|
+
reason: `merge wait bound reached with the PR still in flight (checks=${probe.checksStatus ?? 'unknown'})`,
|
|
867
|
+
prProbe: probe,
|
|
868
|
+
waitBudget,
|
|
869
|
+
elapsedSeconds: waitBudget.waitedSeconds,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
417
873
|
await sleepFn(intervalMs);
|
|
418
874
|
}
|
|
419
875
|
}
|