mandrel 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +59 -28
- package/.agents/agents/acceptance-critic.md +20 -9
- package/.agents/agents/story-worker.md +45 -48
- package/.agents/audit-checklists/performance.md +1 -1
- package/.agents/docs/SDLC.md +60 -46
- package/.agents/docs/agentrc-reference.json +8 -13
- package/.agents/docs/configuration.md +33 -57
- package/.agents/docs/execution-reference.md +39 -10
- package/.agents/docs/quality-gates.md +17 -19
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +64 -79
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +10 -0
- package/.agents/rules/git-conventions-reference.md +42 -51
- package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
- package/.agents/schemas/agentrc.schema.json +35 -46
- package/.agents/schemas/audit-rules.json +59 -1
- package/.agents/schemas/audit-rules.schema.json +33 -1
- package/.agents/schemas/lifecycle/README.md +1 -2
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
- package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
- package/.agents/schemas/signal-event.schema.json +3 -3
- package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +24 -68
- package/.agents/scripts/agents-bootstrap-github.js +1 -1
- package/.agents/scripts/bootstrap.js +3 -3
- package/.agents/scripts/check-dead-exports.js +43 -104
- package/.agents/scripts/check-doc-links.js +2 -2
- package/.agents/scripts/check-lifecycle-lint.js +1 -1
- package/.agents/scripts/check-workflow-cli-lint.js +91 -0
- package/.agents/scripts/deliver-recover.js +122 -0
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/evidence-gate.js +20 -50
- package/.agents/scripts/generate-skills-index.js +17 -1
- package/.agents/scripts/generate-workflows-doc.js +4 -4
- package/.agents/scripts/lib/ITicketingProvider.js +1 -19
- package/.agents/scripts/lib/audit-suite/selector.js +323 -23
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
- package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
- package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
- package/.agents/scripts/lib/checks/index.js +1 -1
- package/.agents/scripts/lib/checks/loop-health.js +12 -11
- package/.agents/scripts/lib/checks/state.js +17 -248
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
- package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
- package/.agents/scripts/lib/cli-args.js +23 -2
- package/.agents/scripts/lib/close-validation/gates.js +13 -13
- package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
- package/.agents/scripts/lib/close-validation/runner.js +13 -21
- package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
- package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config/delivery-routing.js +7 -6
- package/.agents/scripts/lib/config/explain.js +10 -16
- package/.agents/scripts/lib/config/github.js +7 -5
- package/.agents/scripts/lib/config/limits.js +15 -25
- package/.agents/scripts/lib/config/quality.js +11 -14
- package/.agents/scripts/lib/config/runners.js +8 -21
- package/.agents/scripts/lib/config/temp-paths.js +18 -56
- package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
- package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
- package/.agents/scripts/lib/config-settings-schema.js +48 -22
- package/.agents/scripts/lib/dead-exports-knip.js +105 -0
- package/.agents/scripts/lib/dead-exports-mode.js +51 -0
- package/.agents/scripts/lib/duplicate-search.js +38 -7
- package/.agents/scripts/lib/findings/promote-finding.js +23 -14
- package/.agents/scripts/lib/format-generated-json.js +97 -0
- package/.agents/scripts/lib/framework-version.js +19 -189
- package/.agents/scripts/lib/gh-exec.js +8 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
- package/.agents/scripts/lib/git-utils.js +0 -14
- package/.agents/scripts/lib/json-utils.js +1 -2
- package/.agents/scripts/lib/label-constants.js +0 -15
- package/.agents/scripts/lib/label-taxonomy.js +1 -12
- package/.agents/scripts/lib/observability/active-story-env.js +42 -163
- package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
- package/.agents/scripts/lib/observability/signal-validator.js +4 -4
- package/.agents/scripts/lib/observability/signals-writer.js +6 -82
- package/.agents/scripts/lib/observability/source-classifier.js +2 -2
- package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
- package/.agents/scripts/lib/orchestration/change-set.js +103 -0
- package/.agents/scripts/lib/orchestration/code-review.js +70 -191
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
- package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
- package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
- package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
- package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
- package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
- package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
- package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
- package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
- package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
- package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
- package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
- package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
- package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
- package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
- package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
- package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
- package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
- package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
- package/.agents/scripts/lib/planning-corpus.js +12 -286
- package/.agents/scripts/lib/preflight-runner.js +2 -2
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
- package/.agents/scripts/lib/signals/index.js +4 -17
- package/.agents/scripts/lib/signals/read.js +35 -35
- package/.agents/scripts/lib/signals/schema.js +8 -11
- package/.agents/scripts/lib/signals/span-tree.js +7 -7
- package/.agents/scripts/lib/signals/write.js +0 -1
- package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
- package/.agents/scripts/lib/skills/parse-skill.js +16 -3
- package/.agents/scripts/lib/story-adjacency.js +8 -7
- package/.agents/scripts/lib/story-body/story-body.js +81 -13
- package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
- package/.agents/scripts/lib/test-env.js +14 -1
- package/.agents/scripts/lib/test-tiers.js +0 -3
- package/.agents/scripts/lib/ticket-body-sections.js +0 -14
- package/.agents/scripts/lib/validation-evidence.js +31 -59
- package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
- package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
- package/.agents/scripts/plan-context.js +38 -7
- package/.agents/scripts/plan-critics.js +203 -0
- package/.agents/scripts/plan-persist.js +145 -35
- package/.agents/scripts/plan-run-epilogue.js +83 -38
- package/.agents/scripts/post-structured-comment.js +0 -38
- package/.agents/scripts/pr-watch-with-update.js +43 -22
- package/.agents/scripts/providers/github/compose.js +0 -1
- package/.agents/scripts/providers/github/errors.js +0 -19
- package/.agents/scripts/providers/github/issues.js +1 -11
- package/.agents/scripts/providers/github/mappers.js +5 -0
- package/.agents/scripts/providers/github/sub-issues.js +0 -47
- package/.agents/scripts/providers/github/tickets.js +33 -153
- package/.agents/scripts/providers/github.js +17 -6
- package/.agents/scripts/quality-preview.js +13 -6
- package/.agents/scripts/resolve-stories.js +236 -0
- package/.agents/scripts/run-coverage.js +4 -1
- package/.agents/scripts/run-lint.js +2 -2
- package/.agents/scripts/run-verify.js +31 -2
- package/.agents/scripts/signals-view.js +9 -10
- package/.agents/scripts/single-story-close.js +173 -18
- package/.agents/scripts/single-story-confirm-merge.js +288 -15
- package/.agents/scripts/single-story-init.js +6 -10
- package/.agents/scripts/stories-wave-tick.js +380 -53
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/update-ticket-state.js +8 -50
- package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
- package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
- package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
- package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
- package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
- package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
- package/.agents/skills/core/scope-triage/SKILL.md +3 -0
- package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
- package/.agents/skills/core/security-and-hardening/reference.md +375 -0
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
- package/.agents/workflows/audit-architecture.md +3 -4
- package/.agents/workflows/audit-clean-code.md +4 -4
- package/.agents/workflows/audit-documentation.md +4 -5
- package/.agents/workflows/audit-lighthouse.md +8 -0
- package/.agents/workflows/audit-navigability.md +10 -0
- package/.agents/workflows/audit-performance.md +2 -3
- package/.agents/workflows/audit-quality.md +8 -9
- package/.agents/workflows/audit-security.md +1 -2
- package/.agents/workflows/audit-seo.md +10 -0
- package/.agents/workflows/audit-ux-ui.md +7 -0
- package/.agents/workflows/deliver.md +133 -45
- package/.agents/workflows/git-cleanup.md +2 -2
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
- package/.agents/workflows/helpers/code-review.md +14 -12
- package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
- package/.agents/workflows/helpers/deliver-story.md +209 -118
- package/.agents/workflows/helpers/parallel-tooling.md +2 -2
- package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
- package/.agents/workflows/plan.md +239 -19
- package/.agents/workflows/qa-assist.md +6 -6
- package/.agents/workflows/qa-explore.md +3 -3
- package/.agents/workflows/qa-run.md +1 -5
- package/bin/mandrel.js +12 -1
- package/docs/CHANGELOG.md +62 -0
- package/lib/cli/registry.js +262 -19
- package/lib/cli/sync-agents.js +157 -0
- package/lib/cli/sync-commands.js +115 -6
- package/lib/cli/sync.js +168 -6
- package/lib/cli/update.js +105 -8
- package/lib/cli/version-helpers.js +131 -0
- package/lib/migrations/README.md +7 -5
- package/lib/migrations/index.js +17 -9
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
- package/package.json +2 -2
- package/.agents/schemas/epic-perf-report.schema.json +0 -89
- package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
- package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
- package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
- package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
- package/.agents/schemas/risk-verdict.schema.json +0 -53
- package/.agents/schemas/story-perf-summary.schema.json +0 -73
- package/.agents/scripts/analyze-execution.js +0 -444
- package/.agents/scripts/check-prepush-recovery.js +0 -90
- package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
- package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
- package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
- package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
- package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
- package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
- package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
- package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
- package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
- package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
- package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
- package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
- package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
- package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
- package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
- package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
- package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
- package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
- package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
- package/.agents/scripts/resolve-plan-run.js +0 -117
- package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bookkeeping-outbox.js — Epic #4476 (M5): buffered GitHub bookkeeping.
|
|
3
|
-
*
|
|
4
|
-
* Delivery bookkeeping — structured-comment upserts (progress, friction,
|
|
5
|
-
* wave-stall, …) and `agent::*` label flips — is human-visible surface that,
|
|
6
|
-
* in an **unattended** (`--yes` / headless) run, no operator is watching in
|
|
7
|
-
* real time. Emitting each one as a live GitHub round-trip mid-run costs an
|
|
8
|
-
* LLM turn per transition for a surface nobody reads until the run finishes.
|
|
9
|
-
*
|
|
10
|
-
* This module lets those non-urgent mutations be **buffered to a local NDJSON
|
|
11
|
-
* outbox** during the run and **reconciled to GitHub once at finalize**.
|
|
12
|
-
* GitHub stays the source of truth *at rest* (post-reconcile); only the
|
|
13
|
-
* per-transition chatter moves off the token stream. The outbox file survives
|
|
14
|
-
* a crash, so crash recovery drains whatever was buffered before the finalize
|
|
15
|
-
* reconcile ran.
|
|
16
|
-
*
|
|
17
|
-
* ## What is NEVER buffered
|
|
18
|
-
* The `agent::blocked` HITL gate (`.agents/instructions.md` §1.J) is the single
|
|
19
|
-
* authoritative runtime pause point. A genuine blocker MUST surface on GitHub
|
|
20
|
-
* **immediately**, not batched to finalize — an operator can only resume a run
|
|
21
|
-
* they can see is blocked. The {@link transitionStateOrBuffer} facade forces
|
|
22
|
-
* `agent::blocked` (and any caller-marked `urgent`) transition through the
|
|
23
|
-
* live path regardless of headless mode.
|
|
24
|
-
*
|
|
25
|
-
* ## Attended runs are unchanged
|
|
26
|
-
* Buffering is gated on the explicit `headless` signal (Story #4427's
|
|
27
|
-
* `--headless`/`--yes` plumbing). An attended run passes `headless: false` and
|
|
28
|
-
* every comment/label posts live exactly as before — byte-for-byte behaviour.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import {
|
|
32
|
-
appendFileSync,
|
|
33
|
-
existsSync,
|
|
34
|
-
mkdirSync,
|
|
35
|
-
readFileSync,
|
|
36
|
-
writeFileSync,
|
|
37
|
-
} from 'node:fs';
|
|
38
|
-
import path from 'node:path';
|
|
39
|
-
|
|
40
|
-
import { runTempDir } from '../config/temp-paths.js';
|
|
41
|
-
import { STATE_LABELS } from './ticketing/reads.js';
|
|
42
|
-
import { upsertStructuredComment } from './ticketing/state.js';
|
|
43
|
-
import { transitionTicketState } from './ticketing/transition.js';
|
|
44
|
-
|
|
45
|
-
/** Canonical basename for the per-run bookkeeping outbox. */
|
|
46
|
-
const OUTBOX_BASENAME = 'bookkeeping-outbox.ndjson';
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Resolve the canonical outbox path for a run:
|
|
50
|
-
* `temp/run-<id>/bookkeeping-outbox.ndjson`.
|
|
51
|
-
*
|
|
52
|
-
* Uses `runTempDir` directly; callers may still pass an `epicId`-named
|
|
53
|
-
* variable while the broader file/flag rename remains out of this pass.
|
|
54
|
-
*
|
|
55
|
-
* @param {number} epicId
|
|
56
|
-
* @param {object} [config] Resolved config (tempRoot).
|
|
57
|
-
* @returns {string}
|
|
58
|
-
*/
|
|
59
|
-
export function outboxPathFor(epicId, config) {
|
|
60
|
-
return path.join(runTempDir(epicId, config), OUTBOX_BASENAME);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Append one operation record to the outbox, creating the parent dir on
|
|
65
|
-
* demand. Best-effort at the storage layer is the caller's concern; here we
|
|
66
|
-
* let a genuine fs failure propagate so a mis-configured outbox path surfaces
|
|
67
|
-
* loudly in tests.
|
|
68
|
-
*
|
|
69
|
-
* @param {string} outboxPath
|
|
70
|
-
* @param {object} op
|
|
71
|
-
*/
|
|
72
|
-
function appendOp(outboxPath, op) {
|
|
73
|
-
mkdirSync(path.dirname(outboxPath), { recursive: true });
|
|
74
|
-
appendFileSync(outboxPath, `${JSON.stringify(op)}\n`, 'utf8');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Buffer a structured-comment upsert. The op captures everything
|
|
79
|
-
* `upsertStructuredComment` needs at drain time so the reconcile is a pure
|
|
80
|
-
* replay with no re-derivation.
|
|
81
|
-
*
|
|
82
|
-
* @param {{ outboxPath: string, ticketId: number, marker: string,
|
|
83
|
-
* body: string, attrs?: Record<string, string|number>|null,
|
|
84
|
-
* ts?: string }} args
|
|
85
|
-
*/
|
|
86
|
-
export function enqueueComment({
|
|
87
|
-
outboxPath,
|
|
88
|
-
ticketId,
|
|
89
|
-
marker,
|
|
90
|
-
body,
|
|
91
|
-
attrs = null,
|
|
92
|
-
ts = new Date().toISOString(),
|
|
93
|
-
}) {
|
|
94
|
-
appendOp(outboxPath, {
|
|
95
|
-
kind: 'comment',
|
|
96
|
-
ts,
|
|
97
|
-
ticketId,
|
|
98
|
-
marker,
|
|
99
|
-
body,
|
|
100
|
-
...(attrs ? { attrs } : {}),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Buffer an `agent::*` label transition.
|
|
106
|
-
*
|
|
107
|
-
* @param {{ outboxPath: string, ticketId: number, state: string,
|
|
108
|
-
* ts?: string }} args
|
|
109
|
-
*/
|
|
110
|
-
export function enqueueLabel({
|
|
111
|
-
outboxPath,
|
|
112
|
-
ticketId,
|
|
113
|
-
state,
|
|
114
|
-
ts = new Date().toISOString(),
|
|
115
|
-
}) {
|
|
116
|
-
appendOp(outboxPath, { kind: 'label', ts, ticketId, state });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Parse the outbox into an ordered array of operation records. Malformed
|
|
121
|
-
* lines are skipped (never throw). A missing / empty outbox yields `[]`.
|
|
122
|
-
*
|
|
123
|
-
* @param {string} outboxPath
|
|
124
|
-
* @returns {Array<object>}
|
|
125
|
-
*/
|
|
126
|
-
export function readOutbox(outboxPath) {
|
|
127
|
-
if (!outboxPath || !existsSync(outboxPath)) return [];
|
|
128
|
-
let raw;
|
|
129
|
-
try {
|
|
130
|
-
raw = readFileSync(outboxPath, 'utf8');
|
|
131
|
-
} catch {
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
if (!raw) return [];
|
|
135
|
-
const ops = [];
|
|
136
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
137
|
-
if (!line) continue;
|
|
138
|
-
try {
|
|
139
|
-
const op = JSON.parse(line);
|
|
140
|
-
if (op && typeof op === 'object') ops.push(op);
|
|
141
|
-
} catch {
|
|
142
|
-
// skip a torn / malformed line — a partial write from a crash is not
|
|
143
|
-
// fatal to the rest of the batch.
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return ops;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Drain the outbox to GitHub in FIFO order and clear it on success. Both sink
|
|
151
|
-
* operations are idempotent (comment upserts are marker-scoped;
|
|
152
|
-
* `transitionTicketState` applies one state via the canonical
|
|
153
|
-
* remove-all-then-add path), so a re-run after a partial drain converges. A
|
|
154
|
-
* per-op failure is recorded and the drain continues — a single bad ticket
|
|
155
|
-
* must not strand the rest of the batch. The outbox file is only truncated
|
|
156
|
-
* when EVERY op succeeded, so a crash mid-drain leaves the un-applied
|
|
157
|
-
* remainder for the next reconcile.
|
|
158
|
-
*
|
|
159
|
-
* @param {{ outboxPath: string,
|
|
160
|
-
* provider: import('../ITicketingProvider.js').ITicketingProvider,
|
|
161
|
-
* logger?: { warn?: (m: string) => void } }} args
|
|
162
|
-
* @returns {Promise<{ drained: number, comments: number, labels: number,
|
|
163
|
-
* errors: Array<{ op: object, error: string }>, cleared: boolean }>}
|
|
164
|
-
*/
|
|
165
|
-
export async function reconcileOutbox({ outboxPath, provider, logger }) {
|
|
166
|
-
const ops = readOutbox(outboxPath);
|
|
167
|
-
const result = {
|
|
168
|
-
drained: 0,
|
|
169
|
-
comments: 0,
|
|
170
|
-
labels: 0,
|
|
171
|
-
errors: [],
|
|
172
|
-
cleared: false,
|
|
173
|
-
};
|
|
174
|
-
if (ops.length === 0) {
|
|
175
|
-
result.cleared = true;
|
|
176
|
-
return result;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
for (const op of ops) {
|
|
180
|
-
try {
|
|
181
|
-
if (op.kind === 'comment') {
|
|
182
|
-
await upsertStructuredComment(
|
|
183
|
-
provider,
|
|
184
|
-
op.ticketId,
|
|
185
|
-
op.marker,
|
|
186
|
-
op.body,
|
|
187
|
-
op.attrs ?? null,
|
|
188
|
-
);
|
|
189
|
-
result.comments += 1;
|
|
190
|
-
result.drained += 1;
|
|
191
|
-
} else if (op.kind === 'label') {
|
|
192
|
-
await transitionTicketState(provider, op.ticketId, op.state);
|
|
193
|
-
result.labels += 1;
|
|
194
|
-
result.drained += 1;
|
|
195
|
-
} else {
|
|
196
|
-
result.errors.push({ op, error: `unknown op kind "${op.kind}"` });
|
|
197
|
-
}
|
|
198
|
-
} catch (err) {
|
|
199
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
-
result.errors.push({ op, error: message });
|
|
201
|
-
logger?.warn?.(
|
|
202
|
-
`[bookkeeping-outbox] reconcile op failed (${op.kind} #${op.ticketId}): ${message}`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Only truncate when the whole batch landed — otherwise the un-applied
|
|
208
|
-
// ops must survive for the next reconcile (crash-recovery contract).
|
|
209
|
-
if (result.errors.length === 0) {
|
|
210
|
-
try {
|
|
211
|
-
writeFileSync(outboxPath, '', 'utf8');
|
|
212
|
-
result.cleared = true;
|
|
213
|
-
} catch {
|
|
214
|
-
// A clear failure is non-fatal: the ops all landed, and the idempotent
|
|
215
|
-
// sinks make a redundant re-drain a no-op.
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return result;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Facade: post a structured comment live, OR buffer it to the outbox when the
|
|
223
|
-
* run is headless. Comments are never urgent (the operator reads them at
|
|
224
|
-
* finalize), so headless always buffers when an `outboxPath` is available.
|
|
225
|
-
*
|
|
226
|
-
* @param {{ provider: object, ticketId: number, marker: string, body: string,
|
|
227
|
-
* attrs?: Record<string, string|number>|null, headless?: boolean,
|
|
228
|
-
* outboxPath?: string|null }} args
|
|
229
|
-
* @returns {Promise<{ buffered: boolean }>}
|
|
230
|
-
*/
|
|
231
|
-
export async function postCommentOrBuffer({
|
|
232
|
-
provider,
|
|
233
|
-
ticketId,
|
|
234
|
-
marker,
|
|
235
|
-
body,
|
|
236
|
-
attrs = null,
|
|
237
|
-
headless = false,
|
|
238
|
-
outboxPath = null,
|
|
239
|
-
}) {
|
|
240
|
-
if (headless && outboxPath) {
|
|
241
|
-
enqueueComment({ outboxPath, ticketId, marker, body, attrs });
|
|
242
|
-
return { buffered: true };
|
|
243
|
-
}
|
|
244
|
-
await upsertStructuredComment(provider, ticketId, marker, body, attrs);
|
|
245
|
-
return { buffered: false };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Facade: flip an `agent::*` state live, OR buffer it when headless — EXCEPT
|
|
250
|
-
* `agent::blocked` (and any caller-marked `urgent` flip), which ALWAYS goes
|
|
251
|
-
* live so the HITL gate surfaces immediately (§1.J).
|
|
252
|
-
*
|
|
253
|
-
* @param {{ provider: object, ticketId: number, state: string,
|
|
254
|
-
* headless?: boolean, outboxPath?: string|null,
|
|
255
|
-
* urgent?: boolean }} args
|
|
256
|
-
* @returns {Promise<{ buffered: boolean }>}
|
|
257
|
-
*/
|
|
258
|
-
export async function transitionStateOrBuffer({
|
|
259
|
-
provider,
|
|
260
|
-
ticketId,
|
|
261
|
-
state,
|
|
262
|
-
headless = false,
|
|
263
|
-
outboxPath = null,
|
|
264
|
-
urgent = false,
|
|
265
|
-
}) {
|
|
266
|
-
const mustSurfaceNow = urgent || state === STATE_LABELS.BLOCKED;
|
|
267
|
-
if (headless && outboxPath && !mustSurfaceNow) {
|
|
268
|
-
enqueueLabel({ outboxPath, ticketId, state });
|
|
269
|
-
return { buffered: true };
|
|
270
|
-
}
|
|
271
|
-
await transitionTicketState(provider, ticketId, state);
|
|
272
|
-
return { buffered: false };
|
|
273
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ErrorJournal — append-only JSONL log of swallowed errors from the
|
|
3
|
-
* orchestration engine's "silent catch" sites.
|
|
4
|
-
*
|
|
5
|
-
* Design notes (see tech spec #382):
|
|
6
|
-
* - JSONL format: a crashed orchestrator still leaves a valid file up to the
|
|
7
|
-
* last full line.
|
|
8
|
-
* - Lazy open: the fd opens on first `record()` call, so runs without any
|
|
9
|
-
* swallowed errors produce no file.
|
|
10
|
-
* - Additive: does not replace `logger.warn`. Callers emit both so live
|
|
11
|
-
* tails stay useful.
|
|
12
|
-
* - Secret masking: before writing, any string value whose parent key name
|
|
13
|
-
* or content looks secret-like gets a `::add-mask::<value>` directive
|
|
14
|
-
* emitted to stdout so any host with mask support redacts accidental
|
|
15
|
-
* echoes.
|
|
16
|
-
* - Finalize is idempotent: repeat calls are safe.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import fs from 'node:fs/promises';
|
|
20
|
-
import path from 'node:path';
|
|
21
|
-
|
|
22
|
-
const SECRET_KEY_RE = /token|secret|password|bearer|apikey|api[-_]?key|auth/i;
|
|
23
|
-
const SECRET_VALUE_RE =
|
|
24
|
-
/\b(gh[pous]_[A-Za-z0-9]{30,}|sk-[A-Za-z0-9]{20,}|xox[bpars]-[A-Za-z0-9-]+|AKIA[0-9A-Z]{16})\b/;
|
|
25
|
-
// Hoisted so the maintainability analyzer (escomplex) doesn't choke on an
|
|
26
|
-
// inline RegExp literal inside .split() — it mis-parses the sequence.
|
|
27
|
-
const NEWLINE_RE = /\r?\n/;
|
|
28
|
-
|
|
29
|
-
export class ErrorJournal {
|
|
30
|
-
/**
|
|
31
|
-
* @param {{ epicId: number, logDir?: string }} opts
|
|
32
|
-
*/
|
|
33
|
-
constructor({ epicId, logDir = 'temp' } = {}) {
|
|
34
|
-
if (!Number.isInteger(epicId)) {
|
|
35
|
-
throw new TypeError('ErrorJournal requires a numeric epicId');
|
|
36
|
-
}
|
|
37
|
-
this.epicId = epicId;
|
|
38
|
-
this.logDir = logDir;
|
|
39
|
-
this._path = path.join(logDir, `epic-${epicId}-errors.log`);
|
|
40
|
-
this._fh = null;
|
|
41
|
-
this._opening = null;
|
|
42
|
-
this._finalized = false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** @returns {string} Absolute or relative path to the journal file. */
|
|
46
|
-
get path() {
|
|
47
|
-
return this._path;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Append one entry to the journal. Emits `::add-mask::` directives for any
|
|
52
|
-
* secret-looking values before writing.
|
|
53
|
-
*
|
|
54
|
-
* @param {{
|
|
55
|
-
* module: string,
|
|
56
|
-
* op: string,
|
|
57
|
-
* error: unknown,
|
|
58
|
-
* recovery?: string,
|
|
59
|
-
* }} entry
|
|
60
|
-
*/
|
|
61
|
-
async record({ module, op, error, recovery } = {}) {
|
|
62
|
-
if (this._finalized) return;
|
|
63
|
-
const payload = {
|
|
64
|
-
ts: new Date().toISOString(),
|
|
65
|
-
epicId: this.epicId,
|
|
66
|
-
module: module ?? null,
|
|
67
|
-
op: op ?? null,
|
|
68
|
-
error: serializeError(error),
|
|
69
|
-
recovery: recovery ?? null,
|
|
70
|
-
};
|
|
71
|
-
emitMasksFor(payload);
|
|
72
|
-
const fh = await this._ensureOpen();
|
|
73
|
-
await fh.write(`${JSON.stringify(payload)}\n`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Close the underlying file handle. Idempotent: repeat calls are no-ops.
|
|
78
|
-
* Calls to `record` after finalize silently drop.
|
|
79
|
-
*/
|
|
80
|
-
async finalize() {
|
|
81
|
-
if (this._finalized) return;
|
|
82
|
-
this._finalized = true;
|
|
83
|
-
const fh = this._fh;
|
|
84
|
-
this._fh = null;
|
|
85
|
-
this._opening = null;
|
|
86
|
-
if (fh) {
|
|
87
|
-
try {
|
|
88
|
-
await fh.close();
|
|
89
|
-
} catch {
|
|
90
|
-
// idempotent — closing a closed handle is fine
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async _ensureOpen() {
|
|
96
|
-
if (this._fh) return this._fh;
|
|
97
|
-
if (!this._opening) {
|
|
98
|
-
this._opening = (async () => {
|
|
99
|
-
await fs.mkdir(this.logDir, { recursive: true });
|
|
100
|
-
this._fh = await fs.open(this._path, 'a');
|
|
101
|
-
return this._fh;
|
|
102
|
-
})();
|
|
103
|
-
}
|
|
104
|
-
return this._opening;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function serializeError(err) {
|
|
109
|
-
if (err == null) return null;
|
|
110
|
-
if (err instanceof Error) {
|
|
111
|
-
return { name: err.name, message: err.message, stack: err.stack };
|
|
112
|
-
}
|
|
113
|
-
if (typeof err === 'string') return { message: err };
|
|
114
|
-
if (typeof err === 'object') return err;
|
|
115
|
-
return { message: String(err) };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function emitMasksFor(node, parentKey = null) {
|
|
119
|
-
if (node == null) return;
|
|
120
|
-
if (typeof node === 'string') {
|
|
121
|
-
if (
|
|
122
|
-
(parentKey && SECRET_KEY_RE.test(parentKey)) ||
|
|
123
|
-
SECRET_VALUE_RE.test(node)
|
|
124
|
-
) {
|
|
125
|
-
for (const line of node.split(NEWLINE_RE)) {
|
|
126
|
-
const trimmed = line.trim();
|
|
127
|
-
if (trimmed) console.log(`::add-mask::${trimmed}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (Array.isArray(node)) {
|
|
133
|
-
for (const item of node) emitMasksFor(item, parentKey);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (typeof node === 'object') {
|
|
137
|
-
for (const [k, v] of Object.entries(node)) emitMasksFor(v, k);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* emit-story-heartbeat.js — Story #3057.
|
|
3
|
-
*
|
|
4
|
-
* Programmatic helper that appends a single `story.heartbeat` NDJSON
|
|
5
|
-
* record to the Story lifecycle ledger (`temp/.../lifecycle.ndjson` via
|
|
6
|
-
* `lib/config/temp-paths.js`) during a Story's implementation loop. Story-implementation phases can run for many
|
|
7
|
-
* minutes between dispatch and merge; `story.heartbeat` is the
|
|
8
|
-
* inspectable in-progress signal the host-loop reconciler reads to
|
|
9
|
-
* confirm forward progress.
|
|
10
|
-
*
|
|
11
|
-
* Distinct from:
|
|
12
|
-
* - `story.dispatch.start` — one per Story per dispatch attempt
|
|
13
|
-
* (lifecycle-emit-story-dispatch.js).
|
|
14
|
-
* - `story.merged` — one per Story per close, post-merge.
|
|
15
|
-
*
|
|
16
|
-
* The emit is best-effort: a failure to append (missing schema,
|
|
17
|
-
* unreachable ledger path, validation error) MUST NOT block the phase
|
|
18
|
-
* transition itself. Callers should catch and log via the script's
|
|
19
|
-
* Logger; the heartbeat is observability, not state.
|
|
20
|
-
*
|
|
21
|
-
* Schema contract (story.heartbeat.schema.json):
|
|
22
|
-
* { event, storyId, epicId, phase, timestamp, operator? }
|
|
23
|
-
*
|
|
24
|
-
* The schema declares `additionalProperties: false`, so this emitter's
|
|
25
|
-
* signature is deliberately narrow: only the schema-allowed fields are
|
|
26
|
-
* accepted. The earlier per-child Task id and progress counters
|
|
27
|
-
* were dropped under Epic #3078's
|
|
28
|
-
* 2-tier hard cutover — they would fail strict validation and have no
|
|
29
|
-
* meaning now that the Story is the leaf execution unit with no child
|
|
30
|
-
* tickets. The optional `operator` field (Story #3480) records the handle
|
|
31
|
-
* holding the assignee-as-lease claim; it is included only when supplied so
|
|
32
|
-
* pre-lease callers continue to emit the unchanged shape.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
36
|
-
import path from 'node:path';
|
|
37
|
-
import { fileURLToPath } from 'node:url';
|
|
38
|
-
|
|
39
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
40
|
-
import addFormats from 'ajv-formats';
|
|
41
|
-
|
|
42
|
-
import { epicLedgerPath } from '../../config/temp-paths.js';
|
|
43
|
-
|
|
44
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
|
-
const SCHEMA_PATH = path.resolve(
|
|
46
|
-
__dirname,
|
|
47
|
-
'..',
|
|
48
|
-
'..',
|
|
49
|
-
'..',
|
|
50
|
-
'..',
|
|
51
|
-
'schemas',
|
|
52
|
-
'lifecycle',
|
|
53
|
-
'story.heartbeat.schema.json',
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const VALID_PHASES = new Set([
|
|
57
|
-
'init',
|
|
58
|
-
'implementing',
|
|
59
|
-
'closing',
|
|
60
|
-
'blocked',
|
|
61
|
-
'done',
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
let _validator;
|
|
65
|
-
|
|
66
|
-
function getValidator() {
|
|
67
|
-
if (_validator) return _validator;
|
|
68
|
-
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
69
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
70
|
-
addFormats(ajv);
|
|
71
|
-
_validator = ajv.compile(schema);
|
|
72
|
-
return _validator;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Append exactly one `story.heartbeat` NDJSON record to the Epic ledger.
|
|
77
|
-
*
|
|
78
|
-
* @param {object} opts
|
|
79
|
-
* @param {number} opts.storyId Story whose implementation loop is firing.
|
|
80
|
-
* @param {number} opts.epicId Parent Epic — required for the ledger path.
|
|
81
|
-
* @param {string} [opts.phase='implementing']
|
|
82
|
-
* One of init|implementing|closing|blocked|done.
|
|
83
|
-
* @param {string} [opts.timestamp] ISO-8601 wall clock. Defaults to now().
|
|
84
|
-
* @param {string} [opts.operator] Optional lease-owner handle (Story #3480).
|
|
85
|
-
* When a non-empty string is supplied it is
|
|
86
|
-
* recorded on the payload so the
|
|
87
|
-
* assignee-as-lease primitive can decide a
|
|
88
|
-
* claim's liveness from the latest heartbeat
|
|
89
|
-
* for a given owner. Omitted when absent so
|
|
90
|
-
* pre-lease callers emit the unchanged shape.
|
|
91
|
-
* @param {object} [opts.config] Optional resolved config for tempRoot.
|
|
92
|
-
* @param {string} [opts.ledgerPath] Override for tests.
|
|
93
|
-
* @returns {{ ledgerPath: string, record: object }}
|
|
94
|
-
*/
|
|
95
|
-
export function emitStoryHeartbeat(opts) {
|
|
96
|
-
const {
|
|
97
|
-
storyId,
|
|
98
|
-
epicId,
|
|
99
|
-
phase = 'implementing',
|
|
100
|
-
timestamp = new Date().toISOString(),
|
|
101
|
-
operator,
|
|
102
|
-
config,
|
|
103
|
-
ledgerPath: ledgerPathOverride,
|
|
104
|
-
} = opts ?? {};
|
|
105
|
-
|
|
106
|
-
if (!Number.isInteger(storyId) || storyId < 1) {
|
|
107
|
-
throw new Error('emitStoryHeartbeat: storyId must be a positive integer');
|
|
108
|
-
}
|
|
109
|
-
if (!Number.isInteger(epicId) || epicId < 1) {
|
|
110
|
-
throw new Error('emitStoryHeartbeat: epicId must be a positive integer');
|
|
111
|
-
}
|
|
112
|
-
if (!VALID_PHASES.has(phase)) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
`emitStoryHeartbeat: phase "${phase}" must be one of: ${[...VALID_PHASES].join(', ')}`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
if (
|
|
118
|
-
operator !== undefined &&
|
|
119
|
-
(typeof operator !== 'string' || operator.length === 0)
|
|
120
|
-
) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
'emitStoryHeartbeat: operator, when supplied, must be a non-empty string',
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const payload = {
|
|
127
|
-
event: 'story.heartbeat',
|
|
128
|
-
storyId,
|
|
129
|
-
epicId,
|
|
130
|
-
phase,
|
|
131
|
-
timestamp,
|
|
132
|
-
...(operator !== undefined ? { operator } : {}),
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const validator = getValidator();
|
|
136
|
-
if (!validator(payload)) {
|
|
137
|
-
const detail = (validator.errors ?? [])
|
|
138
|
-
.map((e) => `${e.instancePath || '/'} ${e.message}`)
|
|
139
|
-
.join('; ');
|
|
140
|
-
throw new Error(
|
|
141
|
-
`emitStoryHeartbeat: payload failed schema validation: ${detail}`,
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const ledgerPath = ledgerPathOverride ?? epicLedgerPath(epicId, config);
|
|
146
|
-
mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
147
|
-
const record = {
|
|
148
|
-
kind: 'emitted',
|
|
149
|
-
ts: timestamp,
|
|
150
|
-
event: 'story.heartbeat',
|
|
151
|
-
payload,
|
|
152
|
-
};
|
|
153
|
-
appendFileSync(ledgerPath, `${JSON.stringify(record)}\n`, 'utf8');
|
|
154
|
-
return { ledgerPath, record };
|
|
155
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ledger-diff.js — structural-diff and ordering-assertion helpers over a
|
|
3
|
-
* lifecycle ledger (`temp/epic-<id>/lifecycle.ndjson`) for the
|
|
4
|
-
* repeatability invariants.
|
|
5
|
-
*
|
|
6
|
-
* Relocated from the retired `.agents/scripts/lifecycle-diff.js` CLI
|
|
7
|
-
* (#4482): the assertion helpers are the live surface — they pin the
|
|
8
|
-
* cross-listener ordering contracts in the lifecycle invariant tests —
|
|
9
|
-
* while the CLI wrapper had no workflow, script, or CI consumer.
|
|
10
|
-
*
|
|
11
|
-
* Invariants are derived from Tech Spec #2189 § Repeatability Acceptance
|
|
12
|
-
* Criteria.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse an NDJSON lifecycle ledger into an array of records. Blank
|
|
17
|
-
* lines tolerated; malformed JSON throws with line number. Duplicated
|
|
18
|
-
* from `trace-logger.js` to keep the diff surface decoupled from the
|
|
19
|
-
* listener surface.
|
|
20
|
-
*/
|
|
21
|
-
export function parseLedgerText(text) {
|
|
22
|
-
const out = [];
|
|
23
|
-
const lines = String(text || '').split('\n');
|
|
24
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
25
|
-
const line = lines[i].trim();
|
|
26
|
-
if (line.length === 0) continue;
|
|
27
|
-
try {
|
|
28
|
-
out.push(JSON.parse(line));
|
|
29
|
-
} catch (_err) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`ledger-diff: malformed JSON in ledger on line ${i + 1}: ${line.slice(0, 80)}`,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Project a single record into a comparison key. `ts` and `seqId` are
|
|
40
|
-
* intentionally elided; the rest of the record is included so that the
|
|
41
|
-
* structural shape (event order, payload contents, listener attribution
|
|
42
|
-
* on failed records) is the diff surface.
|
|
43
|
-
*
|
|
44
|
-
* Exported for unit tests that pin the diff contract.
|
|
45
|
-
*/
|
|
46
|
-
export function projectRecord(rec) {
|
|
47
|
-
if (!rec || typeof rec !== 'object') return rec;
|
|
48
|
-
const { ts: _ts, seqId: _seqId, ...rest } = rec;
|
|
49
|
-
return rest;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Structural diff of two ledger arrays. Returns an array of mismatch
|
|
54
|
-
* descriptors (empty when identical modulo `ts`/`seqId`).
|
|
55
|
-
*/
|
|
56
|
-
export function diff(ledgerA, ledgerB) {
|
|
57
|
-
const a = ledgerA.map(projectRecord);
|
|
58
|
-
const b = ledgerB.map(projectRecord);
|
|
59
|
-
const out = [];
|
|
60
|
-
const max = Math.max(a.length, b.length);
|
|
61
|
-
for (let i = 0; i < max; i += 1) {
|
|
62
|
-
const left = a[i];
|
|
63
|
-
const right = b[i];
|
|
64
|
-
const leftJson = JSON.stringify(left);
|
|
65
|
-
const rightJson = JSON.stringify(right);
|
|
66
|
-
if (leftJson !== rightJson) {
|
|
67
|
-
out.push({ index: i, left: left ?? null, right: right ?? null });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Assert: epic.merge.armed must be preceded by epic.merge.ready
|
|
75
|
-
* (same run). Returns `{ ok: true }` or `{ ok: false, reason }`.
|
|
76
|
-
*/
|
|
77
|
-
export function assertMergeGateOrdering(records) {
|
|
78
|
-
let sawReady = false;
|
|
79
|
-
let sawReadySeq = null;
|
|
80
|
-
for (const rec of records) {
|
|
81
|
-
if (rec.kind !== 'emitted') continue;
|
|
82
|
-
if (rec.event === 'epic.merge.ready') {
|
|
83
|
-
sawReady = true;
|
|
84
|
-
sawReadySeq = rec.seqId;
|
|
85
|
-
} else if (rec.event === 'epic.merge.armed') {
|
|
86
|
-
if (!sawReady) {
|
|
87
|
-
return {
|
|
88
|
-
ok: false,
|
|
89
|
-
reason: `epic.merge.armed at seqId=${rec.seqId} without preceding epic.merge.ready`,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
if (sawReadySeq != null && rec.seqId <= sawReadySeq) {
|
|
93
|
-
return {
|
|
94
|
-
ok: false,
|
|
95
|
-
reason: `epic.merge.armed seqId=${rec.seqId} must be > epic.merge.ready seqId=${sawReadySeq}`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return { ok: true };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Assert: pr.created must be preceded by either acceptance.reconcile.ok
|
|
105
|
-
* or acceptance.reconcile.waived in the same run. Story #2893 split the
|
|
106
|
-
* waiver path out of `.skipped` so the Finalizer now subscribes to both
|
|
107
|
-
* `.ok` and `.waived`; either is a valid predecessor for pr.created.
|
|
108
|
-
* A reconcile.skipped (empty-spec only after #2893) or reconcile.failed
|
|
109
|
-
* before pr.created remains a violation.
|
|
110
|
-
*/
|
|
111
|
-
export function assertReconcileOrdering(records) {
|
|
112
|
-
let sawReconcileGate = false;
|
|
113
|
-
let sawReconcileGateSeq = null;
|
|
114
|
-
let sawReconcileGateEvent = null;
|
|
115
|
-
for (const rec of records) {
|
|
116
|
-
if (rec.kind !== 'emitted') continue;
|
|
117
|
-
if (
|
|
118
|
-
rec.event === 'acceptance.reconcile.ok' ||
|
|
119
|
-
rec.event === 'acceptance.reconcile.waived'
|
|
120
|
-
) {
|
|
121
|
-
sawReconcileGate = true;
|
|
122
|
-
sawReconcileGateSeq = rec.seqId;
|
|
123
|
-
sawReconcileGateEvent = rec.event;
|
|
124
|
-
} else if (rec.event === 'pr.created') {
|
|
125
|
-
if (!sawReconcileGate) {
|
|
126
|
-
return {
|
|
127
|
-
ok: false,
|
|
128
|
-
reason: `pr.created at seqId=${rec.seqId} without preceding acceptance.reconcile.ok or acceptance.reconcile.waived`,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (sawReconcileGateSeq != null && rec.seqId <= sawReconcileGateSeq) {
|
|
132
|
-
return {
|
|
133
|
-
ok: false,
|
|
134
|
-
reason: `pr.created seqId=${rec.seqId} must be > ${sawReconcileGateEvent} seqId=${sawReconcileGateSeq}`,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return { ok: true };
|
|
140
|
-
}
|