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
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
* an exclusive, time-bounded claim on a ticket so two concurrent runs do not
|
|
6
6
|
* both drive the same Story. Rather than invent a new state column, the lease
|
|
7
7
|
* rides the ticket's existing **assignees** surface: the single assignee *is*
|
|
8
|
-
* the lease owner. Liveness is decided by the owner's
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* the lease owner. Liveness is decided by the owner's last-heartbeat epoch-ms
|
|
9
|
+
* compared against a configured TTL.
|
|
10
|
+
*
|
|
11
|
+
* **There is no live heartbeat source.** The `story.heartbeat` emitter this
|
|
12
|
+
* module was designed against was structurally inert (it demanded an
|
|
13
|
+
* `epicId >= 1` that v2, which has no Epics, never supplies) and has been
|
|
14
|
+
* deleted. Every caller now reaches `acquireLease` through
|
|
15
|
+
* `lease-guard-shared.acquireLeaseFailClosed` with `anchorHeartbeatToNow`,
|
|
16
|
+
* which pins `heartbeatAt` to `now`: the lease **fails closed**, so any
|
|
17
|
+
* foreign claim reads as live and refuses. A stranded claim is cleared with
|
|
18
|
+
* `--steal`, never by TTL expiry. The TTL and the stale-reclaim branch below
|
|
19
|
+
* are therefore reachable only via an explicit caller-supplied `heartbeatAt`
|
|
20
|
+
* — the seam is kept, the automatic expiry is not real.
|
|
11
21
|
*
|
|
12
22
|
* The three exported operations are deliberately thin and provider-agnostic:
|
|
13
23
|
*
|
|
@@ -31,19 +41,16 @@
|
|
|
31
41
|
* Liveness seam: callers supply the owner's last-heartbeat epoch-ms via the
|
|
32
42
|
* `heartbeatAt` option (a number, or `null`/`undefined` when no heartbeat has
|
|
33
43
|
* ever been recorded for the current owner). Threading the timestamp in keeps
|
|
34
|
-
* this module pure and trivially unit-testable — it does not read
|
|
35
|
-
*
|
|
36
|
-
*
|
|
44
|
+
* this module pure and trivially unit-testable — it does not read any ledger
|
|
45
|
+
* itself. A claim with no heartbeat is treated as stale (reclaimable) by
|
|
46
|
+
* `isClaimLive`; note the live guards never take that branch, per the
|
|
47
|
+
* fail-closed anchoring described above.
|
|
37
48
|
*
|
|
38
49
|
* `now` is injectable (epoch ms) for deterministic tests; it defaults to
|
|
39
50
|
* `Date.now()`.
|
|
40
51
|
*/
|
|
41
52
|
|
|
42
|
-
import { readFileSync } from 'node:fs';
|
|
43
|
-
|
|
44
53
|
import { resolveLeaseTtlMs } from '../config/limits.js';
|
|
45
|
-
import { epicLedgerPath } from '../config/temp-paths.js';
|
|
46
|
-
import { parseLedger } from './lifecycle/trace-logger.js';
|
|
47
54
|
|
|
48
55
|
/**
|
|
49
56
|
* The shipped, non-personal operator-identity placeholder (and its bare,
|
|
@@ -87,76 +94,22 @@ export function normalizeOperatorHandle(raw) {
|
|
|
87
94
|
return trimmed;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
/**
|
|
91
|
-
* Read the most-recent `story.heartbeat` epoch-ms recorded for a given lease
|
|
92
|
-
* owner from an Epic lifecycle ledger. Returns `null` when the ledger is
|
|
93
|
-
* absent, unreadable, or carries no heartbeat for that owner — which the lease
|
|
94
|
-
* primitive treats as a stale (reclaimable) claim.
|
|
95
|
-
*
|
|
96
|
-
* The ledger is NDJSON; each `story.heartbeat` record carries
|
|
97
|
-
* `payload.operator` (Story #3480) and `payload.timestamp` (ISO-8601). A
|
|
98
|
-
* malformed ledger downgrades to `null` rather than throwing so a corrupt
|
|
99
|
-
* observability artifact never wedges the lease preflight.
|
|
100
|
-
*
|
|
101
|
-
* This is the shared liveness source the lease guards thread into
|
|
102
|
-
* `acquireLease` via `heartbeatAt`; `/plan` and `/deliver` both
|
|
103
|
-
* reuse it so a live foreign claim actually refuses.
|
|
104
|
-
*
|
|
105
|
-
* @param {object} args
|
|
106
|
-
* @param {number} args.epicId
|
|
107
|
-
* @param {string} args.owner Lease owner whose heartbeat to find.
|
|
108
|
-
* @param {object} [args.config] Resolved config (for ledger path).
|
|
109
|
-
* @param {string} [args.ledgerPath] Explicit path override (tests).
|
|
110
|
-
* @param {(eid: number, config?: object) => string} [args.ledgerPathResolver]
|
|
111
|
-
* Injectable resolver (tests). Defaults to `epicLedgerPath`.
|
|
112
|
-
* @param {(p: string) => string} [args.readFile] Injectable reader (tests).
|
|
113
|
-
* @returns {number|null}
|
|
114
|
-
*/
|
|
115
|
-
export function latestHeartbeatForOwner({
|
|
116
|
-
epicId,
|
|
117
|
-
owner,
|
|
118
|
-
config,
|
|
119
|
-
ledgerPath,
|
|
120
|
-
ledgerPathResolver = epicLedgerPath,
|
|
121
|
-
readFile = (p) => readFileSync(p, 'utf8'),
|
|
122
|
-
}) {
|
|
123
|
-
if (typeof owner !== 'string' || owner.length === 0) return null;
|
|
124
|
-
const resolvedPath = ledgerPath ?? ledgerPathResolver(epicId, config);
|
|
125
|
-
|
|
126
|
-
let text;
|
|
127
|
-
try {
|
|
128
|
-
text = readFile(resolvedPath);
|
|
129
|
-
} catch (_err) {
|
|
130
|
-
// No ledger yet (fresh Epic) → no heartbeat → reclaimable.
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let records;
|
|
135
|
-
try {
|
|
136
|
-
records = parseLedger(text);
|
|
137
|
-
} catch (_err) {
|
|
138
|
-
// Corrupt ledger is an observability problem, not a coordination blocker.
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
let latest = null;
|
|
143
|
-
for (const record of records) {
|
|
144
|
-
const payload = record?.payload;
|
|
145
|
-
if (!payload || payload.event !== 'story.heartbeat') continue;
|
|
146
|
-
if (payload.operator !== owner) continue;
|
|
147
|
-
const ts = Date.parse(payload.timestamp ?? '');
|
|
148
|
-
if (!Number.isFinite(ts)) continue;
|
|
149
|
-
if (latest === null || ts > latest) latest = ts;
|
|
150
|
-
}
|
|
151
|
-
return latest;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
97
|
/**
|
|
155
98
|
* Decide whether a foreign claim is still "live" given the owner's last
|
|
156
99
|
* heartbeat and the configured TTL. A claim is live when a heartbeat exists
|
|
157
100
|
* and is no older than `ttlMs`. A missing heartbeat (`null`/`undefined`) or a
|
|
158
101
|
* heartbeat older than the TTL is stale and therefore reclaimable.
|
|
159
102
|
*
|
|
103
|
+
* The `heartbeatAt` seam is retained, but there is no longer any in-repo
|
|
104
|
+
* heartbeat *source*: the `story.heartbeat` emitter was structurally inert
|
|
105
|
+
* (it required an `epicId >= 1` that v2 never sets) and was deleted along
|
|
106
|
+
* with the ledger reader that scanned for it. Every live caller reaches this
|
|
107
|
+
* through `lease-guard-shared.acquireLeaseFailClosed` with
|
|
108
|
+
* `anchorHeartbeatToNow`, which pins `heartbeatAt` to `now` so ANY foreign
|
|
109
|
+
* claim reads live and the guard fails closed — a stranded claim is cleared
|
|
110
|
+
* with `--steal`, not by TTL expiry. The parameter stays because that
|
|
111
|
+
* anchoring is expressed through it.
|
|
112
|
+
*
|
|
160
113
|
* @param {object} args
|
|
161
114
|
* @param {number|null|undefined} args.heartbeatAt Owner's last heartbeat (epoch ms).
|
|
162
115
|
* @param {number} args.ttlMs Lease TTL in milliseconds.
|
|
@@ -589,13 +589,53 @@ function registryRegistry(producers, assumptionEntries, patterns, cache) {
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
/**
|
|
592
|
-
*
|
|
592
|
+
* Normalize a fan-out probe result into `{ count, files, probe }`.
|
|
593
593
|
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
594
|
+
* The production probe reports its referencing files and the exact command
|
|
595
|
+
* that found them so an operator can reproduce the figure (Story #4547).
|
|
596
|
+
* A bare number stays valid — injected test counters and any consumer
|
|
597
|
+
* counter written against the Story #2962 contract keep working, they just
|
|
598
|
+
* carry no audit trail.
|
|
599
|
+
*/
|
|
600
|
+
function normalizeFanOutProbe(result) {
|
|
601
|
+
if (typeof result === 'number') {
|
|
602
|
+
return { count: result, files: [], probe: null };
|
|
603
|
+
}
|
|
604
|
+
if (result === null || typeof result !== 'object') {
|
|
605
|
+
return { count: 0, files: [], probe: null };
|
|
606
|
+
}
|
|
607
|
+
const files = Array.isArray(result.files) ? result.files : [];
|
|
608
|
+
const count = Number.isFinite(result.count) ? result.count : files.length;
|
|
609
|
+
return { count, files, probe: result.probe ?? null };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Index the basenames this spec *creates*, so a deletion that is really one
|
|
614
|
+
* half of a move can be told apart from a genuine wide-coupling removal.
|
|
615
|
+
*/
|
|
616
|
+
function indexCreatedBasenames(assumptionEntries) {
|
|
617
|
+
const byBasename = new Map();
|
|
618
|
+
for (const entry of assumptionEntries) {
|
|
619
|
+
if (entry.assumption !== 'creates') continue;
|
|
620
|
+
const base = entry.path.slice(entry.path.lastIndexOf('/') + 1);
|
|
621
|
+
if (!byBasename.has(base)) byBasename.set(base, entry.path);
|
|
622
|
+
}
|
|
623
|
+
return byBasename;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Compute `fan-out-warning` findings (Story #2962, reworked in #4547).
|
|
628
|
+
*
|
|
629
|
+
* For each `body.changes` entry whose `assumption` is `"deletes"`, probe the
|
|
630
|
+
* files at the base branch that genuinely import or require the deleted
|
|
631
|
+
* module. When that count exceeds the configured `largeFanOutThreshold`,
|
|
632
|
+
* emit a finding carrying the referencing files and the probe that produced
|
|
633
|
+
* them.
|
|
634
|
+
*
|
|
635
|
+
* The finding also records whether the deletion is **rename-shaped** — the
|
|
636
|
+
* same spec creates a file with the deleted module's basename elsewhere —
|
|
637
|
+
* because the remedy diverges: a move wants its importers repointed in one
|
|
638
|
+
* Story, not a subsystem-by-subsystem migration split across several.
|
|
599
639
|
*
|
|
600
640
|
* The default severity is always `'soft'` — the persist gate enforces a
|
|
601
641
|
* hard refusal via the `--allow-large-fan-out` operator flag, since the
|
|
@@ -613,21 +653,28 @@ function computeFanOutFindings({
|
|
|
613
653
|
if (!Number.isFinite(threshold) || threshold < 0) return [];
|
|
614
654
|
const findings = [];
|
|
615
655
|
const cache = new Map();
|
|
656
|
+
const createdBasenames = indexCreatedBasenames(assumptionEntries);
|
|
616
657
|
for (const entry of assumptionEntries) {
|
|
617
658
|
if (entry.assumption !== 'deletes') continue;
|
|
618
|
-
let
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
cache.set(entry.path,
|
|
659
|
+
let probed = cache.get(entry.path);
|
|
660
|
+
if (probed === undefined) {
|
|
661
|
+
probed = normalizeFanOutProbe(counter({ path: entry.path }));
|
|
662
|
+
cache.set(entry.path, probed);
|
|
622
663
|
}
|
|
623
|
-
if (count <= threshold) continue;
|
|
664
|
+
if (probed.count <= threshold) continue;
|
|
665
|
+
const base = entry.path.slice(entry.path.lastIndexOf('/') + 1);
|
|
666
|
+
const renameTarget = createdBasenames.get(base);
|
|
624
667
|
findings.push({
|
|
625
668
|
kind: 'fan-out-warning',
|
|
626
669
|
severity,
|
|
627
670
|
taskSlug: entry.taskSlug,
|
|
628
671
|
storySlug: entry.storySlug,
|
|
629
672
|
path: entry.path,
|
|
630
|
-
callSiteCount: count,
|
|
673
|
+
callSiteCount: probed.count,
|
|
674
|
+
callSites: probed.files,
|
|
675
|
+
probe: probed.probe,
|
|
676
|
+
renameShaped: renameTarget !== undefined && renameTarget !== entry.path,
|
|
677
|
+
renameTarget: renameTarget === entry.path ? null : (renameTarget ?? null),
|
|
631
678
|
threshold,
|
|
632
679
|
});
|
|
633
680
|
}
|
|
@@ -701,6 +748,60 @@ export function computeConflictFindings({ stories, policy } = {}) {
|
|
|
701
748
|
];
|
|
702
749
|
}
|
|
703
750
|
|
|
751
|
+
/**
|
|
752
|
+
* Render the audit trail behind a fan-out finding's number, so an operator
|
|
753
|
+
* can check the figure rather than trust it (Story #4547).
|
|
754
|
+
*
|
|
755
|
+
* Every importer is named — the list is deliberately **not** truncated. A
|
|
756
|
+
* gate that fires at 100 importers is precisely when the operator needs the
|
|
757
|
+
* list, and a `…and 109 more` tail would leave the figure uncheckable in
|
|
758
|
+
* exactly the case the gate exists for. This message is a fail-closed stop,
|
|
759
|
+
* not a log line; its length is the point.
|
|
760
|
+
*
|
|
761
|
+
* The probe is reported as what it is — the *candidate* net, which each hit
|
|
762
|
+
* is then re-resolved against. It will report at least as many lines as the
|
|
763
|
+
* gate counts files, so labelling it as the thing that produced the number
|
|
764
|
+
* would send an operator chasing a discrepancy that is by design.
|
|
765
|
+
*
|
|
766
|
+
* Returns `''` for a bare-number counter, which carries no audit trail.
|
|
767
|
+
*/
|
|
768
|
+
export function renderFanOutEvidence(finding) {
|
|
769
|
+
const files = Array.isArray(finding.callSites) ? finding.callSites : [];
|
|
770
|
+
const parts = [];
|
|
771
|
+
if (files.length > 0) {
|
|
772
|
+
parts.push(` Importers (${files.length}):`);
|
|
773
|
+
for (const file of files) parts.push(` ${file}`);
|
|
774
|
+
}
|
|
775
|
+
if (finding.probe) {
|
|
776
|
+
parts.push(
|
|
777
|
+
` Candidate probe (each hit re-resolved against its importer's directory):`,
|
|
778
|
+
` ${finding.probe}`,
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
return parts.length > 0 ? `\n${parts.join('\n')}` : '';
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Render the remedy that actually fits the finding. A rename-shaped
|
|
786
|
+
* deletion has nowhere to split to — the importers just need repointing at
|
|
787
|
+
* the path the same plan creates — so telling the operator to split it
|
|
788
|
+
* across Stories leaves the override as the only exit, which is exactly the
|
|
789
|
+
* habit that defeats the gate (Story #4547).
|
|
790
|
+
*/
|
|
791
|
+
export function renderFanOutRemedy(finding) {
|
|
792
|
+
if (finding.renameShaped && finding.renameTarget) {
|
|
793
|
+
return (
|
|
794
|
+
`This deletion is rename-shaped: the same plan creates "${finding.renameTarget}" under the same basename. ` +
|
|
795
|
+
`Repoint the importer(s) at the new path inside this Story — a move has no subsystems to split across — ` +
|
|
796
|
+
`then rerun --allow-large-fan-out.`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
return (
|
|
800
|
+
`Split the deletion into a subsystem-by-subsystem migration across multiple Stories, ` +
|
|
801
|
+
`or rerun --allow-large-fan-out after confirming the deletion is intentional.`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
704
805
|
/**
|
|
705
806
|
* Render a `'hard'`-severity conflict finding as a human-readable error
|
|
706
807
|
* message. Used by the validator when policy flags upgrade a finding to
|
|
@@ -712,14 +813,18 @@ export function renderHardConflictError(finding) {
|
|
|
712
813
|
return `Shared-editor conflict: "${finding.path}" is written by ${finding.storySlugs.length} concurrent Stories (${stories}). Add depends_on chains between them or split the edits into a dedicated late-wave wiring Story.`;
|
|
713
814
|
}
|
|
714
815
|
if (finding.kind === 'implicit-cross-story-dep') {
|
|
715
|
-
return `Implicit cross-Story dependency:
|
|
816
|
+
return `Implicit cross-Story dependency: Story "${finding.consumer.storySlug}" references "${finding.path}" (produced by Story "${finding.producer.storySlug}") via body.${finding.consumer.sourceField}, but Story "${finding.consumer.storySlug}" has no depends_on link to Story "${finding.producer.storySlug}". Add depends_on: ["${finding.producer.storySlug}"] to the consumer Story or remove the reference.`;
|
|
716
817
|
}
|
|
717
818
|
if (finding.kind === 'cross-cutting-registries') {
|
|
718
819
|
const stories = finding.storySlugs.map((s) => `"${s}"`).join(', ');
|
|
719
820
|
return `Cross-cutting registry conflict: ${finding.storySlugs.length} concurrent Stories (${stories}) edit or register into "${finding.registryPath}". Add depends_on chains between them so the registry updates serialize, or split the registration into a dedicated late-wave wiring Story.`;
|
|
720
821
|
}
|
|
721
822
|
if (finding.kind === 'fan-out-warning') {
|
|
722
|
-
return
|
|
823
|
+
return (
|
|
824
|
+
`Large fan-out: Story "${finding.storySlug}" deletes "${finding.path}" ` +
|
|
825
|
+
`with ${finding.callSiteCount} importer(s) on the base branch (threshold ${finding.threshold}). ` +
|
|
826
|
+
`${renderFanOutRemedy(finding)}${renderFanOutEvidence(finding)}`
|
|
827
|
+
);
|
|
723
828
|
}
|
|
724
829
|
if (finding.kind === 'missing-bdd-scaffold') {
|
|
725
830
|
return `Missing BDD scaffold: Story "${finding.consumer.storySlug}" verifies against "${finding.path}" (created by Story "${finding.producer.storySlug}") via body.${finding.consumer.sourceField}, but "${finding.consumer.storySlug}" has no depends_on path to "${finding.producer.storySlug}" — the .feature file is scaffolded in the same wave (or later), so verification runs before the file exists. Add depends_on: ["${finding.producer.storySlug}"] to the consumer Story so the scaffold lands in an earlier wave.`;
|
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
*
|
|
15
15
|
* `DEFAULT_MODEL_CAPACITY` is the **single source of truth** for the capacity
|
|
16
16
|
* thresholds. The decomposer prompt template
|
|
17
|
-
* (`.agents/scripts/lib/templates/decomposer-prompts.js`)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* its threshold sentence from this constant so the two surfaces cannot drift.
|
|
17
|
+
* (`.agents/scripts/lib/templates/decomposer-prompts.js`) generates its
|
|
18
|
+
* threshold sentence from this constant rather than restating divergent
|
|
19
|
+
* numbers, so the two surfaces cannot drift.
|
|
21
20
|
*
|
|
22
21
|
* Capacity model:
|
|
23
22
|
* - Plan-time **session mass** = authored tokens only (`estimateTokens` of
|
|
@@ -3,7 +3,10 @@ import { detectCycle } from '../Graph.js';
|
|
|
3
3
|
import { gitSpawn } from '../git-utils.js';
|
|
4
4
|
|
|
5
5
|
import { Logger } from '../Logger.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
parse as parseStoryBody,
|
|
8
|
+
StoryBodyParseError,
|
|
9
|
+
} from '../story-body/story-body.js';
|
|
7
10
|
import { validateStoryFileAssumptions } from './file-assumptions.js';
|
|
8
11
|
import {
|
|
9
12
|
computeConflictFindings,
|
|
@@ -43,6 +46,95 @@ function collectPathsFromText(text, paths) {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Parse a Story's serialized markdown body, translating a
|
|
51
|
+
* `StoryBodyParseError` into a `ValidationError` that names the offending
|
|
52
|
+
* **section** and **entry** (Story #4541).
|
|
53
|
+
*
|
|
54
|
+
* `StoryBodyParseError` already carries `field` (the section the parser was
|
|
55
|
+
* reading) and `raw` (the entry text that failed); this lifts both into an
|
|
56
|
+
* operator-legible message and a structured `violation` payload so an
|
|
57
|
+
* authoring loop can point at the exact bullet instead of re-deriving it
|
|
58
|
+
* from a downstream freshness miss.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} story Story whose `body` is a non-empty markdown string.
|
|
61
|
+
* @returns {object} The structured body.
|
|
62
|
+
* @throws {ValidationError} `code: 'story-body-unparseable'`.
|
|
63
|
+
*/
|
|
64
|
+
function parseStoryBodyOrThrow(story) {
|
|
65
|
+
try {
|
|
66
|
+
return parseStoryBody(story.body).body;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (!(err instanceof StoryBodyParseError)) throw err;
|
|
69
|
+
const slug = story.slug ?? '<unknown>';
|
|
70
|
+
const section = err.field ?? 'body';
|
|
71
|
+
const entry = err.raw ?? null;
|
|
72
|
+
const entryLine = entry === null ? '' : `\n entry: ${entry}`;
|
|
73
|
+
const violation = { slug, section, entry, reason: err.message };
|
|
74
|
+
const error = new ValidationError(
|
|
75
|
+
`Cross-Validation Failed: Story "${slug}" has an unparseable body — ` +
|
|
76
|
+
`the ## ${section} section could not be read: ${err.message}` +
|
|
77
|
+
`${entryLine}\n\nFix the offending entry; this is a malformed body, ` +
|
|
78
|
+
'not a stale path reference.',
|
|
79
|
+
{ violations: [violation] },
|
|
80
|
+
);
|
|
81
|
+
error.code = 'story-body-unparseable';
|
|
82
|
+
error.violations = [violation];
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Refuse the plan when any Story's serialized body cannot be parsed, before
|
|
89
|
+
* either git-probe gate runs (Story #4541). Ordering matters: the freshness
|
|
90
|
+
* gate consults `body.changes` for its net-new whitelist, so an unparseable
|
|
91
|
+
* body used to reach the operator as a freshness miss naming declared paths.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ tickets: object[] }} opts
|
|
94
|
+
* @throws {ValidationError} `code: 'story-body-unparseable'` on the first
|
|
95
|
+
* offending Story.
|
|
96
|
+
*/
|
|
97
|
+
function assertStoryBodiesParse({ tickets }) {
|
|
98
|
+
for (const story of (tickets ?? []).filter((t) => t.type === 'story')) {
|
|
99
|
+
if (typeof story.body !== 'string' || story.body.trim().length === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
parseStoryBodyOrThrow(story);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve every acceptance line a Story declares, across both authoring
|
|
108
|
+
* shapes (Story #4541).
|
|
109
|
+
*
|
|
110
|
+
* The canonical shape is a **serialized string body** with the criteria at
|
|
111
|
+
* the ticket's **top level** — the machine contract persist syncs into the
|
|
112
|
+
* body. `validateAcceptanceSubjectPrefix` used to read `body.acceptance` on
|
|
113
|
+
* an object body only, so on every real plan it scanned nothing and the gate
|
|
114
|
+
* silently passed. Union both sources (deduplicated) so the gate fires on
|
|
115
|
+
* whichever surface the author used.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} story
|
|
118
|
+
* @returns {string[]}
|
|
119
|
+
*/
|
|
120
|
+
function resolveAcceptanceLines(story) {
|
|
121
|
+
const lines = new Set();
|
|
122
|
+
if (Array.isArray(story?.acceptance)) {
|
|
123
|
+
for (const item of story.acceptance) lines.add(String(item ?? ''));
|
|
124
|
+
}
|
|
125
|
+
const body = story?.body;
|
|
126
|
+
let bodyAcceptance = null;
|
|
127
|
+
if (typeof body === 'string' && body.trim().length > 0) {
|
|
128
|
+
bodyAcceptance = parseStoryBodyOrThrow(story).acceptance;
|
|
129
|
+
} else if (body !== null && typeof body === 'object') {
|
|
130
|
+
bodyAcceptance = body.acceptance;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(bodyAcceptance)) {
|
|
133
|
+
for (const item of bodyAcceptance) lines.add(String(item ?? ''));
|
|
134
|
+
}
|
|
135
|
+
return [...lines];
|
|
136
|
+
}
|
|
137
|
+
|
|
46
138
|
function collectTaskPathReferences(task) {
|
|
47
139
|
const paths = new Set();
|
|
48
140
|
const body = task.body;
|
|
@@ -82,7 +174,7 @@ function collectTaskPathReferences(task) {
|
|
|
82
174
|
* path out of the prose.
|
|
83
175
|
* 3. **Object form** — `{ path: "<path>", assumption: "creates" | ... }`,
|
|
84
176
|
* introduced by Story #2636 as the canonical declaration shape and
|
|
85
|
-
* documented in `
|
|
177
|
+
* documented in `lib/templates/decomposer-prompts.js`. The path is
|
|
86
178
|
* trusted verbatim.
|
|
87
179
|
*
|
|
88
180
|
* Only `body.changes` (and `body.references`) is consulted —
|
|
@@ -100,15 +192,16 @@ function collectTaskChangesPaths(task) {
|
|
|
100
192
|
// arrays before scanning. Without this, a string body causes the
|
|
101
193
|
// object-form branch below to fall through on every item, leaving the
|
|
102
194
|
// freshness gate blind to declared paths.
|
|
195
|
+
//
|
|
196
|
+
// Story #4541: a parse failure is NOT swallowed here. Swallowing it
|
|
197
|
+
// returned an empty whitelist, so a single malformed `## Changes` entry
|
|
198
|
+
// surfaced downstream as "files do not exist at main" naming the very
|
|
199
|
+
// paths the Story *had* declared — a misdiagnosis that cost two authoring
|
|
200
|
+
// round-trips. `assertStoryBodiesParse` runs before the freshness gate and
|
|
201
|
+
// owns that failure with a named error; the throw here is the same error
|
|
202
|
+
// for any caller that drives `validateAcFreshness` directly.
|
|
103
203
|
if (typeof body === 'string' && body.trim().length > 0) {
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
parsed = parseStoryBody(body).body;
|
|
107
|
-
} catch {
|
|
108
|
-
// Unparseable body — no paths to whitelist; the freshness gate will
|
|
109
|
-
// catch any real references in the text scan below.
|
|
110
|
-
return paths;
|
|
111
|
-
}
|
|
204
|
+
const parsed = parseStoryBodyOrThrow(task);
|
|
112
205
|
for (const arrName of ['changes', 'references']) {
|
|
113
206
|
const arr = parsed[arrName];
|
|
114
207
|
if (!Array.isArray(arr)) continue;
|
|
@@ -307,11 +400,16 @@ const SUBJECT_PREFIX_RE = /Commit subject begins with ['"`]([^'"`]+):['"`]/g;
|
|
|
307
400
|
* the form `baseline-refresh` is rejected because no Conventional-Commits
|
|
308
401
|
* type starts with that token.
|
|
309
402
|
*
|
|
310
|
-
* Only
|
|
403
|
+
* Only acceptance criteria are scanned; `body.goal` / `body.verify` /
|
|
311
404
|
* `body.changes` are not commit-subject prescriptions by convention and
|
|
312
405
|
* scanning them would surface false positives from prose that happens to
|
|
313
406
|
* quote a forbidden prefix while explaining why it's forbidden.
|
|
314
407
|
*
|
|
408
|
+
* Both authoring shapes are covered (Story #4541): the canonical top-level
|
|
409
|
+
* `acceptance[]` on a serialized string body, and the pre-serialize
|
|
410
|
+
* `body.acceptance[]` object shape. Scanning only the latter made the gate
|
|
411
|
+
* inert on every real plan.
|
|
412
|
+
*
|
|
315
413
|
* @param {object} opts
|
|
316
414
|
* @param {object[]} opts.tickets - Validated ticket hierarchy.
|
|
317
415
|
* @throws {ValidationError} when one or more Story acceptance items
|
|
@@ -324,11 +422,7 @@ export function validateAcceptanceSubjectPrefix({ tickets }) {
|
|
|
324
422
|
const violations = [];
|
|
325
423
|
const stories = (tickets ?? []).filter((t) => t.type === 'story');
|
|
326
424
|
for (const story of stories) {
|
|
327
|
-
const
|
|
328
|
-
if (body === null || typeof body !== 'object') continue;
|
|
329
|
-
if (!Array.isArray(body.acceptance)) continue;
|
|
330
|
-
for (const item of body.acceptance) {
|
|
331
|
-
const line = String(item ?? '');
|
|
425
|
+
for (const line of resolveAcceptanceLines(story)) {
|
|
332
426
|
// Reset the global regex between iterations.
|
|
333
427
|
SUBJECT_PREFIX_RE.lastIndex = 0;
|
|
334
428
|
let match = SUBJECT_PREFIX_RE.exec(line);
|
|
@@ -381,8 +475,8 @@ function renderMissLine({ slug, path }) {
|
|
|
381
475
|
* The returned tickets array carries two extra non-array properties:
|
|
382
476
|
* - `findings` — structured sizing findings (hard + soft) keyed by the
|
|
383
477
|
* three-layer sizing model. The bounded re-decomposition loop in
|
|
384
|
-
*
|
|
385
|
-
*
|
|
478
|
+
* `/plan` reads `findings.filter(f => f.severity === 'hard')` to decide
|
|
479
|
+
* whether to re-prompt.
|
|
386
480
|
* - `errors` — human-readable strings, one per hard finding. Non-empty
|
|
387
481
|
* `errors[]` is the AC-visible "block normalization" signal; the legacy
|
|
388
482
|
* hierarchy/cycle/freshness checks continue to throw, so callers that
|
|
@@ -540,6 +634,14 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
540
634
|
|
|
541
635
|
assertAcyclic(slugAdjacency);
|
|
542
636
|
|
|
637
|
+
// Story #4541 — refuse an unparseable Story body up front, with a named
|
|
638
|
+
// error pointing at the offending section + entry. Must precede both the
|
|
639
|
+
// subject-prefix scan and the freshness gate: each parses the body, and
|
|
640
|
+
// the freshness gate's net-new whitelist comes from `body.changes`, so a
|
|
641
|
+
// malformed body used to surface as a stale-path miss naming the paths the
|
|
642
|
+
// Story had legitimately declared.
|
|
643
|
+
assertStoryBodiesParse({ tickets });
|
|
644
|
+
|
|
543
645
|
// Reject any Task acceptance item that prescribes a non-Conventional-Commits
|
|
544
646
|
// subject prefix (e.g. legacy "Commit subject begins with 'baseline-refresh:'"
|
|
545
647
|
// from pre-Epic-#2501 planner output). Runs before the freshness gate so
|
|
@@ -648,6 +750,7 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
648
750
|
|
|
649
751
|
// Internal helpers exposed for unit tests; not part of the public surface.
|
|
650
752
|
export const _internal = {
|
|
753
|
+
assertStoryBodiesParse,
|
|
651
754
|
indexTicketsBySlug,
|
|
652
755
|
assertAllTicketsAreStories,
|
|
653
756
|
assertEveryStoryHasInlineContract,
|
|
@@ -357,7 +357,7 @@ async function processCascadeParentLocked(
|
|
|
357
357
|
* unset so the module-level {@link Logger} is used.
|
|
358
358
|
* @returns {Promise<{ cascadedTo: number[], failed: Array<{ parentId: number, error: string }> }>}
|
|
359
359
|
*/
|
|
360
|
-
|
|
360
|
+
async function cascadeCompletion(provider, ticketId, opts = {}) {
|
|
361
361
|
const ticket = await provider.getTicket(ticketId);
|
|
362
362
|
|
|
363
363
|
// Determine if this ticket is agent::done
|
|
@@ -365,43 +365,14 @@ export async function cascadeCompletion(provider, ticketId, opts = {}) {
|
|
|
365
365
|
return { cascadedTo: [], failed: [] };
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
// Story #4545 — one strategy, not three. The `parent: #N` body footer and
|
|
369
|
+
// the native sub-issue link were both written by `createTicket`, the
|
|
370
|
+
// Epic-hierarchy write surface deleted in the same Story; with no writer,
|
|
371
|
+
// the body regex could only ever miss and the native lookup could only
|
|
372
|
+
// ever spend an API call to learn the same. The operator-settable `blocks`
|
|
373
|
+
// annotation is the one parent edge that can still exist.
|
|
368
374
|
const { blocks: parentIds } = await provider.getTicketDependencies(ticketId);
|
|
369
|
-
|
|
370
|
-
// Fallback: parse `parent: #NNN` from the body when `blocks` syntax isn't used (C-5).
|
|
371
|
-
let parsedParents = parentIds;
|
|
372
|
-
if (!parsedParents || parsedParents.length === 0) {
|
|
373
|
-
const parentMatch = ticket.body
|
|
374
|
-
? [...ticket.body.matchAll(/parent:\s*#(\d+)/gi)]
|
|
375
|
-
: [];
|
|
376
|
-
parsedParents = parentMatch.map((m) => Number.parseInt(m[1], 10));
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Story #2982 — third fallback: GitHub's native Sub-Issues API. The
|
|
380
|
-
// resume reconciler can strip the `parent: #N` orchestrator footer
|
|
381
|
-
// from a Story body (see Issue 2 in #2982); without the body marker
|
|
382
|
-
// the cascade silently returned `{ cascadedTo: [], failed: [] }` and
|
|
383
|
-
// left intermediate parent tickets stranded OPEN. The native link is
|
|
384
|
-
// independent of body text, so consult it when the first two
|
|
385
|
-
// strategies came back empty.
|
|
386
|
-
if (
|
|
387
|
-
parsedParents.length === 0 &&
|
|
388
|
-
typeof provider._getNativeParent === 'function' &&
|
|
389
|
-
ticket.nodeId
|
|
390
|
-
) {
|
|
391
|
-
try {
|
|
392
|
-
const nativeParent = await provider._getNativeParent(
|
|
393
|
-
ticket.nodeId,
|
|
394
|
-
ticketId,
|
|
395
|
-
);
|
|
396
|
-
if (typeof nativeParent === 'number') {
|
|
397
|
-
parsedParents = [nativeParent];
|
|
398
|
-
}
|
|
399
|
-
} catch (err) {
|
|
400
|
-
Logger.warn(
|
|
401
|
-
`[cascadeCompletion] native parent lookup failed for #${ticketId}: ${err.message}`,
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
375
|
+
const parsedParents = Array.isArray(parentIds) ? parentIds : [];
|
|
405
376
|
|
|
406
377
|
if (parsedParents.length === 0) {
|
|
407
378
|
return { cascadedTo: [], failed: [] };
|
|
@@ -537,22 +508,18 @@ export async function cascadeParentState(provider, ticketId, opts = {}) {
|
|
|
537
508
|
}
|
|
538
509
|
|
|
539
510
|
/**
|
|
540
|
-
* Resolve the parent issue ids for a ticket
|
|
541
|
-
* annotations
|
|
542
|
-
*
|
|
511
|
+
* Resolve the parent issue ids for a ticket from its `blocks:` dependency
|
|
512
|
+
* annotations. Mirrors the resolution path used by {@link cascadeCompletion}
|
|
513
|
+
* — see there for why the `parent: #NNN` body fallback is gone (Story #4545).
|
|
543
514
|
*
|
|
544
515
|
* @param {import('../../ITicketingProvider.js').ITicketingProvider} provider
|
|
545
|
-
* @param {object}
|
|
516
|
+
* @param {object} _ticket
|
|
546
517
|
* @param {number} ticketId
|
|
547
518
|
* @returns {Promise<number[]>}
|
|
548
519
|
*/
|
|
549
|
-
async function resolveParentIds(provider,
|
|
520
|
+
async function resolveParentIds(provider, _ticket, ticketId) {
|
|
550
521
|
const { blocks: parentIds } = await provider.getTicketDependencies(ticketId);
|
|
551
|
-
|
|
552
|
-
const parentMatch = ticket?.body
|
|
553
|
-
? [...ticket.body.matchAll(/parent:\s*#(\d+)/gi)]
|
|
554
|
-
: [];
|
|
555
|
-
return parentMatch.map((m) => Number.parseInt(m[1], 10));
|
|
522
|
+
return Array.isArray(parentIds) ? parentIds : [];
|
|
556
523
|
}
|
|
557
524
|
|
|
558
525
|
/**
|