@nathapp/nax 0.54.4 → 0.54.6

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.
Files changed (2) hide show
  1. package/dist/nax.js +733 -630
  2. package/package.json +2 -2
package/dist/nax.js CHANGED
@@ -18945,7 +18945,7 @@ Rules:
18945
18945
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
18946
18946
  - **Prefer behavioral tests** \u2014 import functions and call them rather than reading source files. For example, to verify "getPostRunActions() returns empty array", import PluginRegistry and call getPostRunActions(), don't grep the source file for the method name.
18947
18947
  - **File output (REQUIRED)**: Write the acceptance test file DIRECTLY to the path shown below. Do NOT output the test code in your response. After writing the file, reply with a brief confirmation.
18948
- - **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${options.featureDir}/${acceptanceTestFilename(options.language)}\`. Import from package sources using relative paths like \`./src/...\`. No deep \`../../../../\` traversal needed.`;
18948
+ - **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join2(options.workdir, ".nax", "features", options.featureName, acceptanceTestFilename(options.language))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).`;
18949
18949
  const prompt = basePrompt;
18950
18950
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
18951
18951
  const rawOutput = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
@@ -18963,7 +18963,7 @@ Rules:
18963
18963
  outputPreview: rawOutput.slice(0, 300)
18964
18964
  });
18965
18965
  if (!testCode) {
18966
- const targetPath = join2(options.featureDir, acceptanceTestFilename(options.language));
18966
+ const targetPath = join2(options.workdir, ".nax", "features", options.featureName, acceptanceTestFilename(options.language));
18967
18967
  let recoveryFailed = false;
18968
18968
  logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", { targetPath });
18969
18969
  try {
@@ -19826,7 +19826,7 @@ async function saveAcpSession(workdir, featureName, storyId, sessionName, agentN
19826
19826
  getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
19827
19827
  }
19828
19828
  }
19829
- async function clearAcpSession(workdir, featureName, storyId) {
19829
+ async function clearAcpSession(workdir, featureName, storyId, sessionRole) {
19830
19830
  try {
19831
19831
  const path = acpSessionsPath(workdir, featureName);
19832
19832
  let data = {};
@@ -19836,7 +19836,8 @@ async function clearAcpSession(workdir, featureName, storyId) {
19836
19836
  } catch {
19837
19837
  return;
19838
19838
  }
19839
- delete data[storyId];
19839
+ const sidecarKey = sessionRole ? `${storyId}:${sessionRole}` : storyId;
19840
+ delete data[sidecarKey];
19840
19841
  await Bun.write(path, JSON.stringify(data, null, 2));
19841
19842
  } catch (err) {
19842
19843
  getSafeLogger()?.warn("acp-adapter", "Failed to clear session from sidecar", { error: String(err) });
@@ -20024,7 +20025,8 @@ class AcpAgentAdapter {
20024
20025
  await client.start();
20025
20026
  let sessionName = options.acpSessionName;
20026
20027
  if (!sessionName && options.featureName && options.storyId) {
20027
- sessionName = await readAcpSession(options.workdir, options.featureName, options.storyId) ?? undefined;
20028
+ const sidecarKey = options.sessionRole ? `${options.storyId}:${options.sessionRole}` : options.storyId;
20029
+ sessionName = await readAcpSession(options.workdir, options.featureName, sidecarKey) ?? undefined;
20028
20030
  }
20029
20031
  sessionName ??= buildSessionName(options.workdir, options.featureName, options.storyId, options.sessionRole);
20030
20032
  const resolvedPerm = resolvePermissions(options.config, options.pipelineStage ?? "run");
@@ -20035,7 +20037,8 @@ class AcpAgentAdapter {
20035
20037
  });
20036
20038
  const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
20037
20039
  if (options.featureName && options.storyId) {
20038
- await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName, this.name);
20040
+ const sidecarKey = options.sessionRole ? `${options.storyId}:${options.sessionRole}` : options.storyId;
20041
+ await saveAcpSession(options.workdir, options.featureName, sidecarKey, sessionName, this.name);
20039
20042
  }
20040
20043
  let lastResponse = null;
20041
20044
  let timedOut = false;
@@ -20072,7 +20075,8 @@ class AcpAgentAdapter {
20072
20075
  totalExactCostUsd = (totalExactCostUsd ?? 0) + lastResponse.exactCostUsd;
20073
20076
  }
20074
20077
  const outputText = extractOutput(lastResponse);
20075
- const question = extractQuestion(outputText);
20078
+ const isEndTurn = lastResponse.stopReason === "end_turn";
20079
+ const question = isEndTurn ? extractQuestion(outputText) : null;
20076
20080
  if (!question || !options.interactionBridge)
20077
20081
  break;
20078
20082
  getSafeLogger()?.debug("acp-adapter", "Agent asked question, routing to interactionBridge", { question });
@@ -22348,7 +22352,7 @@ var package_default;
22348
22352
  var init_package = __esm(() => {
22349
22353
  package_default = {
22350
22354
  name: "@nathapp/nax",
22351
- version: "0.54.4",
22355
+ version: "0.54.6",
22352
22356
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22353
22357
  type: "module",
22354
22358
  bin: {
@@ -22361,7 +22365,7 @@ var init_package = __esm(() => {
22361
22365
  typecheck: "bun x tsc --noEmit",
22362
22366
  lint: "bun x biome check src/ bin/",
22363
22367
  release: "bun scripts/release.ts",
22364
- test: "bun test test/ --timeout=60000",
22368
+ test: "bun test test/unit/ --timeout=60000 && bun test test/integration/ --timeout=60000 && bun test test/ui/ --timeout=60000",
22365
22369
  "test:watch": "bun test --watch",
22366
22370
  "test:unit": "bun test ./test/unit/ --timeout=60000",
22367
22371
  "test:integration": "bun test ./test/integration/ --timeout=60000",
@@ -22425,8 +22429,8 @@ var init_version = __esm(() => {
22425
22429
  NAX_VERSION = package_default.version;
22426
22430
  NAX_COMMIT = (() => {
22427
22431
  try {
22428
- if (/^[0-9a-f]{6,10}$/.test("d0da600"))
22429
- return "d0da600";
22432
+ if (/^[0-9a-f]{6,10}$/.test("955ab31"))
22433
+ return "955ab31";
22430
22434
  } catch {}
22431
22435
  try {
22432
22436
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22444,316 +22448,37 @@ var init_version = __esm(() => {
22444
22448
  NAX_BUILD_INFO = NAX_COMMIT === "dev" ? `v${NAX_VERSION}` : `v${NAX_VERSION} (${NAX_COMMIT})`;
22445
22449
  });
22446
22450
 
22447
- // src/prd/validate.ts
22448
- function validateStoryId(id) {
22449
- if (!id || id.length === 0) {
22450
- throw new Error("Story ID cannot be empty");
22451
- }
22452
- if (id.includes("..")) {
22453
- throw new Error("Story ID cannot contain path traversal (..)");
22454
- }
22455
- if (id.startsWith("--")) {
22456
- throw new Error("Story ID cannot start with git flags (--)");
22457
- }
22458
- const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
22459
- if (!validPattern.test(id)) {
22460
- throw new Error(`Story ID must match pattern [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}. Got: ${id}`);
22461
- }
22462
- }
22463
-
22464
- // src/errors.ts
22465
- var NaxError, AgentNotFoundError, AgentNotInstalledError, StoryLimitExceededError, LockAcquisitionError;
22466
- var init_errors3 = __esm(() => {
22467
- NaxError = class NaxError extends Error {
22468
- code;
22469
- context;
22470
- constructor(message, code, context) {
22471
- super(message);
22472
- this.code = code;
22473
- this.context = context;
22474
- this.name = "NaxError";
22475
- Error.captureStackTrace(this, this.constructor);
22476
- }
22477
- };
22478
- AgentNotFoundError = class AgentNotFoundError extends NaxError {
22479
- constructor(agentName, binary) {
22480
- super(`Agent "${agentName}" not found or not installed`, "AGENT_NOT_FOUND", { agentName, binary });
22481
- this.name = "AgentNotFoundError";
22482
- }
22483
- };
22484
- AgentNotInstalledError = class AgentNotInstalledError extends NaxError {
22485
- constructor(agentName, binary) {
22486
- super(`Agent "${agentName}" is not installed or not in PATH: ${binary}`, "AGENT_NOT_INSTALLED", {
22487
- agentName,
22488
- binary
22489
- });
22490
- this.name = "AgentNotInstalledError";
22491
- }
22492
- };
22493
- StoryLimitExceededError = class StoryLimitExceededError extends NaxError {
22494
- constructor(totalStories, limit) {
22495
- super(`Feature exceeds story limit: ${totalStories} stories (max: ${limit})`, "STORY_LIMIT_EXCEEDED", {
22496
- totalStories,
22497
- limit
22498
- });
22499
- this.name = "StoryLimitExceededError";
22500
- }
22501
- };
22502
- LockAcquisitionError = class LockAcquisitionError extends NaxError {
22503
- constructor(workdir) {
22504
- super("Another nax process is already running in this directory", "LOCK_ACQUISITION_FAILED", { workdir });
22505
- this.name = "LockAcquisitionError";
22506
- }
22507
- };
22508
- });
22509
-
22510
- // src/metrics/tracker.ts
22511
- import path2 from "path";
22512
- function collectStoryMetrics(ctx, storyStartTime) {
22513
- const story = ctx.story;
22514
- const routing = ctx.routing;
22515
- const agentResult = ctx.agentResult;
22516
- const escalationCount = story.escalations?.length || 0;
22517
- const priorFailureCount = story.priorFailures?.length || 0;
22518
- const attempts = priorFailureCount + Math.max(1, story.attempts || 1);
22519
- const finalTier = escalationCount > 0 ? story.escalations[escalationCount - 1].toTier : routing.modelTier;
22520
- const firstPassSuccess = agentResult?.success === true && escalationCount === 0 && priorFailureCount === 0;
22521
- const modelEntry = ctx.config.models[routing.modelTier];
22522
- const modelDef = modelEntry ? resolveModel(modelEntry) : null;
22523
- const modelUsed = modelDef?.model || routing.modelTier;
22524
- const initialComplexity = story.routing?.initialComplexity ?? routing.complexity;
22525
- const isTddStrategy = routing.testStrategy === "three-session-tdd" || routing.testStrategy === "three-session-tdd-lite";
22526
- const fullSuiteGatePassed = isTddStrategy ? ctx.fullSuiteGatePassed ?? false : false;
22451
+ // src/interaction/bridge-builder.ts
22452
+ function buildInteractionBridge(chain, context, timeoutMs = DEFAULT_INTERACTION_TIMEOUT_MS) {
22453
+ const plugin = chain?.getPrimary();
22454
+ if (!plugin)
22455
+ return;
22527
22456
  return {
22528
- storyId: story.id,
22529
- complexity: routing.complexity,
22530
- initialComplexity,
22531
- modelTier: routing.modelTier,
22532
- modelUsed,
22533
- attempts,
22534
- finalTier,
22535
- success: agentResult?.success || false,
22536
- cost: (ctx.accumulatedAttemptCost ?? 0) + (agentResult?.estimatedCost || 0),
22537
- durationMs: agentResult?.durationMs || 0,
22538
- firstPassSuccess,
22539
- startedAt: storyStartTime,
22540
- completedAt: new Date().toISOString(),
22541
- fullSuiteGatePassed,
22542
- runtimeCrashes: ctx.storyRuntimeCrashes ?? 0
22543
- };
22544
- }
22545
- function collectBatchMetrics(ctx, storyStartTime) {
22546
- const stories = ctx.stories;
22547
- const routing = ctx.routing;
22548
- const agentResult = ctx.agentResult;
22549
- const totalCost = agentResult?.estimatedCost || 0;
22550
- const totalDuration = agentResult?.durationMs || 0;
22551
- const costPerStory = totalCost / stories.length;
22552
- const durationPerStory = totalDuration / stories.length;
22553
- const modelEntry = ctx.config.models[routing.modelTier];
22554
- const modelDef = modelEntry ? resolveModel(modelEntry) : null;
22555
- const modelUsed = modelDef?.model || routing.modelTier;
22556
- return stories.map((story) => {
22557
- const initialComplexity = story.routing?.initialComplexity ?? routing.complexity;
22558
- return {
22559
- storyId: story.id,
22560
- complexity: routing.complexity,
22561
- initialComplexity,
22562
- modelTier: routing.modelTier,
22563
- modelUsed,
22564
- attempts: 1,
22565
- finalTier: routing.modelTier,
22566
- success: true,
22567
- cost: costPerStory,
22568
- durationMs: durationPerStory,
22569
- firstPassSuccess: true,
22570
- startedAt: storyStartTime,
22571
- completedAt: new Date().toISOString(),
22572
- fullSuiteGatePassed: false,
22573
- runtimeCrashes: 0
22574
- };
22575
- });
22576
- }
22577
- async function saveRunMetrics(workdir, runMetrics) {
22578
- const metricsPath = path2.join(workdir, ".nax", "metrics.json");
22579
- const existing = await loadJsonFile(metricsPath, "metrics");
22580
- const allMetrics = Array.isArray(existing) ? existing : [];
22581
- allMetrics.push(runMetrics);
22582
- await saveJsonFile(metricsPath, allMetrics, "metrics");
22583
- }
22584
- async function loadRunMetrics(workdir) {
22585
- const metricsPath = path2.join(workdir, ".nax", "metrics.json");
22586
- const content = await loadJsonFile(metricsPath, "metrics");
22587
- return Array.isArray(content) ? content : [];
22588
- }
22589
- var init_tracker = __esm(() => {
22590
- init_schema();
22591
- init_json_file();
22592
- });
22593
-
22594
- // src/metrics/aggregator.ts
22595
- function calculateAggregateMetrics(runs) {
22596
- if (runs.length === 0) {
22597
- return {
22598
- totalRuns: 0,
22599
- totalCost: 0,
22600
- totalStories: 0,
22601
- firstPassRate: 0,
22602
- escalationRate: 0,
22603
- avgCostPerStory: 0,
22604
- avgCostPerFeature: 0,
22605
- modelEfficiency: {},
22606
- complexityAccuracy: {}
22607
- };
22608
- }
22609
- const allStories = runs.flatMap((run) => run.stories);
22610
- const totalRuns = runs.length;
22611
- const totalCost = runs.reduce((sum, run) => sum + run.totalCost, 0);
22612
- const totalStories = allStories.length;
22613
- const firstPassSuccesses = allStories.filter((s) => s.firstPassSuccess).length;
22614
- const firstPassRate = totalStories > 0 ? firstPassSuccesses / totalStories : 0;
22615
- const escalatedStories = allStories.filter((s) => s.attempts > 1).length;
22616
- const escalationRate = totalStories > 0 ? escalatedStories / totalStories : 0;
22617
- const avgCostPerStory = totalStories > 0 ? totalCost / totalStories : 0;
22618
- const avgCostPerFeature = totalRuns > 0 ? totalCost / totalRuns : 0;
22619
- const modelStats = new Map;
22620
- for (const story of allStories) {
22621
- const modelKey = story.modelUsed;
22622
- const existing = modelStats.get(modelKey) || {
22623
- attempts: 0,
22624
- successes: 0,
22625
- totalCost: 0
22626
- };
22627
- modelStats.set(modelKey, {
22628
- attempts: existing.attempts + story.attempts,
22629
- successes: existing.successes + (story.success ? 1 : 0),
22630
- totalCost: existing.totalCost + story.cost
22631
- });
22632
- }
22633
- const modelEfficiency = {};
22634
- for (const [modelKey, stats] of modelStats) {
22635
- const passRate = stats.attempts > 0 ? stats.successes / stats.attempts : 0;
22636
- const avgCost = stats.successes > 0 ? stats.totalCost / stats.successes : 0;
22637
- modelEfficiency[modelKey] = {
22638
- attempts: stats.attempts,
22639
- successes: stats.successes,
22640
- passRate,
22641
- avgCost,
22642
- totalCost: stats.totalCost
22643
- };
22644
- }
22645
- const complexityStats = new Map;
22646
- for (const story of allStories) {
22647
- const complexity = story.initialComplexity ?? story.complexity;
22648
- const existing = complexityStats.get(complexity) || {
22649
- predicted: 0,
22650
- tierCounts: new Map,
22651
- mismatches: 0
22652
- };
22653
- existing.predicted += 1;
22654
- const finalTier = story.finalTier;
22655
- existing.tierCounts.set(finalTier, (existing.tierCounts.get(finalTier) || 0) + 1);
22656
- if (story.modelTier !== story.finalTier) {
22657
- existing.mismatches += 1;
22658
- }
22659
- complexityStats.set(complexity, existing);
22660
- }
22661
- const complexityAccuracy = {};
22662
- for (const [complexity, stats] of complexityStats) {
22663
- let maxCount = 0;
22664
- let mostCommonTier = "unknown";
22665
- for (const [tier, count] of stats.tierCounts) {
22666
- if (count > maxCount) {
22667
- maxCount = count;
22668
- mostCommonTier = tier;
22457
+ detectQuestion: async (text) => QUESTION_PATTERNS.some((p) => p.test(text)),
22458
+ onQuestionDetected: async (text) => {
22459
+ const requestId = `ix-${context.stage}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
22460
+ await plugin.send({
22461
+ id: requestId,
22462
+ type: "input",
22463
+ featureName: context.featureName ?? "unknown",
22464
+ storyId: context.storyId,
22465
+ stage: context.stage,
22466
+ summary: text,
22467
+ fallback: "continue",
22468
+ createdAt: Date.now()
22469
+ });
22470
+ try {
22471
+ const response = await plugin.receive(requestId, timeoutMs);
22472
+ return response.value ?? "continue";
22473
+ } catch {
22474
+ return "continue";
22669
22475
  }
