mandrel 2.0.0 → 2.1.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 +9 -7
- package/.agents/agents/story-worker.md +41 -46
- package/.agents/audit-checklists/performance.md +1 -1
- package/.agents/docs/SDLC.md +51 -44
- package/.agents/docs/agentrc-reference.json +8 -13
- package/.agents/docs/configuration.md +32 -56
- package/.agents/docs/execution-reference.md +39 -10
- package/.agents/docs/quality-gates.md +14 -16
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +64 -79
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/git-conventions-reference.md +42 -51
- package/.agents/schemas/agentrc.schema.json +34 -45
- 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 +22 -66
- 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/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 +31 -13
- 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/code-review.js +58 -168
- 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 +114 -24
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +3 -7
- 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 -75
- 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/planning/authoring-context.js +41 -40
- 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 +3 -12
- 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 +15 -32
- 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 +6 -5
- package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -3
- 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/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 -6
- 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/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 +79 -4
- 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 +98 -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 +21 -13
- package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
- package/.agents/workflows/helpers/code-review.md +12 -10
- package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
- package/.agents/workflows/helpers/deliver-story.md +193 -118
- package/.agents/workflows/helpers/parallel-tooling.md +2 -2
- package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
- package/.agents/workflows/plan.md +184 -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 +40 -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 +12 -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/package.json +1 -1
- 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
|
@@ -36,16 +36,28 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { parseArgs } from 'node:util';
|
|
39
|
+
import { parseSprintArgs } from './lib/cli-args.js';
|
|
39
40
|
import { runAsCli } from './lib/cli-utils.js';
|
|
40
41
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
42
|
+
import { formatCliError } from './lib/error-redactor.js';
|
|
41
43
|
import { gh as defaultGh } from './lib/gh-exec.js';
|
|
42
44
|
import { getStoryBranch } from './lib/git-utils.js';
|
|
43
45
|
import { Logger } from './lib/Logger.js';
|
|
46
|
+
import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
|
|
47
|
+
import { MERGED_FLIP_FAILED_BLOCK_CLASS } from './lib/orchestration/lifecycle/emit-merge-flip-failed.js';
|
|
44
48
|
import { parsePrNumber } from './lib/orchestration/single-story-close/phases/code-review.js';
|
|
49
|
+
import { runConfirmMergePhase as defaultRunConfirmMergePhase } from './lib/orchestration/single-story-close/phases/confirm-merge.js';
|
|
45
50
|
import { parseCloseOptions } from './lib/orchestration/single-story-close/phases/options.js';
|
|
51
|
+
import { runPostLandTail } from './lib/orchestration/single-story-close/phases/post-land.js';
|
|
52
|
+
import {
|
|
53
|
+
buildTerminalEnvelope,
|
|
54
|
+
emitTerminalEnvelope,
|
|
55
|
+
exitCodeForTerminal,
|
|
56
|
+
NEXT_COMMANDS,
|
|
57
|
+
terminalFromWaitOutcome,
|
|
58
|
+
} from './lib/orchestration/story-deliver-terminal.js';
|
|
46
59
|
import { createProvider } from './lib/provider-factory.js';
|
|
47
60
|
import { confirmStoryMerged } from './lib/single-story/confirm-merge.js';
|
|
48
|
-
import { withConfirmFollowUps } from './lib/single-story/confirm-merge-follow-ups.js';
|
|
49
61
|
|
|
50
62
|
const progress = Logger.createProgress('single-story-confirm-merge', {
|
|
51
63
|
stderr: true,
|
|
@@ -71,6 +83,28 @@ function readPrFlag() {
|
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
/**
|
|
87
|
+
* `--wait`: resume the bounded merge wait rather than probing once.
|
|
88
|
+
*
|
|
89
|
+
* This CLI serves two callers behind one command string. `confirmMerge` wants
|
|
90
|
+
* a fast idempotent flip for a merge that ALREADY happened (an operator-merge
|
|
91
|
+
* flow, or a `merged-flip-failed` retry) and must not stall. `resumeLand`
|
|
92
|
+
* wants to pick up a merge wait the close handed off at its per-invocation
|
|
93
|
+
* bound — that one must actually wait, and must be able to give up.
|
|
94
|
+
*/
|
|
95
|
+
function readWaitFlag() {
|
|
96
|
+
try {
|
|
97
|
+
const { values } = parseArgs({
|
|
98
|
+
args: process.argv.slice(2),
|
|
99
|
+
options: { wait: { type: 'boolean', default: false } },
|
|
100
|
+
strict: false,
|
|
101
|
+
});
|
|
102
|
+
return values.wait === true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
/**
|
|
75
109
|
* Resolve the PR number for the Story branch when one was not passed on
|
|
76
110
|
* the CLI. Probes `gh pr list --head <branch> --state all` (the merged PR
|
|
@@ -99,11 +133,107 @@ async function resolvePrNumber({ cwd, storyBranch, gh }) {
|
|
|
99
133
|
}
|
|
100
134
|
}
|
|
101
135
|
|
|
102
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Story #4578 — async so the runtime-derived friction emit can be awaited.
|
|
138
|
+
* This CLI is the resume path for a parked worker (`--wait`), so a `pending`
|
|
139
|
+
* terminal here means a bounded wait expired again with the PR still in
|
|
140
|
+
* flight: the exact observable the retro was blind to. The emit is
|
|
141
|
+
* best-effort and cannot throw; awaiting it matters because `runAsCli`
|
|
142
|
+
* exits via `process.exit` the moment `main` resolves.
|
|
143
|
+
*/
|
|
144
|
+
async function logConfirmResult(result, terminal, config) {
|
|
145
|
+
// Human-facing dump stays level-gated; the envelope is the machine
|
|
146
|
+
// contract and must survive AGENT_LOG_LEVEL=silent.
|
|
103
147
|
Logger.info(
|
|
104
148
|
`\n--- CONFIRM MERGE RESULT ---\n${JSON.stringify(result, null, 2)}\n--- END RESULT ---\n`,
|
|
105
149
|
);
|
|
106
|
-
|
|
150
|
+
emitTerminalEnvelope(terminal);
|
|
151
|
+
await emitTerminalFriction({ envelope: terminal, config });
|
|
152
|
+
return { success: terminal.status !== 'failed', result, terminal };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Map a `confirmStoryMerged` envelope onto the shared terminal envelope
|
|
157
|
+
* (Story #4543) so this CLI and the in-close land path report one shape.
|
|
158
|
+
*
|
|
159
|
+
* The three confirm outcomes map cleanly:
|
|
160
|
+
* - `done` / `noop` (already-done) → `landed`. Both mean the merge is on
|
|
161
|
+
* the base branch and the Story is `agent::done`.
|
|
162
|
+
* - `pending` (`pr-open` / `no-pr`) → `pending`. Re-run once the merge
|
|
163
|
+
* lands; nothing is wrong.
|
|
164
|
+
* - `flip-failed` → `blocked` with the shared `merged-flip-failed` class:
|
|
165
|
+
* the merge landed, so this is a label-write fault, and the remedy is
|
|
166
|
+
* re-running this very command (it is idempotent).
|
|
167
|
+
* - `pr-not-merged` (closed without merging) → `blocked`. The PR is gone;
|
|
168
|
+
* a re-run cannot fix it, so it needs a human.
|
|
169
|
+
*/
|
|
170
|
+
function buildConfirmTerminal({
|
|
171
|
+
storyId,
|
|
172
|
+
storyBranch,
|
|
173
|
+
baseBranch,
|
|
174
|
+
prNumber,
|
|
175
|
+
confirmation,
|
|
176
|
+
tail,
|
|
177
|
+
elapsedSeconds,
|
|
178
|
+
}) {
|
|
179
|
+
// `pr-not-merged` means the PR is CLOSED — reporting it as OPEN put a
|
|
180
|
+
// fact in the envelope that the blocked reason directly contradicts.
|
|
181
|
+
const prState = confirmation.merged
|
|
182
|
+
? 'MERGED'
|
|
183
|
+
: confirmation.reason === 'pr-not-merged'
|
|
184
|
+
? 'CLOSED'
|
|
185
|
+
: 'OPEN';
|
|
186
|
+
const pr =
|
|
187
|
+
Number.isInteger(prNumber) && prNumber > 0
|
|
188
|
+
? { number: prNumber, state: prState }
|
|
189
|
+
: null;
|
|
190
|
+
const common = { storyId, storyBranch, baseBranch, pr, elapsedSeconds };
|
|
191
|
+
|
|
192
|
+
if (confirmation.action === 'done' || confirmation.action === 'noop') {
|
|
193
|
+
return buildTerminalEnvelope({
|
|
194
|
+
...common,
|
|
195
|
+
status: 'landed',
|
|
196
|
+
phase: tail ? 'post-land' : 'done',
|
|
197
|
+
tail,
|
|
198
|
+
nextCommand: null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (confirmation.action === 'flip-failed') {
|
|
202
|
+
return buildTerminalEnvelope({
|
|
203
|
+
...common,
|
|
204
|
+
status: 'blocked',
|
|
205
|
+
phase: 'confirm-merge',
|
|
206
|
+
blocked: {
|
|
207
|
+
blockClass: MERGED_FLIP_FAILED_BLOCK_CLASS,
|
|
208
|
+
reason:
|
|
209
|
+
'merge confirmed but the agent::closing → agent::done label write failed',
|
|
210
|
+
frictionCommentId: null,
|
|
211
|
+
},
|
|
212
|
+
nextCommand: NEXT_COMMANDS.confirmMerge(storyId),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (confirmation.reason === 'pr-not-merged') {
|
|
216
|
+
return buildTerminalEnvelope({
|
|
217
|
+
...common,
|
|
218
|
+
status: 'blocked',
|
|
219
|
+
phase: 'confirm-merge',
|
|
220
|
+
blocked: {
|
|
221
|
+
blockClass: 'api-race-other',
|
|
222
|
+
reason: 'the PR was closed without merging (state=CLOSED)',
|
|
223
|
+
frictionCommentId: null,
|
|
224
|
+
},
|
|
225
|
+
nextCommand: NEXT_COMMANDS.recover(storyId),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return buildTerminalEnvelope({
|
|
229
|
+
...common,
|
|
230
|
+
status: 'pending',
|
|
231
|
+
phase: 'confirm-merge',
|
|
232
|
+
nextCommand:
|
|
233
|
+
confirmation.reason === 'no-pr'
|
|
234
|
+
? NEXT_COMMANDS.recover(storyId)
|
|
235
|
+
: NEXT_COMMANDS.confirmMerge(storyId),
|
|
236
|
+
});
|
|
107
237
|
}
|
|
108
238
|
|
|
109
239
|
async function resolveConfirmPrNumber({ prParam, cwd, storyBranch, gh }) {
|
|
@@ -122,11 +252,13 @@ export async function runConfirmMerge({
|
|
|
122
252
|
storyId: storyIdParam,
|
|
123
253
|
cwd: cwdParam,
|
|
124
254
|
pr: prParam,
|
|
255
|
+
wait: waitParam,
|
|
125
256
|
injectedProvider,
|
|
126
257
|
injectedConfig,
|
|
127
258
|
injectedGh,
|
|
128
259
|
injectedNotify,
|
|
129
260
|
injectedReadPrMergeState,
|
|
261
|
+
runConfirmMergePhaseFn = defaultRunConfirmMergePhase,
|
|
130
262
|
} = {}) {
|
|
131
263
|
const { storyId, cwd } = parseCloseOptions({
|
|
132
264
|
storyIdParam,
|
|
@@ -135,14 +267,17 @@ export async function runConfirmMerge({
|
|
|
135
267
|
|
|
136
268
|
if (!storyId) {
|
|
137
269
|
throw new Error(
|
|
138
|
-
'Usage: node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--cwd <main-repo>]',
|
|
270
|
+
'Usage: node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--wait] [--cwd <main-repo>]',
|
|
139
271
|
);
|
|
140
272
|
}
|
|
273
|
+
const wait = waitParam ?? readWaitFlag();
|
|
141
274
|
|
|
275
|
+
const startedAtMs = Date.now();
|
|
142
276
|
const config = injectedConfig || resolveConfig({ cwd });
|
|
143
277
|
const provider = injectedProvider || createProvider(config);
|
|
144
278
|
const gh = injectedGh ?? defaultGh;
|
|
145
279
|
const storyBranch = getStoryBranch(storyId);
|
|
280
|
+
const baseBranch = config.project?.baseBranch ?? 'main';
|
|
146
281
|
|
|
147
282
|
progress('INIT', `Confirming merge for standalone Story #${storyId}...`);
|
|
148
283
|
|
|
@@ -157,13 +292,82 @@ export async function runConfirmMerge({
|
|
|
157
292
|
'CONFIRM',
|
|
158
293
|
`⚠️ No PR found for ${storyBranch}; cannot confirm merge. Story stays at agent::closing.`,
|
|
159
294
|
);
|
|
160
|
-
|
|
295
|
+
const noPr = {
|
|
161
296
|
storyId,
|
|
162
297
|
standalone: true,
|
|
163
298
|
action: 'pending',
|
|
164
299
|
reason: 'no-pr',
|
|
165
300
|
merged: false,
|
|
301
|
+
};
|
|
302
|
+
return await logConfirmResult(
|
|
303
|
+
noPr,
|
|
304
|
+
buildConfirmTerminal({
|
|
305
|
+
storyId,
|
|
306
|
+
storyBranch,
|
|
307
|
+
baseBranch,
|
|
308
|
+
prNumber: null,
|
|
309
|
+
confirmation: noPr,
|
|
310
|
+
tail: null,
|
|
311
|
+
elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1000),
|
|
312
|
+
}),
|
|
313
|
+
config,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// `--wait` (what `NEXT_COMMANDS.resumeLand` passes): resume the merge wait
|
|
318
|
+
// the close handed off at its per-invocation bound, by running the SAME
|
|
319
|
+
// phase the in-close path runs. Without this the resume was a single probe
|
|
320
|
+
// that reported `pending` and exited, so the cumulative `maxBudgetSeconds`
|
|
321
|
+
// give-up — the only thing that ever emits `merge.unlanded` and flips a
|
|
322
|
+
// wedged Story to `agent::blocked` — was reachable ONLY inside the original
|
|
323
|
+
// close invocation. A PR that wedged after the close returned could be
|
|
324
|
+
// resumed forever, always answering `pending`, never escalating to anyone.
|
|
325
|
+
//
|
|
326
|
+
// The phase anchors its cumulative budget at the PR's `createdAt`, so the
|
|
327
|
+
// resume does not restart the clock — exactly what the envelope's
|
|
328
|
+
// `waitBudget` contract already promised.
|
|
329
|
+
if (wait) {
|
|
330
|
+
const waitOutcome = await runConfirmMergePhaseFn({
|
|
331
|
+
cwd,
|
|
332
|
+
storyId,
|
|
333
|
+
storyBranch,
|
|
334
|
+
baseBranch,
|
|
335
|
+
prNumber,
|
|
336
|
+
prUrl: `${storyBranch} PR #${prNumber}`,
|
|
337
|
+
// The close already armed it; this CLI is resuming that wait, not
|
|
338
|
+
// deciding whether to arm.
|
|
339
|
+
autoMergeEnabled: true,
|
|
340
|
+
provider,
|
|
341
|
+
config,
|
|
342
|
+
progress,
|
|
343
|
+
injectedGh: gh,
|
|
344
|
+
injectedNotify,
|
|
345
|
+
readPrMergeStateFn: injectedReadPrMergeState,
|
|
166
346
|
});
|
|
347
|
+
const terminal = terminalFromWaitOutcome({
|
|
348
|
+
waitOutcome,
|
|
349
|
+
storyId,
|
|
350
|
+
storyBranch,
|
|
351
|
+
baseBranch,
|
|
352
|
+
prNumber,
|
|
353
|
+
prUrl: null,
|
|
354
|
+
autoMergeEnabled: true,
|
|
355
|
+
// This CLI runs no close gates, so it reports none — `gates` is
|
|
356
|
+
// "each gate this invocation was responsible for".
|
|
357
|
+
gates: undefined,
|
|
358
|
+
elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1000),
|
|
359
|
+
});
|
|
360
|
+
return await logConfirmResult(
|
|
361
|
+
{
|
|
362
|
+
storyId,
|
|
363
|
+
standalone: true,
|
|
364
|
+
action: waitOutcome.terminal,
|
|
365
|
+
resumed: true,
|
|
366
|
+
tail: waitOutcome.tail ?? null,
|
|
367
|
+
},
|
|
368
|
+
terminal,
|
|
369
|
+
config,
|
|
370
|
+
);
|
|
167
371
|
}
|
|
168
372
|
|
|
169
373
|
const confirmation = await confirmStoryMerged({
|
|
@@ -179,21 +383,90 @@ export async function runConfirmMerge({
|
|
|
179
383
|
readPrMergeStateFn: injectedReadPrMergeState,
|
|
180
384
|
});
|
|
181
385
|
|
|
182
|
-
|
|
183
|
-
|
|
386
|
+
// Story #4543 — reach the SAME shared land tail the in-close path reaches
|
|
387
|
+
// (`phases/post-land.js`), rather than this CLI's own follow-ups-only
|
|
388
|
+
// wrapper. That wrapper was the whole reason the two landing surfaces
|
|
389
|
+
// diverged: it captured follow-ups and nothing else, while the resync and
|
|
390
|
+
// cleanup steps lived in prose the caller had to remember to run.
|
|
391
|
+
//
|
|
392
|
+
// Gated on `merged`, NOT on `action === 'done'`. `done` means "this run
|
|
393
|
+
// flipped the label"; a Story already at `agent::done` returns
|
|
394
|
+
// `action: 'noop', merged: true`, and gating on `done` skipped the tail for
|
|
395
|
+
// exactly that case — the belated-manual-confirm backfill this tail exists
|
|
396
|
+
// to make possible (see `story-follow-ups.js`, which documents the gap).
|
|
397
|
+
// Re-running is safe: every step is idempotent (the follow-ups comment is
|
|
398
|
+
// an upsert, ref cleanup and base fast-forward no-op when already done).
|
|
399
|
+
const tail =
|
|
400
|
+
confirmation.merged === true
|
|
401
|
+
? await runPostLandTail({
|
|
402
|
+
storyId,
|
|
403
|
+
storyBranch,
|
|
404
|
+
baseBranch,
|
|
405
|
+
cwd,
|
|
406
|
+
provider,
|
|
407
|
+
config,
|
|
408
|
+
progress,
|
|
409
|
+
})
|
|
410
|
+
: null;
|
|
411
|
+
|
|
412
|
+
const terminal = buildConfirmTerminal({
|
|
184
413
|
storyId,
|
|
414
|
+
storyBranch,
|
|
415
|
+
baseBranch,
|
|
185
416
|
prNumber,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
progress,
|
|
417
|
+
confirmation,
|
|
418
|
+
tail,
|
|
419
|
+
elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1000),
|
|
190
420
|
});
|
|
191
|
-
if (
|
|
192
|
-
progress('DONE', `✅
|
|
421
|
+
if (confirmation.action === 'done') {
|
|
422
|
+
progress('DONE', `✅ Story #${storyId} → agent::done (merged).`);
|
|
423
|
+
}
|
|
424
|
+
return await logConfirmResult(
|
|
425
|
+
{ ...confirmation, standalone: true, tail },
|
|
426
|
+
terminal,
|
|
427
|
+
config,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* CLI entry — mirrors `single-story-close.js`: the exit code comes from the
|
|
433
|
+
* terminal envelope's status, so a caller can tell `pending` (the PR has not
|
|
434
|
+
* merged yet; re-run me) from `landed` without parsing stdout.
|
|
435
|
+
*
|
|
436
|
+
* The catch mirrors close's for the same reason: `confirmStoryMerged`'s PR
|
|
437
|
+
* read is a live `gh` call that throws on a transient API error, and this CLI
|
|
438
|
+
* is a *landing surface* — the one `pending` tells the operator to re-run. If
|
|
439
|
+
* a flaked read exited 1 with no envelope, the surface would be silent exactly
|
|
440
|
+
* where the envelope is the contract. Both landing surfaces emit one envelope
|
|
441
|
+
* or none at all; "none at all" is what Story #4543 removes.
|
|
442
|
+
*/
|
|
443
|
+
async function main() {
|
|
444
|
+
try {
|
|
445
|
+
const outcome = await runConfirmMerge();
|
|
446
|
+
return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
|
|
447
|
+
} catch (err) {
|
|
448
|
+
const storyId = Number(parseSprintArgs().storyId);
|
|
449
|
+
// No story id → a usage error; there is nothing to report an envelope
|
|
450
|
+
// about, so let runAsCli surface it as a plain fatal.
|
|
451
|
+
if (!Number.isInteger(storyId) || storyId <= 0) throw err;
|
|
452
|
+
const terminal = buildTerminalEnvelope({
|
|
453
|
+
storyId,
|
|
454
|
+
status: 'failed',
|
|
455
|
+
phase: 'confirm-merge',
|
|
456
|
+
failure: { reason: String(err?.message ?? err) },
|
|
457
|
+
nextCommand: NEXT_COMMANDS.recover(storyId),
|
|
458
|
+
elapsedSeconds: 0,
|
|
459
|
+
});
|
|
460
|
+
Logger.error(
|
|
461
|
+
`[single-story-confirm-merge] Fatal error: ${formatCliError(err)}`,
|
|
462
|
+
);
|
|
463
|
+
emitTerminalEnvelope(terminal);
|
|
464
|
+
await emitTerminalFriction({ envelope: terminal });
|
|
465
|
+
return exitCodeForTerminal(terminal);
|
|
193
466
|
}
|
|
194
|
-
return logConfirmResult(result);
|
|
195
467
|
}
|
|
196
468
|
|
|
197
|
-
runAsCli(import.meta.url,
|
|
469
|
+
runAsCli(import.meta.url, main, {
|
|
198
470
|
source: 'single-story-confirm-merge',
|
|
471
|
+
propagateExitCode: true,
|
|
199
472
|
});
|
|
@@ -223,12 +223,10 @@ export async function reapMergedStoryBranches({
|
|
|
223
223
|
/**
|
|
224
224
|
* Fetch remote refs, reap merged story branches, and fast-forward the local
|
|
225
225
|
* base branch so new story branches seed from origin's tip. Exported for
|
|
226
|
-
* testing (owns the fast-forward cascade
|
|
227
|
-
* `ensureEpicBranch` in `branch-initializer.js`).
|
|
226
|
+
* testing (owns the fast-forward cascade).
|
|
228
227
|
*
|
|
229
|
-
* Routes the `origin` fetch through `cachedGitFetch` so concurrent
|
|
230
|
-
*
|
|
231
|
-
* stories get via `branch-initializer.js#fetchMainRefs`. Pass `fetchFn` to
|
|
228
|
+
* Routes the `origin` fetch through `cachedGitFetch` so concurrent Story
|
|
229
|
+
* waves share a per-process fetch-coalescing window. Pass `fetchFn` to
|
|
232
230
|
* inject a stub in tests without touching real git.
|
|
233
231
|
*
|
|
234
232
|
* @param {object} opts
|
|
@@ -413,12 +411,10 @@ export async function provisionWorktree({
|
|
|
413
411
|
}
|
|
414
412
|
|
|
415
413
|
try {
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// standalone-trace branch on CC_EPIC_ID being absent.
|
|
414
|
+
// v2 Stories are standalone — no parent Epic. The helper omits
|
|
415
|
+
// CC_EPIC_ID from env + file; the trace hook keys its standalone-trace
|
|
416
|
+
// branch on CC_EPIC_ID being absent.
|
|
420
417
|
setActiveStoryEnv({
|
|
421
|
-
epicId: null,
|
|
422
418
|
storyId,
|
|
423
419
|
workCwd,
|
|
424
420
|
logger: {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
* totalStories: number,
|
|
38
38
|
* concurrencyCap: number,
|
|
39
39
|
* inFlight: number,
|
|
40
|
-
* cycleError: string | null
|
|
40
|
+
* cycleError: string | null,
|
|
41
|
+
* wedged: { reason, stories: [{ id, unmetBlockers }] } | null
|
|
41
42
|
* }
|
|
42
43
|
*
|
|
43
44
|
* The standalone loop calls this once per beat: after each Story closes it
|
|
@@ -54,7 +55,13 @@
|
|
|
54
55
|
* deterministic config source (`delivery.deliverRunner.concurrencyCap`) and
|
|
55
56
|
* one scheduling kernel with every `/deliver` multi-Story invocation.
|
|
56
57
|
*
|
|
57
|
-
*
|
|
58
|
+
* Exit codes: 0 ok · 1 input error · 2 dependency cycle (`cycleError`) ·
|
|
59
|
+
* 3 wedged (`wedged`) — ready is empty, nothing is in flight, and undone
|
|
60
|
+
* Stories are waiting on blockers that are not done. A cycle is a
|
|
61
|
+
* self-referential DAG the operator must fix; a wedge is a well-formed DAG
|
|
62
|
+
* whose gates cannot be satisfied from the supplied `--done` set (usually a
|
|
63
|
+
* blocker outside the delivered set that has not landed). Both are distinct
|
|
64
|
+
* from the ordinary `ready: []` that means "waiting on in-flight work".
|
|
58
65
|
*/
|
|
59
66
|
|
|
60
67
|
import { readFileSync } from 'node:fs';
|
|
@@ -68,6 +75,13 @@ import { AGENT_LABELS } from './lib/label-constants.js';
|
|
|
68
75
|
import { buildStoryAdjacency } from './lib/story-adjacency.js';
|
|
69
76
|
import { selectReadySet } from './lib/wave-runner/ready-set.js';
|
|
70
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Exit code for a wedged run — deliberately distinct from the cycle exit (2)
|
|
80
|
+
* so a caller can tell "your DAG is self-referential" from "your DAG is fine
|
|
81
|
+
* but its gates can never be satisfied from this `--done` set".
|
|
82
|
+
*/
|
|
83
|
+
export const WEDGED_EXIT_CODE = 3;
|
|
84
|
+
|
|
71
85
|
const HELP = `Usage: node .agents/scripts/stories-wave-tick.js --dag '<json>' | --dag-file <path> [--concurrency <n>] [--done <csv>] [--in-flight <n>]
|
|
72
86
|
|
|
73
87
|
Continuous ready-set planner for standalone Story delivery. Consumes a
|
|
@@ -102,13 +116,17 @@ Output envelope:
|
|
|
102
116
|
"totalStories": 2,
|
|
103
117
|
"concurrencyCap": 3,
|
|
104
118
|
"inFlight": 0,
|
|
105
|
-
"cycleError": null
|
|
119
|
+
"cycleError": null,
|
|
120
|
+
"wedged": null
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
Exit codes:
|
|
109
124
|
0 - Success, ready set emitted
|
|
110
125
|
1 - Invalid input (missing/malformed DAG, invalid --concurrency/--in-flight/--done)
|
|
111
126
|
2 - Cycle detected in dependency graph
|
|
127
|
+
3 - Wedged: ready is empty, nothing is in flight, and undone Stories are
|
|
128
|
+
waiting on blockers that are not done. Distinct from an ordinary empty
|
|
129
|
+
ready set (which means "waiting on in-flight work") and from a cycle.
|
|
112
130
|
`;
|
|
113
131
|
|
|
114
132
|
/**
|
|
@@ -322,6 +340,7 @@ export function buildReadySetEnvelope(
|
|
|
322
340
|
concurrencyCap,
|
|
323
341
|
inFlight,
|
|
324
342
|
cycleError: null,
|
|
343
|
+
wedged: null,
|
|
325
344
|
};
|
|
326
345
|
|
|
327
346
|
if (totalStories === 0) {
|
|
@@ -365,7 +384,62 @@ export function buildReadySetEnvelope(
|
|
|
365
384
|
globalCap: concurrencyCap,
|
|
366
385
|
}).map((rec) => rec.id);
|
|
367
386
|
|
|
368
|
-
|
|
387
|
+
// Wedge detection (Story #4540). `ready: []` is normal while work is in
|
|
388
|
+
// flight — the loop is simply waiting. But ready-empty AND nothing in
|
|
389
|
+
// flight AND undone Stories remaining means no beat can ever make
|
|
390
|
+
// progress: the run is stuck, and the previous behaviour was to return
|
|
391
|
+
// exit 0 with an empty ready set forever, indistinguishable from
|
|
392
|
+
// "waiting". Name the stuck ids and their unmet blockers.
|
|
393
|
+
//
|
|
394
|
+
// Distinct from `cycleError`/exit 2: a cycle is a self-referential DAG,
|
|
395
|
+
// whereas this is a DAG whose gates are real but unsatisfiable from the
|
|
396
|
+
// supplied `done` set (typically a blocker outside the delivered set that
|
|
397
|
+
// has not landed).
|
|
398
|
+
const wedge = detectWedge({ nodes, doneIds, ready, inFlight });
|
|
399
|
+
if (wedge) {
|
|
400
|
+
return {
|
|
401
|
+
envelope: { ...base, ready, wedged: wedge },
|
|
402
|
+
exitCode: WEDGED_EXIT_CODE,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { envelope: { ...base, ready, wedged: null }, exitCode: 0 };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Identify a run that cannot progress: nothing dispatchable, nothing in
|
|
411
|
+
* flight, work remaining.
|
|
412
|
+
*
|
|
413
|
+
* @param {{ nodes: object[], doneIds: Set<number>, ready: number[], inFlight: number }} args
|
|
414
|
+
* @returns {{ reason: string, stories: Array<{ id: number, unmetBlockers: number[] }> }|null}
|
|
415
|
+
*/
|
|
416
|
+
export function detectWedge({ nodes, doneIds, ready, inFlight }) {
|
|
417
|
+
if (ready.length > 0 || inFlight > 0) return null;
|
|
418
|
+
const undone = nodes.filter((n) => !doneIds.has(n.id));
|
|
419
|
+
if (undone.length === 0) return null;
|
|
420
|
+
|
|
421
|
+
const stories = undone
|
|
422
|
+
.map((n) => ({
|
|
423
|
+
id: n.id,
|
|
424
|
+
unmetBlockers: (n.dependsOn ?? []).filter((dep) => !doneIds.has(dep)),
|
|
425
|
+
}))
|
|
426
|
+
.filter((s) => s.unmetBlockers.length > 0);
|
|
427
|
+
|
|
428
|
+
// Undone work with no unmet blockers would have been dispatched; if that
|
|
429
|
+
// is the whole set, the cap or in-flight accounting explains the empty
|
|
430
|
+
// ready set rather than a wedge.
|
|
431
|
+
if (stories.length === 0) return null;
|
|
432
|
+
|
|
433
|
+
const detail = stories
|
|
434
|
+
.map((s) => `#${s.id} ← ${s.unmetBlockers.map((d) => `#${d}`).join(', ')}`)
|
|
435
|
+
.join('; ');
|
|
436
|
+
return {
|
|
437
|
+
reason:
|
|
438
|
+
`No Story can be dispatched: nothing is in flight and ${stories.length} ` +
|
|
439
|
+
`Story(ies) are waiting on blockers that are not done — ${detail}. ` +
|
|
440
|
+
`A blocker outside the delivered set must land first, or be included in --ids.`,
|
|
441
|
+
stories,
|
|
442
|
+
};
|
|
369
443
|
}
|
|
370
444
|
|
|
371
445
|
/**
|
|
@@ -405,6 +479,7 @@ export function runStoriesWaveTick({
|
|
|
405
479
|
concurrencyCap,
|
|
406
480
|
inFlight: inFlightValue,
|
|
407
481
|
cycleError: null,
|
|
482
|
+
wedged: null,
|
|
408
483
|
inputError: message,
|
|
409
484
|
},
|
|
410
485
|
exitCode: 1,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Operator planning:
|
|
14
14
|
* node .agents/scripts/plan-context.js --seed "…" | --seed-file <path> | --tickets <ids>
|
|
15
|
-
* node .agents/scripts/plan-persist.js --stories …
|
|
15
|
+
* node .agents/scripts/plan-persist.js --stories …
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { readFile } from 'node:fs/promises';
|
|
@@ -146,7 +146,7 @@ async function runEmitContext({
|
|
|
146
146
|
// corpus digest reads the project's actual docs directory regardless
|
|
147
147
|
// of the directory this CLI happens to be invoked from — matching the
|
|
148
148
|
// sibling resolution pattern in
|
|
149
|
-
//
|
|
149
|
+
// planning/authoring-context.js.
|
|
150
150
|
const docsRoot = path.resolve(
|
|
151
151
|
PROJECT_ROOT,
|
|
152
152
|
config?.project?.paths?.docsRoot ?? 'docs',
|
|
@@ -157,7 +157,7 @@ async function runEmitContext({
|
|
|
157
157
|
loadBodyTemplate(projectRoot),
|
|
158
158
|
fetchOpenStories(provider),
|
|
159
159
|
readTechStackSummary(projectRoot),
|
|
160
|
-
buildCorpusContext({
|
|
160
|
+
buildCorpusContext({ docsContextFiles, docsRoot }),
|
|
161
161
|
]);
|
|
162
162
|
|
|
163
163
|
const duplicateCandidates = rankDuplicateCandidates({
|
|
@@ -8,15 +8,7 @@
|
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
9
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
10
10
|
import { Logger } from './lib/Logger.js';
|
|
11
|
-
import {
|
|
12
|
-
enqueueLabel,
|
|
13
|
-
outboxPathFor,
|
|
14
|
-
} from './lib/orchestration/bookkeeping-outbox.js';
|
|
15
|
-
import {
|
|
16
|
-
cascadeCompletion,
|
|
17
|
-
STATE_LABELS,
|
|
18
|
-
transitionTicketState,
|
|
19
|
-
} from './lib/orchestration/ticketing.js';
|
|
11
|
+
import { transitionTicketState } from './lib/orchestration/ticketing.js';
|
|
20
12
|
import { createProvider } from './lib/provider-factory.js';
|
|
21
13
|
|
|
22
14
|
// ── CLI Main Block ────────────────────────────────────────────────────────
|
|
@@ -31,8 +23,6 @@ if (
|
|
|
31
23
|
ticket: { type: 'string' },
|
|
32
24
|
state: { type: 'string' },
|
|
33
25
|
'remove-label': { type: 'string' },
|
|
34
|
-
buffer: { type: 'boolean' },
|
|
35
|
-
epic: { type: 'string' },
|
|
36
26
|
},
|
|
37
27
|
strict: false,
|
|
38
28
|
});
|
|
@@ -66,50 +56,18 @@ if (
|
|
|
66
56
|
return;
|
|
67
57
|
}
|
|
68
58
|
|
|
69
|
-
// Headless buffering (Epic #4476 M5): buffer an intermediate state flip to
|
|
70
|
-
// the per-Epic outbox instead of a live GitHub round-trip; finalize's
|
|
71
|
-
// bookkeeping-reconcile.js drains it once. `agent::blocked` (the HITL gate,
|
|
72
|
-
// §1.J) and `agent::done` (its cascade must run live) ALWAYS surface
|
|
73
|
-
// immediately and are never buffered. Requires --epic to locate the outbox.
|
|
74
|
-
if (
|
|
75
|
-
values.buffer &&
|
|
76
|
-
state !== STATE_LABELS.BLOCKED &&
|
|
77
|
-
state !== STATE_LABELS.DONE
|
|
78
|
-
) {
|
|
79
|
-
const epicId = Number.parseInt(values.epic ?? '', 10);
|
|
80
|
-
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
81
|
-
throw new Error('--buffer requires --epic <id>');
|
|
82
|
-
}
|
|
83
|
-
enqueueLabel({
|
|
84
|
-
outboxPath: outboxPathFor(epicId, config),
|
|
85
|
-
ticketId,
|
|
86
|
-
state,
|
|
87
|
-
});
|
|
88
|
-
Logger.info(
|
|
89
|
-
`[State-Sync] Buffered #${ticketId} → ${state} (headless; drained at finalize)`,
|
|
90
|
-
);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
59
|
Logger.info(
|
|
95
60
|
`[State-Sync] Transitioning ticket #${ticketId} to ${state}...`,
|
|
96
61
|
);
|
|
62
|
+
// Story #4545 — no second cascade here. `transitionTicketState` already
|
|
63
|
+
// fires the upward cascade on every transition (bulk.js registers
|
|
64
|
+
// `cascadeParentState` as transition.js's cascade runner, and the DONE
|
|
65
|
+
// branch delegates to `cascadeCompletion`), so the explicit call this
|
|
66
|
+
// used to make re-walked the same parents and re-spent the same API
|
|
67
|
+
// calls a second time on every done transition — under a Story-only
|
|
68
|
+
// model where no orchestration parent exists to find.
|
|
97
69
|
await transitionTicketState(provider, ticketId, state);
|
|
98
70
|
|
|
99
|
-
if (state === STATE_LABELS.DONE) {
|
|
100
|
-
Logger.info(`[State-Sync] Cascading completion from #${ticketId}...`);
|
|
101
|
-
const cascade = await cascadeCompletion(provider, ticketId);
|
|
102
|
-
// Hoisted out of the `for...of` initializer because typhonjs-escomplex
|
|
103
|
-
// mis-parses optional chaining there (it would zero out this file's
|
|
104
|
-
// maintainability score).
|
|
105
|
-
const cascadeFailures = cascade?.failed ?? [];
|
|
106
|
-
for (const { parentId, error } of cascadeFailures) {
|
|
107
|
-
Logger.warn(
|
|
108
|
-
`[State-Sync] ⚠️ Cascade partial-failure on parent #${parentId}: ${error}`,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
71
|
// Optional secondary label removal alongside the state transition
|
|
114
72
|
// (e.g. clear `status::blocked` when transitioning back to ready).
|
|
115
73
|
if (removeLabel) {
|