substrate-ai 0.1.25 → 0.1.26
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 +153 -17
- package/dist/cli/templates/statusline.sh +17 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
import { dirname, extname, isAbsolute, join, relative, resolve } from "path";
|
|
10
10
|
import { access, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
|
|
11
11
|
import { execFile } from "child_process";
|
|
12
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
12
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
13
13
|
import yaml, { dump, load } from "js-yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import { fileURLToPath as fileURLToPath$1 } from "node:url";
|
|
@@ -6786,6 +6786,41 @@ Initialize a methodology pack and decision store.
|
|
|
6786
6786
|
\`\`\`
|
|
6787
6787
|
substrate auto init [--pack bmad] [--project-root .]
|
|
6788
6788
|
\`\`\`
|
|
6789
|
+
|
|
6790
|
+
### substrate auto supervisor
|
|
6791
|
+
Long-running process that monitors pipeline health, kills stalled runs, and auto-restarts.
|
|
6792
|
+
|
|
6793
|
+
\`\`\`
|
|
6794
|
+
substrate auto supervisor [options]
|
|
6795
|
+
\`\`\`
|
|
6796
|
+
|
|
6797
|
+
Options:
|
|
6798
|
+
- \`--poll-interval <seconds>\` — Health check interval (default: 60)
|
|
6799
|
+
- \`--stall-threshold <seconds>\` — Staleness before killing (default: 600)
|
|
6800
|
+
- \`--max-restarts <n>\` — Maximum restart attempts (default: 3)
|
|
6801
|
+
- \`--output-format <format>\` — Output format: human (default) or json
|
|
6802
|
+
|
|
6803
|
+
Exit codes: 0 = all succeeded, 1 = failures/escalations, 2 = max restarts exceeded.
|
|
6804
|
+
|
|
6805
|
+
### substrate auto metrics
|
|
6806
|
+
Show historical pipeline run metrics and cross-run comparison.
|
|
6807
|
+
|
|
6808
|
+
\`\`\`
|
|
6809
|
+
substrate auto metrics [options]
|
|
6810
|
+
\`\`\`
|
|
6811
|
+
|
|
6812
|
+
Options:
|
|
6813
|
+
- \`--limit <n>\` — Number of runs to show (default: 10)
|
|
6814
|
+
- \`--compare <run-id-a,run-id-b>\` — Compare two runs side-by-side (token, time, review cycle deltas)
|
|
6815
|
+
- \`--tag-baseline <run-id>\` — Mark a run as the performance baseline
|
|
6816
|
+
- \`--output-format <format>\` — Output format: human (default) or json
|
|
6817
|
+
|
|
6818
|
+
### substrate auto health
|
|
6819
|
+
Check pipeline health, stall detection, and process status.
|
|
6820
|
+
|
|
6821
|
+
\`\`\`
|
|
6822
|
+
substrate auto health [--output-format json]
|
|
6823
|
+
\`\`\`
|
|
6789
6824
|
`;
|
|
6790
6825
|
}
|
|
6791
6826
|
/**
|
|
@@ -8385,6 +8420,12 @@ function writeStoryMetrics(db, input) {
|
|
|
8385
8420
|
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
8421
|
}
|
|
8387
8422
|
/**
|
|
8423
|
+
* Get all story metrics for a given run.
|
|
8424
|
+
*/
|
|
8425
|
+
function getStoryMetricsForRun(db, runId) {
|
|
8426
|
+
return db.prepare("SELECT * FROM story_metrics WHERE run_id = ? ORDER BY id ASC").all(runId);
|
|
8427
|
+
}
|
|
8428
|
+
/**
|
|
8388
8429
|
* Compare two runs and return percentage deltas for key numeric fields.
|
|
8389
8430
|
* Positive deltas mean run B is larger/longer than run A.
|
|
8390
8431
|
* Returns null if either run does not exist.
|
|
@@ -8432,6 +8473,27 @@ function aggregateTokenUsageForRun(db, runId) {
|
|
|
8432
8473
|
cost: 0
|
|
8433
8474
|
};
|
|
8434
8475
|
}
|
|
8476
|
+
/**
|
|
8477
|
+
* Aggregate token usage for a specific story within a pipeline run.
|
|
8478
|
+
* Matches rows where the metadata JSON contains the given storyKey.
|
|
8479
|
+
*/
|
|
8480
|
+
function aggregateTokenUsageForStory(db, runId, storyKey) {
|
|
8481
|
+
const row = db.prepare(`
|
|
8482
|
+
SELECT
|
|
8483
|
+
COALESCE(SUM(input_tokens), 0) as input,
|
|
8484
|
+
COALESCE(SUM(output_tokens), 0) as output,
|
|
8485
|
+
COALESCE(SUM(cost_usd), 0) as cost
|
|
8486
|
+
FROM token_usage
|
|
8487
|
+
WHERE pipeline_run_id = ?
|
|
8488
|
+
AND metadata IS NOT NULL
|
|
8489
|
+
AND json_extract(metadata, '$.storyKey') = ?
|
|
8490
|
+
`).get(runId, storyKey);
|
|
8491
|
+
return row ?? {
|
|
8492
|
+
input: 0,
|
|
8493
|
+
output: 0,
|
|
8494
|
+
cost: 0
|
|
8495
|
+
};
|
|
8496
|
+
}
|
|
8435
8497
|
|
|
8436
8498
|
//#endregion
|
|
8437
8499
|
//#region src/modules/compiled-workflows/prompt-assembler.ts
|
|
@@ -10444,6 +10506,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
10444
10506
|
const startedAt = storyState?.startedAt;
|
|
10445
10507
|
const completedAt = storyState?.completedAt ?? new Date().toISOString();
|
|
10446
10508
|
const wallClockSeconds = startedAt ? Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1e3) : 0;
|
|
10509
|
+
const tokenAgg = aggregateTokenUsageForStory(db, config.pipelineRunId, storyKey);
|
|
10447
10510
|
writeStoryMetrics(db, {
|
|
10448
10511
|
run_id: config.pipelineRunId,
|
|
10449
10512
|
story_key: storyKey,
|
|
@@ -10452,6 +10515,9 @@ function createImplementationOrchestrator(deps) {
|
|
|
10452
10515
|
started_at: startedAt,
|
|
10453
10516
|
completed_at: completedAt,
|
|
10454
10517
|
wall_clock_seconds: wallClockSeconds,
|
|
10518
|
+
input_tokens: tokenAgg.input,
|
|
10519
|
+
output_tokens: tokenAgg.output,
|
|
10520
|
+
cost_usd: tokenAgg.cost,
|
|
10455
10521
|
review_cycles: reviewCycles,
|
|
10456
10522
|
dispatches: _storyDispatches.get(storyKey) ?? 0
|
|
10457
10523
|
});
|
|
@@ -15728,6 +15794,19 @@ const BMAD_BASELINE_TOKENS = 23800;
|
|
|
15728
15794
|
/** Story key pattern: <epic>-<story> e.g. "10-1" */
|
|
15729
15795
|
const STORY_KEY_PATTERN = /^\d+-\d+$/;
|
|
15730
15796
|
/**
|
|
15797
|
+
* Top-level keys in .claude/settings.json that substrate owns.
|
|
15798
|
+
* On init, these are set/updated unconditionally.
|
|
15799
|
+
* User-defined keys outside this set are never touched.
|
|
15800
|
+
*/
|
|
15801
|
+
const SUBSTRATE_OWNED_SETTINGS_KEYS = ["statusLine"];
|
|
15802
|
+
function getSubstrateDefaultSettings() {
|
|
15803
|
+
return { statusLine: {
|
|
15804
|
+
type: "command",
|
|
15805
|
+
command: "bash \"$CLAUDE_PROJECT_DIR\"/.claude/statusline.sh",
|
|
15806
|
+
padding: 0
|
|
15807
|
+
} };
|
|
15808
|
+
}
|
|
15809
|
+
/**
|
|
15731
15810
|
* Format output according to the requested format.
|
|
15732
15811
|
*/
|
|
15733
15812
|
function formatOutput(data, format, success = true, errorMessage) {
|
|
@@ -16028,6 +16107,54 @@ async function scaffoldClaudeMd(projectRoot) {
|
|
|
16028
16107
|
await writeFile(claudeMdPath, newContent, "utf8");
|
|
16029
16108
|
logger$3.info({ claudeMdPath }, "Wrote substrate section to CLAUDE.md");
|
|
16030
16109
|
}
|
|
16110
|
+
/**
|
|
16111
|
+
* Scaffold the statusline script from the bundled template.
|
|
16112
|
+
*
|
|
16113
|
+
* Always overwrites — substrate fully owns this file.
|
|
16114
|
+
*/
|
|
16115
|
+
async function scaffoldStatuslineScript(projectRoot) {
|
|
16116
|
+
const pkgRoot = findPackageRoot(__dirname);
|
|
16117
|
+
const templateName = "statusline.sh";
|
|
16118
|
+
let templatePath = join(pkgRoot, "dist", "cli", "templates", templateName);
|
|
16119
|
+
if (!existsSync(templatePath)) templatePath = join(pkgRoot, "src", "cli", "templates", templateName);
|
|
16120
|
+
let content;
|
|
16121
|
+
try {
|
|
16122
|
+
content = await readFile(templatePath, "utf8");
|
|
16123
|
+
} catch {
|
|
16124
|
+
logger$3.warn({ templatePath }, "statusline.sh template not found; skipping");
|
|
16125
|
+
return;
|
|
16126
|
+
}
|
|
16127
|
+
const claudeDir = join(projectRoot, ".claude");
|
|
16128
|
+
const statuslinePath = join(claudeDir, "statusline.sh");
|
|
16129
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
16130
|
+
await writeFile(statuslinePath, content, "utf8");
|
|
16131
|
+
chmodSync(statuslinePath, 493);
|
|
16132
|
+
logger$3.info({ statuslinePath }, "Wrote .claude/statusline.sh");
|
|
16133
|
+
}
|
|
16134
|
+
/**
|
|
16135
|
+
* Scaffold or merge .claude/settings.json with substrate-owned settings.
|
|
16136
|
+
*
|
|
16137
|
+
* Merge strategy:
|
|
16138
|
+
* - Keys in SUBSTRATE_OWNED_SETTINGS_KEYS are set/updated unconditionally.
|
|
16139
|
+
* - All other keys (permissions, hooks, etc.) are preserved as-is.
|
|
16140
|
+
* - $schema is added only if not already present.
|
|
16141
|
+
*/
|
|
16142
|
+
async function scaffoldClaudeSettings(projectRoot) {
|
|
16143
|
+
const claudeDir = join(projectRoot, ".claude");
|
|
16144
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
16145
|
+
let existing = {};
|
|
16146
|
+
try {
|
|
16147
|
+
const raw = await readFile(settingsPath, "utf8");
|
|
16148
|
+
existing = JSON.parse(raw);
|
|
16149
|
+
} catch {}
|
|
16150
|
+
const defaults = getSubstrateDefaultSettings();
|
|
16151
|
+
const merged = { ...existing };
|
|
16152
|
+
for (const key of SUBSTRATE_OWNED_SETTINGS_KEYS) merged[key] = defaults[key];
|
|
16153
|
+
if (!merged["$schema"]) merged["$schema"] = "https://json.schemastore.org/claude-code-settings.json";
|
|
16154
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
16155
|
+
await writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
16156
|
+
logger$3.info({ settingsPath }, "Wrote substrate settings to .claude/settings.json");
|
|
16157
|
+
}
|
|
16031
16158
|
async function runAutoInit(options) {
|
|
16032
16159
|
const { pack: packName, projectRoot, outputFormat, force = false } = options;
|
|
16033
16160
|
const packPath = join(projectRoot, "packs", packName);
|
|
@@ -16076,6 +16203,8 @@ async function runAutoInit(options) {
|
|
|
16076
16203
|
runMigrations(dbWrapper.db);
|
|
16077
16204
|
dbWrapper.close();
|
|
16078
16205
|
await scaffoldClaudeMd(projectRoot);
|
|
16206
|
+
await scaffoldStatuslineScript(projectRoot);
|
|
16207
|
+
await scaffoldClaudeSettings(projectRoot);
|
|
16079
16208
|
const successMsg = `Pack '${packName}' and database initialized successfully at ${dbPath}`;
|
|
16080
16209
|
if (outputFormat === "json") process.stdout.write(formatOutput({
|
|
16081
16210
|
pack: packName,
|
|
@@ -16260,7 +16389,8 @@ async function runAutoRun(options) {
|
|
|
16260
16389
|
agent: "claude-code",
|
|
16261
16390
|
input_tokens: input,
|
|
16262
16391
|
output_tokens: output,
|
|
16263
|
-
cost_usd: costUsd
|
|
16392
|
+
cost_usd: costUsd,
|
|
16393
|
+
metadata: JSON.stringify({ storyKey: payload.storyKey })
|
|
16264
16394
|
});
|
|
16265
16395
|
}
|
|
16266
16396
|
} catch (err) {
|
|
@@ -16526,10 +16656,13 @@ async function runAutoRun(options) {
|
|
|
16526
16656
|
const runEndMs = Date.now();
|
|
16527
16657
|
const runStartMs = new Date(pipelineRun.created_at).getTime();
|
|
16528
16658
|
const tokenAgg = aggregateTokenUsageForRun(db, pipelineRun.id);
|
|
16659
|
+
const storyMetrics = getStoryMetricsForRun(db, pipelineRun.id);
|
|
16660
|
+
const totalReviewCycles = storyMetrics.reduce((sum, m) => sum + (m.review_cycles ?? 0), 0);
|
|
16661
|
+
const totalDispatches = storyMetrics.reduce((sum, m) => sum + (m.dispatches ?? 0), 0);
|
|
16529
16662
|
writeRunMetrics(db, {
|
|
16530
16663
|
run_id: pipelineRun.id,
|
|
16531
16664
|
methodology: pack.manifest.name,
|
|
16532
|
-
status: failedKeys.length > 0 ? "failed" : "completed",
|
|
16665
|
+
status: failedKeys.length > 0 || escalatedKeys.length > 0 ? "failed" : "completed",
|
|
16533
16666
|
started_at: pipelineRun.created_at,
|
|
16534
16667
|
completed_at: new Date().toISOString(),
|
|
16535
16668
|
wall_clock_seconds: Math.round((runEndMs - runStartMs) / 1e3),
|
|
@@ -16540,6 +16673,8 @@ async function runAutoRun(options) {
|
|
|
16540
16673
|
stories_succeeded: succeededKeys.length,
|
|
16541
16674
|
stories_failed: failedKeys.length,
|
|
16542
16675
|
stories_escalated: escalatedKeys.length,
|
|
16676
|
+
total_review_cycles: totalReviewCycles,
|
|
16677
|
+
total_dispatches: totalDispatches,
|
|
16543
16678
|
concurrency_setting: concurrency
|
|
16544
16679
|
});
|
|
16545
16680
|
} catch (metricsErr) {
|
|
@@ -17573,7 +17708,7 @@ async function runAutoSupervisor(options, deps = {}) {
|
|
|
17573
17708
|
if (health.verdict === "NO_PIPELINE_RUNNING") {
|
|
17574
17709
|
const elapsedSeconds = Math.round((Date.now() - startTime) / 1e3);
|
|
17575
17710
|
const succeeded = Object.entries(health.stories.details).filter(([, s]) => s.phase === "COMPLETE").map(([k]) => k);
|
|
17576
|
-
const failed = Object.entries(health.stories.details).filter(([, s]) => s.phase !== "COMPLETE" && s.phase !== "PENDING").map(([k]) => k);
|
|
17711
|
+
const failed = Object.entries(health.stories.details).filter(([, s]) => s.phase !== "COMPLETE" && s.phase !== "PENDING" && s.phase !== "ESCALATED").map(([k]) => k);
|
|
17577
17712
|
const escalated = Object.entries(health.stories.details).filter(([, s]) => s.phase === "ESCALATED").map(([k]) => k);
|
|
17578
17713
|
emitEvent$1({
|
|
17579
17714
|
type: "supervisor:summary",
|
|
@@ -17585,7 +17720,7 @@ async function runAutoSupervisor(options, deps = {}) {
|
|
|
17585
17720
|
restarts: restartCount
|
|
17586
17721
|
});
|
|
17587
17722
|
log(`\nPipeline reached terminal state. Elapsed: ${elapsedSeconds}s | succeeded: ${succeeded.length} | failed: ${failed.length} | restarts: ${restartCount}`);
|
|
17588
|
-
return failed.length > 0 ? 1 : 0;
|
|
17723
|
+
return failed.length > 0 || escalated.length > 0 ? 1 : 0;
|
|
17589
17724
|
}
|
|
17590
17725
|
if (health.staleness_seconds >= stallThreshold) {
|
|
17591
17726
|
const pids = [...health.process.orchestrator_pid !== null ? [health.process.orchestrator_pid] : [], ...health.process.child_pids];
|
|
@@ -17637,22 +17772,23 @@ async function runAutoSupervisor(options, deps = {}) {
|
|
|
17637
17772
|
attempt: restartCount
|
|
17638
17773
|
});
|
|
17639
17774
|
log(`Supervisor: Restarting pipeline (attempt ${restartCount}/${maxRestarts})`);
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17775
|
+
try {
|
|
17776
|
+
await resumePipeline({
|
|
17777
|
+
runId: health.run_id ?? void 0,
|
|
17778
|
+
outputFormat,
|
|
17779
|
+
projectRoot,
|
|
17780
|
+
concurrency: 3,
|
|
17781
|
+
pack
|
|
17782
|
+
});
|
|
17783
|
+
} catch (err) {
|
|
17647
17784
|
const message = err instanceof Error ? err.message : String(err);
|
|
17648
17785
|
log(`Supervisor: Resume error: ${message}`);
|
|
17649
|
-
if (outputFormat === "json")
|
|
17786
|
+
if (outputFormat === "json") emitEvent$1({
|
|
17650
17787
|
type: "supervisor:error",
|
|
17651
17788
|
reason: "resume_failed",
|
|
17652
|
-
message
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
});
|
|
17789
|
+
message
|
|
17790
|
+
});
|
|
17791
|
+
}
|
|
17656
17792
|
}
|
|
17657
17793
|
await sleep(pollInterval * 1e3);
|
|
17658
17794
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Substrate AI — persistent status line
|
|
3
|
+
# Receives JSON on stdin with session metadata
|
|
4
|
+
|
|
5
|
+
input=$(cat)
|
|
6
|
+
|
|
7
|
+
MODEL=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null)
|
|
8
|
+
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' 2>/dev/null | cut -d. -f1)
|
|
9
|
+
COST=$(echo "$input" | jq -r '.session.cost // "0.00"' 2>/dev/null)
|
|
10
|
+
BRANCH=$(echo "$input" | jq -r '.git.branch // ""' 2>/dev/null)
|
|
11
|
+
|
|
12
|
+
BRANCH_PART=""
|
|
13
|
+
if [ -n "$BRANCH" ]; then
|
|
14
|
+
BRANCH_PART=" | $BRANCH"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
echo "⚡ substrate-ai | $MODEL | ctx ${PCT}% | \$${COST}${BRANCH_PART}"
|