@nathapp/nax 0.50.2 → 0.51.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/CHANGELOG.md +30 -0
- package/dist/nax.js +579 -373
- package/package.json +1 -3
- package/bin/nax.ts +0 -1195
- package/src/acceptance/fix-generator.ts +0 -322
- package/src/acceptance/generator.ts +0 -423
- package/src/acceptance/index.ts +0 -42
- package/src/acceptance/refinement.ts +0 -224
- package/src/acceptance/templates/cli.ts +0 -47
- package/src/acceptance/templates/component.ts +0 -78
- package/src/acceptance/templates/e2e.ts +0 -43
- package/src/acceptance/templates/index.ts +0 -21
- package/src/acceptance/templates/snapshot.ts +0 -50
- package/src/acceptance/templates/unit.ts +0 -48
- package/src/acceptance/types.ts +0 -135
- package/src/agents/acp/adapter.ts +0 -888
- package/src/agents/acp/cost.ts +0 -9
- package/src/agents/acp/index.ts +0 -7
- package/src/agents/acp/interaction-bridge.ts +0 -126
- package/src/agents/acp/parser.ts +0 -119
- package/src/agents/acp/spawn-client.ts +0 -373
- package/src/agents/acp/types.ts +0 -22
- package/src/agents/aider/adapter.ts +0 -135
- package/src/agents/claude/adapter.ts +0 -258
- package/src/agents/claude/complete.ts +0 -80
- package/src/agents/claude/cost.ts +0 -16
- package/src/agents/claude/execution.ts +0 -215
- package/src/agents/claude/index.ts +0 -3
- package/src/agents/claude/interactive.ts +0 -77
- package/src/agents/claude/plan.ts +0 -179
- package/src/agents/codex/adapter.ts +0 -153
- package/src/agents/cost/calculate.ts +0 -154
- package/src/agents/cost/index.ts +0 -10
- package/src/agents/cost/parse.ts +0 -97
- package/src/agents/cost/pricing.ts +0 -59
- package/src/agents/cost/types.ts +0 -45
- package/src/agents/gemini/adapter.ts +0 -177
- package/src/agents/index.ts +0 -18
- package/src/agents/opencode/adapter.ts +0 -106
- package/src/agents/registry.ts +0 -136
- package/src/agents/shared/decompose.ts +0 -154
- package/src/agents/shared/model-resolution.ts +0 -43
- package/src/agents/shared/types-extended.ts +0 -164
- package/src/agents/shared/validation.ts +0 -69
- package/src/agents/shared/version-detection.ts +0 -109
- package/src/agents/types.ts +0 -205
- package/src/analyze/classifier.ts +0 -282
- package/src/analyze/index.ts +0 -16
- package/src/analyze/scanner.ts +0 -171
- package/src/analyze/types.ts +0 -51
- package/src/cli/accept.ts +0 -108
- package/src/cli/agents.ts +0 -87
- package/src/cli/analyze-parser.ts +0 -291
- package/src/cli/analyze.ts +0 -352
- package/src/cli/config-descriptions.ts +0 -218
- package/src/cli/config-diff.ts +0 -103
- package/src/cli/config-display.ts +0 -285
- package/src/cli/config-get.ts +0 -55
- package/src/cli/config.ts +0 -14
- package/src/cli/constitution.ts +0 -17
- package/src/cli/diagnose-analysis.ts +0 -159
- package/src/cli/diagnose-formatter.ts +0 -87
- package/src/cli/diagnose.ts +0 -203
- package/src/cli/generate.ts +0 -250
- package/src/cli/index.ts +0 -42
- package/src/cli/init-context.ts +0 -405
- package/src/cli/init-detect.ts +0 -303
- package/src/cli/init.ts +0 -296
- package/src/cli/interact.ts +0 -295
- package/src/cli/plan.ts +0 -509
- package/src/cli/plugins.ts +0 -122
- package/src/cli/prompts-export.ts +0 -58
- package/src/cli/prompts-init.ts +0 -200
- package/src/cli/prompts-main.ts +0 -183
- package/src/cli/prompts-shared.ts +0 -70
- package/src/cli/prompts-tdd.ts +0 -88
- package/src/cli/prompts.ts +0 -17
- package/src/cli/runs.ts +0 -174
- package/src/cli/status-cost.ts +0 -151
- package/src/cli/status-features.ts +0 -405
- package/src/cli/status.ts +0 -13
- package/src/commands/common.ts +0 -171
- package/src/commands/diagnose.ts +0 -17
- package/src/commands/index.ts +0 -9
- package/src/commands/logs-formatter.ts +0 -201
- package/src/commands/logs-reader.ts +0 -171
- package/src/commands/logs.ts +0 -103
- package/src/commands/precheck.ts +0 -86
- package/src/commands/runs.ts +0 -220
- package/src/commands/unlock.ts +0 -96
- package/src/config/defaults.ts +0 -217
- package/src/config/index.ts +0 -22
- package/src/config/loader.ts +0 -143
- package/src/config/merge.ts +0 -106
- package/src/config/merger.ts +0 -147
- package/src/config/path-security.ts +0 -121
- package/src/config/paths.ts +0 -27
- package/src/config/permissions.ts +0 -63
- package/src/config/runtime-types.ts +0 -520
- package/src/config/schema-types.ts +0 -53
- package/src/config/schema.ts +0 -60
- package/src/config/schemas.ts +0 -425
- package/src/config/test-strategy.ts +0 -71
- package/src/config/types.ts +0 -57
- package/src/config/validate.ts +0 -103
- package/src/constitution/generator.ts +0 -158
- package/src/constitution/generators/aider.ts +0 -41
- package/src/constitution/generators/claude.ts +0 -35
- package/src/constitution/generators/cursor.ts +0 -36
- package/src/constitution/generators/opencode.ts +0 -38
- package/src/constitution/generators/types.ts +0 -33
- package/src/constitution/generators/windsurf.ts +0 -36
- package/src/constitution/index.ts +0 -11
- package/src/constitution/loader.ts +0 -121
- package/src/constitution/types.ts +0 -31
- package/src/context/auto-detect.ts +0 -228
- package/src/context/builder.ts +0 -299
- package/src/context/elements.ts +0 -122
- package/src/context/formatter.ts +0 -107
- package/src/context/generator.ts +0 -343
- package/src/context/generators/aider.ts +0 -34
- package/src/context/generators/claude.ts +0 -28
- package/src/context/generators/codex.ts +0 -28
- package/src/context/generators/cursor.ts +0 -28
- package/src/context/generators/gemini.ts +0 -28
- package/src/context/generators/opencode.ts +0 -30
- package/src/context/generators/windsurf.ts +0 -28
- package/src/context/greenfield.ts +0 -114
- package/src/context/index.ts +0 -34
- package/src/context/injector.ts +0 -279
- package/src/context/parent-context.ts +0 -39
- package/src/context/test-scanner.ts +0 -370
- package/src/context/types.ts +0 -98
- package/src/decompose/apply.ts +0 -50
- package/src/decompose/builder.ts +0 -181
- package/src/decompose/index.ts +0 -8
- package/src/decompose/sections/codebase.ts +0 -26
- package/src/decompose/sections/constraints.ts +0 -32
- package/src/decompose/sections/index.ts +0 -4
- package/src/decompose/sections/sibling-stories.ts +0 -25
- package/src/decompose/sections/target-story.ts +0 -31
- package/src/decompose/types.ts +0 -55
- package/src/decompose/validators/complexity.ts +0 -45
- package/src/decompose/validators/coverage.ts +0 -134
- package/src/decompose/validators/dependency.ts +0 -91
- package/src/decompose/validators/index.ts +0 -35
- package/src/decompose/validators/overlap.ts +0 -128
- package/src/errors.ts +0 -67
- package/src/execution/batching.ts +0 -157
- package/src/execution/crash-heartbeat.ts +0 -77
- package/src/execution/crash-recovery.ts +0 -79
- package/src/execution/crash-signals.ts +0 -165
- package/src/execution/crash-writer.ts +0 -154
- package/src/execution/deferred-review.ts +0 -105
- package/src/execution/dry-run.ts +0 -81
- package/src/execution/escalation/escalation.ts +0 -46
- package/src/execution/escalation/index.ts +0 -13
- package/src/execution/escalation/tier-escalation.ts +0 -346
- package/src/execution/escalation/tier-outcome.ts +0 -143
- package/src/execution/executor-types.ts +0 -73
- package/src/execution/helpers.ts +0 -38
- package/src/execution/index.ts +0 -27
- package/src/execution/iteration-runner.ts +0 -160
- package/src/execution/lifecycle/acceptance-loop.ts +0 -280
- package/src/execution/lifecycle/headless-formatter.ts +0 -83
- package/src/execution/lifecycle/index.ts +0 -11
- package/src/execution/lifecycle/parallel-lifecycle.ts +0 -101
- package/src/execution/lifecycle/precheck-runner.ts +0 -140
- package/src/execution/lifecycle/run-cleanup.ts +0 -81
- package/src/execution/lifecycle/run-completion.ts +0 -247
- package/src/execution/lifecycle/run-initialization.ts +0 -187
- package/src/execution/lifecycle/run-regression.ts +0 -305
- package/src/execution/lifecycle/run-setup.ts +0 -240
- package/src/execution/lifecycle/story-size-prompts.ts +0 -123
- package/src/execution/lock.ts +0 -129
- package/src/execution/parallel-coordinator.ts +0 -281
- package/src/execution/parallel-executor-rectification-pass.ts +0 -117
- package/src/execution/parallel-executor-rectify.ts +0 -136
- package/src/execution/parallel-executor.ts +0 -330
- package/src/execution/parallel-worker.ts +0 -149
- package/src/execution/parallel.ts +0 -13
- package/src/execution/pid-registry.ts +0 -275
- package/src/execution/pipeline-result-handler.ts +0 -221
- package/src/execution/progress.ts +0 -27
- package/src/execution/queue-handler.ts +0 -109
- package/src/execution/runner-completion.ts +0 -171
- package/src/execution/runner-execution.ts +0 -243
- package/src/execution/runner-setup.ts +0 -86
- package/src/execution/runner.ts +0 -265
- package/src/execution/sequential-executor.ts +0 -219
- package/src/execution/status-file.ts +0 -264
- package/src/execution/status-writer.ts +0 -181
- package/src/execution/story-context.ts +0 -266
- package/src/execution/story-selector.ts +0 -76
- package/src/execution/test-output-parser.ts +0 -14
- package/src/execution/timeout-handler.ts +0 -100
- package/src/hooks/index.ts +0 -2
- package/src/hooks/runner.ts +0 -280
- package/src/hooks/types.ts +0 -79
- package/src/interaction/chain.ts +0 -170
- package/src/interaction/index.ts +0 -61
- package/src/interaction/init.ts +0 -84
- package/src/interaction/plugins/auto.ts +0 -243
- package/src/interaction/plugins/cli.ts +0 -300
- package/src/interaction/plugins/telegram.ts +0 -384
- package/src/interaction/plugins/webhook.ts +0 -286
- package/src/interaction/state.ts +0 -171
- package/src/interaction/triggers.ts +0 -250
- package/src/interaction/types.ts +0 -170
- package/src/logger/formatters.ts +0 -84
- package/src/logger/index.ts +0 -16
- package/src/logger/logger.ts +0 -296
- package/src/logger/types.ts +0 -48
- package/src/logging/formatter.ts +0 -355
- package/src/logging/index.ts +0 -22
- package/src/logging/types.ts +0 -93
- package/src/metrics/aggregator.ts +0 -191
- package/src/metrics/index.ts +0 -14
- package/src/metrics/tracker.ts +0 -200
- package/src/metrics/types.ts +0 -115
- package/src/optimizer/index.ts +0 -63
- package/src/optimizer/noop.optimizer.ts +0 -24
- package/src/optimizer/rule-based.optimizer.ts +0 -248
- package/src/optimizer/types.ts +0 -53
- package/src/pipeline/event-bus.ts +0 -297
- package/src/pipeline/events.ts +0 -130
- package/src/pipeline/index.ts +0 -19
- package/src/pipeline/runner.ts +0 -149
- package/src/pipeline/stages/acceptance-setup.ts +0 -140
- package/src/pipeline/stages/acceptance.ts +0 -215
- package/src/pipeline/stages/autofix.ts +0 -262
- package/src/pipeline/stages/completion.ts +0 -110
- package/src/pipeline/stages/constitution.ts +0 -63
- package/src/pipeline/stages/context.ts +0 -122
- package/src/pipeline/stages/execution.ts +0 -359
- package/src/pipeline/stages/index.ts +0 -86
- package/src/pipeline/stages/optimizer.ts +0 -74
- package/src/pipeline/stages/prompt.ts +0 -79
- package/src/pipeline/stages/queue-check.ts +0 -103
- package/src/pipeline/stages/rectify.ts +0 -101
- package/src/pipeline/stages/regression.ts +0 -99
- package/src/pipeline/stages/review.ts +0 -94
- package/src/pipeline/stages/routing.ts +0 -276
- package/src/pipeline/stages/verify.ts +0 -286
- package/src/pipeline/subscribers/events-writer.ts +0 -135
- package/src/pipeline/subscribers/hooks.ts +0 -179
- package/src/pipeline/subscribers/interaction.ts +0 -103
- package/src/pipeline/subscribers/registry.ts +0 -73
- package/src/pipeline/subscribers/reporters.ts +0 -174
- package/src/pipeline/types.ts +0 -220
- package/src/plugins/extensions.ts +0 -225
- package/src/plugins/index.ts +0 -33
- package/src/plugins/loader.ts +0 -352
- package/src/plugins/plugin-logger.ts +0 -41
- package/src/plugins/registry.ts +0 -168
- package/src/plugins/types.ts +0 -206
- package/src/plugins/validator.ts +0 -352
- package/src/prd/index.ts +0 -220
- package/src/prd/schema.ts +0 -268
- package/src/prd/types.ts +0 -273
- package/src/prd/validate.ts +0 -41
- package/src/precheck/checks-agents.ts +0 -63
- package/src/precheck/checks-blockers.ts +0 -23
- package/src/precheck/checks-cli.ts +0 -68
- package/src/precheck/checks-config.ts +0 -102
- package/src/precheck/checks-git.ts +0 -117
- package/src/precheck/checks-system.ts +0 -101
- package/src/precheck/checks-warnings.ts +0 -221
- package/src/precheck/checks.ts +0 -36
- package/src/precheck/index.ts +0 -374
- package/src/precheck/story-size-gate.ts +0 -144
- package/src/precheck/types.ts +0 -31
- package/src/prompts/builder.ts +0 -166
- package/src/prompts/index.ts +0 -2
- package/src/prompts/loader.ts +0 -43
- package/src/prompts/sections/conventions.ts +0 -19
- package/src/prompts/sections/hermetic.ts +0 -41
- package/src/prompts/sections/index.ts +0 -12
- package/src/prompts/sections/isolation.ts +0 -70
- package/src/prompts/sections/role-task.ts +0 -182
- package/src/prompts/sections/story.ts +0 -55
- package/src/prompts/sections/verdict.ts +0 -70
- package/src/prompts/types.ts +0 -21
- package/src/queue/index.ts +0 -2
- package/src/queue/manager.ts +0 -254
- package/src/queue/types.ts +0 -54
- package/src/review/index.ts +0 -8
- package/src/review/orchestrator.ts +0 -154
- package/src/review/runner.ts +0 -303
- package/src/review/types.ts +0 -70
- package/src/routing/batch-route.ts +0 -35
- package/src/routing/builder.ts +0 -81
- package/src/routing/chain.ts +0 -75
- package/src/routing/content-hash.ts +0 -25
- package/src/routing/index.ts +0 -20
- package/src/routing/loader.ts +0 -62
- package/src/routing/router.ts +0 -305
- package/src/routing/strategies/adaptive.ts +0 -215
- package/src/routing/strategies/index.ts +0 -8
- package/src/routing/strategies/keyword.ts +0 -180
- package/src/routing/strategies/llm-prompts.ts +0 -224
- package/src/routing/strategies/llm.ts +0 -320
- package/src/routing/strategies/manual.ts +0 -50
- package/src/routing/strategy.ts +0 -102
- package/src/tdd/cleanup.ts +0 -120
- package/src/tdd/index.ts +0 -22
- package/src/tdd/isolation.ts +0 -117
- package/src/tdd/orchestrator.ts +0 -406
- package/src/tdd/prompts.ts +0 -40
- package/src/tdd/rectification-gate.ts +0 -274
- package/src/tdd/session-runner.ts +0 -263
- package/src/tdd/types.ts +0 -84
- package/src/tdd/verdict-reader.ts +0 -266
- package/src/tdd/verdict.ts +0 -152
- package/src/tui/App.tsx +0 -265
- package/src/tui/components/AgentPanel.tsx +0 -75
- package/src/tui/components/CostOverlay.tsx +0 -118
- package/src/tui/components/HelpOverlay.tsx +0 -107
- package/src/tui/components/StatusBar.tsx +0 -63
- package/src/tui/components/StoriesPanel.tsx +0 -177
- package/src/tui/hooks/useKeyboard.ts +0 -142
- package/src/tui/hooks/useLayout.ts +0 -137
- package/src/tui/hooks/usePipelineEvents.ts +0 -183
- package/src/tui/hooks/usePty.ts +0 -189
- package/src/tui/index.tsx +0 -38
- package/src/tui/types.ts +0 -76
- package/src/utils/errors.ts +0 -12
- package/src/utils/git.ts +0 -245
- package/src/utils/json-file.ts +0 -72
- package/src/utils/log-test-output.ts +0 -25
- package/src/utils/path-security.ts +0 -73
- package/src/utils/queue-writer.ts +0 -54
- package/src/verification/crash-detector.ts +0 -34
- package/src/verification/executor.ts +0 -250
- package/src/verification/index.ts +0 -12
- package/src/verification/orchestrator-types.ts +0 -154
- package/src/verification/orchestrator.ts +0 -76
- package/src/verification/parser.ts +0 -220
- package/src/verification/rectification-loop.ts +0 -172
- package/src/verification/rectification.ts +0 -108
- package/src/verification/runners.ts +0 -129
- package/src/verification/smart-runner.ts +0 -307
- package/src/verification/strategies/acceptance.ts +0 -136
- package/src/verification/strategies/regression.ts +0 -90
- package/src/verification/strategies/scoped.ts +0 -154
- package/src/verification/types.ts +0 -117
- package/src/version.ts +0 -40
- package/src/worktree/dispatcher.ts +0 -6
- package/src/worktree/index.ts +0 -2
- package/src/worktree/manager.ts +0 -193
- package/src/worktree/merge.ts +0 -302
- package/src/worktree/types.ts +0 -4
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Routing Stage
|
|
3
|
-
*
|
|
4
|
-
* Classifies story complexity and determines model tier + test strategy.
|
|
5
|
-
* Uses cached complexity/testStrategy/modelTier from story if contentHash matches.
|
|
6
|
-
* modelTier: uses escalated tier if explicitly set (BUG-032), otherwise derives from config.
|
|
7
|
-
*
|
|
8
|
-
* RRP-003: contentHash staleness detection — if story.routing.contentHash is missing or
|
|
9
|
-
* does not match the current story content, treats cached routing as a miss and re-classifies.
|
|
10
|
-
*
|
|
11
|
-
* SD-004: Oversized story detection — after routing, checks if story exceeds
|
|
12
|
-
* config.decompose.maxAcceptanceCriteria with complex/expert complexity. Decomposes
|
|
13
|
-
* based on trigger mode (auto / confirm / disabled).
|
|
14
|
-
*
|
|
15
|
-
* @returns
|
|
16
|
-
* - `continue`: Routing determined, proceed to next stage
|
|
17
|
-
* - `skip`: Story was decomposed into substories; runner should pick up first substory
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```ts
|
|
21
|
-
* // Story has cached routing with matching contentHash
|
|
22
|
-
* await routingStage.execute(ctx);
|
|
23
|
-
* // ctx.routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "..." }
|
|
24
|
-
* // modelTier is derived from current config.autoMode.complexityRouting
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { join } from "node:path";
|
|
29
|
-
import { getAgent } from "../../agents/registry";
|
|
30
|
-
import type { NaxConfig } from "../../config";
|
|
31
|
-
import { isGreenfieldStory } from "../../context/greenfield";
|
|
32
|
-
import { applyDecomposition } from "../../decompose/apply";
|
|
33
|
-
import { DecomposeBuilder } from "../../decompose/builder";
|
|
34
|
-
import type { DecomposeConfig as BuilderDecomposeConfig, DecomposeResult } from "../../decompose/types";
|
|
35
|
-
import { checkStoryOversized } from "../../interaction/triggers";
|
|
36
|
-
import { getLogger } from "../../logger";
|
|
37
|
-
import { savePRD } from "../../prd";
|
|
38
|
-
import type { PRD, UserStory } from "../../prd";
|
|
39
|
-
import { complexityToModelTier, computeStoryContentHash, routeStory } from "../../routing";
|
|
40
|
-
import { clearCache, routeBatch } from "../../routing/strategies/llm";
|
|
41
|
-
import type { PipelineContext, PipelineStage, RoutingResult, StageResult } from "../types";
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Run story decomposition using DecomposeBuilder.
|
|
45
|
-
* Used as the default implementation in _routingDeps.runDecompose.
|
|
46
|
-
* In production, replace with an LLM-backed adapter.
|
|
47
|
-
*/
|
|
48
|
-
async function runDecompose(
|
|
49
|
-
story: UserStory,
|
|
50
|
-
prd: PRD,
|
|
51
|
-
config: NaxConfig,
|
|
52
|
-
_workdir: string,
|
|
53
|
-
agentGetFn?: import("../types").AgentGetFn,
|
|
54
|
-
): Promise<DecomposeResult> {
|
|
55
|
-
const naxDecompose = config.decompose;
|
|
56
|
-
const builderConfig: BuilderDecomposeConfig = {
|
|
57
|
-
maxSubStories: naxDecompose?.maxSubstories ?? 5,
|
|
58
|
-
maxComplexity: naxDecompose?.maxSubstoryComplexity ?? "medium",
|
|
59
|
-
maxRetries: naxDecompose?.maxRetries ?? 2,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Resolve the default agent adapter for LLM-backed decompose.
|
|
63
|
-
// Falls back to agent.complete() with JSON mode — works with both CLI and ACP adapters.
|
|
64
|
-
const agent = (agentGetFn ?? getAgent)(config.autoMode.defaultAgent);
|
|
65
|
-
if (!agent) {
|
|
66
|
-
throw new Error(`[decompose] Agent "${config.autoMode.defaultAgent}" not found — cannot decompose`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Resolve decompose model: config.decompose.model tier → actual model string
|
|
70
|
-
const decomposeTier = naxDecompose?.model ?? "balanced";
|
|
71
|
-
let decomposeModel: string | undefined;
|
|
72
|
-
try {
|
|
73
|
-
const { resolveModel } = await import("../../config/schema");
|
|
74
|
-
const models = config.models as Record<string, unknown>;
|
|
75
|
-
const entry = models[decomposeTier] ?? models.balanced;
|
|
76
|
-
if (entry) decomposeModel = resolveModel(entry as Parameters<typeof resolveModel>[0]).model;
|
|
77
|
-
} catch {
|
|
78
|
-
// resolveModel can throw on malformed entries — fall through to let complete() handle it
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const storySessionName = `nax-decompose-${story.id.toLowerCase()}`;
|
|
82
|
-
const adapter = {
|
|
83
|
-
async decompose(prompt: string): Promise<string> {
|
|
84
|
-
return agent.complete(prompt, {
|
|
85
|
-
model: decomposeModel,
|
|
86
|
-
jsonMode: true,
|
|
87
|
-
config,
|
|
88
|
-
sessionName: storySessionName,
|
|
89
|
-
});
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export const routingStage: PipelineStage = {
|
|
97
|
-
name: "routing",
|
|
98
|
-
enabled: () => true,
|
|
99
|
-
|
|
100
|
-
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
101
|
-
const logger = getLogger();
|
|
102
|
-
|
|
103
|
-
// Resolve agent adapter for LLM routing (shared with execution)
|
|
104
|
-
const agentName = ctx.config.execution?.agent ?? "claude";
|
|
105
|
-
const adapter = (ctx.agentGetFn ?? _routingDeps.getAgent)(agentName);
|
|
106
|
-
|
|
107
|
-
// Staleness detection (RRP-003):
|
|
108
|
-
// - story.routing absent → cache miss (no prior routing)
|
|
109
|
-
// - story.routing + no contentHash → legacy cache hit (manual / pre-RRP-003 routing, honor as-is)
|
|
110
|
-
// - story.routing + contentHash matches → cache hit
|
|
111
|
-
// - story.routing + contentHash mismatches → cache miss (stale, re-classify)
|
|
112
|
-
const hasExistingRouting = ctx.story.routing !== undefined;
|
|
113
|
-
const hasContentHash = ctx.story.routing?.contentHash !== undefined;
|
|
114
|
-
let currentHash: string | undefined;
|
|
115
|
-
let hashMatch = false;
|
|
116
|
-
if (hasContentHash) {
|
|
117
|
-
currentHash = _routingDeps.computeStoryContentHash(ctx.story);
|
|
118
|
-
hashMatch = ctx.story.routing?.contentHash === currentHash;
|
|
119
|
-
}
|
|
120
|
-
const isCacheHit = hasExistingRouting && (!hasContentHash || hashMatch);
|
|
121
|
-
|
|
122
|
-
let routing: { complexity: string; testStrategy: string; modelTier: string; reasoning?: string };
|
|
123
|
-
|
|
124
|
-
if (isCacheHit) {
|
|
125
|
-
// Cache hit: legacy routing (no contentHash) or matching contentHash — use cached values
|
|
126
|
-
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config, adapter }, ctx.workdir, ctx.plugins);
|
|
127
|
-
// Override with cached values only when they are actually set
|
|
128
|
-
if (ctx.story.routing?.complexity) routing.complexity = ctx.story.routing.complexity;
|
|
129
|
-
// BUG-062: Only honor stored testStrategy for legacy/manual routing (no contentHash).
|
|
130
|
-
// When contentHash exists, the LLM strategy layer already recomputes testStrategy
|
|
131
|
-
// fresh via determineTestStrategy() — don't clobber it with the stale PRD value.
|
|
132
|
-
if (!hasContentHash && ctx.story.routing?.testStrategy) routing.testStrategy = ctx.story.routing.testStrategy;
|
|
133
|
-
// BUG-032: Use escalated modelTier if explicitly set (by handleTierEscalation),
|
|
134
|
-
// otherwise derive from complexity + current config
|
|
135
|
-
if (ctx.story.routing?.modelTier) {
|
|
136
|
-
routing.modelTier = ctx.story.routing.modelTier;
|
|
137
|
-
} else {
|
|
138
|
-
routing.modelTier = _routingDeps.complexityToModelTier(
|
|
139
|
-
routing.complexity as import("../../config").Complexity,
|
|
140
|
-
ctx.config,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
// Cache miss: no routing, or contentHash present but mismatched — fresh classification
|
|
145
|
-
routing = await _routingDeps.routeStory(ctx.story, { config: ctx.config, adapter }, ctx.workdir, ctx.plugins);
|
|
146
|
-
// currentHash already computed if a mismatch was detected; compute now if starting fresh
|
|
147
|
-
currentHash = currentHash ?? _routingDeps.computeStoryContentHash(ctx.story);
|
|
148
|
-
ctx.story.routing = {
|
|
149
|
-
...(ctx.story.routing ?? {}),
|
|
150
|
-
complexity: routing.complexity as import("../../config").Complexity,
|
|
151
|
-
initialComplexity:
|
|
152
|
-
ctx.story.routing?.initialComplexity ?? (routing.complexity as import("../../config").Complexity),
|
|
153
|
-
testStrategy: routing.testStrategy as import("../../config").TestStrategy,
|
|
154
|
-
reasoning: routing.reasoning ?? "",
|
|
155
|
-
contentHash: currentHash,
|
|
156
|
-
};
|
|
157
|
-
if (ctx.prdPath) {
|
|
158
|
-
await _routingDeps.savePRD(ctx.prd, ctx.prdPath);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// BUG-010: Greenfield detection — force test-after if no test files exist
|
|
163
|
-
// MW-011: For monorepo stories, scan the story's package workdir (story.workdir), not the
|
|
164
|
-
// repo root. Scanning the repo root would find tests in OTHER packages and incorrectly
|
|
165
|
-
// classify the story as non-greenfield even when the target package has zero tests.
|
|
166
|
-
const greenfieldDetectionEnabled = ctx.config.tdd.greenfieldDetection ?? true;
|
|
167
|
-
if (greenfieldDetectionEnabled && routing.testStrategy.startsWith("three-session-tdd")) {
|
|
168
|
-
const greenfieldScanDir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : ctx.workdir;
|
|
169
|
-
const isGreenfield = await _routingDeps.isGreenfieldStory(ctx.story, greenfieldScanDir);
|
|
170
|
-
if (isGreenfield) {
|
|
171
|
-
logger.info("routing", "Greenfield detected — forcing test-after strategy", {
|
|
172
|
-
storyId: ctx.story.id,
|
|
173
|
-
originalStrategy: routing.testStrategy,
|
|
174
|
-
scanDir: greenfieldScanDir,
|
|
175
|
-
});
|
|
176
|
-
routing.testStrategy = "test-after";
|
|
177
|
-
routing.reasoning = `${routing.reasoning} [GREENFIELD OVERRIDE: No test files exist, using test-after instead of TDD]`;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Set ctx.routing after all overrides are applied
|
|
182
|
-
ctx.routing = routing as RoutingResult;
|
|
183
|
-
|
|
184
|
-
const isBatch = ctx.stories.length > 1;
|
|
185
|
-
|
|
186
|
-
logger.debug("routing", "Task classified", {
|
|
187
|
-
complexity: ctx.routing.complexity,
|
|
188
|
-
modelTier: ctx.routing.modelTier,
|
|
189
|
-
testStrategy: ctx.routing.testStrategy,
|
|
190
|
-
storyId: ctx.story.id,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
if (!isBatch) {
|
|
194
|
-
logger.debug("routing", ctx.routing.reasoning);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// SD-004: Oversized story detection and decomposition
|
|
198
|
-
const decomposeConfig = ctx.config.decompose;
|
|
199
|
-
if (decomposeConfig && ctx.story.status !== "decomposed") {
|
|
200
|
-
const acCount = ctx.story.acceptanceCriteria.length;
|
|
201
|
-
const complexity = ctx.routing.complexity;
|
|
202
|
-
const isOversized =
|
|
203
|
-
acCount > decomposeConfig.maxAcceptanceCriteria && (complexity === "complex" || complexity === "expert");
|
|
204
|
-
|
|
205
|
-
if (isOversized) {
|
|
206
|
-
if (decomposeConfig.trigger === "disabled") {
|
|
207
|
-
logger.warn(
|
|
208
|
-
"routing",
|
|
209
|
-
`Story ${ctx.story.id} is oversized (${acCount} ACs) but decompose is disabled — continuing with original`,
|
|
210
|
-
);
|
|
211
|
-
} else if (decomposeConfig.trigger === "auto") {
|
|
212
|
-
const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir, ctx.agentGetFn);
|
|
213
|
-
if (result.validation.valid) {
|
|
214
|
-
_routingDeps.applyDecomposition(ctx.prd, result);
|
|
215
|
-
if (ctx.prdPath) {
|
|
216
|
-
await _routingDeps.savePRD(ctx.prd, ctx.prdPath);
|
|
217
|
-
}
|
|
218
|
-
logger.info("routing", `Story ${ctx.story.id} decomposed into ${result.subStories.length} substories`);
|
|
219
|
-
return {
|
|
220
|
-
action: "decomposed",
|
|
221
|
-
reason: `Decomposed into ${result.subStories.length} substories`,
|
|
222
|
-
subStoryCount: result.subStories.length,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
logger.warn("routing", `Story ${ctx.story.id} decompose failed after retries — continuing with original`, {
|
|
226
|
-
errors: result.validation.errors,
|
|
227
|
-
});
|
|
228
|
-
} else if (decomposeConfig.trigger === "confirm") {
|
|
229
|
-
const action = await _routingDeps.checkStoryOversized(
|
|
230
|
-
{ featureName: ctx.prd.feature, storyId: ctx.story.id, criteriaCount: acCount },
|
|
231
|
-
ctx.config,
|
|
232
|
-
// biome-ignore lint/style/noNonNullAssertion: confirm mode is only reached when interaction chain is present in production; tests mock checkStoryOversized directly
|
|
233
|
-
ctx.interaction!,
|
|
234
|
-
);
|
|
235
|
-
if (action === "decompose") {
|
|
236
|
-
const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir, ctx.agentGetFn);
|
|
237
|
-
if (result.validation.valid) {
|
|
238
|
-
_routingDeps.applyDecomposition(ctx.prd, result);
|
|
239
|
-
if (ctx.prdPath) {
|
|
240
|
-
await _routingDeps.savePRD(ctx.prd, ctx.prdPath);
|
|
241
|
-
}
|
|
242
|
-
logger.info("routing", `Story ${ctx.story.id} decomposed into ${result.subStories.length} substories`);
|
|
243
|
-
return {
|
|
244
|
-
action: "decomposed",
|
|
245
|
-
reason: `Decomposed into ${result.subStories.length} substories`,
|
|
246
|
-
subStoryCount: result.subStories.length,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
logger.warn("routing", `Story ${ctx.story.id} decompose failed after retries — continuing with original`, {
|
|
250
|
-
errors: result.validation.errors,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return { action: "continue" };
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Swappable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
263
|
-
* Tests can override individual functions without poisoning the module registry.
|
|
264
|
-
*/
|
|
265
|
-
export const _routingDeps = {
|
|
266
|
-
routeStory,
|
|
267
|
-
complexityToModelTier,
|
|
268
|
-
isGreenfieldStory,
|
|
269
|
-
clearCache,
|
|
270
|
-
savePRD,
|
|
271
|
-
computeStoryContentHash,
|
|
272
|
-
applyDecomposition,
|
|
273
|
-
runDecompose,
|
|
274
|
-
checkStoryOversized,
|
|
275
|
-
getAgent,
|
|
276
|
-
};
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verify Stage
|
|
3
|
-
*
|
|
4
|
-
* Verifies the agent's work meets basic requirements by running tests.
|
|
5
|
-
* This is a lightweight verification before the full review stage.
|
|
6
|
-
*
|
|
7
|
-
* @returns
|
|
8
|
-
* - `continue`: Tests passed, OR TEST_FAILURE (ctx.verifyResult.success===false → rectifyStage handles it)
|
|
9
|
-
* - `escalate`: TIMEOUT or RUNTIME_CRASH (structural — rectify can't fix these)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { basename, join } from "node:path";
|
|
13
|
-
import type { SmartTestRunnerConfig } from "../../config/types";
|
|
14
|
-
import { getLogger } from "../../logger";
|
|
15
|
-
import { logTestOutput } from "../../utils/log-test-output";
|
|
16
|
-
import { detectRuntimeCrash } from "../../verification/crash-detector";
|
|
17
|
-
import type { VerifyStatus } from "../../verification/orchestrator-types";
|
|
18
|
-
import { regression } from "../../verification/runners";
|
|
19
|
-
import { _smartRunnerDeps } from "../../verification/smart-runner";
|
|
20
|
-
import { isMonorepoOrchestratorCommand } from "../../verification/strategies/scoped";
|
|
21
|
-
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
22
|
-
|
|
23
|
-
const DEFAULT_SMART_RUNNER_CONFIG: SmartTestRunnerConfig = {
|
|
24
|
-
enabled: true,
|
|
25
|
-
testFilePatterns: ["test/**/*.test.ts"],
|
|
26
|
-
fallback: "import-grep",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Coerces boolean or partial config into a full SmartTestRunnerConfig
|
|
31
|
-
*/
|
|
32
|
-
function coerceSmartTestRunner(val: boolean | SmartTestRunnerConfig | undefined): SmartTestRunnerConfig {
|
|
33
|
-
if (val === undefined || val === true) return DEFAULT_SMART_RUNNER_CONFIG;
|
|
34
|
-
if (val === false) return { ...DEFAULT_SMART_RUNNER_CONFIG, enabled: false };
|
|
35
|
-
return val;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Build the scoped test command from discovered test files.
|
|
40
|
-
* Uses the testScoped template (with {{files}} placeholder) if configured,
|
|
41
|
-
* otherwise falls back to buildSmartTestCommand heuristic.
|
|
42
|
-
*/
|
|
43
|
-
function buildScopedCommand(testFiles: string[], baseCommand: string, testScopedTemplate?: string): string {
|
|
44
|
-
if (testScopedTemplate) {
|
|
45
|
-
return testScopedTemplate.replace("{{files}}", testFiles.join(" "));
|
|
46
|
-
}
|
|
47
|
-
return _smartRunnerDeps.buildSmartTestCommand(testFiles, baseCommand);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Read the npm package name from <dir>/package.json.
|
|
52
|
-
* Returns null if not found or file has no name field.
|
|
53
|
-
*/
|
|
54
|
-
async function readPackageName(dir: string): Promise<string | null> {
|
|
55
|
-
try {
|
|
56
|
-
const content = await Bun.file(join(dir, "package.json")).json();
|
|
57
|
-
return typeof content.name === "string" ? content.name : null;
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Substitute {{package}} placeholder in a testScoped template.
|
|
65
|
-
*
|
|
66
|
-
* Reads the npm package name from <packageDir>/package.json.
|
|
67
|
-
* Returns null when package.json is absent or has no name field — callers
|
|
68
|
-
* should skip the template entirely in that case (non-JS/non-Node projects
|
|
69
|
-
* have no package identity to inject, so don't fall back to a dir name guess).
|
|
70
|
-
*
|
|
71
|
-
* @param template - Template string (e.g. "bunx turbo test --filter={{package}}")
|
|
72
|
-
* @param packageDir - Absolute path to the package directory
|
|
73
|
-
* @returns Resolved template, or null if {{package}} cannot be resolved
|
|
74
|
-
*/
|
|
75
|
-
async function resolvePackageTemplate(template: string, packageDir: string): Promise<string | null> {
|
|
76
|
-
if (!template.includes("{{package}}")) return template;
|
|
77
|
-
const name = await _verifyDeps.readPackageName(packageDir);
|
|
78
|
-
if (name === null) {
|
|
79
|
-
// No package.json or no name field — skip template, can't resolve {{package}}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
return template.replaceAll("{{package}}", name);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export const verifyStage: PipelineStage = {
|
|
86
|
-
name: "verify",
|
|
87
|
-
enabled: (ctx: PipelineContext) => !ctx.fullSuiteGatePassed,
|
|
88
|
-
skipReason: () => "not needed (full-suite gate already passed)",
|
|
89
|
-
|
|
90
|
-
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
91
|
-
const logger = getLogger();
|
|
92
|
-
|
|
93
|
-
// PKG-003: use centrally resolved effective config (set once per story in iteration-runner)
|
|
94
|
-
// Falls back to ctx.config for contexts that predate PKG-003 (e.g., acceptance-loop)
|
|
95
|
-
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
96
|
-
|
|
97
|
-
// Skip verification if tests are not required
|
|
98
|
-
if (!effectiveConfig.quality.requireTests) {
|
|
99
|
-
logger.debug("verify", "Skipping verification (quality.requireTests = false)", { storyId: ctx.story.id });
|
|
100
|
-
return { action: "continue" };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Skip verification if no test command is configured
|
|
104
|
-
const testCommand = effectiveConfig.review?.commands?.test ?? effectiveConfig.quality.commands.test;
|
|
105
|
-
const testScopedTemplate = effectiveConfig.quality.commands.testScoped;
|
|
106
|
-
if (!testCommand) {
|
|
107
|
-
logger.debug("verify", "Skipping verification (no test command configured)", { storyId: ctx.story.id });
|
|
108
|
-
return { action: "continue" };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
logger.info("verify", "Running verification", { storyId: ctx.story.id });
|
|
112
|
-
|
|
113
|
-
// MW-006: resolve effective workdir for test execution
|
|
114
|
-
const effectiveWorkdir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : ctx.workdir;
|
|
115
|
-
|
|
116
|
-
// Determine effective test command (smart runner or full suite)
|
|
117
|
-
let effectiveCommand = testCommand;
|
|
118
|
-
let isFullSuite = true;
|
|
119
|
-
const smartRunnerConfig = coerceSmartTestRunner(effectiveConfig.execution.smartTestRunner);
|
|
120
|
-
const regressionMode = effectiveConfig.execution.regressionGate?.mode ?? "deferred";
|
|
121
|
-
|
|
122
|
-
// Resolve {{package}} in testScoped template for monorepo stories.
|
|
123
|
-
// Returns null if package.json is absent (non-JS project) — falls through to smart-runner.
|
|
124
|
-
let resolvedTestScopedTemplate: string | undefined = testScopedTemplate;
|
|
125
|
-
if (testScopedTemplate && ctx.story.workdir) {
|
|
126
|
-
const resolved = await resolvePackageTemplate(testScopedTemplate, effectiveWorkdir);
|
|
127
|
-
resolvedTestScopedTemplate = resolved ?? undefined; // null → skip template
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Monorepo orchestrators (turbo, nx) handle change-aware scoping natively via their own
|
|
131
|
-
// filter syntax. Skip nax's smart runner — appending file paths would produce invalid syntax.
|
|
132
|
-
// Instead, use the testScoped template (with {{package}} resolved) to scope per-story.
|
|
133
|
-
const isMonorepoOrchestrator = isMonorepoOrchestratorCommand(testCommand);
|
|
134
|
-
|
|
135
|
-
if (isMonorepoOrchestrator) {
|
|
136
|
-
if (resolvedTestScopedTemplate && ctx.story.workdir) {
|
|
137
|
-
// Use the resolved scoped template (e.g. "bunx turbo test --filter=@koda/cli")
|
|
138
|
-
effectiveCommand = resolvedTestScopedTemplate;
|
|
139
|
-
isFullSuite = false;
|
|
140
|
-
logger.info("verify", "Monorepo orchestrator — using testScoped template", {
|
|
141
|
-
storyId: ctx.story.id,
|
|
142
|
-
command: effectiveCommand,
|
|
143
|
-
});
|
|
144
|
-
} else {
|
|
145
|
-
logger.info("verify", "Monorepo orchestrator — running full suite (no package context)", {
|
|
146
|
-
storyId: ctx.story.id,
|
|
147
|
-
command: effectiveCommand,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
} else if (smartRunnerConfig.enabled) {
|
|
151
|
-
// MW-006: pass packagePrefix so git diff is scoped to the package in monorepos
|
|
152
|
-
const sourceFiles = await _smartRunnerDeps.getChangedSourceFiles(
|
|
153
|
-
effectiveWorkdir,
|
|
154
|
-
ctx.storyGitRef,
|
|
155
|
-
ctx.story.workdir,
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// Pass 1: path convention mapping
|
|
159
|
-
const pass1Files = await _smartRunnerDeps.mapSourceToTests(sourceFiles, effectiveWorkdir);
|
|
160
|
-
if (pass1Files.length > 0) {
|
|
161
|
-
logger.info("verify", `[smart-runner] Pass 1: path convention matched ${pass1Files.length} test files`, {
|
|
162
|
-
storyId: ctx.story.id,
|
|
163
|
-
});
|
|
164
|
-
effectiveCommand = buildScopedCommand(pass1Files, testCommand, resolvedTestScopedTemplate);
|
|
165
|
-
isFullSuite = false;
|
|
166
|
-
} else if (smartRunnerConfig.fallback === "import-grep") {
|
|
167
|
-
// Pass 2: import-grep fallback
|
|
168
|
-
const pass2Files = await _smartRunnerDeps.importGrepFallback(
|
|
169
|
-
sourceFiles,
|
|
170
|
-
effectiveWorkdir,
|
|
171
|
-
smartRunnerConfig.testFilePatterns,
|
|
172
|
-
);
|
|
173
|
-
if (pass2Files.length > 0) {
|
|
174
|
-
logger.info("verify", `[smart-runner] Pass 2: import-grep matched ${pass2Files.length} test files`, {
|
|
175
|
-
storyId: ctx.story.id,
|
|
176
|
-
});
|
|
177
|
-
effectiveCommand = buildScopedCommand(pass2Files, testCommand, resolvedTestScopedTemplate);
|
|
178
|
-
isFullSuite = false;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// US-003: If we are falling back to the full suite AND mode is deferred, skip this stage
|
|
184
|
-
// because the deferred regression gate will handle the full suite at run-end.
|
|
185
|
-
if (isFullSuite && regressionMode === "deferred") {
|
|
186
|
-
logger.info("verify", "[smart-runner] No mapped tests — deferring full suite to run-end (mode: deferred)", {
|
|
187
|
-
storyId: ctx.story.id,
|
|
188
|
-
});
|
|
189
|
-
return { action: "continue" };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (isFullSuite) {
|
|
193
|
-
logger.info("verify", "[smart-runner] No mapped tests — falling back to full suite", {
|
|
194
|
-
storyId: ctx.story.id,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// BUG-044: Log the effective command for observability
|
|
199
|
-
logger.info("verify", isFullSuite ? "Running full suite" : "Running scoped tests", {
|
|
200
|
-
storyId: ctx.story.id,
|
|
201
|
-
command: effectiveCommand,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Use unified regression gate (includes 2s wait for agent process cleanup)
|
|
205
|
-
const result = await _verifyDeps.regression({
|
|
206
|
-
workdir: effectiveWorkdir,
|
|
207
|
-
command: effectiveCommand,
|
|
208
|
-
timeoutSeconds: effectiveConfig.execution.verificationTimeoutSeconds,
|
|
209
|
-
acceptOnTimeout: effectiveConfig.execution.regressionGate?.acceptOnTimeout ?? true,
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Store result on context for rectify stage
|
|
213
|
-
ctx.verifyResult = {
|
|
214
|
-
success: result.success,
|
|
215
|
-
status: (result.status === "TIMEOUT"
|
|
216
|
-
? "TIMEOUT"
|
|
217
|
-
: result.success
|
|
218
|
-
? "PASS"
|
|
219
|
-
: detectRuntimeCrash(result.output)
|
|
220
|
-
? "RUNTIME_CRASH"
|
|
221
|
-
: "TEST_FAILURE") as VerifyStatus,
|
|
222
|
-
storyId: ctx.story.id,
|
|
223
|
-
strategy: "scoped",
|
|
224
|
-
passCount: result.passCount ?? 0,
|
|
225
|
-
failCount: result.failCount ?? 0,
|
|
226
|
-
totalCount: (result.passCount ?? 0) + (result.failCount ?? 0),
|
|
227
|
-
failures: [],
|
|
228
|
-
rawOutput: result.output,
|
|
229
|
-
durationMs: 0,
|
|
230
|
-
countsTowardEscalation: result.countsTowardEscalation,
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// HARD FAILURE: Tests must pass for story to be marked complete
|
|
234
|
-
if (!result.success) {
|
|
235
|
-
// BUG-019: Distinguish timeout from actual test failures
|
|
236
|
-
if (result.status === "TIMEOUT") {
|
|
237
|
-
const timeout = effectiveConfig.execution.verificationTimeoutSeconds;
|
|
238
|
-
logger.error(
|
|
239
|
-
"verify",
|
|
240
|
-
`Test suite exceeded timeout (${timeout}s). This is NOT a test failure — consider increasing execution.verificationTimeoutSeconds or scoping tests.`,
|
|
241
|
-
{
|
|
242
|
-
exitCode: result.status,
|
|
243
|
-
storyId: ctx.story.id,
|
|
244
|
-
timeoutSeconds: timeout,
|
|
245
|
-
},
|
|
246
|
-
);
|
|
247
|
-
} else {
|
|
248
|
-
logger.error("verify", "Tests failed", {
|
|
249
|
-
exitCode: result.status,
|
|
250
|
-
storyId: ctx.story.id,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Log tail of output at debug level for context (ENH-001)
|
|
255
|
-
// BUG-037: Use .slice(-20) to show failures, not prechecks
|
|
256
|
-
if (result.status !== "TIMEOUT") {
|
|
257
|
-
logTestOutput(logger, "verify", result.output, { storyId: ctx.story.id });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// RUNTIME_CRASH and TIMEOUT are structural — escalate immediately (rectify can't fix them)
|
|
261
|
-
if (result.status === "TIMEOUT" || detectRuntimeCrash(result.output)) {
|
|
262
|
-
return {
|
|
263
|
-
action: "escalate",
|
|
264
|
-
reason:
|
|
265
|
-
result.status === "TIMEOUT"
|
|
266
|
-
? `Test suite TIMEOUT after ${effectiveConfig.execution.verificationTimeoutSeconds}s (not a code failure)`
|
|
267
|
-
: `Tests failed with runtime crash (exit code ${result.status ?? "non-zero"})`,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// TEST_FAILURE: ctx.verifyResult is set with success:false — rectifyStage handles it next
|
|
272
|
-
return { action: "continue" };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
logger.info("verify", "Tests passed", { storyId: ctx.story.id });
|
|
276
|
-
return { action: "continue" };
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Swappable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
282
|
-
*/
|
|
283
|
-
export const _verifyDeps = {
|
|
284
|
-
regression,
|
|
285
|
-
readPackageName,
|
|
286
|
-
};
|