@nathapp/nax 0.49.3 → 0.49.6
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/README.md +2 -0
- package/dist/nax.js +217 -121
- package/package.json +1 -1
- package/src/agents/acp/adapter.ts +53 -23
- package/src/agents/acp/spawn-client.ts +0 -2
- package/src/agents/claude/execution.ts +14 -0
- package/src/agents/types.ts +7 -0
- package/src/cli/prompts-main.ts +4 -59
- package/src/cli/prompts-shared.ts +70 -0
- package/src/cli/prompts-tdd.ts +1 -1
- package/src/config/merge.ts +18 -0
- package/src/interaction/plugins/webhook.ts +44 -25
- package/src/tdd/cleanup.ts +15 -6
- package/src/tdd/isolation.ts +9 -2
- package/src/tdd/rectification-gate.ts +41 -10
- package/src/tdd/session-runner.ts +71 -38
- package/src/verification/executor.ts +4 -1
- package/src/verification/strategies/acceptance.ts +4 -1
package/dist/nax.js
CHANGED
|
@@ -3667,6 +3667,16 @@ function buildAllowedEnv(options) {
|
|
|
3667
3667
|
async function executeOnce(binary, options, pidRegistry) {
|
|
3668
3668
|
const cmd = _runOnceDeps.buildCmd(binary, options);
|
|
3669
3669
|
const startTime = Date.now();
|
|
3670
|
+
if (options.sessionRole || options.acpSessionName || options.keepSessionOpen) {
|
|
3671
|
+
const logger2 = getLogger();
|
|
3672
|
+
logger2.debug("agent", "CLI mode: session options received (unused)", {
|
|
3673
|
+
sessionRole: options.sessionRole,
|
|
3674
|
+
acpSessionName: options.acpSessionName,
|
|
3675
|
+
keepSessionOpen: options.keepSessionOpen,
|
|
3676
|
+
featureName: options.featureName,
|
|
3677
|
+
storyId: options.storyId
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3670
3680
|
const proc = Bun.spawn(cmd, {
|
|
3671
3681
|
cwd: options.workdir,
|
|
3672
3682
|
stdout: "pipe",
|
|
@@ -19276,7 +19286,6 @@ class SpawnAcpClient {
|
|
|
19276
19286
|
model;
|
|
19277
19287
|
cwd;
|
|
19278
19288
|
timeoutSeconds;
|
|
19279
|
-
permissionMode;
|
|
19280
19289
|
env;
|
|
19281
19290
|
pidRegistry;
|
|
19282
19291
|
constructor(cmdStr, cwd, timeoutSeconds, pidRegistry) {
|
|
@@ -19290,7 +19299,6 @@ class SpawnAcpClient {
|
|
|
19290
19299
|
this.agentName = lastToken;
|
|
19291
19300
|
this.cwd = cwd || process.cwd();
|
|
19292
19301
|
this.timeoutSeconds = timeoutSeconds || 1800;
|
|
19293
|
-
this.permissionMode = "approve-reads";
|
|
19294
19302
|
this.env = buildAllowedEnv2();
|
|
19295
19303
|
this.pidRegistry = pidRegistry;
|
|
19296
19304
|
}
|
|
@@ -19432,7 +19440,13 @@ async function closeAcpSession(session) {
|
|
|
19432
19440
|
function acpSessionsPath(workdir, featureName) {
|
|
19433
19441
|
return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
|
|
19434
19442
|
}
|
|
19435
|
-
|
|
19443
|
+
function sidecarSessionName(entry) {
|
|
19444
|
+
return typeof entry === "string" ? entry : entry.sessionName;
|
|
19445
|
+
}
|
|
19446
|
+
function sidecarAgentName(entry) {
|
|
19447
|
+
return typeof entry === "string" ? "claude" : entry.agentName;
|
|
19448
|
+
}
|
|
19449
|
+
async function saveAcpSession(workdir, featureName, storyId, sessionName, agentName = "claude") {
|
|
19436
19450
|
try {
|
|
19437
19451
|
const path = acpSessionsPath(workdir, featureName);
|
|
19438
19452
|
let data = {};
|
|
@@ -19440,7 +19454,7 @@ async function saveAcpSession(workdir, featureName, storyId, sessionName) {
|
|
|
19440
19454
|
const existing = await Bun.file(path).text();
|
|
19441
19455
|
data = JSON.parse(existing);
|
|
19442
19456
|
} catch {}
|
|
19443
|
-
data[storyId] = sessionName;
|
|
19457
|
+
data[storyId] = { sessionName, agentName };
|
|
19444
19458
|
await Bun.write(path, JSON.stringify(data, null, 2));
|
|
19445
19459
|
} catch (err) {
|
|
19446
19460
|
getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
|
|
@@ -19467,7 +19481,8 @@ async function readAcpSession(workdir, featureName, storyId) {
|
|
|
19467
19481
|
const path = acpSessionsPath(workdir, featureName);
|
|
19468
19482
|
const existing = await Bun.file(path).text();
|
|
19469
19483
|
const data = JSON.parse(existing);
|
|
19470
|
-
|
|
19484
|
+
const entry = data[storyId];
|
|
19485
|
+
return entry ? sidecarSessionName(entry) : null;
|
|
19471
19486
|
} catch {
|
|
19472
19487
|
return null;
|
|
19473
19488
|
}
|
|
@@ -19486,24 +19501,34 @@ async function sweepFeatureSessions(workdir, featureName) {
|
|
|
19486
19501
|
return;
|
|
19487
19502
|
const logger = getSafeLogger();
|
|
19488
19503
|
logger?.info("acp-adapter", `[sweep] Closing ${entries.length} open sessions for feature: ${featureName}`);
|
|
19489
|
-
const
|
|
19490
|
-
const
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
19496
|
-
|
|
19497
|
-
|
|
19498
|
-
|
|
19504
|
+
const byAgent = new Map;
|
|
19505
|
+
for (const [, entry] of entries) {
|
|
19506
|
+
const agent = sidecarAgentName(entry);
|
|
19507
|
+
const name = sidecarSessionName(entry);
|
|
19508
|
+
if (!byAgent.has(agent))
|
|
19509
|
+
byAgent.set(agent, []);
|
|
19510
|
+
byAgent.get(agent)?.push(name);
|
|
19511
|
+
}
|
|
19512
|
+
for (const [agentName, sessionNames] of byAgent) {
|
|
19513
|
+
const cmdStr = `acpx ${agentName}`;
|
|
19514
|
+
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
19515
|
+
try {
|
|
19516
|
+
await client.start();
|
|
19517
|
+
for (const sessionName of sessionNames) {
|
|
19518
|
+
try {
|
|
19519
|
+
if (client.loadSession) {
|
|
19520
|
+
const session = await client.loadSession(sessionName, agentName, "approve-reads");
|
|
19521
|
+
if (session) {
|
|
19522
|
+
await session.close().catch(() => {});
|
|
19523
|
+
}
|
|
19499
19524
|
}
|
|
19525
|
+
} catch (err) {
|
|
19526
|
+
logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
|
|
19500
19527
|
}
|
|
19501
|
-
} catch (err) {
|
|
19502
|
-
logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
|
|
19503
19528
|
}
|
|
19529
|
+
} finally {
|
|
19530
|
+
await client.close().catch(() => {});
|
|
19504
19531
|
}
|
|
19505
|
-
} finally {
|
|
19506
|
-
await client.close().catch(() => {});
|
|
19507
19532
|
}
|
|
19508
19533
|
try {
|
|
19509
19534
|
await Bun.write(path, JSON.stringify({}, null, 2));
|
|
@@ -19644,7 +19669,7 @@ class AcpAgentAdapter {
|
|
|
19644
19669
|
});
|
|
19645
19670
|
const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
|
|
19646
19671
|
if (options.featureName && options.storyId) {
|
|
19647
|
-
await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName);
|
|
19672
|
+
await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName, this.name);
|
|
19648
19673
|
}
|
|
19649
19674
|
let lastResponse = null;
|
|
19650
19675
|
let timedOut = false;
|
|
@@ -19702,13 +19727,15 @@ class AcpAgentAdapter {
|
|
|
19702
19727
|
}
|
|
19703
19728
|
runState.succeeded = !timedOut && lastResponse?.stopReason === "end_turn";
|
|
19704
19729
|
} finally {
|
|
19705
|
-
if (runState.succeeded) {
|
|
19730
|
+
if (runState.succeeded && !options.keepSessionOpen) {
|
|
19706
19731
|
await closeAcpSession(session);
|
|
19707
19732
|
if (options.featureName && options.storyId) {
|
|
19708
19733
|
await clearAcpSession(options.workdir, options.featureName, options.storyId);
|
|
19709
19734
|
}
|
|
19710
|
-
} else {
|
|
19735
|
+
} else if (!runState.succeeded) {
|
|
19711
19736
|
getSafeLogger()?.info("acp-adapter", "Keeping session open for retry", { sessionName });
|
|
19737
|
+
} else {
|
|
19738
|
+
getSafeLogger()?.debug("acp-adapter", "Keeping session open (keepSessionOpen=true)", { sessionName });
|
|
19712
19739
|
}
|
|
19713
19740
|
await client.close().catch(() => {});
|
|
19714
19741
|
}
|
|
@@ -20726,6 +20753,18 @@ function mergePackageConfig(root, packageOverride) {
|
|
|
20726
20753
|
...packageOverride.review,
|
|
20727
20754
|
commands: {
|
|
20728
20755
|
...root.review.commands,
|
|
20756
|
+
...packageOverride.quality?.commands?.lint !== undefined && {
|
|
20757
|
+
lint: packageOverride.quality.commands.lint
|
|
20758
|
+
},
|
|
20759
|
+
...packageOverride.quality?.commands?.lintFix !== undefined && {
|
|
20760
|
+
lintFix: packageOverride.quality.commands.lintFix
|
|
20761
|
+
},
|
|
20762
|
+
...packageOverride.quality?.commands?.typecheck !== undefined && {
|
|
20763
|
+
typecheck: packageOverride.quality.commands.typecheck
|
|
20764
|
+
},
|
|
20765
|
+
...packageOverride.quality?.commands?.test !== undefined && {
|
|
20766
|
+
test: packageOverride.quality.commands.test
|
|
20767
|
+
},
|
|
20729
20768
|
...packageOverride.review?.commands
|
|
20730
20769
|
}
|
|
20731
20770
|
},
|
|
@@ -22250,7 +22289,7 @@ var package_default;
|
|
|
22250
22289
|
var init_package = __esm(() => {
|
|
22251
22290
|
package_default = {
|
|
22252
22291
|
name: "@nathapp/nax",
|
|
22253
|
-
version: "0.49.
|
|
22292
|
+
version: "0.49.6",
|
|
22254
22293
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22255
22294
|
type: "module",
|
|
22256
22295
|
bin: {
|
|
@@ -22323,8 +22362,8 @@ var init_version = __esm(() => {
|
|
|
22323
22362
|
NAX_VERSION = package_default.version;
|
|
22324
22363
|
NAX_COMMIT = (() => {
|
|
22325
22364
|
try {
|
|
22326
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22327
|
-
return "
|
|
22365
|
+
if (/^[0-9a-f]{6,10}$/.test("a1f7e2d"))
|
|
22366
|
+
return "a1f7e2d";
|
|
22328
22367
|
} catch {}
|
|
22329
22368
|
try {
|
|
22330
22369
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -23309,6 +23348,7 @@ class WebhookInteractionPlugin {
|
|
|
23309
23348
|
server = null;
|
|
23310
23349
|
serverStartPromise = null;
|
|
23311
23350
|
pendingResponses = new Map;
|
|
23351
|
+
receiveCallbacks = new Map;
|
|
23312
23352
|
async init(config2) {
|
|
23313
23353
|
const cfg = WebhookConfigSchema.parse(config2);
|
|
23314
23354
|
this.config = {
|
|
@@ -23358,27 +23398,39 @@ class WebhookInteractionPlugin {
|
|
|
23358
23398
|
}
|
|
23359
23399
|
async receive(requestId, timeout = 60000) {
|
|
23360
23400
|
await this.startServer();
|
|
23361
|
-
const
|
|
23362
|
-
|
|
23363
|
-
|
|
23364
|
-
|
|
23365
|
-
const response = this.pendingResponses.get(requestId);
|
|
23366
|
-
if (response) {
|
|
23367
|
-
this.pendingResponses.delete(requestId);
|
|
23368
|
-
return response;
|
|
23369
|
-
}
|
|
23370
|
-
await _webhookPluginDeps.sleep(backoffMs);
|
|
23371
|
-
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
23401
|
+
const early = this.pendingResponses.get(requestId);
|
|
23402
|
+
if (early) {
|
|
23403
|
+
this.pendingResponses.delete(requestId);
|
|
23404
|
+
return early;
|
|
23372
23405
|
}
|
|
23373
|
-
return {
|
|
23374
|
-
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23406
|
+
return new Promise((resolve7) => {
|
|
23407
|
+
const timer = setTimeout(() => {
|
|
23408
|
+
this.receiveCallbacks.delete(requestId);
|
|
23409
|
+
resolve7({
|
|
23410
|
+
requestId,
|
|
23411
|
+
action: "skip",
|
|
23412
|
+
respondedBy: "timeout",
|
|
23413
|
+
respondedAt: Date.now()
|
|
23414
|
+
});
|
|
23415
|
+
}, timeout);
|
|
23416
|
+
this.receiveCallbacks.set(requestId, (response) => {
|
|
23417
|
+
clearTimeout(timer);
|
|
23418
|
+
this.receiveCallbacks.delete(requestId);
|
|
23419
|
+
resolve7(response);
|
|
23420
|
+
});
|
|
23421
|
+
});
|
|
23379
23422
|
}
|
|
23380
23423
|
async cancel(requestId) {
|
|
23381
23424
|
this.pendingResponses.delete(requestId);
|
|
23425
|
+
this.receiveCallbacks.delete(requestId);
|
|
23426
|
+
}
|
|
23427
|
+
deliverResponse(requestId, response) {
|
|
23428
|
+
const cb = this.receiveCallbacks.get(requestId);
|
|
23429
|
+
if (cb) {
|
|
23430
|
+
cb(response);
|
|
23431
|
+
} else {
|
|
23432
|
+
this.pendingResponses.set(requestId, response);
|
|
23433
|
+
}
|
|
23382
23434
|
}
|
|
23383
23435
|
async startServer() {
|
|
23384
23436
|
if (this.server)
|
|
@@ -23431,7 +23483,7 @@ class WebhookInteractionPlugin {
|
|
|
23431
23483
|
try {
|
|
23432
23484
|
const parsed = JSON.parse(body);
|
|
23433
23485
|
const response = InteractionResponseSchema.parse(parsed);
|
|
23434
|
-
this.
|
|
23486
|
+
this.deliverResponse(requestId, response);
|
|
23435
23487
|
} catch {
|
|
23436
23488
|
return new Response("Bad Request: Invalid response format", { status: 400 });
|
|
23437
23489
|
}
|
|
@@ -23439,7 +23491,7 @@ class WebhookInteractionPlugin {
|
|
|
23439
23491
|
try {
|
|
23440
23492
|
const parsed = await req.json();
|
|
23441
23493
|
const response = InteractionResponseSchema.parse(parsed);
|
|
23442
|
-
this.
|
|
23494
|
+
this.deliverResponse(requestId, response);
|
|
23443
23495
|
} catch {
|
|
23444
23496
|
return new Response("Bad Request: Invalid response format", { status: 400 });
|
|
23445
23497
|
}
|
|
@@ -23466,12 +23518,9 @@ class WebhookInteractionPlugin {
|
|
|
23466
23518
|
}
|
|
23467
23519
|
}
|
|
23468
23520
|
}
|
|
23469
|
-
var
|
|
23521
|
+
var WebhookConfigSchema, InteractionResponseSchema;
|
|
23470
23522
|
var init_webhook = __esm(() => {
|
|
23471
23523
|
init_zod();
|
|
23472
|
-
_webhookPluginDeps = {
|
|
23473
|
-
sleep: (ms) => Bun.sleep(ms)
|
|
23474
|
-
};
|
|
23475
23524
|
WebhookConfigSchema = exports_external.object({
|
|
23476
23525
|
url: exports_external.string().url().optional(),
|
|
23477
23526
|
callbackPort: exports_external.number().int().min(1024).max(65535).optional(),
|
|
@@ -26037,13 +26086,13 @@ function isSourceFile(filePath) {
|
|
|
26037
26086
|
return SRC_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
26038
26087
|
}
|
|
26039
26088
|
async function getChangedFiles2(workdir, fromRef = "HEAD") {
|
|
26040
|
-
const proc =
|
|
26089
|
+
const proc = _isolationDeps.spawn(["git", "diff", "--name-only", fromRef], {
|
|
26041
26090
|
cwd: workdir,
|
|
26042
26091
|
stdout: "pipe",
|
|
26043
26092
|
stderr: "pipe"
|
|
26044
26093
|
});
|
|
26094
|
+
const output = await Bun.readableStreamToText(proc.stdout);
|
|
26045
26095
|
await proc.exited;
|
|
26046
|
-
const output = await new Response(proc.stdout).text();
|
|
26047
26096
|
return output.trim().split(`
|
|
26048
26097
|
`).filter(Boolean);
|
|
26049
26098
|
}
|
|
@@ -26090,8 +26139,9 @@ async function verifyImplementerIsolation(workdir, beforeRef) {
|
|
|
26090
26139
|
description: "Implementer should not modify test files"
|
|
26091
26140
|
};
|
|
26092
26141
|
}
|
|
26093
|
-
var TEST_PATTERNS, SRC_PATTERNS;
|
|
26142
|
+
var _isolationDeps, TEST_PATTERNS, SRC_PATTERNS;
|
|
26094
26143
|
var init_isolation = __esm(() => {
|
|
26144
|
+
_isolationDeps = { spawn: Bun.spawn };
|
|
26095
26145
|
TEST_PATTERNS = [/^test\//, /^tests\//, /^__tests__\//, /\.spec\.\w+$/, /\.test\.\w+$/, /\.e2e-spec\.\w+$/];
|
|
26096
26146
|
SRC_PATTERNS = [/^src\//, /^lib\//, /^packages\//];
|
|
26097
26147
|
});
|
|
@@ -26289,7 +26339,7 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
|
|
|
26289
26339
|
const shell = options?.shell ?? "/bin/sh";
|
|
26290
26340
|
const gracePeriodMs = options?.gracePeriodMs ?? 5000;
|
|
26291
26341
|
const drainTimeoutMs = options?.drainTimeoutMs ?? 2000;
|
|
26292
|
-
const proc =
|
|
26342
|
+
const proc = _executorDeps.spawn([shell, "-c", command], {
|
|
26293
26343
|
stdout: "pipe",
|
|
26294
26344
|
stderr: "pipe",
|
|
26295
26345
|
env: env2 || normalizeEnvironment(process.env),
|
|
@@ -26383,8 +26433,9 @@ function buildTestCommand(baseCommand, options) {
|
|
|
26383
26433
|
}
|
|
26384
26434
|
return command;
|
|
26385
26435
|
}
|
|
26386
|
-
var DEFAULT_STRIP_ENV_VARS;
|
|
26436
|
+
var _executorDeps, DEFAULT_STRIP_ENV_VARS;
|
|
26387
26437
|
var init_executor = __esm(() => {
|
|
26438
|
+
_executorDeps = { spawn: Bun.spawn };
|
|
26388
26439
|
DEFAULT_STRIP_ENV_VARS = ["CLAUDECODE", "REPL_ID", "AGENT"];
|
|
26389
26440
|
});
|
|
26390
26441
|
|
|
@@ -26684,15 +26735,15 @@ var init_verification = __esm(() => {
|
|
|
26684
26735
|
// src/tdd/cleanup.ts
|
|
26685
26736
|
async function getPgid(pid) {
|
|
26686
26737
|
try {
|
|
26687
|
-
const proc =
|
|
26738
|
+
const proc = _cleanupDeps.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
|
|
26688
26739
|
stdout: "pipe",
|
|
26689
26740
|
stderr: "pipe"
|
|
26690
26741
|
});
|
|
26742
|
+
const output = await Bun.readableStreamToText(proc.stdout);
|
|
26691
26743
|
const exitCode = await proc.exited;
|
|
26692
26744
|
if (exitCode !== 0) {
|
|
26693
26745
|
return null;
|
|
26694
26746
|
}
|
|
26695
|
-
const output = await new Response(proc.stdout).text();
|
|
26696
26747
|
const pgid = Number.parseInt(output.trim(), 10);
|
|
26697
26748
|
return Number.isNaN(pgid) ? null : pgid;
|
|
26698
26749
|
} catch {
|
|
@@ -26706,7 +26757,7 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
|
|
|
26706
26757
|
return;
|
|
26707
26758
|
}
|
|
26708
26759
|
try {
|
|
26709
|
-
|
|
26760
|
+
_cleanupDeps.kill(-pgid, "SIGTERM");
|
|
26710
26761
|
} catch (error48) {
|
|
26711
26762
|
const err = error48;
|
|
26712
26763
|
if (err.code !== "ESRCH") {
|
|
@@ -26714,11 +26765,11 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
|
|
|
26714
26765
|
}
|
|
26715
26766
|
return;
|
|
26716
26767
|
}
|
|
26717
|
-
await
|
|
26768
|
+
await _cleanupDeps.sleep(gracePeriodMs);
|
|
26718
26769
|
const pgidAfterWait = await getPgid(pid);
|
|
26719
26770
|
if (pgidAfterWait && pgidAfterWait === pgid) {
|
|
26720
26771
|
try {
|
|
26721
|
-
|
|
26772
|
+
_cleanupDeps.kill(-pgid, "SIGKILL");
|
|
26722
26773
|
} catch {}
|
|
26723
26774
|
}
|
|
26724
26775
|
} catch (error48) {
|
|
@@ -26729,8 +26780,14 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
|
|
|
26729
26780
|
});
|
|
26730
26781
|
}
|
|
26731
26782
|
}
|
|
26783
|
+
var _cleanupDeps;
|
|
26732
26784
|
var init_cleanup = __esm(() => {
|
|
26733
26785
|
init_logger2();
|
|
26786
|
+
_cleanupDeps = {
|
|
26787
|
+
spawn: Bun.spawn,
|
|
26788
|
+
sleep: Bun.sleep,
|
|
26789
|
+
kill: process.kill.bind(process)
|
|
26790
|
+
};
|
|
26734
26791
|
});
|
|
26735
26792
|
|
|
26736
26793
|
// src/tdd/prompts.ts
|
|
@@ -26753,10 +26810,12 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
|
|
|
26753
26810
|
storyId: story.id,
|
|
26754
26811
|
timeout: fullSuiteTimeout
|
|
26755
26812
|
});
|
|
26756
|
-
const fullSuiteResult = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
26813
|
+
const fullSuiteResult = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
26814
|
+
cwd: workdir
|
|
26815
|
+
});
|
|
26757
26816
|
const fullSuitePassed = fullSuiteResult.success && fullSuiteResult.exitCode === 0;
|
|
26758
26817
|
if (!fullSuitePassed && fullSuiteResult.output) {
|
|
26759
|
-
const testSummary = parseBunTestOutput(fullSuiteResult.output);
|
|
26818
|
+
const testSummary = _rectificationGateDeps.parseBunTestOutput(fullSuiteResult.output);
|
|
26760
26819
|
if (testSummary.failed > 0) {
|
|
26761
26820
|
return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName);
|
|
26762
26821
|
}
|
|
@@ -26797,8 +26856,14 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26797
26856
|
failedTests: testSummary.failed,
|
|
26798
26857
|
passedTests: testSummary.passed
|
|
26799
26858
|
});
|
|
26800
|
-
|
|
26859
|
+
const rectificationSessionName = buildSessionName(workdir, featureName, story.id, "implementer");
|
|
26860
|
+
logger.debug("tdd", "Rectification session name (shared across all attempts)", {
|
|
26861
|
+
storyId: story.id,
|
|
26862
|
+
sessionName: rectificationSessionName
|
|
26863
|
+
});
|
|
26864
|
+
while (_rectificationGateDeps.shouldRetryRectification(rectificationState, rectificationConfig)) {
|
|
26801
26865
|
rectificationState.attempt++;
|
|
26866
|
+
const isLastAttempt = rectificationState.attempt >= rectificationConfig.maxRetries;
|
|
26802
26867
|
logger.info("tdd", `-> Implementer rectification attempt ${rectificationState.attempt}/${rectificationConfig.maxRetries}`, { storyId: story.id, currentFailures: rectificationState.currentFailures });
|
|
26803
26868
|
const rectificationPrompt = buildImplementerRectificationPrompt(testSummary.failures, story, contextMarkdown, rectificationConfig);
|
|
26804
26869
|
const rectifyBeforeRef = await captureGitRef(workdir) ?? "HEAD";
|
|
@@ -26814,7 +26879,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26814
26879
|
maxInteractionTurns: config2.agent?.maxInteractionTurns,
|
|
26815
26880
|
featureName,
|
|
26816
26881
|
storyId: story.id,
|
|
26817
|
-
sessionRole: "implementer"
|
|
26882
|
+
sessionRole: "implementer",
|
|
26883
|
+
acpSessionName: rectificationSessionName,
|
|
26884
|
+
keepSessionOpen: !isLastAttempt
|
|
26818
26885
|
});
|
|
26819
26886
|
if (!rectifyResult.success && rectifyResult.pid) {
|
|
26820
26887
|
await cleanupProcessTree(rectifyResult.pid);
|
|
@@ -26842,7 +26909,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26842
26909
|
});
|
|
26843
26910
|
break;
|
|
26844
26911
|
}
|
|
26845
|
-
const retryFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
26912
|
+
const retryFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
26913
|
+
cwd: workdir
|
|
26914
|
+
});
|
|
26846
26915
|
const retrySuitePassed = retryFullSuite.success && retryFullSuite.exitCode === 0;
|
|
26847
26916
|
if (retrySuitePassed) {
|
|
26848
26917
|
logger.info("tdd", "Full suite gate passed after rectification!", {
|
|
@@ -26852,7 +26921,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26852
26921
|
return true;
|
|
26853
26922
|
}
|
|
26854
26923
|
if (retryFullSuite.output) {
|
|
26855
|
-
const newTestSummary = parseBunTestOutput(retryFullSuite.output);
|
|
26924
|
+
const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
|
|
26856
26925
|
rectificationState.currentFailures = newTestSummary.failed;
|
|
26857
26926
|
testSummary.failures = newTestSummary.failures;
|
|
26858
26927
|
testSummary.failed = newTestSummary.failed;
|
|
@@ -26864,7 +26933,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26864
26933
|
remainingFailures: rectificationState.currentFailures
|
|
26865
26934
|
});
|
|
26866
26935
|
}
|
|
26867
|
-
const finalFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
26936
|
+
const finalFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
26937
|
+
cwd: workdir
|
|
26938
|
+
});
|
|
26868
26939
|
const finalSuitePassed = finalFullSuite.success && finalFullSuite.exitCode === 0;
|
|
26869
26940
|
if (!finalSuitePassed) {
|
|
26870
26941
|
logger.warn("tdd", "[WARN] Full suite gate failed after rectification exhausted", {
|
|
@@ -26877,13 +26948,20 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26877
26948
|
logger.info("tdd", "Full suite gate passed", { storyId: story.id });
|
|
26878
26949
|
return true;
|
|
26879
26950
|
}
|
|
26951
|
+
var _rectificationGateDeps;
|
|
26880
26952
|
var init_rectification_gate = __esm(() => {
|
|
26953
|
+
init_adapter2();
|
|
26881
26954
|
init_config();
|
|
26882
26955
|
init_git();
|
|
26883
26956
|
init_verification();
|
|
26884
26957
|
init_cleanup();
|
|
26885
26958
|
init_isolation();
|
|
26886
26959
|
init_prompts();
|
|
26960
|
+
_rectificationGateDeps = {
|
|
26961
|
+
executeWithTimeout,
|
|
26962
|
+
parseBunTestOutput,
|
|
26963
|
+
shouldRetryRectification
|
|
26964
|
+
};
|
|
26887
26965
|
});
|
|
26888
26966
|
|
|
26889
26967
|
// src/prompts/sections/conventions.ts
|
|
@@ -27373,7 +27451,7 @@ ${tail}`;
|
|
|
27373
27451
|
async function rollbackToRef(workdir, ref) {
|
|
27374
27452
|
const logger = getLogger();
|
|
27375
27453
|
logger.warn("tdd", "Rolling back git changes", { ref });
|
|
27376
|
-
const resetProc =
|
|
27454
|
+
const resetProc = _sessionRunnerDeps.spawn(["git", "reset", "--hard", ref], {
|
|
27377
27455
|
cwd: workdir,
|
|
27378
27456
|
stdout: "pipe",
|
|
27379
27457
|
stderr: "pipe"
|
|
@@ -27384,7 +27462,7 @@ async function rollbackToRef(workdir, ref) {
|
|
|
27384
27462
|
logger.error("tdd", "Failed to rollback git changes", { ref, stderr });
|
|
27385
27463
|
throw new Error(`Git rollback failed: ${stderr}`);
|
|
27386
27464
|
}
|
|
27387
|
-
const cleanProc =
|
|
27465
|
+
const cleanProc = _sessionRunnerDeps.spawn(["git", "clean", "-fd"], {
|
|
27388
27466
|
cwd: workdir,
|
|
27389
27467
|
stdout: "pipe",
|
|
27390
27468
|
stderr: "pipe"
|
|
@@ -27399,19 +27477,24 @@ async function rollbackToRef(workdir, ref) {
|
|
|
27399
27477
|
async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
|
|
27400
27478
|
const startTime = Date.now();
|
|
27401
27479
|
let prompt;
|
|
27402
|
-
|
|
27403
|
-
|
|
27404
|
-
|
|
27405
|
-
|
|
27406
|
-
|
|
27407
|
-
|
|
27408
|
-
|
|
27409
|
-
|
|
27410
|
-
|
|
27411
|
-
|
|
27480
|
+
if (_sessionRunnerDeps.buildPrompt) {
|
|
27481
|
+
prompt = await _sessionRunnerDeps.buildPrompt(role, config2, story, workdir, contextMarkdown, lite, constitution);
|
|
27482
|
+
} else {
|
|
27483
|
+
switch (role) {
|
|
27484
|
+
case "test-writer":
|
|
27485
|
+
prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
|
|
27486
|
+
break;
|
|
27487
|
+
case "implementer":
|
|
27488
|
+
prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
|
|
27489
|
+
break;
|
|
27490
|
+
case "verifier":
|
|
27491
|
+
prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
|
|
27492
|
+
break;
|
|
27493
|
+
}
|
|
27412
27494
|
}
|
|
27413
27495
|
const logger = getLogger();
|
|
27414
27496
|
logger.info("tdd", `-> Session: ${role}`, { role, storyId: story.id, lite });
|
|
27497
|
+
const keepSessionOpen = role === "implementer" && (config2.execution.rectification?.enabled ?? false);
|
|
27415
27498
|
const result = await agent.run({
|
|
27416
27499
|
prompt,
|
|
27417
27500
|
workdir,
|
|
@@ -27424,10 +27507,11 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
27424
27507
|
maxInteractionTurns: config2.agent?.maxInteractionTurns,
|
|
27425
27508
|
featureName,
|
|
27426
27509
|
storyId: story.id,
|
|
27427
|
-
sessionRole: role
|
|
27510
|
+
sessionRole: role,
|
|
27511
|
+
keepSessionOpen
|
|
27428
27512
|
});
|
|
27429
27513
|
if (!result.success && result.pid) {
|
|
27430
|
-
await cleanupProcessTree(result.pid);
|
|
27514
|
+
await _sessionRunnerDeps.cleanupProcessTree(result.pid);
|
|
27431
27515
|
}
|
|
27432
27516
|
if (result.success) {
|
|
27433
27517
|
logger.info("tdd", `Session complete: ${role}`, {
|
|
@@ -27449,12 +27533,12 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
27449
27533
|
if (!skipIsolation) {
|
|
27450
27534
|
if (role === "test-writer") {
|
|
27451
27535
|
const allowedPaths = config2.tdd.testWriterAllowedPaths ?? ["src/index.ts", "src/**/index.ts"];
|
|
27452
|
-
isolation = await verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
|
|
27536
|
+
isolation = await _sessionRunnerDeps.verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
|
|
27453
27537
|
} else if (role === "implementer" || role === "verifier") {
|
|
27454
|
-
isolation = await verifyImplementerIsolation(workdir, beforeRef);
|
|
27538
|
+
isolation = await _sessionRunnerDeps.verifyImplementerIsolation(workdir, beforeRef);
|
|
27455
27539
|
}
|
|
27456
27540
|
}
|
|
27457
|
-
const filesChanged = await
|
|
27541
|
+
const filesChanged = await _sessionRunnerDeps.getChangedFiles(workdir, beforeRef);
|
|
27458
27542
|
const durationMs = Date.now() - startTime;
|
|
27459
27543
|
if (isolation && !isolation.passed) {
|
|
27460
27544
|
logger.error("tdd", "Isolation violated", {
|
|
@@ -27497,10 +27581,18 @@ var init_session_runner = __esm(() => {
|
|
|
27497
27581
|
init_logger2();
|
|
27498
27582
|
init_prompts2();
|
|
27499
27583
|
init_git();
|
|
27584
|
+
init_git();
|
|
27500
27585
|
init_cleanup();
|
|
27501
27586
|
init_isolation();
|
|
27502
27587
|
_sessionRunnerDeps = {
|
|
27503
|
-
autoCommitIfDirty
|
|
27588
|
+
autoCommitIfDirty,
|
|
27589
|
+
spawn: Bun.spawn,
|
|
27590
|
+
getChangedFiles: getChangedFiles2,
|
|
27591
|
+
verifyTestWriterIsolation,
|
|
27592
|
+
verifyImplementerIsolation,
|
|
27593
|
+
captureGitRef,
|
|
27594
|
+
cleanupProcessTree,
|
|
27595
|
+
buildPrompt: null
|
|
27504
27596
|
};
|
|
27505
27597
|
});
|
|
27506
27598
|
|
|
@@ -28981,7 +29073,7 @@ class AcceptanceStrategy {
|
|
|
28981
29073
|
}
|
|
28982
29074
|
const start = Date.now();
|
|
28983
29075
|
const timeoutMs = ctx.timeoutSeconds * 1000;
|
|
28984
|
-
const proc =
|
|
29076
|
+
const proc = _acceptanceDeps.spawn(["bun", "test", testPath], {
|
|
28985
29077
|
cwd: ctx.workdir,
|
|
28986
29078
|
stdout: "pipe",
|
|
28987
29079
|
stderr: "pipe"
|
|
@@ -29049,8 +29141,10 @@ ${stderr}`;
|
|
|
29049
29141
|
});
|
|
29050
29142
|
}
|
|
29051
29143
|
}
|
|
29144
|
+
var _acceptanceDeps;
|
|
29052
29145
|
var init_acceptance3 = __esm(() => {
|
|
29053
29146
|
init_logger2();
|
|
29147
|
+
_acceptanceDeps = { spawn: Bun.spawn };
|
|
29054
29148
|
});
|
|
29055
29149
|
|
|
29056
29150
|
// src/verification/strategies/regression.ts
|
|
@@ -68243,6 +68337,45 @@ class PipelineEventEmitter {
|
|
|
68243
68337
|
init_stages();
|
|
68244
68338
|
init_prd();
|
|
68245
68339
|
|
|
68340
|
+
// src/cli/prompts-shared.ts
|
|
68341
|
+
function buildFrontmatter(story, ctx, role) {
|
|
68342
|
+
const lines = [];
|
|
68343
|
+
lines.push(`storyId: ${story.id}`);
|
|
68344
|
+
lines.push(`title: "${story.title}"`);
|
|
68345
|
+
lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
|
|
68346
|
+
lines.push(`modelTier: ${ctx.routing.modelTier}`);
|
|
68347
|
+
if (role) {
|
|
68348
|
+
lines.push(`role: ${role}`);
|
|
68349
|
+
}
|
|
68350
|
+
const builtContext = ctx.builtContext;
|
|
68351
|
+
const contextTokens = builtContext?.totalTokens ?? 0;
|
|
68352
|
+
const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
|
|
68353
|
+
lines.push(`contextTokens: ${contextTokens}`);
|
|
68354
|
+
lines.push(`promptTokens: ${promptTokens}`);
|
|
68355
|
+
if (story.dependencies && story.dependencies.length > 0) {
|
|
68356
|
+
lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
|
|
68357
|
+
}
|
|
68358
|
+
lines.push("contextElements:");
|
|
68359
|
+
if (builtContext) {
|
|
68360
|
+
for (const element of builtContext.elements) {
|
|
68361
|
+
lines.push(` - type: ${element.type}`);
|
|
68362
|
+
if (element.storyId) {
|
|
68363
|
+
lines.push(` storyId: ${element.storyId}`);
|
|
68364
|
+
}
|
|
68365
|
+
if (element.filePath) {
|
|
68366
|
+
lines.push(` filePath: ${element.filePath}`);
|
|
68367
|
+
}
|
|
68368
|
+
lines.push(` tokens: ${element.tokens}`);
|
|
68369
|
+
}
|
|
68370
|
+
}
|
|
68371
|
+
if (builtContext?.truncated) {
|
|
68372
|
+
lines.push("truncated: true");
|
|
68373
|
+
}
|
|
68374
|
+
return `${lines.join(`
|
|
68375
|
+
`)}
|
|
68376
|
+
`;
|
|
68377
|
+
}
|
|
68378
|
+
|
|
68246
68379
|
// src/cli/prompts-tdd.ts
|
|
68247
68380
|
init_prompts2();
|
|
68248
68381
|
import { join as join28 } from "path";
|
|
@@ -68388,43 +68521,6 @@ ${"=".repeat(80)}`);
|
|
|
68388
68521
|
});
|
|
68389
68522
|
return processedStories;
|
|
68390
68523
|
}
|
|
68391
|
-
function buildFrontmatter(story, ctx, role) {
|
|
68392
|
-
const lines = [];
|
|
68393
|
-
lines.push(`storyId: ${story.id}`);
|
|
68394
|
-
lines.push(`title: "${story.title}"`);
|
|
68395
|
-
lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
|
|
68396
|
-
lines.push(`modelTier: ${ctx.routing.modelTier}`);
|
|
68397
|
-
if (role) {
|
|
68398
|
-
lines.push(`role: ${role}`);
|
|
68399
|
-
}
|
|
68400
|
-
const builtContext = ctx.builtContext;
|
|
68401
|
-
const contextTokens = builtContext?.totalTokens ?? 0;
|
|
68402
|
-
const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
|
|
68403
|
-
lines.push(`contextTokens: ${contextTokens}`);
|
|
68404
|
-
lines.push(`promptTokens: ${promptTokens}`);
|
|
68405
|
-
if (story.dependencies && story.dependencies.length > 0) {
|
|
68406
|
-
lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
|
|
68407
|
-
}
|
|
68408
|
-
lines.push("contextElements:");
|
|
68409
|
-
if (builtContext) {
|
|
68410
|
-
for (const element of builtContext.elements) {
|
|
68411
|
-
lines.push(` - type: ${element.type}`);
|
|
68412
|
-
if (element.storyId) {
|
|
68413
|
-
lines.push(` storyId: ${element.storyId}`);
|
|
68414
|
-
}
|
|
68415
|
-
if (element.filePath) {
|
|
68416
|
-
lines.push(` filePath: ${element.filePath}`);
|
|
68417
|
-
}
|
|
68418
|
-
lines.push(` tokens: ${element.tokens}`);
|
|
68419
|
-
}
|
|
68420
|
-
}
|
|
68421
|
-
if (builtContext?.truncated) {
|
|
68422
|
-
lines.push("truncated: true");
|
|
68423
|
-
}
|
|
68424
|
-
return `${lines.join(`
|
|
68425
|
-
`)}
|
|
68426
|
-
`;
|
|
68427
|
-
}
|
|
68428
68524
|
// src/cli/prompts-init.ts
|
|
68429
68525
|
import { existsSync as existsSync19, mkdirSync as mkdirSync4 } from "fs";
|
|
68430
68526
|
import { join as join30 } from "path";
|