@nathapp/nax 0.57.0 → 0.57.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +1561 -896
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -17829,256 +17829,6 @@ var init_zod = __esm(() => {
17829
17829
  init_external();
17830
17830
  });
17831
17831
 
17832
- // src/config/defaults.ts
17833
- var DEFAULT_CONFIG;
17834
- var init_defaults = __esm(() => {
17835
- DEFAULT_CONFIG = {
17836
- version: 1,
17837
- models: {
17838
- claude: {
17839
- fast: "haiku",
17840
- balanced: "sonnet",
17841
- powerful: "opus"
17842
- }
17843
- },
17844
- autoMode: {
17845
- enabled: true,
17846
- defaultAgent: "claude",
17847
- fallbackOrder: ["claude"],
17848
- complexityRouting: {
17849
- simple: "fast",
17850
- medium: "balanced",
17851
- complex: "powerful",
17852
- expert: "powerful"
17853
- },
17854
- escalation: {
17855
- enabled: true,
17856
- tierOrder: [
17857
- { tier: "fast", attempts: 5 },
17858
- { tier: "balanced", attempts: 3 },
17859
- { tier: "powerful", attempts: 2 }
17860
- ],
17861
- escalateEntireBatch: true
17862
- }
17863
- },
17864
- routing: {
17865
- strategy: "keyword",
17866
- llm: {
17867
- model: "fast",
17868
- fallbackToKeywords: true,
17869
- cacheDecisions: true,
17870
- mode: "hybrid",
17871
- timeoutMs: 30000
17872
- }
17873
- },
17874
- execution: {
17875
- maxIterations: 10,
17876
- iterationDelayMs: 2000,
17877
- costLimit: 30,
17878
- sessionTimeoutSeconds: 3600,
17879
- verificationTimeoutSeconds: 600,
17880
- maxStoriesPerFeature: 500,
17881
- rectification: {
17882
- enabled: true,
17883
- maxRetries: 2,
17884
- fullSuiteTimeoutSeconds: 300,
17885
- maxFailureSummaryChars: 2000,
17886
- abortOnIncreasingFailures: true,
17887
- escalateOnExhaustion: true,
17888
- rethinkAtAttempt: 2,
17889
- urgencyAtAttempt: 3
17890
- },
17891
- regressionGate: {
17892
- enabled: true,
17893
- timeoutSeconds: 300,
17894
- acceptOnTimeout: true,
17895
- maxRectificationAttempts: 2
17896
- },
17897
- contextProviderTokenBudget: 2000,
17898
- smartTestRunner: true
17899
- },
17900
- quality: {
17901
- requireTypecheck: true,
17902
- requireLint: true,
17903
- requireTests: true,
17904
- commands: {},
17905
- forceExit: false,
17906
- detectOpenHandles: true,
17907
- detectOpenHandlesRetries: 1,
17908
- gracePeriodMs: 5000,
17909
- drainTimeoutMs: 2000,
17910
- shell: "/bin/sh",
17911
- stripEnvVars: [
17912
- "CLAUDECODE",
17913
- "REPL_ID",
17914
- "AGENT",
17915
- "GITLAB_ACCESS_TOKEN",
17916
- "GITHUB_TOKEN",
17917
- "GITHUB_ACCESS_TOKEN",
17918
- "GH_TOKEN",
17919
- "CI_GIT_TOKEN",
17920
- "CI_JOB_TOKEN",
17921
- "BITBUCKET_ACCESS_TOKEN",
17922
- "NPM_TOKEN",
17923
- "NPM_AUTH_TOKEN",
17924
- "YARN_NPM_AUTH_TOKEN",
17925
- "ANTHROPIC_API_KEY",
17926
- "OPENAI_API_KEY",
17927
- "GEMINI_API_KEY",
17928
- "COHERE_API_KEY",
17929
- "AWS_ACCESS_KEY_ID",
17930
- "AWS_SECRET_ACCESS_KEY",
17931
- "AWS_SESSION_TOKEN",
17932
- "GOOGLE_APPLICATION_CREDENTIALS",
17933
- "GCLOUD_SERVICE_KEY",
17934
- "AZURE_CLIENT_SECRET",
17935
- "AZURE_TENANT_ID",
17936
- "TELEGRAM_BOT_TOKEN",
17937
- "SLACK_TOKEN",
17938
- "SLACK_WEBHOOK_URL",
17939
- "SENTRY_AUTH_TOKEN",
17940
- "DATADOG_API_KEY"
17941
- ],
17942
- testing: {
17943
- hermetic: true
17944
- }
17945
- },
17946
- tdd: {
17947
- maxRetries: 2,
17948
- autoVerifyIsolation: true,
17949
- autoApproveVerifier: true,
17950
- strategy: "auto",
17951
- sessionTiers: {
17952
- testWriter: "balanced",
17953
- verifier: "fast"
17954
- },
17955
- testWriterAllowedPaths: ["src/index.ts", "src/**/index.ts"],
17956
- rollbackOnFailure: true,
17957
- greenfieldDetection: true
17958
- },
17959
- constitution: {
17960
- enabled: true,
17961
- path: "constitution.md",
17962
- maxTokens: 2000
17963
- },
17964
- analyze: {
17965
- llmEnhanced: true,
17966
- model: "balanced",
17967
- fallbackToKeywords: true,
17968
- maxCodebaseSummaryTokens: 5000
17969
- },
17970
- review: {
17971
- enabled: true,
17972
- checks: ["typecheck", "lint"],
17973
- commands: {},
17974
- pluginMode: "per-story",
17975
- semantic: {
17976
- modelTier: "balanced",
17977
- rules: [],
17978
- timeoutMs: 600000,
17979
- excludePatterns: [":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"]
17980
- }
17981
- },
17982
- plan: {
17983
- model: "balanced",
17984
- outputPath: "spec.md"
17985
- },
17986
- acceptance: {
17987
- enabled: true,
17988
- maxRetries: 2,
17989
- generateTests: true,
17990
- testPath: ".nax-acceptance.test.ts",
17991
- model: "fast",
17992
- refinement: true,
17993
- redGate: true,
17994
- timeoutMs: 1800000,
17995
- fix: {
17996
- diagnoseModel: "fast",
17997
- fixModel: "balanced",
17998
- strategy: "diagnose-first",
17999
- maxRetries: 2
18000
- }
18001
- },
18002
- context: {
18003
- fileInjection: "disabled",
18004
- testCoverage: {
18005
- enabled: true,
18006
- detail: "names-and-counts",
18007
- maxTokens: 500,
18008
- testPattern: "**/*.test.{ts,js,tsx,jsx}",
18009
- scopeToStory: true
18010
- },
18011
- autoDetect: {
18012
- enabled: true,
18013
- maxFiles: 5,
18014
- traceImports: false
18015
- }
18016
- },
18017
- interaction: {
18018
- plugin: "cli",
18019
- config: {},
18020
- defaults: {
18021
- timeout: 600000,
18022
- fallback: "escalate"
18023
- },
18024
- triggers: {
18025
- "security-review": true,
18026
- "cost-warning": true
18027
- }
18028
- },
18029
- precheck: {
18030
- storySizeGate: {
18031
- enabled: true,
18032
- maxAcCount: 10,
18033
- maxDescriptionLength: 3000,
18034
- maxBulletPoints: 12,
18035
- action: "block",
18036
- maxReplanAttempts: 3
18037
- }
18038
- },
18039
- prompts: {},
18040
- agent: {
18041
- protocol: "acp"
18042
- },
18043
- debate: {
18044
- enabled: false,
18045
- agents: 3,
18046
- stages: {
18047
- plan: {
18048
- enabled: true,
18049
- resolver: { type: "synthesis" },
18050
- sessionMode: "stateful",
18051
- rounds: 3
18052
- },
18053
- review: {
18054
- enabled: true,
18055
- resolver: { type: "majority-fail-closed" },
18056
- sessionMode: "one-shot",
18057
- rounds: 2
18058
- },
18059
- acceptance: {
18060
- enabled: false,
18061
- resolver: { type: "majority-fail-closed" },
18062
- sessionMode: "one-shot",
18063
- rounds: 1
18064
- },
18065
- rectification: {
18066
- enabled: false,
18067
- resolver: { type: "synthesis" },
18068
- sessionMode: "one-shot",
18069
- rounds: 1
18070
- },
18071
- escalation: {
18072
- enabled: false,
18073
- resolver: { type: "majority-fail-closed" },
18074
- sessionMode: "one-shot",
18075
- rounds: 1
18076
- }
18077
- }
18078
- }
18079
- };
18080
- });
18081
-
18082
17832
  // src/config/schemas.ts
18083
17833
  function isLegacyFlatModels(val) {
18084
17834
  if (typeof val !== "object" || val === null)
@@ -18102,11 +17852,11 @@ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema
18102
17852
  resolver: makeResolverSchema(defaults.resolverType),
18103
17853
  sessionMode: exports_external.enum(["one-shot", "stateful"]).default(defaults.sessionMode),
18104
17854
  rounds: exports_external.number().int().min(1).default(defaults.rounds),
18105
- debaters: exports_external.array(DebaterSchema).min(2, "debaters must have at least 2 entries").optional()
17855
+ debaters: exports_external.array(DebaterSchema).min(2, "debaters must have at least 2 entries").optional(),
17856
+ timeoutSeconds: exports_external.number().int().positive().default(600)
18106
17857
  })), DebateConfigSchema, NaxConfigSchema;
