substrate-ai 0.6.1 → 0.6.2
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/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, createTelemetryAdvisor, detectCycles, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-
|
|
2
|
+
import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, createTelemetryAdvisor, detectCycles, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-DCmne2q6.js";
|
|
3
3
|
import { createLogger } from "../logger-D2fS2ccL.js";
|
|
4
4
|
import { AdapterRegistry } from "../adapter-registry-D2zdMwVu.js";
|
|
5
5
|
import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema } from "../config-migrator-DtZW1maj.js";
|
|
@@ -912,6 +912,22 @@ async function scaffoldClaudeCommands(projectRoot, outputFormat) {
|
|
|
912
912
|
const TaskToolCommandGenerator = resolveExport(taskToolMod, "TaskToolCommandGenerator");
|
|
913
913
|
const manifestMod = _require(join(installerLibPath, "core", "manifest-generator.js"));
|
|
914
914
|
const ManifestGenerator = resolveExport(manifestMod, "ManifestGenerator");
|
|
915
|
+
const pathUtilsMod = _require(join(installerLibPath, "ide", "shared", "path-utils.js"));
|
|
916
|
+
const pathUtils = { toDashPath: pathUtilsMod.toDashPath ?? pathUtilsMod.default?.toDashPath };
|
|
917
|
+
const writeDashFallback = async (baseDir, artifacts, acceptTypes) => {
|
|
918
|
+
let written = 0;
|
|
919
|
+
for (const artifact of artifacts) {
|
|
920
|
+
if (!acceptTypes.includes(artifact.type)) continue;
|
|
921
|
+
const content = artifact.content;
|
|
922
|
+
if (!content || !artifact.relativePath) continue;
|
|
923
|
+
const flatName = pathUtils.toDashPath(artifact.relativePath);
|
|
924
|
+
const dest = join(baseDir, flatName);
|
|
925
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
926
|
+
writeFileSync(dest, content, "utf-8");
|
|
927
|
+
written++;
|
|
928
|
+
}
|
|
929
|
+
return written;
|
|
930
|
+
};
|
|
915
931
|
const nonCoreModules = scanBmadModules(bmadDir);
|
|
916
932
|
const allModules = ["core", ...nonCoreModules];
|
|
917
933
|
try {
|
|
@@ -925,13 +941,13 @@ async function scaffoldClaudeCommands(projectRoot, outputFormat) {
|
|
|
925
941
|
clearBmadCommandFiles(commandsDir);
|
|
926
942
|
const agentGen = new AgentCommandGenerator("_bmad");
|
|
927
943
|
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, nonCoreModules);
|
|
928
|
-
const agentCount = await agentGen.writeDashArtifacts(commandsDir, agentArtifacts);
|
|
944
|
+
const agentCount = typeof agentGen.writeDashArtifacts === "function" ? await agentGen.writeDashArtifacts(commandsDir, agentArtifacts) : await writeDashFallback(commandsDir, agentArtifacts, ["agent-launcher"]);
|
|
929
945
|
const workflowGen = new WorkflowCommandGenerator("_bmad");
|
|
930
946
|
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
|
931
|
-
const workflowCount = await workflowGen.writeDashArtifacts(commandsDir, workflowArtifacts);
|
|
947
|
+
const workflowCount = typeof workflowGen.writeDashArtifacts === "function" ? await workflowGen.writeDashArtifacts(commandsDir, workflowArtifacts) : await writeDashFallback(commandsDir, workflowArtifacts, ["workflow-command", "workflow-launcher"]);
|
|
932
948
|
const taskToolGen = new TaskToolCommandGenerator("_bmad");
|
|
933
949
|
const { artifacts: taskToolArtifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
|
|
934
|
-
const taskToolCount = await taskToolGen.writeDashArtifacts(commandsDir, taskToolArtifacts);
|
|
950
|
+
const taskToolCount = typeof taskToolGen.writeDashArtifacts === "function" ? await taskToolGen.writeDashArtifacts(commandsDir, taskToolArtifacts) : await writeDashFallback(commandsDir, taskToolArtifacts, ["task", "tool"]);
|
|
935
951
|
const total = agentCount + workflowCount + taskToolCount;
|
|
936
952
|
if (outputFormat !== "json") process.stdout.write(`Generated ${String(total)} Claude Code commands (${String(agentCount)} agents, ${String(workflowCount)} workflows, ${String(taskToolCount)} tasks/tools)\n`);
|
|
937
953
|
logger$18.info({
|
|
@@ -3469,7 +3485,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
3469
3485
|
await initSchema(expAdapter);
|
|
3470
3486
|
const { runRunAction: runPipeline } = await import(
|
|
3471
3487
|
/* @vite-ignore */
|
|
3472
|
-
"../run-
|
|
3488
|
+
"../run-CcUT8-DF.js"
|
|
3473
3489
|
);
|
|
3474
3490
|
const runStoryFn = async (opts) => {
|
|
3475
3491
|
const exitCode = await runPipeline({
|
package/dist/index.d.ts
CHANGED
|
@@ -514,6 +514,23 @@ interface SupervisorExperimentErrorEvent {
|
|
|
514
514
|
/** Error message describing why the experiment failed */
|
|
515
515
|
error: string;
|
|
516
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Emitted after all stories complete when the `.substrate/project-profile.yaml`
|
|
519
|
+
* may be outdated relative to the actual project structure (e.g., profile says
|
|
520
|
+
* `type: single` but a `turbo.json` now exists, or new language markers appeared).
|
|
521
|
+
*
|
|
522
|
+
* Non-blocking warning — the pipeline has already finished. The user should
|
|
523
|
+
* re-run `substrate init --force` to regenerate the profile.
|
|
524
|
+
*/
|
|
525
|
+
interface PipelineProfileStaleEvent {
|
|
526
|
+
type: 'pipeline:profile-stale';
|
|
527
|
+
/** ISO-8601 timestamp generated at emit time */
|
|
528
|
+
ts: string;
|
|
529
|
+
/** Human-readable message describing the staleness indicators found */
|
|
530
|
+
message: string;
|
|
531
|
+
/** List of staleness indicators detected (e.g., "turbo.json exists but profile says type: single") */
|
|
532
|
+
indicators: string[];
|
|
533
|
+
}
|
|
517
534
|
/**
|
|
518
535
|
* Emitted when post-sprint contract verification finds a mismatch between
|
|
519
536
|
* declared export/import contracts (Story 25-6).
|
|
@@ -581,7 +598,7 @@ interface RoutingModelSelectedEvent {
|
|
|
581
598
|
* }
|
|
582
599
|
* ```
|
|
583
600
|
*/
|
|
584
|
-
type PipelineEvent = PipelineStartEvent | PipelineCompleteEvent | PipelinePreFlightFailureEvent | PipelineContractMismatchEvent | PipelineContractVerificationSummaryEvent | StoryPhaseEvent | StoryDoneEvent | StoryEscalationEvent | StoryWarnEvent | StoryLogEvent | PipelineHeartbeatEvent | StoryStallEvent | StoryZeroDiffEscalationEvent | StoryBuildVerificationFailedEvent | StoryBuildVerificationPassedEvent | StoryInterfaceChangeWarningEvent | StoryMetricsEvent | SupervisorPollEvent | SupervisorKillEvent | SupervisorRestartEvent | SupervisorAbortEvent | SupervisorSummaryEvent | SupervisorAnalysisCompleteEvent | SupervisorAnalysisErrorEvent | SupervisorExperimentStartEvent | SupervisorExperimentSkipEvent | SupervisorExperimentRecommendationsEvent | SupervisorExperimentCompleteEvent | SupervisorExperimentErrorEvent | RoutingModelSelectedEvent; //#endregion
|
|
601
|
+
type PipelineEvent = PipelineStartEvent | PipelineCompleteEvent | PipelinePreFlightFailureEvent | PipelineProfileStaleEvent | PipelineContractMismatchEvent | PipelineContractVerificationSummaryEvent | StoryPhaseEvent | StoryDoneEvent | StoryEscalationEvent | StoryWarnEvent | StoryLogEvent | PipelineHeartbeatEvent | StoryStallEvent | StoryZeroDiffEscalationEvent | StoryBuildVerificationFailedEvent | StoryBuildVerificationPassedEvent | StoryInterfaceChangeWarningEvent | StoryMetricsEvent | SupervisorPollEvent | SupervisorKillEvent | SupervisorRestartEvent | SupervisorAbortEvent | SupervisorSummaryEvent | SupervisorAnalysisCompleteEvent | SupervisorAnalysisErrorEvent | SupervisorExperimentStartEvent | SupervisorExperimentSkipEvent | SupervisorExperimentRecommendationsEvent | SupervisorExperimentCompleteEvent | SupervisorExperimentErrorEvent | RoutingModelSelectedEvent; //#endregion
|
|
585
602
|
//#region src/core/errors.d.ts
|
|
586
603
|
|
|
587
604
|
/**
|
|
@@ -1264,6 +1281,13 @@ interface OrchestratorEvents {
|
|
|
1264
1281
|
/** Build output (stdout+stderr), truncated to 2000 chars */
|
|
1265
1282
|
output: string;
|
|
1266
1283
|
};
|
|
1284
|
+
/** Project profile may be outdated relative to the actual project structure */
|
|
1285
|
+
'pipeline:profile-stale': {
|
|
1286
|
+
/** Human-readable message describing the staleness indicators found */
|
|
1287
|
+
message: string;
|
|
1288
|
+
/** List of staleness indicators detected */
|
|
1289
|
+
indicators: string[];
|
|
1290
|
+
};
|
|
1267
1291
|
/** Contract verification found a mismatch between declared export/import contracts */
|
|
1268
1292
|
'pipeline:contract-mismatch': {
|
|
1269
1293
|
/** Story key that declared the export for this contract */
|
|
@@ -325,7 +325,8 @@ var InMemoryDatabaseAdapter = class {
|
|
|
325
325
|
if (!m$1) return [];
|
|
326
326
|
return [this._evalSelectExprs(m$1[1].trim())];
|
|
327
327
|
}
|
|
328
|
-
const
|
|
328
|
+
const stripped = sql.replace(/\s+ORDER\s+BY\s+.+?(?=\s+LIMIT\s|\s*$)/is, "").replace(/\s+LIMIT\s+\d+\s*$/is, "");
|
|
329
|
+
const m = /SELECT\s+(.+?)\s+FROM\s+(\w+)(?:\s+WHERE\s+(.+))?$/is.exec(stripped);
|
|
329
330
|
if (!m) return [];
|
|
330
331
|
const colsStr = m[1].trim();
|
|
331
332
|
const tableName = m[2];
|
|
@@ -334,6 +335,7 @@ var InMemoryDatabaseAdapter = class {
|
|
|
334
335
|
let rows = table.map((r) => ({ ...r }));
|
|
335
336
|
if (whereStr) rows = rows.filter((row) => this._matchWhere(whereStr.trim(), row));
|
|
336
337
|
if (colsStr === "*") return rows;
|
|
338
|
+
if (/\b(?:SUM|COALESCE|COUNT|AVG|MIN|MAX)\s*\(/i.test(colsStr)) return [this._evalAggregate(colsStr, rows)];
|
|
337
339
|
return rows.map((row) => this._projectCols(colsStr, row));
|
|
338
340
|
}
|
|
339
341
|
_update(sql) {
|
|
@@ -393,12 +395,22 @@ var InMemoryDatabaseAdapter = class {
|
|
|
393
395
|
if (row[notNullM[1]] === null || row[notNullM[1]] === void 0) return false;
|
|
394
396
|
continue;
|
|
395
397
|
}
|
|
398
|
+
const likeM = /^(\w+)\s+LIKE\s+'(.*)'$/is.exec(trimmed);
|
|
399
|
+
if (likeM) {
|
|
400
|
+
const colVal = row[likeM[1]];
|
|
401
|
+
if (colVal === null || colVal === void 0) return false;
|
|
402
|
+
const pattern = likeM[2].replace(/''/g, "'");
|
|
403
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, (ch) => ch === "%" || ch === "_" ? ch : "\\" + ch);
|
|
404
|
+
const regex = new RegExp("^" + escaped.replace(/%/g, ".*").replace(/_/g, ".") + "$", "s");
|
|
405
|
+
if (!regex.test(String(colVal))) return false;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
396
408
|
}
|
|
397
409
|
return true;
|
|
398
410
|
}
|
|
399
411
|
_projectCols(colsStr, row) {
|
|
400
412
|
const result = {};
|
|
401
|
-
const cols =
|
|
413
|
+
const cols = this._splitTopLevelCommas(colsStr);
|
|
402
414
|
for (const col of cols) {
|
|
403
415
|
const aliasM = /^(.+?)\s+AS\s+(\w+)$/i.exec(col);
|
|
404
416
|
if (aliasM) result[aliasM[2]] = this._evalExprAgainstRow(aliasM[1].trim(), row);
|
|
@@ -408,7 +420,7 @@ var InMemoryDatabaseAdapter = class {
|
|
|
408
420
|
}
|
|
409
421
|
_evalSelectExprs(exprs) {
|
|
410
422
|
const result = {};
|
|
411
|
-
const parts =
|
|
423
|
+
const parts = this._splitTopLevelCommas(exprs);
|
|
412
424
|
for (const part of parts) {
|
|
413
425
|
const aliasM = /^(.+?)\s+AS\s+(\w+)$/i.exec(part);
|
|
414
426
|
if (aliasM) result[aliasM[2]] = this._evalLiteral(aliasM[1].trim());
|
|
@@ -431,6 +443,87 @@ var InMemoryDatabaseAdapter = class {
|
|
|
431
443
|
return literal;
|
|
432
444
|
}
|
|
433
445
|
/**
|
|
446
|
+
* Split a string by commas that are NOT inside parentheses.
|
|
447
|
+
* E.g. "COALESCE(SUM(x), 0) as a, y" → ["COALESCE(SUM(x), 0) as a", "y"]
|
|
448
|
+
*/
|
|
449
|
+
_splitTopLevelCommas(str) {
|
|
450
|
+
const parts = [];
|
|
451
|
+
let current = "";
|
|
452
|
+
let depth = 0;
|
|
453
|
+
let inStr = false;
|
|
454
|
+
for (let i = 0; i < str.length; i++) {
|
|
455
|
+
const ch = str[i];
|
|
456
|
+
if (ch === "'" && !inStr) {
|
|
457
|
+
inStr = true;
|
|
458
|
+
current += ch;
|
|
459
|
+
} else if (ch === "'" && inStr) if (str[i + 1] === "'") {
|
|
460
|
+
current += "''";
|
|
461
|
+
i++;
|
|
462
|
+
} else {
|
|
463
|
+
inStr = false;
|
|
464
|
+
current += ch;
|
|
465
|
+
}
|
|
466
|
+
else if (!inStr && ch === "(") {
|
|
467
|
+
depth++;
|
|
468
|
+
current += ch;
|
|
469
|
+
} else if (!inStr && ch === ")") {
|
|
470
|
+
depth--;
|
|
471
|
+
current += ch;
|
|
472
|
+
} else if (!inStr && ch === "," && depth === 0) {
|
|
473
|
+
parts.push(current.trim());
|
|
474
|
+
current = "";
|
|
475
|
+
} else current += ch;
|
|
476
|
+
}
|
|
477
|
+
if (current.trim() !== "") parts.push(current.trim());
|
|
478
|
+
return parts;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Evaluate aggregate SELECT expressions (SUM, COALESCE, COUNT) across
|
|
482
|
+
* a set of filtered rows, returning a single result row.
|
|
483
|
+
*/
|
|
484
|
+
_evalAggregate(colsStr, rows) {
|
|
485
|
+
const result = {};
|
|
486
|
+
const cols = this._splitTopLevelCommas(colsStr);
|
|
487
|
+
for (const col of cols) {
|
|
488
|
+
const aliasM = /^(.+?)\s+AS\s+(\w+)$/i.exec(col);
|
|
489
|
+
const expr = aliasM ? aliasM[1].trim() : col.trim();
|
|
490
|
+
const alias = aliasM ? aliasM[2] : col.trim();
|
|
491
|
+
result[alias] = this._evalAggregateExpr(expr, rows);
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Evaluate a single aggregate expression against a set of rows.
|
|
497
|
+
* Supports: SUM(col), COALESCE(expr, default), COUNT(*).
|
|
498
|
+
*/
|
|
499
|
+
_evalAggregateExpr(expr, rows) {
|
|
500
|
+
const trimmed = expr.trim();
|
|
501
|
+
const coalesceM = /^COALESCE\((.+)\)$/i.exec(trimmed);
|
|
502
|
+
if (coalesceM) {
|
|
503
|
+
const args = this._splitTopLevelCommas(coalesceM[1]);
|
|
504
|
+
for (const arg of args) {
|
|
505
|
+
const val = this._evalAggregateExpr(arg.trim(), rows);
|
|
506
|
+
if (val !== null && val !== void 0) return val;
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const sumM = /^SUM\((\w+)\)$/i.exec(trimmed);
|
|
511
|
+
if (sumM) {
|
|
512
|
+
const col = sumM[1];
|
|
513
|
+
if (rows.length === 0) return null;
|
|
514
|
+
let total = 0;
|
|
515
|
+
for (const row of rows) total += Number(row[col] ?? 0);
|
|
516
|
+
return total;
|
|
517
|
+
}
|
|
518
|
+
if (/^COUNT\(\*\)$/i.test(trimmed)) return rows.length;
|
|
519
|
+
const countM = /^COUNT\((\w+)\)$/i.exec(trimmed);
|
|
520
|
+
if (countM) {
|
|
521
|
+
const col = countM[1];
|
|
522
|
+
return rows.filter((r) => r[col] !== null && r[col] !== void 0).length;
|
|
523
|
+
}
|
|
524
|
+
return this._evalLiteral(trimmed);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
434
527
|
* Parse a comma-separated list of SQL literal values.
|
|
435
528
|
* Handles: NULL, numbers, single-quoted strings.
|
|
436
529
|
* Simple split by comma (assumes no commas inside string values).
|
|
@@ -1829,8 +1922,8 @@ function resolveBmadMethodVersion(fromDir = __dirname) {
|
|
|
1829
1922
|
const BMAD_BASELINE_TOKENS_FULL = 56800;
|
|
1830
1923
|
/** BMAD baseline token total for create+dev+review comparison */
|
|
1831
1924
|
const BMAD_BASELINE_TOKENS = 23800;
|
|
1832
|
-
/** Story key pattern: <epic>-<story> e.g. "10-1" */
|
|
1833
|
-
const STORY_KEY_PATTERN$1 =
|
|
1925
|
+
/** Story key pattern: <epic>-<story> e.g. "10-1", "1-1a", "NEW-26" */
|
|
1926
|
+
const STORY_KEY_PATTERN$1 = /^[A-Za-z0-9]+-[A-Za-z0-9]+$/;
|
|
1834
1927
|
/**
|
|
1835
1928
|
* Top-level keys in .claude/settings.json that substrate owns.
|
|
1836
1929
|
* On init, these are set/updated unconditionally.
|
|
@@ -3441,6 +3534,28 @@ const PIPELINE_EVENT_METADATA = [
|
|
|
3441
3534
|
description: "Overall verification result."
|
|
3442
3535
|
}
|
|
3443
3536
|
]
|
|
3537
|
+
},
|
|
3538
|
+
{
|
|
3539
|
+
type: "pipeline:profile-stale",
|
|
3540
|
+
description: "Project profile may be outdated. Non-blocking warning — run `substrate init --force` to re-detect.",
|
|
3541
|
+
when: "After all stories complete, before pipeline:complete. Emitted when staleness indicators are found.",
|
|
3542
|
+
fields: [
|
|
3543
|
+
{
|
|
3544
|
+
name: "ts",
|
|
3545
|
+
type: "string",
|
|
3546
|
+
description: "Timestamp."
|
|
3547
|
+
},
|
|
3548
|
+
{
|
|
3549
|
+
name: "message",
|
|
3550
|
+
type: "string",
|
|
3551
|
+
description: "Human-readable staleness warning message."
|
|
3552
|
+
},
|
|
3553
|
+
{
|
|
3554
|
+
name: "indicators",
|
|
3555
|
+
type: "string[]",
|
|
3556
|
+
description: "List of staleness indicators (e.g., \"turbo.json exists but profile says type: single\")."
|
|
3557
|
+
}
|
|
3558
|
+
]
|
|
3444
3559
|
}
|
|
3445
3560
|
];
|
|
3446
3561
|
/**
|
|
@@ -6338,7 +6453,8 @@ var DispatcherShuttingDownError = class extends Error {
|
|
|
6338
6453
|
const YAML_ANCHOR_KEYS = [
|
|
6339
6454
|
"result:",
|
|
6340
6455
|
"verdict:",
|
|
6341
|
-
"story_file:"
|
|
6456
|
+
"story_file:",
|
|
6457
|
+
"expansion_priority:"
|
|
6342
6458
|
];
|
|
6343
6459
|
/**
|
|
6344
6460
|
* Extract the YAML result block from sub-agent output.
|
|
@@ -7188,6 +7304,16 @@ function runBuildVerification(options) {
|
|
|
7188
7304
|
output: combinedOutput,
|
|
7189
7305
|
reason: "build-verification-timeout"
|
|
7190
7306
|
};
|
|
7307
|
+
const missingScriptPattern = /Missing script[:\s]|No script found|Command "build" not found/i;
|
|
7308
|
+
if (missingScriptPattern.test(combinedOutput)) {
|
|
7309
|
+
logger$23.warn("Build script not found — skipping pre-flight (greenfield repo)");
|
|
7310
|
+
return {
|
|
7311
|
+
status: "skipped",
|
|
7312
|
+
exitCode,
|
|
7313
|
+
output: combinedOutput,
|
|
7314
|
+
reason: "build-script-not-found"
|
|
7315
|
+
};
|
|
7316
|
+
}
|
|
7191
7317
|
return {
|
|
7192
7318
|
status: "failed",
|
|
7193
7319
|
exitCode,
|
|
@@ -7665,12 +7791,12 @@ var FileStateStore = class {
|
|
|
7665
7791
|
//#region src/modules/state/dolt-store.ts
|
|
7666
7792
|
const log = createLogger("modules:state:dolt");
|
|
7667
7793
|
/**
|
|
7668
|
-
* Validate that a story key matches the expected pattern (e.g. "26-7").
|
|
7794
|
+
* Validate that a story key matches the expected pattern (e.g. "26-7", "1-1a", "NEW-26").
|
|
7669
7795
|
* Prevents SQL injection via string-interpolated identifiers.
|
|
7670
7796
|
*/
|
|
7671
|
-
const STORY_KEY_PATTERN = /^[
|
|
7797
|
+
const STORY_KEY_PATTERN = /^[A-Za-z0-9]+-[A-Za-z0-9]+$/;
|
|
7672
7798
|
function assertValidStoryKey(storyKey) {
|
|
7673
|
-
if (!STORY_KEY_PATTERN.test(storyKey)) throw new DoltQueryError("assertValidStoryKey", `Invalid story key: '${storyKey}'. Must match pattern <
|
|
7799
|
+
if (!STORY_KEY_PATTERN.test(storyKey)) throw new DoltQueryError("assertValidStoryKey", `Invalid story key: '${storyKey}'. Must match pattern <segment>-<segment> (e.g. "10-1", "1-1a", "NEW-26").`);
|
|
7674
7800
|
}
|
|
7675
7801
|
/**
|
|
7676
7802
|
* Dolt-backed implementation of the StateStore interface.
|
|
@@ -9095,9 +9221,17 @@ function readEpicShardFromFile(projectRoot, epicId) {
|
|
|
9095
9221
|
if (!epicsPath) return "";
|
|
9096
9222
|
const content = readFileSync$1(epicsPath, "utf-8");
|
|
9097
9223
|
const epicNum = epicId.replace(/^epic-/i, "");
|
|
9098
|
-
const
|
|
9099
|
-
const
|
|
9100
|
-
|
|
9224
|
+
const headingPattern = new RegExp(`^(#{2,4})\\s+(?:Epic\\s+)?${epicNum}[.:\\s]`, "m");
|
|
9225
|
+
const headingMatch = headingPattern.exec(content);
|
|
9226
|
+
if (!headingMatch) return "";
|
|
9227
|
+
const startIdx = headingMatch.index;
|
|
9228
|
+
const headingLevel = headingMatch[1].length;
|
|
9229
|
+
const hashes = "#".repeat(headingLevel);
|
|
9230
|
+
const endPattern = new RegExp(`\\n${hashes}\\s`, "g");
|
|
9231
|
+
endPattern.lastIndex = startIdx + headingMatch[0].length;
|
|
9232
|
+
const endMatch = endPattern.exec(content);
|
|
9233
|
+
const endIdx = endMatch ? endMatch.index : content.length;
|
|
9234
|
+
return content.slice(startIdx, endIdx).trim();
|
|
9101
9235
|
} catch (err) {
|
|
9102
9236
|
logger$20.warn({
|
|
9103
9237
|
epicId,
|
|
@@ -9356,9 +9490,9 @@ async function getProjectFindings(db) {
|
|
|
9356
9490
|
sections.push("**Prior escalations:**");
|
|
9357
9491
|
for (const d of diagnoses.slice(-3)) try {
|
|
9358
9492
|
const val = JSON.parse(d.value);
|
|
9359
|
-
sections.push(`- ${d.key.split(":")[0]}: ${val.recommendedAction} — ${val.rationale}`);
|
|
9493
|
+
sections.push(`- ${(d.key ?? "").split(":")[0]}: ${val.recommendedAction} — ${val.rationale}`);
|
|
9360
9494
|
} catch {
|
|
9361
|
-
sections.push(`- ${d.key}: escalated`);
|
|
9495
|
+
sections.push(`- ${d.key ?? "unknown"}: escalated`);
|
|
9362
9496
|
}
|
|
9363
9497
|
}
|
|
9364
9498
|
const highCycleStories = metrics.filter((m) => {
|
|
@@ -9373,16 +9507,16 @@ async function getProjectFindings(db) {
|
|
|
9373
9507
|
sections.push("**Stories with high review cycles:**");
|
|
9374
9508
|
for (const m of highCycleStories) try {
|
|
9375
9509
|
const val = JSON.parse(m.value);
|
|
9376
|
-
sections.push(`- ${m.key.split(":")[0]}: ${val.review_cycles} cycles`);
|
|
9510
|
+
sections.push(`- ${(m.key ?? "").split(":")[0]}: ${val.review_cycles} cycles`);
|
|
9377
9511
|
} catch {}
|
|
9378
9512
|
}
|
|
9379
|
-
const stalls = operational.filter((o) => o.key
|
|
9513
|
+
const stalls = operational.filter((o) => o.key?.startsWith("stall:"));
|
|
9380
9514
|
if (stalls.length > 0) sections.push(`**Prior stalls:** ${stalls.length} stall event(s) recorded`);
|
|
9381
9515
|
if (advisoryNotes.length > 0) {
|
|
9382
9516
|
sections.push("**Advisory notes from prior reviews (LGTM_WITH_NOTES):**");
|
|
9383
9517
|
for (const n of advisoryNotes.slice(-3)) try {
|
|
9384
9518
|
const val = JSON.parse(n.value);
|
|
9385
|
-
const storyId = n.key.split(":")[0];
|
|
9519
|
+
const storyId = (n.key ?? "").split(":")[0];
|
|
9386
9520
|
if (typeof val.notes === "string" && val.notes.length > 0) sections.push(`- ${storyId}: ${val.notes}`);
|
|
9387
9521
|
} catch {
|
|
9388
9522
|
sections.push(`- ${n.key}: advisory notes available`);
|
|
@@ -15902,6 +16036,69 @@ function buildTargetedFilesContent(issueList) {
|
|
|
15902
16036
|
return lines.join("\n");
|
|
15903
16037
|
}
|
|
15904
16038
|
/**
|
|
16039
|
+
* Normalize a title string into a set of meaningful words for comparison.
|
|
16040
|
+
* Strips punctuation, lowercases, and filters out very short words (<=2 chars)
|
|
16041
|
+
* and common stop words to focus on content-bearing terms.
|
|
16042
|
+
*/
|
|
16043
|
+
function titleToWordSet(title) {
|
|
16044
|
+
const stopWords = new Set([
|
|
16045
|
+
"the",
|
|
16046
|
+
"and",
|
|
16047
|
+
"for",
|
|
16048
|
+
"with",
|
|
16049
|
+
"from",
|
|
16050
|
+
"into",
|
|
16051
|
+
"that",
|
|
16052
|
+
"this",
|
|
16053
|
+
"via"
|
|
16054
|
+
]);
|
|
16055
|
+
return new Set(title.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((w) => w.length > 2 && !stopWords.has(w)));
|
|
16056
|
+
}
|
|
16057
|
+
/**
|
|
16058
|
+
* Compute the word overlap ratio between two titles.
|
|
16059
|
+
* Returns a value between 0 and 1, where 1 means all words in the smaller set
|
|
16060
|
+
* are present in the larger set.
|
|
16061
|
+
*
|
|
16062
|
+
* Uses the smaller set as the denominator so that a generated title that is a
|
|
16063
|
+
* reasonable subset or superset of the expected title still scores well.
|
|
16064
|
+
*/
|
|
16065
|
+
function computeTitleOverlap(titleA, titleB) {
|
|
16066
|
+
const wordsA = titleToWordSet(titleA);
|
|
16067
|
+
const wordsB = titleToWordSet(titleB);
|
|
16068
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
16069
|
+
let shared = 0;
|
|
16070
|
+
for (const w of wordsA) if (wordsB.has(w)) shared++;
|
|
16071
|
+
const denominator = Math.min(wordsA.size, wordsB.size);
|
|
16072
|
+
return shared / denominator;
|
|
16073
|
+
}
|
|
16074
|
+
/**
|
|
16075
|
+
* Extract the expected story title from the epic shard content.
|
|
16076
|
+
*
|
|
16077
|
+
* Looks for patterns like:
|
|
16078
|
+
* - "### Story 37-1: Turborepo monorepo scaffold"
|
|
16079
|
+
* - "Story 37-1: Turborepo monorepo scaffold"
|
|
16080
|
+
* - "**37-1**: Turborepo monorepo scaffold"
|
|
16081
|
+
* - "37-1: Turborepo monorepo scaffold"
|
|
16082
|
+
*
|
|
16083
|
+
* Returns the title portion after the story key, or null if no match.
|
|
16084
|
+
*/
|
|
16085
|
+
function extractExpectedStoryTitle(shardContent, storyKey) {
|
|
16086
|
+
if (!shardContent || !storyKey) return null;
|
|
16087
|
+
const escaped = storyKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16088
|
+
const patterns = [
|
|
16089
|
+
new RegExp(`^#{2,4}\\s+Story\\s+${escaped}[:\\s]+\\s*(.+)$`, "mi"),
|
|
16090
|
+
new RegExp(`^Story\\s+${escaped}[:\\s]+\\s*(.+)$`, "mi"),
|
|
16091
|
+
new RegExp(`^\\*\\*${escaped}\\*\\*[:\\s]+\\s*(.+)$`, "mi"),
|
|
16092
|
+
new RegExp(`^${escaped}[:\\s]+\\s*(.+)$`, "mi")
|
|
16093
|
+
];
|
|
16094
|
+
for (const pattern of patterns) {
|
|
16095
|
+
const match$1 = pattern.exec(shardContent);
|
|
16096
|
+
if (match$1?.[1]) return match$1[1].replace(/\*+$/, "").trim();
|
|
16097
|
+
}
|
|
16098
|
+
return null;
|
|
16099
|
+
}
|
|
16100
|
+
const TITLE_OVERLAP_WARNING_THRESHOLD = .3;
|
|
16101
|
+
/**
|
|
15905
16102
|
* Map a StoryPhase to the corresponding WgStoryStatus for wg_stories writes.
|
|
15906
16103
|
* Returns null for PENDING (no write needed).
|
|
15907
16104
|
*/
|
|
@@ -15918,6 +16115,55 @@ function wgStatusForPhase(phase) {
|
|
|
15918
16115
|
}
|
|
15919
16116
|
}
|
|
15920
16117
|
/**
|
|
16118
|
+
* Check whether `.substrate/project-profile.yaml` is stale relative to
|
|
16119
|
+
* the actual project structure.
|
|
16120
|
+
*
|
|
16121
|
+
* Returns an array of human-readable indicator strings. An empty array
|
|
16122
|
+
* means the profile appears current (or doesn't exist).
|
|
16123
|
+
*
|
|
16124
|
+
* Staleness indicators checked:
|
|
16125
|
+
* - Profile says `type: single` but `turbo.json` exists (should be monorepo)
|
|
16126
|
+
* - Profile has no Go language but `go.mod` exists
|
|
16127
|
+
* - Profile has no Python language but `pyproject.toml` exists
|
|
16128
|
+
* - Profile has no Rust language but `Cargo.toml` exists
|
|
16129
|
+
*/
|
|
16130
|
+
function checkProfileStaleness(projectRoot) {
|
|
16131
|
+
const profilePath = join$1(projectRoot, ".substrate", "project-profile.yaml");
|
|
16132
|
+
if (!existsSync$1(profilePath)) return [];
|
|
16133
|
+
let profile;
|
|
16134
|
+
try {
|
|
16135
|
+
const raw = readFileSync$1(profilePath, "utf-8");
|
|
16136
|
+
profile = yaml.load(raw) ?? {};
|
|
16137
|
+
} catch {
|
|
16138
|
+
return [];
|
|
16139
|
+
}
|
|
16140
|
+
const project = profile.project;
|
|
16141
|
+
if (project === void 0) return [];
|
|
16142
|
+
const indicators = [];
|
|
16143
|
+
const declaredLanguages = new Set();
|
|
16144
|
+
if (typeof project.language === "string") declaredLanguages.add(project.language);
|
|
16145
|
+
if (Array.isArray(project.packages)) {
|
|
16146
|
+
for (const pkg of project.packages) if (typeof pkg.language === "string") declaredLanguages.add(pkg.language);
|
|
16147
|
+
}
|
|
16148
|
+
if (project.type === "single" && existsSync$1(join$1(projectRoot, "turbo.json"))) indicators.push("turbo.json exists but profile says type: single (should be monorepo)");
|
|
16149
|
+
const languageMarkers = [
|
|
16150
|
+
{
|
|
16151
|
+
file: "go.mod",
|
|
16152
|
+
language: "go"
|
|
16153
|
+
},
|
|
16154
|
+
{
|
|
16155
|
+
file: "pyproject.toml",
|
|
16156
|
+
language: "python"
|
|
16157
|
+
},
|
|
16158
|
+
{
|
|
16159
|
+
file: "Cargo.toml",
|
|
16160
|
+
language: "rust"
|
|
16161
|
+
}
|
|
16162
|
+
];
|
|
16163
|
+
for (const marker of languageMarkers) if (existsSync$1(join$1(projectRoot, marker.file)) && !declaredLanguages.has(marker.language)) indicators.push(`${marker.file} exists but profile does not declare ${marker.language}`);
|
|
16164
|
+
return indicators;
|
|
16165
|
+
}
|
|
16166
|
+
/**
|
|
15921
16167
|
* Factory function that creates an ImplementationOrchestrator instance.
|
|
15922
16168
|
*
|
|
15923
16169
|
* @param deps - Injected dependencies (db, pack, contextCompiler, dispatcher,
|
|
@@ -16274,8 +16520,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
16274
16520
|
for (const s of _stories.values()) if (s.phase === "COMPLETE" || s.phase === "ESCALATED") completed++;
|
|
16275
16521
|
else if (s.phase === "PENDING") queued++;
|
|
16276
16522
|
else active++;
|
|
16277
|
-
|
|
16278
|
-
if (timeSinceProgress >= HEARTBEAT_INTERVAL_MS) eventBus.emit("orchestrator:heartbeat", {
|
|
16523
|
+
eventBus.emit("orchestrator:heartbeat", {
|
|
16279
16524
|
runId: config.pipelineRunId ?? "",
|
|
16280
16525
|
activeDispatches: active,
|
|
16281
16526
|
completedDispatches: completed,
|
|
@@ -16519,6 +16764,46 @@ function createImplementationOrchestrator(deps) {
|
|
|
16519
16764
|
return;
|
|
16520
16765
|
}
|
|
16521
16766
|
storyFilePath = createResult.story_file;
|
|
16767
|
+
if (createResult.story_title) try {
|
|
16768
|
+
const epicId = storyKey.split("-")[0] ?? storyKey;
|
|
16769
|
+
const implDecisions = await getDecisionsByPhase(db, "implementation");
|
|
16770
|
+
let shardContent;
|
|
16771
|
+
const perStoryShard = implDecisions.find((d) => d.category === "epic-shard" && d.key === storyKey);
|
|
16772
|
+
if (perStoryShard?.value) shardContent = perStoryShard.value;
|
|
16773
|
+
else {
|
|
16774
|
+
const epicShard = implDecisions.find((d) => d.category === "epic-shard" && d.key === epicId);
|
|
16775
|
+
if (epicShard?.value) shardContent = extractStorySection(epicShard.value, storyKey) ?? epicShard.value;
|
|
16776
|
+
}
|
|
16777
|
+
if (shardContent) {
|
|
16778
|
+
const expectedTitle = extractExpectedStoryTitle(shardContent, storyKey);
|
|
16779
|
+
if (expectedTitle) {
|
|
16780
|
+
const overlap = computeTitleOverlap(expectedTitle, createResult.story_title);
|
|
16781
|
+
if (overlap < TITLE_OVERLAP_WARNING_THRESHOLD) {
|
|
16782
|
+
const msg = `Story title mismatch: expected "${expectedTitle}" but got "${createResult.story_title}" (word overlap: ${Math.round(overlap * 100)}%). This may indicate the create-story agent received truncated context.`;
|
|
16783
|
+
logger$27.warn({
|
|
16784
|
+
storyKey,
|
|
16785
|
+
expectedTitle,
|
|
16786
|
+
generatedTitle: createResult.story_title,
|
|
16787
|
+
overlap
|
|
16788
|
+
}, msg);
|
|
16789
|
+
eventBus.emit("orchestrator:story-warn", {
|
|
16790
|
+
storyKey,
|
|
16791
|
+
msg
|
|
16792
|
+
});
|
|
16793
|
+
} else logger$27.debug({
|
|
16794
|
+
storyKey,
|
|
16795
|
+
expectedTitle,
|
|
16796
|
+
generatedTitle: createResult.story_title,
|
|
16797
|
+
overlap
|
|
16798
|
+
}, "Story title validation passed");
|
|
16799
|
+
}
|
|
16800
|
+
}
|
|
16801
|
+
} catch (titleValidationErr) {
|
|
16802
|
+
logger$27.debug({
|
|
16803
|
+
storyKey,
|
|
16804
|
+
err: titleValidationErr
|
|
16805
|
+
}, "Story title validation skipped due to error");
|
|
16806
|
+
}
|
|
16522
16807
|
} catch (err) {
|
|
16523
16808
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
16524
16809
|
endPhase(storyKey, "create-story");
|
|
@@ -17041,6 +17326,28 @@ function createImplementationOrchestrator(deps) {
|
|
|
17041
17326
|
}, "Phantom review detected (0 issues + error) — retrying review once");
|
|
17042
17327
|
continue;
|
|
17043
17328
|
}
|
|
17329
|
+
if (isPhantomReview && timeoutRetried) {
|
|
17330
|
+
logger$27.warn({
|
|
17331
|
+
storyKey,
|
|
17332
|
+
reviewCycles,
|
|
17333
|
+
error: reviewResult.error
|
|
17334
|
+
}, "Consecutive review timeouts detected (original + retry both failed) — escalating immediately");
|
|
17335
|
+
endPhase(storyKey, "code-review");
|
|
17336
|
+
updateStory(storyKey, {
|
|
17337
|
+
phase: "ESCALATED",
|
|
17338
|
+
error: "consecutive-review-timeouts",
|
|
17339
|
+
completedAt: new Date().toISOString()
|
|
17340
|
+
});
|
|
17341
|
+
await writeStoryMetricsBestEffort(storyKey, "escalated", reviewCycles + 1);
|
|
17342
|
+
await emitEscalation({
|
|
17343
|
+
storyKey,
|
|
17344
|
+
lastVerdict: "consecutive-review-timeouts",
|
|
17345
|
+
reviewCycles: reviewCycles + 1,
|
|
17346
|
+
issues: ["Review dispatch failed twice consecutively (original + phantom-retry). Likely resource-constrained or diff too large for reviewer."]
|
|
17347
|
+
});
|
|
17348
|
+
await persistState();
|
|
17349
|
+
return;
|
|
17350
|
+
}
|
|
17044
17351
|
verdict = reviewResult.verdict;
|
|
17045
17352
|
issueList = reviewResult.issue_list ?? [];
|
|
17046
17353
|
if (verdict === "NEEDS_MAJOR_REWORK" && reviewCycles > 0 && previousIssueList.length > 0 && issueList.length < previousIssueList.length) {
|
|
@@ -17836,6 +18143,19 @@ function createImplementationOrchestrator(deps) {
|
|
|
17836
18143
|
} catch (err) {
|
|
17837
18144
|
logger$27.error({ err }, "Post-sprint contract verification threw an error — skipping");
|
|
17838
18145
|
}
|
|
18146
|
+
if (projectRoot !== void 0) try {
|
|
18147
|
+
const indicators = checkProfileStaleness(projectRoot);
|
|
18148
|
+
if (indicators.length > 0) {
|
|
18149
|
+
const message = "Project profile may be outdated — consider running `substrate init --force` to re-detect";
|
|
18150
|
+
eventBus.emit("pipeline:profile-stale", {
|
|
18151
|
+
message,
|
|
18152
|
+
indicators
|
|
18153
|
+
});
|
|
18154
|
+
logger$27.warn({ indicators }, message);
|
|
18155
|
+
}
|
|
18156
|
+
} catch (err) {
|
|
18157
|
+
logger$27.debug({ err }, "Profile staleness check failed (best-effort)");
|
|
18158
|
+
}
|
|
17839
18159
|
let completed = 0;
|
|
17840
18160
|
let escalated = 0;
|
|
17841
18161
|
let failed = 0;
|
|
@@ -17966,14 +18286,14 @@ async function resolveStoryKeys(db, projectRoot, opts) {
|
|
|
17966
18286
|
function parseStoryKeysFromEpics(content) {
|
|
17967
18287
|
if (content.length === 0) return [];
|
|
17968
18288
|
const keys = new Set();
|
|
17969
|
-
const explicitKeyPattern = /\*\*Story key:\*\*\s*`?(
|
|
18289
|
+
const explicitKeyPattern = /\*\*Story key:\*\*\s*`?([A-Za-z0-9]+-[A-Za-z0-9]+)(?:-[^`\s]*)?`?/g;
|
|
17970
18290
|
let match$1;
|
|
17971
18291
|
while ((match$1 = explicitKeyPattern.exec(content)) !== null) if (match$1[1] !== void 0) keys.add(match$1[1]);
|
|
17972
|
-
const headingPattern = /^###\s+Story\s+(
|
|
18292
|
+
const headingPattern = /^###\s+Story\s+([A-Za-z0-9]+)[.\-]([A-Za-z0-9]+)/gm;
|
|
17973
18293
|
while ((match$1 = headingPattern.exec(content)) !== null) if (match$1[1] !== void 0 && match$1[2] !== void 0) keys.add(`${match$1[1]}-${match$1[2]}`);
|
|
17974
|
-
const inlineStoryPattern = /Story\s+(
|
|
18294
|
+
const inlineStoryPattern = /Story\s+([A-Za-z0-9]+)-([A-Za-z0-9]+)[:\s]/g;
|
|
17975
18295
|
while ((match$1 = inlineStoryPattern.exec(content)) !== null) if (match$1[1] !== void 0 && match$1[2] !== void 0) keys.add(`${match$1[1]}-${match$1[2]}`);
|
|
17976
|
-
const filePathPattern = /_bmad-output\/implementation-artifacts\/(
|
|
18296
|
+
const filePathPattern = /_bmad-output\/implementation-artifacts\/([A-Za-z0-9]+-[A-Za-z0-9]+)-/g;
|
|
17977
18297
|
while ((match$1 = filePathPattern.exec(content)) !== null) if (match$1[1] !== void 0) keys.add(match$1[1]);
|
|
17978
18298
|
return sortStoryKeys(Array.from(keys));
|
|
17979
18299
|
}
|
|
@@ -18019,6 +18339,12 @@ function discoverPendingStoryKeys(projectRoot, epicNumber) {
|
|
|
18019
18339
|
allKeys = sortStoryKeys([...new Set(allKeys)]);
|
|
18020
18340
|
}
|
|
18021
18341
|
}
|
|
18342
|
+
const sprintKeys = parseStoryKeysFromSprintStatus(projectRoot);
|
|
18343
|
+
if (sprintKeys.length > 0) {
|
|
18344
|
+
const merged = new Set(allKeys);
|
|
18345
|
+
for (const k of sprintKeys) merged.add(k);
|
|
18346
|
+
allKeys = sortStoryKeys([...merged]);
|
|
18347
|
+
}
|
|
18022
18348
|
if (allKeys.length === 0) return [];
|
|
18023
18349
|
const existingKeys = collectExistingStoryKeys(projectRoot);
|
|
18024
18350
|
return allKeys.filter((k) => !existingKeys.has(k));
|
|
@@ -18069,7 +18395,7 @@ function collectExistingStoryKeys(projectRoot) {
|
|
|
18069
18395
|
} catch {
|
|
18070
18396
|
return existing;
|
|
18071
18397
|
}
|
|
18072
|
-
const filePattern = /^(
|
|
18398
|
+
const filePattern = /^([A-Za-z0-9]+-[A-Za-z0-9]+)-/;
|
|
18073
18399
|
for (const entry of entries) {
|
|
18074
18400
|
if (!entry.endsWith(".md")) continue;
|
|
18075
18401
|
const m = filePattern.exec(entry);
|
|
@@ -18078,6 +18404,33 @@ function collectExistingStoryKeys(projectRoot) {
|
|
|
18078
18404
|
return existing;
|
|
18079
18405
|
}
|
|
18080
18406
|
/**
|
|
18407
|
+
* Parse story keys from sprint-status.yaml.
|
|
18408
|
+
* Reads the development_status map and extracts keys that match the
|
|
18409
|
+
* alphanumeric story key pattern (e.g., 1-1a, NEW-26, E5-accessibility).
|
|
18410
|
+
* Filters out epic status entries (epic-N) and retrospective entries.
|
|
18411
|
+
*/
|
|
18412
|
+
function parseStoryKeysFromSprintStatus(projectRoot) {
|
|
18413
|
+
const candidates = [join$1(projectRoot, "_bmad-output", "implementation-artifacts", "sprint-status.yaml"), join$1(projectRoot, "_bmad-output", "sprint-status.yaml")];
|
|
18414
|
+
const statusPath = candidates.find((p) => existsSync$1(p));
|
|
18415
|
+
if (!statusPath) return [];
|
|
18416
|
+
try {
|
|
18417
|
+
const content = readFileSync$1(statusPath, "utf-8");
|
|
18418
|
+
const keys = [];
|
|
18419
|
+
const linePattern = /^\s{2}([A-Za-z0-9]+-[A-Za-z0-9]+(?:-[A-Za-z0-9-]*)?)\s*:/gm;
|
|
18420
|
+
let match$1;
|
|
18421
|
+
while ((match$1 = linePattern.exec(content)) !== null) {
|
|
18422
|
+
const fullKey = match$1[1];
|
|
18423
|
+
if (/^epic-\d+$/.test(fullKey)) continue;
|
|
18424
|
+
if (fullKey.includes("retrospective")) continue;
|
|
18425
|
+
const segments = fullKey.split("-");
|
|
18426
|
+
if (segments.length >= 2) keys.push(`${segments[0]}-${segments[1]}`);
|
|
18427
|
+
}
|
|
18428
|
+
return [...new Set(keys)];
|
|
18429
|
+
} catch {
|
|
18430
|
+
return [];
|
|
18431
|
+
}
|
|
18432
|
+
}
|
|
18433
|
+
/**
|
|
18081
18434
|
* Collect story keys already completed in previous pipeline runs.
|
|
18082
18435
|
* Scans pipeline_runs with status='completed' and extracts story keys
|
|
18083
18436
|
* with phase='COMPLETE' from their token_usage_json state.
|
|
@@ -18096,16 +18449,26 @@ async function getCompletedStoryKeys(db) {
|
|
|
18096
18449
|
return completed;
|
|
18097
18450
|
}
|
|
18098
18451
|
/**
|
|
18099
|
-
* Sort story keys
|
|
18100
|
-
*
|
|
18452
|
+
* Sort story keys: numeric keys first (by epic then story number),
|
|
18453
|
+
* then alphabetic-prefix keys (NEW-*, E-*) sorted lexicographically.
|
|
18454
|
+
* E.g. ["10-1", "1-2a", "1-2", "NEW-26", "E5-acc"] → ["1-2", "1-2a", "10-1", "E5-acc", "NEW-26"]
|
|
18101
18455
|
*/
|
|
18102
18456
|
function sortStoryKeys(keys) {
|
|
18103
18457
|
return keys.slice().sort((a, b) => {
|
|
18104
|
-
const
|
|
18105
|
-
const
|
|
18106
|
-
const
|
|
18107
|
-
|
|
18108
|
-
|
|
18458
|
+
const aParts = a.split("-");
|
|
18459
|
+
const bParts = b.split("-");
|
|
18460
|
+
const aNum = Number(aParts[0]);
|
|
18461
|
+
const bNum = Number(bParts[0]);
|
|
18462
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
18463
|
+
if (aNum !== bNum) return aNum - bNum;
|
|
18464
|
+
const aStory = Number(aParts[1]);
|
|
18465
|
+
const bStory = Number(bParts[1]);
|
|
18466
|
+
if (!isNaN(aStory) && !isNaN(bStory) && aStory !== bStory) return aStory - bStory;
|
|
18467
|
+
return (aParts[1] ?? "").localeCompare(bParts[1] ?? "");
|
|
18468
|
+
}
|
|
18469
|
+
if (!isNaN(aNum)) return -1;
|
|
18470
|
+
if (!isNaN(bNum)) return 1;
|
|
18471
|
+
return a.localeCompare(b);
|
|
18109
18472
|
});
|
|
18110
18473
|
}
|
|
18111
18474
|
|
|
@@ -22196,7 +22559,7 @@ async function runRunAction(options) {
|
|
|
22196
22559
|
if (storiesArg !== void 0 && storiesArg !== "") {
|
|
22197
22560
|
parsedStoryKeys = storiesArg.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
|
|
22198
22561
|
for (const key of parsedStoryKeys) if (!validateStoryKey(key)) {
|
|
22199
|
-
const errorMsg = `Story key '${key}' is not a valid format. Expected: <epic>-<story> (e.g., 10-1)`;
|
|
22562
|
+
const errorMsg = `Story key '${key}' is not a valid format. Expected: <epic>-<story> (e.g., 10-1, 1-1a, NEW-26)`;
|
|
22200
22563
|
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, errorMsg) + "\n");
|
|
22201
22564
|
else process.stderr.write(`Error: ${errorMsg}\n`);
|
|
22202
22565
|
return 1;
|
|
@@ -22777,6 +23140,14 @@ async function runRunAction(options) {
|
|
|
22777
23140
|
verdict: payload.verdict
|
|
22778
23141
|
});
|
|
22779
23142
|
});
|
|
23143
|
+
eventBus.on("pipeline:profile-stale", (payload) => {
|
|
23144
|
+
ndjsonEmitter.emit({
|
|
23145
|
+
type: "pipeline:profile-stale",
|
|
23146
|
+
ts: new Date().toISOString(),
|
|
23147
|
+
message: payload.message,
|
|
23148
|
+
indicators: payload.indicators
|
|
23149
|
+
});
|
|
23150
|
+
});
|
|
22780
23151
|
}
|
|
22781
23152
|
const ingestionServer = telemetryEnabled ? new IngestionServer({ port: telemetryPort }) : void 0;
|
|
22782
23153
|
if (telemetryPersistence !== void 0) {
|
|
@@ -23301,4 +23672,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
23301
23672
|
|
|
23302
23673
|
//#endregion
|
|
23303
23674
|
export { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, createTelemetryAdvisor, detectCycles, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
23304
|
-
//# sourceMappingURL=run-
|
|
23675
|
+
//# sourceMappingURL=run-DCmne2q6.js.map
|