@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,22 @@
|
|
|
1
|
+
# FEAT-013 — Deprecate test-after from auto routing
|
|
2
|
+
|
|
3
|
+
## Decision
|
|
4
|
+
Remove `test-after` as the default fallback in `auto` mode.
|
|
5
|
+
Change simple/medium stories to route to `three-session-tdd-lite` instead.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
- `test-after` has high test failure rate — tests rubber-stamp implementation rather than driving design
|
|
9
|
+
- Failed test-after stories escalate to powerful tier anyway → costs more in the end
|
|
10
|
+
- `three-session-tdd-lite` = better quality at moderate cost increase
|
|
11
|
+
|
|
12
|
+
## New auto routing decision tree
|
|
13
|
+
```
|
|
14
|
+
security/public-api keywords → three-session-tdd
|
|
15
|
+
complex/expert complexity → three-session-tdd (or lite if ui/cli tags)
|
|
16
|
+
simple/medium (default) → three-session-tdd-lite ← was test-after
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Backward compatibility
|
|
20
|
+
- `tddStrategy: "off"` in config still routes to `test-after` (explicit opt-in)
|
|
21
|
+
- `tddStrategy: "strict"` → three-session-tdd (unchanged)
|
|
22
|
+
- `tddStrategy: "lite"` → three-session-tdd-lite (unchanged)
|
package/nax/config.json
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"iterationDelayMs": 2000,
|
|
61
61
|
"costLimit": 8.0,
|
|
62
62
|
"sessionTimeoutSeconds": 7200,
|
|
63
|
-
"verificationTimeoutSeconds":
|
|
63
|
+
"verificationTimeoutSeconds": 600,
|
|
64
64
|
"maxStoriesPerFeature": 15,
|
|
65
65
|
"rectification": {
|
|
66
66
|
"enabled": true,
|
|
@@ -70,8 +70,11 @@
|
|
|
70
70
|
"abortOnIncreasingFailures": true
|
|
71
71
|
},
|
|
72
72
|
"regressionGate": {
|
|
73
|
-
"enabled":
|
|
74
|
-
"
|
|
73
|
+
"enabled": true,
|
|
74
|
+
"mode": "deferred",
|
|
75
|
+
"timeoutSeconds": 600,
|
|
76
|
+
"acceptOnTimeout": true,
|
|
77
|
+
"maxRectificationAttempts": 2
|
|
75
78
|
}
|
|
76
79
|
},
|
|
77
80
|
"quality": {
|
|
@@ -147,4 +150,4 @@
|
|
|
147
150
|
"scopeToStory": true
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
|
-
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "nax",
|
|
3
|
+
"feature": "bug-039-medium",
|
|
4
|
+
"branchName": "fix/bug-039-process-cleanup-medium",
|
|
5
|
+
"createdAt": "2026-03-06T04:51:00.000Z",
|
|
6
|
+
"updatedAt": "2026-03-06T04:51:00.000Z",
|
|
7
|
+
"userStories": [
|
|
8
|
+
{
|
|
9
|
+
"id": "US-001",
|
|
10
|
+
"title": "runOnce() SIGKILL follow-up after grace period",
|
|
11
|
+
"description": "In src/agents/claude.ts:runOnce(), the timeout handler only sends SIGTERM but never follows up with SIGKILL. Claude may ignore SIGTERM and run indefinitely. Add a SIGKILL after a 5-second grace period, mirroring the pattern in src/verification/executor.ts:executeWithTimeout(). Also move pidRegistry.unregister(processPid) into a finally block so it is always called even if the timeout path throws an exception.",
|
|
12
|
+
"acceptanceCriteria": [
|
|
13
|
+
"When runOnce() times out, SIGTERM is sent first",
|
|
14
|
+
"After 5 seconds grace period, SIGKILL is sent to ensure the process is dead",
|
|
15
|
+
"pidRegistry.unregister(processPid) is called in a finally block — always runs regardless of success or exception path",
|
|
16
|
+
"Existing runOnce() tests still pass",
|
|
17
|
+
"New test: timeout path always unregisters PID even if SIGTERM/SIGKILL throws"
|
|
18
|
+
],
|
|
19
|
+
"tags": ["bug", "process", "cleanup"],
|
|
20
|
+
"dependencies": [],
|
|
21
|
+
"status": "pending",
|
|
22
|
+
"passes": false,
|
|
23
|
+
"escalations": [],
|
|
24
|
+
"attempts": 0
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "US-002",
|
|
28
|
+
"title": "LLM routing stream drain fix on timeout",
|
|
29
|
+
"description": "In src/routing/strategies/llm.ts:callLlmOnce(), when the timeout fires, proc.kill() is called but the stdout/stderr ReadableStreams from the Bun.spawn() are never cancelled or drained. This can cause proc.exited to hang indefinitely because Bun's piped streams may block exit. Fix: before calling proc.kill(), cancel both streams with proc.stdout.cancel() and proc.stderr.cancel() wrapped in try/catch. This ensures proc.exited resolves promptly after kill.",
|
|
30
|
+
"acceptanceCriteria": [
|
|
31
|
+
"On LLM timeout, proc.stdout and proc.stderr are cancelled before proc.kill()",
|
|
32
|
+
"proc.exited resolves promptly after timeout kill (does not hang)",
|
|
33
|
+
"clearTimeout(timeoutId) still called in both success and error paths",
|
|
34
|
+
"Existing LLM routing tests still pass",
|
|
35
|
+
"New test: timeout path resolves without hanging (use fake timers or short timeout)"
|
|
36
|
+
],
|
|
37
|
+
"tags": ["bug", "process", "cleanup", "llm"],
|
|
38
|
+
"dependencies": [],
|
|
39
|
+
"status": "pending",
|
|
40
|
+
"passes": false,
|
|
41
|
+
"escalations": [],
|
|
42
|
+
"attempts": 0
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
package/package.json
CHANGED
package/src/agents/claude.ts
CHANGED
|
@@ -36,6 +36,24 @@ const MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
|
36
36
|
*/
|
|
37
37
|
const MAX_AGENT_STDERR_CHARS = 1000;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Grace period in ms between SIGTERM and SIGKILL on timeout.
|
|
41
|
+
* Mirrors the pattern in src/verification/executor.ts:executeWithTimeout().
|
|
42
|
+
*/
|
|
43
|
+
const SIGKILL_GRACE_PERIOD_MS = 5000;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Injectable dependencies for runOnce() — allows tests to verify
|
|
47
|
+
* that PID cleanup (unregister) always runs even if kill() throws.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export const _runOnceDeps = {
|
|
52
|
+
killProc(proc: { kill(signal?: number | NodeJS.Signals): void }, signal: NodeJS.Signals): void {
|
|
53
|
+
proc.kill(signal);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
39
57
|
/**
|
|
40
58
|
* Claude Code agent adapter implementation.
|
|
41
59
|
*
|
|
@@ -183,25 +201,69 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
183
201
|
let timedOut = false;
|
|
184
202
|
const timeoutId = setTimeout(() => {
|
|
185
203
|
timedOut = true;
|
|
186
|
-
|
|
204
|
+
try {
|
|
205
|
+
_runOnceDeps.killProc(proc, "SIGTERM" as NodeJS.Signals);
|
|
206
|
+
} catch {
|
|
207
|
+
/* already exited */
|
|
208
|
+
}
|
|
209
|
+
setTimeout(() => {
|
|
210
|
+
try {
|
|
211
|
+
_runOnceDeps.killProc(proc, "SIGKILL" as NodeJS.Signals);
|
|
212
|
+
} catch {
|
|
213
|
+
/* already exited */
|
|
214
|
+
}
|
|
215
|
+
}, SIGKILL_GRACE_PERIOD_MS);
|
|
187
216
|
}, options.timeoutSeconds * 1000);
|
|
188
217
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
let exitCode: number;
|
|
219
|
+
try {
|
|
220
|
+
// Hard deadline: if proc.exited doesn't resolve after kill signals are sent
|
|
221
|
+
// (Bun subprocess edge case on some environments), fall back to -1 so the
|
|
222
|
+
// caller can move on. timedOut flag ensures the result is still marked 124.
|
|
223
|
+
const hardDeadlineMs = options.timeoutSeconds * 1000 + SIGKILL_GRACE_PERIOD_MS + 3000;
|
|
224
|
+
exitCode = await Promise.race([
|
|
225
|
+
proc.exited,
|
|
226
|
+
new Promise<number>((resolve) => setTimeout(() => resolve(-1), hardDeadlineMs)),
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
// If hard deadline fired, the subprocess may still be alive (Bun SIGKILL edge case).
|
|
230
|
+
// Force-kill via OS-level signal so that the stdout pipe closes and we don't block.
|
|
231
|
+
if (exitCode === -1) {
|
|
232
|
+
try {
|
|
233
|
+
process.kill(processPid, "SIGKILL");
|
|
234
|
+
} catch {
|
|
235
|
+
/* already gone */
|
|
236
|
+
}
|
|
237
|
+
// Note: process.kill(-pid) (process group) only works if the child called
|
|
238
|
+
// setpgid/setsid. Bun does not do this, so this will silently throw ESRCH.
|
|
239
|
+
// Left here as a best-effort safety net for environments that do set a pgid.
|
|
240
|
+
try {
|
|
241
|
+
process.kill(-processPid, "SIGKILL");
|
|
242
|
+
} catch {
|
|
243
|
+
/* no process group — expected in most environments */
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} finally {
|
|
247
|
+
clearTimeout(timeoutId);
|
|
248
|
+
await pidRegistry.unregister(processPid);
|
|
249
|
+
}
|
|
193
250
|
|
|
194
|
-
|
|
195
|
-
|
|
251
|
+
// Use a deadline on stdout read — if the subprocess pipe is still open
|
|
252
|
+
// (e.g. hard-deadline fired but process didn't fully die), don't block forever.
|
|
253
|
+
const stdout = await Promise.race([
|
|
254
|
+
new Response(proc.stdout).text(),
|
|
255
|
+
new Promise<string>((resolve) => setTimeout(() => resolve(""), 5000)),
|
|
256
|
+
]);
|
|
257
|
+
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
196
258
|
const durationMs = Date.now() - startTime;
|
|
197
259
|
|
|
260
|
+
// Claude Code emits rate limit messages as part of its output.
|
|
261
|
+
const fullOutput = stdout + stderr;
|
|
198
262
|
const rateLimited =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
stdout.includes("Too many requests");
|
|
263
|
+
fullOutput.toLowerCase().includes("rate limit") ||
|
|
264
|
+
fullOutput.includes("429") ||
|
|
265
|
+
fullOutput.toLowerCase().includes("too many requests");
|
|
203
266
|
|
|
204
|
-
const fullOutput = stdout + stderr;
|
|
205
267
|
let costEstimate = estimateCostFromOutput(options.modelTier, fullOutput);
|
|
206
268
|
const logger = getLogger();
|
|
207
269
|
if (!costEstimate) {
|
|
@@ -267,11 +329,43 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
267
329
|
|
|
268
330
|
await pidRegistry.register(proc.pid);
|
|
269
331
|
|
|
270
|
-
|
|
332
|
+
// BUG-039: Hard timeout for decompose — prevents infinite hang if claude hangs
|
|
333
|
+
const DECOMPOSE_TIMEOUT_MS = 300_000; // 5 minutes
|
|
334
|
+
let timedOut = false;
|
|
335
|
+
const decomposeTimerId = setTimeout(() => {
|
|
336
|
+
timedOut = true;
|
|
337
|
+
try {
|
|
338
|
+
proc.kill("SIGTERM");
|
|
339
|
+
} catch {
|
|
340
|
+
/* already exited */
|
|
341
|
+
}
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
try {
|
|
344
|
+
proc.kill("SIGKILL");
|
|
345
|
+
} catch {
|
|
346
|
+
/* already exited */
|
|
347
|
+
}
|
|
348
|
+
}, 5000);
|
|
349
|
+
}, DECOMPOSE_TIMEOUT_MS);
|
|
350
|
+
|
|
351
|
+
let exitCode: number;
|
|
352
|
+
try {
|
|
353
|
+
exitCode = await proc.exited;
|
|
354
|
+
} finally {
|
|
355
|
+
clearTimeout(decomposeTimerId);
|
|
356
|
+
await pidRegistry.unregister(proc.pid);
|
|
357
|
+
}
|
|
271
358
|
|
|
272
|
-
|
|
359
|
+
if (timedOut) {
|
|
360
|
+
throw new Error(`Decompose timed out after ${DECOMPOSE_TIMEOUT_MS / 1000}s`);
|
|
361
|
+
}
|
|
273
362
|
|
|
274
|
-
|
|
363
|
+
// Use a deadline on stdout read — if the subprocess pipe is still open
|
|
364
|
+
// (e.g. hard-deadline fired but process didn't fully die), don't block forever.
|
|
365
|
+
const stdout = await Promise.race([
|
|
366
|
+
new Response(proc.stdout).text(),
|
|
367
|
+
new Promise<string>((resolve) => setTimeout(() => resolve(""), 5000)),
|
|
368
|
+
]);
|
|
275
369
|
const stderr = await new Response(proc.stderr).text();
|
|
276
370
|
|
|
277
371
|
if (exitCode !== 0) {
|
package/src/config/types.ts
CHANGED
|
@@ -140,6 +140,17 @@ export interface QualityConfig {
|
|
|
140
140
|
typecheck?: string;
|
|
141
141
|
lint?: string;
|
|
142
142
|
test?: string;
|
|
143
|
+
/** Auto-fix lint errors (e.g., "biome check --fix") */
|
|
144
|
+
lintFix?: string;
|
|
145
|
+
/** Auto-fix formatting (e.g., "biome format --write") */
|
|
146
|
+
formatFix?: string;
|
|
147
|
+
};
|
|
148
|
+
/** Auto-fix configuration (Phase 2) */
|
|
149
|
+
autofix?: {
|
|
150
|
+
/** Whether to auto-fix lint/format errors before escalating (default: true) */
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
/** Max auto-fix attempts (default: 2) */
|
|
153
|
+
maxAttempts?: number;
|
|
143
154
|
};
|
|
144
155
|
/** Append --forceExit to test command to prevent open handle hangs (default: false) */
|
|
145
156
|
forceExit: boolean;
|
package/src/context/builder.ts
CHANGED
|
@@ -232,13 +232,21 @@ async function addFileElements(
|
|
|
232
232
|
continue;
|
|
233
233
|
}
|
|
234
234
|
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
235
|
+
// FEAT-011: File too large to inline — pass path-only so agent can read it if needed
|
|
235
236
|
const logger = getLogger();
|
|
236
|
-
logger.warn("context", "File too large", {
|
|
237
|
+
logger.warn("context", "File too large for inline — using path-only", {
|
|
237
238
|
filePath: relativeFilePath,
|
|
238
239
|
sizeKB: Math.round(file.size / 1024),
|
|
239
240
|
maxKB: 10,
|
|
240
241
|
storyId: story.id,
|
|
241
242
|
});
|
|
243
|
+
elements.push(
|
|
244
|
+
createFileContext(
|
|
245
|
+
relativeFilePath,
|
|
246
|
+
`_File too large to inline (${Math.round(file.size / 1024)}KB). Path: \`${relativeFilePath}\` — read it directly if needed._`,
|
|
247
|
+
5,
|
|
248
|
+
),
|
|
249
|
+
);
|
|
242
250
|
continue;
|
|
243
251
|
}
|
|
244
252
|
const content = await file.text();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dry Run Handler (ADR-005, Phase 4)
|
|
3
|
+
*
|
|
4
|
+
* Extracted from pipeline-result-handler.ts to slim that file below 200 lines.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getSafeLogger } from "../logger";
|
|
8
|
+
import { pipelineEventBus } from "../pipeline/event-bus";
|
|
9
|
+
import type { PluginRegistry } from "../plugins";
|
|
10
|
+
import { markStoryPassed, savePRD } from "../prd";
|
|
11
|
+
import type { PRD, UserStory } from "../prd/types";
|
|
12
|
+
import type { routeTask } from "../routing";
|
|
13
|
+
import type { StatusWriter } from "./status-writer";
|
|
14
|
+
|
|
15
|
+
export interface DryRunContext {
|
|
16
|
+
prd: PRD;
|
|
17
|
+
prdPath: string;
|
|
18
|
+
storiesToExecute: UserStory[];
|
|
19
|
+
routing: ReturnType<typeof routeTask>;
|
|
20
|
+
statusWriter: StatusWriter;
|
|
21
|
+
pluginRegistry: PluginRegistry;
|
|
22
|
+
runId: string;
|
|
23
|
+
totalCost: number;
|
|
24
|
+
iterations: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DryRunResult {
|
|
28
|
+
storiesCompletedDelta: number;
|
|
29
|
+
prdDirty: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Handle dry-run iteration: log what would happen, mark stories passed. */
|
|
33
|
+
export async function handleDryRun(ctx: DryRunContext): Promise<DryRunResult> {
|
|
34
|
+
const logger = getSafeLogger();
|
|
35
|
+
|
|
36
|
+
ctx.statusWriter.setPrd(ctx.prd);
|
|
37
|
+
ctx.statusWriter.setCurrentStory({
|
|
38
|
+
storyId: ctx.storiesToExecute[0].id,
|
|
39
|
+
title: ctx.storiesToExecute[0].title,
|
|
40
|
+
complexity: ctx.routing.complexity,
|
|
41
|
+
tddStrategy: ctx.routing.testStrategy,
|
|
42
|
+
model: ctx.routing.modelTier,
|
|
43
|
+
attempt: (ctx.storiesToExecute[0].attempts ?? 0) + 1,
|
|
44
|
+
phase: "routing",
|
|
45
|
+
});
|
|
46
|
+
await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
|
|
47
|
+
|
|
48
|
+
for (const s of ctx.storiesToExecute) {
|
|
49
|
+
logger?.info("execution", "[DRY RUN] Would execute agent here", {
|
|
50
|
+
storyId: s.id,
|
|
51
|
+
storyTitle: s.title,
|
|
52
|
+
modelTier: ctx.routing.modelTier,
|
|
53
|
+
complexity: ctx.routing.complexity,
|
|
54
|
+
testStrategy: ctx.routing.testStrategy,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const s of ctx.storiesToExecute) {
|
|
59
|
+
markStoryPassed(ctx.prd, s.id);
|
|
60
|
+
}
|
|
61
|
+
await savePRD(ctx.prd, ctx.prdPath);
|
|
62
|
+
|
|
63
|
+
for (const s of ctx.storiesToExecute) {
|
|
64
|
+
pipelineEventBus.emit({
|
|
65
|
+
type: "story:completed",
|
|
66
|
+
storyId: s.id,
|
|
67
|
+
story: s,
|
|
68
|
+
passed: true,
|
|
69
|
+
durationMs: 0,
|
|
70
|
+
cost: 0,
|
|
71
|
+
modelTier: ctx.routing.modelTier,
|
|
72
|
+
testStrategy: ctx.routing.testStrategy,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ctx.statusWriter.setPrd(ctx.prd);
|
|
77
|
+
ctx.statusWriter.setCurrentStory(null);
|
|
78
|
+
await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
|
|
79
|
+
|
|
80
|
+
return { storiesCompletedDelta: ctx.storiesToExecute.length, prdDirty: true };
|
|
81
|
+
}
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from tier-escalation.ts: handles outcomes when escalation
|
|
5
5
|
* is not possible (no tier available or max attempts reached).
|
|
6
|
+
*
|
|
7
|
+
* Phase 3 (ADR-005): Replaced direct fireHook() calls with event bus emissions.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import { fireHook } from "../../hooks";
|
|
9
10
|
import { getSafeLogger } from "../../logger";
|
|
11
|
+
import { pipelineEventBus } from "../../pipeline/event-bus";
|
|
10
12
|
import { markStoryFailed, markStoryPaused, savePRD } from "../../prd";
|
|
11
13
|
import type { FailureCategory } from "../../tdd/types";
|
|
12
|
-
import { hookCtx } from "../helpers";
|
|
13
14
|
import { appendProgress } from "../progress";
|
|
14
15
|
import type { EscalationHandlerContext, EscalationHandlerResult } from "./tier-escalation";
|
|
15
16
|
import { resolveMaxAttemptsOutcome } from "./tier-escalation";
|
|
@@ -43,16 +44,12 @@ export async function handleNoTierAvailable(
|
|
|
43
44
|
);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
cost: ctx.totalCost,
|
|
53
|
-
}),
|
|
54
|
-
ctx.workdir,
|
|
55
|
-
);
|
|
47
|
+
pipelineEventBus.emit({
|
|
48
|
+
type: "story:paused",
|
|
49
|
+
storyId: ctx.story.id,
|
|
50
|
+
reason: `Execution stopped (${failureCategory ?? "unknown"} requires human review)`,
|
|
51
|
+
cost: ctx.totalCost,
|
|
52
|
+
});
|
|
56
53
|
|
|
57
54
|
return { outcome: "paused", prdDirty: true, prd: pausedPrd };
|
|
58
55
|
}
|
|
@@ -70,17 +67,13 @@ export async function handleNoTierAvailable(
|
|
|
70
67
|
await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} — Execution failed`);
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
cost: ctx.totalCost,
|
|
81
|
-
}),
|
|
82
|
-
ctx.workdir,
|
|
83
|
-
);
|
|
70
|
+
pipelineEventBus.emit({
|
|
71
|
+
type: "story:failed",
|
|
72
|
+
storyId: ctx.story.id,
|
|
73
|
+
story: ctx.story,
|
|
74
|
+
reason: "Execution failed",
|
|
75
|
+
countsTowardEscalation: true,
|
|
76
|
+
});
|
|
84
77
|
|
|
85
78
|
return { outcome: "failed", prdDirty: true, prd: failedPrd };
|
|
86
79
|
}
|
|
@@ -114,16 +107,12 @@ export async function handleMaxAttemptsReached(
|
|
|
114
107
|
);
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
cost: ctx.totalCost,
|
|
124
|
-
}),
|
|
125
|
-
ctx.workdir,
|
|
126
|
-
);
|
|
110
|
+
pipelineEventBus.emit({
|
|
111
|
+
type: "story:paused",
|
|
112
|
+
storyId: ctx.story.id,
|
|
113
|
+
reason: `Max attempts reached (${failureCategory ?? "unknown"} requires human review)`,
|
|
114
|
+
cost: ctx.totalCost,
|
|
115
|
+
});
|
|
127
116
|
|
|
128
117
|
return { outcome: "paused", prdDirty: true, prd: pausedPrd };
|
|
129
118
|
}
|
|
@@ -142,17 +131,13 @@ export async function handleMaxAttemptsReached(
|
|
|
142
131
|
await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} — Max attempts reached`);
|
|
143
132
|
}
|
|
144
133
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
cost: ctx.totalCost,
|
|
153
|
-
}),
|
|
154
|
-
ctx.workdir,
|
|
155
|
-
);
|
|
134
|
+
pipelineEventBus.emit({
|
|
135
|
+
type: "story:failed",
|
|
136
|
+
storyId: ctx.story.id,
|
|
137
|
+
story: ctx.story,
|
|
138
|
+
reason: "Max attempts reached",
|
|
139
|
+
countsTowardEscalation: true,
|
|
140
|
+
});
|
|
156
141
|
|
|
157
142
|
return { outcome: "failed", prdDirty: true, prd: failedPrd };
|
|
158
143
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequential Executor Types (ADR-005, Phase 4)
|
|
3
|
+
*
|
|
4
|
+
* Extracted from sequential-executor.ts to slim it below 200 lines.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NaxConfig } from "../config";
|
|
8
|
+
import type { LoadedHooksConfig } from "../hooks";
|
|
9
|
+
import type { InteractionChain } from "../interaction/chain";
|
|
10
|
+
import type { StoryMetrics } from "../metrics";
|
|
11
|
+
import type { PipelineEventEmitter } from "../pipeline/events";
|
|
12
|
+
import type { RoutingResult } from "../pipeline/types";
|
|
13
|
+
import type { PluginRegistry } from "../plugins";
|
|
14
|
+
import type { PRD, UserStory } from "../prd/types";
|
|
15
|
+
import type { StoryBatch } from "./batching";
|
|
16
|
+
import type { StatusWriter } from "./status-writer";
|
|
17
|
+
|
|
18
|
+
export interface SequentialExecutionContext {
|
|
19
|
+
prdPath: string;
|
|
20
|
+
workdir: string;
|
|
21
|
+
config: NaxConfig;
|
|
22
|
+
hooks: LoadedHooksConfig;
|
|
23
|
+
feature: string;
|
|
24
|
+
featureDir?: string;
|
|
25
|
+
dryRun: boolean;
|
|
26
|
+
useBatch: boolean;
|
|
27
|
+
pluginRegistry: PluginRegistry;
|
|
28
|
+
eventEmitter?: PipelineEventEmitter;
|
|
29
|
+
statusWriter: StatusWriter;
|
|
30
|
+
logFilePath?: string;
|
|
31
|
+
runId: string;
|
|
32
|
+
startTime: number;
|
|
33
|
+
batchPlan: StoryBatch[];
|
|
34
|
+
interactionChain?: InteractionChain | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SequentialExecutionResult {
|
|
38
|
+
prd: PRD;
|
|
39
|
+
iterations: number;
|
|
40
|
+
storiesCompleted: number;
|
|
41
|
+
totalCost: number;
|
|
42
|
+
allStoryMetrics: StoryMetrics[];
|
|
43
|
+
exitReason: "completed" | "cost-limit" | "max-iterations" | "stalled" | "no-stories";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a preview routing from cached story.routing or config defaults.
|
|
48
|
+
* The pipeline routing stage performs full classification and overwrites ctx.routing.
|
|
49
|
+
* This preview is used only for logging, status display, and event emission.
|
|
50
|
+
*/
|
|
51
|
+
export function buildPreviewRouting(story: UserStory, config: NaxConfig): RoutingResult {
|
|
52
|
+
const cached = story.routing;
|
|
53
|
+
const defaultComplexity = "medium" as const;
|
|
54
|
+
const defaultTier = "balanced" as const;
|
|
55
|
+
const defaultStrategy = "test-after" as const;
|
|
56
|
+
return {
|
|
57
|
+
complexity: (cached?.complexity as RoutingResult["complexity"]) ?? defaultComplexity,
|
|
58
|
+
modelTier:
|
|
59
|
+
(cached?.modelTier as RoutingResult["modelTier"]) ??
|
|
60
|
+
(config.autoMode.complexityRouting?.[defaultComplexity] as RoutingResult["modelTier"]) ??
|
|
61
|
+
defaultTier,
|
|
62
|
+
testStrategy: (cached?.testStrategy as RoutingResult["testStrategy"]) ?? defaultStrategy,
|
|
63
|
+
reasoning: cached ? "cached from story.routing" : "preview (pending pipeline routing stage)",
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/execution/index.ts
CHANGED
|
@@ -5,23 +5,6 @@ export { appendProgress } from "./progress";
|
|
|
5
5
|
export { buildSingleSessionPrompt, buildBatchPrompt } from "./prompts";
|
|
6
6
|
export { groupStoriesIntoBatches, type StoryBatch } from "./batching";
|
|
7
7
|
export { escalateTier, getTierConfig, calculateMaxIterations } from "./escalation";
|
|
8
|
-
export {
|
|
9
|
-
verifyAssets,
|
|
10
|
-
executeWithTimeout,
|
|
11
|
-
parseTestOutput,
|
|
12
|
-
getEnvironmentalEscalationThreshold,
|
|
13
|
-
normalizeEnvironment,
|
|
14
|
-
buildTestCommand,
|
|
15
|
-
appendOpenHandlesFlag,
|
|
16
|
-
appendForceExitFlag,
|
|
17
|
-
runVerification,
|
|
18
|
-
type VerificationResult,
|
|
19
|
-
type VerificationStatus,
|
|
20
|
-
type TestOutputAnalysis,
|
|
21
|
-
type AssetVerificationResult,
|
|
22
|
-
type TimeoutExecutionResult,
|
|
23
|
-
} from "./verification";
|
|
24
|
-
export { runPostAgentVerification, type PostVerifyOptions, type PostVerifyResult } from "./post-verify";
|
|
25
8
|
export { readQueueFile, clearQueueFile } from "./queue-handler";
|
|
26
9
|
export {
|
|
27
10
|
hookCtx,
|