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
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* story-deliver-terminal.js — the one terminal envelope every Story
|
|
3
|
+
* close-and-land invocation emits, and the shared next-command vocabulary
|
|
4
|
+
* (Story #4543).
|
|
5
|
+
*
|
|
6
|
+
* Before this module the delivery tail had **two** divergent return
|
|
7
|
+
* contracts, both prose and neither validated:
|
|
8
|
+
* `.agents/workflows/helpers/deliver-story.md` defined one shape and
|
|
9
|
+
* `.agents/agents/story-worker.md` a different one, so a caller could not
|
|
10
|
+
* distinguish a landed Story from a parked one without re-probing GitHub.
|
|
11
|
+
* `story-deliver-terminal.schema.json` is now the SSOT both docs reference
|
|
12
|
+
* rather than restate, and this module is its only writer.
|
|
13
|
+
*
|
|
14
|
+
* Two exports carry the contract:
|
|
15
|
+
*
|
|
16
|
+
* - {@link buildTerminalEnvelope} assembles and **validates** the envelope.
|
|
17
|
+
* It throws on a schema violation rather than emitting a malformed
|
|
18
|
+
* terminal: a silently-wrong terminal is exactly the failure this Story
|
|
19
|
+
* exists to eliminate, so failing loudly at the writer is the point.
|
|
20
|
+
* - {@link NEXT_COMMANDS} is the next-command vocabulary shared with
|
|
21
|
+
* `deliver-recover.js`, so a `pending` envelope and a recovery probe
|
|
22
|
+
* name the same command for the same state instead of inventing two
|
|
23
|
+
* dialects for one condition.
|
|
24
|
+
*
|
|
25
|
+
* `TERMINAL_EXIT_CODES` maps status → process exit code. `pending` gets its
|
|
26
|
+
* **own** code (3) precisely so a caller can tell "slow CI, resume me" from
|
|
27
|
+
* "hard block, come look" without parsing stdout — the distinction the
|
|
28
|
+
* pre-#4543 pipeline collapsed by treating budget exhaustion as a block.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFileSync } from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
36
|
+
import addFormats from 'ajv-formats';
|
|
37
|
+
|
|
38
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const SCHEMA_PATH = path.resolve(
|
|
40
|
+
__dirname,
|
|
41
|
+
'..',
|
|
42
|
+
'..',
|
|
43
|
+
'..',
|
|
44
|
+
'schemas',
|
|
45
|
+
'story-deliver-terminal.schema.json',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const TERMINAL_ENVELOPE_KIND = 'story-deliver-terminal';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* status → process exit code.
|
|
52
|
+
*
|
|
53
|
+
* `pending` is **3**, not 0 and not 1. Zero would tell a headless caller the
|
|
54
|
+
* Story landed when it did not; one would conflate a resumable slow-CI wait
|
|
55
|
+
* with a hard block and send the operator to diagnose branch protection that
|
|
56
|
+
* is working fine. A distinct code is what lets `/deliver` resume without
|
|
57
|
+
* classifying.
|
|
58
|
+
*/
|
|
59
|
+
export const TERMINAL_EXIT_CODES = Object.freeze({
|
|
60
|
+
landed: 0,
|
|
61
|
+
pending: 3,
|
|
62
|
+
blocked: 1,
|
|
63
|
+
failed: 1,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const TERMINAL_STATUSES = Object.freeze([
|
|
67
|
+
'landed',
|
|
68
|
+
'pending',
|
|
69
|
+
'blocked',
|
|
70
|
+
'failed',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The shared next-command vocabulary. Every producer of a "what now?"
|
|
75
|
+
* answer — the `pending` terminal envelope and `deliver-recover.js` —
|
|
76
|
+
* builds its command from here, so the two surfaces never drift into
|
|
77
|
+
* naming different commands for the same observed state.
|
|
78
|
+
*/
|
|
79
|
+
export const NEXT_COMMANDS = Object.freeze({
|
|
80
|
+
/**
|
|
81
|
+
* Resume a bounded merge wait that expired with the PR still in flight.
|
|
82
|
+
*
|
|
83
|
+
* `--wait` is load-bearing: without it the confirm CLI probes once and
|
|
84
|
+
* answers `pending` again, so the cumulative-budget give-up never fires and
|
|
85
|
+
* a wedged PR is never escalated to anyone.
|
|
86
|
+
*/
|
|
87
|
+
resumeLand: (storyId) =>
|
|
88
|
+
`node .agents/scripts/single-story-confirm-merge.js --story ${storyId} --wait`,
|
|
89
|
+
/** Confirm a merged-but-mislabelled Story (the idempotent flip + tail). */
|
|
90
|
+
confirmMerge: (storyId) =>
|
|
91
|
+
`node .agents/scripts/single-story-confirm-merge.js --story ${storyId}`,
|
|
92
|
+
/** Enter the red-CI fix loop against the failing PR. */
|
|
93
|
+
watchCi: (storyId, prNumber) =>
|
|
94
|
+
`node .agents/scripts/pr-watch-with-update.js --pr ${prNumber} --story ${storyId}`,
|
|
95
|
+
/** Re-run close for a Story whose PR was never opened. */
|
|
96
|
+
close: (storyId) =>
|
|
97
|
+
`node .agents/scripts/single-story-close.js --story ${storyId}`,
|
|
98
|
+
/** Resume implementation in the Story worktree. */
|
|
99
|
+
implement: (storyId) =>
|
|
100
|
+
`node .agents/scripts/single-story-init.js --story ${storyId}`,
|
|
101
|
+
/** Re-assert a drifted Projects v2 Status column. */
|
|
102
|
+
resync: (storyId) =>
|
|
103
|
+
`node .agents/scripts/resync-status-column.js --story ${storyId}`,
|
|
104
|
+
/** Probe a stranded Story and print its single next command. */
|
|
105
|
+
recover: (storyId) =>
|
|
106
|
+
`node .agents/scripts/deliver-recover.js --story ${storyId}`,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** @type {Function|null} */
|
|
110
|
+
let _validator = null;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compile (once) and return the terminal-envelope validator.
|
|
114
|
+
*
|
|
115
|
+
* @returns {Function}
|
|
116
|
+
*/
|
|
117
|
+
function getValidator() {
|
|
118
|
+
if (_validator) return _validator;
|
|
119
|
+
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
120
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
121
|
+
addFormats(ajv);
|
|
122
|
+
_validator = ajv.compile(schema);
|
|
123
|
+
return _validator;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate a candidate envelope against the shipped schema.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} envelope
|
|
130
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
131
|
+
*/
|
|
132
|
+
export function validateTerminalEnvelope(envelope) {
|
|
133
|
+
const validate = getValidator();
|
|
134
|
+
const valid = validate(envelope);
|
|
135
|
+
if (valid) return { valid: true, errors: [] };
|
|
136
|
+
const errors = (validate.errors ?? []).map(
|
|
137
|
+
(e) => `${e.instancePath || '/'} ${e.message}`,
|
|
138
|
+
);
|
|
139
|
+
return { valid: false, errors };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Drop `undefined`-valued keys so the schema's `additionalProperties: false`
|
|
144
|
+
* and its nullable unions both stay satisfiable from one optional-argument
|
|
145
|
+
* builder signature.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} obj
|
|
148
|
+
* @returns {object}
|
|
149
|
+
*/
|
|
150
|
+
function compact(obj) {
|
|
151
|
+
const out = {};
|
|
152
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
153
|
+
if (v !== undefined) out[k] = v;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Assemble the terminal envelope and validate it before returning.
|
|
160
|
+
*
|
|
161
|
+
* Throws a `TypeError` naming the schema violations when the assembled
|
|
162
|
+
* object does not validate. That is deliberate: the whole point of the
|
|
163
|
+
* envelope is that a caller can trust its status without re-probing
|
|
164
|
+
* GitHub, so emitting an unvalidated one would reintroduce the ambiguity
|
|
165
|
+
* this replaces.
|
|
166
|
+
*
|
|
167
|
+
* @param {object} args
|
|
168
|
+
* @param {number} args.storyId
|
|
169
|
+
* @param {'landed'|'pending'|'blocked'|'failed'} args.status
|
|
170
|
+
* @param {string} args.phase
|
|
171
|
+
* @param {string} [args.storyBranch]
|
|
172
|
+
* @param {string} [args.baseBranch]
|
|
173
|
+
* @param {object|null} [args.pr]
|
|
174
|
+
* @param {object} [args.gates]
|
|
175
|
+
* @param {object|null} [args.tail]
|
|
176
|
+
* @param {object|null} [args.blocked]
|
|
177
|
+
* @param {object|null} [args.failure]
|
|
178
|
+
* @param {string|null} [args.nextCommand]
|
|
179
|
+
* @param {number} args.elapsedSeconds
|
|
180
|
+
* @param {object|null} [args.waitBudget]
|
|
181
|
+
* @param {string} [args.timestamp]
|
|
182
|
+
* @returns {object} The validated envelope.
|
|
183
|
+
*/
|
|
184
|
+
export function buildTerminalEnvelope({
|
|
185
|
+
storyId,
|
|
186
|
+
status,
|
|
187
|
+
phase,
|
|
188
|
+
storyBranch,
|
|
189
|
+
baseBranch,
|
|
190
|
+
pr,
|
|
191
|
+
gates,
|
|
192
|
+
tail,
|
|
193
|
+
blocked,
|
|
194
|
+
failure,
|
|
195
|
+
nextCommand,
|
|
196
|
+
elapsedSeconds = 0,
|
|
197
|
+
waitBudget,
|
|
198
|
+
timestamp = new Date().toISOString(),
|
|
199
|
+
}) {
|
|
200
|
+
const envelope = compact({
|
|
201
|
+
kind: TERMINAL_ENVELOPE_KIND,
|
|
202
|
+
storyId: Number(storyId),
|
|
203
|
+
status,
|
|
204
|
+
phase,
|
|
205
|
+
storyBranch: storyBranch ?? null,
|
|
206
|
+
baseBranch: baseBranch ?? null,
|
|
207
|
+
pr: pr ?? null,
|
|
208
|
+
gates,
|
|
209
|
+
tail: tail ?? null,
|
|
210
|
+
blocked: blocked ?? null,
|
|
211
|
+
failure: failure ?? null,
|
|
212
|
+
nextCommand: nextCommand ?? null,
|
|
213
|
+
elapsedSeconds: Math.max(0, Number(elapsedSeconds) || 0),
|
|
214
|
+
waitBudget: waitBudget ?? null,
|
|
215
|
+
timestamp,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const { valid, errors } = validateTerminalEnvelope(envelope);
|
|
219
|
+
if (!valid) {
|
|
220
|
+
throw new TypeError(
|
|
221
|
+
`buildTerminalEnvelope: assembled envelope violates story-deliver-terminal.schema.json:\n` +
|
|
222
|
+
errors.map((e) => ` - ${e}`).join('\n'),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return envelope;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolve the process exit code for a terminal envelope.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} envelope
|
|
232
|
+
* @returns {number}
|
|
233
|
+
*/
|
|
234
|
+
export function exitCodeForTerminal(envelope) {
|
|
235
|
+
return TERMINAL_EXIT_CODES[envelope?.status] ?? 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The markers a caller scans stdout for to recover the envelope. */
|
|
239
|
+
export const TERMINAL_BEGIN_MARKER = '--- STORY DELIVER TERMINAL ---';
|
|
240
|
+
export const TERMINAL_END_MARKER = '--- END TERMINAL ---';
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Write a terminal envelope to stdout, between its markers.
|
|
244
|
+
*
|
|
245
|
+
* **Deliberately not `Logger.info`.** The envelope is this CLI's
|
|
246
|
+
* machine-readable contract — every invocation emits exactly ONE, and a
|
|
247
|
+
* headless caller parses it out of stdout to decide what happened.
|
|
248
|
+
* `Logger.info` is level-gated, so under the documented
|
|
249
|
+
* `AGENT_LOG_LEVEL=silent` (§ 1.H) the envelope silently vanished and the
|
|
250
|
+
* caller got a bare exit code: precisely the "no envelope at all" outcome
|
|
251
|
+
* Story #4543 exists to remove. A contract payload must not be suppressible
|
|
252
|
+
* by a verbosity knob.
|
|
253
|
+
*
|
|
254
|
+
* Single home for the marker format so the four emit sites (the runner's
|
|
255
|
+
* terminal, the close CLI's failed-terminal catch, and both confirm-CLI
|
|
256
|
+
* paths) cannot drift apart.
|
|
257
|
+
*
|
|
258
|
+
* @param {object} envelope
|
|
259
|
+
* @param {{ write?: (s: string) => void }} [opts] `write` is a test seam.
|
|
260
|
+
* @returns {void}
|
|
261
|
+
*/
|
|
262
|
+
export function emitTerminalEnvelope(
|
|
263
|
+
envelope,
|
|
264
|
+
{ write = (s) => process.stdout.write(s) } = {},
|
|
265
|
+
) {
|
|
266
|
+
write(
|
|
267
|
+
`\n${TERMINAL_BEGIN_MARKER}\n${JSON.stringify(envelope, null, 2)}\n${TERMINAL_END_MARKER}\n`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Map a `runConfirmMergePhase` outcome onto the schema-validated terminal
|
|
273
|
+
* envelope (Story #4543). One writer, one shape — the two prose contracts
|
|
274
|
+
* this replaces disagreed with each other precisely because each surface
|
|
275
|
+
* assembled its own.
|
|
276
|
+
*
|
|
277
|
+
* @returns {object} A validated `story-deliver-terminal` envelope.
|
|
278
|
+
*/
|
|
279
|
+
export function terminalFromWaitOutcome({
|
|
280
|
+
waitOutcome,
|
|
281
|
+
storyId,
|
|
282
|
+
storyBranch,
|
|
283
|
+
baseBranch,
|
|
284
|
+
prNumber,
|
|
285
|
+
prUrl,
|
|
286
|
+
autoMergeEnabled,
|
|
287
|
+
gates,
|
|
288
|
+
elapsedSeconds,
|
|
289
|
+
}) {
|
|
290
|
+
const prBase = {
|
|
291
|
+
number: prNumber,
|
|
292
|
+
url: prUrl ?? null,
|
|
293
|
+
autoMergeEnabled: Boolean(autoMergeEnabled),
|
|
294
|
+
};
|
|
295
|
+
const common = {
|
|
296
|
+
storyId,
|
|
297
|
+
storyBranch,
|
|
298
|
+
baseBranch,
|
|
299
|
+
gates,
|
|
300
|
+
elapsedSeconds,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (waitOutcome.terminal === 'landed') {
|
|
304
|
+
return buildTerminalEnvelope({
|
|
305
|
+
...common,
|
|
306
|
+
status: 'landed',
|
|
307
|
+
phase: 'post-land',
|
|
308
|
+
pr: {
|
|
309
|
+
...prBase,
|
|
310
|
+
state: 'MERGED',
|
|
311
|
+
// The observed rollup, not an assumed 'success' — a merge can land by
|
|
312
|
+
// admin override or with non-required checks red.
|
|
313
|
+
checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
|
|
314
|
+
},
|
|
315
|
+
tail: waitOutcome.tail,
|
|
316
|
+
nextCommand: null,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (waitOutcome.terminal === 'pending') {
|
|
321
|
+
return buildTerminalEnvelope({
|
|
322
|
+
...common,
|
|
323
|
+
status: 'pending',
|
|
324
|
+
phase: 'confirm-merge',
|
|
325
|
+
pr: {
|
|
326
|
+
...prBase,
|
|
327
|
+
state: waitOutcome.prProbe?.state ?? 'OPEN',
|
|
328
|
+
checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
|
|
329
|
+
},
|
|
330
|
+
waitBudget: waitOutcome.waitBudget,
|
|
331
|
+
nextCommand: NEXT_COMMANDS.resumeLand(storyId),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// blocked — the classifier already named the class and the friction
|
|
336
|
+
// comment already carries the class-specific remediation, so the next
|
|
337
|
+
// command mirrors it rather than inventing a second opinion.
|
|
338
|
+
const nextCommand =
|
|
339
|
+
waitOutcome.blockClass === 'checks-failed'
|
|
340
|
+
? NEXT_COMMANDS.watchCi(storyId, prNumber)
|
|
341
|
+
: waitOutcome.blockClass === 'merged-flip-failed'
|
|
342
|
+
? NEXT_COMMANDS.confirmMerge(storyId)
|
|
343
|
+
: NEXT_COMMANDS.recover(storyId);
|
|
344
|
+
return buildTerminalEnvelope({
|
|
345
|
+
...common,
|
|
346
|
+
status: 'blocked',
|
|
347
|
+
phase: 'confirm-merge',
|
|
348
|
+
pr: {
|
|
349
|
+
...prBase,
|
|
350
|
+
state: waitOutcome.prProbe?.state ?? null,
|
|
351
|
+
checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
|
|
352
|
+
},
|
|
353
|
+
blocked: {
|
|
354
|
+
blockClass: waitOutcome.blockClass,
|
|
355
|
+
reason: waitOutcome.reason,
|
|
356
|
+
frictionCommentId: waitOutcome.frictionCommentId ?? null,
|
|
357
|
+
},
|
|
358
|
+
nextCommand,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
@@ -72,15 +72,65 @@ export async function gatherStoryFrictionSignals(storyId, config) {
|
|
|
72
72
|
return signals;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Render the empty-roll-up line.
|
|
77
|
+
*
|
|
78
|
+
* Story #4578 — an empty roll-up over a multi-Story run must NOT read as
|
|
79
|
+
* success. The pre-#4578 text ("No friction signals — nothing to follow up")
|
|
80
|
+
* was *truthful* about the stream and *false* about the run: a 7-Story
|
|
81
|
+
* delivery containing a mid-run git outage, a parked worker, and a
|
|
82
|
+
* four-round acceptance critic rendered byte-identically to a genuinely
|
|
83
|
+
* clean run. An operator cannot tell "nothing went wrong" from "the
|
|
84
|
+
* telemetry never fired", and the second is likeliest exactly when the run
|
|
85
|
+
* went worst.
|
|
86
|
+
*
|
|
87
|
+
* So the line is a function of `storyCount`:
|
|
88
|
+
* - `storyCount <= 1` → the honest, quiet reading is retained. A single
|
|
89
|
+
* Story that emitted nothing plausibly *was* clean, and crying wolf on
|
|
90
|
+
* every clean Story is how a warning channel gets tuned out.
|
|
91
|
+
* - `storyCount > 1` → zero signals across N Stories is a **claim**, and
|
|
92
|
+
* the surrounding text says so and names the two readings, rather than
|
|
93
|
+
* asserting the flattering one.
|
|
94
|
+
*
|
|
95
|
+
* This mirrors the sibling precedent in `run-epilogue.js`'s
|
|
96
|
+
* `renderDiffLines`, which refuses to let an unresolvable base diff render
|
|
97
|
+
* as "0 changed files".
|
|
98
|
+
*
|
|
99
|
+
* @param {number} storyCount
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
function renderEmptyRollupLines(storyCount) {
|
|
103
|
+
if (storyCount <= 1) {
|
|
104
|
+
return ['_No friction signals — nothing to follow up._'];
|
|
105
|
+
}
|
|
106
|
+
return [
|
|
107
|
+
`> ⚠️ **0 friction signals across ${storyCount} Stories — this is a claim, not a clean bill of health.**`,
|
|
108
|
+
'> Either the run was genuinely friction-free, or telemetry never fired.',
|
|
109
|
+
'> An empty stream is indistinguishable from a clean run, and it is least',
|
|
110
|
+
'> likely to fill exactly when a run is going badly and the agent is busy.',
|
|
111
|
+
'> The runtime emits friction from its own observables (`agent::blocked`',
|
|
112
|
+
'> transitions, failed closes, exhausted merge waits) — so zero here also',
|
|
113
|
+
'> means none of those fired. If the run had friction you can name, that',
|
|
114
|
+
'> gap is itself the follow-up worth filing.',
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
75
118
|
/**
|
|
76
119
|
* @param {{
|
|
77
120
|
* storyId: number,
|
|
78
121
|
* proposals: object,
|
|
79
122
|
* graduated: object,
|
|
80
|
-
*
|
|
123
|
+
* storyCount?: number,
|
|
124
|
+
* }} args - `storyCount` (default 1) is how many Stories the roll-up spans;
|
|
125
|
+
* it decides whether an empty result reads as quiet or as a flagged claim.
|
|
81
126
|
* @returns {string}
|
|
82
127
|
*/
|
|
83
|
-
export function buildFollowUpsCommentBody({
|
|
128
|
+
export function buildFollowUpsCommentBody({
|
|
129
|
+
storyId,
|
|
130
|
+
proposals,
|
|
131
|
+
graduated,
|
|
132
|
+
storyCount = 1,
|
|
133
|
+
}) {
|
|
84
134
|
const filed = Array.isArray(graduated?.filed) ? graduated.filed : [];
|
|
85
135
|
const framework = proposals?.framework ?? [];
|
|
86
136
|
const consumer = proposals?.consumer ?? [];
|
|
@@ -124,7 +174,7 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
|
|
|
124
174
|
consumer.length === 0 &&
|
|
125
175
|
discarded.length === 0
|
|
126
176
|
) {
|
|
127
|
-
lines.push(
|
|
177
|
+
lines.push(...renderEmptyRollupLines(storyCount));
|
|
128
178
|
lines.push('');
|
|
129
179
|
}
|
|
130
180
|
lines.push('```json');
|
|
@@ -132,6 +182,7 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
|
|
|
132
182
|
JSON.stringify(
|
|
133
183
|
{
|
|
134
184
|
storyId,
|
|
185
|
+
storyCount,
|
|
135
186
|
framework: framework.map((i) => i.category),
|
|
136
187
|
consumer: consumer.map((i) => i.category),
|
|
137
188
|
discarded: discarded.map((i) => i.category),
|
|
@@ -139,6 +190,15 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
|
|
|
139
190
|
category: i.category,
|
|
140
191
|
url: i.url ?? null,
|
|
141
192
|
})),
|
|
193
|
+
// Story #4578 — an empty roll-up over N>1 Stories is a claim worth
|
|
194
|
+
// flagging, not a success. Machine-readable twin of the warning
|
|
195
|
+
// prose so a caller need not regex the body.
|
|
196
|
+
emptyRollupSuspect:
|
|
197
|
+
storyCount > 1 &&
|
|
198
|
+
filed.length === 0 &&
|
|
199
|
+
framework.length === 0 &&
|
|
200
|
+
consumer.length === 0 &&
|
|
201
|
+
discarded.length === 0,
|
|
142
202
|
},
|
|
143
203
|
null,
|
|
144
204
|
2,
|
|
@@ -149,8 +209,18 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
|
|
|
149
209
|
}
|
|
150
210
|
|
|
151
211
|
/**
|
|
152
|
-
* Capture and persist Story follow-ups. Never throws —
|
|
153
|
-
* because follow-up filing flaked.
|
|
212
|
+
* Capture and persist Story follow-ups. Never throws — the land must not
|
|
213
|
+
* fail because follow-up filing flaked.
|
|
214
|
+
*
|
|
215
|
+
* Story #4543 retired the `captureFollowUpsAfterConfirm` action-gate wrapper
|
|
216
|
+
* (and its `withConfirmFollowUps` sibling) that used to front this function.
|
|
217
|
+
* Re-deriving "did the merge land?" from a confirmation envelope's `action`
|
|
218
|
+
* field was the coupling that made close-and-land — the DEFAULT path — skip
|
|
219
|
+
* capture entirely: the gate only opened on the standalone CLI's `done`, and
|
|
220
|
+
* a belated manual confirm could not backfill because the Story was already
|
|
221
|
+
* `agent::done` (confirm returns `noop`, the gate never opens). The shared
|
|
222
|
+
* land tail (`single-story-close/phases/post-land.js`) now calls this
|
|
223
|
+
* directly, after the merge is already confirmed.
|
|
154
224
|
*
|
|
155
225
|
* @param {object} args
|
|
156
226
|
* @param {number} args.storyId
|
|
@@ -160,15 +230,6 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
|
|
|
160
230
|
* @param {(tag: string, msg: string) => void} [args.progress]
|
|
161
231
|
* @returns {Promise<object>}
|
|
162
232
|
*/
|
|
163
|
-
/**
|
|
164
|
-
* Capture follow-ups only when merge confirm landed (`action === 'done'`).
|
|
165
|
-
* One-liner seam for the confirm-merge CLI.
|
|
166
|
-
*/
|
|
167
|
-
export async function captureFollowUpsAfterConfirm(confirmation, ctx) {
|
|
168
|
-
if (confirmation?.action !== 'done') return null;
|
|
169
|
-
return captureStoryFollowUps(ctx);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
233
|
export async function captureStoryFollowUps({
|
|
173
234
|
storyId,
|
|
174
235
|
provider,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Logger } from '../Logger.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
STATE_LABELS,
|
|
4
|
+
transitionTicketState,
|
|
5
|
+
upsertStructuredComment,
|
|
6
|
+
} from './ticketing.js';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Fail closed when the repository remote cannot be verified. The Story is
|
|
@@ -30,13 +33,14 @@ export async function handleRemoteVerificationFailure({
|
|
|
30
33
|
`[single-story-init] failed to post remote-verification friction: ${err?.message ?? err}`,
|
|
31
34
|
);
|
|
32
35
|
}
|
|
36
|
+
// Story #4539 — the canonical mutator. This is the path where the
|
|
37
|
+
// skipped Projects v2 column sync (Story #2548) visibly drifts: the
|
|
38
|
+
// Story is still agent::ready (To Do) when the remote probe fails, so
|
|
39
|
+
// a direct label write leaves the board reading To Do for a blocked
|
|
40
|
+
// Story. single-story-init.js's own comment explains exactly why this
|
|
41
|
+
// must not bypass the mutator.
|
|
33
42
|
try {
|
|
34
|
-
await provider
|
|
35
|
-
labels: {
|
|
36
|
-
add: [AGENT_LABELS.BLOCKED],
|
|
37
|
-
remove: [AGENT_LABELS.READY, AGENT_LABELS.EXECUTING],
|
|
38
|
-
},
|
|
39
|
-
});
|
|
43
|
+
await transitionTicketState(provider, storyId, STATE_LABELS.BLOCKED, {});
|
|
40
44
|
} catch (err) {
|
|
41
45
|
Logger.warn(
|
|
42
46
|
`[single-story-init] failed to block Story after remote verification: ${err?.message ?? err}`,
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* story-plan-state.js — read the v2 Story planning checkpoint.
|
|
3
|
+
*
|
|
4
|
+
* `plan-persist.js` upserts one `story-plan-state` structured comment per
|
|
5
|
+
* created Story carrying the persist receipt (when the plan completed, how many
|
|
6
|
+
* Stories it created, and their ids). Story #4542 removed the risk fields it
|
|
7
|
+
* used to carry: the planner-authored verdict, the envelope derived from it,
|
|
8
|
+
* and the review routing computed from that envelope. Nothing read any of them
|
|
9
|
+
* back — review depth is now derived from the diff at close time
|
|
10
|
+
* (`review-depth.js#deriveChangeLevel`), so no checkpoint read sits on the
|
|
11
|
+
* delivery path at all.
|
|
12
|
+
*/
|
|
13
|
+
|
|
1
14
|
import { parseFencedJsonComment } from './structured-comment-parser.js';
|
|
2
15
|
import { findStructuredComment } from './ticketing.js';
|
|
3
16
|
|
|
4
17
|
/**
|
|
5
18
|
* Read the v2 Story planning checkpoint. Missing/malformed comments degrade
|
|
6
|
-
* to null
|
|
19
|
+
* to null — an unplanned Story simply has no persist receipt.
|
|
7
20
|
*/
|
|
8
21
|
export async function readStoryPlanState({
|
|
9
22
|
provider,
|
|
@@ -18,31 +31,3 @@ export async function readStoryPlanState({
|
|
|
18
31
|
const state = parseFencedJsonComment(comment);
|
|
19
32
|
return state && typeof state === 'object' ? state : null;
|
|
20
33
|
}
|
|
21
|
-
|
|
22
|
-
export async function readStoryPlanningRisk(args) {
|
|
23
|
-
const state = await readStoryPlanState(args);
|
|
24
|
-
return state?.planningRisk ?? null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function readStoryPlanningRiskSafe(args) {
|
|
28
|
-
try {
|
|
29
|
-
return await readStoryPlanningRisk(args);
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Prefer an explicit `planningRisk` override; otherwise load the Story
|
|
37
|
-
* checkpoint. Callers that omit the field (or pass `undefined`) get the
|
|
38
|
-
* persisted plan risk; callers that pass `null` keep the neutral posture.
|
|
39
|
-
*/
|
|
40
|
-
export async function resolveStoryPlanningRisk({
|
|
41
|
-
provider,
|
|
42
|
-
storyId,
|
|
43
|
-
planningRisk,
|
|
44
|
-
findCommentFn,
|
|
45
|
-
}) {
|
|
46
|
-
if (planningRisk !== undefined) return planningRisk;
|
|
47
|
-
return readStoryPlanningRiskSafe({ provider, storyId, findCommentFn });
|
|
48
|
-
}
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
* name at least one path-shaped token so vague verbs ("clean up",
|
|
25
25
|
* "refactor") can't slip through.
|
|
26
26
|
*
|
|
27
|
+
* `acceptance` / `verify` are the **top-level machine contract** (Story
|
|
28
|
+
* #4541). The decomposer prompt tells authors to write those lists once at
|
|
29
|
+
* the ticket's top level and omit the matching body sections; persist syncs
|
|
30
|
+
* them into the body at assemble time. Validation runs *before* that sync,
|
|
31
|
+
* so this validator resolves each contract field from the parsed body and
|
|
32
|
+
* falls back to the ticket's top-level array when the body section is
|
|
33
|
+
* absent. Without that fallback the validator rejected the very shape its
|
|
34
|
+
* own prompt prescribes.
|
|
35
|
+
*
|
|
27
36
|
* `body.changes` items must be object-form `{ path: string, assumption: enum }`
|
|
28
37
|
* entries (Story #2636 shape). Plain string bullets are rejected at parse
|
|
29
38
|
* time and by this validator.
|
|
@@ -51,8 +60,8 @@ import { FILE_ASSUMPTION_VALUES } from './file-assumption-enum.js';
|
|
|
51
60
|
|
|
52
61
|
/**
|
|
53
62
|
* Canonical testing-tier labels that a `verify[]` entry must name (in
|
|
54
|
-
* parentheses) to pass plan-time validation. Mirrors the
|
|
55
|
-
*
|
|
63
|
+
* parentheses) to pass plan-time validation. Mirrors the verify-rules contract
|
|
64
|
+
* in `.agents/scripts/lib/templates/decomposer-prompts.js`.
|
|
56
65
|
*
|
|
57
66
|
* Entries that do not end with `(<tier>)` and are not `manual:<reason>` are
|
|
58
67
|
* rejected by `collectVerifyErrors`.
|
|
@@ -164,6 +173,39 @@ function resolveStructuredBody(ticket) {
|
|
|
164
173
|
}
|
|
165
174
|
}
|
|
166
175
|
|
|
176
|
+
/**
|
|
177
|
+
* The two contract fields that live at the ticket's top level and are
|
|
178
|
+
* synced into the body by `plan-persist` at assemble time.
|
|
179
|
+
*/
|
|
180
|
+
const CONTRACT_FIELDS = Object.freeze(['acceptance', 'verify']);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve the body's contract fields against the ticket's top-level arrays
|
|
184
|
+
* (Story #4541). The decomposer prompt prescribes authoring `acceptance[]`
|
|
185
|
+
* / `verify[]` **once** at top level and omitting the matching body
|
|
186
|
+
* sections; `assemblePlanStories#syncContractFieldFromTopLevel` performs
|
|
187
|
+
* the sync, but it runs *after* validation. So an absent body section is
|
|
188
|
+
* not a violation when the ticket carries the list at top level — it is the
|
|
189
|
+
* preferred shape. A body section that is present and disagrees with the
|
|
190
|
+
* top level is left alone here: the sync itself fails closed on that
|
|
191
|
+
* mismatch, and duplicating the check would report it twice.
|
|
192
|
+
*
|
|
193
|
+
* @param {object} ticket
|
|
194
|
+
* @param {object} bodyObject Parsed / structured body.
|
|
195
|
+
* @returns {object} A copy of `bodyObject` with the contract fields resolved.
|
|
196
|
+
*/
|
|
197
|
+
function resolveContractFieldsFromTopLevel(ticket, bodyObject) {
|
|
198
|
+
const resolved = { ...bodyObject };
|
|
199
|
+
for (const field of CONTRACT_FIELDS) {
|
|
200
|
+
const bodyValue = Array.isArray(resolved[field]) ? resolved[field] : [];
|
|
201
|
+
if (bodyValue.length > 0) continue;
|
|
202
|
+
const topLevel = Array.isArray(ticket?.[field]) ? ticket[field] : [];
|
|
203
|
+
if (topLevel.length === 0) continue;
|
|
204
|
+
resolved[field] = topLevel.map(String);
|
|
205
|
+
}
|
|
206
|
+
return resolved;
|
|
207
|
+
}
|
|
208
|
+
|
|
167
209
|
/**
|
|
168
210
|
* Validate one Story body and return every violation it exhibits. Empty
|
|
169
211
|
* array means clean. Splits the per-ticket cascade out of
|
|
@@ -182,13 +224,14 @@ function resolveStructuredBody(ticket) {
|
|
|
182
224
|
*/
|
|
183
225
|
export function validateTaskBodyShape(ticket) {
|
|
184
226
|
const prefix = `Story "${ticket.title}" (${ticket.slug})`;
|
|
185
|
-
const { body, error } = resolveStructuredBody(ticket);
|
|
227
|
+
const { body: parsed, error } = resolveStructuredBody(ticket);
|
|
186
228
|
if (error !== null) {
|
|
187
229
|
return [error];
|
|
188
230
|
}
|
|
189
|
-
if (
|
|
190
|
-
return [`${prefix}: body must be an object, got ${typeof
|
|
231
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
232
|
+
return [`${prefix}: body must be an object, got ${typeof parsed}.`];
|
|
191
233
|
}
|
|
234
|
+
const body = resolveContractFieldsFromTopLevel(ticket, parsed);
|
|
192
235
|
const errors = [];
|
|
193
236
|
if (typeof body.goal !== 'string' || body.goal.trim() === '') {
|
|
194
237
|
errors.push(`${prefix}: body.goal must be a non-empty string.`);
|
|
@@ -300,7 +343,9 @@ function collectReferencesErrors(prefix, rawReferences) {
|
|
|
300
343
|
function collectAcceptanceErrors(prefix, rawAcceptance) {
|
|
301
344
|
const acceptance = Array.isArray(rawAcceptance) ? rawAcceptance : [];
|
|
302
345
|
if (acceptance.length === 0) {
|
|
303
|
-
return [
|
|
346
|
+
return [
|
|
347
|
+
`${prefix}: acceptance must list at least one criterion — author it at the ticket's top level (preferred) or in the body's ## Acceptance section.`,
|
|
348
|
+
];
|
|
304
349
|
}
|
|
305
350
|
return [];
|
|
306
351
|
}
|
|
@@ -323,7 +368,7 @@ function collectVerifyErrors(prefix, rawVerify) {
|
|
|
323
368
|
const verify = Array.isArray(rawVerify) ? rawVerify : [];
|
|
324
369
|
if (verify.length === 0) {
|
|
325
370
|
return [
|
|
326
|
-
`${prefix}:
|
|
371
|
+
`${prefix}: verify must list at least one entry — author it at the ticket's top level (preferred) or in the body's ## Verify section. Use "manual:<reason>" only when truly unverifiable in isolation.`,
|
|
327
372
|
];
|
|
328
373
|
}
|
|
329
374
|
const errors = [];
|