@nathapp/nax 0.33.0 → 0.34.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/README.md +191 -6
- package/dist/nax.js +276 -212
- package/package.json +1 -1
- package/src/execution/crash-recovery.ts +8 -0
- package/src/execution/iteration-runner.ts +2 -0
- package/src/execution/lifecycle/run-completion.ts +100 -15
- package/src/execution/parallel-executor.ts +20 -1
- package/src/execution/pipeline-result-handler.ts +5 -1
- package/src/execution/runner.ts +20 -0
- package/src/execution/sequential-executor.ts +1 -11
- package/src/hooks/types.ts +20 -10
- package/src/metrics/tracker.ts +7 -0
- package/src/metrics/types.ts +2 -0
- package/src/prd/index.ts +2 -1
- package/src/prd/types.ts +6 -1
package/dist/nax.js
CHANGED
|
@@ -20135,8 +20135,8 @@ function isStalled(prd) {
|
|
|
20135
20135
|
const remaining = prd.userStories.filter((s) => s.status !== "passed" && s.status !== "skipped");
|
|
20136
20136
|
if (remaining.length === 0)
|
|
20137
20137
|
return false;
|
|
20138
|
-
const blockedIds = new Set(prd.userStories.filter((s) => s.status === "blocked" || s.status === "failed" || s.status === "paused").map((s) => s.id));
|
|
20139
|
-
return remaining.every((s) => s.status === "blocked" || s.status === "failed" || s.status === "paused" || s.dependencies.some((dep) => blockedIds.has(dep)));
|
|
20138
|
+
const blockedIds = new Set(prd.userStories.filter((s) => s.status === "blocked" || s.status === "failed" || s.status === "paused" || s.status === "regression-failed").map((s) => s.id));
|
|
20139
|
+
return remaining.every((s) => s.status === "blocked" || s.status === "failed" || s.status === "paused" || s.status === "regression-failed" || s.dependencies.some((dep) => blockedIds.has(dep)));
|
|
20140
20140
|
}
|
|
20141
20141
|
|
|
20142
20142
|
// src/prd/index.ts
|
|
@@ -20194,8 +20194,8 @@ function countStories(prd) {
|
|
|
20194
20194
|
return {
|
|
20195
20195
|
total: prd.userStories.length,
|
|
20196
20196
|
passed: prd.userStories.filter((s) => s.passes || s.status === "passed").length,
|
|
20197
|
-
failed: prd.userStories.filter((s) => s.status === "failed").length,
|
|
20198
|
-
pending: prd.userStories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "failed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "paused" && s.status !== "decomposed").length,
|
|
20197
|
+
failed: prd.userStories.filter((s) => s.status === "failed" || s.status === "regression-failed").length,
|
|
20198
|
+
pending: prd.userStories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "failed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "paused" && s.status !== "regression-failed" && s.status !== "decomposed").length,
|
|
20199
20199
|
skipped: prd.userStories.filter((s) => s.status === "skipped").length,
|
|
20200
20200
|
blocked: prd.userStories.filter((s) => s.status === "blocked").length,
|
|
20201
20201
|
paused: prd.userStories.filter((s) => s.status === "paused").length,
|
|
@@ -20242,7 +20242,7 @@ var package_default;
|
|
|
20242
20242
|
var init_package = __esm(() => {
|
|
20243
20243
|
package_default = {
|
|
20244
20244
|
name: "@nathapp/nax",
|
|
20245
|
-
version: "0.
|
|
20245
|
+
version: "0.34.0",
|
|
20246
20246
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
20247
20247
|
type: "module",
|
|
20248
20248
|
bin: {
|
|
@@ -20304,8 +20304,8 @@ var init_version = __esm(() => {
|
|
|
20304
20304
|
NAX_VERSION = package_default.version;
|
|
20305
20305
|
NAX_COMMIT = (() => {
|
|
20306
20306
|
try {
|
|
20307
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
20308
|
-
return "
|
|
20307
|
+
if (/^[0-9a-f]{6,10}$/.test("a679961"))
|
|
20308
|
+
return "a679961";
|
|
20309
20309
|
} catch {}
|
|
20310
20310
|
try {
|
|
20311
20311
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -20384,6 +20384,8 @@ function collectStoryMetrics(ctx, storyStartTime) {
|
|
|
20384
20384
|
const modelDef = modelEntry ? resolveModel(modelEntry) : null;
|
|
20385
20385
|
const modelUsed = modelDef?.model || routing.modelTier;
|
|
20386
20386
|
const initialComplexity = story.routing?.initialComplexity ?? routing.complexity;
|
|
20387
|
+
const isTddStrategy = routing.testStrategy === "three-session-tdd" || routing.testStrategy === "three-session-tdd-lite";
|
|
20388
|
+
const fullSuiteGatePassed = isTddStrategy ? ctx.fullSuiteGatePassed ?? false : false;
|
|
20387
20389
|
return {
|
|
20388
20390
|
storyId: story.id,
|
|
20389
20391
|
complexity: routing.complexity,
|
|
@@ -20397,7 +20399,8 @@ function collectStoryMetrics(ctx, storyStartTime) {
|
|
|
20397
20399
|
durationMs: agentResult?.durationMs || 0,
|
|
20398
20400
|
firstPassSuccess,
|
|
20399
20401
|
startedAt: storyStartTime,
|
|
20400
|
-
completedAt: new Date().toISOString()
|
|
20402
|
+
completedAt: new Date().toISOString(),
|
|
20403
|
+
fullSuiteGatePassed
|
|
20401
20404
|
};
|
|
20402
20405
|
}
|
|
20403
20406
|
function collectBatchMetrics(ctx, storyStartTime) {
|
|
@@ -20426,7 +20429,8 @@ function collectBatchMetrics(ctx, storyStartTime) {
|
|
|
20426
20429
|
durationMs: durationPerStory,
|
|
20427
20430
|
firstPassSuccess: true,
|
|
20428
20431
|
startedAt: storyStartTime,
|
|
20429
|
-
completedAt: new Date().toISOString()
|
|
20432
|
+
completedAt: new Date().toISOString(),
|
|
20433
|
+
fullSuiteGatePassed: false
|
|
20430
20434
|
};
|
|
20431
20435
|
});
|
|
20432
20436
|
}
|
|
@@ -28022,6 +28026,181 @@ var init_precheck = __esm(() => {
|
|
|
28022
28026
|
};
|
|
28023
28027
|
});
|
|
28024
28028
|
|
|
28029
|
+
// src/hooks/runner.ts
|
|
28030
|
+
import { existsSync as existsSync25 } from "fs";
|
|
28031
|
+
import { join as join30 } from "path";
|
|
28032
|
+
async function loadHooksConfig(projectDir, globalDir) {
|
|
28033
|
+
let globalHooks = { hooks: {} };
|
|
28034
|
+
let projectHooks = { hooks: {} };
|
|
28035
|
+
let skipGlobal = false;
|
|
28036
|
+
const projectPath = join30(projectDir, "hooks.json");
|
|
28037
|
+
if (existsSync25(projectPath)) {
|
|
28038
|
+
try {
|
|
28039
|
+
const projectData = await Bun.file(projectPath).json();
|
|
28040
|
+
projectHooks = projectData;
|
|
28041
|
+
skipGlobal = projectData.skipGlobal ?? false;
|
|
28042
|
+
} catch (err) {
|
|
28043
|
+
const logger = getLogger();
|
|
28044
|
+
logger.warn("hooks", "Failed to parse project hooks.json", { path: projectPath, error: String(err) });
|
|
28045
|
+
}
|
|
28046
|
+
}
|
|
28047
|
+
if (!skipGlobal && globalDir) {
|
|
28048
|
+
const globalPath = join30(globalDir, "hooks.json");
|
|
28049
|
+
if (existsSync25(globalPath)) {
|
|
28050
|
+
try {
|
|
28051
|
+
const globalData = await Bun.file(globalPath).json();
|
|
28052
|
+
globalHooks = globalData;
|
|
28053
|
+
} catch (err) {
|
|
28054
|
+
const logger = getLogger();
|
|
28055
|
+
logger.warn("hooks", "Failed to parse global hooks.json", { path: globalPath, error: String(err) });
|
|
28056
|
+
}
|
|
28057
|
+
}
|
|
28058
|
+
}
|
|
28059
|
+
return {
|
|
28060
|
+
...projectHooks,
|
|
28061
|
+
_global: skipGlobal ? undefined : globalHooks,
|
|
28062
|
+
_skipGlobal: skipGlobal
|
|
28063
|
+
};
|
|
28064
|
+
}
|
|
28065
|
+
function escapeEnvValue(value) {
|
|
28066
|
+
return value.replace(/\0/g, "").replace(/\n/g, " ").replace(/\r/g, "");
|
|
28067
|
+
}
|
|
28068
|
+
function buildEnv(ctx) {
|
|
28069
|
+
const env2 = {
|
|
28070
|
+
NAX_EVENT: escapeEnvValue(ctx.event),
|
|
28071
|
+
NAX_FEATURE: escapeEnvValue(ctx.feature)
|
|
28072
|
+
};
|
|
28073
|
+
if (ctx.storyId)
|
|
28074
|
+
env2.NAX_STORY_ID = escapeEnvValue(ctx.storyId);
|
|
28075
|
+
if (ctx.status)
|
|
28076
|
+
env2.NAX_STATUS = escapeEnvValue(ctx.status);
|
|
28077
|
+
if (ctx.reason)
|
|
28078
|
+
env2.NAX_REASON = escapeEnvValue(ctx.reason);
|
|
28079
|
+
if (ctx.cost !== undefined)
|
|
28080
|
+
env2.NAX_COST = ctx.cost.toFixed(4);
|
|
28081
|
+
if (ctx.model)
|
|
28082
|
+
env2.NAX_MODEL = escapeEnvValue(ctx.model);
|
|
28083
|
+
if (ctx.agent)
|
|
28084
|
+
env2.NAX_AGENT = escapeEnvValue(ctx.agent);
|
|
28085
|
+
if (ctx.iteration !== undefined)
|
|
28086
|
+
env2.NAX_ITERATION = String(ctx.iteration);
|
|
28087
|
+
return env2;
|
|
28088
|
+
}
|
|
28089
|
+
function hasShellOperators(command) {
|
|
28090
|
+
const shellOperators = /[|&;$`<>(){}]/;
|
|
28091
|
+
return shellOperators.test(command);
|
|
28092
|
+
}
|
|
28093
|
+
function validateHookCommand(command) {
|
|
28094
|
+
const dangerousPatterns = [
|
|
28095
|
+
/\$\(.*\)/,
|
|
28096
|
+
/`.*`/,
|
|
28097
|
+
/\|\s*bash/,
|
|
28098
|
+
/\|\s*sh/,
|
|
28099
|
+
/;\s*rm\s+-rf/,
|
|
28100
|
+
/&&\s*rm\s+-rf/,
|
|
28101
|
+
/\beval\s+/,
|
|
28102
|
+
/curl\s+[^|]*\|\s*/,
|
|
28103
|
+
/python\s+-c/
|
|
28104
|
+
];
|
|
28105
|
+
for (const pattern of dangerousPatterns) {
|
|
28106
|
+
if (pattern.test(command)) {
|
|
28107
|
+
throw new Error(`Hook command contains dangerous pattern: ${pattern.source}`);
|
|
28108
|
+
}
|
|
28109
|
+
}
|
|
28110
|
+
}
|
|
28111
|
+
function parseCommandToArgv(command) {
|
|
28112
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
28113
|
+
return command.trim().split(/\s+/).map((token) => token.startsWith("~/") ? home + token.slice(1) : token);
|
|
28114
|
+
}
|
|
28115
|
+
async function executeHook(hookDef, ctx, workdir) {
|
|
28116
|
+
if (hookDef.enabled === false) {
|
|
28117
|
+
return { success: true, output: "(disabled)" };
|
|
28118
|
+
}
|
|
28119
|
+
try {
|
|
28120
|
+
validateHookCommand(hookDef.command);
|
|
28121
|
+
} catch (err) {
|
|
28122
|
+
return {
|
|
28123
|
+
success: false,
|
|
28124
|
+
output: `Security validation failed: ${err}`
|
|
28125
|
+
};
|
|
28126
|
+
}
|
|
28127
|
+
const logger = getLogger();
|
|
28128
|
+
if (hasShellOperators(hookDef.command)) {
|
|
28129
|
+
logger.warn("hooks", "[SECURITY] Hook command contains shell operators", {
|
|
28130
|
+
command: hookDef.command,
|
|
28131
|
+
warning: "Shell operators may enable injection attacks. Consider using simple commands only."
|
|
28132
|
+
});
|
|
28133
|
+
}
|
|
28134
|
+
const timeout = hookDef.timeout ?? DEFAULT_TIMEOUT;
|
|
28135
|
+
const env2 = buildEnv(ctx);
|
|
28136
|
+
const contextJson = JSON.stringify(ctx);
|
|
28137
|
+
const argv = parseCommandToArgv(hookDef.command);
|
|
28138
|
+
if (argv.length === 0) {
|
|
28139
|
+
return { success: false, output: "Empty command" };
|
|
28140
|
+
}
|
|
28141
|
+
const proc = Bun.spawn(argv, {
|
|
28142
|
+
cwd: workdir,
|
|
28143
|
+
stdin: new Response(contextJson),
|
|
28144
|
+
stdout: "pipe",
|
|
28145
|
+
stderr: "pipe",
|
|
28146
|
+
env: { ...process.env, ...env2 }
|
|
28147
|
+
});
|
|
28148
|
+
const timeoutId = setTimeout(() => {
|
|
28149
|
+
proc.kill("SIGTERM");
|
|
28150
|
+
}, timeout);
|
|
28151
|
+
const exitCode = await proc.exited;
|
|
28152
|
+
clearTimeout(timeoutId);
|
|
28153
|
+
const stdout = await new Response(proc.stdout).text();
|
|
28154
|
+
const stderr = await new Response(proc.stderr).text();
|
|
28155
|
+
const output = (stdout + stderr).trim();
|
|
28156
|
+
if (exitCode !== 0 && output === "") {
|
|
28157
|
+
return {
|
|
28158
|
+
success: false,
|
|
28159
|
+
output: `Hook timed out after ${timeout}ms`
|
|
28160
|
+
};
|
|
28161
|
+
}
|
|
28162
|
+
return {
|
|
28163
|
+
success: exitCode === 0,
|
|
28164
|
+
output
|
|
28165
|
+
};
|
|
28166
|
+
}
|
|
28167
|
+
async function fireHook(config2, event, ctx, workdir) {
|
|
28168
|
+
const logger = getLogger();
|
|
28169
|
+
if (config2._global && !config2._skipGlobal) {
|
|
28170
|
+
const globalHookDef = config2._global.hooks[event];
|
|
28171
|
+
if (globalHookDef && globalHookDef.enabled !== false) {
|
|
28172
|
+
try {
|
|
28173
|
+
const result = await executeHook(globalHookDef, { ...ctx, event }, workdir);
|
|
28174
|
+
if (!result.success) {
|
|
28175
|
+
logger.warn("hooks", `Global hook ${event} failed`, { event, output: result.output });
|
|
28176
|
+
}
|
|
28177
|
+
} catch (err) {
|
|
28178
|
+
logger.warn("hooks", `Global hook ${event} error`, { event, error: String(err) });
|
|
28179
|
+
}
|
|
28180
|
+
}
|
|
28181
|
+
}
|
|
28182
|
+
const projectHookDef = config2.hooks[event];
|
|
28183
|
+
if (projectHookDef && projectHookDef.enabled !== false) {
|
|
28184
|
+
try {
|
|
28185
|
+
const result = await executeHook(projectHookDef, { ...ctx, event }, workdir);
|
|
28186
|
+
if (!result.success) {
|
|
28187
|
+
logger.warn("hooks", `Project hook ${event} failed`, { event, output: result.output });
|
|
28188
|
+
}
|
|
28189
|
+
} catch (err) {
|
|
28190
|
+
logger.warn("hooks", `Project hook ${event} error`, { event, error: String(err) });
|
|
28191
|
+
}
|
|
28192
|
+
}
|
|
28193
|
+
}
|
|
28194
|
+
var DEFAULT_TIMEOUT = 5000;
|
|
28195
|
+
var init_runner3 = __esm(() => {
|
|
28196
|
+
init_logger2();
|
|
28197
|
+
});
|
|
28198
|
+
|
|
28199
|
+
// src/hooks/index.ts
|
|
28200
|
+
var init_hooks = __esm(() => {
|
|
28201
|
+
init_runner3();
|
|
28202
|
+
});
|
|
28203
|
+
|
|
28025
28204
|
// src/execution/crash-recovery.ts
|
|
28026
28205
|
import { appendFileSync as appendFileSync2 } from "fs";
|
|
28027
28206
|
async function writeFatalLog(jsonlFilePath, signal, error48) {
|
|
@@ -28267,181 +28446,6 @@ function calculateMaxIterations(tierOrder) {
|
|
|
28267
28446
|
return tierOrder.reduce((sum, t) => sum + t.attempts, 0);
|
|
28268
28447
|
}
|
|
28269
28448
|
|
|
28270
|
-
// src/hooks/runner.ts
|
|
28271
|
-
import { existsSync as existsSync25 } from "fs";
|
|
28272
|
-
import { join as join30 } from "path";
|
|
28273
|
-
async function loadHooksConfig(projectDir, globalDir) {
|
|
28274
|
-
let globalHooks = { hooks: {} };
|
|
28275
|
-
let projectHooks = { hooks: {} };
|
|
28276
|
-
let skipGlobal = false;
|
|
28277
|
-
const projectPath = join30(projectDir, "hooks.json");
|
|
28278
|
-
if (existsSync25(projectPath)) {
|
|
28279
|
-
try {
|
|
28280
|
-
const projectData = await Bun.file(projectPath).json();
|
|
28281
|
-
projectHooks = projectData;
|
|
28282
|
-
skipGlobal = projectData.skipGlobal ?? false;
|
|
28283
|
-
} catch (err) {
|
|
28284
|
-
const logger = getLogger();
|
|
28285
|
-
logger.warn("hooks", "Failed to parse project hooks.json", { path: projectPath, error: String(err) });
|
|
28286
|
-
}
|
|
28287
|
-
}
|
|
28288
|
-
if (!skipGlobal && globalDir) {
|
|
28289
|
-
const globalPath = join30(globalDir, "hooks.json");
|
|
28290
|
-
if (existsSync25(globalPath)) {
|
|
28291
|
-
try {
|
|
28292
|
-
const globalData = await Bun.file(globalPath).json();
|
|
28293
|
-
globalHooks = globalData;
|
|
28294
|
-
} catch (err) {
|
|
28295
|
-
const logger = getLogger();
|
|
28296
|
-
logger.warn("hooks", "Failed to parse global hooks.json", { path: globalPath, error: String(err) });
|
|
28297
|
-
}
|
|
28298
|
-
}
|
|
28299
|
-
}
|
|
28300
|
-
return {
|
|
28301
|
-
...projectHooks,
|
|
28302
|
-
_global: skipGlobal ? undefined : globalHooks,
|
|
28303
|
-
_skipGlobal: skipGlobal
|
|
28304
|
-
};
|
|
28305
|
-
}
|
|
28306
|
-
function escapeEnvValue(value) {
|
|
28307
|
-
return value.replace(/\0/g, "").replace(/\n/g, " ").replace(/\r/g, "");
|
|
28308
|
-
}
|
|
28309
|
-
function buildEnv(ctx) {
|
|
28310
|
-
const env2 = {
|
|
28311
|
-
NAX_EVENT: escapeEnvValue(ctx.event),
|
|
28312
|
-
NAX_FEATURE: escapeEnvValue(ctx.feature)
|
|
28313
|
-
};
|
|
28314
|
-
if (ctx.storyId)
|
|
28315
|
-
env2.NAX_STORY_ID = escapeEnvValue(ctx.storyId);
|
|
28316
|
-
if (ctx.status)
|
|
28317
|
-
env2.NAX_STATUS = escapeEnvValue(ctx.status);
|
|
28318
|
-
if (ctx.reason)
|
|
28319
|
-
env2.NAX_REASON = escapeEnvValue(ctx.reason);
|
|
28320
|
-
if (ctx.cost !== undefined)
|
|
28321
|
-
env2.NAX_COST = ctx.cost.toFixed(4);
|
|
28322
|
-
if (ctx.model)
|
|
28323
|
-
env2.NAX_MODEL = escapeEnvValue(ctx.model);
|
|
28324
|
-
if (ctx.agent)
|
|
28325
|
-
env2.NAX_AGENT = escapeEnvValue(ctx.agent);
|
|
28326
|
-
if (ctx.iteration !== undefined)
|
|
28327
|
-
env2.NAX_ITERATION = String(ctx.iteration);
|
|
28328
|
-
return env2;
|
|
28329
|
-
}
|
|
28330
|
-
function hasShellOperators(command) {
|
|
28331
|
-
const shellOperators = /[|&;$`<>(){}]/;
|
|
28332
|
-
return shellOperators.test(command);
|
|
28333
|
-
}
|
|
28334
|
-
function validateHookCommand(command) {
|
|
28335
|
-
const dangerousPatterns = [
|
|
28336
|
-
/\$\(.*\)/,
|
|
28337
|
-
/`.*`/,
|
|
28338
|
-
/\|\s*bash/,
|
|
28339
|
-
/\|\s*sh/,
|
|
28340
|
-
/;\s*rm\s+-rf/,
|
|
28341
|
-
/&&\s*rm\s+-rf/,
|
|
28342
|
-
/\beval\s+/,
|
|
28343
|
-
/curl\s+[^|]*\|\s*/,
|
|
28344
|
-
/python\s+-c/
|
|
28345
|
-
];
|
|
28346
|
-
for (const pattern of dangerousPatterns) {
|
|
28347
|
-
if (pattern.test(command)) {
|
|
28348
|
-
throw new Error(`Hook command contains dangerous pattern: ${pattern.source}`);
|
|
28349
|
-
}
|
|
28350
|
-
}
|
|
28351
|
-
}
|
|
28352
|
-
function parseCommandToArgv(command) {
|
|
28353
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
28354
|
-
return command.trim().split(/\s+/).map((token) => token.startsWith("~/") ? home + token.slice(1) : token);
|
|
28355
|
-
}
|
|
28356
|
-
async function executeHook(hookDef, ctx, workdir) {
|
|
28357
|
-
if (hookDef.enabled === false) {
|
|
28358
|
-
return { success: true, output: "(disabled)" };
|
|
28359
|
-
}
|
|
28360
|
-
try {
|
|
28361
|
-
validateHookCommand(hookDef.command);
|
|
28362
|
-
} catch (err) {
|
|
28363
|
-
return {
|
|
28364
|
-
success: false,
|
|
28365
|
-
output: `Security validation failed: ${err}`
|
|
28366
|
-
};
|
|
28367
|
-
}
|
|
28368
|
-
const logger = getLogger();
|
|
28369
|
-
if (hasShellOperators(hookDef.command)) {
|
|
28370
|
-
logger.warn("hooks", "[SECURITY] Hook command contains shell operators", {
|
|
28371
|
-
command: hookDef.command,
|
|
28372
|
-
warning: "Shell operators may enable injection attacks. Consider using simple commands only."
|
|
28373
|
-
});
|
|
28374
|
-
}
|
|
28375
|
-
const timeout = hookDef.timeout ?? DEFAULT_TIMEOUT;
|
|
28376
|
-
const env2 = buildEnv(ctx);
|
|
28377
|
-
const contextJson = JSON.stringify(ctx);
|
|
28378
|
-
const argv = parseCommandToArgv(hookDef.command);
|
|
28379
|
-
if (argv.length === 0) {
|
|
28380
|
-
return { success: false, output: "Empty command" };
|
|
28381
|
-
}
|
|
28382
|
-
const proc = Bun.spawn(argv, {
|
|
28383
|
-
cwd: workdir,
|
|
28384
|
-
stdin: new Response(contextJson),
|
|
28385
|
-
stdout: "pipe",
|
|
28386
|
-
stderr: "pipe",
|
|
28387
|
-
env: { ...process.env, ...env2 }
|
|
28388
|
-
});
|
|
28389
|
-
const timeoutId = setTimeout(() => {
|
|
28390
|
-
proc.kill("SIGTERM");
|
|
28391
|
-
}, timeout);
|
|
28392
|
-
const exitCode = await proc.exited;
|
|
28393
|
-
clearTimeout(timeoutId);
|
|
28394
|
-
const stdout = await new Response(proc.stdout).text();
|
|
28395
|
-
const stderr = await new Response(proc.stderr).text();
|
|
28396
|
-
const output = (stdout + stderr).trim();
|
|
28397
|
-
if (exitCode !== 0 && output === "") {
|
|
28398
|
-
return {
|
|
28399
|
-
success: false,
|
|
28400
|
-
output: `Hook timed out after ${timeout}ms`
|
|
28401
|
-
};
|
|
28402
|
-
}
|
|
28403
|
-
return {
|
|
28404
|
-
success: exitCode === 0,
|
|
28405
|
-
output
|
|
28406
|
-
};
|
|
28407
|
-
}
|
|
28408
|
-
async function fireHook(config2, event, ctx, workdir) {
|
|
28409
|
-
const logger = getLogger();
|
|
28410
|
-
if (config2._global && !config2._skipGlobal) {
|
|
28411
|
-
const globalHookDef = config2._global.hooks[event];
|
|
28412
|
-
if (globalHookDef && globalHookDef.enabled !== false) {
|
|
28413
|
-
try {
|
|
28414
|
-
const result = await executeHook(globalHookDef, { ...ctx, event }, workdir);
|
|
28415
|
-
if (!result.success) {
|
|
28416
|
-
logger.warn("hooks", `Global hook ${event} failed`, { event, output: result.output });
|
|
28417
|
-
}
|
|
28418
|
-
} catch (err) {
|
|
28419
|
-
logger.warn("hooks", `Global hook ${event} error`, { event, error: String(err) });
|
|
28420
|
-
}
|
|
28421
|
-
}
|
|
28422
|
-
}
|
|
28423
|
-
const projectHookDef = config2.hooks[event];
|
|
28424
|
-
if (projectHookDef && projectHookDef.enabled !== false) {
|
|
28425
|
-
try {
|
|
28426
|
-
const result = await executeHook(projectHookDef, { ...ctx, event }, workdir);
|
|
28427
|
-
if (!result.success) {
|
|
28428
|
-
logger.warn("hooks", `Project hook ${event} failed`, { event, output: result.output });
|
|
28429
|
-
}
|
|
28430
|
-
} catch (err) {
|
|
28431
|
-
logger.warn("hooks", `Project hook ${event} error`, { event, error: String(err) });
|
|
28432
|
-
}
|
|
28433
|
-
}
|
|
28434
|
-
}
|
|
28435
|
-
var DEFAULT_TIMEOUT = 5000;
|
|
28436
|
-
var init_runner3 = __esm(() => {
|
|
28437
|
-
init_logger2();
|
|
28438
|
-
});
|
|
28439
|
-
|
|
28440
|
-
// src/hooks/index.ts
|
|
28441
|
-
var init_hooks = __esm(() => {
|
|
28442
|
-
init_runner3();
|
|
28443
|
-
});
|
|
28444
|
-
|
|
28445
28449
|
// src/execution/escalation/tier-outcome.ts
|
|
28446
28450
|
async function handleNoTierAvailable(ctx, failureCategory) {
|
|
28447
28451
|
const logger = getSafeLogger();
|
|
@@ -29924,7 +29928,8 @@ var init_parallel_lifecycle = __esm(() => {
|
|
|
29924
29928
|
// src/execution/parallel-executor.ts
|
|
29925
29929
|
var exports_parallel_executor = {};
|
|
29926
29930
|
__export(exports_parallel_executor, {
|
|
29927
|
-
runParallelExecution: () => runParallelExecution
|
|
29931
|
+
runParallelExecution: () => runParallelExecution,
|
|
29932
|
+
_parallelExecutorDeps: () => _parallelExecutorDeps
|
|
29928
29933
|
});
|
|
29929
29934
|
import * as os5 from "os";
|
|
29930
29935
|
import path15 from "path";
|
|
@@ -30001,7 +30006,8 @@ async function runParallelExecution(options, initialPrd) {
|
|
|
30001
30006
|
feature,
|
|
30002
30007
|
totalCost
|
|
30003
30008
|
});
|
|
30004
|
-
await fireHook(hooks, "on-complete", hookCtx(feature, { status: "
|
|
30009
|
+
await _parallelExecutorDeps.fireHook(hooks, "on-all-stories-complete", hookCtx(feature, { status: "passed", cost: totalCost }), workdir);
|
|
30010
|
+
await _parallelExecutorDeps.fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: totalCost }), workdir);
|
|
30005
30011
|
const durationMs = Date.now() - startTime;
|
|
30006
30012
|
const runCompletedAt = new Date().toISOString();
|
|
30007
30013
|
const { handleParallelCompletion: handleParallelCompletion2 } = await Promise.resolve().then(() => (init_parallel_lifecycle(), exports_parallel_lifecycle));
|
|
@@ -30049,12 +30055,16 @@ async function runParallelExecution(options, initialPrd) {
|
|
|
30049
30055
|
}
|
|
30050
30056
|
return { prd, totalCost, storiesCompleted, completed: false };
|
|
30051
30057
|
}
|
|
30058
|
+
var _parallelExecutorDeps;
|
|
30052
30059
|
var init_parallel_executor = __esm(() => {
|
|
30053
30060
|
init_hooks();
|
|
30054
30061
|
init_logger2();
|
|
30055
30062
|
init_prd();
|
|
30056
30063
|
init_helpers();
|
|
30057
30064
|
init_parallel();
|
|
30065
|
+
_parallelExecutorDeps = {
|
|
30066
|
+
fireHook
|
|
30067
|
+
};
|
|
30058
30068
|
});
|
|
30059
30069
|
|
|
30060
30070
|
// src/pipeline/subscribers/events-writer.ts
|
|
@@ -30444,11 +30454,13 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
|
|
|
30444
30454
|
}
|
|
30445
30455
|
const storiesCompletedDelta = ctx.storiesToExecute.length;
|
|
30446
30456
|
for (const completedStory of ctx.storiesToExecute) {
|
|
30457
|
+
const now = Date.now();
|
|
30447
30458
|
logger?.info("story.complete", "Story completed successfully", {
|
|
30448
30459
|
storyId: completedStory.id,
|
|
30449
30460
|
storyTitle: completedStory.title,
|
|
30450
30461
|
totalCost: ctx.totalCost + costDelta,
|
|
30451
|
-
durationMs:
|
|
30462
|
+
durationMs: now - ctx.startTime,
|
|
30463
|
+
storyDurationMs: ctx.storyStartTime ? now - ctx.storyStartTime : undefined
|
|
30452
30464
|
});
|
|
30453
30465
|
pipelineEventBus.emit({
|
|
30454
30466
|
type: "story:completed",
|
|
@@ -30469,7 +30481,8 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
|
|
|
30469
30481
|
pendingStories: updatedCounts.pending,
|
|
30470
30482
|
totalCost: ctx.totalCost + costDelta,
|
|
30471
30483
|
costLimit: ctx.config.execution.costLimit,
|
|
30472
|
-
elapsedMs: Date.now() - ctx.startTime
|
|
30484
|
+
elapsedMs: Date.now() - ctx.startTime,
|
|
30485
|
+
storyDurationMs: ctx.storyStartTime ? Date.now() - ctx.storyStartTime : undefined
|
|
30473
30486
|
});
|
|
30474
30487
|
return { storiesCompletedDelta, costDelta, prd, prdDirty: true };
|
|
30475
30488
|
}
|
|
@@ -30575,6 +30588,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
|
|
|
30575
30588
|
prdDirty: dryRunResult.prdDirty
|
|
30576
30589
|
};
|
|
30577
30590
|
}
|
|
30591
|
+
const storyStartTime = Date.now();
|
|
30578
30592
|
const storyGitRef = await captureGitRef(ctx.workdir);
|
|
30579
30593
|
const pipelineContext = {
|
|
30580
30594
|
config: ctx.config,
|
|
@@ -30622,7 +30636,8 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
|
|
|
30622
30636
|
isBatchExecution,
|
|
30623
30637
|
allStoryMetrics,
|
|
30624
30638
|
storyGitRef,
|
|
30625
|
-
interactionChain: ctx.interactionChain
|
|
30639
|
+
interactionChain: ctx.interactionChain,
|
|
30640
|
+
storyStartTime
|
|
30626
30641
|
};
|
|
30627
30642
|
if (pipelineResult.success) {
|
|
30628
30643
|
const r2 = await handlePipelineSuccess(handlerCtx, pipelineResult);
|
|
@@ -30751,14 +30766,6 @@ async function executeSequential(ctx, initialPrd) {
|
|
|
30751
30766
|
return buildResult("pre-merge-aborted");
|
|
30752
30767
|
}
|
|
30753
30768
|
}
|
|
30754
|
-
pipelineEventBus.emit({
|
|
30755
|
-
type: "run:completed",
|
|
30756
|
-
totalStories: 0,
|
|
30757
|
-
passedStories: 0,
|
|
30758
|
-
failedStories: 0,
|
|
30759
|
-
durationMs: Date.now() - ctx.startTime,
|
|
30760
|
-
totalCost
|
|
30761
|
-
});
|
|
30762
30769
|
return buildResult("completed");
|
|
30763
30770
|
}
|
|
30764
30771
|
const selected = selectNextStories(prd, ctx.config, ctx.batchPlan, currentBatchIndex, lastStoryId, ctx.useBatch);
|
|
@@ -31242,6 +31249,16 @@ __export(exports_run_completion, {
|
|
|
31242
31249
|
handleRunCompletion: () => handleRunCompletion,
|
|
31243
31250
|
_runCompletionDeps: () => _runCompletionDeps
|
|
31244
31251
|
});
|
|
31252
|
+
function shouldSkipDeferredRegression(allStoryMetrics, isSequential) {
|
|
31253
|
+
const effectiveSequential = isSequential !== false;
|
|
31254
|
+
if (!effectiveSequential) {
|
|
31255
|
+
return false;
|
|
31256
|
+
}
|
|
31257
|
+
if (allStoryMetrics.length === 0) {
|
|
31258
|
+
return false;
|
|
31259
|
+
}
|
|
31260
|
+
return allStoryMetrics.every((m) => m.fullSuiteGatePassed === true);
|
|
31261
|
+
}
|
|
31245
31262
|
async function handleRunCompletion(options) {
|
|
31246
31263
|
const logger = getSafeLogger();
|
|
31247
31264
|
const {
|
|
@@ -31256,23 +31273,56 @@ async function handleRunCompletion(options) {
|
|
|
31256
31273
|
startTime,
|
|
31257
31274
|
workdir,
|
|
31258
31275
|
statusWriter,
|
|
31259
|
-
config: config2
|
|
31276
|
+
config: config2,
|
|
31277
|
+
hooksConfig,
|
|
31278
|
+
isSequential
|
|
31260
31279
|
} = options;
|
|
31261
31280
|
const regressionMode = config2.execution.regressionGate?.mode;
|
|
31262
31281
|
if (regressionMode === "deferred" && config2.quality.commands.test) {
|
|
31263
|
-
|
|
31264
|
-
|
|
31265
|
-
|
|
31266
|
-
|
|
31267
|
-
|
|
31268
|
-
|
|
31269
|
-
|
|
31270
|
-
|
|
31271
|
-
|
|
31272
|
-
|
|
31282
|
+
if (shouldSkipDeferredRegression(allStoryMetrics, isSequential)) {
|
|
31283
|
+
logger?.info("regression", "Smart-skip: skipping deferred regression (all stories passed full-suite gate in sequential mode)");
|
|
31284
|
+
} else {
|
|
31285
|
+
const regressionResult = await _runCompletionDeps.runDeferredRegression({
|
|
31286
|
+
config: config2,
|
|
31287
|
+
prd,
|
|
31288
|
+
workdir
|
|
31289
|
+
});
|
|
31290
|
+
logger?.info("regression", "Deferred regression gate completed", {
|
|
31291
|
+
success: regressionResult.success,
|
|
31292
|
+
failedTests: regressionResult.failedTests,
|
|
31293
|
+
affectedStories: regressionResult.affectedStories
|
|
31294
|
+
});
|
|
31295
|
+
if (!regressionResult.success) {
|
|
31296
|
+
for (const storyId of regressionResult.affectedStories) {
|
|
31297
|
+
const story = prd.userStories.find((s) => s.id === storyId);
|
|
31298
|
+
if (story) {
|
|
31299
|
+
story.status = "regression-failed";
|
|
31300
|
+
}
|
|
31301
|
+
}
|
|
31302
|
+
statusWriter.setRunStatus("failed");
|
|
31303
|
+
if (hooksConfig) {
|
|
31304
|
+
await _runCompletionDeps.fireHook(hooksConfig, "on-final-regression-fail", {
|
|
31305
|
+
event: "on-final-regression-fail",
|
|
31306
|
+
feature,
|
|
31307
|
+
status: "failed",
|
|
31308
|
+
failedTests: regressionResult.failedTests,
|
|
31309
|
+
affectedStories: regressionResult.affectedStories
|
|
31310
|
+
}, workdir);
|
|
31311
|
+
}
|
|
31312
|
+
}
|
|
31313
|
+
}
|
|
31273
31314
|
}
|
|
31274
31315
|
const durationMs = Date.now() - startTime;
|
|
31275
31316
|
const runCompletedAt = new Date().toISOString();
|
|
31317
|
+
const finalCounts = countStories(prd);
|
|
31318
|
+
pipelineEventBus.emit({
|
|
31319
|
+
type: "run:completed",
|
|
31320
|
+
totalStories: finalCounts.total,
|
|
31321
|
+
passedStories: finalCounts.passed,
|
|
31322
|
+
failedStories: finalCounts.failed,
|
|
31323
|
+
durationMs,
|
|
31324
|
+
totalCost
|
|
31325
|
+
});
|
|
31276
31326
|
const runMetrics = {
|
|
31277
31327
|
runId,
|
|
31278
31328
|
feature,
|
|
@@ -31281,12 +31331,15 @@ async function handleRunCompletion(options) {
|
|
|
31281
31331
|
totalCost,
|
|
31282
31332
|
totalStories: allStoryMetrics.length,
|
|
31283
31333
|
storiesCompleted,
|
|
31284
|
-
storiesFailed:
|
|
31334
|
+
storiesFailed: finalCounts.failed,
|
|
31285
31335
|
totalDurationMs: durationMs,
|
|
31286
31336
|
stories: allStoryMetrics
|
|
31287
31337
|
};
|
|
31288
|
-
|
|
31289
|
-
|
|
31338
|
+
try {
|
|
31339
|
+
await saveRunMetrics(workdir, runMetrics);
|
|
31340
|
+
} catch (err) {
|
|
31341
|
+
logger?.warn("run.complete", "Failed to save run metrics", { error: String(err) });
|
|
31342
|
+
}
|
|
31290
31343
|
const storyMetricsSummary = allStoryMetrics.map((sm) => ({
|
|
31291
31344
|
storyId: sm.storyId,
|
|
31292
31345
|
complexity: sm.complexity,
|
|
@@ -31330,12 +31383,15 @@ async function handleRunCompletion(options) {
|
|
|
31330
31383
|
}
|
|
31331
31384
|
var _runCompletionDeps;
|
|
31332
31385
|
var init_run_completion = __esm(() => {
|
|
31386
|
+
init_runner3();
|
|
31333
31387
|
init_logger2();
|
|
31334
31388
|
init_metrics();
|
|
31389
|
+
init_event_bus();
|
|
31335
31390
|
init_prd();
|
|
31336
31391
|
init_run_regression();
|
|
31337
31392
|
_runCompletionDeps = {
|
|
31338
|
-
runDeferredRegression
|
|
31393
|
+
runDeferredRegression,
|
|
31394
|
+
fireHook
|
|
31339
31395
|
};
|
|
31340
31396
|
});
|
|
31341
31397
|
|
|
@@ -65486,6 +65542,7 @@ async function unlockCommand(options) {
|
|
|
65486
65542
|
init_config();
|
|
65487
65543
|
|
|
65488
65544
|
// src/execution/runner.ts
|
|
65545
|
+
init_hooks();
|
|
65489
65546
|
init_logger2();
|
|
65490
65547
|
init_prd();
|
|
65491
65548
|
init_batch_route();
|
|
@@ -65530,8 +65587,12 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
|
|
|
65530
65587
|
// src/execution/runner.ts
|
|
65531
65588
|
init_crash_recovery();
|
|
65532
65589
|
init_helpers();
|
|
65590
|
+
init_story_context();
|
|
65533
65591
|
init_escalation();
|
|
65534
65592
|
init_escalation();
|
|
65593
|
+
var _runnerDeps = {
|
|
65594
|
+
fireHook
|
|
65595
|
+
};
|
|
65535
65596
|
async function run(options) {
|
|
65536
65597
|
const {
|
|
65537
65598
|
prdPath,
|
|
@@ -65713,6 +65774,9 @@ async function run(options) {
|
|
|
65713
65774
|
iterations = acceptanceResult.iterations;
|
|
65714
65775
|
storiesCompleted = acceptanceResult.storiesCompleted;
|
|
65715
65776
|
}
|
|
65777
|
+
if (isComplete(prd)) {
|
|
65778
|
+
await _runnerDeps.fireHook(hooks, "on-all-stories-complete", hookCtx(feature, { status: "passed", cost: totalCost }), workdir);
|
|
65779
|
+
}
|
|
65716
65780
|
const { handleRunCompletion: handleRunCompletion2 } = await Promise.resolve().then(() => (init_run_completion(), exports_run_completion));
|
|
65717
65781
|
const completionResult = await handleRunCompletion2({
|
|
65718
65782
|
runId,
|
package/package.json
CHANGED
|
@@ -409,3 +409,11 @@ export function resetCrashHandlers(): void {
|
|
|
409
409
|
handlersInstalled = false;
|
|
410
410
|
stopHeartbeat();
|
|
411
411
|
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Returns true if heartbeat timer is currently active.
|
|
415
|
+
* @internal - test use only.
|
|
416
|
+
*/
|
|
417
|
+
export function _isHeartbeatActive(): boolean {
|
|
418
|
+
return heartbeatTimer !== null;
|
|
419
|
+
}
|