@nathapp/nax 0.21.0 → 0.22.1
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/.mcp.json +8 -0
- package/docs/ROADMAP.md +20 -5
- package/docs/adr/ADR-005-implementation-plan.md +655 -0
- package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
- package/package.json +1 -1
- package/src/agents/claude.ts +44 -9
- package/src/config/types.ts +11 -0
- package/src/execution/dry-run.ts +81 -0
- package/src/execution/escalation/tier-outcome.ts +29 -44
- package/src/execution/executor-types.ts +65 -0
- package/src/execution/index.ts +0 -17
- package/src/execution/iteration-runner.ts +132 -0
- package/src/execution/lifecycle/index.ts +0 -1
- package/src/execution/lifecycle/run-regression.ts +5 -5
- package/src/execution/pipeline-result-handler.ts +51 -254
- package/src/execution/sequential-executor.ts +72 -316
- package/src/execution/story-selector.ts +75 -0
- package/src/pipeline/event-bus.ts +276 -0
- package/src/pipeline/runner.ts +51 -77
- package/src/pipeline/stages/autofix.ts +133 -0
- package/src/pipeline/stages/completion.ts +22 -30
- package/src/pipeline/stages/index.ts +30 -13
- package/src/pipeline/stages/rectify.ts +93 -0
- package/src/pipeline/stages/regression.ts +88 -0
- package/src/pipeline/stages/review.ts +19 -153
- package/src/pipeline/stages/verify.ts +18 -2
- package/src/pipeline/subscribers/hooks.ts +133 -0
- package/src/pipeline/subscribers/interaction.ts +68 -0
- package/src/pipeline/subscribers/reporters.ts +174 -0
- package/src/pipeline/types.ts +10 -1
- package/src/review/orchestrator.ts +105 -0
- package/src/tdd/prompts.ts +1 -1
- package/src/verification/index.ts +1 -1
- package/src/verification/orchestrator-types.ts +145 -0
- package/src/verification/orchestrator.ts +76 -0
- package/src/{execution/post-verify-rectification.ts → verification/rectification-loop.ts} +13 -20
- package/src/verification/{gate.ts → runners.ts} +17 -105
- package/src/verification/strategies/acceptance.ts +133 -0
- package/src/verification/strategies/regression.ts +90 -0
- package/src/verification/strategies/scoped.ts +123 -0
- package/test/COVERAGE-GAPS.md +333 -0
- package/test/{acceptance → e2e}/cm-003-default-view.test.ts +1 -0
- package/test/{integration/e2e.test.ts → e2e/plan-analyze-run.test.ts} +1 -0
- package/test/integration/{agent-validation.test.ts → cli/agent-validation.test.ts} +3 -3
- package/test/integration/{cli-config-default-edge-cases.test.ts → cli/cli-config-default-edge-cases.test.ts} +6 -5
- package/test/integration/{cli-config-default-view.test.ts → cli/cli-config-default-view.test.ts} +8 -7
- package/test/integration/{cli-config-diff.test.ts → cli/cli-config-diff.test.ts} +3 -2
- package/test/integration/{cli-config.test.ts → cli/cli-config.test.ts} +3 -2
- package/test/integration/{cli-diagnose.test.ts → cli/cli-diagnose.test.ts} +5 -4
- package/test/integration/{cli-logs.test.ts → cli/cli-logs.test.ts} +12 -3
- package/test/integration/{cli-plugins.test.ts → cli/cli-plugins.test.ts} +4 -3
- package/test/integration/{cli-precheck.test.ts → cli/cli-precheck.test.ts} +4 -3
- package/test/integration/{cli-run-headless.test.ts → cli/cli-run-headless.test.ts} +3 -2
- package/test/integration/{cli.test.ts → cli/cli.test.ts} +2 -1
- package/test/integration/{precheck-integration.test.ts → cli/precheck-integration.test.ts} +10 -9
- package/test/integration/{precheck-orchestrator.test.ts → cli/precheck-orchestrator.test.ts} +4 -3
- package/test/integration/{precheck.test.ts → cli/precheck.test.ts} +5 -4
- package/test/integration/{config-loader.test.ts → config/config-loader.test.ts} +2 -1
- package/test/integration/{config.test.ts → config/config.test.ts} +2 -2
- package/test/integration/config/merger.test.ts +1 -0
- package/test/integration/config/paths.test.ts +1 -0
- package/test/integration/{security-loader.test.ts → config/security-loader.test.ts} +2 -2
- package/test/integration/{context-integration.test.ts → context/context-integration.test.ts} +7 -6
- package/test/integration/{path-security.test.ts → context/context-path-security.test.ts} +2 -2
- package/test/integration/{context-provider-injection.test.ts → context/context-provider-injection.test.ts} +7 -6
- package/test/integration/{context-verification-integration.test.ts → context/context-verification-integration.test.ts} +5 -4
- package/test/integration/{s5-greenfield-fallback.test.ts → context/s5-greenfield-fallback.test.ts} +4 -3
- package/test/integration/{isolation.test.ts → execution/execution-isolation.test.ts} +1 -1
- package/test/integration/{execution.test.ts → execution/execution.test.ts} +8 -8
- package/test/integration/{parallel.test.ts → execution/parallel.test.ts} +2 -1
- package/test/integration/{prd-pause.test.ts → execution/prd-pause.test.ts} +2 -2
- package/test/integration/{prd-resolvers.test.ts → execution/prd-resolvers.test.ts} +3 -2
- package/test/integration/{progress.test.ts → execution/progress.test.ts} +1 -1
- package/test/integration/execution/runner-batching.test.ts +682 -0
- package/test/integration/{runner-config-plugins.test.ts → execution/runner-config-plugins.test.ts} +3 -2
- package/test/integration/execution/runner-escalation.test.ts +561 -0
- package/test/integration/{runner-fixes.test.ts → execution/runner-fixes.test.ts} +4 -3
- package/test/integration/{runner-plugin-integration.test.ts → execution/runner-plugin-integration.test.ts} +6 -5
- package/test/integration/execution/runner-queue-and-attempts.test.ts +476 -0
- package/test/integration/{status-file-integration.test.ts → execution/status-file-integration.test.ts} +9 -8
- package/test/integration/{status-file.test.ts → execution/status-file.test.ts} +3 -2
- package/test/integration/{status-writer.test.ts → execution/status-writer.test.ts} +5 -4
- package/test/integration/{story-id-in-events.test.ts → execution/story-id-in-events.test.ts} +9 -8
- package/test/integration/{interaction-chain-pipeline.test.ts → interaction/interaction-chain-pipeline.test.ts} +26 -14
- package/test/integration/{hooks.test.ts → pipeline/hooks.test.ts} +4 -2
- package/test/integration/{pipeline-acceptance.test.ts → pipeline/pipeline-acceptance.test.ts} +7 -6
- package/test/integration/{pipeline-events.test.ts → pipeline/pipeline-events.test.ts} +7 -6
- package/test/integration/{pipeline.test.ts → pipeline/pipeline.test.ts} +9 -7
- package/test/integration/{reporter-lifecycle.test.ts → pipeline/reporter-lifecycle.test.ts} +9 -7
- package/test/integration/{verify-stage.test.ts → pipeline/verify-stage.test.ts} +7 -5
- package/test/integration/{analyze-integration.test.ts → plan/analyze-integration.test.ts} +3 -2
- package/test/integration/{analyze-scanner.test.ts → plan/analyze-scanner.test.ts} +8 -7
- package/test/integration/{logger.test.ts → plan/logger.test.ts} +1 -1
- package/test/integration/{plan.test.ts → plan/plan.test.ts} +3 -3
- package/test/integration/plugins/config-integration.test.ts +1 -0
- package/test/integration/plugins/config-resolution.test.ts +1 -0
- package/test/integration/plugins/loader.test.ts +1 -0
- package/test/integration/plugins/{registry.test.ts → plugins-registry.test.ts} +1 -0
- package/test/integration/plugins/validator.test.ts +1 -0
- package/test/integration/{review-config-commands.test.ts → review/review-config-commands.test.ts} +4 -3
- package/test/integration/{review-config-schema.test.ts → review/review-config-schema.test.ts} +3 -2
- package/test/integration/{review-plugin-integration.test.ts → review/review-plugin-integration.test.ts} +5 -4
- package/test/integration/{review.test.ts → review/review.test.ts} +3 -2
- package/test/integration/routing/plugin-routing-advanced.test.ts +461 -0
- package/test/integration/{plugin-routing.test.ts → routing/plugin-routing-core.test.ts} +9 -403
- package/test/integration/{routing-stage-bug-021.test.ts → routing/routing-stage-bug-021.test.ts} +8 -7
- package/test/integration/{routing-stage-greenfield.test.ts → routing/routing-stage-greenfield.test.ts} +7 -6
- package/test/integration/{tdd-cleanup.test.ts → tdd/tdd-cleanup.test.ts} +1 -1
- package/test/integration/tdd/tdd-orchestrator-core.test.ts +565 -0
- package/test/integration/tdd/tdd-orchestrator-failureCategory.test.ts +355 -0
- package/test/integration/tdd/tdd-orchestrator-fallback.test.ts +311 -0
- package/test/integration/tdd/tdd-orchestrator-lite.test.ts +289 -0
- package/test/integration/tdd/tdd-orchestrator-prompts.test.ts +260 -0
- package/test/integration/tdd/tdd-orchestrator-verdict.test.ts +536 -0
- package/test/integration/tmp/headless-test/test.jsonl +30 -0
- package/test/integration/{test-scanner.test.ts → verification/test-scanner.test.ts} +1 -1
- package/test/integration/{verification-asset-check.test.ts → verification/verification-asset-check.test.ts} +3 -2
- package/test/unit/acceptance.test.ts +1 -0
- package/test/unit/agent-stderr-capture.test.ts +1 -0
- package/test/unit/agents/claude.test.ts +1 -0
- package/test/unit/analyze-classifier.test.ts +1 -0
- package/test/unit/auto-detect.test.ts +1 -0
- package/test/unit/cli-status.test.ts +1 -0
- package/test/unit/commands/common.test.ts +1 -0
- package/test/unit/commands/logs.test.ts +1 -0
- package/test/unit/commands/unlock.test.ts +1 -0
- package/test/unit/config/defaults.test.ts +1 -0
- package/test/unit/config/regression-gate-schema.test.ts +1 -0
- package/test/unit/config/smart-runner-flag.test.ts +1 -0
- package/test/unit/constitution-generators.test.ts +1 -0
- package/test/unit/constitution.test.ts +1 -0
- package/test/unit/context/context-autodetect.test.ts +297 -0
- package/test/unit/context/context-build.test.ts +575 -0
- package/test/unit/context/context-coverage.test.ts +236 -0
- package/test/unit/context/context-error.test.ts +93 -0
- package/test/unit/context/context-estimate-tokens.test.ts +201 -0
- package/test/unit/context/context-format.test.ts +302 -0
- package/test/unit/context/context-isolation.test.ts +267 -0
- package/test/unit/context/context-sort.test.ts +93 -0
- package/test/unit/context/context-story.test.ts +108 -0
- package/test/{context → unit/context}/prior-failures.test.ts +5 -4
- package/test/unit/context.test.ts +1 -0
- package/test/unit/crash-recovery.test.ts +1 -0
- package/test/unit/escalation.test.ts +1 -0
- package/test/unit/execution/lifecycle/run-completion.test.ts +1 -0
- package/test/unit/execution/lifecycle/run-regression.test.ts +2 -0
- package/test/{execution → unit/execution}/pid-registry.test.ts +2 -1
- package/test/{execution → unit/execution}/structured-failure.test.ts +3 -2
- package/test/unit/execution-logging-stderr.test.ts +1 -0
- package/test/unit/execution-stage.test.ts +1 -0
- package/test/unit/fix-generator.test.ts +1 -0
- package/test/unit/greenfield.test.ts +1 -0
- package/test/unit/interaction/human-review-trigger.test.ts +1 -0
- package/test/unit/interaction-network-failures.test.ts +1 -0
- package/test/unit/interaction-plugins.test.ts +1 -0
- package/test/unit/logging/formatter.test.ts +1 -0
- package/test/unit/merge.test.ts +1 -0
- package/test/unit/pipeline/event-bus.test.ts +105 -0
- package/test/unit/pipeline/routing-partial-override.test.ts +1 -0
- package/test/unit/pipeline/runner-retry.test.ts +89 -0
- package/test/unit/pipeline/stages/autofix.test.ts +97 -0
- package/test/unit/pipeline/stages/rectify.test.ts +101 -0
- package/test/unit/pipeline/stages/regression-stage.test.ts +69 -0
- package/test/unit/pipeline/stages/verify.test.ts +1 -0
- package/test/unit/pipeline/subscribers/hooks.test.ts +45 -0
- package/test/unit/pipeline/subscribers/interaction.test.ts +31 -0
- package/test/unit/pipeline/subscribers/reporters.test.ts +90 -0
- package/test/unit/pipeline/verify-smart-runner.test.ts +1 -0
- package/test/unit/prd-auto-default.test.ts +1 -0
- package/test/unit/prd-failure-category.test.ts +1 -0
- package/test/unit/prd-get-next-story.test.ts +1 -0
- package/test/unit/precheck-checks.test.ts +1 -0
- package/test/unit/precheck-story-size-gate.test.ts +1 -0
- package/test/unit/precheck-types.test.ts +1 -0
- package/test/unit/prompts.test.ts +1 -0
- package/test/unit/rectification.test.ts +2 -1
- package/test/unit/registry.test.ts +1 -0
- package/test/unit/routing/routing-stability.test.ts +1 -0
- package/test/unit/routing/strategies/llm.test.ts +1 -0
- package/test/unit/routing-advanced.test.ts +313 -0
- package/test/unit/routing-core.test.ts +341 -0
- package/test/unit/routing-strategies.test.ts +442 -0
- package/test/unit/storyid-events.test.ts +1 -0
- package/test/{ui → unit/ui}/tui-controls.test.ts +8 -7
- package/test/{ui → unit/ui}/tui-cost-and-pty.test.ts +4 -3
- package/test/{ui → unit/ui}/tui-layout.test.ts +5 -4
- package/test/{ui → unit/ui}/tui-stories.test.ts +5 -4
- package/test/unit/{isolation.test.ts → unit-isolation.test.ts} +1 -0
- package/test/unit/{helpers.test.ts → utils-helpers.test.ts} +1 -0
- package/test/unit/verdict.test.ts +1 -0
- package/test/unit/verification/orchestrator-types.test.ts +54 -0
- package/test/unit/verification/orchestrator.test.ts +66 -0
- package/test/unit/verification/smart-runner-config.test.ts +1 -0
- package/test/unit/verification/smart-runner-discovery.test.ts +8 -7
- package/test/unit/verification/strategies/acceptance.test.ts +33 -0
- package/test/unit/verification/strategies/regression.test.ts +87 -0
- package/test/unit/verification/strategies/scoped.test.ts +100 -0
- package/test/unit/worktree-manager.test.ts +1 -0
- package/src/execution/lifecycle/story-hooks.ts +0 -38
- package/src/execution/post-verify.ts +0 -193
- package/src/execution/rectification.ts +0 -13
- package/src/execution/verification.ts +0 -72
- package/test/integration/rectification-flow.test.ts +0 -512
- package/test/integration/runner.test.ts +0 -1679
- package/test/integration/tdd-orchestrator.test.ts +0 -1762
- package/test/unit/execution/post-verify-regression.test.ts +0 -362
- package/test/unit/execution/post-verify.test.ts +0 -236
- package/test/unit/routing.test.ts +0 -1039
- /package/test/{integration → helpers}/helpers.test.ts +0 -0
- /package/test/integration/worktree/{merge.test.ts → worktree-merge.test.ts} +0 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { PipelineStage } from "../types";
|
|
9
9
|
import { acceptanceStage } from "./acceptance";
|
|
10
|
+
import { autofixStage } from "./autofix";
|
|
10
11
|
import { completionStage } from "./completion";
|
|
11
12
|
import { constitutionStage } from "./constitution";
|
|
12
13
|
import { contextStage } from "./context";
|
|
@@ -14,6 +15,8 @@ import { executionStage } from "./execution";
|
|
|
14
15
|
import { optimizerStage } from "./optimizer";
|
|
15
16
|
import { promptStage } from "./prompt";
|
|
16
17
|
import { queueCheckStage } from "./queue-check";
|
|
18
|
+
import { rectifyStage } from "./rectify";
|
|
19
|
+
import { regressionStage } from "./regression";
|
|
17
20
|
import { reviewStage } from "./review";
|
|
18
21
|
import { routingStage } from "./routing";
|
|
19
22
|
import { verifyStage } from "./verify";
|
|
@@ -21,18 +24,21 @@ import { verifyStage } from "./verify";
|
|
|
21
24
|
/**
|
|
22
25
|
* Default pipeline stages in execution order.
|
|
23
26
|
*
|
|
24
|
-
*
|
|
25
|
-
* 1.
|
|
26
|
-
* 2.
|
|
27
|
-
* 3.
|
|
28
|
-
* 4.
|
|
29
|
-
* 5.
|
|
30
|
-
* 6.
|
|
31
|
-
* 7.
|
|
32
|
-
* 8.
|
|
33
|
-
* 9.
|
|
34
|
-
* 10.
|
|
35
|
-
* 11.
|
|
27
|
+
* New stage order (ADR-005 Phase 2):
|
|
28
|
+
* 1. Check for queue commands (PAUSE/ABORT/SKIP)
|
|
29
|
+
* 2. Route (classify complexity → model tier)
|
|
30
|
+
* 3. Load constitution (project coding standards)
|
|
31
|
+
* 4. Build context (gather relevant code/docs)
|
|
32
|
+
* 5. Assemble prompt (story + context + constitution)
|
|
33
|
+
* 6. Optimize prompt (reduce token usage)
|
|
34
|
+
* 7. Execute agent session (TDD or test-after)
|
|
35
|
+
* 8. Verify output (tests pass — scoped via smart-runner)
|
|
36
|
+
* 9. Rectify (fix test failures before escalating)
|
|
37
|
+
* 10. Review (quality checks: lint, typecheck, format)
|
|
38
|
+
* 11. Autofix (auto-fix lint/format before escalating)
|
|
39
|
+
* 12. Regression (full-suite gate, inline mode only)
|
|
40
|
+
* 13. Mark complete (save PRD, fire hooks, log progress)
|
|
41
|
+
* 14. Acceptance (run AC tests when all stories complete)
|
|
36
42
|
*/
|
|
37
43
|
export const defaultPipeline: PipelineStage[] = [
|
|
38
44
|
queueCheckStage,
|
|
@@ -43,11 +49,19 @@ export const defaultPipeline: PipelineStage[] = [
|
|
|
43
49
|
optimizerStage,
|
|
44
50
|
executionStage,
|
|
45
51
|
verifyStage,
|
|
52
|
+
rectifyStage,
|
|
46
53
|
reviewStage,
|
|
54
|
+
autofixStage,
|
|
55
|
+
regressionStage,
|
|
47
56
|
completionStage,
|
|
48
|
-
acceptanceStage,
|
|
49
57
|
];
|
|
50
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Post-run pipeline stages — run once after all per-story iterations complete.
|
|
61
|
+
* Handles deferred regression and acceptance tests.
|
|
62
|
+
*/
|
|
63
|
+
export const postRunPipeline: PipelineStage[] = [acceptanceStage];
|
|
64
|
+
|
|
51
65
|
// Re-export individual stages for custom pipeline construction
|
|
52
66
|
export { queueCheckStage } from "./queue-check";
|
|
53
67
|
export { routingStage } from "./routing";
|
|
@@ -57,6 +71,9 @@ export { promptStage } from "./prompt";
|
|
|
57
71
|
export { optimizerStage } from "./optimizer";
|
|
58
72
|
export { executionStage } from "./execution";
|
|
59
73
|
export { verifyStage } from "./verify";
|
|
74
|
+
export { rectifyStage } from "./rectify";
|
|
60
75
|
export { reviewStage } from "./review";
|
|
76
|
+
export { autofixStage } from "./autofix";
|
|
77
|
+
export { regressionStage } from "./regression";
|
|
61
78
|
export { completionStage } from "./completion";
|
|
62
79
|
export { acceptanceStage } from "./acceptance";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
/**
|
|
3
|
+
* Rectify Stage (ADR-005, Phase 2)
|
|
4
|
+
*
|
|
5
|
+
* Runs after a failed verify stage. Attempts to fix test failures by
|
|
6
|
+
* running a rectification loop (agent + re-verify cycle).
|
|
7
|
+
*
|
|
8
|
+
* Enabled only when ctx.verifyResult?.success === false.
|
|
9
|
+
*
|
|
10
|
+
* Returns:
|
|
11
|
+
* - `retry` fromStage:"verify" — rectification fixed the failures
|
|
12
|
+
* - `escalate` — max retries exhausted
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getLogger } from "../../logger";
|
|
16
|
+
import { pipelineEventBus } from "../event-bus";
|
|
17
|
+
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
18
|
+
|
|
19
|
+
export const rectifyStage: PipelineStage = {
|
|
20
|
+
name: "rectify",
|
|
21
|
+
|
|
22
|
+
enabled(ctx: PipelineContext): boolean {
|
|
23
|
+
// Only run when verify failed
|
|
24
|
+
if (!ctx.verifyResult) return false;
|
|
25
|
+
if (ctx.verifyResult.success) return false;
|
|
26
|
+
// Only run when rectification is enabled in config
|
|
27
|
+
return ctx.config.execution.rectification?.enabled ?? false;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
31
|
+
const logger = getLogger();
|
|
32
|
+
const { verifyResult } = ctx;
|
|
33
|
+
|
|
34
|
+
if (!verifyResult || verifyResult.success) {
|
|
35
|
+
return { action: "continue" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const testOutput = verifyResult.rawOutput ?? "";
|
|
39
|
+
const maxRetries = ctx.config.execution.rectification?.maxRetries ?? 3;
|
|
40
|
+
|
|
41
|
+
logger.info("rectify", "Starting rectification loop", {
|
|
42
|
+
storyId: ctx.story.id,
|
|
43
|
+
failCount: verifyResult.failCount,
|
|
44
|
+
maxRetries,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
ctx.rectifyAttempt = (ctx.rectifyAttempt ?? 0) + 1;
|
|
48
|
+
const rectifyAttempt = ctx.rectifyAttempt;
|
|
49
|
+
|
|
50
|
+
pipelineEventBus.emit({
|
|
51
|
+
type: "rectify:started",
|
|
52
|
+
storyId: ctx.story.id,
|
|
53
|
+
attempt: rectifyAttempt,
|
|
54
|
+
testOutput,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const testCommand = ctx.config.review?.commands?.test ?? ctx.config.quality.commands.test ?? "bun test";
|
|
58
|
+
const fixed = await _rectifyDeps.runRectificationLoop({
|
|
59
|
+
config: ctx.config,
|
|
60
|
+
workdir: ctx.workdir,
|
|
61
|
+
story: ctx.story,
|
|
62
|
+
testCommand,
|
|
63
|
+
timeoutSeconds: ctx.config.execution.verificationTimeoutSeconds,
|
|
64
|
+
testOutput,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
pipelineEventBus.emit({
|
|
68
|
+
type: "rectify:completed",
|
|
69
|
+
storyId: ctx.story.id,
|
|
70
|
+
attempt: rectifyAttempt,
|
|
71
|
+
fixed,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (fixed) {
|
|
75
|
+
logger.info("rectify", "Rectification succeeded — retrying verify", { storyId: ctx.story.id });
|
|
76
|
+
// Clear verifyResult so verify stage re-runs fresh
|
|
77
|
+
ctx.verifyResult = undefined;
|
|
78
|
+
return { action: "retry", fromStage: "verify" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logger.warn("rectify", "Rectification exhausted — escalating", { storyId: ctx.story.id });
|
|
82
|
+
return {
|
|
83
|
+
action: "escalate",
|
|
84
|
+
reason: `Rectification exhausted after ${maxRetries} attempts (${verifyResult.failCount} test failures)`,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Injectable deps for testing.
|
|
91
|
+
*/
|
|
92
|
+
import { runRectificationLoop } from "../../verification/rectification-loop";
|
|
93
|
+
export const _rectifyDeps = { runRectificationLoop };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
/**
|
|
3
|
+
* Regression Stage (ADR-005, Phase 2)
|
|
4
|
+
*
|
|
5
|
+
* Runs a full-suite regression gate as part of the per-story pipeline,
|
|
6
|
+
* when regressionGate.mode === "per-story" AND verify passed.
|
|
7
|
+
*
|
|
8
|
+
* This replaces the per-story regression gate previously handled in
|
|
9
|
+
* src/execution/post-verify.ts (which will be deleted in Phase 4).
|
|
10
|
+
*
|
|
11
|
+
* Returns:
|
|
12
|
+
* - `continue` — full suite passed
|
|
13
|
+
* - `escalate` — full suite failed after optional rectification
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getLogger } from "../../logger";
|
|
17
|
+
import { verificationOrchestrator } from "../../verification/orchestrator";
|
|
18
|
+
import type { VerifyContext } from "../../verification/orchestrator-types";
|
|
19
|
+
import { pipelineEventBus } from "../event-bus";
|
|
20
|
+
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
21
|
+
|
|
22
|
+
export const regressionStage: PipelineStage = {
|
|
23
|
+
name: "regression",
|
|
24
|
+
|
|
25
|
+
enabled(ctx: PipelineContext): boolean {
|
|
26
|
+
const mode = ctx.config.execution.regressionGate?.mode ?? "deferred";
|
|
27
|
+
if (mode !== "per-story") return false;
|
|
28
|
+
// Only run when verify passed (or was skipped/not set)
|
|
29
|
+
// Only run when verify passed (or was skipped/not set)
|
|
30
|
+
if (ctx.verifyResult && !ctx.verifyResult.success) return false;
|
|
31
|
+
const gateEnabled = ctx.config.execution.regressionGate?.enabled ?? true;
|
|
32
|
+
return gateEnabled;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
36
|
+
const logger = getLogger();
|
|
37
|
+
const testCommand = ctx.config.review?.commands?.test ?? ctx.config.quality.commands.test ?? "bun test";
|
|
38
|
+
const timeoutSeconds = ctx.config.execution.regressionGate?.timeoutSeconds ?? 120;
|
|
39
|
+
|
|
40
|
+
logger.info("regression", "Running full-suite regression gate", { storyId: ctx.story.id });
|
|
41
|
+
|
|
42
|
+
const verifyCtx: VerifyContext = {
|
|
43
|
+
workdir: ctx.workdir,
|
|
44
|
+
testCommand,
|
|
45
|
+
timeoutSeconds,
|
|
46
|
+
storyId: ctx.story.id,
|
|
47
|
+
acceptOnTimeout: ctx.config.execution.regressionGate?.acceptOnTimeout ?? true,
|
|
48
|
+
config: ctx.config,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const result = await _regressionStageDeps.verifyRegression(verifyCtx);
|
|
52
|
+
|
|
53
|
+
pipelineEventBus.emit({ type: "verify:completed", storyId: ctx.story.id, result });
|
|
54
|
+
|
|
55
|
+
if (result.success) {
|
|
56
|
+
logger.info("regression", "Full-suite regression gate passed", { storyId: ctx.story.id });
|
|
57
|
+
return { action: "continue" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (result.status === "TIMEOUT") {
|
|
61
|
+
logger.warn("regression", "Regression gate timed out (accepted as pass)", { storyId: ctx.story.id });
|
|
62
|
+
return { action: "continue" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger.warn("regression", "Full-suite regression detected", {
|
|
66
|
+
storyId: ctx.story.id,
|
|
67
|
+
failCount: result.failCount,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
pipelineEventBus.emit({
|
|
71
|
+
type: "regression:detected",
|
|
72
|
+
storyId: ctx.story.id,
|
|
73
|
+
failedTests: result.failCount,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
action: "escalate",
|
|
78
|
+
reason: `Full-suite regression: ${result.failCount} test(s) failing`,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Injectable deps for testing.
|
|
85
|
+
*/
|
|
86
|
+
export const _regressionStageDeps = {
|
|
87
|
+
verifyRegression: (ctx: VerifyContext) => verificationOrchestrator.verifyRegression(ctx),
|
|
88
|
+
};
|
|
@@ -1,75 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Review Stage
|
|
2
|
+
* Review Stage (ADR-005, Phase 2)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Checks code quality, tests, linting, etc. via review module.
|
|
6
|
-
* After built-in checks, runs plugin reviewers if any are registered.
|
|
4
|
+
* Delegates to ReviewOrchestrator for built-in checks + plugin reviewers.
|
|
7
5
|
*
|
|
8
6
|
* @returns
|
|
9
7
|
* - `continue`: Review passed
|
|
10
|
-
* - `
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* // Review enabled and passes
|
|
15
|
-
* await reviewStage.execute(ctx);
|
|
16
|
-
* // ctx.reviewResult: { success: true, totalDurationMs: 1500, ... }
|
|
17
|
-
*
|
|
18
|
-
* // Review enabled but fails
|
|
19
|
-
* await reviewStage.execute(ctx);
|
|
20
|
-
* // Returns: { action: "fail", reason: "Review failed: typecheck errors" }
|
|
21
|
-
* ```
|
|
8
|
+
* - `escalate`: Built-in check failed (lint/typecheck) — autofix stage handles retry
|
|
9
|
+
* - `fail`: Plugin reviewer hard-failed
|
|
22
10
|
*/
|
|
23
11
|
|
|
24
|
-
|
|
12
|
+
// RE-ARCH: rewrite
|
|
25
13
|
import { getLogger } from "../../logger";
|
|
26
|
-
import {
|
|
14
|
+
import { reviewOrchestrator } from "../../review/orchestrator";
|
|
27
15
|
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
28
16
|
|
|
29
|
-
/**
|
|
30
|
-
* Get list of changed files from git.
|
|
31
|
-
* Includes both staged and unstaged changes.
|
|
32
|
-
*
|
|
33
|
-
* @param workdir - Working directory
|
|
34
|
-
* @returns Array of changed file paths
|
|
35
|
-
*/
|
|
36
|
-
async function getChangedFiles(workdir: string): Promise<string[]> {
|
|
37
|
-
try {
|
|
38
|
-
// Get both staged and unstaged changes
|
|
39
|
-
const [stagedProc, unstagedProc] = [
|
|
40
|
-
spawn({
|
|
41
|
-
cmd: ["git", "diff", "--name-only", "--cached"],
|
|
42
|
-
cwd: workdir,
|
|
43
|
-
stdout: "pipe",
|
|
44
|
-
stderr: "pipe",
|
|
45
|
-
}),
|
|
46
|
-
spawn({
|
|
47
|
-
cmd: ["git", "diff", "--name-only"],
|
|
48
|
-
cwd: workdir,
|
|
49
|
-
stdout: "pipe",
|
|
50
|
-
stderr: "pipe",
|
|
51
|
-
}),
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
await Promise.all([stagedProc.exited, unstagedProc.exited]);
|
|
55
|
-
|
|
56
|
-
const stagedFiles = (await new Response(stagedProc.stdout).text())
|
|
57
|
-
.trim()
|
|
58
|
-
.split("\n")
|
|
59
|
-
.filter((line) => line.length > 0);
|
|
60
|
-
|
|
61
|
-
const unstagedFiles = (await new Response(unstagedProc.stdout).text())
|
|
62
|
-
.trim()
|
|
63
|
-
.split("\n")
|
|
64
|
-
.filter((line) => line.length > 0);
|
|
65
|
-
|
|
66
|
-
// Combine and deduplicate
|
|
67
|
-
return Array.from(new Set([...stagedFiles, ...unstagedFiles]));
|
|
68
|
-
} catch {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
17
|
export const reviewStage: PipelineStage = {
|
|
74
18
|
name: "review",
|
|
75
19
|
enabled: (ctx) => ctx.config.review.enabled,
|
|
@@ -77,105 +21,27 @@ export const reviewStage: PipelineStage = {
|
|
|
77
21
|
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
78
22
|
const logger = getLogger();
|
|
79
23
|
|
|
80
|
-
logger.info("review", "Running review phase");
|
|
24
|
+
logger.info("review", "Running review phase", { storyId: ctx.story.id });
|
|
25
|
+
|
|
26
|
+
const result = await reviewOrchestrator.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins);
|
|
81
27
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
28
|
+
ctx.reviewResult = result.builtIn;
|
|
29
|
+
|
|
30
|
+
if (!result.success) {
|
|
31
|
+
if (result.pluginFailed) {
|
|
32
|
+
logger.error("review", `Plugin reviewer failed: ${result.failureReason}`, { storyId: ctx.story.id });
|
|
33
|
+
return { action: "fail", reason: `Review failed: ${result.failureReason}` };
|
|
34
|
+
}
|
|
85
35
|
|
|
86
|
-
// BUG-030: Review failure (lint/typecheck) should escalate, not hard-fail.
|
|
87
|
-
// Lint/typecheck errors are auto-fixable — give the agent a retry with error context.
|
|
88
|
-
// Only plugin reviewer rejections are hard failures.
|
|
89
|
-
if (!reviewResult.success) {
|
|
90
36
|
logger.warn("review", "Review failed (built-in checks) — escalating for retry", {
|
|
91
|
-
reason:
|
|
37
|
+
reason: result.failureReason,
|
|
92
38
|
storyId: ctx.story.id,
|
|
93
39
|
});
|
|
94
|
-
return { action: "escalate", reason: `Review failed: ${
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Run plugin reviewers if any are registered
|
|
98
|
-
if (ctx.plugins) {
|
|
99
|
-
const pluginReviewers = ctx.plugins.getReviewers();
|
|
100
|
-
if (pluginReviewers.length > 0) {
|
|
101
|
-
logger.info("review", `Running ${pluginReviewers.length} plugin reviewer(s)`);
|
|
102
|
-
|
|
103
|
-
const changedFiles = await getChangedFiles(ctx.workdir);
|
|
104
|
-
const pluginReviewerResults: Array<{
|
|
105
|
-
name: string;
|
|
106
|
-
passed: boolean;
|
|
107
|
-
output: string;
|
|
108
|
-
exitCode?: number;
|
|
109
|
-
error?: string;
|
|
110
|
-
}> = [];
|
|
111
|
-
|
|
112
|
-
for (const reviewer of pluginReviewers) {
|
|
113
|
-
logger.info("review", `Running plugin reviewer: ${reviewer.name}`);
|
|
114
|
-
try {
|
|
115
|
-
const result = await reviewer.check(ctx.workdir, changedFiles);
|
|
116
|
-
|
|
117
|
-
// Capture result for debugging
|
|
118
|
-
pluginReviewerResults.push({
|
|
119
|
-
name: reviewer.name,
|
|
120
|
-
passed: result.passed,
|
|
121
|
-
output: result.output,
|
|
122
|
-
exitCode: result.exitCode,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
if (!result.passed) {
|
|
126
|
-
logger.error("review", `Plugin reviewer failed: ${reviewer.name}`, {
|
|
127
|
-
output: result.output,
|
|
128
|
-
storyId: ctx.story.id,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Store results in review result before failing
|
|
132
|
-
if (ctx.reviewResult) {
|
|
133
|
-
ctx.reviewResult.pluginReviewers = pluginReviewerResults;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
action: "fail",
|
|
138
|
-
reason: `Review failed: plugin reviewer '${reviewer.name}' failed`,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
logger.info("review", `Plugin reviewer passed: ${reviewer.name}`);
|
|
143
|
-
} catch (error) {
|
|
144
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
145
|
-
logger.error("review", `Plugin reviewer error: ${reviewer.name}`, {
|
|
146
|
-
error: errorMsg,
|
|
147
|
-
storyId: ctx.story.id,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Capture error for debugging
|
|
151
|
-
pluginReviewerResults.push({
|
|
152
|
-
name: reviewer.name,
|
|
153
|
-
passed: false,
|
|
154
|
-
output: "",
|
|
155
|
-
error: errorMsg,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Store results in review result before failing
|
|
159
|
-
if (ctx.reviewResult) {
|
|
160
|
-
ctx.reviewResult.pluginReviewers = pluginReviewerResults;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
action: "fail",
|
|
165
|
-
reason: `Review failed: plugin reviewer '${reviewer.name}' threw error`,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Store successful plugin reviewer results
|
|
171
|
-
if (ctx.reviewResult) {
|
|
172
|
-
ctx.reviewResult.pluginReviewers = pluginReviewerResults;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
40
|
+
return { action: "escalate", reason: `Review failed: ${result.failureReason}` };
|
|
175
41
|
}
|
|
176
42
|
|
|
177
43
|
logger.info("review", "Review passed", {
|
|
178
|
-
durationMs:
|
|
44
|
+
durationMs: result.builtIn.totalDurationMs,
|
|
179
45
|
storyId: ctx.story.id,
|
|
180
46
|
});
|
|
181
47
|
return { action: "continue" };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Verify Stage
|
|
3
3
|
*
|
|
4
|
-
* Verifies the agent
|
|
4
|
+
* Verifies the agent's work meets basic requirements by running tests.
|
|
5
5
|
* This is a lightweight verification before the full review stage.
|
|
6
6
|
*
|
|
7
7
|
* @returns
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import type { SmartTestRunnerConfig } from "../../config/types";
|
|
13
13
|
import { getLogger } from "../../logger";
|
|
14
|
-
import {
|
|
14
|
+
import type { VerifyStatus } from "../../verification/orchestrator-types";
|
|
15
|
+
import { regression } from "../../verification/runners";
|
|
15
16
|
import { _smartRunnerDeps } from "../../verification/smart-runner";
|
|
16
17
|
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
17
18
|
|
|
@@ -109,6 +110,21 @@ export const verifyStage: PipelineStage = {
|
|
|
109
110
|
acceptOnTimeout: ctx.config.execution.regressionGate?.acceptOnTimeout ?? true,
|
|
110
111
|
});
|
|
111
112
|
|
|
113
|
+
// Store result on context for rectify stage
|
|
114
|
+
ctx.verifyResult = {
|
|
115
|
+
success: result.success,
|
|
116
|
+
status: (result.status === "TIMEOUT" ? "TIMEOUT" : result.success ? "PASS" : "TEST_FAILURE") as VerifyStatus,
|
|
117
|
+
storyId: ctx.story.id,
|
|
118
|
+
strategy: "scoped",
|
|
119
|
+
passCount: result.passCount ?? 0,
|
|
120
|
+
failCount: result.failCount ?? 0,
|
|
121
|
+
totalCount: (result.passCount ?? 0) + (result.failCount ?? 0),
|
|
122
|
+
failures: [],
|
|
123
|
+
rawOutput: result.output,
|
|
124
|
+
durationMs: 0,
|
|
125
|
+
countsTowardEscalation: result.countsTowardEscalation,
|
|
126
|
+
};
|
|
127
|
+
|
|
112
128
|
// HARD FAILURE: Tests must pass for story to be marked complete
|
|
113
129
|
if (!result.success) {
|
|
114
130
|
// BUG-019: Distinguish timeout from actual test failures
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
/**
|
|
3
|
+
* Hooks Subscriber (ADR-005, Phase 3 US-P3-001)
|
|
4
|
+
*
|
|
5
|
+
* Maps pipeline events to nax lifecycle hooks (on-start, on-story-start,
|
|
6
|
+
* on-story-complete, on-story-fail, on-pause, on-complete).
|
|
7
|
+
*
|
|
8
|
+
* Design:
|
|
9
|
+
* - All hook calls are fire-and-forget (matching prior fireHook behavior)
|
|
10
|
+
* - Errors in hooks are logged, never rethrown
|
|
11
|
+
* - Returns unsubscribe function for cleanup
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { hookCtx } from "../../execution/story-context";
|
|
15
|
+
import { type LoadedHooksConfig, fireHook } from "../../hooks";
|
|
16
|
+
import { getSafeLogger } from "../../logger";
|
|
17
|
+
import type { PipelineEventBus } from "../event-bus";
|
|
18
|
+
|
|
19
|
+
export type UnsubscribeFn = () => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wire pipeline lifecycle hooks to the event bus.
|
|
23
|
+
*
|
|
24
|
+
* @param bus - The pipeline event bus
|
|
25
|
+
* @param hooks - Loaded hooks config (from nax.config)
|
|
26
|
+
* @param workdir - Working directory for hook script execution
|
|
27
|
+
* @param feature - Feature name (for hook context payload)
|
|
28
|
+
* @returns Unsubscribe function (call to remove all subscriptions)
|
|
29
|
+
*/
|
|
30
|
+
export function wireHooks(
|
|
31
|
+
bus: PipelineEventBus,
|
|
32
|
+
hooks: LoadedHooksConfig,
|
|
33
|
+
workdir: string,
|
|
34
|
+
feature: string,
|
|
35
|
+
): UnsubscribeFn {
|
|
36
|
+
const logger = getSafeLogger();
|
|
37
|
+
|
|
38
|
+
const safe = (name: string, fn: () => Promise<void>) => {
|
|
39
|
+
fn().catch((err) => logger?.warn("hooks-subscriber", `Hook "${name}" failed`, { error: String(err) }));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const unsubs: UnsubscribeFn[] = [];
|
|
43
|
+
|
|
44
|
+
// run:started → on-start
|
|
45
|
+
unsubs.push(
|
|
46
|
+
bus.on("run:started", (ev) => {
|
|
47
|
+
safe("on-start", () => fireHook(hooks, "on-start", hookCtx(feature, { status: "running" }), workdir));
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// story:started → on-story-start
|
|
52
|
+
unsubs.push(
|
|
53
|
+
bus.on("story:started", (ev) => {
|
|
54
|
+
safe("on-story-start", () =>
|
|
55
|
+
fireHook(
|
|
56
|
+
hooks,
|
|
57
|
+
"on-story-start",
|
|
58
|
+
hookCtx(feature, { storyId: ev.storyId, model: ev.modelTier, agent: ev.agent }),
|
|
59
|
+
workdir,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// story:completed → on-story-complete
|
|
66
|
+
unsubs.push(
|
|
67
|
+
bus.on("story:completed", (ev) => {
|
|
68
|
+
safe("on-story-complete", () =>
|
|
69
|
+
fireHook(
|
|
70
|
+
hooks,
|
|
71
|
+
"on-story-complete",
|
|
72
|
+
hookCtx(feature, { storyId: ev.storyId, status: "passed", cost: ev.cost }),
|
|
73
|
+
workdir,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// story:failed → on-story-fail
|
|
80
|
+
unsubs.push(
|
|
81
|
+
bus.on("story:failed", (ev) => {
|
|
82
|
+
safe("on-story-fail", () =>
|
|
83
|
+
fireHook(
|
|
84
|
+
hooks,
|
|
85
|
+
"on-story-fail",
|
|
86
|
+
hookCtx(feature, { storyId: ev.storyId, status: "failed", reason: ev.reason }),
|
|
87
|
+
workdir,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// story:paused → on-pause
|
|
94
|
+
unsubs.push(
|
|
95
|
+
bus.on("story:paused", (ev) => {
|
|
96
|
+
safe("on-pause (story)", () =>
|
|
97
|
+
fireHook(
|
|
98
|
+
hooks,
|
|
99
|
+
"on-pause",
|
|
100
|
+
hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }),
|
|
101
|
+
workdir,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// run:paused → on-pause
|
|
108
|
+
unsubs.push(
|
|
109
|
+
bus.on("run:paused", (ev) => {
|
|
110
|
+
safe("on-pause (run)", () =>
|
|
111
|
+
fireHook(
|
|
112
|
+
hooks,
|
|
113
|
+
"on-pause",
|
|
114
|
+
hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }),
|
|
115
|
+
workdir,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// run:completed → on-complete
|
|
122
|
+
unsubs.push(
|
|
123
|
+
bus.on("run:completed", (ev) => {
|
|
124
|
+
safe("on-complete", () =>
|
|
125
|
+
fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: ev.totalCost ?? 0 }), workdir),
|
|
126
|
+
);
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
for (const u of unsubs) u();
|
|
132
|
+
};
|
|
133
|
+
}
|