@nathapp/nax 0.54.4 → 0.54.5

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 +705 -676
  2. package/package.json +1 -1
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 {
@@ -22348,7 +22348,7 @@ var package_default;
22348
22348
  var init_package = __esm(() => {
22349
22349
  package_default = {
22350
22350
  name: "@nathapp/nax",
22351
- version: "0.54.4",
22351
+ version: "0.54.5",
22352
22352
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22353
22353
  type: "module",
22354
22354
  bin: {
@@ -22425,8 +22425,8 @@ var init_version = __esm(() => {
22425
22425
  NAX_VERSION = package_default.version;
22426
22426
  NAX_COMMIT = (() => {
22427
22427
  try {
22428
- if (/^[0-9a-f]{6,10}$/.test("d0da600"))
22429
- return "d0da600";
22428
+ if (/^[0-9a-f]{6,10}$/.test("e98d5b1"))
22429
+ return "e98d5b1";
22430
22430
  } catch {}
22431
22431
  try {
22432
22432
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22444,316 +22444,37 @@ var init_version = __esm(() => {
22444
22444
  NAX_BUILD_INFO = NAX_COMMIT === "dev" ? `v${NAX_VERSION}` : `v${NAX_VERSION} (${NAX_COMMIT})`;
22445
22445
  });
22446
22446
 
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;
22447
+ // src/interaction/bridge-builder.ts
22448
+ function buildInteractionBridge(chain, context, timeoutMs = DEFAULT_INTERACTION_TIMEOUT_MS) {
22449
+ const plugin = chain?.getPrimary();
22450
+ if (!plugin)
22451
+ return;
22527
22452
  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;
22453
+ detectQuestion: async (text) => QUESTION_PATTERNS.some((p) => p.test(text)),
22454
+ onQuestionDetected: async (text) => {
22455
+ const requestId = `ix-${context.stage}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
22456
+ await plugin.send({
22457
+ id: requestId,
22458
+ type: "input",
22459
+ featureName: context.featureName ?? "unknown",
22460
+ storyId: context.storyId,
22461
+ stage: context.stage,
22462
+ summary: text,
22463
+ fallback: "continue",
22464
+ createdAt: Date.now()
22465
+ });
22466
+ try {
22467
+ const response = await plugin.receive(requestId, timeoutMs);
22468
+ return response.value ?? "continue";
22469
+ } catch {
22470
+ return "continue";
22669
22471
  }
22670
22472
  }
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
22473
  };
22689
22474
  }
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
- };
22475
+ var QUESTION_PATTERNS, DEFAULT_INTERACTION_TIMEOUT_MS = 120000;
22476
+ var init_bridge_builder = __esm(() => {
22477
+ QUESTION_PATTERNS = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
22757
22478
  });
22758
22479
 
22759
22480
  // src/interaction/chain.ts
@@ -22844,47 +22565,166 @@ class InteractionChain {
22844
22565
  }
22845
22566
  }
22846
22567
 
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;
22568
+ // src/interaction/plugins/auto.ts
22569
+ class AutoInteractionPlugin {
22570
+ name = "auto";
22571
+ config = {};
22572
+ async init(config2) {
22573
+ const cfg = AutoConfigSchema.parse(config2);
22574
+ this.config = {
22575
+ model: cfg.model ?? "fast",
22576
+ confidenceThreshold: cfg.confidenceThreshold ?? 0.7,
22577
+ maxCostPerDecision: cfg.maxCostPerDecision ?? 0.01,
22578
+ naxConfig: cfg.naxConfig
22579
+ };
22580
+ }
22581
+ async destroy() {}
22582
+ async send(request) {}
22583
+ async receive(_requestId, _timeout = 60000) {
22584
+ throw new Error("Auto plugin requires full request context (not just requestId)");
22585
+ }
22586
+ async decide(request) {
22587
+ if (request.metadata?.trigger === "security-review") {
22588
+ return;
22589
+ }
22590
+ try {
22591
+ if (_autoPluginDeps.callLlm) {
22592
+ const decision2 = await _autoPluginDeps.callLlm(request);
22593
+ if (decision2.confidence < (this.config.confidenceThreshold ?? 0.7)) {
22594
+ return;
22595
+ }
22596
+ return {
22597
+ requestId: request.id,
22598
+ action: decision2.action,
22599
+ value: decision2.value,
22600
+ respondedBy: "auto-ai",
22601
+ respondedAt: Date.now()
22602
+ };
22603
+ }
22604
+ const decision = await this.callLlm(request);
22605
+ if (decision.confidence < (this.config.confidenceThreshold ?? 0.7)) {
22606
+ return;
22607
+ }
22608
+ return {
22609
+ requestId: request.id,
22610
+ action: decision.action,
22611
+ value: decision.value,
22612
+ respondedBy: "auto-ai",
22613
+ respondedAt: Date.now()
22614
+ };
22615
+ } catch (err) {
22616
+ return;
22858
22617
  }
22859
- const json2 = await file2.text();
22860
- const request = JSON.parse(json2);
22861
- return request;
22862
- } catch {
22863
- return null;
22864
22618
  }
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 [];
22619
+ async callLlm(request) {
22620
+ const prompt = this.buildPrompt(request);
22621
+ const adapter = _autoPluginDeps.adapter;
22622
+ if (!adapter) {
22623
+ throw new Error("Auto plugin requires adapter to be injected via _autoPluginDeps.adapter");
22873
22624
  }
22874
- const proc = Bun.spawn(["ls", interactionsDir], {
22875
- stdout: "pipe",
22876
- stderr: "pipe"
22625
+ let modelArg;
22626
+ if (this.config.naxConfig) {
22627
+ const modelTier = this.config.model ?? "fast";
22628
+ const modelEntry = this.config.naxConfig.models[modelTier];
22629
+ if (!modelEntry) {
22630
+ throw new Error(`Model tier "${modelTier}" not found in config.models`);
22631
+ }
22632
+ const modelDef = resolveModel(modelEntry);
22633
+ modelArg = modelDef.model;
22634
+ }
22635
+ const output = await adapter.complete(prompt, {
22636
+ ...modelArg && { model: modelArg },
22637
+ jsonMode: true,
22638
+ ...this.config.naxConfig && { config: this.config.naxConfig },
22639
+ featureName: request.featureName,
22640
+ storyId: request.storyId,
22641
+ sessionRole: "auto"
22877
22642
  });
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 [];
22643
+ return this.parseResponse(output);
22644
+ }
22645
+ buildPrompt(request) {
22646
+ let prompt = `You are an AI decision assistant for a code orchestration system. Given an interaction request, decide the best action.
22647
+
22648
+ ## Interaction Request
22649
+ Type: ${request.type}
22650
+ Stage: ${request.stage}
22651
+ Feature: ${request.featureName}
22652
+ ${request.storyId ? `Story: ${request.storyId}` : ""}
22653
+
22654
+ Summary: ${request.summary.replace(/`/g, "\\`").replace(/\$/g, "\\$")}
22655
+ ${request.detail ? `
22656
+ Detail: ${request.detail.replace(/`/g, "\\`").replace(/\$/g, "\\$")}` : ""}
22657
+ `;
22658
+ if (request.options && request.options.length > 0) {
22659
+ prompt += `
22660
+ Options:
22661
+ `;
22662
+ for (const opt of request.options) {
22663
+ const desc = opt.description ? ` \u2014 ${opt.description}` : "";
22664
+ prompt += ` [${opt.key}] ${opt.label}${desc}
22665
+ `;
22666
+ }
22667
+ }
22668
+ prompt += `
22669
+ Fallback behavior on timeout: ${request.fallback}
22670
+ Safety tier: ${request.metadata?.safety ?? "unknown"}
22671
+
22672
+ ## Available Actions
22673
+ - approve: Proceed with the operation
22674
+ - reject: Deny the operation
22675
+ - choose: Select an option (requires value field)
22676
+ - input: Provide text input (requires value field)
22677
+ - skip: Skip this interaction
22678
+ - abort: Abort execution
22679
+
22680
+ ## Rules
22681
+ 1. For "red" safety tier (security-review, cost-exceeded, merge-conflict): ALWAYS return confidence 0 to escalate to human
22682
+ 2. For "yellow" safety tier (cost-warning, max-retries, pre-merge): High confidence (0.8+) ONLY if clearly safe
22683
+ 3. For "green" safety tier (story-ambiguity, review-gate): Can approve with moderate confidence (0.6+)
22684
+ 4. Default to the fallback behavior if unsure
22685
+ 5. Never auto-approve security issues
22686
+ 6. If the summary mentions "critical" or "security", confidence MUST be < 0.5
22687
+
22688
+ Respond with ONLY this JSON (no markdown, no explanation):
22689
+ {"action":"approve|reject|choose|input|skip|abort","value":"<optional>","confidence":0.0-1.0,"reasoning":"<one line>"}`;
22690
+ return prompt;
22691
+ }
22692
+ parseResponse(output) {
22693
+ let jsonText = output.trim();
22694
+ if (jsonText.startsWith("```")) {
22695
+ const lines = jsonText.split(`
22696
+ `);
22697
+ jsonText = lines.slice(1, -1).join(`
22698
+ `).trim();
22699
+ }
22700
+ if (jsonText.startsWith("json")) {
22701
+ jsonText = jsonText.slice(4).trim();
22702
+ }
22703
+ const parsed = JSON.parse(jsonText);
22704
+ if (!parsed.action || parsed.confidence === undefined || !parsed.reasoning) {
22705
+ throw new Error(`Invalid LLM response: ${jsonText}`);
22706
+ }
22707
+ if (parsed.confidence < 0 || parsed.confidence > 1) {
22708
+ throw new Error(`Invalid confidence: ${parsed.confidence} (must be 0-1)`);
22709
+ }
22710
+ return parsed;
22885
22711
  }
22886
22712
  }
22887
- var init_state = () => {};
22713
+ var AutoConfigSchema, _autoPluginDeps;
22714
+ var init_auto = __esm(() => {
22715
+ init_zod();
22716
+ init_config();
22717
+ AutoConfigSchema = exports_external.object({
22718
+ model: exports_external.string().optional(),
22719
+ confidenceThreshold: exports_external.number().min(0).max(1).optional(),
22720
+ maxCostPerDecision: exports_external.number().positive().optional(),
22721
+ naxConfig: exports_external.any().optional()
22722
+ });
22723
+ _autoPluginDeps = {
22724
+ adapter: null,
22725
+ callLlm: null
22726
+ };
22727
+ });
22888
22728
 
22889
22729
  // src/interaction/plugins/cli.ts
22890
22730
  import * as readline from "readline";
@@ -22953,9 +22793,9 @@ ${request.summary}
22953
22793
  if (!this.rl) {
22954
22794
  throw new Error("CLI plugin not initialized");
22955
22795
  }
22956
- const timeoutPromise = new Promise((resolve5) => {
22796
+ const timeoutPromise = new Promise((resolve4) => {
22957
22797
  setTimeout(() => {
22958
- resolve5({
22798
+ resolve4({
22959
22799
  requestId: request.id,
22960
22800
  action: "skip",
22961
22801
  respondedBy: "timeout",
@@ -23107,9 +22947,9 @@ ${request.summary}
23107
22947
  if (!this.rl) {
23108
22948
  throw new Error("CLI plugin not initialized");
23109
22949
  }
23110
- return new Promise((resolve5) => {
22950
+ return new Promise((resolve4) => {
23111
22951
  this.rl?.question(prompt, (answer) => {
23112
- resolve5(answer);
22952
+ resolve4(answer);
23113
22953
  });
23114
22954
  });
23115
22955
  }
@@ -23506,10 +23346,10 @@ class WebhookInteractionPlugin {
23506
23346
  this.pendingResponses.delete(requestId);
23507
23347
  return early;
23508
23348
  }
23509
- return new Promise((resolve5) => {
23349
+ return new Promise((resolve4) => {
23510
23350
  const timer = setTimeout(() => {
23511
23351
  this.receiveCallbacks.delete(requestId);
23512
- resolve5({
23352
+ resolve4({
23513
23353
  requestId,
23514
23354
  action: "skip",
23515
23355
  respondedBy: "timeout",
@@ -23519,7 +23359,7 @@ class WebhookInteractionPlugin {
23519
23359
  this.receiveCallbacks.set(requestId, (response) => {
23520
23360
  clearTimeout(timer);
23521
23361
  this.receiveCallbacks.delete(requestId);
23522
- resolve5(response);
23362
+ resolve4(response);
23523
23363
  });
23524
23364
  });
23525
23365
  }
@@ -23640,290 +23480,6 @@ var init_webhook = __esm(() => {
23640
23480
  });
23641
23481
  });
23642
23482
 
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
- };
23655
- }
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)");
23660
- }
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
- }
23693
- }
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"
23717
- });
23718
- return this.parseResponse(output);
23719
- }
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.
23722
-
23723
- ## Interaction Request
23724
- Type: ${request.type}
23725
- Stage: ${request.stage}
23726
- Feature: ${request.featureName}
23727
- ${request.storyId ? `Story: ${request.storyId}` : ""}
23728
-
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
- }
23742
- }
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
23754
-
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
23762
-
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;
23766
- }
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}`);
23781
- }
23782
- if (parsed.confidence < 0 || parsed.confidence > 1) {
23783
- throw new Error(`Invalid confidence: ${parsed.confidence} (must be 0-1)`);
23784
- }
23785
- return parsed;
23786
- }
23787
- }
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
23801
- };
23802
- });
23803
-
23804
- // src/interaction/triggers.ts
23805
- function isTriggerEnabled(trigger, config2) {
23806
- const triggerConfig = config2.interaction?.triggers?.[trigger];
23807
- if (triggerConfig === undefined)
23808
- return false;
23809
- if (typeof triggerConfig === "boolean")
23810
- return triggerConfig;
23811
- return triggerConfig.enabled;
23812
- }
23813
- function getTriggerConfig(trigger, config2) {
23814
- const metadata = TRIGGER_METADATA[trigger];
23815
- const triggerConfig = config2.interaction?.triggers?.[trigger];
23816
- const defaults = config2.interaction?.defaults ?? {
23817
- timeout: 600000,
23818
- fallback: "escalate"
23819
- };
23820
- let fallback = metadata.defaultFallback;
23821
- let timeout = defaults.timeout;
23822
- if (typeof triggerConfig === "object") {
23823
- if (triggerConfig.fallback) {
23824
- fallback = triggerConfig.fallback;
23825
- }
23826
- if (triggerConfig.timeout) {
23827
- timeout = triggerConfig.timeout;
23828
- }
23829
- }
23830
- return { fallback, timeout };
23831
- }
23832
- function substituteTemplate(template, context) {
23833
- let result = template;
23834
- for (const [key, value] of Object.entries(context)) {
23835
- if (value !== undefined) {
23836
- result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), String(value));
23837
- }
23838
- }
23839
- return result;
23840
- }
23841
- function createTriggerRequest(trigger, context, config2) {
23842
- const metadata = TRIGGER_METADATA[trigger];
23843
- const { fallback, timeout } = getTriggerConfig(trigger, config2);
23844
- const summary = substituteTemplate(metadata.defaultSummary, context);
23845
- const id = `trigger-${trigger}-${Date.now()}`;
23846
- return {
23847
- id,
23848
- type: "confirm",
23849
- featureName: context.featureName,
23850
- storyId: context.storyId,
23851
- stage: "custom",
23852
- summary,
23853
- fallback,
23854
- timeout,
23855
- createdAt: Date.now(),
23856
- metadata: {
23857
- trigger,
23858
- safety: metadata.safety
23859
- }
23860
- };
23861
- }
23862
- async function executeTrigger(trigger, context, config2, chain) {
23863
- const request = createTriggerRequest(trigger, context, config2);
23864
- const response = await chain.prompt(request);
23865
- return response;
23866
- }
23867
- async function checkSecurityReview(context, config2, chain) {
23868
- if (!isTriggerEnabled("security-review", config2))
23869
- return true;
23870
- const response = await executeTrigger("security-review", context, config2, chain);
23871
- return response.action !== "abort";
23872
- }
23873
- async function checkCostExceeded(context, config2, chain) {
23874
- if (!isTriggerEnabled("cost-exceeded", config2))
23875
- return true;
23876
- const response = await executeTrigger("cost-exceeded", context, config2, chain);
23877
- return response.action !== "abort";
23878
- }
23879
- async function checkMergeConflict(context, config2, chain) {
23880
- if (!isTriggerEnabled("merge-conflict", config2))
23881
- return true;
23882
- const response = await executeTrigger("merge-conflict", context, config2, chain);
23883
- return response.action !== "abort";
23884
- }
23885
- async function checkCostWarning(context, config2, chain) {
23886
- if (!isTriggerEnabled("cost-warning", config2))
23887
- return "continue";
23888
- const response = await executeTrigger("cost-warning", context, config2, chain);
23889
- return response.action === "approve" ? "escalate" : "continue";
23890
- }
23891
- async function checkPreMerge(context, config2, chain) {
23892
- if (!isTriggerEnabled("pre-merge", config2))
23893
- return true;
23894
- const response = await executeTrigger("pre-merge", context, config2, chain);
23895
- return response.action === "approve";
23896
- }
23897
- async function checkStoryAmbiguity(context, config2, chain) {
23898
- if (!isTriggerEnabled("story-ambiguity", config2))
23899
- return true;
23900
- const response = await executeTrigger("story-ambiguity", context, config2, chain);
23901
- return response.action === "approve";
23902
- }
23903
- async function checkReviewGate(context, config2, chain) {
23904
- if (!isTriggerEnabled("review-gate", config2))
23905
- return true;
23906
- const response = await executeTrigger("review-gate", context, config2, chain);
23907
- return response.action === "approve";
23908
- }
23909
- async function checkStoryOversized(context, config2, chain) {
23910
- if (!isTriggerEnabled("story-oversized", config2))
23911
- return "continue";
23912
- try {
23913
- const response = await executeTrigger("story-oversized", context, config2, chain);
23914
- if (response.action === "approve")
23915
- return "decompose";
23916
- if (response.action === "skip")
23917
- return "skip";
23918
- return "continue";
23919
- } catch {
23920
- return "continue";
23921
- }
23922
- }
23923
- var init_triggers = __esm(() => {
23924
- init_types4();
23925
- });
23926
-
23927
23483
  // src/interaction/init.ts
23928
23484
  function createInteractionPlugin(pluginName) {
23929
23485
  switch (pluginName) {
@@ -23978,6 +23534,483 @@ var init_init = __esm(() => {
23978
23534
  init_webhook();
23979
23535
  });
23980
23536
 
23537
+ // src/prd/validate.ts
23538
+ function validateStoryId(id) {
23539
+ if (!id || id.length === 0) {
23540
+ throw new Error("Story ID cannot be empty");
23541
+ }
23542
+ if (id.includes("..")) {
23543
+ throw new Error("Story ID cannot contain path traversal (..)");
23544
+ }
23545
+ if (id.startsWith("--")) {
23546
+ throw new Error("Story ID cannot start with git flags (--)");
23547
+ }
23548
+ const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
23549
+ if (!validPattern.test(id)) {
23550
+ throw new Error(`Story ID must match pattern [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}. Got: ${id}`);
23551
+ }
23552
+ }
23553
+
23554
+ // src/errors.ts
23555
+ var NaxError, AgentNotFoundError, AgentNotInstalledError, StoryLimitExceededError, LockAcquisitionError;
23556
+ var init_errors3 = __esm(() => {
23557
+ NaxError = class NaxError extends Error {
23558
+ code;
23559
+ context;
23560
+ constructor(message, code, context) {
23561
+ super(message);
23562
+ this.code = code;
23563
+ this.context = context;
23564
+ this.name = "NaxError";
23565
+ Error.captureStackTrace(this, this.constructor);
23566
+ }
23567
+ };
23568
+ AgentNotFoundError = class AgentNotFoundError extends NaxError {
23569
+ constructor(agentName, binary) {
23570
+ super(`Agent "${agentName}" not found or not installed`, "AGENT_NOT_FOUND", { agentName, binary });
23571
+ this.name = "AgentNotFoundError";
23572
+ }
23573
+ };
23574
+ AgentNotInstalledError = class AgentNotInstalledError extends NaxError {
23575
+ constructor(agentName, binary) {
23576
+ super(`Agent "${agentName}" is not installed or not in PATH: ${binary}`, "AGENT_NOT_INSTALLED", {
23577
+ agentName,
23578
+ binary
23579
+ });
23580
+ this.name = "AgentNotInstalledError";
23581
+ }
23582
+ };
23583
+ StoryLimitExceededError = class StoryLimitExceededError extends NaxError {
23584
+ constructor(totalStories, limit) {
23585
+ super(`Feature exceeds story limit: ${totalStories} stories (max: ${limit})`, "STORY_LIMIT_EXCEEDED", {
23586
+ totalStories,
23587
+ limit
23588
+ });
23589
+ this.name = "StoryLimitExceededError";
23590
+ }
23591
+ };
23592
+ LockAcquisitionError = class LockAcquisitionError extends NaxError {
23593
+ constructor(workdir) {
23594
+ super("Another nax process is already running in this directory", "LOCK_ACQUISITION_FAILED", { workdir });
23595
+ this.name = "LockAcquisitionError";
23596
+ }
23597
+ };
23598
+ });
23599
+
23600
+ // src/metrics/tracker.ts
23601
+ import path2 from "path";
23602
+ function collectStoryMetrics(ctx, storyStartTime) {
23603
+ const story = ctx.story;
23604
+ const routing = ctx.routing;
23605
+ const agentResult = ctx.agentResult;
23606
+ const escalationCount = story.escalations?.length || 0;
23607
+ const priorFailureCount = story.priorFailures?.length || 0;
23608
+ const attempts = priorFailureCount + Math.max(1, story.attempts || 1);
23609
+ const finalTier = escalationCount > 0 ? story.escalations[escalationCount - 1].toTier : routing.modelTier;
23610
+ const firstPassSuccess = agentResult?.success === true && escalationCount === 0 && priorFailureCount === 0;
23611
+ const modelEntry = ctx.config.models[routing.modelTier];
23612
+ const modelDef = modelEntry ? resolveModel(modelEntry) : null;
23613
+ const modelUsed = modelDef?.model || routing.modelTier;
23614
+ const initialComplexity = story.routing?.initialComplexity ?? routing.complexity;
23615
+ const isTddStrategy = routing.testStrategy === "three-session-tdd" || routing.testStrategy === "three-session-tdd-lite";
23616
+ const fullSuiteGatePassed = isTddStrategy ? ctx.fullSuiteGatePassed ?? false : false;
23617
+ return {
23618
+ storyId: story.id,
23619
+ complexity: routing.complexity,
23620
+ initialComplexity,
23621
+ modelTier: routing.modelTier,
23622
+ modelUsed,
23623
+ attempts,
23624
+ finalTier,
23625
+ success: agentResult?.success || false,
23626
+ cost: (ctx.accumulatedAttemptCost ?? 0) + (agentResult?.estimatedCost || 0),
23627
+ durationMs: agentResult?.durationMs || 0,
23628
+ firstPassSuccess,
23629
+ startedAt: storyStartTime,
23630
+ completedAt: new Date().toISOString(),
23631
+ fullSuiteGatePassed,
23632
+ runtimeCrashes: ctx.storyRuntimeCrashes ?? 0
23633
+ };
23634
+ }
23635
+ function collectBatchMetrics(ctx, storyStartTime) {
23636
+ const stories = ctx.stories;
23637
+ const routing = ctx.routing;
23638
+ const agentResult = ctx.agentResult;
23639
+ const totalCost = agentResult?.estimatedCost || 0;
23640
+ const totalDuration = agentResult?.durationMs || 0;
23641
+ const costPerStory = totalCost / stories.length;
23642
+ const durationPerStory = totalDuration / stories.length;
23643
+ const modelEntry = ctx.config.models[routing.modelTier];
23644
+ const modelDef = modelEntry ? resolveModel(modelEntry) : null;
23645
+ const modelUsed = modelDef?.model || routing.modelTier;
23646
+ return stories.map((story) => {
23647
+ const initialComplexity = story.routing?.initialComplexity ?? routing.complexity;
23648
+ return {
23649
+ storyId: story.id,
23650
+ complexity: routing.complexity,
23651
+ initialComplexity,
23652
+ modelTier: routing.modelTier,
23653
+ modelUsed,
23654
+ attempts: 1,
23655
+ finalTier: routing.modelTier,
23656
+ success: true,
23657
+ cost: costPerStory,
23658
+ durationMs: durationPerStory,
23659
+ firstPassSuccess: true,
23660
+ startedAt: storyStartTime,
23661
+ completedAt: new Date().toISOString(),
23662
+ fullSuiteGatePassed: false,
23663
+ runtimeCrashes: 0
23664
+ };
23665
+ });
23666
+ }
23667
+ async function saveRunMetrics(workdir, runMetrics) {
23668
+ const metricsPath = path2.join(workdir, ".nax", "metrics.json");
23669
+ const existing = await loadJsonFile(metricsPath, "metrics");
23670
+ const allMetrics = Array.isArray(existing) ? existing : [];
23671
+ allMetrics.push(runMetrics);
23672
+ await saveJsonFile(metricsPath, allMetrics, "metrics");
23673
+ }
23674
+ async function loadRunMetrics(workdir) {
23675
+ const metricsPath = path2.join(workdir, ".nax", "metrics.json");
23676
+ const content = await loadJsonFile(metricsPath, "metrics");
23677
+ return Array.isArray(content) ? content : [];
23678
+ }
23679
+ var init_tracker = __esm(() => {
23680
+ init_schema();
23681
+ init_json_file();
23682
+ });
23683
+
23684
+ // src/metrics/aggregator.ts
23685
+ function calculateAggregateMetrics(runs) {
23686
+ if (runs.length === 0) {
23687
+ return {
23688
+ totalRuns: 0,
23689
+ totalCost: 0,
23690
+ totalStories: 0,
23691
+ firstPassRate: 0,
23692
+ escalationRate: 0,
23693
+ avgCostPerStory: 0,
23694
+ avgCostPerFeature: 0,
23695
+ modelEfficiency: {},
23696
+ complexityAccuracy: {}
23697
+ };
23698
+ }
23699
+ const allStories = runs.flatMap((run) => run.stories);
23700
+ const totalRuns = runs.length;
23701
+ const totalCost = runs.reduce((sum, run) => sum + run.totalCost, 0);
23702
+ const totalStories = allStories.length;
23703
+ const firstPassSuccesses = allStories.filter((s) => s.firstPassSuccess).length;
23704
+ const firstPassRate = totalStories > 0 ? firstPassSuccesses / totalStories : 0;
23705
+ const escalatedStories = allStories.filter((s) => s.attempts > 1).length;
23706
+ const escalationRate = totalStories > 0 ? escalatedStories / totalStories : 0;
23707
+ const avgCostPerStory = totalStories > 0 ? totalCost / totalStories : 0;
23708
+ const avgCostPerFeature = totalRuns > 0 ? totalCost / totalRuns : 0;
23709
+ const modelStats = new Map;
23710
+ for (const story of allStories) {
23711
+ const modelKey = story.modelUsed;
23712
+ const existing = modelStats.get(modelKey) || {
23713
+ attempts: 0,
23714
+ successes: 0,
23715
+ totalCost: 0
23716
+ };
23717
+ modelStats.set(modelKey, {
23718
+ attempts: existing.attempts + story.attempts,
23719
+ successes: existing.successes + (story.success ? 1 : 0),
23720
+ totalCost: existing.totalCost + story.cost
23721
+ });
23722
+ }
23723
+ const modelEfficiency = {};
23724
+ for (const [modelKey, stats] of modelStats) {
23725
+ const passRate = stats.attempts > 0 ? stats.successes / stats.attempts : 0;
23726
+ const avgCost = stats.successes > 0 ? stats.totalCost / stats.successes : 0;
23727
+ modelEfficiency[modelKey] = {
23728
+ attempts: stats.attempts,
23729
+ successes: stats.successes,
23730
+ passRate,
23731
+ avgCost,
23732
+ totalCost: stats.totalCost
23733
+ };
23734
+ }
23735
+ const complexityStats = new Map;
23736
+ for (const story of allStories) {
23737
+ const complexity = story.initialComplexity ?? story.complexity;
23738
+ const existing = complexityStats.get(complexity) || {
23739
+ predicted: 0,
23740
+ tierCounts: new Map,
23741
+ mismatches: 0
23742
+ };
23743
+ existing.predicted += 1;
23744
+ const finalTier = story.finalTier;
23745
+ existing.tierCounts.set(finalTier, (existing.tierCounts.get(finalTier) || 0) + 1);
23746
+ if (story.modelTier !== story.finalTier) {
23747
+ existing.mismatches += 1;
23748
+ }
23749
+ complexityStats.set(complexity, existing);
23750
+ }
23751
+ const complexityAccuracy = {};
23752
+ for (const [complexity, stats] of complexityStats) {
23753
+ let maxCount = 0;
23754
+ let mostCommonTier = "unknown";
23755
+ for (const [tier, count] of stats.tierCounts) {
23756
+ if (count > maxCount) {
23757
+ maxCount = count;
23758
+ mostCommonTier = tier;
23759
+ }
23760
+ }
23761
+ const mismatchRate = stats.predicted > 0 ? stats.mismatches / stats.predicted : 0;
23762
+ complexityAccuracy[complexity] = {
23763
+ predicted: stats.predicted,
23764
+ actualTierUsed: mostCommonTier,
23765
+ mismatchRate
23766
+ };
23767
+ }
23768
+ return {
23769
+ totalRuns,
23770
+ totalCost,
23771
+ totalStories,
23772
+ firstPassRate,
23773
+ escalationRate,
23774
+ avgCostPerStory,
23775
+ avgCostPerFeature,
23776
+ modelEfficiency,
23777
+ complexityAccuracy
23778
+ };
23779
+ }
23780
+ function getLastRun(runs) {
23781
+ if (runs.length === 0) {
23782
+ return null;
23783
+ }
23784
+ return runs[runs.length - 1];
23785
+ }
23786
+
23787
+ // src/metrics/index.ts
23788
+ var init_metrics = __esm(() => {
23789
+ init_tracker();
23790
+ });
23791
+
23792
+ // src/interaction/types.ts
23793
+ var TRIGGER_METADATA;
23794
+ var init_types4 = __esm(() => {
23795
+ TRIGGER_METADATA = {
23796
+ "security-review": {
23797
+ defaultFallback: "abort",
23798
+ safety: "red",
23799
+ defaultSummary: "Security review failed \u2014 abort execution?"
23800
+ },
23801
+ "cost-exceeded": {
23802
+ defaultFallback: "abort",
23803
+ safety: "red",
23804
+ defaultSummary: "Cost limit exceeded ({{cost}} USD) \u2014 abort execution?"
23805
+ },
23806
+ "merge-conflict": {
23807
+ defaultFallback: "abort",
23808
+ safety: "red",
23809
+ defaultSummary: "Merge conflict detected in {{storyId}} \u2014 abort execution?"
23810
+ },
23811
+ "cost-warning": {
23812
+ defaultFallback: "escalate",
23813
+ safety: "yellow",
23814
+ defaultSummary: "Cost warning: {{cost}} USD / {{limit}} USD \u2014 escalate to higher tier?"
23815
+ },
23816
+ "max-retries": {
23817
+ defaultFallback: "skip",
23818
+ safety: "yellow",
23819
+ defaultSummary: "Max retries reached for {{storyId}} \u2014 skip story?"
23820
+ },
23821
+ "pre-merge": {
23822
+ defaultFallback: "escalate",
23823
+ safety: "yellow",
23824
+ defaultSummary: "Pre-merge checkpoint for {{storyId}} \u2014 proceed with merge?"
23825
+ },
23826
+ "human-review": {
23827
+ defaultFallback: "skip",
23828
+ safety: "yellow",
23829
+ defaultSummary: "Human review required for story {{storyId}} \u2014 skip and continue?"
23830
+ },
23831
+ "story-oversized": {
23832
+ defaultFallback: "continue",
23833
+ safety: "yellow",
23834
+ defaultSummary: "Story {{storyId}} is oversized ({{criteriaCount}} acceptance criteria) \u2014 decompose into smaller stories?"
23835
+ },
23836
+ "story-ambiguity": {
23837
+ defaultFallback: "continue",
23838
+ safety: "green",
23839
+ defaultSummary: "Story {{storyId}} requirements unclear \u2014 continue with best effort?"
23840
+ },
23841
+ "review-gate": {
23842
+ defaultFallback: "continue",
23843
+ safety: "green",
23844
+ defaultSummary: "Code review checkpoint for {{storyId}} \u2014 proceed?"
23845
+ }
23846
+ };
23847
+ });
23848
+
23849
+ // src/interaction/state.ts
23850
+ import * as path3 from "path";
23851
+ async function loadPendingInteraction(requestId, featureDir) {
23852
+ const interactionsDir = path3.join(featureDir, "interactions");
23853
+ const filename = `${requestId}.json`;
23854
+ const filePath = path3.join(interactionsDir, filename);
23855
+ try {
23856
+ const file2 = Bun.file(filePath);
23857
+ const exists = await file2.exists();
23858
+ if (!exists) {
23859
+ return null;
23860
+ }
23861
+ const json2 = await file2.text();
23862
+ const request = JSON.parse(json2);
23863
+ return request;
23864
+ } catch {
23865
+ return null;
23866
+ }
23867
+ }
23868
+ async function listPendingInteractions(featureDir) {
23869
+ const interactionsDir = path3.join(featureDir, "interactions");
23870
+ try {
23871
+ const dir = Bun.file(interactionsDir);
23872
+ const exists = await dir.exists();
23873
+ if (!exists) {
23874
+ return [];
23875
+ }
23876
+ const proc = Bun.spawn(["ls", interactionsDir], {
23877
+ stdout: "pipe",
23878
+ stderr: "pipe"
23879
+ });
23880
+ const output = await new Response(proc.stdout).text();
23881
+ await proc.exited;
23882
+ const files = output.split(`
23883
+ `).filter((f) => f.endsWith(".json") && f !== ".gitkeep").map((f) => f.replace(".json", ""));
23884
+ return files;
23885
+ } catch {
23886
+ return [];
23887
+ }
23888
+ }
23889
+ var init_state = () => {};
23890
+
23891
+ // src/interaction/triggers.ts
23892
+ function isTriggerEnabled(trigger, config2) {
23893
+ const triggerConfig = config2.interaction?.triggers?.[trigger];
23894
+ if (triggerConfig === undefined)
23895
+ return false;
23896
+ if (typeof triggerConfig === "boolean")
23897
+ return triggerConfig;
23898
+ return triggerConfig.enabled;
23899
+ }
23900
+ function getTriggerConfig(trigger, config2) {
23901
+ const metadata = TRIGGER_METADATA[trigger];
23902
+ const triggerConfig = config2.interaction?.triggers?.[trigger];
23903
+ const defaults = config2.interaction?.defaults ?? {
23904
+ timeout: 600000,
23905
+ fallback: "escalate"
23906
+ };
23907
+ let fallback = metadata.defaultFallback;
23908
+ let timeout = defaults.timeout;
23909
+ if (typeof triggerConfig === "object") {
23910
+ if (triggerConfig.fallback) {
23911
+ fallback = triggerConfig.fallback;
23912
+ }
23913
+ if (triggerConfig.timeout) {
23914
+ timeout = triggerConfig.timeout;
23915
+ }
23916
+ }
23917
+ return { fallback, timeout };
23918
+ }
23919
+ function substituteTemplate(template, context) {
23920
+ let result = template;
23921
+ for (const [key, value] of Object.entries(context)) {
23922
+ if (value !== undefined) {
23923
+ result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), String(value));
23924
+ }
23925
+ }
23926
+ return result;
23927
+ }
23928
+ function createTriggerRequest(trigger, context, config2) {
23929
+ const metadata = TRIGGER_METADATA[trigger];
23930
+ const { fallback, timeout } = getTriggerConfig(trigger, config2);
23931
+ const summary = substituteTemplate(metadata.defaultSummary, context);
23932
+ const id = `trigger-${trigger}-${Date.now()}`;
23933
+ return {
23934
+ id,
23935
+ type: "confirm",
23936
+ featureName: context.featureName,
23937
+ storyId: context.storyId,
23938
+ stage: "custom",
23939
+ summary,
23940
+ fallback,
23941
+ timeout,
23942
+ createdAt: Date.now(),
23943
+ metadata: {
23944
+ trigger,
23945
+ safety: metadata.safety
23946
+ }
23947
+ };
23948
+ }
23949
+ async function executeTrigger(trigger, context, config2, chain) {
23950
+ const request = createTriggerRequest(trigger, context, config2);
23951
+ const response = await chain.prompt(request);
23952
+ return response;
23953
+ }
23954
+ async function checkSecurityReview(context, config2, chain) {
23955
+ if (!isTriggerEnabled("security-review", config2))
23956
+ return true;
23957
+ const response = await executeTrigger("security-review", context, config2, chain);
23958
+ return response.action !== "abort";
23959
+ }
23960
+ async function checkCostExceeded(context, config2, chain) {
23961
+ if (!isTriggerEnabled("cost-exceeded", config2))
23962
+ return true;
23963
+ const response = await executeTrigger("cost-exceeded", context, config2, chain);
23964
+ return response.action !== "abort";
23965
+ }
23966
+ async function checkMergeConflict(context, config2, chain) {
23967
+ if (!isTriggerEnabled("merge-conflict", config2))
23968
+ return true;
23969
+ const response = await executeTrigger("merge-conflict", context, config2, chain);
23970
+ return response.action !== "abort";
23971
+ }
23972
+ async function checkCostWarning(context, config2, chain) {
23973
+ if (!isTriggerEnabled("cost-warning", config2))
23974
+ return "continue";
23975
+ const response = await executeTrigger("cost-warning", context, config2, chain);
23976
+ return response.action === "approve" ? "escalate" : "continue";
23977
+ }
23978
+ async function checkPreMerge(context, config2, chain) {
23979
+ if (!isTriggerEnabled("pre-merge", config2))
23980
+ return true;
23981
+ const response = await executeTrigger("pre-merge", context, config2, chain);
23982
+ return response.action === "approve";
23983
+ }
23984
+ async function checkStoryAmbiguity(context, config2, chain) {
23985
+ if (!isTriggerEnabled("story-ambiguity", config2))
23986
+ return true;
23987
+ const response = await executeTrigger("story-ambiguity", context, config2, chain);
23988
+ return response.action === "approve";
23989
+ }
23990
+ async function checkReviewGate(context, config2, chain) {
23991
+ if (!isTriggerEnabled("review-gate", config2))
23992
+ return true;
23993
+ const response = await executeTrigger("review-gate", context, config2, chain);
23994
+ return response.action === "approve";
23995
+ }
23996
+ async function checkStoryOversized(context, config2, chain) {
23997
+ if (!isTriggerEnabled("story-oversized", config2))
23998
+ return "continue";
23999
+ try {
24000
+ const response = await executeTrigger("story-oversized", context, config2, chain);
24001
+ if (response.action === "approve")
24002
+ return "decompose";
24003
+ if (response.action === "skip")
24004
+ return "skip";
24005
+ return "continue";
24006
+ } catch {
24007
+ return "continue";
24008
+ }
24009
+ }
24010
+ var init_triggers = __esm(() => {
24011
+ init_types4();
24012
+ });
24013
+
23981
24014
  // src/interaction/index.ts
23982
24015
  var init_interaction = __esm(() => {
23983
24016
  init_types4();
@@ -23988,6 +24021,7 @@ var init_interaction = __esm(() => {
23988
24021
  init_auto();
23989
24022
  init_triggers();
23990
24023
  init_init();
24024
+ init_bridge_builder();
23991
24025
  });
23992
24026
 
23993
24027
  // src/pipeline/runner.ts
@@ -24446,10 +24480,11 @@ ${stderr}` };
24446
24480
  if (workdirGroups.size === 0) {
24447
24481
  workdirGroups.set("", { stories: [], criteria: [] });
24448
24482
  }
24483
+ const featureName = ctx.prd.feature;
24449
24484
  const testPaths = [];
24450
24485
  for (const [workdir] of workdirGroups) {
24451
24486
  const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
24452
- const testPath = path5.join(packageDir, acceptanceTestFilename(language));
24487
+ const testPath = path5.join(packageDir, ".nax", "features", featureName, acceptanceTestFilename(language));
24453
24488
  testPaths.push({ testPath, packageDir });
24454
24489
  }
24455
24490
  let totalCriteria = 0;
@@ -28397,7 +28432,7 @@ async function rollbackToRef(workdir, ref) {
28397
28432
  }
28398
28433
  logger.info("tdd", "Successfully rolled back git changes", { ref });
28399
28434
  }
28400
- async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
28435
+ async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName, interactionBridge) {
28401
28436
  const startTime = Date.now();
28402
28437
  let prompt;
28403
28438
  if (_sessionRunnerDeps.buildPrompt) {
@@ -28431,7 +28466,8 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
28431
28466
  featureName,
28432
28467
  storyId: story.id,
28433
28468
  sessionRole: role,
28434
- keepSessionOpen
28469
+ keepSessionOpen,
28470
+ interactionBridge
28435
28471
  });
28436
28472
  if (!result.success && result.pid) {
28437
28473
  await _sessionRunnerDeps.cleanupProcessTree(result.pid);
@@ -28796,7 +28832,8 @@ async function runThreeSessionTdd(options) {
28796
28832
  constitution,
28797
28833
  dryRun = false,
28798
28834
  lite = false,
28799
- _recursionDepth = 0
28835
+ _recursionDepth = 0,
28836
+ interactionChain
28800
28837
  } = options;
28801
28838
  const logger = getLogger();
28802
28839
  const MAX_RECURSION_DEPTH = 2;
@@ -28855,7 +28892,7 @@ async function runThreeSessionTdd(options) {
28855
28892
  let session1;
28856
28893
  if (!isRetry) {
28857
28894
  const testWriterTier = config2.tdd.sessionTiers?.testWriter ?? "balanced";
28858
- session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution, featureName);
28895
+ 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
28896
  sessions.push(session1);
28860
28897
  }
28861
28898
  if (session1 && !session1.success) {
@@ -28917,7 +28954,7 @@ async function runThreeSessionTdd(options) {
28917
28954
  });
28918
28955
  const session2Ref = await captureGitRef(workdir) ?? "HEAD";
28919
28956
  const implementerTier = config2.tdd.sessionTiers?.implementer ?? modelTier;
28920
- const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution, featureName);
28957
+ 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
28958
  sessions.push(session2);
28922
28959
  if (!session2.success) {
28923
28960
  needsHumanReview = true;
@@ -29035,6 +29072,7 @@ async function runThreeSessionTdd(options) {
29035
29072
  var init_orchestrator2 = __esm(() => {
29036
29073
  init_config();
29037
29074
  init_greenfield();
29075
+ init_bridge_builder();
29038
29076
  init_logger2();
29039
29077
  init_git();
29040
29078
  init_verification();
@@ -29100,6 +29138,7 @@ var executionStage, _executionDeps;
29100
29138
  var init_execution2 = __esm(() => {
29101
29139
  init_agents();
29102
29140
  init_config();
29141
+ init_bridge_builder();
29103
29142
  init_triggers();
29104
29143
  init_logger2();
29105
29144
  init_tdd();
@@ -29134,7 +29173,8 @@ var init_execution2 = __esm(() => {
29134
29173
  contextMarkdown: ctx.contextMarkdown,
29135
29174
  constitution: ctx.constitution?.content,
29136
29175
  dryRun: false,
29137
- lite: isLiteMode
29176
+ lite: isLiteMode,
29177
+ interactionChain: ctx.interaction
29138
29178
  });
29139
29179
  ctx.agentResult = {
29140
29180
  success: tddResult.success,
@@ -29208,34 +29248,11 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
29208
29248
  pidRegistry: ctx.pidRegistry,
29209
29249
  featureName: ctx.prd.feature,
29210
29250
  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
- })()
29251
+ interactionBridge: buildInteractionBridge(ctx.interaction, {
29252
+ featureName: ctx.prd.feature,
29253
+ storyId: ctx.story.id,
29254
+ stage: "execution"
29255
+ })
29239
29256
  });
29240
29257
  ctx.agentResult = result;
29241
29258
  await autoCommitIfDirty(storyWorkdir, "execution", "single-session", ctx.story.id);
@@ -32238,7 +32255,8 @@ async function checkGitignoreCoversNax(workdir) {
32238
32255
  ".nax/features/*/status.json",
32239
32256
  ".nax-pids",
32240
32257
  ".nax-wt/",
32241
- "**/.nax-acceptance*"
32258
+ "**/.nax-acceptance*",
32259
+ "**/.nax/features/*/"
32242
32260
  ];
32243
32261
  const missing = patterns.filter((pattern) => !content.includes(pattern));
32244
32262
  const passed = missing.length === 0;
@@ -68283,7 +68301,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
68283
68301
  init_registry();
68284
68302
  import { existsSync as existsSync11 } from "fs";
68285
68303
  import { join as join11 } from "path";
68286
- import { createInterface } from "readline";
68304
+ import { createInterface as createInterface2 } from "readline";
68287
68305
  init_test_strategy();
68288
68306
 
68289
68307
  // src/context/generator.ts
@@ -68821,6 +68839,8 @@ async function generateForPackage(packageDir, config2, dryRun = false, repoRoot)
68821
68839
 
68822
68840
  // src/cli/plan.ts
68823
68841
  init_pid_registry();
68842
+ init_bridge_builder();
68843
+ init_init();
68824
68844
  init_logger2();
68825
68845
 
68826
68846
  // src/prd/schema.ts
@@ -69015,7 +69035,8 @@ var _planDeps = {
69015
69035
  existsSync: (path) => existsSync11(path),
69016
69036
  discoverWorkspacePackages: (repoRoot) => discoverWorkspacePackages(repoRoot),
69017
69037
  readPackageJsonAt: (path) => Bun.file(path).json().catch(() => null),
69018
- createInteractionBridge: () => createCliInteractionBridge()
69038
+ createInteractionBridge: () => createCliInteractionBridge(),
69039
+ initInteractionChain: (cfg, headless) => initInteractionChain(cfg, headless)
69019
69040
  };
69020
69041
  async function planCommand(workdir, config2, options) {
69021
69042
  const naxDir = join11(workdir, ".nax");
@@ -69078,7 +69099,13 @@ async function planCommand(workdir, config2, options) {
69078
69099
  const adapter = _planDeps.getAgent(agentName, config2);
69079
69100
  if (!adapter)
69080
69101
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
69081
- const interactionBridge = _planDeps.createInteractionBridge();
69102
+ const headless = !process.stdin.isTTY;
69103
+ const interactionChain = config2 ? await _planDeps.initInteractionChain(config2, headless) : null;
69104
+ const configuredBridge = interactionChain ? buildInteractionBridge(interactionChain, {
69105
+ featureName: options.feature,
69106
+ stage: "pre-flight"
69107
+ }) : undefined;
69108
+ const interactionBridge = configuredBridge ?? _planDeps.createInteractionBridge();
69082
69109
  const pidRegistry = new PidRegistry(workdir);
69083
69110
  const resolvedPerm = resolvePermissions(config2, "plan");
69084
69111
  const resolvedModel = config2?.plan?.model ?? "balanced";
@@ -69107,6 +69134,8 @@ async function planCommand(workdir, config2, options) {
69107
69134
  });
69108
69135
  } finally {
69109
69136
  await pidRegistry.killAll().catch(() => {});
69137
+ if (interactionChain)
69138
+ await interactionChain.destroy().catch(() => {});
69110
69139
  logger?.info("plan", "Interactive session ended", { durationMs: Date.now() - planStartTime });
69111
69140
  }
69112
69141
  if (!_planDeps.existsSync(outputPath)) {
@@ -69133,7 +69162,7 @@ function createCliInteractionBridge() {
69133
69162
  \uD83E\uDD16 Agent: ${text}
69134
69163
  You: `);
69135
69164
  return new Promise((resolve4) => {
69136
- const rl = createInterface({ input: process.stdin, terminal: false });
69165
+ const rl = createInterface2({ input: process.stdin, terminal: false });
69137
69166
  rl.once("line", (line) => {
69138
69167
  rl.close();
69139
69168
  resolve4(line.trim());