@nathapp/nax 0.54.8 → 0.54.9

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 +1930 -2112
  2. package/package.json +3 -1
package/dist/nax.js CHANGED
@@ -3004,7 +3004,7 @@ ${JSON.stringify(entry.data, null, 2)}`;
3004
3004
  }
3005
3005
  close() {}
3006
3006
  }
3007
- function initLogger(options) {
3007
+ function initLogger(options = { level: "warn" }) {
3008
3008
  if (instance) {
3009
3009
  throw new Error("Logger already initialized. Call getLogger() to access existing instance.");
3010
3010
  }
@@ -17805,7 +17805,7 @@ var init_zod = __esm(() => {
17805
17805
  });
17806
17806
 
17807
17807
  // src/config/schemas.ts
17808
- var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, DecomposeConfigSchema, ProjectProfileSchema, NaxConfigSchema;
17808
+ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, DecomposeConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, NaxConfigSchema;
17809
17809
  var init_schemas3 = __esm(() => {
17810
17810
  init_zod();
17811
17811
  TokenPricingSchema = exports_external.object({
@@ -18107,6 +18107,10 @@ var init_schemas3 = __esm(() => {
18107
18107
  testFramework: exports_external.string().optional(),
18108
18108
  lintTool: exports_external.string().optional()
18109
18109
  });
18110
+ VALID_AGENT_TYPES = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
18111
+ GenerateConfigSchema = exports_external.object({
18112
+ agents: exports_external.array(exports_external.enum(VALID_AGENT_TYPES)).optional()
18113
+ });
18110
18114
  NaxConfigSchema = exports_external.object({
18111
18115
  version: exports_external.number(),
18112
18116
  models: ModelMapSchema,
@@ -18130,6 +18134,7 @@ var init_schemas3 = __esm(() => {
18130
18134
  precheck: PrecheckConfigSchema.optional(),
18131
18135
  prompts: PromptsConfigSchema.optional(),
18132
18136
  decompose: DecomposeConfigSchema.optional(),
18137
+ generate: GenerateConfigSchema.optional(),
18133
18138
  project: ProjectProfileSchema.optional()
18134
18139
  }).refine((data) => data.version === 1, {
18135
18140
  message: "Invalid version: expected 1",
@@ -21101,7 +21106,7 @@ var init_paths = () => {};
21101
21106
 
21102
21107
  // src/config/loader.ts
21103
21108
  import { existsSync as existsSync5 } from "fs";
21104
- import { dirname, join as join6, resolve as resolve3 } from "path";
21109
+ import { basename, dirname, join as join6, resolve as resolve3 } from "path";
21105
21110
  function globalConfigPath() {
21106
21111
  return join6(globalConfigDir(), "config.json");
21107
21112
  }
@@ -21154,14 +21159,14 @@ function applyBatchModeCompat(conf) {
21154
21159
  }
21155
21160
  return conf;
21156
21161
  }
21157
- async function loadConfig(projectDir, cliOverrides) {
21162
+ async function loadConfig(startDir, cliOverrides) {
21158
21163
  let rawConfig = structuredClone(DEFAULT_CONFIG);
21159
21164
  const globalConfRaw = await loadJsonFile(globalConfigPath(), "config");
21160
21165
  if (globalConfRaw) {
21161
21166
  const globalConf = applyBatchModeCompat(applyRemovedStrategyCompat(globalConfRaw));
21162
21167
  rawConfig = deepMergeConfig(rawConfig, globalConf);
21163
21168
  }
21164
- const projDir = projectDir ?? findProjectDir();
21169
+ const projDir = startDir ? basename(startDir) === PROJECT_NAX_DIR ? startDir : findProjectDir(startDir) : findProjectDir();
21165
21170
  if (projDir) {
21166
21171
  const projConf = await loadJsonFile(join6(projDir, "config.json"), "config");
21167
21172
  if (projConf) {
@@ -22365,7 +22370,7 @@ var package_default;
22365
22370
  var init_package = __esm(() => {
22366
22371
  package_default = {
22367
22372
  name: "@nathapp/nax",
22368
- version: "0.54.8",
22373
+ version: "0.54.9",
22369
22374
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22370
22375
  type: "module",
22371
22376
  bin: {
@@ -22377,8 +22382,10 @@ var init_package = __esm(() => {
22377
22382
  build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
22378
22383
  typecheck: "bun x tsc --noEmit",
22379
22384
  lint: "bun x biome check src/ bin/",
22385
+ "lint:fix": "bun x biome lint --write src/ bin/",
22380
22386
  release: "bun scripts/release.ts",
22381
22387
  test: "bun test test/unit/ --timeout=60000 && bun test test/integration/ --timeout=60000 && bun test test/ui/ --timeout=60000",
22388
+ "test:bail": "bun test test/unit/ --timeout=60000 --bail && bun test test/integration/ --timeout=60000 --bail && bun test test/ui/ --timeout=60000 --bail",
22382
22389
  "test:watch": "bun test --watch",
22383
22390
  "test:unit": "bun test ./test/unit/ --timeout=60000",
22384
22391
  "test:integration": "bun test ./test/integration/ --timeout=60000",
@@ -22442,8 +22449,8 @@ var init_version = __esm(() => {
22442
22449
  NAX_VERSION = package_default.version;
22443
22450
  NAX_COMMIT = (() => {
22444
22451
  try {
22445
- if (/^[0-9a-f]{6,10}$/.test("6f97ec3"))
22446
- return "6f97ec3";
22452
+ if (/^[0-9a-f]{6,10}$/.test("3852fac"))
22453
+ return "3852fac";
22447
22454
  } catch {}
22448
22455
  try {
22449
22456
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -24718,6 +24725,36 @@ async function captureGitRef(workdir) {
24718
24725
  return;
24719
24726
  }
24720
24727
  }
24728
+ async function isGitRefValid(workdir, ref) {
24729
+ try {
24730
+ const { exitCode } = await gitWithTimeout(["cat-file", "-e", `${ref}^{commit}`], workdir);
24731
+ return exitCode === 0;
24732
+ } catch {
24733
+ return false;
24734
+ }
24735
+ }
24736
+ async function getMergeBase(workdir) {
24737
+ for (const branch of ["origin/main", "origin/master"]) {
24738
+ try {
24739
+ const { stdout, exitCode } = await gitWithTimeout(["merge-base", "HEAD", branch], workdir);
24740
+ if (exitCode === 0) {
24741
+ const sha = stdout.trim();
24742
+ if (sha)
24743
+ return sha;
24744
+ }
24745
+ } catch {}
24746
+ }
24747
+ try {
24748
+ const { stdout, exitCode } = await gitWithTimeout(["rev-list", "--max-parents=0", "HEAD"], workdir);
24749
+ if (exitCode === 0) {
24750
+ const sha = stdout.trim().split(`
24751
+ `)[0];
24752
+ if (sha)
24753
+ return sha;
24754
+ }
24755
+ } catch {}
24756
+ return;
24757
+ }
24721
24758
  async function hasCommitsForStory(workdir, storyId, maxCommits = 20) {
24722
24759
  try {
24723
24760
  const { stdout, exitCode } = await gitWithTimeout(["log", `-${maxCommits}`, "--oneline", "--grep", storyId], workdir);
@@ -25009,7 +25046,21 @@ function toReviewFindings(findings) {
25009
25046
  async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, modelResolver, naxConfig) {
25010
25047
  const startTime = Date.now();
25011
25048
  const logger = getSafeLogger();
25012
- if (!storyGitRef) {
25049
+ let effectiveRef;
25050
+ if (storyGitRef && await _semanticDeps.isGitRefValid(workdir, storyGitRef)) {
25051
+ effectiveRef = storyGitRef;
25052
+ } else {
25053
+ const fallback = await _semanticDeps.getMergeBase(workdir);
25054
+ if (fallback) {
25055
+ logger?.info("review", "storyGitRef missing or invalid \u2014 using merge-base fallback", {
25056
+ storyId: story.id,
25057
+ storyGitRef,
25058
+ fallback
25059
+ });
25060
+ effectiveRef = fallback;
25061
+ }
25062
+ }
25063
+ if (!effectiveRef) {
25013
25064
  return {
25014
25065
  check: "semantic",
25015
25066
  success: true,
@@ -25024,9 +25075,9 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
25024
25075
  modelTier: semanticConfig.modelTier,
25025
25076
  configProvided: !!naxConfig
25026
25077
  });
25027
- const rawDiff = await collectDiff(workdir, storyGitRef, semanticConfig.excludePatterns);
25078
+ const rawDiff = await collectDiff(workdir, effectiveRef, semanticConfig.excludePatterns);
25028
25079
  const needsTruncation = rawDiff.length > DIFF_CAP_BYTES;
25029
- const stat = needsTruncation ? await collectDiffStat(workdir, storyGitRef) : undefined;
25080
+ const stat = needsTruncation ? await collectDiffStat(workdir, effectiveRef) : undefined;
25030
25081
  const diff = truncateDiff(rawDiff, stat);
25031
25082
  const agent = modelResolver(semanticConfig.modelTier);
25032
25083
  if (!agent) {
@@ -25134,8 +25185,11 @@ ${formatFindings(parsed.findings)}`;
25134
25185
  var _semanticDeps, DIFF_CAP_BYTES = 51200;
25135
25186
  var init_semantic = __esm(() => {
25136
25187
  init_logger2();
25188
+ init_git();
25137
25189
  _semanticDeps = {
25138
- spawn: spawn2
25190
+ spawn: spawn2,
25191
+ isGitRefValid,
25192
+ getMergeBase
25139
25193
  };
25140
25194
  });
25141
25195
 
