@nathapp/nax 0.20.0 → 0.22.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/.claude/settings.json +15 -0
- package/.mcp.json +8 -0
- package/docs/20260304-review-nax.md +492 -0
- package/docs/ROADMAP.md +65 -18
- package/docs/adr/ADR-005-implementation-plan.md +655 -0
- package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
- package/docs/specs/bug-039-orphan-processes.md +131 -0
- package/docs/specs/bug-040-review-rectification.md +82 -0
- package/docs/specs/bug-041-cross-story-test-isolation.md +88 -0
- package/docs/specs/bug-042-verifier-failure-capture.md +117 -0
- package/docs/specs/feat-010-smart-runner-git-history.md +96 -0
- package/docs/specs/feat-011-file-context-strategy.md +73 -0
- package/docs/specs/feat-012-tdd-writer-tier.md +79 -0
- package/docs/specs/feat-013-test-after-review.md +89 -0
- package/docs/specs/feat-014-heartbeat-observability.md +127 -0
- package/memory/topic/feat-010-baseref.md +28 -0
- package/memory/topic/feat-013-test-after-deprecation.md +22 -0
- package/nax/config.json +7 -4
- package/nax/features/bug-039-medium/prd.json +45 -0
- package/package.json +2 -2
- package/src/agents/claude.ts +109 -15
- package/src/config/types.ts +11 -0
- package/src/context/builder.ts +9 -1
- 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 -315
- 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 +19 -3
- 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 +12 -1
- package/src/review/orchestrator.ts +105 -0
- package/src/review/runner.ts +39 -4
- package/src/routing/router.ts +3 -3
- package/src/routing/strategies/keyword.ts +5 -2
- package/src/routing/strategies/llm.ts +27 -1
- package/src/tdd/prompts.ts +1 -1
- package/src/utils/git.ts +49 -25
- package/src/verification/executor.ts +8 -2
- 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/smart-runner.ts +6 -10
- 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} +10 -404
- 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 +107 -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 +7 -3
- 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 +2 -1
- package/test/unit/prd-auto-default.test.ts +3 -2
- 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 +2 -1
- package/test/unit/routing/strategies/llm.test.ts +251 -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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
/**
|
|
3
|
+
* Interaction Subscriber (ADR-005, Phase 3 US-P3-003)
|
|
4
|
+
*
|
|
5
|
+
* Maps pipeline events to interaction trigger calls.
|
|
6
|
+
* Currently handles:
|
|
7
|
+
* - human-review:requested → executeTrigger("human-review")
|
|
8
|
+
*
|
|
9
|
+
* Future triggers (story-ambiguity, merge-conflict, etc.) can be added
|
|
10
|
+
* by subscribing to the appropriate events.
|
|
11
|
+
*
|
|
12
|
+
* Design:
|
|
13
|
+
* - Interaction triggers MAY block (await user response) — uses emitAsync where needed
|
|
14
|
+
* - Errors are caught and logged; never rethrown to avoid blocking the pipeline
|
|
15
|
+
* - Returns unsubscribe function for cleanup
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { NaxConfig } from "../../config";
|
|
19
|
+
import type { InteractionChain } from "../../interaction/chain";
|
|
20
|
+
import { executeTrigger, isTriggerEnabled } from "../../interaction/triggers";
|
|
21
|
+
import { getSafeLogger } from "../../logger";
|
|
22
|
+
import type { PipelineEventBus } from "../event-bus";
|
|
23
|
+
import type { UnsubscribeFn } from "./hooks";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wire interaction triggers to the event bus.
|
|
27
|
+
*
|
|
28
|
+
* @param bus - The pipeline event bus
|
|
29
|
+
* @param interactionChain - The active interaction chain (may be null)
|
|
30
|
+
* @param config - Nax config (for isTriggerEnabled checks)
|
|
31
|
+
* @returns Unsubscribe function
|
|
32
|
+
*/
|
|
33
|
+
export function wireInteraction(
|
|
34
|
+
bus: PipelineEventBus,
|
|
35
|
+
interactionChain: InteractionChain | null | undefined,
|
|
36
|
+
config: NaxConfig,
|
|
37
|
+
): UnsubscribeFn {
|
|
38
|
+
const logger = getSafeLogger();
|
|
39
|
+
const unsubs: UnsubscribeFn[] = [];
|
|
40
|
+
|
|
41
|
+
// human-review:requested → executeTrigger("human-review")
|
|
42
|
+
if (interactionChain && isTriggerEnabled("human-review", config)) {
|
|
43
|
+
unsubs.push(
|
|
44
|
+
bus.on("human-review:requested", (ev) => {
|
|
45
|
+
executeTrigger(
|
|
46
|
+
"human-review",
|
|
47
|
+
{
|
|
48
|
+
featureName: ev.feature ?? "",
|
|
49
|
+
storyId: ev.storyId,
|
|
50
|
+
iteration: ev.attempts ?? 0,
|
|
51
|
+
reason: ev.reason,
|
|
52
|
+
},
|
|
53
|
+
config,
|
|
54
|
+
interactionChain,
|
|
55
|
+
).catch((err) => {
|
|
56
|
+
logger?.warn("interaction-subscriber", "human-review trigger failed", {
|
|
57
|
+
storyId: ev.storyId,
|
|
58
|
+
error: String(err),
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
for (const u of unsubs) u();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// RE-ARCH: keep
|
|
2
|
+
/**
|
|
3
|
+
* Reporters Subscriber (ADR-005, Phase 3 US-P3-002)
|
|
4
|
+
*
|
|
5
|
+
* Maps pipeline events to IReporter plugin methods
|
|
6
|
+
* (onRunStart, onStoryComplete, onRunEnd).
|
|
7
|
+
*
|
|
8
|
+
* Design:
|
|
9
|
+
* - Each reporter call is fire-and-forget
|
|
10
|
+
* - Errors in individual reporters are caught and logged
|
|
11
|
+
* - Returns unsubscribe function for cleanup
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getSafeLogger } from "../../logger";
|
|
15
|
+
import type { PluginRegistry } from "../../plugins";
|
|
16
|
+
import type { PipelineEventBus } from "../event-bus";
|
|
17
|
+
import type { UnsubscribeFn } from "./hooks";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wire reporter plugin lifecycle events to the event bus.
|
|
21
|
+
*
|
|
22
|
+
* @param bus - The pipeline event bus
|
|
23
|
+
* @param pluginRegistry - Plugin registry exposing getReporters()
|
|
24
|
+
* @param runId - Current run ID (for reporter events)
|
|
25
|
+
* @param startTime - Run start timestamp in ms (for duration calculation)
|
|
26
|
+
* @returns Unsubscribe function
|
|
27
|
+
*/
|
|
28
|
+
export function wireReporters(
|
|
29
|
+
bus: PipelineEventBus,
|
|
30
|
+
pluginRegistry: PluginRegistry,
|
|
31
|
+
runId: string,
|
|
32
|
+
startTime: number,
|
|
33
|
+
): UnsubscribeFn {
|
|
34
|
+
const logger = getSafeLogger();
|
|
35
|
+
|
|
36
|
+
const safe = (name: string, fn: () => Promise<void>) => {
|
|
37
|
+
fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) }));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const unsubs: UnsubscribeFn[] = [];
|
|
41
|
+
|
|
42
|
+
// run:started → reporter.onRunStart
|
|
43
|
+
unsubs.push(
|
|
44
|
+
bus.on("run:started", (ev) => {
|
|
45
|
+
safe("onRunStart", async () => {
|
|
46
|
+
const reporters = pluginRegistry.getReporters();
|
|
47
|
+
for (const r of reporters) {
|
|
48
|
+
if (r.onRunStart) {
|
|
49
|
+
try {
|
|
50
|
+
await r.onRunStart({
|
|
51
|
+
runId,
|
|
52
|
+
feature: ev.feature,
|
|
53
|
+
totalStories: ev.totalStories,
|
|
54
|
+
startTime: new Date(startTime).toISOString(),
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// story:completed → reporter.onStoryComplete(status: "completed")
|
|
66
|
+
unsubs.push(
|
|
67
|
+
bus.on("story:completed", (ev) => {
|
|
68
|
+
safe("onStoryComplete(completed)", async () => {
|
|
69
|
+
const reporters = pluginRegistry.getReporters();
|
|
70
|
+
for (const r of reporters) {
|
|
71
|
+
if (r.onStoryComplete) {
|
|
72
|
+
try {
|
|
73
|
+
await r.onStoryComplete({
|
|
74
|
+
runId,
|
|
75
|
+
storyId: ev.storyId,
|
|
76
|
+
status: "completed",
|
|
77
|
+
durationMs: ev.durationMs,
|
|
78
|
+
cost: ev.cost ?? 0,
|
|
79
|
+
tier: ev.modelTier ?? "balanced",
|
|
80
|
+
testStrategy: ev.testStrategy ?? "test-after",
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// story:failed → reporter.onStoryComplete(status: "failed")
|
|
92
|
+
unsubs.push(
|
|
93
|
+
bus.on("story:failed", (ev) => {
|
|
94
|
+
safe("onStoryComplete(failed)", async () => {
|
|
95
|
+
const reporters = pluginRegistry.getReporters();
|
|
96
|
+
for (const r of reporters) {
|
|
97
|
+
if (r.onStoryComplete) {
|
|
98
|
+
try {
|
|
99
|
+
await r.onStoryComplete({
|
|
100
|
+
runId,
|
|
101
|
+
storyId: ev.storyId,
|
|
102
|
+
status: "failed",
|
|
103
|
+
durationMs: Date.now() - startTime,
|
|
104
|
+
cost: 0,
|
|
105
|
+
tier: "balanced",
|
|
106
|
+
testStrategy: "test-after",
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// story:paused → reporter.onStoryComplete(status: "paused")
|
|
118
|
+
unsubs.push(
|
|
119
|
+
bus.on("story:paused", (ev) => {
|
|
120
|
+
safe("onStoryComplete(paused)", async () => {
|
|
121
|
+
const reporters = pluginRegistry.getReporters();
|
|
122
|
+
for (const r of reporters) {
|
|
123
|
+
if (r.onStoryComplete) {
|
|
124
|
+
try {
|
|
125
|
+
await r.onStoryComplete({
|
|
126
|
+
runId,
|
|
127
|
+
storyId: ev.storyId,
|
|
128
|
+
status: "paused",
|
|
129
|
+
durationMs: Date.now() - startTime,
|
|
130
|
+
cost: 0,
|
|
131
|
+
tier: "balanced",
|
|
132
|
+
testStrategy: "test-after",
|
|
133
|
+
});
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// run:completed → reporter.onRunEnd
|
|
144
|
+
unsubs.push(
|
|
145
|
+
bus.on("run:completed", (ev) => {
|
|
146
|
+
safe("onRunEnd", async () => {
|
|
147
|
+
const reporters = pluginRegistry.getReporters();
|
|
148
|
+
for (const r of reporters) {
|
|
149
|
+
if (r.onRunEnd) {
|
|
150
|
+
try {
|
|
151
|
+
await r.onRunEnd({
|
|
152
|
+
runId,
|
|
153
|
+
totalDurationMs: Date.now() - startTime,
|
|
154
|
+
totalCost: ev.totalCost ?? 0,
|
|
155
|
+
storySummary: {
|
|
156
|
+
completed: ev.passedStories,
|
|
157
|
+
failed: ev.failedStories,
|
|
158
|
+
skipped: 0,
|
|
159
|
+
paused: 0,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
for (const u of unsubs) u();
|
|
173
|
+
};
|
|
174
|
+
}
|
package/src/pipeline/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { PluginRegistry } from "../plugins/registry";
|
|
|
15
15
|
import type { PRD, UserStory } from "../prd/types";
|
|
16
16
|
import type { ReviewResult } from "../review/types";
|
|
17
17
|
import type { FailureCategory } from "../tdd/types";
|
|
18
|
+
import type { VerifyResult } from "../verification/orchestrator-types";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Routing result from complexity classification
|
|
@@ -82,6 +83,8 @@ export interface PipelineContext {
|
|
|
82
83
|
prompt?: string;
|
|
83
84
|
/** Agent execution result (set by executionStage) */
|
|
84
85
|
agentResult?: AgentResult;
|
|
86
|
+
/** Verify result (set by verifyStage) */
|
|
87
|
+
verifyResult?: VerifyResult;
|
|
85
88
|
/** Review result (set by reviewStage) */
|
|
86
89
|
reviewResult?: ReviewResult;
|
|
87
90
|
/** Acceptance test failures (set by acceptanceStage) */
|
|
@@ -91,6 +94,12 @@ export interface PipelineContext {
|
|
|
91
94
|
};
|
|
92
95
|
/** Story start timestamp (ISO string, set by runner before pipeline) */
|
|
93
96
|
storyStartTime?: string;
|
|
97
|
+
/** Tracks how many times the rectify stage has run this pipeline (for event attempt numbers). */
|
|
98
|
+
rectifyAttempt?: number;
|
|
99
|
+
/** Tracks how many times the autofix stage has run this pipeline (for event attempt numbers). */
|
|
100
|
+
autofixAttempt?: number;
|
|
101
|
+
/** Git HEAD ref captured before agent ran this attempt (FEAT-010: precise smart-runner diff) */
|
|
102
|
+
storyGitRef?: string;
|
|
94
103
|
/** Collected story metrics (set by completionStage) */
|
|
95
104
|
storyMetrics?: StoryMetrics[];
|
|
96
105
|
/** Whether to retry the story in lite mode after a failure */
|
|
@@ -112,7 +121,9 @@ export type StageAction =
|
|
|
112
121
|
/** Escalate to a higher tier and retry the pipeline */
|
|
113
122
|
| { action: "escalate"; reason?: string; cost?: number }
|
|
114
123
|
/** Pause execution (user intervention required via queue command) */
|
|
115
|
-
| { action: "pause"; reason: string; cost?: number }
|
|
124
|
+
| { action: "pause"; reason: string; cost?: number }
|
|
125
|
+
/** Retry from a specific stage (used by rectify/autofix stages) */
|
|
126
|
+
| { action: "retry"; fromStage: string; cost?: number };
|
|
116
127
|
|
|
117
128
|
/**
|
|
118
129
|
* Result returned by a pipeline stage after execution.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Orchestrator (ADR-005, Phase 2)
|
|
3
|
+
*
|
|
4
|
+
* Single entry point for all post-implementation review. Delegates to the
|
|
5
|
+
* review runner and plugin reviewers. Provides a unified result.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const result = await reviewOrchestrator.review(config, workdir, executionConfig, plugins);
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from "bun";
|
|
12
|
+
import type { NaxConfig } from "../config";
|
|
13
|
+
import { getSafeLogger } from "../logger";
|
|
14
|
+
import type { PluginRegistry } from "../plugins";
|
|
15
|
+
import { runReview } from "./runner";
|
|
16
|
+
import type { ReviewConfig, ReviewResult } from "./types";
|
|
17
|
+
|
|
18
|
+
async function getChangedFiles(workdir: string): Promise<string[]> {
|
|
19
|
+
try {
|
|
20
|
+
const [stagedProc, unstagedProc] = [
|
|
21
|
+
spawn({ cmd: ["git", "diff", "--name-only", "--cached"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
|
|
22
|
+
spawn({ cmd: ["git", "diff", "--name-only"], cwd: workdir, stdout: "pipe", stderr: "pipe" }),
|
|
23
|
+
];
|
|
24
|
+
await Promise.all([stagedProc.exited, unstagedProc.exited]);
|
|
25
|
+
const staged = (await new Response(stagedProc.stdout).text()).trim().split("\n").filter(Boolean);
|
|
26
|
+
const unstaged = (await new Response(unstagedProc.stdout).text()).trim().split("\n").filter(Boolean);
|
|
27
|
+
return Array.from(new Set([...staged, ...unstaged]));
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OrchestratorReviewResult {
|
|
34
|
+
/** Built-in review result (typecheck, lint, format) */
|
|
35
|
+
builtIn: ReviewResult;
|
|
36
|
+
/** Whether ALL checks passed (built-in + plugin reviewers) */
|
|
37
|
+
success: boolean;
|
|
38
|
+
/** Failure reason if success === false */
|
|
39
|
+
failureReason?: string;
|
|
40
|
+
/** Plugin reviewer hard-failure flag (determines escalate vs fail) */
|
|
41
|
+
pluginFailed: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ReviewOrchestrator {
|
|
45
|
+
/** Run built-in checks + plugin reviewers. Returns unified result. */
|
|
46
|
+
async review(
|
|
47
|
+
reviewConfig: ReviewConfig,
|
|
48
|
+
workdir: string,
|
|
49
|
+
executionConfig: NaxConfig["execution"],
|
|
50
|
+
plugins?: PluginRegistry,
|
|
51
|
+
): Promise<OrchestratorReviewResult> {
|
|
52
|
+
const logger = getSafeLogger();
|
|
53
|
+
|
|
54
|
+
const builtIn = await runReview(reviewConfig, workdir, executionConfig);
|
|
55
|
+
|
|
56
|
+
if (!builtIn.success) {
|
|
57
|
+
return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (plugins) {
|
|
61
|
+
const reviewers = plugins.getReviewers();
|
|
62
|
+
if (reviewers.length > 0) {
|
|
63
|
+
const changedFiles = await getChangedFiles(workdir);
|
|
64
|
+
const pluginResults: ReviewResult["pluginReviewers"] = [];
|
|
65
|
+
|
|
66
|
+
for (const reviewer of reviewers) {
|
|
67
|
+
logger?.info("review", `Running plugin reviewer: ${reviewer.name}`);
|
|
68
|
+
try {
|
|
69
|
+
const result = await reviewer.check(workdir, changedFiles);
|
|
70
|
+
pluginResults.push({
|
|
71
|
+
name: reviewer.name,
|
|
72
|
+
passed: result.passed,
|
|
73
|
+
output: result.output,
|
|
74
|
+
exitCode: result.exitCode,
|
|
75
|
+
});
|
|
76
|
+
if (!result.passed) {
|
|
77
|
+
builtIn.pluginReviewers = pluginResults;
|
|
78
|
+
return {
|
|
79
|
+
builtIn,
|
|
80
|
+
success: false,
|
|
81
|
+
failureReason: `plugin reviewer '${reviewer.name}' failed`,
|
|
82
|
+
pluginFailed: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
87
|
+
pluginResults.push({ name: reviewer.name, passed: false, output: "", error: errorMsg });
|
|
88
|
+
builtIn.pluginReviewers = pluginResults;
|
|
89
|
+
return {
|
|
90
|
+
builtIn,
|
|
91
|
+
success: false,
|
|
92
|
+
failureReason: `plugin reviewer '${reviewer.name}' threw error`,
|
|
93
|
+
pluginFailed: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
builtIn.pluginReviewers = pluginResults;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { builtIn, success: true, pluginFailed: false };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const reviewOrchestrator = new ReviewOrchestrator();
|
package/src/review/runner.ts
CHANGED
|
@@ -76,8 +76,13 @@ async function resolveCommand(
|
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Default timeout for review checks (lint, typecheck). BUG-039. */
|
|
80
|
+
const REVIEW_CHECK_TIMEOUT_MS = 120_000;
|
|
81
|
+
|
|
79
82
|
/**
|
|
80
|
-
* Run a single review check
|
|
83
|
+
* Run a single review check with a hard timeout.
|
|
84
|
+
*
|
|
85
|
+
* BUG-039: Added SIGTERM + SIGKILL cleanup to prevent orphan lint/typecheck processes.
|
|
81
86
|
*/
|
|
82
87
|
async function runCheck(check: ReviewCheckName, command: string, workdir: string): Promise<ReviewCheckResult> {
|
|
83
88
|
const startTime = Date.now();
|
|
@@ -96,8 +101,38 @@ async function runCheck(check: ReviewCheckName, command: string, workdir: string
|
|
|
96
101
|
stderr: "pipe",
|
|
97
102
|
});
|
|
98
103
|
|
|
104
|
+
// BUG-039: Hard timeout — kill the process if it hangs
|
|
105
|
+
let timedOut = false;
|
|
106
|
+
const timerId = setTimeout(() => {
|
|
107
|
+
timedOut = true;
|
|
108
|
+
try {
|
|
109
|
+
proc.kill("SIGTERM");
|
|
110
|
+
} catch {
|
|
111
|
+
/* already exited */
|
|
112
|
+
}
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
try {
|
|
115
|
+
proc.kill("SIGKILL");
|
|
116
|
+
} catch {
|
|
117
|
+
/* already exited */
|
|
118
|
+
}
|
|
119
|
+
}, 5000);
|
|
120
|
+
}, REVIEW_CHECK_TIMEOUT_MS);
|
|
121
|
+
|
|
99
122
|
// Wait for completion
|
|
100
|
-
const
|
|
123
|
+
const exitCode = await proc.exited;
|
|
124
|
+
clearTimeout(timerId);
|
|
125
|
+
|
|
126
|
+
if (timedOut) {
|
|
127
|
+
return {
|
|
128
|
+
check,
|
|
129
|
+
command,
|
|
130
|
+
success: false,
|
|
131
|
+
exitCode: -1,
|
|
132
|
+
output: `[nax] ${check} timed out after ${REVIEW_CHECK_TIMEOUT_MS / 1000}s`,
|
|
133
|
+
durationMs: Date.now() - startTime,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
101
136
|
|
|
102
137
|
// Collect output
|
|
103
138
|
const stdout = await new Response(proc.stdout).text();
|
|
@@ -107,8 +142,8 @@ async function runCheck(check: ReviewCheckName, command: string, workdir: string
|
|
|
107
142
|
return {
|
|
108
143
|
check,
|
|
109
144
|
command,
|
|
110
|
-
success:
|
|
111
|
-
exitCode
|
|
145
|
+
success: exitCode === 0,
|
|
146
|
+
exitCode,
|
|
112
147
|
output,
|
|
113
148
|
durationMs: Date.now() - startTime,
|
|
114
149
|
};
|
package/src/routing/router.ts
CHANGED
|
@@ -152,7 +152,7 @@ const LITE_TAGS = ["ui", "layout", "cli", "integration", "polyglot"];
|
|
|
152
152
|
* - 'auto' → existing heuristic logic, plus:
|
|
153
153
|
* if tags include ui/layout/cli/integration/polyglot → three-session-tdd-lite
|
|
154
154
|
* if security/public-api/complex/expert → three-session-tdd
|
|
155
|
-
* otherwise → test-after
|
|
155
|
+
* otherwise → three-session-tdd-lite (test-after deprecated from auto mode)
|
|
156
156
|
*
|
|
157
157
|
* @param complexity - Pre-classified complexity level
|
|
158
158
|
* @param title - Story title
|
|
@@ -201,8 +201,8 @@ export function determineTestStrategy(
|
|
|
201
201
|
return hasLiteTag ? "three-session-tdd-lite" : "three-session-tdd";
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
// Simple/medium → test-after
|
|
205
|
-
return "
|
|
204
|
+
// Simple/medium → three-session-tdd-lite (FEAT-013: test-after deprecated from auto mode)
|
|
205
|
+
return "three-session-tdd-lite";
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/** Map complexity to model tier */
|
|
@@ -117,7 +117,8 @@ function determineTestStrategy(
|
|
|
117
117
|
return "three-session-tdd";
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
// FEAT-013: test-after deprecated from auto mode — use three-session-tdd-lite as default
|
|
121
|
+
return "three-session-tdd-lite";
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
/** Map complexity to model tier */
|
|
@@ -160,7 +161,9 @@ export const keywordStrategy: RoutingStrategy = {
|
|
|
160
161
|
modelTier,
|
|
161
162
|
testStrategy,
|
|
162
163
|
reasoning:
|
|
163
|
-
reasons.length > 0
|
|
164
|
+
reasons.length > 0
|
|
165
|
+
? `three-session-tdd: ${reasons.join(", ")}`
|
|
166
|
+
: `three-session-tdd-lite: simple task (${complexity})`,
|
|
164
167
|
};
|
|
165
168
|
},
|
|
166
169
|
};
|
|
@@ -49,6 +49,22 @@ function evictOldest(): void {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** Minimal proc shape returned by spawn for piped stdio. */
|
|
53
|
+
export interface PipedProc {
|
|
54
|
+
stdout: ReadableStream<Uint8Array>;
|
|
55
|
+
stderr: ReadableStream<Uint8Array>;
|
|
56
|
+
exited: Promise<number>;
|
|
57
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Swappable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
62
|
+
*/
|
|
63
|
+
export const _deps = {
|
|
64
|
+
spawn: (cmd: string[], opts: { stdout: "pipe"; stderr: "pipe" }): PipedProc =>
|
|
65
|
+
Bun.spawn(cmd, opts) as unknown as PipedProc,
|
|
66
|
+
};
|
|
67
|
+
|
|
52
68
|
/**
|
|
53
69
|
* Call LLM via claude CLI with timeout.
|
|
54
70
|
*
|
|
@@ -69,7 +85,7 @@ async function callLlmOnce(modelTier: string, prompt: string, config: NaxConfig,
|
|
|
69
85
|
const modelArg = modelDef.model;
|
|
70
86
|
|
|
71
87
|
// Spawn claude CLI with timeout
|
|
72
|
-
const proc =
|
|
88
|
+
const proc = _deps.spawn(["claude", "-p", prompt, "--model", modelArg], {
|
|
73
89
|
stdout: "pipe",
|
|
74
90
|
stderr: "pipe",
|
|
75
91
|
});
|
|
@@ -100,6 +116,16 @@ async function callLlmOnce(modelTier: string, prompt: string, config: NaxConfig,
|
|
|
100
116
|
return result;
|
|
101
117
|
} catch (err) {
|
|
102
118
|
clearTimeout(timeoutId);
|
|
119
|
+
try {
|
|
120
|
+
proc.stdout.cancel();
|
|
121
|
+
} catch {
|
|
122
|
+
// ignore cancel errors
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
proc.stderr.cancel();
|
|
126
|
+
} catch {
|
|
127
|
+
// ignore cancel errors
|
|
128
|
+
}
|
|
103
129
|
proc.kill();
|
|
104
130
|
throw err;
|
|
105
131
|
}
|
package/src/tdd/prompts.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RectificationConfig } from "../config";
|
|
2
|
-
import { createRectificationPrompt } from "../execution/rectification";
|
|
3
2
|
import type { TestFailure } from "../execution/test-output-parser";
|
|
4
3
|
import type { UserStory } from "../prd";
|
|
4
|
+
import { createRectificationPrompt } from "../verification/rectification";
|
|
5
5
|
import type { TddSessionRole } from "./types";
|
|
6
6
|
|
|
7
7
|
/**
|
package/src/utils/git.ts
CHANGED
|
@@ -4,6 +4,48 @@
|
|
|
4
4
|
|
|
5
5
|
import { spawn } from "bun";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Default timeout for git subprocess calls.
|
|
9
|
+
* Prevents git from hanging indefinitely on locked repos or network mounts.
|
|
10
|
+
*/
|
|
11
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Spawn a git command with a hard timeout.
|
|
15
|
+
*
|
|
16
|
+
* Kills the process with SIGKILL after GIT_TIMEOUT_MS if it hasn't exited.
|
|
17
|
+
* Returns empty stdout and exit code 1 on timeout.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export async function gitWithTimeout(args: string[], workdir: string): Promise<{ stdout: string; exitCode: number }> {
|
|
22
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
23
|
+
cwd: workdir,
|
|
24
|
+
stdout: "pipe",
|
|
25
|
+
stderr: "pipe",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let timedOut = false;
|
|
29
|
+
const timerId = setTimeout(() => {
|
|
30
|
+
timedOut = true;
|
|
31
|
+
try {
|
|
32
|
+
proc.kill("SIGKILL");
|
|
33
|
+
} catch {
|
|
34
|
+
// Process may have already exited
|
|
35
|
+
}
|
|
36
|
+
}, GIT_TIMEOUT_MS);
|
|
37
|
+
|
|
38
|
+
const exitCode = await proc.exited;
|
|
39
|
+
clearTimeout(timerId);
|
|
40
|
+
|
|
41
|
+
if (timedOut) {
|
|
42
|
+
return { stdout: "", exitCode: 1 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stdout = await new Response(proc.stdout).text();
|
|
46
|
+
return { stdout, exitCode };
|
|
47
|
+
}
|
|
48
|
+
|
|
7
49
|
/**
|
|
8
50
|
* Capture current git HEAD ref.
|
|
9
51
|
*
|
|
@@ -23,17 +65,8 @@ import { spawn } from "bun";
|
|
|
23
65
|
*/
|
|
24
66
|
export async function captureGitRef(workdir: string): Promise<string | undefined> {
|
|
25
67
|
try {
|
|
26
|
-
const
|
|
27
|
-
cmd: ["git", "rev-parse", "HEAD"],
|
|
28
|
-
cwd: workdir,
|
|
29
|
-
stdout: "pipe",
|
|
30
|
-
stderr: "pipe",
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const exitCode = await proc.exited;
|
|
68
|
+
const { stdout, exitCode } = await gitWithTimeout(["rev-parse", "HEAD"], workdir);
|
|
34
69
|
if (exitCode !== 0) return undefined;
|
|
35
|
-
|
|
36
|
-
const stdout = await new Response(proc.stdout).text();
|
|
37
70
|
return stdout.trim() || undefined;
|
|
38
71
|
} catch {
|
|
39
72
|
return undefined;
|
|
@@ -43,7 +76,7 @@ export async function captureGitRef(workdir: string): Promise<string | undefined
|
|
|
43
76
|
/**
|
|
44
77
|
* Check if a story ID appears in recent git commit messages.
|
|
45
78
|
*
|
|
46
|
-
* Searches the last
|
|
79
|
+
* Searches the last N commits for commit messages containing the story ID.
|
|
47
80
|
* Used for state reconciliation: if a failed story has commits in git history,
|
|
48
81
|
* it means the story was partially completed and should be marked as passed.
|
|
49
82
|
*
|
|
@@ -62,21 +95,12 @@ export async function captureGitRef(workdir: string): Promise<string | undefined
|
|
|
62
95
|
*/
|
|
63
96
|
export async function hasCommitsForStory(workdir: string, storyId: string, maxCommits = 20): Promise<boolean> {
|
|
64
97
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
stderr: "pipe",
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const exitCode = await proc.exited;
|
|
98
|
+
const { stdout, exitCode } = await gitWithTimeout(
|
|
99
|
+
["log", `-${maxCommits}`, "--oneline", "--grep", storyId],
|
|
100
|
+
workdir,
|
|
101
|
+
);
|
|
73
102
|
if (exitCode !== 0) return false;
|
|
74
|
-
|
|
75
|
-
const stdout = await new Response(proc.stdout).text();
|
|
76
|
-
const commits = stdout.trim();
|
|
77
|
-
|
|
78
|
-
// If any commits found, return true
|
|
79
|
-
return commits.length > 0;
|
|
103
|
+
return stdout.trim().length > 0;
|
|
80
104
|
} catch {
|
|
81
105
|
return false;
|
|
82
106
|
}
|
|
@@ -17,8 +17,14 @@ import type { TestExecutionResult } from "./types";
|
|
|
17
17
|
*/
|
|
18
18
|
async function drainWithDeadline(proc: Subprocess, deadlineMs: number): Promise<string> {
|
|
19
19
|
const EMPTY = Symbol("timeout");
|
|
20
|
-
const race = <T>(p: Promise<T>) =>
|
|
21
|
-
|
|
20
|
+
const race = <T>(p: Promise<T>) => {
|
|
21
|
+
// BUG-039: Store timer handle so it can be cleared after race resolves (prevent leak)
|
|
22
|
+
let timerId: ReturnType<typeof setTimeout>;
|
|
23
|
+
const timeoutPromise = new Promise<typeof EMPTY>((r) => {
|
|
24
|
+
timerId = setTimeout(() => r(EMPTY), deadlineMs);
|
|
25
|
+
});
|
|
26
|
+
return Promise.race([p, timeoutPromise]).finally(() => clearTimeout(timerId));
|
|
27
|
+
};
|
|
22
28
|
|
|
23
29
|
let out = "";
|
|
24
30
|
try {
|