@nathapp/nax 0.54.3 → 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 +795 -715
  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.3",
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("5acee1f"))
22429
- return "5acee1f";
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
  }
@@ -23144,28 +22984,38 @@ class TelegramInteractionPlugin {
23144
22984
  if (!this.botToken || !this.chatId) {
23145
22985
  throw new Error("Telegram plugin not initialized");
23146
22986
  }
23147
- const text = this.formatMessage(request);
22987
+ const header = this.buildHeader(request);
23148
22988
  const keyboard = this.buildKeyboard(request);
22989
+ const body = this.buildBody(request);
22990
+ const chunks = this.splitText(body, MAX_MESSAGE_CHARS - header.length - 10);
23149
22991
  try {
23150
- const response = await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
23151
- method: "POST",
23152
- headers: { "Content-Type": "application/json" },
23153
- body: JSON.stringify({
23154
- chat_id: this.chatId,
23155
- text,
23156
- reply_markup: keyboard ? { inline_keyboard: keyboard } : undefined,
23157
- parse_mode: "Markdown"
23158
- })
23159
- });
23160
- if (!response.ok) {
23161
- const errorBody = await response.text().catch(() => "");
23162
- throw new Error(`Telegram API error (${response.status}): ${errorBody || response.statusText}`);
23163
- }
23164
- const data = await response.json();
23165
- if (!data.ok) {
23166
- throw new Error(`Telegram API returned ok=false: ${JSON.stringify(data)}`);
22992
+ const sentIds = [];
22993
+ for (let i = 0;i < chunks.length; i++) {
22994
+ const isLast = i === chunks.length - 1;
22995
+ const partLabel = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : "";
22996
+ const text = `${header}
22997
+ ${partLabel}${chunks[i]}`;
22998
+ const response = await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
22999
+ method: "POST",
23000
+ headers: { "Content-Type": "application/json" },
23001
+ body: JSON.stringify({
23002
+ chat_id: this.chatId,
23003
+ text,
23004
+ reply_markup: isLast && keyboard ? { inline_keyboard: keyboard } : undefined,
23005
+ parse_mode: "Markdown"
23006
+ })
23007
+ });
23008
+ if (!response.ok) {
23009
+ const errorBody = await response.text().catch(() => "");
23010
+ throw new Error(`Telegram API error (${response.status}): ${errorBody || response.statusText}`);
23011
+ }
23012
+ const data = await response.json();
23013
+ if (!data.ok) {
23014
+ throw new Error(`Telegram API returned ok=false: ${JSON.stringify(data)}`);
23015
+ }
23016
+ sentIds.push(data.result.message_id);
23167
23017
  }