@@ -26259,8 +26313,8 @@ function extractTestStructure(source) {
26259
26313
  function deriveTestPatterns(contextFiles) {
26260
26314
  const patterns = new Set;
26261
26315
  for (const filePath of contextFiles) {
26262
- const basename = path6.basename(filePath);
26263
- const basenameNoExt = basename.replace(/\.(ts|js|tsx|jsx)$/, "");
26316
+ const basename2 = path6.basename(filePath);
26317
+ const basenameNoExt = basename2.replace(/\.(ts|js|tsx|jsx)$/, "");
26264
26318
  patterns.add(`${basenameNoExt}.test.ts`);
26265
26319
  patterns.add(`${basenameNoExt}.test.js`);
26266
26320
  patterns.add(`${basenameNoExt}.test.tsx`);
@@ -26313,8 +26367,8 @@ async function scanTestFiles(options) {
26313
26367
  const files = [];
26314
26368
  for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
26315
26369
  if (allowedBasenames !== null) {
26316
- const basename = path6.basename(filePath);
26317
- if (!allowedBasenames.has(basename)) {
26370
+ const basename2 = path6.basename(filePath);
26371
+ if (!allowedBasenames.has(basename2)) {
26318
26372
  continue;
26319
26373
  }
26320
26374
  }
@@ -30303,8 +30357,8 @@ function extractSearchTerms(sourceFile) {
30303
30357
  const withoutSrc = sourceFile.replace(/^src\//, "");
30304
30358
  const withoutExt = withoutSrc.replace(/\.ts$/, "");
30305
30359
  const parts = withoutExt.split("/");
30306
- const basename = parts[parts.length - 1];
30307
- return [`/${basename}`, withoutExt];
30360
+ const basename2 = parts[parts.length - 1];
30361
+ return [`/${basename2}`, withoutExt];
30308
30362
  }
30309
30363
  async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
30310
30364
  if (sourceFiles.length === 0 || testFilePatterns.length === 0)
@@ -31077,7 +31131,7 @@ __export(exports_init_context, {
31077
31131
  });
31078
31132
  import { existsSync as existsSync20 } from "fs";
31079
31133
  import { mkdir } from "fs/promises";
31080
- import { basename as basename2, join as join30 } from "path";
31134
+ import { basename as basename3, join as join30 } from "path";
31081
31135
  async function findFiles(dir, maxFiles = 200) {
31082
31136
  try {
31083
31137
  const proc = Bun.spawnSync([
@@ -31165,7 +31219,7 @@ async function scanProject(projectRoot) {
31165
31219
  const readmeSnippet = await readReadmeSnippet(projectRoot);
31166
31220
  const entryPoints = await detectEntryPoints(projectRoot);
31167
31221
  const configFiles = await detectConfigFiles(projectRoot);
31168
- const projectName = packageManifest?.name || basename2(projectRoot);
31222
+ const projectName = packageManifest?.name || basename3(projectRoot);
31169
31223
  return {
31170
31224
  projectName,
31171
31225
  fileTree,
@@ -31747,11 +31801,11 @@ function getSafeLogger6() {
31747
31801
  return getSafeLogger();
31748
31802
  }
31749
31803
  function extractPluginName(pluginPath) {
31750
- const basename4 = path12.basename(pluginPath);
31751
- if (basename4 === "index.ts" || basename4 === "index.js" || basename4 === "index.mjs") {
31804
+ const basename5 = path12.basename(pluginPath);
31805
+ if (basename5 === "index.ts" || basename5 === "index.js" || basename5 === "index.mjs") {
31752
31806
  return path12.basename(path12.dirname(pluginPath));
31753
31807
  }
31754
- return basename4.replace(/\.(ts|js|mjs)$/, "");
31808
+ return basename5.replace(/\.(ts|js|mjs)$/, "");
31755
31809
  }
31756
31810
  async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, disabledPlugins) {
31757
31811
  const loadedPlugins = [];
@@ -33956,2119 +34010,1712 @@ var init_headless_formatter = __esm(() => {
33956
34010
  init_version();
33957
34011
  });
33958
34012
 
33959
- // src/worktree/manager.ts
33960
- var exports_manager = {};
33961
- __export(exports_manager, {
33962
- _managerDeps: () => _managerDeps,
33963
- WorktreeManager: () => WorktreeManager
33964
- });
33965
- import { existsSync as existsSync32, symlinkSync } from "fs";
33966
- import { join as join47 } from "path";
33967
-
33968
- class WorktreeManager {
33969
- async create(projectRoot, storyId) {
33970
- validateStoryId(storyId);
33971
- const worktreePath = join47(projectRoot, ".nax-wt", storyId);
33972
- const branchName = `nax/${storyId}`;
33973
- try {
33974
- const proc = _managerDeps.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], {
33975
- cwd: projectRoot,
33976
- stdout: "pipe",
33977
- stderr: "pipe"
33978
- });
33979
- const exitCode = await proc.exited;
33980
- if (exitCode !== 0) {
33981
- const stderr = await new Response(proc.stderr).text();
33982
- throw new Error(`Failed to create worktree: ${stderr || "unknown error"}`);
33983
- }
33984
- } catch (error48) {
33985
- if (error48 instanceof Error) {
33986
- if (error48.message.includes("not a git repository")) {
33987
- throw new Error(`Not a git repository: ${projectRoot}`);
33988
- }
33989
- if (error48.message.includes("already exists")) {
33990
- throw new Error(`Worktree for story ${storyId} already exists at ${worktreePath}`);
33991
- }
33992
- throw error48;
33993
- }
33994
- throw new Error(`Failed to create worktree: ${String(error48)}`);
33995
- }
33996
- const nodeModulesSource = join47(projectRoot, "node_modules");
33997
- if (existsSync32(nodeModulesSource)) {
33998
- const nodeModulesTarget = join47(worktreePath, "node_modules");
33999
- try {
34000
- symlinkSync(nodeModulesSource, nodeModulesTarget, "dir");
34001
- } catch (error48) {
34002
- await this.remove(projectRoot, storyId);
34003
- throw new Error(`Failed to symlink node_modules: ${errorMessage(error48)}`);
34004
- }
34005
- }
34006
- const envSource = join47(projectRoot, ".env");
34007
- if (existsSync32(envSource)) {
34008
- const envTarget = join47(worktreePath, ".env");
34013
+ // src/pipeline/subscribers/events-writer.ts
34014
+ import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
34015
+ import { homedir as homedir5 } from "os";
34016
+ import { basename as basename6, join as join47 } from "path";
34017
+ function wireEventsWriter(bus, feature, runId, workdir) {
34018
+ const logger = getSafeLogger();
34019
+ const project = basename6(workdir);
34020
+ const eventsDir = join47(homedir5(), ".nax", "events", project);
34021
+ const eventsFile = join47(eventsDir, "events.jsonl");
34022
+ let dirReady = false;
34023
+ const write = (line) => {
34024
+ return (async () => {
34009
34025
  try {
34010
- symlinkSync(envSource, envTarget, "file");
34011
- } catch (error48) {
34012
- await this.remove(projectRoot, storyId);
34013
- throw new Error(`Failed to symlink .env: ${errorMessage(error48)}`);
34014
- }
34015
- }
34016
- }
34017
- async remove(projectRoot, storyId) {
34018
- validateStoryId(storyId);
34019
- const worktreePath = join47(projectRoot, ".nax-wt", storyId);
34020
- const branchName = `nax/${storyId}`;
34021
- try {
34022
- const proc = _managerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
34023
- cwd: projectRoot,
34024
- stdout: "pipe",
34025
- stderr: "pipe"
34026
- });
34027
- const exitCode = await proc.exited;
34028
- if (exitCode !== 0) {
34029
- const stderr = await new Response(proc.stderr).text();
34030
- if (stderr.includes("not found") || stderr.includes("does not exist") || stderr.includes("no such worktree") || stderr.includes("is not a working tree")) {
34031
- throw new Error(`Worktree not found: ${worktreePath}`);
34032
- }
34033
- throw new Error(`Failed to remove worktree: ${stderr || "unknown error"}`);
34034
- }
34035
- } catch (error48) {
34036
- if (error48 instanceof Error) {
34037
- throw error48;
34038
- }
34039
- throw new Error(`Failed to remove worktree: ${String(error48)}`);
34040
- }
34041
- try {
34042
- const proc = _managerDeps.spawn(["git", "branch", "-D", branchName], {
34043
- cwd: projectRoot,
34044
- stdout: "pipe",
34045
- stderr: "pipe"
34046
- });
34047
- const exitCode = await proc.exited;
34048
- if (exitCode !== 0) {
34049
- const stderr = await new Response(proc.stderr).text();
34050
- if (!stderr.includes("not found")) {
34051
- const logger = getSafeLogger();
34052
- logger?.warn("worktree", `Failed to delete branch ${branchName}`, { stderr });
34026
+ if (!dirReady) {
34027
+ await mkdir2(eventsDir, { recursive: true });
34028
+ dirReady = true;
34053
34029
  }
34054
- }
34055
- } catch (error48) {
34056
- const logger = getSafeLogger();
34057
- logger?.warn("worktree", `Failed to delete branch ${branchName}`, {
34058
- error: errorMessage(error48)
34059
- });
34060
- }
34061
- }
34062
- async list(projectRoot) {
34063
- try {
34064
- const proc = _managerDeps.spawn(["git", "worktree", "list", "--porcelain"], {
34065
- cwd: projectRoot,
34066
- stdout: "pipe",
34067
- stderr: "pipe"
34068
- });
34069
- const exitCode = await proc.exited;
34070
- if (exitCode !== 0) {
34071
- const stderr = await new Response(proc.stderr).text();
34072
- throw new Error(`Failed to list worktrees: ${stderr || "unknown error"}`);
34073
- }
34074
- const stdout = await new Response(proc.stdout).text();
34075
- return this.parseWorktreeList(stdout);
34076
- } catch (error48) {
34077
- if (error48 instanceof Error) {
34078
- throw error48;
34079
- }
34080
- throw new Error(`Failed to list worktrees: ${String(error48)}`);
34081
- }
34082
- }
34083
- parseWorktreeList(output) {
34084
- const worktrees = [];
34085
- const lines = output.trim().split(`
34030
+ await appendFile2(eventsFile, `${JSON.stringify(line)}
34086
34031
  `);
34087
- let currentWorktree = {};
34088
- for (const line of lines) {
34089
- if (line.startsWith("worktree ")) {
34090
- currentWorktree.path = line.substring("worktree ".length);
34091
- } else if (line.startsWith("branch ")) {
34092
- currentWorktree.branch = line.substring("branch ".length).replace("refs/heads/", "");
34093
- } else if (line === "") {
34094
- if (currentWorktree.path && currentWorktree.branch) {
34095
- worktrees.push(currentWorktree);
34096
- }
34097
- currentWorktree = {};
34032
+ } catch (err) {
34033
+ logger?.warn("events-writer", "Failed to write event line (non-fatal)", {
34034
+ event: line.event,
34035
+ error: String(err)
34036
+ });
34098
34037
  }
34099
- }
34100
- if (currentWorktree.path && currentWorktree.branch) {
34101
- worktrees.push(currentWorktree);
34102
- }
34103
- return worktrees;
34104
- }
34038
+ })();
34039
+ };
34040
+ const unsubs = [];
34041
+ unsubs.push(bus.on("run:started", (_ev) => {
34042
+ return write({ ts: new Date().toISOString(), event: "run:started", runId, feature, project });
34043
+ }));
34044
+ unsubs.push(bus.on("story:started", (ev) => {
34045
+ return write({
34046
+ ts: new Date().toISOString(),
34047
+ event: "story:started",
34048
+ runId,
34049
+ feature,
34050
+ project,
34051
+ storyId: ev.storyId
34052
+ });
34053
+ }));
34054
+ unsubs.push(bus.on("story:completed", (ev) => {
34055
+ return write({
34056
+ ts: new Date().toISOString(),
34057
+ event: "story:completed",
34058
+ runId,
34059
+ feature,
34060
+ project,
34061
+ storyId: ev.storyId
34062
+ });
34063
+ }));
34064
+ unsubs.push(bus.on("story:decomposed", (ev) => {
34065
+ return write({
34066
+ ts: new Date().toISOString(),
34067
+ event: "story:decomposed",
34068
+ runId,
34069
+ feature,
34070
+ project,
34071
+ storyId: ev.storyId,
34072
+ data: { subStoryCount: ev.subStoryCount }
34073
+ });
34074
+ }));
34075
+ unsubs.push(bus.on("story:failed", (ev) => {
34076
+ return write({
34077
+ ts: new Date().toISOString(),
34078
+ event: "story:failed",
34079
+ runId,
34080
+ feature,
34081
+ project,
34082
+ storyId: ev.storyId
34083
+ });
34084
+ }));
34085
+ unsubs.push(bus.on("run:completed", (_ev) => {
34086
+ return write({ ts: new Date().toISOString(), event: "on-complete", runId, feature, project });
34087
+ }));
34088
+ unsubs.push(bus.on("run:paused", (ev) => {
34089
+ return write({
34090
+ ts: new Date().toISOString(),
34091
+ event: "run:paused",
34092
+ runId,
34093
+ feature,
34094
+ project,
34095
+ ...ev.storyId !== undefined && { storyId: ev.storyId }
34096
+ });
34097
+ }));
34098
+ return () => {
34099
+ for (const u of unsubs)
34100
+ u();
34101
+ };
34105
34102
  }
34106
- var _managerDeps;
34107
- var init_manager = __esm(() => {
34103
+ var init_events_writer = __esm(() => {
34108
34104
  init_logger2();
34109
- init_bun_deps();
34110
- _managerDeps = {
34111
- spawn
34112
- };
34113
34105
  });
34114
34106
 
34115
- // src/worktree/merge.ts
34116
- var exports_merge = {};
34117
- __export(exports_merge, {
34118
- _mergeDeps: () => _mergeDeps,
34119
- MergeEngine: () => MergeEngine
34107
+ // src/pipeline/subscribers/hooks.ts
34108
+ function wireHooks(bus, hooks, workdir, feature) {
34109
+ const logger = getSafeLogger();
34110
+ const safe = (name, fn) => {
34111
+ return fn().catch((err) => logger?.warn("hooks-subscriber", `Hook "${name}" failed`, { error: String(err) })).catch(() => {});
34112
+ };
34113
+ const unsubs = [];
34114
+ unsubs.push(bus.on("run:started", (ev) => {
34115
+ return safe("on-start", () => fireHook(hooks, "on-start", hookCtx(feature, { status: "running" }), workdir));
34116
+ }));
34117
+ unsubs.push(bus.on("story:started", (ev) => {
34118
+ return safe("on-story-start", () => fireHook(hooks, "on-story-start", hookCtx(feature, { storyId: ev.storyId, model: ev.modelTier, agent: ev.agent }), workdir));
34119
+ }));
34120
+ unsubs.push(bus.on("story:completed", (ev) => {
34121
+ return safe("on-story-complete", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "passed", cost: ev.cost }), workdir));
34122
+ }));
34123
+ unsubs.push(bus.on("story:decomposed", (ev) => {
34124
+ return safe("on-story-complete (decomposed)", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "decomposed", subStoryCount: ev.subStoryCount }), workdir));
34125
+ }));
34126
+ unsubs.push(bus.on("story:failed", (ev) => {
34127
+ return safe("on-story-fail", () => fireHook(hooks, "on-story-fail", hookCtx(feature, { storyId: ev.storyId, status: "failed", reason: ev.reason }), workdir));
34128
+ }));
34129
+ unsubs.push(bus.on("story:paused", (ev) => {
34130
+ return safe("on-pause (story)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
34131
+ }));
34132
+ unsubs.push(bus.on("run:paused", (ev) => {
34133
+ return safe("on-pause (run)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
34134
+ }));
34135
+ unsubs.push(bus.on("run:completed", (ev) => {
34136
+ return safe("on-complete", () => fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: ev.totalCost ?? 0 }), workdir));
34137
+ }));
34138
+ unsubs.push(bus.on("run:resumed", (ev) => {
34139
+ return safe("on-resume", () => fireHook(hooks, "on-resume", hookCtx(feature, { status: "running" }), workdir));
34140
+ }));
34141
+ unsubs.push(bus.on("story:completed", (ev) => {
34142
+ return safe("on-session-end (completed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "passed" }), workdir));
34143
+ }));
34144
+ unsubs.push(bus.on("story:failed", (ev) => {
34145
+ return safe("on-session-end (failed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "failed" }), workdir));
34146
+ }));
34147
+ unsubs.push(bus.on("run:errored", (ev) => {
34148
+ return safe("on-error", () => fireHook(hooks, "on-error", hookCtx(feature, { reason: ev.reason }), workdir));
34149
+ }));
34150
+ return () => {
34151
+ for (const u of unsubs)
34152
+ u();
34153
+ };
34154
+ }
34155
+ var init_hooks2 = __esm(() => {
34156
+ init_story_context();
34157
+ init_hooks();
34158
+ init_logger2();
34120
34159
  });
34121
34160
 
34122
- class MergeEngine {
34123
- worktreeManager;
34124
- constructor(worktreeManager) {
34125
- this.worktreeManager = worktreeManager;
34126
- }
34127
- async merge(projectRoot, storyId) {
34128
- const branchName = `nax/${storyId}`;
34129
- try {
34130
- const mergeProc = _mergeDeps.spawn(["git", "merge", "--no-ff", branchName, "-m", `Merge branch '${branchName}'`], {
34131
- cwd: projectRoot,
34132
- stdout: "pipe",
34133
- stderr: "pipe"
34161
+ // src/pipeline/subscribers/interaction.ts
34162
+ function wireInteraction(bus, interactionChain, config2) {
34163
+ const logger = getSafeLogger();
34164
+ const unsubs = [];
34165
+ if (interactionChain && isTriggerEnabled("human-review", config2)) {
34166
+ unsubs.push(bus.on("human-review:requested", (ev) => {
34167
+ executeTrigger("human-review", {
34168
+ featureName: ev.feature ?? "",
34169
+ storyId: ev.storyId,
34170
+ iteration: ev.attempts ?? 0,
34171
+ reason: ev.reason
34172
+ }, config2, interactionChain).catch((err) => {
34173
+ logger?.warn("interaction-subscriber", "human-review trigger failed", {
34174
+ storyId: ev.storyId,
34175
+ error: String(err)
34176
+ });
34134
34177
  });
34135
- const exitCode = await mergeProc.exited;
34136
- const stderr = await new Response(mergeProc.stderr).text();
34137
- const stdout = await new Response(mergeProc.stdout).text();
34138
- if (exitCode === 0) {
34139
- try {
34140
- await this.worktreeManager.remove(projectRoot, storyId);
34141
- } catch (error48) {
34142
- const logger = getSafeLogger();
34143
- logger?.warn("worktree", `Failed to cleanup worktree for ${storyId}`, {
34144
- error: errorMessage(error48)
34178
+ }));
34179
+ }
34180
+ if (interactionChain && isTriggerEnabled("max-retries", config2)) {
34181
+ unsubs.push(bus.on("story:failed", (ev) => {
34182
+ if (!ev.countsTowardEscalation) {
34183
+ return;
34184
+ }
34185
+ executeTrigger("max-retries", {
34186
+ featureName: ev.feature ?? "",
34187
+ storyId: ev.storyId,
34188
+ iteration: ev.attempts ?? 0
34189
+ }, config2, interactionChain).then((response) => {
34190
+ if (response.action === "abort") {
34191
+ logger?.warn("interaction-subscriber", "max-retries abort requested", {
34192
+ storyId: ev.storyId
34145
34193
  });
34146
34194
  }
34147
- return { success: true };
34148
- }
34149
- const output = `${stdout}
34150
- ${stderr}`;
34151
- if (output.includes("CONFLICT") || output.includes("conflict") || output.includes("Automatic merge failed")) {
34152
- const conflictFiles = await this.getConflictFiles(projectRoot);
34153
- await this.abortMerge(projectRoot);
34154
- return {
34155
- success: false,
34156
- conflictFiles
34157
- };
34158
- }
34159
- throw new Error(`Merge failed: ${stderr || stdout || "unknown error"}`);
34160
- } catch (error48) {
34161
- if (error48 instanceof Error) {
34162
- throw error48;
34163
- }
34164
- throw new Error(`Failed to merge branch ${branchName}: ${String(error48)}`);
34165
- }
34195
+ }).catch((err) => {
34196
+ logger?.warn("interaction-subscriber", "max-retries trigger failed", {
34197
+ storyId: ev.storyId,
34198
+ error: String(err)
34199
+ });
34200
+ });
34201
+ }));
34166
34202
  }
34167
- async mergeAll(projectRoot, storyIds, dependencies) {
34168
- const orderedStories = this.topologicalSort(storyIds, dependencies);
34169
- const results = [];
34170
- const failedStories = new Set;
34171
- for (const storyId of orderedStories) {
34172
- const deps = dependencies[storyId] || [];
34173
- const hasFailedDeps = deps.some((dep) => failedStories.has(dep));
34174
- if (hasFailedDeps) {
34175
- results.push({
34176
- success: false,
34177
- storyId,
34178
- conflictFiles: []
34203
+ return () => {
34204
+ for (const u of unsubs)
34205
+ u();
34206
+ };
34207
+ }
34208
+ var init_interaction2 = __esm(() => {
34209
+ init_triggers();
34210
+ init_logger2();
34211
+ });
34212
+
34213
+ // src/pipeline/subscribers/registry.ts
34214
+ import { mkdir as mkdir3, writeFile } from "fs/promises";
34215
+ import { homedir as homedir6 } from "os";
34216
+ import { basename as basename7, join as join48 } from "path";
34217
+ function wireRegistry(bus, feature, runId, workdir) {
34218
+ const logger = getSafeLogger();
34219
+ const project = basename7(workdir);
34220
+ const runDir = join48(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
34221
+ const metaFile = join48(runDir, "meta.json");
34222
+ const unsub = bus.on("run:started", (_ev) => {
34223
+ return (async () => {
34224
+ try {
34225
+ await mkdir3(runDir, { recursive: true });
34226
+ const meta3 = {
34227
+ runId,
34228
+ project,
34229
+ feature,
34230
+ workdir,
34231
+ statusPath: join48(workdir, ".nax", "features", feature, "status.json"),
34232
+ eventsDir: join48(workdir, ".nax", "features", feature, "runs"),
34233
+ registeredAt: new Date().toISOString()
34234
+ };
34235
+ await writeFile(metaFile, JSON.stringify(meta3, null, 2));
34236
+ } catch (err) {
34237
+ logger?.warn("registry-writer", "Failed to write meta.json (non-fatal)", {
34238
+ path: metaFile,
34239
+ error: String(err)
34179
34240
  });
34180
- failedStories.add(storyId);
34181
- continue;
34182
34241
  }
34183
- let result = await this.merge(projectRoot, storyId);
34184
- if (!result.success && result.conflictFiles) {
34185
- try {
34186
- await this.rebaseWorktree(projectRoot, storyId);
34187
- result = await this.merge(projectRoot, storyId);
34188
- if (!result.success) {
34189
- results.push({
34190
- success: false,
34191
- storyId,
34192
- conflictFiles: result.conflictFiles,
34193
- retryCount: 1
34242
+ })();
34243
+ });
34244
+ return unsub;
34245
+ }
34246
+ var init_registry3 = __esm(() => {
34247
+ init_logger2();
34248
+ });
34249
+
34250
+ // src/pipeline/subscribers/reporters.ts
34251
+ function wireReporters(bus, pluginRegistry, runId, startTime) {
34252
+ const logger = getSafeLogger();
34253
+ const safe = (name, fn) => {
34254
+ return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
34255
+ };
34256
+ const unsubs = [];
34257
+ unsubs.push(bus.on("run:started", (ev) => {
34258
+ return safe("onRunStart", async () => {
34259
+ const reporters = pluginRegistry.getReporters();
34260
+ for (const r of reporters) {
34261
+ if (r.onRunStart) {
34262
+ try {
34263
+ await r.onRunStart({
34264
+ runId,
34265
+ feature: ev.feature,
34266
+ totalStories: ev.totalStories,
34267
+ startTime: new Date(startTime).toISOString()
34194
34268
  });
34195
- failedStories.add(storyId);
34196
- continue;
34269
+ } catch (err) {
34270
+ logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
34197
34271
  }
34198
- results.push({
34199
- success: true,
34200
- storyId,
34201
- retryCount: 1
34202
- });
34203
- } catch (error48) {
34204
- results.push({
34205
- success: false,
34206
- storyId,
34207
- conflictFiles: result.conflictFiles,
34208
- retryCount: 1
34209
- });
34210
- failedStories.add(storyId);
34211
34272
  }
34212
- } else if (result.success) {
34213
- results.push({
34214
- success: true,
34215
- storyId,
34216
- retryCount: 0
34217
- });
34218
- } else {
34219
- results.push({
34220
- success: false,
34221
- storyId,
34222
- retryCount: 0
34223
- });
34224
- failedStories.add(storyId);
34225
- }
34226
- }
34227
- return results;
34228
- }
34229
- topologicalSort(storyIds, dependencies) {
34230
- const visited = new Set;
34231
- const sorted = [];
34232
- const visiting = new Set;
34233
- const visit = (storyId) => {
34234
- if (visited.has(storyId)) {
34235
- return;
34236
- }
34237
- if (visiting.has(storyId)) {
34238
- throw new Error(`Circular dependency detected involving ${storyId}`);
34239
34273
  }
34240
- visiting.add(storyId);
34241
- const deps = dependencies[storyId] || [];
34242
- for (const dep of deps) {
34243
- if (storyIds.includes(dep)) {
34244
- visit(dep);
34274
+ });
34275
+ }));
34276
+ unsubs.push(bus.on("story:completed", (ev) => {
34277
+ return safe("onStoryComplete(completed)", async () => {
34278
+ const reporters = pluginRegistry.getReporters();
34279
+ for (const r of reporters) {
34280
+ if (r.onStoryComplete) {
34281
+ try {
34282
+ await r.onStoryComplete({
34283
+ runId,
34284
+ storyId: ev.storyId,
34285
+ status: "completed",
34286
+ runElapsedMs: ev.runElapsedMs,
34287
+ cost: ev.cost ?? 0,
34288
+ tier: ev.modelTier ?? "balanced",
34289
+ testStrategy: ev.testStrategy ?? "test-after"
34290
+ });
34291
+ } catch (err) {
34292
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
34293
+ }
34245
34294
  }
34246
34295
  }
34247
- visiting.delete(storyId);
34248
- visited.add(storyId);
34249
- sorted.push(storyId);
34250
- };
34251
- for (const storyId of storyIds) {
34252
- visit(storyId);
34253
- }
34254
- return sorted;
34255
- }
34256
- async rebaseWorktree(projectRoot, storyId) {
34257
- const worktreePath = `${projectRoot}/.nax-wt/${storyId}`;
34258
- try {
34259
- const currentBranchProc = _mergeDeps.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
34260
- cwd: projectRoot,
34261
- stdout: "pipe",
34262
- stderr: "pipe"
34263
- });
34264
- const exitCode = await currentBranchProc.exited;
34265
- if (exitCode !== 0) {
34266
- throw new Error("Failed to get current branch");
34296
+ });
34297
+ }));
34298
+ unsubs.push(bus.on("story:failed", (ev) => {
34299
+ return safe("onStoryComplete(failed)", async () => {
34300
+ const reporters = pluginRegistry.getReporters();
34301
+ for (const r of reporters) {
34302
+ if (r.onStoryComplete) {
34303
+ try {
34304
+ await r.onStoryComplete({
34305
+ runId,
34306
+ storyId: ev.storyId,
34307
+ status: "failed",
34308
+ runElapsedMs: Date.now() - startTime,
34309
+ cost: 0,
34310
+ tier: "balanced",
34311
+ testStrategy: "test-after"
34312
+ });
34313
+ } catch (err) {
34314
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
34315
+ }
34316
+ }
34267
34317
  }
34268
- const currentBranch = (await new Response(currentBranchProc.stdout).text()).trim();
34269
- const rebaseProc = _mergeDeps.spawn(["git", "rebase", currentBranch], {
34270
- cwd: worktreePath,
34271
- stdout: "pipe",
34272
- stderr: "pipe"
34273
- });
34274
- const rebaseExitCode = await rebaseProc.exited;
34275
- if (rebaseExitCode !== 0) {
34276
- const stderr = await new Response(rebaseProc.stderr).text();
34277
- const abortProc = _mergeDeps.spawn(["git", "rebase", "--abort"], {
34278
- cwd: worktreePath,
34279
- stdout: "pipe",
34280
- stderr: "pipe"
34281
- });
34282
- await abortProc.exited;
34283
- throw new Error(`Rebase failed: ${stderr || "unknown error"}`);
34318
+ });
34319
+ }));
34320
+ unsubs.push(bus.on("story:paused", (ev) => {
34321
+ return safe("onStoryComplete(paused)", async () => {
34322
+ const reporters = pluginRegistry.getReporters();
34323
+ for (const r of reporters) {
34324
+ if (r.onStoryComplete) {
34325
+ try {
34326
+ await r.onStoryComplete({
34327
+ runId,
34328
+ storyId: ev.storyId,
34329
+ status: "paused",
34330
+ runElapsedMs: Date.now() - startTime,
34331
+ cost: 0,
34332
+ tier: "balanced",
34333
+ testStrategy: "test-after"
34334
+ });
34335
+ } catch (err) {
34336
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
34337
+ }
34338
+ }
34284
34339
  }
34285
- } catch (error48) {
34286
- if (error48 instanceof Error) {
34287
- throw error48;
34340
+ });
34341
+ }));
34342
+ unsubs.push(bus.on("run:completed", (ev) => {
34343
+ return safe("onRunEnd", async () => {
34344
+ const reporters = pluginRegistry.getReporters();
34345
+ for (const r of reporters) {
34346
+ if (r.onRunEnd) {
34347
+ try {
34348
+ await r.onRunEnd({
34349
+ runId,
34350
+ totalDurationMs: Date.now() - startTime,
34351
+ totalCost: ev.totalCost ?? 0,
34352
+ storySummary: {
34353
+ completed: ev.passedStories,
34354
+ failed: ev.failedStories,
34355
+ skipped: 0,
34356
+ paused: 0
34357
+ }
34358
+ });
34359
+ } catch (err) {
34360
+ logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
34361
+ }
34362
+ }
34288
34363
  }
34289
- throw new Error(`Failed to rebase worktree ${storyId}: ${String(error48)}`);
34290
- }
34364
+ });
34365
+ }));
34366
+ return () => {
34367
+ for (const u of unsubs)
34368
+ u();
34369
+ };
34370
+ }
34371
+ var init_reporters = __esm(() => {
34372
+ init_logger2();
34373
+ });
34374
+
34375
+ // src/execution/deferred-review.ts
34376
+ var {spawn: spawn5 } = globalThis.Bun;
34377
+ async function captureRunStartRef(workdir) {
34378
+ try {
34379
+ const proc = _deferredReviewDeps.spawn({
34380
+ cmd: ["git", "rev-parse", "HEAD"],
34381
+ cwd: workdir,
34382
+ stdout: "pipe",
34383
+ stderr: "pipe"
34384
+ });
34385
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
34386
+ return stdout.trim();
34387
+ } catch {
34388
+ return "";
34291
34389
  }
34292
- async getConflictFiles(projectRoot) {
34293
- try {
34294
- const proc = _mergeDeps.spawn(["git", "diff", "--name-only", "--diff-filter=U"], {
34295
- cwd: projectRoot,
34296
- stdout: "pipe",
34297
- stderr: "pipe"
34298
- });
34299
- const exitCode = await proc.exited;
34300
- if (exitCode !== 0) {
34301
- return [];
34302
- }
34303
- const stdout = await new Response(proc.stdout).text();
34304
- return stdout.trim().split(`
34305
- `).filter((line) => line.length > 0);
34306
- } catch {
34307
- return [];
34308
- }
34390
+ }
34391
+ async function getChangedFilesForDeferred(workdir, baseRef) {
34392
+ try {
34393
+ const proc = _deferredReviewDeps.spawn({
34394
+ cmd: ["git", "diff", "--name-only", `${baseRef}...HEAD`],
34395
+ cwd: workdir,
34396
+ stdout: "pipe",
34397
+ stderr: "pipe"
34398
+ });
34399
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
34400
+ return stdout.trim().split(`
34401
+ `).filter(Boolean);
34402
+ } catch {
34403
+ return [];
34309
34404
  }
34310
- async abortMerge(projectRoot) {
34405
+ }
34406
+ async function runDeferredReview(workdir, reviewConfig, plugins, runStartRef) {
34407
+ if (!reviewConfig || reviewConfig.pluginMode !== "deferred") {
34408
+ return;
34409
+ }
34410
+ const reviewers = plugins.getReviewers();
34411
+ if (reviewers.length === 0) {
34412
+ return;
34413
+ }
34414
+ const changedFiles = await getChangedFilesForDeferred(workdir, runStartRef);
34415
+ const reviewerResults = [];
34416
+ let anyFailed = false;
34417
+ for (const reviewer of reviewers) {
34311
34418
  try {
34312
- const proc = _mergeDeps.spawn(["git", "merge", "--abort"], {
34313
- cwd: projectRoot,
34314
- stdout: "pipe",
34315
- stderr: "pipe"
34419
+ const result = await reviewer.check(workdir, changedFiles);
34420
+ reviewerResults.push({
34421
+ name: reviewer.name,
34422
+ passed: result.passed,
34423
+ output: result.output,
34424
+ exitCode: result.exitCode
34316
34425
  });
34317
- await proc.exited;
34426
+ if (!result.passed) {
34427
+ anyFailed = true;
34428
+ }
34318
34429
  } catch (error48) {
34319
- const logger = getSafeLogger();
34320
- logger?.warn("worktree", "Failed to abort merge", {
34321
- error: errorMessage(error48)
34430
+ const errorMsg = error48 instanceof Error ? error48.message : String(error48);
34431
+ reviewerResults.push({
34432
+ name: reviewer.name,
34433
+ passed: false,
34434
+ output: "",
34435
+ error: errorMsg
34322
34436
  });
34437
+ anyFailed = true;
34323
34438
  }
34324
34439
  }
34440
+ return { runStartRef, changedFiles, reviewerResults, anyFailed };
34325
34441
  }
34326
- var _mergeDeps;
34327
- var init_merge = __esm(() => {
34328
- init_logger2();
34329
- init_bun_deps();
34330
- _mergeDeps = {
34331
- spawn
34332
- };
34442
+ var _deferredReviewDeps;
34443
+ var init_deferred_review = __esm(() => {
34444
+ _deferredReviewDeps = { spawn: spawn5 };
34333
34445
  });
34334
34446
 
34335
- // src/execution/parallel-worker.ts
34336
- async function executeStoryInWorktree(story, worktreePath, context, routing, eventEmitter) {
34447
+ // src/execution/executor-types.ts
34448
+ function buildPreviewRouting(story, config2) {
34449
+ const cached2 = story.routing;
34450
+ const defaultComplexity = "medium";
34451
+ const defaultTier = "balanced";
34452
+ const defaultStrategy = "test-after";
34453
+ return {
34454
+ complexity: cached2?.complexity ?? defaultComplexity,
34455
+ modelTier: cached2?.modelTier ?? config2.autoMode.complexityRouting?.[defaultComplexity] ?? defaultTier,
34456
+ testStrategy: cached2?.testStrategy ?? defaultStrategy,
34457
+ reasoning: cached2 ? "cached from story.routing" : "preview (pending pipeline routing stage)"
34458
+ };
34459
+ }
34460
+
34461
+ // src/execution/dry-run.ts
34462
+ async function handleDryRun(ctx) {
34337
34463
  const logger = getSafeLogger();
34338
- try {
34339
- const pipelineContext = {
34340
- ...context,
34341
- effectiveConfig: context.effectiveConfig ?? context.config,
34342
- story,
34343
- stories: [story],
34344
- workdir: worktreePath,
34345
- routing
34346
- };
34347
- logger?.debug("parallel", "Executing story in worktree", {
34348
- storyId: story.id,
34349
- worktreePath
34464
+ ctx.statusWriter.setPrd(ctx.prd);
34465
+ ctx.statusWriter.setCurrentStory({
34466
+ storyId: ctx.storiesToExecute[0].id,
34467
+ title: ctx.storiesToExecute[0].title,
34468
+ complexity: ctx.routing.complexity,
34469
+ tddStrategy: ctx.routing.testStrategy,
34470
+ model: ctx.routing.modelTier,
34471
+ attempt: (ctx.storiesToExecute[0].attempts ?? 0) + 1,
34472
+ phase: "routing"
34473
+ });
34474
+ await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
34475
+ for (const s of ctx.storiesToExecute) {
34476
+ logger?.info("execution", "[DRY RUN] Would execute agent here", {
34477
+ storyId: s.id,
34478
+ storyTitle: s.title,
34479
+ modelTier: ctx.routing.modelTier,
34480
+ complexity: ctx.routing.complexity,
34481
+ testStrategy: ctx.routing.testStrategy
34350
34482
  });
34351
- const result = await runPipeline(defaultPipeline, pipelineContext, eventEmitter);
34352
- return {
34353
- success: result.success,
34354
- cost: result.context.agentResult?.estimatedCost || 0,
34355
- error: result.success ? undefined : result.reason
34356
- };
34357
- } catch (error48) {
34358
- return {
34359
- success: false,
34360
- cost: 0,
34361
- error: errorMessage(error48)
34362
- };
34363
34483
  }
34364
- }
34365
- async function executeParallelBatch(stories, projectRoot, config2, context, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs) {
34366
- const logger = getSafeLogger();
34367
- const results = {
34368
- pipelinePassed: [],
34369
- merged: [],
34370
- failed: [],
34371
- totalCost: 0,
34372
- mergeConflicts: [],
34373
- storyCosts: new Map
34374
- };
34375
- const executing = new Set;
34376
- for (const story of stories) {
34377
- const worktreePath = worktreePaths.get(story.id);
34378
- if (!worktreePath) {
34379
- results.failed.push({
34380
- story,
34381
- error: "Worktree not created"
34382
- });
34383
- continue;
34384
- }
34385
- const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
34386
- const storyConfig = storyEffectiveConfigs?.get(story.id);
34387
- const storyContext = storyConfig ? { ...context, effectiveConfig: storyConfig } : context;
34388
- const executePromise = executeStoryInWorktree(story, worktreePath, storyContext, routing, eventEmitter).then((result) => {
34389
- results.totalCost += result.cost;
34390
- results.storyCosts.set(story.id, result.cost);
34391
- if (result.success) {
34392
- results.pipelinePassed.push(story);
34393
- logger?.info("parallel", "Story execution succeeded", {
34394
- storyId: story.id,
34395
- cost: result.cost
34396
- });
34397
- } else {
34398
- results.failed.push({ story, error: result.error || "Unknown error" });
34399
- logger?.error("parallel", "Story execution failed", {
34400
- storyId: story.id,
34401
- error: result.error
34402
- });
34403
- }
34404
- }).finally(() => {
34405
- executing.delete(executePromise);
34484
+ for (const s of ctx.storiesToExecute) {
34485
+ markStoryPassed(ctx.prd, s.id);
34486
+ }
34487
+ await savePRD(ctx.prd, ctx.prdPath);
34488
+ for (const s of ctx.storiesToExecute) {
34489
+ pipelineEventBus.emit({
34490
+ type: "story:completed",
34491
+ storyId: s.id,
34492
+ story: s,
34493
+ passed: true,
34494
+ runElapsedMs: 0,
34495
+ cost: 0,
34496
+ modelTier: ctx.routing.modelTier,
34497
+ testStrategy: ctx.routing.testStrategy
34406
34498
  });
34407
- executing.add(executePromise);
34408
- if (executing.size >= maxConcurrency) {
34409
- await Promise.race(executing);
34410
- }
34411
34499
  }
34412
- await Promise.all(executing);
34413
- return results;
34500
+ ctx.statusWriter.setPrd(ctx.prd);
34501
+ ctx.statusWriter.setCurrentStory(null);
34502
+ await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
34503
+ return { storiesCompletedDelta: ctx.storiesToExecute.length, prdDirty: true };
34414
34504
  }
34415
- var init_parallel_worker = __esm(() => {
34505
+ var init_dry_run = __esm(() => {
34416
34506
  init_logger2();
34417
- init_runner();
34418
- init_stages();
34419
- init_routing();
34507
+ init_event_bus();
34508
+ init_prd();
34420
34509
  });
34421
34510
 
34422
- // src/execution/parallel-coordinator.ts
34423
- import { existsSync as existsSync33, symlinkSync as symlinkSync2 } from "fs";
34424
- import os3 from "os";
34425
- import { join as join48 } from "path";
34426
- function groupStoriesByDependencies(stories) {
34427
- const batches = [];
34428
- const processed = new Set;
34429
- const storyMap = new Map(stories.map((s) => [s.id, s]));
34430
- while (processed.size < stories.length) {
34431
- const batch = [];
34432
- for (const story of stories) {
34433
- if (processed.has(story.id))
34434
- continue;
34435
- const depsCompleted = story.dependencies.every((dep) => processed.has(dep) || !storyMap.has(dep));
34436
- if (depsCompleted) {
34437
- batch.push(story);
34438
- }
34439
- }
34440
- if (batch.length === 0) {
34441
- const remaining = stories.filter((s) => !processed.has(s.id));
34442
- const logger = getSafeLogger();
34443
- logger?.error("parallel", "Cannot resolve story dependencies", {
34444
- remainingStories: remaining.map((s) => s.id)
34445
- });
34446
- throw new Error("Circular dependency or missing dependency detected");
34447
- }
34448
- for (const story of batch) {
34449
- processed.add(story.id);
34511
+ // src/execution/escalation/tier-outcome.ts
34512
+ async function handleNoTierAvailable(ctx, failureCategory) {
34513
+ const logger = getSafeLogger();
34514
+ const outcome = resolveMaxAttemptsOutcome(failureCategory);
34515
+ if (outcome === "pause") {
34516
+ const pausedPrd = { ...ctx.prd };
34517
+ markStoryPaused(pausedPrd, ctx.story.id);
34518
+ await savePRD(pausedPrd, ctx.prdPath);
34519
+ logger?.warn("execution", "Story paused - no tier available (needs human review)", {
34520
+ storyId: ctx.story.id,
34521
+ failureCategory
34522
+ });
34523
+ if (ctx.featureDir) {
34524
+ await appendProgress(ctx.featureDir, ctx.story.id, "paused", `${ctx.story.title} \u2014 Execution stopped (needs human review)`);
34450
34525
  }
34451
- batches.push(batch);
34452
- }
34453
- return batches;
34454
- }
34455
- function buildDependencyMap(stories) {
34456
- const deps = {};
34457
- for (const story of stories) {
34458
- deps[story.id] = story.dependencies;
34526
+ pipelineEventBus.emit({
34527
+ type: "story:paused",
34528
+ storyId: ctx.story.id,
34529
+ reason: `Execution stopped (${failureCategory ?? "unknown"} requires human review)`,
34530
+ cost: ctx.totalCost
34531
+ });
34532
+ return { outcome: "paused", prdDirty: true, prd: pausedPrd };
34459
34533
  }
34460
- return deps;
34461
- }
34462
- function resolveMaxConcurrency(parallel) {
34463
- if (parallel === 0) {
34464
- return os3.cpus().length;
34534
+ const failedPrd = { ...ctx.prd };
34535
+ markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
34536
+ await savePRD(failedPrd, ctx.prdPath);
34537
+ logger?.error("execution", "Story failed - execution failed", {
34538
+ storyId: ctx.story.id
34539
+ });
34540
+ if (ctx.featureDir) {
34541
+ await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} \u2014 Execution failed`);
34465
34542
  }
34466
- return Math.max(1, parallel);
34543
+ pipelineEventBus.emit({
34544
+ type: "story:failed",
34545
+ storyId: ctx.story.id,
34546
+ story: ctx.story,
34547
+ reason: "Execution failed",
34548
+ countsTowardEscalation: true
34549
+ });
34550
+ return { outcome: "failed", prdDirty: true, prd: failedPrd };
34467
34551
  }
34468
- async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn, pidRegistry, interactionChain) {
34552
+ async function handleMaxAttemptsReached(ctx, failureCategory) {
34469
34553
  const logger = getSafeLogger();
34470
- const maxConcurrency = resolveMaxConcurrency(parallel);
34471
- const worktreeManager = new WorktreeManager;
34472
- const mergeEngine = new MergeEngine(worktreeManager);
34473
- logger?.info("parallel", "Starting parallel execution", {
34474
- totalStories: stories.length,
34475
- maxConcurrency
34476
- });
34477
- const batches = groupStoriesByDependencies(stories);
34478
- logger?.info("parallel", "Grouped stories into batches", {
34479
- batchCount: batches.length,
34480
- batches: batches.map((b, i) => ({ index: i, storyCount: b.length, storyIds: b.map((s) => s.id) }))
34481
- });
34482
- let storiesCompleted = 0;
34483
- let totalCost = 0;
34484
- const currentPrd = prd;
34485
- const allMergeConflicts = [];
34486
- for (let batchIndex = 0;batchIndex < batches.length; batchIndex++) {
34487
- const batch = batches[batchIndex];
34488
- logger?.info("parallel", `Executing batch ${batchIndex + 1}/${batches.length}`, {
34489
- storyCount: batch.length,
34490
- storyIds: batch.map((s) => s.id)
34554
+ const outcome = resolveMaxAttemptsOutcome(failureCategory);
34555
+ if (outcome === "pause") {
34556
+ const pausedPrd = { ...ctx.prd };
34557
+ markStoryPaused(pausedPrd, ctx.story.id);
34558
+ await savePRD(pausedPrd, ctx.prdPath);
34559
+ logger?.warn("execution", "Story paused - max attempts reached (needs human review)", {
34560
+ storyId: ctx.story.id,
34561
+ failureCategory
34491
34562
  });
34492
- const baseContext = {
34493
- config: config2,
34494
- effectiveConfig: config2,
34495
- prd: currentPrd,
34496
- featureDir,
34497
- hooks,
34498
- plugins,
34499
- storyStartTime: new Date().toISOString(),
34500
- agentGetFn,
34501
- pidRegistry,
34502
- interaction: interactionChain ?? undefined
34503
- };
34504
- const worktreePaths = new Map;
34505
- const storyEffectiveConfigs = new Map;
34506
- for (const story of batch) {
34507
- const worktreePath = join48(projectRoot, ".nax-wt", story.id);
34508
- try {
34509
- await worktreeManager.create(projectRoot, story.id);
34510
- worktreePaths.set(story.id, worktreePath);
34511
- logger?.info("parallel", "Created worktree for story", {
34512
- storyId: story.id,
34513
- worktreePath
34514
- });
34515
- if (story.workdir) {
34516
- const pkgNodeModulesSrc = join48(projectRoot, story.workdir, "node_modules");
34517
- const pkgNodeModulesDst = join48(worktreePath, story.workdir, "node_modules");
34518
- if (existsSync33(pkgNodeModulesSrc) && !existsSync33(pkgNodeModulesDst)) {
34519
- try {
34520
- symlinkSync2(pkgNodeModulesSrc, pkgNodeModulesDst, "dir");
34521
- logger?.debug("parallel", "Symlinked package node_modules", {
34522
- storyId: story.id,
34523
- src: pkgNodeModulesSrc
34524
- });
34525
- } catch (symlinkError) {
34526
- logger?.warn("parallel", "Failed to symlink package node_modules \u2014 test runner may not find deps", {
34527
- storyId: story.id,
34528
- error: errorMessage(symlinkError)
34529
- });
34530
- }
34531
- }
34532
- }
34533
- const rootConfigPath = join48(projectRoot, ".nax", "config.json");
34534
- const effectiveConfig = story.workdir ? await loadConfigForWorkdir(rootConfigPath, story.workdir) : config2;
34535
- storyEffectiveConfigs.set(story.id, effectiveConfig);
34536
- } catch (error48) {
34537
- markStoryFailed(currentPrd, story.id, undefined, undefined);
34538
- logger?.error("parallel", "Failed to create worktree", {
34539
- storyId: story.id,
34540
- error: errorMessage(error48)
34541
- });
34542
- }
34543
- }
34544
- const batchResult = await executeParallelBatch(batch, projectRoot, config2, baseContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs);
34545
- totalCost += batchResult.totalCost;
34546
- if (batchResult.pipelinePassed.length > 0) {
34547
- const successfulIds = batchResult.pipelinePassed.map((s) => s.id);
34548
- const deps = buildDependencyMap(batch);
34549
- logger?.info("parallel", "Merging successful stories", {
34550
- storyIds: successfulIds
34551
- });
34552
- const mergeResults = await mergeEngine.mergeAll(projectRoot, successfulIds, deps);
34553
- for (const mergeResult of mergeResults) {
34554
- if (mergeResult.success) {
34555
- markStoryPassed(currentPrd, mergeResult.storyId);
34556
- storiesCompleted++;
34557
- const mergedStory = batchResult.pipelinePassed.find((s) => s.id === mergeResult.storyId);
34558
- if (mergedStory)
34559
- batchResult.merged.push(mergedStory);
34560
- logger?.info("parallel", "Story merged successfully", {
34561
- storyId: mergeResult.storyId,
34562
- retryCount: mergeResult.retryCount
34563
- });
34564
- } else {
34565
- markStoryFailed(currentPrd, mergeResult.storyId, undefined, undefined);
34566
- batchResult.mergeConflicts.push({
34567
- storyId: mergeResult.storyId,
34568
- conflictFiles: mergeResult.conflictFiles || [],
34569
- originalCost: batchResult.storyCosts.get(mergeResult.storyId) ?? 0
34570
- });
34571
- logger?.error("parallel", "Merge conflict", {
34572
- storyId: mergeResult.storyId,
34573
- conflictFiles: mergeResult.conflictFiles
34574
- });
34575
- logger?.warn("parallel", "Worktree preserved for manual conflict resolution", {
34576
- storyId: mergeResult.storyId,
34577
- worktreePath: join48(projectRoot, ".nax-wt", mergeResult.storyId)
34578
- });
34579
- }
34580
- }
34581
- }
34582
- for (const { story, error: error48 } of batchResult.failed) {
34583
- markStoryFailed(currentPrd, story.id, undefined, undefined);
34584
- logger?.error("parallel", "Cleaning up failed story worktree", {
34585
- storyId: story.id,
34586
- error: error48
34587
- });
34588
- try {
34589
- await worktreeManager.remove(projectRoot, story.id);
34590
- } catch (cleanupError) {
34591
- logger?.warn("parallel", "Failed to clean up worktree", {
34592
- storyId: story.id,
34593
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
34594
- });
34595
- }
34563
+ if (ctx.featureDir) {
34564
+ await appendProgress(ctx.featureDir, ctx.story.id, "paused", `${ctx.story.title} \u2014 Max attempts reached (needs human review)`);
34596
34565
  }
34597
- await savePRD(currentPrd, prdPath);
34598
- allMergeConflicts.push(...batchResult.mergeConflicts);
34599
- logger?.info("parallel", `Batch ${batchIndex + 1} complete`, {
34600
- pipelinePassed: batchResult.pipelinePassed.length,
34601
- merged: batchResult.merged.length,
34602
- failed: batchResult.failed.length,
34603
- mergeConflicts: batchResult.mergeConflicts.length,
34604
- batchCost: batchResult.totalCost
34566
+ pipelineEventBus.emit({
34567
+ type: "story:paused",
34568
+ storyId: ctx.story.id,
34569
+ reason: `Max attempts reached (${failureCategory ?? "unknown"} requires human review)`,
34570
+ cost: ctx.totalCost
34605
34571
  });
34572
+ return { outcome: "paused", prdDirty: true, prd: pausedPrd };
34606
34573
  }
34607
- logger?.info("parallel", "Parallel execution complete", {
34608
- storiesCompleted,
34609
- totalCost
34574
+ const failedPrd = { ...ctx.prd };
34575
+ markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
34576
+ await savePRD(failedPrd, ctx.prdPath);
34577
+ logger?.error("execution", "Story failed - max attempts reached", {
34578
+ storyId: ctx.story.id,
34579
+ failureCategory
34610
34580
  });
34611
- return { storiesCompleted, totalCost, updatedPrd: currentPrd, mergeConflicts: allMergeConflicts };
34581
+ if (ctx.featureDir) {
34582
+ await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} \u2014 Max attempts reached`);
34583
+ }
34584
+ pipelineEventBus.emit({
34585
+ type: "story:failed",
34586
+ storyId: ctx.story.id,
34587
+ story: ctx.story,
34588
+ reason: "Max attempts reached",
34589
+ countsTowardEscalation: true
34590
+ });
34591
+ return { outcome: "failed", prdDirty: true, prd: failedPrd };
34612
34592
  }
34613
- var init_parallel_coordinator = __esm(() => {
34614
- init_loader();
34593
+ var init_tier_outcome = __esm(() => {
34615
34594
  init_logger2();
34595
+ init_event_bus();
34616
34596
  init_prd();
34617
- init_manager();
34618
- init_merge();
34619
- init_parallel_worker();
34620
- });
34621
-
34622
- // src/execution/parallel.ts
34623
- var init_parallel = __esm(() => {
34624
- init_parallel_coordinator();
34597
+ init_progress();
34598
+ init_tier_escalation();
34625
34599
  });
34626
34600
 
34627
- // src/execution/parallel-executor-rectify.ts
34628
- var exports_parallel_executor_rectify = {};
34629
- __export(exports_parallel_executor_rectify, {
34630
- rectifyConflictedStory: () => rectifyConflictedStory
34631
- });
34632
- import path15 from "path";
34633
- async function rectifyConflictedStory(options) {
34634
- const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter, agentGetFn } = options;
34635
- const logger = getSafeLogger();
34636
- logger?.info("parallel", "Rectifying story on updated base", { storyId, attempt: "rectification" });
34637
- try {
34638
- const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
34639
- const { MergeEngine: MergeEngine2 } = await Promise.resolve().then(() => (init_merge(), exports_merge));
34640
- const { runPipeline: runPipeline2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
34641
- const { defaultPipeline: defaultPipeline2 } = await Promise.resolve().then(() => (init_stages(), exports_stages));
34642
- const { routeTask: routeTask2 } = await Promise.resolve().then(() => (init_routing(), exports_routing));
34643
- const worktreeManager = new WorktreeManager2;
34644
- const mergeEngine = new MergeEngine2(worktreeManager);
34645
- try {
34646
- await worktreeManager.remove(workdir, storyId);
34647
- } catch {}
34648
- await worktreeManager.create(workdir, storyId);
34649
- const worktreePath = path15.join(workdir, ".nax-wt", storyId);
34650
- const story = prd.userStories.find((s) => s.id === storyId);
34651
- if (!story) {
34652
- return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
34653
- }
34654
- const routing = routeTask2(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
34655
- const pipelineContext = {
34656
- config: config2,
34657
- effectiveConfig: config2,
34658
- prd,
34659
- story,
34660
- stories: [story],
34661
- workdir: worktreePath,
34662
- featureDir: undefined,
34663
- hooks,
34664
- plugins: pluginRegistry,
34665
- storyStartTime: new Date().toISOString(),
34666
- routing,
34667
- agentGetFn
34668
- };
34669
- const pipelineResult = await runPipeline2(defaultPipeline2, pipelineContext, eventEmitter);
34670
- const cost = pipelineResult.context.agentResult?.estimatedCost ?? 0;
34671
- if (!pipelineResult.success) {
34672
- logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
34673
- return { success: false, storyId, cost, finalConflict: false, pipelineFailure: true };
34674
- }
34675
- const mergeResults = await mergeEngine.mergeAll(workdir, [storyId], { [storyId]: [] });
34676
- const mergeResult = mergeResults[0];
34677
- if (!mergeResult || !mergeResult.success) {
34678
- const conflictFiles = mergeResult?.conflictFiles ?? [];
34679
- logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
34680
- return { success: false, storyId, cost, finalConflict: true, conflictFiles };
34681
- }
34682
- logger?.info("parallel", "Rectification succeeded - story merged", {
34683
- storyId,
34684
- originalCost: options.originalCost,
34685
- rectificationCost: cost
34686
- });
34687
- return { success: true, storyId, cost };
34688
- } catch (error48) {
34689
- logger?.error("parallel", "Rectification failed - preserving worktree", {
34690
- storyId,
34691
- error: errorMessage(error48)
34692
- });
34693
- return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
34601
+ // src/execution/escalation/tier-escalation.ts
34602
+ function buildEscalationFailure(story, currentTier, reviewFindings, cost) {
34603
+ const stage = reviewFindings && reviewFindings.length > 0 ? "review" : "escalation";
34604
+ return {
34605
+ attempt: (story.attempts ?? 0) + 1,
34606
+ modelTier: currentTier,
34607
+ stage,
34608
+ summary: `Failed with tier ${currentTier}, escalating to next tier`,
34609
+ reviewFindings: reviewFindings && reviewFindings.length > 0 ? reviewFindings : undefined,
34610
+ cost: cost ?? 0,
34611
+ timestamp: new Date().toISOString()
34612
+ };
34613
+ }
34614
+ function resolveMaxAttemptsOutcome(failureCategory) {
34615
+ if (!failureCategory) {
34616
+ return "fail";
34617
+ }
34618
+ switch (failureCategory) {
34619
+ case "isolation-violation":
34620
+ case "verifier-rejected":
34621
+ case "greenfield-no-tests":
34622
+ return "pause";
34623
+ case "runtime-crash":
34624
+ return "pause";
34625
+ case "session-failure":
34626
+ case "tests-failing":
34627
+ return "fail";
34628
+ default:
34629
+ return "fail";
34694
34630
  }
34695
34631
  }
34696
- var init_parallel_executor_rectify = __esm(() => {
34697
- init_logger2();
34698
- });
34699
-
34700
- // src/execution/parallel-executor-rectification-pass.ts
34701
- async function runRectificationPass(conflictedStories, options, prd, rectifyConflictedStory2) {
34632
+ function shouldRetrySameTier(verifyResult) {
34633
+ return verifyResult?.status === "RUNTIME_CRASH";
34634
+ }
34635
+ async function handleTierEscalation(ctx) {
34702
34636
  const logger = getSafeLogger();
34703
- const { workdir, config: config2, hooks, pluginRegistry, eventEmitter, agentGetFn } = options;
34704
- const rectify = rectifyConflictedStory2 || (async (opts) => {
34705
- const { rectifyConflictedStory: importedRectify } = await Promise.resolve().then(() => (init_parallel_executor_rectify(), exports_parallel_executor_rectify));
34706
- return importedRectify(opts);
34707
- });
34708
- const rectificationMetrics = [];
34709
- let rectifiedCount = 0;
34710
- let stillConflictingCount = 0;
34711
- let additionalCost = 0;
34712
- logger?.info("parallel", "Starting merge conflict rectification", {
34713
- stories: conflictedStories.map((s) => s.storyId),
34714
- totalConflicts: conflictedStories.length
34715
- });
34716
- for (const conflictInfo of conflictedStories) {
34717
- const result = await rectify({
34718
- ...conflictInfo,
34719
- workdir,
34720
- config: config2,
34721
- hooks,
34722
- pluginRegistry,
34723
- prd,
34724
- eventEmitter,
34725
- agentGetFn
34637
+ if (shouldRetrySameTier(ctx.verifyResult)) {
34638
+ logger?.warn("escalation", "Runtime crash detected \u2014 retrying same tier (transient, not a code issue)", {
34639
+ storyId: ctx.story.id
34726
34640
  });
34727
- additionalCost += result.cost;
34728
- if (result.success) {
34729
- markStoryPassed(prd, result.storyId);
34730
- rectifiedCount++;
34731
- rectificationMetrics.push({
34732
- storyId: result.storyId,
34733
- complexity: "unknown",
34734
- modelTier: "parallel",
34735
- modelUsed: "parallel",
34736
- attempts: 1,
34737
- finalTier: "parallel",
34738
- success: true,
34739
- cost: result.cost,
34740
- durationMs: 0,
34741
- firstPassSuccess: false,
34742
- startedAt: new Date().toISOString(),
34743
- completedAt: new Date().toISOString(),
34744
- source: "rectification",
34745
- rectifiedFromConflict: true,
34746
- originalCost: conflictInfo.originalCost,
34747
- rectificationCost: result.cost
34641
+ return { outcome: "retry-same", prdDirty: false, prd: ctx.prd };
34642
+ }
34643
+ const nextTier = escalateTier(ctx.routing.modelTier, ctx.config.autoMode.escalation.tierOrder);
34644
+ const escalateWholeBatch = ctx.config.autoMode.escalation.escalateEntireBatch ?? true;
34645
+ const storiesToEscalate = ctx.isBatchExecution && escalateWholeBatch ? ctx.storiesToExecute : [ctx.story];
34646
+ const escalateRetryAsLite = ctx.pipelineResult.context.retryAsLite === true;
34647
+ const escalateFailureCategory = ctx.pipelineResult.context.tddFailureCategory;
34648
+ const escalateReviewFindings = ctx.pipelineResult.context.reviewFindings;
34649
+ const escalateRetryAsTestAfter = escalateFailureCategory === "greenfield-no-tests";
34650
+ const routingMode = ctx.config.routing.llm?.mode ?? "hybrid";
34651
+ if (!nextTier || !ctx.config.autoMode.escalation.enabled) {
34652
+ return await handleNoTierAvailable(ctx, escalateFailureCategory);
34653
+ }
34654
+ const maxAttempts = calculateMaxIterations(ctx.config.autoMode.escalation.tierOrder);
34655
+ const canEscalate = storiesToEscalate.every((s) => (s.attempts ?? 0) < maxAttempts);
34656
+ if (!canEscalate) {
34657
+ return await handleMaxAttemptsReached(ctx, escalateFailureCategory);
34658
+ }
34659
+ for (const s of storiesToEscalate) {
34660
+ const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
34661
+ const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
34662
+ if (shouldSwitchToTestAfter) {
34663
+ logger?.warn("escalation", "Switching strategy to test-after (greenfield-no-tests fallback)", {
34664
+ storyId: s.id,
34665
+ fromStrategy: currentTestStrategy,
34666
+ toStrategy: "test-after"
34748
34667
  });
34749
34668
  } else {
34750
- const isFinalConflict = result.finalConflict === true;
34751
- if (isFinalConflict) {
34752
- stillConflictingCount++;
34753
- }
34669
+ logger?.warn("escalation", "Escalating story to next tier", {
34670
+ storyId: s.id,
34671
+ nextTier,
34672
+ retryAsLite: escalateRetryAsLite
34673
+ });
34754
34674
  }
34755
34675
  }
34756
- logger?.info("parallel", "Rectification complete", {
34757
- rectified: rectifiedCount,
34758
- stillConflicting: stillConflictingCount
34759
- });
34760
- return { rectifiedCount, stillConflictingCount, additionalCost, updatedPrd: prd, rectificationMetrics };
34676
+ const pipelineReason = ctx.pipelineResult.reason ? `: ${ctx.pipelineResult.reason}` : "";
34677
+ const errorMessage2 = `Attempt ${ctx.story.attempts + 1} failed with model tier: ${ctx.routing.modelTier}${ctx.isBatchExecution ? " (in batch)" : ""}${pipelineReason}`;
34678
+ const updatedPrd = {
34679
+ ...ctx.prd,
34680
+ userStories: ctx.prd.userStories.map((s) => {
34681
+ const shouldEscalate = storiesToEscalate.some((story) => story.id === s.id);
34682
+ if (!shouldEscalate)
34683
+ return s;
34684
+ const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
34685
+ const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
34686
+ const baseRouting = s.routing ?? { ...ctx.routing };
34687
+ const updatedRouting = {
34688
+ ...baseRouting,
34689
+ modelTier: shouldSwitchToTestAfter ? baseRouting.modelTier : nextTier,
34690
+ ...escalateRetryAsLite ? { testStrategy: "three-session-tdd-lite" } : {},
34691
+ ...shouldSwitchToTestAfter ? { testStrategy: "test-after" } : {}
34692
+ };
34693
+ const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
34694
+ const isChangingTier = currentStoryTier !== nextTier;
34695
+ const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
34696
+ const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings, ctx.attemptCost);
34697
+ return {
34698
+ ...s,
34699
+ attempts: shouldResetAttempts ? 0 : (s.attempts ?? 0) + 1,
34700
+ routing: updatedRouting,
34701
+ priorErrors: [...s.priorErrors || [], errorMessage2],
34702
+ priorFailures: [...s.priorFailures || [], escalationFailure]
34703
+ };
34704
+ })
34705
+ };
34706
+ await _tierEscalationDeps.savePRD(updatedPrd, ctx.prdPath);
34707
+ for (const story of storiesToEscalate) {
34708
+ clearCacheForStory(story.id);
34709
+ }
34710
+ if (routingMode === "hybrid") {
34711
+ await tryLlmBatchRoute(ctx.config, storiesToEscalate, "hybrid-re-route-pipeline");
34712
+ }
34713
+ return {
34714
+ outcome: "escalated",
34715
+ prdDirty: true,
34716
+ prd: updatedPrd
34717
+ };
34761
34718
  }
34762
- var init_parallel_executor_rectification_pass = __esm(() => {
34719
+ var _tierEscalationDeps;
34720
+ var init_tier_escalation = __esm(() => {
34721
+ init_hooks();
34763
34722
  init_logger2();
34764
34723
  init_prd();
34724
+ init_routing();
34725
+ init_llm();
34726
+ init_escalation();
34727
+ init_helpers();
34728
+ init_progress();
34729
+ init_tier_outcome();
34730
+ _tierEscalationDeps = {
34731
+ savePRD
34732
+ };
34765
34733
  });
34766
34734
 
34767
- // src/execution/lifecycle/parallel-lifecycle.ts
34768
- var exports_parallel_lifecycle = {};
34769
- __export(exports_parallel_lifecycle, {
34770
- handleParallelCompletion: () => handleParallelCompletion
34735
+ // src/execution/escalation/index.ts
34736
+ var init_escalation = __esm(() => {
34737
+ init_tier_escalation();
34771
34738
  });
34772
- async function handleParallelCompletion(options) {
34739
+
34740
+ // src/execution/pipeline-result-handler.ts
34741
+ function filterOutputFiles(files) {
34742
+ const NOISE = [
34743
+ /\.test\.(ts|js|tsx|jsx)$/,
34744
+ /\.spec\.(ts|js|tsx|jsx)$/,
34745
+ /package-lock\.json$/,
34746
+ /bun\.lock(b?)$/,
34747
+ /\.gitignore$/,
34748
+ /^nax\//
34749
+ ];
34750
+ return files.filter((f) => !NOISE.some((p) => p.test(f))).slice(0, 15);
34751
+ }
34752
+ async function handlePipelineSuccess(ctx, pipelineResult) {
34773
34753
  const logger = getSafeLogger();
34774
- const {
34775
- runId,
34776
- feature,
34777
- startedAt,
34778
- completedAt,
34779
- prd,
34780
- allStoryMetrics,
34781
- totalCost,
34782
- storiesCompleted,
34783
- durationMs,
34784
- workdir,
34785
- pluginRegistry
34786
- } = options;
34787
- const runMetrics = {
34788
- runId,
34789
- feature,
34790
- startedAt,
34791
- completedAt,
34792
- totalCost,
34793
- totalStories: allStoryMetrics.length,
34794
- storiesCompleted,
34795
- storiesFailed: countStories(prd).failed,
34796
- totalDurationMs: durationMs,
34797
- stories: allStoryMetrics
34798
- };
34799
- await saveRunMetrics(workdir, runMetrics);
34800
- const finalCounts = countStories(prd);
34801
- logger?.info("run.complete", "Feature execution completed", {
34802
- runId,
34803
- feature,
34804
- success: true,
34805
- totalStories: finalCounts.total,
34806
- storiesCompleted,
34807
- storiesFailed: finalCounts.failed,
34808
- storiesPending: finalCounts.pending,
34809
- totalCost,
34810
- durationMs
34811
- });
34812
- const reporters = pluginRegistry.getReporters();
34813
- for (const reporter of reporters) {
34814
- if (reporter.onRunEnd) {
34754
+ const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
34755
+ const prd = ctx.prd;
34756
+ if (pipelineResult.context.storyMetrics) {
34757
+ ctx.allStoryMetrics.push(...pipelineResult.context.storyMetrics);
34758
+ }
34759
+ const storiesCompletedDelta = ctx.storiesToExecute.length;
34760
+ for (const completedStory of ctx.storiesToExecute) {
34761
+ const now = Date.now();
34762
+ logger?.info("story.complete", "Story completed successfully", {
34763
+ storyId: completedStory.id,
34764
+ storyTitle: completedStory.title,
34765
+ totalCost: ctx.totalCost + costDelta,
34766
+ runElapsedMs: now - ctx.startTime,
34767
+ storyDurationMs: ctx.storyStartTime ? now - ctx.storyStartTime : undefined
34768
+ });
34769
+ }
34770
+ if (ctx.storyGitRef) {
34771
+ for (const completedStory of ctx.storiesToExecute) {
34815
34772
  try {
34816
- await reporter.onRunEnd({
34817
- runId,
34818
- totalDurationMs: durationMs,
34819
- totalCost,
34820
- storySummary: {
34821
- completed: storiesCompleted,
34822
- failed: finalCounts.failed,
34823
- skipped: finalCounts.skipped,
34824
- paused: finalCounts.paused
34825
- }
34773
+ const rawFiles = await captureOutputFiles(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
34774
+ const filtered = filterOutputFiles(rawFiles);
34775
+ if (filtered.length > 0) {
34776
+ completedStory.outputFiles = filtered;
34777
+ }
34778
+ const diffSummary = await captureDiffSummary(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
34779
+ if (diffSummary) {
34780
+ completedStory.diffSummary = diffSummary;
34781
+ }
34782
+ } catch {}
34783
+ }
34784
+ }
34785
+ const updatedCounts = countStories(prd);
34786
+ logger?.info("progress", "Progress update", {
34787
+ totalStories: updatedCounts.total,
34788
+ passedStories: updatedCounts.passed,
34789
+ failedStories: updatedCounts.failed,
34790
+ pendingStories: updatedCounts.pending,
34791
+ totalCost: ctx.totalCost + costDelta,
34792
+ costLimit: ctx.config.execution.costLimit,
34793
+ elapsedMs: Date.now() - ctx.startTime,
34794
+ storyDurationMs: ctx.storyStartTime ? Date.now() - ctx.storyStartTime : undefined
34795
+ });
34796
+ return { storiesCompletedDelta, costDelta, prd, prdDirty: true };
34797
+ }
34798
+ async function handlePipelineFailure(ctx, pipelineResult) {
34799
+ const logger = getSafeLogger();
34800
+ let prd = ctx.prd;
34801
+ let prdDirty = false;
34802
+ const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
34803
+ switch (pipelineResult.finalAction) {
34804
+ case "pause":
34805
+ markStoryPaused(prd, ctx.story.id);
34806
+ await savePRD(prd, ctx.prdPath);
34807
+ prdDirty = true;
34808
+ logger?.warn("pipeline", "Story paused", { storyId: ctx.story.id, reason: pipelineResult.reason });
34809
+ pipelineEventBus.emit({
34810
+ type: "story:paused",
34811
+ storyId: ctx.story.id,
34812
+ reason: pipelineResult.reason || "Pipeline paused",
34813
+ cost: ctx.totalCost
34814
+ });
34815
+ break;
34816
+ case "skip":
34817
+ logger?.warn("pipeline", "Story skipped", { storyId: ctx.story.id, reason: pipelineResult.reason });
34818
+ prdDirty = true;
34819
+ break;
34820
+ case "fail":
34821
+ markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory, pipelineResult.stoppedAtStage);
34822
+ await savePRD(prd, ctx.prdPath);
34823
+ prdDirty = true;
34824
+ logger?.error("pipeline", "Story failed", { storyId: ctx.story.id, reason: pipelineResult.reason });
34825
+ if (ctx.featureDir) {
34826
+ await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} \u2014 ${pipelineResult.reason}`);
34827
+ }
34828
+ pipelineEventBus.emit({
34829
+ type: "story:failed",
34830
+ storyId: ctx.story.id,
34831
+ story: ctx.story,
34832
+ reason: pipelineResult.reason || "Pipeline failed",
34833
+ countsTowardEscalation: true,
34834
+ feature: ctx.feature,
34835
+ attempts: ctx.story.attempts
34836
+ });
34837
+ if (ctx.story.attempts !== undefined && ctx.story.attempts >= ctx.config.execution.rectification.maxRetries) {
34838
+ await pipelineEventBus.emitAsync({
34839
+ type: "human-review:requested",
34840
+ storyId: ctx.story.id,
34841
+ reason: pipelineResult.reason || "Max retries exceeded",
34842
+ feature: ctx.feature,
34843
+ attempts: ctx.story.attempts
34826
34844
  });
34827
- } catch (error48) {
34828
- logger?.warn("plugins", `Reporter '${reporter.name}' onRunEnd failed`, { error: error48 });
34829
34845
  }
34846
+ break;
34847
+ case "escalate": {
34848
+ const escalationResult = await handleTierEscalation({
34849
+ story: ctx.story,
34850
+ storiesToExecute: ctx.storiesToExecute,
34851
+ isBatchExecution: ctx.isBatchExecution,
34852
+ routing: ctx.routing,
34853
+ pipelineResult,
34854
+ config: ctx.config,
34855
+ prd,
34856
+ prdPath: ctx.prdPath,
34857
+ featureDir: ctx.featureDir,
34858
+ hooks: ctx.hooks,
34859
+ feature: ctx.feature,
34860
+ totalCost: ctx.totalCost,
34861
+ workdir: ctx.workdir,
34862
+ attemptCost: pipelineResult.context.agentResult?.estimatedCost || 0
34863
+ });
34864
+ prd = escalationResult.prd;
34865
+ prdDirty = escalationResult.prdDirty;
34866
+ break;
34830
34867
  }
34831
34868
  }
34869
+ return { prd, prdDirty, costDelta };
34832
34870
  }
34833
- var init_parallel_lifecycle = __esm(() => {
34871
+ var init_pipeline_result_handler = __esm(() => {
34834
34872
  init_logger2();
34835
- init_metrics();
34873
+ init_event_bus();
34836
34874
  init_prd();
34875
+ init_git();
34876
+ init_escalation();
34877
+ init_progress();
34837
34878
  });
34838
34879
 
34839
- // src/execution/parallel-executor.ts
34840
- var exports_parallel_executor = {};
34841
- __export(exports_parallel_executor, {
34842
- runParallelExecution: () => runParallelExecution,
34843
- _parallelExecutorDeps: () => _parallelExecutorDeps
34844
- });
34845
- import * as os4 from "os";
34846
- import path16 from "path";
34847
- async function runParallelExecution(options, initialPrd) {
34880
+ // src/execution/iteration-runner.ts
34881
+ import { join as join49 } from "path";
34882
+ async function runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics) {
34848
34883
  const logger = getSafeLogger();
34849
- const {
34850
- prdPath,
34851
- workdir,
34852
- config: config2,
34853
- hooks,
34854
- feature,
34855
- featureDir,
34856
- parallelCount,
34857
- eventEmitter,
34858
- statusWriter,
34859
- runId,
34860
- startedAt,
34861
- startTime,
34862
- pluginRegistry,
34863
- formatterMode,
34864
- headless
34865
- } = options;
34866
- let { totalCost, iterations, storiesCompleted, allStoryMetrics } = options;
34867
- let prd = initialPrd;
34868
- const readyStories = getAllReadyStories(prd);
34869
- if (readyStories.length === 0) {
34870
- logger?.info("parallel", "No stories ready for parallel execution");
34871
- return {
34872
- prd,
34873
- totalCost,
34874
- storiesCompleted,
34875
- completed: false,
34876
- storyMetrics: [],
34877
- rectificationStats: { rectified: 0, stillConflicting: 0 }
34878
- };
34879
- }
34880
- const maxConcurrency = parallelCount === 0 ? os4.cpus().length : Math.max(1, parallelCount);
34881
- logger?.info("parallel", "Starting parallel execution mode", {
34882
- totalStories: readyStories.length,
34883
- maxConcurrency
34884
- });
34885
- statusWriter.setPrd(prd);
34886
- await statusWriter.update(totalCost, iterations, {
34887
- parallel: {
34888
- enabled: true,
34889
- maxConcurrency,
34890
- activeStories: readyStories.map((s) => ({
34891
- storyId: s.id,
34892
- worktreePath: path16.join(workdir, ".nax-wt", s.id)
34893
- }))
34894
- }
34895
- });
34896
- const initialPassedIds = new Set(initialPrd.userStories.filter((s) => s.status === "passed").map((s) => s.id));
34897
- const batchStartedAt = new Date().toISOString();
34898
- const batchStartMs = Date.now();
34899
- const batchStoryMetrics = [];
34900
- let conflictedStories = [];
34901
- try {
34902
- const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn, options.pidRegistry, options.interactionChain);
34903
- const batchDurationMs = Date.now() - batchStartMs;
34904
- const batchCompletedAt = new Date().toISOString();
34905
- prd = parallelResult.updatedPrd;
34906
- storiesCompleted += parallelResult.storiesCompleted;
34907
- totalCost += parallelResult.totalCost;
34908
- conflictedStories = parallelResult.mergeConflicts ?? [];
34909
- const newlyPassedStories = prd.userStories.filter((s) => s.status === "passed" && !initialPassedIds.has(s.id));
34910
- const costPerStory = newlyPassedStories.length > 0 ? parallelResult.totalCost / newlyPassedStories.length : 0;
34911
- for (const story of newlyPassedStories) {
34912
- batchStoryMetrics.push({
34913
- storyId: story.id,
34914
- complexity: "unknown",
34915
- modelTier: "parallel",
34916
- modelUsed: "parallel",
34917
- attempts: 1,
34918
- finalTier: "parallel",
34919
- success: true,
34920
- cost: costPerStory,
34921
- durationMs: batchDurationMs,
34922
- firstPassSuccess: true,
34923
- startedAt: batchStartedAt,
34924
- completedAt: batchCompletedAt,
34925
- source: "parallel"
34926
- });
34927
- }
34928
- allStoryMetrics.push(...batchStoryMetrics);
34929
- for (const conflict of conflictedStories) {
34930
- logger?.info("parallel", "Merge conflict detected - scheduling for rectification", {
34931
- storyId: conflict.storyId,
34932
- conflictFiles: conflict.conflictFiles
34933
- });
34934
- }
34935
- statusWriter.setPrd(prd);
34936
- await statusWriter.update(totalCost, iterations, {
34937
- parallel: {
34938
- enabled: true,
34939
- maxConcurrency,
34940
- activeStories: []
34941
- }
34942
- });
34943
- } catch (error48) {
34944
- logger?.error("parallel", "Parallel execution failed", {
34945
- error: errorMessage(error48)
34946
- });
34947
- await statusWriter.update(totalCost, iterations, {
34948
- parallel: undefined
34949
- });
34950
- throw error48;
34951
- }
34952
- let rectificationStats = { rectified: 0, stillConflicting: 0 };
34953
- if (conflictedStories.length > 0) {
34954
- const rectResult = await runRectificationPass(conflictedStories, options, prd, _parallelExecutorDeps.rectifyConflictedStory);
34955
- prd = rectResult.updatedPrd;
34956
- storiesCompleted += rectResult.rectifiedCount;
34957
- totalCost += rectResult.additionalCost;
34958
- rectificationStats = {
34959
- rectified: rectResult.rectifiedCount,
34960
- stillConflicting: rectResult.stillConflictingCount
34961
- };
34962
- batchStoryMetrics.push(...rectResult.rectificationMetrics);
34963
- allStoryMetrics.push(...rectResult.rectificationMetrics);
34964
- }
34965
- if (isComplete(prd)) {
34966
- logger?.info("execution", "All stories complete!", {
34967
- feature,
34968
- totalCost
34969
- });
34970
- await _parallelExecutorDeps.fireHook(hooks, "on-all-stories-complete", hookCtx(feature, { status: "passed", cost: totalCost }), workdir);
34971
- await _parallelExecutorDeps.fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: totalCost }), workdir);
34972
- const durationMs = Date.now() - startTime;
34973
- const runCompletedAt = new Date().toISOString();
34974
- const { handleParallelCompletion: handleParallelCompletion2 } = await Promise.resolve().then(() => (init_parallel_lifecycle(), exports_parallel_lifecycle));
34975
- await handleParallelCompletion2({
34976
- runId,
34977
- feature,
34978
- startedAt,
34979
- completedAt: runCompletedAt,
34884
+ const { story, storiesToExecute, routing, isBatchExecution } = selection;
34885
+ if (ctx.dryRun) {
34886
+ const dryRunResult = await handleDryRun({
34980
34887
  prd,
34981
- allStoryMetrics,
34888
+ prdPath: ctx.prdPath,
34889
+ storiesToExecute,
34890
+ routing,
34891
+ statusWriter: ctx.statusWriter,
34892
+ pluginRegistry: ctx.pluginRegistry,
34893
+ runId: ctx.runId,
34982
34894
  totalCost,
34983
- storiesCompleted,
34984
- durationMs,
34985
- workdir,
34986
- pluginRegistry
34895
+ iterations
34987
34896
  });
34988
- const finalCounts = countStories(prd);
34989
- statusWriter.setPrd(prd);
34990
- statusWriter.setCurrentStory(null);
34991
- statusWriter.setRunStatus("completed");
34992
- await statusWriter.update(totalCost, iterations);
34993
- if (headless && formatterMode !== "json") {
34994
- const { outputRunFooter: outputRunFooter2 } = await Promise.resolve().then(() => (init_headless_formatter(), exports_headless_formatter));
34995
- outputRunFooter2({
34996
- finalCounts: {
34997
- total: finalCounts.total,
34998
- passed: finalCounts.passed,
34999
- failed: finalCounts.failed,
35000
- skipped: finalCounts.skipped
35001
- },
35002
- durationMs,
35003
- totalCost,
35004
- startedAt,
35005
- completedAt: runCompletedAt,
35006
- formatterMode
35007
- });
35008
- }
35009
34897
  return {
35010
34898
  prd,
35011
- totalCost,
35012
- storiesCompleted,
35013
- completed: true,
35014
- durationMs,
35015
- storyMetrics: batchStoryMetrics,
35016
- rectificationStats
34899
+ storiesCompletedDelta: dryRunResult.storiesCompletedDelta,
34900
+ costDelta: 0,
34901
+ prdDirty: dryRunResult.prdDirty
35017
34902
  };
35018
34903
  }
35019
- return { prd, totalCost, storiesCompleted, completed: false, storyMetrics: batchStoryMetrics, rectificationStats };
35020
- }
35021
- var _parallelExecutorDeps;
35022
- var init_parallel_executor = __esm(() => {
35023
- init_hooks();
35024
- init_logger2();
35025
- init_prd();
35026
- init_helpers();
35027
- init_parallel();
35028
- init_parallel_executor_rectification_pass();
35029
- init_parallel_executor_rectify();
35030
- _parallelExecutorDeps = {
35031
- fireHook,
35032
- executeParallel,
35033
- rectifyConflictedStory
35034
- };
35035
- });
35036
-
35037
- // src/pipeline/subscribers/events-writer.ts
35038
- import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
35039
- import { homedir as homedir5 } from "os";
35040
- import { basename as basename5, join as join49 } from "path";
35041
- function wireEventsWriter(bus, feature, runId, workdir) {
35042
- const logger = getSafeLogger();
35043
- const project = basename5(workdir);
35044
- const eventsDir = join49(homedir5(), ".nax", "events", project);
35045
- const eventsFile = join49(eventsDir, "events.jsonl");
35046
- let dirReady = false;
35047
- const write = (line) => {
35048
- return (async () => {
35049
- try {
35050
- if (!dirReady) {
35051
- await mkdir2(eventsDir, { recursive: true });
35052
- dirReady = true;
35053
- }
35054
- await appendFile2(eventsFile, `${JSON.stringify(line)}
35055
- `);
35056
- } catch (err) {
35057
- logger?.warn("events-writer", "Failed to write event line (non-fatal)", {
35058
- event: line.event,
35059
- error: String(err)
35060
- });
35061
- }
35062
- })();
34904
+ const storyStartTime = Date.now();
34905
+ let storyGitRef;
34906
+ if (story.storyGitRef && await isGitRefValid(ctx.workdir, story.storyGitRef)) {
34907
+ storyGitRef = story.storyGitRef;
34908
+ } else {
34909
+ storyGitRef = await captureGitRef(ctx.workdir);
34910
+ if (storyGitRef) {
34911
+ story.storyGitRef = storyGitRef;
34912
+ await savePRD(prd, ctx.prdPath);
34913
+ }
34914
+ }
34915
+ const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
34916
+ const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join49(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
34917
+ const pipelineContext = {
34918
+ config: ctx.config,
34919
+ effectiveConfig,
34920
+ prd,
34921
+ story,
34922
+ stories: storiesToExecute,
34923
+ routing,
34924
+ workdir: ctx.workdir,
34925
+ prdPath: ctx.prdPath,
34926
+ featureDir: ctx.featureDir,
34927
+ hooks: ctx.hooks,
34928
+ plugins: ctx.pluginRegistry,
34929
+ storyStartTime: new Date().toISOString(),
34930
+ storyGitRef: storyGitRef ?? undefined,
34931
+ interaction: ctx.interactionChain ?? undefined,
34932
+ agentGetFn: ctx.agentGetFn,
34933
+ pidRegistry: ctx.pidRegistry,
34934
+ accumulatedAttemptCost: accumulatedAttemptCost > 0 ? accumulatedAttemptCost : undefined
35063
34935
  };
35064
- const unsubs = [];
35065
- unsubs.push(bus.on("run:started", (_ev) => {
35066
- return write({ ts: new Date().toISOString(), event: "run:started", runId, feature, project });
35067
- }));
35068
- unsubs.push(bus.on("story:started", (ev) => {
35069
- return write({
35070
- ts: new Date().toISOString(),
35071
- event: "story:started",
35072
- runId,
35073
- feature,
35074
- project,
35075
- storyId: ev.storyId
35076
- });
35077
- }));
35078
- unsubs.push(bus.on("story:completed", (ev) => {
35079
- return write({
35080
- ts: new Date().toISOString(),
35081
- event: "story:completed",
35082
- runId,
35083
- feature,
35084
- project,
35085
- storyId: ev.storyId
35086
- });
35087
- }));
35088
- unsubs.push(bus.on("story:decomposed", (ev) => {
35089
- return write({
35090
- ts: new Date().toISOString(),
35091
- event: "story:decomposed",
35092
- runId,
35093
- feature,
35094
- project,
35095
- storyId: ev.storyId,
35096
- data: { subStoryCount: ev.subStoryCount }
35097
- });
35098
- }));
35099
- unsubs.push(bus.on("story:failed", (ev) => {
35100
- return write({
35101
- ts: new Date().toISOString(),
35102
- event: "story:failed",
35103
- runId,
35104
- feature,
35105
- project,
35106
- storyId: ev.storyId
35107
- });
35108
- }));
35109
- unsubs.push(bus.on("run:completed", (_ev) => {
35110
- return write({ ts: new Date().toISOString(), event: "on-complete", runId, feature, project });
35111
- }));
35112
- unsubs.push(bus.on("run:paused", (ev) => {
35113
- return write({
35114
- ts: new Date().toISOString(),
35115
- event: "run:paused",
35116
- runId,
35117
- feature,
35118
- project,
35119
- ...ev.storyId !== undefined && { storyId: ev.storyId }
35120
- });
35121
- }));
35122
- return () => {
35123
- for (const u of unsubs)
35124
- u();
35125
- };
35126
- }
35127
- var init_events_writer = __esm(() => {
35128
- init_logger2();
35129
- });
35130
-
35131
- // src/pipeline/subscribers/hooks.ts
35132
- function wireHooks(bus, hooks, workdir, feature) {
35133
- const logger = getSafeLogger();
35134
- const safe = (name, fn) => {
35135
- return fn().catch((err) => logger?.warn("hooks-subscriber", `Hook "${name}" failed`, { error: String(err) })).catch(() => {});
35136
- };
35137
- const unsubs = [];
35138
- unsubs.push(bus.on("run:started", (ev) => {
35139
- return safe("on-start", () => fireHook(hooks, "on-start", hookCtx(feature, { status: "running" }), workdir));
35140
- }));
35141
- unsubs.push(bus.on("story:started", (ev) => {
35142
- return safe("on-story-start", () => fireHook(hooks, "on-story-start", hookCtx(feature, { storyId: ev.storyId, model: ev.modelTier, agent: ev.agent }), workdir));
35143
- }));
35144
- unsubs.push(bus.on("story:completed", (ev) => {
35145
- return safe("on-story-complete", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "passed", cost: ev.cost }), workdir));
35146
- }));
35147
- unsubs.push(bus.on("story:decomposed", (ev) => {
35148
- return safe("on-story-complete (decomposed)", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "decomposed", subStoryCount: ev.subStoryCount }), workdir));
35149
- }));
35150
- unsubs.push(bus.on("story:failed", (ev) => {
35151
- return safe("on-story-fail", () => fireHook(hooks, "on-story-fail", hookCtx(feature, { storyId: ev.storyId, status: "failed", reason: ev.reason }), workdir));
35152
- }));
35153
- unsubs.push(bus.on("story:paused", (ev) => {
35154
- return safe("on-pause (story)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
35155
- }));
35156
- unsubs.push(bus.on("run:paused", (ev) => {
35157
- return safe("on-pause (run)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
35158
- }));
35159
- unsubs.push(bus.on("run:completed", (ev) => {
35160
- return safe("on-complete", () => fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: ev.totalCost ?? 0 }), workdir));
35161
- }));
35162
- unsubs.push(bus.on("run:resumed", (ev) => {
35163
- return safe("on-resume", () => fireHook(hooks, "on-resume", hookCtx(feature, { status: "running" }), workdir));
35164
- }));
35165
- unsubs.push(bus.on("story:completed", (ev) => {
35166
- return safe("on-session-end (completed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "passed" }), workdir));
35167
- }));
35168
- unsubs.push(bus.on("story:failed", (ev) => {
35169
- return safe("on-session-end (failed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "failed" }), workdir));
35170
- }));
35171
- unsubs.push(bus.on("run:errored", (ev) => {
35172
- return safe("on-error", () => fireHook(hooks, "on-error", hookCtx(feature, { reason: ev.reason }), workdir));
35173
- }));
35174
- return () => {
35175
- for (const u of unsubs)
35176
- u();
35177
- };
35178
- }
35179
- var init_hooks2 = __esm(() => {
35180
- init_story_context();
35181
- init_hooks();
35182
- init_logger2();
35183
- });
35184
-
35185
- // src/pipeline/subscribers/interaction.ts
35186
- function wireInteraction(bus, interactionChain, config2) {
35187
- const logger = getSafeLogger();
35188
- const unsubs = [];
35189
- if (interactionChain && isTriggerEnabled("human-review", config2)) {
35190
- unsubs.push(bus.on("human-review:requested", (ev) => {
35191
- executeTrigger("human-review", {
35192
- featureName: ev.feature ?? "",
35193
- storyId: ev.storyId,
35194
- iteration: ev.attempts ?? 0,
35195
- reason: ev.reason
35196
- }, config2, interactionChain).catch((err) => {
35197
- logger?.warn("interaction-subscriber", "human-review trigger failed", {
35198
- storyId: ev.storyId,
35199
- error: String(err)
35200
- });
35201
- });
35202
- }));
35203
- }
35204
- if (interactionChain && isTriggerEnabled("max-retries", config2)) {
35205
- unsubs.push(bus.on("story:failed", (ev) => {
35206
- if (!ev.countsTowardEscalation) {
35207
- return;
35208
- }
35209
- executeTrigger("max-retries", {
35210
- featureName: ev.feature ?? "",
35211
- storyId: ev.storyId,
35212
- iteration: ev.attempts ?? 0
35213
- }, config2, interactionChain).then((response) => {
35214
- if (response.action === "abort") {
35215
- logger?.warn("interaction-subscriber", "max-retries abort requested", {
35216
- storyId: ev.storyId
35217
- });
35218
- }
35219
- }).catch((err) => {
35220
- logger?.warn("interaction-subscriber", "max-retries trigger failed", {
35221
- storyId: ev.storyId,
35222
- error: String(err)
35223
- });
35224
- });
35225
- }));
35226
- }
35227
- return () => {
35228
- for (const u of unsubs)
35229
- u();
35230
- };
35231
- }
35232
- var init_interaction2 = __esm(() => {
35233
- init_triggers();
35234
- init_logger2();
35235
- });
35236
-
35237
- // src/pipeline/subscribers/registry.ts
35238
- import { mkdir as mkdir3, writeFile } from "fs/promises";
35239
- import { homedir as homedir6 } from "os";
35240
- import { basename as basename6, join as join50 } from "path";
35241
- function wireRegistry(bus, feature, runId, workdir) {
35242
- const logger = getSafeLogger();
35243
- const project = basename6(workdir);
35244
- const runDir = join50(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
35245
- const metaFile = join50(runDir, "meta.json");
35246
- const unsub = bus.on("run:started", (_ev) => {
35247
- return (async () => {
35248
- try {
35249
- await mkdir3(runDir, { recursive: true });
35250
- const meta3 = {
35251
- runId,
35252
- project,
35253
- feature,
35254
- workdir,
35255
- statusPath: join50(workdir, ".nax", "features", feature, "status.json"),
35256
- eventsDir: join50(workdir, ".nax", "features", feature, "runs"),
35257
- registeredAt: new Date().toISOString()
35258
- };
35259
- await writeFile(metaFile, JSON.stringify(meta3, null, 2));
35260
- } catch (err) {
35261
- logger?.warn("registry-writer", "Failed to write meta.json (non-fatal)", {
35262
- path: metaFile,
35263
- error: String(err)
35264
- });
35265
- }
35266
- })();
35267
- });
35268
- return unsub;
35269
- }
35270
- var init_registry3 = __esm(() => {
35271
- init_logger2();
35272
- });
35273
-
35274
- // src/pipeline/subscribers/reporters.ts
35275
- function wireReporters(bus, pluginRegistry, runId, startTime) {
35276
- const logger = getSafeLogger();
35277
- const safe = (name, fn) => {
35278
- return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
35279
- };
35280
- const unsubs = [];
35281
- unsubs.push(bus.on("run:started", (ev) => {
35282
- return safe("onRunStart", async () => {
35283
- const reporters = pluginRegistry.getReporters();
35284
- for (const r of reporters) {
35285
- if (r.onRunStart) {
35286
- try {
35287
- await r.onRunStart({
35288
- runId,
35289
- feature: ev.feature,
35290
- totalStories: ev.totalStories,
35291
- startTime: new Date(startTime).toISOString()
35292
- });
35293
- } catch (err) {
35294
- logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
35295
- }
35296
- }
35297
- }
35298
- });
35299
- }));
35300
- unsubs.push(bus.on("story:completed", (ev) => {
35301
- return safe("onStoryComplete(completed)", async () => {
35302
- const reporters = pluginRegistry.getReporters();
35303
- for (const r of reporters) {
35304
- if (r.onStoryComplete) {
35305
- try {
35306
- await r.onStoryComplete({
35307
- runId,
35308
- storyId: ev.storyId,
35309
- status: "completed",
35310
- runElapsedMs: ev.runElapsedMs,
35311
- cost: ev.cost ?? 0,
35312
- tier: ev.modelTier ?? "balanced",
35313
- testStrategy: ev.testStrategy ?? "test-after"
35314
- });
35315
- } catch (err) {
35316
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
35317
- }
35318
- }
35319
- }
35320
- });
35321
- }));
35322
- unsubs.push(bus.on("story:failed", (ev) => {
35323
- return safe("onStoryComplete(failed)", async () => {
35324
- const reporters = pluginRegistry.getReporters();
35325
- for (const r of reporters) {
35326
- if (r.onStoryComplete) {
35327
- try {
35328
- await r.onStoryComplete({
35329
- runId,
35330
- storyId: ev.storyId,
35331
- status: "failed",
35332
- runElapsedMs: Date.now() - startTime,
35333
- cost: 0,
35334
- tier: "balanced",
35335
- testStrategy: "test-after"
35336
- });
35337
- } catch (err) {
35338
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
35339
- }
35340
- }
35341
- }
35342
- });
35343
- }));
35344
- unsubs.push(bus.on("story:paused", (ev) => {
35345
- return safe("onStoryComplete(paused)", async () => {
35346
- const reporters = pluginRegistry.getReporters();
35347
- for (const r of reporters) {
35348
- if (r.onStoryComplete) {
35349
- try {
35350
- await r.onStoryComplete({
35351
- runId,
35352
- storyId: ev.storyId,
35353
- status: "paused",
35354
- runElapsedMs: Date.now() - startTime,
35355
- cost: 0,
35356
- tier: "balanced",
35357
- testStrategy: "test-after"
35358
- });
35359
- } catch (err) {
35360
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
35361
- }
35362
- }
35363
- }
35364
- });
35365
- }));
35366
- unsubs.push(bus.on("run:completed", (ev) => {
35367
- return safe("onRunEnd", async () => {
35368
- const reporters = pluginRegistry.getReporters();
35369
- for (const r of reporters) {
35370
- if (r.onRunEnd) {
35371
- try {
35372
- await r.onRunEnd({
35373
- runId,
35374
- totalDurationMs: Date.now() - startTime,
35375
- totalCost: ev.totalCost ?? 0,
35376
- storySummary: {
35377
- completed: ev.passedStories,
35378
- failed: ev.failedStories,
35379
- skipped: 0,
35380
- paused: 0
35381
- }
35382
- });
35383
- } catch (err) {
35384
- logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
35385
- }
35386
- }
35387
- }
35388
- });
35389
- }));
35390
- return () => {
35391
- for (const u of unsubs)
35392
- u();
35393
- };
35394
- }
35395
- var init_reporters = __esm(() => {
35396
- init_logger2();
35397
- });
35398
-
35399
- // src/execution/deferred-review.ts
35400
- var {spawn: spawn5 } = globalThis.Bun;
35401
- async function captureRunStartRef(workdir) {
35402
- try {
35403
- const proc = _deferredReviewDeps.spawn({
35404
- cmd: ["git", "rev-parse", "HEAD"],
35405
- cwd: workdir,
35406
- stdout: "pipe",
35407
- stderr: "pipe"
35408
- });
35409
- const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
35410
- return stdout.trim();
35411
- } catch {
35412
- return "";
35413
- }
35414
- }
35415
- async function getChangedFilesForDeferred(workdir, baseRef) {
35416
- try {
35417
- const proc = _deferredReviewDeps.spawn({
35418
- cmd: ["git", "diff", "--name-only", `${baseRef}...HEAD`],
35419
- cwd: workdir,
35420
- stdout: "pipe",
35421
- stderr: "pipe"
35422
- });
35423
- const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
35424
- return stdout.trim().split(`
35425
- `).filter(Boolean);
35426
- } catch {
35427
- return [];
35428
- }
35429
- }
35430
- async function runDeferredReview(workdir, reviewConfig, plugins, runStartRef) {
35431
- if (!reviewConfig || reviewConfig.pluginMode !== "deferred") {
35432
- return;
35433
- }
35434
- const reviewers = plugins.getReviewers();
35435
- if (reviewers.length === 0) {
35436
- return;
35437
- }
35438
- const changedFiles = await getChangedFilesForDeferred(workdir, runStartRef);
35439
- const reviewerResults = [];
35440
- let anyFailed = false;
35441
- for (const reviewer of reviewers) {
35442
- try {
35443
- const result = await reviewer.check(workdir, changedFiles);
35444
- reviewerResults.push({
35445
- name: reviewer.name,
35446
- passed: result.passed,
35447
- output: result.output,
35448
- exitCode: result.exitCode
35449
- });
35450
- if (!result.passed) {
35451
- anyFailed = true;
35452
- }
35453
- } catch (error48) {
35454
- const errorMsg = error48 instanceof Error ? error48.message : String(error48);
35455
- reviewerResults.push({
35456
- name: reviewer.name,
35457
- passed: false,
35458
- output: "",
35459
- error: errorMsg
35460
- });
35461
- anyFailed = true;
35462
- }
35463
- }
35464
- return { runStartRef, changedFiles, reviewerResults, anyFailed };
35465
- }
35466
- var _deferredReviewDeps;
35467
- var init_deferred_review = __esm(() => {
35468
- _deferredReviewDeps = { spawn: spawn5 };
35469
- });
35470
-
35471
- // src/execution/dry-run.ts
35472
- async function handleDryRun(ctx) {
35473
- const logger = getSafeLogger();
35474
- ctx.statusWriter.setPrd(ctx.prd);
34936
+ ctx.statusWriter.setPrd(prd);
35475
34937
  ctx.statusWriter.setCurrentStory({
35476
- storyId: ctx.storiesToExecute[0].id,
35477
- title: ctx.storiesToExecute[0].title,
35478
- complexity: ctx.routing.complexity,
35479
- tddStrategy: ctx.routing.testStrategy,
35480
- model: ctx.routing.modelTier,
35481
- attempt: (ctx.storiesToExecute[0].attempts ?? 0) + 1,
34938
+ storyId: story.id,
34939
+ title: story.title,
34940
+ complexity: routing.complexity,
34941
+ tddStrategy: routing.testStrategy,
34942
+ model: routing.modelTier,
34943
+ attempt: (story.attempts ?? 0) + 1,
35482
34944
  phase: "routing"
35483
34945
  });
35484
- await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
35485
- for (const s of ctx.storiesToExecute) {
35486
- logger?.info("execution", "[DRY RUN] Would execute agent here", {
35487
- storyId: s.id,
35488
- storyTitle: s.title,
35489
- modelTier: ctx.routing.modelTier,
35490
- complexity: ctx.routing.complexity,
35491
- testStrategy: ctx.routing.testStrategy
35492
- });
35493
- }
35494
- for (const s of ctx.storiesToExecute) {
35495
- markStoryPassed(ctx.prd, s.id);
35496
- }
35497
- await savePRD(ctx.prd, ctx.prdPath);
35498
- for (const s of ctx.storiesToExecute) {
35499
- pipelineEventBus.emit({
35500
- type: "story:completed",
35501
- storyId: s.id,
35502
- story: s,
35503
- passed: true,
35504
- runElapsedMs: 0,
35505
- cost: 0,
35506
- modelTier: ctx.routing.modelTier,
35507
- testStrategy: ctx.routing.testStrategy
35508
- });
34946
+ await ctx.statusWriter.update(totalCost, iterations);
34947
+ const pipelineResult = await runPipeline(defaultPipeline, pipelineContext, ctx.eventEmitter);
34948
+ const currentPrd = pipelineResult.context.prd;
34949
+ const handlerCtx = {
34950
+ config: ctx.config,
34951
+ prd: currentPrd,
34952
+ prdPath: ctx.prdPath,
34953
+ workdir: ctx.workdir,
34954
+ featureDir: ctx.featureDir,
34955
+ hooks: ctx.hooks,
34956
+ feature: ctx.feature,
34957
+ totalCost,
34958
+ startTime: ctx.startTime,
34959
+ runId: ctx.runId,
34960
+ pluginRegistry: ctx.pluginRegistry,
34961
+ story,
34962
+ storiesToExecute,
34963
+ routing: pipelineResult.context.routing ?? routing,
34964
+ isBatchExecution,
34965
+ allStoryMetrics,
34966
+ storyGitRef,
34967
+ interactionChain: ctx.interactionChain,
34968
+ storyStartTime
34969
+ };
34970
+ if (pipelineResult.success) {
34971
+ const r2 = await handlePipelineSuccess(handlerCtx, pipelineResult);
34972
+ return {
34973
+ prd: r2.prd,
34974
+ storiesCompletedDelta: r2.storiesCompletedDelta,
34975
+ costDelta: r2.costDelta,
34976
+ prdDirty: r2.prdDirty,
34977
+ finalAction: pipelineResult.finalAction
34978
+ };
35509
34979
  }
35510
- ctx.statusWriter.setPrd(ctx.prd);
35511
- ctx.statusWriter.setCurrentStory(null);
35512
- await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
35513
- return { storiesCompletedDelta: ctx.storiesToExecute.length, prdDirty: true };
34980
+ const r = await handlePipelineFailure(handlerCtx, pipelineResult);
34981
+ return {
34982
+ prd: r.prd,
34983
+ storiesCompletedDelta: 0,
34984
+ costDelta: r.costDelta,
34985
+ prdDirty: r.prdDirty,
34986
+ finalAction: pipelineResult.finalAction,
34987
+ reason: pipelineResult.reason,
34988
+ subStoryCount: pipelineResult.subStoryCount
34989
+ };
35514
34990
  }
35515
- var init_dry_run = __esm(() => {
34991
+ var _iterationRunnerDeps;
34992
+ var init_iteration_runner = __esm(() => {
34993
+ init_loader();
35516
34994
  init_logger2();
35517
- init_event_bus();
34995
+ init_runner();
34996
+ init_stages();
35518
34997
  init_prd();
34998
+ init_git();
34999
+ init_dry_run();
35000
+ init_pipeline_result_handler();
35001
+ _iterationRunnerDeps = {
35002
+ loadConfigForWorkdir
35003
+ };
35519
35004
  });
35520
35005
 
35521
- // src/execution/escalation/tier-outcome.ts
35522
- async function handleNoTierAvailable(ctx, failureCategory) {
35523
- const logger = getSafeLogger();
35524
- const outcome = resolveMaxAttemptsOutcome(failureCategory);
35525
- if (outcome === "pause") {
35526
- const pausedPrd = { ...ctx.prd };
35527
- markStoryPaused(pausedPrd, ctx.story.id);
35528
- await savePRD(pausedPrd, ctx.prdPath);
35529
- logger?.warn("execution", "Story paused - no tier available (needs human review)", {
35530
- storyId: ctx.story.id,
35531
- failureCategory
35532
- });
35533
- if (ctx.featureDir) {
35534
- await appendProgress(ctx.featureDir, ctx.story.id, "paused", `${ctx.story.title} \u2014 Execution stopped (needs human review)`);
35006
+ // src/execution/story-selector.ts
35007
+ function selectNextStories(prd, config2, batchPlan, currentBatchIndex, lastStoryId, useBatch) {
35008
+ if (useBatch && currentBatchIndex < batchPlan.length) {
35009
+ const batch = batchPlan[currentBatchIndex];
35010
+ const storiesToExecute = batch.stories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed");
35011
+ if (storiesToExecute.length === 0) {
35012
+ return { selection: null, nextBatchIndex: currentBatchIndex + 1 };
35535
35013
  }
35536
- pipelineEventBus.emit({
35537
- type: "story:paused",
35538
- storyId: ctx.story.id,
35539
- reason: `Execution stopped (${failureCategory ?? "unknown"} requires human review)`,
35540
- cost: ctx.totalCost
35541
- });
35542
- return { outcome: "paused", prdDirty: true, prd: pausedPrd };
35543
- }
35544
- const failedPrd = { ...ctx.prd };
35545
- markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
35546
- await savePRD(failedPrd, ctx.prdPath);
35547
- logger?.error("execution", "Story failed - execution failed", {
35548
- storyId: ctx.story.id
35549
- });
35550
- if (ctx.featureDir) {
35551
- await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} \u2014 Execution failed`);
35014
+ const story2 = storiesToExecute[0];
35015
+ return {
35016
+ selection: {
35017
+ story: story2,
35018
+ storiesToExecute,
35019
+ routing: buildPreviewRouting(story2, config2),
35020
+ isBatchExecution: batch.isBatch && storiesToExecute.length > 1
35021
+ },
35022
+ nextBatchIndex: currentBatchIndex + 1
35023
+ };
35552
35024
  }
35553
- pipelineEventBus.emit({
35554
- type: "story:failed",
35555
- storyId: ctx.story.id,
35556
- story: ctx.story,
35557
- reason: "Execution failed",
35558
- countsTowardEscalation: true
35559
- });
35560
- return { outcome: "failed", prdDirty: true, prd: failedPrd };
35025
+ const story = getNextStory(prd, lastStoryId, config2.execution.rectification?.maxRetries ?? 2);
35026
+ if (!story)
35027
+ return null;
35028
+ return {
35029
+ selection: {
35030
+ story,
35031
+ storiesToExecute: [story],
35032
+ routing: buildPreviewRouting(story, config2),
35033
+ isBatchExecution: false
35034
+ },
35035
+ nextBatchIndex: currentBatchIndex
35036
+ };
35561
35037
  }
35562
- async function handleMaxAttemptsReached(ctx, failureCategory) {
35563
- const logger = getSafeLogger();
35564
- const outcome = resolveMaxAttemptsOutcome(failureCategory);
35565
- if (outcome === "pause") {
35566
- const pausedPrd = { ...ctx.prd };
35567
- markStoryPaused(pausedPrd, ctx.story.id);
35568
- await savePRD(pausedPrd, ctx.prdPath);
35569
- logger?.warn("execution", "Story paused - max attempts reached (needs human review)", {
35570
- storyId: ctx.story.id,
35571
- failureCategory
35038
+ function selectIndependentBatch(stories, maxCount) {
35039
+ const storyMap = new Map(stories.map((s) => [s.id, s]));
35040
+ const result = [];
35041
+ for (const story of stories) {
35042
+ if (result.length >= maxCount)
35043
+ break;
35044
+ if (story.passes || story.status === "passed" || story.status === "skipped" || story.status === "failed" || story.status === "paused" || story.status === "decomposed")
35045
+ continue;
35046
+ const allDepsFulfilled = story.dependencies.every((depId) => {
35047
+ const dep = storyMap.get(depId);
35048
+ if (!dep)
35049
+ return true;
35050
+ return dep.passes || dep.status === "passed";
35572
35051
  });
35573
- if (ctx.featureDir) {
35574
- await appendProgress(ctx.featureDir, ctx.story.id, "paused", `${ctx.story.title} \u2014 Max attempts reached (needs human review)`);
35052
+ if (allDepsFulfilled) {
35053
+ result.push(story);
35575
35054
  }
35576
- pipelineEventBus.emit({
35577
- type: "story:paused",
35578
- storyId: ctx.story.id,
35579
- reason: `Max attempts reached (${failureCategory ?? "unknown"} requires human review)`,
35580
- cost: ctx.totalCost
35581
- });
35582
- return { outcome: "paused", prdDirty: true, prd: pausedPrd };
35583
35055
  }
35584
- const failedPrd = { ...ctx.prd };
35585
- markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
35586
- await savePRD(failedPrd, ctx.prdPath);
35587
- logger?.error("execution", "Story failed - max attempts reached", {
35588
- storyId: ctx.story.id,
35589
- failureCategory
35590
- });
35591
- if (ctx.featureDir) {
35592
- await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} \u2014 Max attempts reached`);
35593
- }
35594
- pipelineEventBus.emit({
35595
- type: "story:failed",
35596
- storyId: ctx.story.id,
35597
- story: ctx.story,
35598
- reason: "Max attempts reached",
35599
- countsTowardEscalation: true
35600
- });
35601
- return { outcome: "failed", prdDirty: true, prd: failedPrd };
35056
+ return result;
35602
35057
  }
35603
- var init_tier_outcome = __esm(() => {
35058
+ var init_story_selector = __esm(() => {
35604
35059
  init_logger2();
35605
- init_event_bus();
35606
35060
  init_prd();
35607
- init_progress();
35608
- init_tier_escalation();
35609
35061
  });
35610
35062
 
35611
- // src/execution/escalation/tier-escalation.ts
35612
- function buildEscalationFailure(story, currentTier, reviewFindings, cost) {
35613
- const stage = reviewFindings && reviewFindings.length > 0 ? "review" : "escalation";
35614
- return {
35615
- attempt: (story.attempts ?? 0) + 1,
35616
- modelTier: currentTier,
35617
- stage,
35618
- summary: `Failed with tier ${currentTier}, escalating to next tier`,
35619
- reviewFindings: reviewFindings && reviewFindings.length > 0 ? reviewFindings : undefined,
35620
- cost: cost ?? 0,
35621
- timestamp: new Date().toISOString()
35622
- };
35623
- }
35624
- function resolveMaxAttemptsOutcome(failureCategory) {
35625
- if (!failureCategory) {
35626
- return "fail";
35627
- }
35628
- switch (failureCategory) {
35629
- case "isolation-violation":
35630
- case "verifier-rejected":
35631
- case "greenfield-no-tests":
35632
- return "pause";
35633
- case "runtime-crash":
35634
- return "pause";
35635
- case "session-failure":
35636
- case "tests-failing":
35637
- return "fail";
35638
- default:
35639
- return "fail";
35063
+ // src/execution/parallel-worker.ts
35064
+ var exports_parallel_worker = {};
35065
+ __export(exports_parallel_worker, {
35066
+ executeStoryInWorktree: () => executeStoryInWorktree,
35067
+ executeParallelBatch: () => executeParallelBatch
35068
+ });
35069
+ async function executeStoryInWorktree(story, worktreePath, context, routing, eventEmitter) {
35070
+ const logger = getSafeLogger();
35071
+ try {
35072
+ const pipelineContext = {
35073
+ ...context,
35074
+ effectiveConfig: context.effectiveConfig ?? context.config,
35075
+ story,
35076
+ stories: [story],
35077
+ workdir: worktreePath,
35078
+ routing
35079
+ };
35080
+ logger?.debug("parallel", "Executing story in worktree", {
35081
+ storyId: story.id,
35082
+ worktreePath
35083
+ });
35084
+ const result = await runPipeline(defaultPipeline, pipelineContext, eventEmitter);
35085
+ return {
35086
+ success: result.success,
35087
+ cost: result.context.agentResult?.estimatedCost || 0,
35088
+ error: result.success ? undefined : result.reason,
35089
+ pipelineResult: result
35090
+ };
35091
+ } catch (error48) {
35092
+ return {
35093
+ success: false,
35094
+ cost: 0,
35095
+ error: errorMessage(error48)
35096
+ };
35640
35097
  }
35641
35098
  }
35642
- function shouldRetrySameTier(verifyResult) {
35643
- return verifyResult?.status === "RUNTIME_CRASH";
35644
- }
35645
- async function handleTierEscalation(ctx) {
35099
+ async function executeParallelBatch(stories, projectRoot, config2, context, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs) {
35646
35100
  const logger = getSafeLogger();
35647
- if (shouldRetrySameTier(ctx.verifyResult)) {
35648
- logger?.warn("escalation", "Runtime crash detected \u2014 retrying same tier (transient, not a code issue)", {
35649
- storyId: ctx.story.id
35101
+ const results = {
35102
+ pipelinePassed: [],
35103
+ merged: [],
35104
+ failed: [],
35105
+ totalCost: 0,
35106
+ mergeConflicts: [],
35107
+ storyCosts: new Map
35108
+ };
35109
+ const executing = new Set;
35110
+ for (const story of stories) {
35111
+ const worktreePath = worktreePaths.get(story.id);
35112
+ if (!worktreePath) {
35113
+ results.failed.push({
35114
+ story,
35115
+ error: "Worktree not created"
35116
+ });
35117
+ continue;
35118
+ }
35119
+ const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
35120
+ const storyConfig = storyEffectiveConfigs?.get(story.id);
35121
+ const storyContext = storyConfig ? { ...context, effectiveConfig: storyConfig } : context;
35122
+ const executePromise = executeStoryInWorktree(story, worktreePath, storyContext, routing, eventEmitter).then((result) => {
35123
+ results.totalCost += result.cost;
35124
+ results.storyCosts.set(story.id, result.cost);
35125
+ if (result.success) {
35126
+ results.pipelinePassed.push(story);
35127
+ logger?.info("parallel", "Story execution succeeded", {
35128
+ storyId: story.id,
35129
+ cost: result.cost
35130
+ });
35131
+ } else {
35132
+ results.failed.push({ story, error: result.error || "Unknown error", pipelineResult: result.pipelineResult });
35133
+ logger?.error("parallel", "Story execution failed", {
35134
+ storyId: story.id,
35135
+ error: result.error
35136
+ });
35137
+ }
35138
+ }).finally(() => {
35139
+ executing.delete(executePromise);
35650
35140
  });
35651
- return { outcome: "retry-same", prdDirty: false, prd: ctx.prd };
35652
- }
35653
- const nextTier = escalateTier(ctx.routing.modelTier, ctx.config.autoMode.escalation.tierOrder);
35654
- const escalateWholeBatch = ctx.config.autoMode.escalation.escalateEntireBatch ?? true;
35655
- const storiesToEscalate = ctx.isBatchExecution && escalateWholeBatch ? ctx.storiesToExecute : [ctx.story];
35656
- const escalateRetryAsLite = ctx.pipelineResult.context.retryAsLite === true;
35657
- const escalateFailureCategory = ctx.pipelineResult.context.tddFailureCategory;
35658
- const escalateReviewFindings = ctx.pipelineResult.context.reviewFindings;
35659
- const escalateRetryAsTestAfter = escalateFailureCategory === "greenfield-no-tests";
35660
- const routingMode = ctx.config.routing.llm?.mode ?? "hybrid";
35661
- if (!nextTier || !ctx.config.autoMode.escalation.enabled) {
35662
- return await handleNoTierAvailable(ctx, escalateFailureCategory);
35141
+ executing.add(executePromise);
35142
+ if (executing.size >= maxConcurrency) {
35143
+ await Promise.race(executing);
35144
+ }
35663
35145
  }
35664
- const maxAttempts = calculateMaxIterations(ctx.config.autoMode.escalation.tierOrder);
35665
- const canEscalate = storiesToEscalate.every((s) => (s.attempts ?? 0) < maxAttempts);
35666
- if (!canEscalate) {
35667
- return await handleMaxAttemptsReached(ctx, escalateFailureCategory);
35146
+ await Promise.all(executing);
35147
+ return results;
35148
+ }
35149
+ var init_parallel_worker = __esm(() => {
35150
+ init_logger2();
35151
+ init_runner();
35152
+ init_stages();
35153
+ init_routing();
35154
+ });
35155
+
35156
+ // src/worktree/manager.ts
35157
+ var exports_manager = {};
35158
+ __export(exports_manager, {
35159
+ _managerDeps: () => _managerDeps,
35160
+ WorktreeManager: () => WorktreeManager
35161
+ });
35162
+ import { existsSync as existsSync32, symlinkSync } from "fs";
35163
+ import { join as join50 } from "path";
35164
+
35165
+ class WorktreeManager {
35166
+ async create(projectRoot, storyId) {
35167
+ validateStoryId(storyId);
35168
+ const worktreePath = join50(projectRoot, ".nax-wt", storyId);
35169
+ const branchName = `nax/${storyId}`;
35170
+ try {
35171
+ const proc = _managerDeps.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], {
35172
+ cwd: projectRoot,
35173
+ stdout: "pipe",
35174
+ stderr: "pipe"
35175
+ });
35176
+ const exitCode = await proc.exited;
35177
+ if (exitCode !== 0) {
35178
+ const stderr = await new Response(proc.stderr).text();
35179
+ throw new Error(`Failed to create worktree: ${stderr || "unknown error"}`);
35180
+ }
35181
+ } catch (error48) {
35182
+ if (error48 instanceof Error) {
35183
+ if (error48.message.includes("not a git repository")) {
35184
+ throw new Error(`Not a git repository: ${projectRoot}`);
35185
+ }
35186
+ if (error48.message.includes("already exists")) {
35187
+ throw new Error(`Worktree for story ${storyId} already exists at ${worktreePath}`);
35188
+ }
35189
+ throw error48;
35190
+ }
35191
+ throw new Error(`Failed to create worktree: ${String(error48)}`);
35192
+ }
35193
+ const nodeModulesSource = join50(projectRoot, "node_modules");
35194
+ if (existsSync32(nodeModulesSource)) {
35195
+ const nodeModulesTarget = join50(worktreePath, "node_modules");
35196
+ try {
35197
+ symlinkSync(nodeModulesSource, nodeModulesTarget, "dir");
35198
+ } catch (error48) {
35199
+ await this.remove(projectRoot, storyId);
35200
+ throw new Error(`Failed to symlink node_modules: ${errorMessage(error48)}`);
35201
+ }
35202
+ }
35203
+ const envSource = join50(projectRoot, ".env");
35204
+ if (existsSync32(envSource)) {
35205
+ const envTarget = join50(worktreePath, ".env");
35206
+ try {
35207
+ symlinkSync(envSource, envTarget, "file");
35208
+ } catch (error48) {
35209
+ await this.remove(projectRoot, storyId);
35210
+ throw new Error(`Failed to symlink .env: ${errorMessage(error48)}`);
35211
+ }
35212
+ }
35668
35213
  }
35669
- for (const s of storiesToEscalate) {
35670
- const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
35671
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
35672
- if (shouldSwitchToTestAfter) {
35673
- logger?.warn("escalation", "Switching strategy to test-after (greenfield-no-tests fallback)", {
35674
- storyId: s.id,
35675
- fromStrategy: currentTestStrategy,
35676
- toStrategy: "test-after"
35214
+ async remove(projectRoot, storyId) {
35215
+ validateStoryId(storyId);
35216
+ const worktreePath = join50(projectRoot, ".nax-wt", storyId);
35217
+ const branchName = `nax/${storyId}`;
35218
+ try {
35219
+ const proc = _managerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
35220
+ cwd: projectRoot,
35221
+ stdout: "pipe",
35222
+ stderr: "pipe"
35677
35223
  });
35678
- } else {
35679
- logger?.warn("escalation", "Escalating story to next tier", {
35680
- storyId: s.id,
35681
- nextTier,
35682
- retryAsLite: escalateRetryAsLite
35224
+ const exitCode = await proc.exited;
35225
+ if (exitCode !== 0) {
35226
+ const stderr = await new Response(proc.stderr).text();
35227
+ if (stderr.includes("not found") || stderr.includes("does not exist") || stderr.includes("no such worktree") || stderr.includes("is not a working tree")) {
35228
+ throw new Error(`Worktree not found: ${worktreePath}`);
35229
+ }
35230
+ throw new Error(`Failed to remove worktree: ${stderr || "unknown error"}`);
35231
+ }
35232
+ } catch (error48) {
35233
+ if (error48 instanceof Error) {
35234
+ throw error48;
35235
+ }
35236
+ throw new Error(`Failed to remove worktree: ${String(error48)}`);
35237
+ }
35238
+ try {
35239
+ const proc = _managerDeps.spawn(["git", "branch", "-D", branchName], {
35240
+ cwd: projectRoot,
35241
+ stdout: "pipe",
35242
+ stderr: "pipe"
35243
+ });
35244
+ const exitCode = await proc.exited;
35245
+ if (exitCode !== 0) {
35246
+ const stderr = await new Response(proc.stderr).text();
35247
+ if (!stderr.includes("not found")) {
35248
+ const logger = getSafeLogger();
35249
+ logger?.warn("worktree", `Failed to delete branch ${branchName}`, { stderr });
35250
+ }
35251
+ }
35252
+ } catch (error48) {
35253
+ const logger = getSafeLogger();
35254
+ logger?.warn("worktree", `Failed to delete branch ${branchName}`, {
35255
+ error: errorMessage(error48)
35683
35256
  });
35684
35257
  }
35685
35258
  }
35686
- const pipelineReason = ctx.pipelineResult.reason ? `: ${ctx.pipelineResult.reason}` : "";
35687
- const errorMessage2 = `Attempt ${ctx.story.attempts + 1} failed with model tier: ${ctx.routing.modelTier}${ctx.isBatchExecution ? " (in batch)" : ""}${pipelineReason}`;
35688
- const updatedPrd = {
35689
- ...ctx.prd,
35690
- userStories: ctx.prd.userStories.map((s) => {
35691
- const shouldEscalate = storiesToEscalate.some((story) => story.id === s.id);
35692
- if (!shouldEscalate)
35693
- return s;
35694
- const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
35695
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
35696
- const baseRouting = s.routing ?? { ...ctx.routing };
35697
- const updatedRouting = {
35698
- ...baseRouting,
35699
- modelTier: shouldSwitchToTestAfter ? baseRouting.modelTier : nextTier,
35700
- ...escalateRetryAsLite ? { testStrategy: "three-session-tdd-lite" } : {},
35701
- ...shouldSwitchToTestAfter ? { testStrategy: "test-after" } : {}
35702
- };
35703
- const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
35704
- const isChangingTier = currentStoryTier !== nextTier;
35705
- const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
35706
- const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings, ctx.attemptCost);
35707
- return {
35708
- ...s,
35709
- attempts: shouldResetAttempts ? 0 : (s.attempts ?? 0) + 1,
35710
- routing: updatedRouting,
35711
- priorErrors: [...s.priorErrors || [], errorMessage2],
35712
- priorFailures: [...s.priorFailures || [], escalationFailure]
35713
- };
35714
- })
35715
- };
35716
- await _tierEscalationDeps.savePRD(updatedPrd, ctx.prdPath);
35717
- for (const story of storiesToEscalate) {
35718
- clearCacheForStory(story.id);
35259
+ async list(projectRoot) {
35260
+ try {
35261
+ const proc = _managerDeps.spawn(["git", "worktree", "list", "--porcelain"], {
35262
+ cwd: projectRoot,
35263
+ stdout: "pipe",
35264
+ stderr: "pipe"
35265
+ });
35266
+ const exitCode = await proc.exited;
35267
+ if (exitCode !== 0) {
35268
+ const stderr = await new Response(proc.stderr).text();
35269
+ throw new Error(`Failed to list worktrees: ${stderr || "unknown error"}`);
35270
+ }
35271
+ const stdout = await new Response(proc.stdout).text();
35272
+ return this.parseWorktreeList(stdout);
35273
+ } catch (error48) {
35274
+ if (error48 instanceof Error) {
35275
+ throw error48;
35276
+ }
35277
+ throw new Error(`Failed to list worktrees: ${String(error48)}`);
35278
+ }
35719
35279
  }
35720
- if (routingMode === "hybrid") {
35721
- await tryLlmBatchRoute(ctx.config, storiesToEscalate, "hybrid-re-route-pipeline");
35280
+ parseWorktreeList(output) {
35281
+ const worktrees = [];
35282
+ const lines = output.trim().split(`
35283
+ `);
35284
+ let currentWorktree = {};
35285
+ for (const line of lines) {
35286
+ if (line.startsWith("worktree ")) {
35287
+ currentWorktree.path = line.substring("worktree ".length);
35288
+ } else if (line.startsWith("branch ")) {
35289
+ currentWorktree.branch = line.substring("branch ".length).replace("refs/heads/", "");
35290
+ } else if (line === "") {
35291
+ if (currentWorktree.path && currentWorktree.branch) {
35292
+ worktrees.push(currentWorktree);
35293
+ }
35294
+ currentWorktree = {};
35295
+ }
35296
+ }
35297
+ if (currentWorktree.path && currentWorktree.branch) {
35298
+ worktrees.push(currentWorktree);
35299
+ }
35300
+ return worktrees;
35722
35301
  }
35723
- return {
35724
- outcome: "escalated",
35725
- prdDirty: true,
35726
- prd: updatedPrd
35727
- };
35728
35302
  }
35729
- var _tierEscalationDeps;
35730
- var init_tier_escalation = __esm(() => {
35731
- init_hooks();
35303
+ var _managerDeps;
35304
+ var init_manager = __esm(() => {
35732
35305
  init_logger2();
35733
- init_prd();
35734
- init_routing();
35735
- init_llm();
35736
- init_escalation();
35737
- init_helpers();
35738
- init_progress();
35739
- init_tier_outcome();
35740
- _tierEscalationDeps = {
35741
- savePRD
35306
+ init_bun_deps();
35307
+ _managerDeps = {
35308
+ spawn
35742
35309
  };
35743
35310
  });
35744
35311
 
35745
- // src/execution/escalation/index.ts
35746
- var init_escalation = __esm(() => {
35747
- init_tier_escalation();
35312
+ // src/worktree/merge.ts
35313
+ var exports_merge = {};
35314
+ __export(exports_merge, {
35315
+ _mergeDeps: () => _mergeDeps,
35316
+ MergeEngine: () => MergeEngine
35748
35317
  });
35749
35318
 
35750
- // src/execution/pipeline-result-handler.ts
35751
- function filterOutputFiles(files) {
35752
- const NOISE = [
35753
- /\.test\.(ts|js|tsx|jsx)$/,
35754
- /\.spec\.(ts|js|tsx|jsx)$/,
35755
- /package-lock\.json$/,
35756
- /bun\.lock(b?)$/,
35757
- /\.gitignore$/,
35758
- /^nax\//
35759
- ];
35760
- return files.filter((f) => !NOISE.some((p) => p.test(f))).slice(0, 15);
35761
- }
35762
- async function handlePipelineSuccess(ctx, pipelineResult) {
35763
- const logger = getSafeLogger();
35764
- const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
35765
- const prd = ctx.prd;
35766
- if (pipelineResult.context.storyMetrics) {
35767
- ctx.allStoryMetrics.push(...pipelineResult.context.storyMetrics);
35319
+ class MergeEngine {
35320
+ worktreeManager;
35321
+ constructor(worktreeManager) {
35322
+ this.worktreeManager = worktreeManager;
35768
35323
  }
35769
- const storiesCompletedDelta = ctx.storiesToExecute.length;
35770
- for (const completedStory of ctx.storiesToExecute) {
35771
- const now = Date.now();
35772
- logger?.info("story.complete", "Story completed successfully", {
35773
- storyId: completedStory.id,
35774
- storyTitle: completedStory.title,
35775
- totalCost: ctx.totalCost + costDelta,
35776
- runElapsedMs: now - ctx.startTime,
35777
- storyDurationMs: ctx.storyStartTime ? now - ctx.storyStartTime : undefined
35778
- });
35324
+ async merge(projectRoot, storyId) {
35325
+ const branchName = `nax/${storyId}`;
35326
+ try {
35327
+ const mergeProc = _mergeDeps.spawn(["git", "merge", "--no-ff", branchName, "-m", `Merge branch '${branchName}'`], {
35328
+ cwd: projectRoot,
35329
+ stdout: "pipe",
35330
+ stderr: "pipe"
35331
+ });
35332
+ const exitCode = await mergeProc.exited;
35333
+ const stderr = await new Response(mergeProc.stderr).text();
35334
+ const stdout = await new Response(mergeProc.stdout).text();
35335
+ if (exitCode === 0) {
35336
+ try {
35337
+ await this.worktreeManager.remove(projectRoot, storyId);
35338
+ } catch (error48) {
35339
+ const logger = getSafeLogger();
35340
+ logger?.warn("worktree", `Failed to cleanup worktree for ${storyId}`, {
35341
+ error: errorMessage(error48)
35342
+ });
35343
+ }
35344
+ return { success: true };
35345
+ }
35346
+ const output = `${stdout}
35347
+ ${stderr}`;
35348
+ if (output.includes("CONFLICT") || output.includes("conflict") || output.includes("Automatic merge failed")) {
35349
+ const conflictFiles = await this.getConflictFiles(projectRoot);
35350
+ await this.abortMerge(projectRoot);
35351
+ return {
35352
+ success: false,
35353
+ conflictFiles
35354
+ };
35355
+ }
35356
+ throw new Error(`Merge failed: ${stderr || stdout || "unknown error"}`);
35357
+ } catch (error48) {
35358
+ if (error48 instanceof Error) {
35359
+ throw error48;
35360
+ }
35361
+ throw new Error(`Failed to merge branch ${branchName}: ${String(error48)}`);
35362
+ }
35779
35363
  }
35780
- if (ctx.storyGitRef) {
35781
- for (const completedStory of ctx.storiesToExecute) {
35782
- try {
35783
- const rawFiles = await captureOutputFiles(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
35784
- const filtered = filterOutputFiles(rawFiles);
35785
- if (filtered.length > 0) {
35786
- completedStory.outputFiles = filtered;
35364
+ async mergeAll(projectRoot, storyIds, dependencies) {
35365
+ const orderedStories = this.topologicalSort(storyIds, dependencies);
35366
+ const results = [];
35367
+ const failedStories = new Set;
35368
+ for (const storyId of orderedStories) {
35369
+ const deps = dependencies[storyId] || [];
35370
+ const hasFailedDeps = deps.some((dep) => failedStories.has(dep));
35371
+ if (hasFailedDeps) {
35372
+ results.push({
35373
+ success: false,
35374
+ storyId,
35375
+ conflictFiles: []
35376
+ });
35377
+ failedStories.add(storyId);
35378
+ continue;
35379
+ }
35380
+ let result = await this.merge(projectRoot, storyId);
35381
+ if (!result.success && result.conflictFiles) {
35382
+ try {
35383
+ await this.rebaseWorktree(projectRoot, storyId);
35384
+ result = await this.merge(projectRoot, storyId);
35385
+ if (!result.success) {
35386
+ results.push({
35387
+ success: false,
35388
+ storyId,
35389
+ conflictFiles: result.conflictFiles,
35390
+ retryCount: 1
35391
+ });
35392
+ failedStories.add(storyId);
35393
+ continue;
35394
+ }
35395
+ results.push({
35396
+ success: true,
35397
+ storyId,
35398
+ retryCount: 1
35399
+ });
35400
+ } catch (error48) {
35401
+ results.push({
35402
+ success: false,
35403
+ storyId,
35404
+ conflictFiles: result.conflictFiles,
35405
+ retryCount: 1
35406
+ });
35407
+ failedStories.add(storyId);
35787
35408
  }
35788
- const diffSummary = await captureDiffSummary(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
35789
- if (diffSummary) {
35790
- completedStory.diffSummary = diffSummary;
35409
+ } else if (result.success) {
35410
+ results.push({
35411
+ success: true,
35412
+ storyId,
35413
+ retryCount: 0
35414
+ });
35415
+ } else {
35416
+ results.push({
35417
+ success: false,
35418
+ storyId,
35419
+ retryCount: 0
35420
+ });
35421
+ failedStories.add(storyId);
35422
+ }
35423
+ }
35424
+ return results;
35425
+ }
35426
+ topologicalSort(storyIds, dependencies) {
35427
+ const visited = new Set;
35428
+ const sorted = [];
35429
+ const visiting = new Set;
35430
+ const visit = (storyId) => {
35431
+ if (visited.has(storyId)) {
35432
+ return;
35433
+ }
35434
+ if (visiting.has(storyId)) {
35435
+ throw new Error(`Circular dependency detected involving ${storyId}`);
35436
+ }
35437
+ visiting.add(storyId);
35438
+ const deps = dependencies[storyId] || [];
35439
+ for (const dep of deps) {
35440
+ if (storyIds.includes(dep)) {
35441
+ visit(dep);
35791
35442
  }
35792
- } catch {}
35443
+ }
35444
+ visiting.delete(storyId);
35445
+ visited.add(storyId);
35446
+ sorted.push(storyId);
35447
+ };
35448
+ for (const storyId of storyIds) {
35449
+ visit(storyId);
35793
35450
  }
35451
+ return sorted;
35794
35452
  }
35795
- const updatedCounts = countStories(prd);
35796
- logger?.info("progress", "Progress update", {
35797
- totalStories: updatedCounts.total,
35798
- passedStories: updatedCounts.passed,
35799
- failedStories: updatedCounts.failed,
35800
- pendingStories: updatedCounts.pending,
35801
- totalCost: ctx.totalCost + costDelta,
35802
- costLimit: ctx.config.execution.costLimit,
35803
- elapsedMs: Date.now() - ctx.startTime,
35804
- storyDurationMs: ctx.storyStartTime ? Date.now() - ctx.storyStartTime : undefined
35805
- });
35806
- return { storiesCompletedDelta, costDelta, prd, prdDirty: true };
35807
- }
35808
- async function handlePipelineFailure(ctx, pipelineResult) {
35809
- const logger = getSafeLogger();
35810
- let prd = ctx.prd;
35811
- let prdDirty = false;
35812
- const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
35813
- switch (pipelineResult.finalAction) {
35814
- case "pause":
35815
- markStoryPaused(prd, ctx.story.id);
35816
- await savePRD(prd, ctx.prdPath);
35817
- prdDirty = true;
35818
- logger?.warn("pipeline", "Story paused", { storyId: ctx.story.id, reason: pipelineResult.reason });
35819
- pipelineEventBus.emit({
35820
- type: "story:paused",
35821
- storyId: ctx.story.id,
35822
- reason: pipelineResult.reason || "Pipeline paused",
35823
- cost: ctx.totalCost
35453
+ async rebaseWorktree(projectRoot, storyId) {
35454
+ const worktreePath = `${projectRoot}/.nax-wt/${storyId}`;
35455
+ try {
35456
+ const currentBranchProc = _mergeDeps.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
35457
+ cwd: projectRoot,
35458
+ stdout: "pipe",
35459
+ stderr: "pipe"
35824
35460
  });
35825
- break;
35826
- case "skip":
35827
- logger?.warn("pipeline", "Story skipped", { storyId: ctx.story.id, reason: pipelineResult.reason });
35828
- prdDirty = true;
35829
- break;
35830
- case "fail":
35831
- markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory, pipelineResult.stoppedAtStage);
35832
- await savePRD(prd, ctx.prdPath);
35833
- prdDirty = true;
35834
- logger?.error("pipeline", "Story failed", { storyId: ctx.story.id, reason: pipelineResult.reason });
35835
- if (ctx.featureDir) {
35836
- await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} \u2014 ${pipelineResult.reason}`);
35461
+ const exitCode = await currentBranchProc.exited;
35462
+ if (exitCode !== 0) {
35463
+ throw new Error("Failed to get current branch");
35837
35464
  }
35838
- pipelineEventBus.emit({
35839
- type: "story:failed",
35840
- storyId: ctx.story.id,
35841
- story: ctx.story,
35842
- reason: pipelineResult.reason || "Pipeline failed",
35843
- countsTowardEscalation: true,
35844
- feature: ctx.feature,
35845
- attempts: ctx.story.attempts
35465
+ const currentBranch = (await new Response(currentBranchProc.stdout).text()).trim();
35466
+ const rebaseProc = _mergeDeps.spawn(["git", "rebase", currentBranch], {
35467
+ cwd: worktreePath,
35468
+ stdout: "pipe",
35469
+ stderr: "pipe"
35846
35470
  });
35847
- if (ctx.story.attempts !== undefined && ctx.story.attempts >= ctx.config.execution.rectification.maxRetries) {
35848
- await pipelineEventBus.emitAsync({
35849
- type: "human-review:requested",
35850
- storyId: ctx.story.id,
35851
- reason: pipelineResult.reason || "Max retries exceeded",
35852
- feature: ctx.feature,
35853
- attempts: ctx.story.attempts
35471
+ const rebaseExitCode = await rebaseProc.exited;
35472
+ if (rebaseExitCode !== 0) {
35473
+ const stderr = await new Response(rebaseProc.stderr).text();
35474
+ const abortProc = _mergeDeps.spawn(["git", "rebase", "--abort"], {
35475
+ cwd: worktreePath,
35476
+ stdout: "pipe",
35477
+ stderr: "pipe"
35854
35478
  });
35479
+ await abortProc.exited;
35480
+ throw new Error(`Rebase failed: ${stderr || "unknown error"}`);
35855
35481
  }
35856
- break;
35857
- case "escalate": {
35858
- const escalationResult = await handleTierEscalation({
35859
- story: ctx.story,
35860
- storiesToExecute: ctx.storiesToExecute,
35861
- isBatchExecution: ctx.isBatchExecution,
35862
- routing: ctx.routing,
35863
- pipelineResult,
35864
- config: ctx.config,
35865
- prd,
35866
- prdPath: ctx.prdPath,
35867
- featureDir: ctx.featureDir,
35868
- hooks: ctx.hooks,
35869
- feature: ctx.feature,
35870
- totalCost: ctx.totalCost,
35871
- workdir: ctx.workdir,
35872
- attemptCost: pipelineResult.context.agentResult?.estimatedCost || 0
35482
+ } catch (error48) {
35483
+ if (error48 instanceof Error) {
35484
+ throw error48;
35485
+ }
35486
+ throw new Error(`Failed to rebase worktree ${storyId}: ${String(error48)}`);
35487
+ }
35488
+ }
35489
+ async getConflictFiles(projectRoot) {
35490
+ try {
35491
+ const proc = _mergeDeps.spawn(["git", "diff", "--name-only", "--diff-filter=U"], {
35492
+ cwd: projectRoot,
35493
+ stdout: "pipe",
35494
+ stderr: "pipe"
35495
+ });
35496
+ const exitCode = await proc.exited;
35497
+ if (exitCode !== 0) {
35498
+ return [];
35499
+ }
35500
+ const stdout = await new Response(proc.stdout).text();
35501
+ return stdout.trim().split(`
35502
+ `).filter((line) => line.length > 0);
35503
+ } catch {
35504
+ return [];
35505
+ }
35506
+ }
35507
+ async abortMerge(projectRoot) {
35508
+ try {
35509
+ const proc = _mergeDeps.spawn(["git", "merge", "--abort"], {
35510
+ cwd: projectRoot,
35511
+ stdout: "pipe",
35512
+ stderr: "pipe"
35513
+ });
35514
+ await proc.exited;
35515
+ } catch (error48) {
35516
+ const logger = getSafeLogger();
35517
+ logger?.warn("worktree", "Failed to abort merge", {
35518
+ error: errorMessage(error48)
35873
35519
  });
35874
- prd = escalationResult.prd;
35875
- prdDirty = escalationResult.prdDirty;
35876
- break;
35877
35520
  }
35878
35521
  }
35879
- return { prd, prdDirty, costDelta };
35880
35522
  }
35881
- var init_pipeline_result_handler = __esm(() => {
35523
+ var _mergeDeps;
35524
+ var init_merge = __esm(() => {
35882
35525
  init_logger2();
35883
- init_event_bus();
35884
- init_prd();
35885
- init_git();
35886
- init_escalation();
35887
- init_progress();
35526
+ init_bun_deps();
35527
+ _mergeDeps = {
35528
+ spawn
35529
+ };
35888
35530
  });
35889
35531
 
35890
- // src/execution/iteration-runner.ts
35891
- import { join as join51 } from "path";
35892
- async function runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics) {
35532
+ // src/execution/merge-conflict-rectify.ts
35533
+ var exports_merge_conflict_rectify = {};
35534
+ __export(exports_merge_conflict_rectify, {
35535
+ rectifyConflictedStory: () => rectifyConflictedStory
35536
+ });
35537
+ import path15 from "path";
35538
+ async function rectifyConflictedStory(options) {
35539
+ const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter, agentGetFn } = options;
35893
35540
  const logger = getSafeLogger();
35894
- const { story, storiesToExecute, routing, isBatchExecution } = selection;
35895
- if (ctx.dryRun) {
35896
- const dryRunResult = await handleDryRun({
35541
+ logger?.info("parallel", "Rectifying story on updated base", { storyId, attempt: "rectification" });
35542
+ try {
35543
+ const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
35544
+ const { MergeEngine: MergeEngine2 } = await Promise.resolve().then(() => (init_merge(), exports_merge));
35545
+ const { runPipeline: runPipeline2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
35546
+ const { defaultPipeline: defaultPipeline2 } = await Promise.resolve().then(() => (init_stages(), exports_stages));
35547
+ const { routeTask: routeTask2 } = await Promise.resolve().then(() => (init_routing(), exports_routing));
35548
+ const worktreeManager = new WorktreeManager2;
35549
+ const mergeEngine = new MergeEngine2(worktreeManager);
35550
+ try {
35551
+ await worktreeManager.remove(workdir, storyId);
35552
+ } catch {}
35553
+ await worktreeManager.create(workdir, storyId);
35554
+ const worktreePath = path15.join(workdir, ".nax-wt", storyId);
35555
+ const story = prd.userStories.find((s) => s.id === storyId);
35556
+ if (!story) {
35557
+ return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
35558
+ }
35559
+ const routing = routeTask2(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
35560
+ const pipelineContext = {
35561
+ config: config2,
35562
+ effectiveConfig: config2,
35897
35563
  prd,
35898
- prdPath: ctx.prdPath,
35899
- storiesToExecute,
35564
+ story,
35565
+ stories: [story],
35566
+ workdir: worktreePath,
35567
+ featureDir: undefined,
35568
+ hooks,
35569
+ plugins: pluginRegistry,
35570
+ storyStartTime: new Date().toISOString(),
35900
35571
  routing,
35901
- statusWriter: ctx.statusWriter,
35902
- pluginRegistry: ctx.pluginRegistry,
35903
- runId: ctx.runId,
35904
- totalCost,
35905
- iterations
35906
- });
35907
- return {
35908
- prd,
35909
- storiesCompletedDelta: dryRunResult.storiesCompletedDelta,
35910
- costDelta: 0,
35911
- prdDirty: dryRunResult.prdDirty
35912
- };
35913
- }
35914
- const storyStartTime = Date.now();
35915
- const storyGitRef = await captureGitRef(ctx.workdir);
35916
- const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
35917
- const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join51(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
35918
- const pipelineContext = {
35919
- config: ctx.config,
35920
- effectiveConfig,
35921
- prd,
35922
- story,
35923
- stories: storiesToExecute,
35924
- routing,
35925
- workdir: ctx.workdir,
35926
- prdPath: ctx.prdPath,
35927
- featureDir: ctx.featureDir,
35928
- hooks: ctx.hooks,
35929
- plugins: ctx.pluginRegistry,
35930
- storyStartTime: new Date().toISOString(),
35931
- storyGitRef: storyGitRef ?? undefined,
35932
- interaction: ctx.interactionChain ?? undefined,
35933
- agentGetFn: ctx.agentGetFn,
35934
- pidRegistry: ctx.pidRegistry,
35935
- accumulatedAttemptCost: accumulatedAttemptCost > 0 ? accumulatedAttemptCost : undefined
35936
- };
35937
- ctx.statusWriter.setPrd(prd);
35938
- ctx.statusWriter.setCurrentStory({
35939
- storyId: story.id,
35940
- title: story.title,
35941
- complexity: routing.complexity,
35942
- tddStrategy: routing.testStrategy,
35943
- model: routing.modelTier,
35944
- attempt: (story.attempts ?? 0) + 1,
35945
- phase: "routing"
35946
- });
35947
- await ctx.statusWriter.update(totalCost, iterations);
35948
- const pipelineResult = await runPipeline(defaultPipeline, pipelineContext, ctx.eventEmitter);
35949
- const currentPrd = pipelineResult.context.prd;
35950
- const handlerCtx = {
35951
- config: ctx.config,
35952
- prd: currentPrd,
35953
- prdPath: ctx.prdPath,
35954
- workdir: ctx.workdir,
35955
- featureDir: ctx.featureDir,
35956
- hooks: ctx.hooks,
35957
- feature: ctx.feature,
35958
- totalCost,
35959
- startTime: ctx.startTime,
35960
- runId: ctx.runId,
35961
- pluginRegistry: ctx.pluginRegistry,
35962
- story,
35963
- storiesToExecute,
35964
- routing: pipelineResult.context.routing ?? routing,
35965
- isBatchExecution,
35966
- allStoryMetrics,
35967
- storyGitRef,
35968
- interactionChain: ctx.interactionChain,
35969
- storyStartTime
35970
- };
35971
- if (pipelineResult.success) {
35972
- const r2 = await handlePipelineSuccess(handlerCtx, pipelineResult);
35973
- return {
35974
- prd: r2.prd,
35975
- storiesCompletedDelta: r2.storiesCompletedDelta,
35976
- costDelta: r2.costDelta,
35977
- prdDirty: r2.prdDirty,
35978
- finalAction: pipelineResult.finalAction
35572
+ agentGetFn
35979
35573
  };
35574
+ const pipelineResult = await runPipeline2(defaultPipeline2, pipelineContext, eventEmitter);
35575
+ const cost = pipelineResult.context.agentResult?.estimatedCost ?? 0;
35576
+ if (!pipelineResult.success) {
35577
+ logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
35578
+ return { success: false, storyId, cost, finalConflict: false, pipelineFailure: true };
35579
+ }
35580
+ const mergeResults = await mergeEngine.mergeAll(workdir, [storyId], { [storyId]: [] });
35581
+ const mergeResult = mergeResults[0];
35582
+ if (!mergeResult || !mergeResult.success) {
35583
+ const conflictFiles = mergeResult?.conflictFiles ?? [];
35584
+ logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
35585
+ return { success: false, storyId, cost, finalConflict: true, conflictFiles };
35586
+ }
35587
+ logger?.info("parallel", "Rectification succeeded - story merged", {
35588
+ storyId,
35589
+ originalCost: options.originalCost,
35590
+ rectificationCost: cost
35591
+ });
35592
+ return { success: true, storyId, cost };
35593
+ } catch (error48) {
35594
+ logger?.error("parallel", "Rectification failed - preserving worktree", {
35595
+ storyId,
35596
+ error: errorMessage(error48)
35597
+ });
35598
+ return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
35980
35599
  }
35981
- const r = await handlePipelineFailure(handlerCtx, pipelineResult);
35982
- return {
35983
- prd: r.prd,
35984
- storiesCompletedDelta: 0,
35985
- costDelta: r.costDelta,
35986
- prdDirty: r.prdDirty,
35987
- finalAction: pipelineResult.finalAction,
35988
- reason: pipelineResult.reason,
35989
- subStoryCount: pipelineResult.subStoryCount
35990
- };
35991
35600
  }
35992
- var _iterationRunnerDeps;
35993
- var init_iteration_runner = __esm(() => {
35994
- init_loader();
35601
+ var init_merge_conflict_rectify = __esm(() => {
35995
35602
  init_logger2();
35996
- init_runner();
35997
- init_stages();
35998
- init_git();
35999
- init_dry_run();
36000
- init_pipeline_result_handler();
36001
- _iterationRunnerDeps = {
36002
- loadConfigForWorkdir
36003
- };
36004
35603
  });
36005
35604
 
36006
- // src/execution/executor-types.ts
36007
- function buildPreviewRouting(story, config2) {
36008
- const cached2 = story.routing;
36009
- const defaultComplexity = "medium";
36010
- const defaultTier = "balanced";
36011
- const defaultStrategy = "test-after";
36012
- return {
36013
- complexity: cached2?.complexity ?? defaultComplexity,
36014
- modelTier: cached2?.modelTier ?? config2.autoMode.complexityRouting?.[defaultComplexity] ?? defaultTier,
36015
- testStrategy: cached2?.testStrategy ?? defaultStrategy,
36016
- reasoning: cached2 ? "cached from story.routing" : "preview (pending pipeline routing stage)"
36017
- };
36018
- }
36019
-
36020
- // src/execution/story-selector.ts
36021
- function selectNextStories(prd, config2, batchPlan, currentBatchIndex, lastStoryId, useBatch) {
36022
- if (useBatch && currentBatchIndex < batchPlan.length) {
36023
- const batch = batchPlan[currentBatchIndex];
36024
- const storiesToExecute = batch.stories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed");
36025
- if (storiesToExecute.length === 0) {
36026
- return { selection: null, nextBatchIndex: currentBatchIndex + 1 };
35605
+ // src/execution/parallel-batch.ts
35606
+ var exports_parallel_batch = {};
35607
+ __export(exports_parallel_batch, {
35608
+ runParallelBatch: () => runParallelBatch,
35609
+ _parallelBatchDeps: () => _parallelBatchDeps
35610
+ });
35611
+ import path16 from "path";
35612
+ async function runParallelBatch(options) {
35613
+ const { stories, ctx, prd } = options;
35614
+ const { workdir, config: config2, maxConcurrency, pipelineContext, eventEmitter, agentGetFn, hooks, pluginRegistry } = ctx;
35615
+ const worktreeManager = await _parallelBatchDeps.createWorktreeManager();
35616
+ const worktreePaths = new Map;
35617
+ const storyStartTimes = new Map;
35618
+ for (const story of stories) {
35619
+ storyStartTimes.set(story.id, Date.now());
35620
+ await worktreeManager.create(workdir, story.id);
35621
+ worktreePaths.set(story.id, path16.join(workdir, ".nax-wt", story.id));
35622
+ }
35623
+ const workerResult = await _parallelBatchDeps.executeParallelBatch(stories, workdir, config2, pipelineContext, worktreePaths, maxConcurrency, eventEmitter);
35624
+ const batchEndMs = Date.now();
35625
+ const completed = workerResult.merged;
35626
+ const failed = workerResult.failed.map((f) => ({
35627
+ story: f.story,
35628
+ pipelineResult: f.pipelineResult ?? {
35629
+ success: false,
35630
+ finalAction: "fail",
35631
+ reason: f.error,
35632
+ context: { ...pipelineContext, story: f.story, stories: [f.story], workdir }
36027
35633
  }
36028
- const story2 = storiesToExecute[0];
36029
- return {
36030
- selection: {
36031
- story: story2,
36032
- storiesToExecute,
36033
- routing: buildPreviewRouting(story2, config2),
36034
- isBatchExecution: batch.isBatch && storiesToExecute.length > 1
36035
- },
36036
- nextBatchIndex: currentBatchIndex + 1
36037
- };
35634
+ }));
35635
+ const storyEndTimes = new Map;
35636
+ for (const story of [...workerResult.pipelinePassed, ...workerResult.merged]) {
35637
+ storyEndTimes.set(story.id, batchEndMs);
36038
35638
  }
36039
- const story = getNextStory(prd, lastStoryId, config2.execution.rectification?.maxRetries ?? 2);
36040
- if (!story)
36041
- return null;
36042
- return {
36043
- selection: {
36044
- story,
36045
- storiesToExecute: [story],
36046
- routing: buildPreviewRouting(story, config2),
36047
- isBatchExecution: false
35639
+ for (const { story } of workerResult.failed) {
35640
+ storyEndTimes.set(story.id, batchEndMs);
35641
+ }
35642
+ const mergeConflicts = [];
35643
+ for (const conflict of workerResult.mergeConflicts) {
35644
+ const story = stories.find((s) => s.id === conflict.storyId);
35645
+ if (!story)
35646
+ continue;
35647
+ try {
35648
+ const rectResult = await _parallelBatchDeps.rectifyConflictedStory({
35649
+ ...conflict,
35650
+ workdir,
35651
+ config: config2,
35652
+ hooks,
35653
+ pluginRegistry,
35654
+ prd,
35655
+ eventEmitter,
35656
+ agentGetFn
35657
+ });
35658
+ mergeConflicts.push({ story, rectified: rectResult.success, cost: rectResult.cost });
35659
+ } catch (err) {
35660
+ const logger = getSafeLogger();
35661
+ logger?.warn("[parallel-batch]", "rectification failed for story", {
35662
+ storyId: story.id,
35663
+ error: err.message
35664
+ });
35665
+ mergeConflicts.push({ story, rectified: false, cost: 0 });
35666
+ }
35667
+ storyEndTimes.set(conflict.storyId, Date.now());
35668
+ }
35669
+ const storyCosts = workerResult.storyCosts;
35670
+ const totalCost = [...storyCosts.values()].reduce((sum, c) => sum + c, 0);
35671
+ const storyDurations = new Map;
35672
+ for (const story of stories) {
35673
+ const startMs = storyStartTimes.get(story.id);
35674
+ const endMs = storyEndTimes.get(story.id);
35675
+ if (startMs !== undefined && endMs !== undefined) {
35676
+ storyDurations.set(story.id, endMs - startMs);
35677
+ }
35678
+ }
35679
+ return { completed, failed, mergeConflicts, storyCosts, storyDurations, totalCost };
35680
+ }
35681
+ var _parallelBatchDeps;
35682
+ var init_parallel_batch = __esm(() => {
35683
+ init_logger2();
35684
+ _parallelBatchDeps = {
35685
+ executeParallelBatch: async (_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter) => {
35686
+ const { executeParallelBatch: executeParallelBatch2 } = await Promise.resolve().then(() => (init_parallel_worker(), exports_parallel_worker));
35687
+ return executeParallelBatch2(_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter);
36048
35688
  },
36049
- nextBatchIndex: currentBatchIndex
35689
+ createWorktreeManager: async () => {
35690
+ const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
35691
+ return new WorktreeManager2;
35692
+ },
35693
+ createMergeEngine: async (worktreeManager) => {
35694
+ const { MergeEngine: MergeEngine2 } = await Promise.resolve().then(() => (init_merge(), exports_merge));
35695
+ return new MergeEngine2(worktreeManager);
35696
+ },
35697
+ rectifyConflictedStory: async (opts) => {
35698
+ const { rectifyConflictedStory: rectifyConflictedStory2 } = await Promise.resolve().then(() => (init_merge_conflict_rectify(), exports_merge_conflict_rectify));
35699
+ return rectifyConflictedStory2(opts);
35700
+ }
36050
35701
  };
36051
- }
36052
- var init_story_selector = __esm(() => {
36053
- init_prd();
36054
35702
  });
36055
35703
 
36056
- // src/execution/sequential-executor.ts
36057
- var exports_sequential_executor = {};
36058
- __export(exports_sequential_executor, {
36059
- executeSequential: () => executeSequential
35704
+ // src/execution/unified-executor.ts
35705
+ var exports_unified_executor = {};
35706
+ __export(exports_unified_executor, {
35707
+ executeUnified: () => executeUnified,
35708
+ _unifiedExecutorDeps: () => _unifiedExecutorDeps
36060
35709
  });
36061
- async function executeSequential(ctx, initialPrd) {
35710
+ async function executeUnified(ctx, initialPrd) {
36062
35711
  const logger = getSafeLogger();
36063
- let [prd, prdDirty, iterations, storiesCompleted, totalCost, lastStoryId, currentBatchIndex] = [
36064
- initialPrd,
36065
- false,
36066
- 0,
36067
- 0,
36068
- 0,
36069
- null,
36070
- 0
36071
- ];
35712
+ let prd = initialPrd;
35713
+ let prdDirty = false;
35714
+ let iterations = 0;
35715
+ let storiesCompleted = 0;
35716
+ let totalCost = 0;
35717
+ let lastStoryId = null;
35718
+ let currentBatchIndex = 0;
36072
35719
  const allStoryMetrics = [];
36073
35720
  let warningSent = false;
36074
35721
  let deferredReview;
@@ -36095,20 +35742,22 @@ async function executeSequential(ctx, initialPrd) {
36095
35742
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
36096
35743
  return buildResult2("completed");
36097
35744
  }
36098
- logger?.info("execution", "Running pre-run pipeline (acceptance test setup)");
36099
- const preRunCtx = {
36100
- config: ctx.config,
36101
- effectiveConfig: ctx.config,
36102
- prd,
36103
- workdir: ctx.workdir,
36104
- featureDir: ctx.featureDir,
36105
- story: prd.userStories[0],
36106
- stories: prd.userStories,
36107
- routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "" },
36108
- hooks: ctx.hooks,
36109
- agentGetFn: ctx.agentGetFn
36110
- };
36111
- await runPipeline(preRunPipeline, preRunCtx, ctx.eventEmitter);
35745
+ if (ctx.config.acceptance?.enabled) {
35746
+ logger?.info("execution", "Running pre-run pipeline (acceptance test setup)");
35747
+ const preRunCtx = {
35748
+ config: ctx.config,
35749
+ effectiveConfig: ctx.config,
35750
+ prd,
35751
+ workdir: ctx.workdir,
35752
+ featureDir: ctx.featureDir,
35753
+ story: prd.userStories[0],
35754
+ stories: prd.userStories,
35755
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "" },
35756
+ hooks: ctx.hooks,
35757
+ agentGetFn: ctx.agentGetFn
35758
+ };
35759
+ await runPipeline(preRunPipeline, preRunCtx, ctx.eventEmitter);
35760
+ }
36112
35761
  while (iterations < ctx.config.execution.maxIterations) {
36113
35762
  iterations++;
36114
35763
  if (Math.round(process.memoryUsage().heapUsed / 1024 / 1024) > 1024)
@@ -36120,13 +35769,203 @@ async function executeSequential(ctx, initialPrd) {
36120
35769
  if (isComplete(prd)) {
36121
35770
  if (ctx.interactionChain && isTriggerEnabled("pre-merge", ctx.config)) {
36122
35771
  const shouldProceed = await checkPreMerge({ featureName: ctx.feature, totalStories: prd.userStories.length, cost: totalCost }, ctx.config, ctx.interactionChain);
36123
- if (!shouldProceed) {
35772
+ if (!shouldProceed)
36124
35773
  return buildResult2("pre-merge-aborted");
36125
- }
36126
35774
  }
36127
35775
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
36128
35776
  return buildResult2("completed");
36129
35777
  }
35778
+ const costLimit = ctx.config.execution.costLimit;
35779
+ if ((ctx.parallelCount ?? 0) > 0) {
35780
+ const readyStories = getAllReadyStories(prd);
35781
+ const batch = _unifiedExecutorDeps.selectIndependentBatch(readyStories, ctx.parallelCount);
35782
+ if (batch.length > 1) {
35783
+ for (const story of batch) {
35784
+ pipelineEventBus.emit({
35785
+ type: "story:started",
35786
+ storyId: story.id,
35787
+ story,
35788
+ workdir: ctx.workdir,
35789
+ modelTier: story.routing?.modelTier ?? ctx.config.autoMode.complexityRouting?.[story.routing?.complexity ?? "medium"] ?? "balanced",
35790
+ agent: ctx.config.autoMode.defaultAgent,
35791
+ iteration: iterations
35792
+ });
35793
+ }
35794
+ const batchStartedAt = new Date().toISOString();
35795
+ const storyStartMs = new Map;
35796
+ for (const s of batch)
35797
+ storyStartMs.set(s.id, Date.now());
35798
+ const batchResult = await _unifiedExecutorDeps.runParallelBatch({
35799
+ stories: batch,
35800
+ ctx: {
35801
+ workdir: ctx.workdir,
35802
+ config: ctx.config,
35803
+ hooks: ctx.hooks,
35804
+ pluginRegistry: ctx.pluginRegistry,
35805
+ maxConcurrency: ctx.parallelCount,
35806
+ pipelineContext: {
35807
+ config: ctx.config,
35808
+ effectiveConfig: ctx.config,
35809
+ prd,
35810
+ hooks: ctx.hooks,
35811
+ featureDir: ctx.featureDir,
35812
+ agentGetFn: ctx.agentGetFn,
35813
+ pidRegistry: ctx.pidRegistry
35814
+ },
35815
+ eventEmitter: ctx.eventEmitter,
35816
+ agentGetFn: ctx.agentGetFn
35817
+ },
35818
+ prd
35819
+ });
35820
+ for (const { story, pipelineResult } of batchResult.failed) {
35821
+ const storyRouting = prd.userStories.find((s) => s.id === story.id)?.routing;
35822
+ await handlePipelineFailure({
35823
+ config: ctx.config,
35824
+ prd,
35825
+ prdPath: ctx.prdPath,
35826
+ workdir: ctx.workdir,
35827
+ featureDir: ctx.featureDir,
35828
+ hooks: ctx.hooks,
35829
+ feature: ctx.feature,
35830
+ totalCost,
35831
+ startTime: ctx.startTime,
35832
+ runId: ctx.runId,
35833
+ pluginRegistry: ctx.pluginRegistry,
35834
+ story,
35835
+ storiesToExecute: [story],
35836
+ routing: {
35837
+ complexity: storyRouting?.complexity ?? "medium",
35838
+ modelTier: storyRouting?.modelTier ?? "balanced",
35839
+ testStrategy: storyRouting?.testStrategy ?? "test-after",
35840
+ reasoning: storyRouting?.reasoning ?? ""
35841
+ },
35842
+ isBatchExecution: false,
35843
+ allStoryMetrics,
35844
+ storyGitRef: null,
35845
+ interactionChain: ctx.interactionChain
35846
+ }, pipelineResult);
35847
+ }
35848
+ totalCost += batchResult.totalCost;
35849
+ storiesCompleted += batchResult.completed.length;
35850
+ prdDirty = true;
35851
+ const batchCompletedAt = new Date().toISOString();
35852
+ for (const story of batchResult.completed) {
35853
+ const storyCost = batchResult.storyCosts.get(story.id) ?? 0;
35854
+ const storyStartTime = storyStartMs.get(story.id) ?? Date.now();
35855
+ const storyDuration = batchResult.storyDurations?.get(story.id) ?? Date.now() - storyStartTime;
35856
+ allStoryMetrics.push({
35857
+ storyId: story.id,
35858
+ complexity: story.routing?.complexity ?? "medium",
35859
+ modelTier: story.routing?.modelTier ?? "balanced",
35860
+ modelUsed: ctx.config.autoMode.defaultAgent,
35861
+ attempts: 1,
35862
+ finalTier: story.routing?.modelTier ?? "balanced",
35863
+ success: true,
35864
+ cost: storyCost,
35865
+ durationMs: storyDuration,
35866
+ firstPassSuccess: true,
35867
+ startedAt: batchStartedAt,
35868
+ completedAt: batchCompletedAt,
35869
+ source: "parallel"
35870
+ });
35871
+ }
35872
+ for (const conflict of batchResult.mergeConflicts) {
35873
+ if (conflict.rectified) {
35874
+ const storyStartTime = storyStartMs.get(conflict.story.id) ?? Date.now();
35875
+ const storyDuration = batchResult.storyDurations?.get(conflict.story.id) ?? Date.now() - storyStartTime;
35876
+ allStoryMetrics.push({
35877
+ storyId: conflict.story.id,
35878
+ complexity: conflict.story.routing?.complexity ?? "medium",
35879
+ modelTier: conflict.story.routing?.modelTier ?? "balanced",
35880
+ modelUsed: ctx.config.autoMode.defaultAgent,
35881
+ attempts: 1,
35882
+ finalTier: conflict.story.routing?.modelTier ?? "balanced",
35883
+ success: true,
35884
+ cost: batchResult.storyCosts.get(conflict.story.id) ?? 0,
35885
+ durationMs: storyDuration,
35886
+ firstPassSuccess: false,
35887
+ startedAt: batchStartedAt,
35888
+ completedAt: batchCompletedAt,
35889
+ source: "rectification",
35890
+ rectificationCost: conflict.cost
35891
+ });
35892
+ }
35893
+ }
35894
+ if (totalCost >= costLimit) {
35895
+ return buildResult2("cost-limit");
35896
+ }
35897
+ continue;
35898
+ }
35899
+ if (batch.length === 1) {
35900
+ const singleStory = batch[0];
35901
+ const singleSelection = {
35902
+ story: singleStory,
35903
+ storiesToExecute: [singleStory],
35904
+ routing: buildPreviewRouting(singleStory, ctx.config),
35905
+ isBatchExecution: false
35906
+ };
35907
+ if (!ctx.useBatch)
35908
+ lastStoryId = singleStory.id;
35909
+ if (totalCost >= costLimit) {
35910
+ const shouldProceed = ctx.interactionChain && isTriggerEnabled("cost-exceeded", ctx.config) ? await checkCostExceeded({ featureName: ctx.feature, cost: totalCost, limit: costLimit }, ctx.config, ctx.interactionChain) : false;
35911
+ if (!shouldProceed) {
35912
+ pipelineEventBus.emit({
35913
+ type: "run:paused",
35914
+ reason: `Cost limit reached: $${totalCost.toFixed(2)}`,
35915
+ storyId: singleStory.id,
35916
+ cost: totalCost
35917
+ });
35918
+ return buildResult2("cost-limit");
35919
+ }
35920
+ pipelineEventBus.emit({ type: "run:resumed", feature: ctx.feature });
35921
+ }
35922
+ pipelineEventBus.emit({
35923
+ type: "story:started",
35924
+ storyId: singleStory.id,
35925
+ story: singleStory,
35926
+ workdir: ctx.workdir,
35927
+ modelTier: singleSelection.routing.modelTier,
35928
+ agent: ctx.config.autoMode.defaultAgent,
35929
+ iteration: iterations
35930
+ });
35931
+ const singleIter = await _unifiedExecutorDeps.runIteration(ctx, prd, singleSelection, iterations, totalCost, allStoryMetrics);
35932
+ [prd, storiesCompleted, totalCost, prdDirty] = [
35933
+ singleIter.prd,
35934
+ storiesCompleted + singleIter.storiesCompletedDelta,
35935
+ totalCost + singleIter.costDelta,
35936
+ singleIter.prdDirty
35937
+ ];
35938
+ if (singleIter.finalAction === "decomposed") {
35939
+ iterations--;
35940
+ pipelineEventBus.emit({
35941
+ type: "story:decomposed",
35942
+ storyId: singleStory.id,
35943
+ story: singleStory,
35944
+ subStoryCount: singleIter.subStoryCount ?? 0
35945
+ });
35946
+ if (singleIter.prdDirty) {
35947
+ prd = await loadPRD(ctx.prdPath);
35948
+ prdDirty = false;
35949
+ }
35950
+ ctx.statusWriter.setPrd(prd);
35951
+ continue;
35952
+ }
35953
+ if (singleIter.prdDirty) {
35954
+ prd = await loadPRD(ctx.prdPath);
35955
+ prdDirty = false;
35956
+ }
35957
+ ctx.statusWriter.setPrd(prd);
35958
+ ctx.statusWriter.setCurrentStory(null);
35959
+ await ctx.statusWriter.update(totalCost, iterations);
35960
+ if (isStalled(prd)) {
35961
+ pipelineEventBus.emit({ type: "run:paused", reason: "All remaining stories blocked", cost: totalCost });
35962
+ return buildResult2("stalled");
35963
+ }
35964
+ if (ctx.config.execution.iterationDelayMs > 0)
35965
+ await Bun.sleep(ctx.config.execution.iterationDelayMs);
35966
+ continue;
35967
+ }
35968
+ }
36130
35969
  const selected = selectNextStories(prd, ctx.config, ctx.batchPlan, currentBatchIndex, lastStoryId, ctx.useBatch);
36131
35970
  if (!selected)
36132
35971
  return buildResult2("no-stories");
@@ -36138,8 +35977,8 @@ async function executeSequential(ctx, initialPrd) {
36138
35977
  const { selection } = selected;
36139
35978
  if (!ctx.useBatch)
36140
35979
  lastStoryId = selection.story.id;
36141
- if (totalCost >= ctx.config.execution.costLimit) {
36142
- const shouldProceed = ctx.interactionChain && isTriggerEnabled("cost-exceeded", ctx.config) ? await checkCostExceeded({ featureName: ctx.feature, cost: totalCost, limit: ctx.config.execution.costLimit }, ctx.config, ctx.interactionChain) : false;
35980
+ if (totalCost >= costLimit) {
35981
+ const shouldProceed = ctx.interactionChain && isTriggerEnabled("cost-exceeded", ctx.config) ? await checkCostExceeded({ featureName: ctx.feature, cost: totalCost, limit: costLimit }, ctx.config, ctx.interactionChain) : false;
36143
35982
  if (!shouldProceed) {
36144
35983
  pipelineEventBus.emit({
36145
35984
  type: "run:paused",
@@ -36160,7 +35999,7 @@ async function executeSequential(ctx, initialPrd) {
36160
35999
  agent: ctx.config.autoMode.defaultAgent,
36161
36000
  iteration: iterations
36162
36001
  });
36163
- const iter = await runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics);
36002
+ const iter = await _unifiedExecutorDeps.runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics);
36164
36003
  [prd, storiesCompleted, totalCost, prdDirty] = [
36165
36004
  iter.prd,
36166
36005
  storiesCompleted + iter.storiesCompletedDelta,
@@ -36183,7 +36022,6 @@ async function executeSequential(ctx, initialPrd) {
36183
36022
  continue;
36184
36023
  }
36185
36024
  if (ctx.interactionChain && isTriggerEnabled("cost-warning", ctx.config) && !warningSent) {
36186
- const costLimit = ctx.config.execution.costLimit;
36187
36025
  const triggerCfg = ctx.config.interaction?.triggers?.["cost-warning"];
36188
36026
  const threshold = typeof triggerCfg === "object" ? triggerCfg.threshold ?? 0.8 : 0.8;
36189
36027
  if (totalCost >= costLimit * threshold) {
@@ -36205,12 +36043,17 @@ async function executeSequential(ctx, initialPrd) {
36205
36043
  if (ctx.config.execution.iterationDelayMs > 0)
36206
36044
  await Bun.sleep(ctx.config.execution.iterationDelayMs);
36207
36045
  }
36208
- logger?.info("execution", "Running post-run pipeline (acceptance tests)");
36209
- await runPipeline(postRunPipeline, { config: ctx.config, prd, workdir: ctx.workdir, story: prd.userStories[0] }, ctx.eventEmitter);
36046
+ if (ctx.config.acceptance?.enabled) {
36047
+ logger?.info("execution", "Running post-run pipeline (acceptance tests)");
36048
+ await runPipeline(postRunPipeline, { config: ctx.config, prd, workdir: ctx.workdir, story: prd.userStories[0] }, ctx.eventEmitter);
36049
+ }
36210
36050
  return buildResult2("max-iterations");
36211
- } finally {}
36051
+ } finally {
36052
+ stopHeartbeat();
36053
+ }
36212
36054
  }
36213
- var init_sequential_executor = __esm(() => {
36055
+ var _unifiedExecutorDeps;
36056
+ var init_unified_executor = __esm(() => {
36214
36057
  init_triggers();
36215
36058
  init_logger2();
36216
36059
  init_event_bus();
@@ -36224,21 +36067,31 @@ var init_sequential_executor = __esm(() => {
36224
36067
  init_prd();
36225
36068
  init_crash_recovery();
36226
36069
  init_deferred_review();
36070
+ init_helpers();
36227
36071
  init_iteration_runner();
36072
+ init_pipeline_result_handler();
36228
36073
  init_story_selector();
36074
+ _unifiedExecutorDeps = {
36075
+ runParallelBatch: async (opts) => {
36076
+ const { runParallelBatch: runParallelBatch2 } = await Promise.resolve().then(() => (init_parallel_batch(), exports_parallel_batch));
36077
+ return runParallelBatch2(opts);
36078
+ },
36079
+ runIteration,
36080
+ selectIndependentBatch
36081
+ };
36229
36082
  });
36230
36083
 
36231
36084
  // src/project/detector.ts
36232
- import { join as join52 } from "path";
36085
+ import { join as join51 } from "path";
36233
36086
  async function detectLanguage(workdir, pkg) {
36234
36087
  const deps = _detectorDeps;
36235
- if (await deps.fileExists(join52(workdir, "go.mod")))
36088
+ if (await deps.fileExists(join51(workdir, "go.mod")))
36236
36089
  return "go";
36237
- if (await deps.fileExists(join52(workdir, "Cargo.toml")))
36090
+ if (await deps.fileExists(join51(workdir, "Cargo.toml")))
36238
36091
  return "rust";
36239
- if (await deps.fileExists(join52(workdir, "pyproject.toml")))
36092
+ if (await deps.fileExists(join51(workdir, "pyproject.toml")))
36240
36093
  return "python";
36241
- if (await deps.fileExists(join52(workdir, "requirements.txt")))
36094
+ if (await deps.fileExists(join51(workdir, "requirements.txt")))
36242
36095
  return "python";
36243
36096
  if (pkg != null) {
36244
36097
  const allDeps = {
@@ -36298,18 +36151,18 @@ async function detectLintTool(workdir, language) {
36298
36151
  if (language === "python")
36299
36152
  return "ruff";
36300
36153
  const deps = _detectorDeps;
36301
- if (await deps.fileExists(join52(workdir, "biome.json")))
36154
+ if (await deps.fileExists(join51(workdir, "biome.json")))
36302
36155
  return "biome";
36303
- if (await deps.fileExists(join52(workdir, ".eslintrc")))
36156
+ if (await deps.fileExists(join51(workdir, ".eslintrc")))
36304
36157
  return "eslint";
36305
- if (await deps.fileExists(join52(workdir, ".eslintrc.js")))
36158
+ if (await deps.fileExists(join51(workdir, ".eslintrc.js")))
36306
36159
  return "eslint";
36307
- if (await deps.fileExists(join52(workdir, ".eslintrc.json")))
36160
+ if (await deps.fileExists(join51(workdir, ".eslintrc.json")))
36308
36161
  return "eslint";
36309
36162
  return;
36310
36163
  }
36311
36164
  async function detectProjectProfile(workdir, existing) {
36312
- const pkg = await _detectorDeps.readJson(join52(workdir, "package.json"));
36165
+ const pkg = await _detectorDeps.readJson(join51(workdir, "package.json"));
36313
36166
  const language = existing.language !== undefined ? existing.language : await detectLanguage(workdir, pkg);
36314
36167
  const type = existing.type !== undefined ? existing.type : detectType(pkg);
36315
36168
  const testFramework = existing.testFramework !== undefined ? existing.testFramework : await detectTestFramework(workdir, language, pkg);
@@ -36402,7 +36255,7 @@ async function writeStatusFile(filePath, status) {
36402
36255
  var init_status_file = () => {};
36403
36256
 
36404
36257
  // src/execution/status-writer.ts
36405
- import { join as join53 } from "path";
36258
+ import { join as join52 } from "path";
36406
36259
 
36407
36260
  class StatusWriter {
36408
36261
  statusFile;
@@ -36470,7 +36323,7 @@ class StatusWriter {
36470
36323
  if (!this._prd)
36471
36324
  return;
36472
36325
  const safeLogger = getSafeLogger();
36473
- const featureStatusPath = join53(featureDir, "status.json");
36326
+ const featureStatusPath = join52(featureDir, "status.json");
36474
36327
  try {
36475
36328
  const base = this.getSnapshot(totalCost, iterations);
36476
36329
  if (!base) {
@@ -36678,7 +36531,7 @@ __export(exports_run_initialization, {
36678
36531
  initializeRun: () => initializeRun,
36679
36532
  _reconcileDeps: () => _reconcileDeps
36680
36533
  });
36681
- import { join as join54 } from "path";
36534
+ import { join as join53 } from "path";
36682
36535
  async function reconcileState(prd, prdPath, workdir, config2) {
36683
36536
  const logger = getSafeLogger();
36684
36537
  let reconciledCount = 0;
@@ -36696,7 +36549,7 @@ async function reconcileState(prd, prdPath, workdir, config2) {
36696
36549
  });
36697
36550
  continue;
36698
36551
  }
36699
- const effectiveWorkdir = story.workdir ? join54(workdir, story.workdir) : workdir;
36552
+ const effectiveWorkdir = story.workdir ? join53(workdir, story.workdir) : workdir;
36700
36553
  try {
36701
36554
  const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
36702
36555
  if (!reviewResult.success) {
@@ -36798,7 +36651,7 @@ __export(exports_run_setup, {
36798
36651
  setupRun: () => setupRun,
36799
36652
  _runSetupDeps: () => _runSetupDeps
36800
36653
  });
36801
- import * as os5 from "os";
36654
+ import * as os3 from "os";
36802
36655
  import path18 from "path";
36803
36656
  async function setupRun(options) {
36804
36657
  const logger = getSafeLogger();
@@ -36895,7 +36748,7 @@ async function setupRun(options) {
36895
36748
  explicit: Object.fromEntries(explicitFields.map((f) => [f, existingProjectConfig[f]])),
36896
36749
  detected: Object.fromEntries(autodetectedFields.map((f) => [f, detectedProfile[f]]))
36897
36750
  });
36898
- const globalPluginsDir = path18.join(os5.homedir(), ".nax", "plugins");
36751
+ const globalPluginsDir = path18.join(os3.homedir(), ".nax", "plugins");
36899
36752
  const projectPluginsDir = path18.join(workdir, ".nax", "plugins");
36900
36753
  const configPlugins = config2.plugins || [];
36901
36754
  const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins);
@@ -67904,9 +67757,9 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
67904
67757
 
67905
67758
  // bin/nax.ts
67906
67759
  init_source();
67907
- import { existsSync as existsSync35, mkdirSync as mkdirSync6 } from "fs";
67760
+ import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
67908
67761
  import { homedir as homedir8 } from "os";
67909
- import { join as join56 } from "path";
67762
+ import { join as join55 } from "path";
67910
67763
 
67911
67764
  // node_modules/commander/esm.mjs
67912
67765
  var import__ = __toESM(require_commander(), 1);
@@ -70845,7 +70698,7 @@ import { existsSync as existsSync22 } from "fs";
70845
70698
  import { join as join35 } from "path";
70846
70699
  var VALID_AGENTS = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
70847
70700
  async function generateCommand(options) {
70848
- const workdir = process.cwd();
70701
+ const workdir = options.dir ?? process.cwd();
70849
70702
  const dryRun = options.dryRun ?? false;
70850
70703
  let config2;
70851
70704
  try {
@@ -72219,50 +72072,8 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
72219
72072
  if (options.useBatch) {
72220
72073
  await tryLlmBatchRoute(options.config, readyStories, "routing");
72221
72074
  }
72222
- if (options.parallel !== undefined) {
72223
- const runParallelExecution2 = options.runParallelExecution ?? (await Promise.resolve().then(() => (init_parallel_executor(), exports_parallel_executor))).runParallelExecution;
72224
- const parallelResult = await runParallelExecution2({
72225
- prdPath: options.prdPath,
72226
- workdir: options.workdir,
72227
- config: options.config,
72228
- hooks: options.hooks,
72229
- feature: options.feature,
72230
- featureDir: options.featureDir,
72231
- parallelCount: options.parallel,
72232
- eventEmitter: options.eventEmitter,
72233
- statusWriter: options.statusWriter,
72234
- runId: options.runId,
72235
- startedAt: options.startedAt,
72236
- startTime: options.startTime,
72237
- totalCost,
72238
- iterations,
72239
- storiesCompleted,
72240
- allStoryMetrics,
72241
- pluginRegistry,
72242
- formatterMode: options.formatterMode,
72243
- headless: options.headless,
72244
- agentGetFn: options.agentGetFn,
72245
- pidRegistry: options.pidRegistry,
72246
- interactionChain: options.interactionChain
72247
- }, prd);
72248
- prd = parallelResult.prd;
72249
- totalCost = parallelResult.totalCost;
72250
- storiesCompleted = parallelResult.storiesCompleted;
72251
- allStoryMetrics.push(...parallelResult.storyMetrics);
72252
- if (parallelResult.completed && parallelResult.durationMs !== undefined) {
72253
- return {
72254
- prd,
72255
- iterations,
72256
- storiesCompleted,
72257
- totalCost,
72258
- allStoryMetrics,
72259
- completedEarly: true,
72260
- durationMs: parallelResult.durationMs
72261
- };
72262
- }
72263
- }
72264
- const { executeSequential: executeSequential2 } = await Promise.resolve().then(() => (init_sequential_executor(), exports_sequential_executor));
72265
- const sequentialResult = await executeSequential2({
72075
+ const { executeUnified: executeUnified2 } = await Promise.resolve().then(() => (init_unified_executor(), exports_unified_executor));
72076
+ const unifiedResult = await executeUnified2({
72266
72077
  prdPath: options.prdPath,
72267
72078
  workdir: options.workdir,
72268
72079
  config: options.config,
@@ -72277,23 +72088,18 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
72277
72088
  logFilePath: options.logFilePath,
72278
72089
  runId: options.runId,
72279
72090
  startTime: options.startTime,
72280
- batchPlan,
72091
+ parallelCount: options.parallel,
72281
72092
  agentGetFn: options.agentGetFn,
72282
72093
  pidRegistry: options.pidRegistry,
72283
- interactionChain: options.interactionChain
72094
+ interactionChain: options.interactionChain,
72095
+ batchPlan
72284
72096
  }, prd);
72285
- prd = sequentialResult.prd;
72286
- iterations = sequentialResult.iterations;
72287
- totalCost += sequentialResult.totalCost;
72288
- storiesCompleted += sequentialResult.storiesCompleted;
72289
- allStoryMetrics.push(...sequentialResult.allStoryMetrics);
72290
- return {
72291
- prd,
72292
- iterations,
72293
- storiesCompleted,
72294
- totalCost,
72295
- allStoryMetrics
72296
- };
72097
+ prd = unifiedResult.prd;
72098
+ iterations = unifiedResult.iterations;
72099
+ storiesCompleted = unifiedResult.storiesCompleted;
72100
+ totalCost = unifiedResult.totalCost;
72101
+ allStoryMetrics.push(...unifiedResult.allStoryMetrics);
72102
+ return { prd, iterations, storiesCompleted, totalCost, allStoryMetrics };
72297
72103
  }
72298
72104
 
72299
72105
  // src/execution/runner-setup.ts
@@ -72327,10 +72133,6 @@ async function runSetupPhase(options) {
72327
72133
  // src/execution/runner.ts
72328
72134
  init_escalation();
72329
72135
  init_escalation();
72330
- var _runnerDeps = {
72331
- fireHook,
72332
- runParallelExecution: null
72333
- };
72334
72136
  async function run(options) {
72335
72137
  const {
72336
72138
  prdPath,
@@ -72404,7 +72206,6 @@ async function run(options) {
72404
72206
  formatterMode,
72405
72207
  headless,
72406
72208
  parallel,
72407
- runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
72408
72209
  agentGetFn,
72409
72210
  pidRegistry,
72410
72211
  interactionChain
@@ -72660,7 +72461,7 @@ __export(exports_base, {
72660
72461
  ConEmu: () => ConEmu
72661
72462
  });
72662
72463
  import process4 from "process";
72663
- import os6 from "os";
72464
+ import os4 from "os";
72664
72465
 
72665
72466
  // node_modules/environment/index.js
72666
72467
  var isBrowser = globalThis.window?.document !== undefined;
@@ -72759,7 +72560,7 @@ var isOldWindows = () => {
72759
72560
  if (isBrowser || !isWindows2) {
72760
72561
  return false;
72761
72562
  }
72762
- const parts = os6.release().split(".");
72563
+ const parts = os4.release().split(".");
72763
72564
  const major = Number(parts[0]);
72764
72565
  const build = Number(parts[2] ?? 0);
72765
72566
  if (major < 10) {
@@ -79838,15 +79639,15 @@ Next: nax generate --package ${options.package}`));
79838
79639
  }
79839
79640
  return;
79840
79641
  }
79841
- const naxDir = join56(workdir, ".nax");
79842
- if (existsSync35(naxDir) && !options.force) {
79642
+ const naxDir = join55(workdir, ".nax");
79643
+ if (existsSync34(naxDir) && !options.force) {
79843
79644
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
79844
79645
  return;
79845
79646
  }
79846
- mkdirSync6(join56(naxDir, "features"), { recursive: true });
79847
- mkdirSync6(join56(naxDir, "hooks"), { recursive: true });
79848
- await Bun.write(join56(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
79849
- await Bun.write(join56(naxDir, "hooks.json"), JSON.stringify({
79647
+ mkdirSync6(join55(naxDir, "features"), { recursive: true });
79648
+ mkdirSync6(join55(naxDir, "hooks"), { recursive: true });
79649
+ await Bun.write(join55(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
79650
+ await Bun.write(join55(naxDir, "hooks.json"), JSON.stringify({
79850
79651
  hooks: {
79851
79652
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
79852
79653
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -79854,12 +79655,12 @@ Next: nax generate --package ${options.package}`));
79854
79655
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
79855
79656
  }
79856
79657
  }, null, 2));
79857
- await Bun.write(join56(naxDir, ".gitignore"), `# nax temp files
79658
+ await Bun.write(join55(naxDir, ".gitignore"), `# nax temp files
79858
79659
  *.tmp
79859
79660
  .paused.json
79860
79661
  .nax-verifier-verdict.json
79861
79662
  `);
79862
- await Bun.write(join56(naxDir, "context.md"), `# Project Context
79663
+ await Bun.write(join55(naxDir, "context.md"), `# Project Context
79863
79664
 
79864
79665
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
79865
79666
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -79956,7 +79757,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79956
79757
  console.error(source_default.red("Error: --plan requires --from <spec-path>"));
79957
79758
  process.exit(1);
79958
79759
  }
79959
- if (options.from && !existsSync35(options.from)) {
79760
+ if (options.from && !existsSync34(options.from)) {
79960
79761
  console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
79961
79762
  process.exit(1);
79962
79763
  }
@@ -79985,10 +79786,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
79985
79786
  console.error(source_default.red("nax not initialized. Run: nax init"));
79986
79787
  process.exit(1);
79987
79788
  }
79988
- const featureDir = join56(naxDir, "features", options.feature);
79989
- const prdPath = join56(featureDir, "prd.json");
79789
+ const featureDir = join55(naxDir, "features", options.feature);
79790
+ const prdPath = join55(featureDir, "prd.json");
79990
79791
  if (options.plan && options.from) {
79991
- if (existsSync35(prdPath) && !options.force) {
79792
+ if (existsSync34(prdPath) && !options.force) {
79992
79793
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
79993
79794
  console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
79994
79795
  process.exit(1);
@@ -80008,10 +79809,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
80008
79809
  }
80009
79810
  }
80010
79811
  try {
80011
- const planLogDir = join56(featureDir, "plan");
79812
+ const planLogDir = join55(featureDir, "plan");
80012
79813
  mkdirSync6(planLogDir, { recursive: true });
80013
79814
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
80014
- const planLogPath = join56(planLogDir, `${planLogId}.jsonl`);
79815
+ const planLogPath = join55(planLogDir, `${planLogId}.jsonl`);
80015
79816
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
80016
79817
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
80017
79818
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -80044,15 +79845,15 @@ program2.command("run").description("Run the orchestration loop for a feature").
80044
79845
  process.exit(1);
80045
79846
  }
80046
79847
  }
80047
- if (!existsSync35(prdPath)) {
79848
+ if (!existsSync34(prdPath)) {
80048
79849
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
80049
79850
  process.exit(1);
80050
79851
  }
80051
79852
  resetLogger();
80052
- const runsDir = join56(featureDir, "runs");
79853
+ const runsDir = join55(featureDir, "runs");
80053
79854
  mkdirSync6(runsDir, { recursive: true });
80054
79855
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
80055
- const logFilePath = join56(runsDir, `${runId}.jsonl`);
79856
+ const logFilePath = join55(runsDir, `${runId}.jsonl`);
80056
79857
  const isTTY = process.stdout.isTTY ?? false;
80057
79858
  const headlessFlag = options.headless ?? false;
80058
79859
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -80068,7 +79869,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
80068
79869
  config2.autoMode.defaultAgent = options.agent;
80069
79870
  }
80070
79871
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
80071
- const globalNaxDir = join56(homedir8(), ".nax");
79872
+ const globalNaxDir = join55(homedir8(), ".nax");
80072
79873
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
80073
79874
  const eventEmitter = new PipelineEventEmitter;
80074
79875
  let tuiInstance;
@@ -80091,7 +79892,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
80091
79892
  } else {
80092
79893
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
80093
79894
  }
80094
- const statusFilePath = join56(workdir, ".nax", "status.json");
79895
+ const statusFilePath = join55(workdir, ".nax", "status.json");
80095
79896
  let parallel;
80096
79897
  if (options.parallel !== undefined) {
80097
79898
  parallel = Number.parseInt(options.parallel, 10);
@@ -80117,9 +79918,9 @@ program2.command("run").description("Run the orchestration loop for a feature").
80117
79918
  headless: useHeadless,
80118
79919
  skipPrecheck: options.skipPrecheck ?? false
80119
79920
  });
80120
- const latestSymlink = join56(runsDir, "latest.jsonl");
79921
+ const latestSymlink = join55(runsDir, "latest.jsonl");
80121
79922
  try {
80122
- if (existsSync35(latestSymlink)) {
79923
+ if (existsSync34(latestSymlink)) {
80123
79924
  Bun.spawnSync(["rm", latestSymlink]);
80124
79925
  }
80125
79926
  Bun.spawnSync(["ln", "-s", `${runId}.jsonl`, latestSymlink], {
@@ -80155,9 +79956,9 @@ features.command("create <name>").description("Create a new feature").option("-d
80155
79956
  console.error(source_default.red("nax not initialized. Run: nax init"));
80156
79957
  process.exit(1);
80157
79958
  }
80158
- const featureDir = join56(naxDir, "features", name);
79959
+ const featureDir = join55(naxDir, "features", name);
80159
79960
  mkdirSync6(featureDir, { recursive: true });
80160
- await Bun.write(join56(featureDir, "spec.md"), `# Feature: ${name}
79961
+ await Bun.write(join55(featureDir, "spec.md"), `# Feature: ${name}
80161
79962
 
80162
79963
  ## Overview
80163
79964
 
@@ -80190,7 +79991,7 @@ features.command("create <name>").description("Create a new feature").option("-d
80190
79991
 
80191
79992
  <!-- What this feature explicitly does NOT cover. -->
80192
79993
  `);
80193
- await Bun.write(join56(featureDir, "progress.txt"), `# Progress: ${name}
79994
+ await Bun.write(join55(featureDir, "progress.txt"), `# Progress: ${name}
80194
79995
 
80195
79996
  Created: ${new Date().toISOString()}
80196
79997
 
@@ -80216,8 +80017,8 @@ features.command("list").description("List all features").option("-d, --dir <pat
80216
80017
  console.error(source_default.red("nax not initialized."));
80217
80018
  process.exit(1);
80218
80019
  }
80219
- const featuresDir = join56(naxDir, "features");
80220
- if (!existsSync35(featuresDir)) {
80020
+ const featuresDir = join55(naxDir, "features");
80021
+ if (!existsSync34(featuresDir)) {
80221
80022
  console.log(source_default.dim("No features yet."));
80222
80023
  return;
80223
80024
  }
@@ -80231,8 +80032,8 @@ features.command("list").description("List all features").option("-d, --dir <pat
80231
80032
  Features:
80232
80033
  `));
80233
80034
  for (const name of entries) {
80234
- const prdPath = join56(featuresDir, name, "prd.json");
80235
- if (existsSync35(prdPath)) {
80035
+ const prdPath = join55(featuresDir, name, "prd.json");
80036
+ if (existsSync34(prdPath)) {
80236
80037
  const prd = await loadPRD(prdPath);
80237
80038
  const c = countStories(prd);
80238
80039
  console.log(` ${name} \u2014 ${c.passed}/${c.total} stories done`);
@@ -80262,10 +80063,10 @@ Use: nax plan -f <feature> --from <spec>`));
80262
80063
  process.exit(1);
80263
80064
  }
80264
80065
  const config2 = await loadConfig(workdir);
80265
- const featureLogDir = join56(naxDir, "features", options.feature, "plan");
80066
+ const featureLogDir = join55(naxDir, "features", options.feature, "plan");
80266
80067
  mkdirSync6(featureLogDir, { recursive: true });
80267
80068
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
80268
- const planLogPath = join56(featureLogDir, `${planLogId}.jsonl`);
80069
+ const planLogPath = join55(featureLogDir, `${planLogId}.jsonl`);
80269
80070
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
80270
80071
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
80271
80072
  try {
@@ -80302,8 +80103,8 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
80302
80103
  console.error(source_default.red("nax not initialized. Run: nax init"));
80303
80104
  process.exit(1);
80304
80105
  }
80305
- const featureDir = join56(naxDir, "features", options.feature);
80306
- if (!existsSync35(featureDir)) {
80106
+ const featureDir = join55(naxDir, "features", options.feature);
80107
+ if (!existsSync34(featureDir)) {
80307
80108
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
80308
80109
  process.exit(1);
80309
80110
  }
@@ -80318,7 +80119,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
80318
80119
  specPath: options.from,
80319
80120
  reclassify: options.reclassify
80320
80121
  });
80321
- const prdPath = join56(featureDir, "prd.json");
80122
+ const prdPath = join55(featureDir, "prd.json");
80322
80123
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
80323
80124
  const c = countStories(prd);
80324
80125
  console.log(source_default.green(`
@@ -80351,9 +80152,17 @@ program2.command("agents").description("List available coding agents with status
80351
80152
  process.exit(1);
80352
80153
  }
80353
80154
  });
80354
- program2.command("config").description("Display effective merged configuration").option("--explain", "Show detailed field descriptions", false).option("--diff", "Show only fields where project overrides global", false).action(async (options) => {
80155
+ program2.command("config").description("Display effective merged configuration").option("-d, --dir <path>", "Project directory", process.cwd()).option("--explain", "Show detailed field descriptions", false).option("--diff", "Show only fields where project overrides global", false).action(async (options) => {
80156
+ let workdir;
80157
+ try {
80158
+ workdir = validateDirectory(options.dir);
80159
+ } catch (err) {
80160
+ console.error(source_default.red(`Invalid directory: ${err.message}`));
80161
+ process.exit(1);
80162
+ return;
80163
+ }
80355
80164
  try {
80356
- const config2 = await loadConfig();
80165
+ const config2 = await loadConfig(workdir);
80357
80166
  await configCommand(config2, { explain: options.explain, diff: options.diff });
80358
80167
  } catch (err) {
80359
80168
  console.error(source_default.red(`Error: ${err.message}`));
@@ -80547,9 +80356,18 @@ program2.command("prompts").description("Assemble or initialize prompts").option
80547
80356
  process.exit(1);
80548
80357
  }
80549
80358
  });
80550
- program2.command("generate").description("Generate agent config files (CLAUDE.md, AGENTS.md, etc.) from nax/context.md").option("-c, --context <path>", "Context file path (default: nax/context.md)").option("-o, --output <dir>", "Output directory (default: project root)").option("-a, --agent <name>", "Specific agent (claude|opencode|cursor|windsurf|aider)").option("--dry-run", "Preview without writing files", false).option("--no-auto-inject", "Disable auto-injection of project metadata").option("--package <dir>", "Generate CLAUDE.md for a specific package (e.g. packages/api)").option("--all-packages", "Generate CLAUDE.md for all discovered packages", false).action(async (options) => {
80359
+ program2.command("generate").description("Generate agent config files (CLAUDE.md, AGENTS.md, etc.) from nax/context.md").option("-d, --dir <path>", "Project directory", process.cwd()).option("-c, --context <path>", "Context file path (default: nax/context.md)").option("-o, --output <dir>", "Output directory (default: project root)").option("-a, --agent <name>", "Specific agent (claude|opencode|cursor|windsurf|aider)").option("--dry-run", "Preview without writing files", false).option("--no-auto-inject", "Disable auto-injection of project metadata").option("--package <dir>", "Generate CLAUDE.md for a specific package (e.g. packages/api)").option("--all-packages", "Generate CLAUDE.md for all discovered packages", false).action(async (options) => {
80360
+ let workdir;
80361
+ try {
80362
+ workdir = validateDirectory(options.dir);
80363
+ } catch (err) {
80364
+ console.error(source_default.red(`Invalid directory: ${err.message}`));
80365
+ process.exit(1);
80366
+ return;
80367
+ }
80551
80368
  try {
80552
80369
  await generateCommand({
80370
+ dir: workdir,
80553
80371
  context: options.context,
80554
80372
  output: options.output,
80555
80373
  agent: options.agent,