mandrel 1.93.0 → 2.0.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 -73
- package/.agents/agents/acceptance-critic.md +129 -0
- package/.agents/agents/story-worker.md +161 -0
- package/.agents/docs/SDLC.md +489 -1285
- package/.agents/docs/agentrc-reference.json +177 -67
- package/.agents/docs/configuration.md +108 -136
- package/.agents/docs/execution-reference.md +44 -22
- package/.agents/docs/quality-gates.md +13 -19
- package/.agents/docs/workflows.md +3 -3
- package/.agents/instructions.md +107 -108
- package/.agents/rules/ci-remediation.md +8 -12
- package/.agents/rules/git-conventions-reference.md +224 -0
- package/.agents/rules/git-conventions.md +42 -223
- package/.agents/rules/security-baseline.md +5 -0
- package/.agents/rules/testing-standards.md +106 -13
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +71 -201
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/lifecycle/retro.end.schema.json +1 -1
- package/.agents/schemas/risk-verdict.schema.json +0 -13
- package/.agents/scripts/acceptance-eval.js +62 -18
- package/.agents/scripts/agents-bootstrap-github.js +1 -1
- package/.agents/scripts/analyze-execution.js +1 -1
- package/.agents/scripts/audit-to-stories.js +7 -7
- package/.agents/scripts/boot-sweep.js +1 -1
- package/.agents/scripts/check-context-budget.js +62 -5
- package/.agents/scripts/check-lifecycle-lint.js +6 -9
- package/.agents/scripts/check-prepush-recovery.js +1 -1
- package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
- package/.agents/scripts/diagnose-friction.js +0 -6
- package/.agents/scripts/lib/Logger.js +6 -10
- package/.agents/scripts/lib/audit-suite/runner.js +2 -2
- package/.agents/scripts/lib/audit-suite/selector.js +5 -5
- package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
- package/.agents/scripts/lib/baselines/kernel.js +206 -18
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -4
- package/.agents/scripts/lib/baselines/reader.js +1 -6
- package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +56 -18
- package/.agents/scripts/lib/checks/core-bare-clean.js +2 -2
- package/.agents/scripts/lib/checks/index.js +2 -1
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +23 -21
- package/.agents/scripts/lib/cli/standard-args.js +13 -22
- package/.agents/scripts/lib/cli-args.js +16 -7
- package/.agents/scripts/lib/close-validation/gates.js +160 -22
- package/.agents/scripts/lib/config/acceptance-eval.js +52 -5
- package/.agents/scripts/lib/config/ci.js +6 -31
- package/.agents/scripts/lib/config/delivery-routing.js +103 -0
- package/.agents/scripts/lib/config/explain.js +57 -36
- package/.agents/scripts/lib/config/limits.js +17 -58
- package/.agents/scripts/lib/config/paths.js +0 -2
- package/.agents/scripts/lib/config/quality.js +1 -1
- package/.agents/scripts/lib/config/runners.js +17 -50
- package/.agents/scripts/lib/config/temp-paths.js +19 -14
- package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
- package/.agents/scripts/lib/config-resolver.js +3 -8
- package/.agents/scripts/lib/config-settings-schema-delivery.js +46 -136
- package/.agents/scripts/lib/config-settings-schema-quality.js +17 -14
- package/.agents/scripts/lib/config-settings-schema.js +52 -38
- package/.agents/scripts/lib/dependency-parser.js +3 -2
- package/.agents/scripts/lib/doc-tiers.js +39 -4
- package/.agents/scripts/lib/duplicate-search.js +211 -41
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +5 -5
- package/.agents/scripts/lib/framework-version.js +2 -3
- package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
- package/.agents/scripts/lib/git-branch-lifecycle.js +17 -22
- package/.agents/scripts/lib/git-utils.js +32 -6
- package/.agents/scripts/lib/github/framework-repo.js +6 -0
- package/.agents/scripts/lib/label-constants.js +10 -23
- package/.agents/scripts/lib/label-taxonomy.js +9 -43
- package/.agents/scripts/lib/observability/active-story-env.js +112 -3
- package/.agents/scripts/lib/observability/hook-heartbeat.js +187 -0
- package/.agents/scripts/lib/observability/source-classifier.js +3 -3
- package/.agents/scripts/lib/observability/tool-trace-hook.js +15 -4
- package/.agents/scripts/lib/onboard/init-tail.js +1 -3
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +111 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +32 -4
- package/.agents/scripts/lib/orchestration/audit-lens-routing.js +128 -0
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +273 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +204 -0
- package/.agents/scripts/lib/orchestration/code-review.js +20 -268
- package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +1 -1
- package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
- package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
- package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +8 -8
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +17 -43
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
- package/.agents/scripts/lib/orchestration/merge-block-class.js +1 -1
- package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
- package/.agents/scripts/lib/orchestration/plan-context.js +248 -266
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +3 -2
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +61 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +97 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +223 -854
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +361 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +35 -108
- package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
- package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +14 -14
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
- package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/risk-verdict.js +3 -4
- package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +2 -2
- package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +8 -20
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -2
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
- package/.agents/scripts/lib/orchestration/preflight-cache.js +5 -5
- package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
- package/.agents/scripts/lib/orchestration/resolve-plan-run.js +155 -0
- package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
- package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +8 -8
- package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
- package/.agents/scripts/lib/orchestration/review-depth.js +26 -12
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
- package/.agents/scripts/lib/orchestration/run-epilogue.js +426 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +1 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +95 -41
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +16 -13
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +40 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +11 -3
- package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
- package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +15 -12
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +237 -0
- package/.agents/scripts/lib/orchestration/story-init-remote.js +47 -0
- package/.agents/scripts/lib/orchestration/story-plan-state.js +48 -0
- package/.agents/scripts/lib/orchestration/{epic-runner → story-progress}/story-run-progress-writer.js +3 -3
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -18
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +11 -61
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +189 -373
- package/.agents/scripts/lib/orchestration/ticket-validator.js +3 -8
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +0 -25
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +29 -26
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -5
- package/.agents/scripts/lib/planning-corpus.js +16 -11
- package/.agents/scripts/lib/preflight-runner.js +2 -2
- package/.agents/scripts/lib/provider-factory.js +1 -1
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
- package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +36 -0
- package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
- package/.agents/scripts/lib/story-adjacency.js +11 -14
- package/.agents/scripts/lib/story-body/story-body.js +124 -70
- package/.agents/scripts/lib/story-plan.js +2 -4
- package/.agents/scripts/lib/templates/decomposer-prompts.js +46 -45
- package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
- package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +26 -26
- package/.agents/scripts/lib/validation-evidence.js +1 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -6
- package/.agents/scripts/lib/workspace-provisioner.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +5 -7
- package/.agents/scripts/lint-issue-body.js +71 -40
- package/.agents/scripts/mandrel-update-preflight.js +1 -1
- package/.agents/scripts/notify.js +4 -3
- package/.agents/scripts/plan-context.js +64 -74
- package/.agents/scripts/plan-persist.js +121 -280
- package/.agents/scripts/plan-run-epilogue.js +97 -0
- package/.agents/scripts/post-structured-comment.js +38 -0
- package/.agents/scripts/providers/github/issues.js +17 -33
- package/.agents/scripts/providers/github/mappers.js +0 -12
- package/.agents/scripts/providers/github/tickets.js +2 -5
- package/.agents/scripts/resolve-plan-run.js +117 -0
- package/.agents/scripts/signals-view.js +24 -19
- package/.agents/scripts/single-story-close.js +11 -14
- package/.agents/scripts/single-story-confirm-merge.js +39 -23
- package/.agents/scripts/single-story-init.js +29 -20
- package/.agents/scripts/stories-wave-tick.js +6 -6
- package/.agents/scripts/story-plan.js +26 -47
- package/.agents/scripts/sync-claude-agents.js +165 -0
- package/.agents/scripts/update-ticket-state.js +37 -15
- package/.agents/skills/core/analyze-execution/SKILL.md +21 -18
- package/.agents/skills/core/api-and-interface-design/SKILL.md +5 -3
- package/.agents/skills/core/code-review-and-quality/SKILL.md +63 -7
- package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +1 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +149 -0
- package/.agents/skills/core/idea-refinement/SKILL.md +8 -14
- package/.agents/skills/core/qa-coverage-mapping/SKILL.md +7 -7
- package/.agents/skills/core/scope-triage/SKILL.md +28 -172
- package/.agents/skills/skills.index.json +8 -418
- package/.agents/starter-agentrc.json +0 -5
- package/.agents/templates/agent-protocol.md +9 -10
- package/.agents/workflows/audit-architecture.md +3 -3
- package/.agents/workflows/audit-clean-code.md +3 -3
- package/.agents/workflows/audit-dependencies.md +3 -3
- package/.agents/workflows/audit-devops.md +3 -3
- package/.agents/workflows/audit-documentation.md +5 -5
- package/.agents/workflows/audit-lighthouse.md +3 -3
- package/.agents/workflows/audit-navigability.md +3 -2
- package/.agents/workflows/audit-performance.md +3 -3
- package/.agents/workflows/audit-privacy.md +3 -3
- package/.agents/workflows/audit-quality.md +3 -3
- package/.agents/workflows/audit-security.md +3 -3
- package/.agents/workflows/audit-seo.md +3 -3
- package/.agents/workflows/audit-sre.md +3 -3
- package/.agents/workflows/audit-to-stories.md +20 -20
- package/.agents/workflows/audit-ux-ui.md +3 -3
- package/.agents/workflows/deliver.md +122 -131
- package/.agents/workflows/git-cleanup.md +3 -4
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +52 -40
- package/.agents/workflows/helpers/code-review.md +70 -193
- package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +12 -14
- package/.agents/workflows/helpers/{single-story-deliver.md → deliver-story.md} +113 -139
- package/.agents/workflows/helpers/diagnose.md +10 -10
- package/.agents/workflows/helpers/mandrel-sync-config.md +1 -1
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/helpers/signals.md +16 -16
- package/.agents/workflows/helpers/worktree-lifecycle.md +48 -64
- package/.agents/workflows/mandrel-update.md +3 -2
- package/.agents/workflows/plan.md +112 -145
- package/.agents/workflows/qa-assist.md +24 -30
- package/.agents/workflows/qa-explore.md +29 -38
- package/.agents/workflows/qa-run.md +2 -2
- package/README.md +9 -8
- package/docs/CHANGELOG.md +46 -0
- package/lib/cli/registry.js +95 -0
- package/lib/migrations/index.js +6 -5
- package/package.json +5 -3
- package/.agents/personas/architect.md +0 -113
- package/.agents/personas/devops-engineer.md +0 -38
- package/.agents/personas/engineer-mobile.md +0 -120
- package/.agents/personas/engineer-web.md +0 -111
- package/.agents/personas/engineer.md +0 -119
- package/.agents/personas/product.md +0 -94
- package/.agents/personas/project-manager.md +0 -114
- package/.agents/personas/qa-engineer.md +0 -95
- package/.agents/personas/refactorer.md +0 -113
- package/.agents/personas/security-engineer.md +0 -112
- package/.agents/personas/sre.md +0 -86
- package/.agents/personas/technical-writer.md +0 -101
- package/.agents/personas/ux-designer.md +0 -95
- package/.agents/schemas/dispatch-manifest.json +0 -232
- package/.agents/schemas/epic-spec.schema.json +0 -153
- package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
- package/.agents/scripts/dispatcher.js +0 -295
- package/.agents/scripts/epic-audit-prepare.js +0 -497
- package/.agents/scripts/epic-audit-recheck.js +0 -274
- package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
- package/.agents/scripts/epic-deliver-preflight.js +0 -462
- package/.agents/scripts/epic-deliver-prepare.js +0 -590
- package/.agents/scripts/epic-execute-record-wave.js +0 -449
- package/.agents/scripts/epic-plan-clarity.js +0 -211
- package/.agents/scripts/epic-plan-decompose.js +0 -54
- package/.agents/scripts/epic-plan-healthcheck.js +0 -581
- package/.agents/scripts/epic-plan-spec.js +0 -64
- package/.agents/scripts/epic-reconcile.js +0 -625
- package/.agents/scripts/lib/baseline-snapshot.js +0 -979
- package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
- package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
- package/.agents/scripts/lib/config/lifecycle.js +0 -40
- package/.agents/scripts/lib/config/preflight.js +0 -58
- package/.agents/scripts/lib/config/retro.js +0 -77
- package/.agents/scripts/lib/epic-merge-lock.js +0 -322
- package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
- package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -660
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -388
- package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
- package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
- package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
- package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
- package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
- package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
- package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -384
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -278
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
- package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
- package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
- package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
- package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
- package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
- package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
- package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
- package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +0 -109
- package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
- package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
- package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
- package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
- package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
- package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
- package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
- package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
- package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
- package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
- package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
- package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -301
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
- package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
- package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
- package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
- package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
- package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
- package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
- package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
- package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
- package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
- package/.agents/scripts/lib/push-epic-retry.js +0 -209
- package/.agents/scripts/lib/spec/index.js +0 -36
- package/.agents/scripts/lib/spec/loader.js +0 -425
- package/.agents/scripts/lib/spec/state.js +0 -208
- package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
- package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
- package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
- package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
- package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
- package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
- package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
- package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
- package/.agents/scripts/lib/wave-runner/tick.js +0 -754
- package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
- package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
- package/.agents/scripts/lifecycle-emit.js +0 -510
- package/.agents/scripts/plan-critics.js +0 -199
- package/.agents/scripts/retro-run.js +0 -218
- package/.agents/scripts/standalone-feedback-rollup.js +0 -188
- package/.agents/scripts/story-close.js +0 -294
- package/.agents/scripts/story-init.js +0 -599
- package/.agents/scripts/story-phase.js +0 -369
- package/.agents/scripts/wave-tick.js +0 -335
- package/.agents/skills/core/baseline-refresh/SKILL.md +0 -181
- package/.agents/skills/core/ci-cd-and-automation/SKILL.md +0 -274
- package/.agents/skills/core/ci-cd-and-automation/examples.md +0 -211
- package/.agents/skills/core/code-simplification/SKILL.md +0 -389
- package/.agents/skills/core/context-engineering/SKILL.md +0 -309
- package/.agents/skills/core/context-engineering/examples.md +0 -58
- package/.agents/skills/core/deprecation-and-migration/SKILL.md +0 -250
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
- package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
- package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
- package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -413
- package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
- package/.agents/skills/core/frontend-ui-engineering/SKILL.md +0 -357
- package/.agents/skills/core/hydrate-context/SKILL.md +0 -123
- package/.agents/skills/core/idea-refinement/examples.md +0 -437
- package/.agents/skills/core/idea-refinement/frameworks.md +0 -135
- package/.agents/skills/core/incremental-implementation/SKILL.md +0 -271
- package/.agents/skills/core/introducing-a-baseline-gate/SKILL.md +0 -213
- package/.agents/skills/core/knowledge-transfer/SKILL.md +0 -180
- package/.agents/skills/core/mutation-survivor-remediation/SKILL.md +0 -117
- package/.agents/skills/core/performance-optimization/SKILL.md +0 -314
- package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +0 -277
- package/.agents/skills/core/property-based-testing/SKILL.md +0 -148
- package/.agents/skills/core/refactoring-discipline/SKILL.md +0 -111
- package/.agents/skills/core/shipping-and-launch/SKILL.md +0 -328
- package/.agents/skills/core/spec-driven-development/SKILL.md +0 -252
- package/.agents/skills/core/test-driven-development/SKILL.md +0 -475
- package/.agents/skills/core/using-agent-skills/SKILL.md +0 -232
- package/.agents/skills/stack/architecture/monorepo-path-strategist/SKILL.md +0 -31
- package/.agents/skills/stack/architecture/structured-output-zod/SKILL.md +0 -51
- package/.agents/skills/stack/architecture/subagent-orchestration/SKILL.md +0 -76
- package/.agents/skills/stack/backend/cloudflare-hono-architect/SKILL.md +0 -31
- package/.agents/skills/stack/backend/cloudflare-hono-architect/examples/route-template.ts +0 -33
- package/.agents/skills/stack/backend/cloudflare-queue-manager/SKILL.md +0 -31
- package/.agents/skills/stack/backend/cloudflare-workers/SKILL.md +0 -51
- package/.agents/skills/stack/backend/highlevel-crm/SKILL.md +0 -54
- package/.agents/skills/stack/backend/sqlite-drizzle-expert/SKILL.md +0 -29
- package/.agents/skills/stack/backend/sqlite-drizzle-expert/examples/schema-template.ts +0 -30
- package/.agents/skills/stack/backend/stripe-integration/SKILL.md +0 -57
- package/.agents/skills/stack/backend/stripe-integration/scripts/listen-stripe.sh +0 -9
- package/.agents/skills/stack/backend/turso-sqlite/SKILL.md +0 -48
- package/.agents/skills/stack/frontend/astro/SKILL.md +0 -62
- package/.agents/skills/stack/frontend/astro-react-island-strategist/SKILL.md +0 -30
- package/.agents/skills/stack/frontend/expo-react-native-developer/SKILL.md +0 -29
- package/.agents/skills/stack/frontend/google-analytics-v4/SKILL.md +0 -50
- package/.agents/skills/stack/frontend/tailwind-v4/SKILL.md +0 -58
- package/.agents/skills/stack/frontend/ui-accessibility-engineer/SKILL.md +0 -34
- package/.agents/skills/stack/qa/audit-accessibility/SKILL.md +0 -51
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +0 -199
- package/.agents/skills/stack/security/backend-security-patterns/SKILL.md +0 -68
- package/.agents/workflows/helpers/deliver-epic-reference.md +0 -534
- package/.agents/workflows/helpers/deliver-epic.md +0 -955
- package/.agents/workflows/helpers/deliver-stories.md +0 -440
- package/.agents/workflows/helpers/epic-audit.md +0 -189
- package/.agents/workflows/helpers/epic-deliver-story.md +0 -427
- package/.agents/workflows/helpers/epic-testing.md +0 -125
- package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
- package/.agents/workflows/helpers/plan-epic.md +0 -351
- package/.agents/workflows/helpers/plan-story.md +0 -251
- package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
- /package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +0 -0
|
@@ -1,984 +0,0 @@
|
|
|
1
|
-
// .agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js
|
|
2
|
-
/**
|
|
3
|
-
* AutomergePredicate — lifecycle listener that decides whether the Epic
|
|
4
|
-
* PR is safe to auto-merge after the required-check watch settles.
|
|
5
|
-
* Story #2256 / Task #2260 (Epic #2172); inlined from the now-deleted
|
|
6
|
-
* legacy `automerge-predicate` module in Story #2415 (Epic #2307).
|
|
7
|
-
* Rewritten in Story #4361 (Epic #4355) so green required CI is the
|
|
8
|
-
* arming signal, gated by the `delivery.ci.autoMerge` policy.
|
|
9
|
-
*
|
|
10
|
-
* Subscribes to:
|
|
11
|
-
* - `epic.automerge.start` (production path, Story #3901) → the
|
|
12
|
-
* `/deliver` Phase 8.5 boundary that the `lifecycle-emit.js`
|
|
13
|
-
* CLI actually fires. This event carries `prUrl` but NO
|
|
14
|
-
* `checkOutcomes`. Before Story #4361 the listener trusted Phase 8
|
|
15
|
-
* (`pr-watch-with-update.js`) to have polled every required check to
|
|
16
|
-
* green and therefore skipped any CI probe on this path. That left a
|
|
17
|
-
* hole (Story #3901): if the Phase 8 watch was interrupted (host
|
|
18
|
-
* crash, `/loop` handoff, operator resume) the predicate could arm
|
|
19
|
-
* merge with red or pending required checks. Story #4361 closes it —
|
|
20
|
-
* the listener now runs a LIVE `gh pr checks --required` probe on
|
|
21
|
-
* `epic.automerge.start` and refuses to arm when any required check
|
|
22
|
-
* is not green, regardless of what Phase 8 believed.
|
|
23
|
-
* - `epic.watch.end` (test-only `Watcher` path) → carries an
|
|
24
|
-
* all-settled `checkOutcomes` map. Any non-passing required check
|
|
25
|
-
* is a hard block evaluated BEFORE the structured-signal evaluator.
|
|
26
|
-
* On this path the pre-supplied `checkOutcomes` map IS the CI truth,
|
|
27
|
-
* so no live probe is issued (the map is authoritative).
|
|
28
|
-
*
|
|
29
|
-
* Policy (`delivery.ci.autoMerge`, Story #4356 / #4361):
|
|
30
|
-
* - `"trust-ci"` (framework default) — green required CI is the arming
|
|
31
|
-
* signal. The ONLY structured conditions that block arming are an
|
|
32
|
-
* unresolved 🔴 critical (red) code-review finding or an
|
|
33
|
-
* `agent::blocked` state (a story-level blocker recorded in
|
|
34
|
-
* run-state, or a missing run-state checkpoint we cannot certify).
|
|
35
|
-
* Manual interventions, 🟠 warning-level findings, and a non-clean
|
|
36
|
-
* retro are RECORDED for audit (surfaced on the verdict and the
|
|
37
|
-
* classification log) but no longer block the merge.
|
|
38
|
-
* - `"strict"` — restores the prior clean-sprint predicate EXACTLY:
|
|
39
|
-
* zero manual interventions, every Story done, no story blockers,
|
|
40
|
-
* no 🔴/🟠 review findings, and a machine-readable `cleanSprint`
|
|
41
|
-
* retro trailer. Any dirty signal blocks.
|
|
42
|
-
*
|
|
43
|
-
* Code-review parse-miss policy (Story #4222): a code-review comment that is
|
|
44
|
-
* present but whose severity bullets cannot be parsed is treated as a DISTINCT
|
|
45
|
-
* condition — surfaced via the `codeReviewUnparseable` signal — and FAILS OPEN
|
|
46
|
-
* rather than blocking. Failing closed on a format miss is indistinguishable,
|
|
47
|
-
* to the operator and to downstream telemetry, from a real disqualifying
|
|
48
|
-
* finding; a parser miss must never masquerade as "the signal said no" inside
|
|
49
|
-
* a generic `epic.merge.blocked`. Genuine critical/high findings still block
|
|
50
|
-
* (per policy), because those require the counts to have parsed.
|
|
51
|
-
*
|
|
52
|
-
* Idempotency contract (AC-10): per-instance `Set<string>` of
|
|
53
|
-
* `${event}:${seqId}` keys. A repeat `(event, seqId)` short-circuits
|
|
54
|
-
* without re-evaluating and emits nothing. The evaluator is read-only
|
|
55
|
-
* on GitHub state, so re-running it is safe; the seqId guard is the
|
|
56
|
-
* defence against double-emit.
|
|
57
|
-
*
|
|
58
|
-
* Side-effect firewall: the listener calls the read-only evaluator, runs
|
|
59
|
-
* the read-only `gh pr checks` probe, and emits on the bus. It does NOT
|
|
60
|
-
* mutate labels, post comments, or call `notify`. Downstream consumers
|
|
61
|
-
* (`AutomergeArmer` on `epic.merge.ready`; LabelTransitioner /
|
|
62
|
-
* StructuredCommentPoster on `epic.merge.blocked`) own those side
|
|
63
|
-
* effects. AutomergeArmer remains the SOLE site that shells `gh pr merge`
|
|
64
|
-
* (the merge-lockout lint in `check-lifecycle-lint.js` enforces this).
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
|
-
import { spawnSync } from 'node:child_process';
|
|
68
|
-
|
|
69
|
-
import { hasSurvivingCritical } from '../../../audit-suite/findings.js';
|
|
70
|
-
import { getCiDelivery } from '../../../config/ci.js';
|
|
71
|
-
import { parsePrNumberFromUrl } from '../../../github-url.js';
|
|
72
|
-
import * as epicRunStateStore from '../../epic-run-state-store.js';
|
|
73
|
-
import { findStructuredComment } from '../../ticketing.js';
|
|
74
|
-
import { emitMergeUnlanded } from '../emit-merge-unlanded.js';
|
|
75
|
-
import { normalizeCheckState, RECOGNIZED_CHECK_STATES } from './watcher.js';
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Outcomes that count as "this required check did not block the merge".
|
|
79
|
-
* `'neutral'` and `'skipped'` are non-failures by GitHub's own
|
|
80
|
-
* convention; `'success'` is the happy path.
|
|
81
|
-
*
|
|
82
|
-
* Pure — exported for tests.
|
|
83
|
-
*/
|
|
84
|
-
export const NON_FAILING_CHECK_OUTCOMES = Object.freeze(
|
|
85
|
-
new Set(['success', 'neutral', 'skipped']),
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Reason categories emitted by the structured-signal evaluator. The
|
|
90
|
-
* policy layer (`applyAutoMergePolicy`) decides which categories block
|
|
91
|
-
* arming under each `delivery.ci.autoMerge` posture. Pure constant —
|
|
92
|
-
* exported so the taxonomy is reviewable as code.
|
|
93
|
-
*
|
|
94
|
-
* - `criticalReview` — an unresolved 🔴 critical (red) code-review
|
|
95
|
-
* finding. Blocks under BOTH policies.
|
|
96
|
-
* - `blockedState` — an `agent::blocked` state (story-level blocker
|
|
97
|
-
* recorded in run-state, non-done stories, or a missing run-state
|
|
98
|
-
* checkpoint). Blocks under BOTH policies.
|
|
99
|
-
* - `warningReview` — a 🟠 warning-level (high-risk) code-review
|
|
100
|
-
* finding. Blocks under `strict` only.
|
|
101
|
-
* - `intervention` — a recorded manual intervention. Blocks under
|
|
102
|
-
* `strict` only.
|
|
103
|
-
* - `retro` — a non-clean / missing retro verdict trailer.
|
|
104
|
-
* Blocks under `strict` only.
|
|
105
|
-
*/
|
|
106
|
-
export const REASON_CATEGORY = Object.freeze({
|
|
107
|
-
CRITICAL_REVIEW: 'criticalReview',
|
|
108
|
-
BLOCKED_STATE: 'blockedState',
|
|
109
|
-
WARNING_REVIEW: 'warningReview',
|
|
110
|
-
INTERVENTION: 'intervention',
|
|
111
|
-
RETRO: 'retro',
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Categories that block arming under the default `"trust-ci"` policy.
|
|
116
|
-
* Everything else is recorded for audit but does not block. Pure —
|
|
117
|
-
* exported for tests.
|
|
118
|
-
*/
|
|
119
|
-
export const TRUST_CI_BLOCKING_CATEGORIES = Object.freeze(
|
|
120
|
-
new Set([REASON_CATEGORY.CRITICAL_REVIEW, REASON_CATEGORY.BLOCKED_STATE]),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Regex that extracts the machine-readable auto-merge verdict trailer
|
|
125
|
-
* emitted by the retro body composer (`retro/phases/compose-body.js`,
|
|
126
|
-
* Story #3901). Shape:
|
|
127
|
-
* `<!-- automerge-verdict: {"cleanSprint":true,"scorecard":{…}} -->`
|
|
128
|
-
*
|
|
129
|
-
* Reading a parsed JSON boolean replaces the pre-#3901 emoji
|
|
130
|
-
* `.includes('🟢 Clean sprint')` string-match — a brittle prose scan
|
|
131
|
-
* that false-positived on any retro that quoted the marker and
|
|
132
|
-
* false-negatived on any compact-body copy edit. Pure — exported for
|
|
133
|
-
* tests so the trailer contract is reviewable as code.
|
|
134
|
-
*/
|
|
135
|
-
export const AUTOMERGE_VERDICT_TRAILER_RE =
|
|
136
|
-
/<!--\s*automerge-verdict:\s*(\{[\s\S]*?\})\s*-->/;
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Parse the machine-readable auto-merge verdict trailer out of a retro
|
|
140
|
-
* body. Returns the decoded object on success, or `null` when the
|
|
141
|
-
* trailer is absent or its JSON payload is malformed (a malformed
|
|
142
|
-
* trailer is treated as "no verdict", which downstream disqualifies
|
|
143
|
-
* the Epic rather than silently passing). Pure — exported for tests.
|
|
144
|
-
*
|
|
145
|
-
* @param {string} body
|
|
146
|
-
* @returns {{ cleanSprint?: boolean, scorecard?: object } | null}
|
|
147
|
-
*/
|
|
148
|
-
export function parseAutomergeVerdictTrailer(body) {
|
|
149
|
-
if (typeof body !== 'string' || body.length === 0) return null;
|
|
150
|
-
const m = AUTOMERGE_VERDICT_TRAILER_RE.exec(body);
|
|
151
|
-
if (!m) return null;
|
|
152
|
-
try {
|
|
153
|
-
const parsed = JSON.parse(m[1]);
|
|
154
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
155
|
-
? parsed
|
|
156
|
-
: null;
|
|
157
|
-
} catch {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Reduce a `checkOutcomes` map to the list of names that did NOT pass.
|
|
164
|
-
* Pure — exported for tests so the failure-classification rule is
|
|
165
|
-
* reviewable as code. Returns `[]` for an all-green map.
|
|
166
|
-
*/
|
|
167
|
-
export function listFailingChecks(checkOutcomes) {
|
|
168
|
-
const failures = [];
|
|
169
|
-
for (const [name, outcome] of Object.entries(checkOutcomes ?? {})) {
|
|
170
|
-
if (!NON_FAILING_CHECK_OUTCOMES.has(outcome)) {
|
|
171
|
-
failures.push({ name, outcome });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return failures;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Format a non-empty failing-check list into a single-line `reason`
|
|
179
|
-
* string for the `epic.merge.blocked` emit. Pure — exported for tests.
|
|
180
|
-
*/
|
|
181
|
-
export function formatCheckFailureReason(failures) {
|
|
182
|
-
const parts = failures.slice(0, 5).map((f) => `${f.name}=${f.outcome}`);
|
|
183
|
-
const suffix = failures.length > 5 ? `; +${failures.length - 5} more` : '';
|
|
184
|
-
return `required checks not green: ${parts.join(', ')}${suffix}`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Default live `gh pr checks --required` probe. Mirrors the Watcher's
|
|
189
|
-
* spawn shape (`--json name,state,bucket,workflow`) so the required-set
|
|
190
|
-
* projection is identical. Pure-spawn helper — exported so tests can
|
|
191
|
-
* stub the shell-out. Returns the raw spawn envelope; the caller
|
|
192
|
-
* classifies the payload.
|
|
193
|
-
*
|
|
194
|
-
* @param {{ prUrl: string, cwd?: string, spawnFn?: typeof spawnSync }} opts
|
|
195
|
-
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
196
|
-
*/
|
|
197
|
-
export function probeRequiredChecks({ prUrl, cwd, spawnFn = spawnSync }) {
|
|
198
|
-
const result = spawnFn(
|
|
199
|
-
'gh',
|
|
200
|
-
['pr', 'checks', prUrl, '--required', '--json', 'name,state,bucket'],
|
|
201
|
-
{ cwd, encoding: 'utf-8', shell: false },
|
|
202
|
-
);
|
|
203
|
-
return {
|
|
204
|
-
status: result.status ?? 1,
|
|
205
|
-
stdout: result.stdout ?? '',
|
|
206
|
-
stderr: result.stderr ?? '',
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Classify a live `gh pr checks --required` probe envelope into a
|
|
212
|
-
* `{ ok, reason, outcomes }` verdict. Pure — exported for tests.
|
|
213
|
-
*
|
|
214
|
-
* - `ok: true` → every required check is green (non-failing) AND the
|
|
215
|
-
* probe returned a parseable, non-empty required-check set.
|
|
216
|
-
* - `ok: false` → at least one required check is not green, the probe
|
|
217
|
-
* shelled out non-zero, or the payload could not be parsed. A probe
|
|
218
|
-
* we cannot read is a hard block: we NEVER arm merge on an
|
|
219
|
-
* unreadable CI signal (fail closed on the CI gate specifically —
|
|
220
|
-
* the opposite of the code-review parse-miss policy, because CI
|
|
221
|
-
* greenness is the whole point of the trust-ci arming signal).
|
|
222
|
-
*
|
|
223
|
-
* `gh pr checks --required` exits non-zero (status 8) when a required
|
|
224
|
-
* check has failed OR is still pending, so a non-zero status is treated
|
|
225
|
-
* as "not green" only when we cannot otherwise read a clean set from
|
|
226
|
-
* stdout. We parse stdout first (it is populated even on the non-zero
|
|
227
|
-
* exit) and classify from the outcomes.
|
|
228
|
-
*
|
|
229
|
-
* Story #4472 — checks-less repos. In a repo with zero required checks
|
|
230
|
-
* (no branch protection, or protection that requires no status checks),
|
|
231
|
-
* `gh pr checks --required` writes NOTHING to stdout and reports
|
|
232
|
-
* `no checks reported on the <branch> branch` to stderr with a non-zero
|
|
233
|
-
* exit. That is the SAME empty-parsed-set condition the outcomes loop
|
|
234
|
-
* below already treats as green — there is simply nothing to gate on — so
|
|
235
|
-
* we must not conflate it with a genuine probe failure (auth, network, no
|
|
236
|
-
* PR). We detect the `no checks reported` stderr signature and return
|
|
237
|
-
* green, UNLESS the consumer opted into `delivery.ci.requireChecks`, in
|
|
238
|
-
* which case the absent CI gate is a deliberate hard block.
|
|
239
|
-
*
|
|
240
|
-
* @param {{ status: number, stdout: string, stderr: string }} probe
|
|
241
|
-
* @param {{ requireChecks?: boolean }} [opts] When `requireChecks` is
|
|
242
|
-
* true, a checks-less repo fails closed instead of arming.
|
|
243
|
-
* @returns {{ ok: boolean, reason: string|null, outcomes: Record<string, string> }}
|
|
244
|
-
*/
|
|
245
|
-
export function classifyRequiredChecksProbe(
|
|
246
|
-
probe,
|
|
247
|
-
{ requireChecks = false } = {},
|
|
248
|
-
) {
|
|
249
|
-
const stdout = String(probe?.stdout ?? '').trim();
|
|
250
|
-
const stderr = String(probe?.stderr ?? '').trim();
|
|
251
|
-
// Empty stdout: either a checks-less repo (green, nothing to gate on) or
|
|
252
|
-
// a genuine probe failure. The `no checks reported` stderr signature
|
|
253
|
-
// distinguishes them.
|
|
254
|
-
if (stdout.length === 0) {
|
|
255
|
-
const noChecksReported = /no checks reported/i.test(stderr);
|
|
256
|
-
if (noChecksReported && !requireChecks) {
|
|
257
|
-
// Zero required checks configured — matches the empty-parsed-set
|
|
258
|
-
// "treated as green" branch below. Nothing to gate on.
|
|
259
|
-
return { ok: true, reason: null, outcomes: {} };
|
|
260
|
-
}
|
|
261
|
-
if (noChecksReported && requireChecks) {
|
|
262
|
-
return {
|
|
263
|
-
ok: false,
|
|
264
|
-
reason:
|
|
265
|
-
'no required checks reported and delivery.ci.requireChecks is set — failing closed per policy',
|
|
266
|
-
outcomes: {},
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
ok: false,
|
|
271
|
-
reason:
|
|
272
|
-
`live required-check probe failed (status=${probe?.status ?? 'unknown'})` +
|
|
273
|
-
(stderr ? `: ${stderr.slice(0, 200)}` : ''),
|
|
274
|
-
outcomes: {},
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
let entries;
|
|
278
|
-
try {
|
|
279
|
-
const parsed = JSON.parse(stdout);
|
|
280
|
-
entries = Array.isArray(parsed) ? parsed : null;
|
|
281
|
-
} catch {
|
|
282
|
-
entries = null;
|
|
283
|
-
}
|
|
284
|
-
if (!entries) {
|
|
285
|
-
return {
|
|
286
|
-
ok: false,
|
|
287
|
-
reason: 'live required-check probe returned an unparseable payload',
|
|
288
|
-
outcomes: {},
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
const outcomes = {};
|
|
292
|
-
for (const e of entries) {
|
|
293
|
-
if (e && typeof e === 'object' && typeof e.name === 'string') {
|
|
294
|
-
const raw = String(e.state || e.bucket || '')
|
|
295
|
-
.trim()
|
|
296
|
-
.toLowerCase();
|
|
297
|
-
// Fail closed on the arming probe: a token we have not enumerated
|
|
298
|
-
// could be a genuinely-failing GitHub conclusion. normalizeCheckState
|
|
299
|
-
// collapses unknowns to 'skipped' (safe for the watch path, unsafe
|
|
300
|
-
// here), so map an unrecognized token to 'unknown' — which is not in
|
|
301
|
-
// NON_FAILING_CHECK_OUTCOMES and therefore blocks arming.
|
|
302
|
-
outcomes[e.name] = RECOGNIZED_CHECK_STATES.has(raw)
|
|
303
|
-
? normalizeCheckState(raw)
|
|
304
|
-
: 'unknown';
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// An empty required set (no required checks configured) is treated as
|
|
308
|
-
// green — there is nothing to gate on. This matches branch protection
|
|
309
|
-
// that requires no status checks.
|
|
310
|
-
const failing = [];
|
|
311
|
-
for (const [name, outcome] of Object.entries(outcomes)) {
|
|
312
|
-
// `pending` is NOT in NON_FAILING — a still-running required check
|
|
313
|
-
// blocks arming (the Phase 8 watch was interrupted before green).
|
|
314
|
-
if (!NON_FAILING_CHECK_OUTCOMES.has(outcome)) {
|
|
315
|
-
failing.push({ name, outcome });
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (failing.length > 0) {
|
|
319
|
-
return { ok: false, reason: formatCheckFailureReason(failing), outcomes };
|
|
320
|
-
}
|
|
321
|
-
return { ok: true, reason: null, outcomes };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Regex-parse the rendered severity bullets on the code-review markdown
|
|
326
|
-
* body. Pure. Exported for tests.
|
|
327
|
-
*
|
|
328
|
-
* @param {string} body
|
|
329
|
-
* @returns {{ critical: number|null, high: number|null, medium: number|null, suggestion: number|null }}
|
|
330
|
-
*/
|
|
331
|
-
export function parseSeverityCounts(body) {
|
|
332
|
-
if (typeof body !== 'string' || body.length === 0) {
|
|
333
|
-
return { critical: null, high: null, medium: null, suggestion: null };
|
|
334
|
-
}
|
|
335
|
-
const match = (re) => {
|
|
336
|
-
const m = body.match(re);
|
|
337
|
-
if (!m) return null;
|
|
338
|
-
const n = Number.parseInt(m[1], 10);
|
|
339
|
-
return Number.isFinite(n) ? n : null;
|
|
340
|
-
};
|
|
341
|
-
return {
|
|
342
|
-
critical: match(/🔴\s*Critical Blocker:\s*(\d+)/i),
|
|
343
|
-
high: match(/🟠\s*High Risk:\s*(\d+)/i),
|
|
344
|
-
medium: match(/🟡\s*Medium Risk:\s*(\d+)/i),
|
|
345
|
-
suggestion: match(/🟢\s*Suggestion:\s*(\d+)/i),
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Push a categorized reason onto the reasons array. Each reason carries
|
|
351
|
-
* a `category` (from `REASON_CATEGORY`) so the policy layer can decide
|
|
352
|
-
* whether it blocks under the active posture, plus a human-facing
|
|
353
|
-
* `message`. Pure helper.
|
|
354
|
-
*/
|
|
355
|
-
function pushReason(reasons, category, message) {
|
|
356
|
-
reasons.push({ category, message });
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function evaluateStateSignals(state, reasons) {
|
|
360
|
-
const interventionCount = Array.isArray(state?.manualInterventions)
|
|
361
|
-
? state.manualInterventions.length
|
|
362
|
-
: 0;
|
|
363
|
-
if (!state) {
|
|
364
|
-
// A missing checkpoint means we cannot certify the run at all — this
|
|
365
|
-
// is a blocked-state condition (we do not know whether a Story is
|
|
366
|
-
// blocked), so it blocks under BOTH policies.
|
|
367
|
-
pushReason(
|
|
368
|
-
reasons,
|
|
369
|
-
REASON_CATEGORY.BLOCKED_STATE,
|
|
370
|
-
'epic-run-state checkpoint missing — cannot certify clean run',
|
|
371
|
-
);
|
|
372
|
-
} else if (interventionCount > 0) {
|
|
373
|
-
pushReason(
|
|
374
|
-
reasons,
|
|
375
|
-
REASON_CATEGORY.INTERVENTION,
|
|
376
|
-
`manual interventions recorded (${interventionCount}): ${state.manualInterventions
|
|
377
|
-
.map((i) => i.reason)
|
|
378
|
-
.slice(0, 3)
|
|
379
|
-
.join('; ')}${interventionCount > 3 ? '; …' : ''}`,
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
// Story #4155 — the ready-set runtime records a flat per-Story status map
|
|
383
|
-
// on the checkpoint (`stories: { [id]: { status, blockerCommentId? } }`)
|
|
384
|
-
// instead of a per-wave `waves[]` history. The clean-run certification
|
|
385
|
-
// reads it directly: a run is clean only when every Story reached `done`
|
|
386
|
-
// and none carries a recorded blocker comment.
|
|
387
|
-
const stories =
|
|
388
|
-
state?.stories && typeof state.stories === 'object' ? state.stories : {};
|
|
389
|
-
const storyStatuses = Object.values(stories).map(
|
|
390
|
-
(s) => s?.status ?? 'pending',
|
|
391
|
-
);
|
|
392
|
-
const nonDoneStatuses = storyStatuses.filter((s) => s !== 'done');
|
|
393
|
-
if (nonDoneStatuses.length > 0) {
|
|
394
|
-
// A story not done is an unfinished / blocked run — blocks under both.
|
|
395
|
-
pushReason(
|
|
396
|
-
reasons,
|
|
397
|
-
REASON_CATEGORY.BLOCKED_STATE,
|
|
398
|
-
`${nonDoneStatuses.length} story(ies) not done (statuses: ${nonDoneStatuses.join(', ')})`,
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
const storyBlockers = countStoryBlockers(stories);
|
|
402
|
-
if (storyBlockers > 0) {
|
|
403
|
-
// A recorded story-level blocker is the `agent::blocked` state — it
|
|
404
|
-
// blocks under both policies.
|
|
405
|
-
pushReason(
|
|
406
|
-
reasons,
|
|
407
|
-
REASON_CATEGORY.BLOCKED_STATE,
|
|
408
|
-
`${storyBlockers} story-level blocker(s) recorded in run-state`,
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
return { interventionCount, storyStatuses, storyBlockers };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Count blockers in the flat per-Story `stories` status map: each Story with
|
|
416
|
-
* a recorded `blockerCommentId` and each Story whose status is not `done`
|
|
417
|
-
* contributes one blocker (matching the prior per-wave count semantics).
|
|
418
|
-
*
|
|
419
|
-
* @param {Record<string, { status?: string, blockerCommentId?: string }>} stories
|
|
420
|
-
* @returns {number}
|
|
421
|
-
*/
|
|
422
|
-
function countStoryBlockers(stories) {
|
|
423
|
-
let blockers = 0;
|
|
424
|
-
for (const s of Object.values(stories ?? {})) {
|
|
425
|
-
if (
|
|
426
|
-
s &&
|
|
427
|
-
typeof s.blockerCommentId === 'string' &&
|
|
428
|
-
s.blockerCommentId.length > 0
|
|
429
|
-
) {
|
|
430
|
-
blockers += 1;
|
|
431
|
-
}
|
|
432
|
-
if (s?.status && s.status !== 'done') {
|
|
433
|
-
blockers += 1;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return blockers;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function evaluateCodeReviewSignals(codeReview, reasons) {
|
|
440
|
-
const codeReviewFound = !!codeReview && typeof codeReview.body === 'string';
|
|
441
|
-
const severity = codeReviewFound
|
|
442
|
-
? parseSeverityCounts(codeReview.body)
|
|
443
|
-
: { critical: null, high: null, medium: null, suggestion: null };
|
|
444
|
-
if (!codeReviewFound) {
|
|
445
|
-
// A missing code-review comment is a soft signal — it does not, on
|
|
446
|
-
// its own, prove a critical finding. Categorize it as a warning-level
|
|
447
|
-
// reason so it blocks under `strict` (which demands the clean gate)
|
|
448
|
-
// but not under `trust-ci` (which only blocks on a PARSED 🔴 count).
|
|
449
|
-
pushReason(
|
|
450
|
-
reasons,
|
|
451
|
-
REASON_CATEGORY.WARNING_REVIEW,
|
|
452
|
-
'code-review structured comment not found on Epic',
|
|
453
|
-
);
|
|
454
|
-
return { codeReviewFound, codeReviewUnparseable: false, severity };
|
|
455
|
-
}
|
|
456
|
-
// "Present but unparseable" is a DISTINCT condition from "present and says
|
|
457
|
-
// no" (Story #4222). The canonical renderer
|
|
458
|
-
// (`review-providers/findings-renderer.js`) always emits all four severity
|
|
459
|
-
// bullets, so a body whose critical/high counts we cannot extract is a
|
|
460
|
-
// FORMAT MISS, not a disqualifying signal. Failing closed here — pushing a
|
|
461
|
-
// generic block reason — is indistinguishable, to the operator and to
|
|
462
|
-
// downstream telemetry (the mandrel-bench Autonomy dimension), from a real
|
|
463
|
-
// critical finding: it stalls an otherwise-clean unattended run for a
|
|
464
|
-
// non-reason.
|
|
465
|
-
//
|
|
466
|
-
// Chosen policy: FAIL OPEN on an unparseable code-review body. We surface
|
|
467
|
-
// the condition explicitly via the `codeReviewUnparseable` signal so
|
|
468
|
-
// telemetry can tell a parser miss from a true HITL hand-off, but we do NOT
|
|
469
|
-
// add a disqualifying `reasons[]` entry — the absence of a parseable
|
|
470
|
-
// critical/high count cannot, on its own, block a run whose other signals
|
|
471
|
-
// are clean. Genuine disqualifying review findings (critical > 0 /
|
|
472
|
-
// high > 0) still block below, because those require the counts to have
|
|
473
|
-
// parsed successfully.
|
|
474
|
-
const codeReviewUnparseable =
|
|
475
|
-
severity.critical === null || severity.high === null;
|
|
476
|
-
if (codeReviewUnparseable) {
|
|
477
|
-
return { codeReviewFound, codeReviewUnparseable, severity };
|
|
478
|
-
}
|
|
479
|
-
// Route the halt-on-critical decision through the single halting rule of
|
|
480
|
-
// the unified verification-results contract (Story #4411) rather than a
|
|
481
|
-
// re-derived `critical > 0` expression. The unparseable branch above has
|
|
482
|
-
// already returned, so `severity.critical` is a concrete number here.
|
|
483
|
-
if (hasSurvivingCritical(severity)) {
|
|
484
|
-
pushReason(
|
|
485
|
-
reasons,
|
|
486
|
-
REASON_CATEGORY.CRITICAL_REVIEW,
|
|
487
|
-
`code-review has ${severity.critical} 🔴 Critical Blocker(s)`,
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
if (severity.high > 0) {
|
|
491
|
-
pushReason(
|
|
492
|
-
reasons,
|
|
493
|
-
REASON_CATEGORY.WARNING_REVIEW,
|
|
494
|
-
`code-review has ${severity.high} 🟠 High Risk finding(s)`,
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
return { codeReviewFound, codeReviewUnparseable, severity };
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function evaluateRetroSignals(retro, reasons) {
|
|
501
|
-
const retroFound = !!retro && typeof retro.body === 'string';
|
|
502
|
-
if (!retroFound) {
|
|
503
|
-
pushReason(
|
|
504
|
-
reasons,
|
|
505
|
-
REASON_CATEGORY.RETRO,
|
|
506
|
-
'retro structured comment not found on Epic',
|
|
507
|
-
);
|
|
508
|
-
return { retroFound, retroCompact: false };
|
|
509
|
-
}
|
|
510
|
-
// Read the machine-readable verdict trailer instead of string-matching
|
|
511
|
-
// the human-facing "🟢 Clean sprint" prose (Story #3901). A missing or
|
|
512
|
-
// malformed trailer is a hard disqualifier under `strict` — we never arm
|
|
513
|
-
// strict-policy auto-merge on a retro whose verdict we cannot read.
|
|
514
|
-
const verdict = parseAutomergeVerdictTrailer(retro.body);
|
|
515
|
-
if (!verdict) {
|
|
516
|
-
pushReason(
|
|
517
|
-
reasons,
|
|
518
|
-
REASON_CATEGORY.RETRO,
|
|
519
|
-
'retro is missing the machine-readable automerge-verdict trailer (cannot certify clean sprint)',
|
|
520
|
-
);
|
|
521
|
-
return { retroFound, retroCompact: false };
|
|
522
|
-
}
|
|
523
|
-
const retroCompact = verdict.cleanSprint === true;
|
|
524
|
-
if (!retroCompact) {
|
|
525
|
-
pushReason(
|
|
526
|
-
reasons,
|
|
527
|
-
REASON_CATEGORY.RETRO,
|
|
528
|
-
'retro automerge-verdict trailer reports cleanSprint=false (full retro indicates friction / parked / interventions)',
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
return { retroFound, retroCompact };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Pure verdict-from-signals function. Composes the three signal sources into
|
|
536
|
-
* a single envelope. The `clean` boolean is the strict-policy verdict (true
|
|
537
|
-
* iff there are zero reasons of any category) — it is preserved so the
|
|
538
|
-
* `strict` policy restores the prior predicate EXACTLY. `reasons` is the
|
|
539
|
-
* flat string[] of human-facing messages (byte-identical to the pre-#4361
|
|
540
|
-
* output for the same inputs); `categorizedReasons` is the same list tagged
|
|
541
|
-
* with a `REASON_CATEGORY` so the policy filter (`applyAutoMergePolicy`) can
|
|
542
|
-
* narrow which reasons block per posture. Exported for tests.
|
|
543
|
-
*
|
|
544
|
-
* @param {{
|
|
545
|
-
* state: object|null,
|
|
546
|
-
* codeReview: { body: string }|null,
|
|
547
|
-
* retro: { body: string }|null,
|
|
548
|
-
* }} input
|
|
549
|
-
* @returns {{
|
|
550
|
-
* clean: boolean,
|
|
551
|
-
* reasons: string[],
|
|
552
|
-
* categorizedReasons: Array<{ category: string, message: string }>,
|
|
553
|
-
* signals: {
|
|
554
|
-
* manualInterventions: number,
|
|
555
|
-
* storyStatuses: string[],
|
|
556
|
-
* storyBlockers: number,
|
|
557
|
-
* severity: { critical: number|null, high: number|null, medium: number|null, suggestion: number|null },
|
|
558
|
-
* codeReviewUnparseable: boolean,
|
|
559
|
-
* retroCompact: boolean,
|
|
560
|
-
* codeReviewFound: boolean,
|
|
561
|
-
* retroFound: boolean,
|
|
562
|
-
* stateFound: boolean,
|
|
563
|
-
* },
|
|
564
|
-
* }}
|
|
565
|
-
*/
|
|
566
|
-
export function deriveAutoMergeVerdict({ state, codeReview, retro }) {
|
|
567
|
-
const categorizedReasons = [];
|
|
568
|
-
const stateSig = evaluateStateSignals(state, categorizedReasons);
|
|
569
|
-
const reviewSig = evaluateCodeReviewSignals(codeReview, categorizedReasons);
|
|
570
|
-
const retroSig = evaluateRetroSignals(retro, categorizedReasons);
|
|
571
|
-
|
|
572
|
-
return {
|
|
573
|
-
// `clean` is the STRICT verdict: no reason of any category. This is the
|
|
574
|
-
// exact pre-#4361 predicate, preserved so `strict` policy is unchanged.
|
|
575
|
-
clean: categorizedReasons.length === 0,
|
|
576
|
-
// Flat string[] — byte-identical to the pre-#4361 reason messages.
|
|
577
|
-
reasons: categorizedReasons.map((r) => r.message),
|
|
578
|
-
categorizedReasons,
|
|
579
|
-
signals: {
|
|
580
|
-
manualInterventions: stateSig.interventionCount,
|
|
581
|
-
storyStatuses: stateSig.storyStatuses,
|
|
582
|
-
storyBlockers: stateSig.storyBlockers,
|
|
583
|
-
severity: reviewSig.severity,
|
|
584
|
-
codeReviewUnparseable: reviewSig.codeReviewUnparseable,
|
|
585
|
-
retroCompact: retroSig.retroCompact,
|
|
586
|
-
codeReviewFound: reviewSig.codeReviewFound,
|
|
587
|
-
retroFound: retroSig.retroFound,
|
|
588
|
-
stateFound: !!state,
|
|
589
|
-
},
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Apply the `delivery.ci.autoMerge` policy to a categorized verdict.
|
|
595
|
-
* Returns the EFFECTIVE arming decision plus the split of blocking vs.
|
|
596
|
-
* recorded-only reasons. Pure — exported for tests.
|
|
597
|
-
*
|
|
598
|
-
* - `"strict"` → every reason blocks (identical to the pre-#4361
|
|
599
|
-
* `clean` verdict). `recordedReasons` is empty.
|
|
600
|
-
* - `"trust-ci"` → only `criticalReview` / `blockedState` reasons
|
|
601
|
-
* block; the rest (interventions, warnings, retro) land in
|
|
602
|
-
* `recordedReasons` for audit and do NOT gate the merge.
|
|
603
|
-
*
|
|
604
|
-
* @param {{ clean: boolean, categorizedReasons: Array<{ category: string, message: string }> }} verdict
|
|
605
|
-
* @param {'trust-ci'|'strict'} policy
|
|
606
|
-
* @returns {{
|
|
607
|
-
* arm: boolean,
|
|
608
|
-
* policy: 'trust-ci'|'strict',
|
|
609
|
-
* blockingReasons: Array<{ category: string, message: string }>,
|
|
610
|
-
* recordedReasons: Array<{ category: string, message: string }>,
|
|
611
|
-
* }}
|
|
612
|
-
*/
|
|
613
|
-
export function applyAutoMergePolicy(verdict, policy) {
|
|
614
|
-
const reasons = Array.isArray(verdict?.categorizedReasons)
|
|
615
|
-
? verdict.categorizedReasons
|
|
616
|
-
: [];
|
|
617
|
-
if (policy === 'strict') {
|
|
618
|
-
return {
|
|
619
|
-
arm: reasons.length === 0,
|
|
620
|
-
policy: 'strict',
|
|
621
|
-
blockingReasons: reasons,
|
|
622
|
-
recordedReasons: [],
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
// trust-ci (default): only critical-review and blocked-state reasons gate.
|
|
626
|
-
const blockingReasons = [];
|
|
627
|
-
const recordedReasons = [];
|
|
628
|
-
for (const r of reasons) {
|
|
629
|
-
if (TRUST_CI_BLOCKING_CATEGORIES.has(r.category)) {
|
|
630
|
-
blockingReasons.push(r);
|
|
631
|
-
} else {
|
|
632
|
-
recordedReasons.push(r);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
return {
|
|
636
|
-
arm: blockingReasons.length === 0,
|
|
637
|
-
policy: 'trust-ci',
|
|
638
|
-
blockingReasons,
|
|
639
|
-
recordedReasons,
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Join a categorized reason list into a single-line `reason` string for
|
|
645
|
-
* the `epic.merge.blocked` emit / classification log. Pure.
|
|
646
|
-
*/
|
|
647
|
-
function formatReasons(reasons, prefix = '') {
|
|
648
|
-
const messages = reasons.map((r) => r.message);
|
|
649
|
-
const head = messages.slice(0, 3).join('; ');
|
|
650
|
-
const suffix = messages.length > 3 ? `; +${messages.length - 3} more` : '';
|
|
651
|
-
return `${prefix}${head}${suffix}`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* IO-bound entry. Loads all three signal sources from the structured-comment
|
|
656
|
-
* surface on the Epic ticket and hands them to `deriveAutoMergeVerdict`.
|
|
657
|
-
* DI-friendly via the `findCommentFn` and `readRunStateFn` hooks; both
|
|
658
|
-
* default to the production stack (the `epic-run-state-store.read` function
|
|
659
|
-
* replaces the previous `checkpointerFactory` indirection introduced by the
|
|
660
|
-
* now-deleted `Checkpointer` class).
|
|
661
|
-
*
|
|
662
|
-
* @param {{
|
|
663
|
-
* provider: object,
|
|
664
|
-
* epicId: number,
|
|
665
|
-
* findCommentFn?: typeof findStructuredComment,
|
|
666
|
-
* readRunStateFn?: typeof epicRunStateStore.read,
|
|
667
|
-
* }} opts
|
|
668
|
-
* @returns {Promise<{ clean: boolean, reasons: object[], signals: object }>}
|
|
669
|
-
*/
|
|
670
|
-
export async function evaluateAutoMergePredicate({
|
|
671
|
-
provider,
|
|
672
|
-
epicId,
|
|
673
|
-
findCommentFn = findStructuredComment,
|
|
674
|
-
readRunStateFn = epicRunStateStore.read,
|
|
675
|
-
}) {
|
|
676
|
-
if (!provider)
|
|
677
|
-
throw new TypeError('evaluateAutoMergePredicate: provider required');
|
|
678
|
-
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
679
|
-
throw new TypeError(
|
|
680
|
-
'evaluateAutoMergePredicate: epicId must be a positive integer',
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Sequential awaits (not Promise.all) — the lifecycle lint surface forbids
|
|
685
|
-
// Promise.all under `lib/orchestration/lifecycle/**` because parallelizing
|
|
686
|
-
// listener invocations breaks bus repeatability. This evaluator is read-
|
|
687
|
-
// only IO, but the rule is directory-scoped; sequencing here is a
|
|
688
|
-
// cheap concession for living inside the listener tree.
|
|
689
|
-
const state = await readRunStateFn({ provider, epicId });
|
|
690
|
-
const codeReview = await findCommentFn(
|
|
691
|
-
provider,
|
|
692
|
-
epicId,
|
|
693
|
-
'verification-results',
|
|
694
|
-
);
|
|
695
|
-
let retro = await findCommentFn(provider, epicId, 'retro');
|
|
696
|
-
if (!retro) {
|
|
697
|
-
retro = await findCommentFn(provider, epicId, 'retro-partial');
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
return deriveAutoMergeVerdict({ state, codeReview, retro });
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* AutomergePredicate listener.
|
|
705
|
-
*/
|
|
706
|
-
export class AutomergePredicate {
|
|
707
|
-
/**
|
|
708
|
-
* @param {object} opts
|
|
709
|
-
* @param {object} opts.bus
|
|
710
|
-
* @param {number} opts.epicId
|
|
711
|
-
* @param {object} opts.provider GitHub provider (passed through to the
|
|
712
|
-
* evaluator). Required for the read of run-state + structured
|
|
713
|
-
* comments.
|
|
714
|
-
* @param {object} [opts.config] Resolved agent config. Read for the
|
|
715
|
-
* `delivery.ci.autoMerge` policy and the `delivery.ci.requireChecks`
|
|
716
|
-
* fail-closed-without-checks policy via `getCiDelivery`. Defaults to the
|
|
717
|
-
* framework defaults (`trust-ci` / `requireChecks: false`) when omitted.
|
|
718
|
-
* @param {boolean} [opts.headless] When true (a `/deliver --yes` run), a
|
|
719
|
-
* predicate refusal escalates to an explicit `merge.unlanded` +
|
|
720
|
-
* `epic.blocked` terminal instead of silently parking on the
|
|
721
|
-
* operator-merges path (Story #4472). Defaults to `false` (attended).
|
|
722
|
-
* @param {string} [opts.cwd] Working directory for the live
|
|
723
|
-
* `gh pr checks --required` probe. Defaults to `process.cwd()`.
|
|
724
|
-
* @param {Function} [opts.evaluatePredicateFn] override of
|
|
725
|
-
* `evaluateAutoMergePredicate` for tests.
|
|
726
|
-
* @param {Function} [opts.probeRequiredChecksFn] override of
|
|
727
|
-
* `probeRequiredChecks` for tests.
|
|
728
|
-
* @param {{ info?: Function, warn?: Function, debug?: Function }} [opts.logger]
|
|
729
|
-
*/
|
|
730
|
-
constructor(opts = {}) {
|
|
731
|
-
if (
|
|
732
|
-
!opts.bus ||
|
|
733
|
-
typeof opts.bus.on !== 'function' ||
|
|
734
|
-
typeof opts.bus.emit !== 'function'
|
|
735
|
-
) {
|
|
736
|
-
throw new TypeError(
|
|
737
|
-
'AutomergePredicate requires a bus with on() and emit()',
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
if (!Number.isInteger(opts.epicId) || opts.epicId < 1) {
|
|
741
|
-
throw new TypeError('AutomergePredicate requires a numeric epicId');
|
|
742
|
-
}
|
|
743
|
-
if (!opts.provider) {
|
|
744
|
-
throw new TypeError('AutomergePredicate requires a provider');
|
|
745
|
-
}
|
|
746
|
-
this.bus = opts.bus;
|
|
747
|
-
this.epicId = opts.epicId;
|
|
748
|
-
this.provider = opts.provider;
|
|
749
|
-
this.cwd = opts.cwd ?? process.cwd();
|
|
750
|
-
// Resolve the merge posture + fail-closed policy once at construction.
|
|
751
|
-
// `getCiDelivery` applies the framework defaults (`trust-ci` /
|
|
752
|
-
// `requireChecks: false`) for any omitted field.
|
|
753
|
-
const ci = getCiDelivery(opts.config ?? null);
|
|
754
|
-
this.policy = ci.autoMerge;
|
|
755
|
-
this.requireChecks = ci.requireChecks;
|
|
756
|
-
this.headless = opts.headless === true;
|
|
757
|
-
this.evaluatePredicateFn =
|
|
758
|
-
opts.evaluatePredicateFn ?? evaluateAutoMergePredicate;
|
|
759
|
-
this.probeRequiredChecksFn =
|
|
760
|
-
opts.probeRequiredChecksFn ?? probeRequiredChecks;
|
|
761
|
-
// Injected for tests so the headless terminal escalation can be
|
|
762
|
-
// observed without touching disk.
|
|
763
|
-
this.emitMergeUnlandedFn = opts.emitMergeUnlandedFn ?? emitMergeUnlanded;
|
|
764
|
-
this.logger = opts.logger ?? console;
|
|
765
|
-
/** @type {Set<string>} `${event}:${seqId}` idempotency cache. */
|
|
766
|
-
this._seen = new Set();
|
|
767
|
-
/**
|
|
768
|
-
* Classification log — every event we observe lands here with the
|
|
769
|
-
* outcome (`ready`, `blocked`, `skipped-duplicate`, `failed`).
|
|
770
|
-
* Mirrors the Finalizer / Reconciler "no silent skip" surface.
|
|
771
|
-
*/
|
|
772
|
-
this.classifications = [];
|
|
773
|
-
this.events = Object.freeze(['epic.automerge.start', 'epic.watch.end']);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
register() {
|
|
777
|
-
return this.events.map((event) =>
|
|
778
|
-
this.bus.on(event, async (ctx) => this.handle(ctx)),
|
|
779
|
-
);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
async handle({ event, seqId, payload }) {
|
|
783
|
-
const key = `${event}:${seqId}`;
|
|
784
|
-
if (this._seen.has(key)) {
|
|
785
|
-
this.classifications.push({
|
|
786
|
-
event,
|
|
787
|
-
seqId,
|
|
788
|
-
outcome: 'skipped',
|
|
789
|
-
reason: 'duplicate-seqId',
|
|
790
|
-
});
|
|
791
|
-
this.logger.debug?.(
|
|
792
|
-
`[AutomergePredicate] skip duplicate ${key} (idempotent)`,
|
|
793
|
-
);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
this._seen.add(key);
|
|
797
|
-
|
|
798
|
-
const prUrl = payload?.prUrl;
|
|
799
|
-
if (typeof prUrl !== 'string' || prUrl.length === 0) {
|
|
800
|
-
this.classifications.push({
|
|
801
|
-
event,
|
|
802
|
-
seqId,
|
|
803
|
-
outcome: 'failed',
|
|
804
|
-
reason: 'no-pr-url',
|
|
805
|
-
});
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
// Gate 1 — required-check freshness. Any non-passing required check is
|
|
809
|
-
// a hard block: short-circuit before consulting the structured-signal
|
|
810
|
-
// evaluator so the operator sees the CI failure as the reason, not a
|
|
811
|
-
// downstream signal.
|
|
812
|
-
//
|
|
813
|
-
// Two sources of CI truth:
|
|
814
|
-
// (a) `epic.watch.end` (test-only Watcher path) carries a settled
|
|
815
|
-
// `checkOutcomes` map — that map is authoritative, so we classify
|
|
816
|
-
// it directly and issue NO live probe.
|
|
817
|
-
// (b) `epic.automerge.start` (production Phase 8.5) carries no map.
|
|
818
|
-
// Story #4361: we run a LIVE `gh pr checks --required` probe here
|
|
819
|
-
// so an interrupted Phase 8 watch cannot arm merge on red/pending
|
|
820
|
-
// required checks (closes the Story #3901 hole).
|
|
821
|
-
if (payload?.checkOutcomes !== undefined) {
|
|
822
|
-
const failures = listFailingChecks(payload.checkOutcomes);
|
|
823
|
-
if (failures.length > 0) {
|
|
824
|
-
const reason = formatCheckFailureReason(failures);
|
|
825
|
-
this.classifications.push({ event, seqId, outcome: 'blocked', reason });
|
|
826
|
-
await this._emitBlocked(prUrl, reason);
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
} else {
|
|
830
|
-
// Live probe — production path. Fail closed on any non-green,
|
|
831
|
-
// pending, or unreadable required-check result.
|
|
832
|
-
let probeVerdict;
|
|
833
|
-
try {
|
|
834
|
-
const probe = this.probeRequiredChecksFn({ prUrl, cwd: this.cwd });
|
|
835
|
-
probeVerdict = classifyRequiredChecksProbe(probe, {
|
|
836
|
-
requireChecks: this.requireChecks,
|
|
837
|
-
});
|
|
838
|
-
} catch (err) {
|
|
839
|
-
probeVerdict = {
|
|
840
|
-
ok: false,
|
|
841
|
-
reason: `live required-check probe threw: ${err?.message ?? err}`,
|
|
842
|
-
outcomes: {},
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
if (!probeVerdict.ok) {
|
|
846
|
-
const reason = probeVerdict.reason ?? 'required checks not green';
|
|
847
|
-
this.classifications.push({ event, seqId, outcome: 'blocked', reason });
|
|
848
|
-
await this._emitBlocked(prUrl, reason);
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Gate 2 — structured-signal verdict, filtered by the merge policy.
|
|
854
|
-
let verdict;
|
|
855
|
-
try {
|
|
856
|
-
verdict = await this.evaluatePredicateFn({
|
|
857
|
-
provider: this.provider,
|
|
858
|
-
epicId: this.epicId,
|
|
859
|
-
});
|
|
860
|
-
} catch (err) {
|
|
861
|
-
const reason = `predicate-threw:${err?.message ?? err}`;
|
|
862
|
-
this.classifications.push({ event, seqId, outcome: 'failed', reason });
|
|
863
|
-
this.logger.warn?.(
|
|
864
|
-
`[AutomergePredicate] evaluator threw (swallowed): ${err?.message ?? err}`,
|
|
865
|
-
);
|
|
866
|
-
// Conservative: a thrown evaluator is treated as blocked rather
|
|
867
|
-
// than ready — we never arm auto-merge on uncertain signals.
|
|
868
|
-
await this._emitBlocked(prUrl, reason);
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const decision = applyAutoMergePolicy(verdict, this.policy);
|
|
873
|
-
// Surface recorded-but-non-blocking reasons for audit even when the
|
|
874
|
-
// trust-ci policy arms anyway (interventions / warnings / non-clean
|
|
875
|
-
// retro). These never gate the merge but must not be silently dropped.
|
|
876
|
-
const recorded = decision.recordedReasons ?? [];
|
|
877
|
-
|
|
878
|
-
if (decision.arm) {
|
|
879
|
-
this.classifications.push({
|
|
880
|
-
event,
|
|
881
|
-
seqId,
|
|
882
|
-
outcome: 'ready',
|
|
883
|
-
policy: decision.policy,
|
|
884
|
-
signals: verdict.signals,
|
|
885
|
-
...(recorded.length > 0
|
|
886
|
-
? { recordedReasons: recorded.map((r) => r.message) }
|
|
887
|
-
: {}),
|
|
888
|
-
});
|
|
889
|
-
if (recorded.length > 0) {
|
|
890
|
-
this.logger.info?.(
|
|
891
|
-
`[AutomergePredicate] arming under ${decision.policy}; recorded (non-blocking): ${formatReasons(recorded)}`,
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
try {
|
|
895
|
-
await this.bus.emit('epic.merge.ready', {
|
|
896
|
-
prUrl,
|
|
897
|
-
reason: `all required checks green; ${decision.policy} policy signals clear`,
|
|
898
|
-
});
|
|
899
|
-
} catch (err) {
|
|
900
|
-
this.logger.warn?.(
|
|
901
|
-
`[AutomergePredicate] epic.merge.ready emit failed (swallowed): ${err?.message ?? err}`,
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const blocking = decision.blockingReasons ?? [];
|
|
908
|
-
const reason =
|
|
909
|
-
blocking.length > 0
|
|
910
|
-
? formatReasons(blocking)
|
|
911
|
-
: 'predicate dirty (no reasons reported)';
|
|
912
|
-
this.classifications.push({
|
|
913
|
-
event,
|
|
914
|
-
seqId,
|
|
915
|
-
outcome: 'blocked',
|
|
916
|
-
policy: decision.policy,
|
|
917
|
-
reason,
|
|
918
|
-
...(recorded.length > 0
|
|
919
|
-
? { recordedReasons: recorded.map((r) => r.message) }
|
|
920
|
-
: {}),
|
|
921
|
-
});
|
|
922
|
-
await this._emitBlocked(prUrl, reason);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* Emit `epic.merge.blocked`. Helper carved out so the blocking paths
|
|
927
|
-
* (CI failure / predicate dirty / evaluator throw) share the same emit
|
|
928
|
-
* shape.
|
|
929
|
-
*
|
|
930
|
-
* Story #4472 — must-land coverage of predicate refusal. In a headless
|
|
931
|
-
* (`/deliver --yes`) run there is no operator to act on a bare
|
|
932
|
-
* `epic.merge.blocked` (nothing in the listener chain consumes it), so
|
|
933
|
-
* the run would silently park on the operator-merges path. When
|
|
934
|
-
* `this.headless`, we additionally attribute the refusal to the
|
|
935
|
-
* lifecycle ledger via `merge.unlanded` (blockClass `predicate-refused`)
|
|
936
|
-
* and drive the explicit `epic.blocked` terminal — the same
|
|
937
|
-
* escalation the MergeWatcher performs on post-arm budget exhaustion —
|
|
938
|
-
* so the Epic transitions to `agent::blocked` with an operator-visible
|
|
939
|
-
* reason instead of stalling.
|
|
940
|
-
*/
|
|
941
|
-
async _emitBlocked(prUrl, reason) {
|
|
942
|
-
try {
|
|
943
|
-
await this.bus.emit('epic.merge.blocked', { prUrl, reason });
|
|
944
|
-
} catch (err) {
|
|
945
|
-
this.logger.warn?.(
|
|
946
|
-
`[AutomergePredicate] epic.merge.blocked emit failed (swallowed): ${err?.message ?? err}`,
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
|
-
if (!this.headless) return;
|
|
950
|
-
// Ledger attribution — best-effort; a failed append must NOT mask the
|
|
951
|
-
// epic.blocked transition below.
|
|
952
|
-
try {
|
|
953
|
-
const prNumber = parsePrNumberFromUrl(prUrl);
|
|
954
|
-
if (Number.isInteger(prNumber) && prNumber > 0) {
|
|
955
|
-
this.emitMergeUnlandedFn({
|
|
956
|
-
scope: 'epic',
|
|
957
|
-
ticketId: this.epicId,
|
|
958
|
-
prNumber,
|
|
959
|
-
blockClass: 'predicate-refused',
|
|
960
|
-
reason,
|
|
961
|
-
elapsedSeconds: 0,
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
} catch (err) {
|
|
965
|
-
this.logger.warn?.(
|
|
966
|
-
`[AutomergePredicate] emitMergeUnlanded failed (swallowed): ${err?.message ?? err}`,
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
try {
|
|
970
|
-
await this.bus.emit('epic.blocked', {
|
|
971
|
-
reason: `merge-predicate:refused`,
|
|
972
|
-
});
|
|
973
|
-
} catch (err) {
|
|
974
|
-
this.logger.warn?.(
|
|
975
|
-
`[AutomergePredicate] epic.blocked emit on predicate refusal failed (swallowed): ${err?.message ?? err}`,
|
|
976
|
-
);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
reset() {
|
|
981
|
-
this._seen.clear();
|
|
982
|
-
this.classifications = [];
|
|
983
|
-
}
|
|
984
|
-
}
|