substrate-ai 0.1.22 → 0.1.23
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/dist/{app-Bltq6BEm.js → app-CY3MaJtP.js} +55 -2
- package/dist/cli/index.js +2175 -671
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/packs/bmad/data/elicitation-methods.csv +51 -0
- package/packs/bmad/manifest.yaml +65 -0
- package/packs/bmad/prompts/architecture-step-2-decisions.md +7 -0
- package/packs/bmad/prompts/critique-analysis.md +88 -0
- package/packs/bmad/prompts/critique-architecture.md +96 -0
- package/packs/bmad/prompts/critique-planning.md +96 -0
- package/packs/bmad/prompts/critique-stories.md +93 -0
- package/packs/bmad/prompts/elicitation-apply.md +40 -0
- package/packs/bmad/prompts/readiness-check.md +139 -0
- package/packs/bmad/prompts/refine-artifact.md +52 -0
- package/packs/bmad/prompts/ux-step-1-discovery.md +69 -0
- package/packs/bmad/prompts/ux-step-2-design-system.md +64 -0
- package/packs/bmad/prompts/ux-step-3-journeys.md +80 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { AdapterRegistry, ConfigError, ConfigIncompatibleFormatError, DatabaseWrapper, MonitorDatabaseImpl, ParseError, RecommendationEngine, TaskGraphFileSchema, ValidationError, computeChangedKeys, createConfigWatcher, createDatabaseService, createEventBus, createGitWorktreeManager, createLogger, createMonitorAgent, createMonitorDatabase, createRoutingEngine, createTaskGraphEngine, createTuiApp, createWorkerPoolManager, deepMask, detectCycle, getAllTasks, getLatestSessionId, getLogByEvent, getSession, getSessionLog, getTaskLog, isTuiCapable, parseGraphFile, printNonTtyWarning, queryLogFiltered, runMigrations, validateDependencies, validateGraph } from "../app-
|
|
2
|
+
import { AdapterRegistry, ConfigError, ConfigIncompatibleFormatError, DatabaseWrapper, MonitorDatabaseImpl, ParseError, RecommendationEngine, TaskGraphFileSchema, ValidationError, computeChangedKeys, createConfigWatcher, createDatabaseService, createEventBus, createGitWorktreeManager, createLogger, createMonitorAgent, createMonitorDatabase, createRoutingEngine, createTaskGraphEngine, createTuiApp, createWorkerPoolManager, deepMask, detectCycle, getAllTasks, getLatestSessionId, getLogByEvent, getSession, getSessionLog, getTaskLog, isTuiCapable, parseGraphFile, printNonTtyWarning, queryLogFiltered, runMigrations, validateDependencies, validateGraph } from "../app-CY3MaJtP.js";
|
|
3
3
|
import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema, SUPPORTED_CONFIG_FORMAT_VERSIONS, SubstrateConfigSchema } from "../config-schema-C9tTMcm1.js";
|
|
4
4
|
import { defaultConfigMigrator } from "../version-manager-impl-O25ieEjS.js";
|
|
5
5
|
import { registerUpgradeCommand } from "../upgrade-CHhsJc_q.js";
|
|
@@ -389,7 +389,7 @@ function listTemplates() {
|
|
|
389
389
|
|
|
390
390
|
//#endregion
|
|
391
391
|
//#region src/cli/commands/init.ts
|
|
392
|
-
const logger$
|
|
392
|
+
const logger$34 = createLogger("init");
|
|
393
393
|
/**
|
|
394
394
|
* Detect whether the CLI was invoked via `npx substrate`.
|
|
395
395
|
* When true, prefix suggested commands with `npx `.
|
|
@@ -573,7 +573,7 @@ async function runInit(options = {}) {
|
|
|
573
573
|
discoveryReport = await registry.discoverAndRegister();
|
|
574
574
|
} catch (err) {
|
|
575
575
|
const message = err instanceof Error ? err.message : String(err);
|
|
576
|
-
logger$
|
|
576
|
+
logger$34.error({ err }, "Adapter discovery failed");
|
|
577
577
|
process.stderr.write(` Error: adapter discovery failed — ${message}\n`);
|
|
578
578
|
return INIT_EXIT_ERROR;
|
|
579
579
|
}
|
|
@@ -611,7 +611,7 @@ async function runInit(options = {}) {
|
|
|
611
611
|
await writeFile(routingPolicyPath, routingHeader + yaml.dump(routingPolicy), "utf-8");
|
|
612
612
|
} catch (err) {
|
|
613
613
|
const message = err instanceof Error ? err.message : String(err);
|
|
614
|
-
logger$
|
|
614
|
+
logger$34.error({ err }, "Failed to write config files");
|
|
615
615
|
process.stderr.write(` Error: failed to write configuration — ${message}\n`);
|
|
616
616
|
return INIT_EXIT_ERROR;
|
|
617
617
|
}
|
|
@@ -686,7 +686,7 @@ function formatUnsupportedVersionError(formatType, version, supported) {
|
|
|
686
686
|
|
|
687
687
|
//#endregion
|
|
688
688
|
//#region src/modules/config/config-system-impl.ts
|
|
689
|
-
const logger$
|
|
689
|
+
const logger$33 = createLogger("config");
|
|
690
690
|
function deepMerge(base, override) {
|
|
691
691
|
const result = { ...base };
|
|
692
692
|
for (const [key, val] of Object.entries(override)) if (val !== null && val !== void 0 && typeof val === "object" && !Array.isArray(val) && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key])) result[key] = deepMerge(result[key], val);
|
|
@@ -731,7 +731,7 @@ function readEnvOverrides() {
|
|
|
731
731
|
}
|
|
732
732
|
const parsed = PartialSubstrateConfigSchema.safeParse(overrides);
|
|
733
733
|
if (!parsed.success) {
|
|
734
|
-
logger$
|
|
734
|
+
logger$33.warn({ errors: parsed.error.issues }, "Invalid environment variable overrides ignored");
|
|
735
735
|
return {};
|
|
736
736
|
}
|
|
737
737
|
return parsed.data;
|
|
@@ -795,7 +795,7 @@ var ConfigSystemImpl = class {
|
|
|
795
795
|
throw new ConfigError(`Configuration validation failed:\n${issues}`, { issues: result.error.issues });
|
|
796
796
|
}
|
|
797
797
|
this._config = result.data;
|
|
798
|
-
logger$
|
|
798
|
+
logger$33.debug("Configuration loaded successfully");
|
|
799
799
|
}
|
|
800
800
|
getConfig() {
|
|
801
801
|
if (this._config === null) throw new ConfigError("Configuration has not been loaded. Call load() before getConfig().", {});
|
|
@@ -858,7 +858,7 @@ var ConfigSystemImpl = class {
|
|
|
858
858
|
if (version !== void 0 && typeof version === "string" && !isVersionSupported(version, SUPPORTED_CONFIG_FORMAT_VERSIONS)) if (defaultConfigMigrator.canMigrate(version, CURRENT_CONFIG_FORMAT_VERSION)) {
|
|
859
859
|
const migrationOutput = defaultConfigMigrator.migrate(rawObj, version, CURRENT_CONFIG_FORMAT_VERSION, filePath);
|
|
860
860
|
if (migrationOutput.result.success) {
|
|
861
|
-
logger$
|
|
861
|
+
logger$33.info({
|
|
862
862
|
from: version,
|
|
863
863
|
to: CURRENT_CONFIG_FORMAT_VERSION,
|
|
864
864
|
backup: migrationOutput.result.backupPath
|
|
@@ -901,7 +901,7 @@ function createConfigSystem(options = {}) {
|
|
|
901
901
|
|
|
902
902
|
//#endregion
|
|
903
903
|
//#region src/cli/commands/config.ts
|
|
904
|
-
const logger$
|
|
904
|
+
const logger$32 = createLogger("config-cmd");
|
|
905
905
|
const CONFIG_EXIT_SUCCESS = 0;
|
|
906
906
|
const CONFIG_EXIT_ERROR = 1;
|
|
907
907
|
const CONFIG_EXIT_INVALID = 2;
|
|
@@ -927,7 +927,7 @@ async function runConfigShow(opts = {}) {
|
|
|
927
927
|
return CONFIG_EXIT_INVALID;
|
|
928
928
|
}
|
|
929
929
|
const message = err instanceof Error ? err.message : String(err);
|
|
930
|
-
logger$
|
|
930
|
+
logger$32.error({ err }, "Failed to load configuration");
|
|
931
931
|
process.stderr.write(` Error loading configuration: ${message}\n`);
|
|
932
932
|
return CONFIG_EXIT_ERROR;
|
|
933
933
|
}
|
|
@@ -1001,7 +1001,7 @@ async function runConfigExport(opts = {}) {
|
|
|
1001
1001
|
return CONFIG_EXIT_INVALID;
|
|
1002
1002
|
}
|
|
1003
1003
|
const message = err instanceof Error ? err.message : String(err);
|
|
1004
|
-
logger$
|
|
1004
|
+
logger$32.error({ err }, "Failed to load configuration");
|
|
1005
1005
|
process.stderr.write(`Error loading configuration: ${message}\n`);
|
|
1006
1006
|
return CONFIG_EXIT_ERROR;
|
|
1007
1007
|
}
|
|
@@ -1155,7 +1155,7 @@ function registerConfigCommand(program, _version) {
|
|
|
1155
1155
|
|
|
1156
1156
|
//#endregion
|
|
1157
1157
|
//#region src/cli/commands/merge.ts
|
|
1158
|
-
const logger$
|
|
1158
|
+
const logger$31 = createLogger("merge-cmd");
|
|
1159
1159
|
const MERGE_EXIT_SUCCESS = 0;
|
|
1160
1160
|
const MERGE_EXIT_CONFLICT = 1;
|
|
1161
1161
|
const MERGE_EXIT_ERROR = 2;
|
|
@@ -1193,7 +1193,7 @@ async function mergeTask(taskId, targetBranch, projectRoot) {
|
|
|
1193
1193
|
projectRoot
|
|
1194
1194
|
});
|
|
1195
1195
|
try {
|
|
1196
|
-
logger$
|
|
1196
|
+
logger$31.info({
|
|
1197
1197
|
taskId,
|
|
1198
1198
|
targetBranch
|
|
1199
1199
|
}, "Running conflict detection...");
|
|
@@ -1215,7 +1215,7 @@ async function mergeTask(taskId, targetBranch, projectRoot) {
|
|
|
1215
1215
|
} catch (err) {
|
|
1216
1216
|
const message = err instanceof Error ? err.message : String(err);
|
|
1217
1217
|
console.error(`Error merging task "${taskId}": ${message}`);
|
|
1218
|
-
logger$
|
|
1218
|
+
logger$31.error({
|
|
1219
1219
|
taskId,
|
|
1220
1220
|
err
|
|
1221
1221
|
}, "merge --task failed");
|
|
@@ -1269,7 +1269,7 @@ async function mergeAll(targetBranch, projectRoot, taskIds) {
|
|
|
1269
1269
|
error: message
|
|
1270
1270
|
});
|
|
1271
1271
|
console.log(` Error for task "${taskId}": ${message}`);
|
|
1272
|
-
logger$
|
|
1272
|
+
logger$31.error({
|
|
1273
1273
|
taskId,
|
|
1274
1274
|
err
|
|
1275
1275
|
}, "merge --all: task failed");
|
|
@@ -1322,7 +1322,7 @@ function registerMergeCommand(program, projectRoot = process.cwd()) {
|
|
|
1322
1322
|
|
|
1323
1323
|
//#endregion
|
|
1324
1324
|
//#region src/cli/commands/worktrees.ts
|
|
1325
|
-
const logger$
|
|
1325
|
+
const logger$30 = createLogger("worktrees-cmd");
|
|
1326
1326
|
const WORKTREES_EXIT_SUCCESS = 0;
|
|
1327
1327
|
const WORKTREES_EXIT_ERROR = 1;
|
|
1328
1328
|
/** Valid task statuses for filtering */
|
|
@@ -1449,7 +1449,7 @@ async function listWorktreesAction(options) {
|
|
|
1449
1449
|
try {
|
|
1450
1450
|
worktreeInfos = await manager.listWorktrees();
|
|
1451
1451
|
} catch (err) {
|
|
1452
|
-
logger$
|
|
1452
|
+
logger$30.error({ err }, "Failed to list worktrees");
|
|
1453
1453
|
const message = err instanceof Error ? err.message : String(err);
|
|
1454
1454
|
process.stderr.write(`Error listing worktrees: ${message}\n`);
|
|
1455
1455
|
return WORKTREES_EXIT_ERROR;
|
|
@@ -1476,7 +1476,7 @@ async function listWorktreesAction(options) {
|
|
|
1476
1476
|
} catch (err) {
|
|
1477
1477
|
const message = err instanceof Error ? err.message : String(err);
|
|
1478
1478
|
process.stderr.write(`Error: ${message}\n`);
|
|
1479
|
-
logger$
|
|
1479
|
+
logger$30.error({ err }, "listWorktreesAction failed");
|
|
1480
1480
|
return WORKTREES_EXIT_ERROR;
|
|
1481
1481
|
}
|
|
1482
1482
|
}
|
|
@@ -1738,7 +1738,7 @@ function getPlanningCostTotal(db, sessionId) {
|
|
|
1738
1738
|
|
|
1739
1739
|
//#endregion
|
|
1740
1740
|
//#region src/cli/commands/cost.ts
|
|
1741
|
-
const logger$
|
|
1741
|
+
const logger$29 = createLogger("cost-cmd");
|
|
1742
1742
|
const COST_EXIT_SUCCESS = 0;
|
|
1743
1743
|
const COST_EXIT_ERROR = 1;
|
|
1744
1744
|
/**
|
|
@@ -1984,7 +1984,7 @@ async function runCostAction(options) {
|
|
|
1984
1984
|
} catch (err) {
|
|
1985
1985
|
const message = err instanceof Error ? err.message : String(err);
|
|
1986
1986
|
process.stderr.write(`Error: ${message}\n`);
|
|
1987
|
-
logger$
|
|
1987
|
+
logger$29.error({ err }, "runCostAction failed");
|
|
1988
1988
|
return COST_EXIT_ERROR;
|
|
1989
1989
|
} finally {
|
|
1990
1990
|
if (wrapper !== null) try {
|
|
@@ -2046,7 +2046,7 @@ function emitStatusSnapshot(snapshot) {
|
|
|
2046
2046
|
|
|
2047
2047
|
//#endregion
|
|
2048
2048
|
//#region src/recovery/crash-recovery.ts
|
|
2049
|
-
const logger$
|
|
2049
|
+
const logger$28 = createLogger("crash-recovery");
|
|
2050
2050
|
var CrashRecoveryManager = class {
|
|
2051
2051
|
db;
|
|
2052
2052
|
gitWorktreeManager;
|
|
@@ -2099,7 +2099,7 @@ var CrashRecoveryManager = class {
|
|
|
2099
2099
|
});
|
|
2100
2100
|
}
|
|
2101
2101
|
if (this.gitWorktreeManager !== void 0) this.cleanupOrphanedWorktrees().catch((err) => {
|
|
2102
|
-
logger$
|
|
2102
|
+
logger$28.warn({ err }, "Worktree cleanup failed during recovery (non-fatal)");
|
|
2103
2103
|
});
|
|
2104
2104
|
let newlyReady = 0;
|
|
2105
2105
|
if (sessionId !== void 0) {
|
|
@@ -2109,7 +2109,7 @@ var CrashRecoveryManager = class {
|
|
|
2109
2109
|
const row = db.prepare("SELECT COUNT(*) as count FROM ready_tasks").get();
|
|
2110
2110
|
newlyReady = row.count;
|
|
2111
2111
|
}
|
|
2112
|
-
logger$
|
|
2112
|
+
logger$28.info({
|
|
2113
2113
|
event: "recovery:complete",
|
|
2114
2114
|
recovered,
|
|
2115
2115
|
failed,
|
|
@@ -2131,10 +2131,10 @@ var CrashRecoveryManager = class {
|
|
|
2131
2131
|
if (this.gitWorktreeManager === void 0) return 0;
|
|
2132
2132
|
try {
|
|
2133
2133
|
const count = await this.gitWorktreeManager.cleanupAllWorktrees();
|
|
2134
|
-
logger$
|
|
2134
|
+
logger$28.info({ count }, "Cleaned up orphaned worktrees");
|
|
2135
2135
|
return count;
|
|
2136
2136
|
} catch (err) {
|
|
2137
|
-
logger$
|
|
2137
|
+
logger$28.warn({ err }, "Failed to clean up orphaned worktrees — continuing");
|
|
2138
2138
|
return 0;
|
|
2139
2139
|
}
|
|
2140
2140
|
}
|
|
@@ -2217,7 +2217,7 @@ function setupGracefulShutdown(options) {
|
|
|
2217
2217
|
|
|
2218
2218
|
//#endregion
|
|
2219
2219
|
//#region src/cli/commands/start.ts
|
|
2220
|
-
const logger$
|
|
2220
|
+
const logger$27 = createLogger("start-cmd");
|
|
2221
2221
|
const START_EXIT_SUCCESS = 0;
|
|
2222
2222
|
const START_EXIT_ERROR = 1;
|
|
2223
2223
|
const START_EXIT_USAGE_ERROR = 2;
|
|
@@ -2326,7 +2326,7 @@ async function runStartAction(options) {
|
|
|
2326
2326
|
let configWatcher$1 = null;
|
|
2327
2327
|
const configFilePath = join(projectRoot, "substrate.config.yaml");
|
|
2328
2328
|
if (noWatchConfig) {
|
|
2329
|
-
logger$
|
|
2329
|
+
logger$27.info("Config hot-reload disabled (--no-watch-config).");
|
|
2330
2330
|
process.stdout.write("Config hot-reload disabled (--no-watch-config).\n");
|
|
2331
2331
|
} else {
|
|
2332
2332
|
let currentHotConfig = config;
|
|
@@ -2341,7 +2341,7 @@ async function runStartAction(options) {
|
|
|
2341
2341
|
const changedKeys = computeChangedKeys(previousConfig, newConfig);
|
|
2342
2342
|
currentHotConfig = newConfig;
|
|
2343
2343
|
const n = changedKeys.length;
|
|
2344
|
-
logger$
|
|
2344
|
+
logger$27.info({
|
|
2345
2345
|
changedKeys,
|
|
2346
2346
|
configPath: configFilePath
|
|
2347
2347
|
}, `Config reloaded: ${n} setting(s) changed`);
|
|
@@ -2353,7 +2353,7 @@ async function runStartAction(options) {
|
|
|
2353
2353
|
});
|
|
2354
2354
|
},
|
|
2355
2355
|
onError: (err) => {
|
|
2356
|
-
logger$
|
|
2356
|
+
logger$27.error({
|
|
2357
2357
|
err,
|
|
2358
2358
|
configPath: configFilePath
|
|
2359
2359
|
}, `Config reload failed: ${err.message}. Continuing with previous config.`);
|
|
@@ -2366,7 +2366,7 @@ async function runStartAction(options) {
|
|
|
2366
2366
|
let cleanupShutdown = null;
|
|
2367
2367
|
if (resolvedGraphFile === null) if (interruptedSession !== void 0) {
|
|
2368
2368
|
process.stdout.write(`Resuming interrupted session ${interruptedSession.id}\n`);
|
|
2369
|
-
logger$
|
|
2369
|
+
logger$27.info({ sessionId: interruptedSession.id }, "session:resumed");
|
|
2370
2370
|
const recovery = new CrashRecoveryManager({
|
|
2371
2371
|
db: databaseService.db,
|
|
2372
2372
|
gitWorktreeManager
|
|
@@ -2490,7 +2490,7 @@ async function runStartAction(options) {
|
|
|
2490
2490
|
} catch (err) {
|
|
2491
2491
|
const message = err instanceof Error ? err.message : String(err);
|
|
2492
2492
|
process.stderr.write(`Error: ${message}\n`);
|
|
2493
|
-
logger$
|
|
2493
|
+
logger$27.error({ err }, "runStartAction failed");
|
|
2494
2494
|
return START_EXIT_ERROR;
|
|
2495
2495
|
} finally {
|
|
2496
2496
|
try {
|
|
@@ -2648,7 +2648,7 @@ function renderTaskGraph(snapshot, tasks) {
|
|
|
2648
2648
|
|
|
2649
2649
|
//#endregion
|
|
2650
2650
|
//#region src/cli/commands/status.ts
|
|
2651
|
-
const logger$
|
|
2651
|
+
const logger$26 = createLogger("status-cmd");
|
|
2652
2652
|
const STATUS_EXIT_SUCCESS = 0;
|
|
2653
2653
|
const STATUS_EXIT_ERROR = 1;
|
|
2654
2654
|
const STATUS_EXIT_NOT_FOUND = 2;
|
|
@@ -2801,7 +2801,7 @@ async function runStatusAction(options) {
|
|
|
2801
2801
|
} catch (err) {
|
|
2802
2802
|
const message = err instanceof Error ? err.message : String(err);
|
|
2803
2803
|
process.stderr.write(`Error: ${message}\n`);
|
|
2804
|
-
logger$
|
|
2804
|
+
logger$26.error({ err }, "runStatusAction failed");
|
|
2805
2805
|
return STATUS_EXIT_ERROR;
|
|
2806
2806
|
} finally {
|
|
2807
2807
|
if (wrapper !== null) try {
|
|
@@ -2834,7 +2834,7 @@ function registerStatusCommand(program, _version = "0.0.0", projectRoot = proces
|
|
|
2834
2834
|
|
|
2835
2835
|
//#endregion
|
|
2836
2836
|
//#region src/cli/commands/pause.ts
|
|
2837
|
-
const logger$
|
|
2837
|
+
const logger$25 = createLogger("pause-cmd");
|
|
2838
2838
|
const PAUSE_EXIT_SUCCESS = 0;
|
|
2839
2839
|
const PAUSE_EXIT_ERROR = 1;
|
|
2840
2840
|
const PAUSE_EXIT_USAGE_ERROR = 2;
|
|
@@ -2903,7 +2903,7 @@ async function runPauseAction(options) {
|
|
|
2903
2903
|
} catch (err) {
|
|
2904
2904
|
const message = err instanceof Error ? err.message : String(err);
|
|
2905
2905
|
process.stderr.write(`Error: ${message}\n`);
|
|
2906
|
-
logger$
|
|
2906
|
+
logger$25.error({ err }, "runPauseAction failed");
|
|
2907
2907
|
return PAUSE_EXIT_ERROR;
|
|
2908
2908
|
} finally {
|
|
2909
2909
|
if (wrapper !== null) try {
|
|
@@ -2933,7 +2933,7 @@ function registerPauseCommand(program, version = "0.0.0", projectRoot = process.
|
|
|
2933
2933
|
|
|
2934
2934
|
//#endregion
|
|
2935
2935
|
//#region src/cli/commands/resume.ts
|
|
2936
|
-
const logger$
|
|
2936
|
+
const logger$24 = createLogger("resume-cmd");
|
|
2937
2937
|
const RESUME_EXIT_SUCCESS = 0;
|
|
2938
2938
|
const RESUME_EXIT_ERROR = 1;
|
|
2939
2939
|
const RESUME_EXIT_USAGE_ERROR = 2;
|
|
@@ -3018,7 +3018,7 @@ async function runResumeAction(options) {
|
|
|
3018
3018
|
} catch (err) {
|
|
3019
3019
|
const message = err instanceof Error ? err.message : String(err);
|
|
3020
3020
|
process.stderr.write(`Error: ${message}\n`);
|
|
3021
|
-
logger$
|
|
3021
|
+
logger$24.error({ err }, "runResumeAction failed");
|
|
3022
3022
|
return RESUME_EXIT_ERROR;
|
|
3023
3023
|
} finally {
|
|
3024
3024
|
if (wrapper !== null) try {
|
|
@@ -3051,7 +3051,7 @@ function registerResumeCommand(program, version = "0.0.0", projectRoot = process
|
|
|
3051
3051
|
|
|
3052
3052
|
//#endregion
|
|
3053
3053
|
//#region src/cli/commands/cancel.ts
|
|
3054
|
-
const logger$
|
|
3054
|
+
const logger$23 = createLogger("cancel-cmd");
|
|
3055
3055
|
const CANCEL_EXIT_SUCCESS = 0;
|
|
3056
3056
|
const CANCEL_EXIT_ERROR = 1;
|
|
3057
3057
|
const CANCEL_EXIT_USAGE_ERROR = 2;
|
|
@@ -3148,7 +3148,7 @@ async function runCancelAction(options) {
|
|
|
3148
3148
|
} catch (err) {
|
|
3149
3149
|
const message = err instanceof Error ? err.message : String(err);
|
|
3150
3150
|
process.stderr.write(`Error: ${message}\n`);
|
|
3151
|
-
logger$
|
|
3151
|
+
logger$23.error({ err }, "runCancelAction failed");
|
|
3152
3152
|
return CANCEL_EXIT_ERROR;
|
|
3153
3153
|
} finally {
|
|
3154
3154
|
if (wrapper !== null) try {
|
|
@@ -3263,7 +3263,7 @@ function renderFailedTasksJson(tasks) {
|
|
|
3263
3263
|
|
|
3264
3264
|
//#endregion
|
|
3265
3265
|
//#region src/cli/commands/retry.ts
|
|
3266
|
-
const logger$
|
|
3266
|
+
const logger$22 = createLogger("retry-cmd");
|
|
3267
3267
|
const RETRY_EXIT_SUCCESS = 0;
|
|
3268
3268
|
const RETRY_EXIT_PARTIAL_FAILURE = 1;
|
|
3269
3269
|
const RETRY_EXIT_USAGE_ERROR = 2;
|
|
@@ -3368,7 +3368,7 @@ async function runRetryAction(options) {
|
|
|
3368
3368
|
} catch (err) {
|
|
3369
3369
|
const message = err instanceof Error ? err.message : String(err);
|
|
3370
3370
|
process.stderr.write(`Error: ${message}\n`);
|
|
3371
|
-
logger$
|
|
3371
|
+
logger$22.error({ err }, "runRetryAction failed");
|
|
3372
3372
|
return RETRY_EXIT_USAGE_ERROR;
|
|
3373
3373
|
} finally {
|
|
3374
3374
|
if (wrapper !== null) try {
|
|
@@ -3497,11 +3497,11 @@ async function runFollowMode(opts) {
|
|
|
3497
3497
|
});
|
|
3498
3498
|
});
|
|
3499
3499
|
const sigintHandler = () => {
|
|
3500
|
-
logger$
|
|
3500
|
+
logger$22.info("SIGINT received — initiating graceful shutdown");
|
|
3501
3501
|
taskGraphEngine.cancelAll();
|
|
3502
3502
|
};
|
|
3503
3503
|
const sigtermHandler = () => {
|
|
3504
|
-
logger$
|
|
3504
|
+
logger$22.info("SIGTERM received — initiating graceful shutdown");
|
|
3505
3505
|
taskGraphEngine.cancelAll();
|
|
3506
3506
|
};
|
|
3507
3507
|
process.once("SIGINT", sigintHandler);
|
|
@@ -3514,7 +3514,7 @@ async function runFollowMode(opts) {
|
|
|
3514
3514
|
} catch (err) {
|
|
3515
3515
|
const message = err instanceof Error ? err.message : String(err);
|
|
3516
3516
|
process.stderr.write(`Error: ${message}\n`);
|
|
3517
|
-
logger$
|
|
3517
|
+
logger$22.error({ err }, "runFollowMode failed");
|
|
3518
3518
|
return RETRY_EXIT_USAGE_ERROR;
|
|
3519
3519
|
} finally {
|
|
3520
3520
|
try {
|
|
@@ -3974,7 +3974,7 @@ function buildMultiAgentInstructionsSection(agentCount) {
|
|
|
3974
3974
|
|
|
3975
3975
|
//#endregion
|
|
3976
3976
|
//#region src/modules/plan-generator/plan-generator.ts
|
|
3977
|
-
const logger$
|
|
3977
|
+
const logger$21 = createLogger("plan-generator");
|
|
3978
3978
|
/**
|
|
3979
3979
|
* Wrapper around execFile that immediately closes stdin on the child process.
|
|
3980
3980
|
* Some CLI tools (e.g. Claude Code) wait for stdin to close before processing
|
|
@@ -4151,7 +4151,7 @@ var PlanGenerator = class {
|
|
|
4151
4151
|
else {
|
|
4152
4152
|
const slugified = dep.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 64);
|
|
4153
4153
|
if (taskKeys.has(slugified)) resolvedDeps.push(slugified);
|
|
4154
|
-
else logger$
|
|
4154
|
+
else logger$21.warn({
|
|
4155
4155
|
taskKey,
|
|
4156
4156
|
dep
|
|
4157
4157
|
}, `depends_on reference '${dep}' not found in task keys; removing`);
|
|
@@ -4898,7 +4898,7 @@ function getLatestPlanVersion(db, planId) {
|
|
|
4898
4898
|
|
|
4899
4899
|
//#endregion
|
|
4900
4900
|
//#region src/modules/plan-generator/plan-refiner.ts
|
|
4901
|
-
const logger$
|
|
4901
|
+
const logger$20 = createLogger("plan-refiner");
|
|
4902
4902
|
var PlanRefiner = class {
|
|
4903
4903
|
db;
|
|
4904
4904
|
planGenerator;
|
|
@@ -4941,7 +4941,7 @@ var PlanRefiner = class {
|
|
|
4941
4941
|
newFeedback: feedback,
|
|
4942
4942
|
availableAgents: this.availableAgents
|
|
4943
4943
|
});
|
|
4944
|
-
logger$
|
|
4944
|
+
logger$20.info({
|
|
4945
4945
|
planId,
|
|
4946
4946
|
currentVersion,
|
|
4947
4947
|
feedbackRounds: feedbackHistory.length
|
|
@@ -4988,7 +4988,7 @@ var PlanRefiner = class {
|
|
|
4988
4988
|
newVersion,
|
|
4989
4989
|
taskCount
|
|
4990
4990
|
});
|
|
4991
|
-
logger$
|
|
4991
|
+
logger$20.info({
|
|
4992
4992
|
planId,
|
|
4993
4993
|
newVersion,
|
|
4994
4994
|
taskCount
|
|
@@ -5070,7 +5070,7 @@ function normalizeForDiff(value) {
|
|
|
5070
5070
|
|
|
5071
5071
|
//#endregion
|
|
5072
5072
|
//#region src/cli/commands/plan-refine.ts
|
|
5073
|
-
const logger$
|
|
5073
|
+
const logger$19 = createLogger("plan-refine-cmd");
|
|
5074
5074
|
const REFINE_EXIT_SUCCESS = 0;
|
|
5075
5075
|
const REFINE_EXIT_ERROR = 1;
|
|
5076
5076
|
const REFINE_EXIT_USAGE_ERROR = 2;
|
|
@@ -5112,7 +5112,7 @@ async function runPlanRefineAction(options) {
|
|
|
5112
5112
|
let result;
|
|
5113
5113
|
try {
|
|
5114
5114
|
result = await refiner.refine(planId, feedback, (event, payload) => {
|
|
5115
|
-
logger$
|
|
5115
|
+
logger$19.info({
|
|
5116
5116
|
event,
|
|
5117
5117
|
payload
|
|
5118
5118
|
}, "Plan refinement event");
|
|
@@ -5155,7 +5155,7 @@ async function runPlanRefineAction(options) {
|
|
|
5155
5155
|
} catch (err) {
|
|
5156
5156
|
const message = err instanceof Error ? err.message : String(err);
|
|
5157
5157
|
process.stderr.write(`Error: ${message}\n`);
|
|
5158
|
-
logger$
|
|
5158
|
+
logger$19.error({ err }, "runPlanRefineAction failed");
|
|
5159
5159
|
return REFINE_EXIT_ERROR;
|
|
5160
5160
|
} finally {
|
|
5161
5161
|
dbWrapper.close();
|
|
@@ -5180,7 +5180,7 @@ function registerPlanRefineCommand(planCmd, _version = "0.0.0", projectRoot = pr
|
|
|
5180
5180
|
|
|
5181
5181
|
//#endregion
|
|
5182
5182
|
//#region src/cli/commands/plan-diff.ts
|
|
5183
|
-
const logger$
|
|
5183
|
+
const logger$18 = createLogger("plan-diff-cmd");
|
|
5184
5184
|
const DIFF_EXIT_SUCCESS = 0;
|
|
5185
5185
|
const DIFF_EXIT_ERROR = 1;
|
|
5186
5186
|
const DIFF_EXIT_NOT_FOUND = 2;
|
|
@@ -5223,7 +5223,7 @@ async function runPlanDiffAction(options) {
|
|
|
5223
5223
|
} catch (err) {
|
|
5224
5224
|
const message = err instanceof Error ? err.message : String(err);
|
|
5225
5225
|
process.stderr.write(`Error: ${message}\n`);
|
|
5226
|
-
logger$
|
|
5226
|
+
logger$18.error({ err }, "runPlanDiffAction failed");
|
|
5227
5227
|
return DIFF_EXIT_ERROR;
|
|
5228
5228
|
} finally {
|
|
5229
5229
|
dbWrapper.close();
|
|
@@ -5271,7 +5271,7 @@ function registerPlanDiffCommand(planCmd, _version = "0.0.0", projectRoot = proc
|
|
|
5271
5271
|
|
|
5272
5272
|
//#endregion
|
|
5273
5273
|
//#region src/cli/commands/plan-rollback.ts
|
|
5274
|
-
const logger$
|
|
5274
|
+
const logger$17 = createLogger("plan-rollback-cmd");
|
|
5275
5275
|
const ROLLBACK_EXIT_SUCCESS = 0;
|
|
5276
5276
|
const ROLLBACK_EXIT_ERROR = 1;
|
|
5277
5277
|
const ROLLBACK_EXIT_USAGE_ERROR = 2;
|
|
@@ -5319,7 +5319,7 @@ async function runPlanRollbackAction(options, onEvent) {
|
|
|
5319
5319
|
toVersion,
|
|
5320
5320
|
newVersion
|
|
5321
5321
|
});
|
|
5322
|
-
logger$
|
|
5322
|
+
logger$17.info({
|
|
5323
5323
|
planId,
|
|
5324
5324
|
fromVersion,
|
|
5325
5325
|
toVersion,
|
|
@@ -5360,7 +5360,7 @@ async function runPlanRollbackAction(options, onEvent) {
|
|
|
5360
5360
|
} catch (err) {
|
|
5361
5361
|
const message = err instanceof Error ? err.message : String(err);
|
|
5362
5362
|
process.stderr.write(`Error: ${message}\n`);
|
|
5363
|
-
logger$
|
|
5363
|
+
logger$17.error({ err }, "runPlanRollbackAction failed");
|
|
5364
5364
|
return ROLLBACK_EXIT_ERROR;
|
|
5365
5365
|
} finally {
|
|
5366
5366
|
dbWrapper.close();
|
|
@@ -5554,7 +5554,7 @@ function validatePlan(raw, adapterRegistry, options) {
|
|
|
5554
5554
|
|
|
5555
5555
|
//#endregion
|
|
5556
5556
|
//#region src/cli/commands/plan.ts
|
|
5557
|
-
const logger$
|
|
5557
|
+
const logger$16 = createLogger("plan-cmd");
|
|
5558
5558
|
const PLAN_EXIT_SUCCESS = 0;
|
|
5559
5559
|
const PLAN_EXIT_ERROR = 1;
|
|
5560
5560
|
const PLAN_EXIT_USAGE_ERROR = 2;
|
|
@@ -5698,7 +5698,7 @@ async function runPlanReviewAction(options) {
|
|
|
5698
5698
|
}
|
|
5699
5699
|
const message = err instanceof Error ? err.message : String(err);
|
|
5700
5700
|
process.stderr.write(`Error: ${message}\n`);
|
|
5701
|
-
logger$
|
|
5701
|
+
logger$16.error({ err }, "runPlanReviewAction failed");
|
|
5702
5702
|
return PLAN_EXIT_ERROR;
|
|
5703
5703
|
}
|
|
5704
5704
|
if (dryRun) {
|
|
@@ -5724,7 +5724,7 @@ async function runPlanReviewAction(options) {
|
|
|
5724
5724
|
if (ext.endsWith(".yaml") || ext.endsWith(".yml")) taskGraph = load(planYaml);
|
|
5725
5725
|
else taskGraph = JSON.parse(planYaml);
|
|
5726
5726
|
} catch {
|
|
5727
|
-
logger$
|
|
5727
|
+
logger$16.warn("Could not read generated plan file for DB storage");
|
|
5728
5728
|
}
|
|
5729
5729
|
if (outputFormat === "json") {
|
|
5730
5730
|
const envelope = {
|
|
@@ -7181,7 +7181,7 @@ function truncateToTokens(text, maxTokens) {
|
|
|
7181
7181
|
|
|
7182
7182
|
//#endregion
|
|
7183
7183
|
//#region src/modules/context-compiler/context-compiler-impl.ts
|
|
7184
|
-
const logger$
|
|
7184
|
+
const logger$15 = createLogger("context-compiler");
|
|
7185
7185
|
/**
|
|
7186
7186
|
* Fraction of the original token budget that must remain (after required +
|
|
7187
7187
|
* important sections) before an optional section is included.
|
|
@@ -7273,7 +7273,7 @@ var ContextCompilerImpl = class {
|
|
|
7273
7273
|
includedParts.push(truncated);
|
|
7274
7274
|
remainingBudget -= truncatedTokens;
|
|
7275
7275
|
anyTruncated = true;
|
|
7276
|
-
logger$
|
|
7276
|
+
logger$15.warn({
|
|
7277
7277
|
section: section.name,
|
|
7278
7278
|
originalTokens: tokens,
|
|
7279
7279
|
budgetTokens: truncatedTokens
|
|
@@ -7287,7 +7287,7 @@ var ContextCompilerImpl = class {
|
|
|
7287
7287
|
});
|
|
7288
7288
|
} else {
|
|
7289
7289
|
anyTruncated = true;
|
|
7290
|
-
logger$
|
|
7290
|
+
logger$15.warn({
|
|
7291
7291
|
section: section.name,
|
|
7292
7292
|
tokens
|
|
7293
7293
|
}, "Context compiler: omitted \"important\" section — no budget remaining");
|
|
@@ -7314,7 +7314,7 @@ var ContextCompilerImpl = class {
|
|
|
7314
7314
|
} else {
|
|
7315
7315
|
if (tokens > 0) {
|
|
7316
7316
|
anyTruncated = true;
|
|
7317
|
-
logger$
|
|
7317
|
+
logger$15.warn({
|
|
7318
7318
|
section: section.name,
|
|
7319
7319
|
tokens,
|
|
7320
7320
|
budgetFractionRemaining: budgetFractionRemaining.toFixed(2)
|
|
@@ -7416,6 +7416,8 @@ const DEFAULT_TIMEOUTS = {
|
|
|
7416
7416
|
"code-review": 9e5,
|
|
7417
7417
|
"minor-fixes": 6e5,
|
|
7418
7418
|
"major-rework": 9e5,
|
|
7419
|
+
"readiness-check": 6e5,
|
|
7420
|
+
"elicitation": 9e5,
|
|
7419
7421
|
"analysis-vision": 18e4,
|
|
7420
7422
|
"analysis-scope": 18e4,
|
|
7421
7423
|
"planning-classification": 18e4,
|
|
@@ -7580,7 +7582,7 @@ function parseYamlResult(yamlText, schema) {
|
|
|
7580
7582
|
|
|
7581
7583
|
//#endregion
|
|
7582
7584
|
//#region src/modules/agent-dispatch/dispatcher-impl.ts
|
|
7583
|
-
const logger$
|
|
7585
|
+
const logger$14 = createLogger("agent-dispatch");
|
|
7584
7586
|
const SHUTDOWN_GRACE_MS = 1e4;
|
|
7585
7587
|
const SHUTDOWN_MAX_WAIT_MS = 3e4;
|
|
7586
7588
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -7649,7 +7651,7 @@ var DispatcherImpl = class {
|
|
|
7649
7651
|
resolve: typedResolve,
|
|
7650
7652
|
reject
|
|
7651
7653
|
});
|
|
7652
|
-
logger$
|
|
7654
|
+
logger$14.debug({
|
|
7653
7655
|
id,
|
|
7654
7656
|
queueLength: this._queue.length
|
|
7655
7657
|
}, "Dispatch queued");
|
|
@@ -7679,7 +7681,7 @@ var DispatcherImpl = class {
|
|
|
7679
7681
|
}
|
|
7680
7682
|
async shutdown() {
|
|
7681
7683
|
this._shuttingDown = true;
|
|
7682
|
-
logger$
|
|
7684
|
+
logger$14.info({
|
|
7683
7685
|
running: this._running.size,
|
|
7684
7686
|
queued: this._queue.length
|
|
7685
7687
|
}, "Dispatcher shutting down");
|
|
@@ -7712,13 +7714,13 @@ var DispatcherImpl = class {
|
|
|
7712
7714
|
}
|
|
7713
7715
|
}, 50);
|
|
7714
7716
|
});
|
|
7715
|
-
logger$
|
|
7717
|
+
logger$14.info("Dispatcher shutdown complete");
|
|
7716
7718
|
}
|
|
7717
7719
|
async _startDispatch(id, request, resolve$2) {
|
|
7718
7720
|
const { prompt, agent, taskType, timeout, outputSchema, workingDirectory, model, maxTurns } = request;
|
|
7719
7721
|
const adapter = this._adapterRegistry.get(agent);
|
|
7720
7722
|
if (adapter === void 0) {
|
|
7721
|
-
logger$
|
|
7723
|
+
logger$14.warn({
|
|
7722
7724
|
id,
|
|
7723
7725
|
agent
|
|
7724
7726
|
}, "No adapter found for agent");
|
|
@@ -7762,7 +7764,7 @@ var DispatcherImpl = class {
|
|
|
7762
7764
|
});
|
|
7763
7765
|
const startedAt = Date.now();
|
|
7764
7766
|
proc.on("error", (err) => {
|
|
7765
|
-
logger$
|
|
7767
|
+
logger$14.error({
|
|
7766
7768
|
id,
|
|
7767
7769
|
binary: cmd.binary,
|
|
7768
7770
|
error: err.message
|
|
@@ -7770,7 +7772,7 @@ var DispatcherImpl = class {
|
|
|
7770
7772
|
});
|
|
7771
7773
|
if (proc.stdin !== null) {
|
|
7772
7774
|
proc.stdin.on("error", (err) => {
|
|
7773
|
-
if (err.code !== "EPIPE") logger$
|
|
7775
|
+
if (err.code !== "EPIPE") logger$14.warn({
|
|
7774
7776
|
id,
|
|
7775
7777
|
error: err.message
|
|
7776
7778
|
}, "stdin write error");
|
|
@@ -7812,7 +7814,7 @@ var DispatcherImpl = class {
|
|
|
7812
7814
|
agent,
|
|
7813
7815
|
taskType
|
|
7814
7816
|
});
|
|
7815
|
-
logger$
|
|
7817
|
+
logger$14.debug({
|
|
7816
7818
|
id,
|
|
7817
7819
|
agent,
|
|
7818
7820
|
taskType,
|
|
@@ -7829,7 +7831,7 @@ var DispatcherImpl = class {
|
|
|
7829
7831
|
dispatchId: id,
|
|
7830
7832
|
timeoutMs
|
|
7831
7833
|
});
|
|
7832
|
-
logger$
|
|
7834
|
+
logger$14.warn({
|
|
7833
7835
|
id,
|
|
7834
7836
|
agent,
|
|
7835
7837
|
taskType,
|
|
@@ -7883,7 +7885,7 @@ var DispatcherImpl = class {
|
|
|
7883
7885
|
exitCode: code,
|
|
7884
7886
|
output: stdout
|
|
7885
7887
|
});
|
|
7886
|
-
logger$
|
|
7888
|
+
logger$14.debug({
|
|
7887
7889
|
id,
|
|
7888
7890
|
agent,
|
|
7889
7891
|
taskType,
|
|
@@ -7909,7 +7911,7 @@ var DispatcherImpl = class {
|
|
|
7909
7911
|
error: stderr || `Process exited with code ${String(code)}`,
|
|
7910
7912
|
exitCode: code
|
|
7911
7913
|
});
|
|
7912
|
-
logger$
|
|
7914
|
+
logger$14.debug({
|
|
7913
7915
|
id,
|
|
7914
7916
|
agent,
|
|
7915
7917
|
taskType,
|
|
@@ -7961,7 +7963,7 @@ var DispatcherImpl = class {
|
|
|
7961
7963
|
const next = this._queue.shift();
|
|
7962
7964
|
if (next === void 0) return;
|
|
7963
7965
|
next.handle.status = "running";
|
|
7964
|
-
logger$
|
|
7966
|
+
logger$14.debug({
|
|
7965
7967
|
id: next.id,
|
|
7966
7968
|
queueLength: this._queue.length
|
|
7967
7969
|
}, "Dequeued dispatch");
|
|
@@ -8320,9 +8322,120 @@ function getTokenUsageSummary(db, runId) {
|
|
|
8320
8322
|
return stmt.all(runId);
|
|
8321
8323
|
}
|
|
8322
8324
|
|
|
8325
|
+
//#endregion
|
|
8326
|
+
//#region src/persistence/queries/metrics.ts
|
|
8327
|
+
/**
|
|
8328
|
+
* Write or update run-level metrics.
|
|
8329
|
+
*/
|
|
8330
|
+
function writeRunMetrics(db, input) {
|
|
8331
|
+
const stmt = db.prepare(`
|
|
8332
|
+
INSERT OR REPLACE INTO run_metrics (
|
|
8333
|
+
run_id, methodology, status, started_at, completed_at,
|
|
8334
|
+
wall_clock_seconds, total_input_tokens, total_output_tokens, total_cost_usd,
|
|
8335
|
+
stories_attempted, stories_succeeded, stories_failed, stories_escalated,
|
|
8336
|
+
total_review_cycles, total_dispatches, concurrency_setting, max_concurrent_actual, restarts,
|
|
8337
|
+
is_baseline
|
|
8338
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8339
|
+
`);
|
|
8340
|
+
stmt.run(input.run_id, input.methodology, input.status, input.started_at, input.completed_at ?? null, input.wall_clock_seconds ?? 0, input.total_input_tokens ?? 0, input.total_output_tokens ?? 0, input.total_cost_usd ?? 0, input.stories_attempted ?? 0, input.stories_succeeded ?? 0, input.stories_failed ?? 0, input.stories_escalated ?? 0, input.total_review_cycles ?? 0, input.total_dispatches ?? 0, input.concurrency_setting ?? 1, input.max_concurrent_actual ?? 1, input.restarts ?? 0, input.is_baseline ?? 0);
|
|
8341
|
+
}
|
|
8342
|
+
/**
|
|
8343
|
+
* Get run metrics for a specific run.
|
|
8344
|
+
*/
|
|
8345
|
+
function getRunMetrics(db, runId) {
|
|
8346
|
+
return db.prepare("SELECT * FROM run_metrics WHERE run_id = ?").get(runId);
|
|
8347
|
+
}
|
|
8348
|
+
/**
|
|
8349
|
+
* List the most recent N run metrics rows, newest first.
|
|
8350
|
+
*/
|
|
8351
|
+
function listRunMetrics(db, limit = 10) {
|
|
8352
|
+
return db.prepare("SELECT * FROM run_metrics ORDER BY started_at DESC LIMIT ?").all(limit);
|
|
8353
|
+
}
|
|
8354
|
+
/**
|
|
8355
|
+
* Tag a run as the baseline (clears any existing baseline first).
|
|
8356
|
+
*/
|
|
8357
|
+
function tagRunAsBaseline(db, runId) {
|
|
8358
|
+
db.transaction(() => {
|
|
8359
|
+
db.prepare("UPDATE run_metrics SET is_baseline = 0").run();
|
|
8360
|
+
db.prepare("UPDATE run_metrics SET is_baseline = 1 WHERE run_id = ?").run(runId);
|
|
8361
|
+
})();
|
|
8362
|
+
}
|
|
8363
|
+
/**
|
|
8364
|
+
* Write or update story-level metrics.
|
|
8365
|
+
*/
|
|
8366
|
+
function writeStoryMetrics(db, input) {
|
|
8367
|
+
const stmt = db.prepare(`
|
|
8368
|
+
INSERT INTO story_metrics (
|
|
8369
|
+
run_id, story_key, result, phase_durations_json, started_at, completed_at,
|
|
8370
|
+
wall_clock_seconds, input_tokens, output_tokens, cost_usd,
|
|
8371
|
+
review_cycles, dispatches
|
|
8372
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8373
|
+
ON CONFLICT(run_id, story_key) DO UPDATE SET
|
|
8374
|
+
result = excluded.result,
|
|
8375
|
+
phase_durations_json = excluded.phase_durations_json,
|
|
8376
|
+
started_at = COALESCE(excluded.started_at, story_metrics.started_at),
|
|
8377
|
+
completed_at = excluded.completed_at,
|
|
8378
|
+
wall_clock_seconds = excluded.wall_clock_seconds,
|
|
8379
|
+
input_tokens = excluded.input_tokens,
|
|
8380
|
+
output_tokens = excluded.output_tokens,
|
|
8381
|
+
cost_usd = excluded.cost_usd,
|
|
8382
|
+
review_cycles = excluded.review_cycles,
|
|
8383
|
+
dispatches = excluded.dispatches
|
|
8384
|
+
`);
|
|
8385
|
+
stmt.run(input.run_id, input.story_key, input.result, input.phase_durations_json ?? null, input.started_at ?? null, input.completed_at ?? null, input.wall_clock_seconds ?? 0, input.input_tokens ?? 0, input.output_tokens ?? 0, input.cost_usd ?? 0, input.review_cycles ?? 0, input.dispatches ?? 0);
|
|
8386
|
+
}
|
|
8387
|
+
/**
|
|
8388
|
+
* Compare two runs and return percentage deltas for key numeric fields.
|
|
8389
|
+
* Positive deltas mean run B is larger/longer than run A.
|
|
8390
|
+
* Returns null if either run does not exist.
|
|
8391
|
+
*/
|
|
8392
|
+
function compareRunMetrics(db, runIdA, runIdB) {
|
|
8393
|
+
const a = getRunMetrics(db, runIdA);
|
|
8394
|
+
const b = getRunMetrics(db, runIdB);
|
|
8395
|
+
if (!a || !b) return null;
|
|
8396
|
+
const pct = (base, diff) => base === 0 ? 0 : Math.round(diff / base * 100 * 10) / 10;
|
|
8397
|
+
const inputDelta = b.total_input_tokens - a.total_input_tokens;
|
|
8398
|
+
const outputDelta = b.total_output_tokens - a.total_output_tokens;
|
|
8399
|
+
const clockDelta = (b.wall_clock_seconds ?? 0) - (a.wall_clock_seconds ?? 0);
|
|
8400
|
+
const cycleDelta = b.total_review_cycles - a.total_review_cycles;
|
|
8401
|
+
const costDelta = (b.total_cost_usd ?? 0) - (a.total_cost_usd ?? 0);
|
|
8402
|
+
return {
|
|
8403
|
+
run_id_a: runIdA,
|
|
8404
|
+
run_id_b: runIdB,
|
|
8405
|
+
token_input_delta: inputDelta,
|
|
8406
|
+
token_output_delta: outputDelta,
|
|
8407
|
+
token_input_pct: pct(a.total_input_tokens, inputDelta),
|
|
8408
|
+
token_output_pct: pct(a.total_output_tokens, outputDelta),
|
|
8409
|
+
wall_clock_delta_seconds: clockDelta,
|
|
8410
|
+
wall_clock_pct: pct(a.wall_clock_seconds ?? 0, clockDelta),
|
|
8411
|
+
review_cycles_delta: cycleDelta,
|
|
8412
|
+
review_cycles_pct: pct(a.total_review_cycles, cycleDelta),
|
|
8413
|
+
cost_delta: costDelta,
|
|
8414
|
+
cost_pct: pct(a.total_cost_usd ?? 0, costDelta)
|
|
8415
|
+
};
|
|
8416
|
+
}
|
|
8417
|
+
/**
|
|
8418
|
+
* Aggregate token usage from the token_usage table for a pipeline run.
|
|
8419
|
+
*/
|
|
8420
|
+
function aggregateTokenUsageForRun(db, runId) {
|
|
8421
|
+
const row = db.prepare(`
|
|
8422
|
+
SELECT
|
|
8423
|
+
COALESCE(SUM(input_tokens), 0) as input,
|
|
8424
|
+
COALESCE(SUM(output_tokens), 0) as output,
|
|
8425
|
+
COALESCE(SUM(cost_usd), 0) as cost
|
|
8426
|
+
FROM token_usage
|
|
8427
|
+
WHERE pipeline_run_id = ?
|
|
8428
|
+
`).get(runId);
|
|
8429
|
+
return row ?? {
|
|
8430
|
+
input: 0,
|
|
8431
|
+
output: 0,
|
|
8432
|
+
cost: 0
|
|
8433
|
+
};
|
|
8434
|
+
}
|
|
8435
|
+
|
|
8323
8436
|
//#endregion
|
|
8324
8437
|
//#region src/modules/compiled-workflows/prompt-assembler.ts
|
|
8325
|
-
const logger$
|
|
8438
|
+
const logger$13 = createLogger("compiled-workflows:prompt-assembler");
|
|
8326
8439
|
/**
|
|
8327
8440
|
* Assemble a final prompt from a template and sections map.
|
|
8328
8441
|
*
|
|
@@ -8347,7 +8460,7 @@ function assemblePrompt(template, sections, tokenCeiling = 2200) {
|
|
|
8347
8460
|
tokenCount,
|
|
8348
8461
|
truncated: false
|
|
8349
8462
|
};
|
|
8350
|
-
logger$
|
|
8463
|
+
logger$13.warn({
|
|
8351
8464
|
tokenCount,
|
|
8352
8465
|
ceiling: tokenCeiling
|
|
8353
8466
|
}, "Prompt exceeds token ceiling — truncating optional sections");
|
|
@@ -8363,10 +8476,10 @@ function assemblePrompt(template, sections, tokenCeiling = 2200) {
|
|
|
8363
8476
|
const targetSectionTokens = Math.max(0, currentSectionTokens - overBy);
|
|
8364
8477
|
if (targetSectionTokens === 0) {
|
|
8365
8478
|
contentMap[section.name] = "";
|
|
8366
|
-
logger$
|
|
8479
|
+
logger$13.warn({ sectionName: section.name }, "Section eliminated to fit token budget");
|
|
8367
8480
|
} else {
|
|
8368
8481
|
contentMap[section.name] = truncateToTokens(section.content, targetSectionTokens);
|
|
8369
|
-
logger$
|
|
8482
|
+
logger$13.warn({
|
|
8370
8483
|
sectionName: section.name,
|
|
8371
8484
|
targetSectionTokens
|
|
8372
8485
|
}, "Section truncated to fit token budget");
|
|
@@ -8377,7 +8490,7 @@ function assemblePrompt(template, sections, tokenCeiling = 2200) {
|
|
|
8377
8490
|
}
|
|
8378
8491
|
if (tokenCount <= tokenCeiling) break;
|
|
8379
8492
|
}
|
|
8380
|
-
if (tokenCount > tokenCeiling) logger$
|
|
8493
|
+
if (tokenCount > tokenCeiling) logger$13.warn({
|
|
8381
8494
|
tokenCount,
|
|
8382
8495
|
ceiling: tokenCeiling
|
|
8383
8496
|
}, "Required sections alone exceed token ceiling — returning over-budget prompt");
|
|
@@ -8518,7 +8631,7 @@ const CodeReviewResultSchema = z.object({
|
|
|
8518
8631
|
|
|
8519
8632
|
//#endregion
|
|
8520
8633
|
//#region src/modules/compiled-workflows/create-story.ts
|
|
8521
|
-
const logger$
|
|
8634
|
+
const logger$12 = createLogger("compiled-workflows:create-story");
|
|
8522
8635
|
/**
|
|
8523
8636
|
* Hard ceiling for the assembled create-story prompt.
|
|
8524
8637
|
*/
|
|
@@ -8542,7 +8655,7 @@ const TOKEN_CEILING$2 = 3e3;
|
|
|
8542
8655
|
*/
|
|
8543
8656
|
async function runCreateStory(deps, params) {
|
|
8544
8657
|
const { epicId, storyKey, pipelineRunId } = params;
|
|
8545
|
-
logger$
|
|
8658
|
+
logger$12.debug({
|
|
8546
8659
|
epicId,
|
|
8547
8660
|
storyKey,
|
|
8548
8661
|
pipelineRunId
|
|
@@ -8552,7 +8665,7 @@ async function runCreateStory(deps, params) {
|
|
|
8552
8665
|
template = await deps.pack.getPrompt("create-story");
|
|
8553
8666
|
} catch (err) {
|
|
8554
8667
|
const error = err instanceof Error ? err.message : String(err);
|
|
8555
|
-
logger$
|
|
8668
|
+
logger$12.error({ error }, "Failed to retrieve create-story prompt template");
|
|
8556
8669
|
return {
|
|
8557
8670
|
result: "failed",
|
|
8558
8671
|
error: `Failed to retrieve prompt template: ${error}`,
|
|
@@ -8594,7 +8707,7 @@ async function runCreateStory(deps, params) {
|
|
|
8594
8707
|
priority: "important"
|
|
8595
8708
|
}
|
|
8596
8709
|
], TOKEN_CEILING$2);
|
|
8597
|
-
logger$
|
|
8710
|
+
logger$12.debug({
|
|
8598
8711
|
tokenCount,
|
|
8599
8712
|
truncated,
|
|
8600
8713
|
tokenCeiling: TOKEN_CEILING$2
|
|
@@ -8611,7 +8724,7 @@ async function runCreateStory(deps, params) {
|
|
|
8611
8724
|
dispatchResult = await handle.result;
|
|
8612
8725
|
} catch (err) {
|
|
8613
8726
|
const error = err instanceof Error ? err.message : String(err);
|
|
8614
|
-
logger$
|
|
8727
|
+
logger$12.error({
|
|
8615
8728
|
epicId,
|
|
8616
8729
|
storyKey,
|
|
8617
8730
|
error
|
|
@@ -8632,7 +8745,7 @@ async function runCreateStory(deps, params) {
|
|
|
8632
8745
|
if (dispatchResult.status === "failed") {
|
|
8633
8746
|
const errorMsg = dispatchResult.parseError ?? `Dispatch failed with exit code ${dispatchResult.exitCode}`;
|
|
8634
8747
|
const stderrDetail = dispatchResult.output ? ` Output: ${dispatchResult.output}` : "";
|
|
8635
|
-
logger$
|
|
8748
|
+
logger$12.warn({
|
|
8636
8749
|
epicId,
|
|
8637
8750
|
storyKey,
|
|
8638
8751
|
exitCode: dispatchResult.exitCode
|
|
@@ -8644,7 +8757,7 @@ async function runCreateStory(deps, params) {
|
|
|
8644
8757
|
};
|
|
8645
8758
|
}
|
|
8646
8759
|
if (dispatchResult.status === "timeout") {
|
|
8647
|
-
logger$
|
|
8760
|
+
logger$12.warn({
|
|
8648
8761
|
epicId,
|
|
8649
8762
|
storyKey
|
|
8650
8763
|
}, "Create-story dispatch timed out");
|
|
@@ -8657,7 +8770,7 @@ async function runCreateStory(deps, params) {
|
|
|
8657
8770
|
if (dispatchResult.parsed === null) {
|
|
8658
8771
|
const details = dispatchResult.parseError ?? "No YAML block found in output";
|
|
8659
8772
|
const rawSnippet = dispatchResult.output ? dispatchResult.output.slice(0, 1e3) : "(empty)";
|
|
8660
|
-
logger$
|
|
8773
|
+
logger$12.warn({
|
|
8661
8774
|
epicId,
|
|
8662
8775
|
storyKey,
|
|
8663
8776
|
details,
|
|
@@ -8673,7 +8786,7 @@ async function runCreateStory(deps, params) {
|
|
|
8673
8786
|
const parseResult = CreateStoryResultSchema.safeParse(dispatchResult.parsed);
|
|
8674
8787
|
if (!parseResult.success) {
|
|
8675
8788
|
const details = parseResult.error.message;
|
|
8676
|
-
logger$
|
|
8789
|
+
logger$12.warn({
|
|
8677
8790
|
epicId,
|
|
8678
8791
|
storyKey,
|
|
8679
8792
|
details
|
|
@@ -8686,7 +8799,7 @@ async function runCreateStory(deps, params) {
|
|
|
8686
8799
|
};
|
|
8687
8800
|
}
|
|
8688
8801
|
const parsed = parseResult.data;
|
|
8689
|
-
logger$
|
|
8802
|
+
logger$12.info({
|
|
8690
8803
|
epicId,
|
|
8691
8804
|
storyKey,
|
|
8692
8805
|
storyFile: parsed.story_file,
|
|
@@ -8708,7 +8821,7 @@ function getImplementationDecisions(deps) {
|
|
|
8708
8821
|
try {
|
|
8709
8822
|
return getDecisionsByPhase(deps.db, "implementation");
|
|
8710
8823
|
} catch (err) {
|
|
8711
|
-
logger$
|
|
8824
|
+
logger$12.warn({ error: err instanceof Error ? err.message : String(err) }, "Failed to retrieve implementation decisions");
|
|
8712
8825
|
return [];
|
|
8713
8826
|
}
|
|
8714
8827
|
}
|
|
@@ -8724,13 +8837,13 @@ function getEpicShard(decisions, epicId, projectRoot) {
|
|
|
8724
8837
|
if (projectRoot) {
|
|
8725
8838
|
const fallback = readEpicShardFromFile(projectRoot, epicId);
|
|
8726
8839
|
if (fallback) {
|
|
8727
|
-
logger$
|
|
8840
|
+
logger$12.info({ epicId }, "Using file-based fallback for epic shard (decisions table empty)");
|
|
8728
8841
|
return fallback;
|
|
8729
8842
|
}
|
|
8730
8843
|
}
|
|
8731
8844
|
return "";
|
|
8732
8845
|
} catch (err) {
|
|
8733
|
-
logger$
|
|
8846
|
+
logger$12.warn({
|
|
8734
8847
|
epicId,
|
|
8735
8848
|
error: err instanceof Error ? err.message : String(err)
|
|
8736
8849
|
}, "Failed to retrieve epic shard");
|
|
@@ -8747,7 +8860,7 @@ function getPrevDevNotes(decisions, epicId) {
|
|
|
8747
8860
|
if (devNotes.length === 0) return "";
|
|
8748
8861
|
return devNotes[devNotes.length - 1].value;
|
|
8749
8862
|
} catch (err) {
|
|
8750
|
-
logger$
|
|
8863
|
+
logger$12.warn({
|
|
8751
8864
|
epicId,
|
|
8752
8865
|
error: err instanceof Error ? err.message : String(err)
|
|
8753
8866
|
}, "Failed to retrieve prev dev notes");
|
|
@@ -8767,13 +8880,13 @@ function getArchConstraints$1(deps) {
|
|
|
8767
8880
|
if (deps.projectRoot) {
|
|
8768
8881
|
const fallback = readArchConstraintsFromFile(deps.projectRoot);
|
|
8769
8882
|
if (fallback) {
|
|
8770
|
-
logger$
|
|
8883
|
+
logger$12.info("Using file-based fallback for architecture constraints (decisions table empty)");
|
|
8771
8884
|
return fallback;
|
|
8772
8885
|
}
|
|
8773
8886
|
}
|
|
8774
8887
|
return "";
|
|
8775
8888
|
} catch (err) {
|
|
8776
|
-
logger$
|
|
8889
|
+
logger$12.warn({ error: err instanceof Error ? err.message : String(err) }, "Failed to retrieve architecture constraints");
|
|
8777
8890
|
return "";
|
|
8778
8891
|
}
|
|
8779
8892
|
}
|
|
@@ -8793,7 +8906,7 @@ function readEpicShardFromFile(projectRoot, epicId) {
|
|
|
8793
8906
|
const match = pattern.exec(content);
|
|
8794
8907
|
return match ? match[0].trim() : "";
|
|
8795
8908
|
} catch (err) {
|
|
8796
|
-
logger$
|
|
8909
|
+
logger$12.warn({
|
|
8797
8910
|
epicId,
|
|
8798
8911
|
error: err instanceof Error ? err.message : String(err)
|
|
8799
8912
|
}, "File-based epic shard fallback failed");
|
|
@@ -8816,7 +8929,7 @@ function readArchConstraintsFromFile(projectRoot) {
|
|
|
8816
8929
|
const content = readFileSync$1(archPath, "utf-8");
|
|
8817
8930
|
return content.slice(0, 1500);
|
|
8818
8931
|
} catch (err) {
|
|
8819
|
-
logger$
|
|
8932
|
+
logger$12.warn({ error: err instanceof Error ? err.message : String(err) }, "File-based architecture fallback failed");
|
|
8820
8933
|
return "";
|
|
8821
8934
|
}
|
|
8822
8935
|
}
|
|
@@ -8829,14 +8942,14 @@ async function getStoryTemplate(deps) {
|
|
|
8829
8942
|
try {
|
|
8830
8943
|
return await deps.pack.getTemplate("story");
|
|
8831
8944
|
} catch (err) {
|
|
8832
|
-
logger$
|
|
8945
|
+
logger$12.warn({ error: err instanceof Error ? err.message : String(err) }, "Failed to retrieve story template from pack");
|
|
8833
8946
|
return "";
|
|
8834
8947
|
}
|
|
8835
8948
|
}
|
|
8836
8949
|
|
|
8837
8950
|
//#endregion
|
|
8838
8951
|
//#region src/modules/compiled-workflows/git-helpers.ts
|
|
8839
|
-
const logger$
|
|
8952
|
+
const logger$11 = createLogger("compiled-workflows:git-helpers");
|
|
8840
8953
|
/**
|
|
8841
8954
|
* Capture the full git diff for HEAD (working tree vs current commit).
|
|
8842
8955
|
*
|
|
@@ -8960,7 +9073,7 @@ async function runGitCommand(args, cwd, logLabel) {
|
|
|
8960
9073
|
stderr += chunk.toString("utf-8");
|
|
8961
9074
|
});
|
|
8962
9075
|
proc.on("error", (err) => {
|
|
8963
|
-
logger$
|
|
9076
|
+
logger$11.warn({
|
|
8964
9077
|
label: logLabel,
|
|
8965
9078
|
cwd,
|
|
8966
9079
|
error: err.message
|
|
@@ -8969,7 +9082,7 @@ async function runGitCommand(args, cwd, logLabel) {
|
|
|
8969
9082
|
});
|
|
8970
9083
|
proc.on("close", (code) => {
|
|
8971
9084
|
if (code !== 0) {
|
|
8972
|
-
logger$
|
|
9085
|
+
logger$11.warn({
|
|
8973
9086
|
label: logLabel,
|
|
8974
9087
|
cwd,
|
|
8975
9088
|
code,
|
|
@@ -8985,7 +9098,7 @@ async function runGitCommand(args, cwd, logLabel) {
|
|
|
8985
9098
|
|
|
8986
9099
|
//#endregion
|
|
8987
9100
|
//#region src/modules/compiled-workflows/dev-story.ts
|
|
8988
|
-
const logger$
|
|
9101
|
+
const logger$10 = createLogger("compiled-workflows:dev-story");
|
|
8989
9102
|
/** Hard token ceiling for the assembled dev-story prompt */
|
|
8990
9103
|
const TOKEN_CEILING$1 = 24e3;
|
|
8991
9104
|
/** Default timeout for dev-story dispatches in milliseconds (30 min) */
|
|
@@ -9007,7 +9120,7 @@ const DEFAULT_VITEST_PATTERNS = `## Test Patterns (defaults)
|
|
|
9007
9120
|
*/
|
|
9008
9121
|
async function runDevStory(deps, params) {
|
|
9009
9122
|
const { storyKey, storyFilePath, taskScope, priorFiles } = params;
|
|
9010
|
-
logger$
|
|
9123
|
+
logger$10.info({
|
|
9011
9124
|
storyKey,
|
|
9012
9125
|
storyFilePath
|
|
9013
9126
|
}, "Starting compiled dev-story workflow");
|
|
@@ -9049,10 +9162,10 @@ async function runDevStory(deps, params) {
|
|
|
9049
9162
|
let template;
|
|
9050
9163
|
try {
|
|
9051
9164
|
template = await deps.pack.getPrompt("dev-story");
|
|
9052
|
-
logger$
|
|
9165
|
+
logger$10.debug({ storyKey }, "Retrieved dev-story prompt template from pack");
|
|
9053
9166
|
} catch (err) {
|
|
9054
9167
|
const error = err instanceof Error ? err.message : String(err);
|
|
9055
|
-
logger$
|
|
9168
|
+
logger$10.error({
|
|
9056
9169
|
storyKey,
|
|
9057
9170
|
error
|
|
9058
9171
|
}, "Failed to retrieve dev-story prompt template");
|
|
@@ -9063,14 +9176,14 @@ async function runDevStory(deps, params) {
|
|
|
9063
9176
|
storyContent = await readFile$2(storyFilePath, "utf-8");
|
|
9064
9177
|
} catch (err) {
|
|
9065
9178
|
if (err.code === "ENOENT") {
|
|
9066
|
-
logger$
|
|
9179
|
+
logger$10.error({
|
|
9067
9180
|
storyKey,
|
|
9068
9181
|
storyFilePath
|
|
9069
9182
|
}, "Story file not found");
|
|
9070
9183
|
return makeFailureResult("story_file_not_found");
|
|
9071
9184
|
}
|
|
9072
9185
|
const error = err instanceof Error ? err.message : String(err);
|
|
9073
|
-
logger$
|
|
9186
|
+
logger$10.error({
|
|
9074
9187
|
storyKey,
|
|
9075
9188
|
storyFilePath,
|
|
9076
9189
|
error
|
|
@@ -9078,7 +9191,7 @@ async function runDevStory(deps, params) {
|
|
|
9078
9191
|
return makeFailureResult(`story_file_read_error: ${error}`);
|
|
9079
9192
|
}
|
|
9080
9193
|
if (storyContent.trim().length === 0) {
|
|
9081
|
-
logger$
|
|
9194
|
+
logger$10.error({
|
|
9082
9195
|
storyKey,
|
|
9083
9196
|
storyFilePath
|
|
9084
9197
|
}, "Story file is empty");
|
|
@@ -9090,17 +9203,17 @@ async function runDevStory(deps, params) {
|
|
|
9090
9203
|
const testPatternDecisions = solutioningDecisions.filter((d) => d.category === "test-patterns");
|
|
9091
9204
|
if (testPatternDecisions.length > 0) {
|
|
9092
9205
|
testPatternsContent = "## Test Patterns\n" + testPatternDecisions.map((d) => `- ${d.key}: ${d.value}`).join("\n");
|
|
9093
|
-
logger$
|
|
9206
|
+
logger$10.debug({
|
|
9094
9207
|
storyKey,
|
|
9095
9208
|
count: testPatternDecisions.length
|
|
9096
9209
|
}, "Loaded test patterns from decision store");
|
|
9097
9210
|
} else {
|
|
9098
9211
|
testPatternsContent = DEFAULT_VITEST_PATTERNS;
|
|
9099
|
-
logger$
|
|
9212
|
+
logger$10.debug({ storyKey }, "No test-pattern decisions found — using default Vitest patterns");
|
|
9100
9213
|
}
|
|
9101
9214
|
} catch (err) {
|
|
9102
9215
|
const error = err instanceof Error ? err.message : String(err);
|
|
9103
|
-
logger$
|
|
9216
|
+
logger$10.warn({
|
|
9104
9217
|
storyKey,
|
|
9105
9218
|
error
|
|
9106
9219
|
}, "Failed to load test patterns — using defaults");
|
|
@@ -9143,7 +9256,7 @@ async function runDevStory(deps, params) {
|
|
|
9143
9256
|
}
|
|
9144
9257
|
];
|
|
9145
9258
|
const { prompt, tokenCount, truncated } = assemblePrompt(template, sections, TOKEN_CEILING$1);
|
|
9146
|
-
logger$
|
|
9259
|
+
logger$10.info({
|
|
9147
9260
|
storyKey,
|
|
9148
9261
|
tokenCount,
|
|
9149
9262
|
ceiling: TOKEN_CEILING$1,
|
|
@@ -9162,7 +9275,7 @@ async function runDevStory(deps, params) {
|
|
|
9162
9275
|
dispatchResult = await handle.result;
|
|
9163
9276
|
} catch (err) {
|
|
9164
9277
|
const error = err instanceof Error ? err.message : String(err);
|
|
9165
|
-
logger$
|
|
9278
|
+
logger$10.error({
|
|
9166
9279
|
storyKey,
|
|
9167
9280
|
error
|
|
9168
9281
|
}, "Dispatch threw an unexpected error");
|
|
@@ -9173,11 +9286,11 @@ async function runDevStory(deps, params) {
|
|
|
9173
9286
|
output: dispatchResult.tokenEstimate.output
|
|
9174
9287
|
};
|
|
9175
9288
|
if (dispatchResult.status === "timeout") {
|
|
9176
|
-
logger$
|
|
9289
|
+
logger$10.error({
|
|
9177
9290
|
storyKey,
|
|
9178
9291
|
durationMs: dispatchResult.durationMs
|
|
9179
9292
|
}, "Dev-story dispatch timed out");
|
|
9180
|
-
if (dispatchResult.output.length > 0) logger$
|
|
9293
|
+
if (dispatchResult.output.length > 0) logger$10.info({
|
|
9181
9294
|
storyKey,
|
|
9182
9295
|
partialOutput: dispatchResult.output.slice(0, 500)
|
|
9183
9296
|
}, "Partial output before timeout");
|
|
@@ -9187,12 +9300,12 @@ async function runDevStory(deps, params) {
|
|
|
9187
9300
|
};
|
|
9188
9301
|
}
|
|
9189
9302
|
if (dispatchResult.status === "failed" || dispatchResult.exitCode !== 0) {
|
|
9190
|
-
logger$
|
|
9303
|
+
logger$10.error({
|
|
9191
9304
|
storyKey,
|
|
9192
9305
|
exitCode: dispatchResult.exitCode,
|
|
9193
9306
|
status: dispatchResult.status
|
|
9194
9307
|
}, "Dev-story dispatch failed");
|
|
9195
|
-
if (dispatchResult.output.length > 0) logger$
|
|
9308
|
+
if (dispatchResult.output.length > 0) logger$10.info({
|
|
9196
9309
|
storyKey,
|
|
9197
9310
|
partialOutput: dispatchResult.output.slice(0, 500)
|
|
9198
9311
|
}, "Partial output from failed dispatch");
|
|
@@ -9204,7 +9317,7 @@ async function runDevStory(deps, params) {
|
|
|
9204
9317
|
if (dispatchResult.parseError !== null || dispatchResult.parsed === null) {
|
|
9205
9318
|
const details = dispatchResult.parseError ?? "parsed result was null";
|
|
9206
9319
|
const rawSnippet = dispatchResult.output ? dispatchResult.output.slice(0, 1e3) : "(empty)";
|
|
9207
|
-
logger$
|
|
9320
|
+
logger$10.error({
|
|
9208
9321
|
storyKey,
|
|
9209
9322
|
parseError: details,
|
|
9210
9323
|
rawOutputSnippet: rawSnippet
|
|
@@ -9212,12 +9325,12 @@ async function runDevStory(deps, params) {
|
|
|
9212
9325
|
let filesModified = [];
|
|
9213
9326
|
try {
|
|
9214
9327
|
filesModified = await getGitChangedFiles(deps.projectRoot ?? process.cwd());
|
|
9215
|
-
if (filesModified.length > 0) logger$
|
|
9328
|
+
if (filesModified.length > 0) logger$10.info({
|
|
9216
9329
|
storyKey,
|
|
9217
9330
|
fileCount: filesModified.length
|
|
9218
9331
|
}, "Recovered files_modified from git status (YAML fallback)");
|
|
9219
9332
|
} catch (err) {
|
|
9220
|
-
logger$
|
|
9333
|
+
logger$10.warn({
|
|
9221
9334
|
storyKey,
|
|
9222
9335
|
error: err instanceof Error ? err.message : String(err)
|
|
9223
9336
|
}, "Failed to recover files_modified from git");
|
|
@@ -9234,7 +9347,7 @@ async function runDevStory(deps, params) {
|
|
|
9234
9347
|
};
|
|
9235
9348
|
}
|
|
9236
9349
|
const parsed = dispatchResult.parsed;
|
|
9237
|
-
logger$
|
|
9350
|
+
logger$10.info({
|
|
9238
9351
|
storyKey,
|
|
9239
9352
|
result: parsed.result,
|
|
9240
9353
|
acMet: parsed.ac_met.length
|
|
@@ -9373,7 +9486,7 @@ function extractFilesInScope(storyContent) {
|
|
|
9373
9486
|
|
|
9374
9487
|
//#endregion
|
|
9375
9488
|
//#region src/modules/compiled-workflows/code-review.ts
|
|
9376
|
-
const logger$
|
|
9489
|
+
const logger$9 = createLogger("compiled-workflows:code-review");
|
|
9377
9490
|
/**
|
|
9378
9491
|
* Hard token ceiling for the assembled code-review prompt (50,000 tokens).
|
|
9379
9492
|
* Quality reviews require seeing actual code diffs, not just file names.
|
|
@@ -9413,7 +9526,7 @@ function defaultFailResult(error, tokenUsage) {
|
|
|
9413
9526
|
async function runCodeReview(deps, params) {
|
|
9414
9527
|
const { storyKey, storyFilePath, workingDirectory, pipelineRunId, filesModified, previousIssues } = params;
|
|
9415
9528
|
const cwd = workingDirectory ?? process.cwd();
|
|
9416
|
-
logger$
|
|
9529
|
+
logger$9.debug({
|
|
9417
9530
|
storyKey,
|
|
9418
9531
|
storyFilePath,
|
|
9419
9532
|
cwd,
|
|
@@ -9424,7 +9537,7 @@ async function runCodeReview(deps, params) {
|
|
|
9424
9537
|
template = await deps.pack.getPrompt("code-review");
|
|
9425
9538
|
} catch (err) {
|
|
9426
9539
|
const error = err instanceof Error ? err.message : String(err);
|
|
9427
|
-
logger$
|
|
9540
|
+
logger$9.error({ error }, "Failed to retrieve code-review prompt template");
|
|
9428
9541
|
return defaultFailResult(`Failed to retrieve prompt template: ${error}`, {
|
|
9429
9542
|
input: 0,
|
|
9430
9543
|
output: 0
|
|
@@ -9435,7 +9548,7 @@ async function runCodeReview(deps, params) {
|
|
|
9435
9548
|
storyContent = await readFile$2(storyFilePath, "utf-8");
|
|
9436
9549
|
} catch (err) {
|
|
9437
9550
|
const error = err instanceof Error ? err.message : String(err);
|
|
9438
|
-
logger$
|
|
9551
|
+
logger$9.error({
|
|
9439
9552
|
storyFilePath,
|
|
9440
9553
|
error
|
|
9441
9554
|
}, "Failed to read story file");
|
|
@@ -9455,12 +9568,12 @@ async function runCodeReview(deps, params) {
|
|
|
9455
9568
|
const scopedTotal = nonDiffTokens + countTokens(scopedDiff);
|
|
9456
9569
|
if (scopedTotal <= TOKEN_CEILING) {
|
|
9457
9570
|
gitDiffContent = scopedDiff;
|
|
9458
|
-
logger$
|
|
9571
|
+
logger$9.debug({
|
|
9459
9572
|
fileCount: filesModified.length,
|
|
9460
9573
|
tokenCount: scopedTotal
|
|
9461
9574
|
}, "Using scoped file diff");
|
|
9462
9575
|
} else {
|
|
9463
|
-
logger$
|
|
9576
|
+
logger$9.warn({
|
|
9464
9577
|
estimatedTotal: scopedTotal,
|
|
9465
9578
|
ceiling: TOKEN_CEILING,
|
|
9466
9579
|
fileCount: filesModified.length
|
|
@@ -9474,7 +9587,7 @@ async function runCodeReview(deps, params) {
|
|
|
9474
9587
|
const fullTotal = nonDiffTokens + countTokens(fullDiff);
|
|
9475
9588
|
if (fullTotal <= TOKEN_CEILING) gitDiffContent = fullDiff;
|
|
9476
9589
|
else {
|
|
9477
|
-
logger$
|
|
9590
|
+
logger$9.warn({
|
|
9478
9591
|
estimatedTotal: fullTotal,
|
|
9479
9592
|
ceiling: TOKEN_CEILING
|
|
9480
9593
|
}, "Full git diff would exceed token ceiling — using stat-only summary");
|
|
@@ -9512,11 +9625,11 @@ async function runCodeReview(deps, params) {
|
|
|
9512
9625
|
}
|
|
9513
9626
|
];
|
|
9514
9627
|
const assembleResult = assemblePrompt(template, sections, TOKEN_CEILING);
|
|
9515
|
-
if (assembleResult.truncated) logger$
|
|
9628
|
+
if (assembleResult.truncated) logger$9.warn({
|
|
9516
9629
|
storyKey,
|
|
9517
9630
|
tokenCount: assembleResult.tokenCount
|
|
9518
9631
|
}, "Code-review prompt truncated to fit token ceiling");
|
|
9519
|
-
logger$
|
|
9632
|
+
logger$9.debug({
|
|
9520
9633
|
storyKey,
|
|
9521
9634
|
tokenCount: assembleResult.tokenCount,
|
|
9522
9635
|
truncated: assembleResult.truncated
|
|
@@ -9534,7 +9647,7 @@ async function runCodeReview(deps, params) {
|
|
|
9534
9647
|
dispatchResult = await handle.result;
|
|
9535
9648
|
} catch (err) {
|
|
9536
9649
|
const error = err instanceof Error ? err.message : String(err);
|
|
9537
|
-
logger$
|
|
9650
|
+
logger$9.error({
|
|
9538
9651
|
storyKey,
|
|
9539
9652
|
error
|
|
9540
9653
|
}, "Code-review dispatch threw unexpected error");
|
|
@@ -9550,7 +9663,7 @@ async function runCodeReview(deps, params) {
|
|
|
9550
9663
|
const rawOutput = dispatchResult.output ?? void 0;
|
|
9551
9664
|
if (dispatchResult.status === "failed") {
|
|
9552
9665
|
const errorMsg = `Dispatch status: failed. Exit code: ${dispatchResult.exitCode}. ${dispatchResult.parseError ?? ""} ${dispatchResult.output ? `Stderr: ${dispatchResult.output}` : ""}`.trim();
|
|
9553
|
-
logger$
|
|
9666
|
+
logger$9.warn({
|
|
9554
9667
|
storyKey,
|
|
9555
9668
|
exitCode: dispatchResult.exitCode
|
|
9556
9669
|
}, "Code-review dispatch failed");
|
|
@@ -9560,7 +9673,7 @@ async function runCodeReview(deps, params) {
|
|
|
9560
9673
|
};
|
|
9561
9674
|
}
|
|
9562
9675
|
if (dispatchResult.status === "timeout") {
|
|
9563
|
-
logger$
|
|
9676
|
+
logger$9.warn({ storyKey }, "Code-review dispatch timed out");
|
|
9564
9677
|
return {
|
|
9565
9678
|
...defaultFailResult("Dispatch status: timeout. The agent did not complete within the allowed time.", tokenUsage),
|
|
9566
9679
|
rawOutput
|
|
@@ -9568,7 +9681,7 @@ async function runCodeReview(deps, params) {
|
|
|
9568
9681
|
}
|
|
9569
9682
|
if (dispatchResult.parsed === null) {
|
|
9570
9683
|
const details = dispatchResult.parseError ?? "No YAML block found in output";
|
|
9571
|
-
logger$
|
|
9684
|
+
logger$9.warn({
|
|
9572
9685
|
storyKey,
|
|
9573
9686
|
details
|
|
9574
9687
|
}, "Code-review output schema validation failed");
|
|
@@ -9585,7 +9698,7 @@ async function runCodeReview(deps, params) {
|
|
|
9585
9698
|
const parseResult = CodeReviewResultSchema.safeParse(dispatchResult.parsed);
|
|
9586
9699
|
if (!parseResult.success) {
|
|
9587
9700
|
const details = parseResult.error.message;
|
|
9588
|
-
logger$
|
|
9701
|
+
logger$9.warn({
|
|
9589
9702
|
storyKey,
|
|
9590
9703
|
details
|
|
9591
9704
|
}, "Code-review output failed schema validation");
|
|
@@ -9600,13 +9713,13 @@ async function runCodeReview(deps, params) {
|
|
|
9600
9713
|
};
|
|
9601
9714
|
}
|
|
9602
9715
|
const parsed = parseResult.data;
|
|
9603
|
-
if (parsed.agentVerdict !== parsed.verdict) logger$
|
|
9716
|
+
if (parsed.agentVerdict !== parsed.verdict) logger$9.info({
|
|
9604
9717
|
storyKey,
|
|
9605
9718
|
agentVerdict: parsed.agentVerdict,
|
|
9606
9719
|
pipelineVerdict: parsed.verdict,
|
|
9607
9720
|
issues: parsed.issues
|
|
9608
9721
|
}, "Pipeline overrode agent verdict based on issue severities");
|
|
9609
|
-
logger$
|
|
9722
|
+
logger$9.info({
|
|
9610
9723
|
storyKey,
|
|
9611
9724
|
verdict: parsed.verdict,
|
|
9612
9725
|
issues: parsed.issues
|
|
@@ -9631,7 +9744,7 @@ function getArchConstraints(deps) {
|
|
|
9631
9744
|
if (constraints.length === 0) return "";
|
|
9632
9745
|
return constraints.map((d) => `${d.key}: ${d.value}`).join("\n");
|
|
9633
9746
|
} catch (err) {
|
|
9634
|
-
logger$
|
|
9747
|
+
logger$9.warn({ error: err instanceof Error ? err.message : String(err) }, "Failed to retrieve architecture constraints");
|
|
9635
9748
|
return "";
|
|
9636
9749
|
}
|
|
9637
9750
|
}
|
|
@@ -9963,7 +10076,7 @@ function detectConflictGroups(storyKeys, config) {
|
|
|
9963
10076
|
|
|
9964
10077
|
//#endregion
|
|
9965
10078
|
//#region src/modules/implementation-orchestrator/seed-methodology-context.ts
|
|
9966
|
-
const logger$
|
|
10079
|
+
const logger$8 = createLogger("implementation-orchestrator:seed");
|
|
9967
10080
|
/** Max chars for the architecture summary seeded into decisions */
|
|
9968
10081
|
const MAX_ARCH_CHARS = 6e3;
|
|
9969
10082
|
/** Max chars per epic shard */
|
|
@@ -9997,12 +10110,12 @@ function seedMethodologyContext(db, projectRoot) {
|
|
|
9997
10110
|
const testCount = seedTestPatterns(db, projectRoot);
|
|
9998
10111
|
if (testCount === -1) result.skippedCategories.push("test-patterns");
|
|
9999
10112
|
else result.decisionsCreated += testCount;
|
|
10000
|
-
logger$
|
|
10113
|
+
logger$8.info({
|
|
10001
10114
|
decisionsCreated: result.decisionsCreated,
|
|
10002
10115
|
skippedCategories: result.skippedCategories
|
|
10003
10116
|
}, "Methodology context seeding complete");
|
|
10004
10117
|
} catch (err) {
|
|
10005
|
-
logger$
|
|
10118
|
+
logger$8.warn({ error: err instanceof Error ? err.message : String(err) }, "Methodology context seeding failed (non-fatal)");
|
|
10006
10119
|
}
|
|
10007
10120
|
return result;
|
|
10008
10121
|
}
|
|
@@ -10046,7 +10159,7 @@ function seedArchitecture(db, projectRoot) {
|
|
|
10046
10159
|
});
|
|
10047
10160
|
count = 1;
|
|
10048
10161
|
}
|
|
10049
|
-
logger$
|
|
10162
|
+
logger$8.debug({ count }, "Seeded architecture decisions");
|
|
10050
10163
|
return count;
|
|
10051
10164
|
}
|
|
10052
10165
|
/**
|
|
@@ -10074,7 +10187,7 @@ function seedEpicShards(db, projectRoot) {
|
|
|
10074
10187
|
});
|
|
10075
10188
|
count++;
|
|
10076
10189
|
}
|
|
10077
|
-
logger$
|
|
10190
|
+
logger$8.debug({ count }, "Seeded epic shard decisions");
|
|
10078
10191
|
return count;
|
|
10079
10192
|
}
|
|
10080
10193
|
/**
|
|
@@ -10095,7 +10208,7 @@ function seedTestPatterns(db, projectRoot) {
|
|
|
10095
10208
|
value: patterns.slice(0, MAX_TEST_PATTERNS_CHARS),
|
|
10096
10209
|
rationale: "Detected from project configuration at orchestrator startup"
|
|
10097
10210
|
});
|
|
10098
|
-
logger$
|
|
10211
|
+
logger$8.debug("Seeded test patterns decision");
|
|
10099
10212
|
return 1;
|
|
10100
10213
|
}
|
|
10101
10214
|
/**
|
|
@@ -10287,7 +10400,7 @@ function createPauseGate() {
|
|
|
10287
10400
|
*/
|
|
10288
10401
|
function createImplementationOrchestrator(deps) {
|
|
10289
10402
|
const { db, pack, contextCompiler, dispatcher, eventBus, config, projectRoot } = deps;
|
|
10290
|
-
const logger$
|
|
10403
|
+
const logger$35 = createLogger("implementation-orchestrator");
|
|
10291
10404
|
let _state = "IDLE";
|
|
10292
10405
|
let _startedAt;
|
|
10293
10406
|
let _completedAt;
|
|
@@ -10299,6 +10412,56 @@ function createImplementationOrchestrator(deps) {
|
|
|
10299
10412
|
let _heartbeatTimer = null;
|
|
10300
10413
|
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
10301
10414
|
const WATCHDOG_TIMEOUT_MS = 6e5;
|
|
10415
|
+
const _phaseStartMs = new Map();
|
|
10416
|
+
const _phaseEndMs = new Map();
|
|
10417
|
+
const _storyDispatches = new Map();
|
|
10418
|
+
function startPhase(storyKey, phase) {
|
|
10419
|
+
if (!_phaseStartMs.has(storyKey)) _phaseStartMs.set(storyKey, new Map());
|
|
10420
|
+
_phaseStartMs.get(storyKey).set(phase, Date.now());
|
|
10421
|
+
}
|
|
10422
|
+
function endPhase(storyKey, phase) {
|
|
10423
|
+
if (!_phaseEndMs.has(storyKey)) _phaseEndMs.set(storyKey, new Map());
|
|
10424
|
+
_phaseEndMs.get(storyKey).set(phase, Date.now());
|
|
10425
|
+
}
|
|
10426
|
+
function incrementDispatches(storyKey) {
|
|
10427
|
+
_storyDispatches.set(storyKey, (_storyDispatches.get(storyKey) ?? 0) + 1);
|
|
10428
|
+
}
|
|
10429
|
+
function buildPhaseDurationsJson(storyKey) {
|
|
10430
|
+
const starts = _phaseStartMs.get(storyKey);
|
|
10431
|
+
const ends = _phaseEndMs.get(storyKey);
|
|
10432
|
+
if (!starts || starts.size === 0) return "{}";
|
|
10433
|
+
const durations = {};
|
|
10434
|
+
for (const [phase, startMs] of starts) {
|
|
10435
|
+
const endMs = ends?.get(phase) ?? Date.now();
|
|
10436
|
+
durations[phase] = Math.round((endMs - startMs) / 1e3);
|
|
10437
|
+
}
|
|
10438
|
+
return JSON.stringify(durations);
|
|
10439
|
+
}
|
|
10440
|
+
function writeStoryMetricsBestEffort(storyKey, result, reviewCycles) {
|
|
10441
|
+
if (config.pipelineRunId === void 0) return;
|
|
10442
|
+
try {
|
|
10443
|
+
const storyState = _stories.get(storyKey);
|
|
10444
|
+
const startedAt = storyState?.startedAt;
|
|
10445
|
+
const completedAt = storyState?.completedAt ?? new Date().toISOString();
|
|
10446
|
+
const wallClockSeconds = startedAt ? Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1e3) : 0;
|
|
10447
|
+
writeStoryMetrics(db, {
|
|
10448
|
+
run_id: config.pipelineRunId,
|
|
10449
|
+
story_key: storyKey,
|
|
10450
|
+
result,
|
|
10451
|
+
phase_durations_json: buildPhaseDurationsJson(storyKey),
|
|
10452
|
+
started_at: startedAt,
|
|
10453
|
+
completed_at: completedAt,
|
|
10454
|
+
wall_clock_seconds: wallClockSeconds,
|
|
10455
|
+
review_cycles: reviewCycles,
|
|
10456
|
+
dispatches: _storyDispatches.get(storyKey) ?? 0
|
|
10457
|
+
});
|
|
10458
|
+
} catch (err) {
|
|
10459
|
+
logger$35.warn({
|
|
10460
|
+
err,
|
|
10461
|
+
storyKey
|
|
10462
|
+
}, "Failed to write story metrics (best-effort)");
|
|
10463
|
+
}
|
|
10464
|
+
}
|
|
10302
10465
|
function getStatus() {
|
|
10303
10466
|
const stories = {};
|
|
10304
10467
|
for (const [key, s] of _stories) stories[key] = { ...s };
|
|
@@ -10328,7 +10491,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10328
10491
|
token_usage_json: serialized
|
|
10329
10492
|
});
|
|
10330
10493
|
} catch (err) {
|
|
10331
|
-
logger$
|
|
10494
|
+
logger$35.warn("Failed to persist orchestrator state", { err });
|
|
10332
10495
|
}
|
|
10333
10496
|
}
|
|
10334
10497
|
function recordProgress() {
|
|
@@ -10353,7 +10516,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10353
10516
|
const elapsed = Date.now() - _lastProgressTs;
|
|
10354
10517
|
if (elapsed >= WATCHDOG_TIMEOUT_MS) {
|
|
10355
10518
|
for (const [key, s] of _stories) if (s.phase !== "PENDING" && s.phase !== "COMPLETE" && s.phase !== "ESCALATED") {
|
|
10356
|
-
logger$
|
|
10519
|
+
logger$35.warn({
|
|
10357
10520
|
storyKey: key,
|
|
10358
10521
|
phase: s.phase,
|
|
10359
10522
|
elapsedMs: elapsed
|
|
@@ -10389,9 +10552,10 @@ function createImplementationOrchestrator(deps) {
|
|
|
10389
10552
|
* exhausted retries the story is ESCALATED.
|
|
10390
10553
|
*/
|
|
10391
10554
|
async function processStory(storyKey) {
|
|
10392
|
-
logger$
|
|
10555
|
+
logger$35.info("Processing story", { storyKey });
|
|
10393
10556
|
await waitIfPaused();
|
|
10394
10557
|
if (_state !== "RUNNING") return;
|
|
10558
|
+
startPhase(storyKey, "create-story");
|
|
10395
10559
|
updateStory(storyKey, {
|
|
10396
10560
|
phase: "IN_STORY_CREATION",
|
|
10397
10561
|
startedAt: new Date().toISOString()
|
|
@@ -10403,7 +10567,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10403
10567
|
const match = files.find((f) => f.startsWith(`${storyKey}-`) && f.endsWith(".md"));
|
|
10404
10568
|
if (match) {
|
|
10405
10569
|
storyFilePath = join$1(artifactsDir, match);
|
|
10406
|
-
logger$
|
|
10570
|
+
logger$35.info({
|
|
10407
10571
|
storyKey,
|
|
10408
10572
|
storyFilePath
|
|
10409
10573
|
}, "Found existing story file — skipping create-story");
|
|
@@ -10420,6 +10584,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10420
10584
|
}
|
|
10421
10585
|
} catch {}
|
|
10422
10586
|
if (storyFilePath === void 0) try {
|
|
10587
|
+
incrementDispatches(storyKey);
|
|
10423
10588
|
const createResult = await runCreateStory({
|
|
10424
10589
|
db,
|
|
10425
10590
|
pack,
|
|
@@ -10431,6 +10596,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10431
10596
|
storyKey,
|
|
10432
10597
|
pipelineRunId: config.pipelineRunId
|
|
10433
10598
|
});
|
|
10599
|
+
endPhase(storyKey, "create-story");
|
|
10434
10600
|
eventBus.emit("orchestrator:story-phase-complete", {
|
|
10435
10601
|
storyKey,
|
|
10436
10602
|
phase: "IN_STORY_CREATION",
|
|
@@ -10444,6 +10610,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10444
10610
|
error: errMsg,
|
|
10445
10611
|
completedAt: new Date().toISOString()
|
|
10446
10612
|
});
|
|
10613
|
+
writeStoryMetricsBestEffort(storyKey, "failed", 0);
|
|
10447
10614
|
eventBus.emit("orchestrator:story-escalated", {
|
|
10448
10615
|
storyKey,
|
|
10449
10616
|
lastVerdict: "create-story-failed",
|
|
@@ -10460,6 +10627,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10460
10627
|
error: errMsg,
|
|
10461
10628
|
completedAt: new Date().toISOString()
|
|
10462
10629
|
});
|
|
10630
|
+
writeStoryMetricsBestEffort(storyKey, "failed", 0);
|
|
10463
10631
|
eventBus.emit("orchestrator:story-escalated", {
|
|
10464
10632
|
storyKey,
|
|
10465
10633
|
lastVerdict: "create-story-no-file",
|
|
@@ -10472,11 +10640,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
10472
10640
|
storyFilePath = createResult.story_file;
|
|
10473
10641
|
} catch (err) {
|
|
10474
10642
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
10643
|
+
endPhase(storyKey, "create-story");
|
|
10475
10644
|
updateStory(storyKey, {
|
|
10476
10645
|
phase: "ESCALATED",
|
|
10477
10646
|
error: errMsg,
|
|
10478
10647
|
completedAt: new Date().toISOString()
|
|
10479
10648
|
});
|
|
10649
|
+
writeStoryMetricsBestEffort(storyKey, "failed", 0);
|
|
10480
10650
|
eventBus.emit("orchestrator:story-escalated", {
|
|
10481
10651
|
storyKey,
|
|
10482
10652
|
lastVerdict: "create-story-exception",
|
|
@@ -10488,6 +10658,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10488
10658
|
}
|
|
10489
10659
|
await waitIfPaused();
|
|
10490
10660
|
if (_state !== "RUNNING") return;
|
|
10661
|
+
startPhase(storyKey, "dev-story");
|
|
10491
10662
|
updateStory(storyKey, { phase: "IN_DEV" });
|
|
10492
10663
|
persistState();
|
|
10493
10664
|
let devFilesModified = [];
|
|
@@ -10497,7 +10668,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10497
10668
|
try {
|
|
10498
10669
|
storyContentForAnalysis = await readFile$2(storyFilePath ?? "", "utf-8");
|
|
10499
10670
|
} catch (err) {
|
|
10500
|
-
logger$
|
|
10671
|
+
logger$35.error({
|
|
10501
10672
|
storyKey,
|
|
10502
10673
|
storyFilePath,
|
|
10503
10674
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -10505,7 +10676,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10505
10676
|
}
|
|
10506
10677
|
const analysis = analyzeStoryComplexity(storyContentForAnalysis);
|
|
10507
10678
|
const batches = planTaskBatches(analysis);
|
|
10508
|
-
logger$
|
|
10679
|
+
logger$35.info({
|
|
10509
10680
|
storyKey,
|
|
10510
10681
|
estimatedScope: analysis.estimatedScope,
|
|
10511
10682
|
batchCount: batches.length,
|
|
@@ -10523,12 +10694,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
10523
10694
|
if (_state !== "RUNNING") break;
|
|
10524
10695
|
const taskScope = batch.taskIds.map((id, i) => `T${id}: ${batch.taskTitles[i] ?? ""}`).join("\n");
|
|
10525
10696
|
const priorFiles = allFilesModified.size > 0 ? Array.from(allFilesModified) : void 0;
|
|
10526
|
-
logger$
|
|
10697
|
+
logger$35.info({
|
|
10527
10698
|
storyKey,
|
|
10528
10699
|
batchIndex: batch.batchIndex,
|
|
10529
10700
|
taskCount: batch.taskIds.length
|
|
10530
10701
|
}, "Dispatching dev-story batch");
|
|
10531
10702
|
const batchStartMs = Date.now();
|
|
10703
|
+
incrementDispatches(storyKey);
|
|
10532
10704
|
let batchResult;
|
|
10533
10705
|
try {
|
|
10534
10706
|
batchResult = await runDevStory({
|
|
@@ -10546,7 +10718,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10546
10718
|
});
|
|
10547
10719
|
} catch (batchErr) {
|
|
10548
10720
|
const errMsg = batchErr instanceof Error ? batchErr.message : String(batchErr);
|
|
10549
|
-
logger$
|
|
10721
|
+
logger$35.warn({
|
|
10550
10722
|
storyKey,
|
|
10551
10723
|
batchIndex: batch.batchIndex,
|
|
10552
10724
|
error: errMsg
|
|
@@ -10566,7 +10738,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10566
10738
|
filesModified: batchFilesModified,
|
|
10567
10739
|
result: batchResult.result === "success" ? "success" : "failed"
|
|
10568
10740
|
};
|
|
10569
|
-
logger$
|
|
10741
|
+
logger$35.info(batchMetrics, "Batch dev-story metrics");
|
|
10570
10742
|
for (const f of batchFilesModified) allFilesModified.add(f);
|
|
10571
10743
|
if (batchFilesModified.length > 0) batchFileGroups.push({
|
|
10572
10744
|
batchIndex: batch.batchIndex,
|
|
@@ -10588,13 +10760,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
10588
10760
|
})
|
|
10589
10761
|
});
|
|
10590
10762
|
} catch (tokenErr) {
|
|
10591
|
-
logger$
|
|
10763
|
+
logger$35.warn({
|
|
10592
10764
|
storyKey,
|
|
10593
10765
|
batchIndex: batch.batchIndex,
|
|
10594
10766
|
err: tokenErr
|
|
10595
10767
|
}, "Failed to record batch token usage");
|
|
10596
10768
|
}
|
|
10597
|
-
if (batchResult.result === "failed") logger$
|
|
10769
|
+
if (batchResult.result === "failed") logger$35.warn({
|
|
10598
10770
|
storyKey,
|
|
10599
10771
|
batchIndex: batch.batchIndex,
|
|
10600
10772
|
error: batchResult.error
|
|
@@ -10608,6 +10780,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10608
10780
|
}
|
|
10609
10781
|
devFilesModified = Array.from(allFilesModified);
|
|
10610
10782
|
} else {
|
|
10783
|
+
incrementDispatches(storyKey);
|
|
10611
10784
|
const devResult = await runDevStory({
|
|
10612
10785
|
db,
|
|
10613
10786
|
pack,
|
|
@@ -10626,7 +10799,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10626
10799
|
result: devResult
|
|
10627
10800
|
});
|
|
10628
10801
|
persistState();
|
|
10629
|
-
if (devResult.result === "failed") logger$
|
|
10802
|
+
if (devResult.result === "failed") logger$35.warn("Dev-story reported failure, proceeding to code review", {
|
|
10630
10803
|
storyKey,
|
|
10631
10804
|
error: devResult.error,
|
|
10632
10805
|
filesModified: devFilesModified.length
|
|
@@ -10634,11 +10807,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
10634
10807
|
}
|
|
10635
10808
|
} catch (err) {
|
|
10636
10809
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
10810
|
+
endPhase(storyKey, "dev-story");
|
|
10637
10811
|
updateStory(storyKey, {
|
|
10638
10812
|
phase: "ESCALATED",
|
|
10639
10813
|
error: errMsg,
|
|
10640
10814
|
completedAt: new Date().toISOString()
|
|
10641
10815
|
});
|
|
10816
|
+
writeStoryMetricsBestEffort(storyKey, "failed", 0);
|
|
10642
10817
|
eventBus.emit("orchestrator:story-escalated", {
|
|
10643
10818
|
storyKey,
|
|
10644
10819
|
lastVerdict: "dev-story-exception",
|
|
@@ -10648,6 +10823,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10648
10823
|
persistState();
|
|
10649
10824
|
return;
|
|
10650
10825
|
}
|
|
10826
|
+
endPhase(storyKey, "dev-story");
|
|
10651
10827
|
let reviewCycles = 0;
|
|
10652
10828
|
let keepReviewing = true;
|
|
10653
10829
|
let timeoutRetried = false;
|
|
@@ -10655,6 +10831,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10655
10831
|
while (keepReviewing) {
|
|
10656
10832
|
await waitIfPaused();
|
|
10657
10833
|
if (_state !== "RUNNING") return;
|
|
10834
|
+
if (reviewCycles === 0) startPhase(storyKey, "code-review");
|
|
10658
10835
|
updateStory(storyKey, {
|
|
10659
10836
|
phase: "IN_REVIEW",
|
|
10660
10837
|
reviewCycles
|
|
@@ -10679,11 +10856,12 @@ function createImplementationOrchestrator(deps) {
|
|
|
10679
10856
|
"NEEDS_MAJOR_REWORK": 2
|
|
10680
10857
|
};
|
|
10681
10858
|
for (const group of batchFileGroups) {
|
|
10682
|
-
logger$
|
|
10859
|
+
logger$35.info({
|
|
10683
10860
|
storyKey,
|
|
10684
10861
|
batchIndex: group.batchIndex,
|
|
10685
10862
|
fileCount: group.files.length
|
|
10686
10863
|
}, "Running batched code review");
|
|
10864
|
+
incrementDispatches(storyKey);
|
|
10687
10865
|
const batchReview = await runCodeReview({
|
|
10688
10866
|
db,
|
|
10689
10867
|
pack,
|
|
@@ -10715,30 +10893,33 @@ function createImplementationOrchestrator(deps) {
|
|
|
10715
10893
|
rawOutput: lastRawOutput,
|
|
10716
10894
|
tokenUsage: aggregateTokens
|
|
10717
10895
|
};
|
|
10718
|
-
logger$
|
|
10896
|
+
logger$35.info({
|
|
10719
10897
|
storyKey,
|
|
10720
10898
|
batchCount: batchFileGroups.length,
|
|
10721
10899
|
verdict: worstVerdict,
|
|
10722
10900
|
issues: allIssues.length
|
|
10723
10901
|
}, "Batched code review complete — aggregate result");
|
|
10724
|
-
} else
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10902
|
+
} else {
|
|
10903
|
+
incrementDispatches(storyKey);
|
|
10904
|
+
reviewResult = await runCodeReview({
|
|
10905
|
+
db,
|
|
10906
|
+
pack,
|
|
10907
|
+
contextCompiler,
|
|
10908
|
+
dispatcher,
|
|
10909
|
+
projectRoot
|
|
10910
|
+
}, {
|
|
10911
|
+
storyKey,
|
|
10912
|
+
storyFilePath: storyFilePath ?? "",
|
|
10913
|
+
workingDirectory: projectRoot,
|
|
10914
|
+
pipelineRunId: config.pipelineRunId,
|
|
10915
|
+
filesModified: devFilesModified,
|
|
10916
|
+
...previousIssueList.length > 0 ? { previousIssues: previousIssueList } : {}
|
|
10917
|
+
});
|
|
10918
|
+
}
|
|
10738
10919
|
const isPhantomReview = reviewResult.verdict !== "SHIP_IT" && (reviewResult.issue_list === void 0 || reviewResult.issue_list.length === 0) && reviewResult.error !== void 0;
|
|
10739
10920
|
if (isPhantomReview && !timeoutRetried) {
|
|
10740
10921
|
timeoutRetried = true;
|
|
10741
|
-
logger$
|
|
10922
|
+
logger$35.warn({
|
|
10742
10923
|
storyKey,
|
|
10743
10924
|
reviewCycles,
|
|
10744
10925
|
error: reviewResult.error
|
|
@@ -10748,7 +10929,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10748
10929
|
verdict = reviewResult.verdict;
|
|
10749
10930
|
issueList = reviewResult.issue_list ?? [];
|
|
10750
10931
|
if (verdict === "NEEDS_MAJOR_REWORK" && reviewCycles > 0 && previousIssueList.length > 0 && issueList.length < previousIssueList.length) {
|
|
10751
|
-
logger$
|
|
10932
|
+
logger$35.info({
|
|
10752
10933
|
storyKey,
|
|
10753
10934
|
originalVerdict: verdict,
|
|
10754
10935
|
issuesBefore: previousIssueList.length,
|
|
@@ -10784,7 +10965,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10784
10965
|
if (_decomposition !== void 0) parts.push(`decomposed: ${_decomposition.batchCount} batches`);
|
|
10785
10966
|
parts.push(`${fileCount} files`);
|
|
10786
10967
|
parts.push(`${totalTokensK} tokens`);
|
|
10787
|
-
logger$
|
|
10968
|
+
logger$35.info({
|
|
10788
10969
|
storyKey,
|
|
10789
10970
|
verdict,
|
|
10790
10971
|
agentVerdict: reviewResult.agentVerdict
|
|
@@ -10792,11 +10973,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
10792
10973
|
}
|
|
10793
10974
|
} catch (err) {
|
|
10794
10975
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
10976
|
+
endPhase(storyKey, "code-review");
|
|
10795
10977
|
updateStory(storyKey, {
|
|
10796
10978
|
phase: "ESCALATED",
|
|
10797
10979
|
error: errMsg,
|
|
10798
10980
|
completedAt: new Date().toISOString()
|
|
10799
10981
|
});
|
|
10982
|
+
writeStoryMetricsBestEffort(storyKey, "failed", reviewCycles);
|
|
10800
10983
|
eventBus.emit("orchestrator:story-escalated", {
|
|
10801
10984
|
storyKey,
|
|
10802
10985
|
lastVerdict: "code-review-exception",
|
|
@@ -10807,10 +10990,12 @@ function createImplementationOrchestrator(deps) {
|
|
|
10807
10990
|
return;
|
|
10808
10991
|
}
|
|
10809
10992
|
if (verdict === "SHIP_IT") {
|
|
10993
|
+
endPhase(storyKey, "code-review");
|
|
10810
10994
|
updateStory(storyKey, {
|
|
10811
10995
|
phase: "COMPLETE",
|
|
10812
10996
|
completedAt: new Date().toISOString()
|
|
10813
10997
|
});
|
|
10998
|
+
writeStoryMetricsBestEffort(storyKey, "success", reviewCycles + 1);
|
|
10814
10999
|
eventBus.emit("orchestrator:story-complete", {
|
|
10815
11000
|
storyKey,
|
|
10816
11001
|
reviewCycles
|
|
@@ -10822,11 +11007,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
10822
11007
|
if (reviewCycles >= config.maxReviewCycles - 1) {
|
|
10823
11008
|
const finalReviewCycles = reviewCycles + 1;
|
|
10824
11009
|
if (verdict !== "NEEDS_MINOR_FIXES") {
|
|
11010
|
+
endPhase(storyKey, "code-review");
|
|
10825
11011
|
updateStory(storyKey, {
|
|
10826
11012
|
phase: "ESCALATED",
|
|
10827
11013
|
reviewCycles: finalReviewCycles,
|
|
10828
11014
|
completedAt: new Date().toISOString()
|
|
10829
11015
|
});
|
|
11016
|
+
writeStoryMetricsBestEffort(storyKey, "escalated", finalReviewCycles);
|
|
10830
11017
|
eventBus.emit("orchestrator:story-escalated", {
|
|
10831
11018
|
storyKey,
|
|
10832
11019
|
lastVerdict: verdict,
|
|
@@ -10836,7 +11023,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10836
11023
|
persistState();
|
|
10837
11024
|
return;
|
|
10838
11025
|
}
|
|
10839
|
-
logger$
|
|
11026
|
+
logger$35.info({
|
|
10840
11027
|
storyKey,
|
|
10841
11028
|
reviewCycles: finalReviewCycles,
|
|
10842
11029
|
issueCount: issueList.length
|
|
@@ -10886,7 +11073,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10886
11073
|
fixPrompt = assembled.prompt;
|
|
10887
11074
|
} catch {
|
|
10888
11075
|
fixPrompt = `Fix story ${storyKey}: verdict=${verdict}, minor fixes needed`;
|
|
10889
|
-
logger$
|
|
11076
|
+
logger$35.warn("Failed to assemble auto-approve fix prompt, using fallback", { storyKey });
|
|
10890
11077
|
}
|
|
10891
11078
|
const handle = dispatcher.dispatch({
|
|
10892
11079
|
prompt: fixPrompt,
|
|
@@ -10903,18 +11090,20 @@ function createImplementationOrchestrator(deps) {
|
|
|
10903
11090
|
output: fixResult.tokenEstimate.output
|
|
10904
11091
|
} : void 0 }
|
|
10905
11092
|
});
|
|
10906
|
-
if (fixResult.status === "timeout") logger$
|
|
11093
|
+
if (fixResult.status === "timeout") logger$35.warn("Auto-approve fix timed out — approving anyway (issues were minor)", { storyKey });
|
|
10907
11094
|
} catch (err) {
|
|
10908
|
-
logger$
|
|
11095
|
+
logger$35.warn("Auto-approve fix dispatch failed — approving anyway (issues were minor)", {
|
|
10909
11096
|
storyKey,
|
|
10910
11097
|
err
|
|
10911
11098
|
});
|
|
10912
11099
|
}
|
|
11100
|
+
endPhase(storyKey, "code-review");
|
|
10913
11101
|
updateStory(storyKey, {
|
|
10914
11102
|
phase: "COMPLETE",
|
|
10915
11103
|
reviewCycles: finalReviewCycles,
|
|
10916
11104
|
completedAt: new Date().toISOString()
|
|
10917
11105
|
});
|
|
11106
|
+
writeStoryMetricsBestEffort(storyKey, "success", finalReviewCycles);
|
|
10918
11107
|
eventBus.emit("orchestrator:story-complete", {
|
|
10919
11108
|
storyKey,
|
|
10920
11109
|
reviewCycles: finalReviewCycles
|
|
@@ -10975,11 +11164,12 @@ function createImplementationOrchestrator(deps) {
|
|
|
10975
11164
|
fixPrompt = assembled.prompt;
|
|
10976
11165
|
} catch {
|
|
10977
11166
|
fixPrompt = `Fix story ${storyKey}: verdict=${verdict}, taskType=${taskType}`;
|
|
10978
|
-
logger$
|
|
11167
|
+
logger$35.warn("Failed to assemble fix prompt, using fallback", {
|
|
10979
11168
|
storyKey,
|
|
10980
11169
|
taskType
|
|
10981
11170
|
});
|
|
10982
11171
|
}
|
|
11172
|
+
incrementDispatches(storyKey);
|
|
10983
11173
|
const handle = dispatcher.dispatch({
|
|
10984
11174
|
prompt: fixPrompt,
|
|
10985
11175
|
agent: "claude-code",
|
|
@@ -10997,15 +11187,17 @@ function createImplementationOrchestrator(deps) {
|
|
|
10997
11187
|
} : void 0 }
|
|
10998
11188
|
});
|
|
10999
11189
|
if (fixResult.status === "timeout") {
|
|
11000
|
-
logger$
|
|
11190
|
+
logger$35.warn("Fix dispatch timed out — escalating story", {
|
|
11001
11191
|
storyKey,
|
|
11002
11192
|
taskType
|
|
11003
11193
|
});
|
|
11194
|
+
endPhase(storyKey, "code-review");
|
|
11004
11195
|
updateStory(storyKey, {
|
|
11005
11196
|
phase: "ESCALATED",
|
|
11006
11197
|
error: `fix-dispatch-timeout (${taskType})`,
|
|
11007
11198
|
completedAt: new Date().toISOString()
|
|
11008
11199
|
});
|
|
11200
|
+
writeStoryMetricsBestEffort(storyKey, "escalated", reviewCycles + 1);
|
|
11009
11201
|
eventBus.emit("orchestrator:story-escalated", {
|
|
11010
11202
|
storyKey,
|
|
11011
11203
|
lastVerdict: verdict,
|
|
@@ -11015,13 +11207,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
11015
11207
|
persistState();
|
|
11016
11208
|
return;
|
|
11017
11209
|
}
|
|
11018
|
-
if (fixResult.status === "failed") logger$
|
|
11210
|
+
if (fixResult.status === "failed") logger$35.warn("Fix dispatch failed", {
|
|
11019
11211
|
storyKey,
|
|
11020
11212
|
taskType,
|
|
11021
11213
|
exitCode: fixResult.exitCode
|
|
11022
11214
|
});
|
|
11023
11215
|
} catch (err) {
|
|
11024
|
-
logger$
|
|
11216
|
+
logger$35.warn("Fix dispatch failed, continuing to next review", {
|
|
11025
11217
|
storyKey,
|
|
11026
11218
|
taskType,
|
|
11027
11219
|
err
|
|
@@ -11074,11 +11266,11 @@ function createImplementationOrchestrator(deps) {
|
|
|
11074
11266
|
}
|
|
11075
11267
|
async function run(storyKeys) {
|
|
11076
11268
|
if (_state === "RUNNING" || _state === "PAUSED") {
|
|
11077
|
-
logger$
|
|
11269
|
+
logger$35.warn("run() called while orchestrator is already running or paused — ignoring", { state: _state });
|
|
11078
11270
|
return getStatus();
|
|
11079
11271
|
}
|
|
11080
11272
|
if (_state === "COMPLETE") {
|
|
11081
|
-
logger$
|
|
11273
|
+
logger$35.warn("run() called on a COMPLETE orchestrator — ignoring", { state: _state });
|
|
11082
11274
|
return getStatus();
|
|
11083
11275
|
}
|
|
11084
11276
|
_state = "RUNNING";
|
|
@@ -11096,13 +11288,13 @@ function createImplementationOrchestrator(deps) {
|
|
|
11096
11288
|
startHeartbeat();
|
|
11097
11289
|
if (projectRoot !== void 0) {
|
|
11098
11290
|
const seedResult = seedMethodologyContext(db, projectRoot);
|
|
11099
|
-
if (seedResult.decisionsCreated > 0) logger$
|
|
11291
|
+
if (seedResult.decisionsCreated > 0) logger$35.info({
|
|
11100
11292
|
decisionsCreated: seedResult.decisionsCreated,
|
|
11101
11293
|
skippedCategories: seedResult.skippedCategories
|
|
11102
11294
|
}, "Methodology context seeded from planning artifacts");
|
|
11103
11295
|
}
|
|
11104
11296
|
const groups = detectConflictGroups(storyKeys);
|
|
11105
|
-
logger$
|
|
11297
|
+
logger$35.info("Orchestrator starting", {
|
|
11106
11298
|
storyCount: storyKeys.length,
|
|
11107
11299
|
groupCount: groups.length,
|
|
11108
11300
|
maxConcurrency: config.maxConcurrency
|
|
@@ -11114,7 +11306,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
11114
11306
|
_state = "FAILED";
|
|
11115
11307
|
_completedAt = new Date().toISOString();
|
|
11116
11308
|
persistState();
|
|
11117
|
-
logger$
|
|
11309
|
+
logger$35.error("Orchestrator failed with unhandled error", { err });
|
|
11118
11310
|
return getStatus();
|
|
11119
11311
|
}
|
|
11120
11312
|
stopHeartbeat();
|
|
@@ -11141,7 +11333,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
11141
11333
|
_pauseGate = createPauseGate();
|
|
11142
11334
|
_state = "PAUSED";
|
|
11143
11335
|
eventBus.emit("orchestrator:paused", {});
|
|
11144
|
-
logger$
|
|
11336
|
+
logger$35.info("Orchestrator paused");
|
|
11145
11337
|
}
|
|
11146
11338
|
function resume() {
|
|
11147
11339
|
if (_state !== "PAUSED") return;
|
|
@@ -11152,7 +11344,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
11152
11344
|
}
|
|
11153
11345
|
_state = "RUNNING";
|
|
11154
11346
|
eventBus.emit("orchestrator:resumed", {});
|
|
11155
|
-
logger$
|
|
11347
|
+
logger$35.info("Orchestrator resumed");
|
|
11156
11348
|
}
|
|
11157
11349
|
return {
|
|
11158
11350
|
run,
|
|
@@ -11330,6 +11522,31 @@ function createPlanningPhaseDefinition() {
|
|
|
11330
11522
|
};
|
|
11331
11523
|
}
|
|
11332
11524
|
/**
|
|
11525
|
+
* Create the UX Design phase definition.
|
|
11526
|
+
*
|
|
11527
|
+
* Entry gates: 'prd' artifact from planning must exist
|
|
11528
|
+
* Exit gates: 'ux-design' artifact must exist for this run
|
|
11529
|
+
*
|
|
11530
|
+
* This phase is inserted between planning and solutioning when UX design is
|
|
11531
|
+
* enabled in the pack manifest (`uxDesign: true`).
|
|
11532
|
+
*/
|
|
11533
|
+
function createUxDesignPhaseDefinition() {
|
|
11534
|
+
return {
|
|
11535
|
+
name: "ux-design",
|
|
11536
|
+
description: "Design the user experience: personas, core experience vision, design system, visual foundation, user journeys, and accessibility guidelines.",
|
|
11537
|
+
entryGates: [createArtifactExistsGate("planning", "prd")],
|
|
11538
|
+
exitGates: [createArtifactExistsGate("ux-design", "ux-design")],
|
|
11539
|
+
onEnter: async (_db, runId) => {
|
|
11540
|
+
logPhase(`UX Design phase starting for run ${runId}`);
|
|
11541
|
+
},
|
|
11542
|
+
onExit: async (db, runId) => {
|
|
11543
|
+
const artifact = getArtifactByTypeForRun(db, runId, "ux-design", "ux-design");
|
|
11544
|
+
if (artifact === void 0) logPhase(`UX Design phase exit WARNING: ux-design artifact not found for run ${runId}`);
|
|
11545
|
+
else logPhase(`UX Design phase completed for run ${runId} — ux-design artifact registered: ${artifact.id}`);
|
|
11546
|
+
}
|
|
11547
|
+
};
|
|
11548
|
+
}
|
|
11549
|
+
/**
|
|
11333
11550
|
* Create the Solutioning phase definition.
|
|
11334
11551
|
*
|
|
11335
11552
|
* Entry gates: 'prd' artifact from planning must exist
|
|
@@ -11383,15 +11600,19 @@ function createImplementationPhaseDefinition() {
|
|
|
11383
11600
|
};
|
|
11384
11601
|
}
|
|
11385
11602
|
/**
|
|
11386
|
-
* Return
|
|
11603
|
+
* Return the built-in phase definitions in execution order.
|
|
11604
|
+
*
|
|
11605
|
+
* When `uxDesignEnabled` is true, the `ux-design` phase is inserted between
|
|
11606
|
+
* `planning` and `solutioning`, with its own entry/exit gates.
|
|
11607
|
+
*
|
|
11608
|
+
* @param config - Optional configuration for conditional phase inclusion
|
|
11387
11609
|
*/
|
|
11388
|
-
function createBuiltInPhases() {
|
|
11389
|
-
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
];
|
|
11610
|
+
function createBuiltInPhases(config) {
|
|
11611
|
+
const phases = [createAnalysisPhaseDefinition(), createPlanningPhaseDefinition()];
|
|
11612
|
+
if (config?.uxDesignEnabled === true) phases.push(createUxDesignPhaseDefinition());
|
|
11613
|
+
phases.push(createSolutioningPhaseDefinition());
|
|
11614
|
+
phases.push(createImplementationPhaseDefinition());
|
|
11615
|
+
return phases;
|
|
11395
11616
|
}
|
|
11396
11617
|
|
|
11397
11618
|
//#endregion
|
|
@@ -11454,7 +11675,8 @@ var PhaseOrchestratorImpl = class {
|
|
|
11454
11675
|
this._db = deps.db;
|
|
11455
11676
|
this._pack = deps.pack;
|
|
11456
11677
|
this._qualityGates = deps.qualityGates;
|
|
11457
|
-
this.
|
|
11678
|
+
const uxDesignEnabled = this._pack.manifest.uxDesign === true;
|
|
11679
|
+
this._phases = createBuiltInPhases({ uxDesignEnabled });
|
|
11458
11680
|
const builtInNames = new Set(this._phases.map((p) => p.name));
|
|
11459
11681
|
const packPhases = this._pack.getPhases();
|
|
11460
11682
|
for (const packPhase of packPhases) if (!builtInNames.has(packPhase.name)) this._phases.push({
|
|
@@ -11610,12 +11832,44 @@ var PhaseOrchestratorImpl = class {
|
|
|
11610
11832
|
getPhases() {
|
|
11611
11833
|
return [...this._phases];
|
|
11612
11834
|
}
|
|
11835
|
+
markPhaseFailed(runId, phase, reason) {
|
|
11836
|
+
updatePipelineRun(this._db, runId, { status: "failed" });
|
|
11837
|
+
const run = getPipelineRunById(this._db, runId);
|
|
11838
|
+
if (!run) return;
|
|
11839
|
+
const config = parseConfigJson(run.config_json);
|
|
11840
|
+
const history = config.phaseHistory;
|
|
11841
|
+
const currentEntry = history.find((h) => h.phase === phase && !h.completedAt);
|
|
11842
|
+
if (currentEntry) {
|
|
11843
|
+
currentEntry.completedAt = new Date().toISOString();
|
|
11844
|
+
currentEntry.gateResults = [{
|
|
11845
|
+
gate: "sub-phase-execution",
|
|
11846
|
+
passed: false,
|
|
11847
|
+
error: reason
|
|
11848
|
+
}];
|
|
11849
|
+
} else history.push({
|
|
11850
|
+
phase,
|
|
11851
|
+
startedAt: new Date().toISOString(),
|
|
11852
|
+
completedAt: new Date().toISOString(),
|
|
11853
|
+
gateResults: [{
|
|
11854
|
+
gate: "sub-phase-execution",
|
|
11855
|
+
passed: false,
|
|
11856
|
+
error: reason
|
|
11857
|
+
}]
|
|
11858
|
+
});
|
|
11859
|
+
const newConfigJson = JSON.stringify({
|
|
11860
|
+
...config,
|
|
11861
|
+
phaseHistory: history
|
|
11862
|
+
});
|
|
11863
|
+
updatePipelineRunConfig(this._db, runId, newConfigJson);
|
|
11864
|
+
}
|
|
11613
11865
|
};
|
|
11614
11866
|
/**
|
|
11615
11867
|
* Create a new PhaseOrchestrator with the given dependencies.
|
|
11616
11868
|
*
|
|
11617
|
-
* The orchestrator is pre-loaded with the
|
|
11618
|
-
* (analysis, planning, solutioning, implementation).
|
|
11869
|
+
* The orchestrator is pre-loaded with the built-in phases
|
|
11870
|
+
* (analysis, planning, [ux-design,] solutioning, implementation).
|
|
11871
|
+
* The optional ux-design phase is inserted between planning and solutioning
|
|
11872
|
+
* when `pack.manifest.uxDesign === true`.
|
|
11619
11873
|
*/
|
|
11620
11874
|
function createPhaseOrchestrator(deps) {
|
|
11621
11875
|
return new PhaseOrchestratorImpl(deps);
|
|
@@ -11698,334 +11952,479 @@ function summarizeDecisions(decisions, maxChars) {
|
|
|
11698
11952
|
}
|
|
11699
11953
|
|
|
11700
11954
|
//#endregion
|
|
11701
|
-
//#region src/modules/phase-orchestrator/
|
|
11702
|
-
const logger$5 = createLogger("step-runner");
|
|
11955
|
+
//#region src/modules/phase-orchestrator/schemas/critique-output.ts
|
|
11703
11956
|
/**
|
|
11704
|
-
*
|
|
11705
|
-
*
|
|
11706
|
-
* @param decisions - Decision records from the store
|
|
11707
|
-
* @param sectionTitle - Title for the markdown section
|
|
11708
|
-
* @returns Formatted markdown string
|
|
11957
|
+
* A single issue identified by the critique agent.
|
|
11709
11958
|
*/
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11959
|
+
const CritiqueIssueSchema = z.object({
|
|
11960
|
+
severity: z.enum([
|
|
11961
|
+
"blocker",
|
|
11962
|
+
"major",
|
|
11963
|
+
"minor"
|
|
11964
|
+
]),
|
|
11965
|
+
category: z.string().min(1),
|
|
11966
|
+
description: z.string().min(5),
|
|
11967
|
+
suggestion: z.string().min(5)
|
|
11968
|
+
});
|
|
11969
|
+
/**
|
|
11970
|
+
* Full output schema for critique agent responses.
|
|
11971
|
+
*
|
|
11972
|
+
* The critique agent must emit a YAML block matching this schema.
|
|
11973
|
+
* - `pass` means the artifact meets quality standards with no blocking issues.
|
|
11974
|
+
* - `needs_work` means one or more issues must be addressed before the artifact
|
|
11975
|
+
* can be considered complete.
|
|
11976
|
+
*/
|
|
11977
|
+
const CritiqueOutputSchema = z.object({
|
|
11978
|
+
verdict: z.enum(["pass", "needs_work"]),
|
|
11979
|
+
issue_count: z.number().int().min(0),
|
|
11980
|
+
issues: z.array(CritiqueIssueSchema).default([])
|
|
11981
|
+
});
|
|
11982
|
+
|
|
11983
|
+
//#endregion
|
|
11984
|
+
//#region src/modules/phase-orchestrator/critique-loop.ts
|
|
11985
|
+
const logger$7 = createLogger("critique-loop");
|
|
11986
|
+
/**
|
|
11987
|
+
* Maps a phase name to the critique prompt template name.
|
|
11988
|
+
* Falls back to `critique-${phase}` for unknown phases.
|
|
11989
|
+
*/
|
|
11990
|
+
function getCritiquePromptName(phase) {
|
|
11991
|
+
const mapping = {
|
|
11992
|
+
analysis: "critique-analysis",
|
|
11993
|
+
planning: "critique-planning",
|
|
11994
|
+
solutioning: "critique-architecture",
|
|
11995
|
+
architecture: "critique-architecture",
|
|
11996
|
+
stories: "critique-stories"
|
|
11997
|
+
};
|
|
11998
|
+
return mapping[phase] ?? `critique-${phase}`;
|
|
11999
|
+
}
|
|
12000
|
+
/**
|
|
12001
|
+
* Execute a critique-and-refine loop on a phase artifact.
|
|
12002
|
+
*
|
|
12003
|
+
* Dispatches a critique agent, checks the verdict, and if the artifact
|
|
12004
|
+
* needs work, dispatches a refinement agent and repeats up to maxIterations.
|
|
12005
|
+
* After each critique, stores the result in the decision store under category 'critique'.
|
|
12006
|
+
*
|
|
12007
|
+
* @param artifact - The artifact content (raw text/YAML/markdown) to critique
|
|
12008
|
+
* @param phaseId - Phase name used to select the critique prompt template
|
|
12009
|
+
* @param runId - Pipeline run ID for decision store scoping
|
|
12010
|
+
* @param phase - Phase name for decision store persistence
|
|
12011
|
+
* @param deps - Shared phase dependencies (db, pack, dispatcher)
|
|
12012
|
+
* @param options - Critique loop configuration options
|
|
12013
|
+
* @returns CritiqueLoopResult with verdict, token costs, and timing
|
|
12014
|
+
*/
|
|
12015
|
+
async function runCritiqueLoop(artifact, phaseId, runId, phase, deps, options = {}) {
|
|
12016
|
+
const { maxIterations = 2, projectContext = "", phaseContext = "" } = options;
|
|
12017
|
+
const startMs = Date.now();
|
|
12018
|
+
const critiqueTokens = {
|
|
12019
|
+
input: 0,
|
|
12020
|
+
output: 0
|
|
12021
|
+
};
|
|
12022
|
+
const refinementTokens = {
|
|
12023
|
+
input: 0,
|
|
12024
|
+
output: 0
|
|
12025
|
+
};
|
|
12026
|
+
let iterations = 0;
|
|
12027
|
+
let currentArtifact = artifact;
|
|
12028
|
+
let lastCritiqueOutput = null;
|
|
12029
|
+
const critiquePromptName = getCritiquePromptName(phaseId);
|
|
12030
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
12031
|
+
iterations = i + 1;
|
|
12032
|
+
let critiquePrompt;
|
|
11716
12033
|
try {
|
|
11717
|
-
const
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
12034
|
+
const critiqueTemplate = await deps.pack.getPrompt(critiquePromptName);
|
|
12035
|
+
critiquePrompt = critiqueTemplate.replace("{{artifact_content}}", currentArtifact).replace("{{project_context}}", projectContext);
|
|
12036
|
+
} catch (err) {
|
|
12037
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12038
|
+
logger$7.warn({
|
|
12039
|
+
phaseId,
|
|
12040
|
+
promptName: critiquePromptName,
|
|
12041
|
+
err: message
|
|
12042
|
+
}, "Critique loop: failed to load critique prompt template — skipping critique");
|
|
12043
|
+
return {
|
|
12044
|
+
verdict: "pass",
|
|
12045
|
+
iterations,
|
|
12046
|
+
remainingIssues: [],
|
|
12047
|
+
critiqueTokens,
|
|
12048
|
+
refinementTokens,
|
|
12049
|
+
totalMs: Date.now() - startMs,
|
|
12050
|
+
error: `Failed to load critique prompt '${critiquePromptName}': ${message}`
|
|
12051
|
+
};
|
|
12052
|
+
}
|
|
12053
|
+
let critiqueOutput;
|
|
12054
|
+
try {
|
|
12055
|
+
const handle = deps.dispatcher.dispatch({
|
|
12056
|
+
prompt: critiquePrompt,
|
|
12057
|
+
agent: "claude-code",
|
|
12058
|
+
taskType: "critique",
|
|
12059
|
+
outputSchema: CritiqueOutputSchema
|
|
12060
|
+
});
|
|
12061
|
+
const result = await handle.result;
|
|
12062
|
+
critiqueTokens.input += result.tokenEstimate.input;
|
|
12063
|
+
critiqueTokens.output += result.tokenEstimate.output;
|
|
12064
|
+
if (result.status !== "completed" || result.parsed === null) {
|
|
12065
|
+
const errMsg = result.parseError ?? `Critique dispatch ended with status '${result.status}'`;
|
|
12066
|
+
logger$7.warn({
|
|
12067
|
+
phaseId,
|
|
12068
|
+
iteration: i + 1,
|
|
12069
|
+
err: errMsg
|
|
12070
|
+
}, "Critique loop: critique dispatch failed — treating as pass to avoid blocking pipeline");
|
|
12071
|
+
return {
|
|
12072
|
+
verdict: "pass",
|
|
12073
|
+
iterations,
|
|
12074
|
+
remainingIssues: [],
|
|
12075
|
+
critiqueTokens,
|
|
12076
|
+
refinementTokens,
|
|
12077
|
+
totalMs: Date.now() - startMs,
|
|
12078
|
+
error: errMsg
|
|
12079
|
+
};
|
|
12080
|
+
}
|
|
12081
|
+
critiqueOutput = result.parsed;
|
|
12082
|
+
lastCritiqueOutput = critiqueOutput;
|
|
12083
|
+
} catch (err) {
|
|
12084
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12085
|
+
logger$7.warn({
|
|
12086
|
+
phaseId,
|
|
12087
|
+
iteration: i + 1,
|
|
12088
|
+
err: message
|
|
12089
|
+
}, "Critique loop: critique dispatch threw — treating as pass to avoid blocking pipeline");
|
|
12090
|
+
return {
|
|
12091
|
+
verdict: "pass",
|
|
12092
|
+
iterations,
|
|
12093
|
+
remainingIssues: [],
|
|
12094
|
+
critiqueTokens,
|
|
12095
|
+
refinementTokens,
|
|
12096
|
+
totalMs: Date.now() - startMs,
|
|
12097
|
+
error: message
|
|
12098
|
+
};
|
|
12099
|
+
}
|
|
12100
|
+
try {
|
|
12101
|
+
upsertDecision(deps.db, {
|
|
12102
|
+
pipeline_run_id: runId,
|
|
12103
|
+
phase,
|
|
12104
|
+
category: "critique",
|
|
12105
|
+
key: `${phaseId}-iteration-${i + 1}-verdict`,
|
|
12106
|
+
value: critiqueOutput.verdict,
|
|
12107
|
+
rationale: `Critique loop iteration ${i + 1} of ${maxIterations}`
|
|
12108
|
+
});
|
|
12109
|
+
upsertDecision(deps.db, {
|
|
12110
|
+
pipeline_run_id: runId,
|
|
12111
|
+
phase,
|
|
12112
|
+
category: "critique",
|
|
12113
|
+
key: `${phaseId}-iteration-${i + 1}-issue_count`,
|
|
12114
|
+
value: String(critiqueOutput.issue_count)
|
|
12115
|
+
});
|
|
12116
|
+
if (critiqueOutput.issues.length > 0) upsertDecision(deps.db, {
|
|
12117
|
+
pipeline_run_id: runId,
|
|
12118
|
+
phase,
|
|
12119
|
+
category: "critique",
|
|
12120
|
+
key: `${phaseId}-iteration-${i + 1}-issues`,
|
|
12121
|
+
value: JSON.stringify(critiqueOutput.issues)
|
|
12122
|
+
});
|
|
12123
|
+
} catch (err) {
|
|
12124
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12125
|
+
logger$7.warn({
|
|
12126
|
+
phaseId,
|
|
12127
|
+
iteration: i + 1,
|
|
12128
|
+
err: message
|
|
12129
|
+
}, "Critique loop: failed to store critique decision — continuing");
|
|
12130
|
+
}
|
|
12131
|
+
if (critiqueOutput.verdict === "pass") {
|
|
12132
|
+
logger$7.info({
|
|
12133
|
+
phaseId,
|
|
12134
|
+
iteration: i + 1
|
|
12135
|
+
}, "Critique loop: artifact passed critique — loop complete");
|
|
12136
|
+
return {
|
|
12137
|
+
verdict: "pass",
|
|
12138
|
+
iterations,
|
|
12139
|
+
remainingIssues: [],
|
|
12140
|
+
critiqueTokens,
|
|
12141
|
+
refinementTokens,
|
|
12142
|
+
totalMs: Date.now() - startMs
|
|
12143
|
+
};
|
|
12144
|
+
}
|
|
12145
|
+
logger$7.info({
|
|
12146
|
+
phaseId,
|
|
12147
|
+
iteration: i + 1,
|
|
12148
|
+
issueCount: critiqueOutput.issue_count
|
|
12149
|
+
}, "Critique loop: artifact needs work — dispatching refinement");
|
|
12150
|
+
if (i < maxIterations - 1) {
|
|
12151
|
+
let refinePrompt;
|
|
12152
|
+
try {
|
|
12153
|
+
const refineTemplate = await deps.pack.getPrompt("refine-artifact");
|
|
12154
|
+
const issuesText = critiqueOutput.issues.map((issue) => `- [${issue.severity}] ${issue.category}: ${issue.description}\n Suggestion: ${issue.suggestion}`).join("\n");
|
|
12155
|
+
refinePrompt = refineTemplate.replace("{{original_artifact}}", currentArtifact).replace("{{critique_issues}}", issuesText).replace("{{phase_context}}", phaseContext);
|
|
12156
|
+
} catch (err) {
|
|
12157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12158
|
+
logger$7.warn({
|
|
12159
|
+
phaseId,
|
|
12160
|
+
iteration: i + 1,
|
|
12161
|
+
err: message
|
|
12162
|
+
}, "Critique loop: failed to load refinement prompt — stopping loop");
|
|
12163
|
+
break;
|
|
12164
|
+
}
|
|
12165
|
+
try {
|
|
12166
|
+
const refineHandle = deps.dispatcher.dispatch({
|
|
12167
|
+
prompt: refinePrompt,
|
|
12168
|
+
agent: "claude-code",
|
|
12169
|
+
taskType: "critique",
|
|
12170
|
+
outputSchema: void 0
|
|
12171
|
+
});
|
|
12172
|
+
const refineResult = await refineHandle.result;
|
|
12173
|
+
refinementTokens.input += refineResult.tokenEstimate.input;
|
|
12174
|
+
refinementTokens.output += refineResult.tokenEstimate.output;
|
|
12175
|
+
if (refineResult.status === "completed" && refineResult.output) {
|
|
12176
|
+
const originalLength = currentArtifact.length;
|
|
12177
|
+
const refinedLength = refineResult.output.length;
|
|
12178
|
+
const delta = refinedLength - originalLength;
|
|
12179
|
+
logger$7.info({
|
|
12180
|
+
phaseId,
|
|
12181
|
+
iteration: i + 1,
|
|
12182
|
+
originalLength,
|
|
12183
|
+
refinedLength,
|
|
12184
|
+
delta
|
|
12185
|
+
}, "Critique loop: refinement complete");
|
|
12186
|
+
currentArtifact = refineResult.output;
|
|
12187
|
+
} else {
|
|
12188
|
+
logger$7.warn({
|
|
12189
|
+
phaseId,
|
|
12190
|
+
iteration: i + 1,
|
|
12191
|
+
status: refineResult.status
|
|
12192
|
+
}, "Critique loop: refinement dispatch failed — stopping loop");
|
|
12193
|
+
break;
|
|
12194
|
+
}
|
|
12195
|
+
} catch (err) {
|
|
12196
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12197
|
+
logger$7.warn({
|
|
12198
|
+
phaseId,
|
|
12199
|
+
iteration: i + 1,
|
|
12200
|
+
err: message
|
|
12201
|
+
}, "Critique loop: refinement dispatch threw — stopping loop");
|
|
12202
|
+
break;
|
|
12203
|
+
}
|
|
11725
12204
|
}
|
|
11726
12205
|
}
|
|
11727
|
-
|
|
12206
|
+
const remainingIssues = lastCritiqueOutput?.issues ?? [];
|
|
12207
|
+
if (remainingIssues.length > 0) {
|
|
12208
|
+
logger$7.warn({
|
|
12209
|
+
phaseId,
|
|
12210
|
+
maxIterations,
|
|
12211
|
+
issueCount: remainingIssues.length
|
|
12212
|
+
}, "Critique loop: max iterations reached with unresolved issues");
|
|
12213
|
+
for (const issue of remainingIssues) logger$7.warn({
|
|
12214
|
+
phaseId,
|
|
12215
|
+
severity: issue.severity,
|
|
12216
|
+
category: issue.category,
|
|
12217
|
+
description: issue.description
|
|
12218
|
+
}, `Critique loop: unresolved issue — ${issue.severity}: ${issue.description}`);
|
|
12219
|
+
}
|
|
12220
|
+
return {
|
|
12221
|
+
verdict: "needs_work",
|
|
12222
|
+
iterations,
|
|
12223
|
+
remainingIssues,
|
|
12224
|
+
critiqueTokens,
|
|
12225
|
+
refinementTokens,
|
|
12226
|
+
totalMs: Date.now() - startMs
|
|
12227
|
+
};
|
|
11728
12228
|
}
|
|
12229
|
+
|
|
12230
|
+
//#endregion
|
|
12231
|
+
//#region src/modules/phase-orchestrator/elicitation-selector.ts
|
|
12232
|
+
const logger$6 = createLogger("elicitation-selector");
|
|
11729
12233
|
/**
|
|
11730
|
-
*
|
|
12234
|
+
* Affinity scores (0.0–1.0) for each category per content type.
|
|
11731
12235
|
*
|
|
11732
|
-
*
|
|
11733
|
-
*
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
12236
|
+
* Higher score → more likely to be selected for that content type.
|
|
12237
|
+
* Based on the method-to-phase affinity matrix from the Dev Notes.
|
|
12238
|
+
*/
|
|
12239
|
+
const CATEGORY_AFFINITY = {
|
|
12240
|
+
brief: {
|
|
12241
|
+
core: 1,
|
|
12242
|
+
collaboration: .9,
|
|
12243
|
+
creative: .8,
|
|
12244
|
+
research: .5,
|
|
12245
|
+
risk: .4,
|
|
12246
|
+
advanced: .3,
|
|
12247
|
+
technical: .2,
|
|
12248
|
+
competitive: .2,
|
|
12249
|
+
learning: .1,
|
|
12250
|
+
philosophical: .1,
|
|
12251
|
+
retrospective: .1
|
|
12252
|
+
},
|
|
12253
|
+
prd: {
|
|
12254
|
+
risk: 1,
|
|
12255
|
+
core: .9,
|
|
12256
|
+
research: .8,
|
|
12257
|
+
collaboration: .6,
|
|
12258
|
+
creative: .4,
|
|
12259
|
+
advanced: .3,
|
|
12260
|
+
technical: .2,
|
|
12261
|
+
competitive: .2,
|
|
12262
|
+
learning: .1,
|
|
12263
|
+
philosophical: .1,
|
|
12264
|
+
retrospective: .1
|
|
12265
|
+
},
|
|
12266
|
+
architecture: {
|
|
12267
|
+
technical: 1,
|
|
12268
|
+
competitive: .9,
|
|
12269
|
+
risk: .8,
|
|
12270
|
+
core: .5,
|
|
12271
|
+
advanced: .5,
|
|
12272
|
+
research: .4,
|
|
12273
|
+
collaboration: .3,
|
|
12274
|
+
creative: .2,
|
|
12275
|
+
learning: .1,
|
|
12276
|
+
philosophical: .1,
|
|
12277
|
+
retrospective: .1
|
|
12278
|
+
},
|
|
12279
|
+
stories: {
|
|
12280
|
+
collaboration: 1,
|
|
12281
|
+
risk: .9,
|
|
12282
|
+
core: .5,
|
|
12283
|
+
research: .4,
|
|
12284
|
+
creative: .3,
|
|
12285
|
+
technical: .3,
|
|
12286
|
+
advanced: .2,
|
|
12287
|
+
competitive: .2,
|
|
12288
|
+
learning: .1,
|
|
12289
|
+
philosophical: .1,
|
|
12290
|
+
retrospective: .1
|
|
12291
|
+
},
|
|
12292
|
+
"ux-design": {
|
|
12293
|
+
creative: 1,
|
|
12294
|
+
collaboration: .9,
|
|
12295
|
+
research: .8,
|
|
12296
|
+
core: .5,
|
|
12297
|
+
risk: .4,
|
|
12298
|
+
advanced: .3,
|
|
12299
|
+
competitive: .3,
|
|
12300
|
+
technical: .2,
|
|
12301
|
+
learning: .1,
|
|
12302
|
+
philosophical: .1,
|
|
12303
|
+
retrospective: .1
|
|
11744
12304
|
}
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
12305
|
+
};
|
|
12306
|
+
const RECENCY_PENALTY_FACTOR = .2;
|
|
12307
|
+
/**
|
|
12308
|
+
* Parse a CSV string (with header) into an array of ElicitationMethod objects.
|
|
12309
|
+
*
|
|
12310
|
+
* Expected CSV format (columns): num,category,method_name,description,output_pattern
|
|
12311
|
+
*
|
|
12312
|
+
* @param csvContent - Raw CSV file content including header row
|
|
12313
|
+
* @returns Array of parsed elicitation methods
|
|
12314
|
+
*/
|
|
12315
|
+
function parseMethodsCsv(csvContent) {
|
|
12316
|
+
const lines = csvContent.trim().split("\n");
|
|
12317
|
+
if (lines.length < 2) return [];
|
|
12318
|
+
const methods = [];
|
|
12319
|
+
for (let i = 1; i < lines.length; i++) {
|
|
12320
|
+
const line = lines[i].trim();
|
|
12321
|
+
if (!line) continue;
|
|
12322
|
+
const firstComma = line.indexOf(",");
|
|
12323
|
+
const secondComma = line.indexOf(",", firstComma + 1);
|
|
12324
|
+
const thirdComma = line.indexOf(",", secondComma + 1);
|
|
12325
|
+
const lastComma = line.lastIndexOf(",");
|
|
12326
|
+
if (firstComma < 0 || secondComma < 0 || thirdComma < 0 || lastComma <= thirdComma) continue;
|
|
12327
|
+
const category = line.slice(firstComma + 1, secondComma);
|
|
12328
|
+
const name = line.slice(secondComma + 1, thirdComma);
|
|
12329
|
+
const description = line.slice(thirdComma + 1, lastComma);
|
|
12330
|
+
const output_pattern = line.slice(lastComma + 1);
|
|
12331
|
+
if (!category || !name || !description || !output_pattern) continue;
|
|
12332
|
+
methods.push({
|
|
12333
|
+
name,
|
|
12334
|
+
category,
|
|
12335
|
+
description,
|
|
12336
|
+
output_pattern
|
|
12337
|
+
});
|
|
11756
12338
|
}
|
|
11757
|
-
|
|
11758
|
-
const stepName = source.slice(5);
|
|
11759
|
-
const output = stepOutputs.get(stepName);
|
|
11760
|
-
if (!output) return "";
|
|
11761
|
-
const parts = [];
|
|
11762
|
-
for (const [key, value] of Object.entries(output)) {
|
|
11763
|
-
if (key === "result") continue;
|
|
11764
|
-
if (Array.isArray(value)) {
|
|
11765
|
-
parts.push(`### ${key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}`);
|
|
11766
|
-
for (const item of value) if (typeof item === "object" && item !== null) parts.push(`- ${JSON.stringify(item)}`);
|
|
11767
|
-
else parts.push(`- ${String(item)}`);
|
|
11768
|
-
} else if (typeof value === "object" && value !== null) parts.push(`- **${key}**: ${JSON.stringify(value)}`);
|
|
11769
|
-
else parts.push(`- **${key}**: ${String(value)}`);
|
|
11770
|
-
}
|
|
11771
|
-
return parts.join("\n");
|
|
11772
|
-
}
|
|
11773
|
-
return "";
|
|
12339
|
+
return methods;
|
|
11774
12340
|
}
|
|
11775
12341
|
/**
|
|
11776
|
-
*
|
|
12342
|
+
* Load elicitation methods from the pack data directory.
|
|
11777
12343
|
*
|
|
11778
|
-
*
|
|
11779
|
-
*
|
|
12344
|
+
* Reads from packs/bmad/data/elicitation-methods.csv relative to process.cwd().
|
|
12345
|
+
* Returns an empty array if the file cannot be read.
|
|
11780
12346
|
*
|
|
11781
|
-
* @
|
|
11782
|
-
* @param deps - Shared phase dependencies
|
|
11783
|
-
* @param runId - Pipeline run ID
|
|
11784
|
-
* @param phase - Phase name (for decision store persistence)
|
|
11785
|
-
* @param params - Runtime parameters map (concept, product_brief, etc.)
|
|
11786
|
-
* @returns Aggregated multi-step result
|
|
12347
|
+
* @returns Array of all available elicitation methods
|
|
11787
12348
|
*/
|
|
11788
|
-
|
|
11789
|
-
const
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
let prompt = template;
|
|
11796
|
-
for (const ref of step.context) {
|
|
11797
|
-
const value = resolveContext(ref, deps, runId, params, stepOutputs);
|
|
11798
|
-
prompt = prompt.replace(`{{${ref.placeholder}}}`, value);
|
|
11799
|
-
}
|
|
11800
|
-
const allDecisions = getDecisionsByPhaseForRun(deps.db, runId, phase);
|
|
11801
|
-
const budgetTokens = calculateDynamicBudget(4e3, allDecisions.length);
|
|
11802
|
-
let estimatedTokens = Math.ceil(prompt.length / 4);
|
|
11803
|
-
if (estimatedTokens > budgetTokens) {
|
|
11804
|
-
const decisionRefs = step.context.filter((ref) => ref.source.startsWith("decision:"));
|
|
11805
|
-
if (decisionRefs.length > 0) {
|
|
11806
|
-
logger$5.warn({
|
|
11807
|
-
step: step.name,
|
|
11808
|
-
estimatedTokens,
|
|
11809
|
-
budgetTokens
|
|
11810
|
-
}, "Prompt exceeds budget — attempting decision summarization");
|
|
11811
|
-
let summarizedPrompt = template;
|
|
11812
|
-
for (const ref of step.context) {
|
|
11813
|
-
let value;
|
|
11814
|
-
if (ref.source.startsWith("decision:")) {
|
|
11815
|
-
const path$1 = ref.source.slice(9);
|
|
11816
|
-
const [decPhase, decCategory] = path$1.split(".");
|
|
11817
|
-
if (decPhase && decCategory) {
|
|
11818
|
-
const decisions = getDecisionsByPhaseForRun(deps.db, runId, decPhase);
|
|
11819
|
-
const filtered = decisions.filter((d) => d.category === decCategory);
|
|
11820
|
-
const budgetChars = budgetTokens * 4;
|
|
11821
|
-
const availableChars = Math.max(200, Math.floor(budgetChars / decisionRefs.length));
|
|
11822
|
-
value = summarizeDecisions(filtered.map((d) => ({
|
|
11823
|
-
key: d.key,
|
|
11824
|
-
value: d.value,
|
|
11825
|
-
category: d.category
|
|
11826
|
-
})), availableChars);
|
|
11827
|
-
} else value = resolveContext(ref, deps, runId, params, stepOutputs);
|
|
11828
|
-
} else value = resolveContext(ref, deps, runId, params, stepOutputs);
|
|
11829
|
-
summarizedPrompt = summarizedPrompt.replace(`{{${ref.placeholder}}}`, value);
|
|
11830
|
-
}
|
|
11831
|
-
prompt = summarizedPrompt;
|
|
11832
|
-
estimatedTokens = Math.ceil(prompt.length / 4);
|
|
11833
|
-
if (estimatedTokens <= budgetTokens) logger$5.info({
|
|
11834
|
-
step: step.name,
|
|
11835
|
-
estimatedTokens,
|
|
11836
|
-
budgetTokens
|
|
11837
|
-
}, "Decision summarization brought prompt within budget");
|
|
11838
|
-
}
|
|
11839
|
-
if (estimatedTokens > budgetTokens) {
|
|
11840
|
-
const errorMsg = `Step '${step.name}' prompt exceeds token budget after summarization: ${estimatedTokens} tokens (max ${budgetTokens})`;
|
|
11841
|
-
stepResults.push({
|
|
11842
|
-
name: step.name,
|
|
11843
|
-
success: false,
|
|
11844
|
-
parsed: null,
|
|
11845
|
-
error: errorMsg,
|
|
11846
|
-
tokenUsage: {
|
|
11847
|
-
input: 0,
|
|
11848
|
-
output: 0
|
|
11849
|
-
}
|
|
11850
|
-
});
|
|
11851
|
-
return {
|
|
11852
|
-
success: false,
|
|
11853
|
-
steps: stepResults,
|
|
11854
|
-
tokenUsage: {
|
|
11855
|
-
input: totalInput,
|
|
11856
|
-
output: totalOutput
|
|
11857
|
-
},
|
|
11858
|
-
error: errorMsg
|
|
11859
|
-
};
|
|
11860
|
-
}
|
|
11861
|
-
}
|
|
11862
|
-
const handle = deps.dispatcher.dispatch({
|
|
11863
|
-
prompt,
|
|
11864
|
-
agent: "claude-code",
|
|
11865
|
-
taskType: step.taskType,
|
|
11866
|
-
outputSchema: step.outputSchema
|
|
11867
|
-
});
|
|
11868
|
-
const dispatchResult = await handle.result;
|
|
11869
|
-
const tokenUsage = {
|
|
11870
|
-
input: dispatchResult.tokenEstimate.input,
|
|
11871
|
-
output: dispatchResult.tokenEstimate.output
|
|
11872
|
-
};
|
|
11873
|
-
totalInput += tokenUsage.input;
|
|
11874
|
-
totalOutput += tokenUsage.output;
|
|
11875
|
-
if (dispatchResult.status === "timeout") {
|
|
11876
|
-
const errorMsg = `Step '${step.name}' timed out after ${dispatchResult.durationMs}ms`;
|
|
11877
|
-
stepResults.push({
|
|
11878
|
-
name: step.name,
|
|
11879
|
-
success: false,
|
|
11880
|
-
parsed: null,
|
|
11881
|
-
error: errorMsg,
|
|
11882
|
-
tokenUsage
|
|
11883
|
-
});
|
|
11884
|
-
return {
|
|
11885
|
-
success: false,
|
|
11886
|
-
steps: stepResults,
|
|
11887
|
-
tokenUsage: {
|
|
11888
|
-
input: totalInput,
|
|
11889
|
-
output: totalOutput
|
|
11890
|
-
},
|
|
11891
|
-
error: errorMsg
|
|
11892
|
-
};
|
|
11893
|
-
}
|
|
11894
|
-
if (dispatchResult.status === "failed") {
|
|
11895
|
-
const errorMsg = `Step '${step.name}' dispatch failed: ${dispatchResult.parseError ?? dispatchResult.output}`;
|
|
11896
|
-
stepResults.push({
|
|
11897
|
-
name: step.name,
|
|
11898
|
-
success: false,
|
|
11899
|
-
parsed: null,
|
|
11900
|
-
error: errorMsg,
|
|
11901
|
-
tokenUsage
|
|
11902
|
-
});
|
|
11903
|
-
return {
|
|
11904
|
-
success: false,
|
|
11905
|
-
steps: stepResults,
|
|
11906
|
-
tokenUsage: {
|
|
11907
|
-
input: totalInput,
|
|
11908
|
-
output: totalOutput
|
|
11909
|
-
},
|
|
11910
|
-
error: errorMsg
|
|
11911
|
-
};
|
|
11912
|
-
}
|
|
11913
|
-
if (dispatchResult.parsed === null || dispatchResult.parseError !== null) {
|
|
11914
|
-
const errorMsg = `Step '${step.name}' schema validation failed: ${dispatchResult.parseError ?? "No parsed output"}`;
|
|
11915
|
-
stepResults.push({
|
|
11916
|
-
name: step.name,
|
|
11917
|
-
success: false,
|
|
11918
|
-
parsed: null,
|
|
11919
|
-
error: errorMsg,
|
|
11920
|
-
tokenUsage
|
|
11921
|
-
});
|
|
11922
|
-
return {
|
|
11923
|
-
success: false,
|
|
11924
|
-
steps: stepResults,
|
|
11925
|
-
tokenUsage: {
|
|
11926
|
-
input: totalInput,
|
|
11927
|
-
output: totalOutput
|
|
11928
|
-
},
|
|
11929
|
-
error: errorMsg
|
|
11930
|
-
};
|
|
11931
|
-
}
|
|
11932
|
-
const parsed = dispatchResult.parsed;
|
|
11933
|
-
if (parsed.result === "failed") {
|
|
11934
|
-
const errorMsg = `Step '${step.name}' agent reported failure`;
|
|
11935
|
-
stepResults.push({
|
|
11936
|
-
name: step.name,
|
|
11937
|
-
success: false,
|
|
11938
|
-
parsed: null,
|
|
11939
|
-
error: errorMsg,
|
|
11940
|
-
tokenUsage
|
|
11941
|
-
});
|
|
11942
|
-
return {
|
|
11943
|
-
success: false,
|
|
11944
|
-
steps: stepResults,
|
|
11945
|
-
tokenUsage: {
|
|
11946
|
-
input: totalInput,
|
|
11947
|
-
output: totalOutput
|
|
11948
|
-
},
|
|
11949
|
-
error: errorMsg
|
|
11950
|
-
};
|
|
11951
|
-
}
|
|
11952
|
-
stepOutputs.set(step.name, parsed);
|
|
11953
|
-
for (const mapping of step.persist) {
|
|
11954
|
-
const fieldValue = parsed[mapping.field];
|
|
11955
|
-
if (fieldValue === void 0) continue;
|
|
11956
|
-
if (mapping.key === "array" && Array.isArray(fieldValue)) for (const [index, item] of fieldValue.entries()) upsertDecision(deps.db, {
|
|
11957
|
-
pipeline_run_id: runId,
|
|
11958
|
-
phase,
|
|
11959
|
-
category: mapping.category,
|
|
11960
|
-
key: `${step.name}-${index}`,
|
|
11961
|
-
value: typeof item === "object" ? JSON.stringify(item) : String(item)
|
|
11962
|
-
});
|
|
11963
|
-
else if (typeof fieldValue === "object" && fieldValue !== null) upsertDecision(deps.db, {
|
|
11964
|
-
pipeline_run_id: runId,
|
|
11965
|
-
phase,
|
|
11966
|
-
category: mapping.category,
|
|
11967
|
-
key: mapping.key,
|
|
11968
|
-
value: JSON.stringify(fieldValue)
|
|
11969
|
-
});
|
|
11970
|
-
else upsertDecision(deps.db, {
|
|
11971
|
-
pipeline_run_id: runId,
|
|
11972
|
-
phase,
|
|
11973
|
-
category: mapping.category,
|
|
11974
|
-
key: mapping.key,
|
|
11975
|
-
value: String(fieldValue)
|
|
11976
|
-
});
|
|
11977
|
-
}
|
|
11978
|
-
let artifactId;
|
|
11979
|
-
if (step.registerArtifact) {
|
|
11980
|
-
const artifact = registerArtifact(deps.db, {
|
|
11981
|
-
pipeline_run_id: runId,
|
|
11982
|
-
phase,
|
|
11983
|
-
type: step.registerArtifact.type,
|
|
11984
|
-
path: step.registerArtifact.path,
|
|
11985
|
-
summary: step.registerArtifact.summarize(parsed)
|
|
11986
|
-
});
|
|
11987
|
-
artifactId = artifact.id;
|
|
11988
|
-
}
|
|
11989
|
-
const stepResult = {
|
|
11990
|
-
name: step.name,
|
|
11991
|
-
success: true,
|
|
11992
|
-
parsed,
|
|
11993
|
-
error: null,
|
|
11994
|
-
tokenUsage
|
|
11995
|
-
};
|
|
11996
|
-
if (artifactId !== void 0) stepResult.artifactId = artifactId;
|
|
11997
|
-
stepResults.push(stepResult);
|
|
12349
|
+
function loadElicitationMethods() {
|
|
12350
|
+
const csvPath = join(process.cwd(), "packs", "bmad", "data", "elicitation-methods.csv");
|
|
12351
|
+
try {
|
|
12352
|
+
const content = readFileSync(csvPath, "utf-8");
|
|
12353
|
+
const methods = parseMethodsCsv(content);
|
|
12354
|
+
logger$6.debug({ count: methods.length }, "Loaded elicitation methods");
|
|
12355
|
+
return methods;
|
|
11998
12356
|
} catch (err) {
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12357
|
+
logger$6.warn({
|
|
12358
|
+
csvPath,
|
|
12359
|
+
err
|
|
12360
|
+
}, "Failed to load elicitation methods CSV");
|
|
12361
|
+
return [];
|
|
12362
|
+
}
|
|
12363
|
+
}
|
|
12364
|
+
/**
|
|
12365
|
+
* Select 1–2 elicitation methods appropriate for the given context.
|
|
12366
|
+
*
|
|
12367
|
+
* Selection algorithm:
|
|
12368
|
+
* 1. Score each method: categoryAffinity × recencyFactor × riskBoost × complexityBoost
|
|
12369
|
+
* 2. Sort descending by score
|
|
12370
|
+
* 3. Return top 1–2 methods (always at least 1 if methods are available)
|
|
12371
|
+
*
|
|
12372
|
+
* Methods used in previous rounds (listed in `usedMethods`) are deprioritized
|
|
12373
|
+
* via `RECENCY_PENALTY_FACTOR` to encourage category rotation.
|
|
12374
|
+
*
|
|
12375
|
+
* @param context - Elicitation context describing the artifact and domain
|
|
12376
|
+
* @param usedMethods - Names of methods already used in this pipeline run
|
|
12377
|
+
* @param methods - Optional pre-loaded method list (defaults to loadElicitationMethods())
|
|
12378
|
+
* @returns Array of 0–2 selected ElicitationMethod objects
|
|
12379
|
+
*/
|
|
12380
|
+
function selectMethods(context, usedMethods, methods) {
|
|
12381
|
+
const allMethods = methods ?? loadElicitationMethods();
|
|
12382
|
+
if (allMethods.length === 0) return [];
|
|
12383
|
+
const affinity = CATEGORY_AFFINITY[context.content_type] ?? {};
|
|
12384
|
+
const usedSet = new Set(usedMethods);
|
|
12385
|
+
const complexityScore = context.complexity_score ?? .5;
|
|
12386
|
+
const riskLevel = context.risk_level ?? "medium";
|
|
12387
|
+
const scored = allMethods.map((method) => {
|
|
12388
|
+
const categoryScore = affinity[method.category] ?? .3;
|
|
12389
|
+
const recencyFactor = usedSet.has(method.name) ? RECENCY_PENALTY_FACTOR : 1;
|
|
12390
|
+
const riskBoost = riskLevel === "high" && method.category === "risk" ? 1.3 : 1;
|
|
12391
|
+
const complexityBoost = complexityScore > .7 && (method.category === "technical" || method.category === "advanced") ? 1.2 : 1;
|
|
12392
|
+
const score = categoryScore * recencyFactor * riskBoost * complexityBoost;
|
|
12011
12393
|
return {
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
tokenUsage: {
|
|
12015
|
-
input: totalInput,
|
|
12016
|
-
output: totalOutput
|
|
12017
|
-
},
|
|
12018
|
-
error: errorMsg
|
|
12394
|
+
method,
|
|
12395
|
+
score
|
|
12019
12396
|
};
|
|
12397
|
+
});
|
|
12398
|
+
scored.sort((a, b) => {
|
|
12399
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
12400
|
+
return a.method.name.localeCompare(b.method.name);
|
|
12401
|
+
});
|
|
12402
|
+
return scored.slice(0, 2).map((s) => s.method);
|
|
12403
|
+
}
|
|
12404
|
+
/**
|
|
12405
|
+
* Derive an ElicitationContext content_type from a phase name and step name.
|
|
12406
|
+
*
|
|
12407
|
+
* Mapping:
|
|
12408
|
+
* - analysis phase → 'brief'
|
|
12409
|
+
* - planning phase → 'prd'
|
|
12410
|
+
* - solutioning + arch step → 'architecture'
|
|
12411
|
+
* - solutioning + story/epic step → 'stories'
|
|
12412
|
+
* - ux-design phase → 'ux-design'
|
|
12413
|
+
* - default → 'brief'
|
|
12414
|
+
*
|
|
12415
|
+
* @param phase - Pipeline phase name
|
|
12416
|
+
* @param stepName - Step name within the phase
|
|
12417
|
+
* @returns Content type for method selection
|
|
12418
|
+
*/
|
|
12419
|
+
function deriveContentType(phase, stepName) {
|
|
12420
|
+
if (phase === "analysis") return "brief";
|
|
12421
|
+
if (phase === "planning") return "prd";
|
|
12422
|
+
if (phase === "ux-design") return "ux-design";
|
|
12423
|
+
if (phase === "solutioning") {
|
|
12424
|
+
if (stepName.includes("arch")) return "architecture";
|
|
12425
|
+
if (stepName.includes("stor") || stepName.includes("epic")) return "stories";
|
|
12020
12426
|
}
|
|
12021
|
-
return
|
|
12022
|
-
success: true,
|
|
12023
|
-
steps: stepResults,
|
|
12024
|
-
tokenUsage: {
|
|
12025
|
-
input: totalInput,
|
|
12026
|
-
output: totalOutput
|
|
12027
|
-
}
|
|
12028
|
-
};
|
|
12427
|
+
return "brief";
|
|
12029
12428
|
}
|
|
12030
12429
|
|
|
12031
12430
|
//#endregion
|
|
@@ -12201,6 +12600,502 @@ const EpicDesignOutputSchema = z.object({
|
|
|
12201
12600
|
fr_coverage: z.array(z.string()).default([])
|
|
12202
12601
|
})).min(1).optional()
|
|
12203
12602
|
});
|
|
12603
|
+
/**
|
|
12604
|
+
* Step 1 output: UX Discovery + Core Experience.
|
|
12605
|
+
* Covers user personas, core experience goals, and emotional response targets.
|
|
12606
|
+
* Content fields are optional to allow `{result: 'failed'}` without Zod rejection.
|
|
12607
|
+
*/
|
|
12608
|
+
const UxDiscoveryOutputSchema = z.object({
|
|
12609
|
+
result: z.enum(["success", "failed"]),
|
|
12610
|
+
target_personas: z.array(z.string().min(1)).optional(),
|
|
12611
|
+
core_experience: z.string().optional(),
|
|
12612
|
+
emotional_goals: z.array(z.string().min(1)).optional(),
|
|
12613
|
+
inspiration_references: z.array(z.string()).default([])
|
|
12614
|
+
});
|
|
12615
|
+
/**
|
|
12616
|
+
* Step 2 output: Design System + Visual Foundation.
|
|
12617
|
+
* Covers design system approach, visual language, and design directions.
|
|
12618
|
+
* Content fields are optional to allow `{result: 'failed'}` without Zod rejection.
|
|
12619
|
+
*/
|
|
12620
|
+
const UxDesignSystemOutputSchema = z.object({
|
|
12621
|
+
result: z.enum(["success", "failed"]),
|
|
12622
|
+
design_system: z.string().optional(),
|
|
12623
|
+
visual_foundation: z.string().optional(),
|
|
12624
|
+
design_principles: z.array(z.string().min(1)).optional(),
|
|
12625
|
+
color_and_typography: z.string().optional()
|
|
12626
|
+
});
|
|
12627
|
+
/**
|
|
12628
|
+
* Step 3 output: User Journeys + Component Strategy + Accessibility.
|
|
12629
|
+
* Covers user flows, component architecture, UX patterns, and a11y guidelines.
|
|
12630
|
+
* Content fields are optional to allow `{result: 'failed'}` without Zod rejection.
|
|
12631
|
+
*/
|
|
12632
|
+
const UxJourneysOutputSchema = z.object({
|
|
12633
|
+
result: z.enum(["success", "failed"]),
|
|
12634
|
+
user_journeys: z.array(z.string().min(1)).optional(),
|
|
12635
|
+
component_strategy: z.string().optional(),
|
|
12636
|
+
ux_patterns: z.array(z.string()).default([]),
|
|
12637
|
+
accessibility_guidelines: z.array(z.string()).default([])
|
|
12638
|
+
});
|
|
12639
|
+
/**
|
|
12640
|
+
* Zod schema for the YAML output emitted by an elicitation sub-agent.
|
|
12641
|
+
* The agent returns structured insights from applying an elicitation method.
|
|
12642
|
+
*/
|
|
12643
|
+
const ElicitationOutputSchema = z.object({
|
|
12644
|
+
result: z.enum(["success", "failed"]),
|
|
12645
|
+
insights: z.string()
|
|
12646
|
+
});
|
|
12647
|
+
|
|
12648
|
+
//#endregion
|
|
12649
|
+
//#region src/modules/phase-orchestrator/step-runner.ts
|
|
12650
|
+
const logger$5 = createLogger("step-runner");
|
|
12651
|
+
/**
|
|
12652
|
+
* Format an array of decision records into a markdown section for injection.
|
|
12653
|
+
*
|
|
12654
|
+
* @param decisions - Decision records from the store
|
|
12655
|
+
* @param sectionTitle - Title for the markdown section
|
|
12656
|
+
* @returns Formatted markdown string
|
|
12657
|
+
*/
|
|
12658
|
+
function formatDecisionsForInjection(decisions, sectionTitle) {
|
|
12659
|
+
if (decisions.length === 0) return "";
|
|
12660
|
+
const parts = [];
|
|
12661
|
+
if (sectionTitle) parts.push(`## ${sectionTitle}`);
|
|
12662
|
+
for (const d of decisions) {
|
|
12663
|
+
const rationale = d.rationale ? ` (${d.rationale})` : "";
|
|
12664
|
+
try {
|
|
12665
|
+
const parsed = JSON.parse(d.value);
|
|
12666
|
+
if (Array.isArray(parsed)) {
|
|
12667
|
+
parts.push(`### ${d.key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}`);
|
|
12668
|
+
for (const item of parsed) parts.push(`- ${String(item)}`);
|
|
12669
|
+
} else if (typeof parsed === "object" && parsed !== null) parts.push(`- **${d.key}**: ${JSON.stringify(parsed)}${rationale}`);
|
|
12670
|
+
else parts.push(`- **${d.key}**: ${String(parsed)}${rationale}`);
|
|
12671
|
+
} catch {
|
|
12672
|
+
parts.push(`- **${d.key}**: ${d.value}${rationale}`);
|
|
12673
|
+
}
|
|
12674
|
+
}
|
|
12675
|
+
return parts.join("\n");
|
|
12676
|
+
}
|
|
12677
|
+
/**
|
|
12678
|
+
* Resolve a single context reference to a string value.
|
|
12679
|
+
*
|
|
12680
|
+
* @param ref - The context reference to resolve
|
|
12681
|
+
* @param deps - Phase dependencies (for DB access)
|
|
12682
|
+
* @param runId - Pipeline run ID
|
|
12683
|
+
* @param params - Runtime parameters map
|
|
12684
|
+
* @param stepOutputs - Map of step name → raw parsed output from prior steps
|
|
12685
|
+
* @returns Resolved string value
|
|
12686
|
+
*/
|
|
12687
|
+
function resolveContext(ref, deps, runId, params, stepOutputs) {
|
|
12688
|
+
const { source } = ref;
|
|
12689
|
+
if (source.startsWith("param:")) {
|
|
12690
|
+
const key = source.slice(6);
|
|
12691
|
+
return params[key] ?? "";
|
|
12692
|
+
}
|
|
12693
|
+
if (source.startsWith("decision:")) {
|
|
12694
|
+
const path$1 = source.slice(9);
|
|
12695
|
+
const [phase, category] = path$1.split(".");
|
|
12696
|
+
if (!phase || !category) return "";
|
|
12697
|
+
const decisions = getDecisionsByPhaseForRun(deps.db, runId, phase);
|
|
12698
|
+
const filtered = decisions.filter((d) => d.category === category);
|
|
12699
|
+
return formatDecisionsForInjection(filtered.map((d) => ({
|
|
12700
|
+
key: d.key,
|
|
12701
|
+
value: d.value,
|
|
12702
|
+
rationale: d.rationale ?? null
|
|
12703
|
+
})), category.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
|
|
12704
|
+
}
|
|
12705
|
+
if (source.startsWith("step:")) {
|
|
12706
|
+
const stepName = source.slice(5);
|
|
12707
|
+
const output = stepOutputs.get(stepName);
|
|
12708
|
+
if (!output) return "";
|
|
12709
|
+
const parts = [];
|
|
12710
|
+
for (const [key, value] of Object.entries(output)) {
|
|
12711
|
+
if (key === "result") continue;
|
|
12712
|
+
if (Array.isArray(value)) {
|
|
12713
|
+
parts.push(`### ${key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}`);
|
|
12714
|
+
for (const item of value) if (typeof item === "object" && item !== null) parts.push(`- ${JSON.stringify(item)}`);
|
|
12715
|
+
else parts.push(`- ${String(item)}`);
|
|
12716
|
+
} else if (typeof value === "object" && value !== null) parts.push(`- **${key}**: ${JSON.stringify(value)}`);
|
|
12717
|
+
else parts.push(`- **${key}**: ${String(value)}`);
|
|
12718
|
+
}
|
|
12719
|
+
return parts.join("\n");
|
|
12720
|
+
}
|
|
12721
|
+
return "";
|
|
12722
|
+
}
|
|
12723
|
+
/**
|
|
12724
|
+
* Execute a sequence of steps, accumulating context and persisting results.
|
|
12725
|
+
*
|
|
12726
|
+
* Halts on the first step that fails. Each step's output is available to
|
|
12727
|
+
* subsequent steps via the stepOutputs map and decision store.
|
|
12728
|
+
*
|
|
12729
|
+
* @param steps - Ordered list of step definitions to execute
|
|
12730
|
+
* @param deps - Shared phase dependencies
|
|
12731
|
+
* @param runId - Pipeline run ID
|
|
12732
|
+
* @param phase - Phase name (for decision store persistence)
|
|
12733
|
+
* @param params - Runtime parameters map (concept, product_brief, etc.)
|
|
12734
|
+
* @returns Aggregated multi-step result
|
|
12735
|
+
*/
|
|
12736
|
+
async function runSteps(steps, deps, runId, phase, params) {
|
|
12737
|
+
const stepResults = [];
|
|
12738
|
+
const stepOutputs = new Map();
|
|
12739
|
+
let totalInput = 0;
|
|
12740
|
+
let totalOutput = 0;
|
|
12741
|
+
let totalElicitationInput = 0;
|
|
12742
|
+
let totalElicitationOutput = 0;
|
|
12743
|
+
const usedElicitationMethods = [];
|
|
12744
|
+
for (const step of steps) try {
|
|
12745
|
+
const template = await deps.pack.getPrompt(step.name);
|
|
12746
|
+
let prompt = template;
|
|
12747
|
+
for (const ref of step.context) {
|
|
12748
|
+
const value = resolveContext(ref, deps, runId, params, stepOutputs);
|
|
12749
|
+
prompt = prompt.replace(`{{${ref.placeholder}}}`, value);
|
|
12750
|
+
}
|
|
12751
|
+
const allDecisions = getDecisionsByPhaseForRun(deps.db, runId, phase);
|
|
12752
|
+
const budgetTokens = calculateDynamicBudget(4e3, allDecisions.length);
|
|
12753
|
+
let estimatedTokens = Math.ceil(prompt.length / 4);
|
|
12754
|
+
if (estimatedTokens > budgetTokens) {
|
|
12755
|
+
const decisionRefs = step.context.filter((ref) => ref.source.startsWith("decision:"));
|
|
12756
|
+
if (decisionRefs.length > 0) {
|
|
12757
|
+
logger$5.warn({
|
|
12758
|
+
step: step.name,
|
|
12759
|
+
estimatedTokens,
|
|
12760
|
+
budgetTokens
|
|
12761
|
+
}, "Prompt exceeds budget — attempting decision summarization");
|
|
12762
|
+
let summarizedPrompt = template;
|
|
12763
|
+
for (const ref of step.context) {
|
|
12764
|
+
let value;
|
|
12765
|
+
if (ref.source.startsWith("decision:")) {
|
|
12766
|
+
const path$1 = ref.source.slice(9);
|
|
12767
|
+
const [decPhase, decCategory] = path$1.split(".");
|
|
12768
|
+
if (decPhase && decCategory) {
|
|
12769
|
+
const decisions = getDecisionsByPhaseForRun(deps.db, runId, decPhase);
|
|
12770
|
+
const filtered = decisions.filter((d) => d.category === decCategory);
|
|
12771
|
+
const budgetChars = budgetTokens * 4;
|
|
12772
|
+
const availableChars = Math.max(200, Math.floor(budgetChars / decisionRefs.length));
|
|
12773
|
+
value = summarizeDecisions(filtered.map((d) => ({
|
|
12774
|
+
key: d.key,
|
|
12775
|
+
value: d.value,
|
|
12776
|
+
category: d.category
|
|
12777
|
+
})), availableChars);
|
|
12778
|
+
} else value = resolveContext(ref, deps, runId, params, stepOutputs);
|
|
12779
|
+
} else value = resolveContext(ref, deps, runId, params, stepOutputs);
|
|
12780
|
+
summarizedPrompt = summarizedPrompt.replace(`{{${ref.placeholder}}}`, value);
|
|
12781
|
+
}
|
|
12782
|
+
prompt = summarizedPrompt;
|
|
12783
|
+
estimatedTokens = Math.ceil(prompt.length / 4);
|
|
12784
|
+
if (estimatedTokens <= budgetTokens) logger$5.info({
|
|
12785
|
+
step: step.name,
|
|
12786
|
+
estimatedTokens,
|
|
12787
|
+
budgetTokens
|
|
12788
|
+
}, "Decision summarization brought prompt within budget");
|
|
12789
|
+
}
|
|
12790
|
+
if (estimatedTokens > budgetTokens) {
|
|
12791
|
+
const errorMsg = `Step '${step.name}' prompt exceeds token budget after summarization: ${estimatedTokens} tokens (max ${budgetTokens})`;
|
|
12792
|
+
stepResults.push({
|
|
12793
|
+
name: step.name,
|
|
12794
|
+
success: false,
|
|
12795
|
+
parsed: null,
|
|
12796
|
+
error: errorMsg,
|
|
12797
|
+
tokenUsage: {
|
|
12798
|
+
input: 0,
|
|
12799
|
+
output: 0
|
|
12800
|
+
}
|
|
12801
|
+
});
|
|
12802
|
+
return {
|
|
12803
|
+
success: false,
|
|
12804
|
+
steps: stepResults,
|
|
12805
|
+
tokenUsage: {
|
|
12806
|
+
input: totalInput,
|
|
12807
|
+
output: totalOutput
|
|
12808
|
+
},
|
|
12809
|
+
elicitationTokenUsage: {
|
|
12810
|
+
input: totalElicitationInput,
|
|
12811
|
+
output: totalElicitationOutput
|
|
12812
|
+
},
|
|
12813
|
+
error: errorMsg
|
|
12814
|
+
};
|
|
12815
|
+
}
|
|
12816
|
+
}
|
|
12817
|
+
const handle = deps.dispatcher.dispatch({
|
|
12818
|
+
prompt,
|
|
12819
|
+
agent: "claude-code",
|
|
12820
|
+
taskType: step.taskType,
|
|
12821
|
+
outputSchema: step.outputSchema
|
|
12822
|
+
});
|
|
12823
|
+
const dispatchResult = await handle.result;
|
|
12824
|
+
const tokenUsage = {
|
|
12825
|
+
input: dispatchResult.tokenEstimate.input,
|
|
12826
|
+
output: dispatchResult.tokenEstimate.output
|
|
12827
|
+
};
|
|
12828
|
+
totalInput += tokenUsage.input;
|
|
12829
|
+
totalOutput += tokenUsage.output;
|
|
12830
|
+
if (dispatchResult.status === "timeout") {
|
|
12831
|
+
const errorMsg = `Step '${step.name}' timed out after ${dispatchResult.durationMs}ms`;
|
|
12832
|
+
stepResults.push({
|
|
12833
|
+
name: step.name,
|
|
12834
|
+
success: false,
|
|
12835
|
+
parsed: null,
|
|
12836
|
+
error: errorMsg,
|
|
12837
|
+
tokenUsage
|
|
12838
|
+
});
|
|
12839
|
+
return {
|
|
12840
|
+
success: false,
|
|
12841
|
+
steps: stepResults,
|
|
12842
|
+
tokenUsage: {
|
|
12843
|
+
input: totalInput,
|
|
12844
|
+
output: totalOutput
|
|
12845
|
+
},
|
|
12846
|
+
elicitationTokenUsage: {
|
|
12847
|
+
input: totalElicitationInput,
|
|
12848
|
+
output: totalElicitationOutput
|
|
12849
|
+
},
|
|
12850
|
+
error: errorMsg
|
|
12851
|
+
};
|
|
12852
|
+
}
|
|
12853
|
+
if (dispatchResult.status === "failed") {
|
|
12854
|
+
const errorMsg = `Step '${step.name}' dispatch failed: ${dispatchResult.parseError ?? dispatchResult.output}`;
|
|
12855
|
+
stepResults.push({
|
|
12856
|
+
name: step.name,
|
|
12857
|
+
success: false,
|
|
12858
|
+
parsed: null,
|
|
12859
|
+
error: errorMsg,
|
|
12860
|
+
tokenUsage
|
|
12861
|
+
});
|
|
12862
|
+
return {
|
|
12863
|
+
success: false,
|
|
12864
|
+
steps: stepResults,
|
|
12865
|
+
tokenUsage: {
|
|
12866
|
+
input: totalInput,
|
|
12867
|
+
output: totalOutput
|
|
12868
|
+
},
|
|
12869
|
+
elicitationTokenUsage: {
|
|
12870
|
+
input: totalElicitationInput,
|
|
12871
|
+
output: totalElicitationOutput
|
|
12872
|
+
},
|
|
12873
|
+
error: errorMsg
|
|
12874
|
+
};
|
|
12875
|
+
}
|
|
12876
|
+
if (dispatchResult.parsed === null || dispatchResult.parseError !== null) {
|
|
12877
|
+
const errorMsg = `Step '${step.name}' schema validation failed: ${dispatchResult.parseError ?? "No parsed output"}`;
|
|
12878
|
+
stepResults.push({
|
|
12879
|
+
name: step.name,
|
|
12880
|
+
success: false,
|
|
12881
|
+
parsed: null,
|
|
12882
|
+
error: errorMsg,
|
|
12883
|
+
tokenUsage
|
|
12884
|
+
});
|
|
12885
|
+
return {
|
|
12886
|
+
success: false,
|
|
12887
|
+
steps: stepResults,
|
|
12888
|
+
tokenUsage: {
|
|
12889
|
+
input: totalInput,
|
|
12890
|
+
output: totalOutput
|
|
12891
|
+
},
|
|
12892
|
+
elicitationTokenUsage: {
|
|
12893
|
+
input: totalElicitationInput,
|
|
12894
|
+
output: totalElicitationOutput
|
|
12895
|
+
},
|
|
12896
|
+
error: errorMsg
|
|
12897
|
+
};
|
|
12898
|
+
}
|
|
12899
|
+
const parsed = dispatchResult.parsed;
|
|
12900
|
+
if (parsed.result === "failed") {
|
|
12901
|
+
const errorMsg = `Step '${step.name}' agent reported failure`;
|
|
12902
|
+
stepResults.push({
|
|
12903
|
+
name: step.name,
|
|
12904
|
+
success: false,
|
|
12905
|
+
parsed: null,
|
|
12906
|
+
error: errorMsg,
|
|
12907
|
+
tokenUsage
|
|
12908
|
+
});
|
|
12909
|
+
return {
|
|
12910
|
+
success: false,
|
|
12911
|
+
steps: stepResults,
|
|
12912
|
+
tokenUsage: {
|
|
12913
|
+
input: totalInput,
|
|
12914
|
+
output: totalOutput
|
|
12915
|
+
},
|
|
12916
|
+
elicitationTokenUsage: {
|
|
12917
|
+
input: totalElicitationInput,
|
|
12918
|
+
output: totalElicitationOutput
|
|
12919
|
+
},
|
|
12920
|
+
error: errorMsg
|
|
12921
|
+
};
|
|
12922
|
+
}
|
|
12923
|
+
stepOutputs.set(step.name, parsed);
|
|
12924
|
+
for (const mapping of step.persist) {
|
|
12925
|
+
const fieldValue = parsed[mapping.field];
|
|
12926
|
+
if (fieldValue === void 0) continue;
|
|
12927
|
+
if (mapping.key === "array" && Array.isArray(fieldValue)) for (const [index, item] of fieldValue.entries()) upsertDecision(deps.db, {
|
|
12928
|
+
pipeline_run_id: runId,
|
|
12929
|
+
phase,
|
|
12930
|
+
category: mapping.category,
|
|
12931
|
+
key: `${step.name}-${index}`,
|
|
12932
|
+
value: typeof item === "object" ? JSON.stringify(item) : String(item)
|
|
12933
|
+
});
|
|
12934
|
+
else if (typeof fieldValue === "object" && fieldValue !== null) upsertDecision(deps.db, {
|
|
12935
|
+
pipeline_run_id: runId,
|
|
12936
|
+
phase,
|
|
12937
|
+
category: mapping.category,
|
|
12938
|
+
key: mapping.key,
|
|
12939
|
+
value: JSON.stringify(fieldValue)
|
|
12940
|
+
});
|
|
12941
|
+
else upsertDecision(deps.db, {
|
|
12942
|
+
pipeline_run_id: runId,
|
|
12943
|
+
phase,
|
|
12944
|
+
category: mapping.category,
|
|
12945
|
+
key: mapping.key,
|
|
12946
|
+
value: String(fieldValue)
|
|
12947
|
+
});
|
|
12948
|
+
}
|
|
12949
|
+
let artifactId;
|
|
12950
|
+
if (step.registerArtifact) {
|
|
12951
|
+
const artifact = registerArtifact(deps.db, {
|
|
12952
|
+
pipeline_run_id: runId,
|
|
12953
|
+
phase,
|
|
12954
|
+
type: step.registerArtifact.type,
|
|
12955
|
+
path: step.registerArtifact.path,
|
|
12956
|
+
summary: step.registerArtifact.summarize(parsed)
|
|
12957
|
+
});
|
|
12958
|
+
artifactId = artifact.id;
|
|
12959
|
+
}
|
|
12960
|
+
if (step.critique === true) try {
|
|
12961
|
+
const artifactContent = JSON.stringify(parsed, null, 2);
|
|
12962
|
+
const critiqueResult = await runCritiqueLoop(artifactContent, phase, runId, phase, deps);
|
|
12963
|
+
totalInput += critiqueResult.critiqueTokens.input + critiqueResult.refinementTokens.input;
|
|
12964
|
+
totalOutput += critiqueResult.critiqueTokens.output + critiqueResult.refinementTokens.output;
|
|
12965
|
+
logger$5.info({
|
|
12966
|
+
step: step.name,
|
|
12967
|
+
verdict: critiqueResult.verdict,
|
|
12968
|
+
iterations: critiqueResult.iterations,
|
|
12969
|
+
totalMs: critiqueResult.totalMs
|
|
12970
|
+
}, "Step critique loop complete");
|
|
12971
|
+
} catch (critiqueErr) {
|
|
12972
|
+
const critiqueMsg = critiqueErr instanceof Error ? critiqueErr.message : String(critiqueErr);
|
|
12973
|
+
logger$5.warn({
|
|
12974
|
+
step: step.name,
|
|
12975
|
+
err: critiqueMsg
|
|
12976
|
+
}, "Step critique loop threw an error — continuing without critique");
|
|
12977
|
+
}
|
|
12978
|
+
let stepElicitationTokens;
|
|
12979
|
+
if (step.elicitate === true) try {
|
|
12980
|
+
const contentType = deriveContentType(phase, step.name);
|
|
12981
|
+
const selectedMethods = selectMethods({ content_type: contentType }, usedElicitationMethods);
|
|
12982
|
+
if (selectedMethods.length > 0) {
|
|
12983
|
+
logger$5.info({
|
|
12984
|
+
step: step.name,
|
|
12985
|
+
methods: selectedMethods.map((m) => m.name),
|
|
12986
|
+
contentType
|
|
12987
|
+
}, "Running automated elicitation");
|
|
12988
|
+
const elicitationTemplate = await deps.pack.getPrompt("elicitation-apply");
|
|
12989
|
+
const artifactContent = JSON.stringify(parsed, null, 2);
|
|
12990
|
+
let elicitInput = 0;
|
|
12991
|
+
let elicitOutput = 0;
|
|
12992
|
+
let roundIndex = 0;
|
|
12993
|
+
for (const method of selectedMethods) {
|
|
12994
|
+
roundIndex++;
|
|
12995
|
+
const elicitPrompt = elicitationTemplate.replace(/\{\{method_name\}\}/g, method.name).replace(/\{\{method_description\}\}/g, method.description).replace(/\{\{output_pattern\}\}/g, method.output_pattern).replace(/\{\{artifact_content\}\}/g, artifactContent);
|
|
12996
|
+
const elicitHandle = deps.dispatcher.dispatch({
|
|
12997
|
+
prompt: elicitPrompt,
|
|
12998
|
+
agent: "claude-code",
|
|
12999
|
+
taskType: "elicitation",
|
|
13000
|
+
outputSchema: ElicitationOutputSchema
|
|
13001
|
+
});
|
|
13002
|
+
const elicitResult = await elicitHandle.result;
|
|
13003
|
+
elicitInput += elicitResult.tokenEstimate.input;
|
|
13004
|
+
elicitOutput += elicitResult.tokenEstimate.output;
|
|
13005
|
+
if (elicitResult.status === "completed" && elicitResult.parsed !== null) {
|
|
13006
|
+
const elicitParsed = elicitResult.parsed;
|
|
13007
|
+
if (elicitParsed.result === "success" && elicitParsed.insights) {
|
|
13008
|
+
upsertDecision(deps.db, {
|
|
13009
|
+
pipeline_run_id: runId,
|
|
13010
|
+
phase,
|
|
13011
|
+
category: "elicitation",
|
|
13012
|
+
key: `${phase}-round-${roundIndex}-method`,
|
|
13013
|
+
value: method.name
|
|
13014
|
+
});
|
|
13015
|
+
upsertDecision(deps.db, {
|
|
13016
|
+
pipeline_run_id: runId,
|
|
13017
|
+
phase,
|
|
13018
|
+
category: "elicitation",
|
|
13019
|
+
key: `${phase}-round-${roundIndex}-insights`,
|
|
13020
|
+
value: elicitParsed.insights
|
|
13021
|
+
});
|
|
13022
|
+
logger$5.info({
|
|
13023
|
+
step: step.name,
|
|
13024
|
+
method: method.name,
|
|
13025
|
+
roundIndex
|
|
13026
|
+
}, "Elicitation insights stored in decision store");
|
|
13027
|
+
}
|
|
13028
|
+
} else logger$5.warn({
|
|
13029
|
+
step: step.name,
|
|
13030
|
+
method: method.name,
|
|
13031
|
+
status: elicitResult.status
|
|
13032
|
+
}, "Elicitation dispatch did not produce valid output — skipping");
|
|
13033
|
+
usedElicitationMethods.push(method.name);
|
|
13034
|
+
}
|
|
13035
|
+
stepElicitationTokens = {
|
|
13036
|
+
input: elicitInput,
|
|
13037
|
+
output: elicitOutput
|
|
13038
|
+
};
|
|
13039
|
+
totalElicitationInput += elicitInput;
|
|
13040
|
+
totalElicitationOutput += elicitOutput;
|
|
13041
|
+
}
|
|
13042
|
+
} catch (elicitErr) {
|
|
13043
|
+
const elicitMsg = elicitErr instanceof Error ? elicitErr.message : String(elicitErr);
|
|
13044
|
+
logger$5.warn({
|
|
13045
|
+
step: step.name,
|
|
13046
|
+
err: elicitMsg
|
|
13047
|
+
}, "Step elicitation threw an error — continuing without elicitation");
|
|
13048
|
+
}
|
|
13049
|
+
const stepResult = {
|
|
13050
|
+
name: step.name,
|
|
13051
|
+
success: true,
|
|
13052
|
+
parsed,
|
|
13053
|
+
error: null,
|
|
13054
|
+
tokenUsage
|
|
13055
|
+
};
|
|
13056
|
+
if (artifactId !== void 0) stepResult.artifactId = artifactId;
|
|
13057
|
+
if (stepElicitationTokens !== void 0) stepResult.elicitationTokenUsage = stepElicitationTokens;
|
|
13058
|
+
stepResults.push(stepResult);
|
|
13059
|
+
} catch (err) {
|
|
13060
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13061
|
+
const errorMsg = `Step '${step.name}' unexpected error: ${message}`;
|
|
13062
|
+
stepResults.push({
|
|
13063
|
+
name: step.name,
|
|
13064
|
+
success: false,
|
|
13065
|
+
parsed: null,
|
|
13066
|
+
error: errorMsg,
|
|
13067
|
+
tokenUsage: {
|
|
13068
|
+
input: 0,
|
|
13069
|
+
output: 0
|
|
13070
|
+
}
|
|
13071
|
+
});
|
|
13072
|
+
return {
|
|
13073
|
+
success: false,
|
|
13074
|
+
steps: stepResults,
|
|
13075
|
+
tokenUsage: {
|
|
13076
|
+
input: totalInput,
|
|
13077
|
+
output: totalOutput
|
|
13078
|
+
},
|
|
13079
|
+
elicitationTokenUsage: {
|
|
13080
|
+
input: totalElicitationInput,
|
|
13081
|
+
output: totalElicitationOutput
|
|
13082
|
+
},
|
|
13083
|
+
error: errorMsg
|
|
13084
|
+
};
|
|
13085
|
+
}
|
|
13086
|
+
return {
|
|
13087
|
+
success: true,
|
|
13088
|
+
steps: stepResults,
|
|
13089
|
+
tokenUsage: {
|
|
13090
|
+
input: totalInput,
|
|
13091
|
+
output: totalOutput
|
|
13092
|
+
},
|
|
13093
|
+
elicitationTokenUsage: {
|
|
13094
|
+
input: totalElicitationInput,
|
|
13095
|
+
output: totalElicitationOutput
|
|
13096
|
+
}
|
|
13097
|
+
};
|
|
13098
|
+
}
|
|
12204
13099
|
|
|
12205
13100
|
//#endregion
|
|
12206
13101
|
//#region src/modules/phase-orchestrator/phases/analysis.ts
|
|
@@ -12877,64 +13772,43 @@ async function runPlanningPhase(deps, params) {
|
|
|
12877
13772
|
}
|
|
12878
13773
|
|
|
12879
13774
|
//#endregion
|
|
12880
|
-
//#region src/modules/
|
|
13775
|
+
//#region src/modules/phase-orchestrator/schemas/readiness-output.ts
|
|
12881
13776
|
/**
|
|
12882
|
-
*
|
|
13777
|
+
* A single finding identified by the readiness check agent.
|
|
12883
13778
|
*/
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
* - If fail and no retries remain → return `{ action: 'warn', ... }`
|
|
12901
|
-
*/
|
|
12902
|
-
evaluate(output) {
|
|
12903
|
-
const evaluation = this.config.evaluator(output);
|
|
12904
|
-
if (evaluation.pass) return {
|
|
12905
|
-
action: "proceed",
|
|
12906
|
-
issues: evaluation.issues,
|
|
12907
|
-
retriesRemaining: this.config.maxRetries - this._retryCount,
|
|
12908
|
-
result: output
|
|
12909
|
-
};
|
|
12910
|
-
const retriesRemaining = this.config.maxRetries - this._retryCount;
|
|
12911
|
-
if (retriesRemaining > 0) {
|
|
12912
|
-
this._retryCount += 1;
|
|
12913
|
-
return {
|
|
12914
|
-
action: "retry",
|
|
12915
|
-
issues: evaluation.issues,
|
|
12916
|
-
retriesRemaining: retriesRemaining - 1
|
|
12917
|
-
};
|
|
12918
|
-
}
|
|
12919
|
-
return {
|
|
12920
|
-
action: "warn",
|
|
12921
|
-
issues: evaluation.issues,
|
|
12922
|
-
retriesRemaining: 0
|
|
12923
|
-
};
|
|
12924
|
-
}
|
|
12925
|
-
/**
|
|
12926
|
-
* Reset the retry counter to 0, allowing re-use of this gate.
|
|
12927
|
-
*/
|
|
12928
|
-
reset() {
|
|
12929
|
-
this._retryCount = 0;
|
|
12930
|
-
}
|
|
12931
|
-
};
|
|
13779
|
+
const ReadinessFindingSchema = z.object({
|
|
13780
|
+
category: z.enum([
|
|
13781
|
+
"fr_coverage",
|
|
13782
|
+
"architecture_compliance",
|
|
13783
|
+
"story_quality",
|
|
13784
|
+
"ux_alignment",
|
|
13785
|
+
"dependency_validity"
|
|
13786
|
+
]),
|
|
13787
|
+
severity: z.enum([
|
|
13788
|
+
"blocker",
|
|
13789
|
+
"major",
|
|
13790
|
+
"minor"
|
|
13791
|
+
]),
|
|
13792
|
+
description: z.string().min(1),
|
|
13793
|
+
affected_items: z.array(z.string()).default([])
|
|
13794
|
+
});
|
|
12932
13795
|
/**
|
|
12933
|
-
*
|
|
13796
|
+
* Full output schema for the readiness check agent.
|
|
13797
|
+
*
|
|
13798
|
+
* The agent must emit a YAML block matching this schema.
|
|
13799
|
+
* - READY: all FRs covered, no blockers, acceptable quality — pipeline may proceed
|
|
13800
|
+
* - NEEDS_WORK: gaps identified but fixable via targeted story regeneration
|
|
13801
|
+
* - NOT_READY: critical failures that cannot be auto-remediated
|
|
12934
13802
|
*/
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
13803
|
+
const ReadinessOutputSchema = z.object({
|
|
13804
|
+
verdict: z.enum([
|
|
13805
|
+
"READY",
|
|
13806
|
+
"NEEDS_WORK",
|
|
13807
|
+
"NOT_READY"
|
|
13808
|
+
]),
|
|
13809
|
+
coverage_score: z.number().min(0).max(100),
|
|
13810
|
+
findings: z.array(ReadinessFindingSchema).default([])
|
|
13811
|
+
});
|
|
12938
13812
|
|
|
12939
13813
|
//#endregion
|
|
12940
13814
|
//#region src/modules/phase-orchestrator/phases/solutioning.ts
|
|
@@ -12956,6 +13830,12 @@ const STORY_REQUIREMENTS_PLACEHOLDER = "{{requirements}}";
|
|
|
12956
13830
|
const STORY_ARCHITECTURE_PLACEHOLDER = "{{architecture_decisions}}";
|
|
12957
13831
|
/** Gap analysis placeholder used in retry prompt */
|
|
12958
13832
|
const GAP_ANALYSIS_PLACEHOLDER = "{{gap_analysis}}";
|
|
13833
|
+
/** Placeholders in readiness-check prompt template */
|
|
13834
|
+
const READINESS_FR_PLACEHOLDER = "{{functional_requirements}}";
|
|
13835
|
+
const READINESS_NFR_PLACEHOLDER = "{{non_functional_requirements}}";
|
|
13836
|
+
const READINESS_ARCH_PLACEHOLDER = STORY_ARCHITECTURE_PLACEHOLDER;
|
|
13837
|
+
const READINESS_STORIES_PLACEHOLDER = "{{stories}}";
|
|
13838
|
+
const READINESS_UX_PLACEHOLDER = "{{ux_decisions}}";
|
|
12959
13839
|
/**
|
|
12960
13840
|
* Format functional and non-functional requirements from the planning phase
|
|
12961
13841
|
* into a compact text block suitable for prompt injection.
|
|
@@ -13227,73 +14107,146 @@ async function runStoryGeneration(deps, params, gapAnalysis) {
|
|
|
13227
14107
|
};
|
|
13228
14108
|
}
|
|
13229
14109
|
/**
|
|
13230
|
-
*
|
|
13231
|
-
*
|
|
13232
|
-
* For each functional requirement from the planning phase, checks if at least
|
|
13233
|
-
* one solutioning-phase story references it (simple substring keyword match).
|
|
13234
|
-
*
|
|
13235
|
-
* @param deps - Shared phase dependencies
|
|
13236
|
-
* @param runId - Pipeline run ID to scope the query
|
|
13237
|
-
* @returns Object with `passed` boolean and optional `gaps` array (uncovered FRs)
|
|
14110
|
+
* Format functional requirements from pre-fetched planning phase decisions for prompt injection.
|
|
14111
|
+
* Accepts pre-fetched planning decisions to avoid duplicate DB queries (shared with formatNFRsForReadiness).
|
|
13238
14112
|
*/
|
|
13239
|
-
|
|
13240
|
-
const { db } = deps;
|
|
13241
|
-
const planningDecisions = getDecisionsByPhaseForRun(db, runId, "planning");
|
|
14113
|
+
function formatFRsForReadiness(planningDecisions) {
|
|
13242
14114
|
const frDecisions = planningDecisions.filter((d) => d.category === "functional-requirements");
|
|
13243
|
-
|
|
13244
|
-
|
|
14115
|
+
if (frDecisions.length === 0) return "(No functional requirements found)";
|
|
14116
|
+
const lines = [];
|
|
14117
|
+
for (const [i, d] of frDecisions.entries()) try {
|
|
13245
14118
|
const fr = JSON.parse(d.value);
|
|
13246
|
-
|
|
14119
|
+
lines.push(`- [${d.key ?? `FR-${i}`}] [${fr.priority?.toUpperCase() ?? "MUST"}] ${fr.description}`);
|
|
14120
|
+
} catch {
|
|
14121
|
+
lines.push(`- [${d.key ?? `FR-${i}`}] ${d.value}`);
|
|
14122
|
+
}
|
|
14123
|
+
return lines.join("\n");
|
|
14124
|
+
}
|
|
14125
|
+
/**
|
|
14126
|
+
* Format non-functional requirements from pre-fetched planning phase decisions for prompt injection.
|
|
14127
|
+
* Accepts pre-fetched planning decisions to avoid duplicate DB queries (shared with formatFRsForReadiness).
|
|
14128
|
+
*/
|
|
14129
|
+
function formatNFRsForReadiness(planningDecisions) {
|
|
14130
|
+
const nfrDecisions = planningDecisions.filter((d) => d.category === "non-functional-requirements");
|
|
14131
|
+
if (nfrDecisions.length === 0) return "(No non-functional requirements found)";
|
|
14132
|
+
const lines = [];
|
|
14133
|
+
for (const d of nfrDecisions) try {
|
|
14134
|
+
const nfr = JSON.parse(d.value);
|
|
14135
|
+
lines.push(`- [${nfr.category?.toUpperCase() ?? "NFR"}] ${nfr.description}`);
|
|
13247
14136
|
} catch {
|
|
13248
|
-
|
|
14137
|
+
lines.push(`- ${d.value}`);
|
|
13249
14138
|
}
|
|
14139
|
+
return lines.join("\n");
|
|
14140
|
+
}
|
|
14141
|
+
/**
|
|
14142
|
+
* Format all stories from solutioning phase for prompt injection.
|
|
14143
|
+
*/
|
|
14144
|
+
function formatStoriesForReadiness(db, runId) {
|
|
13250
14145
|
const solutioningDecisions = getDecisionsByPhaseForRun(db, runId, "solutioning");
|
|
13251
14146
|
const storyDecisions = solutioningDecisions.filter((d) => d.category === "stories");
|
|
13252
|
-
|
|
14147
|
+
if (storyDecisions.length === 0) return "(No stories found)";
|
|
14148
|
+
const lines = [];
|
|
13253
14149
|
for (const d of storyDecisions) try {
|
|
13254
14150
|
const story = JSON.parse(d.value);
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
14151
|
+
lines.push(`### Story ${story.key ?? d.key}: ${story.title ?? "(untitled)"}`);
|
|
14152
|
+
lines.push(`**Priority**: ${story.priority ?? "must"}`);
|
|
14153
|
+
lines.push(`**Description**: ${story.description ?? ""}`);
|
|
14154
|
+
const acList = story.acceptance_criteria ?? story.ac;
|
|
14155
|
+
if (acList && acList.length > 0) {
|
|
14156
|
+
lines.push("**Acceptance Criteria**:");
|
|
14157
|
+
for (const ac of acList) lines.push(` - ${ac}`);
|
|
14158
|
+
}
|
|
14159
|
+
lines.push("");
|
|
13259
14160
|
} catch {
|
|
13260
|
-
|
|
13261
|
-
|
|
13262
|
-
|
|
13263
|
-
|
|
14161
|
+
lines.push(`### Story ${d.key}: ${d.value}`);
|
|
14162
|
+
lines.push("");
|
|
14163
|
+
}
|
|
14164
|
+
return lines.join("\n");
|
|
14165
|
+
}
|
|
14166
|
+
/**
|
|
14167
|
+
* Format UX decisions from the UX design phase (if any) for prompt injection.
|
|
14168
|
+
*/
|
|
14169
|
+
function formatUxDecisionsForReadiness(db, runId) {
|
|
14170
|
+
const uxDecisions = getDecisionsByPhaseForRun(db, runId, "ux-design");
|
|
14171
|
+
if (uxDecisions.length === 0) return "";
|
|
14172
|
+
const lines = ["### UX Design Decisions"];
|
|
14173
|
+
for (const d of uxDecisions) lines.push(`- **${d.key}** [${d.category}]: ${d.value}`);
|
|
14174
|
+
return lines.join("\n");
|
|
14175
|
+
}
|
|
14176
|
+
/**
|
|
14177
|
+
* Run the adversarial readiness check by dispatching a sub-agent.
|
|
14178
|
+
*
|
|
14179
|
+
* Assembles comprehensive context (FRs, NFRs, architecture decisions, stories,
|
|
14180
|
+
* optional UX decisions) and dispatches a readiness-check agent to perform a
|
|
14181
|
+
* proper adversarial review — replacing the old QualityGate keyword matcher.
|
|
14182
|
+
*
|
|
14183
|
+
* @param deps - Shared phase dependencies
|
|
14184
|
+
* @param runId - Pipeline run ID to scope the query
|
|
14185
|
+
* @returns Readiness check result with verdict, findings, and coverage score
|
|
14186
|
+
*/
|
|
14187
|
+
async function runReadinessCheck(deps, runId) {
|
|
14188
|
+
const { db, pack, dispatcher } = deps;
|
|
14189
|
+
const zeroTokenUsage = {
|
|
14190
|
+
input: 0,
|
|
14191
|
+
output: 0
|
|
14192
|
+
};
|
|
14193
|
+
let template;
|
|
14194
|
+
try {
|
|
14195
|
+
template = await pack.getPrompt("readiness-check");
|
|
14196
|
+
} catch {
|
|
14197
|
+
return {
|
|
14198
|
+
verdict: "error",
|
|
14199
|
+
error: "readiness-check prompt template not found in methodology pack",
|
|
14200
|
+
tokenUsage: zeroTokenUsage
|
|
14201
|
+
};
|
|
13264
14202
|
}
|
|
13265
|
-
const
|
|
13266
|
-
|
|
13267
|
-
|
|
14203
|
+
const planningDecisions = getDecisionsByPhaseForRun(db, runId, "planning");
|
|
14204
|
+
const formattedFRs = formatFRsForReadiness(planningDecisions);
|
|
14205
|
+
const formattedNFRs = formatNFRsForReadiness(planningDecisions);
|
|
14206
|
+
const formattedArchitecture = formatArchitectureDecisions(db, runId);
|
|
14207
|
+
const formattedStories = formatStoriesForReadiness(db, runId);
|
|
14208
|
+
const formattedUx = formatUxDecisionsForReadiness(db, runId);
|
|
14209
|
+
let prompt = template.replace(READINESS_FR_PLACEHOLDER, formattedFRs).replace(READINESS_NFR_PLACEHOLDER, formattedNFRs).replace(READINESS_ARCH_PLACEHOLDER, formattedArchitecture).replace(READINESS_STORIES_PLACEHOLDER, formattedStories);
|
|
14210
|
+
if (formattedUx) prompt = prompt.replace(READINESS_UX_PLACEHOLDER, formattedUx);
|
|
14211
|
+
else prompt = prompt.replace(READINESS_UX_PLACEHOLDER, "");
|
|
14212
|
+
const handle = dispatcher.dispatch({
|
|
14213
|
+
prompt,
|
|
14214
|
+
agent: "claude-code",
|
|
14215
|
+
taskType: "readiness-check",
|
|
14216
|
+
outputSchema: ReadinessOutputSchema
|
|
14217
|
+
});
|
|
14218
|
+
const dispatchResult = await handle.result;
|
|
14219
|
+
const tokenEstimate = dispatchResult.tokenEstimate;
|
|
14220
|
+
const tokenUsage = {
|
|
14221
|
+
input: tokenEstimate.input,
|
|
14222
|
+
output: tokenEstimate.output
|
|
14223
|
+
};
|
|
14224
|
+
logger$4.info({
|
|
14225
|
+
runId,
|
|
14226
|
+
durationMs: dispatchResult.durationMs,
|
|
14227
|
+
tokens: tokenEstimate
|
|
14228
|
+
}, "Readiness check dispatch completed");
|
|
14229
|
+
if (dispatchResult.status === "timeout") return {
|
|
14230
|
+
verdict: "error",
|
|
14231
|
+
error: `Readiness check agent timed out after ${dispatchResult.durationMs}ms`,
|
|
14232
|
+
tokenUsage
|
|
14233
|
+
};
|
|
14234
|
+
if (dispatchResult.status === "failed") return {
|
|
14235
|
+
verdict: "error",
|
|
14236
|
+
error: `Readiness check dispatch failed: ${dispatchResult.parseError ?? dispatchResult.output}`,
|
|
14237
|
+
tokenUsage
|
|
14238
|
+
};
|
|
14239
|
+
if (dispatchResult.parsed === null || dispatchResult.parseError !== null) return {
|
|
14240
|
+
verdict: "error",
|
|
14241
|
+
error: `Readiness check schema validation failed: ${dispatchResult.parseError ?? "No parsed output"}`,
|
|
14242
|
+
tokenUsage
|
|
13268
14243
|
};
|
|
13269
|
-
const
|
|
13270
|
-
name: "solutioning-readiness",
|
|
13271
|
-
maxRetries: 0,
|
|
13272
|
-
evaluator: (output) => {
|
|
13273
|
-
const data = output;
|
|
13274
|
-
const gaps$1 = [];
|
|
13275
|
-
for (const fr of data.functionalRequirements) {
|
|
13276
|
-
const frLower = fr.toLowerCase();
|
|
13277
|
-
const frKeywords = frLower.split(/\s+/).filter((w) => w.length > 4);
|
|
13278
|
-
const covered = data.stories.some((story) => {
|
|
13279
|
-
const storyText = [story.description, ...story.ac].join(" ").toLowerCase();
|
|
13280
|
-
return frKeywords.some((kw) => storyText.includes(kw)) || storyText.includes(frLower);
|
|
13281
|
-
});
|
|
13282
|
-
if (!covered) gaps$1.push(fr);
|
|
13283
|
-
}
|
|
13284
|
-
return {
|
|
13285
|
-
pass: gaps$1.length === 0,
|
|
13286
|
-
issues: gaps$1.map((g) => `Uncovered FR: ${g}`),
|
|
13287
|
-
severity: gaps$1.length === 0 ? "info" : "error"
|
|
13288
|
-
};
|
|
13289
|
-
}
|
|
13290
|
-
});
|
|
13291
|
-
const gateResult = gate.evaluate(coverageData);
|
|
13292
|
-
if (gateResult.action === "proceed") return { passed: true };
|
|
13293
|
-
const gaps = gateResult.issues.map((issue) => issue.replace(/^Uncovered FR: /, ""));
|
|
14244
|
+
const parsed = dispatchResult.parsed;
|
|
13294
14245
|
return {
|
|
13295
|
-
|
|
13296
|
-
|
|
14246
|
+
verdict: parsed.verdict,
|
|
14247
|
+
findings: parsed.findings ?? [],
|
|
14248
|
+
coverageScore: parsed.coverage_score,
|
|
14249
|
+
tokenUsage
|
|
13297
14250
|
};
|
|
13298
14251
|
}
|
|
13299
14252
|
/**
|
|
@@ -13579,65 +14532,228 @@ async function runSolutioningPhase(deps, params) {
|
|
|
13579
14532
|
}
|
|
13580
14533
|
};
|
|
13581
14534
|
const readinessResult = await runReadinessCheck(deps, params.runId);
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
|
|
13586
|
-
|
|
13587
|
-
|
|
13588
|
-
|
|
13589
|
-
|
|
13590
|
-
].join("\n");
|
|
13591
|
-
const retryResult = await runStoryGeneration(deps, params, gapAnalysis);
|
|
13592
|
-
totalInput += retryResult.tokenUsage.input;
|
|
13593
|
-
totalOutput += retryResult.tokenUsage.output;
|
|
13594
|
-
if ("error" in retryResult) return {
|
|
14535
|
+
totalInput += readinessResult.tokenUsage.input;
|
|
14536
|
+
totalOutput += readinessResult.tokenUsage.output;
|
|
14537
|
+
if (readinessResult.verdict === "error") {
|
|
14538
|
+
logger$4.error({
|
|
14539
|
+
runId: params.runId,
|
|
14540
|
+
error: readinessResult.error
|
|
14541
|
+
}, "Readiness check agent failed");
|
|
14542
|
+
return {
|
|
13595
14543
|
result: "failed",
|
|
13596
|
-
error: "
|
|
13597
|
-
details:
|
|
14544
|
+
error: "readiness_check_error",
|
|
14545
|
+
details: readinessResult.error,
|
|
13598
14546
|
readiness_passed: false,
|
|
13599
|
-
gaps,
|
|
13600
14547
|
artifact_ids: [archResult.artifactId, storyResult.artifactId],
|
|
13601
14548
|
tokenUsage: {
|
|
13602
14549
|
input: totalInput,
|
|
13603
14550
|
output: totalOutput
|
|
13604
14551
|
}
|
|
13605
14552
|
};
|
|
13606
|
-
|
|
13607
|
-
|
|
14553
|
+
}
|
|
14554
|
+
logger$4.info({
|
|
14555
|
+
runId: params.runId,
|
|
14556
|
+
verdict: readinessResult.verdict,
|
|
14557
|
+
coverageScore: readinessResult.coverageScore,
|
|
14558
|
+
findingCount: readinessResult.findings.length
|
|
14559
|
+
}, "Readiness check verdict received");
|
|
14560
|
+
if (readinessResult.verdict === "NOT_READY") {
|
|
14561
|
+
const blockers = readinessResult.findings.filter((f) => f.severity === "blocker");
|
|
14562
|
+
const majorFindings = readinessResult.findings.filter((f) => f.severity === "major");
|
|
14563
|
+
for (const [i, finding] of readinessResult.findings.entries()) upsertDecision(deps.db, {
|
|
14564
|
+
pipeline_run_id: params.runId,
|
|
14565
|
+
phase: "solutioning",
|
|
14566
|
+
category: "readiness-findings",
|
|
14567
|
+
key: `finding-${i + 1}`,
|
|
14568
|
+
value: JSON.stringify(finding)
|
|
14569
|
+
});
|
|
14570
|
+
logger$4.error({
|
|
14571
|
+
runId: params.runId,
|
|
14572
|
+
verdict: "NOT_READY",
|
|
14573
|
+
coverageScore: readinessResult.coverageScore,
|
|
14574
|
+
blockers: blockers.length,
|
|
14575
|
+
major: majorFindings.length,
|
|
14576
|
+
findings: readinessResult.findings
|
|
14577
|
+
}, "Readiness check returned NOT_READY — solutioning phase failed");
|
|
14578
|
+
if (deps.eventBus) {
|
|
14579
|
+
deps.eventBus.emit("solutioning:readiness-check", {
|
|
14580
|
+
runId: params.runId,
|
|
14581
|
+
verdict: "NOT_READY",
|
|
14582
|
+
coverageScore: readinessResult.coverageScore,
|
|
14583
|
+
findingCount: readinessResult.findings.length,
|
|
14584
|
+
blockerCount: blockers.length
|
|
14585
|
+
});
|
|
14586
|
+
deps.eventBus.emit("solutioning:readiness-failed", {
|
|
14587
|
+
runId: params.runId,
|
|
14588
|
+
verdict: "NOT_READY",
|
|
14589
|
+
coverageScore: readinessResult.coverageScore,
|
|
14590
|
+
findings: readinessResult.findings.map((f) => ({
|
|
14591
|
+
category: f.category,
|
|
14592
|
+
severity: f.severity,
|
|
14593
|
+
description: f.description,
|
|
14594
|
+
affected_items: f.affected_items
|
|
14595
|
+
}))
|
|
14596
|
+
});
|
|
14597
|
+
}
|
|
14598
|
+
return {
|
|
13608
14599
|
result: "failed",
|
|
13609
|
-
error: "
|
|
13610
|
-
details:
|
|
14600
|
+
error: "readiness_not_ready",
|
|
14601
|
+
details: `Readiness check returned NOT_READY: ${blockers.length} blockers, coverage score ${readinessResult.coverageScore}%`,
|
|
13611
14602
|
readiness_passed: false,
|
|
13612
|
-
gaps:
|
|
13613
|
-
artifact_ids: [
|
|
13614
|
-
archResult.artifactId,
|
|
13615
|
-
storyResult.artifactId,
|
|
13616
|
-
retryResult.artifactId
|
|
13617
|
-
],
|
|
14603
|
+
gaps: readinessResult.findings.filter((f) => f.category === "fr_coverage").map((f) => f.description),
|
|
14604
|
+
artifact_ids: [archResult.artifactId, storyResult.artifactId],
|
|
13618
14605
|
tokenUsage: {
|
|
13619
14606
|
input: totalInput,
|
|
13620
14607
|
output: totalOutput
|
|
13621
14608
|
}
|
|
13622
14609
|
};
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
|
|
13637
|
-
|
|
14610
|
+
}
|
|
14611
|
+
if (readinessResult.verdict === "NEEDS_WORK") {
|
|
14612
|
+
const blockers = readinessResult.findings.filter((f) => f.severity === "blocker");
|
|
14613
|
+
if (blockers.length > 0) {
|
|
14614
|
+
for (const [i, finding] of readinessResult.findings.entries()) upsertDecision(deps.db, {
|
|
14615
|
+
pipeline_run_id: params.runId,
|
|
14616
|
+
phase: "solutioning",
|
|
14617
|
+
category: "readiness-findings",
|
|
14618
|
+
key: `finding-${i + 1}`,
|
|
14619
|
+
value: JSON.stringify(finding)
|
|
14620
|
+
});
|
|
14621
|
+
if (deps.eventBus) deps.eventBus.emit("solutioning:readiness-check", {
|
|
14622
|
+
runId: params.runId,
|
|
14623
|
+
verdict: "NEEDS_WORK",
|
|
14624
|
+
coverageScore: readinessResult.coverageScore,
|
|
14625
|
+
findingCount: readinessResult.findings.length,
|
|
14626
|
+
blockerCount: blockers.length
|
|
14627
|
+
});
|
|
14628
|
+
const gapAnalysis = [
|
|
14629
|
+
"## Gap Analysis: Readiness Check Blocker Findings",
|
|
14630
|
+
"The readiness check identified the following blocker issues that must be addressed:",
|
|
14631
|
+
"",
|
|
14632
|
+
...blockers.map((f) => [`### [${f.category.toUpperCase()}] ${f.description}`, f.affected_items.length > 0 ? `Affected: ${f.affected_items.join(", ")}` : ""].filter(Boolean).join("\n")),
|
|
14633
|
+
"",
|
|
14634
|
+
"Please generate additional or revised stories to specifically address each blocker above."
|
|
14635
|
+
].join("\n");
|
|
14636
|
+
logger$4.info({
|
|
14637
|
+
runId: params.runId,
|
|
14638
|
+
blockerCount: blockers.length
|
|
14639
|
+
}, "Readiness NEEDS_WORK with blockers — retrying story generation with gap analysis");
|
|
14640
|
+
const retryResult = await runStoryGeneration(deps, params, gapAnalysis);
|
|
14641
|
+
totalInput += retryResult.tokenUsage.input;
|
|
14642
|
+
totalOutput += retryResult.tokenUsage.output;
|
|
14643
|
+
if ("error" in retryResult) return {
|
|
14644
|
+
result: "failed",
|
|
14645
|
+
error: "story_generation_retry_failed",
|
|
14646
|
+
details: retryResult.error,
|
|
14647
|
+
readiness_passed: false,
|
|
14648
|
+
gaps: blockers.map((f) => f.description),
|
|
14649
|
+
artifact_ids: [archResult.artifactId, storyResult.artifactId],
|
|
14650
|
+
tokenUsage: {
|
|
14651
|
+
input: totalInput,
|
|
14652
|
+
output: totalOutput
|
|
14653
|
+
}
|
|
14654
|
+
};
|
|
14655
|
+
const retryReadiness = await runReadinessCheck(deps, params.runId);
|
|
14656
|
+
totalInput += retryReadiness.tokenUsage.input;
|
|
14657
|
+
totalOutput += retryReadiness.tokenUsage.output;
|
|
14658
|
+
if (retryReadiness.verdict === "error") return {
|
|
14659
|
+
result: "failed",
|
|
14660
|
+
error: "readiness_check_error",
|
|
14661
|
+
details: retryReadiness.error,
|
|
14662
|
+
readiness_passed: false,
|
|
14663
|
+
artifact_ids: [
|
|
14664
|
+
archResult.artifactId,
|
|
14665
|
+
storyResult.artifactId,
|
|
14666
|
+
retryResult.artifactId
|
|
14667
|
+
],
|
|
14668
|
+
tokenUsage: {
|
|
14669
|
+
input: totalInput,
|
|
14670
|
+
output: totalOutput
|
|
14671
|
+
}
|
|
14672
|
+
};
|
|
14673
|
+
if (retryReadiness.verdict === "NOT_READY" || retryReadiness.verdict === "NEEDS_WORK") {
|
|
14674
|
+
const retryBlockers = retryReadiness.findings.filter((f) => f.severity === "blocker");
|
|
14675
|
+
logger$4.error({
|
|
14676
|
+
runId: params.runId,
|
|
14677
|
+
verdict: retryReadiness.verdict,
|
|
14678
|
+
retryBlockers: retryBlockers.length
|
|
14679
|
+
}, "Readiness check failed after maximum retries");
|
|
14680
|
+
return {
|
|
14681
|
+
result: "failed",
|
|
14682
|
+
error: "readiness_check_failed",
|
|
14683
|
+
details: `Readiness check failed after maximum retries: verdict=${retryReadiness.verdict}, coverage=${retryReadiness.coverageScore}%`,
|
|
14684
|
+
readiness_passed: false,
|
|
14685
|
+
gaps: retryReadiness.findings.filter((f) => f.category === "fr_coverage").map((f) => f.description),
|
|
14686
|
+
artifact_ids: [
|
|
14687
|
+
archResult.artifactId,
|
|
14688
|
+
storyResult.artifactId,
|
|
14689
|
+
retryResult.artifactId
|
|
14690
|
+
],
|
|
14691
|
+
tokenUsage: {
|
|
14692
|
+
input: totalInput,
|
|
14693
|
+
output: totalOutput
|
|
14694
|
+
}
|
|
14695
|
+
};
|
|
13638
14696
|
}
|
|
13639
|
-
|
|
14697
|
+
const retryStories = retryResult.epics.reduce((sum, epic) => sum + epic.stories.length, 0);
|
|
14698
|
+
const minorFindings$1 = retryReadiness.findings.filter((f) => f.severity === "minor");
|
|
14699
|
+
if (minorFindings$1.length > 0) logger$4.warn({
|
|
14700
|
+
runId: params.runId,
|
|
14701
|
+
minorFindings: minorFindings$1
|
|
14702
|
+
}, "Readiness READY with minor findings after retry");
|
|
14703
|
+
if (deps.eventBus) deps.eventBus.emit("solutioning:readiness-check", {
|
|
14704
|
+
runId: params.runId,
|
|
14705
|
+
verdict: "READY",
|
|
14706
|
+
coverageScore: retryReadiness.coverageScore,
|
|
14707
|
+
findingCount: retryReadiness.findings.length,
|
|
14708
|
+
blockerCount: 0
|
|
14709
|
+
});
|
|
14710
|
+
return {
|
|
14711
|
+
result: "success",
|
|
14712
|
+
architecture_decisions: archResult.decisions.length,
|
|
14713
|
+
epics: retryResult.epics.length,
|
|
14714
|
+
stories: retryStories,
|
|
14715
|
+
readiness_passed: true,
|
|
14716
|
+
artifact_ids: [
|
|
14717
|
+
archResult.artifactId,
|
|
14718
|
+
storyResult.artifactId,
|
|
14719
|
+
retryResult.artifactId
|
|
14720
|
+
],
|
|
14721
|
+
tokenUsage: {
|
|
14722
|
+
input: totalInput,
|
|
14723
|
+
output: totalOutput
|
|
14724
|
+
}
|
|
14725
|
+
};
|
|
14726
|
+
}
|
|
14727
|
+
const majorFindings = readinessResult.findings.filter((f) => f.severity === "major");
|
|
14728
|
+
logger$4.warn({
|
|
14729
|
+
runId: params.runId,
|
|
14730
|
+
majorCount: majorFindings.length,
|
|
14731
|
+
findings: readinessResult.findings
|
|
14732
|
+
}, "Readiness NEEDS_WORK (no blockers) — proceeding with warnings");
|
|
14733
|
+
if (deps.eventBus) deps.eventBus.emit("solutioning:readiness-check", {
|
|
14734
|
+
runId: params.runId,
|
|
14735
|
+
verdict: "NEEDS_WORK",
|
|
14736
|
+
coverageScore: readinessResult.coverageScore,
|
|
14737
|
+
findingCount: readinessResult.findings.length,
|
|
14738
|
+
blockerCount: 0
|
|
14739
|
+
});
|
|
14740
|
+
}
|
|
14741
|
+
const minorFindings = readinessResult.findings.filter((f) => f.severity === "minor");
|
|
14742
|
+
if (minorFindings.length > 0) {
|
|
14743
|
+
const verdictLabel = readinessResult.verdict === "READY" ? "READY" : "NEEDS_WORK (no blockers)";
|
|
14744
|
+
logger$4.warn({
|
|
14745
|
+
runId: params.runId,
|
|
14746
|
+
verdict: readinessResult.verdict,
|
|
14747
|
+
minorFindings
|
|
14748
|
+
}, `Readiness ${verdictLabel} with minor findings — proceeding`);
|
|
13640
14749
|
}
|
|
14750
|
+
if (readinessResult.verdict === "READY" && deps.eventBus) deps.eventBus.emit("solutioning:readiness-check", {
|
|
14751
|
+
runId: params.runId,
|
|
14752
|
+
verdict: "READY",
|
|
14753
|
+
coverageScore: readinessResult.coverageScore,
|
|
14754
|
+
findingCount: readinessResult.findings.length,
|
|
14755
|
+
blockerCount: 0
|
|
14756
|
+
});
|
|
13641
14757
|
const totalStories = storyResult.epics.reduce((sum, epic) => sum + epic.stories.length, 0);
|
|
13642
14758
|
return {
|
|
13643
14759
|
result: "success",
|
|
@@ -13664,6 +14780,222 @@ async function runSolutioningPhase(deps, params) {
|
|
|
13664
14780
|
}
|
|
13665
14781
|
}
|
|
13666
14782
|
|
|
14783
|
+
//#endregion
|
|
14784
|
+
//#region src/modules/phase-orchestrator/phases/ux-design.ts
|
|
14785
|
+
/**
|
|
14786
|
+
* Build step definitions for 3-step UX design decomposition.
|
|
14787
|
+
*
|
|
14788
|
+
* Step 1: Discovery + Core Experience
|
|
14789
|
+
* - Injects product brief and requirements context
|
|
14790
|
+
* - Produces: target_personas, core_experience, emotional_goals, inspiration_references
|
|
14791
|
+
*
|
|
14792
|
+
* Step 2: Design System + Visual Foundation
|
|
14793
|
+
* - Injects product brief, requirements, and Step 1 discoveries
|
|
14794
|
+
* - Produces: design_system, visual_foundation, design_principles, color_and_typography
|
|
14795
|
+
*
|
|
14796
|
+
* Step 3: User Journeys + Components + Accessibility
|
|
14797
|
+
* - Injects product brief, requirements, Step 1 discoveries, Step 2 design system
|
|
14798
|
+
* - Produces: user_journeys, component_strategy, ux_patterns, accessibility_guidelines
|
|
14799
|
+
* - Registers 'ux-design' artifact
|
|
14800
|
+
*/
|
|
14801
|
+
function buildUxDesignSteps() {
|
|
14802
|
+
return [
|
|
14803
|
+
{
|
|
14804
|
+
name: "ux-step-1-discovery",
|
|
14805
|
+
taskType: "ux-discovery",
|
|
14806
|
+
outputSchema: UxDiscoveryOutputSchema,
|
|
14807
|
+
context: [{
|
|
14808
|
+
placeholder: "product_brief",
|
|
14809
|
+
source: "decision:analysis.product-brief"
|
|
14810
|
+
}, {
|
|
14811
|
+
placeholder: "requirements",
|
|
14812
|
+
source: "decision:planning.functional-requirements"
|
|
14813
|
+
}],
|
|
14814
|
+
persist: [
|
|
14815
|
+
{
|
|
14816
|
+
field: "target_personas",
|
|
14817
|
+
category: "ux-design",
|
|
14818
|
+
key: "target_personas"
|
|
14819
|
+
},
|
|
14820
|
+
{
|
|
14821
|
+
field: "core_experience",
|
|
14822
|
+
category: "ux-design",
|
|
14823
|
+
key: "core_experience"
|
|
14824
|
+
},
|
|
14825
|
+
{
|
|
14826
|
+
field: "emotional_goals",
|
|
14827
|
+
category: "ux-design",
|
|
14828
|
+
key: "emotional_goals"
|
|
14829
|
+
},
|
|
14830
|
+
{
|
|
14831
|
+
field: "inspiration_references",
|
|
14832
|
+
category: "ux-design",
|
|
14833
|
+
key: "inspiration_references"
|
|
14834
|
+
}
|
|
14835
|
+
],
|
|
14836
|
+
elicitate: true,
|
|
14837
|
+
elicitationMethods: ["User Persona Focus Group", "SCAMPER"]
|
|
14838
|
+
},
|
|
14839
|
+
{
|
|
14840
|
+
name: "ux-step-2-design-system",
|
|
14841
|
+
taskType: "ux-design-system",
|
|
14842
|
+
outputSchema: UxDesignSystemOutputSchema,
|
|
14843
|
+
context: [
|
|
14844
|
+
{
|
|
14845
|
+
placeholder: "product_brief",
|
|
14846
|
+
source: "decision:analysis.product-brief"
|
|
14847
|
+
},
|
|
14848
|
+
{
|
|
14849
|
+
placeholder: "requirements",
|
|
14850
|
+
source: "decision:planning.functional-requirements"
|
|
14851
|
+
},
|
|
14852
|
+
{
|
|
14853
|
+
placeholder: "ux_discovery",
|
|
14854
|
+
source: "step:ux-step-1-discovery"
|
|
14855
|
+
}
|
|
14856
|
+
],
|
|
14857
|
+
persist: [
|
|
14858
|
+
{
|
|
14859
|
+
field: "design_system",
|
|
14860
|
+
category: "ux-design",
|
|
14861
|
+
key: "design_system"
|
|
14862
|
+
},
|
|
14863
|
+
{
|
|
14864
|
+
field: "visual_foundation",
|
|
14865
|
+
category: "ux-design",
|
|
14866
|
+
key: "visual_foundation"
|
|
14867
|
+
},
|
|
14868
|
+
{
|
|
14869
|
+
field: "design_principles",
|
|
14870
|
+
category: "ux-design",
|
|
14871
|
+
key: "design_principles"
|
|
14872
|
+
},
|
|
14873
|
+
{
|
|
14874
|
+
field: "color_and_typography",
|
|
14875
|
+
category: "ux-design",
|
|
14876
|
+
key: "color_and_typography"
|
|
14877
|
+
}
|
|
14878
|
+
],
|
|
14879
|
+
elicitate: true,
|
|
14880
|
+
elicitationMethods: ["SCAMPER", "Design Thinking"]
|
|
14881
|
+
},
|
|
14882
|
+
{
|
|
14883
|
+
name: "ux-step-3-journeys",
|
|
14884
|
+
taskType: "ux-journeys",
|
|
14885
|
+
outputSchema: UxJourneysOutputSchema,
|
|
14886
|
+
context: [
|
|
14887
|
+
{
|
|
14888
|
+
placeholder: "product_brief",
|
|
14889
|
+
source: "decision:analysis.product-brief"
|
|
14890
|
+
},
|
|
14891
|
+
{
|
|
14892
|
+
placeholder: "requirements",
|
|
14893
|
+
source: "decision:planning.functional-requirements"
|
|
14894
|
+
},
|
|
14895
|
+
{
|
|
14896
|
+
placeholder: "ux_discovery",
|
|
14897
|
+
source: "step:ux-step-1-discovery"
|
|
14898
|
+
},
|
|
14899
|
+
{
|
|
14900
|
+
placeholder: "design_system",
|
|
14901
|
+
source: "step:ux-step-2-design-system"
|
|
14902
|
+
}
|
|
14903
|
+
],
|
|
14904
|
+
persist: [
|
|
14905
|
+
{
|
|
14906
|
+
field: "user_journeys",
|
|
14907
|
+
category: "ux-design",
|
|
14908
|
+
key: "user_journeys"
|
|
14909
|
+
},
|
|
14910
|
+
{
|
|
14911
|
+
field: "component_strategy",
|
|
14912
|
+
category: "ux-design",
|
|
14913
|
+
key: "component_strategy"
|
|
14914
|
+
},
|
|
14915
|
+
{
|
|
14916
|
+
field: "ux_patterns",
|
|
14917
|
+
category: "ux-design",
|
|
14918
|
+
key: "ux_patterns"
|
|
14919
|
+
},
|
|
14920
|
+
{
|
|
14921
|
+
field: "accessibility_guidelines",
|
|
14922
|
+
category: "ux-design",
|
|
14923
|
+
key: "accessibility_guidelines"
|
|
14924
|
+
}
|
|
14925
|
+
],
|
|
14926
|
+
registerArtifact: {
|
|
14927
|
+
type: "ux-design",
|
|
14928
|
+
path: "decision-store://ux-design/ux-design",
|
|
14929
|
+
summarize: (parsed) => {
|
|
14930
|
+
const journeys = Array.isArray(parsed.user_journeys) ? parsed.user_journeys : void 0;
|
|
14931
|
+
const patterns = Array.isArray(parsed.ux_patterns) ? parsed.ux_patterns : void 0;
|
|
14932
|
+
const count = (journeys?.length ?? 0) + (patterns?.length ?? 0);
|
|
14933
|
+
return count > 0 ? `${count} UX decisions captured (journeys + patterns)` : "UX design complete";
|
|
14934
|
+
}
|
|
14935
|
+
},
|
|
14936
|
+
critique: true
|
|
14937
|
+
}
|
|
14938
|
+
];
|
|
14939
|
+
}
|
|
14940
|
+
/**
|
|
14941
|
+
* Execute the UX design phase of the BMAD pipeline.
|
|
14942
|
+
*
|
|
14943
|
+
* Runs 3 sequential steps covering discovery, design system, and user journeys.
|
|
14944
|
+
* Each step builds on prior step decisions via the decision store.
|
|
14945
|
+
*
|
|
14946
|
+
* On success, a 'ux-design' artifact is registered and UX decisions are
|
|
14947
|
+
* available to the architecture phase via `decision:ux-design.*`.
|
|
14948
|
+
*
|
|
14949
|
+
* @param deps - Shared phase dependencies (db, pack, contextCompiler, dispatcher)
|
|
14950
|
+
* @param params - Phase parameters (runId)
|
|
14951
|
+
* @returns UxDesignResult with success/failure status and token usage
|
|
14952
|
+
*/
|
|
14953
|
+
async function runUxDesignPhase(deps, params) {
|
|
14954
|
+
const { runId } = params;
|
|
14955
|
+
const zeroTokenUsage = {
|
|
14956
|
+
input: 0,
|
|
14957
|
+
output: 0
|
|
14958
|
+
};
|
|
14959
|
+
try {
|
|
14960
|
+
const steps = buildUxDesignSteps();
|
|
14961
|
+
const result = await runSteps(steps, deps, runId, "ux-design", {});
|
|
14962
|
+
if (!result.success) return {
|
|
14963
|
+
result: "failed",
|
|
14964
|
+
error: result.error ?? "ux_design_multi_step_failed",
|
|
14965
|
+
details: result.error ?? "UX design multi-step execution failed",
|
|
14966
|
+
tokenUsage: result.tokenUsage
|
|
14967
|
+
};
|
|
14968
|
+
const lastStep = result.steps[result.steps.length - 1];
|
|
14969
|
+
const artifactId = lastStep?.artifactId;
|
|
14970
|
+
if (!artifactId) {
|
|
14971
|
+
const artifact = registerArtifact(deps.db, {
|
|
14972
|
+
pipeline_run_id: runId,
|
|
14973
|
+
phase: "ux-design",
|
|
14974
|
+
type: "ux-design",
|
|
14975
|
+
path: "decision-store://ux-design/ux-design",
|
|
14976
|
+
summary: "UX design phase completed"
|
|
14977
|
+
});
|
|
14978
|
+
return {
|
|
14979
|
+
result: "success",
|
|
14980
|
+
artifact_id: artifact.id,
|
|
14981
|
+
tokenUsage: result.tokenUsage
|
|
14982
|
+
};
|
|
14983
|
+
}
|
|
14984
|
+
return {
|
|
14985
|
+
result: "success",
|
|
14986
|
+
artifact_id: artifactId,
|
|
14987
|
+
tokenUsage: result.tokenUsage
|
|
14988
|
+
};
|
|
14989
|
+
} catch (err) {
|
|
14990
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14991
|
+
return {
|
|
14992
|
+
result: "failed",
|
|
14993
|
+
error: message,
|
|
14994
|
+
tokenUsage: zeroTokenUsage
|
|
14995
|
+
};
|
|
14996
|
+
}
|
|
14997
|
+
}
|
|
14998
|
+
|
|
13667
14999
|
//#endregion
|
|
13668
15000
|
//#region src/modules/stop-after/types.ts
|
|
13669
15001
|
/**
|
|
@@ -14659,7 +15991,7 @@ function mapInternalPhaseToEventPhase(internalPhase) {
|
|
|
14659
15991
|
}
|
|
14660
15992
|
}
|
|
14661
15993
|
async function runAutoRun(options) {
|
|
14662
|
-
const { pack: packName, from: startPhase, stopAfter, concept: conceptArg, conceptFile, stories: storiesArg, concurrency, outputFormat, projectRoot, events: eventsFlag, verbose: verboseFlag, tui: tuiFlag } = options;
|
|
15994
|
+
const { pack: packName, from: startPhase, stopAfter, concept: conceptArg, conceptFile, stories: storiesArg, concurrency, outputFormat, projectRoot, events: eventsFlag, verbose: verboseFlag, tui: tuiFlag, skipUx } = options;
|
|
14663
15995
|
if (startPhase !== void 0 && !VALID_PHASES.includes(startPhase)) {
|
|
14664
15996
|
const errorMsg = `Invalid phase '${startPhase}'. Valid phases: ${VALID_PHASES.join(", ")}`;
|
|
14665
15997
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
@@ -14713,7 +16045,9 @@ async function runAutoRun(options) {
|
|
|
14713
16045
|
concept,
|
|
14714
16046
|
concurrency,
|
|
14715
16047
|
outputFormat,
|
|
14716
|
-
projectRoot
|
|
16048
|
+
projectRoot,
|
|
16049
|
+
...eventsFlag === true ? { events: true } : {},
|
|
16050
|
+
...skipUx === true ? { skipUx: true } : {}
|
|
14717
16051
|
});
|
|
14718
16052
|
let storyKeys = [];
|
|
14719
16053
|
if (storiesArg !== void 0 && storiesArg !== "") {
|
|
@@ -15071,6 +16405,29 @@ async function runAutoRun(options) {
|
|
|
15071
16405
|
else if (s.phase === "ESCALATED") if (s.error !== void 0) failedKeys.push(key);
|
|
15072
16406
|
else escalatedKeys.push(key);
|
|
15073
16407
|
else failedKeys.push(key);
|
|
16408
|
+
try {
|
|
16409
|
+
const runEndMs = Date.now();
|
|
16410
|
+
const runStartMs = new Date(pipelineRun.created_at).getTime();
|
|
16411
|
+
const tokenAgg = aggregateTokenUsageForRun(db, pipelineRun.id);
|
|
16412
|
+
writeRunMetrics(db, {
|
|
16413
|
+
run_id: pipelineRun.id,
|
|
16414
|
+
methodology: pack.manifest.name,
|
|
16415
|
+
status: failedKeys.length > 0 ? "failed" : "completed",
|
|
16416
|
+
started_at: pipelineRun.created_at,
|
|
16417
|
+
completed_at: new Date().toISOString(),
|
|
16418
|
+
wall_clock_seconds: Math.round((runEndMs - runStartMs) / 1e3),
|
|
16419
|
+
total_input_tokens: tokenAgg.input,
|
|
16420
|
+
total_output_tokens: tokenAgg.output,
|
|
16421
|
+
total_cost_usd: tokenAgg.cost,
|
|
16422
|
+
stories_attempted: storyKeys.length,
|
|
16423
|
+
stories_succeeded: succeededKeys.length,
|
|
16424
|
+
stories_failed: failedKeys.length,
|
|
16425
|
+
stories_escalated: escalatedKeys.length,
|
|
16426
|
+
concurrency_setting: concurrency
|
|
16427
|
+
});
|
|
16428
|
+
} catch (metricsErr) {
|
|
16429
|
+
logger$3.warn({ err: metricsErr }, "Failed to write run metrics (best-effort)");
|
|
16430
|
+
}
|
|
15074
16431
|
if (progressRenderer !== void 0) progressRenderer.render({
|
|
15075
16432
|
type: "pipeline:complete",
|
|
15076
16433
|
ts: new Date().toISOString(),
|
|
@@ -15123,7 +16480,7 @@ async function runAutoRun(options) {
|
|
|
15123
16480
|
}
|
|
15124
16481
|
}
|
|
15125
16482
|
async function runFullPipeline(options) {
|
|
15126
|
-
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot } = options;
|
|
16483
|
+
const { packName, packPath, dbDir, dbPath, startPhase, stopAfter, concept, concurrency, outputFormat, projectRoot, events: eventsFlag, skipUx } = options;
|
|
15127
16484
|
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
15128
16485
|
const dbWrapper = new DatabaseWrapper(dbPath);
|
|
15129
16486
|
try {
|
|
@@ -15163,9 +16520,16 @@ async function runFullPipeline(options) {
|
|
|
15163
16520
|
contextCompiler,
|
|
15164
16521
|
dispatcher
|
|
15165
16522
|
};
|
|
16523
|
+
const packForOrchestrator = skipUx === true && pack.manifest.uxDesign === true ? {
|
|
16524
|
+
...pack,
|
|
16525
|
+
manifest: {
|
|
16526
|
+
...pack.manifest,
|
|
16527
|
+
uxDesign: false
|
|
16528
|
+
}
|
|
16529
|
+
} : pack;
|
|
15166
16530
|
const phaseOrchestrator = createPhaseOrchestrator({
|
|
15167
16531
|
db,
|
|
15168
|
-
pack
|
|
16532
|
+
pack: packForOrchestrator
|
|
15169
16533
|
});
|
|
15170
16534
|
const startedAt = Date.now();
|
|
15171
16535
|
const runId = await phaseOrchestrator.startRun(concept ?? "", startPhase);
|
|
@@ -15173,7 +16537,14 @@ async function runFullPipeline(options) {
|
|
|
15173
16537
|
process.stdout.write(`Starting full pipeline from phase: ${startPhase}\n`);
|
|
15174
16538
|
process.stdout.write(`Pipeline run ID: ${runId}\n`);
|
|
15175
16539
|
}
|
|
15176
|
-
const
|
|
16540
|
+
const uxEnabled = packForOrchestrator.manifest.uxDesign === true;
|
|
16541
|
+
const phaseOrder = uxEnabled ? [
|
|
16542
|
+
"analysis",
|
|
16543
|
+
"planning",
|
|
16544
|
+
"ux-design",
|
|
16545
|
+
"solutioning",
|
|
16546
|
+
"implementation"
|
|
16547
|
+
] : [
|
|
15177
16548
|
"analysis",
|
|
15178
16549
|
"planning",
|
|
15179
16550
|
"solutioning",
|
|
@@ -15232,6 +16603,29 @@ async function runFullPipeline(options) {
|
|
|
15232
16603
|
process.stdout.write(`[PLANNING] Complete — ${result.requirements_count ?? 0} requirements, ${result.user_stories_count ?? 0} user stories\n`);
|
|
15233
16604
|
process.stdout.write(` Tokens: ${result.tokenUsage.input.toLocaleString()} input / ${result.tokenUsage.output.toLocaleString()} output\n`);
|
|
15234
16605
|
}
|
|
16606
|
+
} else if (currentPhase === "ux-design") {
|
|
16607
|
+
const result = await runUxDesignPhase(phaseDeps, { runId });
|
|
16608
|
+
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
16609
|
+
const costUsd = (result.tokenUsage.input * 3 + result.tokenUsage.output * 15) / 1e6;
|
|
16610
|
+
addTokenUsage(db, runId, {
|
|
16611
|
+
phase: "ux-design",
|
|
16612
|
+
agent: "claude-code",
|
|
16613
|
+
input_tokens: result.tokenUsage.input,
|
|
16614
|
+
output_tokens: result.tokenUsage.output,
|
|
16615
|
+
cost_usd: costUsd
|
|
16616
|
+
});
|
|
16617
|
+
}
|
|
16618
|
+
if (result.result === "failed") {
|
|
16619
|
+
updatePipelineRun(db, runId, { status: "failed" });
|
|
16620
|
+
const errorMsg = `UX design phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}`;
|
|
16621
|
+
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
16622
|
+
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
16623
|
+
return 1;
|
|
16624
|
+
}
|
|
16625
|
+
if (outputFormat === "human") {
|
|
16626
|
+
process.stdout.write(`[UX-DESIGN] Complete — UX design artifact registered (artifact: ${result.artifact_id ?? "n/a"})\n`);
|
|
16627
|
+
process.stdout.write(` Tokens: ${result.tokenUsage.input.toLocaleString()} input / ${result.tokenUsage.output.toLocaleString()} output\n`);
|
|
16628
|
+
}
|
|
15235
16629
|
} else if (currentPhase === "solutioning") {
|
|
15236
16630
|
const result = await runSolutioningPhase(phaseDeps, { runId });
|
|
15237
16631
|
if (result.tokenUsage.input > 0 || result.tokenUsage.output > 0) {
|
|
@@ -15245,8 +16639,17 @@ async function runFullPipeline(options) {
|
|
|
15245
16639
|
});
|
|
15246
16640
|
}
|
|
15247
16641
|
if (result.result === "failed") {
|
|
15248
|
-
updatePipelineRun(db, runId, { status: "failed" });
|
|
15249
16642
|
const errorMsg = `Solutioning phase failed: ${result.error ?? "unknown error"}${result.details ? ` — ${result.details}` : ""}`;
|
|
16643
|
+
phaseOrchestrator.markPhaseFailed(runId, "solutioning", errorMsg);
|
|
16644
|
+
if (eventsFlag === true) {
|
|
16645
|
+
const ndjsonEmitter = createEventEmitter(process.stdout);
|
|
16646
|
+
ndjsonEmitter.emit({
|
|
16647
|
+
type: "story:warn",
|
|
16648
|
+
ts: new Date().toISOString(),
|
|
16649
|
+
key: "solutioning",
|
|
16650
|
+
msg: errorMsg
|
|
16651
|
+
});
|
|
16652
|
+
}
|
|
15250
16653
|
if (outputFormat === "human") process.stderr.write(`Error: ${errorMsg}\n`);
|
|
15251
16654
|
else process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
15252
16655
|
return 1;
|
|
@@ -16430,6 +17833,90 @@ async function runAmendCommand(options) {
|
|
|
16430
17833
|
} catch {}
|
|
16431
17834
|
}
|
|
16432
17835
|
}
|
|
17836
|
+
async function runAutoMetrics(options) {
|
|
17837
|
+
const { outputFormat, projectRoot, limit = 10, compare, tagBaseline } = options;
|
|
17838
|
+
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
17839
|
+
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
17840
|
+
if (!existsSync(dbPath)) {
|
|
17841
|
+
if (outputFormat === "json") process.stdout.write(formatOutput({
|
|
17842
|
+
runs: [],
|
|
17843
|
+
message: "No metrics yet — no pipeline database found."
|
|
17844
|
+
}, "json", true) + "\n");
|
|
17845
|
+
else process.stdout.write("No metrics yet — no pipeline database found.\n");
|
|
17846
|
+
return 0;
|
|
17847
|
+
}
|
|
17848
|
+
const dbWrapper = new DatabaseWrapper(dbPath);
|
|
17849
|
+
try {
|
|
17850
|
+
dbWrapper.open();
|
|
17851
|
+
runMigrations(dbWrapper.db);
|
|
17852
|
+
const db = dbWrapper.db;
|
|
17853
|
+
if (tagBaseline !== void 0) {
|
|
17854
|
+
const row = getRunMetrics(db, tagBaseline);
|
|
17855
|
+
if (!row) {
|
|
17856
|
+
const msg = `Run '${tagBaseline}' not found in run_metrics.`;
|
|
17857
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
|
|
17858
|
+
else process.stderr.write(`Error: ${msg}\n`);
|
|
17859
|
+
return 1;
|
|
17860
|
+
}
|
|
17861
|
+
tagRunAsBaseline(db, tagBaseline);
|
|
17862
|
+
if (outputFormat === "json") process.stdout.write(formatOutput({ tagged_baseline: tagBaseline }, "json", true) + "\n");
|
|
17863
|
+
else process.stdout.write(`Baseline tagged: ${tagBaseline}\n`);
|
|
17864
|
+
return 0;
|
|
17865
|
+
}
|
|
17866
|
+
if (compare !== void 0) {
|
|
17867
|
+
const [idA, idB] = compare;
|
|
17868
|
+
const delta = compareRunMetrics(db, idA, idB);
|
|
17869
|
+
if (delta === null) {
|
|
17870
|
+
const msg = `One or both run IDs not found in metrics: ${idA}, ${idB}`;
|
|
17871
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
|
|
17872
|
+
else process.stderr.write(`Error: ${msg}\n`);
|
|
17873
|
+
return 1;
|
|
17874
|
+
}
|
|
17875
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(delta, "json", true) + "\n");
|
|
17876
|
+
else {
|
|
17877
|
+
const sign = (n) => n > 0 ? "+" : "";
|
|
17878
|
+
process.stdout.write(`\nMetrics Comparison: ${idA.slice(0, 8)} vs ${idB.slice(0, 8)}\n`);
|
|
17879
|
+
process.stdout.write(` Input tokens: ${sign(delta.token_input_delta)}${delta.token_input_delta.toLocaleString()} (${sign(delta.token_input_pct)}${delta.token_input_pct}%)\n`);
|
|
17880
|
+
process.stdout.write(` Output tokens: ${sign(delta.token_output_delta)}${delta.token_output_delta.toLocaleString()} (${sign(delta.token_output_pct)}${delta.token_output_pct}%)\n`);
|
|
17881
|
+
process.stdout.write(` Wall clock: ${sign(delta.wall_clock_delta_seconds)}${delta.wall_clock_delta_seconds}s (${sign(delta.wall_clock_pct)}${delta.wall_clock_pct}%)\n`);
|
|
17882
|
+
process.stdout.write(` Review cycles: ${sign(delta.review_cycles_delta)}${delta.review_cycles_delta} (${sign(delta.review_cycles_pct)}${delta.review_cycles_pct}%)\n`);
|
|
17883
|
+
process.stdout.write(` Cost USD: ${sign(delta.cost_delta)}$${Math.abs(delta.cost_delta).toFixed(4)} (${sign(delta.cost_pct)}${delta.cost_pct}%)\n`);
|
|
17884
|
+
}
|
|
17885
|
+
return 0;
|
|
17886
|
+
}
|
|
17887
|
+
const runs = listRunMetrics(db, limit);
|
|
17888
|
+
if (outputFormat === "json") process.stdout.write(formatOutput({ runs }, "json", true) + "\n");
|
|
17889
|
+
else {
|
|
17890
|
+
if (runs.length === 0) {
|
|
17891
|
+
process.stdout.write("No run metrics recorded yet. Run `substrate auto run` to generate metrics.\n");
|
|
17892
|
+
return 0;
|
|
17893
|
+
}
|
|
17894
|
+
process.stdout.write(`\nPipeline Run Metrics (last ${runs.length} runs)\n`);
|
|
17895
|
+
process.stdout.write("─".repeat(80) + "\n");
|
|
17896
|
+
for (const run of runs) {
|
|
17897
|
+
const isBaseline = run.is_baseline ? " [BASELINE]" : "";
|
|
17898
|
+
process.stdout.write(`\nRun: ${run.run_id}${isBaseline}\n`);
|
|
17899
|
+
process.stdout.write(` Status: ${run.status} | Methodology: ${run.methodology}\n`);
|
|
17900
|
+
process.stdout.write(` Started: ${run.started_at}\n`);
|
|
17901
|
+
if (run.completed_at) process.stdout.write(` Completed: ${run.completed_at} (${run.wall_clock_seconds}s)\n`);
|
|
17902
|
+
process.stdout.write(` Stories: attempted=${run.stories_attempted} succeeded=${run.stories_succeeded} failed=${run.stories_failed} escalated=${run.stories_escalated}\n`);
|
|
17903
|
+
process.stdout.write(` Tokens: ${(run.total_input_tokens ?? 0).toLocaleString()} in / ${(run.total_output_tokens ?? 0).toLocaleString()} out $${(run.total_cost_usd ?? 0).toFixed(4)}\n`);
|
|
17904
|
+
process.stdout.write(` Cycles: ${run.total_review_cycles} | Dispatches: ${run.total_dispatches} | Concurrency: ${run.concurrency_setting}\n`);
|
|
17905
|
+
}
|
|
17906
|
+
}
|
|
17907
|
+
return 0;
|
|
17908
|
+
} catch (err) {
|
|
17909
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17910
|
+
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
|
|
17911
|
+
else process.stderr.write(`Error: ${msg}\n`);
|
|
17912
|
+
logger$3.error({ err }, "auto metrics failed");
|
|
17913
|
+
return 1;
|
|
17914
|
+
} finally {
|
|
17915
|
+
try {
|
|
17916
|
+
dbWrapper.close();
|
|
17917
|
+
} catch {}
|
|
17918
|
+
}
|
|
17919
|
+
}
|
|
16433
17920
|
/**
|
|
16434
17921
|
* Register the `substrate auto` command group with the CLI program.
|
|
16435
17922
|
*
|
|
@@ -16451,7 +17938,7 @@ function registerAutoCommand(program, _version = "0.0.0", projectRoot = process.
|
|
|
16451
17938
|
});
|
|
16452
17939
|
process.exitCode = exitCode;
|
|
16453
17940
|
});
|
|
16454
|
-
auto.command("run").description("Run the autonomous pipeline (use --from to start from a specific phase)").option("--pack <name>", "Methodology pack name", "bmad").option("--from <phase>", "Start from this phase: analysis, planning, solutioning, implementation").option("--stop-after <phase>", "Stop pipeline after this phase completes").option("--concept <text>", "Inline concept text (required when --from analysis)").option("--concept-file <path>", "Path to a file containing the concept text").option("--stories <keys>", "Comma-separated story keys (e.g., 10-1,10-2)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 3).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--events", "Emit structured NDJSON events on stdout for programmatic consumption").option("--verbose", "Show detailed pino log output").option("--help-agent", "Print a machine-optimized prompt fragment for AI agents and exit").option("--tui", "Show TUI dashboard").action(async (opts) => {
|
|
17941
|
+
auto.command("run").description("Run the autonomous pipeline (use --from to start from a specific phase)").option("--pack <name>", "Methodology pack name", "bmad").option("--from <phase>", "Start from this phase: analysis, planning, solutioning, implementation").option("--stop-after <phase>", "Stop pipeline after this phase completes").option("--concept <text>", "Inline concept text (required when --from analysis)").option("--concept-file <path>", "Path to a file containing the concept text").option("--stories <keys>", "Comma-separated story keys (e.g., 10-1,10-2)").option("--concurrency <n>", "Maximum parallel conflict groups", (v) => parseInt(v, 10), 3).option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--events", "Emit structured NDJSON events on stdout for programmatic consumption").option("--verbose", "Show detailed pino log output").option("--help-agent", "Print a machine-optimized prompt fragment for AI agents and exit").option("--tui", "Show TUI dashboard").option("--skip-ux", "Skip the UX design phase even if enabled in the pack manifest").action(async (opts) => {
|
|
16455
17942
|
if (opts.helpAgent) {
|
|
16456
17943
|
process.exitCode = await runHelpAgent();
|
|
16457
17944
|
return;
|
|
@@ -16480,7 +17967,8 @@ function registerAutoCommand(program, _version = "0.0.0", projectRoot = process.
|
|
|
16480
17967
|
projectRoot: opts.projectRoot,
|
|
16481
17968
|
events: opts.events,
|
|
16482
17969
|
verbose: opts.verbose,
|
|
16483
|
-
tui: opts.tui
|
|
17970
|
+
tui: opts.tui,
|
|
17971
|
+
skipUx: opts.skipUx
|
|
16484
17972
|
});
|
|
16485
17973
|
process.exitCode = exitCode;
|
|
16486
17974
|
});
|
|
@@ -16539,6 +18027,22 @@ function registerAutoCommand(program, _version = "0.0.0", projectRoot = process.
|
|
|
16539
18027
|
});
|
|
16540
18028
|
process.exitCode = exitCode;
|
|
16541
18029
|
});
|
|
18030
|
+
auto.command("metrics").description("Show historical pipeline run metrics and cross-run comparison").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").option("--limit <n>", "Number of runs to show (default: 10)", (v) => parseInt(v, 10), 10).option("--compare <run-id-a,run-id-b>", "Compare two runs side-by-side (comma-separated IDs, e.g. abc123,def456)").option("--tag-baseline <run-id>", "Mark a run as the performance baseline").action(async (opts) => {
|
|
18031
|
+
const outputFormat = opts.outputFormat === "json" ? "json" : "human";
|
|
18032
|
+
let compareIds;
|
|
18033
|
+
if (opts.compare !== void 0) {
|
|
18034
|
+
const parts = opts.compare.split(",").map((s) => s.trim());
|
|
18035
|
+
if (parts.length === 2 && parts[0] && parts[1]) compareIds = [parts[0], parts[1]];
|
|
18036
|
+
}
|
|
18037
|
+
const exitCode = await runAutoMetrics({
|
|
18038
|
+
outputFormat,
|
|
18039
|
+
projectRoot: opts.projectRoot,
|
|
18040
|
+
limit: opts.limit,
|
|
18041
|
+
compare: compareIds,
|
|
18042
|
+
tagBaseline: opts.tagBaseline
|
|
18043
|
+
});
|
|
18044
|
+
process.exitCode = exitCode;
|
|
18045
|
+
});
|
|
16542
18046
|
}
|
|
16543
18047
|
|
|
16544
18048
|
//#endregion
|