23168
- this.pendingMessages.set(request.id, data.result.message_id);
23018
+ this.pendingMessages.set(request.id, sentIds);
23169
23019
  } catch (err) {
23170
23020
  const msg = err instanceof Error ? err.message : String(err);
23171
23021
  throw new Error(`Failed to send Telegram message: ${msg}`);
@@ -23202,10 +23052,9 @@ class TelegramInteractionPlugin {
23202
23052
  await this.sendTimeoutMessage(requestId);
23203
23053
  this.pendingMessages.delete(requestId);
23204
23054
  }
23205
- formatMessage(request) {
23055
+ buildHeader(request) {
23206
23056
  const emoji3 = this.getStageEmoji(request.stage);
23207
23057
  let text = `${emoji3} *${request.stage.toUpperCase()}*
23208
-
23209
23058
  `;
23210
23059
  text += `*Feature:* ${request.featureName}
23211
23060
  `;
@@ -23214,11 +23063,15 @@ class TelegramInteractionPlugin {
23214
23063
  `;
23215
23064
  }
23216
23065
  text += `
23217
- ${request.summary}
23066
+ `;
23067
+ return text;
23068
+ }
23069
+ buildBody(request) {
23070
+ let text = `${this.sanitizeMarkdown(request.summary)}
23218
23071
  `;
23219
23072
  if (request.detail) {
23220
23073
  text += `
23221
- ${request.detail}
23074
+ ${this.sanitizeMarkdown(request.detail)}
23222
23075
  `;
23223
23076
  }
23224
23077
  if (request.options && request.options.length > 0) {
@@ -23226,8 +23079,8 @@ ${request.detail}
23226
23079
  *Options:*
23227
23080
  `;
23228
23081
  for (const opt of request.options) {
23229
- const desc = opt.description ? ` \u2014 ${opt.description}` : "";
23230
- text += ` \u2022 ${opt.label}${desc}
23082
+ const desc = opt.description ? ` - ${this.sanitizeMarkdown(opt.description)}` : "";
23083
+ text += ` - ${opt.label}${desc}
23231
23084
  `;
23232
23085
  }
23233
23086
  }
@@ -23238,6 +23091,30 @@ ${request.detail}
23238
23091
  }
23239
23092
  return text;
23240
23093
  }
23094
+ sanitizeMarkdown(text) {
23095
+ return text.replace(/\\(?=[_*`\[])/g, "\\\\").replace(/_/g, "\\_").replace(/`/g, "\\`").replace(/\*/g, "\\*").replace(/\[/g, "\\[");
23096
+ }
23097
+ splitText(text, maxChars) {
23098
+ if (text.length <= maxChars)
23099
+ return [text];
23100
+ const chunks = [];
23101
+ let remaining = text;
23102
+ while (remaining.length > maxChars) {
23103
+ const slice = remaining.slice(0, maxChars);
23104
+ const lastNewline = slice.lastIndexOf(`
23105
+ `);
23106
+ if (lastNewline > maxChars * 0.5) {
23107
+ chunks.push(remaining.slice(0, lastNewline));
23108
+ remaining = remaining.slice(lastNewline + 1);
23109
+ } else {
23110
+ chunks.push(slice);
23111
+ remaining = remaining.slice(maxChars);
23112
+ }
23113
+ }
23114
+ if (remaining.length > 0)
23115
+ chunks.push(remaining);
23116
+ return chunks;
23117
+ }
23241
23118
  buildKeyboard(request) {
23242
23119
  switch (request.type) {
23243
23120
  case "confirm":
@@ -23345,8 +23222,11 @@ ${request.detail}
23345
23222
  };
23346
23223
  }
23347
23224
  if (update.message?.text) {
23348
- const messageId = this.pendingMessages.get(requestId);
23349
- if (!messageId)
23225
+ const messageIds = this.pendingMessages.get(requestId);
23226
+ if (!messageIds)
23227
+ return null;
23228
+ const replyToId = update.message.reply_to_message?.message_id;
23229
+ if (replyToId !== undefined && !messageIds.includes(replyToId))
23350
23230
  return null;
23351
23231
  return {
23352
23232
  requestId,
@@ -23372,20 +23252,20 @@ ${request.detail}
23372
23252
  } catch {}
23373
23253
  }
23374
23254
  async sendTimeoutMessage(requestId) {
23375
- const messageId = this.pendingMessages.get(requestId);
23376
- if (!messageId || !this.botToken || !this.chatId) {
23255
+ const messageIds = this.pendingMessages.get(requestId);
23256
+ if (!messageIds || !this.botToken || !this.chatId) {
23377
23257
  this.pendingMessages.delete(requestId);
23378
23258
  return;
23379
23259
  }
23260
+ const lastId = messageIds[messageIds.length - 1];
23380
23261
  try {
23381
23262
  await fetch(`https://api.telegram.org/bot${this.botToken}/editMessageText`, {
23382
23263
  method: "POST",
23383
23264
  headers: { "Content-Type": "application/json" },
23384
23265
  body: JSON.stringify({
23385
23266
  chat_id: this.chatId,
23386
- message_id: messageId,
23387
- text: "\u23F1 *EXPIRED* \u2014 Interaction timed out",
23388
- parse_mode: "Markdown"
23267
+ message_id: lastId,
23268
+ text: "\u23F1 EXPIRED \u2014 Interaction timed out"
23389
23269
  })
23390
23270
  });
23391
23271
  } catch {} finally {
@@ -23393,7 +23273,7 @@ ${request.detail}
23393
23273
  }
23394
23274
  }
23395
23275
  }
23396
- var TelegramConfigSchema;
23276
+ var MAX_MESSAGE_CHARS = 4000, TelegramConfigSchema;
23397
23277
  var init_telegram = __esm(() => {
23398
23278
  init_zod();
23399
23279
  TelegramConfigSchema = exports_external.object({
@@ -23466,10 +23346,10 @@ class WebhookInteractionPlugin {
23466
23346
  this.pendingResponses.delete(requestId);
23467
23347
  return early;
23468
23348
  }
23469
- return new Promise((resolve5) => {
23349
+ return new Promise((resolve4) => {
23470
23350
  const timer = setTimeout(() => {
23471
23351
  this.receiveCallbacks.delete(requestId);
23472
- resolve5({
23352
+ resolve4({
23473
23353
  requestId,
23474
23354
  action: "skip",
23475
23355
  respondedBy: "timeout",
@@ -23479,7 +23359,7 @@ class WebhookInteractionPlugin {
23479
23359
  this.receiveCallbacks.set(requestId, (response) => {
23480
23360
  clearTimeout(timer);
23481
23361
  this.receiveCallbacks.delete(requestId);
23482
- resolve5(response);
23362
+ resolve4(response);
23483
23363
  });
23484
23364
  });
23485
23365
  }
@@ -23600,290 +23480,6 @@ var init_webhook = __esm(() => {
23600
23480
  });
23601
23481
  });
23602
23482
 
23603
- // src/interaction/plugins/auto.ts
23604
- class AutoInteractionPlugin {
23605
- name = "auto";
23606
- config = {};
23607
- async init(config2) {
23608
- const cfg = AutoConfigSchema.parse(config2);
23609
- this.config = {
23610
- model: cfg.model ?? "fast",
23611
- confidenceThreshold: cfg.confidenceThreshold ?? 0.7,
23612
- maxCostPerDecision: cfg.maxCostPerDecision ?? 0.01,
23613
- naxConfig: cfg.naxConfig
23614
- };
23615
- }
23616
- async destroy() {}
23617
- async send(request) {}
23618
- async receive(_requestId, _timeout = 60000) {
23619
- throw new Error("Auto plugin requires full request context (not just requestId)");
23620
- }
23621
- async decide(request) {
23622
- if (request.metadata?.trigger === "security-review") {
23623
- return;
23624
- }
23625
- try {
23626
- if (_autoPluginDeps.callLlm) {
23627
- const decision2 = await _autoPluginDeps.callLlm(request);
23628
- if (decision2.confidence < (this.config.confidenceThreshold ?? 0.7)) {
23629
- return;
23630
- }
23631
- return {
23632
- requestId: request.id,
23633
- action: decision2.action,
23634
- value: decision2.value,
23635
- respondedBy: "auto-ai",
23636
- respondedAt: Date.now()
23637
- };
23638
- }
23639
- const decision = await this.callLlm(request);
23640
- if (decision.confidence < (this.config.confidenceThreshold ?? 0.7)) {
23641
- return;
23642
- }
23643
- return {
23644
- requestId: request.id,
23645
- action: decision.action,
23646
- value: decision.value,
23647
- respondedBy: "auto-ai",
23648
- respondedAt: Date.now()
23649
- };
23650
- } catch (err) {
23651
- return;
23652
- }
23653
- }
23654
- async callLlm(request) {
23655
- const prompt = this.buildPrompt(request);
23656
- const adapter = _autoPluginDeps.adapter;
23657
- if (!adapter) {
23658
- throw new Error("Auto plugin requires adapter to be injected via _autoPluginDeps.adapter");
23659
- }
23660
- let modelArg;
23661
- if (this.config.naxConfig) {
23662
- const modelTier = this.config.model ?? "fast";
23663
- const modelEntry = this.config.naxConfig.models[modelTier];
23664
- if (!modelEntry) {
23665
- throw new Error(`Model tier "${modelTier}" not found in config.models`);
23666
- }
23667
- const modelDef = resolveModel(modelEntry);
23668
- modelArg = modelDef.model;
23669
- }
23670
- const output = await adapter.complete(prompt, {
23671
- ...modelArg && { model: modelArg },
23672
- jsonMode: true,
23673
- ...this.config.naxConfig && { config: this.config.naxConfig },
23674
- featureName: request.featureName,
23675
- storyId: request.storyId,
23676
- sessionRole: "auto"
23677
- });
23678
- return this.parseResponse(output);
23679
- }
23680
- buildPrompt(request) {
23681
- let prompt = `You are an AI decision assistant for a code orchestration system. Given an interaction request, decide the best action.
23682
-
23683
- ## Interaction Request
23684
- Type: ${request.type}
23685
- Stage: ${request.stage}
23686
- Feature: ${request.featureName}
23687
- ${request.storyId ? `Story: ${request.storyId}` : ""}
23688
-
23689
- Summary: ${request.summary.replace(/`/g, "\\`").replace(/\$/g, "\\$")}
23690
- ${request.detail ? `
23691
- Detail: ${request.detail.replace(/`/g, "\\`").replace(/\$/g, "\\$")}` : ""}
23692
- `;
23693
- if (request.options && request.options.length > 0) {
23694
- prompt += `
23695
- Options:
23696
- `;
23697
- for (const opt of request.options) {
23698
- const desc = opt.description ? ` \u2014 ${opt.description}` : "";
23699
- prompt += ` [${opt.key}] ${opt.label}${desc}
23700
- `;
23701
- }
23702
- }
23703
- prompt += `
23704
- Fallback behavior on timeout: ${request.fallback}
23705
- Safety tier: ${request.metadata?.safety ?? "unknown"}
23706
-
23707
- ## Available Actions
23708
- - approve: Proceed with the operation
23709
- - reject: Deny the operation
23710
- - choose: Select an option (requires value field)
23711
- - input: Provide text input (requires value field)
23712
- - skip: Skip this interaction
23713
- - abort: Abort execution
23714
-
23715
- ## Rules
23716
- 1. For "red" safety tier (security-review, cost-exceeded, merge-conflict): ALWAYS return confidence 0 to escalate to human
23717
- 2. For "yellow" safety tier (cost-warning, max-retries, pre-merge): High confidence (0.8+) ONLY if clearly safe
23718
- 3. For "green" safety tier (story-ambiguity, review-gate): Can approve with moderate confidence (0.6+)
23719
- 4. Default to the fallback behavior if unsure
23720
- 5. Never auto-approve security issues
23721
- 6. If the summary mentions "critical" or "security", confidence MUST be < 0.5
23722
-
23723
- Respond with ONLY this JSON (no markdown, no explanation):
23724
- {"action":"approve|reject|choose|input|skip|abort","value":"<optional>","confidence":0.0-1.0,"reasoning":"<one line>"}`;
23725
- return prompt;
23726
- }
23727
- parseResponse(output) {
23728
- let jsonText = output.trim();
23729
- if (jsonText.startsWith("```")) {
23730
- const lines = jsonText.split(`
23731
- `);
23732
- jsonText = lines.slice(1, -1).join(`
23733
- `).trim();
23734
- }
23735
- if (jsonText.startsWith("json")) {
23736
- jsonText = jsonText.slice(4).trim();
23737
- }
23738
- const parsed = JSON.parse(jsonText);
23739
- if (!parsed.action || parsed.confidence === undefined || !parsed.reasoning) {
23740
- throw new Error(`Invalid LLM response: ${jsonText}`);
23741
- }
23742
- if (parsed.confidence < 0 || parsed.confidence > 1) {
23743
- throw new Error(`Invalid confidence: ${parsed.confidence} (must be 0-1)`);
23744
- }
23745
- return parsed;
23746
- }
23747
- }
23748
- var AutoConfigSchema, _autoPluginDeps;
23749
- var init_auto = __esm(() => {
23750
- init_zod();
23751
- init_config();
23752
- AutoConfigSchema = exports_external.object({
23753
- model: exports_external.string().optional(),
23754
- confidenceThreshold: exports_external.number().min(0).max(1).optional(),
23755
- maxCostPerDecision: exports_external.number().positive().optional(),
23756
- naxConfig: exports_external.any().optional()
23757
- });
23758
- _autoPluginDeps = {
23759
- adapter: null,
23760
- callLlm: null
23761
- };
23762
- });
23763
-
23764
- // src/interaction/triggers.ts
23765
- function isTriggerEnabled(trigger, config2) {
23766
- const triggerConfig = config2.interaction?.triggers?.[trigger];
23767
- if (triggerConfig === undefined)
23768
- return false;
23769
- if (typeof triggerConfig === "boolean")
23770
- return triggerConfig;
23771
- return triggerConfig.enabled;
23772
- }
23773
- function getTriggerConfig(trigger, config2) {
23774
- const metadata = TRIGGER_METADATA[trigger];
23775
- const triggerConfig = config2.interaction?.triggers?.[trigger];
23776
- const defaults = config2.interaction?.defaults ?? {
23777
- timeout: 600000,
23778
- fallback: "escalate"
23779
- };
23780
- let fallback = metadata.defaultFallback;
23781
- let timeout = defaults.timeout;
23782
- if (typeof triggerConfig === "object") {
23783
- if (triggerConfig.fallback) {
23784
- fallback = triggerConfig.fallback;
23785
- }
23786
- if (triggerConfig.timeout) {
23787
- timeout = triggerConfig.timeout;
23788
- }
23789
- }
23790
- return { fallback, timeout };
23791
- }
23792
- function substituteTemplate(template, context) {
23793
- let result = template;
23794
- for (const [key, value] of Object.entries(context)) {
23795
- if (value !== undefined) {
23796
- result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), String(value));
23797
- }
23798
- }
23799
- return result;
23800
- }
23801
- function createTriggerRequest(trigger, context, config2) {
23802
- const metadata = TRIGGER_METADATA[trigger];
23803
- const { fallback, timeout } = getTriggerConfig(trigger, config2);
23804
- const summary = substituteTemplate(metadata.defaultSummary, context);
23805
- const id = `trigger-${trigger}-${Date.now()}`;
23806
- return {
23807
- id,
23808
- type: "confirm",
23809
- featureName: context.featureName,
23810
- storyId: context.storyId,
23811
- stage: "custom",
23812
- summary,
23813
- fallback,
23814
- timeout,
23815
- createdAt: Date.now(),
23816
- metadata: {
23817
- trigger,
23818
- safety: metadata.safety
23819
- }
23820
- };
23821
- }
23822
- async function executeTrigger(trigger, context, config2, chain) {
23823
- const request = createTriggerRequest(trigger, context, config2);
23824
- const response = await chain.prompt(request);
23825
- return response;
23826
- }
23827
- async function checkSecurityReview(context, config2, chain) {
23828
- if (!isTriggerEnabled("security-review", config2))
23829
- return true;
23830
- const response = await executeTrigger("security-review", context, config2, chain);
23831
- return response.action !== "abort";
23832
- }
23833
- async function checkCostExceeded(context, config2, chain) {
23834
- if (!isTriggerEnabled("cost-exceeded", config2))
23835
- return true;
23836
- const response = await executeTrigger("cost-exceeded", context, config2, chain);
23837
- return response.action !== "abort";
23838
- }
23839
- async function checkMergeConflict(context, config2, chain) {
23840
- if (!isTriggerEnabled("merge-conflict", config2))
23841
- return true;
23842
- const response = await executeTrigger("merge-conflict", context, config2, chain);
23843
- return response.action !== "abort";
23844
- }
23845
- async function checkCostWarning(context, config2, chain) {
23846
- if (!isTriggerEnabled("cost-warning", config2))
23847
- return "continue";
23848
- const response = await executeTrigger("cost-warning", context, config2, chain);
23849
- return response.action === "approve" ? "escalate" : "continue";
23850
- }
23851
- async function checkPreMerge(context, config2, chain) {
23852
- if (!isTriggerEnabled("pre-merge", config2))
23853
- return true;
23854
- const response = await executeTrigger("pre-merge", context, config2, chain);
23855
- return response.action === "approve";
23856
- }
23857
- async function checkStoryAmbiguity(context, config2, chain) {
23858
- if (!isTriggerEnabled("story-ambiguity", config2))
23859
- return true;
23860
- const response = await executeTrigger("story-ambiguity", context, config2, chain);
23861
- return response.action === "approve";
23862
- }
23863
- async function checkReviewGate(context, config2, chain) {
23864
- if (!isTriggerEnabled("review-gate", config2))
23865
- return true;
23866
- const response = await executeTrigger("review-gate", context, config2, chain);
23867
- return response.action === "approve";
23868
- }
23869
- async function checkStoryOversized(context, config2, chain) {
23870
- if (!isTriggerEnabled("story-oversized", config2))
23871
- return "continue";
23872
- try {
23873
- const response = await executeTrigger("story-oversized", context, config2, chain);
23874
- if (response.action === "approve")
23875
- return "decompose";
23876
- if (response.action === "skip")
23877
- return "skip";
23878
- return "continue";
23879
- } catch {
23880
- return "continue";
23881
- }
23882
- }
23883
- var init_triggers = __esm(() => {
23884
- init_types4();
23885
- });
23886
-
23887
23483
  // src/interaction/init.ts
23888
23484
  function createInteractionPlugin(pluginName) {
23889
23485
  switch (pluginName) {
@@ -23938,6 +23534,483 @@ var init_init = __esm(() => {
23938
23534
  init_webhook();
23939
23535
  });
23940
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
+
23941
24014
  // src/interaction/index.ts
23942
24015
  var init_interaction = __esm(() => {
23943
24016
  init_types4();
@@ -23948,6 +24021,7 @@ var init_interaction = __esm(() => {
23948
24021
  init_auto();
23949
24022
  init_triggers();
23950
24023
  init_init();
24024
+ init_bridge_builder();
23951
24025
  });
23952
24026
 
23953
24027
  // src/pipeline/runner.ts
@@ -24406,10 +24480,11 @@ ${stderr}` };
24406
24480
  if (workdirGroups.size === 0) {
24407
24481
  workdirGroups.set("", { stories: [], criteria: [] });
24408
24482
  }
24483
+ const featureName = ctx.prd.feature;
24409
24484
  const testPaths = [];
24410
24485
  for (const [workdir] of workdirGroups) {
24411
24486
  const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
24412
- const testPath = path5.join(packageDir, acceptanceTestFilename(language));
24487
+ const testPath = path5.join(packageDir, ".nax", "features", featureName, acceptanceTestFilename(language));
24413
24488
  testPaths.push({ testPath, packageDir });
24414
24489
  }
24415
24490
  let totalCriteria = 0;
@@ -28357,7 +28432,7 @@ async function rollbackToRef(workdir, ref) {
28357
28432
  }
28358
28433
  logger.info("tdd", "Successfully rolled back git changes", { ref });
28359
28434
  }
28360
- 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) {
28361
28436
  const startTime = Date.now();
28362
28437
  let prompt;
28363
28438
  if (_sessionRunnerDeps.buildPrompt) {
@@ -28391,7 +28466,8 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
28391
28466
  featureName,
28392
28467
  storyId: story.id,
28393
28468
  sessionRole: role,
28394
- keepSessionOpen
28469
+ keepSessionOpen,
28470
+ interactionBridge
28395
28471
  });
28396
28472
  if (!result.success && result.pid) {
28397
28473
  await _sessionRunnerDeps.cleanupProcessTree(result.pid);
@@ -28756,7 +28832,8 @@ async function runThreeSessionTdd(options) {
28756
28832
  constitution,
28757
28833
  dryRun = false,
28758
28834
  lite = false,
28759
- _recursionDepth = 0
28835
+ _recursionDepth = 0,
28836
+ interactionChain
28760
28837
  } = options;
28761
28838
  const logger = getLogger();
28762
28839
  const MAX_RECURSION_DEPTH = 2;
@@ -28815,7 +28892,7 @@ async function runThreeSessionTdd(options) {
28815
28892
  let session1;
28816
28893
  if (!isRetry) {
28817
28894
  const testWriterTier = config2.tdd.sessionTiers?.testWriter ?? "balanced";
28818
- 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" }));
28819
28896
  sessions.push(session1);
28820
28897
  }
28821
28898
  if (session1 && !session1.success) {
@@ -28877,7 +28954,7 @@ async function runThreeSessionTdd(options) {
28877
28954
  });
28878
28955
  const session2Ref = await captureGitRef(workdir) ?? "HEAD";
28879
28956
  const implementerTier = config2.tdd.sessionTiers?.implementer ?? modelTier;
28880
- 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" }));
28881
28958
  sessions.push(session2);
28882
28959
  if (!session2.success) {
28883
28960
  needsHumanReview = true;
@@ -28995,6 +29072,7 @@ async function runThreeSessionTdd(options) {
28995
29072
  var init_orchestrator2 = __esm(() => {
28996
29073
  init_config();
28997
29074
  init_greenfield();
29075
+ init_bridge_builder();
28998
29076
  init_logger2();
28999
29077
  init_git();
29000
29078
  init_verification();
@@ -29060,6 +29138,7 @@ var executionStage, _executionDeps;
29060
29138
  var init_execution2 = __esm(() => {
29061
29139
  init_agents();
29062
29140
  init_config();
29141
+ init_bridge_builder();
29063
29142
  init_triggers();
29064
29143
  init_logger2();
29065
29144
  init_tdd();
@@ -29094,7 +29173,8 @@ var init_execution2 = __esm(() => {
29094
29173
  contextMarkdown: ctx.contextMarkdown,
29095
29174
  constitution: ctx.constitution?.content,
29096
29175
  dryRun: false,
29097
- lite: isLiteMode
29176
+ lite: isLiteMode,
29177
+ interactionChain: ctx.interaction
29098
29178
  });
29099
29179
  ctx.agentResult = {
29100
29180
  success: tddResult.success,
@@ -29168,34 +29248,11 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
29168
29248
  pidRegistry: ctx.pidRegistry,
29169
29249
  featureName: ctx.prd.feature,
29170
29250
  storyId: ctx.story.id,
29171
- interactionBridge: (() => {
29172
- const plugin = ctx.interaction?.getPrimary();
29173
- if (!plugin)
29174
- return;
29175
- const QUESTION_PATTERNS = [/\?/, /\bwhich\b/i, /\bshould i\b/i, /\bunclear\b/i, /\bplease clarify\b/i];
29176
- return {
29177
- detectQuestion: async (text) => QUESTION_PATTERNS.some((p) => p.test(text)),
29178
- onQuestionDetected: async (text) => {
29179
- const requestId = `ix-acp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
29180
- await plugin.send({
29181
- id: requestId,
29182
- type: "input",
29183
- featureName: ctx.prd.feature,
29184
- storyId: ctx.story.id,
29185
- stage: "execution",
29186
- summary: text,
29187
- fallback: "continue",
29188
- createdAt: Date.now()
29189
- });
29190
- try {
29191
- const response = await plugin.receive(requestId, 120000);
29192
- return response.value ?? "continue";
29193
- } catch {
29194
- return "continue";
29195
- }
29196
- }
29197
- };
29198
- })()
29251
+ interactionBridge: buildInteractionBridge(ctx.interaction, {
29252
+ featureName: ctx.prd.feature,
29253
+ storyId: ctx.story.id,
29254
+ stage: "execution"
29255
+ })
29199
29256
  });
29200
29257
  ctx.agentResult = result;
29201
29258
  await autoCommitIfDirty(storyWorkdir, "execution", "single-session", ctx.story.id);
@@ -32198,7 +32255,8 @@ async function checkGitignoreCoversNax(workdir) {
32198
32255
  ".nax/features/*/status.json",
32199
32256
  ".nax-pids",
32200
32257
  ".nax-wt/",
32201
- "**/.nax-acceptance*"
32258
+ "**/.nax-acceptance*",
32259
+ "**/.nax/features/*/"
32202
32260
  ];
32203
32261
  const missing = patterns.filter((pattern) => !content.includes(pattern));
32204
32262
  const passed = missing.length === 0;
@@ -34481,7 +34539,7 @@ __export(exports_parallel_executor_rectify, {
34481
34539
  });
34482
34540
  import path15 from "path";
34483
34541
  async function rectifyConflictedStory(options) {
34484
- const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter } = options;
34542
+ const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter, agentGetFn } = options;
34485
34543
  const logger = getSafeLogger();
34486
34544
  logger?.info("parallel", "Rectifying story on updated base", { storyId, attempt: "rectification" });
34487
34545
  try {
@@ -34513,7 +34571,8 @@ async function rectifyConflictedStory(options) {
34513
34571
  hooks,
34514
34572
  plugins: pluginRegistry,
34515
34573
  storyStartTime: new Date().toISOString(),
34516
- routing
34574
+ routing,
34575
+ agentGetFn
34517
34576
  };
34518
34577
  const pipelineResult = await runPipeline2(defaultPipeline2, pipelineContext, eventEmitter);
34519
34578
  const cost = pipelineResult.context.agentResult?.estimatedCost ?? 0;
@@ -34549,7 +34608,7 @@ var init_parallel_executor_rectify = __esm(() => {
34549
34608
  // src/execution/parallel-executor-rectification-pass.ts
34550
34609
  async function runRectificationPass(conflictedStories, options, prd, rectifyConflictedStory2) {
34551
34610
  const logger = getSafeLogger();
34552
- const { workdir, config: config2, hooks, pluginRegistry, eventEmitter } = options;
34611
+ const { workdir, config: config2, hooks, pluginRegistry, eventEmitter, agentGetFn } = options;
34553
34612
  const rectify = rectifyConflictedStory2 || (async (opts) => {
34554
34613
  const { rectifyConflictedStory: importedRectify } = await Promise.resolve().then(() => (init_parallel_executor_rectify(), exports_parallel_executor_rectify));
34555
34614
  return importedRectify(opts);
@@ -34570,7 +34629,8 @@ async function runRectificationPass(conflictedStories, options, prd, rectifyConf
34570
34629
  hooks,
34571
34630
  pluginRegistry,
34572
34631
  prd,
34573
- eventEmitter
34632
+ eventEmitter,
34633
+ agentGetFn
34574
34634
  });
34575
34635
  additionalCost += result.cost;
34576
34636
  if (result.success) {
@@ -68241,7 +68301,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
68241
68301
  init_registry();
68242
68302
  import { existsSync as existsSync11 } from "fs";
68243
68303
  import { join as join11 } from "path";
68244
- import { createInterface } from "readline";
68304
+ import { createInterface as createInterface2 } from "readline";
68245
68305
  init_test_strategy();
68246
68306
 
68247
68307
  // src/context/generator.ts
@@ -68779,6 +68839,8 @@ async function generateForPackage(packageDir, config2, dryRun = false, repoRoot)
68779
68839
 
68780
68840
  // src/cli/plan.ts
68781
68841
  init_pid_registry();
68842
+ init_bridge_builder();
68843
+ init_init();
68782
68844
  init_logger2();
68783
68845
 
68784
68846
  // src/prd/schema.ts
@@ -68907,11 +68969,19 @@ function validateStory(raw, index, allIds) {
68907
68969
  ...contextFiles.length > 0 ? { contextFiles } : {}
68908
68970
  };
68909
68971
  }
68972
+ function sanitizeInvalidEscapes(text) {
68973
+ let result = text.replace(/\\x([0-9a-fA-F]{1,2})/g, (_, hex3) => `\\u00${hex3.padStart(2, "0")}`);
68974
+ result = result.replace(/\\u([0-9a-fA-F]{1,3})(?![0-9a-fA-F])/g, (_, digits) => `\\u${digits.padStart(4, "0")}`);
68975
+ result = result.replace(/\\u(?![0-9a-fA-F])/g, "\\");
68976
+ result = result.replace(/\\([^"\\\/bfnrtu])/g, "$1");
68977
+ return result;
68978
+ }
68910
68979
  function parseRawString(text) {
68911
68980
  const extracted = extractJsonFromMarkdown(text);
68912
68981
  const cleaned = stripTrailingCommas(extracted);
68982
+ const sanitized = sanitizeInvalidEscapes(cleaned);
68913
68983
  try {
68914
- return JSON.parse(cleaned);
68984
+ return JSON.parse(sanitized);
68915
68985
  } catch (err) {
68916
68986
  const parseErr = err;
68917
68987
  throw new Error(`[schema] Failed to parse JSON: ${parseErr.message}`, { cause: parseErr });
@@ -68965,7 +69035,8 @@ var _planDeps = {
68965
69035
  existsSync: (path) => existsSync11(path),
68966
69036
  discoverWorkspacePackages: (repoRoot) => discoverWorkspacePackages(repoRoot),
68967
69037
  readPackageJsonAt: (path) => Bun.file(path).json().catch(() => null),
68968
- createInteractionBridge: () => createCliInteractionBridge()
69038
+ createInteractionBridge: () => createCliInteractionBridge(),
69039
+ initInteractionChain: (cfg, headless) => initInteractionChain(cfg, headless)
68969
69040
  };
68970
69041
  async function planCommand(workdir, config2, options) {
68971
69042
  const naxDir = join11(workdir, ".nax");
@@ -69028,7 +69099,13 @@ async function planCommand(workdir, config2, options) {
69028
69099
  const adapter = _planDeps.getAgent(agentName, config2);
69029
69100
  if (!adapter)
69030
69101
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
69031
- 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();
69032
69109
  const pidRegistry = new PidRegistry(workdir);
69033
69110
  const resolvedPerm = resolvePermissions(config2, "plan");
69034
69111
  const resolvedModel = config2?.plan?.model ?? "balanced";
@@ -69057,6 +69134,8 @@ async function planCommand(workdir, config2, options) {
69057
69134
  });
69058
69135
  } finally {
69059
69136
  await pidRegistry.killAll().catch(() => {});
69137
+ if (interactionChain)
69138
+ await interactionChain.destroy().catch(() => {});
69060
69139
  logger?.info("plan", "Interactive session ended", { durationMs: Date.now() - planStartTime });
69061
69140
  }
69062
69141
  if (!_planDeps.existsSync(outputPath)) {
@@ -69083,7 +69162,7 @@ function createCliInteractionBridge() {
69083
69162
  \uD83E\uDD16 Agent: ${text}
69084
69163
  You: `);
69085
69164
  return new Promise((resolve4) => {
69086
- const rl = createInterface({ input: process.stdin, terminal: false });
69165
+ const rl = createInterface2({ input: process.stdin, terminal: false });
69087
69166
  rl.once("line", (line) => {
69088
69167
  rl.close();
69089
69168
  resolve4(line.trim());
@@ -72036,7 +72115,8 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
72036
72115
  allStoryMetrics,
72037
72116
  pluginRegistry,
72038
72117
  formatterMode: options.formatterMode,
72039
- headless: options.headless
72118
+ headless: options.headless,
72119
+ agentGetFn: options.agentGetFn
72040
72120
  }, prd);
72041
72121
  prd = parallelResult.prd;
72042
72122
  totalCost = parallelResult.totalCost;