18107
17858
  var init_schemas3 = __esm(() => {
18108
17859
  init_zod();
18109
- init_defaults();
18110
17860
  TokenPricingSchema = exports_external.object({
18111
17861
  inputPer1M: exports_external.number().min(0),
18112
17862
  outputPer1M: exports_external.number().min(0)
@@ -18121,8 +17871,7 @@ var init_schemas3 = __esm(() => {
18121
17871
  PerAgentModelMapSchema = exports_external.record(exports_external.string().min(1), exports_external.record(exports_external.string().min(1), ModelEntrySchema));
18122
17872
  ModelMapSchema = exports_external.preprocess((val) => {
18123
17873
  if (isLegacyFlatModels(val)) {
18124
- const defaultAgent = DEFAULT_CONFIG.autoMode.defaultAgent;
18125
- return { [defaultAgent]: val };
17874
+ return { claude: val };
18126
17875
  }
18127
17876
  return val;
18128
17877
  }, PerAgentModelMapSchema);
@@ -18214,6 +17963,19 @@ var init_schemas3 = __esm(() => {
18214
17963
  formatFix: exports_external.string().optional(),
18215
17964
  build: exports_external.string().optional()
18216
17965
  }),
17966
+ autofix: exports_external.object({
17967
+ enabled: exports_external.boolean().default(true),
17968
+ maxAttempts: exports_external.number().int().min(1).default(3),
17969
+ maxTotalAttempts: exports_external.number().int().min(1).default(12),
17970
+ rethinkAtAttempt: exports_external.number().int().min(1).default(2),
17971
+ urgencyAtAttempt: exports_external.number().int().min(1).default(3)
17972
+ }).default({
17973
+ enabled: true,
17974
+ maxAttempts: 3,
17975
+ maxTotalAttempts: 12,
17976
+ rethinkAtAttempt: 2,
17977
+ urgencyAtAttempt: 3
17978
+ }),
18217
17979
  forceExit: exports_external.boolean().default(false),
18218
17980
  detectOpenHandles: exports_external.boolean().default(true),
18219
17981
  detectOpenHandlesRetries: exports_external.number().int().min(0).max(5).default(1),
@@ -18303,13 +18065,14 @@ var init_schemas3 = __esm(() => {
18303
18065
  });
18304
18066
  PlanConfigSchema = exports_external.object({
18305
18067
  model: ModelTierSchema,
18306
- outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty")
18068
+ outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty"),
18069
+ timeoutSeconds: exports_external.number().int().positive().default(600)
18307
18070
  });
18308
18071
  AcceptanceFixConfigSchema = exports_external.object({
18309
- diagnoseModel: exports_external.string().min(1, "acceptance.fix.diagnoseModel must be non-empty"),
18310
- fixModel: exports_external.string().min(1, "acceptance.fix.fixModel must be non-empty"),
18311
- strategy: exports_external.enum(["diagnose-first", "implement-only"]),
18312
- maxRetries: exports_external.number().int().nonnegative()
18072
+ diagnoseModel: exports_external.string().min(1, "acceptance.fix.diagnoseModel must be non-empty").default("fast"),
18073
+ fixModel: exports_external.string().min(1, "acceptance.fix.fixModel must be non-empty").default("balanced"),
18074
+ strategy: exports_external.enum(["diagnose-first", "implement-only"]).default("diagnose-first"),
18075
+ maxRetries: exports_external.number().int().nonnegative().default(2)
18313
18076
  });
18314
18077
  AcceptanceConfigSchema = exports_external.object({
18315
18078
  enabled: exports_external.boolean(),
@@ -18429,6 +18192,7 @@ var init_schemas3 = __esm(() => {
18429
18192
  DebateConfigSchema = exports_external.preprocess(toObject, exports_external.object({
18430
18193
  enabled: exports_external.boolean().default(false),
18431
18194
  agents: exports_external.number().int().min(2).default(3),
18195
+ maxConcurrentDebaters: exports_external.number().int().min(1).max(10).default(2),
18432
18196
  stages: exports_external.preprocess(toObject, exports_external.object({
18433
18197
  plan: DebateStageConfigSchema({ enabled: true, resolverType: "synthesis", sessionMode: "stateful", rounds: 3 }),
18434
18198
  review: DebateStageConfigSchema({
@@ -18458,36 +18222,289 @@ var init_schemas3 = __esm(() => {
18458
18222
  }))
18459
18223
  }));
18460
18224
  NaxConfigSchema = exports_external.object({
18461
- version: exports_external.number(),
18462
- models: ModelMapSchema,
18463
- autoMode: AutoModeConfigSchema,
18464
- routing: RoutingConfigSchema,
18465
- execution: ExecutionConfigSchema,
18466
- quality: QualityConfigSchema,
18467
- tdd: TddConfigSchema,
18468
- constitution: ConstitutionConfigSchema,
18469
- analyze: AnalyzeConfigSchema,
18470
- review: ReviewConfigSchema,
18471
- plan: PlanConfigSchema,
18472
- acceptance: AcceptanceConfigSchema,
18473
- context: ContextConfigSchema,
18225
+ version: exports_external.number().default(1),
18226
+ models: ModelMapSchema.default({
18227
+ claude: {
18228
+ fast: "haiku",
18229
+ balanced: "sonnet",
18230
+ powerful: "opus"
18231
+ }
18232
+ }),
18233
+ autoMode: AutoModeConfigSchema.default({
18234
+ enabled: true,
18235
+ defaultAgent: "claude",
18236
+ fallbackOrder: ["claude"],
18237
+ complexityRouting: {
18238
+ simple: "fast",
18239
+ medium: "balanced",
18240
+ complex: "powerful",
18241
+ expert: "powerful"
18242
+ },
18243
+ escalation: {
18244
+ enabled: true,
18245
+ tierOrder: [
18246
+ { tier: "fast", attempts: 5 },
18247
+ { tier: "balanced", attempts: 3 },
18248
+ { tier: "powerful", attempts: 2 }
18249
+ ],
18250
+ escalateEntireBatch: true
18251
+ }
18252
+ }),
18253
+ routing: RoutingConfigSchema.default({
18254
+ strategy: "keyword",
18255
+ llm: {
18256
+ model: "fast",
18257
+ fallbackToKeywords: true,
18258
+ cacheDecisions: true,
18259
+ mode: "hybrid",
18260
+ timeoutMs: 30000
18261
+ }
18262
+ }),
18263
+ execution: ExecutionConfigSchema.default({
18264
+ maxIterations: 10,
18265
+ iterationDelayMs: 2000,
18266
+ costLimit: 30,
18267
+ sessionTimeoutSeconds: 3600,
18268
+ verificationTimeoutSeconds: 600,
18269
+ maxStoriesPerFeature: 500,
18270
+ rectification: {
18271
+ enabled: true,
18272
+ maxRetries: 2,
18273
+ fullSuiteTimeoutSeconds: 300,
18274
+ maxFailureSummaryChars: 2000,
18275
+ abortOnIncreasingFailures: true,
18276
+ escalateOnExhaustion: true,
18277
+ rethinkAtAttempt: 2,
18278
+ urgencyAtAttempt: 3
18279
+ },
18280
+ regressionGate: {
18281
+ enabled: true,
18282
+ timeoutSeconds: 300,
18283
+ acceptOnTimeout: true,
18284
+ mode: "deferred",
18285
+ maxRectificationAttempts: 3
18286
+ },
18287
+ contextProviderTokenBudget: 2000,
18288
+ lintCommand: null,
18289
+ typecheckCommand: null,
18290
+ dangerouslySkipPermissions: true,
18291
+ permissionProfile: "unrestricted",
18292
+ smartTestRunner: true
18293
+ }),
18294
+ quality: QualityConfigSchema.default({
18295
+ requireTypecheck: true,
18296
+ requireLint: true,
18297
+ requireTests: true,
18298
+ commands: {},
18299
+ autofix: {
18300
+ enabled: true,
18301
+ maxAttempts: 3,
18302
+ maxTotalAttempts: 12,
18303
+ rethinkAtAttempt: 2,
18304
+ urgencyAtAttempt: 3
18305
+ },
18306
+ forceExit: false,
18307
+ detectOpenHandles: true,
18308
+ detectOpenHandlesRetries: 1,
18309
+ gracePeriodMs: 5000,
18310
+ drainTimeoutMs: 2000,
18311
+ shell: "/bin/sh",
18312
+ stripEnvVars: [
18313
+ "CLAUDECODE",
18314
+ "REPL_ID",
18315
+ "AGENT",
18316
+ "GITLAB_ACCESS_TOKEN",
18317
+ "GITHUB_TOKEN",
18318
+ "GITHUB_ACCESS_TOKEN",
18319
+ "GH_TOKEN",
18320
+ "CI_GIT_TOKEN",
18321
+ "CI_JOB_TOKEN",
18322
+ "BITBUCKET_ACCESS_TOKEN",
18323
+ "NPM_TOKEN",
18324
+ "NPM_AUTH_TOKEN",
18325
+ "YARN_NPM_AUTH_TOKEN",
18326
+ "ANTHROPIC_API_KEY",
18327
+ "OPENAI_API_KEY",
18328
+ "GEMINI_API_KEY",
18329
+ "COHERE_API_KEY",
18330
+ "AWS_ACCESS_KEY_ID",
18331
+ "AWS_SECRET_ACCESS_KEY",
18332
+ "AWS_SESSION_TOKEN",
18333
+ "GOOGLE_APPLICATION_CREDENTIALS",
18334
+ "GCLOUD_SERVICE_KEY",
18335
+ "AZURE_CLIENT_SECRET",
18336
+ "AZURE_TENANT_ID",
18337
+ "TELEGRAM_BOT_TOKEN",
18338
+ "SLACK_TOKEN",
18339
+ "SLACK_WEBHOOK_URL",
18340
+ "SENTRY_AUTH_TOKEN",
18341
+ "DATADOG_API_KEY"
18342
+ ],
18343
+ testing: {
18344
+ hermetic: true
18345
+ }
18346
+ }),
18347
+ tdd: TddConfigSchema.default({
18348
+ maxRetries: 2,
18349
+ autoVerifyIsolation: true,
18350
+ autoApproveVerifier: true,
18351
+ strategy: "auto",
18352
+ sessionTiers: {
18353
+ testWriter: "balanced",
18354
+ verifier: "fast"
18355
+ },
18356
+ testWriterAllowedPaths: ["src/index.ts", "src/**/index.ts"],
18357
+ rollbackOnFailure: true,
18358
+ greenfieldDetection: true
18359
+ }),
18360
+ constitution: ConstitutionConfigSchema.default({
18361
+ enabled: true,
18362
+ path: "constitution.md",
18363
+ maxTokens: 2000
18364
+ }),
18365
+ analyze: AnalyzeConfigSchema.default({
18366
+ llmEnhanced: true,
18367
+ model: "balanced",
18368
+ fallbackToKeywords: true,
18369
+ maxCodebaseSummaryTokens: 5000
18370
+ }),
18371
+ review: ReviewConfigSchema.default({
18372
+ enabled: true,
18373
+ checks: ["typecheck", "lint"],
18374
+ commands: {},
18375
+ pluginMode: "per-story",
18376
+ semantic: {
18377
+ modelTier: "balanced",
18378
+ rules: [],
18379
+ timeoutMs: 600000,
18380
+ excludePatterns: [":!test/", ":!tests/", ":!*_test.go", ":!*.test.ts", ":!*.spec.ts", ":!**/__tests__/"]
18381
+ }
18382
+ }),
18383
+ plan: PlanConfigSchema.default({
18384
+ model: "balanced",
18385
+ outputPath: "spec.md",
18386
+ timeoutSeconds: 600
18387
+ }),
18388
+ acceptance: AcceptanceConfigSchema.default({
18389
+ enabled: true,
18390
+ maxRetries: 2,
18391
+ generateTests: true,
18392
+ testPath: ".nax-acceptance.test.ts",
18393
+ model: "fast",
18394
+ refinement: true,
18395
+ redGate: true,
18396
+ timeoutMs: 1800000,
18397
+ fix: {
18398
+ diagnoseModel: "fast",
18399
+ fixModel: "balanced",
18400
+ strategy: "diagnose-first",
18401
+ maxRetries: 2
18402
+ }
18403
+ }),
18404
+ context: ContextConfigSchema.default({
18405
+ fileInjection: "disabled",
18406
+ testCoverage: {
18407
+ enabled: true,
18408
+ detail: "names-and-counts",
18409
+ maxTokens: 500,
18410
+ testPattern: "**/*.test.{ts,js,tsx,jsx}",
18411
+ scopeToStory: true
18412
+ },
18413
+ autoDetect: {
18414
+ enabled: true,
18415
+ maxFiles: 5,
18416
+ traceImports: false
18417
+ }
18418
+ }),
18474
18419
  optimizer: OptimizerConfigSchema.optional(),
18475
18420
  plugins: exports_external.array(PluginConfigEntrySchema).optional(),
18476
18421
  disabledPlugins: exports_external.array(exports_external.string()).optional(),
18477
18422
  hooks: HooksConfigSchema.optional(),
18478
- interaction: InteractionConfigSchema.optional(),
18479
- agent: AgentConfigSchema.optional(),
18480
- precheck: PrecheckConfigSchema.optional(),
18423
+ interaction: InteractionConfigSchema.optional().default({
18424
+ plugin: "cli",
18425
+ config: {},
18426
+ defaults: {
18427
+ timeout: 600000,
18428
+ fallback: "escalate"
18429
+ },
18430
+ triggers: {
18431
+ "security-review": true,
18432
+ "cost-warning": true
18433
+ }
18434
+ }),
18435
+ agent: AgentConfigSchema.optional().default({
18436
+ protocol: "acp",
18437
+ maxInteractionTurns: 10
18438
+ }),
18439
+ precheck: PrecheckConfigSchema.optional().default({
18440
+ storySizeGate: {
18441
+ enabled: true,
18442
+ maxAcCount: 10,
18443
+ maxDescriptionLength: 3000,
18444
+ maxBulletPoints: 12,
18445
+ action: "block",
18446
+ maxReplanAttempts: 3
18447
+ }
18448
+ }),
18481
18449
  prompts: PromptsConfigSchema.optional(),
18482
18450
  generate: GenerateConfigSchema.optional(),
18483
18451
  project: ProjectProfileSchema.optional(),
18484
- debate: DebateConfigSchema.optional().default(() => DEFAULT_CONFIG.debate)
18452
+ debate: DebateConfigSchema.optional().default(() => ({
18453
+ enabled: false,
18454
+ agents: 3,
18455
+ maxConcurrentDebaters: 2,
18456
+ stages: {
18457
+ plan: {
18458
+ enabled: true,
18459
+ resolver: { type: "synthesis" },
18460
+ sessionMode: "stateful",
18461
+ rounds: 3,
18462
+ timeoutSeconds: 600
18463
+ },
18464
+ review: {
18465
+ enabled: true,
18466
+ resolver: { type: "majority-fail-closed" },
18467
+ sessionMode: "one-shot",
18468
+ rounds: 2,
18469
+ timeoutSeconds: 600
18470
+ },
18471
+ acceptance: {
18472
+ enabled: false,
18473
+ resolver: { type: "majority-fail-closed" },
18474
+ sessionMode: "one-shot",
18475
+ rounds: 1,
18476
+ timeoutSeconds: 600
18477
+ },
18478
+ rectification: {
18479
+ enabled: false,
18480
+ resolver: { type: "synthesis" },
18481
+ sessionMode: "one-shot",
18482
+ rounds: 1,
18483
+ timeoutSeconds: 600
18484
+ },
18485
+ escalation: {
18486
+ enabled: false,
18487
+ resolver: { type: "majority-fail-closed" },
18488
+ sessionMode: "one-shot",
18489
+ rounds: 1,
18490
+ timeoutSeconds: 600
18491
+ }
18492
+ }
18493
+ })),
18494
+ profile: exports_external.string().default("default")
18485
18495
  }).refine((data) => data.version === 1, {
18486
18496
  message: "Invalid version: expected 1",
18487
18497
  path: ["version"]
18488
18498
  });
18489
18499
  });
18490
18500
 
18501
+ // src/config/defaults.ts
18502
+ var DEFAULT_CONFIG;
18503
+ var init_defaults = __esm(() => {
18504
+ init_schemas3();
18505
+ DEFAULT_CONFIG = NaxConfigSchema.parse({});
18506
+ });
18507
+
18491
18508
  // src/config/schema.ts
18492
18509
  var exports_schema = {};
18493
18510
  __export(exports_schema, {
@@ -19846,9 +19863,9 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
19846
19863
  modelDef,
19847
19864
  prompt: "",
19848
19865
  modelTier: options.modelTier || "balanced",
19849
- timeoutSeconds: 600
19866
+ timeoutSeconds: options.timeoutSeconds ?? 600
19850
19867
  };
19851
- const PLAN_TIMEOUT_MS = 600000;
19868
+ const planTimeoutMs = (options.timeoutSeconds ?? 600) * 1000;
19852
19869
  if (options.interactive) {
19853
19870
  const proc = Bun.spawn(cmd, {
19854
19871
  cwd: options.workdir,
@@ -19860,7 +19877,7 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
19860
19877
  await pidRegistry.register(proc.pid);
19861
19878
  let exitCode;
19862
19879
  try {
19863
- const timeoutResult = await withProcessTimeout(proc, PLAN_TIMEOUT_MS, {
19880
+ const timeoutResult = await withProcessTimeout(proc, planTimeoutMs, {
19864
19881
  graceMs: 5000
19865
19882
  });
19866
19883
  exitCode = timeoutResult.exitCode;
@@ -19886,7 +19903,7 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
19886
19903
  await pidRegistry.register(proc.pid);
19887
19904
  let exitCode;
19888
19905
  try {
19889
- const timeoutResult = await withProcessTimeout(proc, PLAN_TIMEOUT_MS, {
19906
+ const timeoutResult = await withProcessTimeout(proc, planTimeoutMs, {
19890
19907
  graceMs: 5000
19891
19908
  });
19892
19909
  exitCode = timeoutResult.exitCode;
@@ -20680,26 +20697,179 @@ var init_path_security = () => {};
20680
20697
  import { homedir as homedir2 } from "os";
20681
20698
  import { join as join3, resolve as resolve2 } from "path";
20682
20699
  function globalConfigDir() {
20700
+ const override = process.env[GLOBAL_CONFIG_DIR_ENV];
20701
+ if (override)
20702
+ return override;
20683
20703
  return join3(homedir2(), ".nax");
20684
20704
  }
20685
- var PROJECT_NAX_DIR = ".nax";
20705
+ function projectConfigDir(projectRoot) {
20706
+ return join3(resolve2(projectRoot), PROJECT_NAX_DIR);
20707
+ }
20708
+ var GLOBAL_CONFIG_DIR_ENV = "NAX_GLOBAL_CONFIG_DIR", PROJECT_NAX_DIR = ".nax";
20686
20709
  var init_paths = () => {};
20687
20710
 
20711
+ // src/config/dotenv.ts
20712
+ function parseDotenv(content) {
20713
+ if (!content)
20714
+ return {};
20715
+ const result = {};
20716
+ for (const rawLine of content.split(`
20717
+ `)) {
20718
+ const line = rawLine.trim();
20719
+ if (!line || line.startsWith("#"))
20720
+ continue;
20721
+ const stripped = line.startsWith("export ") ? line.slice(7).trim() : line;
20722
+ const eqIndex = stripped.indexOf("=");
20723
+ if (eqIndex === -1)
20724
+ continue;
20725
+ const key = stripped.slice(0, eqIndex).trim();
20726
+ let value = stripped.slice(eqIndex + 1).trim();
20727
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
20728
+ value = value.slice(1, -1);
20729
+ }
20730
+ result[key] = value;
20731
+ }
20732
+ return result;
20733
+ }
20734
+ function resolveEnvVars(config2, env2) {
20735
+ if (typeof config2 === "string") {
20736
+ return resolveString(config2, env2);
20737
+ }
20738
+ if (Array.isArray(config2)) {
20739
+ return config2.map((item) => resolveEnvVars(item, env2));
20740
+ }
20741
+ if (config2 !== null && typeof config2 === "object") {
20742
+ const result = {};
20743
+ for (const [key, value] of Object.entries(config2)) {
20744
+ result[key] = resolveEnvVars(value, env2);
20745
+ }
20746
+ return result;
20747
+ }
20748
+ return config2;
20749
+ }
20750
+ function resolveString(str, env2) {
20751
+ return str.replace(/\$\$([A-Za-z_][A-Za-z0-9_]*)/g, `${DOUBLE_DOLLAR_PLACEHOLDER}$1`).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, varName) => {
20752
+ if (!(varName in env2)) {
20753
+ throw new Error(`Environment variable $${varName} (${varName}) is not defined`);
20754
+ }
20755
+ return env2[varName];
20756
+ }).replace(new RegExp(`${DOUBLE_DOLLAR_PLACEHOLDER}([A-Za-z_][A-Za-z0-9_]*)`, "g"), "$$$1");
20757
+ }
20758
+ var DOUBLE_DOLLAR_PLACEHOLDER = "__DOLLAR_ESCAPE__";
20759
+
20760
+ // src/config/profile.ts
20761
+ import { readdirSync } from "fs";
20762
+ import { join as join4 } from "path";
20763
+ async function loadProfile(profileName, projectRoot) {
20764
+ const globalPath = join4(globalConfigDir(), "profiles", `${profileName}.json`);
20765
+ const projectPath = join4(projectConfigDir(projectRoot), "profiles", `${profileName}.json`);
20766
+ const globalFile = Bun.file(globalPath);
20767
+ const projectFile = Bun.file(projectPath);
20768
+ const [globalExists, projectExists] = await Promise.all([globalFile.exists(), projectFile.exists()]);
20769
+ if (!globalExists && !projectExists) {
20770
+ const available = await listAvailableProfileNames(projectRoot);
20771
+ const availableList = available.length > 0 ? available.join(", ") : "(none)";
20772
+ throw new Error(`Profile "${profileName}" not found. Available: ${availableList}`);
20773
+ }
20774
+ let base = {};
20775
+ if (globalExists) {
20776
+ base = await globalFile.json();
20777
+ }
20778
+ if (projectExists) {
20779
+ const projectData = await projectFile.json();
20780
+ base = deepMergeConfig(base, projectData);
20781
+ }
20782
+ return base;
20783
+ }
20784
+ async function loadProfileEnv(profileName, projectRoot) {
20785
+ const globalPath = join4(globalConfigDir(), "profiles", `${profileName}.env`);
20786
+ const projectPath = join4(projectConfigDir(projectRoot), "profiles", `${profileName}.env`);
20787
+ const globalFile = Bun.file(globalPath);
20788
+ const projectFile = Bun.file(projectPath);
20789
+ const [globalExists, projectExists] = await Promise.all([globalFile.exists(), projectFile.exists()]);
20790
+ if (!globalExists && !projectExists) {
20791
+ return {};
20792
+ }
20793
+ let merged = {};
20794
+ if (globalExists) {
20795
+ const globalContent = await globalFile.text();
20796
+ merged = { ...merged, ...parseDotenv(globalContent) };
20797
+ }
20798
+ if (projectExists) {
20799
+ const projectContent = await projectFile.text();
20800
+ merged = { ...merged, ...parseDotenv(projectContent) };
20801
+ }
20802
+ return merged;
20803
+ }
20804
+ async function resolveProfileName(cliOptions, env2, projectRoot) {
20805
+ if (cliOptions.profile) {
20806
+ return cliOptions.profile;
20807
+ }
20808
+ if (env2.NAX_PROFILE) {
20809
+ return env2.NAX_PROFILE;
20810
+ }
20811
+ const projectConfigPath = join4(projectConfigDir(projectRoot), "config.json");
20812
+ const projectConfigFile = Bun.file(projectConfigPath);
20813
+ if (await projectConfigFile.exists()) {
20814
+ const config2 = await projectConfigFile.json();
20815
+ if (typeof config2.profile === "string" && config2.profile && config2.profile !== "default") {
20816
+ return config2.profile;
20817
+ }
20818
+ }
20819
+ const globalConfigPath = join4(globalConfigDir(), "config.json");
20820
+ const globalConfigFile = Bun.file(globalConfigPath);
20821
+ if (await globalConfigFile.exists()) {
20822
+ const config2 = await globalConfigFile.json();
20823
+ if (typeof config2.profile === "string" && config2.profile && config2.profile !== "default") {
20824
+ return config2.profile;
20825
+ }
20826
+ }
20827
+ return "default";
20828
+ }
20829
+ async function listAvailableProfileNames(projectRoot) {
20830
+ const entries = await listProfiles(projectRoot);
20831
+ const names = [...new Set(entries.map((e) => e.name))].sort();
20832
+ return names;
20833
+ }
20834
+ async function listProfiles(projectRoot) {
20835
+ const globalProfilesDir = join4(globalConfigDir(), "profiles");
20836
+ const projectProfilesDir = join4(projectConfigDir(projectRoot), "profiles");
20837
+ const entries = [];
20838
+ for (const dir of [globalProfilesDir, projectProfilesDir]) {
20839
+ let files;
20840
+ try {
20841
+ files = readdirSync(dir);
20842
+ } catch {
20843
+ continue;
20844
+ }
20845
+ for (const file3 of files) {
20846
+ if (file3.endsWith(".json")) {
20847
+ const name = file3.replace(/\.json$/, "");
20848
+ entries.push({ name, path: join4(dir, file3) });
20849
+ }
20850
+ }
20851
+ }
20852
+ return entries;
20853
+ }
20854
+ var init_profile = __esm(() => {
20855
+ init_paths();
20856
+ });
20857
+
20688
20858
  // src/config/loader.ts
20689
20859
  import { existsSync as existsSync4 } from "fs";
20690
- import { basename, dirname, join as join4, resolve as resolve3 } from "path";
20860
+ import { basename, dirname, join as join5, resolve as resolve3 } from "path";
20691
20861
  function globalConfigPath() {
20692
- return join4(globalConfigDir(), "config.json");
20862
+ return join5(globalConfigDir(), "config.json");
20693
20863
  }
20694
20864
  function findProjectDir(startDir = process.cwd()) {
20695
20865
  let dir = resolve3(startDir);
20696
20866
  let depth = 0;
20697
20867
  while (depth < MAX_DIRECTORY_DEPTH) {
20698
- const candidate = join4(dir, PROJECT_NAX_DIR);
20699
- if (existsSync4(join4(candidate, "config.json"))) {
20868
+ const candidate = join5(dir, PROJECT_NAX_DIR);
20869
+ if (existsSync4(join5(candidate, "config.json"))) {
20700
20870
  return candidate;
20701
20871
  }
20702
- const parent = join4(dir, "..");
20872
+ const parent = join5(dir, "..");
20703
20873
  if (parent === dir)
20704
20874
  break;
20705
20875
  dir = parent;
@@ -20742,22 +20912,36 @@ function applyBatchModeCompat(conf) {
20742
20912
  }
20743
20913
  async function loadConfig(startDir, cliOverrides) {
20744
20914
  let rawConfig = structuredClone(DEFAULT_CONFIG);
20915
+ const projDir = startDir ? basename(startDir) === PROJECT_NAX_DIR ? startDir : findProjectDir(startDir) : findProjectDir();
20916
+ const projectRoot = startDir ? basename(startDir) === PROJECT_NAX_DIR ? dirname(startDir) : startDir : process.cwd();
20917
+ const profileName = await resolveProfileName(cliOverrides ?? {}, process.env, projectRoot);
20918
+ if (profileName !== "default") {
20919
+ const profileData = await loadProfile(profileName, projectRoot);
20920
+ rawConfig = deepMergeConfig(rawConfig, profileData);
20921
+ await loadProfileEnv(profileName, projectRoot);
20922
+ }
20745
20923
  const globalConfRaw = await loadJsonFile(globalConfigPath(), "config");
20746
20924
  if (globalConfRaw) {
20747
- const globalConf = applyBatchModeCompat(applyRemovedStrategyCompat(globalConfRaw));
20925
+ const { profile: _gProfile, ...globalConfStripped } = globalConfRaw;
20926
+ const globalConf = applyBatchModeCompat(applyRemovedStrategyCompat(globalConfStripped));
20748
20927
  rawConfig = deepMergeConfig(rawConfig, globalConf);
20749
20928
  }
20750
- const projDir = startDir ? basename(startDir) === PROJECT_NAX_DIR ? startDir : findProjectDir(startDir) : findProjectDir();
20751
20929
  if (projDir) {
20752
- const projConf = await loadJsonFile(join4(projDir, "config.json"), "config");
20930
+ const projConf = await loadJsonFile(join5(projDir, "config.json"), "config");
20753
20931
  if (projConf) {
20754
- const resolvedProjConf = applyBatchModeCompat(applyRemovedStrategyCompat(projConf));
20932
+ const { profile: _pProfile, ...projConfStripped } = projConf;
20933
+ const resolvedProjConf = applyBatchModeCompat(applyRemovedStrategyCompat(projConfStripped));
20755
20934
  rawConfig = deepMergeConfig(rawConfig, resolvedProjConf);
20756
20935
  }
20757
20936
  }
20758
20937
  if (cliOverrides) {
20759
20938
  rawConfig = deepMergeConfig(rawConfig, cliOverrides);
20760
20939
  }
20940
+ rawConfig.profile = profileName;
20941
+ const hasMergedConfigs = globalConfRaw || projDir !== null || cliOverrides !== undefined || profileName !== "default";
20942
+ if (!hasMergedConfigs) {
20943
+ return structuredClone(DEFAULT_CONFIG);
20944
+ }
20761
20945
  const result = NaxConfigSchema.safeParse(rawConfig);
20762
20946
  if (!result.success) {
20763
20947
  const errors3 = result.error.issues.map((err) => {
@@ -20779,7 +20963,7 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir) {
20779
20963
  return rootConfig;
20780
20964
  }
20781
20965
  const repoRoot = dirname(rootNaxDir);
20782
- const packageConfigPath = join4(repoRoot, PROJECT_NAX_DIR, "mono", packageDir, "config.json");
20966
+ const packageConfigPath = join5(repoRoot, PROJECT_NAX_DIR, "mono", packageDir, "config.json");
20783
20967
  const packageOverride = await loadJsonFile(packageConfigPath, "config");
20784
20968
  if (!packageOverride) {
20785
20969
  logger.debug("config", "Per-package config not found \u2014 falling back to root config", {
@@ -20796,6 +20980,7 @@ var init_loader = __esm(() => {
20796
20980
  init_json_file();
20797
20981
  init_path_security();
20798
20982
  init_paths();
20983
+ init_profile();
20799
20984
  init_schema();
20800
20985
  });
20801
20986
  // src/config/index.ts
@@ -20804,6 +20989,7 @@ var init_config = __esm(() => {
20804
20989
  init_loader();
20805
20990
  init_path_security();
20806
20991
  init_paths();
20992
+ init_profile();
20807
20993
  });
20808
20994
 
20809
20995
  // src/utils/errors.ts
@@ -20989,7 +21175,7 @@ __export(exports_generator, {
20989
21175
  acceptanceTestFilename: () => acceptanceTestFilename,
20990
21176
  _generatorPRDDeps: () => _generatorPRDDeps
20991
21177
  });
20992
- import { join as join5 } from "path";
21178
+ import { join as join6 } from "path";
20993
21179
  function skeletonImportLine(testFramework) {
20994
21180
  if (!testFramework)
20995
21181
  return `import { describe, test, expect } from "bun:test";`;
@@ -21085,7 +21271,7 @@ Rules:
21085
21271
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
21086
21272
  - **Prefer behavioral tests** \u2014 import functions and call them rather than reading source files. For example, to verify "getPostRunActions() returns empty array", import PluginRegistry and call getPostRunActions(), don't grep the source file for the method name.
21087
21273
  - **File output (REQUIRED)**: Write the acceptance test file DIRECTLY to the path shown below. Do NOT output the test code in your response. After writing the file, reply with a brief confirmation.
21088
- - **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join5(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).`;
21274
+ - **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join6(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).`;
21089
21275
  const prompt = basePrompt;
21090
21276
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
21091
21277
  const completeResult = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
@@ -21104,7 +21290,7 @@ Rules:
21104
21290
  outputPreview: rawOutput.slice(0, 300)
21105
21291
  });
21106
21292
  if (!testCode) {
21107
- const targetPath = join5(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath));
21293
+ const targetPath = join6(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath));
21108
21294
  let recoveryFailed = false;
21109
21295
  logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", { targetPath });
21110
21296
  try {
@@ -21159,7 +21345,7 @@ Rules:
21159
21345
  testable: c.testable,
21160
21346
  storyId: c.storyId
21161
21347
  })), null, 2);
21162
- await _generatorPRDDeps.writeFile(join5(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
21348
+ await _generatorPRDDeps.writeFile(join6(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
21163
21349
  return { testCode, criteria };
21164
21350
  }
21165
21351
  function parseAcceptanceCriteria(specContent) {
@@ -22156,7 +22342,7 @@ var package_default;
22156
22342
  var init_package = __esm(() => {
22157
22343
  package_default = {
22158
22344
  name: "@nathapp/nax",
22159
- version: "0.57.0",
22345
+ version: "0.57.1",
22160
22346
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22161
22347
  type: "module",
22162
22348
  bin: {
@@ -22235,8 +22421,8 @@ var init_version = __esm(() => {
22235
22421
  NAX_VERSION = package_default.version;
22236
22422
  NAX_COMMIT = (() => {
22237
22423
  try {
22238
- if (/^[0-9a-f]{6,10}$/.test("478df448"))
22239
- return "478df448";
22424
+ if (/^[0-9a-f]{6,10}$/.test("39861723"))
22425
+ return "39861723";
22240
22426
  } catch {}
22241
22427
  try {
22242
22428
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22383,6 +22569,27 @@ var init_prd = __esm(() => {
22383
22569
  PRD_MAX_FILE_SIZE = 5 * 1024 * 1024;
22384
22570
  });
22385
22571
 
22572
+ // src/debate/concurrency.ts
22573
+ async function allSettledBounded(tasks, limit) {
22574
+ if (tasks.length === 0)
22575
+ return [];
22576
+ const results = new Array(tasks.length);
22577
+ let nextIndex = 0;
22578
+ async function worker() {
22579
+ while (nextIndex < tasks.length) {
22580
+ const i = nextIndex++;
22581
+ try {
22582
+ results[i] = { status: "fulfilled", value: await tasks[i]() };
22583
+ } catch (reason) {
22584
+ results[i] = { status: "rejected", reason };
22585
+ }
22586
+ }
22587
+ }
22588
+ const concurrency = Math.max(1, Math.min(limit, tasks.length));
22589
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
22590
+ return results;
22591
+ }
22592
+
22386
22593
  // src/debate/prompts.ts
22387
22594
  function buildCritiquePrompt(taskPrompt, allProposals, debaterIndex) {
22388
22595
  const othersProposals = allProposals.filter((_, i) => i !== debaterIndex);
@@ -22489,7 +22696,7 @@ var DEFAULT_FALLBACK_AGENT = "claude";
22489
22696
  var init_resolvers = () => {};
22490
22697
 
22491
22698
  // src/debate/session.ts
22492
- import { join as join11 } from "path";
22699
+ import { join as join12 } from "path";
22493
22700
  function resolveDebaterModel(debater, config2) {
22494
22701
  const tier = debater.model ?? "fast";
22495
22702
  if (!config2?.models)
@@ -22523,10 +22730,11 @@ function modelTierFromDebater(debater) {
22523
22730
  function isTierLabel(value) {
22524
22731
  return value === "fast" || value === "balanced" || value === "powerful";
22525
22732
  }
22526
- async function runComplete(adapter, prompt, options, modelTier) {
22733
+ async function runComplete(adapter, prompt, options, modelTier, timeoutMs) {
22527
22734
  return adapter.complete(prompt, {
22528
22735
  ...options,
22529
- modelTier
22736
+ modelTier,
22737
+ ...timeoutMs !== undefined && { timeoutMs }
22530
22738
  });
22531
22739
  }
22532
22740
 
@@ -22538,6 +22746,9 @@ class DebateSession {
22538
22746
  workdir;
22539
22747
  featureName;
22540
22748
  timeoutSeconds;
22749
+ get timeoutMs() {
22750
+ return this.timeoutSeconds * 1000;
22751
+ }
22541
22752
  constructor(opts) {
22542
22753
  this.storyId = opts.storyId;
22543
22754
  this.stage = opts.stage;
@@ -22545,7 +22756,7 @@ class DebateSession {
22545
22756
  this.config = opts.config;
22546
22757
  this.workdir = opts.workdir ?? process.cwd();
22547
22758
  this.featureName = opts.featureName ?? opts.stage;
22548
- this.timeoutSeconds = opts.timeoutSeconds ?? opts.config?.execution?.sessionTimeoutSeconds ?? 600;
22759
+ this.timeoutSeconds = opts.timeoutSeconds ?? opts.stageConfig.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
22549
22760
  }
22550
22761
  pipelineStageForDebate() {
22551
22762
  switch (this.stage) {
@@ -22650,7 +22861,10 @@ class DebateSession {
22650
22861
  stage: this.stage,
22651
22862
  debaters: resolved.map((r) => r.debater.agent)
22652
22863
  });
22653
- const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }, debaterIdx) => this.runStatefulTurn(adapter, debater, prompt, `debate-${this.stage}-${debaterIdx}`, config2.rounds > 1)));
22864
+ const cfg = this.config;
22865
+ const debate = cfg?.debate;
22866
+ const concurrencyLimit = debate?.maxConcurrentDebaters ?? 2;
22867
+ const proposalSettled = await allSettledBounded(resolved.map(({ debater, adapter }, debaterIdx) => () => this.runStatefulTurn(adapter, debater, prompt, `debate-${this.stage}-${debaterIdx}`, config2.rounds > 1)), concurrencyLimit);
22654
22868
  const successfulProposals = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
22655
22869
  for (const r of proposalSettled) {
22656
22870
  if (r.status === "fulfilled") {
@@ -22730,7 +22944,7 @@ class DebateSession {
22730
22944
  let critiqueOutputs = [];
22731
22945
  if (config2.rounds > 1) {
22732
22946
  const proposalOutputs2 = successfulProposals.map((s) => s.output);
22733
- const critiqueSettled = await Promise.allSettled(successfulProposals.map((proposal, successfulIdx) => this.runStatefulTurn(proposal.adapter, proposal.debater, buildCritiquePrompt(prompt, proposalOutputs2, successfulIdx), proposal.roleKey ?? `debate-${this.stage}-${successfulIdx}`, false)));
22947
+ const critiqueSettled = await allSettledBounded(successfulProposals.map((proposal, successfulIdx) => () => this.runStatefulTurn(proposal.adapter, proposal.debater, buildCritiquePrompt(prompt, proposalOutputs2, successfulIdx), proposal.roleKey ?? `debate-${this.stage}-${successfulIdx}`, false)), concurrencyLimit);
22734
22948
  for (const r of critiqueSettled) {
22735
22949
  if (r.status === "fulfilled") {
22736
22950
  totalCostUsd += r.value.cost;
@@ -22780,13 +22994,17 @@ class DebateSession {
22780
22994
  stage: this.stage,
22781
22995
  debaters: resolved.map((r) => r.debater.agent)
22782
22996
  });
22783
- const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => runComplete(adapter, prompt, {
22997
+ const cfg = this.config;
22998
+ const debate = cfg?.debate;
22999
+ const concurrencyLimit = debate?.maxConcurrentDebaters ?? 2;
23000
+ const proposalSettled = await allSettledBounded(resolved.map(({ debater, adapter }, i) => () => runComplete(adapter, prompt, {
22784
23001
  model: resolveDebaterModel(debater, this.config),
22785
23002
  featureName: this.stage,
22786
23003
  config: this.config,
22787
23004
  storyId: this.storyId,
22788
- sessionRole: "debate-proposal"
22789
- }, modelTierFromDebater(debater)).then((result) => ({ debater, adapter, output: result.output, cost: result.costUsd }))));
23005
+ sessionRole: `debate-proposal-${i}`,
23006
+ timeoutMs: this.timeoutMs
23007
+ }, modelTierFromDebater(debater)).then((result) => ({ debater, adapter, output: result.output, cost: result.costUsd }))), concurrencyLimit);
22790
23008
  const successful = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
22791
23009
  for (const r of proposalSettled) {
22792
23010
  if (r.status === "fulfilled") {
@@ -22839,7 +23057,8 @@ class DebateSession {
22839
23057
  featureName: this.stage,
22840
23058
  config: this.config,
22841
23059
  storyId: this.storyId,
22842
- sessionRole: "debate-fallback"
23060
+ sessionRole: "debate-fallback",
23061
+ timeoutMs: this.timeoutMs
22843
23062
  }, modelTierFromDebater(fallbackDebater));
22844
23063
  totalCostUsd += fallbackResult.costUsd;
22845
23064
  logger?.info("debate", "debate:result", {
@@ -22864,13 +23083,14 @@ class DebateSession {
22864
23083
  let critiqueOutputs = [];
22865
23084
  if (config2.rounds > 1) {
22866
23085
  const proposalOutputs2 = successful.map((p) => p.output);
22867
- const critiqueSettled = await Promise.allSettled(successful.map(({ debater, adapter }, i) => runComplete(adapter, buildCritiquePrompt(prompt, proposalOutputs2, i), {
23086
+ const critiqueSettled = await allSettledBounded(successful.map(({ debater, adapter }, i) => () => runComplete(adapter, buildCritiquePrompt(prompt, proposalOutputs2, i), {
22868
23087
  model: resolveDebaterModel(debater, this.config),
22869
23088
  featureName: this.stage,
22870
23089
  config: this.config,
22871
23090
  storyId: this.storyId,
22872
- sessionRole: "debate-critique"
22873
- }, modelTierFromDebater(debater))));
23091
+ sessionRole: `debate-critique-${i}`,
23092
+ timeoutMs: this.timeoutMs
23093
+ }, modelTierFromDebater(debater))), concurrencyLimit);
22874
23094
  for (const r of critiqueSettled) {
22875
23095
  if (r.status === "fulfilled") {
22876
23096
  totalCostUsd += r.value.costUsd;
@@ -22920,8 +23140,11 @@ class DebateSession {
22920
23140
  stage: this.stage,
22921
23141
  debaters: resolved.map((r) => r.debater.agent)
22922
23142
  });
22923
- const planSettled = await Promise.allSettled(resolved.map(async ({ debater, adapter }, i) => {
22924
- const tempOutputPath = join11(opts.outputDir, `prd-debate-${i}.json`);
23143
+ const cfg = this.config;
23144
+ const debate = cfg?.debate;
23145
+ const concurrencyLimit = debate?.maxConcurrentDebaters ?? 2;
23146
+ const settled = await allSettledBounded(resolved.map(({ debater, adapter }, i) => async () => {
23147
+ const tempOutputPath = join12(opts.outputDir, `prd-debate-${i}.json`);
22925
23148
  const debaterPrompt = `${basePrompt}
22926
23149
 
22927
23150
  Write the PRD JSON directly to this file path: ${tempOutputPath}
@@ -22936,12 +23159,28 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22936
23159
  dangerouslySkipPermissions: opts.dangerouslySkipPermissions,
22937
23160
  maxInteractionTurns: opts.maxInteractionTurns,
22938
23161
  featureName: opts.feature,
22939
- sessionRole: "plan"
23162
+ storyId: this.storyId,
23163
+ sessionRole: `plan-${i}`
22940
23164
  });
22941
23165
  const output = await _debateSessionDeps.readFile(tempOutputPath);
22942
23166
  return { debater, adapter, output, cost: 0 };
22943
- }));
22944
- const successful = planSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
23167
+ }), concurrencyLimit);
23168
+ const successful = [];
23169
+ for (let i = 0;i < settled.length; i++) {
23170
+ const res = settled[i];
23171
+ if (res.status === "fulfilled") {
23172
+ successful.push(res.value);
23173
+ } else {
23174
+ const { debater } = resolved[i];
23175
+ logger?.warn("debate", "debate:debater-failed", {
23176
+ storyId: this.storyId,
23177
+ stage: this.stage,
23178
+ debaterIndex: i,
23179
+ agent: debater.agent,
23180
+ error: res.reason instanceof Error ? res.reason.message : String(res.reason)
23181
+ });
23182
+ }
23183
+ }
22945
23184
  for (let i = 0;i < successful.length; i++) {
22946
23185
  logger?.info("debate", "debate:proposal", {
22947
23186
  storyId: this.storyId,
@@ -23012,7 +23251,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
23012
23251
  model: resolveDebaterModel({ agent: agentName }, this.config),
23013
23252
  config: this.config,
23014
23253
  storyId: this.storyId,
23015
- sessionRole: "synthesis"
23254
+ sessionRole: "synthesis",
23255
+ timeoutMs: this.timeoutMs
23016
23256
  }
23017
23257
  });
23018
23258
  return {
@@ -23034,7 +23274,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
23034
23274
  model: resolveDebaterModel({ agent: agentName }, this.config),
23035
23275
  config: this.config,
23036
23276
  storyId: this.storyId,
23037
- sessionRole: "judge"
23277
+ sessionRole: "judge",
23278
+ timeoutMs: this.timeoutMs
23038
23279
  }
23039
23280
  });
23040
23281
  return {
@@ -23048,7 +23289,7 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
23048
23289
  };
23049
23290
  }
23050
23291
  }
23051
- var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps;
23292
+ var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps, DEFAULT_TIMEOUT_SECONDS = 600;
23052
23293
  var init_session = __esm(() => {
23053
23294
  init_registry();
23054
23295
  init_config();
@@ -23252,13 +23493,15 @@ class AutoInteractionPlugin {
23252
23493
  const modelDef = resolveModelForAgent(naxConfig.models, naxConfig.autoMode.defaultAgent, modelTier, naxConfig.autoMode.defaultAgent);
23253
23494
  modelArg = modelDef.model;
23254
23495
  }
23496
+ const timeoutMs = this.config.naxConfig ? (this.config.naxConfig.execution?.sessionTimeoutSeconds ?? 600) * 1000 : undefined;
23255
23497
  const result = await adapter.complete(prompt, {
23256
23498
  ...modelArg && { model: modelArg },
23257
23499
  jsonMode: true,
23258
23500
  ...this.config.naxConfig && { config: this.config.naxConfig },
23259
23501
  featureName: request.featureName,
23260
23502
  storyId: request.storyId,
23261
- sessionRole: "auto"
23503
+ sessionRole: "auto",
23504
+ ...timeoutMs !== undefined && { timeoutMs }
23262
23505
  });
23263
23506
  const output = typeof result === "string" ? result : result.output;
23264
23507
  return this.parseResponse(output);
@@ -25863,59 +26106,6 @@ ${stderr}`;
25863
26106
  };
25864
26107
  });
25865
26108
 
25866
- // src/agents/claude/index.ts
25867
- var init_claude = __esm(() => {
25868
- init_adapter3();
25869
- init_execution();
25870
- });
25871
-
25872
- // src/agents/shared/validation.ts
25873
- function validateAgentForTier(agent, tier) {
25874
- return agent.capabilities.supportedTiers.includes(tier);
25875
- }
25876
- function validateAgentFeature(agent, feature) {
25877
- return agent.capabilities.features.has(feature);
25878
- }
25879
- function describeAgentCapabilities(agent) {
25880
- const tiers = agent.capabilities.supportedTiers.join(",");
25881
- const features = Array.from(agent.capabilities.features).join(",");
25882
- const maxTokens = agent.capabilities.maxContextTokens;
25883
- return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
25884
- }
25885
-
25886
- // src/agents/index.ts
25887
- var exports_agents = {};
25888
- __export(exports_agents, {
25889
- validateAgentForTier: () => validateAgentForTier,
25890
- validateAgentFeature: () => validateAgentFeature,
25891
- parseTokenUsage: () => parseTokenUsage,
25892
- getInstalledAgents: () => getInstalledAgents,
25893
- getAllAgentNames: () => getAllAgentNames,
25894
- getAgentVersions: () => getAgentVersions,
25895
- getAgentVersion: () => getAgentVersion,
25896
- getAgent: () => getAgent,
25897
- formatCostWithConfidence: () => formatCostWithConfidence,
25898
- estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
25899
- estimateCostFromOutput: () => estimateCostFromOutput,
25900
- estimateCostByDuration: () => estimateCostByDuration,
25901
- estimateCost: () => estimateCost,
25902
- describeAgentCapabilities: () => describeAgentCapabilities,
25903
- checkAgentHealth: () => checkAgentHealth,
25904
- MODEL_PRICING: () => MODEL_PRICING,
25905
- CompleteError: () => CompleteError,
25906
- ClaudeCodeAdapter: () => ClaudeCodeAdapter,
25907
- COST_RATES: () => COST_RATES,
25908
- AllAgentsUnavailableError: () => AllAgentsUnavailableError
25909
- });
25910
- var init_agents = __esm(() => {
25911
- init_types2();
25912
- init_claude();
25913
- init_registry();
25914
- init_cost();
25915
- init_version_detection();
25916
- init_errors();
25917
- });
25918
-
25919
26109
  // src/pipeline/stages/acceptance-setup.ts
25920
26110
  var exports_acceptance_setup = {};
25921
26111
  __export(exports_acceptance_setup, {
@@ -26059,7 +26249,6 @@ ${stderr}` };
26059
26249
  }
26060
26250
  if (shouldGenerate) {
26061
26251
  totalCriteria = allCriteria.length;
26062
- const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
26063
26252
  const agent = (ctx.agentGetFn ?? _acceptanceSetupDeps.getAgent)(ctx.config.autoMode.defaultAgent);
26064
26253
  let allRefinedCriteria;
26065
26254
  if (ctx.config.acceptance.refinement) {
@@ -26087,7 +26276,7 @@ ${stderr}` };
26087
26276
  testableCount = allRefinedCriteria.filter((r) => r.testable).length;
26088
26277
  for (const [workdir, group] of workdirGroups) {
26089
26278
  const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
26090
- const testPath = path5.join(packageDir, resolveAcceptanceTestFile(language, testPathConfig));
26279
+ const testPath = path5.join(packageDir, ".nax", "features", featureName, resolveAcceptanceTestFile(language, testPathConfig));
26091
26280
  const groupStoryIds = new Set(group.stories.map((s) => s.id));
26092
26281
  const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
26093
26282
  const result = await _acceptanceSetupDeps.generate(group.stories, groupRefined, {
@@ -26144,6 +26333,59 @@ ${stderr}` };
26144
26333
  };
26145
26334
  });
26146
26335
 
26336
+ // src/agents/claude/index.ts
26337
+ var init_claude = __esm(() => {
26338
+ init_adapter3();
26339
+ init_execution();
26340
+ });
26341
+
26342
+ // src/agents/shared/validation.ts
26343
+ function validateAgentForTier(agent, tier) {
26344
+ return agent.capabilities.supportedTiers.includes(tier);
26345
+ }
26346
+ function validateAgentFeature(agent, feature) {
26347
+ return agent.capabilities.features.has(feature);
26348
+ }
26349
+ function describeAgentCapabilities(agent) {
26350
+ const tiers = agent.capabilities.supportedTiers.join(",");
26351
+ const features = Array.from(agent.capabilities.features).join(",");
26352
+ const maxTokens = agent.capabilities.maxContextTokens;
26353
+ return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
26354
+ }
26355
+
26356
+ // src/agents/index.ts
26357
+ var exports_agents = {};
26358
+ __export(exports_agents, {
26359
+ validateAgentForTier: () => validateAgentForTier,
26360
+ validateAgentFeature: () => validateAgentFeature,
26361
+ parseTokenUsage: () => parseTokenUsage,
26362
+ getInstalledAgents: () => getInstalledAgents,
26363
+ getAllAgentNames: () => getAllAgentNames,
26364
+ getAgentVersions: () => getAgentVersions,
26365
+ getAgentVersion: () => getAgentVersion,
26366
+ getAgent: () => getAgent,
26367
+ formatCostWithConfidence: () => formatCostWithConfidence,
26368
+ estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
26369
+ estimateCostFromOutput: () => estimateCostFromOutput,
26370
+ estimateCostByDuration: () => estimateCostByDuration,
26371
+ estimateCost: () => estimateCost,
26372
+ describeAgentCapabilities: () => describeAgentCapabilities,
26373
+ checkAgentHealth: () => checkAgentHealth,
26374
+ MODEL_PRICING: () => MODEL_PRICING,
26375
+ CompleteError: () => CompleteError,
26376
+ ClaudeCodeAdapter: () => ClaudeCodeAdapter,
26377
+ COST_RATES: () => COST_RATES,
26378
+ AllAgentsUnavailableError: () => AllAgentsUnavailableError
26379
+ });
26380
+ var init_agents = __esm(() => {
26381
+ init_types2();
26382
+ init_claude();
26383
+ init_registry();
26384
+ init_cost();
26385
+ init_version_detection();
26386
+ init_errors();
26387
+ });
26388
+
26147
26389
  // src/quality/runner.ts
26148
26390
  var {spawn: spawn2 } = globalThis.Bun;
26149
26391
  async function runQualityCommand(opts) {
@@ -26233,6 +26475,59 @@ var init_quality = __esm(() => {
26233
26475
  init_runner2();
26234
26476
  });
26235
26477
 
26478
+ // src/verification/shared-rectification-loop.ts
26479
+ function resolveLogData(data, state) {
26480
+ if (!data) {
26481
+ return;
26482
+ }
26483
+ return typeof data === "function" ? data(state) : data;
26484
+ }
26485
+ function buildProgressivePromptPreamble(opts) {
26486
+ const rethinkAt = Math.min(opts.rethinkAtAttempt ?? 2, opts.maxAttempts);
26487
+ const urgencyAt = Math.min(opts.urgencyAtAttempt ?? 3, opts.maxAttempts);
26488
+ const shouldRethink = opts.attempt >= rethinkAt;
26489
+ const shouldUrgency = opts.attempt >= urgencyAt;
26490
+ if (!shouldRethink && !shouldUrgency) {
26491
+ return "";
26492
+ }
26493
+ if (shouldUrgency) {
26494
+ opts.logger?.info(opts.stage, "Progressive prompt escalation: urgency + rethink injected", {
26495
+ attempt: opts.attempt,
26496
+ rethinkAtAttempt: rethinkAt,
26497
+ urgencyAtAttempt: urgencyAt,
26498
+ maxAttempts: opts.maxAttempts
26499
+ });
26500
+ } else {
26501
+ opts.logger?.info(opts.stage, "Progressive prompt escalation: rethink injected", {
26502
+ attempt: opts.attempt,
26503
+ rethinkAtAttempt: rethinkAt,
26504
+ maxAttempts: opts.maxAttempts
26505
+ });
26506
+ }
26507
+ const urgencySection = shouldUrgency ? opts.urgencySection : "";
26508
+ const rethinkSection = shouldRethink ? opts.rethinkSection : "";
26509
+ return `${urgencySection}${rethinkSection}`;
26510
+ }
26511
+ async function runSharedRectificationLoop(opts) {
26512
+ opts.logger?.info(opts.stage, opts.startMessage, resolveLogData(opts.startData, opts.state));
26513
+ while (opts.canContinue(opts.state)) {
26514
+ opts.state.attempt++;
26515
+ opts.logger?.info(opts.stage, opts.attemptMessage(opts.state.attempt, opts.maxAttempts, opts.state), resolveLogData(opts.attemptData, opts.state));
26516
+ const prompt = await opts.buildPrompt(opts.state.attempt, opts.state);
26517
+ await opts.runAttempt(opts.state.attempt, prompt, opts.state);
26518
+ const passed = await opts.checkResult(opts.state.attempt, opts.state);
26519
+ if (passed) {
26520
+ return true;
26521
+ }
26522
+ await opts.onAttemptFailure?.(opts.state.attempt, opts.state);
26523
+ }
26524
+ await opts.onLoopEnd?.(opts.state);
26525
+ if (opts.state.attempt >= opts.maxAttempts) {
26526
+ return await opts.onExhausted?.(opts.state) ?? false;
26527
+ }
26528
+ return false;
26529
+ }
26530
+
26236
26531
  // src/pipeline/event-bus.ts
26237
26532
  class PipelineEventBus {
26238
26533
  subscribers = new Map;
@@ -26684,6 +26979,16 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
26684
26979
  const needsTruncation = rawDiff.length > DIFF_CAP_BYTES;
26685
26980
  const stat = needsTruncation ? await collectDiffStat(workdir, effectiveRef) : undefined;
26686
26981
  const diff = truncateDiff(rawDiff, stat);
26982
+ if (!diff) {
26983
+ return {
26984
+ check: "semantic",
26985
+ success: true,
26986
+ command: "",
26987
+ exitCode: 0,
26988
+ output: "skipped: no production code changes",
26989
+ durationMs: Date.now() - startTime
26990
+ };
26991
+ }
26687
26992
  const agent = modelResolver(semanticConfig.modelTier);
26688
26993
  if (!agent) {
26689
26994
  logger?.warn("semantic", "No agent available for semantic review \u2014 skipping", {
@@ -27178,7 +27483,7 @@ __export(exports_review, {
27178
27483
  reviewStage: () => reviewStage,
27179
27484
  _reviewDeps: () => _reviewDeps
27180
27485
  });
27181
- import { join as join17 } from "path";
27486
+ import { join as join18 } from "path";
27182
27487
  var reviewStage, _reviewDeps;
27183
27488
  var init_review = __esm(() => {
27184
27489
  init_agents();
@@ -27192,7 +27497,7 @@ var init_review = __esm(() => {
27192
27497
  const logger = getLogger();
27193
27498
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
27194
27499
  logger.info("review", "Running review phase", { storyId: ctx.story.id });
27195
- const effectiveWorkdir = ctx.story.workdir ? join17(ctx.workdir, ctx.story.workdir) : ctx.workdir;
27500
+ const effectiveWorkdir = ctx.story.workdir ? join18(ctx.workdir, ctx.story.workdir) : ctx.workdir;
27196
27501
  const agentResolver = ctx.agentGetFn ?? getAgent;
27197
27502
  const agentName = effectiveConfig.autoMode?.defaultAgent;
27198
27503
  const modelResolver = (_tier) => agentName ? agentResolver(agentName) ?? null : null;
@@ -27244,7 +27549,7 @@ var init_review = __esm(() => {
27244
27549
  });
27245
27550
 
27246
27551
  // src/pipeline/stages/autofix.ts
27247
- import { join as join18 } from "path";
27552
+ import { join as join19 } from "path";
27248
27553
  async function recheckReview(ctx) {
27249
27554
  const { reviewStage: reviewStage2 } = await Promise.resolve().then(() => (init_review(), exports_review));
27250
27555
  if (!reviewStage2.enabled(ctx))
@@ -27277,11 +27582,38 @@ Fix ALL errors listed above. Do NOT change test files or test behavior.
27277
27582
  Do NOT add new features \u2014 only fix the quality check errors.
27278
27583
  Commit your fixes when done.${scopeConstraint}`;
27279
27584
  }
27585
+ function buildAutofixEscalationPreamble(attempt, maxAttempts, rethinkAtAttempt, urgencyAtAttempt) {
27586
+ return buildProgressivePromptPreamble({
27587
+ attempt,
27588
+ maxAttempts,
27589
+ rethinkAtAttempt,
27590
+ urgencyAtAttempt,
27591
+ stage: "autofix",
27592
+ logger: getLogger(),
27593
+ urgencySection: `## Final Autofix Attempt Before Escalation
27594
+
27595
+ This is attempt ${attempt}. If the review still fails after this, autofix will escalate instead of retrying.
27596
+ A different approach is required. Do not repeat the same fix.
27597
+
27598
+ `,
27599
+ rethinkSection: `## Previous Attempt Did Not Fix the Failures
27600
+
27601
+ Your previous fix attempt (attempt ${attempt}) did not resolve the quality errors. Rethink your approach.
27602
+
27603
+ - Do not repeat the same edit pattern.
27604
+ - Re-read the failing diagnostics carefully.
27605
+ - Try a fundamentally different fix strategy if the earlier one did not work.
27606
+
27607
+ `
27608
+ });
27609
+ }
27280
27610
  async function runAgentRectification(ctx) {
27281
27611
  const logger = getLogger();
27282
27612
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
27283
27613
  const maxPerCycle = effectiveConfig.quality.autofix?.maxAttempts ?? 2;
27284
27614
  const maxTotal = effectiveConfig.quality.autofix?.maxTotalAttempts ?? 10;
27615
+ const rethinkAtAttempt = effectiveConfig.quality.autofix?.rethinkAtAttempt ?? 2;
27616
+ const urgencyAtAttempt = effectiveConfig.quality.autofix?.urgencyAtAttempt ?? 3;
27285
27617
  const consumed = ctx.autofixAttempt ?? 0;
27286
27618
  const failedChecks = collectFailedChecks(ctx);
27287
27619
  if (failedChecks.length === 0) {
@@ -27298,56 +27630,90 @@ async function runAgentRectification(ctx) {
27298
27630
  }
27299
27631
  const remainingBudget = maxTotal - consumed;
27300
27632
  const maxAttempts = Math.min(maxPerCycle, remainingBudget);
27301
- logger.info("autofix", "Starting agent rectification for review failures", {
27633
+ const agentGetFn = ctx.agentGetFn ?? _autofixDeps.getAgent;
27634
+ const loopState = {
27635
+ attempt: 0,
27636
+ failedChecks
27637
+ };
27638
+ return runSharedRectificationLoop({
27639
+ stage: "autofix",
27302
27640
  storyId: ctx.story.id,
27303
- failedChecks: failedChecks.map((c) => c.check),
27304
27641
  maxAttempts,
27305
- totalUsed: consumed,
27306
- maxTotalAttempts: maxTotal
27307
- });
27308
- const agentGetFn = ctx.agentGetFn ?? _autofixDeps.getAgent;
27309
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
27310
- ctx.autofixAttempt = consumed + attempt;
27311
- logger.info("autofix", `Agent rectification attempt ${ctx.autofixAttempt}/${maxTotal}`, { storyId: ctx.story.id });
27312
- const agent = agentGetFn(ctx.config.autoMode.defaultAgent);
27313
- if (!agent) {
27314
- logger.error("autofix", "Agent not found \u2014 cannot run agent rectification", { storyId: ctx.story.id });
27315
- return false;
27316
- }
27317
- const prompt = buildReviewRectificationPrompt(failedChecks, ctx.story);
27318
- const modelTier = ctx.story.routing?.modelTier ?? ctx.config.autoMode.escalation.tierOrder[0]?.tier ?? "balanced";
27319
- const modelDef = resolveModelForAgent(ctx.config.models, ctx.routing.agent ?? ctx.config.autoMode.defaultAgent, modelTier, ctx.config.autoMode.defaultAgent);
27320
- const rectificationWorkdir = ctx.story.workdir ? join18(ctx.workdir, ctx.story.workdir) : ctx.workdir;
27321
- await agent.run({
27322
- prompt,
27323
- workdir: rectificationWorkdir,
27324
- modelTier,
27325
- modelDef,
27326
- timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
27327
- dangerouslySkipPermissions: resolvePermissions(ctx.config, "rectification").skipPermissions,
27328
- pipelineStage: "rectification",
27329
- config: ctx.config,
27330
- maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
27642
+ state: loopState,
27643
+ logger,
27644
+ startMessage: "Starting agent rectification for review failures",
27645
+ startData: {
27331
27646
  storyId: ctx.story.id,
27332
- sessionRole: "implementer"
27333
- });
27334
- const passed = await _autofixDeps.recheckReview(ctx);
27335
- if (passed) {
27336
- logger.info("autofix", `[OK] Agent rectification succeeded on attempt ${attempt}`, {
27647
+ failedChecks: failedChecks.map((check2) => check2.check),
27648
+ maxAttempts,
27649
+ totalUsed: consumed,
27650
+ maxTotalAttempts: maxTotal
27651
+ },
27652
+ attemptMessage: (attempt) => `Agent rectification attempt ${consumed + attempt}/${maxTotal}`,
27653
+ attemptData: { storyId: ctx.story.id },
27654
+ canContinue: (state) => state.failedChecks.length > 0 && state.attempt < maxAttempts,
27655
+ buildPrompt: (attempt, state) => {
27656
+ let prompt = buildReviewRectificationPrompt(state.failedChecks, ctx.story);
27657
+ const escalationPreamble = buildAutofixEscalationPreamble(attempt, maxAttempts, rethinkAtAttempt, urgencyAtAttempt);
27658
+ if (escalationPreamble) {
27659
+ prompt = `${escalationPreamble}${prompt}`;
27660
+ }
27661
+ return prompt;
27662
+ },
27663
+ runAttempt: async (attempt, prompt) => {
27664
+ ctx.autofixAttempt = consumed + attempt;
27665
+ const agent = agentGetFn(ctx.config.autoMode.defaultAgent);
27666
+ if (!agent) {
27667
+ logger.error("autofix", "Agent not found \u2014 cannot run agent rectification", { storyId: ctx.story.id });
27668
+ throw new Error("AUTOFIX_AGENT_NOT_FOUND");
27669
+ }
27670
+ const modelTier = ctx.story.routing?.modelTier ?? ctx.config.autoMode.escalation.tierOrder[0]?.tier ?? "balanced";
27671
+ const modelDef = resolveModelForAgent(ctx.config.models, ctx.routing.agent ?? ctx.config.autoMode.defaultAgent, modelTier, ctx.config.autoMode.defaultAgent);
27672
+ const rectificationWorkdir = ctx.story.workdir ? join19(ctx.workdir, ctx.story.workdir) : ctx.workdir;
27673
+ await agent.run({
27674
+ prompt,
27675
+ workdir: rectificationWorkdir,
27676
+ modelTier,
27677
+ modelDef,
27678
+ timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
27679
+ dangerouslySkipPermissions: resolvePermissions(ctx.config, "rectification").skipPermissions,
27680
+ pipelineStage: "rectification",
27681
+ config: ctx.config,
27682
+ maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
27683
+ storyId: ctx.story.id,
27684
+ sessionRole: "implementer"
27685
+ });
27686
+ },
27687
+ checkResult: async (attempt, state) => {
27688
+ const passed = await _autofixDeps.recheckReview(ctx);
27689
+ if (passed) {
27690
+ logger.info("autofix", `[OK] Agent rectification succeeded on attempt ${attempt}`, {
27691
+ storyId: ctx.story.id
27692
+ });
27693
+ return true;
27694
+ }
27695
+ const updatedFailed = collectFailedChecks(ctx);
27696
+ if (updatedFailed.length > 0) {
27697
+ state.failedChecks.splice(0, state.failedChecks.length, ...updatedFailed);
27698
+ }
27699
+ return false;
27700
+ },
27701
+ onAttemptFailure: (attempt) => {
27702
+ logger.warn("autofix", `Agent rectification still failing after attempt ${attempt}`, {
27337
27703
  storyId: ctx.story.id
27338
27704
  });
27339
- return true;
27705
+ },
27706
+ onLoopEnd: (state) => {
27707
+ if (state.attempt >= maxAttempts) {
27708
+ logger.warn("autofix", "Agent rectification exhausted", { storyId: ctx.story.id });
27709
+ }
27340
27710
  }
27341
- const updatedFailed = collectFailedChecks(ctx);
27342
- if (updatedFailed.length > 0) {
27343
- failedChecks.splice(0, failedChecks.length, ...updatedFailed);
27711
+ }).catch((error48) => {
27712
+ if (error48 instanceof Error && error48.message === "AUTOFIX_AGENT_NOT_FOUND") {
27713
+ return false;
27344
27714
  }
27345
- logger.warn("autofix", `Agent rectification still failing after attempt ${attempt}`, {
27346
- storyId: ctx.story.id
27347
- });
27348
- }
27349
- logger.warn("autofix", "Agent rectification exhausted", { storyId: ctx.story.id });
27350
- return false;
27715
+ throw error48;
27716
+ });
27351
27717
  }
27352
27718
  var autofixStage, _autofixDeps;
27353
27719
  var init_autofix = __esm(() => {
@@ -27381,7 +27747,7 @@ var init_autofix = __esm(() => {
27381
27747
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
27382
27748
  const lintFixCmd = effectiveConfig.quality.commands.lintFix;
27383
27749
  const formatFixCmd = effectiveConfig.quality.commands.formatFix;
27384
- const effectiveWorkdir = ctx.story.workdir ? join18(ctx.workdir, ctx.story.workdir) : ctx.workdir;
27750
+ const effectiveWorkdir = ctx.story.workdir ? join19(ctx.workdir, ctx.story.workdir) : ctx.workdir;
27385
27751
  const failedCheckNames = new Set((reviewResult.checks ?? []).filter((c) => !c.success).map((c) => c.check));
27386
27752
  const hasLintFailure = failedCheckNames.has("lint");
27387
27753
  logger.info("autofix", "Starting autofix", {
@@ -27465,10 +27831,10 @@ var init_autofix = __esm(() => {
27465
27831
 
27466
27832
  // src/execution/progress.ts
27467
27833
  import { appendFile as appendFile2, mkdir } from "fs/promises";
27468
- import { join as join19 } from "path";
27834
+ import { join as join20 } from "path";
27469
27835
  async function appendProgress(featureDir, storyId, status, message) {
27470
27836
  await mkdir(featureDir, { recursive: true });
27471
- const progressPath = join19(featureDir, "progress.txt");
27837
+ const progressPath = join20(featureDir, "progress.txt");
27472
27838
  const timestamp = new Date().toISOString();
27473
27839
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
27474
27840
  `;
@@ -27551,7 +27917,7 @@ function estimateTokens(text) {
27551
27917
 
27552
27918
  // src/constitution/loader.ts
27553
27919
  import { existsSync as existsSync19 } from "fs";
27554
- import { join as join20 } from "path";
27920
+ import { join as join21 } from "path";
27555
27921
  function truncateToTokens(text, maxTokens) {
27556
27922
  const maxChars = maxTokens * 3;
27557
27923
  if (text.length <= maxChars) {
@@ -27573,7 +27939,7 @@ async function loadConstitution(projectDir, config2) {
27573
27939
  }
27574
27940
  let combinedContent = "";
27575
27941
  if (!config2.skipGlobal) {
27576
- const globalPath = join20(globalConfigDir(), config2.path);
27942
+ const globalPath = join21(globalConfigDir(), config2.path);
27577
27943
  if (existsSync19(globalPath)) {
27578
27944
  const validatedPath = validateFilePath(globalPath, globalConfigDir());
27579
27945
  const globalFile = Bun.file(validatedPath);
@@ -27583,7 +27949,7 @@ async function loadConstitution(projectDir, config2) {
27583
27949
  }
27584
27950
  }
27585
27951
  }
27586
- const projectPath = join20(projectDir, config2.path);
27952
+ const projectPath = join21(projectDir, config2.path);
27587
27953
  if (existsSync19(projectPath)) {
27588
27954
  const validatedPath = validateFilePath(projectPath, projectDir);
27589
27955
  const projectFile = Bun.file(validatedPath);
@@ -28611,7 +28977,7 @@ var init_helpers = __esm(() => {
28611
28977
  });
28612
28978
 
28613
28979
  // src/pipeline/stages/context.ts
28614
- import { join as join21 } from "path";
28980
+ import { join as join22 } from "path";
28615
28981
  var contextStage;
28616
28982
  var init_context2 = __esm(() => {
28617
28983
  init_helpers();
@@ -28621,7 +28987,7 @@ var init_context2 = __esm(() => {
28621
28987
  enabled: () => true,
28622
28988
  async execute(ctx) {
28623
28989
  const logger = getLogger();
28624
- const packageWorkdir = ctx.story.workdir ? join21(ctx.workdir, ctx.story.workdir) : undefined;
28990
+ const packageWorkdir = ctx.story.workdir ? join22(ctx.workdir, ctx.story.workdir) : undefined;
28625
28991
  const result = await buildStoryContextFull(ctx.prd, ctx.story, ctx.config, packageWorkdir);
28626
28992
  if (result) {
28627
28993
  ctx.contextMarkdown = result.markdown;
@@ -28755,14 +29121,14 @@ var init_isolation = __esm(() => {
28755
29121
 
28756
29122
  // src/context/greenfield.ts
28757
29123
  import { readdir } from "fs/promises";
28758
- import { join as join22 } from "path";
29124
+ import { join as join23 } from "path";
28759
29125
  async function scanForTestFiles(dir, testPattern, isRootCall = true) {
28760
29126
  const results = [];
28761
29127
  const ignoreDirs = new Set(["node_modules", "dist", "build", ".next", ".git"]);
28762
29128
  try {
28763
29129
  const entries = await readdir(dir, { withFileTypes: true });
28764
29130
  for (const entry of entries) {
28765
- const fullPath = join22(dir, entry.name);
29131
+ const fullPath = join23(dir, entry.name);
28766
29132
  if (entry.isDirectory()) {
28767
29133
  if (ignoreDirs.has(entry.name))
28768
29134
  continue;
@@ -29117,13 +29483,13 @@ function parseTestOutput(output, exitCode) {
29117
29483
 
29118
29484
  // src/verification/runners.ts
29119
29485
  import { existsSync as existsSync20 } from "fs";
29120
- import { join as join23 } from "path";
29486
+ import { join as join24 } from "path";
29121
29487
  async function verifyAssets(workingDirectory, expectedFiles) {
29122
29488
  if (!expectedFiles || expectedFiles.length === 0)
29123
29489
  return { success: true, missingFiles: [] };
29124
29490
  const missingFiles = [];
29125
29491
  for (const file3 of expectedFiles) {
29126
- if (!existsSync20(join23(workingDirectory, file3)))
29492
+ if (!existsSync20(join24(workingDirectory, file3)))
29127
29493
  missingFiles.push(file3);
29128
29494
  }
29129
29495
  if (missingFiles.length > 0) {
@@ -29227,27 +29593,14 @@ function shouldRetryRectification(state, config2) {
29227
29593
  return true;
29228
29594
  }
29229
29595
  function buildEscalationPreamble(attempt, config2) {
29230
- const logger = getSafeLogger();
29231
- const rethinkAt = Math.min(config2.rethinkAtAttempt ?? 2, config2.maxRetries);
29232
- const urgencyAt = Math.min(config2.urgencyAtAttempt ?? 3, config2.maxRetries);
29233
- if (attempt < rethinkAt)
29234
- return "";
29235
- const isUrgent = attempt >= urgencyAt;
29236
- if (isUrgent) {
29237
- logger?.info("rectification", "Progressive prompt escalation: urgency + rethink injected", {
29238
- attempt,
29239
- urgencyAtAttempt: urgencyAt,
29240
- rethinkAtAttempt: rethinkAt,
29241
- maxRetries: config2.maxRetries
29242
- });
29243
- } else {
29244
- logger?.info("rectification", "Progressive prompt escalation: rethink injected", {
29245
- attempt,
29246
- rethinkAtAttempt: rethinkAt,
29247
- maxRetries: config2.maxRetries
29248
- });
29249
- }
29250
- const rethinkSection = `## \u26A0\uFE0F Previous Attempt Did Not Fix the Failures
29596
+ return buildProgressivePromptPreamble({
29597
+ attempt,
29598
+ maxAttempts: config2.maxRetries,
29599
+ rethinkAtAttempt: config2.rethinkAtAttempt,
29600
+ urgencyAtAttempt: config2.urgencyAtAttempt,
29601
+ stage: "rectification",
29602
+ logger: getSafeLogger(),
29603
+ rethinkSection: `## \u26A0\uFE0F Previous Attempt Did Not Fix the Failures
29251
29604
 
29252
29605
  Your previous fix attempt (attempt ${attempt}) did not resolve all failures. **Step back and reconsider your approach.**
29253
29606
 
@@ -29256,14 +29609,14 @@ Your previous fix attempt (attempt ${attempt}) did not resolve all failures. **S
29256
29609
  - Re-read the story context and test failures carefully before making changes.
29257
29610
  - Consider: are there missing edge cases, incorrect assumptions, or a design flaw in the implementation?
29258
29611
 
29259
- `;
29260
- const urgencySection = isUrgent ? `## \uD83D\uDEA8 Final Rectification Attempt Before Model Escalation
29612
+ `,
29613
+ urgencySection: `## \uD83D\uDEA8 Final Rectification Attempt Before Model Escalation
29261
29614
 
29262
29615
  This is attempt ${attempt} \u2014 if the tests still fail after this, the task will escalate to a stronger model tier.
29263
29616
  A **completely different approach** is required. Do not repeat what you have already tried.
29264
29617
 
29265
- ` : "";
29266
- return `${urgencySection}${rethinkSection}`;
29618
+ `
29619
+ });
29267
29620
  }
29268
29621
  function createRectificationPrompt(failures, story, config2, attempt) {
29269
29622
  const maxChars = config2?.maxFailureSummaryChars ?? 2000;
@@ -29523,77 +29876,112 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
29523
29876
  storyId: story.id,
29524
29877
  sessionName: rectificationSessionName
29525
29878
  });
29526
- while (_rectificationGateDeps.shouldRetryRectification(rectificationState, rectificationConfig)) {
29527
- rectificationState.attempt++;
29528
- const isLastAttempt = rectificationState.attempt >= rectificationConfig.maxRetries;
29529
- logger.info("tdd", `-> Implementer rectification attempt ${rectificationState.attempt}/${rectificationConfig.maxRetries}`, { storyId: story.id, currentFailures: rectificationState.currentFailures });
29530
- const rectificationPrompt = buildImplementerRectificationPrompt(testSummary.failures, story, contextMarkdown, rectificationConfig);
29531
- const rectifyBeforeRef = await captureGitRef(workdir) ?? "HEAD";
29532
- const rectifyResult = await agent.run({
29533
- prompt: rectificationPrompt,
29534
- workdir,
29535
- modelTier: implementerTier,
29536
- modelDef: resolveModelForAgent(config2.models, story.routing?.agent ?? config2.autoMode.defaultAgent, implementerTier, config2.autoMode.defaultAgent),
29537
- timeoutSeconds: config2.execution.sessionTimeoutSeconds,
29538
- dangerouslySkipPermissions: resolvePermissions(config2, "rectification").skipPermissions,
29539
- pipelineStage: "rectification",
29540
- config: config2,
29541
- maxInteractionTurns: config2.agent?.maxInteractionTurns,
29542
- featureName,
29879
+ const loopState = {
29880
+ ...rectificationState,
29881
+ isolationPassed: true
29882
+ };
29883
+ const fixed = await runSharedRectificationLoop({
29884
+ stage: "tdd",
29885
+ storyId: story.id,
29886
+ maxAttempts: rectificationConfig.maxRetries,
29887
+ state: loopState,
29888
+ logger,
29889
+ startMessage: "Full suite gate detected regressions",
29890
+ startData: {
29543
29891
  storyId: story.id,
29544
- sessionRole: "implementer",
29545
- acpSessionName: rectificationSessionName,
29546
- keepSessionOpen: !isLastAttempt
29547
- });
29548
- if (!rectifyResult.success && rectifyResult.pid) {
29549
- await cleanupProcessTree(rectifyResult.pid);
29550
- }
29551
- if (rectifyResult.success) {
29552
- logger.info("tdd", "Rectification agent session complete", {
29553
- storyId: story.id,
29554
- attempt: rectificationState.attempt,
29555
- cost: rectifyResult.estimatedCost
29556
- });
29557
- } else {
29558
- logger.warn("tdd", "Rectification agent session failed", {
29892
+ failedTests: testSummary.failed,
29893
+ passedTests: testSummary.passed
29894
+ },
29895
+ attemptMessage: (attempt) => `-> Implementer rectification attempt ${attempt}/${rectificationConfig.maxRetries}`,
29896
+ attemptData: (state) => ({
29897
+ storyId: story.id,
29898
+ currentFailures: state.currentFailures
29899
+ }),
29900
+ canContinue: (state) => state.isolationPassed && _rectificationGateDeps.shouldRetryRectification(state, rectificationConfig),
29901
+ buildPrompt: () => buildImplementerRectificationPrompt(testSummary.failures, story, contextMarkdown, rectificationConfig),
29902
+ runAttempt: async (attempt, rectificationPrompt) => {
29903
+ const isLastAttempt = attempt >= rectificationConfig.maxRetries;
29904
+ const rectifyBeforeRef = await captureGitRef(workdir) ?? "HEAD";
29905
+ const rectifyResult = await agent.run({
29906
+ prompt: rectificationPrompt,
29907
+ workdir,
29908
+ modelTier: implementerTier,
29909
+ modelDef: resolveModelForAgent(config2.models, story.routing?.agent ?? config2.autoMode.defaultAgent, implementerTier, config2.autoMode.defaultAgent),
29910
+ timeoutSeconds: config2.execution.sessionTimeoutSeconds,
29911
+ dangerouslySkipPermissions: resolvePermissions(config2, "rectification").skipPermissions,
29912
+ pipelineStage: "rectification",
29913
+ config: config2,
29914
+ maxInteractionTurns: config2.agent?.maxInteractionTurns,
29915
+ featureName,
29559
29916
  storyId: story.id,
29560
- attempt: rectificationState.attempt,
29561
- exitCode: rectifyResult.exitCode
29917
+ sessionRole: "implementer",
29918
+ acpSessionName: rectificationSessionName,
29919
+ keepSessionOpen: !isLastAttempt
29562
29920
  });
29563
- }
29564
- await autoCommitIfDirty(workdir, "tdd", "rectification", story.id);
29565
- const rectifyIsolation = lite ? undefined : await verifyImplementerIsolation(workdir, rectifyBeforeRef);
29566
- if (rectifyIsolation && !rectifyIsolation.passed) {
29567
- logger.error("tdd", "Rectification violated isolation", {
29568
- storyId: story.id,
29569
- attempt: rectificationState.attempt,
29570
- violations: rectifyIsolation.violations
29921
+ if (!rectifyResult.success && rectifyResult.pid) {
29922
+ await cleanupProcessTree(rectifyResult.pid);
29923
+ }
29924
+ if (rectifyResult.success) {
29925
+ logger.info("tdd", "Rectification agent session complete", {
29926
+ storyId: story.id,
29927
+ attempt,
29928
+ cost: rectifyResult.estimatedCost
29929
+ });
29930
+ } else {
29931
+ logger.warn("tdd", "Rectification agent session failed", {
29932
+ storyId: story.id,
29933
+ attempt,
29934
+ exitCode: rectifyResult.exitCode
29935
+ });
29936
+ }
29937
+ await autoCommitIfDirty(workdir, "tdd", "rectification", story.id);
29938
+ const rectifyIsolation = lite ? undefined : await verifyImplementerIsolation(workdir, rectifyBeforeRef);
29939
+ if (rectifyIsolation && !rectifyIsolation.passed) {
29940
+ loopState.isolationPassed = false;
29941
+ logger.error("tdd", "Rectification violated isolation", {
29942
+ storyId: story.id,
29943
+ attempt,
29944
+ violations: rectifyIsolation.violations
29945
+ });
29946
+ }
29947
+ },
29948
+ checkResult: async (attempt, state) => {
29949
+ if (!state.isolationPassed) {
29950
+ return false;
29951
+ }
29952
+ const retryFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
29953
+ cwd: workdir
29571
29954
  });
29572
- break;
29573
- }
29574
- const retryFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
29575
- cwd: workdir
29576
- });
29577
- const retrySuitePassed = retryFullSuite.success && retryFullSuite.exitCode === 0;
29578
- if (retrySuitePassed) {
29579
- logger.info("tdd", "Full suite gate passed after rectification!", {
29955
+ const retrySuitePassed = retryFullSuite.success && retryFullSuite.exitCode === 0;
29956
+ if (retrySuitePassed) {
29957
+ logger.info("tdd", "Full suite gate passed after rectification!", {
29958
+ storyId: story.id,
29959
+ attempt
29960
+ });
29961
+ return true;
29962
+ }
29963
+ if (retryFullSuite.output) {
29964
+ const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
29965
+ state.currentFailures = newTestSummary.failed;
29966
+ testSummary.failures = newTestSummary.failures;
29967
+ testSummary.failed = newTestSummary.failed;
29968
+ testSummary.passed = newTestSummary.passed;
29969
+ }
29970
+ return false;
29971
+ },
29972
+ onAttemptFailure: (attempt, state) => {
29973
+ if (!state.isolationPassed) {
29974
+ return;
29975
+ }
29976
+ logger.warn("tdd", "Full suite still failing after rectification attempt", {
29580
29977
  storyId: story.id,
29581
- attempt: rectificationState.attempt
29978
+ attempt,
29979
+ remainingFailures: state.currentFailures
29582
29980
  });
29583
- return true;
29584
29981
  }
29585
- if (retryFullSuite.output) {
29586
- const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
29587
- rectificationState.currentFailures = newTestSummary.failed;
29588
- testSummary.failures = newTestSummary.failures;
29589
- testSummary.failed = newTestSummary.failed;
29590
- testSummary.passed = newTestSummary.passed;
29591
- }
29592
- logger.warn("tdd", "Full suite still failing after rectification attempt", {
29593
- storyId: story.id,
29594
- attempt: rectificationState.attempt,
29595
- remainingFailures: rectificationState.currentFailures
29596
- });
29982
+ });
29983
+ if (fixed) {
29984
+ return true;
29597
29985
  }
29598
29986
  const finalFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
29599
29987
  cwd: workdir
@@ -30017,13 +30405,13 @@ var exports_loader = {};
30017
30405
  __export(exports_loader, {
30018
30406
  loadOverride: () => loadOverride
30019
30407
  });
30020
- import { join as join24 } from "path";
30408
+ import { join as join25 } from "path";
30021
30409
  async function loadOverride(role, workdir, config2) {
30022
30410
  const overridePath = config2.prompts?.overrides?.[role];
30023
30411
  if (!overridePath) {
30024
30412
  return null;
30025
30413
  }
30026
- const absolutePath = join24(workdir, overridePath);
30414
+ const absolutePath = join25(workdir, overridePath);
30027
30415
  const file3 = Bun.file(absolutePath);
30028
30416
  if (!await file3.exists()) {
30029
30417
  return null;
@@ -30882,11 +31270,11 @@ var init_tdd = __esm(() => {
30882
31270
 
30883
31271
  // src/pipeline/stages/execution.ts
30884
31272
  import { existsSync as existsSync21 } from "fs";
30885
- import { join as join25 } from "path";
31273
+ import { join as join26 } from "path";
30886
31274
  function resolveStoryWorkdir(repoRoot, storyWorkdir) {
30887
31275
  if (!storyWorkdir)
30888
31276
  return repoRoot;
30889
- const resolved = join25(repoRoot, storyWorkdir);
31277
+ const resolved = join26(repoRoot, storyWorkdir);
30890
31278
  if (!existsSync21(resolved)) {
30891
31279
  throw new Error(`[execution] story.workdir "${storyWorkdir}" does not exist at "${resolved}"`);
30892
31280
  }
@@ -31556,8 +31944,15 @@ async function _defaultRunDebate(storyId, stageConfig, prompt, config2) {
31556
31944
  if (resolved.length === 0) {
31557
31945
  return { output: null, totalCostUsd: 0 };
31558
31946
  }
31947
+ const timeoutMs = (config2?.execution?.sessionTimeoutSeconds ?? 600) * 1000;
31559
31948
  const startMs = Date.now();
31560
- const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => adapter.complete(prompt, { model: debater.model }).then((out) => typeof out === "string" ? out : out.output)));
31949
+ const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => adapter.complete(prompt, {
31950
+ model: debater.model,
31951
+ config: config2,
31952
+ storyId,
31953
+ sessionRole: "debate-proposal",
31954
+ timeoutMs
31955
+ }).then((out) => typeof out === "string" ? out : out.output)));
31561
31956
  const durationMs = Date.now() - startMs;
31562
31957
  const successful = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
31563
31958
  if (successful.length === 0) {
@@ -31584,167 +31979,202 @@ async function runRectificationLoop2(opts) {
31584
31979
  currentFailures: testSummary.failed,
31585
31980
  lastExitCode: 1
31586
31981
  };
31587
- logger?.info("rectification", `Starting ${label} loop`, {
31982
+ return runSharedRectificationLoop({
31983
+ stage: "rectification",
31588
31984
  storyId: story.id,
31589
- initialFailures: rectificationState.initialFailures,
31590
- maxRetries: rectificationConfig.maxRetries
31591
- });
31592
- while (shouldRetryRectification(rectificationState, rectificationConfig)) {
31593
- rectificationState.attempt++;
31594
- logger?.info("rectification", `${label} attempt ${rectificationState.attempt}/${rectificationConfig.maxRetries}`, {
31985
+ maxAttempts: rectificationConfig.maxRetries,
31986
+ state: rectificationState,
31987
+ logger,
31988
+ startMessage: `Starting ${label} loop`,
31989
+ startData: {
31990
+ storyId: story.id,
31991
+ initialFailures: rectificationState.initialFailures,
31992
+ maxRetries: rectificationConfig.maxRetries
31993
+ },
31994
+ attemptMessage: (attempt) => `${label} attempt ${attempt}/${rectificationConfig.maxRetries}`,
31995
+ attemptData: () => ({
31595
31996
  storyId: story.id,
31596
31997
  currentFailures: rectificationState.currentFailures
31597
- });
31598
- let diagnosisPrefix = null;
31599
- const debateStageConfig = config2.debate?.stages?.rectification;
31600
- if (debateStageConfig?.enabled) {
31601
- const failureSummary = formatFailureSummary(testSummary.failures);
31602
- const diagnosisPrompt = `Analyze the following test failures and identify the root cause:
31998
+ }),
31999
+ canContinue: (state) => shouldRetryRectification(state, rectificationConfig),
32000
+ buildPrompt: async (attempt) => {
32001
+ let diagnosisPrefix = null;
32002
+ const debateStageConfig = config2.debate?.stages?.rectification;
32003
+ if (debateStageConfig?.enabled) {
32004
+ const failureSummary = formatFailureSummary(testSummary.failures);
32005
+ const diagnosisPrompt = `Analyze the following test failures and identify the root cause:
31603
32006
 
31604
32007
  ${failureSummary}`;
31605
- try {
31606
- const debateResult = await _rectificationDeps.runDebate(story.id, debateStageConfig, diagnosisPrompt, config2);
31607
- if (debateResult.totalCostUsd > 0 && story.routing) {
31608
- story.routing.estimatedCost = (story.routing.estimatedCost ?? 0) + debateResult.totalCostUsd;
31609
- }
31610
- if (debateResult.output !== null) {
31611
- diagnosisPrefix = `## Root Cause Analysis
32008
+ try {
32009
+ const debateResult = await _rectificationDeps.runDebate(story.id, debateStageConfig, diagnosisPrompt, config2);
32010
+ if (debateResult.totalCostUsd > 0 && story.routing) {
32011
+ story.routing.estimatedCost = (story.routing.estimatedCost ?? 0) + debateResult.totalCostUsd;
32012
+ }
32013
+ if (debateResult.output !== null) {
32014
+ diagnosisPrefix = `## Root Cause Analysis
31612
32015
 
31613
32016
  ${debateResult.output}`;
31614
- } else {
31615
- logger?.info("rectification", "debate diagnosis fallback \u2014 all debaters failed", {
32017
+ } else {
32018
+ logger?.info("rectification", "debate diagnosis fallback \u2014 all debaters failed", {
32019
+ storyId: story.id,
32020
+ attempt,
32021
+ event: "fallback"
32022
+ });
32023
+ }
32024
+ } catch (_error) {
32025
+ logger?.info("rectification", "debate diagnosis fallback \u2014 debate threw error", {
31616
32026
  storyId: story.id,
31617
- attempt: rectificationState.attempt,
32027
+ attempt,
31618
32028
  event: "fallback"
31619
32029
  });
31620
32030
  }
31621
- } catch (err) {
31622
- logger?.info("rectification", "debate diagnosis fallback \u2014 debate threw error", {
31623
- storyId: story.id,
31624
- attempt: rectificationState.attempt,
31625
- event: "fallback"
31626
- });
31627
32031
  }
31628
- }
31629
- let rectificationPrompt = createRectificationPrompt(testSummary.failures, story, rectificationConfig, rectificationState.attempt);
31630
- if (diagnosisPrefix)
31631
- rectificationPrompt = `${diagnosisPrefix}
32032
+ let rectificationPrompt = createRectificationPrompt(testSummary.failures, story, rectificationConfig, attempt);
32033
+ if (diagnosisPrefix) {
32034
+ rectificationPrompt = `${diagnosisPrefix}
31632
32035
 
31633
32036
  ${rectificationPrompt}`;
31634
- if (promptPrefix)
31635
- rectificationPrompt = `${promptPrefix}
32037
+ }
32038
+ if (promptPrefix) {
32039
+ rectificationPrompt = `${promptPrefix}
31636
32040
 
31637
32041
  ${rectificationPrompt}`;
31638
- const agent = agentGetFn ? agentGetFn(config2.autoMode.defaultAgent) : _rectificationDeps.getAgent(config2.autoMode.defaultAgent, config2);
31639
- if (!agent) {
31640
- logger?.error("rectification", "Agent not found, cannot retry");
31641
- break;
31642
- }
31643
- const complexity = story.routing?.complexity ?? "medium";
31644
- const modelTier = config2.autoMode.complexityRouting?.[complexity] || config2.autoMode.escalation.tierOrder[0]?.tier || "balanced";
31645
- const modelDef = resolveModelForAgent(config2.models, story.routing?.agent ?? config2.autoMode.defaultAgent, modelTier, config2.autoMode.defaultAgent);
31646
- const agentResult = await agent.run({
31647
- prompt: rectificationPrompt,
31648
- workdir,
31649
- modelTier,
31650
- modelDef,
31651
- timeoutSeconds: config2.execution.sessionTimeoutSeconds,
31652
- dangerouslySkipPermissions: resolvePermissions(config2, "rectification").skipPermissions,
31653
- pipelineStage: "rectification",
31654
- config: config2,
31655
- maxInteractionTurns: config2.agent?.maxInteractionTurns,
31656
- featureName,
31657
- storyId: story.id,
31658
- sessionRole: "implementer"
31659
- });
31660
- if (agentResult.success) {
31661
- logger?.info("rectification", `Agent ${label} session complete`, {
32042
+ }
32043
+ return rectificationPrompt;
32044
+ },
32045
+ runAttempt: async (attempt, rectificationPrompt) => {
32046
+ const agent = agentGetFn ? agentGetFn(config2.autoMode.defaultAgent) : _rectificationDeps.getAgent(config2.autoMode.defaultAgent, config2);
32047
+ if (!agent) {
32048
+ logger?.error("rectification", "Agent not found, cannot retry");
32049
+ throw new Error("RECTIFICATION_AGENT_NOT_FOUND");
32050
+ }
32051
+ const complexity = story.routing?.complexity ?? "medium";
32052
+ const modelTier = config2.autoMode.complexityRouting?.[complexity] || config2.autoMode.escalation.tierOrder[0]?.tier || "balanced";
32053
+ const modelDef = resolveModelForAgent(config2.models, story.routing?.agent ?? config2.autoMode.defaultAgent, modelTier, config2.autoMode.defaultAgent);
32054
+ const agentResult = await agent.run({
32055
+ prompt: rectificationPrompt,
32056
+ workdir,
32057
+ modelTier,
32058
+ modelDef,
32059
+ timeoutSeconds: config2.execution.sessionTimeoutSeconds,
32060
+ dangerouslySkipPermissions: resolvePermissions(config2, "rectification").skipPermissions,
32061
+ pipelineStage: "rectification",
32062
+ config: config2,
32063
+ maxInteractionTurns: config2.agent?.maxInteractionTurns,
32064
+ featureName,
31662
32065
  storyId: story.id,
31663
- attempt: rectificationState.attempt,
31664
- cost: agentResult.estimatedCost
32066
+ sessionRole: "implementer"
31665
32067
  });
31666
- } else {
31667
- logger?.warn("rectification", `Agent ${label} session failed`, {
31668
- storyId: story.id,
31669
- attempt: rectificationState.attempt,
31670
- exitCode: agentResult.exitCode
32068
+ if (agentResult.success) {
32069
+ logger?.info("rectification", `Agent ${label} session complete`, {
32070
+ storyId: story.id,
32071
+ attempt,
32072
+ cost: agentResult.estimatedCost
32073
+ });
32074
+ } else {
32075
+ logger?.warn("rectification", `Agent ${label} session failed`, {
32076
+ storyId: story.id,
32077
+ attempt,
32078
+ exitCode: agentResult.exitCode
32079
+ });
32080
+ }
32081
+ },
32082
+ checkResult: async (attempt, state) => {
32083
+ const retryVerification = await _rectificationDeps.runVerification({
32084
+ workdir,
32085
+ expectedFiles: getExpectedFiles(story),
32086
+ command: testCommand,
32087
+ timeoutSeconds,
32088
+ forceExit: config2.quality.forceExit,
32089
+ detectOpenHandles: config2.quality.detectOpenHandles,
32090
+ detectOpenHandlesRetries: config2.quality.detectOpenHandlesRetries,
32091
+ timeoutRetryCount: 0,
32092
+ gracePeriodMs: config2.quality.gracePeriodMs,
32093
+ drainTimeoutMs: config2.quality.drainTimeoutMs,
32094
+ shell: config2.quality.shell,
32095
+ stripEnvVars: config2.quality.stripEnvVars
31671
32096
  });
31672
- }
31673
- const retryVerification = await _rectificationDeps.runVerification({
31674
- workdir,
31675
- expectedFiles: getExpectedFiles(story),
31676
- command: testCommand,
31677
- timeoutSeconds,
31678
- forceExit: config2.quality.forceExit,
31679
- detectOpenHandles: config2.quality.detectOpenHandles,
31680
- detectOpenHandlesRetries: config2.quality.detectOpenHandlesRetries,
31681
- timeoutRetryCount: 0,
31682
- gracePeriodMs: config2.quality.gracePeriodMs,
31683
- drainTimeoutMs: config2.quality.drainTimeoutMs,
31684
- shell: config2.quality.shell,
31685
- stripEnvVars: config2.quality.stripEnvVars
31686
- });
31687
- if (retryVerification.success) {
31688
- logger?.info("rectification", `[OK] ${label} succeeded!`, {
32097
+ if (retryVerification.success) {
32098
+ logger?.info("rectification", `[OK] ${label} succeeded!`, {
32099
+ storyId: story.id,
32100
+ attempt,
32101
+ initialFailures: state.initialFailures
32102
+ });
32103
+ return true;
32104
+ }
32105
+ if (retryVerification.output) {
32106
+ const newTestSummary = parseBunTestOutput(retryVerification.output);
32107
+ state.currentFailures = newTestSummary.failed;
32108
+ state.lastExitCode = retryVerification.status === "SUCCESS" ? 0 : 1;
32109
+ testSummary.failures = newTestSummary.failures;
32110
+ testSummary.failed = newTestSummary.failed;
32111
+ testSummary.passed = newTestSummary.passed;
32112
+ if (newTestSummary.failed === 0) {
32113
+ state.lastExitCode = 0;
32114
+ logger?.info("rectification", `[OK] ${label} succeeded after parsing retry output`, {
32115
+ storyId: story.id,
32116
+ attempt,
32117
+ initialFailures: state.initialFailures
32118
+ });
32119
+ return true;
32120
+ }
32121
+ }
32122
+ return false;
32123
+ },
32124
+ onAttemptFailure: (attempt, state) => {
32125
+ const failingTests = testSummary.failures.slice(0, 10).map((failure) => failure.testName);
32126
+ const logData = {
31689
32127
  storyId: story.id,
31690
- attempt: rectificationState.attempt,
31691
- initialFailures: rectificationState.initialFailures
31692
- });
31693
- return true;
31694
- }
31695
- if (retryVerification.output) {
31696
- const newTestSummary = parseBunTestOutput(retryVerification.output);
31697
- rectificationState.currentFailures = newTestSummary.failed;
31698
- rectificationState.lastExitCode = retryVerification.status === "SUCCESS" ? 0 : 1;
31699
- testSummary.failures = newTestSummary.failures;
31700
- testSummary.failed = newTestSummary.failed;
31701
- testSummary.passed = newTestSummary.passed;
31702
- }
31703
- const failingTests = testSummary.failures.slice(0, 10).map((f) => f.testName);
31704
- const logData = {
31705
- storyId: story.id,
31706
- attempt: rectificationState.attempt,
31707
- remainingFailures: rectificationState.currentFailures,
31708
- failingTests
31709
- };
31710
- if (testSummary.failures.length > 10 || testSummary.failures.length === 0 && testSummary.failed > 0) {
31711
- logData.totalFailingTests = testSummary.failed;
31712
- }
31713
- logger?.warn("rectification", `${label} still failing after attempt`, logData);
31714
- }
31715
- if (rectificationState.attempt >= rectificationConfig.maxRetries) {
31716
- logger?.warn("rectification", `${label} exhausted max retries`, {
31717
- storyId: story.id,
31718
- attempts: rectificationState.attempt,
31719
- remainingFailures: rectificationState.currentFailures
31720
- });
31721
- } else if (rectificationState.currentFailures > rectificationState.initialFailures) {
31722
- logger?.warn("rectification", `${label} aborted due to further regression`, {
31723
- storyId: story.id,
31724
- initialFailures: rectificationState.initialFailures,
31725
- currentFailures: rectificationState.currentFailures
31726
- });
31727
- }
31728
- const shouldEscalate = rectificationConfig.escalateOnExhaustion !== false && config2.autoMode?.escalation?.enabled === true && rectificationState.attempt >= rectificationConfig.maxRetries && rectificationState.currentFailures > 0;
31729
- if (shouldEscalate) {
31730
- const complexity = story.routing?.complexity ?? "medium";
31731
- const currentTier = config2.autoMode.complexityRouting?.[complexity] || config2.autoMode.escalation.tierOrder[0]?.tier || "balanced";
31732
- const tierOrder = config2.autoMode.escalation.tierOrder;
31733
- const escalationResult = _rectificationDeps.escalateTier(currentTier, tierOrder);
31734
- const escalatedTier = escalationResult?.tier ?? null;
31735
- const escalatedAgent = escalationResult?.agent;
31736
- if (escalatedTier !== null) {
32128
+ attempt,
32129
+ remainingFailures: state.currentFailures,
32130
+ failingTests
32131
+ };
32132
+ if (testSummary.failures.length > 10 || testSummary.failures.length === 0 && testSummary.failed > 0) {
32133
+ logData.totalFailingTests = testSummary.failed;
32134
+ }
32135
+ logger?.warn("rectification", `${label} still failing after attempt`, logData);
32136
+ },
32137
+ onLoopEnd: (state) => {
32138
+ if (state.attempt >= rectificationConfig.maxRetries) {
32139
+ logger?.warn("rectification", `${label} exhausted max retries`, {
32140
+ storyId: story.id,
32141
+ attempts: state.attempt,
32142
+ remainingFailures: state.currentFailures
32143
+ });
32144
+ } else if (state.currentFailures > state.initialFailures) {
32145
+ logger?.warn("rectification", `${label} aborted due to further regression`, {
32146
+ storyId: story.id,
32147
+ initialFailures: state.initialFailures,
32148
+ currentFailures: state.currentFailures
32149
+ });
32150
+ }
32151
+ },
32152
+ onExhausted: async (state) => {
32153
+ const shouldEscalate = rectificationConfig.escalateOnExhaustion !== false && config2.autoMode?.escalation?.enabled === true && state.currentFailures > 0;
32154
+ if (!shouldEscalate) {
32155
+ return false;
32156
+ }
32157
+ const complexity = story.routing?.complexity ?? "medium";
32158
+ const currentTier = config2.autoMode.complexityRouting?.[complexity] || config2.autoMode.escalation.tierOrder[0]?.tier || "balanced";
32159
+ const tierOrder = config2.autoMode.escalation.tierOrder;
32160
+ const escalationResult = _rectificationDeps.escalateTier(currentTier, tierOrder);
32161
+ const escalatedTier = escalationResult?.tier ?? null;
32162
+ const escalatedAgent = escalationResult?.agent;
32163
+ if (escalatedTier === null) {
32164
+ return false;
32165
+ }
31737
32166
  const agentName = escalatedAgent ?? story.routing?.agent ?? config2.autoMode.defaultAgent;
31738
32167
  const agent = agentGetFn ? agentGetFn(agentName) : _rectificationDeps.getAgent(agentName, config2);
31739
32168
  if (!agent) {
31740
32169
  return false;
31741
32170
  }
31742
32171
  const escalatedModelDef = resolveModelForAgent(config2.models, agentName, escalatedTier, config2.autoMode.defaultAgent);
31743
- let escalationPrompt = createEscalatedRectificationPrompt(testSummary.failures, story, rectificationState.attempt, currentTier, escalatedTier, rectificationConfig);
31744
- if (promptPrefix)
32172
+ let escalationPrompt = createEscalatedRectificationPrompt(testSummary.failures, story, state.attempt, currentTier, escalatedTier, rectificationConfig);
32173
+ if (promptPrefix) {
31745
32174
  escalationPrompt = `${promptPrefix}
31746
32175
 
31747
32176
  ${escalationPrompt}`;
32177
+ }
31748
32178
  const escalationRunResult = await agent.run({
31749
32179
  prompt: escalationPrompt,
31750
32180
  workdir,
@@ -31787,9 +32217,14 @@ ${escalationPrompt}`;
31787
32217
  return true;
31788
32218
  }
31789
32219
  logger?.warn("rectification", "escalated rectification also failed", { storyId: story.id, escalatedTier });
32220
+ return false;
31790
32221
  }
31791
- }
31792
- return false;
32222
+ }).catch((error48) => {
32223
+ if (error48 instanceof Error && error48.message === "RECTIFICATION_AGENT_NOT_FOUND") {
32224
+ return false;
32225
+ }
32226
+ throw error48;
32227
+ });
31793
32228
  }
31794
32229
  var _rectificationDeps;
31795
32230
  var init_rectification_loop = __esm(() => {
@@ -32450,7 +32885,7 @@ var init_regression2 = __esm(() => {
32450
32885
  });
32451
32886
 
32452
32887
  // src/pipeline/stages/routing.ts
32453
- import { join as join26 } from "path";
32888
+ import { join as join27 } from "path";
32454
32889
  var routingStage, _routingDeps;
32455
32890
  var init_routing2 = __esm(() => {
32456
32891
  init_registry();
@@ -32487,7 +32922,7 @@ var init_routing2 = __esm(() => {
32487
32922
  }
32488
32923
  const greenfieldDetectionEnabled = effectiveConfig.tdd.greenfieldDetection ?? true;
32489
32924
  if (greenfieldDetectionEnabled && routing.testStrategy.startsWith("three-session-tdd")) {
32490
- const greenfieldScanDir = ctx.story.workdir ? join26(ctx.workdir, ctx.story.workdir) : ctx.workdir;
32925
+ const greenfieldScanDir = ctx.story.workdir ? join27(ctx.workdir, ctx.story.workdir) : ctx.workdir;
32491
32926
  const isGreenfield = await _routingDeps.isGreenfieldStory(ctx.story, greenfieldScanDir);
32492
32927
  if (isGreenfield) {
32493
32928
  logger.info("routing", "Greenfield detected \u2014 forcing test-after strategy", {
@@ -32539,7 +32974,7 @@ var init_crash_detector = __esm(() => {
32539
32974
  });
32540
32975
 
32541
32976
  // src/pipeline/stages/verify.ts
32542
- import { join as join27 } from "path";
32977
+ import { join as join28 } from "path";
32543
32978
  function coerceSmartTestRunner(val) {
32544
32979
  if (val === undefined || val === true)
32545
32980
  return DEFAULT_SMART_RUNNER_CONFIG2;
@@ -32555,7 +32990,7 @@ function buildScopedCommand2(testFiles, baseCommand, testScopedTemplate) {
32555
32990
  }
32556
32991
  async function readPackageName(dir) {
32557
32992
  try {
32558
- const content = await Bun.file(join27(dir, "package.json")).json();
32993
+ const content = await Bun.file(join28(dir, "package.json")).json();
32559
32994
  return typeof content.name === "string" ? content.name : null;
32560
32995
  } catch {
32561
32996
  return null;
@@ -32600,7 +33035,7 @@ var init_verify = __esm(() => {
32600
33035
  return { action: "continue" };
32601
33036
  }
32602
33037
  logger.info("verify", "Running verification", { storyId: ctx.story.id });
32603
- const effectiveWorkdir = ctx.story.workdir ? join27(ctx.workdir, ctx.story.workdir) : ctx.workdir;
33038
+ const effectiveWorkdir = ctx.story.workdir ? join28(ctx.workdir, ctx.story.workdir) : ctx.workdir;
32604
33039
  let effectiveCommand = testCommand;
32605
33040
  let isFullSuite = true;
32606
33041
  const smartRunnerConfig = coerceSmartTestRunner(effectiveConfig.execution.smartTestRunner);
@@ -32818,7 +33253,7 @@ __export(exports_init_context, {
32818
33253
  });
32819
33254
  import { existsSync as existsSync24 } from "fs";
32820
33255
  import { mkdir as mkdir2 } from "fs/promises";
32821
- import { basename as basename3, join as join31 } from "path";
33256
+ import { basename as basename3, join as join32 } from "path";
32822
33257
  async function findFiles(dir, maxFiles = 200) {
32823
33258
  try {
32824
33259
  const proc = Bun.spawnSync([
@@ -32846,7 +33281,7 @@ async function findFiles(dir, maxFiles = 200) {
32846
33281
  return [];
32847
33282
  }
32848
33283
  async function readPackageManifest(projectRoot) {
32849
- const packageJsonPath = join31(projectRoot, "package.json");
33284
+ const packageJsonPath = join32(projectRoot, "package.json");
32850
33285
  if (!existsSync24(packageJsonPath)) {
32851
33286
  return null;
32852
33287
  }
@@ -32864,7 +33299,7 @@ async function readPackageManifest(projectRoot) {
32864
33299
  }
32865
33300
  }
32866
33301
  async function readReadmeSnippet(projectRoot) {
32867
- const readmePath = join31(projectRoot, "README.md");
33302
+ const readmePath = join32(projectRoot, "README.md");
32868
33303
  if (!existsSync24(readmePath)) {
32869
33304
  return null;
32870
33305
  }
@@ -32882,7 +33317,7 @@ async function detectEntryPoints(projectRoot) {
32882
33317
  const candidates = ["src/index.ts", "src/main.ts", "main.go", "src/lib.rs"];
32883
33318
  const found = [];
32884
33319
  for (const candidate of candidates) {
32885
- const path12 = join31(projectRoot, candidate);
33320
+ const path12 = join32(projectRoot, candidate);
32886
33321
  if (existsSync24(path12)) {
32887
33322
  found.push(candidate);
32888
33323
  }
@@ -32893,7 +33328,7 @@ async function detectConfigFiles(projectRoot) {
32893
33328
  const candidates = ["tsconfig.json", "biome.json", "turbo.json", ".env.example"];
32894
33329
  const found = [];
32895
33330
  for (const candidate of candidates) {
32896
- const path12 = join31(projectRoot, candidate);
33331
+ const path12 = join32(projectRoot, candidate);
32897
33332
  if (existsSync24(path12)) {
32898
33333
  found.push(candidate);
32899
33334
  }
@@ -33054,8 +33489,8 @@ function generatePackageContextTemplate(packagePath) {
33054
33489
  }
33055
33490
  async function initPackage(repoRoot, packagePath, force = false) {
33056
33491
  const logger = getLogger();
33057
- const naxDir = join31(repoRoot, ".nax", "mono", packagePath);
33058
- const contextPath = join31(naxDir, "context.md");
33492
+ const naxDir = join32(repoRoot, ".nax", "mono", packagePath);
33493
+ const contextPath = join32(naxDir, "context.md");
33059
33494
  if (existsSync24(contextPath) && !force) {
33060
33495
  logger.info("init", "Package context.md already exists (use --force to overwrite)", { path: contextPath });
33061
33496
  return;
@@ -33069,8 +33504,8 @@ async function initPackage(repoRoot, packagePath, force = false) {
33069
33504
  }
33070
33505
  async function initContext(projectRoot, options = {}) {
33071
33506
  const logger = getLogger();
33072
- const naxDir = join31(projectRoot, ".nax");
33073
- const contextPath = join31(naxDir, "context.md");
33507
+ const naxDir = join32(projectRoot, ".nax");
33508
+ const contextPath = join32(naxDir, "context.md");
33074
33509
  if (existsSync24(contextPath) && !options.force) {
33075
33510
  logger.info("init", "context.md already exists, skipping (use --force to overwrite)", { path: contextPath });
33076
33511
  return;
@@ -33100,7 +33535,7 @@ var init_init_context = __esm(() => {
33100
33535
 
33101
33536
  // src/utils/path-security.ts
33102
33537
  import { realpathSync as realpathSync3 } from "fs";
33103
- import { dirname as dirname4, isAbsolute as isAbsolute4, join as join32, normalize as normalize2, resolve as resolve5 } from "path";
33538
+ import { dirname as dirname4, isAbsolute as isAbsolute4, join as join33, normalize as normalize2, resolve as resolve5 } from "path";
33104
33539
  function safeRealpathForComparison(p) {
33105
33540
  try {
33106
33541
  return realpathSync3(p);
@@ -33109,7 +33544,7 @@ function safeRealpathForComparison(p) {
33109
33544
  if (parent === p)
33110
33545
  return normalize2(p);
33111
33546
  const resolvedParent = safeRealpathForComparison(parent);
33112
- return join32(resolvedParent, p.split("/").pop() ?? "");
33547
+ return join33(resolvedParent, p.split("/").pop() ?? "");
33113
33548
  }
33114
33549
  }
33115
33550
  function validateModulePath(modulePath, allowedRoots) {
@@ -33127,7 +33562,7 @@ function validateModulePath(modulePath, allowedRoots) {
33127
33562
  } else {
33128
33563
  for (let i = 0;i < allowedRoots.length; i++) {
33129
33564
  const originalRoot = resolve5(allowedRoots[i]);
33130
- const absoluteInput = resolve5(join32(originalRoot, modulePath));
33565
+ const absoluteInput = resolve5(join33(originalRoot, modulePath));
33131
33566
  const resolved = safeRealpathForComparison(absoluteInput);
33132
33567
  const resolvedRoot = resolvedRoots[i];
33133
33568
  if (resolved.startsWith(`${resolvedRoot}/`) || resolved === resolvedRoot) {
@@ -33663,19 +34098,19 @@ var init_loader4 = __esm(() => {
33663
34098
  });
33664
34099
 
33665
34100
  // src/hooks/runner.ts
33666
- import { join as join46 } from "path";
34101
+ import { join as join48 } from "path";
33667
34102
  async function loadHooksConfig(projectDir, globalDir) {
33668
34103
  let globalHooks = { hooks: {} };
33669
34104
  let projectHooks = { hooks: {} };
33670
34105
  let skipGlobal = false;
33671
- const projectPath = join46(projectDir, "hooks.json");
34106
+ const projectPath = join48(projectDir, "hooks.json");
33672
34107
  const projectData = await loadJsonFile(projectPath, "hooks");
33673
34108
  if (projectData) {
33674
34109
  projectHooks = projectData;
33675
34110
  skipGlobal = projectData.skipGlobal ?? false;
33676
34111
  }
33677
34112
  if (!skipGlobal && globalDir) {
33678
- const globalPath = join46(globalDir, "hooks.json");
34113
+ const globalPath = join48(globalDir, "hooks.json");
33679
34114
  const globalData = await loadJsonFile(globalPath, "hooks");
33680
34115
  if (globalData) {
33681
34116
  globalHooks = globalData;
@@ -34214,7 +34649,7 @@ async function diagnoseAcceptanceFailure(agent, options) {
34214
34649
  reasoning: "diagnosis failed \u2014 falling back to source fix",
34215
34650
  confidence: 0
34216
34651
  };
34217
- } catch (err) {
34652
+ } catch {
34218
34653
  return {
34219
34654
  verdict: "source_bug",
34220
34655
  reasoning: "diagnosis failed \u2014 falling back to source fix",
@@ -34324,11 +34759,12 @@ var init_fix_executor = __esm(() => {
34324
34759
  var exports_acceptance_loop = {};
34325
34760
  __export(exports_acceptance_loop, {
34326
34761
  runAcceptanceLoop: () => runAcceptanceLoop,
34762
+ regenerateAcceptanceTest: () => regenerateAcceptanceTest,
34327
34763
  isTestLevelFailure: () => isTestLevelFailure,
34328
34764
  isStubTestFile: () => isStubTestFile,
34329
34765
  _acceptanceLoopDeps: () => _acceptanceLoopDeps
34330
34766
  });
34331
- import path14, { join as join47 } from "path";
34767
+ import path14, { join as join49 } from "path";
34332
34768
  function isStubTestFile(content) {
34333
34769
  return /expect\s*\(\s*true\s*\)\s*\.\s*toBe\s*\(\s*(?:false|true)\s*\)/.test(content);
34334
34770
  }
@@ -34399,7 +34835,7 @@ async function executeFixStory(ctx, story, prd, iterations) {
34399
34835
  agent: ctx.config.autoMode.defaultAgent,
34400
34836
  iteration: iterations
34401
34837
  }), ctx.workdir);
34402
- const fixEffectiveConfig = story.workdir ? await loadConfigForWorkdir(join47(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
34838
+ const fixEffectiveConfig = story.workdir ? await loadConfigForWorkdir(join49(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
34403
34839
  const fixContext = {
34404
34840
  config: ctx.config,
34405
34841
  effectiveConfig: fixEffectiveConfig,
@@ -34882,6 +35318,16 @@ async function runDeferredRegression(options) {
34882
35318
  };
34883
35319
  }
34884
35320
  const testSummary = _regressionDeps.parseBunTestOutput(fullSuiteResult.output);
35321
+ if (testSummary.failed === 0 && testSummary.passed === 0) {
35322
+ logger?.warn("regression", "No test results parsed from output \u2014 test runner likely crashed or errored (not a regression, accepting as pass)", { output: fullSuiteResult.output.slice(0, 500) });
35323
+ return {
35324
+ success: true,
35325
+ failedTests: 0,
35326
+ passedTests: 0,
35327
+ rectificationAttempts: 0,
35328
+ affectedStories: []
35329
+ };
35330
+ }
34885
35331
  const affectedStories = new Set;
34886
35332
  const affectedStoriesObjs = new Map;
34887
35333
  logger?.warn("regression", "Regression detected", {
@@ -35202,12 +35648,12 @@ var init_headless_formatter = __esm(() => {
35202
35648
  // src/pipeline/subscribers/events-writer.ts
35203
35649
  import { appendFile as appendFile3, mkdir as mkdir3 } from "fs/promises";
35204
35650
  import { homedir as homedir5 } from "os";
35205
- import { basename as basename6, join as join48 } from "path";
35651
+ import { basename as basename6, join as join50 } from "path";
35206
35652
  function wireEventsWriter(bus, feature, runId, workdir) {
35207
35653
  const logger = getSafeLogger();
35208
35654
  const project = basename6(workdir);
35209
- const eventsDir = join48(homedir5(), ".nax", "events", project);
35210
- const eventsFile = join48(eventsDir, "events.jsonl");
35655
+ const eventsDir = join50(homedir5(), ".nax", "events", project);
35656
+ const eventsFile = join50(eventsDir, "events.jsonl");
35211
35657
  let dirReady = false;
35212
35658
  const write = (line) => {
35213
35659
  return (async () => {
@@ -35388,12 +35834,12 @@ var init_interaction2 = __esm(() => {
35388
35834
  // src/pipeline/subscribers/registry.ts
35389
35835
  import { mkdir as mkdir4, writeFile } from "fs/promises";
35390
35836
  import { homedir as homedir6 } from "os";
35391
- import { basename as basename7, join as join49 } from "path";
35837
+ import { basename as basename7, join as join51 } from "path";
35392
35838
  function wireRegistry(bus, feature, runId, workdir) {
35393
35839
  const logger = getSafeLogger();
35394
35840
  const project = basename7(workdir);
35395
- const runDir = join49(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
35396
- const metaFile = join49(runDir, "meta.json");
35841
+ const runDir = join51(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
35842
+ const metaFile = join51(runDir, "meta.json");
35397
35843
  const unsub = bus.on("run:started", (_ev) => {
35398
35844
  return (async () => {
35399
35845
  try {
@@ -35403,8 +35849,8 @@ function wireRegistry(bus, feature, runId, workdir) {
35403
35849
  project,
35404
35850
  feature,
35405
35851
  workdir,
35406
- statusPath: join49(workdir, ".nax", "features", feature, "status.json"),
35407
- eventsDir: join49(workdir, ".nax", "features", feature, "runs"),
35852
+ statusPath: join51(workdir, ".nax", "features", feature, "status.json"),
35853
+ eventsDir: join51(workdir, ".nax", "features", feature, "runs"),
35408
35854
  registeredAt: new Date().toISOString()
35409
35855
  };
35410
35856
  await writeFile(metaFile, JSON.stringify(meta3, null, 2));
@@ -36056,7 +36502,7 @@ var init_pipeline_result_handler = __esm(() => {
36056
36502
  });
36057
36503
 
36058
36504
  // src/execution/iteration-runner.ts
36059
- import { join as join50 } from "path";
36505
+ import { join as join52 } from "path";
36060
36506
  async function runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics) {
36061
36507
  const logger = getSafeLogger();
36062
36508
  const { story, storiesToExecute, routing, isBatchExecution } = selection;
@@ -36091,7 +36537,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
36091
36537
  }
36092
36538
  }
36093
36539
  const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
36094
- const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join50(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
36540
+ const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join52(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
36095
36541
  const pipelineContext = {
36096
36542
  config: ctx.config,
36097
36543
  effectiveConfig,
@@ -36350,13 +36796,13 @@ __export(exports_manager, {
36350
36796
  });
36351
36797
  import { existsSync as existsSync32, symlinkSync } from "fs";
36352
36798
  import { mkdir as mkdir5 } from "fs/promises";
36353
- import { join as join51 } from "path";
36799
+ import { join as join53 } from "path";
36354
36800
 
36355
36801
  class WorktreeManager {
36356
36802
  async ensureGitExcludes(projectRoot) {
36357
36803
  const logger = getSafeLogger();
36358
- const infoDir = join51(projectRoot, ".git", "info");
36359
- const excludePath = join51(infoDir, "exclude");
36804
+ const infoDir = join53(projectRoot, ".git", "info");
36805
+ const excludePath = join53(infoDir, "exclude");
36360
36806
  try {
36361
36807
  await mkdir5(infoDir, { recursive: true });
36362
36808
  let existing = "";
@@ -36383,7 +36829,7 @@ ${missing.join(`
36383
36829
  }
36384
36830
  async create(projectRoot, storyId) {
36385
36831
  validateStoryId(storyId);
36386
- const worktreePath = join51(projectRoot, ".nax-wt", storyId);
36832
+ const worktreePath = join53(projectRoot, ".nax-wt", storyId);
36387
36833
  const branchName = `nax/${storyId}`;
36388
36834
  try {
36389
36835
  const pruneProc = _managerDeps.spawn(["git", "worktree", "prune"], {
@@ -36424,9 +36870,9 @@ ${missing.join(`
36424
36870
  }
36425
36871
  throw new Error(`Failed to create worktree: ${String(error48)}`);
36426
36872
  }
36427
- const nodeModulesSource = join51(projectRoot, "node_modules");
36873
+ const nodeModulesSource = join53(projectRoot, "node_modules");
36428
36874
  if (existsSync32(nodeModulesSource)) {
36429
- const nodeModulesTarget = join51(worktreePath, "node_modules");
36875
+ const nodeModulesTarget = join53(worktreePath, "node_modules");
36430
36876
  try {
36431
36877
  symlinkSync(nodeModulesSource, nodeModulesTarget, "dir");
36432
36878
  } catch (error48) {
@@ -36434,9 +36880,9 @@ ${missing.join(`
36434
36880
  throw new Error(`Failed to symlink node_modules: ${errorMessage(error48)}`);
36435
36881
  }
36436
36882
  }
36437
- const envSource = join51(projectRoot, ".env");
36883
+ const envSource = join53(projectRoot, ".env");
36438
36884
  if (existsSync32(envSource)) {
36439
- const envTarget = join51(worktreePath, ".env");
36885
+ const envTarget = join53(worktreePath, ".env");
36440
36886
  try {
36441
36887
  symlinkSync(envSource, envTarget, "file");
36442
36888
  } catch (error48) {
@@ -36447,7 +36893,7 @@ ${missing.join(`
36447
36893
  }
36448
36894
  async remove(projectRoot, storyId) {
36449
36895
  validateStoryId(storyId);
36450
- const worktreePath = join51(projectRoot, ".nax-wt", storyId);
36896
+ const worktreePath = join53(projectRoot, ".nax-wt", storyId);
36451
36897
  const branchName = `nax/${storyId}`;
36452
36898
  try {
36453
36899
  const proc = _managerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
@@ -37063,13 +37509,25 @@ async function executeUnified(ctx, initialPrd) {
37063
37509
  prd = await loadPRD(ctx.prdPath);
37064
37510
  prdDirty = false;
37065
37511
  }
37512
+ const storyCounts = countStories(prd);
37513
+ logger?.debug("execution", "Loop iteration", {
37514
+ iteration: iterations,
37515
+ isComplete: isComplete(prd),
37516
+ passed: storyCounts.passed,
37517
+ pending: storyCounts.pending,
37518
+ failed: storyCounts.failed,
37519
+ total: storyCounts.total
37520
+ });
37066
37521
  if (isComplete(prd)) {
37522
+ logger?.debug("execution", "All stories complete \u2014 entering completion path");
37067
37523
  if (ctx.interactionChain && isTriggerEnabled("pre-merge", ctx.config)) {
37068
37524
  const shouldProceed = await checkPreMerge({ featureName: ctx.feature, totalStories: prd.userStories.length, cost: totalCost }, ctx.config, ctx.interactionChain);
37069
37525
  if (!shouldProceed)
37070
37526
  return buildResult2("pre-merge-aborted");
37071
37527
  }
37528
+ logger?.debug("execution", "Running deferred review");
37072
37529
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
37530
+ logger?.debug("execution", "Deferred review done \u2014 returning completed");
37073
37531
  return buildResult2("completed");
37074
37532
  }
37075
37533
  const costLimit = ctx.config.execution.costLimit;
@@ -37322,9 +37780,7 @@ async function executeUnified(ctx, initialPrd) {
37322
37780
  }, ctx.eventEmitter);
37323
37781
  }
37324
37782
  return buildResult2("max-iterations");
37325
- } finally {
37326
- stopHeartbeat();
37327
- }
37783
+ } finally {}
37328
37784
  }
37329
37785
  var _unifiedExecutorDeps;
37330
37786
  var init_unified_executor = __esm(() => {
@@ -37356,16 +37812,16 @@ var init_unified_executor = __esm(() => {
37356
37812
  });
37357
37813
 
37358
37814
  // src/project/detector.ts
37359
- import { join as join52 } from "path";
37815
+ import { join as join54 } from "path";
37360
37816
  async function detectLanguage(workdir, pkg) {
37361
37817
  const deps = _detectorDeps;
37362
- if (await deps.fileExists(join52(workdir, "go.mod")))
37818
+ if (await deps.fileExists(join54(workdir, "go.mod")))
37363
37819
  return "go";
37364
- if (await deps.fileExists(join52(workdir, "Cargo.toml")))
37820
+ if (await deps.fileExists(join54(workdir, "Cargo.toml")))
37365
37821
  return "rust";
37366
- if (await deps.fileExists(join52(workdir, "pyproject.toml")))
37822
+ if (await deps.fileExists(join54(workdir, "pyproject.toml")))
37367
37823
  return "python";
37368
- if (await deps.fileExists(join52(workdir, "requirements.txt")))
37824
+ if (await deps.fileExists(join54(workdir, "requirements.txt")))
37369
37825
  return "python";
37370
37826
  if (pkg != null) {
37371
37827
  const allDeps = {
@@ -37425,18 +37881,18 @@ async function detectLintTool(workdir, language) {
37425
37881
  if (language === "python")
37426
37882
  return "ruff";
37427
37883
  const deps = _detectorDeps;
37428
- if (await deps.fileExists(join52(workdir, "biome.json")))
37884
+ if (await deps.fileExists(join54(workdir, "biome.json")))
37429
37885
  return "biome";
37430
- if (await deps.fileExists(join52(workdir, ".eslintrc")))
37886
+ if (await deps.fileExists(join54(workdir, ".eslintrc")))
37431
37887
  return "eslint";
37432
- if (await deps.fileExists(join52(workdir, ".eslintrc.js")))
37888
+ if (await deps.fileExists(join54(workdir, ".eslintrc.js")))
37433
37889
  return "eslint";
37434
- if (await deps.fileExists(join52(workdir, ".eslintrc.json")))
37890
+ if (await deps.fileExists(join54(workdir, ".eslintrc.json")))
37435
37891
  return "eslint";
37436
37892
  return;
37437
37893
  }
37438
37894
  async function detectProjectProfile(workdir, existing) {
37439
- const pkg = await _detectorDeps.readJson(join52(workdir, "package.json"));
37895
+ const pkg = await _detectorDeps.readJson(join54(workdir, "package.json"));
37440
37896
  const language = existing.language !== undefined ? existing.language : await detectLanguage(workdir, pkg);
37441
37897
  const type = existing.type !== undefined ? existing.type : detectType(pkg);
37442
37898
  const testFramework = existing.testFramework !== undefined ? existing.testFramework : await detectTestFramework(workdir, language, pkg);
@@ -37529,7 +37985,7 @@ async function writeStatusFile(filePath, status) {
37529
37985
  var init_status_file = () => {};
37530
37986
 
37531
37987
  // src/execution/status-writer.ts
37532
- import { join as join53 } from "path";
37988
+ import { join as join55 } from "path";
37533
37989
 
37534
37990
  class StatusWriter {
37535
37991
  statusFile;
@@ -37603,7 +38059,7 @@ class StatusWriter {
37603
38059
  if (!this._prd)
37604
38060
  return;
37605
38061
  const safeLogger = getSafeLogger();
37606
- const featureStatusPath = join53(featureDir, "status.json");
38062
+ const featureStatusPath = join55(featureDir, "status.json");
37607
38063
  const write = async () => {
37608
38064
  try {
37609
38065
  const base = this.getSnapshot(totalCost, iterations);
@@ -37719,7 +38175,7 @@ var exports_precheck_runner = {};
37719
38175
  __export(exports_precheck_runner, {
37720
38176
  runPrecheckValidation: () => runPrecheckValidation
37721
38177
  });
37722
- import { mkdirSync as mkdirSync4 } from "fs";
38178
+ import { mkdirSync as mkdirSync5 } from "fs";
37723
38179
  import path17 from "path";
37724
38180
  async function runPrecheckValidation(ctx) {
37725
38181
  const logger = getSafeLogger();
@@ -37734,7 +38190,7 @@ async function runPrecheckValidation(ctx) {
37734
38190
  format: "human"
37735
38191
  });
37736
38192
  if (ctx.logFilePath) {
37737
- mkdirSync4(path17.dirname(ctx.logFilePath), { recursive: true });
38193
+ mkdirSync5(path17.dirname(ctx.logFilePath), { recursive: true });
37738
38194
  const precheckLog = {
37739
38195
  type: "precheck",
37740
38196
  timestamp: new Date().toISOString(),
@@ -37814,7 +38270,7 @@ __export(exports_run_initialization, {
37814
38270
  initializeRun: () => initializeRun,
37815
38271
  _reconcileDeps: () => _reconcileDeps
37816
38272
  });
37817
- import { join as join54 } from "path";
38273
+ import { join as join56 } from "path";
37818
38274
  async function reconcileState(prd, prdPath, workdir, config2) {
37819
38275
  const logger = getSafeLogger();
37820
38276
  let reconciledCount = 0;
@@ -37832,7 +38288,7 @@ async function reconcileState(prd, prdPath, workdir, config2) {
37832
38288
  });
37833
38289
  continue;
37834
38290
  }
37835
- const effectiveWorkdir = story.workdir ? join54(workdir, story.workdir) : workdir;
38291
+ const effectiveWorkdir = story.workdir ? join56(workdir, story.workdir) : workdir;
37836
38292
  try {
37837
38293
  const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
37838
38294
  if (!reviewResult.success) {
@@ -69040,9 +69496,9 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
69040
69496
 
69041
69497
  // bin/nax.ts
69042
69498
  init_source();
69043
- import { existsSync as existsSync34, mkdirSync as mkdirSync5 } from "fs";
69499
+ import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
69044
69500
  import { homedir as homedir8 } from "os";
69045
- import { join as join56 } from "path";
69501
+ import { join as join58 } from "path";
69046
69502
 
69047
69503
  // node_modules/commander/esm.mjs
69048
69504
  var import__ = __toESM(require_commander(), 1);
@@ -69064,14 +69520,14 @@ var {
69064
69520
  init_acceptance();
69065
69521
  init_registry();
69066
69522
  import { existsSync as existsSync8 } from "fs";
69067
- import { join as join8 } from "path";
69523
+ import { join as join9 } from "path";
69068
69524
 
69069
69525
  // src/analyze/scanner.ts
69070
- import { existsSync as existsSync5, readdirSync } from "fs";
69071
- import { join as join6 } from "path";
69526
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
69527
+ import { join as join7 } from "path";
69072
69528
  async function scanCodebase(workdir) {
69073
- const srcPath = join6(workdir, "src");
69074
- const packageJsonPath = join6(workdir, "package.json");
69529
+ const srcPath = join7(workdir, "src");
69530
+ const packageJsonPath = join7(workdir, "package.json");
69075
69531
  const fileTree = existsSync5(srcPath) ? await generateFileTree(srcPath, 3) : "No src/ directory";
69076
69532
  let dependencies = {};
69077
69533
  let devDependencies = {};
@@ -69096,7 +69552,7 @@ async function generateFileTree(dir, maxDepth, currentDepth = 0, prefix = "") {
69096
69552
  }
69097
69553
  const entries = [];
69098
69554
  try {
69099
- const dirEntries = readdirSync(dir, { withFileTypes: true });
69555
+ const dirEntries = readdirSync2(dir, { withFileTypes: true });
69100
69556
  dirEntries.sort((a, b) => {
69101
69557
  if (a.isDirectory() && !b.isDirectory())
69102
69558
  return -1;
@@ -69112,7 +69568,7 @@ async function generateFileTree(dir, maxDepth, currentDepth = 0, prefix = "") {
69112
69568
  const isDir = dirent.isDirectory();
69113
69569
  entries.push(`${prefix}${connector}${dirent.name}${isDir ? "/" : ""}`);
69114
69570
  if (isDir) {
69115
- const subtree = await generateFileTree(join6(dir, dirent.name), maxDepth, currentDepth + 1, prefix + childPrefix);
69571
+ const subtree = await generateFileTree(join7(dir, dirent.name), maxDepth, currentDepth + 1, prefix + childPrefix);
69116
69572
  if (subtree) {
69117
69573
  entries.push(subtree);
69118
69574
  }
@@ -69136,16 +69592,16 @@ function detectTestPatterns(workdir, dependencies, devDependencies) {
69136
69592
  } else {
69137
69593
  patterns.push("Test framework: likely bun:test (no framework dependency)");
69138
69594
  }
69139
- if (existsSync5(join6(workdir, "test"))) {
69595
+ if (existsSync5(join7(workdir, "test"))) {
69140
69596
  patterns.push("Test directory: test/");
69141
69597
  }
69142
- if (existsSync5(join6(workdir, "__tests__"))) {
69598
+ if (existsSync5(join7(workdir, "__tests__"))) {
69143
69599
  patterns.push("Test directory: __tests__/");
69144
69600
  }
69145
- if (existsSync5(join6(workdir, "tests"))) {
69601
+ if (existsSync5(join7(workdir, "tests"))) {
69146
69602
  patterns.push("Test directory: tests/");
69147
69603
  }
69148
- const hasTestFiles = existsSync5(join6(workdir, "test")) || existsSync5(join6(workdir, "src"));
69604
+ const hasTestFiles = existsSync5(join7(workdir, "test")) || existsSync5(join7(workdir, "src"));
69149
69605
  if (hasTestFiles) {
69150
69606
  patterns.push("Test files: *.test.ts, *.spec.ts");
69151
69607
  }
@@ -69161,7 +69617,7 @@ init_version();
69161
69617
  // src/cli/analyze-parser.ts
69162
69618
  init_registry();
69163
69619
  import { existsSync as existsSync7 } from "fs";
69164
- import { join as join7 } from "path";
69620
+ import { join as join8 } from "path";
69165
69621
  init_config();
69166
69622
  init_logger2();
69167
69623
  init_prd();
@@ -69291,7 +69747,7 @@ function estimateLOCFromComplexity(complexity) {
69291
69747
  }
69292
69748
  }
69293
69749
  async function reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config2) {
69294
- const prdPath = join7(featureDir, "prd.json");
69750
+ const prdPath = join8(featureDir, "prd.json");
69295
69751
  if (!existsSync7(prdPath)) {
69296
69752
  throw new Error(`prd.json not found at ${prdPath}. Run analyze without --reclassify first.`);
69297
69753
  }
@@ -69392,11 +69848,11 @@ function reclassifyWithKeywords(story, config2) {
69392
69848
  // src/cli/analyze.ts
69393
69849
  async function analyzeFeature(options) {
69394
69850
  const { featureDir, featureName, branchName, config: config2, specPath: explicitSpecPath, reclassify = false } = options;
69395
- const workdir = join8(featureDir, "../..");
69851
+ const workdir = join9(featureDir, "../..");
69396
69852
  if (reclassify) {
69397
69853
  return await reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config2);
69398
69854
  }
69399
- const specPath = explicitSpecPath || join8(featureDir, "spec.md");
69855
+ const specPath = explicitSpecPath || join9(featureDir, "spec.md");
69400
69856
  if (!existsSync8(specPath))
69401
69857
  throw new Error(`spec.md not found at ${specPath}`);
69402
69858
  const specContent = await Bun.file(specPath).text();
@@ -69513,7 +69969,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
69513
69969
  modelDef,
69514
69970
  config: config2
69515
69971
  });
69516
- const acceptanceTestPath = join8(featureDir, config2.acceptance.testPath);
69972
+ const acceptanceTestPath = join9(featureDir, config2.acceptance.testPath);
69517
69973
  await Bun.write(acceptanceTestPath, result.testCode);
69518
69974
  logger.info("cli", "[OK] Acceptance tests generated", {
69519
69975
  criteriaCount: result.criteria.length,
@@ -69526,18 +69982,18 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
69526
69982
  // src/cli/plan.ts
69527
69983
  init_registry();
69528
69984
  import { existsSync as existsSync15 } from "fs";
69529
- import { join as join12 } from "path";
69985
+ import { join as join13 } from "path";
69530
69986
  import { createInterface as createInterface2 } from "readline";
69531
69987
  init_test_strategy();
69532
69988
 
69533
69989
  // src/context/generator.ts
69534
69990
  init_path_security();
69535
69991
  import { existsSync as existsSync10, readFileSync } from "fs";
69536
- import { join as join10, relative } from "path";
69992
+ import { join as join11, relative } from "path";
69537
69993
 
69538
69994
  // src/context/injector.ts
69539
69995
  import { existsSync as existsSync9 } from "fs";
69540
- import { join as join9 } from "path";
69996
+ import { join as join10 } from "path";
69541
69997
  var NOTABLE_NODE_DEPS = [
69542
69998
  "@nestjs",
69543
69999
  "express",
@@ -69567,7 +70023,7 @@ var NOTABLE_NODE_DEPS = [
69567
70023
  "ioredis"
69568
70024
  ];
69569
70025
  async function detectNode(workdir) {
69570
- const pkgPath = join9(workdir, "package.json");
70026
+ const pkgPath = join10(workdir, "package.json");
69571
70027
  if (!existsSync9(pkgPath))
69572
70028
  return null;
69573
70029
  try {
@@ -69584,7 +70040,7 @@ async function detectNode(workdir) {
69584
70040
  }
69585
70041
  }
69586
70042
  async function detectGo(workdir) {
69587
- const goMod = join9(workdir, "go.mod");
70043
+ const goMod = join10(workdir, "go.mod");
69588
70044
  if (!existsSync9(goMod))
69589
70045
  return null;
69590
70046
  try {
@@ -69608,7 +70064,7 @@ async function detectGo(workdir) {
69608
70064
  }
69609
70065
  }
69610
70066
  async function detectRust(workdir) {
69611
- const cargoPath = join9(workdir, "Cargo.toml");
70067
+ const cargoPath = join10(workdir, "Cargo.toml");
69612
70068
  if (!existsSync9(cargoPath))
69613
70069
  return null;
69614
70070
  try {
@@ -69624,8 +70080,8 @@ async function detectRust(workdir) {
69624
70080
  }
69625
70081
  }
69626
70082
  async function detectPython(workdir) {
69627
- const pyproject = join9(workdir, "pyproject.toml");
69628
- const requirements = join9(workdir, "requirements.txt");
70083
+ const pyproject = join10(workdir, "pyproject.toml");
70084
+ const requirements = join10(workdir, "requirements.txt");
69629
70085
  if (!existsSync9(pyproject) && !existsSync9(requirements))
69630
70086
  return null;
69631
70087
  try {
@@ -69644,7 +70100,7 @@ async function detectPython(workdir) {
69644
70100
  }
69645
70101
  }
69646
70102
  async function detectPhp(workdir) {
69647
- const composerPath = join9(workdir, "composer.json");
70103
+ const composerPath = join10(workdir, "composer.json");
69648
70104
  if (!existsSync9(composerPath))
69649
70105
  return null;
69650
70106
  try {
@@ -69657,7 +70113,7 @@ async function detectPhp(workdir) {
69657
70113
  }
69658
70114
  }
69659
70115
  async function detectRuby(workdir) {
69660
- const gemfile = join9(workdir, "Gemfile");
70116
+ const gemfile = join10(workdir, "Gemfile");
69661
70117
  if (!existsSync9(gemfile))
69662
70118
  return null;
69663
70119
  try {
@@ -69669,9 +70125,9 @@ async function detectRuby(workdir) {
69669
70125
  }
69670
70126
  }
69671
70127
  async function detectJvm(workdir) {
69672
- const pom = join9(workdir, "pom.xml");
69673
- const gradle = join9(workdir, "build.gradle");
69674
- const gradleKts = join9(workdir, "build.gradle.kts");
70128
+ const pom = join10(workdir, "pom.xml");
70129
+ const gradle = join10(workdir, "build.gradle");
70130
+ const gradleKts = join10(workdir, "build.gradle.kts");
69675
70131
  if (!existsSync9(pom) && !existsSync9(gradle) && !existsSync9(gradleKts))
69676
70132
  return null;
69677
70133
  try {
@@ -69679,7 +70135,7 @@ async function detectJvm(workdir) {
69679
70135
  const content2 = await Bun.file(pom).text();
69680
70136
  const nameMatch = content2.match(/<artifactId>([^<]+)<\/artifactId>/);
69681
70137
  const deps2 = [...content2.matchAll(/<artifactId>([^<]+)<\/artifactId>/g)].map((m) => m[1]).filter((d) => d !== nameMatch?.[1]).slice(0, 10);
69682
- const lang2 = existsSync9(join9(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
70138
+ const lang2 = existsSync9(join10(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
69683
70139
  return { name: nameMatch?.[1], lang: lang2, dependencies: deps2 };
69684
70140
  }
69685
70141
  const gradleFile = existsSync9(gradleKts) ? gradleKts : gradle;
@@ -69906,7 +70362,7 @@ async function generateFor(agent, options, config2) {
69906
70362
  try {
69907
70363
  const context = await loadContextContent(options, config2);
69908
70364
  const content = generator.generate(context);
69909
- const outputPath = join10(options.outputDir, generator.outputFile);
70365
+ const outputPath = join11(options.outputDir, generator.outputFile);
69910
70366
  validateFilePath(outputPath, options.outputDir);
69911
70367
  if (!options.dryRun) {
69912
70368
  await _generatorDeps.writeFile(outputPath, content);
@@ -69924,7 +70380,7 @@ async function generateAll(options, config2, agentFilter) {
69924
70380
  for (const [agentKey, generator] of entries) {
69925
70381
  try {
69926
70382
  const content = generator.generate(context);
69927
- const outputPath = join10(options.outputDir, generator.outputFile);
70383
+ const outputPath = join11(options.outputDir, generator.outputFile);
69928
70384
  validateFilePath(outputPath, options.outputDir);
69929
70385
  if (!options.dryRun) {
69930
70386
  await _generatorDeps.writeFile(outputPath, content);
@@ -69944,7 +70400,7 @@ async function discoverPackages(repoRoot) {
69944
70400
  const glob = new Bun.Glob(pattern);
69945
70401
  for await (const match of glob.scan({ cwd: repoRoot, dot: true })) {
69946
70402
  const pkgRelative = match.replace(/^\.nax\/mono\//, "").replace(/\/context\.md$/, "");
69947
- const pkgAbsolute = join10(repoRoot, pkgRelative);
70403
+ const pkgAbsolute = join11(repoRoot, pkgRelative);
69948
70404
  if (!seen.has(pkgAbsolute)) {
69949
70405
  seen.add(pkgAbsolute);
69950
70406
  packages.push(pkgAbsolute);
@@ -69976,7 +70432,7 @@ async function discoverWorkspacePackages(repoRoot) {
69976
70432
  }
69977
70433
  }
69978
70434
  }
69979
- const turboPath = join10(repoRoot, "turbo.json");
70435
+ const turboPath = join11(repoRoot, "turbo.json");
69980
70436
  if (_generatorDeps.existsSync(turboPath)) {
69981
70437
  try {
69982
70438
  const turbo = JSON.parse(_generatorDeps.readFileSync(turboPath, "utf-8"));
@@ -69985,7 +70441,7 @@ async function discoverWorkspacePackages(repoRoot) {
69985
70441
  }
69986
70442
  } catch {}
69987
70443
  }
69988
- const pkgPath = join10(repoRoot, "package.json");
70444
+ const pkgPath = join11(repoRoot, "package.json");
69989
70445
  if (_generatorDeps.existsSync(pkgPath)) {
69990
70446
  try {
69991
70447
  const pkg = JSON.parse(_generatorDeps.readFileSync(pkgPath, "utf-8"));
@@ -69995,7 +70451,7 @@ async function discoverWorkspacePackages(repoRoot) {
69995
70451
  await resolveGlobs(patterns);
69996
70452
  } catch {}
69997
70453
  }
69998
- const pnpmPath = join10(repoRoot, "pnpm-workspace.yaml");
70454
+ const pnpmPath = join11(repoRoot, "pnpm-workspace.yaml");
69999
70455
  if (_generatorDeps.existsSync(pnpmPath)) {
70000
70456
  try {
70001
70457
  const raw = _generatorDeps.readFileSync(pnpmPath, "utf-8");
@@ -70023,7 +70479,7 @@ async function discoverWorkspacePackages(repoRoot) {
70023
70479
  async function generateForPackage(packageDir, config2, dryRun = false, repoRoot) {
70024
70480
  const resolvedRepoRoot = repoRoot ?? packageDir;
70025
70481
  const relativePkgPath = relative(resolvedRepoRoot, packageDir);
70026
- const contextPath = join10(resolvedRepoRoot, ".nax", "mono", relativePkgPath, "context.md");
70482
+ const contextPath = join11(resolvedRepoRoot, ".nax", "mono", relativePkgPath, "context.md");
70027
70483
  if (!_generatorDeps.existsSync(contextPath)) {
70028
70484
  return [
70029
70485
  {
@@ -70249,12 +70705,13 @@ function validatePlanOutput(raw, feature, branch) {
70249
70705
  }
70250
70706
 
70251
70707
  // src/cli/plan.ts
70708
+ var DEFAULT_TIMEOUT_SECONDS2 = 600;
70252
70709
  var _planDeps = {
70253
70710
  readFile: (path) => Bun.file(path).text(),
70254
70711
  writeFile: (path, content) => Bun.write(path, content).then(() => {}),
70255
70712
  scanCodebase: (workdir) => scanCodebase(workdir),
70256
70713
  getAgent: (name, cfg) => cfg ? createAgentRegistry(cfg).getAgent(name) : getAgent(name),
70257
- readPackageJson: (workdir) => Bun.file(join12(workdir, "package.json")).json().catch(() => null),
70714
+ readPackageJson: (workdir) => Bun.file(join13(workdir, "package.json")).json().catch(() => null),
70258
70715
  spawnSync: (cmd, opts) => {
70259
70716
  const result = Bun.spawnSync(cmd, opts ? { cwd: opts.cwd } : {});
70260
70717
  return { stdout: result.stdout, exitCode: result.exitCode };
@@ -70274,7 +70731,7 @@ var _planDeps = {
70274
70731
  planDecompose: (workdir, config2, opts) => planDecomposeCommand(workdir, config2, opts)
70275
70732
  };
70276
70733
  async function planCommand(workdir, config2, options) {
70277
- const naxDir = join12(workdir, ".nax");
70734
+ const naxDir = join13(workdir, ".nax");
70278
70735
  if (!existsSync15(naxDir)) {
70279
70736
  throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
70280
70737
  }
@@ -70290,16 +70747,16 @@ async function planCommand(workdir, config2, options) {
70290
70747
  const codebaseContext = buildCodebaseContext2(scan);
70291
70748
  const relativePackages = discoveredPackages.map((p) => p.startsWith("/") ? p.replace(`${workdir}/`, "") : p);
70292
70749
  const packageDetails = relativePackages.length > 0 ? await Promise.all(relativePackages.map(async (rel) => {
70293
- const pkgJson = await _planDeps.readPackageJsonAt(join12(workdir, rel, "package.json"));
70750
+ const pkgJson = await _planDeps.readPackageJsonAt(join13(workdir, rel, "package.json"));
70294
70751
  return buildPackageSummary(rel, pkgJson);
70295
70752
  })) : [];
70296
70753
  const projectName = detectProjectName(workdir, pkg);
70297
70754
  const branchName = options.branch ?? `feat/${options.feature}`;
70298
- const outputDir = join12(naxDir, "features", options.feature);
70299
- const outputPath = join12(outputDir, "prd.json");
70755
+ const outputDir = join13(naxDir, "features", options.feature);
70756
+ const outputPath = join13(outputDir, "prd.json");
70300
70757
  await _planDeps.mkdirp(outputDir);
70301
70758
  const agentName = config2?.autoMode?.defaultAgent ?? "claude";
70302
- const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
70759
+ const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
70303
70760
  let rawResponse;
70304
70761
  const debateEnabled = config2?.debate?.enabled && config2?.debate?.stages?.plan?.enabled;
70305
70762
  if (debateEnabled) {
@@ -70383,6 +70840,7 @@ async function planCommand(workdir, config2, options) {
70383
70840
  }
70384
70841
  rawResponse = await _planDeps.readFile(outputPath);
70385
70842
  } else {
70843
+ const timeoutMs = (config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2) * 1000;
70386
70844
  const completeResult = await adapter.complete(prompt, {
70387
70845
  model: autoModel,
70388
70846
  jsonMode: true,
@@ -70390,7 +70848,7 @@ async function planCommand(workdir, config2, options) {
70390
70848
  config: config2,
70391
70849
  featureName: options.feature,
70392
70850
  sessionRole: "plan",
70393
- timeoutMs: (config2?.execution?.sessionTimeoutSeconds ?? 3600) * 1000
70851
+ timeoutMs
70394
70852
  });
70395
70853
  let result = typeof completeResult === "string" ? completeResult : completeResult.output;
70396
70854
  try {
@@ -70730,7 +71188,7 @@ Return JSON with this exact structure (no markdown, no explanation \u2014 JSON o
70730
71188
  }`;
70731
71189
  }
70732
71190
  async function planDecomposeCommand(workdir, config2, options) {
70733
- const prdPath = join12(workdir, ".nax", "features", options.feature, "prd.json");
71191
+ const prdPath = join13(workdir, ".nax", "features", options.feature, "prd.json");
70734
71192
  if (!_planDeps.existsSync(prdPath)) {
70735
71193
  throw new NaxError(`PRD not found: ${prdPath}`, "PRD_NOT_FOUND", {
70736
71194
  stage: "decompose",
@@ -70760,9 +71218,20 @@ async function planDecomposeCommand(workdir, config2, options) {
70760
71218
  const adapter = _planDeps.getAgent(agentName, config2);
70761
71219
  if (!adapter)
70762
71220
  throw new Error(`[decompose] No agent adapter found for '${agentName}'`);
71221
+ let decomposeModel;
71222
+ try {
71223
+ const planTier = config2?.plan?.model ?? "balanced";
71224
+ const { resolveModelForAgent: resolveModelForAgent2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
71225
+ if (config2?.models) {
71226
+ const defaultAgent = config2.autoMode?.defaultAgent ?? "claude";
71227
+ decomposeModel = resolveModelForAgent2(config2.models, defaultAgent, planTier, defaultAgent).model;
71228
+ }
71229
+ } catch {}
70763
71230
  const stages = config2?.debate?.stages;
70764
71231
  const debateEnabled = config2?.debate?.enabled && stages?.decompose?.enabled;
70765
71232
  let rawResponse;
71233
+ const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
71234
+ const timeoutMs = timeoutSeconds * 1000;
70766
71235
  if (debateEnabled) {
70767
71236
  const stageConfig = stages?.decompose;
70768
71237
  const debateSession = _planDeps.createDebateSession({
@@ -70772,30 +71241,32 @@ async function planDecomposeCommand(workdir, config2, options) {
70772
71241
  config: config2,
70773
71242
  workdir,
70774
71243
  featureName: options.feature,
70775
- timeoutSeconds: config2?.execution?.sessionTimeoutSeconds
71244
+ timeoutSeconds
70776
71245
  });
70777
71246
  const debateResult = await debateSession.run(prompt);
70778
71247
  if (debateResult.outcome !== "failed" && debateResult.output) {
70779
71248
  rawResponse = debateResult.output;
70780
71249
  } else {
70781
71250
  const completeResult = await adapter.complete(prompt, {
71251
+ model: decomposeModel,
70782
71252
  jsonMode: true,
70783
71253
  workdir,
70784
71254
  sessionRole: "decompose",
70785
71255
  featureName: options.feature,
70786
71256
  storyId: options.storyId,
70787
- timeoutMs: (config2?.execution?.sessionTimeoutSeconds ?? 3600) * 1000
71257
+ timeoutMs
70788
71258
  });
70789
71259
  rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
70790
71260
  }
70791
71261
  } else {
70792
71262
  const completeResult = await adapter.complete(prompt, {
71263
+ model: decomposeModel,
70793
71264
  jsonMode: true,
70794
71265
  workdir,
70795
71266
  sessionRole: "decompose",
70796
71267
  featureName: options.feature,
70797
71268
  storyId: options.storyId,
70798
- timeoutMs: (config2?.execution?.sessionTimeoutSeconds ?? 3600) * 1000
71269
+ timeoutMs
70799
71270
  });
70800
71271
  rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
70801
71272
  }
@@ -71019,14 +71490,14 @@ async function displayModelEfficiency(workdir) {
71019
71490
  }
71020
71491
  // src/cli/status-features.ts
71021
71492
  init_source();
71022
- import { existsSync as existsSync17, readdirSync as readdirSync3 } from "fs";
71023
- import { join as join15 } from "path";
71493
+ import { existsSync as existsSync17, readdirSync as readdirSync4 } from "fs";
71494
+ import { join as join16 } from "path";
71024
71495
 
71025
71496
  // src/commands/common.ts
71026
71497
  init_path_security();
71027
71498
  init_errors();
71028
- import { existsSync as existsSync16, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
71029
- import { join as join13, resolve as resolve4 } from "path";
71499
+ import { existsSync as existsSync16, readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
71500
+ import { join as join14, resolve as resolve4 } from "path";
71030
71501
  function resolveProject(options = {}) {
71031
71502
  const { dir, feature } = options;
71032
71503
  let projectRoot;
@@ -71034,12 +71505,12 @@ function resolveProject(options = {}) {
71034
71505
  let configPath;
71035
71506
  if (dir) {
71036
71507
  projectRoot = realpathSync2(resolve4(dir));
71037
- naxDir = join13(projectRoot, ".nax");
71508
+ naxDir = join14(projectRoot, ".nax");
71038
71509
  if (!existsSync16(naxDir)) {
71039
71510
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
71040
71511
  Expected to find: ${naxDir}`, "NAX_DIR_NOT_FOUND", { projectRoot, naxDir });
71041
71512
  }
71042
- configPath = join13(naxDir, "config.json");
71513
+ configPath = join14(naxDir, "config.json");
71043
71514
  if (!existsSync16(configPath)) {
71044
71515
  throw new NaxError(`.nax directory found but config.json is missing: ${naxDir}
71045
71516
  Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
@@ -71047,24 +71518,24 @@ Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
71047
71518
  } else {
71048
71519
  const found = findProjectRoot(process.cwd());
71049
71520
  if (!found) {
71050
- const cwdNaxDir = join13(process.cwd(), ".nax");
71521
+ const cwdNaxDir = join14(process.cwd(), ".nax");
71051
71522
  if (existsSync16(cwdNaxDir)) {
71052
- const cwdConfigPath = join13(cwdNaxDir, "config.json");
71523
+ const cwdConfigPath = join14(cwdNaxDir, "config.json");
71053
71524
  throw new NaxError(`.nax directory found but config.json is missing: ${cwdNaxDir}
71054
71525
  Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, configPath: cwdConfigPath });
71055
71526
  }
71056
71527
  throw new NaxError("No nax project found. Run this command from within a nax project directory, or use -d flag to specify the project path.", "PROJECT_NOT_FOUND", { cwd: process.cwd() });
71057
71528
  }
71058
71529
  projectRoot = found;
71059
- naxDir = join13(projectRoot, ".nax");
71060
- configPath = join13(naxDir, "config.json");
71530
+ naxDir = join14(projectRoot, ".nax");
71531
+ configPath = join14(naxDir, "config.json");
71061
71532
  }
71062
71533
  let featureDir;
71063
71534
  if (feature) {
71064
- const featuresDir = join13(naxDir, "features");
71065
- featureDir = join13(featuresDir, feature);
71535
+ const featuresDir = join14(naxDir, "features");
71536
+ featureDir = join14(featuresDir, feature);
71066
71537
  if (!existsSync16(featureDir)) {
71067
- const availableFeatures = existsSync16(featuresDir) ? readdirSync2(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
71538
+ const availableFeatures = existsSync16(featuresDir) ? readdirSync3(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
71068
71539
  const availableMsg = availableFeatures.length > 0 ? `
71069
71540
 
71070
71541
  Available features:
@@ -71089,12 +71560,12 @@ function findProjectRoot(startDir) {
71089
71560
  let current = resolve4(startDir);
71090
71561
  let depth = 0;
71091
71562
  while (depth < MAX_DIRECTORY_DEPTH) {
71092
- const naxDir = join13(current, ".nax");
71093
- const configPath = join13(naxDir, "config.json");
71563
+ const naxDir = join14(current, ".nax");
71564
+ const configPath = join14(naxDir, "config.json");
71094
71565
  if (existsSync16(configPath)) {
71095
71566
  return realpathSync2(current);
71096
71567
  }
71097
- const parent = join13(current, "..");
71568
+ const parent = join14(current, "..");
71098
71569
  if (parent === current) {
71099
71570
  break;
71100
71571
  }
@@ -71116,7 +71587,7 @@ function isPidAlive(pid) {
71116
71587
  }
71117
71588
  }
71118
71589
  async function loadStatusFile(featureDir) {
71119
- const statusPath = join15(featureDir, "status.json");
71590
+ const statusPath = join16(featureDir, "status.json");
71120
71591
  if (!existsSync17(statusPath)) {
71121
71592
  return null;
71122
71593
  }
@@ -71128,7 +71599,7 @@ async function loadStatusFile(featureDir) {
71128
71599
  }
71129
71600
  }
71130
71601
  async function loadProjectStatusFile(projectDir) {
71131
- const statusPath = join15(projectDir, ".nax", "status.json");
71602
+ const statusPath = join16(projectDir, ".nax", "status.json");
71132
71603
  if (!existsSync17(statusPath)) {
71133
71604
  return null;
71134
71605
  }
@@ -71140,7 +71611,7 @@ async function loadProjectStatusFile(projectDir) {
71140
71611
  }
71141
71612
  }
71142
71613
  async function getFeatureSummary(featureName, featureDir) {
71143
- const prdPath = join15(featureDir, "prd.json");
71614
+ const prdPath = join16(featureDir, "prd.json");
71144
71615
  if (!existsSync17(prdPath)) {
71145
71616
  return {
71146
71617
  name: featureName,
@@ -71183,9 +71654,9 @@ async function getFeatureSummary(featureName, featureDir) {
71183
71654
  };
71184
71655
  }
71185
71656
  }
71186
- const runsDir = join15(featureDir, "runs");
71657
+ const runsDir = join16(featureDir, "runs");
71187
71658
  if (existsSync17(runsDir)) {
71188
- const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl").map((e) => e.name).sort().reverse();
71659
+ const runs = readdirSync4(runsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl").map((e) => e.name).sort().reverse();
71189
71660
  if (runs.length > 0) {
71190
71661
  const latestRun = runs[0].replace(".jsonl", "");
71191
71662
  summary.lastRun = latestRun;
@@ -71194,12 +71665,12 @@ async function getFeatureSummary(featureName, featureDir) {
71194
71665
  return summary;
71195
71666
  }
71196
71667
  async function displayAllFeatures(projectDir) {
71197
- const featuresDir = join15(projectDir, ".nax", "features");
71668
+ const featuresDir = join16(projectDir, ".nax", "features");
71198
71669
  if (!existsSync17(featuresDir)) {
71199
71670
  console.log(source_default.dim("No features found."));
71200
71671
  return;
71201
71672
  }
71202
- const features = readdirSync3(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
71673
+ const features = readdirSync4(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
71203
71674
  if (features.length === 0) {
71204
71675
  console.log(source_default.dim("No features found."));
71205
71676
  return;
@@ -71235,7 +71706,7 @@ async function displayAllFeatures(projectDir) {
71235
71706
  console.log();
71236
71707
  }
71237
71708
  }
71238
- const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join15(featuresDir, name))));
71709
+ const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join16(featuresDir, name))));
71239
71710
  console.log(source_default.bold(`\uD83D\uDCCA Features
71240
71711
  `));
71241
71712
  const header = ` ${"Feature".padEnd(25)} ${"Done".padEnd(6)} ${"Failed".padEnd(8)} ${"Pending".padEnd(9)} ${"Last Run".padEnd(22)} ${"Cost".padEnd(10)} Status`;
@@ -71261,7 +71732,7 @@ async function displayAllFeatures(projectDir) {
71261
71732
  console.log();
71262
71733
  }
71263
71734
  async function displayFeatureDetails(featureName, featureDir) {
71264
- const prdPath = join15(featureDir, "prd.json");
71735
+ const prdPath = join16(featureDir, "prd.json");
71265
71736
  if (!existsSync17(prdPath)) {
71266
71737
  console.log(source_default.bold(`
71267
71738
  \uD83D\uDCCA ${featureName}
@@ -71382,8 +71853,8 @@ async function displayFeatureStatus(options = {}) {
71382
71853
  // src/cli/runs.ts
71383
71854
  init_errors();
71384
71855
  init_logger2();
71385
- import { existsSync as existsSync18, readdirSync as readdirSync4 } from "fs";
71386
- import { join as join16 } from "path";
71856
+ import { existsSync as existsSync18, readdirSync as readdirSync5 } from "fs";
71857
+ import { join as join17 } from "path";
71387
71858
  async function parseRunLog(logPath) {
71388
71859
  const logger = getLogger();
71389
71860
  try {
@@ -71399,19 +71870,19 @@ async function parseRunLog(logPath) {
71399
71870
  async function runsListCommand(options) {
71400
71871
  const logger = getLogger();
71401
71872
  const { feature, workdir } = options;
71402
- const runsDir = join16(workdir, ".nax", "features", feature, "runs");
71873
+ const runsDir = join17(workdir, ".nax", "features", feature, "runs");
71403
71874
  if (!existsSync18(runsDir)) {
71404
71875
  logger.info("cli", "No runs found for feature", { feature, hint: `Directory not found: ${runsDir}` });
71405
71876
  return;
71406
71877
  }
71407
- const files = readdirSync4(runsDir).filter((f) => f.endsWith(".jsonl"));
71878
+ const files = readdirSync5(runsDir).filter((f) => f.endsWith(".jsonl"));
71408
71879
  if (files.length === 0) {
71409
71880
  logger.info("cli", "No runs found for feature", { feature });
71410
71881
  return;
71411
71882
  }
71412
71883
  logger.info("cli", `Runs for ${feature}`, { count: files.length });
71413
71884
  for (const file3 of files.sort().reverse()) {
71414
- const logPath = join16(runsDir, file3);
71885
+ const logPath = join17(runsDir, file3);
71415
71886
  const entries = await parseRunLog(logPath);
71416
71887
  const startEvent = entries.find((e) => e.message === "run.start");
71417
71888
  const completeEvent = entries.find((e) => e.message === "run.complete");
@@ -71437,7 +71908,7 @@ async function runsListCommand(options) {
71437
71908
  async function runsShowCommand(options) {
71438
71909
  const logger = getLogger();
71439
71910
  const { runId, feature, workdir } = options;
71440
- const logPath = join16(workdir, ".nax", "features", feature, "runs", `${runId}.jsonl`);
71911
+ const logPath = join17(workdir, ".nax", "features", feature, "runs", `${runId}.jsonl`);
71441
71912
  if (!existsSync18(logPath)) {
71442
71913
  logger.error("cli", "Run not found", { runId, feature, logPath });
71443
71914
  throw new NaxError("Run not found", "RUN_NOT_FOUND", { runId, feature, logPath });
@@ -71476,7 +71947,7 @@ async function runsShowCommand(options) {
71476
71947
  // src/cli/prompts-main.ts
71477
71948
  init_logger2();
71478
71949
  import { existsSync as existsSync22, mkdirSync as mkdirSync2 } from "fs";
71479
- import { join as join29 } from "path";
71950
+ import { join as join30 } from "path";
71480
71951
 
71481
71952
  // src/pipeline/index.ts
71482
71953
  init_runner();
@@ -71551,7 +72022,7 @@ function buildFrontmatter(story, ctx, role) {
71551
72022
 
71552
72023
  // src/cli/prompts-tdd.ts
71553
72024
  init_prompts2();
71554
- import { join as join28 } from "path";
72025
+ import { join as join29 } from "path";
71555
72026
  async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
71556
72027
  const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
71557
72028
  PromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build(),
@@ -71570,7 +72041,7 @@ ${frontmatter}---
71570
72041
 
71571
72042
  ${session.prompt}`;
71572
72043
  if (outputDir) {
71573
- const promptFile = join28(outputDir, `${story.id}.${session.role}.md`);
72044
+ const promptFile = join29(outputDir, `${story.id}.${session.role}.md`);
71574
72045
  await Bun.write(promptFile, fullOutput);
71575
72046
  logger.info("cli", "Written TDD prompt file", {
71576
72047
  storyId: story.id,
@@ -71586,7 +72057,7 @@ ${"=".repeat(80)}`);
71586
72057
  }
71587
72058
  }
71588
72059
  if (outputDir && ctx.contextMarkdown) {
71589
- const contextFile = join28(outputDir, `${story.id}.context.md`);
72060
+ const contextFile = join29(outputDir, `${story.id}.context.md`);
71590
72061
  const frontmatter = buildFrontmatter(story, ctx);
71591
72062
  const contextOutput = `---
71592
72063
  ${frontmatter}---
@@ -71600,12 +72071,12 @@ ${ctx.contextMarkdown}`;
71600
72071
  async function promptsCommand(options) {
71601
72072
  const logger = getLogger();
71602
72073
  const { feature, workdir, config: config2, storyId, outputDir } = options;
71603
- const naxDir = join29(workdir, ".nax");
72074
+ const naxDir = join30(workdir, ".nax");
71604
72075
  if (!existsSync22(naxDir)) {
71605
72076
  throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
71606
72077
  }
71607
- const featureDir = join29(naxDir, "features", feature);
71608
- const prdPath = join29(featureDir, "prd.json");
72078
+ const featureDir = join30(naxDir, "features", feature);
72079
+ const prdPath = join30(featureDir, "prd.json");
71609
72080
  if (!existsSync22(prdPath)) {
71610
72081
  throw new Error(`Feature "${feature}" not found or missing prd.json`);
71611
72082
  }
@@ -71666,10 +72137,10 @@ ${frontmatter}---
71666
72137
 
71667
72138
  ${ctx.prompt}`;
71668
72139
  if (outputDir) {
71669
- const promptFile = join29(outputDir, `${story.id}.prompt.md`);
72140
+ const promptFile = join30(outputDir, `${story.id}.prompt.md`);
71670
72141
  await Bun.write(promptFile, fullOutput);
71671
72142
  if (ctx.contextMarkdown) {
71672
- const contextFile = join29(outputDir, `${story.id}.context.md`);
72143
+ const contextFile = join30(outputDir, `${story.id}.context.md`);
71673
72144
  const contextOutput = `---
71674
72145
  ${frontmatter}---
71675
72146
 
@@ -71696,7 +72167,7 @@ ${"=".repeat(80)}`);
71696
72167
  }
71697
72168
  // src/cli/prompts-init.ts
71698
72169
  import { existsSync as existsSync23, mkdirSync as mkdirSync3 } from "fs";
71699
- import { join as join30 } from "path";
72170
+ import { join as join31 } from "path";
71700
72171
  var TEMPLATE_ROLES = [
71701
72172
  { file: "test-writer.md", role: "test-writer" },
71702
72173
  { file: "implementer.md", role: "implementer", variant: "standard" },
@@ -71720,9 +72191,9 @@ var TEMPLATE_HEADER = `<!--
71720
72191
  `;
71721
72192
  async function promptsInitCommand(options) {
71722
72193
  const { workdir, force = false, autoWireConfig = true } = options;
71723
- const templatesDir = join30(workdir, ".nax", "templates");
72194
+ const templatesDir = join31(workdir, ".nax", "templates");
71724
72195
  mkdirSync3(templatesDir, { recursive: true });
71725
- const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync23(join30(templatesDir, f)));
72196
+ const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync23(join31(templatesDir, f)));
71726
72197
  if (existingFiles.length > 0 && !force) {
71727
72198
  console.warn(`[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.
71728
72199
  Pass --force to overwrite existing templates.`);
@@ -71730,7 +72201,7 @@ async function promptsInitCommand(options) {
71730
72201
  }
71731
72202
  const written = [];
71732
72203
  for (const template of TEMPLATE_ROLES) {
71733
- const filePath = join30(templatesDir, template.file);
72204
+ const filePath = join31(templatesDir, template.file);
71734
72205
  const roleBody = template.role === "implementer" ? buildRoleTaskSection(template.role, template.variant) : buildRoleTaskSection(template.role);
71735
72206
  const content = TEMPLATE_HEADER + roleBody;
71736
72207
  await Bun.write(filePath, content);
@@ -71746,7 +72217,7 @@ async function promptsInitCommand(options) {
71746
72217
  return written;
71747
72218
  }
71748
72219
  async function autoWirePromptsConfig(workdir) {
71749
- const configPath = join30(workdir, "nax.config.json");
72220
+ const configPath = join31(workdir, "nax.config.json");
71750
72221
  if (!existsSync23(configPath)) {
71751
72222
  const exampleConfig = JSON.stringify({
71752
72223
  prompts: {
@@ -71911,8 +72382,8 @@ function pad(str, width) {
71911
72382
  init_config();
71912
72383
  init_logger2();
71913
72384
  init_prd();
71914
- import { existsSync as existsSync25, readdirSync as readdirSync5 } from "fs";
71915
- import { join as join35 } from "path";
72385
+ import { existsSync as existsSync25, readdirSync as readdirSync6 } from "fs";
72386
+ import { join as join36 } from "path";
71916
72387
 
71917
72388
  // src/cli/diagnose-analysis.ts
71918
72389
  function detectFailurePattern(story, prd, status) {
@@ -72111,7 +72582,7 @@ function isProcessAlive2(pid) {
72111
72582
  }
72112
72583
  }
72113
72584
  async function loadStatusFile2(workdir) {
72114
- const statusPath = join35(workdir, ".nax", "status.json");
72585
+ const statusPath = join36(workdir, ".nax", "status.json");
72115
72586
  if (!existsSync25(statusPath))
72116
72587
  return null;
72117
72588
  try {
@@ -72139,7 +72610,7 @@ async function countCommitsSince(workdir, since) {
72139
72610
  }
72140
72611
  }
72141
72612
  async function checkLock(workdir) {
72142
- const lockFile = Bun.file(join35(workdir, "nax.lock"));
72613
+ const lockFile = Bun.file(join36(workdir, "nax.lock"));
72143
72614
  if (!await lockFile.exists())
72144
72615
  return { lockPresent: false };
72145
72616
  try {
@@ -72157,8 +72628,8 @@ async function diagnoseCommand(options = {}) {
72157
72628
  const logger = getLogger();
72158
72629
  const workdir = options.workdir ?? process.cwd();
72159
72630
  const naxSubdir = findProjectDir(workdir);
72160
- let projectDir = naxSubdir ? join35(naxSubdir, "..") : null;
72161
- if (!projectDir && existsSync25(join35(workdir, ".nax"))) {
72631
+ let projectDir = naxSubdir ? join36(naxSubdir, "..") : null;
72632
+ if (!projectDir && existsSync25(join36(workdir, ".nax"))) {
72162
72633
  projectDir = workdir;
72163
72634
  }
72164
72635
  if (!projectDir)
@@ -72169,18 +72640,18 @@ async function diagnoseCommand(options = {}) {
72169
72640
  if (status2) {
72170
72641
  feature = status2.run.feature;
72171
72642
  } else {
72172
- const featuresDir = join35(projectDir, ".nax", "features");
72643
+ const featuresDir = join36(projectDir, ".nax", "features");
72173
72644
  if (!existsSync25(featuresDir))
72174
72645
  throw new Error("No features found in project");
72175
- const features = readdirSync5(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
72646
+ const features = readdirSync6(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
72176
72647
  if (features.length === 0)
72177
72648
  throw new Error("No features found");
72178
72649
  feature = features[0];
72179
72650
  logger.info("diagnose", "No feature specified, using first found", { feature });
72180
72651
  }
72181
72652
  }
72182
- const featureDir = join35(projectDir, ".nax", "features", feature);
72183
- const prdPath = join35(featureDir, "prd.json");
72653
+ const featureDir = join36(projectDir, ".nax", "features", feature);
72654
+ const prdPath = join36(featureDir, "prd.json");
72184
72655
  if (!existsSync25(prdPath))
72185
72656
  throw new Error(`Feature not found: ${feature}`);
72186
72657
  const prd = await loadPRD(prdPath);
@@ -72223,7 +72694,7 @@ init_interaction();
72223
72694
  init_source();
72224
72695
  init_loader();
72225
72696
  import { existsSync as existsSync26 } from "fs";
72226
- import { join as join36 } from "path";
72697
+ import { join as join37 } from "path";
72227
72698
  var VALID_AGENTS = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
72228
72699
  async function generateCommand(options) {
72229
72700
  const workdir = options.dir ?? process.cwd();
@@ -72266,7 +72737,7 @@ async function generateCommand(options) {
72266
72737
  return;
72267
72738
  }
72268
72739
  if (options.package) {
72269
- const packageDir = join36(workdir, options.package);
72740
+ const packageDir = join37(workdir, options.package);
72270
72741
  if (dryRun) {
72271
72742
  console.log(source_default.yellow("\u26A0 Dry run \u2014 no files will be written"));
72272
72743
  }
@@ -72286,8 +72757,8 @@ async function generateCommand(options) {
72286
72757
  process.exit(1);
72287
72758
  return;
72288
72759
  }
72289
- const contextPath = options.context ? join36(workdir, options.context) : join36(workdir, ".nax/context.md");
72290
- const outputDir = options.output ? join36(workdir, options.output) : workdir;
72760
+ const contextPath = options.context ? join37(workdir, options.context) : join37(workdir, ".nax/context.md");
72761
+ const outputDir = options.output ? join37(workdir, options.output) : workdir;
72291
72762
  const autoInject = !options.noAutoInject;
72292
72763
  if (!existsSync26(contextPath)) {
72293
72764
  console.error(source_default.red(`\u2717 Context file not found: ${contextPath}`));
@@ -72392,7 +72863,7 @@ async function generateCommand(options) {
72392
72863
  // src/cli/config-display.ts
72393
72864
  init_loader();
72394
72865
  import { existsSync as existsSync28 } from "fs";
72395
- import { join as join38 } from "path";
72866
+ import { join as join39 } from "path";
72396
72867
 
72397
72868
  // src/cli/config-descriptions.ts
72398
72869
  var FIELD_DESCRIPTIONS = {
@@ -72630,7 +73101,7 @@ function deepEqual(a, b) {
72630
73101
  init_defaults();
72631
73102
  init_loader();
72632
73103
  import { existsSync as existsSync27 } from "fs";
72633
- import { join as join37 } from "path";
73104
+ import { join as join38 } from "path";
72634
73105
  async function loadConfigFile(path14) {
72635
73106
  if (!existsSync27(path14))
72636
73107
  return null;
@@ -72652,7 +73123,7 @@ async function loadProjectConfig() {
72652
73123
  const projectDir = findProjectDir();
72653
73124
  if (!projectDir)
72654
73125
  return null;
72655
- const projectPath = join37(projectDir, "config.json");
73126
+ const projectPath = join38(projectDir, "config.json");
72656
73127
  return await loadConfigFile(projectPath);
72657
73128
  }
72658
73129
 
@@ -72712,7 +73183,7 @@ async function configCommand(config2, options = {}) {
72712
73183
  function determineConfigSources() {
72713
73184
  const globalPath = globalConfigPath();
72714
73185
  const projectDir = findProjectDir();
72715
- const projectPath = projectDir ? join38(projectDir, "config.json") : null;
73186
+ const projectPath = projectDir ? join39(projectDir, "config.json") : null;
72716
73187
  return {
72717
73188
  global: fileExists(globalPath) ? globalPath : null,
72718
73189
  project: projectPath && fileExists(projectPath) ? projectPath : null
@@ -72782,6 +73253,26 @@ function displayConfigWithDescriptions(obj, path14, sources, indent = 0) {
72782
73253
  }
72783
73254
  }
72784
73255
  }
73256
+ if (indent === 0 && !entries.find(([k]) => k === "prompts")) {
73257
+ console.log("# prompts: Prompt template overrides (PB-003: PromptBuilder)");
73258
+ const description = FIELD_DESCRIPTIONS["prompts.overrides"];
73259
+ if (description) {
73260
+ console.log(`# prompts.overrides: ${description}`);
73261
+ }
73262
+ const roles = ["test-writer", "implementer", "verifier", "single-session"];
73263
+ console.log("overrides:");
73264
+ for (const role of roles) {
73265
+ const roleDesc = FIELD_DESCRIPTIONS[`prompts.overrides.${role}`];
73266
+ if (roleDesc) {
73267
+ console.log(` # ${roleDesc}`);
73268
+ const match = roleDesc.match(/e\.g\., "([^"]+)"/);
73269
+ if (match) {
73270
+ console.log(` # ${role}: "${match[1]}"`);
73271
+ }
73272
+ }
73273
+ }
73274
+ console.log();
73275
+ }
72785
73276
  }
72786
73277
  function formatValue(value) {
72787
73278
  if (value === null)
@@ -72836,6 +73327,107 @@ function formatValueForTable(value) {
72836
73327
  }
72837
73328
  return String(value);
72838
73329
  }
73330
+ // src/cli/config-profile.ts
73331
+ init_paths();
73332
+ init_profile();
73333
+ import { mkdirSync as mkdirSync4 } from "fs";
73334
+ import { readdirSync as readdirSync7 } from "fs";
73335
+ import { join as join40 } from "path";
73336
+ var _profileCLIDeps = {
73337
+ env: process.env
73338
+ };
73339
+ var SENSITIVE_KEY_PATTERN = /key|token|secret|password|credential/i;
73340
+ var VAR_PATTERN = /\$[A-Za-z_][A-Za-z0-9_]*/;
73341
+ async function profileListCommand(startDir) {
73342
+ const globalProfilesDir = join40(globalConfigDir(), "profiles");
73343
+ const projectProfilesDir = join40(projectConfigDir(startDir), "profiles");
73344
+ const globalProfiles = scanProfileDir(globalProfilesDir);
73345
+ const projectProfiles = scanProfileDir(projectProfilesDir);
73346
+ const activeProfile = await resolveProfileName({}, _profileCLIDeps.env, startDir);
73347
+ const lines = [];
73348
+ lines.push("global:");
73349
+ if (globalProfiles.length === 0) {
73350
+ lines.push(" (none)");
73351
+ } else {
73352
+ for (const name of globalProfiles) {
73353
+ const marker = name === activeProfile ? "* " : " ";
73354
+ lines.push(`${marker}${name}`);
73355
+ }
73356
+ }
73357
+ if (projectProfiles.length > 0) {
73358
+ lines.push("project:");
73359
+ for (const name of projectProfiles) {
73360
+ const marker = name === activeProfile ? "* " : " ";
73361
+ lines.push(`${marker}${name}`);
73362
+ }
73363
+ }
73364
+ return lines.join(`
73365
+ `);
73366
+ }
73367
+ function scanProfileDir(dir) {
73368
+ try {
73369
+ return readdirSync7(dir).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
73370
+ } catch {
73371
+ return [];
73372
+ }
73373
+ }
73374
+ async function profileShowCommand(profileName, startDir, opts) {
73375
+ const rawProfile = await loadProfile(profileName, startDir);
73376
+ const envVars = await loadProfileEnv(profileName, startDir);
73377
+ if (opts.unmask) {
73378
+ const resolved = resolveEnvVars(rawProfile, envVars);
73379
+ const warning = "WARNING: Sensitive values are displayed in plaintext.";
73380
+ return `${warning}
73381
+ ${JSON.stringify(resolved, null, 2)}`;
73382
+ }
73383
+ const masked = maskProfileValues(rawProfile);
73384
+ return JSON.stringify(masked, null, 2);
73385
+ }
73386
+ function maskProfileValues(obj) {
73387
+ const result = {};
73388
+ for (const [key, value] of Object.entries(obj)) {
73389
+ if (SENSITIVE_KEY_PATTERN.test(key)) {
73390
+ result[key] = "***";
73391
+ } else if (typeof value === "string" && VAR_PATTERN.test(value)) {
73392
+ result[key] = "***";
73393
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
73394
+ result[key] = maskProfileValues(value);
73395
+ } else {
73396
+ result[key] = value;
73397
+ }
73398
+ }
73399
+ return result;
73400
+ }
73401
+ async function profileUseCommand(profileName, startDir) {
73402
+ const configPath = join40(projectConfigDir(startDir), "config.json");
73403
+ const configFile = Bun.file(configPath);
73404
+ let existing = {};
73405
+ if (await configFile.exists()) {
73406
+ existing = await configFile.json();
73407
+ }
73408
+ if (profileName === "default") {
73409
+ const { profile: _removed, ...rest } = existing;
73410
+ await Bun.write(configPath, JSON.stringify(rest, null, 2));
73411
+ return "Profile reset to default.";
73412
+ }
73413
+ const updated = { ...existing, profile: profileName };
73414
+ await Bun.write(configPath, JSON.stringify(updated, null, 2));
73415
+ return `Now using profile: ${profileName}`;
73416
+ }
73417
+ async function profileCurrentCommand(startDir) {
73418
+ return resolveProfileName({}, _profileCLIDeps.env, startDir);
73419
+ }
73420
+ async function profileCreateCommand(profileName, startDir) {
73421
+ const profilesDir = join40(projectConfigDir(startDir), "profiles");
73422
+ const profilePath = join40(profilesDir, `${profileName}.json`);
73423
+ const profileFile = Bun.file(profilePath);
73424
+ if (await profileFile.exists()) {
73425
+ throw new Error(`Profile "${profileName}" already exists at ${profilePath}`);
73426
+ }
73427
+ mkdirSync4(profilesDir, { recursive: true });
73428
+ await Bun.write(profilePath, "{}");
73429
+ return profilePath;
73430
+ }
72839
73431
  // src/cli/agents.ts
72840
73432
  init_registry();
72841
73433
  init_version_detection();
@@ -72892,24 +73484,24 @@ async function diagnose(options) {
72892
73484
 
72893
73485
  // src/commands/logs.ts
72894
73486
  import { existsSync as existsSync30 } from "fs";
72895
- import { join as join42 } from "path";
73487
+ import { join as join44 } from "path";
72896
73488
 
72897
73489
  // src/commands/logs-formatter.ts
72898
73490
  init_source();
72899
73491
  init_formatter();
72900
- import { readdirSync as readdirSync7 } from "fs";
72901
- import { join as join41 } from "path";
73492
+ import { readdirSync as readdirSync9 } from "fs";
73493
+ import { join as join43 } from "path";
72902
73494
 
72903
73495
  // src/commands/logs-reader.ts
72904
- import { existsSync as existsSync29, readdirSync as readdirSync6 } from "fs";
73496
+ import { existsSync as existsSync29, readdirSync as readdirSync8 } from "fs";
72905
73497
  import { readdir as readdir3 } from "fs/promises";
72906
- import { join as join40 } from "path";
73498
+ import { join as join42 } from "path";
72907
73499
 
72908
73500
  // src/utils/paths.ts
72909
73501
  import { homedir as homedir4 } from "os";
72910
- import { join as join39 } from "path";
73502
+ import { join as join41 } from "path";
72911
73503
  function getRunsDir() {
72912
- return process.env.NAX_RUNS_DIR ?? join39(homedir4(), ".nax", "runs");
73504
+ return process.env.NAX_RUNS_DIR ?? join41(homedir4(), ".nax", "runs");
72913
73505
  }
72914
73506
 
72915
73507
  // src/commands/logs-reader.ts
@@ -72926,7 +73518,7 @@ async function resolveRunFileFromRegistry(runId) {
72926
73518
  }
72927
73519
  let matched = null;
72928
73520
  for (const entry of entries) {
72929
- const metaPath = join40(runsDir, entry, "meta.json");
73521
+ const metaPath = join42(runsDir, entry, "meta.json");
72930
73522
  try {
72931
73523
  const meta3 = await Bun.file(metaPath).json();
72932
73524
  if (meta3.runId === runId || meta3.runId.startsWith(runId)) {
@@ -72942,20 +73534,20 @@ async function resolveRunFileFromRegistry(runId) {
72942
73534
  console.log(`Log directory unavailable for run: ${runId}`);
72943
73535
  return null;
72944
73536
  }
72945
- const files = readdirSync6(matched.eventsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
73537
+ const files = readdirSync8(matched.eventsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
72946
73538
  if (files.length === 0) {
72947
73539
  console.log(`No log files found for run: ${runId}`);
72948
73540
  return null;
72949
73541
  }
72950
73542
  const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
72951
- return join40(matched.eventsDir, specificFile ?? files[0]);
73543
+ return join42(matched.eventsDir, specificFile ?? files[0]);
72952
73544
  }
72953
73545
  async function selectRunFile(runsDir) {
72954
- const files = readdirSync6(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
73546
+ const files = readdirSync8(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
72955
73547
  if (files.length === 0) {
72956
73548
  return null;
72957
73549
  }
72958
- return join40(runsDir, files[0]);
73550
+ return join42(runsDir, files[0]);
72959
73551
  }
72960
73552
  async function extractRunSummary(filePath) {
72961
73553
  const file3 = Bun.file(filePath);
@@ -73029,7 +73621,7 @@ var LOG_LEVEL_PRIORITY2 = {
73029
73621
  error: 3
73030
73622
  };
73031
73623
  async function displayRunsList(runsDir) {
73032
- const files = readdirSync7(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
73624
+ const files = readdirSync9(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
73033
73625
  if (files.length === 0) {
73034
73626
  console.log(source_default.dim("No runs found"));
73035
73627
  return;
@@ -73040,7 +73632,7 @@ Runs:
73040
73632
  console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
73041
73633
  console.log(source_default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
73042
73634
  for (const file3 of files) {
73043
- const filePath = join41(runsDir, file3);
73635
+ const filePath = join43(runsDir, file3);
73044
73636
  const summary = await extractRunSummary(filePath);
73045
73637
  const timestamp = file3.replace(".jsonl", "");
73046
73638
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
@@ -73154,7 +73746,7 @@ async function logsCommand(options) {
73154
73746
  return;
73155
73747
  }
73156
73748
  const resolved = resolveProject({ dir: options.dir });
73157
- const naxDir = join42(resolved.projectDir, ".nax");
73749
+ const naxDir = join44(resolved.projectDir, ".nax");
73158
73750
  const configPath = resolved.configPath;
73159
73751
  const configFile = Bun.file(configPath);
73160
73752
  const config2 = await configFile.json();
@@ -73162,8 +73754,8 @@ async function logsCommand(options) {
73162
73754
  if (!featureName) {
73163
73755
  throw new Error("No feature specified in config.json");
73164
73756
  }
73165
- const featureDir = join42(naxDir, "features", featureName);
73166
- const runsDir = join42(featureDir, "runs");
73757
+ const featureDir = join44(naxDir, "features", featureName);
73758
+ const runsDir = join44(featureDir, "runs");
73167
73759
  if (!existsSync30(runsDir)) {
73168
73760
  throw new Error(`No runs directory found for feature: ${featureName}`);
73169
73761
  }
@@ -73188,7 +73780,7 @@ init_config();
73188
73780
  init_prd();
73189
73781
  init_precheck();
73190
73782
  import { existsSync as existsSync31 } from "fs";
73191
- import { join as join43 } from "path";
73783
+ import { join as join45 } from "path";
73192
73784
  async function precheckCommand(options) {
73193
73785
  const resolved = resolveProject({
73194
73786
  dir: options.dir,
@@ -73210,9 +73802,9 @@ async function precheckCommand(options) {
73210
73802
  process.exit(1);
73211
73803
  }
73212
73804
  }
73213
- const naxDir = join43(resolved.projectDir, ".nax");
73214
- const featureDir = join43(naxDir, "features", featureName);
73215
- const prdPath = join43(featureDir, "prd.json");
73805
+ const naxDir = join45(resolved.projectDir, ".nax");
73806
+ const featureDir = join45(naxDir, "features", featureName);
73807
+ const prdPath = join45(featureDir, "prd.json");
73216
73808
  if (!existsSync31(featureDir)) {
73217
73809
  console.error(source_default.red(`Feature not found: ${featureName}`));
73218
73810
  process.exit(1);
@@ -73234,7 +73826,7 @@ async function precheckCommand(options) {
73234
73826
  // src/commands/runs.ts
73235
73827
  init_source();
73236
73828
  import { readdir as readdir4 } from "fs/promises";
73237
- import { join as join44 } from "path";
73829
+ import { join as join46 } from "path";
73238
73830
  var DEFAULT_LIMIT = 20;
73239
73831
  var _runsCmdDeps = {
73240
73832
  getRunsDir
@@ -73289,7 +73881,7 @@ async function runsCommand(options = {}) {
73289
73881
  }
73290
73882
  const rows = [];
73291
73883
  for (const entry of entries) {
73292
- const metaPath = join44(runsDir, entry, "meta.json");
73884
+ const metaPath = join46(runsDir, entry, "meta.json");
73293
73885
  let meta3;
73294
73886
  try {
73295
73887
  meta3 = await Bun.file(metaPath).json();
@@ -73366,7 +73958,7 @@ async function runsCommand(options = {}) {
73366
73958
 
73367
73959
  // src/commands/unlock.ts
73368
73960
  init_source();
73369
- import { join as join45 } from "path";
73961
+ import { join as join47 } from "path";
73370
73962
  function isProcessAlive3(pid) {
73371
73963
  try {
73372
73964
  process.kill(pid, 0);
@@ -73381,7 +73973,7 @@ function formatLockAge(ageMs) {
73381
73973
  }
73382
73974
  async function unlockCommand(options) {
73383
73975
  const workdir = options.dir ?? process.cwd();
73384
- const lockPath = join45(workdir, "nax.lock");
73976
+ const lockPath = join47(workdir, "nax.lock");
73385
73977
  const lockFile = Bun.file(lockPath);
73386
73978
  const exists = await lockFile.exists();
73387
73979
  if (!exists) {
@@ -73438,6 +74030,10 @@ init_crash_recovery();
73438
74030
  init_story_context();
73439
74031
  async function runCompletionPhase(options) {
73440
74032
  const logger = getSafeLogger();
74033
+ logger?.debug("execution", "Completion phase started", {
74034
+ acceptanceEnabled: options.config.acceptance?.enabled,
74035
+ isComplete: isComplete(options.prd)
74036
+ });
73441
74037
  if (options.config.acceptance.enabled && isComplete(options.prd)) {
73442
74038
  const { runAcceptanceLoop: runAcceptanceLoop2 } = await Promise.resolve().then(() => (init_acceptance_loop(), exports_acceptance_loop));
73443
74039
  const acceptanceResult = await runAcceptanceLoop2({
@@ -73505,9 +74101,12 @@ async function runCompletionPhase(options) {
73505
74101
  formatterMode: options.formatterMode
73506
74102
  });
73507
74103
  }
74104
+ logger?.debug("execution", "Completion phase \u2014 stopping heartbeat and writing exit summary");
73508
74105
  stopHeartbeat();
73509
74106
  await writeExitSummary(options.logFilePath, options.totalCost, options.iterations, options.storiesCompleted, durationMs);
74107
+ logger?.debug("execution", "Completion phase \u2014 auto-committing dirty files");
73510
74108
  await autoCommitIfDirty(options.workdir, "run.complete", "run-summary", options.feature);
74109
+ logger?.debug("execution", "Completion phase done \u2014 returning to runner");
73511
74110
  return {
73512
74111
  durationMs,
73513
74112
  runCompletedAt
@@ -73645,6 +74244,12 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
73645
74244
  storiesCompleted = unifiedResult.storiesCompleted;
73646
74245
  totalCost = unifiedResult.totalCost;
73647
74246
  allStoryMetrics.push(...unifiedResult.allStoryMetrics);
74247
+ logger?.debug("execution", "Execution phase complete \u2014 handing off to completion phase", {
74248
+ exitReason: unifiedResult.exitReason,
74249
+ iterations,
74250
+ storiesCompleted,
74251
+ totalCost
74252
+ });
73648
74253
  return { prd, iterations, storiesCompleted, totalCost, allStoryMetrics };
73649
74254
  }
73650
74255
 
@@ -73803,15 +74408,20 @@ async function run(options) {
73803
74408
  durationMs
73804
74409
  };
73805
74410
  } finally {
74411
+ const logger2 = getSafeLogger();
74412
+ logger2?.debug("execution", "Runner finally block \u2014 starting cleanup");
73806
74413
  stopHeartbeat();
73807
74414
  cleanupCrashHandlers();
74415
+ logger2?.debug("execution", "Runner finally \u2014 sweeping ACP sessions");
73808
74416
  await sweepFeatureSessions(workdir, feature).catch(() => {});
74417
+ logger2?.debug("execution", "Runner finally \u2014 ACP sweep done");
73809
74418
  let branch = "";
73810
74419
  try {
73811
74420
  const { stdout, exitCode } = await gitWithTimeout(["branch", "--show-current"], workdir);
73812
74421
  if (exitCode === 0)
73813
74422
  branch = stdout.trim();
73814
74423
  } catch {}
74424
+ logger2?.debug("execution", "Runner finally \u2014 running cleanupRun");
73815
74425
  const { cleanupRun: cleanupRun2 } = await Promise.resolve().then(() => (init_run_cleanup(), exports_run_cleanup));
73816
74426
  await cleanupRun2({
73817
74427
  runId,
@@ -73827,6 +74437,7 @@ async function run(options) {
73827
74437
  branch,
73828
74438
  version: NAX_VERSION
73829
74439
  });
74440
+ logger2?.debug("execution", "Runner finally \u2014 cleanupRun done, run() returning");
73830
74441
  }
73831
74442
  }
73832
74443
 
@@ -81185,15 +81796,15 @@ Next: nax generate --package ${options.package}`));
81185
81796
  }
81186
81797
  return;
81187
81798
  }
81188
- const naxDir = join56(workdir, ".nax");
81799
+ const naxDir = join58(workdir, ".nax");
81189
81800
  if (existsSync34(naxDir) && !options.force) {
81190
81801
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
81191
81802
  return;
81192
81803
  }
81193
- mkdirSync5(join56(naxDir, "features"), { recursive: true });
81194
- mkdirSync5(join56(naxDir, "hooks"), { recursive: true });
81195
- await Bun.write(join56(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
81196
- await Bun.write(join56(naxDir, "hooks.json"), JSON.stringify({
81804
+ mkdirSync6(join58(naxDir, "features"), { recursive: true });
81805
+ mkdirSync6(join58(naxDir, "hooks"), { recursive: true });
81806
+ await Bun.write(join58(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
81807
+ await Bun.write(join58(naxDir, "hooks.json"), JSON.stringify({
81197
81808
  hooks: {
81198
81809
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
81199
81810
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -81201,12 +81812,12 @@ Next: nax generate --package ${options.package}`));
81201
81812
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
81202
81813
  }
81203
81814
  }, null, 2));
81204
- await Bun.write(join56(naxDir, ".gitignore"), `# nax temp files
81815
+ await Bun.write(join58(naxDir, ".gitignore"), `# nax temp files
81205
81816
  *.tmp
81206
81817
  .paused.json
81207
81818
  .nax-verifier-verdict.json
81208
81819
  `);
81209
- await Bun.write(join56(naxDir, "context.md"), `# Project Context
81820
+ await Bun.write(join58(naxDir, "context.md"), `# Project Context
81210
81821
 
81211
81822
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
81212
81823
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -81291,7 +81902,7 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
81291
81902
  console.log(source_default.dim(`
81292
81903
  Next: nax features create <name>`));
81293
81904
  });
81294
- program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
81905
+ program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).option("--profile <name>", "Profile to use (overrides config.json profile)").action(async (options) => {
81295
81906
  let workdir;
81296
81907
  try {
81297
81908
  workdir = validateDirectory(options.dir);
@@ -81327,13 +81938,17 @@ program2.command("run").description("Run the orchestration loop for a feature").
81327
81938
  formatterMode = "quiet";
81328
81939
  }
81329
81940
  const naxDir = findProjectDir(workdir);
81330
- const config2 = await loadConfig(naxDir ?? undefined);
81941
+ const cliOverrides = {};
81942
+ if (options.profile) {
81943
+ cliOverrides.profile = options.profile;
81944
+ }
81945
+ const config2 = await loadConfig(naxDir ?? undefined, cliOverrides);
81331
81946
  if (!naxDir) {
81332
81947
  console.error(source_default.red("nax not initialized. Run: nax init"));
81333
81948
  process.exit(1);
81334
81949
  }
81335
- const featureDir = join56(naxDir, "features", options.feature);
81336
- const prdPath = join56(featureDir, "prd.json");
81950
+ const featureDir = join58(naxDir, "features", options.feature);
81951
+ const prdPath = join58(featureDir, "prd.json");
81337
81952
  if (options.plan && options.from) {
81338
81953
  if (existsSync34(prdPath) && !options.force) {
81339
81954
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
@@ -81355,10 +81970,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
81355
81970
  }
81356
81971
  }
81357
81972
  try {
81358
- const planLogDir = join56(featureDir, "plan");
81359
- mkdirSync5(planLogDir, { recursive: true });
81973
+ const planLogDir = join58(featureDir, "plan");
81974
+ mkdirSync6(planLogDir, { recursive: true });
81360
81975
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
81361
- const planLogPath = join56(planLogDir, `${planLogId}.jsonl`);
81976
+ const planLogPath = join58(planLogDir, `${planLogId}.jsonl`);
81362
81977
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
81363
81978
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
81364
81979
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -81402,10 +82017,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
81402
82017
  process.exit(1);
81403
82018
  }
81404
82019
  resetLogger();
81405
- const runsDir = join56(featureDir, "runs");
81406
- mkdirSync5(runsDir, { recursive: true });
82020
+ const runsDir = join58(featureDir, "runs");
82021
+ mkdirSync6(runsDir, { recursive: true });
81407
82022
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
81408
- const logFilePath = join56(runsDir, `${runId}.jsonl`);
82023
+ const logFilePath = join58(runsDir, `${runId}.jsonl`);
81409
82024
  const isTTY = process.stdout.isTTY ?? false;
81410
82025
  const headlessFlag = options.headless ?? false;
81411
82026
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -81421,7 +82036,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
81421
82036
  config2.autoMode.defaultAgent = options.agent;
81422
82037
  }
81423
82038
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
81424
- const globalNaxDir = join56(homedir8(), ".nax");
82039
+ const globalNaxDir = join58(homedir8(), ".nax");
81425
82040
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
81426
82041
  const eventEmitter = new PipelineEventEmitter;
81427
82042
  let tuiInstance;
@@ -81444,7 +82059,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
81444
82059
  } else {
81445
82060
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
81446
82061
  }
81447
- const statusFilePath = join56(workdir, ".nax", "status.json");
82062
+ const statusFilePath = join58(workdir, ".nax", "status.json");
81448
82063
  let parallel;
81449
82064
  if (options.parallel !== undefined) {
81450
82065
  parallel = Number.parseInt(options.parallel, 10);
@@ -81470,7 +82085,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
81470
82085
  headless: useHeadless,
81471
82086
  skipPrecheck: options.skipPrecheck ?? false
81472
82087
  });
81473
- const latestSymlink = join56(runsDir, "latest.jsonl");
82088
+ const latestSymlink = join58(runsDir, "latest.jsonl");
81474
82089
  try {
81475
82090
  if (existsSync34(latestSymlink)) {
81476
82091
  Bun.spawnSync(["rm", latestSymlink]);
@@ -81508,9 +82123,9 @@ features.command("create <name>").description("Create a new feature").option("-d
81508
82123
  console.error(source_default.red("nax not initialized. Run: nax init"));
81509
82124
  process.exit(1);
81510
82125
  }
81511
- const featureDir = join56(naxDir, "features", name);
81512
- mkdirSync5(featureDir, { recursive: true });
81513
- await Bun.write(join56(featureDir, "spec.md"), `# Feature: ${name}
82126
+ const featureDir = join58(naxDir, "features", name);
82127
+ mkdirSync6(featureDir, { recursive: true });
82128
+ await Bun.write(join58(featureDir, "spec.md"), `# Feature: ${name}
81514
82129
 
81515
82130
  ## Overview
81516
82131
 
@@ -81543,7 +82158,7 @@ features.command("create <name>").description("Create a new feature").option("-d
81543
82158
 
81544
82159
  <!-- What this feature explicitly does NOT cover. -->
81545
82160
  `);
81546
- await Bun.write(join56(featureDir, "progress.txt"), `# Progress: ${name}
82161
+ await Bun.write(join58(featureDir, "progress.txt"), `# Progress: ${name}
81547
82162
 
81548
82163
  Created: ${new Date().toISOString()}
81549
82164
 
@@ -81569,13 +82184,13 @@ features.command("list").description("List all features").option("-d, --dir <pat
81569
82184
  console.error(source_default.red("nax not initialized."));
81570
82185
  process.exit(1);
81571
82186
  }
81572
- const featuresDir = join56(naxDir, "features");
82187
+ const featuresDir = join58(naxDir, "features");
81573
82188
  if (!existsSync34(featuresDir)) {
81574
82189
  console.log(source_default.dim("No features yet."));
81575
82190
  return;
81576
82191
  }
81577
- const { readdirSync: readdirSync8 } = await import("fs");
81578
- const entries = readdirSync8(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
82192
+ const { readdirSync: readdirSync10 } = await import("fs");
82193
+ const entries = readdirSync10(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
81579
82194
  if (entries.length === 0) {
81580
82195
  console.log(source_default.dim("No features yet."));
81581
82196
  return;
@@ -81584,7 +82199,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
81584
82199
  Features:
81585
82200
  `));
81586
82201
  for (const name of entries) {
81587
- const prdPath = join56(featuresDir, name, "prd.json");
82202
+ const prdPath = join58(featuresDir, name, "prd.json");
81588
82203
  if (existsSync34(prdPath)) {
81589
82204
  const prd = await loadPRD(prdPath);
81590
82205
  const c = countStories(prd);
@@ -81595,7 +82210,7 @@ Features:
81595
82210
  }
81596
82211
  console.log();
81597
82212
  });
81598
- program2.command("plan [description]").description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')").option("--from <spec-path>", "Path to spec file (required unless --decompose is used)").requiredOption("-f, --feature <name>", "Feature name (required)").option("--auto", "Run in one-shot LLM mode (alias: --one-shot)", false).option("--one-shot", "Run in one-shot LLM mode (alias: --auto)", false).option("-b, --branch <branch>", "Override default branch name").option("-d, --dir <path>", "Project directory", process.cwd()).option("--decompose <storyId>", "Decompose an existing story into sub-stories").action(async (description, options) => {
82213
+ program2.command("plan [description]").description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')").option("--from <spec-path>", "Path to spec file (required unless --decompose is used)").requiredOption("-f, --feature <name>", "Feature name (required)").option("--auto", "Run in one-shot LLM mode (alias: --one-shot)", false).option("--one-shot", "Run in one-shot LLM mode (alias: --auto)", false).option("-b, --branch <branch>", "Override default branch name").option("-d, --dir <path>", "Project directory", process.cwd()).option("--decompose <storyId>", "Decompose an existing story into sub-stories").option("--profile <name>", "Profile to use (overrides config.json profile)").action(async (description, options) => {
81599
82214
  if (description) {
81600
82215
  console.error(source_default.red(`Error: Positional args removed in plan v2.
81601
82216
 
@@ -81614,11 +82229,15 @@ Use: nax plan -f <feature> --from <spec>`));
81614
82229
  console.error(source_default.red("nax not initialized. Run: nax init"));
81615
82230
  process.exit(1);
81616
82231
  }
81617
- const config2 = await loadConfig(workdir);
81618
- const featureLogDir = join56(naxDir, "features", options.feature, "plan");
81619
- mkdirSync5(featureLogDir, { recursive: true });
82232
+ const cliOverrides = {};
82233
+ if (options.profile) {
82234
+ cliOverrides.profile = options.profile;
82235
+ }
82236
+ const config2 = await loadConfig(workdir, cliOverrides);
82237
+ const featureLogDir = join58(naxDir, "features", options.feature, "plan");
82238
+ mkdirSync6(featureLogDir, { recursive: true });
81620
82239
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
81621
- const planLogPath = join56(featureLogDir, `${planLogId}.jsonl`);
82240
+ const planLogPath = join58(featureLogDir, `${planLogId}.jsonl`);
81622
82241
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
81623
82242
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
81624
82243
  try {
@@ -81669,7 +82288,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
81669
82288
  console.error(source_default.red("nax not initialized. Run: nax init"));
81670
82289
  process.exit(1);
81671
82290
  }
81672
- const featureDir = join56(naxDir, "features", options.feature);
82291
+ const featureDir = join58(naxDir, "features", options.feature);
81673
82292
  if (!existsSync34(featureDir)) {
81674
82293
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
81675
82294
  process.exit(1);
@@ -81685,7 +82304,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
81685
82304
  specPath: options.from,
81686
82305
  reclassify: options.reclassify
81687
82306
  });
81688
- const prdPath = join56(featureDir, "prd.json");
82307
+ const prdPath = join58(featureDir, "prd.json");
81689
82308
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
81690
82309
  const c = countStories(prd);
81691
82310
  console.log(source_default.green(`
@@ -81718,7 +82337,7 @@ program2.command("agents").description("List available coding agents with status
81718
82337
  process.exit(1);
81719
82338
  }
81720
82339
  });
81721
- 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) => {
82340
+ var configCmd = 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) => {
81722
82341
  let workdir;
81723
82342
  try {
81724
82343
  workdir = validateDirectory(options.dir);
@@ -81735,6 +82354,52 @@ program2.command("config").description("Display effective merged configuration")
81735
82354
  process.exit(1);
81736
82355
  }
81737
82356
  });
82357
+ var configProfileCmd = configCmd.command("profile").description("Manage config profiles");
82358
+ configProfileCmd.command("list").description("List all available profiles grouped by scope").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
82359
+ try {
82360
+ const output = await profileListCommand(options.dir);
82361
+ console.log(output);
82362
+ } catch (err) {
82363
+ console.error(source_default.red(`Error: ${err.message}`));
82364
+ process.exit(1);
82365
+ }
82366
+ });
82367
+ configProfileCmd.command("show <name>").description("Show resolved profile JSON").option("-d, --dir <path>", "Project directory", process.cwd()).option("--unmask", "Show raw values including secrets", false).action(async (name, options) => {
82368
+ try {
82369
+ const output = await profileShowCommand(name, options.dir, { unmask: options.unmask });
82370
+ console.log(output);
82371
+ } catch (err) {
82372
+ console.error(source_default.red(`Error: ${err.message}`));
82373
+ process.exit(1);
82374
+ }
82375
+ });
82376
+ configProfileCmd.command("use <name>").description("Set the active profile (use 'default' to clear)").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (name, options) => {
82377
+ try {
82378
+ const msg = await profileUseCommand(name, options.dir);
82379
+ console.log(msg);
82380
+ } catch (err) {
82381
+ console.error(source_default.red(`Error: ${err.message}`));
82382
+ process.exit(1);
82383
+ }
82384
+ });
82385
+ configProfileCmd.command("current").description("Show the currently active profile name").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
82386
+ try {
82387
+ const name = await profileCurrentCommand(options.dir);
82388
+ console.log(name);
82389
+ } catch (err) {
82390
+ console.error(source_default.red(`Error: ${err.message}`));
82391
+ process.exit(1);
82392
+ }
82393
+ });
82394
+ configProfileCmd.command("create <name>").description("Create a new empty profile").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (name, options) => {
82395
+ try {
82396
+ const path19 = await profileCreateCommand(name, options.dir);
82397
+ console.log(`Created profile at: ${path19}`);
82398
+ } catch (err) {
82399
+ console.error(source_default.red(`Error: ${err.message}`));
82400
+ process.exit(1);
82401
+ }
82402
+ });
81738
82403
  program2.command("status").description("Show current run status").option("-f, --feature <name>", "Feature name").option("-d, --dir <path>", "Project directory", process.cwd()).option("--cost", "Show cost metrics across all runs", false).option("--last", "Show last run metrics (requires --cost)", false).option("--model", "Show per-model efficiency (requires --cost)", false).action(async (options) => {
81739
82404
  let workdir;
81740
82405
  try {