@nathapp/nax 0.43.1 → 0.45.0
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.
- package/bin/nax.ts +22 -0
- package/dist/nax.js +320 -88
- package/package.json +1 -1
- package/src/agents/acp/adapter.ts +98 -5
- package/src/agents/claude-decompose.ts +6 -21
- package/src/agents/types-extended.ts +1 -1
- package/src/cli/plan.ts +4 -11
- package/src/cli/status-features.ts +19 -0
- package/src/config/test-strategy.ts +70 -0
- package/src/execution/lifecycle/acceptance-loop.ts +2 -0
- package/src/execution/lifecycle/run-setup.ts +4 -0
- package/src/execution/parallel-coordinator.ts +3 -1
- package/src/execution/parallel-executor.ts +3 -0
- package/src/execution/runner-execution.ts +16 -2
- package/src/execution/runner.ts +4 -0
- package/src/execution/story-context.ts +6 -0
- package/src/prd/schema.ts +4 -14
- package/src/precheck/index.ts +155 -44
- package/src/verification/rectification-loop.ts +18 -5
package/dist/nax.js
CHANGED
|
@@ -3294,6 +3294,55 @@ var init_claude_complete = __esm(() => {
|
|
|
3294
3294
|
};
|
|
3295
3295
|
});
|
|
3296
3296
|
|
|
3297
|
+
// src/config/test-strategy.ts
|
|
3298
|
+
function resolveTestStrategy(raw) {
|
|
3299
|
+
if (!raw)
|
|
3300
|
+
return "test-after";
|
|
3301
|
+
if (VALID_TEST_STRATEGIES.includes(raw))
|
|
3302
|
+
return raw;
|
|
3303
|
+
if (raw === "tdd")
|
|
3304
|
+
return "tdd-simple";
|
|
3305
|
+
if (raw === "three-session")
|
|
3306
|
+
return "three-session-tdd";
|
|
3307
|
+
if (raw === "tdd-lite")
|
|
3308
|
+
return "three-session-tdd-lite";
|
|
3309
|
+
return "test-after";
|
|
3310
|
+
}
|
|
3311
|
+
var VALID_TEST_STRATEGIES, COMPLEXITY_GUIDE = `## Complexity Classification Guide
|
|
3312
|
+
|
|
3313
|
+
- simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
|
|
3314
|
+
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
|
|
3315
|
+
- complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
|
|
3316
|
+
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
|
|
3317
|
+
|
|
3318
|
+
### Security Override
|
|
3319
|
+
|
|
3320
|
+
Security-critical functions (authentication, cryptography, tokens, sessions, credentials,
|
|
3321
|
+
password hashing, access control) must be classified at MINIMUM "medium" complexity
|
|
3322
|
+
regardless of LOC count. These require at minimum "tdd-simple" test strategy.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
|
|
3323
|
+
|
|
3324
|
+
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
3325
|
+
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
3326
|
+
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
3327
|
+
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.`, GROUPING_RULES = `## Grouping Rules
|
|
3328
|
+
|
|
3329
|
+
- Combine small, related tasks into a single "simple" or "medium" story.
|
|
3330
|
+
- Do NOT create separate stories for every single file or function unless complex.
|
|
3331
|
+
- Do NOT create standalone stories purely for test coverage or testing.
|
|
3332
|
+
Each story's testStrategy already handles testing (tdd-simple writes tests first,
|
|
3333
|
+
three-session-tdd uses separate test-writer session, test-after writes tests after).
|
|
3334
|
+
Only create a dedicated test story for unique integration/E2E test logic that spans
|
|
3335
|
+
multiple stories and cannot be covered by individual story test strategies.
|
|
3336
|
+
- Aim for coherent units of value. Maximum recommended stories: 10-15 per feature.`;
|
|
3337
|
+
var init_test_strategy = __esm(() => {
|
|
3338
|
+
VALID_TEST_STRATEGIES = [
|
|
3339
|
+
"test-after",
|
|
3340
|
+
"tdd-simple",
|
|
3341
|
+
"three-session-tdd",
|
|
3342
|
+
"three-session-tdd-lite"
|
|
3343
|
+
];
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3297
3346
|
// src/agents/claude-decompose.ts
|
|
3298
3347
|
function buildDecomposePrompt(options) {
|
|
3299
3348
|
return `You are a requirements analyst. Break down the following feature specification into user stories and classify each story's complexity.
|
|
@@ -3316,24 +3365,13 @@ Decompose this spec into user stories. For each story, provide:
|
|
|
3316
3365
|
9. reasoning: Why this complexity level
|
|
3317
3366
|
10. estimatedLOC: Estimated lines of code to change
|
|
3318
3367
|
11. risks: Array of implementation risks
|
|
3319
|
-
12. testStrategy: "three-session-tdd" | "
|
|
3368
|
+
12. testStrategy: "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite"
|
|
3320
3369
|
|
|
3321
|
-
|
|
3322
|
-
- "three-session-tdd": ONLY for complex/expert tasks that are security-critical (auth, encryption, tokens, credentials) or define public API contracts consumers depend on
|
|
3323
|
-
- "test-after": for all other tasks including simple/medium complexity
|
|
3324
|
-
- A "simple" complexity task should almost never be "three-session-tdd"
|
|
3370
|
+
${COMPLEXITY_GUIDE}
|
|
3325
3371
|
|
|
3326
|
-
|
|
3327
|
-
- simple: 1-3 files, <100 LOC, straightforward implementation, existing patterns
|
|
3328
|
-
- medium: 3-6 files, 100-300 LOC, moderate logic, some new patterns
|
|
3329
|
-
- complex: 6+ files, 300-800 LOC, architectural changes, cross-cutting concerns
|
|
3330
|
-
- expert: Security/crypto/real-time/distributed systems, >800 LOC, new infrastructure
|
|
3372
|
+
${TEST_STRATEGY_GUIDE}
|
|
3331
3373
|
|
|
3332
|
-
|
|
3333
|
-
- Combine small, related tasks (e.g., multiple utility functions, interfaces) into a single "simple" or "medium" story.
|
|
3334
|
-
- Do NOT create separate stories for every single file or function unless complex.
|
|
3335
|
-
- Aim for coherent units of value (e.g., "Implement User Authentication" vs "Create User Interface", "Create Login Service").
|
|
3336
|
-
- Maximum recommended stories: 10-15 per feature. Group aggressively if list grows too long.
|
|
3374
|
+
${GROUPING_RULES}
|
|
3337
3375
|
|
|
3338
3376
|
Consider:
|
|
3339
3377
|
1. Does infrastructure exist? (e.g., "add caching" when no cache layer exists = complex)
|
|
@@ -3402,7 +3440,7 @@ ${output.slice(0, 500)}`);
|
|
|
3402
3440
|
reasoning: String(record.reasoning || "No reasoning provided"),
|
|
3403
3441
|
estimatedLOC: Number(record.estimatedLOC) || 0,
|
|
3404
3442
|
risks: Array.isArray(record.risks) ? record.risks : [],
|
|
3405
|
-
testStrategy: record.testStrategy === "
|
|
3443
|
+
testStrategy: resolveTestStrategy(typeof record.testStrategy === "string" ? record.testStrategy : undefined)
|
|
3406
3444
|
};
|
|
3407
3445
|
});
|
|
3408
3446
|
if (stories.length === 0) {
|
|
@@ -3416,6 +3454,9 @@ function coerceComplexity(value) {
|
|
|
3416
3454
|
}
|
|
3417
3455
|
return "medium";
|
|
3418
3456
|
}
|
|
3457
|
+
var init_claude_decompose = __esm(() => {
|
|
3458
|
+
init_test_strategy();
|
|
3459
|
+
});
|
|
3419
3460
|
|
|
3420
3461
|
// src/agents/cost.ts
|
|
3421
3462
|
function parseTokenUsage(output) {
|
|
@@ -18398,6 +18439,7 @@ var init_claude = __esm(() => {
|
|
|
18398
18439
|
init_pid_registry();
|
|
18399
18440
|
init_logger2();
|
|
18400
18441
|
init_claude_complete();
|
|
18442
|
+
init_claude_decompose();
|
|
18401
18443
|
init_claude_execution();
|
|
18402
18444
|
init_claude_interactive();
|
|
18403
18445
|
init_claude_plan();
|
|
@@ -19233,6 +19275,20 @@ var init_cost2 = __esm(() => {
|
|
|
19233
19275
|
});
|
|
19234
19276
|
|
|
19235
19277
|
// src/agents/acp/adapter.ts
|
|
19278
|
+
var exports_adapter = {};
|
|
19279
|
+
__export(exports_adapter, {
|
|
19280
|
+
sweepStaleFeatureSessions: () => sweepStaleFeatureSessions,
|
|
19281
|
+
sweepFeatureSessions: () => sweepFeatureSessions,
|
|
19282
|
+
saveAcpSession: () => saveAcpSession,
|
|
19283
|
+
runSessionPrompt: () => runSessionPrompt,
|
|
19284
|
+
readAcpSession: () => readAcpSession,
|
|
19285
|
+
ensureAcpSession: () => ensureAcpSession,
|
|
19286
|
+
closeAcpSession: () => closeAcpSession,
|
|
19287
|
+
clearAcpSession: () => clearAcpSession,
|
|
19288
|
+
buildSessionName: () => buildSessionName,
|
|
19289
|
+
_acpAdapterDeps: () => _acpAdapterDeps,
|
|
19290
|
+
AcpAgentAdapter: () => AcpAgentAdapter
|
|
19291
|
+
});
|
|
19236
19292
|
import { createHash } from "crypto";
|
|
19237
19293
|
import { join as join3 } from "path";
|
|
19238
19294
|
function resolveRegistryEntry(agentName) {
|
|
@@ -19336,6 +19392,59 @@ async function readAcpSession(workdir, featureName, storyId) {
|
|
|
19336
19392
|
return null;
|
|
19337
19393
|
}
|
|
19338
19394
|
}
|
|
19395
|
+
async function sweepFeatureSessions(workdir, featureName) {
|
|
19396
|
+
const path = acpSessionsPath(workdir, featureName);
|
|
19397
|
+
let sessions;
|
|
19398
|
+
try {
|
|
19399
|
+
const text = await Bun.file(path).text();
|
|
19400
|
+
sessions = JSON.parse(text);
|
|
19401
|
+
} catch {
|
|
19402
|
+
return;
|
|
19403
|
+
}
|
|
19404
|
+
const entries = Object.entries(sessions);
|
|
19405
|
+
if (entries.length === 0)
|
|
19406
|
+
return;
|
|
19407
|
+
const logger = getSafeLogger();
|
|
19408
|
+
logger?.info("acp-adapter", `[sweep] Closing ${entries.length} open sessions for feature: ${featureName}`);
|
|
19409
|
+
const cmdStr = "acpx claude";
|
|
19410
|
+
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
19411
|
+
try {
|
|
19412
|
+
await client.start();
|
|
19413
|
+
for (const [, sessionName] of entries) {
|
|
19414
|
+
try {
|
|
19415
|
+
if (client.loadSession) {
|
|
19416
|
+
const session = await client.loadSession(sessionName, "claude", "approve-reads");
|
|
19417
|
+
if (session) {
|
|
19418
|
+
await session.close().catch(() => {});
|
|
19419
|
+
}
|
|
19420
|
+
}
|
|
19421
|
+
} catch (err) {
|
|
19422
|
+
logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
|
|
19423
|
+
}
|
|
19424
|
+
}
|
|
19425
|
+
} finally {
|
|
19426
|
+
await client.close().catch(() => {});
|
|
19427
|
+
}
|
|
19428
|
+
try {
|
|
19429
|
+
await Bun.write(path, JSON.stringify({}, null, 2));
|
|
19430
|
+
} catch (err) {
|
|
19431
|
+
logger?.warn("acp-adapter", "[sweep] Failed to clear sidecar after sweep", { error: String(err) });
|
|
19432
|
+
}
|
|
19433
|
+
}
|
|
19434
|
+
async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SESSION_AGE_MS) {
|
|
19435
|
+
const path = acpSessionsPath(workdir, featureName);
|
|
19436
|
+
const file2 = Bun.file(path);
|
|
19437
|
+
if (!await file2.exists())
|
|
19438
|
+
return;
|
|
19439
|
+
const ageMs = Date.now() - file2.lastModified;
|
|
19440
|
+
if (ageMs < maxAgeMs)
|
|
19441
|
+
return;
|
|
19442
|
+
getSafeLogger()?.info("acp-adapter", `[sweep] Sidecar is ${Math.round(ageMs / 60000)}m old \u2014 sweeping stale sessions`, {
|
|
19443
|
+
featureName,
|
|
19444
|
+
ageMs
|
|
19445
|
+
});
|
|
19446
|
+
await sweepFeatureSessions(workdir, featureName);
|
|
19447
|
+
}
|
|
19339
19448
|
function extractOutput(response) {
|
|
19340
19449
|
if (!response)
|
|
19341
19450
|
return "";
|
|
@@ -19459,6 +19568,7 @@ class AcpAgentAdapter {
|
|
|
19459
19568
|
}
|
|
19460
19569
|
let lastResponse = null;
|
|
19461
19570
|
let timedOut = false;
|
|
19571
|
+
const runState = { succeeded: false };
|
|
19462
19572
|
const totalTokenUsage = { input_tokens: 0, output_tokens: 0 };
|
|
19463
19573
|
try {
|
|
19464
19574
|
let currentPrompt = options.prompt;
|
|
@@ -19499,12 +19609,17 @@ class AcpAgentAdapter {
|
|
|
19499
19609
|
if (turnCount >= MAX_TURNS && options.interactionBridge) {
|
|
19500
19610
|
getSafeLogger()?.warn("acp-adapter", "Reached max turns limit", { sessionName, maxTurns: MAX_TURNS });
|
|
19501
19611
|
}
|
|
19612
|
+
runState.succeeded = !timedOut && lastResponse?.stopReason === "end_turn";
|
|
19502
19613
|
} finally {
|
|
19503
|
-
|
|
19504
|
-
|
|
19505
|
-
|
|
19506
|
-
|
|
19614
|
+
if (runState.succeeded) {
|
|
19615
|
+
await closeAcpSession(session);
|
|
19616
|
+
if (options.featureName && options.storyId) {
|
|
19617
|
+
await clearAcpSession(options.workdir, options.featureName, options.storyId);
|
|
19618
|
+
}
|
|
19619
|
+
} else {
|
|
19620
|
+
getSafeLogger()?.info("acp-adapter", "Keeping session open for retry", { sessionName });
|
|
19507
19621
|
}
|
|
19622
|
+
await client.close().catch(() => {});
|
|
19508
19623
|
}
|
|
19509
19624
|
const durationMs = Date.now() - startTime;
|
|
19510
19625
|
if (timedOut) {
|
|
@@ -19654,9 +19769,10 @@ class AcpAgentAdapter {
|
|
|
19654
19769
|
return { stories };
|
|
19655
19770
|
}
|
|
19656
19771
|
}
|
|
19657
|
-
var MAX_AGENT_OUTPUT_CHARS2 = 5000, MAX_RATE_LIMIT_RETRIES = 3, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, _acpAdapterDeps;
|
|
19772
|
+
var MAX_AGENT_OUTPUT_CHARS2 = 5000, MAX_RATE_LIMIT_RETRIES = 3, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, _acpAdapterDeps, MAX_SESSION_AGE_MS;
|
|
19658
19773
|
var init_adapter = __esm(() => {
|
|
19659
19774
|
init_logger2();
|
|
19775
|
+
init_claude_decompose();
|
|
19660
19776
|
init_spawn_client();
|
|
19661
19777
|
init_types2();
|
|
19662
19778
|
init_cost2();
|
|
@@ -19698,6 +19814,7 @@ var init_adapter = __esm(() => {
|
|
|
19698
19814
|
return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
|
|
19699
19815
|
}
|
|
19700
19816
|
};
|
|
19817
|
+
MAX_SESSION_AGE_MS = 2 * 60 * 60 * 1000;
|
|
19701
19818
|
});
|
|
19702
19819
|
|
|
19703
19820
|
// src/agents/adapters/aider.ts
|
|
@@ -21968,7 +22085,7 @@ var package_default;
|
|
|
21968
22085
|
var init_package = __esm(() => {
|
|
21969
22086
|
package_default = {
|
|
21970
22087
|
name: "@nathapp/nax",
|
|
21971
|
-
version: "0.
|
|
22088
|
+
version: "0.45.0",
|
|
21972
22089
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
21973
22090
|
type: "module",
|
|
21974
22091
|
bin: {
|
|
@@ -22041,8 +22158,8 @@ var init_version = __esm(() => {
|
|
|
22041
22158
|
NAX_VERSION = package_default.version;
|
|
22042
22159
|
NAX_COMMIT = (() => {
|
|
22043
22160
|
try {
|
|
22044
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22045
|
-
return "
|
|
22161
|
+
if (/^[0-9a-f]{6,10}$/.test("d6bdccb"))
|
|
22162
|
+
return "d6bdccb";
|
|
22046
22163
|
} catch {}
|
|
22047
22164
|
try {
|
|
22048
22165
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -25320,6 +25437,11 @@ async function buildStoryContextFull(prd, story, config2) {
|
|
|
25320
25437
|
}
|
|
25321
25438
|
function getAllReadyStories(prd) {
|
|
25322
25439
|
const completedIds = new Set(prd.userStories.filter((s) => s.passes || s.status === "skipped").map((s) => s.id));
|
|
25440
|
+
const logger = getSafeLogger2();
|
|
25441
|
+
logger?.debug("routing", "getAllReadyStories: completed set", {
|
|
25442
|
+
completedIds: [...completedIds],
|
|
25443
|
+
totalStories: prd.userStories.length
|
|
25444
|
+
});
|
|
25323
25445
|
return prd.userStories.filter((s) => !s.passes && s.status !== "skipped" && s.status !== "failed" && s.status !== "paused" && s.status !== "blocked" && s.dependencies.every((dep) => completedIds.has(dep)));
|
|
25324
25446
|
}
|
|
25325
25447
|
var CONTEXT_MAX_TOKENS = 1e5, CONTEXT_RESERVED_TOKENS = 1e4;
|
|
@@ -28235,7 +28357,7 @@ var init_test_output_parser = () => {};
|
|
|
28235
28357
|
|
|
28236
28358
|
// src/verification/rectification-loop.ts
|
|
28237
28359
|
async function runRectificationLoop2(opts) {
|
|
28238
|
-
const { config: config2, workdir, story, testCommand, timeoutSeconds, testOutput, promptPrefix } = opts;
|
|
28360
|
+
const { config: config2, workdir, story, testCommand, timeoutSeconds, testOutput, promptPrefix, featureName } = opts;
|
|
28239
28361
|
const logger = getSafeLogger();
|
|
28240
28362
|
const rectificationConfig = config2.execution.rectification;
|
|
28241
28363
|
const testSummary = parseBunTestOutput(testOutput);
|
|
@@ -28261,7 +28383,7 @@ async function runRectificationLoop2(opts) {
|
|
|
28261
28383
|
rectificationPrompt = `${promptPrefix}
|
|
28262
28384
|
|
|
28263
28385
|
${rectificationPrompt}`;
|
|
28264
|
-
const agent = getAgent(config2.autoMode.defaultAgent);
|
|
28386
|
+
const agent = _rectificationDeps.getAgent(config2.autoMode.defaultAgent);
|
|
28265
28387
|
if (!agent) {
|
|
28266
28388
|
logger?.error("rectification", "Agent not found, cannot retry");
|
|
28267
28389
|
break;
|
|
@@ -28277,7 +28399,10 @@ ${rectificationPrompt}`;
|
|
|
28277
28399
|
dangerouslySkipPermissions: resolvePermissions(config2, "rectification").skipPermissions,
|
|
28278
28400
|
pipelineStage: "rectification",
|
|
28279
28401
|
config: config2,
|
|
28280
|
-
maxInteractionTurns: config2.agent?.maxInteractionTurns
|
|
28402
|
+
maxInteractionTurns: config2.agent?.maxInteractionTurns,
|
|
28403
|
+
featureName,
|
|
28404
|
+
storyId: story.id,
|
|
28405
|
+
sessionRole: "implementer"
|
|
28281
28406
|
});
|
|
28282
28407
|
if (agentResult.success) {
|
|
28283
28408
|
logger?.info("rectification", `Agent ${label} session complete`, {
|
|
@@ -28292,7 +28417,7 @@ ${rectificationPrompt}`;
|
|
|
28292
28417
|
exitCode: agentResult.exitCode
|
|
28293
28418
|
});
|
|
28294
28419
|
}
|
|
28295
|
-
const retryVerification = await
|
|
28420
|
+
const retryVerification = await _rectificationDeps.runVerification({
|
|
28296
28421
|
workdir,
|
|
28297
28422
|
expectedFiles: getExpectedFiles(story),
|
|
28298
28423
|
command: testCommand,
|
|
@@ -28342,6 +28467,7 @@ ${rectificationPrompt}`;
|
|
|
28342
28467
|
}
|
|
28343
28468
|
return false;
|
|
28344
28469
|
}
|
|
28470
|
+
var _rectificationDeps;
|
|
28345
28471
|
var init_rectification_loop = __esm(() => {
|
|
28346
28472
|
init_agents();
|
|
28347
28473
|
init_config();
|
|
@@ -28350,6 +28476,10 @@ var init_rectification_loop = __esm(() => {
|
|
|
28350
28476
|
init_prd();
|
|
28351
28477
|
init_rectification();
|
|
28352
28478
|
init_runners();
|
|
28479
|
+
_rectificationDeps = {
|
|
28480
|
+
getAgent,
|
|
28481
|
+
runVerification: fullSuite
|
|
28482
|
+
};
|
|
28353
28483
|
});
|
|
28354
28484
|
|
|
28355
28485
|
// src/pipeline/stages/rectify.ts
|
|
@@ -30451,8 +30581,85 @@ async function checkStorySizeGate(config2, prd) {
|
|
|
30451
30581
|
var exports_precheck = {};
|
|
30452
30582
|
__export(exports_precheck, {
|
|
30453
30583
|
runPrecheck: () => runPrecheck,
|
|
30584
|
+
runEnvironmentPrecheck: () => runEnvironmentPrecheck,
|
|
30454
30585
|
EXIT_CODES: () => EXIT_CODES
|
|
30455
30586
|
});
|
|
30587
|
+
function getEarlyEnvironmentBlockers(workdir) {
|
|
30588
|
+
return [() => checkGitRepoExists(workdir), () => checkWorkingTreeClean(workdir), () => checkStaleLock(workdir)];
|
|
30589
|
+
}
|
|
30590
|
+
function getLateEnvironmentBlockers(config2, workdir) {
|
|
30591
|
+
return [
|
|
30592
|
+
() => checkAgentCLI(config2),
|
|
30593
|
+
() => checkDependenciesInstalled(workdir),
|
|
30594
|
+
() => checkTestCommand(config2),
|
|
30595
|
+
() => checkLintCommand(config2),
|
|
30596
|
+
() => checkTypecheckCommand(config2),
|
|
30597
|
+
() => checkGitUserConfigured(workdir)
|
|
30598
|
+
];
|
|
30599
|
+
}
|
|
30600
|
+
function getEnvironmentBlockers(config2, workdir) {
|
|
30601
|
+
return [...getEarlyEnvironmentBlockers(workdir), ...getLateEnvironmentBlockers(config2, workdir)];
|
|
30602
|
+
}
|
|
30603
|
+
function getEnvironmentWarnings(config2, workdir) {
|
|
30604
|
+
return [
|
|
30605
|
+
() => checkClaudeMdExists(workdir),
|
|
30606
|
+
() => checkDiskSpace(),
|
|
30607
|
+
() => checkOptionalCommands(config2, workdir),
|
|
30608
|
+
() => checkGitignoreCoversNax(workdir),
|
|
30609
|
+
() => checkPromptOverrideFiles(config2, workdir),
|
|
30610
|
+
() => checkMultiAgentHealth()
|
|
30611
|
+
];
|
|
30612
|
+
}
|
|
30613
|
+
function getProjectBlockers(prd) {
|
|
30614
|
+
return [() => checkPRDValid(prd)];
|
|
30615
|
+
}
|
|
30616
|
+
function getProjectWarnings(prd) {
|
|
30617
|
+
return [() => checkPendingStories(prd)];
|
|
30618
|
+
}
|
|
30619
|
+
function normalizeChecks(result) {
|
|
30620
|
+
return Array.isArray(result) ? result : [result];
|
|
30621
|
+
}
|
|
30622
|
+
async function runEnvironmentPrecheck(config2, workdir, options) {
|
|
30623
|
+
const format = options?.format ?? "human";
|
|
30624
|
+
const silent = options?.silent ?? false;
|
|
30625
|
+
const passed = [];
|
|
30626
|
+
const blockers = [];
|
|
30627
|
+
const warnings = [];
|
|
30628
|
+
for (const checkFn of getEnvironmentBlockers(config2, workdir)) {
|
|
30629
|
+
const checks3 = normalizeChecks(await checkFn());
|
|
30630
|
+
let blocked = false;
|
|
30631
|
+
for (const check2 of checks3) {
|
|
30632
|
+
if (!silent && format === "human")
|
|
30633
|
+
printCheckResult(check2);
|
|
30634
|
+
if (check2.passed) {
|
|
30635
|
+
passed.push(check2);
|
|
30636
|
+
} else {
|
|
30637
|
+
blockers.push(check2);
|
|
30638
|
+
blocked = true;
|
|
30639
|
+
break;
|
|
30640
|
+
}
|
|
30641
|
+
}
|
|
30642
|
+
if (blocked)
|
|
30643
|
+
break;
|
|
30644
|
+
}
|
|
30645
|
+
if (blockers.length === 0) {
|
|
30646
|
+
for (const checkFn of getEnvironmentWarnings(config2, workdir)) {
|
|
30647
|
+
for (const check2 of normalizeChecks(await checkFn())) {
|
|
30648
|
+
if (!silent && format === "human")
|
|
30649
|
+
printCheckResult(check2);
|
|
30650
|
+
if (check2.passed) {
|
|
30651
|
+
passed.push(check2);
|
|
30652
|
+
} else {
|
|
30653
|
+
warnings.push(check2);
|
|
30654
|
+
}
|
|
30655
|
+
}
|
|
30656
|
+
}
|
|
30657
|
+
}
|
|
30658
|
+
if (!silent && format === "json") {
|
|
30659
|
+
console.log(JSON.stringify({ passed: blockers.length === 0, blockers, warnings }, null, 2));
|
|
30660
|
+
}
|
|
30661
|
+
return { passed: blockers.length === 0, blockers, warnings };
|
|
30662
|
+
}
|
|
30456
30663
|
async function runPrecheck(config2, prd, options) {
|
|
30457
30664
|
const workdir = options?.workdir || process.cwd();
|
|
30458
30665
|
const format = options?.format || "human";
|
|
@@ -30461,47 +30668,33 @@ async function runPrecheck(config2, prd, options) {
|
|
|
30461
30668
|
const blockers = [];
|
|
30462
30669
|
const warnings = [];
|
|
30463
30670
|
const tier1Checks = [
|
|
30464
|
-
(
|
|
30465
|
-
()
|
|
30466
|
-
(
|
|
30467
|
-
() => checkPRDValid(prd),
|
|
30468
|
-
() => checkAgentCLI(config2),
|
|
30469
|
-
() => checkDependenciesInstalled(workdir),
|
|
30470
|
-
() => checkTestCommand(config2),
|
|
30471
|
-
() => checkLintCommand(config2),
|
|
30472
|
-
() => checkTypecheckCommand(config2),
|
|
30473
|
-
() => checkGitUserConfigured(workdir)
|
|
30671
|
+
...getEarlyEnvironmentBlockers(workdir),
|
|
30672
|
+
...getProjectBlockers(prd),
|
|
30673
|
+
...getLateEnvironmentBlockers(config2, workdir)
|
|
30474
30674
|
];
|
|
30675
|
+
let tier1Blocked = false;
|
|
30475
30676
|
for (const checkFn of tier1Checks) {
|
|
30476
|
-
const
|
|
30477
|
-
|
|
30478
|
-
|
|
30677
|
+
for (const check2 of normalizeChecks(await checkFn())) {
|
|
30678
|
+
if (format === "human")
|
|
30679
|
+
printCheckResult(check2);
|
|
30680
|
+
if (check2.passed) {
|
|
30681
|
+
passed.push(check2);
|
|
30682
|
+
} else {
|
|
30683
|
+
blockers.push(check2);
|
|
30684
|
+
tier1Blocked = true;
|
|
30685
|
+
break;
|
|
30686
|
+
}
|
|
30479
30687
|
}
|
|
30480
|
-
if (
|
|
30481
|
-
passed.push(result);
|
|
30482
|
-
} else {
|
|
30483
|
-
blockers.push(result);
|
|
30688
|
+
if (tier1Blocked)
|
|
30484
30689
|
break;
|
|
30485
|
-
}
|
|
30486
30690
|
}
|
|
30487
30691
|
let flaggedStories = [];
|
|
30488
30692
|
if (blockers.length === 0) {
|
|
30489
|
-
const tier2Checks = [
|
|
30490
|
-
() => checkClaudeMdExists(workdir),
|
|
30491
|
-
() => checkDiskSpace(),
|
|
30492
|
-
() => checkPendingStories(prd),
|
|
30493
|
-
() => checkOptionalCommands(config2, workdir),
|
|
30494
|
-
() => checkGitignoreCoversNax(workdir),
|
|
30495
|
-
() => checkPromptOverrideFiles(config2, workdir),
|
|
30496
|
-
() => checkMultiAgentHealth()
|
|
30497
|
-
];
|
|
30693
|
+
const tier2Checks = [...getEnvironmentWarnings(config2, workdir), ...getProjectWarnings(prd)];
|
|
30498
30694
|
for (const checkFn of tier2Checks) {
|
|
30499
|
-
const
|
|
30500
|
-
|
|
30501
|
-
for (const check2 of checksToProcess) {
|
|
30502
|
-
if (format === "human") {
|
|
30695
|
+
for (const check2 of normalizeChecks(await checkFn())) {
|
|
30696
|
+
if (format === "human")
|
|
30503
30697
|
printCheckResult(check2);
|
|
30504
|
-
}
|
|
30505
30698
|
if (check2.passed) {
|
|
30506
30699
|
passed.push(check2);
|
|
30507
30700
|
} else {
|
|
@@ -31092,7 +31285,8 @@ async function executeFixStory(ctx, story, prd, iterations) {
|
|
|
31092
31285
|
featureDir: ctx.featureDir,
|
|
31093
31286
|
hooks: ctx.hooks,
|
|
31094
31287
|
plugins: ctx.pluginRegistry,
|
|
31095
|
-
storyStartTime: new Date().toISOString()
|
|
31288
|
+
storyStartTime: new Date().toISOString(),
|
|
31289
|
+
agentGetFn: ctx.agentGetFn
|
|
31096
31290
|
};
|
|
31097
31291
|
const result = await runPipeline(defaultPipeline, fixContext, ctx.eventEmitter);
|
|
31098
31292
|
logger?.info("acceptance", `Fix story ${story.id} ${result.success ? "passed" : "failed"}`);
|
|
@@ -31128,7 +31322,8 @@ async function runAcceptanceLoop(ctx) {
|
|
|
31128
31322
|
workdir: ctx.workdir,
|
|
31129
31323
|
featureDir: ctx.featureDir,
|
|
31130
31324
|
hooks: ctx.hooks,
|
|
31131
|
-
plugins: ctx.pluginRegistry
|
|
31325
|
+
plugins: ctx.pluginRegistry,
|
|
31326
|
+
agentGetFn: ctx.agentGetFn
|
|
31132
31327
|
};
|
|
31133
31328
|
const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
|
|
31134
31329
|
const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
|
|
@@ -32109,7 +32304,7 @@ function resolveMaxConcurrency(parallel) {
|
|
|
32109
32304
|
}
|
|
32110
32305
|
return Math.max(1, parallel);
|
|
32111
32306
|
}
|
|
32112
|
-
async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter) {
|
|
32307
|
+
async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn) {
|
|
32113
32308
|
const logger = getSafeLogger();
|
|
32114
32309
|
const maxConcurrency = resolveMaxConcurrency(parallel);
|
|
32115
32310
|
const worktreeManager = new WorktreeManager;
|
|
@@ -32139,7 +32334,8 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
|
|
|
32139
32334
|
featureDir,
|
|
32140
32335
|
hooks,
|
|
32141
32336
|
plugins,
|
|
32142
|
-
storyStartTime: new Date().toISOString()
|
|
32337
|
+
storyStartTime: new Date().toISOString(),
|
|
32338
|
+
agentGetFn
|
|
32143
32339
|
};
|
|
32144
32340
|
const worktreePaths = new Map;
|
|
32145
32341
|
for (const story of batch) {
|
|
@@ -32513,7 +32709,7 @@ async function runParallelExecution(options, initialPrd) {
|
|
|
32513
32709
|
const batchStoryMetrics = [];
|
|
32514
32710
|
let conflictedStories = [];
|
|
32515
32711
|
try {
|
|
32516
|
-
const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter);
|
|
32712
|
+
const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn);
|
|
32517
32713
|
const batchDurationMs = Date.now() - batchStartMs;
|
|
32518
32714
|
const batchCompletedAt = new Date().toISOString();
|
|
32519
32715
|
prd = parallelResult.updatedPrd;
|
|
@@ -34258,6 +34454,8 @@ async function setupRun(options) {
|
|
|
34258
34454
|
} else {
|
|
34259
34455
|
logger?.warn("precheck", "Precheck validations skipped (--skip-precheck)");
|
|
34260
34456
|
}
|
|
34457
|
+
const { sweepStaleFeatureSessions: sweepStaleFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter(), exports_adapter));
|
|
34458
|
+
await sweepStaleFeatureSessions2(workdir, feature).catch(() => {});
|
|
34261
34459
|
const lockAcquired = await acquireLock(workdir);
|
|
34262
34460
|
if (!lockAcquired) {
|
|
34263
34461
|
logger?.error("execution", "Another nax process is already running in this directory");
|
|
@@ -65707,17 +65905,13 @@ init_registry();
|
|
|
65707
65905
|
import { existsSync as existsSync9 } from "fs";
|
|
65708
65906
|
import { join as join10 } from "path";
|
|
65709
65907
|
import { createInterface } from "readline";
|
|
65908
|
+
init_test_strategy();
|
|
65710
65909
|
init_pid_registry();
|
|
65711
65910
|
init_logger2();
|
|
65712
65911
|
|
|
65713
65912
|
// src/prd/schema.ts
|
|
65913
|
+
init_test_strategy();
|
|
65714
65914
|
var VALID_COMPLEXITY = ["simple", "medium", "complex", "expert"];
|
|
65715
|
-
var VALID_TEST_STRATEGIES = [
|
|
65716
|
-
"test-after",
|
|
65717
|
-
"tdd-simple",
|
|
65718
|
-
"three-session-tdd",
|
|
65719
|
-
"three-session-tdd-lite"
|
|
65720
|
-
];
|
|
65721
65915
|
var STORY_ID_NO_SEPARATOR = /^([A-Za-z]+)(\d+)$/;
|
|
65722
65916
|
function extractJsonFromMarkdown(text) {
|
|
65723
65917
|
const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
|
|
@@ -65787,9 +65981,7 @@ function validateStory(raw, index, allIds) {
|
|
|
65787
65981
|
throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
|
|
65788
65982
|
}
|
|
65789
65983
|
const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
|
|
65790
|
-
const
|
|
65791
|
-
const normalizedStrategy = typeof rawTestStrategy === "string" ? STRATEGY_ALIASES[rawTestStrategy] ?? rawTestStrategy : rawTestStrategy;
|
|
65792
|
-
const testStrategy = normalizedStrategy !== undefined && VALID_TEST_STRATEGIES.includes(normalizedStrategy) ? normalizedStrategy : "tdd-simple";
|
|
65984
|
+
const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
|
|
65793
65985
|
const rawDeps = s.dependencies;
|
|
65794
65986
|
const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
|
|
65795
65987
|
for (const dep of dependencies) {
|
|
@@ -66056,19 +66248,11 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
|
|
|
66056
66248
|
]
|
|
66057
66249
|
}
|
|
66058
66250
|
|
|
66059
|
-
|
|
66251
|
+
${COMPLEXITY_GUIDE}
|
|
66060
66252
|
|
|
66061
|
-
|
|
66062
|
-
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
|
|
66063
|
-
- complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
|
|
66064
|
-
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
|
|
66253
|
+
${TEST_STRATEGY_GUIDE}
|
|
66065
66254
|
|
|
66066
|
-
|
|
66067
|
-
|
|
66068
|
-
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
66069
|
-
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
66070
|
-
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
66071
|
-
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.
|
|
66255
|
+
${GROUPING_RULES}
|
|
66072
66256
|
|
|
66073
66257
|
${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
|
|
66074
66258
|
Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.` : "Output ONLY the JSON object. Do not wrap in markdown code blocks."}`;
|
|
@@ -66342,6 +66526,15 @@ async function loadProjectStatusFile(projectDir) {
|
|
|
66342
66526
|
}
|
|
66343
66527
|
async function getFeatureSummary(featureName, featureDir) {
|
|
66344
66528
|
const prdPath = join13(featureDir, "prd.json");
|
|
66529
|
+
if (!existsSync11(prdPath)) {
|
|
66530
|
+
return {
|
|
66531
|
+
name: featureName,
|
|
66532
|
+
done: 0,
|
|
66533
|
+
failed: 0,
|
|
66534
|
+
pending: 0,
|
|
66535
|
+
total: 0
|
|
66536
|
+
};
|
|
66537
|
+
}
|
|
66345
66538
|
const prd = await loadPRD(prdPath);
|
|
66346
66539
|
const counts = countStories(prd);
|
|
66347
66540
|
const summary = {
|
|
@@ -66454,6 +66647,13 @@ async function displayAllFeatures(projectDir) {
|
|
|
66454
66647
|
}
|
|
66455
66648
|
async function displayFeatureDetails(featureName, featureDir) {
|
|
66456
66649
|
const prdPath = join13(featureDir, "prd.json");
|
|
66650
|
+
if (!existsSync11(prdPath)) {
|
|
66651
|
+
console.log(source_default.bold(`
|
|
66652
|
+
\uD83D\uDCCA ${featureName}
|
|
66653
|
+
`));
|
|
66654
|
+
console.log(source_default.dim(`No prd.json found. Run: nax plan -f ${featureName} --from <spec>`));
|
|
66655
|
+
return;
|
|
66656
|
+
}
|
|
66457
66657
|
const prd = await loadPRD(prdPath);
|
|
66458
66658
|
const counts = countStories(prd);
|
|
66459
66659
|
const status = await loadStatusFile(featureDir);
|
|
@@ -68888,6 +69088,7 @@ async function unlockCommand(options) {
|
|
|
68888
69088
|
init_config();
|
|
68889
69089
|
|
|
68890
69090
|
// src/execution/runner.ts
|
|
69091
|
+
init_adapter();
|
|
68891
69092
|
init_registry();
|
|
68892
69093
|
init_hooks();
|
|
68893
69094
|
init_logger2();
|
|
@@ -69063,9 +69264,20 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
|
|
|
69063
69264
|
batchingEnabled: options.useBatch
|
|
69064
69265
|
});
|
|
69065
69266
|
clearCache();
|
|
69066
|
-
const
|
|
69267
|
+
const readyStories = getAllReadyStories(prd);
|
|
69268
|
+
logger?.debug("routing", "Ready stories for batch routing", {
|
|
69269
|
+
readyCount: readyStories.length,
|
|
69270
|
+
readyIds: readyStories.map((s) => s.id),
|
|
69271
|
+
allStories: prd.userStories.map((s) => ({
|
|
69272
|
+
id: s.id,
|
|
69273
|
+
status: s.status,
|
|
69274
|
+
passes: s.passes,
|
|
69275
|
+
deps: s.dependencies
|
|
69276
|
+
}))
|
|
69277
|
+
});
|
|
69278
|
+
const batchPlan = options.useBatch ? precomputeBatchPlan(readyStories, 4) : [];
|
|
69067
69279
|
if (options.useBatch) {
|
|
69068
|
-
await tryLlmBatchRoute(options.config,
|
|
69280
|
+
await tryLlmBatchRoute(options.config, readyStories, "routing");
|
|
69069
69281
|
}
|
|
69070
69282
|
if (options.parallel !== undefined) {
|
|
69071
69283
|
const runParallelExecution2 = options.runParallelExecution ?? (await Promise.resolve().then(() => (init_parallel_executor(), exports_parallel_executor))).runParallelExecution;
|
|
@@ -69301,6 +69513,7 @@ async function run(options) {
|
|
|
69301
69513
|
} finally {
|
|
69302
69514
|
stopHeartbeat();
|
|
69303
69515
|
cleanupCrashHandlers();
|
|
69516
|
+
await sweepFeatureSessions(workdir, feature).catch(() => {});
|
|
69304
69517
|
const { cleanupRun: cleanupRun2 } = await Promise.resolve().then(() => (init_run_cleanup(), exports_run_cleanup));
|
|
69305
69518
|
await cleanupRun2({
|
|
69306
69519
|
runId,
|
|
@@ -76762,7 +76975,7 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
|
|
|
76762
76975
|
console.log(source_default.dim(`
|
|
76763
76976
|
Next: nax features create <name>`));
|
|
76764
76977
|
});
|
|
76765
|
-
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("--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) => {
|
|
76978
|
+
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) => {
|
|
76766
76979
|
let workdir;
|
|
76767
76980
|
try {
|
|
76768
76981
|
workdir = validateDirectory(options.dir);
|
|
@@ -76806,6 +77019,25 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
76806
77019
|
const featureDir = join43(naxDir, "features", options.feature);
|
|
76807
77020
|
const prdPath = join43(featureDir, "prd.json");
|
|
76808
77021
|
if (options.plan && options.from) {
|
|
77022
|
+
if (existsSync32(prdPath) && !options.force) {
|
|
77023
|
+
console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
|
|
77024
|
+
console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
|
|
77025
|
+
process.exit(1);
|
|
77026
|
+
}
|
|
77027
|
+
if (!options.skipPrecheck) {
|
|
77028
|
+
const { runEnvironmentPrecheck: runEnvironmentPrecheck2 } = await Promise.resolve().then(() => (init_precheck(), exports_precheck));
|
|
77029
|
+
console.log(source_default.dim(`
|
|
77030
|
+
[Pre-plan environment check]`));
|
|
77031
|
+
const envResult = await runEnvironmentPrecheck2(config2, workdir);
|
|
77032
|
+
if (!envResult.passed) {
|
|
77033
|
+
console.error(source_default.red(`
|
|
77034
|
+
\u274C Environment precheck failed \u2014 cannot proceed with planning.`));
|
|
77035
|
+
for (const b of envResult.blockers) {
|
|
77036
|
+
console.error(source_default.red(` ${b.name}: ${b.message}`));
|
|
77037
|
+
}
|
|
77038
|
+
process.exit(1);
|
|
77039
|
+
}
|
|
77040
|
+
}
|
|
76809
77041
|
try {
|
|
76810
77042
|
mkdirSync6(featureDir, { recursive: true });
|
|
76811
77043
|
const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|