@nathapp/nax 0.42.0 ā 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nax.ts +5 -3
- package/dist/nax.js +93 -36
- package/package.json +1 -1
- package/src/agents/acp/adapter.ts +60 -25
- package/src/agents/types.ts +6 -0
- package/src/cli/plan.ts +24 -11
- package/src/pipeline/stages/acceptance.ts +21 -6
- package/src/prd/schema.ts +6 -2
package/bin/nax.ts
CHANGED
|
@@ -277,6 +277,7 @@ program
|
|
|
277
277
|
.option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)")
|
|
278
278
|
.option("--plan", "Run plan phase first before execution", false)
|
|
279
279
|
.option("--from <spec-path>", "Path to spec file (required when --plan is used)")
|
|
280
|
+
.option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false)
|
|
280
281
|
.option("--headless", "Force headless mode (disable TUI, use pipe mode)", false)
|
|
281
282
|
.option("--verbose", "Enable verbose logging (debug level)", false)
|
|
282
283
|
.option("--quiet", "Quiet mode (warnings and errors only)", false)
|
|
@@ -347,7 +348,7 @@ program
|
|
|
347
348
|
const generatedPrdPath = await planCommand(workdir, config, {
|
|
348
349
|
from: options.from,
|
|
349
350
|
feature: options.feature,
|
|
350
|
-
auto:
|
|
351
|
+
auto: options.oneShot ?? false, // interactive by default; --one-shot skips Q&A
|
|
351
352
|
branch: undefined,
|
|
352
353
|
});
|
|
353
354
|
|
|
@@ -625,7 +626,8 @@ program
|
|
|
625
626
|
.description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')")
|
|
626
627
|
.requiredOption("--from <spec-path>", "Path to spec file (required)")
|
|
627
628
|
.requiredOption("-f, --feature <name>", "Feature name (required)")
|
|
628
|
-
.option("--auto", "Run in
|
|
629
|
+
.option("--auto", "Run in one-shot LLM mode (alias: --one-shot)", false)
|
|
630
|
+
.option("--one-shot", "Run in one-shot LLM mode (alias: --auto)", false)
|
|
629
631
|
.option("-b, --branch <branch>", "Override default branch name")
|
|
630
632
|
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
631
633
|
.action(async (description, options) => {
|
|
@@ -658,7 +660,7 @@ program
|
|
|
658
660
|
const prdPath = await planCommand(workdir, config, {
|
|
659
661
|
from: options.from,
|
|
660
662
|
feature: options.feature,
|
|
661
|
-
auto: options.auto,
|
|
663
|
+
auto: options.auto || options.oneShot, // --auto and --one-shot are aliases
|
|
662
664
|
branch: options.branch,
|
|
663
665
|
});
|
|
664
666
|
|
package/dist/nax.js
CHANGED
|
@@ -19439,29 +19439,57 @@ class AcpAgentAdapter {
|
|
|
19439
19439
|
}
|
|
19440
19440
|
async complete(prompt, _options) {
|
|
19441
19441
|
const model = _options?.model ?? "default";
|
|
19442
|
-
const
|
|
19443
|
-
const client = _acpAdapterDeps.createClient(cmdStr);
|
|
19444
|
-
await client.start();
|
|
19442
|
+
const timeoutMs = _options?.timeoutMs ?? 120000;
|
|
19445
19443
|
const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
|
|
19446
|
-
let
|
|
19447
|
-
|
|
19448
|
-
|
|
19449
|
-
const
|
|
19450
|
-
|
|
19451
|
-
|
|
19452
|
-
|
|
19453
|
-
|
|
19444
|
+
let lastError;
|
|
19445
|
+
for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
19446
|
+
const cmdStr = `acpx --model ${model} ${this.name}`;
|
|
19447
|
+
const client = _acpAdapterDeps.createClient(cmdStr);
|
|
19448
|
+
await client.start();
|
|
19449
|
+
let session = null;
|
|
19450
|
+
try {
|
|
19451
|
+
session = await client.createSession({ agentName: this.name, permissionMode });
|
|
19452
|
+
let timeoutId;
|
|
19453
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
19454
|
+
timeoutId = setTimeout(() => reject(new Error(`complete() timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
19455
|
+
});
|
|
19456
|
+
timeoutPromise.catch(() => {});
|
|
19457
|
+
const promptPromise = session.prompt(prompt);
|
|
19458
|
+
let response;
|
|
19459
|
+
try {
|
|
19460
|
+
response = await Promise.race([promptPromise, timeoutPromise]);
|
|
19461
|
+
} finally {
|
|
19462
|
+
clearTimeout(timeoutId);
|
|
19463
|
+
}
|
|
19464
|
+
if (response.stopReason === "error") {
|
|
19465
|
+
throw new CompleteError("complete() failed: stop reason is error");
|
|
19466
|
+
}
|
|
19467
|
+
const text = response.messages.filter((m) => m.role === "assistant").map((m) => m.content).join(`
|
|
19454
19468
|
`).trim();
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19469
|
+
if (!text) {
|
|
19470
|
+
throw new CompleteError("complete() returned empty output");
|
|
19471
|
+
}
|
|
19472
|
+
return text;
|
|
19473
|
+
} catch (err) {
|
|
19474
|
+
const error48 = err instanceof Error ? err : new Error(String(err));
|
|
19475
|
+
lastError = error48;
|
|
19476
|
+
const shouldRetry = isRateLimitError(error48) && attempt < MAX_RATE_LIMIT_RETRIES - 1;
|
|
19477
|
+
if (!shouldRetry)
|
|
19478
|
+
throw error48;
|
|
19479
|
+
const backoffMs = 2 ** (attempt + 1) * 1000;
|
|
19480
|
+
getSafeLogger()?.warn("acp-adapter", "complete() rate limited, retrying", {
|
|
19481
|
+
backoffSeconds: backoffMs / 1000,
|
|
19482
|
+
attempt: attempt + 1
|
|
19483
|
+
});
|
|
19484
|
+
await _acpAdapterDeps.sleep(backoffMs);
|
|
19485
|
+
} finally {
|
|
19486
|
+
if (session) {
|
|
19487
|
+
await session.close().catch(() => {});
|
|
19488
|
+
}
|
|
19489
|
+
await client.close().catch(() => {});
|
|
19462
19490
|
}
|
|
19463
|
-
await client.close().catch(() => {});
|
|
19464
19491
|
}
|
|
19492
|
+
throw lastError ?? new CompleteError("complete() failed with unknown error");
|
|
19465
19493
|
}
|
|
19466
19494
|
async plan(options) {
|
|
19467
19495
|
const modelDef = options.modelDef ?? { provider: "anthropic", model: "default" };
|
|
@@ -21807,7 +21835,7 @@ var package_default;
|
|
|
21807
21835
|
var init_package = __esm(() => {
|
|
21808
21836
|
package_default = {
|
|
21809
21837
|
name: "@nathapp/nax",
|
|
21810
|
-
version: "0.42.
|
|
21838
|
+
version: "0.42.1",
|
|
21811
21839
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
21812
21840
|
type: "module",
|
|
21813
21841
|
bin: {
|
|
@@ -21872,8 +21900,8 @@ var init_version = __esm(() => {
|
|
|
21872
21900
|
NAX_VERSION = package_default.version;
|
|
21873
21901
|
NAX_COMMIT = (() => {
|
|
21874
21902
|
try {
|
|
21875
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
21876
|
-
return "
|
|
21903
|
+
if (/^[0-9a-f]{6,10}$/.test("29c340c"))
|
|
21904
|
+
return "29c340c";
|
|
21877
21905
|
} catch {}
|
|
21878
21906
|
try {
|
|
21879
21907
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -23550,11 +23578,23 @@ ${stderr}`;
|
|
|
23550
23578
|
logger.info("acceptance", "All acceptance tests passed");
|
|
23551
23579
|
return { action: "continue" };
|
|
23552
23580
|
}
|
|
23553
|
-
if (
|
|
23554
|
-
logger.
|
|
23581
|
+
if (failedACs.length > 0 && actualFailures.length === 0) {
|
|
23582
|
+
logger.info("acceptance", "All failed ACs are overridden \u2014 treating as pass");
|
|
23583
|
+
return { action: "continue" };
|
|
23584
|
+
}
|
|
23585
|
+
if (failedACs.length === 0 && exitCode !== 0) {
|
|
23586
|
+
logger.error("acceptance", "Tests errored with no AC failures parsed", {
|
|
23587
|
+
exitCode,
|
|
23555
23588
|
output
|
|
23556
23589
|
});
|
|
23557
|
-
|
|
23590
|
+
ctx.acceptanceFailures = {
|
|
23591
|
+
failedACs: ["AC-ERROR"],
|
|
23592
|
+
testOutput: output
|
|
23593
|
+
};
|
|
23594
|
+
return {
|
|
23595
|
+
action: "fail",
|
|
23596
|
+
reason: `Acceptance tests errored (exit code ${exitCode}): syntax error, import failure, or unhandled exception`
|
|
23597
|
+
};
|
|
23558
23598
|
}
|
|
23559
23599
|
if (actualFailures.length > 0) {
|
|
23560
23600
|
const overriddenFailures = failedACs.filter((acId) => overrides[acId]);
|
|
@@ -65505,6 +65545,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
|
|
|
65505
65545
|
init_registry();
|
|
65506
65546
|
import { existsSync as existsSync9 } from "fs";
|
|
65507
65547
|
import { join as join10 } from "path";
|
|
65548
|
+
import { createInterface } from "readline";
|
|
65508
65549
|
init_logger2();
|
|
65509
65550
|
|
|
65510
65551
|
// src/prd/schema.ts
|
|
@@ -65584,7 +65625,9 @@ function validateStory(raw, index, allIds) {
|
|
|
65584
65625
|
throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
|
|
65585
65626
|
}
|
|
65586
65627
|
const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
|
|
65587
|
-
const
|
|
65628
|
+
const STRATEGY_ALIASES = { "tdd-lite": "three-session-tdd-lite" };
|
|
65629
|
+
const normalizedStrategy = typeof rawTestStrategy === "string" ? STRATEGY_ALIASES[rawTestStrategy] ?? rawTestStrategy : rawTestStrategy;
|
|
65630
|
+
const testStrategy = normalizedStrategy !== undefined && VALID_TEST_STRATEGIES.includes(normalizedStrategy) ? normalizedStrategy : "tdd-simple";
|
|
65588
65631
|
const rawDeps = s.dependencies;
|
|
65589
65632
|
const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
|
|
65590
65633
|
for (const dep of dependencies) {
|
|
@@ -65722,7 +65765,20 @@ function createCliInteractionBridge() {
|
|
|
65722
65765
|
return text.includes("?");
|
|
65723
65766
|
},
|
|
65724
65767
|
async onQuestionDetected(text) {
|
|
65725
|
-
|
|
65768
|
+
if (!process.stdin.isTTY) {
|
|
65769
|
+
return "";
|
|
65770
|
+
}
|
|
65771
|
+
process.stdout.write(`
|
|
65772
|
+
\uD83E\uDD16 Agent: ${text}
|
|
65773
|
+
You: `);
|
|
65774
|
+
return new Promise((resolve6) => {
|
|
65775
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
65776
|
+
rl.once("line", (line) => {
|
|
65777
|
+
rl.close();
|
|
65778
|
+
resolve6(line.trim());
|
|
65779
|
+
});
|
|
65780
|
+
rl.once("close", () => resolve6(""));
|
|
65781
|
+
});
|
|
65726
65782
|
}
|
|
65727
65783
|
};
|
|
65728
65784
|
}
|
|
@@ -65798,7 +65854,7 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
|
|
|
65798
65854
|
"passes": false,
|
|
65799
65855
|
"routing": {
|
|
65800
65856
|
"complexity": "simple | medium | complex | expert",
|
|
65801
|
-
"testStrategy": "test-after | tdd-
|
|
65857
|
+
"testStrategy": "test-after | tdd-simple | three-session-tdd | three-session-tdd-lite",
|
|
65802
65858
|
"reasoning": "string \u2014 brief classification rationale"
|
|
65803
65859
|
},
|
|
65804
65860
|
"escalations": [],
|
|
@@ -65810,15 +65866,16 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
|
|
|
65810
65866
|
## Complexity Classification Guide
|
|
65811
65867
|
|
|
65812
65868
|
- simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
|
|
65813
|
-
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-
|
|
65869
|
+
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
|
|
65814
65870
|
- complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
|
|
65815
|
-
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd
|
|
65871
|
+
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
|
|
65816
65872
|
|
|
65817
65873
|
## Test Strategy Guide
|
|
65818
65874
|
|
|
65819
65875
|
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
65820
|
-
- tdd-
|
|
65821
|
-
- three-session-tdd: Complex
|
|
65876
|
+
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
65877
|
+
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
65878
|
+
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.
|
|
65822
65879
|
|
|
65823
65880
|
Output ONLY the JSON object. Do not wrap in markdown code blocks.`;
|
|
65824
65881
|
}
|
|
@@ -76508,7 +76565,7 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
|
|
|
76508
76565
|
console.log(source_default.dim(`
|
|
76509
76566
|
Next: nax features create <name>`));
|
|
76510
76567
|
});
|
|
76511
|
-
program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
|
|
76568
|
+
program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
|
|
76512
76569
|
let workdir;
|
|
76513
76570
|
try {
|
|
76514
76571
|
workdir = validateDirectory(options.dir);
|
|
@@ -76557,7 +76614,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
76557
76614
|
const generatedPrdPath = await planCommand(workdir, config2, {
|
|
76558
76615
|
from: options.from,
|
|
76559
76616
|
feature: options.feature,
|
|
76560
|
-
auto:
|
|
76617
|
+
auto: options.oneShot ?? false,
|
|
76561
76618
|
branch: undefined
|
|
76562
76619
|
});
|
|
76563
76620
|
const generatedPrd = await loadPRD(generatedPrdPath);
|
|
@@ -76774,7 +76831,7 @@ Features:
|
|
|
76774
76831
|
}
|
|
76775
76832
|
console.log();
|
|
76776
76833
|
});
|
|
76777
|
-
program2.command("plan [description]").description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')").requiredOption("--from <spec-path>", "Path to spec file (required)").requiredOption("-f, --feature <name>", "Feature name (required)").option("--auto", "Run in
|
|
76834
|
+
program2.command("plan [description]").description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')").requiredOption("--from <spec-path>", "Path to spec file (required)").requiredOption("-f, --feature <name>", "Feature name (required)").option("--auto", "Run in one-shot LLM mode (alias: --one-shot)", false).option("--one-shot", "Run in one-shot LLM mode (alias: --auto)", false).option("-b, --branch <branch>", "Override default branch name").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (description, options) => {
|
|
76778
76835
|
if (description) {
|
|
76779
76836
|
console.error(source_default.red(`Error: Positional args removed in plan v2.
|
|
76780
76837
|
|
|
@@ -76798,7 +76855,7 @@ Use: nax plan -f <feature> --from <spec>`));
|
|
|
76798
76855
|
const prdPath = await planCommand(workdir, config2, {
|
|
76799
76856
|
from: options.from,
|
|
76800
76857
|
feature: options.feature,
|
|
76801
|
-
auto: options.auto,
|
|
76858
|
+
auto: options.auto || options.oneShot,
|
|
76802
76859
|
branch: options.branch
|
|
76803
76860
|
});
|
|
76804
76861
|
console.log(source_default.green(`
|
package/package.json
CHANGED
|
@@ -552,39 +552,74 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
552
552
|
|
|
553
553
|
async complete(prompt: string, _options?: CompleteOptions): Promise<string> {
|
|
554
554
|
const model = _options?.model ?? "default";
|
|
555
|
-
const
|
|
556
|
-
const client = _acpAdapterDeps.createClient(cmdStr);
|
|
557
|
-
await client.start();
|
|
558
|
-
|
|
559
|
-
// complete() is one-shot ā ephemeral session, no session name, no sidecar
|
|
555
|
+
const timeoutMs = _options?.timeoutMs ?? 120_000; // 2-min safety net by default
|
|
560
556
|
const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
|
|
561
557
|
|
|
562
|
-
let
|
|
563
|
-
try {
|
|
564
|
-
session = await client.createSession({ agentName: this.name, permissionMode });
|
|
565
|
-
const response = await session.prompt(prompt);
|
|
558
|
+
let lastError: Error | undefined;
|
|
566
559
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
560
|
+
for (let attempt = 0; attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
561
|
+
const cmdStr = `acpx --model ${model} ${this.name}`;
|
|
562
|
+
const client = _acpAdapterDeps.createClient(cmdStr);
|
|
563
|
+
await client.start();
|
|
570
564
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
.
|
|
575
|
-
.trim();
|
|
565
|
+
let session: AcpSession | null = null;
|
|
566
|
+
try {
|
|
567
|
+
// complete() is one-shot ā ephemeral session, no session name, no sidecar
|
|
568
|
+
session = await client.createSession({ agentName: this.name, permissionMode });
|
|
576
569
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
570
|
+
// Enforce timeout via Promise.race ā session.prompt() can hang indefinitely
|
|
571
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
572
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
573
|
+
timeoutId = setTimeout(() => reject(new Error(`complete() timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
574
|
+
});
|
|
575
|
+
timeoutPromise.catch(() => {}); // prevent unhandled rejection if promptPromise wins
|
|
580
576
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
577
|
+
const promptPromise = session.prompt(prompt);
|
|
578
|
+
|
|
579
|
+
let response: AcpSessionResponse;
|
|
580
|
+
try {
|
|
581
|
+
response = await Promise.race([promptPromise, timeoutPromise]);
|
|
582
|
+
} finally {
|
|
583
|
+
clearTimeout(timeoutId);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (response.stopReason === "error") {
|
|
587
|
+
throw new CompleteError("complete() failed: stop reason is error");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const text = response.messages
|
|
591
|
+
.filter((m) => m.role === "assistant")
|
|
592
|
+
.map((m) => m.content)
|
|
593
|
+
.join("\n")
|
|
594
|
+
.trim();
|
|
595
|
+
|
|
596
|
+
if (!text) {
|
|
597
|
+
throw new CompleteError("complete() returned empty output");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return text;
|
|
601
|
+
} catch (err) {
|
|
602
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
603
|
+
lastError = error;
|
|
604
|
+
|
|
605
|
+
const shouldRetry = isRateLimitError(error) && attempt < MAX_RATE_LIMIT_RETRIES - 1;
|
|
606
|
+
if (!shouldRetry) throw error;
|
|
607
|
+
|
|
608
|
+
const backoffMs = 2 ** (attempt + 1) * 1000;
|
|
609
|
+
getSafeLogger()?.warn("acp-adapter", "complete() rate limited, retrying", {
|
|
610
|
+
backoffSeconds: backoffMs / 1000,
|
|
611
|
+
attempt: attempt + 1,
|
|
612
|
+
});
|
|
613
|
+
await _acpAdapterDeps.sleep(backoffMs);
|
|
614
|
+
} finally {
|
|
615
|
+
if (session) {
|
|
616
|
+
await session.close().catch(() => {});
|
|
617
|
+
}
|
|
618
|
+
await client.close().catch(() => {});
|
|
585
619
|
}
|
|
586
|
-
await client.close().catch(() => {});
|
|
587
620
|
}
|
|
621
|
+
|
|
622
|
+
throw lastError ?? new CompleteError("complete() failed with unknown error");
|
|
588
623
|
}
|
|
589
624
|
|
|
590
625
|
async plan(options: PlanOptions): Promise<PlanResult> {
|
package/src/agents/types.ts
CHANGED
|
@@ -100,6 +100,12 @@ export interface CompleteOptions {
|
|
|
100
100
|
model?: string;
|
|
101
101
|
/** Whether to skip permission prompts (maps to permissionMode in ACP) */
|
|
102
102
|
dangerouslySkipPermissions?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Timeout for the completion call in milliseconds.
|
|
105
|
+
* Adapters that support it (e.g. ACP) will enforce this as a hard deadline.
|
|
106
|
+
* Callers may also wrap complete() in their own Promise.race for shorter timeouts.
|
|
107
|
+
*/
|
|
108
|
+
timeoutMs?: number;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/**
|
package/src/cli/plan.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Reads a spec file (--from), builds a planning prompt with codebase context,
|
|
5
5
|
* calls adapter.complete(), validates the JSON response, and writes prd.json.
|
|
6
6
|
*
|
|
7
|
-
* Interactive mode
|
|
7
|
+
* Interactive mode: uses ACP session + stdin bridge for Q&A.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
12
13
|
import { getAgent } from "../agents/registry";
|
|
13
14
|
import type { AgentAdapter } from "../agents/types";
|
|
14
15
|
import { scanCodebase } from "../analyze/scanner";
|
|
@@ -153,15 +154,26 @@ function createCliInteractionBridge(): {
|
|
|
153
154
|
} {
|
|
154
155
|
return {
|
|
155
156
|
async detectQuestion(text: string): Promise<boolean> {
|
|
156
|
-
// Simple heuristic: detect if text contains a question mark
|
|
157
157
|
return text.includes("?");
|
|
158
158
|
},
|
|
159
159
|
|
|
160
160
|
async onQuestionDetected(text: string): Promise<string> {
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
// In non-TTY mode (headless/pipes), skip interaction and continue
|
|
162
|
+
if (!process.stdin.isTTY) {
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Print agent question and read one line from stdin
|
|
167
|
+
process.stdout.write(`\nš¤ Agent: ${text}\nYou: `);
|
|
168
|
+
|
|
169
|
+
return new Promise<string>((resolve) => {
|
|
170
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
171
|
+
rl.once("line", (line) => {
|
|
172
|
+
rl.close();
|
|
173
|
+
resolve(line.trim());
|
|
174
|
+
});
|
|
175
|
+
rl.once("close", () => resolve(""));
|
|
176
|
+
});
|
|
165
177
|
},
|
|
166
178
|
};
|
|
167
179
|
}
|
|
@@ -262,7 +274,7 @@ Generate a JSON object with this exact structure (no markdown, no explanation
|
|
|
262
274
|
"passes": false,
|
|
263
275
|
"routing": {
|
|
264
276
|
"complexity": "simple | medium | complex | expert",
|
|
265
|
-
"testStrategy": "test-after | tdd-
|
|
277
|
+
"testStrategy": "test-after | tdd-simple | three-session-tdd | three-session-tdd-lite",
|
|
266
278
|
"reasoning": "string ā brief classification rationale"
|
|
267
279
|
},
|
|
268
280
|
"escalations": [],
|
|
@@ -274,15 +286,16 @@ Generate a JSON object with this exact structure (no markdown, no explanation
|
|
|
274
286
|
## Complexity Classification Guide
|
|
275
287
|
|
|
276
288
|
- simple: ā¤50 LOC, single-file change, purely additive, no new dependencies ā test-after
|
|
277
|
-
- medium: 50ā200 LOC, 2ā5 files, standard patterns, clear requirements ā tdd-
|
|
289
|
+
- medium: 50ā200 LOC, 2ā5 files, standard patterns, clear requirements ā tdd-simple
|
|
278
290
|
- complex: 200ā500 LOC, multiple modules, new abstractions or integrations ā three-session-tdd
|
|
279
|
-
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk ā three-session-tdd
|
|
291
|
+
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk ā three-session-tdd-lite
|
|
280
292
|
|
|
281
293
|
## Test Strategy Guide
|
|
282
294
|
|
|
283
295
|
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
284
|
-
- tdd-
|
|
285
|
-
- three-session-tdd: Complex
|
|
296
|
+
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
297
|
+
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
298
|
+
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.
|
|
286
299
|
|
|
287
300
|
Output ONLY the JSON object. Do not wrap in markdown code blocks.`;
|
|
288
301
|
}
|
|
@@ -149,19 +149,34 @@ export const acceptanceStage: PipelineStage = {
|
|
|
149
149
|
const overrides = ctx.prd.acceptanceOverrides || {};
|
|
150
150
|
const actualFailures = failedACs.filter((acId) => !overrides[acId]);
|
|
151
151
|
|
|
152
|
-
// If all
|
|
152
|
+
// If all tests passed cleanly
|
|
153
153
|
if (actualFailures.length === 0 && exitCode === 0) {
|
|
154
154
|
logger.info("acceptance", "All acceptance tests passed");
|
|
155
155
|
return { action: "continue" };
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
// All parsed AC failures are overridden ā treat as success even with non-zero exit
|
|
159
|
+
if (failedACs.length > 0 && actualFailures.length === 0) {
|
|
160
|
+
logger.info("acceptance", "All failed ACs are overridden ā treating as pass");
|
|
161
|
+
return { action: "continue" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Non-zero exit but no AC failures parsed at all ā test crashed (syntax error, import failure, etc.)
|
|
165
|
+
if (failedACs.length === 0 && exitCode !== 0) {
|
|
166
|
+
logger.error("acceptance", "Tests errored with no AC failures parsed", {
|
|
167
|
+
exitCode,
|
|
162
168
|
output,
|
|
163
169
|
});
|
|
164
|
-
|
|
170
|
+
|
|
171
|
+
ctx.acceptanceFailures = {
|
|
172
|
+
failedACs: ["AC-ERROR"],
|
|
173
|
+
testOutput: output,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
action: "fail",
|
|
178
|
+
reason: `Acceptance tests errored (exit code ${exitCode}): syntax error, import failure, or unhandled exception`,
|
|
179
|
+
};
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
// If we have actual failures, report them
|
package/src/prd/schema.ts
CHANGED
|
@@ -140,10 +140,14 @@ function validateStory(raw: unknown, index: number, allIds: Set<string>): UserSt
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
// testStrategy ā accept from routing.testStrategy or top-level testStrategy
|
|
143
|
+
// Also map legacy/LLM-hallucinated aliases: tdd-lite ā tdd-simple
|
|
143
144
|
const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
|
|
145
|
+
const STRATEGY_ALIASES: Record<string, TestStrategy> = { "tdd-lite": "three-session-tdd-lite" };
|
|
146
|
+
const normalizedStrategy =
|
|
147
|
+
typeof rawTestStrategy === "string" ? (STRATEGY_ALIASES[rawTestStrategy] ?? rawTestStrategy) : rawTestStrategy;
|
|
144
148
|
const testStrategy: TestStrategy =
|
|
145
|
-
|
|
146
|
-
? (
|
|
149
|
+
normalizedStrategy !== undefined && (VALID_TEST_STRATEGIES as unknown[]).includes(normalizedStrategy)
|
|
150
|
+
? (normalizedStrategy as TestStrategy)
|
|
147
151
|
: "tdd-simple";
|
|
148
152
|
|
|
149
153
|
// dependencies
|