mandrel 1.58.0 → 1.60.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 +100 -98
- package/.agents/docs/SDLC.md +140 -141
- package/.agents/docs/configuration.md +16 -16
- package/.agents/docs/workflows.md +7 -8
- package/.agents/instructions.md +12 -11
- package/.agents/personas/architect.md +1 -1
- package/.agents/personas/product.md +1 -1
- package/.agents/personas/project-manager.md +14 -14
- package/.agents/personas/technical-writer.md +1 -1
- package/.agents/rules/changelog-style.md +5 -5
- package/.agents/rules/git-conventions.md +3 -3
- package/.agents/schemas/agentrc.schema.json +3 -3
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/schemas/dispatch-manifest.json +4 -4
- package/.agents/schemas/epic-spec.schema.json +15 -45
- package/.agents/schemas/lifecycle/README.md +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/README.md +1 -1
- package/.agents/scripts/acceptance-eval.js +21 -4
- package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
- package/.agents/scripts/analyze-execution.js +2 -2
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/audit-to-stories.js +1 -1
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/check-doc-links.js +2 -3
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/diagnose-friction.js +1 -1
- package/.agents/scripts/dispatcher.js +2 -2
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/epic-audit-prepare.js +3 -3
- package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
- package/.agents/scripts/epic-deliver-preflight.js +11 -9
- package/.agents/scripts/epic-deliver-prepare.js +13 -5
- package/.agents/scripts/epic-execute-record-wave.js +5 -5
- package/.agents/scripts/epic-plan-healthcheck.js +6 -10
- package/.agents/scripts/epic-plan-spec-validate.js +1 -1
- package/.agents/scripts/epic-reconcile.js +11 -29
- package/.agents/scripts/evidence-gate.js +2 -2
- package/.agents/scripts/generate-workflows-doc.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +40 -24
- package/.agents/scripts/lib/ITicketingProvider.js +1 -1
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
- package/.agents/scripts/lib/baseline-snapshot.js +7 -7
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/codebase-snapshot.js +1 -1
- package/.agents/scripts/lib/config/explain.js +1 -1
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config/runners.js +2 -2
- package/.agents/scripts/lib/config/runtime.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +2 -2
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
- package/.agents/scripts/lib/config-settings-schema.js +3 -3
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
- package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/findings/classify-finding.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +10 -10
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/label-constants.js +3 -4
- package/.agents/scripts/lib/label-taxonomy.js +3 -8
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +81 -7
- package/.agents/scripts/lib/orchestration/code-review.js +95 -82
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
- package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
- package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +14 -37
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +22 -22
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +206 -58
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +27 -3
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +28 -8
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +13 -41
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +2 -3
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -8
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +10 -10
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +8 -20
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
- package/.agents/scripts/lib/orchestration/file-assumptions.js +6 -5
- package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
- package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
- package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +3 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +8 -8
- package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +36 -13
- package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
- package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
- package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +32 -5
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
- package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +4 -35
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +44 -73
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +16 -7
- package/.agents/scripts/lib/orchestration/ticketing/state.js +53 -439
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +3 -3
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +2 -8
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
- package/.agents/scripts/lib/preflight-runner.js +1 -1
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
- package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
- package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
- package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
- package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/signals/schema.js +1 -1
- package/.agents/scripts/lib/spec/index.js +1 -1
- package/.agents/scripts/lib/spec/loader.js +2 -2
- package/.agents/scripts/lib/spec/state.js +7 -16
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
- package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
- package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
- package/.agents/scripts/lib/story-lifecycle.js +9 -9
- package/.agents/scripts/lib/story-plan.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
- package/.agents/scripts/lifecycle-emit.js +1 -1
- package/.agents/scripts/providers/github/board-add.js +1 -1
- package/.agents/scripts/providers/github/errors.js +1 -1
- package/.agents/scripts/providers/github/mappers.js +2 -2
- package/.agents/scripts/providers/github/tickets.js +114 -10
- package/.agents/scripts/resync-status-column.js +1 -1
- package/.agents/scripts/retro-run.js +2 -2
- package/.agents/scripts/run-lint.js +10 -1
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/single-story-init.js +1 -1
- package/.agents/scripts/stories-wave-tick.js +13 -10
- package/.agents/scripts/story-close.js +1 -1
- package/.agents/scripts/story-init.js +162 -26
- package/.agents/scripts/story-phase.js +5 -5
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/sync-branch-from-base.js +2 -2
- package/.agents/scripts/validate-docs-freshness.js +1 -1
- package/.agents/scripts/wave-tick.js +1 -1
- package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
- package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
- package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
- package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
- package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
- package/.agents/skills/core/scope-triage/SKILL.md +9 -10
- package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
- package/.agents/skills/skills.index.json +7 -7
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/templates/agent-protocol.md +2 -2
- package/.agents/workflows/agents-update.md +2 -2
- package/.agents/workflows/audit-architecture.md +2 -2
- package/.agents/workflows/audit-clean-code.md +2 -2
- package/.agents/workflows/audit-dependencies.md +1 -1
- package/.agents/workflows/audit-devops.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/audit-lighthouse.md +1 -1
- package/.agents/workflows/audit-performance.md +2 -2
- package/.agents/workflows/audit-privacy.md +1 -1
- package/.agents/workflows/audit-quality.md +2 -2
- package/.agents/workflows/audit-security.md +2 -2
- package/.agents/workflows/audit-seo.md +1 -1
- package/.agents/workflows/audit-sre.md +1 -1
- package/.agents/workflows/audit-to-stories.md +10 -10
- package/.agents/workflows/audit-ux-ui.md +1 -1
- package/.agents/workflows/deliver.md +85 -0
- package/.agents/workflows/explain.md +3 -3
- package/.agents/workflows/git-merge-pr.md +1 -1
- package/.agents/workflows/git-pr-all.md +13 -10
- package/.agents/workflows/git-push.md +6 -3
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
- package/.agents/workflows/helpers/code-review.md +5 -5
- package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +59 -66
- package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
- package/.agents/workflows/helpers/diagnose.md +1 -1
- package/.agents/workflows/helpers/epic-audit.md +6 -6
- package/.agents/workflows/helpers/epic-deliver-story.md +28 -39
- package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
- package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
- package/.agents/workflows/helpers/epic-testing.md +3 -3
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
- package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
- package/.agents/workflows/helpers/signals.md +1 -1
- package/.agents/workflows/helpers/single-story-deliver.md +12 -11
- package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
- package/.agents/workflows/onboard.md +21 -20
- package/.agents/workflows/plan.md +89 -0
- package/.agents/workflows/qa-explore.md +1 -1
- package/.agents/workflows/qa-run-harness.md +1 -1
- package/README.md +17 -20
- package/docs/CHANGELOG.md +1149 -0
- package/lib/cli/__tests__/update-changelog-surface.test.js +357 -0
- package/lib/cli/__tests__/update-reexec.test.js +513 -0
- package/lib/cli/init.js +338 -0
- package/lib/cli/update.js +413 -52
- package/package.json +3 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- package/.agents/scripts/story-deliver-prepare.js +0 -267
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: ratchet-down architecture gate for import cycles (Story #3991).
|
|
3
|
+
*
|
|
4
|
+
* Walks every `.js` file under `.agents/scripts/` (excluding
|
|
5
|
+
* `node_modules`), parses relative static-import edges
|
|
6
|
+
* (`from './…/x.js'`), detects directed cycles via DFS, and compares
|
|
7
|
+
* them against the committed allowlist at `baselines/arch-cycles.json`.
|
|
8
|
+
*
|
|
9
|
+
* Ratchet semantics mirror `check-dead-exports.js`:
|
|
10
|
+
* - Any detected cycle NOT in the allowlist → exit 1, cycle path printed.
|
|
11
|
+
* - Allowlisted cycle no longer detected → printed as `-` (removal),
|
|
12
|
+
* warning that the allowlist can shrink. Removals-only exits 0.
|
|
13
|
+
* - Clean diff → exit 0.
|
|
14
|
+
*
|
|
15
|
+
* Cycles are normalized by rotating to the lexicographically-smallest
|
|
16
|
+
* member so the same cycle always serializes identically regardless of
|
|
17
|
+
* the DFS entry point.
|
|
18
|
+
*
|
|
19
|
+
* Flags:
|
|
20
|
+
* --baseline <path> override the allowlist path (default
|
|
21
|
+
* `baselines/arch-cycles.json`, resolved from cwd)
|
|
22
|
+
* --root <path> override the scanned root (default
|
|
23
|
+
* `.agents/scripts`, resolved from cwd)
|
|
24
|
+
* --json write the structured envelope to stdout
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import process from 'node:process';
|
|
30
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse argv for `--baseline <path>`, `--root <path>`, and `--json`.
|
|
34
|
+
* Exported so unit tests can pin the parser.
|
|
35
|
+
*
|
|
36
|
+
* @param {string[]} argv
|
|
37
|
+
* @returns {{ baselinePath: string | null, rootPath: string | null, json: boolean }}
|
|
38
|
+
*/
|
|
39
|
+
export function parseArgv(argv = []) {
|
|
40
|
+
let baselinePath = null;
|
|
41
|
+
let rootPath = null;
|
|
42
|
+
let json = false;
|
|
43
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
44
|
+
const a = argv[i];
|
|
45
|
+
if (a === '--baseline') {
|
|
46
|
+
const next = argv[i + 1];
|
|
47
|
+
if (next && !next.startsWith('--')) {
|
|
48
|
+
baselinePath = next;
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
} else if (a === '--root') {
|
|
52
|
+
const next = argv[i + 1];
|
|
53
|
+
if (next && !next.startsWith('--')) {
|
|
54
|
+
rootPath = next;
|
|
55
|
+
i += 1;
|
|
56
|
+
}
|
|
57
|
+
} else if (a === '--json') {
|
|
58
|
+
json = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { baselinePath, rootPath, json };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively collect `.js` files under `rootDir`, skipping
|
|
66
|
+
* `node_modules`. Returns absolute paths, sorted for determinism.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} rootDir
|
|
69
|
+
* @returns {string[]}
|
|
70
|
+
*/
|
|
71
|
+
export function collectJsFiles(rootDir) {
|
|
72
|
+
const out = [];
|
|
73
|
+
const walk = (dir) => {
|
|
74
|
+
let entries;
|
|
75
|
+
try {
|
|
76
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
77
|
+
} catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.name === 'node_modules') continue;
|
|
82
|
+
const full = path.join(dir, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
walk(full);
|
|
85
|
+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
86
|
+
out.push(full);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(rootDir);
|
|
91
|
+
return out.sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const IMPORT_RE = /from\s+['"](\.\.?\/[^'"]+\.js)['"]/g;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Pure helper: extract relative static-import specifiers from source text.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} source
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
export function parseRelativeImports(source) {
|
|
103
|
+
const specs = [];
|
|
104
|
+
for (const m of source.matchAll(IMPORT_RE)) {
|
|
105
|
+
specs.push(m[1]);
|
|
106
|
+
}
|
|
107
|
+
return specs;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a directed import graph over the given files. Node identity is the
|
|
112
|
+
* file path relative to `rootDir`, posix-separated, so the graph (and any
|
|
113
|
+
* cycles found in it) serializes identically across platforms. Edges that
|
|
114
|
+
* resolve outside the scanned file set are dropped.
|
|
115
|
+
*
|
|
116
|
+
* @param {string[]} files absolute paths
|
|
117
|
+
* @param {string} rootDir
|
|
118
|
+
* @param {{ readFile?: (p: string) => string }} [opts]
|
|
119
|
+
* @returns {Map<string, string[]>}
|
|
120
|
+
*/
|
|
121
|
+
export function buildGraph(files, rootDir, { readFile } = {}) {
|
|
122
|
+
const read = readFile ?? ((p) => fs.readFileSync(p, 'utf-8'));
|
|
123
|
+
const toId = (abs) => path.relative(rootDir, abs).split(path.sep).join('/');
|
|
124
|
+
const idSet = new Set(files.map(toId));
|
|
125
|
+
const graph = new Map();
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const id = toId(file);
|
|
128
|
+
let source;
|
|
129
|
+
try {
|
|
130
|
+
source = read(file);
|
|
131
|
+
} catch {
|
|
132
|
+
graph.set(id, []);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const edges = [];
|
|
136
|
+
for (const spec of parseRelativeImports(source)) {
|
|
137
|
+
const target = path
|
|
138
|
+
.relative(rootDir, path.resolve(path.dirname(file), spec))
|
|
139
|
+
.split(path.sep)
|
|
140
|
+
.join('/');
|
|
141
|
+
if (idSet.has(target) && target !== id) edges.push(target);
|
|
142
|
+
}
|
|
143
|
+
graph.set(id, [...new Set(edges)].sort());
|
|
144
|
+
}
|
|
145
|
+
return graph;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pure helper: rotate a cycle (array of module ids, no repeated terminal
|
|
150
|
+
* element) so it starts at its lexicographically-smallest member. The same
|
|
151
|
+
* cycle therefore always serializes identically regardless of where the
|
|
152
|
+
* DFS entered it.
|
|
153
|
+
*
|
|
154
|
+
* @param {string[]} cycle
|
|
155
|
+
* @returns {string[]}
|
|
156
|
+
*/
|
|
157
|
+
export function normalizeCycle(cycle) {
|
|
158
|
+
if (cycle.length === 0) return [];
|
|
159
|
+
let minIdx = 0;
|
|
160
|
+
for (let i = 1; i < cycle.length; i += 1) {
|
|
161
|
+
if (cycle[i] < cycle[minIdx]) minIdx = i;
|
|
162
|
+
}
|
|
163
|
+
return [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Detect directed cycles in the graph via iterative-stack DFS (white /
|
|
168
|
+
* gray / black coloring). Each back edge to a gray node yields the cycle
|
|
169
|
+
* slice currently on the DFS path. Cycles are normalized and deduplicated
|
|
170
|
+
* by their serialized form, then sorted for stable output.
|
|
171
|
+
*
|
|
172
|
+
* @param {Map<string, string[]>} graph
|
|
173
|
+
* @returns {string[][]} normalized cycles
|
|
174
|
+
*/
|
|
175
|
+
export function findCycles(graph) {
|
|
176
|
+
const WHITE = 0;
|
|
177
|
+
const GRAY = 1;
|
|
178
|
+
const BLACK = 2;
|
|
179
|
+
const color = new Map();
|
|
180
|
+
for (const node of graph.keys()) color.set(node, WHITE);
|
|
181
|
+
const seen = new Map();
|
|
182
|
+
|
|
183
|
+
const pathStack = [];
|
|
184
|
+
const onPath = new Map(); // node -> index in pathStack
|
|
185
|
+
|
|
186
|
+
const visit = (start) => {
|
|
187
|
+
// Iterative DFS frame stack: [node, edge cursor].
|
|
188
|
+
const frames = [[start, 0]];
|
|
189
|
+
color.set(start, GRAY);
|
|
190
|
+
onPath.set(start, pathStack.length);
|
|
191
|
+
pathStack.push(start);
|
|
192
|
+
while (frames.length > 0) {
|
|
193
|
+
const frame = frames[frames.length - 1];
|
|
194
|
+
const [node] = frame;
|
|
195
|
+
const edges = graph.get(node) ?? [];
|
|
196
|
+
if (frame[1] < edges.length) {
|
|
197
|
+
const next = edges[frame[1]];
|
|
198
|
+
frame[1] += 1;
|
|
199
|
+
const c = color.get(next);
|
|
200
|
+
if (c === GRAY) {
|
|
201
|
+
const cycle = normalizeCycle(pathStack.slice(onPath.get(next)));
|
|
202
|
+
seen.set(cycle.join(' -> '), cycle);
|
|
203
|
+
} else if (c === WHITE) {
|
|
204
|
+
color.set(next, GRAY);
|
|
205
|
+
onPath.set(next, pathStack.length);
|
|
206
|
+
pathStack.push(next);
|
|
207
|
+
frames.push([next, 0]);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
color.set(node, BLACK);
|
|
211
|
+
onPath.delete(node);
|
|
212
|
+
pathStack.pop();
|
|
213
|
+
frames.pop();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
for (const node of [...graph.keys()].sort()) {
|
|
219
|
+
if (color.get(node) === WHITE) visit(node);
|
|
220
|
+
}
|
|
221
|
+
return [...seen.values()].sort((a, b) =>
|
|
222
|
+
a.join(' -> ').localeCompare(b.join(' -> ')),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Pure helper: read the allowlist envelope from disk. Returns the parsed
|
|
228
|
+
* object or `null` when the file is missing or unparseable.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} baselinePath
|
|
231
|
+
* @returns {{ cycles?: string[][] } | null}
|
|
232
|
+
*/
|
|
233
|
+
export function loadBaseline(baselinePath) {
|
|
234
|
+
try {
|
|
235
|
+
if (!fs.existsSync(baselinePath)) return null;
|
|
236
|
+
const parsed = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
237
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
238
|
+
return parsed;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Pure helper: diff detected cycles against the allowlist. Both sides are
|
|
246
|
+
* normalized before comparison so rotation differences never count as
|
|
247
|
+
* drift. Identity is the ` -> `-joined normalized cycle.
|
|
248
|
+
*
|
|
249
|
+
* @param {string[][]} allowlisted
|
|
250
|
+
* @param {string[][]} detected
|
|
251
|
+
* @returns {{ added: string[][], removed: string[][] }}
|
|
252
|
+
*/
|
|
253
|
+
export function diffCycles(allowlisted, detected) {
|
|
254
|
+
const key = (c) => normalizeCycle(c).join(' -> ');
|
|
255
|
+
const baseSet = new Set((allowlisted ?? []).map(key));
|
|
256
|
+
const currentSet = new Set((detected ?? []).map(key));
|
|
257
|
+
const added = (detected ?? []).filter((c) => !baseSet.has(key(c)));
|
|
258
|
+
const removed = (allowlisted ?? []).filter((c) => !currentSet.has(key(c)));
|
|
259
|
+
const sortFn = (a, b) => key(a).localeCompare(key(b));
|
|
260
|
+
return { added: added.sort(sortFn), removed: removed.sort(sortFn) };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Pure helper: render the human-readable diff. `+` lines are new cycles
|
|
265
|
+
* (gate fail), `-` lines are fixed cycles whose allowlist entry can be
|
|
266
|
+
* removed. A one-line summary always follows.
|
|
267
|
+
*
|
|
268
|
+
* @param {{ added: string[][], removed: string[][] }} diff
|
|
269
|
+
* @returns {string}
|
|
270
|
+
*/
|
|
271
|
+
export function renderDiff(diff) {
|
|
272
|
+
const lines = [];
|
|
273
|
+
const fmt = (c) => `${c.join(' -> ')} -> ${c[0]}`;
|
|
274
|
+
for (const c of diff.added) lines.push(`+ ${fmt(c)}`);
|
|
275
|
+
for (const c of diff.removed) lines.push(`- ${fmt(c)}`);
|
|
276
|
+
if (diff.removed.length > 0) {
|
|
277
|
+
lines.push(
|
|
278
|
+
`[arch-cycles] ⚠ ${diff.removed.length} allowlisted cycle(s) no longer detected — shrink baselines/arch-cycles.json`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const tag = diff.added.length > 0 ? '(gate fail)' : '(ok)';
|
|
282
|
+
lines.push(
|
|
283
|
+
`[arch-cycles] added=${diff.added.length} removed=${diff.removed.length} ${tag}`,
|
|
284
|
+
);
|
|
285
|
+
return lines.join('\n');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Top-level CLI entry. Exported so tests can drive the full pipeline
|
|
290
|
+
* against a tmpdir fixture graph.
|
|
291
|
+
*
|
|
292
|
+
* @param {{
|
|
293
|
+
* argv?: string[],
|
|
294
|
+
* cwd?: string,
|
|
295
|
+
* stdout?: { write: (s: string) => void },
|
|
296
|
+
* stderr?: { write: (s: string) => void },
|
|
297
|
+
* }} [opts]
|
|
298
|
+
* @returns {Promise<number>} 0 = clean or removals-only; 1 = new cycle detected
|
|
299
|
+
*/
|
|
300
|
+
export async function runCli({
|
|
301
|
+
argv = process.argv.slice(2),
|
|
302
|
+
cwd = process.cwd(),
|
|
303
|
+
stdout = process.stdout,
|
|
304
|
+
stderr = process.stderr,
|
|
305
|
+
} = {}) {
|
|
306
|
+
const { baselinePath, rootPath, json } = parseArgv(argv);
|
|
307
|
+
const resolvedRoot = path.resolve(
|
|
308
|
+
cwd,
|
|
309
|
+
rootPath ?? path.join('.agents', 'scripts'),
|
|
310
|
+
);
|
|
311
|
+
const resolvedBaselinePath = path.resolve(
|
|
312
|
+
cwd,
|
|
313
|
+
baselinePath ?? path.join('baselines', 'arch-cycles.json'),
|
|
314
|
+
);
|
|
315
|
+
if (!fs.existsSync(resolvedRoot)) {
|
|
316
|
+
throw new Error(`[arch-cycles] scan root not found: ${resolvedRoot}`);
|
|
317
|
+
}
|
|
318
|
+
const baseline = loadBaseline(resolvedBaselinePath);
|
|
319
|
+
const allowlisted = Array.isArray(baseline?.cycles) ? baseline.cycles : [];
|
|
320
|
+
|
|
321
|
+
const files = collectJsFiles(resolvedRoot);
|
|
322
|
+
const graph = buildGraph(files, resolvedRoot);
|
|
323
|
+
const detected = findCycles(graph);
|
|
324
|
+
const diff = diffCycles(allowlisted, detected);
|
|
325
|
+
const exitCode = diff.added.length > 0 ? 1 : 0;
|
|
326
|
+
|
|
327
|
+
if (json) {
|
|
328
|
+
const envelope = {
|
|
329
|
+
kind: 'arch-cycles-report',
|
|
330
|
+
root: resolvedRoot,
|
|
331
|
+
baselinePath: resolvedBaselinePath,
|
|
332
|
+
allowlisted: allowlisted.map(normalizeCycle),
|
|
333
|
+
detected,
|
|
334
|
+
added: diff.added,
|
|
335
|
+
removed: diff.removed,
|
|
336
|
+
exitCode,
|
|
337
|
+
};
|
|
338
|
+
stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
339
|
+
} else {
|
|
340
|
+
if (!baseline) {
|
|
341
|
+
stderr.write(
|
|
342
|
+
`[arch-cycles] ⚠ allowlist not found at ${resolvedBaselinePath} — treating as empty\n`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
stdout.write(`\n--- arch-cycles preview ---\n`);
|
|
346
|
+
stdout.write(`${renderDiff(diff)}\n`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return exitCode;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function main() {
|
|
353
|
+
return runCli();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
runAsCli(import.meta.url, main, {
|
|
357
|
+
source: 'arch-cycles',
|
|
358
|
+
propagateExitCode: true,
|
|
359
|
+
errorPrefix: '[arch-cycles] ❌ Fatal error',
|
|
360
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Story #2662 — Internal-link and slash-command resolver for active docs.
|
|
6
6
|
//
|
|
7
7
|
// Scans every `*.md` under `docs/` and `.agents/` (excluding
|
|
8
|
-
// `docs/CHANGELOG.md`
|
|
8
|
+
// `docs/CHANGELOG.md`) and validates:
|
|
9
9
|
//
|
|
10
10
|
// 1. Every Markdown relative-path link `[text](relative/path[#anchor])`
|
|
11
11
|
// resolves to a real file on disk (anchors are not validated, only
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// 3. No active doc mentions any retired slash command. The retired-command
|
|
21
21
|
// blocklist is seeded with `agents-bootstrap-github`,
|
|
22
|
-
// `single-story-plan` (renamed to `/
|
|
22
|
+
// `single-story-plan` (renamed to `/plan`), and `mandrel`
|
|
23
23
|
// (retired in favor of the generated `.agents/docs/workflows.md`
|
|
24
24
|
// catalog) and takes precedence over the workflow-resolution check —
|
|
25
25
|
// a retired token is always a non-zero exit even if a stale workflow
|
|
@@ -125,7 +125,6 @@ export const SLASH_ALLOWLIST = new Set([
|
|
|
125
125
|
|
|
126
126
|
function isExcludedRelPath(relPath) {
|
|
127
127
|
if (relPath === 'docs/CHANGELOG.md') return true;
|
|
128
|
-
if (relPath.startsWith('docs/archive/')) return true;
|
|
129
128
|
return false;
|
|
130
129
|
}
|
|
131
130
|
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* 2. With `--skip-when-no-crap-files`: read `git diff --name-only <ref>...HEAD`
|
|
10
10
|
* (default ref `main`) and exit 0 if no changed file lives under
|
|
11
11
|
* `crap.targetDirs`.
|
|
12
|
-
* 3.
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
12
|
+
* 3. Test freshness: content digest of `crap.targetDirs` vs. the persisted
|
|
13
|
+
* capture stamp (`coverage/.capture-stamp.json`), falling back to the
|
|
14
|
+
* artifact-mtime heuristic when no stamp exists. Exit 0 when fresh.
|
|
15
|
+
* 4. Otherwise spawn `npm run test:coverage`, write a fresh capture stamp
|
|
16
|
+
* on success, and propagate the exit code.
|
|
15
17
|
*
|
|
16
18
|
* Exit codes:
|
|
17
19
|
* 0 — coverage is fresh (or capture skipped/succeeded).
|
|
@@ -24,8 +26,10 @@ import { getChangedFiles } from './lib/changed-files.js';
|
|
|
24
26
|
import { getQuality, resolveConfig } from './lib/config-resolver.js';
|
|
25
27
|
import {
|
|
26
28
|
anyChangedUnderTargets,
|
|
29
|
+
computeContentDigest,
|
|
27
30
|
isCoverageFresh,
|
|
28
31
|
runCapture,
|
|
32
|
+
writeCaptureStamp,
|
|
29
33
|
} from './lib/coverage-capture.js';
|
|
30
34
|
|
|
31
35
|
import { Logger } from './lib/Logger.js';
|
|
@@ -99,6 +103,23 @@ function main() {
|
|
|
99
103
|
Logger.error(
|
|
100
104
|
`[coverage-capture] ✖ npm run test:coverage exited ${code}. Fix failing tests or coverage-threshold breaches before re-running the CRAP gate.`,
|
|
101
105
|
);
|
|
106
|
+
return code;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Persist the content digest next to the fresh artifact so subsequent
|
|
110
|
+
// freshness checks are content-aware (mtime churn from branch switches no
|
|
111
|
+
// longer invalidates). Best-effort — a missing stamp just means the next
|
|
112
|
+
// check falls back to the mtime heuristic.
|
|
113
|
+
const digest = computeContentDigest(args.cwd, crap.targetDirs);
|
|
114
|
+
if (
|
|
115
|
+
digest &&
|
|
116
|
+
writeCaptureStamp({
|
|
117
|
+
cwd: args.cwd,
|
|
118
|
+
coveragePath: crap.coveragePath,
|
|
119
|
+
digest,
|
|
120
|
+
})
|
|
121
|
+
) {
|
|
122
|
+
Logger.info('[coverage-capture] Wrote content-digest capture stamp.');
|
|
102
123
|
}
|
|
103
124
|
return code;
|
|
104
125
|
}
|
|
@@ -134,7 +134,7 @@ function buildFrictionSignal({
|
|
|
134
134
|
timestamp: new Date().toISOString(),
|
|
135
135
|
epicId: epicId ?? null,
|
|
136
136
|
storyId: storyId ?? null,
|
|
137
|
-
//
|
|
137
|
+
// 2-tier hierarchy (Epic #3163): no Task tier, so friction signals
|
|
138
138
|
// carry no Task id. The field is retained for schema compatibility
|
|
139
139
|
// and always null.
|
|
140
140
|
taskId: null,
|
|
@@ -72,12 +72,12 @@ import { loadSpec, loadState } from './lib/spec/index.js';
|
|
|
72
72
|
* loaded state mapping. The spec-aware renderer (`buildManifestFromSpec`)
|
|
73
73
|
* looks up each Story's status via `state.mapping[slug].lastObservedAgentState`;
|
|
74
74
|
* that field is only refreshed by the structural reconciler, so during
|
|
75
|
-
* `/
|
|
75
|
+
* `/deliver` execution it stays `null` and every Story renders as ⬜
|
|
76
76
|
* pending even after the Story merges. The wave-runner replaced the
|
|
77
77
|
* dispatcher-per-wave refresh loop and the local state.json never sees
|
|
78
78
|
* the progress signal.
|
|
79
79
|
*
|
|
80
|
-
* Under the
|
|
80
|
+
* Under the 2-tier hierarchy (Epic #3163) the runtime manifest's wave
|
|
81
81
|
* records carry `stories[]` (each with the live `storyId` + `status` from
|
|
82
82
|
* `fetchEpicContext`'s GH query), not the retired `tasks[]` shape. The
|
|
83
83
|
* overlay walks `manifest.waves[].stories[]` and copies each Story's
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* any still-stuck entries by enumerating the processes holding handles
|
|
9
9
|
* inside the worktree path and terminating them.
|
|
10
10
|
*
|
|
11
|
-
* Invoked by `/
|
|
11
|
+
* Invoked by `/deliver`, `/epic-plan-spec` / `/epic-plan-decompose`
|
|
12
12
|
* (via `drainPendingCleanupAtBoot` → `worktree-sweep.js`), and
|
|
13
13
|
* `story-close` so the pending-cleanup ledger drains automatically
|
|
14
14
|
* across the sprint lifecycle. Operators can also run it standalone:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* node:coverage ignore file */
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* epic-audit-prepare.js — Phase 4 prepare CLI for `/
|
|
5
|
+
* epic-audit-prepare.js — Phase 4 prepare CLI for `/deliver`.
|
|
6
6
|
*
|
|
7
7
|
* Thin glue around the audit-suite `selectAudits` SDK. Reads the Epic
|
|
8
8
|
* ticket, runs the change-set selector against the Epic branch diff
|
|
@@ -193,7 +193,7 @@ function resolveTaskSizing(config) {
|
|
|
193
193
|
* Best-effort and total, mirroring `resolveRiskRoutedLenses`: a
|
|
194
194
|
* missing/unparseable checkpoint, an absent `planningRisk` field, or a
|
|
195
195
|
* provider read failure all degrade to `standard` — the neutral default that
|
|
196
|
-
* preserves today's behavior — so an Epic that skipped `/
|
|
196
|
+
* preserves today's behavior — so an Epic that skipped `/plan` (no
|
|
197
197
|
* checkpoint) still gets a passing `standard` pass with no new failure mode.
|
|
198
198
|
* The changed-file count can only escalate a low-risk Epic to `deep` (a wide
|
|
199
199
|
* diff) and never downgrades a high-risk one; an unknown/absent count is the
|
|
@@ -295,7 +295,7 @@ export async function runEpicAuditPrepare(values, deps = {}) {
|
|
|
295
295
|
const runner = deps.selectAudits ?? selectAudits;
|
|
296
296
|
|
|
297
297
|
// Pin the change set to the requested Epic's own branch rather than the
|
|
298
|
-
// shared checkout's HEAD (Story #3362). Under two concurrent /
|
|
298
|
+
// shared checkout's HEAD (Story #3362). Under two concurrent /deliver
|
|
299
299
|
// runs sharing one working copy, a HEAD-relative diff silently reports the
|
|
300
300
|
// *other* Epic's change set; `refs/heads/epic/<id>` is unambiguous.
|
|
301
301
|
const epicBranch = `epic/${epicId}`;
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* epic-deliver-note-intervention.js — record a manual-intervention event
|
|
6
|
-
* against the active `/
|
|
6
|
+
* against the active `/deliver` run-state checkpoint.
|
|
7
7
|
*
|
|
8
|
-
* The host LLM driving `/
|
|
8
|
+
* The host LLM driving `/deliver` invokes this CLI whenever it does
|
|
9
9
|
* something out-of-band that disqualifies the Epic from auto-merge:
|
|
10
10
|
*
|
|
11
11
|
* - `AskUserQuestion` to the operator mid-run
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* epic-deliver-preflight.js — Story #2899 (Epic #2880, F13).
|
|
6
6
|
*
|
|
7
|
-
* Estimates the cost of an upcoming `/
|
|
7
|
+
* Estimates the cost of an upcoming `/deliver` run *before* Story
|
|
8
8
|
* fan-out and surfaces the result to the operator on two channels:
|
|
9
9
|
*
|
|
10
10
|
* 1. A JSON envelope on stdout (always) with the keys
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* The CLI is intentionally side-effect-light when `--dry-run` is set: no
|
|
21
21
|
* comment write, no lifecycle emit. The slash-command workflow
|
|
22
|
-
* (`/
|
|
22
|
+
* (`/deliver` Phase 1) calls the CLI without `--dry-run` so the
|
|
23
23
|
* comment is upserted; if any threshold is breached, the workflow flips
|
|
24
24
|
* the Epic to `agent::blocked` and surfaces the envelope in chat for
|
|
25
25
|
* operator review. The CLI itself does NOT flip labels — the workflow
|
|
@@ -70,7 +70,7 @@ const HELP = `Usage: node .agents/scripts/epic-deliver-preflight.js \\
|
|
|
70
70
|
[--per-story-claude-tokens <n>]
|
|
71
71
|
|
|
72
72
|
Estimates Story count, install cost, wave count, GitHub API request volume,
|
|
73
|
-
and Claude Max quota burn for an Epic *before* /
|
|
73
|
+
and Claude Max quota burn for an Epic *before* /deliver fan-out.
|
|
74
74
|
|
|
75
75
|
Flags:
|
|
76
76
|
--dry-run Compute the estimate and print the JSON
|
|
@@ -103,7 +103,7 @@ const DEFAULTS = Object.freeze({
|
|
|
103
103
|
perStoryApiRequests: 25,
|
|
104
104
|
perStoryClaudeTokens: 200_000,
|
|
105
105
|
// Base GH API budget: snapshot getTicket + getSubTickets + plan/manifest
|
|
106
|
-
// reads/writes that happen once per /
|
|
106
|
+
// reads/writes that happen once per /deliver run regardless of
|
|
107
107
|
// Story count. Empirical observation from a 5-Story Epic shows ~30
|
|
108
108
|
// requests for the non-per-Story floor.
|
|
109
109
|
baseApiRequests: 30,
|
|
@@ -240,7 +240,7 @@ export function renderPreflightBody({
|
|
|
240
240
|
);
|
|
241
241
|
} else {
|
|
242
242
|
lines.push(
|
|
243
|
-
'⛔ **Threshold breaches** — `/
|
|
243
|
+
'⛔ **Threshold breaches** — `/deliver` will flip the Epic to `agent::blocked` for operator review:',
|
|
244
244
|
);
|
|
245
245
|
for (const b of breaches) {
|
|
246
246
|
lines.push(`- \`${b.key}\` = ${b.observed} (max ${b.max})`);
|
|
@@ -284,7 +284,7 @@ export async function runPreflight({
|
|
|
284
284
|
const provider = injectedProvider ?? createProvider(config);
|
|
285
285
|
const thresholds = getPreflight(config);
|
|
286
286
|
|
|
287
|
-
// Compose the same two phases /
|
|
287
|
+
// Compose the same two phases /deliver Phase 1 runs so the
|
|
288
288
|
// preflight numbers match the actual dispatch plan.
|
|
289
289
|
const ctx = { epicId, provider };
|
|
290
290
|
let state = {};
|
|
@@ -297,9 +297,11 @@ export async function runPreflight({
|
|
|
297
297
|
// Persist the snapshot/DAG envelope so `epic-deliver-prepare.js` can
|
|
298
298
|
// reuse it instead of re-walking the hierarchy. The cache key is a
|
|
299
299
|
// deterministic fingerprint of the Epic ticket returned by the same
|
|
300
|
-
// `getTicket(epicId)` call that drove `runSnapshotPhase
|
|
301
|
-
//
|
|
302
|
-
|
|
300
|
+
// `getTicket(epicId)` call that drove `runSnapshotPhase` **plus** the
|
|
301
|
+
// Story snapshots that drove the wave DAG (Story #4019), so any drift —
|
|
302
|
+
// Epic label/body/updatedAt or a Story-dependency edit — forces a cache
|
|
303
|
+
// miss in prepare.
|
|
304
|
+
const baseSha = computeBaseSha(state.epic, state.stories);
|
|
303
305
|
let cacheWritten = false;
|
|
304
306
|
if (!dryRun) {
|
|
305
307
|
await writePreflightCache({
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* node:coverage ignore file */
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* epic-deliver-prepare.js — Step 0/1 of the operator-driven `/
|
|
5
|
+
* epic-deliver-prepare.js — Step 0/1 of the operator-driven `/deliver`.
|
|
6
6
|
*
|
|
7
7
|
* Composes the existing engine phases that the in-process epic-runner used to
|
|
8
8
|
* call sequentially, but does NOT dispatch any waves. The CLI is the single
|
|
@@ -226,16 +226,24 @@ export async function runEpicDeliverPrepare({
|
|
|
226
226
|
|
|
227
227
|
// Story #3027: try the preflight cache first so we don't re-walk Epic
|
|
228
228
|
// → Feature → Story when `epic-deliver-preflight.js` already did. The
|
|
229
|
-
// cache key is a deterministic fingerprint of the Epic ticket
|
|
230
|
-
//
|
|
231
|
-
//
|
|
229
|
+
// cache key is a deterministic fingerprint of the Epic ticket plus the
|
|
230
|
+
// cached Story snapshots (Story #4019): the Epic re-fetch plus one
|
|
231
|
+
// getTicket per cached Story is still far cheaper than the full
|
|
232
|
+
// hierarchy BFS, and a Story-dependency edit now invalidates the cache.
|
|
233
|
+
// Cache miss or baseSha mismatch → fall back to a fresh pass.
|
|
232
234
|
const ctx = { epicId, provider };
|
|
233
235
|
let state = {};
|
|
234
236
|
let cacheStatus = 'miss';
|
|
235
237
|
const cached = await readPreflightCache({ epicId, cwd });
|
|
236
238
|
if (cached) {
|
|
237
239
|
const freshEpic = await provider.getTicket(epicId);
|
|
238
|
-
const
|
|
240
|
+
const cachedStoryIds = cached.stories
|
|
241
|
+
.map((s) => Number(s?.id ?? s?.number))
|
|
242
|
+
.filter((id) => Number.isInteger(id) && id > 0);
|
|
243
|
+
const freshStories = await Promise.all(
|
|
244
|
+
cachedStoryIds.map((id) => provider.getTicket(id)),
|
|
245
|
+
);
|
|
246
|
+
const freshBaseSha = computeBaseSha(freshEpic, freshStories);
|
|
239
247
|
if (freshBaseSha === cached.baseSha) {
|
|
240
248
|
state = {
|
|
241
249
|
epic: cached.epic,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* advance the `epic-run-state` checkpoint, and re-render the unified
|
|
7
7
|
* `epic-run-progress` rollup on the Epic.
|
|
8
8
|
*
|
|
9
|
-
* The slash-command (`/
|
|
9
|
+
* The slash-command (`/deliver`) calls this CLI once per wave, after
|
|
10
10
|
* its host-level Agent-tool fan-out drains. It is the only writer of the
|
|
11
11
|
* `epic-run-progress` structured comment for the wave-completion path —
|
|
12
12
|
* there is no separate `/wave-execute` skill, no `wave-run-progress`
|
|
@@ -42,7 +42,7 @@ import { getRunners } from './lib/config/runners.js';
|
|
|
42
42
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
43
43
|
import { Logger } from './lib/Logger.js';
|
|
44
44
|
import * as epicRunStateStore from './lib/orchestration/epic-run-state-store.js';
|
|
45
|
-
import { upsertEpicRunProgress } from './lib/orchestration/epic-runner/progress-reporter.js';
|
|
45
|
+
import { upsertEpicRunProgress } from './lib/orchestration/epic-runner/progress-reporter/composition.js';
|
|
46
46
|
import {
|
|
47
47
|
emitStoryDispatchEnd,
|
|
48
48
|
storyStatusToDispatchOutcome,
|
|
@@ -95,7 +95,7 @@ const HELP = `Usage: node .agents/scripts/epic-execute-record-wave.js \\
|
|
|
95
95
|
|
|
96
96
|
Records the wave's per-Story outcomes, advances the epic-run-state
|
|
97
97
|
checkpoint, and upserts the unified epic-run-progress rollup on the Epic.
|
|
98
|
-
Prints the next action for the /
|
|
98
|
+
Prints the next action for the /deliver slash command.
|
|
99
99
|
`;
|
|
100
100
|
|
|
101
101
|
/**
|
|
@@ -237,7 +237,7 @@ export async function runEpicExecuteRecordWave({
|
|
|
237
237
|
// Closes the start/end pairing the wave-tick reconciler and the
|
|
238
238
|
// `--check-idle` watchdog use to derive in-flight Stories. Before this
|
|
239
239
|
// the only producer was `wave-session.js`, which the host-LLM driven
|
|
240
|
-
// /
|
|
240
|
+
// /deliver path never imports — so every dispatched Story stayed
|
|
241
241
|
// "in-flight" forever and completed Stories tripped the watchdog.
|
|
242
242
|
// Best-effort: a failed append must not block the wave loop.
|
|
243
243
|
emitWaveDispatchEnds({ epicId, verified, config });
|
|
@@ -257,7 +257,7 @@ export async function runEpicExecuteRecordWave({
|
|
|
257
257
|
|
|
258
258
|
// 7. Fire the curated webhook events for this wave boundary. Mirrors the
|
|
259
259
|
// wave-loop emits in `lib/orchestration/epic-runner/phases/iterate-waves.js`
|
|
260
|
-
// for the host-LLM driven /
|
|
260
|
+
// for the host-LLM driven /deliver path (which does not pass
|
|
261
261
|
// through `runEpic`). Each helper is fire-and-forget — webhook
|
|
262
262
|
// misconfig or a transient Slack outage must not block the wave loop.
|
|
263
263
|
await emitWaveBoundaryNotifications({
|