22670
22476
  }
22671
- const mismatchRate = stats.predicted > 0 ? stats.mismatches / stats.predicted : 0;
22672
- complexityAccuracy[complexity] = {
22673
- predicted: stats.predicted,
22674
- actualTierUsed: mostCommonTier,
22675
- mismatchRate
22676
- };
22677
- }
22678
- return {
22679
- totalRuns,
22680
- totalCost,
22681
- totalStories,
22682
- firstPassRate,
22683
- escalationRate,
22684
- avgCostPerStory,
22685
- avgCostPerFeature,
22686
- modelEfficiency,
22687
- complexityAccuracy
22688
22477
  };
22689
22478
  }
22690
- function getLastRun(runs) {
22691
- if (runs.length === 0) {
22692
- return null;
22693
- }
22694
- return runs[runs.length - 1];
22695
- }
22696
-
22697
- // src/metrics/index.ts
22698
- var init_metrics = __esm(() => {
22699
- init_tracker();
22700
- });
22701
-
22702
- // src/interaction/types.ts
22703
- var TRIGGER_METADATA;
22704
- var init_types4 = __esm(() => {
22705
- TRIGGER_METADATA = {
22706
- "security-review": {
22707
- defaultFallback: "abort",
22708
- safety: "red",
22709
- defaultSummary: "Security review failed \u2014 abort execution?"
22710
- },
22711
- "cost-exceeded": {
22712
- defaultFallback: "abort",
22713
- safety: "red",
22714
- defaultSummary: "Cost limit exceeded ({{cost}} USD) \u2014 abort execution?"
22715
- },
22716
- "merge-conflict": {
22717
- defaultFallback: "abort",
22718
- safety: "red",
22719
- defaultSummary: "Merge conflict detected in {{storyId}} \u2014 abort execution?"
22720
- },
22721
- "cost-warning": {
22722
- defaultFallback: "escalate",
22723
- safety: "yellow",
22724
- defaultSummary: "Cost warning: {{cost}} USD / {{limit}} USD \u2014 escalate to higher tier?"
22725
- },
22726
- "max-retries": {
22727
- defaultFallback: "skip",
22728
- safety: "yellow",
22729
- defaultSummary: "Max retries reached for {{storyId}} \u2014 skip story?"
22730
- },
22731
- "pre-merge": {
22732
- defaultFallback: "escalate",
22733
- safety: "yellow",
22734
- defaultSummary: "Pre-merge checkpoint for {{storyId}} \u2014 proceed with merge?"
22735
- },
22736
- "human-review": {
22737
- defaultFallback: "skip",
22738
- safety: "yellow",
22739
- defaultSummary: "Human review required for story {{storyId}} \u2014 skip and continue?"
22740
- },
22741
- "story-oversized": {
22742
- defaultFallback: "continue",
22743
- safety: "yellow",
22744
- defaultSummary: "Story {{storyId}} is oversized ({{criteriaCount}} acceptance criteria) \u2014 decompose into smaller stories?"
22745
- },
22746
- "story-ambiguity": {
22747
- defaultFallback: "continue",
22748
- safety: "green",
22749
- defaultSummary: "Story {{storyId}} requirements unclear \u2014 continue with best effort?"
22750
- },
22751
- "review-gate": {
22752
- defaultFallback: "continue",
22753
- safety: "green",
22754
- defaultSummary: "Code review checkpoint for {{storyId}} \u2014 proceed?"
22755
- }
22756
- };
22479
+ var QUESTION_PATTERNS, DEFAULT_INTERACTION_TIMEOUT_MS = 120000;
22480
+ var init_bridge_builder = __esm(() => {
22481
+ QUESTION_PATTERNS = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
22757
22482
  });
22758
22483
 
22759
22484
  // src/interaction/chain.ts
@@ -22844,47 +22569,166 @@ class InteractionChain {
22844
22569
  }
22845
22570
  }
22846
22571
 
22847
- // src/interaction/state.ts
22848
- import * as path3 from "path";
22849
- async function loadPendingInteraction(requestId, featureDir) {
22850
- const interactionsDir = path3.join(featureDir, "interactions");
22851
- const filename = `${requestId}.json`;
22852
- const filePath = path3.join(interactionsDir, filename);
22853
- try {
22854
- const file2 = Bun.file(filePath);
22855
- const exists = await file2.exists();
22856
- if (!exists) {
22857
- return null;
22572
+ // src/interaction/plugins/auto.ts
22573
+ class AutoInteractionPlugin {
22574
+ name = "auto";
22575
+ config = {};
22576
+ async init(config2) {
22577
+ const cfg = AutoConfigSchema.parse(config2);
22578
+ this.config = {
22579
+ model: cfg.model ?? "fast",
22580
+ confidenceThreshold: cfg.confidenceThreshold ?? 0.7,
22581
+ maxCostPerDecision: cfg.maxCostPerDecision ?? 0.01,
22582
+ naxConfig: cfg.naxConfig
22583
+ };
22584
+ }
22585
+ async destroy() {}
22586
+ async send(request) {}
22587
+ async receive(_requestId, _timeout = 60000) {
22588
+ throw new Error("Auto plugin requires full request context (not just requestId)");
22589
+ }
22590
+ async decide(request) {
22591
+ if (request.metadata?.trigger === "security-review") {
22592
+ return;
22593
+ }
22594
+ try {
22595
+ if (_autoPluginDeps.callLlm) {
22596
+ const decision2 = await _autoPluginDeps.callLlm(request);
22597
+ if (decision2.confidence < (this.config.confidenceThreshold ?? 0.7)) {
22598
+ return;
22599
+ }
22600
+ return {
22601
+ requestId: request.id,
22602
+ action: decision2.action,
22603
+ value: decision2.value,
22604
+ respondedBy: "auto-ai",
22605
+ respondedAt: Date.now()
22606
+ };
22607
+ }
22608
+ const decision = await this.callLlm(request);
22609
+ if (decision.confidence < (this.config.confidenceThreshold ?? 0.7)) {
22610
+ return;
22611
+ }
22612
+ return {
22613
+ requestId: request.id,
22614
+ action: decision.action,
22615
+ value: decision.value,
22616
+ respondedBy: "auto-ai",
22617
+ respondedAt: Date.now()
22618
+ };
22619
+ } catch (err) {
22620
+ return;
22858
22621
  }
22859
- const json2 = await file2.text();
22860
- const request = JSON.parse(json2);
22861
- return request;
22862
- } catch {
22863
- return null;
22864
22622
  }
22865
- }
22866
- async function listPendingInteractions(featureDir) {
22867
- const interactionsDir = path3.join(featureDir, "interactions");
22868
- try {
22869
- const dir = Bun.file(interactionsDir);
22870
- const exists = await dir.exists();
22871
- if (!exists) {
22872
- return [];
22623
+ async callLlm(request) {
22624
+ const prompt = this.buildPrompt(request);
22625
+ const adapter = _autoPluginDeps.adapter;
22626
+ if (!adapter) {
22627
+ throw new Error("Auto plugin requires adapter to be injected via _autoPluginDeps.adapter");
22873
22628
  }
22874
- const proc = Bun.spawn(["ls", interactionsDir], {
22875
- stdout: "pipe",
22876
- stderr: "pipe"
22629
+ let modelArg;
22630
+ if (this.config.naxConfig) {
22631
+ const modelTier = this.config.model ?? "fast";
22632
+ const modelEntry = this.config.naxConfig.models[modelTier];
22633
+ if (!modelEntry) {
22634
+ throw new Error(`Model tier "${modelTier}" not found in config.models`);
22635
+ }
22636
+ const modelDef = resolveModel(modelEntry);
22637
+ modelArg = modelDef.model;
22638
+ }
22639
+ const output = await adapter.complete(prompt, {
22640
+ ...modelArg && { model: modelArg },
22641
+ jsonMode: true,
22642
+ ...this.config.naxConfig && { config: this.config.naxConfig },
22643
+ featureName: request.featureName,
22644
+ storyId: request.storyId,
22645
+ sessionRole: "auto"
22877
22646
  });
22878
- const output = await new Response(proc.stdout).text();
22879
- await proc.exited;
22880
- const files = output.split(`
22881
- `).filter((f) => f.endsWith(".json") && f !== ".gitkeep").map((f) => f.replace(".json", ""));
22882
- return files;
22883
- } catch {
22884
- return [];
22647
+ return this.parseResponse(output);
22648
+ }
22649
+ buildPrompt(request) {
22650
+ let prompt = `You are an AI decision assistant for a code orchestration system. Given an interaction request, decide the best action.
22651
+
22652
+ ## Interaction Request
22653
+ Type: ${request.type}
22654
+ Stage: ${request.stage}
22655
+ Feature: ${request.featureName}
22656
+ ${request.storyId ? `Story: ${request.storyId}` : ""}
22657
+
22658
+ Summary: ${request.summary.replace(/`/g, "\\`").replace(/\$/g, "\\$")}
22659
+ ${request.detail ? `
22660
+ Detail: ${request.detail.replace(/`/g, "\\`").replace(/\$/g, "\\$")}` : ""}
22661
+ `;
22662
+ if (request.options && request.options.length > 0) {
22663
+ prompt += `
22664
+ Options:
22665
+ `;
22666
+ for (const opt of request.options) {
22667
+ const desc = opt.description ? ` \u2014 ${opt.description}` : "";
22668
+ prompt += ` [${opt.key}] ${opt.label}${desc}
22669
+ `;
22670
+ }
22671
+ }
22672
+ prompt += `
22673
+ Fallback behavior on timeout: ${request.fallback}
22674
+ Safety tier: ${request.metadata?.safety ?? "unknown"}
22675
+
22676
+ ## Available Actions
22677
+ - approve: Proceed with the operation
22678
+ - reject: Deny the operation
22679
+ - choose: Select an option (requires value field)
22680
+ - input: Provide text input (requires value field)
22681
+ - skip: Skip this interaction
22682
+ - abort: Abort execution
22683
+
22684
+ ## Rules
22685
+ 1. For "red" safety tier (security-review, cost-exceeded, merge-conflict): ALWAYS return confidence 0 to escalate to human
22686
+ 2. For "yellow" safety tier (cost-warning, max-retries, pre-merge): High confidence (0.8+) ONLY if clearly safe
22687
+ 3. For "green" safety tier (story-ambiguity, review-gate): Can approve with moderate confidence (0.6+)
22688
+ 4. Default to the fallback behavior if unsure
22689
+ 5. Never auto-approve security issues
22690
+ 6. If the summary mentions "critical" or "security", confidence MUST be < 0.5
22691
+
22692
+ Respond with ONLY this JSON (no markdown, no explanation):
22693
+ {"action":"approve|reject|choose|input|skip|abort","value":"<optional>","confidence":0.0-1.0,"reasoning":"<one line>"}`;
22694
+ return prompt;
22695
+ }
22696
+ parseResponse(output) {
22697
+ let jsonText = output.trim();
22698
+ if (jsonText.startsWith("```")) {
22699
+ const lines = jsonText.split(`
22700
+ `);
22701
+ jsonText = lines.slice(1, -1).join(`
22702
+ `).trim();
22703
+ }
22704
+ if (jsonText.startsWith("json")) {
22705
+ jsonText = jsonText.slice(4).trim();
22706
+ }
22707
+ const parsed = JSON.parse(jsonText);
22708
+ if (!parsed.action || parsed.confidence === undefined || !parsed.reasoning) {
22709
+ throw new Error(`Invalid LLM response: ${jsonText}`);
22710
+ }
22711
+ if (parsed.confidence < 0 || parsed.confidence > 1) {
22712
+ throw new Error(`Invalid confidence: ${parsed.confidence} (must be 0-1)`);
22713
+ }
22714
+ return parsed;
22885
22715
  }
22886
22716
  }
22887
- var init_state = () => {};
22717
+ var AutoConfigSchema, _autoPluginDeps;
22718
+ var init_auto = __esm(() => {
22719
+ init_zod();
22720
+ init_config();
22721
+ AutoConfigSchema = exports_external.object({
22722
+ model: exports_external.string().optional(),
22723
+ confidenceThreshold: exports_external.number().min(0).max(1).optional(),
22724
+ maxCostPerDecision: exports_external.number().positive().optional(),
22725
+ naxConfig: exports_external.any().optional()
22726
+ });
22727
+ _autoPluginDeps = {
22728
+ adapter: null,
22729
+ callLlm: null
22730
+ };
22731
+ });
22888
22732
 
22889
22733
  // src/interaction/plugins/cli.ts
22890
22734
  import * as readline from "readline";
@@ -22953,9 +22797,9 @@ ${request.summary}
22953
22797
  if (!this.rl) {
22954
22798
  throw new Error("CLI plugin not initialized");
22955
22799
  }
22956
- const timeoutPromise = new Promise((resolve5) => {
22800
+ const timeoutPromise = new Promise((resolve4) => {
22957
22801
  setTimeout(() => {
22958
- resolve5({
22802
+ resolve4({
22959
22803
  requestId: request.id,
22960
22804
  action: "skip",
22961
22805
  respondedBy: "timeout",
@@ -23107,9 +22951,9 @@ ${request.summary}
23107
22951
  if (!this.rl) {
23108
22952
  throw new Error("CLI plugin not initialized");
23109
22953
  }
23110
- return new Promise((resolve5) => {
22954
+ return new Promise((resolve4) => {
23111
22955
  this.rl?.question(prompt, (answer) => {
23112
- resolve5(answer);
22956
+ resolve4(answer);
23113
22957
  });
23114
22958
  });
23115
22959
  }
@@ -23506,10 +23350,10 @@ class WebhookInteractionPlugin {
23506
23350
  this.pendingResponses.delete(requestId);
23507
23351
  return early;
23508
23352
  }
23509
- return new Promise((resolve5) => {
23353
+ return new Promise((resolve4) => {
23510
23354
  const timer = setTimeout(() => {
23511
23355
  this.receiveCallbacks.delete(requestId);
23512
- resolve5({
23356
+ resolve4({
23513
23357
  requestId,
23514
23358
  action: "skip",
23515
23359
  respondedBy: "timeout",
@@ -23519,7 +23363,7 @@ class WebhookInteractionPlugin {
23519
23363
  this.receiveCallbacks.set(requestId, (response) => {
23520
23364
  clearTimeout(timer);
23521
23365
  this.receiveCallbacks.delete(requestId);
23522
- resolve5(response);
23366
+ resolve4(response);
23523
23367
  });
23524
23368
  });
23525
23369
  }
@@ -23640,167 +23484,414 @@ var init_webhook = __esm(() => {
23640
23484
  });
23641
23485
  });
23642
23486
 
23643
- // src/interaction/plugins/auto.ts
23644
- class AutoInteractionPlugin {
23645
- name = "auto";
23646
- config = {};
23647
- async init(config2) {
23648
- const cfg = AutoConfigSchema.parse(config2);
23649
- this.config = {
23650
- model: cfg.model ?? "fast",
23651
- confidenceThreshold: cfg.confidenceThreshold ?? 0.7,
23652
- maxCostPerDecision: cfg.maxCostPerDecision ?? 0.01,
23653
- naxConfig: cfg.naxConfig
23654
- };
23487
+ // src/interaction/init.ts
23488
+ function createInteractionPlugin(pluginName) {
23489
+ switch (pluginName) {
23490
+ case "cli":
23491
+ return new CLIInteractionPlugin;
23492
+ case "telegram":
23493
+ return new TelegramInteractionPlugin;
23494
+ case "webhook":
23495
+ return new WebhookInteractionPlugin;
23496
+ case "auto":
23497
+ return new AutoInteractionPlugin;
23498
+ default:
23499
+ throw new Error(`Unknown interaction plugin: ${pluginName}`);
23655
23500
  }
23656
- async destroy() {}
23657
- async send(request) {}
23658
- async receive(_requestId, _timeout = 60000) {
23659
- throw new Error("Auto plugin requires full request context (not just requestId)");
23501
+ }
23502
+ async function initInteractionChain(config2, headless) {
23503
+ const logger = getSafeLogger();
23504
+ if (!config2.interaction) {
23505
+ logger?.debug("interaction", "No interaction config - skipping interaction system");
23506
+ return null;
23660
23507
  }
23661
- async decide(request) {
23662
- if (request.metadata?.trigger === "security-review") {
23663
- return;
23664
- }
23665
- try {
23666
- if (_autoPluginDeps.callLlm) {
23667
- const decision2 = await _autoPluginDeps.callLlm(request);
23668
- if (decision2.confidence < (this.config.confidenceThreshold ?? 0.7)) {
23669
- return;
23670
- }
23671
- return {
23672
- requestId: request.id,
23673
- action: decision2.action,
23674
- value: decision2.value,
23675
- respondedBy: "auto-ai",
23676
- respondedAt: Date.now()
23677
- };
23678
- }
23679
- const decision = await this.callLlm(request);
23680
- if (decision.confidence < (this.config.confidenceThreshold ?? 0.7)) {
23681
- return;
23682
- }
23683
- return {
23684
- requestId: request.id,
23685
- action: decision.action,
23686
- value: decision.value,
23687
- respondedBy: "auto-ai",
23688
- respondedAt: Date.now()
23689
- };
23690
- } catch (err) {
23691
- return;
23692
- }
23508
+ const pluginName = config2.interaction.plugin;
23509
+ if (headless && pluginName === "cli") {
23510
+ logger?.debug("interaction", "Headless mode with CLI plugin - skipping interaction system (stdin unavailable)");
23511
+ return null;
23693
23512
  }
23694
- async callLlm(request) {
23695
- const prompt = this.buildPrompt(request);
23696
- const adapter = _autoPluginDeps.adapter;
23697
- if (!adapter) {
23698
- throw new Error("Auto plugin requires adapter to be injected via _autoPluginDeps.adapter");
23699
- }
23700
- let modelArg;
23701
- if (this.config.naxConfig) {
23702
- const modelTier = this.config.model ?? "fast";
23703
- const modelEntry = this.config.naxConfig.models[modelTier];
23704
- if (!modelEntry) {
23705
- throw new Error(`Model tier "${modelTier}" not found in config.models`);
23706
- }
23707
- const modelDef = resolveModel(modelEntry);
23708
- modelArg = modelDef.model;
23709
- }
23710
- const output = await adapter.complete(prompt, {
23711
- ...modelArg && { model: modelArg },
23712
- jsonMode: true,
23713
- ...this.config.naxConfig && { config: this.config.naxConfig },
23714
- featureName: request.featureName,
23715
- storyId: request.storyId,
23716
- sessionRole: "auto"
23513
+ const chain = new InteractionChain({
23514
+ defaultTimeout: config2.interaction.defaults.timeout,
23515
+ defaultFallback: config2.interaction.defaults.fallback
23516
+ });
23517
+ try {
23518
+ const plugin = createInteractionPlugin(pluginName);
23519
+ chain.register(plugin, 100);
23520
+ const pluginConfig = config2.interaction.config ?? {};
23521
+ await chain.init({ [pluginName]: pluginConfig });
23522
+ logger?.info("interaction", `Initialized ${pluginName} interaction plugin`, {
23523
+ timeout: config2.interaction.defaults.timeout,
23524
+ fallback: config2.interaction.defaults.fallback
23717
23525
  });
23718
- return this.parseResponse(output);
23526
+ return chain;
23527
+ } catch (err) {
23528
+ const error48 = err instanceof Error ? err.message : String(err);
23529
+ logger?.error("interaction", `Failed to initialize interaction plugin: ${error48}`);
23530
+ throw err;
23719
23531
  }
23720
- buildPrompt(request) {
23721
- let prompt = `You are an AI decision assistant for a code orchestration system. Given an interaction request, decide the best action.
23532
+ }
23533
+ var init_init = __esm(() => {
23534
+ init_logger2();
23535
+ init_auto();
23536
+ init_cli();
23537
+ init_telegram();
23538
+ init_webhook();
23539
+ });
23722
23540
 
23723
- ## Interaction Request
23724
- Type: ${request.type}
23725
- Stage: ${request.stage}
23726
- Feature: ${request.featureName}
23727
- ${request.storyId ? `Story: ${request.storyId}` : ""}
23541
+ // src/prd/validate.ts
23542
+ function validateStoryId(id) {
23543
+ if (!id || id.length === 0) {
23544
+ throw new Error("Story ID cannot be empty");
23545
+ }
23546
+ if (id.includes("..")) {
23547
+ throw new Error("Story ID cannot contain path traversal (..)");
23548
+ }
23549
+ if (id.startsWith("--")) {
23550
+ throw new Error("Story ID cannot start with git flags (--)");
23551
+ }
23552
+ const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
23553
+ if (!validPattern.test(id)) {
23554
+ throw new Error(`Story ID must match pattern [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}. Got: ${id}`);
23555
+ }
23556
+ }
23728
23557
 
23729
- Summary: ${request.summary.replace(/`/g, "\\`").replace(/\$/g, "\\$")}
23730
- ${request.detail ? `
23731
- Detail: ${request.detail.replace(/`/g, "\\`").replace(/\$/g, "\\$")}` : ""}
23732
- `;
23733
- if (request.options && request.options.length > 0) {
23734
- prompt += `
23735
- Options:
23736
- `;
23737
- for (const opt of request.options) {
23738
- const desc = opt.description ? ` \u2014 ${opt.description}` : "";
23739
- prompt += ` [${opt.key}] ${opt.label}${desc}
23740
- `;
23741
- }
23558
+ // src/errors.ts
23559
+ var NaxError, AgentNotFoundError, AgentNotInstalledError, StoryLimitExceededError, LockAcquisitionError;
23560
+ var init_errors3 = __esm(() => {
23561
+ NaxError = class NaxError extends Error {
23562
+ code;
23563
+ context;
23564
+ constructor(message, code, context) {
23565
+ super(message);
23566
+ this.code = code;
23567
+ this.context = context;
23568
+ this.name = "NaxError";
23569
+ Error.captureStackTrace(this, this.constructor);
23742
23570
  }
23743
- prompt += `
23744
- Fallback behavior on timeout: ${request.fallback}
23745
- Safety tier: ${request.metadata?.safety ?? "unknown"}
23746
-
23747
- ## Available Actions
23748
- - approve: Proceed with the operation
23749
- - reject: Deny the operation
23750
- - choose: Select an option (requires value field)
23751
- - input: Provide text input (requires value field)
23752
- - skip: Skip this interaction
23753
- - abort: Abort execution
23571
+ };
23572
+ AgentNotFoundError = class AgentNotFoundError extends NaxError {
23573
+ constructor(agentName, binary) {
23574
+ super(`Agent "${agentName}" not found or not installed`, "AGENT_NOT_FOUND", { agentName, binary });
23575
+ this.name = "AgentNotFoundError";
23576
+ }
23577
+ };
23578
+ AgentNotInstalledError = class AgentNotInstalledError extends NaxError {
23579
+ constructor(agentName, binary) {
23580
+ super(`Agent "${agentName}" is not installed or not in PATH: ${binary}`, "AGENT_NOT_INSTALLED", {
23581
+ agentName,
23582
+ binary
23583
+ });
23584
+ this.name = "AgentNotInstalledError";
23585
+ }
23586
+ };
23587
+ StoryLimitExceededError = class StoryLimitExceededError extends NaxError {
23588
+ constructor(totalStories, limit) {
23589
+ super(`Feature exceeds story limit: ${totalStories} stories (max: ${limit})`, "STORY_LIMIT_EXCEEDED", {
23590
+ totalStories,
23591
+ limit
23592
+ });
23593
+ this.name = "StoryLimitExceededError";
23594
+ }
23595
+ };
23596
+ LockAcquisitionError = class LockAcquisitionError extends NaxError {
23597
+ constructor(workdir) {
23598
+ super("Another nax process is already running in this directory", "LOCK_ACQUISITION_FAILED", { workdir });
23599
+ this.name = "LockAcquisitionError";
23600
+ }
23601
+ };
23602
+ });
23754
23603
 
23755
- ## Rules
23756
- 1. For "red" safety tier (security-review, cost-exceeded, merge-conflict): ALWAYS return confidence 0 to escalate to human
23757
- 2. For "yellow" safety tier (cost-warning, max-retries, pre-merge): High confidence (0.8+) ONLY if clearly safe
23758
- 3. For "green" safety tier (story-ambiguity, review-gate): Can approve with moderate confidence (0.6+)
23759
- 4. Default to the fallback behavior if unsure
23760
- 5. Never auto-approve security issues
23761
- 6. If the summary mentions "critical" or "security", confidence MUST be < 0.5
23604
+ // src/metrics/tracker.ts
23605
+ import path2 from "path";
23606
+ function collectStoryMetrics(ctx, storyStartTime) {
23607
+ const story = ctx.story;
23608
+ const routing = ctx.routing;
23609
+ const agentResult = ctx.agentResult;
23610
+ const escalationCount = story.escalations?.length || 0;
23611
+ const priorFailureCount = story.priorFailures?.length || 0;
23612
+ const attempts = priorFailureCount + Math.max(1, story.attempts || 1);
23613
+ const finalTier = escalationCount > 0 ? story.escalations[escalationCount - 1].toTier : routing.modelTier;
23614
+ const firstPassSuccess = agentResult?.success === true && escalationCount === 0 && priorFailureCount === 0;
23615
+ const modelEntry = ctx.config.models[routing.modelTier];
23616
+ const modelDef = modelEntry ? resolveModel(modelEntry) : null;
23617
+ const modelUsed = modelDef?.model || routing.modelTier;
23618
+ const initialComplexity = story.routing?.initialComplexity ?? routing.complexity;
23619
+ const isTddStrategy = routing.testStrategy === "three-session-tdd" || routing.testStrategy === "three-session-tdd-lite";
23620
+ const fullSuiteGatePassed = isTddStrategy ? ctx.fullSuiteGatePassed ?? false : false;
23621
+ return {
23622
+ storyId: story.id,
23623
+ complexity: routing.complexity,
23624
+ initialComplexity,
23625
+ modelTier: routing.modelTier,
23626
+ modelUsed,
23627
+ attempts,
23628
+ finalTier,
23629
+ success: agentResult?.success || false,
23630
+ cost: (ctx.accumulatedAttemptCost ?? 0) + (agentResult?.estimatedCost || 0),
23631
+ durationMs: agentResult?.durationMs || 0,
23632
+ firstPassSuccess,
23633
+ startedAt: storyStartTime,
23634
+ completedAt: new Date().toISOString(),
23635
+ fullSuiteGatePassed,
23636
+ runtimeCrashes: ctx.storyRuntimeCrashes ?? 0
23637
+ };
23638
+ }
23639
+ function collectBatchMetrics(ctx, storyStartTime) {
23640
+ const stories = ctx.stories;
23641
+ const routing = ctx.routing;
23642
+ const agentResult = ctx.agentResult;
23643
+ const totalCost = agentResult?.estimatedCost || 0;
23644
+ const totalDuration = agentResult?.durationMs || 0;
23645
+ const costPerStory = totalCost / stories.length;
23646
+ const durationPerStory = totalDuration / stories.length;
23647
+ const modelEntry = ctx.config.models[routing.modelTier];
23648
+ const modelDef = modelEntry ? resolveModel(modelEntry) : null;
23649
+ const modelUsed = modelDef?.model || routing.modelTier;
23650
+ return stories.map((story) => {
23651
+ const initialComplexity = story.routing?.initialComplexity ?? routing.complexity;
23652
+ return {
23653
+ storyId: story.id,
23654
+ complexity: routing.complexity,
23655
+ initialComplexity,
23656
+ modelTier: routing.modelTier,
23657
+ modelUsed,
23658
+ attempts: 1,
23659
+ finalTier: routing.modelTier,
23660
+ success: true,
23661
+ cost: costPerStory,
23662
+ durationMs: durationPerStory,
23663
+ firstPassSuccess: true,
23664
+ startedAt: storyStartTime,
23665
+ completedAt: new Date().toISOString(),
23666
+ fullSuiteGatePassed: false,
23667
+ runtimeCrashes: 0
23668
+ };
23669
+ });
23670
+ }
23671
+ async function saveRunMetrics(workdir, runMetrics) {
23672
+ const metricsPath = path2.join(workdir, ".nax", "metrics.json");
23673
+ const existing = await loadJsonFile(metricsPath, "metrics");
23674
+ const allMetrics = Array.isArray(existing) ? existing : [];
23675
+ allMetrics.push(runMetrics);
23676
+ await saveJsonFile(metricsPath, allMetrics, "metrics");
23677
+ }
23678
+ async function loadRunMetrics(workdir) {
23679
+ const metricsPath = path2.join(workdir, ".nax", "metrics.json");
23680
+ const content = await loadJsonFile(metricsPath, "metrics");
23681
+ return Array.isArray(content) ? content : [];
23682
+ }
23683
+ var init_tracker = __esm(() => {
23684
+ init_schema();
23685
+ init_json_file();
23686
+ });
23762
23687
 
23763
- Respond with ONLY this JSON (no markdown, no explanation):
23764
- {"action":"approve|reject|choose|input|skip|abort","value":"<optional>","confidence":0.0-1.0,"reasoning":"<one line>"}`;
23765
- return prompt;
23688
+ // src/metrics/aggregator.ts
23689
+ function calculateAggregateMetrics(runs) {
23690
+ if (runs.length === 0) {
23691
+ return {
23692
+ totalRuns: 0,
23693
+ totalCost: 0,
23694
+ totalStories: 0,
23695
+ firstPassRate: 0,
23696
+ escalationRate: 0,
23697
+ avgCostPerStory: 0,
23698
+ avgCostPerFeature: 0,
23699
+ modelEfficiency: {},
23700
+ complexityAccuracy: {}
23701
+ };
23766
23702
  }
23767
- parseResponse(output) {
23768
- let jsonText = output.trim();
23769
- if (jsonText.startsWith("```")) {
23770
- const lines = jsonText.split(`
23771
- `);
23772
- jsonText = lines.slice(1, -1).join(`
23773
- `).trim();
23774
- }
23775
- if (jsonText.startsWith("json")) {
23776
- jsonText = jsonText.slice(4).trim();
23777
- }
23778
- const parsed = JSON.parse(jsonText);
23779
- if (!parsed.action || parsed.confidence === undefined || !parsed.reasoning) {
23780
- throw new Error(`Invalid LLM response: ${jsonText}`);
23703
+ const allStories = runs.flatMap((run) => run.stories);
23704
+ const totalRuns = runs.length;
23705
+ const totalCost = runs.reduce((sum, run) => sum + run.totalCost, 0);
23706
+ const totalStories = allStories.length;
23707
+ const firstPassSuccesses = allStories.filter((s) => s.firstPassSuccess).length;
23708
+ const firstPassRate = totalStories > 0 ? firstPassSuccesses / totalStories : 0;
23709
+ const escalatedStories = allStories.filter((s) => s.attempts > 1).length;
23710
+ const escalationRate = totalStories > 0 ? escalatedStories / totalStories : 0;
23711
+ const avgCostPerStory = totalStories > 0 ? totalCost / totalStories : 0;
23712
+ const avgCostPerFeature = totalRuns > 0 ? totalCost / totalRuns : 0;
23713
+ const modelStats = new Map;
23714
+ for (const story of allStories) {
23715
+ const modelKey = story.modelUsed;
23716
+ const existing = modelStats.get(modelKey) || {
23717
+ attempts: 0,
23718
+ successes: 0,
23719
+ totalCost: 0
23720
+ };
23721
+ modelStats.set(modelKey, {
23722
+ attempts: existing.attempts + story.attempts,
23723
+ successes: existing.successes + (story.success ? 1 : 0),
23724
+ totalCost: existing.totalCost + story.cost
23725
+ });
23726
+ }
23727
+ const modelEfficiency = {};
23728
+ for (const [modelKey, stats] of modelStats) {
23729
+ const passRate = stats.attempts > 0 ? stats.successes / stats.attempts : 0;
23730
+ const avgCost = stats.successes > 0 ? stats.totalCost / stats.successes : 0;
23731
+ modelEfficiency[modelKey] = {
23732
+ attempts: stats.attempts,
23733
+ successes: stats.successes,
23734
+ passRate,
23735
+ avgCost,
23736
+ totalCost: stats.totalCost
23737
+ };
23738
+ }
23739
+ const complexityStats = new Map;
23740
+ for (const story of allStories) {
23741
+ const complexity = story.initialComplexity ?? story.complexity;
23742
+ const existing = complexityStats.get(complexity) || {
23743
+ predicted: 0,
23744
+ tierCounts: new Map,
23745
+ mismatches: 0
23746
+ };
23747
+ existing.predicted += 1;
23748
+ const finalTier = story.finalTier;
23749
+ existing.tierCounts.set(finalTier, (existing.tierCounts.get(finalTier) || 0) + 1);
23750
+ if (story.modelTier !== story.finalTier) {
23751
+ existing.mismatches += 1;
23781
23752
  }
23782
- if (parsed.confidence < 0 || parsed.confidence > 1) {
23783
- throw new Error(`Invalid confidence: ${parsed.confidence} (must be 0-1)`);
23753
+ complexityStats.set(complexity, existing);
23754
+ }
23755
+ const complexityAccuracy = {};
23756
+ for (const [complexity, stats] of complexityStats) {
23757
+ let maxCount = 0;
23758
+ let mostCommonTier = "unknown";
23759
+ for (const [tier, count] of stats.tierCounts) {
23760
+ if (count > maxCount) {
23761
+ maxCount = count;
23762
+ mostCommonTier = tier;
23763
+ }
23784
23764
  }
23785
- return parsed;
23765
+ const mismatchRate = stats.predicted > 0 ? stats.mismatches / stats.predicted : 0;
23766
+ complexityAccuracy[complexity] = {
23767
+ predicted: stats.predicted,
23768
+ actualTierUsed: mostCommonTier,
23769
+ mismatchRate
23770
+ };
23786
23771
  }
23772
+ return {
23773
+ totalRuns,
23774
+ totalCost,
23775
+ totalStories,
23776
+ firstPassRate,
23777
+ escalationRate,
23778
+ avgCostPerStory,
23779
+ avgCostPerFeature,
23780
+ modelEfficiency,
23781
+ complexityAccuracy
23782
+ };
23787
23783
  }
23788
- var AutoConfigSchema, _autoPluginDeps;
23789
- var init_auto = __esm(() => {
23790
- init_zod();
23791
- init_config();
23792
- AutoConfigSchema = exports_external.object({
23793
- model: exports_external.string().optional(),
23794
- confidenceThreshold: exports_external.number().min(0).max(1).optional(),
23795
- maxCostPerDecision: exports_external.number().positive().optional(),
23796
- naxConfig: exports_external.any().optional()
23797
- });
23798
- _autoPluginDeps = {
23799
- adapter: null,
23800
- callLlm: null
23784
+ function getLastRun(runs) {
23785
+ if (runs.length === 0) {
23786
+ return null;
23787
+ }
23788
+ return runs[runs.length - 1];
23789
+ }
23790
+
23791
+ // src/metrics/index.ts
23792
+ var init_metrics = __esm(() => {
23793
+ init_tracker();
23794
+ });
23795
+
23796
+ // src/interaction/types.ts
23797
+ var TRIGGER_METADATA;
23798
+ var init_types4 = __esm(() => {
23799
+ TRIGGER_METADATA = {
23800
+ "security-review": {
23801
+ defaultFallback: "abort",
23802
+ safety: "red",
23803
+ defaultSummary: "Security review failed \u2014 abort execution?"
23804
+ },
23805
+ "cost-exceeded": {
23806
+ defaultFallback: "abort",
23807
+ safety: "red",
23808
+ defaultSummary: "Cost limit exceeded ({{cost}} USD) \u2014 abort execution?"
23809
+ },
23810
+ "merge-conflict": {
23811
+ defaultFallback: "abort",
23812
+ safety: "red",
23813
+ defaultSummary: "Merge conflict detected in {{storyId}} \u2014 abort execution?"
23814
+ },
23815
+ "cost-warning": {
23816
+ defaultFallback: "escalate",
23817
+ safety: "yellow",
23818
+ defaultSummary: "Cost warning: {{cost}} USD / {{limit}} USD \u2014 escalate to higher tier?"
23819
+ },
23820
+ "max-retries": {
23821
+ defaultFallback: "skip",
23822
+ safety: "yellow",
23823
+ defaultSummary: "Max retries reached for {{storyId}} \u2014 skip story?"
23824
+ },
23825
+ "pre-merge": {
23826
+ defaultFallback: "escalate",
23827
+ safety: "yellow",
23828
+ defaultSummary: "Pre-merge checkpoint for {{storyId}} \u2014 proceed with merge?"
23829
+ },
23830
+ "human-review": {
23831
+ defaultFallback: "skip",
23832
+ safety: "yellow",
23833
+ defaultSummary: "Human review required for story {{storyId}} \u2014 skip and continue?"
23834
+ },
23835
+ "story-oversized": {
23836
+ defaultFallback: "continue",
23837
+ safety: "yellow",
23838
+ defaultSummary: "Story {{storyId}} is oversized ({{criteriaCount}} acceptance criteria) \u2014 decompose into smaller stories?"
23839
+ },
23840
+ "story-ambiguity": {
23841
+ defaultFallback: "continue",
23842
+ safety: "green",
23843
+ defaultSummary: "Story {{storyId}} requirements unclear \u2014 continue with best effort?"
23844
+ },
23845
+ "review-gate": {
23846
+ defaultFallback: "continue",
23847
+ safety: "green",
23848
+ defaultSummary: "Code review checkpoint for {{storyId}} \u2014 proceed?"
23849
+ }
23801
23850
  };
23802
23851
  });
23803
23852
 
23853
+ // src/interaction/state.ts
23854
+ import * as path3 from "path";
23855
+ async function loadPendingInteraction(requestId, featureDir) {
23856
+ const interactionsDir = path3.join(featureDir, "interactions");
23857
+ const filename = `${requestId}.json`;
23858
+ const filePath = path3.join(interactionsDir, filename);
23859
+ try {
23860
+ const file2 = Bun.file(filePath);
23861
+ const exists = await file2.exists();
23862
+ if (!exists) {
23863
+ return null;
23864
+ }
23865
+ const json2 = await file2.text();
23866
+ const request = JSON.parse(json2);
23867
+ return request;
23868
+ } catch {
23869
+ return null;
23870
+ }
23871
+ }
23872
+ async function listPendingInteractions(featureDir) {
23873
+ const interactionsDir = path3.join(featureDir, "interactions");
23874
+ try {
23875
+ const dir = Bun.file(interactionsDir);
23876
+ const exists = await dir.exists();
23877
+ if (!exists) {
23878
+ return [];
23879
+ }
23880
+ const proc = Bun.spawn(["ls", interactionsDir], {
23881
+ stdout: "pipe",
23882
+ stderr: "pipe"
23883
+ });
23884
+ const output = await new Response(proc.stdout).text();
23885
+ await proc.exited;
23886
+ const files = output.split(`
23887
+ `).filter((f) => f.endsWith(".json") && f !== ".gitkeep").map((f) => f.replace(".json", ""));
23888
+ return files;
23889
+ } catch {
23890
+ return [];
23891
+ }
23892
+ }
23893
+ var init_state = () => {};
23894
+
23804
23895
  // src/interaction/triggers.ts
23805
23896
  function isTriggerEnabled(trigger, config2) {
23806
23897
  const triggerConfig = config2.interaction?.triggers?.[trigger];
@@ -23924,60 +24015,6 @@ var init_triggers = __esm(() => {
23924
24015
  init_types4();
23925
24016
  });
23926
24017
 
23927
- // src/interaction/init.ts
23928
- function createInteractionPlugin(pluginName) {
23929
- switch (pluginName) {
23930
- case "cli":
23931
- return new CLIInteractionPlugin;
23932
- case "telegram":
23933
- return new TelegramInteractionPlugin;
23934
- case "webhook":
23935
- return new WebhookInteractionPlugin;
23936
- case "auto":
23937
- return new AutoInteractionPlugin;
23938
- default:
23939
- throw new Error(`Unknown interaction plugin: ${pluginName}`);
23940
- }
23941
- }
23942
- async function initInteractionChain(config2, headless) {
23943
- const logger = getSafeLogger();
23944
- if (!config2.interaction) {
23945
- logger?.debug("interaction", "No interaction config - skipping interaction system");
23946
- return null;
23947
- }
23948
- const pluginName = config2.interaction.plugin;
23949
- if (headless && pluginName === "cli") {
23950
- logger?.debug("interaction", "Headless mode with CLI plugin - skipping interaction system (stdin unavailable)");
23951
- return null;
23952
- }
23953
- const chain = new InteractionChain({
23954
- defaultTimeout: config2.interaction.defaults.timeout,
23955
- defaultFallback: config2.interaction.defaults.fallback
23956
- });
23957
- try {
23958
- const plugin = createInteractionPlugin(pluginName);
23959
- chain.register(plugin, 100);
23960
- const pluginConfig = config2.interaction.config ?? {};
23961
- await chain.init({ [pluginName]: pluginConfig });
23962
- logger?.info("interaction", `Initialized ${pluginName} interaction plugin`, {
23963
- timeout: config2.interaction.defaults.timeout,
23964
- fallback: config2.interaction.defaults.fallback
23965
- });
23966
- return chain;
23967
- } catch (err) {
23968
- const error48 = err instanceof Error ? err.message : String(err);
23969
- logger?.error("interaction", `Failed to initialize interaction plugin: ${error48}`);
23970
- throw err;
23971
- }
23972
- }
23973
- var init_init = __esm(() => {
23974
- init_logger2();
23975
- init_auto();
23976
- init_cli();
23977
- init_telegram();
23978
- init_webhook();
23979
- });
23980
-
23981
24018
  // src/interaction/index.ts
23982
24019
  var init_interaction = __esm(() => {
23983
24020
  init_types4();
@@ -23988,6 +24025,7 @@ var init_interaction = __esm(() => {
23988
24025
  init_auto();
23989
24026
  init_triggers();
23990
24027
  init_init();
24028
+ init_bridge_builder();
23991
24029
  });
23992
24030
 
23993
24031
  // src/pipeline/runner.ts
@@ -24446,10 +24484,11 @@ ${stderr}` };
24446
24484
  if (workdirGroups.size === 0) {
24447
24485
  workdirGroups.set("", { stories: [], criteria: [] });
24448
24486
  }
24487
+ const featureName = ctx.prd.feature;
24449
24488
  const testPaths = [];
24450
24489
  for (const [workdir] of workdirGroups) {
24451
24490
  const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
24452
- const testPath = path5.join(packageDir, acceptanceTestFilename(language));
24491
+ const testPath = path5.join(packageDir, ".nax", "features", featureName, acceptanceTestFilename(language));
24453
24492
  testPaths.push({ testPath, packageDir });
24454
24493
  }
24455
24494
  let totalCriteria = 0;
@@ -27429,6 +27468,9 @@ function shouldRetryRectification(state, config2) {
27429
27468
  if (state.attempt >= config2.maxRetries) {
27430
27469
  return false;
27431
27470
  }
27471
+ if (state.lastExitCode !== undefined && state.lastExitCode !== 0 && state.currentFailures === 0) {
27472
+ return true;
27473
+ }
27432
27474
  if (state.currentFailures === 0) {
27433
27475
  return false;
27434
27476
  }
@@ -28397,7 +28439,7 @@ async function rollbackToRef(workdir, ref) {
28397
28439
  }
28398
28440
  logger.info("tdd", "Successfully rolled back git changes", { ref });
28399
28441
  }
28400
- async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
28442
+ async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName, interactionBridge) {
28401
28443
  const startTime = Date.now();
28402
28444
  let prompt;
28403
28445
  if (_sessionRunnerDeps.buildPrompt) {
@@ -28431,7 +28473,8 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
28431
28473
  featureName,
28432
28474
  storyId: story.id,
28433
28475
  sessionRole: role,
28434
- keepSessionOpen
28476
+ keepSessionOpen,
28477
+ interactionBridge
28435
28478
  });
28436
28479
  if (!result.success && result.pid) {
28437
28480
  await _sessionRunnerDeps.cleanupProcessTree(result.pid);
@@ -28796,7 +28839,8 @@ async function runThreeSessionTdd(options) {
28796
28839
  constitution,
28797
28840
  dryRun = false,
28798
28841
  lite = false,
28799
- _recursionDepth = 0
28842
+ _recursionDepth = 0,
28843
+ interactionChain
28800
28844
  } = options;
28801
28845
  const logger = getLogger();
28802
28846
  const MAX_RECURSION_DEPTH = 2;
@@ -28855,7 +28899,7 @@ async function runThreeSessionTdd(options) {
28855
28899
  let session1;
28856
28900
  if (!isRetry) {
28857
28901
  const testWriterTier = config2.tdd.sessionTiers?.testWriter ?? "balanced";
28858
- session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution, featureName);
28902
+ session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution, featureName, buildInteractionBridge(interactionChain, { featureName, storyId: story.id, stage: "execution" }));
28859
28903
  sessions.push(session1);
28860
28904
  }
28861
28905
  if (session1 && !session1.success) {
@@ -28917,7 +28961,7 @@ async function runThreeSessionTdd(options) {
28917
28961
  });
28918
28962
  const session2Ref = await captureGitRef(workdir) ?? "HEAD";
28919
28963
  const implementerTier = config2.tdd.sessionTiers?.implementer ?? modelTier;
28920
- const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution, featureName);
28964
+ const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution, featureName, buildInteractionBridge(interactionChain, { featureName, storyId: story.id, stage: "execution" }));
28921
28965
  sessions.push(session2);
28922
28966
  if (!session2.success) {
28923
28967
  needsHumanReview = true;
@@ -29035,6 +29079,7 @@ async function runThreeSessionTdd(options) {
29035
29079
  var init_orchestrator2 = __esm(() => {
29036
29080
  init_config();
29037
29081
  init_greenfield();
29082
+ init_bridge_builder();
29038
29083
  init_logger2();
29039
29084
  init_git();
29040
29085
  init_verification();
@@ -29100,6 +29145,7 @@ var executionStage, _executionDeps;
29100
29145
  var init_execution2 = __esm(() => {
29101
29146
  init_agents();
29102
29147
  init_config();
29148
+ init_bridge_builder();
29103
29149
  init_triggers();
29104
29150
  init_logger2();
29105
29151
  init_tdd();
@@ -29134,7 +29180,8 @@ var init_execution2 = __esm(() => {
29134
29180
  contextMarkdown: ctx.contextMarkdown,
29135
29181
  constitution: ctx.constitution?.content,
29136
29182
  dryRun: false,
29137
- lite: isLiteMode
29183
+ lite: isLiteMode,
29184
+ interactionChain: ctx.interaction
29138
29185
  });
29139
29186
  ctx.agentResult = {
29140
29187
  success: tddResult.success,
@@ -29208,34 +29255,11 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
29208
29255
  pidRegistry: ctx.pidRegistry,
29209
29256
  featureName: ctx.prd.feature,
29210
29257
  storyId: ctx.story.id,
29211
- interactionBridge: (() => {
29212
- const plugin = ctx.interaction?.getPrimary();
29213
- if (!plugin)
29214
- return;
29215
- const QUESTION_PATTERNS = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
29216
- return {
29217
- detectQuestion: async (text) => QUESTION_PATTERNS.some((p) => p.test(text)),
29218
- onQuestionDetected: async (text) => {
29219
- const requestId = `ix-acp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
29220
- await plugin.send({
29221
- id: requestId,
29222
- type: "input",
29223
- featureName: ctx.prd.feature,
29224
- storyId: ctx.story.id,
29225
- stage: "execution",
29226
- summary: text,
29227
- fallback: "continue",
29228
- createdAt: Date.now()
29229
- });
29230
- try {
29231
- const response = await plugin.receive(requestId, 120000);
29232
- return response.value ?? "continue";
29233
- } catch {
29234
- return "continue";
29235
- }
29236
- }
29237
- };
29238
- })()
29258
+ interactionBridge: buildInteractionBridge(ctx.interaction, {
29259
+ featureName: ctx.prd.feature,
29260
+ storyId: ctx.story.id,
29261
+ stage: "execution"
29262
+ })
29239
29263
  });
29240
29264
  ctx.agentResult = result;
29241
29265
  await autoCommitIfDirty(storyWorkdir, "execution", "single-session", ctx.story.id);
@@ -29737,7 +29761,8 @@ async function runRectificationLoop2(opts) {
29737
29761
  const rectificationState = {
29738
29762
  attempt: 0,
29739
29763
  initialFailures: testSummary.failed,
29740
- currentFailures: testSummary.failed
29764
+ currentFailures: testSummary.failed,
29765
+ lastExitCode: 1
29741
29766
  };
29742
29767
  logger?.info("rectification", `Starting ${label} loop`, {
29743
29768
  storyId: story.id,
@@ -29815,6 +29840,7 @@ ${rectificationPrompt}`;
29815
29840
  if (retryVerification.output) {
29816
29841
  const newTestSummary = parseBunTestOutput(retryVerification.output);
29817
29842
  rectificationState.currentFailures = newTestSummary.failed;
29843
+ rectificationState.lastExitCode = retryVerification.status === "SUCCESS" ? 0 : 1;
29818
29844
  testSummary.failures = newTestSummary.failures;
29819
29845
  testSummary.failed = newTestSummary.failed;
29820
29846
  testSummary.passed = newTestSummary.passed;
@@ -30023,7 +30049,8 @@ function makeFailResult(storyId, strategy, status, opts = {}) {
30023
30049
  failures: opts.failures ?? [],
30024
30050
  rawOutput: opts.rawOutput,
30025
30051
  durationMs: opts.durationMs ?? 0,
30026
- countsTowardEscalation: opts.countsTowardEscalation ?? true
30052
+ countsTowardEscalation: opts.countsTowardEscalation ?? true,
30053
+ exitCode: opts.exitCode
30027
30054
  };
30028
30055
  }
30029
30056
  function makePassResult(storyId, strategy, opts = {}) {
@@ -30432,7 +30459,8 @@ class ScopedStrategy {
30432
30459
  passCount: parsed.passed,
30433
30460
  failCount: parsed.failed,
30434
30461
  failures: parsed.failures,
30435
- durationMs
30462
+ durationMs,
30463
+ exitCode: result.status === "TEST_FAILURE" ? 1 : undefined
30436
30464
  });
30437
30465
  }
30438
30466
  }
@@ -32238,7 +32266,8 @@ async function checkGitignoreCoversNax(workdir) {
32238
32266
  ".nax/features/*/status.json",
32239
32267
  ".nax-pids",
32240
32268
  ".nax-wt/",
32241
- "**/.nax-acceptance*"
32269
+ "**/.nax-acceptance*",
32270
+ "**/.nax/features/*/"
32242
32271
  ];
32243
32272
  const missing = patterns.filter((pattern) => !content.includes(pattern));
32244
32273
  const passed = missing.length === 0;
@@ -34280,7 +34309,7 @@ async function executeStoryInWorktree(story, worktreePath, context, routing, eve
34280
34309
  };
34281
34310
  }
34282
34311
  }
34283
- async function executeParallelBatch(stories, projectRoot, config2, context, worktreePaths, maxConcurrency, eventEmitter) {
34312
+ async function executeParallelBatch(stories, projectRoot, config2, context, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs) {
34284
34313
  const logger = getSafeLogger();
34285
34314
  const results = {
34286
34315
  pipelinePassed: [],
@@ -34301,7 +34330,9 @@ async function executeParallelBatch(stories, projectRoot, config2, context, work
34301
34330
  continue;
34302
34331
  }
34303
34332
  const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
34304
- const executePromise = executeStoryInWorktree(story, worktreePath, context, routing, eventEmitter).then((result) => {
34333
+ const storyConfig = storyEffectiveConfigs?.get(story.id);
34334
+ const storyContext = storyConfig ? { ...context, effectiveConfig: storyConfig } : context;
34335
+ const executePromise = executeStoryInWorktree(story, worktreePath, storyContext, routing, eventEmitter).then((result) => {
34305
34336
  results.totalCost += result.cost;
34306
34337
  results.storyCosts.set(story.id, result.cost);
34307
34338
  if (result.success) {
@@ -34336,6 +34367,7 @@ var init_parallel_worker = __esm(() => {
34336
34367
  });
34337
34368
 
34338
34369
  // src/execution/parallel-coordinator.ts
34370
+ import { existsSync as existsSync33, symlinkSync as symlinkSync2 } from "fs";
34339
34371
  import os3 from "os";
34340
34372
  import { join as join48 } from "path";
34341
34373
  function groupStoriesByDependencies(stories) {
@@ -34380,7 +34412,7 @@ function resolveMaxConcurrency(parallel) {
34380
34412
  }
34381
34413
  return Math.max(1, parallel);
34382
34414
  }
34383
- async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn) {
34415
+ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn, pidRegistry, interactionChain) {
34384
34416
  const logger = getSafeLogger();
34385
34417
  const maxConcurrency = resolveMaxConcurrency(parallel);
34386
34418
  const worktreeManager = new WorktreeManager;
@@ -34412,9 +34444,12 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34412
34444
  hooks,
34413
34445
  plugins,
34414
34446
  storyStartTime: new Date().toISOString(),
34415
- agentGetFn
34447
+ agentGetFn,
34448
+ pidRegistry,
34449
+ interaction: interactionChain ?? undefined
34416
34450
  };
34417
34451
  const worktreePaths = new Map;
34452
+ const storyEffectiveConfigs = new Map;
34418
34453
  for (const story of batch) {
34419
34454
  const worktreePath = join48(projectRoot, ".nax-wt", story.id);
34420
34455
  try {
@@ -34424,6 +34459,27 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34424
34459
  storyId: story.id,
34425
34460
  worktreePath
34426
34461
  });
34462
+ if (story.workdir) {
34463
+ const pkgNodeModulesSrc = join48(projectRoot, story.workdir, "node_modules");
34464
+ const pkgNodeModulesDst = join48(worktreePath, story.workdir, "node_modules");
34465
+ if (existsSync33(pkgNodeModulesSrc) && !existsSync33(pkgNodeModulesDst)) {
34466
+ try {
34467
+ symlinkSync2(pkgNodeModulesSrc, pkgNodeModulesDst, "dir");
34468
+ logger?.debug("parallel", "Symlinked package node_modules", {
34469
+ storyId: story.id,
34470
+ src: pkgNodeModulesSrc
34471
+ });
34472
+ } catch (symlinkError) {
34473
+ logger?.warn("parallel", "Failed to symlink package node_modules \u2014 test runner may not find deps", {
34474
+ storyId: story.id,
34475
+ error: errorMessage(symlinkError)
34476
+ });
34477
+ }
34478
+ }
34479
+ }
34480
+ const rootConfigPath = join48(projectRoot, ".nax", "config.json");
34481
+ const effectiveConfig = story.workdir ? await loadConfigForWorkdir(rootConfigPath, story.workdir) : config2;
34482
+ storyEffectiveConfigs.set(story.id, effectiveConfig);
34427
34483
  } catch (error48) {
34428
34484
  markStoryFailed(currentPrd, story.id, undefined, undefined);
34429
34485
  logger?.error("parallel", "Failed to create worktree", {
@@ -34432,7 +34488,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34432
34488
  });
34433
34489
  }
34434
34490
  }
34435
- const batchResult = await executeParallelBatch(batch, projectRoot, config2, baseContext, worktreePaths, maxConcurrency, eventEmitter);
34491
+ const batchResult = await executeParallelBatch(batch, projectRoot, config2, baseContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs);
34436
34492
  totalCost += batchResult.totalCost;
34437
34493
  if (batchResult.pipelinePassed.length > 0) {
34438
34494
  const successfulIds = batchResult.pipelinePassed.map((s) => s.id);
@@ -34502,6 +34558,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34502
34558
  return { storiesCompleted, totalCost, updatedPrd: currentPrd, mergeConflicts: allMergeConflicts };
34503
34559
  }
34504
34560
  var init_parallel_coordinator = __esm(() => {
34561
+ init_loader();
34505
34562
  init_logger2();
34506
34563
  init_prd();
34507
34564
  init_manager();
@@ -34789,7 +34846,7 @@ async function runParallelExecution(options, initialPrd) {
34789
34846
  const batchStoryMetrics = [];
34790
34847
  let conflictedStories = [];
34791
34848
  try {
34792
- const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn);
34849
+ const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn, options.pidRegistry, options.interactionChain);
34793
34850
  const batchDurationMs = Date.now() - batchStartMs;
34794
34851
  const batchCompletedAt = new Date().toISOString();
34795
34852
  prd = parallelResult.updatedPrd;
@@ -67794,7 +67851,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
67794
67851
 
67795
67852
  // bin/nax.ts
67796
67853
  init_source();
67797
- import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
67854
+ import { existsSync as existsSync35, mkdirSync as mkdirSync6 } from "fs";
67798
67855
  import { homedir as homedir8 } from "os";
67799
67856
  import { join as join56 } from "path";
67800
67857
 
@@ -68283,7 +68340,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
68283
68340
  init_registry();
68284
68341
  import { existsSync as existsSync11 } from "fs";
68285
68342
  import { join as join11 } from "path";
68286
- import { createInterface } from "readline";
68343
+ import { createInterface as createInterface2 } from "readline";
68287
68344
  init_test_strategy();
68288
68345
 
68289
68346
  // src/context/generator.ts
@@ -68821,6 +68878,8 @@ async function generateForPackage(packageDir, config2, dryRun = false, repoRoot)
68821
68878
 
68822
68879
  // src/cli/plan.ts
68823
68880
  init_pid_registry();
68881
+ init_bridge_builder();
68882
+ init_init();
68824
68883
  init_logger2();
68825
68884
 
68826
68885
  // src/prd/schema.ts
@@ -69015,7 +69074,8 @@ var _planDeps = {
69015
69074
  existsSync: (path) => existsSync11(path),
69016
69075
  discoverWorkspacePackages: (repoRoot) => discoverWorkspacePackages(repoRoot),
69017
69076
  readPackageJsonAt: (path) => Bun.file(path).json().catch(() => null),
69018
- createInteractionBridge: () => createCliInteractionBridge()
69077
+ createInteractionBridge: () => createCliInteractionBridge(),
69078
+ initInteractionChain: (cfg, headless) => initInteractionChain(cfg, headless)
69019
69079
  };
69020
69080
  async function planCommand(workdir, config2, options) {
69021
69081
  const naxDir = join11(workdir, ".nax");
@@ -69046,9 +69106,10 @@ async function planCommand(workdir, config2, options) {
69046
69106
  const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
69047
69107
  let rawResponse;
69048
69108
  if (options.auto) {
69049
- const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
69050
- const cliAdapter = _planDeps.getAgent(agentName);
69051
- if (!cliAdapter)
69109
+ const isAcp = config2?.agent?.protocol === "acp";
69110
+ const prompt = buildPlanningPrompt(specContent, codebaseContext, isAcp ? outputPath : undefined, relativePackages, packageDetails, config2?.project);
69111
+ const adapter = _planDeps.getAgent(agentName, config2);
69112
+ if (!adapter)
69052
69113
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
69053
69114
  let autoModel;
69054
69115
  try {
@@ -69059,26 +69120,64 @@ async function planCommand(workdir, config2, options) {
69059
69120
  if (entry)
69060
69121
  autoModel = resolveModel2(entry).model;
69061
69122
  } catch {}
69062
- rawResponse = await cliAdapter.complete(prompt, {
69063
- model: autoModel,
69064
- jsonMode: true,
69065
- workdir,
69066
- config: config2,
69067
- featureName: options.feature,
69068
- sessionRole: "plan"
69069
- });
69070
- try {
69071
- const envelope = JSON.parse(rawResponse);
69072
- if (envelope?.type === "result" && typeof envelope?.result === "string") {
69073
- rawResponse = envelope.result;
69123
+ if (isAcp) {
69124
+ logger?.info("plan", "Starting ACP auto planning session", {
69125
+ agent: agentName,
69126
+ model: autoModel ?? config2?.plan?.model ?? "balanced",
69127
+ workdir,
69128
+ feature: options.feature,
69129
+ timeoutSeconds
69130
+ });
69131
+ const pidRegistry = new PidRegistry(workdir);
69132
+ try {
69133
+ await adapter.plan({
69134
+ prompt,
69135
+ workdir,
69136
+ interactive: false,
69137
+ timeoutSeconds,
69138
+ config: config2,
69139
+ modelTier: config2?.plan?.model ?? "balanced",
69140
+ dangerouslySkipPermissions: resolvePermissions(config2, "plan").skipPermissions,
69141
+ maxInteractionTurns: config2?.agent?.maxInteractionTurns,
69142
+ featureName: options.feature,
69143
+ pidRegistry,
69144
+ sessionRole: "plan"
69145
+ });
69146
+ } finally {
69147
+ await pidRegistry.killAll().catch(() => {});
69074
69148
  }
69075
- } catch {}
69149
+ if (!_planDeps.existsSync(outputPath)) {
69150
+ throw new Error(`[plan] ACP agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
69151
+ }
69152
+ rawResponse = await _planDeps.readFile(outputPath);
69153
+ } else {
69154
+ rawResponse = await adapter.complete(prompt, {
69155
+ model: autoModel,
69156
+ jsonMode: true,
69157
+ workdir,
69158
+ config: config2,
69159
+ featureName: options.feature,
69160
+ sessionRole: "plan"
69161
+ });
69162
+ try {
69163
+ const envelope = JSON.parse(rawResponse);
69164
+ if (envelope?.type === "result" && typeof envelope?.result === "string") {
69165
+ rawResponse = envelope.result;
69166
+ }
69167
+ } catch {}
69168
+ }
69076
69169
  } else {
69077
69170
  const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails, config2?.project);
69078
69171
  const adapter = _planDeps.getAgent(agentName, config2);
69079
69172
  if (!adapter)
69080
69173
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
69081
- const interactionBridge = _planDeps.createInteractionBridge();
69174
+ const headless = !process.stdin.isTTY;
69175
+ const interactionChain = config2 ? await _planDeps.initInteractionChain(config2, headless) : null;
69176
+ const configuredBridge = interactionChain ? buildInteractionBridge(interactionChain, {
69177
+ featureName: options.feature,
69178
+ stage: "pre-flight"
69179
+ }) : undefined;
69180
+ const interactionBridge = configuredBridge ?? _planDeps.createInteractionBridge();
69082
69181
  const pidRegistry = new PidRegistry(workdir);
69083
69182
  const resolvedPerm = resolvePermissions(config2, "plan");
69084
69183
  const resolvedModel = config2?.plan?.model ?? "balanced";
@@ -69107,6 +69206,8 @@ async function planCommand(workdir, config2, options) {
69107
69206
  });
69108
69207
  } finally {
69109
69208
  await pidRegistry.killAll().catch(() => {});
69209
+ if (interactionChain)
69210
+ await interactionChain.destroy().catch(() => {});
69110
69211
  logger?.info("plan", "Interactive session ended", { durationMs: Date.now() - planStartTime });
69111
69212
  }
69112
69213
  if (!_planDeps.existsSync(outputPath)) {
@@ -69133,7 +69234,7 @@ function createCliInteractionBridge() {
69133
69234
  \uD83E\uDD16 Agent: ${text}
69134
69235
  You: `);
69135
69236
  return new Promise((resolve4) => {
69136
- const rl = createInterface({ input: process.stdin, terminal: false });
69237
+ const rl = createInterface2({ input: process.stdin, terminal: false });
69137
69238
  rl.once("line", (line) => {
69138
69239
  rl.close();
69139
69240
  resolve4(line.trim());
@@ -72087,7 +72188,9 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
72087
72188
  pluginRegistry,
72088
72189
  formatterMode: options.formatterMode,
72089
72190
  headless: options.headless,
72090
- agentGetFn: options.agentGetFn
72191
+ agentGetFn: options.agentGetFn,
72192
+ pidRegistry: options.pidRegistry,
72193
+ interactionChain: options.interactionChain
72091
72194
  }, prd);
72092
72195
  prd = parallelResult.prd;
72093
72196
  totalCost = parallelResult.totalCost;
@@ -79683,7 +79786,7 @@ Next: nax generate --package ${options.package}`));
79683
79786
  return;
79684
79787
  }
79685
79788
  const naxDir = join56(workdir, ".nax");
79686
- if (existsSync34(naxDir) && !options.force) {
79789
+ if (existsSync35(naxDir) && !options.force) {
79687
79790
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
79688
79791
  return;
79689
79792
  }
@@ -79800,7 +79903,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79800
79903
  console.error(source_default.red("Error: --plan requires --from <spec-path>"));
79801
79904
  process.exit(1);
79802
79905
  }
79803
- if (options.from && !existsSync34(options.from)) {
79906
+ if (options.from && !existsSync35(options.from)) {
79804
79907
  console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
79805
79908
  process.exit(1);
79806
79909
  }
@@ -79832,7 +79935,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79832
79935
  const featureDir = join56(naxDir, "features", options.feature);
79833
79936
  const prdPath = join56(featureDir, "prd.json");
79834
79937
  if (options.plan && options.from) {
79835
- if (existsSync34(prdPath) && !options.force) {
79938
+ if (existsSync35(prdPath) && !options.force) {
79836
79939
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
79837
79940
  console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
79838
79941
  process.exit(1);
@@ -79888,7 +79991,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79888
79991
  process.exit(1);
79889
79992
  }
79890
79993
  }
79891
- if (!existsSync34(prdPath)) {
79994
+ if (!existsSync35(prdPath)) {
79892
79995
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
79893
79996
  process.exit(1);
79894
79997
  }
@@ -79963,7 +80066,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79963
80066
  });
79964
80067
  const latestSymlink = join56(runsDir, "latest.jsonl");
79965
80068
  try {
79966
- if (existsSync34(latestSymlink)) {
80069
+ if (existsSync35(latestSymlink)) {
79967
80070
  Bun.spawnSync(["rm", latestSymlink]);
79968
80071
  }
79969
80072
  Bun.spawnSync(["ln", "-s", `${runId}.jsonl`, latestSymlink], {
@@ -80061,7 +80164,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
80061
80164
  process.exit(1);
80062
80165
  }
80063
80166
  const featuresDir = join56(naxDir, "features");
80064
- if (!existsSync34(featuresDir)) {
80167
+ if (!existsSync35(featuresDir)) {
80065
80168
  console.log(source_default.dim("No features yet."));
80066
80169
  return;
80067
80170
  }
@@ -80076,7 +80179,7 @@ Features:
80076
80179
  `));
80077
80180
  for (const name of entries) {
80078
80181
  const prdPath = join56(featuresDir, name, "prd.json");
80079
- if (existsSync34(prdPath)) {
80182
+ if (existsSync35(prdPath)) {
80080
80183
  const prd = await loadPRD(prdPath);
80081
80184
  const c = countStories(prd);
80082
80185
  console.log(` ${name} \u2014 ${c.passed}/${c.total} stories done`);
@@ -80147,7 +80250,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
80147
80250
  process.exit(1);
80148
80251
  }
80149
80252
  const featureDir = join56(naxDir, "features", options.feature);
80150
- if (!existsSync34(featureDir)) {
80253
+ if (!existsSync35(featureDir)) {
80151
80254
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
80152
80255
  process.exit(1);
80153
80256
  }