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,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/wave-runner/live-probe.js — the state-probing adapter that feeds the
|
|
3
|
+
* ready-set kernel from live GitHub state.
|
|
4
|
+
*
|
|
5
|
+
* `selectReadySet` (`./ready-set.js`) is deliberately a pure, side-effect-free
|
|
6
|
+
* kernel: callers hand it the live Story records, the done set, and the
|
|
7
|
+
* in-flight count, and it decides. Until now the only adapter was the
|
|
8
|
+
* flag-driven one (`stories-wave-tick.js --dag/--done/--in-flight`), which
|
|
9
|
+
* pushed the *gathering* of those inputs onto the caller — in practice onto
|
|
10
|
+
* the host LLM following `/deliver`'s prose, re-seeding `--done` and counting
|
|
11
|
+
* `--in-flight` by hand every beat. That is hand-maintained accounting on the
|
|
12
|
+
* one correctness-critical path where a mistake silently wedges a run (a
|
|
13
|
+
* dropped foreign blocker) or double-dispatches a Story (a miscounted slot).
|
|
14
|
+
*
|
|
15
|
+
* This module closes that gap by **probing** the same facts the host was
|
|
16
|
+
* transcribing:
|
|
17
|
+
*
|
|
18
|
+
* - **done** — an `agent::done` label OR a closed issue, the same predicate
|
|
19
|
+
* `classifyStory` already applies, evaluated over live state rather than a
|
|
20
|
+
* `--done` CSV the caller maintained across beats. Foreign blockers
|
|
21
|
+
* (outside the delivered set) are resolved too, which is what makes
|
|
22
|
+
* cross-run delivery work: a blocker that merged weeks ago in another run
|
|
23
|
+
* is simply done.
|
|
24
|
+
* - **in-flight** — derived from live `agent::executing` / `agent::closing`
|
|
25
|
+
* labels, **unioned with the ids the host says it has dispatched**
|
|
26
|
+
* (`--dispatched`). The label alone is not sufficient: the kernel's
|
|
27
|
+
* contract counts "executing / closing / dispatched-not-yet-labelled" as
|
|
28
|
+
* in-flight, and `single-story-init.js` flips `agent::executing` at step 6
|
|
29
|
+
* of 6 — *after* a 3–6 minute worktree install. For that whole window a
|
|
30
|
+
* dispatched Story still reads `agent::ready`, so a label-only derivation
|
|
31
|
+
* re-emits it in the next beat's `ready[]` and the host dispatches it a
|
|
32
|
+
* second time onto the same branch and worktree (Story #4601).
|
|
33
|
+
* - **blocked** — the ids carrying `agent::blocked`. `classifyStory` has
|
|
34
|
+
* always returned this class; nothing consumed it, so a blocked Story was
|
|
35
|
+
* neither done, ready, nor in-flight and the beat reported a permanent
|
|
36
|
+
* "waiting" (Story #4601).
|
|
37
|
+
*
|
|
38
|
+
* It is an **adapter, not a kernel change**: it gathers inputs and hands them
|
|
39
|
+
* to `selectReadySet` unchanged. The kernel stays pure and flag-driven, and
|
|
40
|
+
* the legacy flag mode stays byte-compatible.
|
|
41
|
+
*
|
|
42
|
+
* The graph resolution is **not** reimplemented here — it reuses
|
|
43
|
+
* `resolve-stories.js`'s machinery wholesale (body `depends_on` ∪ native
|
|
44
|
+
* `blocked_by` edges, foreign-blocker resolution, `files[]` footprints), so
|
|
45
|
+
* the probe and `/deliver`'s step-1 resolution cannot disagree about what
|
|
46
|
+
* depends on what.
|
|
47
|
+
*
|
|
48
|
+
* @module lib/wave-runner/live-probe
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
fetchStories,
|
|
53
|
+
readNativeEdges,
|
|
54
|
+
resolveForeignDone,
|
|
55
|
+
resolveStoriesProvider,
|
|
56
|
+
} from '../../resolve-stories.js';
|
|
57
|
+
import { AGENT_LABELS } from '../label-constants.js';
|
|
58
|
+
import { buildStoriesEnvelope } from '../orchestration/resolve-stories.js';
|
|
59
|
+
import { classifyStory, storyIdOf } from './ready-set.js';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Identify the Stories that currently occupy a dispatch slot, as an id set.
|
|
63
|
+
*
|
|
64
|
+
* Two sources, unioned — which is exactly the kernel's stated contract
|
|
65
|
+
* ("executing / closing / dispatched-not-yet-labelled"):
|
|
66
|
+
*
|
|
67
|
+
* 1. **Live labels.** `classifyStory` folds `agent::executing` and
|
|
68
|
+
* `agent::closing` into one `executing` class — both are in-flight and
|
|
69
|
+
* neither may be re-dispatched.
|
|
70
|
+
* 2. **`dispatched`** — ids the host has spawned. This closes the init
|
|
71
|
+
* window: `single-story-init.js` flips `agent::executing` last, after a
|
|
72
|
+
* 3–6 minute install, so between spawn and flip a dispatched Story reads
|
|
73
|
+
* `agent::ready` and a label-only derivation hands it back as ready.
|
|
74
|
+
*
|
|
75
|
+
* `dispatched` is deliberately **not** the `--done`-style accounting probe
|
|
76
|
+
* mode retired. Three properties keep it from becoming one:
|
|
77
|
+
*
|
|
78
|
+
* - **It is a set union, not a counter.** Re-passing an id that has since
|
|
79
|
+
* picked up its `agent::executing` label cannot double-count a slot.
|
|
80
|
+
* - **Live state overrules the claim.** An id the host still lists but that
|
|
81
|
+
* now classifies `done` (or `blocked`) is dropped, so a stale entry can
|
|
82
|
+
* never occupy a slot forever and starve the run.
|
|
83
|
+
* - **Therefore the host's correct strategy is monotonic append**: pass
|
|
84
|
+
* every id you have dispatched this run and never reason about removing
|
|
85
|
+
* one. There is no drop-a-slot decision to get wrong — the probe subtracts
|
|
86
|
+
* reality from the claim. Forgetting an id degrades to the pre-#4601
|
|
87
|
+
* behaviour rather than to something worse.
|
|
88
|
+
*
|
|
89
|
+
* Ids outside the probed set are ignored: they are not part of this run and
|
|
90
|
+
* must not consume its cap.
|
|
91
|
+
*
|
|
92
|
+
* @param {Array<{id?: number, number?: number, labels?: string[], state?: string}>} storyRecords
|
|
93
|
+
* @param {Iterable<number>} [dispatched] Ids the host has spawned.
|
|
94
|
+
* @returns {Set<number>} In-flight Story ids.
|
|
95
|
+
*/
|
|
96
|
+
function deriveInFlightIds(storyRecords, dispatched = []) {
|
|
97
|
+
const claimed = new Set(dispatched);
|
|
98
|
+
const inFlight = new Set();
|
|
99
|
+
for (const rec of storyRecords) {
|
|
100
|
+
const id = storyIdOf(rec);
|
|
101
|
+
if (id === null) continue;
|
|
102
|
+
const cls = classifyStory(rec);
|
|
103
|
+
if (cls === 'executing' || (claimed.has(id) && cls === 'ready')) {
|
|
104
|
+
inFlight.add(id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return inFlight;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The ids carrying `agent::blocked`.
|
|
112
|
+
*
|
|
113
|
+
* `classifyStory` has always returned a `blocked` class, but no adapter
|
|
114
|
+
* consumed it: a blocked Story was never done, never ready, and never counted
|
|
115
|
+
* in-flight, so `detectWedge` dropped it (its "undone work with no unmet
|
|
116
|
+
* blockers would have been dispatched" invariant is precisely what probe mode
|
|
117
|
+
* broke) and the beat reported exit 0 / `ready: []` / `wedged: null` forever.
|
|
118
|
+
* `/deliver` reads that as "waiting", so the `agent::blocked` HITL pause — the
|
|
119
|
+
* one runtime gate in the protocol — was never surfaced to the operator.
|
|
120
|
+
*
|
|
121
|
+
* @param {Array<{id?: number, number?: number, labels?: string[], state?: string}>} storyRecords
|
|
122
|
+
* @returns {number[]} Blocked Story ids, ascending.
|
|
123
|
+
*/
|
|
124
|
+
function deriveBlockedIds(storyRecords) {
|
|
125
|
+
return storyRecords
|
|
126
|
+
.filter((rec) => classifyStory(rec) === 'blocked')
|
|
127
|
+
.map((rec) => storyIdOf(rec))
|
|
128
|
+
.filter((id) => id !== null)
|
|
129
|
+
.sort((a, b) => a - b);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the provider + repo coordinates the probe reads through.
|
|
134
|
+
*
|
|
135
|
+
* Shares `resolve-stories.js`'s provider seam, so probe mode authenticates and
|
|
136
|
+
* targets exactly the same repo `/deliver`'s resolution step does. Tests
|
|
137
|
+
* inject a stub provider instead of calling this.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} [deps]
|
|
140
|
+
* @param {Function} [deps.resolveProvider] Injection seam for tests.
|
|
141
|
+
* @returns {{ provider: object, owner: string|undefined, repo: string|undefined }}
|
|
142
|
+
*/
|
|
143
|
+
export function createProbeContext({
|
|
144
|
+
resolveProvider = resolveStoriesProvider,
|
|
145
|
+
} = {}) {
|
|
146
|
+
const { provider, config } = resolveProvider();
|
|
147
|
+
return { provider, owner: config?.github?.owner, repo: config?.github?.repo };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Probe live state for a set of Story ids and return the exact inputs
|
|
152
|
+
* `selectReadySet` consumes.
|
|
153
|
+
*
|
|
154
|
+
* Mirrors `resolve-stories.js`'s two-pass envelope build: a provisional pass
|
|
155
|
+
* yields the DAG whose foreign dependency ids are then resolved against live
|
|
156
|
+
* issue state, and the second pass folds those satisfied foreign blockers into
|
|
157
|
+
* `done[]`. Skipping that pass would withhold any Story whose blocker landed
|
|
158
|
+
* outside the delivered set — the cross-run wedge the resolver exists to fix.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} args
|
|
161
|
+
* @param {number[]} args.ids Story ids in the run.
|
|
162
|
+
* @param {object} args.provider GitHub provider (stubbed in tests).
|
|
163
|
+
* @param {string} [args.owner]
|
|
164
|
+
* @param {string} [args.repo]
|
|
165
|
+
* @param {boolean} [args.native=true] Read native `blocked_by` edges.
|
|
166
|
+
* @param {number[]} [args.dispatched=[]] Ids the host has spawned but may not
|
|
167
|
+
* yet have observed labelled `agent::executing` (see `deriveInFlightIds`).
|
|
168
|
+
* @param {(msg: string) => void} [args.warn]
|
|
169
|
+
* Each returned node carries its **live labels**. That is load-bearing, not
|
|
170
|
+
* decoration: `selectReadySet` classifies from labels, so a node stripped of
|
|
171
|
+
* them reads as `ready` and an `agent::executing` Story gets re-dispatched
|
|
172
|
+
* onto a second branch while its first run is still going. The resolver's DAG
|
|
173
|
+
* projection (`{id, dependsOn, files}`) drops labels because flag mode's
|
|
174
|
+
* caller tracked in-flight itself; probe mode must put them back.
|
|
175
|
+
*
|
|
176
|
+
* @returns {Promise<{
|
|
177
|
+
* nodes: Array<{id: number, dependsOn: number[], files: string[], labels: string[]}>,
|
|
178
|
+
* doneIds: Set<number>,
|
|
179
|
+
* inFlight: number,
|
|
180
|
+
* blockedIds: number[]
|
|
181
|
+
* }>}
|
|
182
|
+
*/
|
|
183
|
+
export async function probeLiveState({
|
|
184
|
+
ids,
|
|
185
|
+
provider,
|
|
186
|
+
owner,
|
|
187
|
+
repo,
|
|
188
|
+
native = true,
|
|
189
|
+
dispatched = [],
|
|
190
|
+
warn,
|
|
191
|
+
}) {
|
|
192
|
+
const stories = await fetchStories(provider, ids);
|
|
193
|
+
const nativeEdges = native
|
|
194
|
+
? await readNativeEdges({ provider, stories, owner, repo })
|
|
195
|
+
: new Map();
|
|
196
|
+
|
|
197
|
+
const provisional = buildStoriesEnvelope({ stories, nativeEdges, warn });
|
|
198
|
+
const foreignDone = await resolveForeignDone({
|
|
199
|
+
provider,
|
|
200
|
+
dag: provisional.dag,
|
|
201
|
+
inSetIds: new Set(stories.map((s) => s.id)),
|
|
202
|
+
});
|
|
203
|
+
const envelope = buildStoriesEnvelope({
|
|
204
|
+
stories,
|
|
205
|
+
nativeEdges,
|
|
206
|
+
foreignDone,
|
|
207
|
+
warn: () => {},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const labelsById = new Map(stories.map((s) => [s.id, s.labels ?? []]));
|
|
211
|
+
const inFlightIds = deriveInFlightIds(stories, dispatched);
|
|
212
|
+
return {
|
|
213
|
+
nodes: envelope.dag.map((node) => ({
|
|
214
|
+
...node,
|
|
215
|
+
labels: projectInFlightLabels(
|
|
216
|
+
labelsById.get(node.id) ?? [],
|
|
217
|
+
inFlightIds.has(node.id),
|
|
218
|
+
),
|
|
219
|
+
})),
|
|
220
|
+
doneIds: new Set(envelope.done),
|
|
221
|
+
inFlight: inFlightIds.size,
|
|
222
|
+
blockedIds: deriveBlockedIds(stories),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Project the in-flight fact onto a node's labels, synthesizing
|
|
228
|
+
* `agent::executing` for a Story that is dispatched but not yet labelled.
|
|
229
|
+
*
|
|
230
|
+
* This is the load-bearing half of the dispatch-window fix, and it is why
|
|
231
|
+
* `inFlight` alone is not enough. The two inputs do **different** jobs inside
|
|
232
|
+
* `selectReadySet`:
|
|
233
|
+
*
|
|
234
|
+
* - `inFlight` is only a **count**. It reserves capacity (`slots = cap −
|
|
235
|
+
* inFlight`) and nothing more.
|
|
236
|
+
* - **Eligibility is decided per-record by `classifyStory`**, from labels.
|
|
237
|
+
*
|
|
238
|
+
* So a dispatched-but-unlabelled Story counted only via `inFlight` still
|
|
239
|
+
* classifies `ready`, stays eligible, and — whenever a slot remains — is
|
|
240
|
+
* admitted to the very same beat that reserved a slot for it. It would be
|
|
241
|
+
* re-dispatched onto its own live branch, with the miscount merely reshaped
|
|
242
|
+
* rather than fixed. Handing the kernel the label makes it apply the rule it
|
|
243
|
+
* already has, and keeps the kernel itself untouched: the adapter's job is to
|
|
244
|
+
* supply the input the kernel's contract ("executing / closing / dispatched-
|
|
245
|
+
* not-yet-labelled") already specifies.
|
|
246
|
+
*
|
|
247
|
+
* @param {string[]} labels The Story's live labels.
|
|
248
|
+
* @param {boolean} inFlight Whether the Story occupies a dispatch slot.
|
|
249
|
+
* @returns {string[]} Labels, with `agent::executing` added when needed.
|
|
250
|
+
*/
|
|
251
|
+
function projectInFlightLabels(labels, inFlight) {
|
|
252
|
+
if (!inFlight || classifyStory({ labels }) === 'executing') return labels;
|
|
253
|
+
return [...labels, AGENT_LABELS.EXECUTING];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validate the mode-selecting flags, keeping probe mode and the legacy
|
|
258
|
+
* flag mode mutually exclusive.
|
|
259
|
+
*
|
|
260
|
+
* The exclusion is not pedantry: `--probe-live` derives `done` and `in-flight`
|
|
261
|
+
* from live state, so honouring a caller-supplied `--done` alongside it would
|
|
262
|
+
* silently reintroduce the hand-maintained accounting probe mode exists to
|
|
263
|
+
* retire — and quietly disagree with reality when the two differ.
|
|
264
|
+
*
|
|
265
|
+
* `--dispatched` is the deliberate exception, and it is **additive rather than
|
|
266
|
+
* authoritative**: it does not replace the derived in-flight set, it is unioned
|
|
267
|
+
* into it and then filtered by live state (see `deriveInFlightIds`). It carries
|
|
268
|
+
* the one fact the host knows and GitHub does not yet — "I spawned this id, the
|
|
269
|
+
* label has not appeared yet" — so it cannot disagree with reality the way an
|
|
270
|
+
* authoritative `--in-flight <n>` could. `--in-flight` therefore stays excluded.
|
|
271
|
+
*
|
|
272
|
+
* @param {object} flags
|
|
273
|
+
* @param {boolean} [flags.probeLive]
|
|
274
|
+
* @param {string} [flags.stories]
|
|
275
|
+
* @param {string} [flags.dag]
|
|
276
|
+
* @param {string} [flags.dagFile]
|
|
277
|
+
* @param {string} [flags.done]
|
|
278
|
+
* @param {string} [flags.inFlight]
|
|
279
|
+
* @param {string} [flags.dispatched]
|
|
280
|
+
* @returns {string|null} An error message, or `null` when the flags are valid.
|
|
281
|
+
*/
|
|
282
|
+
export function validateProbeFlags({
|
|
283
|
+
probeLive,
|
|
284
|
+
stories,
|
|
285
|
+
dag,
|
|
286
|
+
dagFile,
|
|
287
|
+
done,
|
|
288
|
+
inFlight,
|
|
289
|
+
dispatched,
|
|
290
|
+
} = {}) {
|
|
291
|
+
if (!probeLive) {
|
|
292
|
+
if (dispatched != null) {
|
|
293
|
+
return '--dispatched requires --probe-live (it augments the live-derived in-flight set; flag mode uses --in-flight <n>)';
|
|
294
|
+
}
|
|
295
|
+
return stories
|
|
296
|
+
? '--stories requires --probe-live (it names the run to probe from live state)'
|
|
297
|
+
: null;
|
|
298
|
+
}
|
|
299
|
+
const conflicting = [
|
|
300
|
+
dag ? '--dag' : null,
|
|
301
|
+
dagFile ? '--dag-file' : null,
|
|
302
|
+
done != null ? '--done' : null,
|
|
303
|
+
inFlight != null ? '--in-flight' : null,
|
|
304
|
+
].filter(Boolean);
|
|
305
|
+
if (conflicting.length > 0) {
|
|
306
|
+
return (
|
|
307
|
+
`--probe-live is mutually exclusive with ${conflicting.join(', ')}: it resolves the graph ` +
|
|
308
|
+
`and derives done / in-flight from live state. Drop the flag(s), or use the legacy flag mode.`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (!stories) {
|
|
312
|
+
return '--probe-live requires --stories <csv> of Story ids';
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
@@ -156,15 +156,37 @@ export function storyFootprint(story) {
|
|
|
156
156
|
return out;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Does a declared path contain a glob metacharacter? Mirrors the detection
|
|
161
|
+
* in `story-body.js#extractChangePaths`, whose `isGlob` flag documents an
|
|
162
|
+
* "unknown-width footprint" policy that was never implemented downstream.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} path
|
|
165
|
+
* @returns {boolean}
|
|
166
|
+
*/
|
|
167
|
+
function isGlobPath(path) {
|
|
168
|
+
return path.includes('*') || path.includes('?') || path.includes('{');
|
|
169
|
+
}
|
|
170
|
+
|
|
159
171
|
/**
|
|
160
172
|
* File-overlap co-dispatch guard. Returns `true` when two Stories' declared
|
|
161
|
-
* file footprints intersect
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
173
|
+
* file footprints intersect — meaning they would race the same file if
|
|
174
|
+
* dispatched onto parallel `story-<id>` branches in the same beat. Two
|
|
175
|
+
* Stories that overlap MUST NOT both appear in one dispatch set; one is
|
|
176
|
+
* withheld until the other clears.
|
|
165
177
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
178
|
+
* Two deliberate asymmetries:
|
|
179
|
+
*
|
|
180
|
+
* - **An empty footprint means "no known overlap"** → `false`. A Story that
|
|
181
|
+
* declares no files is never withheld. This is permissive by necessity:
|
|
182
|
+
* an undeclared footprint carries no information, and withholding on
|
|
183
|
+
* absence would serialize every run.
|
|
184
|
+
* - **A glob footprint overlaps EVERYTHING** → `true` (Story #4539/#4540).
|
|
185
|
+
* Comparison is exact-string, so a Story declaring
|
|
186
|
+
* `.agents/scripts/lib/**` would not match another declaring
|
|
187
|
+
* `.agents/scripts/lib/story-adjacency.js` — the guard would silently
|
|
188
|
+
* pass two Stories that genuinely race. Unknown width is not the same as
|
|
189
|
+
* no width: fail safe by serializing.
|
|
168
190
|
*
|
|
169
191
|
* @param {StoryRecord} a
|
|
170
192
|
* @param {StoryRecord} b
|
|
@@ -176,8 +198,12 @@ export function storiesOverlap(a, b) {
|
|
|
176
198
|
const fb = storyFootprint(b);
|
|
177
199
|
if (fb.size === 0) return false;
|
|
178
200
|
for (const path of fa) {
|
|
201
|
+
if (isGlobPath(path)) return true;
|
|
179
202
|
if (fb.has(path)) return true;
|
|
180
203
|
}
|
|
204
|
+
for (const path of fb) {
|
|
205
|
+
if (isGlobPath(path)) return true;
|
|
206
|
+
}
|
|
181
207
|
return false;
|
|
182
208
|
}
|
|
183
209
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* on a Windows lock-class failure, the entry is appended to
|
|
8
8
|
* `.worktrees/.pending-cleanup.json`. The plan-time `worktree-sweep.js`
|
|
9
9
|
* reader (shipped in Epic #349) picks up the manifest on the next
|
|
10
|
-
* `/
|
|
10
|
+
* `/plan` run and retries removal
|
|
11
11
|
* (`git worktree remove` then `fs.rm`) — by then the live file handles from Node / AV / the Windows
|
|
12
12
|
* Search indexer are almost always gone. If `MAX_SWEEP_ATTEMPTS` elapses
|
|
13
13
|
* without clearing, an `OPERATOR ACTION REQUIRED: persistent-lock` line
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import { rm as fsPromisesRm } from 'node:fs/promises';
|
|
18
|
-
import { isInsideWorktree, samePath
|
|
18
|
+
import { isInsideWorktree, samePath } from '../inspector.js';
|
|
19
19
|
import { sleepSync } from '../node-modules-strategy.js';
|
|
20
20
|
import { checkMergeReachability } from './merge-reachability.js';
|
|
21
21
|
import { recordPendingCleanup } from './pending-cleanup.js';
|
|
@@ -72,21 +72,58 @@ export async function isSafeToRemove(ctx, wtPath, opts = {}) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Returns true iff `branch` is already
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* Returns true iff `branch`'s work is demonstrably already integrated into
|
|
76
|
+
* `baseRef`. Used only to license discarding a **dirty** tree, so it must
|
|
77
|
+
* err toward `false`.
|
|
78
|
+
*
|
|
79
|
+
* Two-phase:
|
|
80
|
+
* 1. `merge-base --is-ancestor` — cheap SHA reachability; true for
|
|
81
|
+
* fast-forward and merge-commit integration.
|
|
82
|
+
* 2. `git cherry <baseRef> <branch>` — compares **patch-ids** rather than
|
|
83
|
+
* SHAs, marking a commit `-` when an equivalent change already exists
|
|
84
|
+
* upstream. All-`-` (or empty) means every commit on the branch is
|
|
85
|
+
* present in the base under some SHA.
|
|
86
|
+
*
|
|
87
|
+
* **Known limit — this is not a general squash detector.** A squash collapses
|
|
88
|
+
* N commits into ONE new commit whose patch equals their *combined* diff, so
|
|
89
|
+
* no individual original commit has an upstream patch-id equivalent and
|
|
90
|
+
* `git cherry` marks them all `+`. Phase 2 therefore only recognises a
|
|
91
|
+
* squash-landed branch when the branch had a **single** commit (verified
|
|
92
|
+
* empirically, Story #4539). A multi-commit squash still reads as unmerged
|
|
93
|
+
* and the dirty tree is refused — the safe direction, and the same answer
|
|
94
|
+
* the previous ancestor-only check gave. Detecting the general case needs
|
|
95
|
+
* the PR's merged state, which this module has no client for.
|
|
96
|
+
*
|
|
97
|
+
* A missing ref or a git failure yields false, so callers default to the
|
|
98
|
+
* safe, non-forcing behavior. Module-private: `ensureSafeOrForceDiscard` is
|
|
99
|
+
* its only caller, and the symbol it replaced was exported with no consumer
|
|
100
|
+
* and then baselined as a dead export — repeating that would just hide a
|
|
101
|
+
* new corpse.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} ctx
|
|
104
|
+
* @param {string} branch
|
|
105
|
+
* @param {string} baseRef
|
|
106
|
+
* @returns {boolean}
|
|
79
107
|
*/
|
|
80
|
-
|
|
81
|
-
if (!branch || !
|
|
82
|
-
const
|
|
108
|
+
function isBranchMergedIntoBase(ctx, branch, baseRef) {
|
|
109
|
+
if (!branch || !baseRef) return false;
|
|
110
|
+
const ancestor = ctx.git.gitSpawn(
|
|
83
111
|
ctx.repoRoot,
|
|
84
112
|
'merge-base',
|
|
85
113
|
'--is-ancestor',
|
|
86
114
|
branch,
|
|
87
|
-
|
|
115
|
+
baseRef,
|
|
88
116
|
);
|
|
89
|
-
|
|
117
|
+
if (ancestor.status === 0) return true;
|
|
118
|
+
|
|
119
|
+
const cherry = ctx.git.gitSpawn(ctx.repoRoot, 'cherry', baseRef, branch);
|
|
120
|
+
if (cherry.status !== 0) return false;
|
|
121
|
+
const lines = (cherry.stdout ?? '')
|
|
122
|
+
.split('\n')
|
|
123
|
+
.map((l) => l.trim())
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
// Every line starting with '+' is a commit with no upstream equivalent.
|
|
126
|
+
return lines.every((line) => line.startsWith('-'));
|
|
90
127
|
}
|
|
91
128
|
|
|
92
129
|
/**
|
|
@@ -470,28 +507,40 @@ function checkReapPreconditions(ctx, _storyId, opts, wtPath) {
|
|
|
470
507
|
ok: false,
|
|
471
508
|
result: { removed: false, reason: 'not-a-worktree', path: wtPath },
|
|
472
509
|
};
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
510
|
+
// Story #4539 removed an `epic-branch-required` gate here: a
|
|
511
|
+
// `story-<id>` worktree used to be unreapable unless the caller supplied
|
|
512
|
+
// an Epic integration branch. v2 has no Epic branch, and the only v2
|
|
513
|
+
// caller (the close path) never passed one — so EVERY close silently
|
|
514
|
+
// failed to reap while reporting success, and cleanup fell to the next
|
|
515
|
+
// boot-sweep.
|
|
516
|
+
//
|
|
517
|
+
// Nothing is lost by dropping it. The close path reaps AFTER pushing
|
|
518
|
+
// `story-<id>` to origin and opening the PR, so the work is durable
|
|
519
|
+
// off-machine; and the real safety net is unchanged — `isSafeToRemove`
|
|
520
|
+
// still refuses a dirty tree (`uncommitted-changes`), which is what
|
|
521
|
+
// actually protects unsaved work.
|
|
479
522
|
return { ok: true };
|
|
480
523
|
}
|
|
481
524
|
|
|
482
525
|
async function ensureSafeOrForceDiscard(ctx, storyId, wtPath, opts) {
|
|
526
|
+
const baseRef = opts.baseRef ?? opts.epicBranch ?? null;
|
|
483
527
|
const safety = await isSafeToRemove(ctx, wtPath, {
|
|
484
|
-
epicBranch:
|
|
528
|
+
epicBranch: baseRef,
|
|
485
529
|
});
|
|
486
530
|
if (safety.safe) return { ok: true, discardedPaths: null };
|
|
487
531
|
|
|
488
532
|
const discardAfterMerge = opts.discardAfterMerge !== false;
|
|
489
533
|
const branchName = `story-${validateStoryId(storyId)}`;
|
|
534
|
+
// Discarding a dirty tree is only permissible when the branch's work is
|
|
535
|
+
// demonstrably already integrated. See `isBranchMergedIntoBase` for what
|
|
536
|
+
// that can and cannot prove — notably a multi-commit squash reads as
|
|
537
|
+
// unmerged, so the discard is refused and the tree survives. Refusing is
|
|
538
|
+
// the safe direction: the cost is a stale worktree, not lost work.
|
|
490
539
|
const canForceReap =
|
|
491
540
|
discardAfterMerge &&
|
|
492
541
|
safety.reason === 'uncommitted-changes' &&
|
|
493
|
-
|
|
494
|
-
|
|
542
|
+
baseRef &&
|
|
543
|
+
isBranchMergedIntoBase(ctx, branchName, baseRef);
|
|
495
544
|
if (!canForceReap) {
|
|
496
545
|
ctx.logger.warn(
|
|
497
546
|
`reap-skipped storyId=${storyId} reason=${safety.reason} path=${wtPath}`,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - `creation.js` — `ensure`
|
|
8
8
|
* - `registry-sync.js` — `pathFor`, `list`, `prune`, `getWorktreeList`,
|
|
9
9
|
* `invalidateWorktreeCache`, `findByPath`
|
|
10
|
-
* - `reap.js` — `isSafeToRemove`,
|
|
10
|
+
* - `reap.js` — `isSafeToRemove`,
|
|
11
11
|
* `removeWorktreeWithRecovery`, `reap`
|
|
12
12
|
* - `gc.js` — `gc`
|
|
13
13
|
* - `drift-detection.js` — `sweepStaleLocks`
|
|
@@ -26,7 +26,6 @@ export { sweepStaleLocks } from './lifecycle/drift-detection.js';
|
|
|
26
26
|
export { gc } from './lifecycle/gc.js';
|
|
27
27
|
export {
|
|
28
28
|
isSafeToRemove,
|
|
29
|
-
isStoryAlreadyMergedIntoEpic,
|
|
30
29
|
reap,
|
|
31
30
|
removeWorktreeWithRecovery,
|
|
32
31
|
} from './lifecycle/reap.js';
|
|
@@ -18,8 +18,14 @@
|
|
|
18
18
|
* Stories. Envelope carries `sourceTickets[]`.
|
|
19
19
|
*
|
|
20
20
|
* Flags:
|
|
21
|
+
* --out <path> Also write the envelope to <path> (parent dirs created).
|
|
22
|
+
* `/plan` points this at `<plan-dir>/plan-context.json`,
|
|
23
|
+
* which is where `plan-persist.js` auto-discovers the
|
|
24
|
+
* `--tickets` source ids from (Story #4554). Without a
|
|
25
|
+
* captured envelope persist cannot know a `--tickets` run
|
|
26
|
+
* happened, and superseding degrades to the
|
|
27
|
+
* `--source-tickets` flag.
|
|
21
28
|
* --pretty Pretty-print the JSON envelope.
|
|
22
|
-
* --full-context Bypass the planning-context budget (unbounded body).
|
|
23
29
|
*
|
|
24
30
|
* stdout is reserved for the JSON envelope (Story #2278 discipline):
|
|
25
31
|
* `routeAllOutputToStderr()` runs before any pipeline code so a captured
|
|
@@ -34,13 +40,15 @@
|
|
|
34
40
|
// first import so the check runs before any third-party-importing sibling
|
|
35
41
|
// module is evaluated (Story #3432).
|
|
36
42
|
import './lib/runtime-deps/ensure-installed.js';
|
|
43
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
44
|
+
import path from 'node:path';
|
|
37
45
|
import { parseArgs } from 'node:util';
|
|
38
46
|
import { runAsCli } from './lib/cli-utils.js';
|
|
39
47
|
import {
|
|
40
48
|
resolveConfig,
|
|
41
49
|
validateOrchestrationConfig,
|
|
42
50
|
} from './lib/config-resolver.js';
|
|
43
|
-
import { routeAllOutputToStderr } from './lib/Logger.js';
|
|
51
|
+
import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
|
|
44
52
|
import { buildPlanContext } from './lib/orchestration/plan-context.js';
|
|
45
53
|
import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
|
|
46
54
|
import { createProvider } from './lib/provider-factory.js';
|
|
@@ -84,8 +92,8 @@ export async function emitPlanContext({
|
|
|
84
92
|
provider,
|
|
85
93
|
config,
|
|
86
94
|
settings,
|
|
87
|
-
fullContext = false,
|
|
88
95
|
pretty = false,
|
|
96
|
+
outPath = null,
|
|
89
97
|
cwd,
|
|
90
98
|
stdout = process.stdout,
|
|
91
99
|
}) {
|
|
@@ -98,24 +106,48 @@ export async function emitPlanContext({
|
|
|
98
106
|
provider,
|
|
99
107
|
config,
|
|
100
108
|
settings,
|
|
101
|
-
fullContext,
|
|
102
109
|
cwd,
|
|
103
110
|
});
|
|
104
111
|
const json = pretty
|
|
105
112
|
? JSON.stringify(envelope, null, 2)
|
|
106
113
|
: JSON.stringify(envelope);
|
|
107
114
|
stdout.write(`${json}\n`);
|
|
115
|
+
if (outPath) await writeEnvelopeFile(outPath, json);
|
|
108
116
|
return envelope;
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Persist the envelope to `--out` so `plan-persist.js` can derive the
|
|
121
|
+
* `--tickets` source ids from it without an operator re-typing them.
|
|
122
|
+
*
|
|
123
|
+
* Writing is part of emitting, not a best-effort extra: a failed write means
|
|
124
|
+
* persist will silently see no source tickets, so it throws rather than
|
|
125
|
+
* warning past the problem.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} outPath
|
|
128
|
+
* @param {string} json
|
|
129
|
+
*/
|
|
130
|
+
async function writeEnvelopeFile(outPath, json) {
|
|
131
|
+
const resolved = path.resolve(outPath);
|
|
132
|
+
try {
|
|
133
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
134
|
+
await writeFile(resolved, `${json}\n`, 'utf8');
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[plan-context] cannot write envelope to ${resolved}: ${err.message}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
Logger.info(`[plan-context] wrote envelope to ${resolved}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
111
143
|
async function main() {
|
|
112
144
|
const { values } = parseArgs({
|
|
113
145
|
options: {
|
|
114
146
|
seed: { type: 'string' },
|
|
115
147
|
'seed-file': { type: 'string' },
|
|
116
148
|
tickets: { type: 'string' },
|
|
149
|
+
out: { type: 'string' },
|
|
117
150
|
pretty: { type: 'boolean', default: false },
|
|
118
|
-
'full-context': { type: 'boolean', default: false },
|
|
119
151
|
},
|
|
120
152
|
strict: true,
|
|
121
153
|
});
|
|
@@ -173,7 +205,6 @@ async function main() {
|
|
|
173
205
|
{
|
|
174
206
|
cli: 'plan-context',
|
|
175
207
|
mode,
|
|
176
|
-
epicId: null,
|
|
177
208
|
config,
|
|
178
209
|
},
|
|
179
210
|
() =>
|
|
@@ -185,8 +216,8 @@ async function main() {
|
|
|
185
216
|
provider,
|
|
186
217
|
config,
|
|
187
218
|
settings,
|
|
188
|
-
fullContext: values['full-context'],
|
|
189
219
|
pretty: values.pretty,
|
|
220
|
+
outPath: values.out || null,
|
|
190
221
|
}),
|
|
191
222
|
);
|
|
192
223
|
}
|