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
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deliver-recover.js — probe a stranded Story and name its ONE next command
|
|
3
|
+
* (Story #4543).
|
|
4
|
+
*
|
|
5
|
+
* Recovery here is a **read, not a repair**. Every input is already
|
|
6
|
+
* probeable — labels and lease from the ticket, the branch locally and via
|
|
7
|
+
* its tracking ref, the worktree path, the PR by head branch (with state,
|
|
8
|
+
* merge, and checks probes) — so this module is probes, a decision table
|
|
9
|
+
* over `label × PR × branch × worktree`, and one command with the evidence it
|
|
10
|
+
* was derived from. It never mutates anything, and it never prints a menu of
|
|
11
|
+
* options: a menu is what an operator already has, and it is the thing they
|
|
12
|
+
* cannot act on.
|
|
13
|
+
*
|
|
14
|
+
* The strand shapes the table resolves, and why each is real:
|
|
15
|
+
*
|
|
16
|
+
* - `executing` with no PR → resume implementation. The work never reached
|
|
17
|
+
* close.
|
|
18
|
+
* - `closing` with a pending PR → resume the land. The overwhelmingly
|
|
19
|
+
* common shape now that the merge wait is bounded: the wait returned
|
|
20
|
+
* `pending` and something has to pick it back up.
|
|
21
|
+
* - `closing` with a red PR → enter the fix loop. Waiting is pointless; no
|
|
22
|
+
* budget turns a failed check green.
|
|
23
|
+
* - `closing` with a MERGED PR → run confirm. **This is the strand a
|
|
24
|
+
* `/deliver` re-run refuses outright**, because `single-story-init.js`
|
|
25
|
+
* hard-errors on an already-closed Story — so before this surface, the
|
|
26
|
+
* merged-but-label-stale Story had no automated way back.
|
|
27
|
+
* - `done` with a drifted board → run resync. The GitHub Projects bot won
|
|
28
|
+
* the race.
|
|
29
|
+
* - `blocked` → print the class-specific remediation the friction comment
|
|
30
|
+
* already names, rather than inventing a second opinion about a
|
|
31
|
+
* condition that was already classified.
|
|
32
|
+
*
|
|
33
|
+
* The command vocabulary is shared with the terminal envelope
|
|
34
|
+
* (`story-deliver-terminal.js#NEXT_COMMANDS`), so recovery and normal
|
|
35
|
+
* resumption speak one language instead of two dialects for one state.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { gh as defaultGh } from '../gh-exec.js';
|
|
39
|
+
import { gitSpawn as defaultGitSpawn, getStoryBranch } from '../git-utils.js';
|
|
40
|
+
import { deriveChecksStatus } from './merge-poll.js';
|
|
41
|
+
import { NEXT_COMMANDS } from './story-deliver-terminal.js';
|
|
42
|
+
import { STATE_LABELS } from './ticketing.js';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Probe the ticket: state labels, issue open/closed, and the lease holder.
|
|
46
|
+
*
|
|
47
|
+
* @returns {Promise<object>}
|
|
48
|
+
*/
|
|
49
|
+
export async function probeTicket({ provider, storyId }) {
|
|
50
|
+
try {
|
|
51
|
+
const ticket = await provider.getTicket(storyId);
|
|
52
|
+
const labels = Array.isArray(ticket?.labels) ? ticket.labels : [];
|
|
53
|
+
const stateLabel =
|
|
54
|
+
labels.find((l) => typeof l === 'string' && l.startsWith('agent::')) ??
|
|
55
|
+
null;
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
stateLabel,
|
|
59
|
+
labels,
|
|
60
|
+
issueState: ticket?.state ?? null,
|
|
61
|
+
title: ticket?.title ?? null,
|
|
62
|
+
lease: ticket?.assignees?.[0] ?? ticket?.assignee ?? null,
|
|
63
|
+
};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return { ok: false, error: String(err?.message ?? err) };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Probe the Story branch locally and its remote tracking ref, plus the
|
|
71
|
+
* worktree path. Pure git — no network.
|
|
72
|
+
*
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*/
|
|
75
|
+
export function probeBranch({ cwd, storyBranch, config, gitSpawnFn }) {
|
|
76
|
+
const spawn = gitSpawnFn ?? defaultGitSpawn;
|
|
77
|
+
const localRef = spawn(
|
|
78
|
+
cwd,
|
|
79
|
+
'show-ref',
|
|
80
|
+
'--verify',
|
|
81
|
+
'--quiet',
|
|
82
|
+
`refs/heads/${storyBranch}`,
|
|
83
|
+
);
|
|
84
|
+
const remoteRef = spawn(
|
|
85
|
+
cwd,
|
|
86
|
+
'show-ref',
|
|
87
|
+
'--verify',
|
|
88
|
+
'--quiet',
|
|
89
|
+
`refs/remotes/origin/${storyBranch}`,
|
|
90
|
+
);
|
|
91
|
+
const worktreeRoot =
|
|
92
|
+
config?.delivery?.worktreeIsolation?.root ?? '.worktrees';
|
|
93
|
+
const worktrees = spawn(cwd, 'worktree', 'list', '--porcelain');
|
|
94
|
+
const worktreePath =
|
|
95
|
+
worktrees.status === 0 &&
|
|
96
|
+
typeof worktrees.stdout === 'string' &&
|
|
97
|
+
worktrees.stdout.includes(`${worktreeRoot}/${storyBranch}`)
|
|
98
|
+
? `${worktreeRoot}/${storyBranch}`
|
|
99
|
+
: null;
|
|
100
|
+
return {
|
|
101
|
+
local: localRef.status === 0,
|
|
102
|
+
remote: remoteRef.status === 0,
|
|
103
|
+
worktreePath,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Probe the PR for the Story branch. `--state all` is required: a merged PR
|
|
109
|
+
* is no longer `open`, and the merged-but-label-stale strand is precisely
|
|
110
|
+
* the one that matters most here.
|
|
111
|
+
*
|
|
112
|
+
* @returns {Promise<object|null>}
|
|
113
|
+
*/
|
|
114
|
+
export async function probePr({ storyBranch, gh = defaultGh }) {
|
|
115
|
+
try {
|
|
116
|
+
const rows = await gh.pr.list(
|
|
117
|
+
['--head', storyBranch, '--state', 'all'],
|
|
118
|
+
['number', 'url', 'state', 'mergedAt', 'statusCheckRollup'],
|
|
119
|
+
);
|
|
120
|
+
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
121
|
+
const row = rows[0];
|
|
122
|
+
return {
|
|
123
|
+
number: Number(row?.number) || null,
|
|
124
|
+
url: row?.url ?? null,
|
|
125
|
+
state: row?.state ?? null,
|
|
126
|
+
mergedAt: row?.mergedAt ?? null,
|
|
127
|
+
checksStatus: deriveChecksStatus(row?.statusCheckRollup),
|
|
128
|
+
};
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return { error: String(err?.message ?? err) };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The decision table. Pure: every input is an already-observed probe, so the
|
|
136
|
+
* mapping is testable without git, GitHub, or a clock.
|
|
137
|
+
*
|
|
138
|
+
* Returns exactly one `{ shape, nextCommand, evidence[], detail }` — never a
|
|
139
|
+
* list of candidates.
|
|
140
|
+
*
|
|
141
|
+
* @param {{ storyId: number, ticket: object, branch: object, pr: object|null }} probes
|
|
142
|
+
* @returns {{ shape: string, nextCommand: string|null, detail: string, evidence: string[] }}
|
|
143
|
+
*/
|
|
144
|
+
export function decideRecovery({ storyId, ticket, branch, pr }) {
|
|
145
|
+
const evidence = [
|
|
146
|
+
`label=${ticket?.stateLabel ?? 'none'}`,
|
|
147
|
+
`issue=${ticket?.issueState ?? 'unknown'}`,
|
|
148
|
+
`pr=${pr?.number ? `#${pr.number} ${pr.state ?? '?'}` : 'none'}`,
|
|
149
|
+
`checks=${pr?.checksStatus ?? 'n/a'}`,
|
|
150
|
+
`branch.local=${branch?.local ?? false}`,
|
|
151
|
+
`branch.remote=${branch?.remote ?? false}`,
|
|
152
|
+
`worktree=${branch?.worktreePath ?? 'none'}`,
|
|
153
|
+
`lease=${ticket?.lease ?? 'unclaimed'}`,
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const label = ticket?.stateLabel;
|
|
157
|
+
const merged = pr?.state === 'MERGED' || Boolean(pr?.mergedAt);
|
|
158
|
+
|
|
159
|
+
// A merged PR outranks every label reading. The code is on the base
|
|
160
|
+
// branch; whatever the label says, the only thing left is the flip + tail.
|
|
161
|
+
if (merged && label !== STATE_LABELS.DONE) {
|
|
162
|
+
return {
|
|
163
|
+
shape: 'merged-label-stale',
|
|
164
|
+
nextCommand: NEXT_COMMANDS.confirmMerge(storyId),
|
|
165
|
+
detail:
|
|
166
|
+
`PR #${pr.number} is MERGED but the Story is at \`${label ?? 'no state label'}\`. ` +
|
|
167
|
+
`A /deliver re-run cannot fix this — single-story-init.js hard-errors on an ` +
|
|
168
|
+
`already-closed Story. The confirm CLI is idempotent and flips the label from ` +
|
|
169
|
+
`the already-merged PR, then runs the land tail.`,
|
|
170
|
+
evidence,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (label === STATE_LABELS.BLOCKED) {
|
|
175
|
+
return {
|
|
176
|
+
shape: 'blocked',
|
|
177
|
+
nextCommand: NEXT_COMMANDS.recover(storyId),
|
|
178
|
+
detail:
|
|
179
|
+
`Story is at \`agent::blocked\`. The block was already classified when it was ` +
|
|
180
|
+
`filed — read the \`friction\` comment on #${storyId} for the class-specific ` +
|
|
181
|
+
`remediation, resolve it, then transition back to \`agent::executing\`. ` +
|
|
182
|
+
`Re-run this probe afterwards to confirm the strand cleared.`,
|
|
183
|
+
evidence,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (label === STATE_LABELS.DONE) {
|
|
188
|
+
return {
|
|
189
|
+
shape: 'done-board-drift',
|
|
190
|
+
nextCommand: NEXT_COMMANDS.resync(storyId),
|
|
191
|
+
detail:
|
|
192
|
+
`Story is \`agent::done\`. Nothing to deliver. If the Projects board still shows ` +
|
|
193
|
+
`it as In Progress, the GitHub built-in workflow won the post-merge race; the ` +
|
|
194
|
+
`resync re-asserts the column and is a no-op otherwise.`,
|
|
195
|
+
evidence,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (label === STATE_LABELS.CLOSING) {
|
|
200
|
+
if (pr?.checksStatus === 'failure') {
|
|
201
|
+
return {
|
|
202
|
+
shape: 'closing-pr-red',
|
|
203
|
+
nextCommand: NEXT_COMMANDS.watchCi(storyId, pr.number),
|
|
204
|
+
detail:
|
|
205
|
+
`PR #${pr.number} has a red required check. Waiting cannot help — fix the ` +
|
|
206
|
+
`failure and push a new commit on \`story-${storyId}\`; auto-merge stays armed ` +
|
|
207
|
+
`across retries.`,
|
|
208
|
+
evidence,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (pr?.number) {
|
|
212
|
+
return {
|
|
213
|
+
shape: 'closing-pr-pending',
|
|
214
|
+
nextCommand: NEXT_COMMANDS.resumeLand(storyId),
|
|
215
|
+
detail:
|
|
216
|
+
`PR #${pr.number} is open and healthy. This is the normal resumable shape after ` +
|
|
217
|
+
`a bounded merge wait returned \`pending\`. The confirm CLI polls it to a ` +
|
|
218
|
+
`confirmed merge and runs the land tail.`,
|
|
219
|
+
evidence,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
shape: 'closing-no-pr',
|
|
224
|
+
nextCommand: NEXT_COMMANDS.close(storyId),
|
|
225
|
+
detail:
|
|
226
|
+
`Story is at \`agent::closing\` but no PR exists for \`story-${storyId}\`. The ` +
|
|
227
|
+
`close did not reach the pull-request phase; re-run it (close is idempotent and ` +
|
|
228
|
+
`reuses an existing PR when one is found).`,
|
|
229
|
+
evidence,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (label === STATE_LABELS.EXECUTING) {
|
|
234
|
+
if (pr?.number) {
|
|
235
|
+
return {
|
|
236
|
+
shape: 'executing-with-pr',
|
|
237
|
+
nextCommand: NEXT_COMMANDS.close(storyId),
|
|
238
|
+
detail:
|
|
239
|
+
`PR #${pr.number} exists but the Story is still \`agent::executing\` — the close ` +
|
|
240
|
+
`opened the PR and then died before the label flip. Re-run close; it reuses the ` +
|
|
241
|
+
`open PR rather than opening a duplicate.`,
|
|
242
|
+
evidence,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
shape: 'executing-no-pr',
|
|
247
|
+
nextCommand: NEXT_COMMANDS.implement(storyId),
|
|
248
|
+
detail:
|
|
249
|
+
`Story is \`agent::executing\` with no PR. Implementation never finished. Re-init ` +
|
|
250
|
+
`(idempotent — it reuses the existing branch and worktree) and resume in the ` +
|
|
251
|
+
`worktree it prints.`,
|
|
252
|
+
evidence,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
shape: 'ready',
|
|
258
|
+
nextCommand: NEXT_COMMANDS.close(storyId),
|
|
259
|
+
detail:
|
|
260
|
+
`Story is at \`${label ?? 'no agent:: state label'}\` — not mid-delivery, so there ` +
|
|
261
|
+
`is no strand to recover. Deliver it normally via /deliver ${storyId}.`,
|
|
262
|
+
evidence,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Probe live state and resolve the single next command. Read-only.
|
|
268
|
+
*
|
|
269
|
+
* @param {object} args
|
|
270
|
+
* @param {number} args.storyId
|
|
271
|
+
* @param {string} args.cwd
|
|
272
|
+
* @param {object} args.provider
|
|
273
|
+
* @param {object} [args.config]
|
|
274
|
+
* @param {object} [args.gh]
|
|
275
|
+
* @param {Function} [args.gitSpawnFn]
|
|
276
|
+
* @returns {Promise<object>}
|
|
277
|
+
*/
|
|
278
|
+
export async function recoverStory({
|
|
279
|
+
storyId,
|
|
280
|
+
cwd,
|
|
281
|
+
provider,
|
|
282
|
+
config,
|
|
283
|
+
gh = defaultGh,
|
|
284
|
+
gitSpawnFn,
|
|
285
|
+
}) {
|
|
286
|
+
const storyBranch = getStoryBranch(storyId);
|
|
287
|
+
const ticket = await probeTicket({ provider, storyId });
|
|
288
|
+
if (!ticket.ok) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`deliver-recover: could not read Story #${storyId}: ${ticket.error}`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const branch = probeBranch({ cwd, storyBranch, config, gitSpawnFn });
|
|
294
|
+
const pr = await probePr({ storyBranch, gh });
|
|
295
|
+
const decision = decideRecovery({ storyId, ticket, branch, pr });
|
|
296
|
+
return {
|
|
297
|
+
storyId,
|
|
298
|
+
storyBranch,
|
|
299
|
+
probes: { ticket, branch, pr },
|
|
300
|
+
...decision,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Render the operator-facing report: the shape, the one command, and the
|
|
306
|
+
* evidence it was derived from — so the operator can check the reasoning
|
|
307
|
+
* rather than trust it.
|
|
308
|
+
*
|
|
309
|
+
* @param {object} recovery
|
|
310
|
+
* @returns {string}
|
|
311
|
+
*/
|
|
312
|
+
export function renderRecovery(recovery) {
|
|
313
|
+
const lines = [
|
|
314
|
+
`Story #${recovery.storyId} — ${recovery.shape}`,
|
|
315
|
+
'',
|
|
316
|
+
recovery.detail,
|
|
317
|
+
'',
|
|
318
|
+
'Evidence:',
|
|
319
|
+
...recovery.evidence.map((e) => ` - ${e}`),
|
|
320
|
+
'',
|
|
321
|
+
];
|
|
322
|
+
if (recovery.nextCommand) {
|
|
323
|
+
lines.push('Next command:', ` ${recovery.nextCommand}`, '');
|
|
324
|
+
} else {
|
|
325
|
+
lines.push('Next command: none — nothing to do.', '');
|
|
326
|
+
}
|
|
327
|
+
return lines.join('\n');
|
|
328
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* detectors-phase.js — pure
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* detectors-phase.js — pure phase that runs the per-Story signal detectors
|
|
3
|
+
* (rework + retry) over the local NDJSON stream.
|
|
4
|
+
*
|
|
5
|
+
* Story #4545 — this phase has no production caller. Its sequencer
|
|
6
|
+
* (`post-merge-pipeline.js`) went in the v2.0.0 cutover, and the
|
|
7
|
+
* `analyze-execution.js` consumer it fed was deleted with the
|
|
8
|
+
* execution-analysis surface. Kept alive only by its own test — the
|
|
9
|
+
* test-importer blind spot the dead-exports ratchet cannot see.
|
|
5
10
|
*
|
|
6
11
|
* Extracted from `post-merge-pipeline.js` (Story #1770 / Task #1779) to
|
|
7
12
|
* keep the parent sequencer's maintainability score above its baseline
|
|
@@ -39,8 +44,9 @@
|
|
|
39
44
|
* @module lib/orchestration/detectors-phase
|
|
40
45
|
*/
|
|
41
46
|
|
|
47
|
+
import path from 'node:path';
|
|
42
48
|
import { getSignals } from '../config/limits.js';
|
|
43
|
-
import {
|
|
49
|
+
import { storyTempDir } from '../config/temp-paths.js';
|
|
44
50
|
import { Logger } from '../Logger.js';
|
|
45
51
|
import { appendSignal } from '../observability/signals-writer.js';
|
|
46
52
|
import { detectRetry, detectRework } from '../signals/detectors/index.js';
|
|
@@ -114,7 +120,7 @@ function isValidIdPair(eid, sid) {
|
|
|
114
120
|
|
|
115
121
|
/**
|
|
116
122
|
* Run the rework + retry detectors for one Story and persist every
|
|
117
|
-
* emission to `temp/
|
|
123
|
+
* emission to `temp/run-<id>/stories/story-<sid>/signals.ndjson`.
|
|
118
124
|
*
|
|
119
125
|
* @param {{
|
|
120
126
|
* epicId: number|string,
|
|
@@ -159,7 +165,7 @@ export async function detectorsPhase(ctx) {
|
|
|
159
165
|
return { rework: 0, retry: 0 };
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
const tracesPath =
|
|
168
|
+
const tracesPath = path.join(storyTempDir(eid, sid, config), 'traces.ndjson');
|
|
163
169
|
const taskId = resolveLastTaskId(tasks);
|
|
164
170
|
const baseArgs = { tracesPath, epicId: eid, storyId: sid, taskId };
|
|
165
171
|
const common = {
|
|
@@ -59,6 +59,36 @@ export function planFastForward(ctx) {
|
|
|
59
59
|
return { runnable: true, behind: ff.behind, currentBranch: cur };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Return HEAD to the branch it started on.
|
|
64
|
+
*
|
|
65
|
+
* {@link maybeCheckout} moves the checkout to `baseBranch` so `merge
|
|
66
|
+
* --ff-only` has somewhere to land. Leaving it there silently relocates the
|
|
67
|
+
* operator: this phase runs from the MAIN checkout — which may be parked on
|
|
68
|
+
* unrelated work — and the close tail can reach it long after the operator
|
|
69
|
+
* walked away (a belated `single-story-confirm-merge --wait`, for instance).
|
|
70
|
+
* Fast-forwarding the base branch is the contract every delivering flow owes
|
|
71
|
+
* the checkout; moving someone off the branch they were using is not, and it
|
|
72
|
+
* is the kind of surprise that gets subsequent work committed to the wrong
|
|
73
|
+
* branch.
|
|
74
|
+
*
|
|
75
|
+
* Best-effort: a failed restore warns and never fails the phase — the
|
|
76
|
+
* fast-forward already succeeded, and `planFastForward` refuses on a dirty
|
|
77
|
+
* tree, so nothing uncommitted is ever at risk here.
|
|
78
|
+
*/
|
|
79
|
+
function restoreBranchIfMoved({ plan, baseBranch, cwd, checkoutFn, logger }) {
|
|
80
|
+
const original = plan.currentBranch;
|
|
81
|
+
if (!original || original === baseBranch) return;
|
|
82
|
+
const co = checkoutFn(cwd, original);
|
|
83
|
+
if (!co.ok) {
|
|
84
|
+
logger.warn?.(
|
|
85
|
+
`${TAG} ⚠️ checkout left on ${baseBranch}: restoring ${original} failed: ${co.stderr}`,
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
logger.info?.(`${TAG} ↩️ restored checkout to ${original}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
62
92
|
function maybeCheckout({ plan, baseBranch, cwd, checkoutFn, logger }) {
|
|
63
93
|
if (!plan.currentBranch || plan.currentBranch === baseBranch) {
|
|
64
94
|
return { ok: true };
|
|
@@ -108,6 +138,9 @@ export function executeFastForward(ctx) {
|
|
|
108
138
|
logger.warn?.(
|
|
109
139
|
`${TAG} ❌ merge --ff-only ${ref} failed: ${mergeRes.stderr}`,
|
|
110
140
|
);
|
|
141
|
+
// We already moved HEAD to run the merge; put it back even on the
|
|
142
|
+
// failure path rather than stranding the operator on the base branch.
|
|
143
|
+
restoreBranchIfMoved({ plan, baseBranch, cwd, checkoutFn, logger });
|
|
111
144
|
return {
|
|
112
145
|
ok: false,
|
|
113
146
|
applied: false,
|
|
@@ -119,5 +152,6 @@ export function executeFastForward(ctx) {
|
|
|
119
152
|
logger.info?.(
|
|
120
153
|
`${TAG} ✅ fast-forwarded ${baseBranch} by ${plan.behind} commit(s)`,
|
|
121
154
|
);
|
|
155
|
+
restoreBranchIfMoved({ plan, baseBranch, cwd, checkoutFn, logger });
|
|
122
156
|
return { ok: true, applied: true, skipped: false, behind: plan.behind };
|
|
123
157
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* lease-guard-shared.js — Story #3992: single-source the lease-acquisition
|
|
3
3
|
* kernel shared by the three per-surface lease guards.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The per-surface lease guards — today `single-story-lease-guard.js`;
|
|
6
|
+
* historically also the retired Epic-tier deliver/plan guards —
|
|
7
|
+
* each carried their own copy of
|
|
7
8
|
* the operator-handle resolution and the fail-closed acquire wrapper around
|
|
8
9
|
* `ticket-lease.acquireLease` (anchor `heartbeatAt` to `now` so a foreign
|
|
9
10
|
* assignee always reads as a live claim, then throw an operator-facing
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* emit-ledger-event.js — shared validate-and-append core for the
|
|
3
|
+
* merge-terminal lifecycle emitters (Story #4539).
|
|
4
|
+
*
|
|
5
|
+
* `emit-merge-unlanded.js` and `emit-merge-flip-failed.js` report the two
|
|
6
|
+
* terminal outcomes a landing attempt can reach without ending at
|
|
7
|
+
* `agent::done`. They share everything except their schema and their
|
|
8
|
+
* payload's meaning, so the Ajv compile/cache, the scope→ledger routing,
|
|
9
|
+
* and the NDJSON append live here once rather than being copy-pasted.
|
|
10
|
+
*
|
|
11
|
+
* Like its callers, this is a bare `appendFileSync` rather than a bus
|
|
12
|
+
* publish: these events fire from the `single-story-close` flow, which has
|
|
13
|
+
* no bus at all, so a direct append keeps the call site dependency-free.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
21
|
+
import addFormats from 'ajv-formats';
|
|
22
|
+
|
|
23
|
+
import { storyLedgerPath } from '../../config/temp-paths.js';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const SCHEMA_DIR = path.resolve(
|
|
27
|
+
__dirname,
|
|
28
|
+
'..',
|
|
29
|
+
'..',
|
|
30
|
+
'..',
|
|
31
|
+
'..',
|
|
32
|
+
'schemas',
|
|
33
|
+
'lifecycle',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The scopes a merge-terminal event may be WRITTEN with. v2.0.0 removed the
|
|
38
|
+
* Epic tier, so `'story'` is the only emittable scope.
|
|
39
|
+
*
|
|
40
|
+
* Note the deliberate asymmetry with the `merge.unlanded` /
|
|
41
|
+
* `merge.flip-failed` schema enums, which still accept `'epic'`: archived
|
|
42
|
+
* ledger records carry `scope: 'epic'` and the schemas must keep validating
|
|
43
|
+
* them on read. The value stays READABLE; only the writer path is gone —
|
|
44
|
+
* the same split `merge-block-class.js` keeps for `predicate-refused`.
|
|
45
|
+
*/
|
|
46
|
+
const VALID_SCOPES = new Set(['story']);
|
|
47
|
+
|
|
48
|
+
/** @type {Map<string, Function>} */
|
|
49
|
+
const _validators = new Map();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compile (once) and return the validator for a lifecycle schema file.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} schemaFile Basename under `.agents/schemas/lifecycle/`.
|
|
55
|
+
* @returns {Function}
|
|
56
|
+
*/
|
|
57
|
+
function getValidator(schemaFile) {
|
|
58
|
+
const cached = _validators.get(schemaFile);
|
|
59
|
+
if (cached) return cached;
|
|
60
|
+
const schema = JSON.parse(
|
|
61
|
+
readFileSync(path.resolve(SCHEMA_DIR, schemaFile), 'utf8'),
|
|
62
|
+
);
|
|
63
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
64
|
+
addFormats(ajv);
|
|
65
|
+
const validator = ajv.compile(schema);
|
|
66
|
+
_validators.set(schemaFile, validator);
|
|
67
|
+
return validator;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Assert the fields every merge-terminal event shares. Throws with the
|
|
72
|
+
* caller's own emitter name so the message stays attributable.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} emitter
|
|
75
|
+
* @param {{ scope: string, ticketId: number, prNumber: number, reason: string, elapsedSeconds: number }} fields
|
|
76
|
+
*/
|
|
77
|
+
export function assertMergeTerminalFields(
|
|
78
|
+
emitter,
|
|
79
|
+
{ scope, ticketId, prNumber, reason, elapsedSeconds },
|
|
80
|
+
) {
|
|
81
|
+
if (!VALID_SCOPES.has(scope)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`${emitter}: scope "${scope}" must be one of: ${[...VALID_SCOPES].join(', ')}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (!Number.isInteger(ticketId) || ticketId < 1) {
|
|
87
|
+
throw new Error(`${emitter}: ticketId must be a positive integer`);
|
|
88
|
+
}
|
|
89
|
+
if (!Number.isInteger(prNumber) || prNumber < 1) {
|
|
90
|
+
throw new Error(`${emitter}: prNumber must be a positive integer`);
|
|
91
|
+
}
|
|
92
|
+
if (typeof reason !== 'string' || reason.length === 0) {
|
|
93
|
+
throw new Error(`${emitter}: reason must be a non-empty string`);
|
|
94
|
+
}
|
|
95
|
+
if (typeof elapsedSeconds !== 'number' || elapsedSeconds < 0) {
|
|
96
|
+
throw new Error(`${emitter}: elapsedSeconds must be a non-negative number`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate a payload against its lifecycle schema and append exactly one
|
|
102
|
+
* NDJSON record to the scope-resolved ledger.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} args
|
|
105
|
+
* @param {string} args.emitter Caller name, for error attribution.
|
|
106
|
+
* @param {string} args.schemaFile Basename under `schemas/lifecycle/`.
|
|
107
|
+
* @param {object} args.payload The event payload (already assembled).
|
|
108
|
+
* @param {number} args.ticketId
|
|
109
|
+
* @param {string} args.timestamp
|
|
110
|
+
* @param {object} [args.config]
|
|
111
|
+
* @param {string} [args.ledgerPath] Override for tests / non-default layouts.
|
|
112
|
+
* @returns {{ ledgerPath: string, record: object }}
|
|
113
|
+
*/
|
|
114
|
+
export function appendLedgerEvent({
|
|
115
|
+
emitter,
|
|
116
|
+
schemaFile,
|
|
117
|
+
payload,
|
|
118
|
+
ticketId,
|
|
119
|
+
timestamp,
|
|
120
|
+
config,
|
|
121
|
+
ledgerPath: ledgerPathOverride,
|
|
122
|
+
}) {
|
|
123
|
+
const validator = getValidator(schemaFile);
|
|
124
|
+
if (!validator(payload)) {
|
|
125
|
+
const detail = (validator.errors ?? [])
|
|
126
|
+
.map((e) => `${e.instancePath || '/'} ${e.message}`)
|
|
127
|
+
.join('; ');
|
|
128
|
+
throw new Error(`${emitter}: payload failed schema validation: ${detail}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const ledgerPath =
|
|
132
|
+
ledgerPathOverride ?? storyLedgerPath(null, ticketId, config);
|
|
133
|
+
mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
134
|
+
const record = {
|
|
135
|
+
kind: 'emitted',
|
|
136
|
+
ts: timestamp,
|
|
137
|
+
event: payload.event,
|
|
138
|
+
payload,
|
|
139
|
+
};
|
|
140
|
+
appendFileSync(ledgerPath, `${JSON.stringify(record)}\n`, 'utf8');
|
|
141
|
+
return { ledgerPath, record };
|
|
142
|
+
}
|
|
@@ -9,14 +9,12 @@
|
|
|
9
9
|
* appends an inspectable `emitted` line a reconciler can read for
|
|
10
10
|
* forward-progress evidence.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* heartbeat carries Story-phase info for a single in-flight Story and is
|
|
14
|
-
* always Epic-scoped (its ledger path is `epicLedgerPath(epicId)`). A
|
|
15
|
-
* host loop is not bound to a Story tier, so `loop.tick` carries a
|
|
12
|
+
* A host loop is not bound to a Story tier, so `loop.tick` carries a
|
|
16
13
|
* free-form `loopName`, a monotonic `round` counter, the loop's
|
|
17
|
-
* configured `cadence` label, and a per-round `status
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* configured `cadence` label, and a per-round `status`. It is now the only
|
|
15
|
+
* forward-progress beat: the Story-scoped `story.heartbeat` it was once
|
|
16
|
+
* contrasted against was Epic-scoped by construction, could never fire under
|
|
17
|
+
* v2 (which has no Epics), and was deleted (A22).
|
|
20
18
|
*
|
|
21
19
|
* Bus path (Story acceptance: "Emitting a loop.tick event THROUGH the
|
|
22
20
|
* lifecycle bus appends a record to the per-run ledger"): this helper
|
|
@@ -39,7 +37,7 @@
|
|
|
39
37
|
* Ledger path resolution: a caller supplies EITHER an explicit
|
|
40
38
|
* `ledgerPath` (the host-loop case — the loop owns where its ledger
|
|
41
39
|
* lives) OR an `epicId`, in which case the canonical
|
|
42
|
-
* `
|
|
40
|
+
* `runLedgerPath(epicId)` is used so an Epic-scoped loop's ticks land
|
|
43
41
|
* in the same `temp/run-<id>/lifecycle.ndjson` the rest of the run
|
|
44
42
|
* reads. Exactly one of the two MUST be supplied.
|
|
45
43
|
*/
|
|
@@ -47,7 +45,7 @@
|
|
|
47
45
|
import path from 'node:path';
|
|
48
46
|
import { fileURLToPath } from 'node:url';
|
|
49
47
|
|
|
50
|
-
import {
|
|
48
|
+
import { runLedgerPath } from '../../config/temp-paths.js';
|
|
51
49
|
import { createBus } from './bus.js';
|
|
52
50
|
import { createLedgerWriter } from './ledger-writer.js';
|
|
53
51
|
|
|
@@ -104,7 +102,7 @@ function decomposeLedgerPath(ledgerPath) {
|
|
|
104
102
|
* One of running|done|blocked.
|
|
105
103
|
* @param {string} [opts.timestamp] ISO-8601 wall clock. Defaults to now().
|
|
106
104
|
* @param {number} [opts.epicId] When supplied (and no `ledgerPath`),
|
|
107
|
-
* the canonical `
|
|
105
|
+
* the canonical `runLedgerPath(epicId)`
|
|
108
106
|
* is used for the ledger.
|
|
109
107
|
* @param {object} [opts.config] Optional resolved config for tempRoot
|
|
110
108
|
* (only consulted on the `epicId` path).
|
|
@@ -158,7 +156,7 @@ export async function emitLoopTick(opts) {
|
|
|
158
156
|
if (!Number.isInteger(epicId) || epicId < 1) {
|
|
159
157
|
throw new Error('emitLoopTick: epicId must be a positive integer');
|
|
160
158
|
}
|
|
161
|
-
ledgerPath =
|
|
159
|
+
ledgerPath = runLedgerPath(epicId, config);
|
|
162
160
|
}
|
|
163
161
|
|
|
164
162
|
const payload = {
|