substrate-ai 0.20.23 → 0.20.28
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 +4 -4
- package/dist/{health-CsRLsKgu.js → health-C6x1jDlX.js} +1 -1
- package/dist/{health-Cq8K_jrJ.js → health-Dx9hm9x1.js} +407 -21
- package/dist/{run-B3e4O0Rk.js → run-BmRu588B.js} +35 -3
- package/dist/{run-DEeTPCdU.js → run-CV3EAUZK.js} +2 -2
- package/package.json +1 -1
- package/packs/bmad/prompts/create-story.md +73 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { FileStateStore, RunManifest, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts } from "../health-
|
|
2
|
+
import { FileStateStore, RunManifest, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, buildPipelineStatusOutput, createDatabaseAdapter, createStateStore, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts } from "../health-Dx9hm9x1.js";
|
|
3
3
|
import { createLogger } from "../logger-KeHncl-f.js";
|
|
4
4
|
import { createEventBus } from "../helpers-CElYrONe.js";
|
|
5
5
|
import { AdapterRegistry, BudgetConfigSchema, CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, ConfigError, CostTrackerConfigSchema, DEFAULT_CONFIG, DoltClient, DoltNotInstalled, GlobalSettingsSchema, IngestionServer, MonitorDatabaseImpl, OPERATIONAL_FINDING, PartialGlobalSettingsSchema, PartialProviderConfigSchema, ProvidersSchema, RoutingRecommender, STORY_METRICS, TelemetryConfigSchema, addTokenUsage, aggregateTokenUsageForRun, checkDoltInstalled, compareRunMetrics, createAmendmentRun, createConfigSystem, createDecision, createDoltClient, createPipelineRun, getActiveDecisions, getAllCostEntriesFiltered, getBaselineRunMetrics, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestCompletedRun, getLatestRun, getPipelineRunById, getPlanningCostTotal, getRetryableEscalations, getRunMetrics, getRunningPipelineRuns, getSessionCostSummary, getSessionCostSummaryFiltered, getStoryMetricsForRun, getTokenUsageSummary, incrementRunRestarts, initSchema, initializeDolt, listRunMetrics, loadParentRunDecisions, supersedeDecision, tagRunAsBaseline, updatePipelineRun } from "../dist-CqtWS9wF.js";
|
|
6
6
|
import "../adapter-registry-DXLMTmfD.js";
|
|
7
|
-
import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-
|
|
7
|
+
import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-BmRu588B.js";
|
|
8
8
|
import "../errors-1uLGqnvr.js";
|
|
9
9
|
import "../routing-CcBOCuC9.js";
|
|
10
10
|
import "../decisions-C0pz9Clx.js";
|
|
@@ -3667,7 +3667,7 @@ async function runStatusAction(options) {
|
|
|
3667
3667
|
logger$12.debug({ err }, "Work graph query failed, continuing without work graph data");
|
|
3668
3668
|
}
|
|
3669
3669
|
if (run === void 0) {
|
|
3670
|
-
const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-
|
|
3670
|
+
const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-C6x1jDlX.js");
|
|
3671
3671
|
const substrateDirPath = join(projectRoot, ".substrate");
|
|
3672
3672
|
const processInfo = inspectProcessTree$1({
|
|
3673
3673
|
projectRoot,
|
|
@@ -5198,7 +5198,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
5198
5198
|
await initSchema(expAdapter);
|
|
5199
5199
|
const { runRunAction: runPipeline } = await import(
|
|
5200
5200
|
/* @vite-ignore */
|
|
5201
|
-
"../run-
|
|
5201
|
+
"../run-CV3EAUZK.js"
|
|
5202
5202
|
);
|
|
5203
5203
|
const runStoryFn = async (opts) => {
|
|
5204
5204
|
const exitCode = await runPipeline({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-
|
|
1
|
+
import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-Dx9hm9x1.js";
|
|
2
2
|
import "./logger-KeHncl-f.js";
|
|
3
3
|
import "./dist-CqtWS9wF.js";
|
|
4
4
|
import "./decisions-C0pz9Clx.js";
|
|
@@ -3428,13 +3428,24 @@ const PROBE_TAIL_BYTES = 4 * 1024;
|
|
|
3428
3428
|
* Required fields (`name`, `sandbox`, `command`) force authors to make
|
|
3429
3429
|
* intent explicit — no silent defaults that could mask a miswritten probe.
|
|
3430
3430
|
* Optional fields cover operational knobs with sensible fallbacks.
|
|
3431
|
+
*
|
|
3432
|
+
* Story 60-4: `expect_stdout_no_regex` and `expect_stdout_regex` close the
|
|
3433
|
+
* exit-0-with-error-body gap. A probe that calls a tool returning HTTP 200
|
|
3434
|
+
* with `{"isError": true}` (MCP convention) or `{"status": "error"}` (REST
|
|
3435
|
+
* convention) exits 0 — exit-code-only verification accepts the broken tool
|
|
3436
|
+
* as passing. Authors of probes that hit MCP / REST / JSON-RPC / A2A surfaces
|
|
3437
|
+
* declare success-shape patterns to assert response payload structure beyond
|
|
3438
|
+
* the shell exit code. Driven by strata Run 12 evidence: four MCP tools
|
|
3439
|
+
* shipped SHIP_IT while throwing real Python TypeErrors against real data.
|
|
3431
3440
|
*/
|
|
3432
3441
|
const RuntimeProbeSchema = z.object({
|
|
3433
3442
|
name: z.string().min(1, "probe name is required"),
|
|
3434
3443
|
sandbox: RuntimeProbeSandboxSchema,
|
|
3435
3444
|
command: z.string().min(1, "probe command is required"),
|
|
3436
3445
|
timeout_ms: z.number().int().positive().optional(),
|
|
3437
|
-
description: z.string().optional()
|
|
3446
|
+
description: z.string().optional(),
|
|
3447
|
+
expect_stdout_no_regex: z.array(z.string().min(1)).optional(),
|
|
3448
|
+
expect_stdout_regex: z.array(z.string().min(1)).optional()
|
|
3438
3449
|
});
|
|
3439
3450
|
/** Zod schema for the full list (wrapping the per-probe schema). */
|
|
3440
3451
|
const RuntimeProbeListSchema = z.array(RuntimeProbeSchema);
|
|
@@ -3586,6 +3597,46 @@ function tail(text, bytes = PROBE_TAIL_BYTES) {
|
|
|
3586
3597
|
return text.length <= bytes ? text : text.slice(text.length - bytes);
|
|
3587
3598
|
}
|
|
3588
3599
|
/**
|
|
3600
|
+
* Story 60-4: evaluate `expect_stdout_no_regex` and `expect_stdout_regex`
|
|
3601
|
+
* patterns against the captured stdout. Runs against the full (un-tailed)
|
|
3602
|
+
* stdout so authors can match payload shape even when the response is
|
|
3603
|
+
* larger than PROBE_TAIL_BYTES.
|
|
3604
|
+
*
|
|
3605
|
+
* Returns an array of human-readable failure descriptions. Empty array
|
|
3606
|
+
* means all assertions passed.
|
|
3607
|
+
*
|
|
3608
|
+
* Invalid regex patterns (RegExp constructor throws) are reported as
|
|
3609
|
+
* assertion failures themselves rather than crashing the executor — this
|
|
3610
|
+
* way a typo in one author's probe surfaces as a deterministic finding,
|
|
3611
|
+
* not a pipeline crash that masks the rest of the run.
|
|
3612
|
+
*/
|
|
3613
|
+
function evaluateStdoutAssertions(probe, stdout) {
|
|
3614
|
+
const failures = [];
|
|
3615
|
+
for (const pattern of probe.expect_stdout_no_regex ?? []) {
|
|
3616
|
+
let re;
|
|
3617
|
+
try {
|
|
3618
|
+
re = new RegExp(pattern);
|
|
3619
|
+
} catch (err) {
|
|
3620
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3621
|
+
failures.push(`expect_stdout_no_regex pattern is not a valid regex (${detail}): ${pattern}`);
|
|
3622
|
+
continue;
|
|
3623
|
+
}
|
|
3624
|
+
if (re.test(stdout)) failures.push(`expect_stdout_no_regex: stdout matched forbidden pattern: ${pattern}`);
|
|
3625
|
+
}
|
|
3626
|
+
for (const pattern of probe.expect_stdout_regex ?? []) {
|
|
3627
|
+
let re;
|
|
3628
|
+
try {
|
|
3629
|
+
re = new RegExp(pattern);
|
|
3630
|
+
} catch (err) {
|
|
3631
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3632
|
+
failures.push(`expect_stdout_regex pattern is not a valid regex (${detail}): ${pattern}`);
|
|
3633
|
+
continue;
|
|
3634
|
+
}
|
|
3635
|
+
if (!re.test(stdout)) failures.push(`expect_stdout_regex: stdout did not match required pattern: ${pattern}`);
|
|
3636
|
+
}
|
|
3637
|
+
return failures;
|
|
3638
|
+
}
|
|
3639
|
+
/**
|
|
3589
3640
|
* Execute one probe on the host and return a structured ProbeResult.
|
|
3590
3641
|
*
|
|
3591
3642
|
* Behavior notes:
|
|
@@ -3657,13 +3708,23 @@ function executeProbeOnHost(probe, options = {}) {
|
|
|
3657
3708
|
child.on("close", (code) => {
|
|
3658
3709
|
clearTimeout(timeoutHandle);
|
|
3659
3710
|
const duration = Date.now() - start;
|
|
3711
|
+
let outcome = code === 0 ? "pass" : "fail";
|
|
3712
|
+
let assertionFailures;
|
|
3713
|
+
if (outcome === "pass") {
|
|
3714
|
+
const failures = evaluateStdoutAssertions(probe, stdout);
|
|
3715
|
+
if (failures.length > 0) {
|
|
3716
|
+
outcome = "fail";
|
|
3717
|
+
assertionFailures = failures;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3660
3720
|
finalize({
|
|
3661
|
-
outcome
|
|
3721
|
+
outcome,
|
|
3662
3722
|
command: probe.command,
|
|
3663
3723
|
...code !== null ? { exitCode: code } : {},
|
|
3664
3724
|
stdoutTail: tail(stdout),
|
|
3665
3725
|
stderrTail: tail(stderr),
|
|
3666
|
-
durationMs: duration
|
|
3726
|
+
durationMs: duration,
|
|
3727
|
+
...assertionFailures !== void 0 ? { assertionFailures } : {}
|
|
3667
3728
|
});
|
|
3668
3729
|
});
|
|
3669
3730
|
});
|
|
@@ -3676,6 +3737,77 @@ const CATEGORY_SKIP = "runtime-probe-skip";
|
|
|
3676
3737
|
const CATEGORY_DEFERRED = "runtime-probe-deferred";
|
|
3677
3738
|
const CATEGORY_FAIL = "runtime-probe-fail";
|
|
3678
3739
|
const CATEGORY_TIMEOUT = "runtime-probe-timeout";
|
|
3740
|
+
/**
|
|
3741
|
+
* Story 60-4: command exited 0 but a stdout-shape assertion declared by the
|
|
3742
|
+
* author tripped. Distinct from `runtime-probe-fail` (non-zero exit code)
|
|
3743
|
+
* so retry prompts and post-run analysis can tell "tool crashed politely"
|
|
3744
|
+
* from "tool errored loudly".
|
|
3745
|
+
*/
|
|
3746
|
+
const CATEGORY_ASSERTION_FAIL = "runtime-probe-assertion-fail";
|
|
3747
|
+
/**
|
|
3748
|
+
* Story 60-11: source AC describes an event-driven mechanism (hook, timer,
|
|
3749
|
+
* signal, webhook) but no probe's command invokes a known production-trigger
|
|
3750
|
+
* pattern. Strata Run 13 (Story 1-12, 2026-04-26): vault conflict hook
|
|
3751
|
+
* shipped SHIP_IT non-functional because the dev's probe ran the hook script
|
|
3752
|
+
* directly with `bash .git/hooks/post-merge` — git only fires post-merge on
|
|
3753
|
+
* a SUCCESSFUL merge, so under conflict (the hook's actual use case) the
|
|
3754
|
+
* production trigger never fires. Direct-invocation probe missed it; only
|
|
3755
|
+
* e2e smoke caught it. Sibling to obs_012's success-shape gap.
|
|
3756
|
+
*
|
|
3757
|
+
* Severity is warn (advisory, non-blocking) until the heuristic is
|
|
3758
|
+
* calibrated against several runs. Flip to error once false-positive rate
|
|
3759
|
+
* is verified low.
|
|
3760
|
+
*/
|
|
3761
|
+
const CATEGORY_MISSING_TRIGGER = "runtime-probe-missing-production-trigger";
|
|
3762
|
+
/**
|
|
3763
|
+
* Source-AC keywords that signal an event-driven implementation. Word-boundary
|
|
3764
|
+
* matched, case-insensitive. When any of these appears in source AC text AND
|
|
3765
|
+
* no probe's command invokes a known production trigger, the check emits a
|
|
3766
|
+
* `runtime-probe-missing-production-trigger` warn finding.
|
|
3767
|
+
*
|
|
3768
|
+
* Each keyword is paired with the trigger patterns that satisfy it:
|
|
3769
|
+
* - `git hook` / `post-merge` / etc. → satisfied by `git merge|pull|push|commit|rebase`
|
|
3770
|
+
* - `systemd` / `timer` / `unit` → satisfied by `systemctl ... start|enable|trigger`
|
|
3771
|
+
* - `cron` / `crontab` / `schedule` → satisfied by `crontab|run-parts|schedule`
|
|
3772
|
+
* - `signal` / `SIGHUP|SIGTERM|SIGUSR` → satisfied by `kill -<signal>`
|
|
3773
|
+
* - `webhook` / `HTTP POST` / `endpoint` → satisfied by `curl ... -X POST` or wget
|
|
3774
|
+
* - `inotify` / `path watch` → satisfied by `touch|mkdir|rm` (filesystem mutation)
|
|
3775
|
+
*/
|
|
3776
|
+
const EVENT_DRIVEN_KEYWORDS = [
|
|
3777
|
+
/\b(?:git\s+hook|post-merge|post-commit|post-rewrite|pre-push|pre-commit|pre-merge-commit)\b/i,
|
|
3778
|
+
/\b(?:systemd\s+(?:unit|service|timer|path)|systemctl|\.timer\b|\.service\b)\b/i,
|
|
3779
|
+
/\b(?:cron\s*(?:job|tab|expression)?|crontab|scheduled\s+task)\b/i,
|
|
3780
|
+
/\b(?:signal\s+handler|SIG(?:HUP|TERM|INT|USR1|USR2|KILL))\b/,
|
|
3781
|
+
/\b(?:webhook|HTTP\s+(?:POST|GET)\s+endpoint|REST\s+endpoint)\b/i,
|
|
3782
|
+
/\b(?:inotify|path\s+watch|file\s+watcher)\b/i
|
|
3783
|
+
];
|
|
3784
|
+
/**
|
|
3785
|
+
* Production-trigger command patterns. If ANY probe's command matches one of
|
|
3786
|
+
* these, the heuristic considers the trigger covered. Word-boundary matched.
|
|
3787
|
+
*/
|
|
3788
|
+
const TRIGGER_COMMAND_PATTERNS = [
|
|
3789
|
+
/\bgit\s+(?:merge|pull|push|commit|rebase|cherry-pick)\b/,
|
|
3790
|
+
/\bsystemctl\b/,
|
|
3791
|
+
/\bcrontab\b|\brun-parts\b/,
|
|
3792
|
+
/\bkill\s+-/,
|
|
3793
|
+
/\bcurl\s+(?:[^|]*\s)?-X\s+(?:POST|GET|PUT|DELETE)/i,
|
|
3794
|
+
/\bwget\s+--method=(?:POST|GET|PUT|DELETE)/i,
|
|
3795
|
+
/\b(?:touch|mkdir|rm)\s+/
|
|
3796
|
+
];
|
|
3797
|
+
/**
|
|
3798
|
+
* Returns true if the source AC text mentions an event-driven mechanism.
|
|
3799
|
+
*/
|
|
3800
|
+
function detectsEventDrivenAC(sourceEpicContent) {
|
|
3801
|
+
for (const pattern of EVENT_DRIVEN_KEYWORDS) if (pattern.test(sourceEpicContent)) return true;
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
/**
|
|
3805
|
+
* Returns true if any probe's command line invokes a known production trigger.
|
|
3806
|
+
*/
|
|
3807
|
+
function probesInvokeProductionTrigger(probes) {
|
|
3808
|
+
for (const probe of probes) for (const pattern of TRIGGER_COMMAND_PATTERNS) if (pattern.test(probe.command)) return true;
|
|
3809
|
+
return false;
|
|
3810
|
+
}
|
|
3679
3811
|
const defaultExecutors = { host: (probe) => executeProbeOnHost(probe, { cwd: process.cwd() }) };
|
|
3680
3812
|
var RuntimeProbeCheck = class {
|
|
3681
3813
|
name = "runtime-probes";
|
|
@@ -3729,6 +3861,13 @@ var RuntimeProbeCheck = class {
|
|
|
3729
3861
|
findings: []
|
|
3730
3862
|
};
|
|
3731
3863
|
const findings = [];
|
|
3864
|
+
if (context.sourceEpicContent !== void 0) {
|
|
3865
|
+
if (detectsEventDrivenAC(context.sourceEpicContent) && !probesInvokeProductionTrigger(parsed.probes)) findings.push({
|
|
3866
|
+
category: CATEGORY_MISSING_TRIGGER,
|
|
3867
|
+
severity: "warn",
|
|
3868
|
+
message: "source AC describes an event-driven mechanism (hook / timer / signal / webhook) but no probe's command invokes a known production trigger (git merge/pull/push, systemctl, crontab, kill -<sig>, curl -X POST, etc.). Probes that call the implementation directly skip the wiring layer the AC's user-facing event would exercise — see strata Run 13 / Story 1-12 for the canonical case (post-merge hook never fires under git's conflict semantic). Authoring guidance: probes/event-driven section of create-story.md."
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3732
3871
|
for (const probe of parsed.probes) {
|
|
3733
3872
|
if (probe.sandbox === "twin") {
|
|
3734
3873
|
findings.push({
|
|
@@ -3740,9 +3879,12 @@ var RuntimeProbeCheck = class {
|
|
|
3740
3879
|
}
|
|
3741
3880
|
const result = await this._executors.host(probe);
|
|
3742
3881
|
if (result.outcome === "pass") continue;
|
|
3743
|
-
const category = result.outcome === "timeout" ? CATEGORY_TIMEOUT : CATEGORY_FAIL;
|
|
3882
|
+
const category = result.outcome === "timeout" ? CATEGORY_TIMEOUT : result.assertionFailures !== void 0 ? CATEGORY_ASSERTION_FAIL : CATEGORY_FAIL;
|
|
3744
3883
|
const descriptor = probe.description ? ` (${probe.description})` : "";
|
|
3745
|
-
|
|
3884
|
+
let message;
|
|
3885
|
+
if (result.outcome === "timeout") message = `probe "${probe.name}"${descriptor} timed out after ${result.durationMs}ms`;
|
|
3886
|
+
else if (result.assertionFailures !== void 0) message = `probe "${probe.name}"${descriptor} exit 0 but stdout assertion failed: ` + result.assertionFailures.join("; ");
|
|
3887
|
+
else message = `probe "${probe.name}"${descriptor} failed with exit ${result.exitCode ?? "unknown"}`;
|
|
3746
3888
|
findings.push({
|
|
3747
3889
|
category,
|
|
3748
3890
|
severity: "error",
|
|
@@ -3786,6 +3928,56 @@ const SKIP_DIRS = new Set([
|
|
|
3786
3928
|
/** Max depth for the basename walk. Prevents pathological traversal. */
|
|
3787
3929
|
const MAX_WALK_DEPTH = 8;
|
|
3788
3930
|
/**
|
|
3931
|
+
* Story 60-7: detect operational/runtime path references in source AC.
|
|
3932
|
+
*
|
|
3933
|
+
* Source ACs frequently mention runtime locations the implementation
|
|
3934
|
+
* INTERACTS WITH but does not SHIP — install destinations, system paths,
|
|
3935
|
+
* user home references, git internals. The check's existing path-clause
|
|
3936
|
+
* pipeline treats every backtick path as a deliverable and emits
|
|
3937
|
+
* architectural-drift error when it isn't found in code. This produces
|
|
3938
|
+
* false-positive verification failures.
|
|
3939
|
+
*
|
|
3940
|
+
* Concrete strata example (Run a880f201, Story 1-12, 2026-04-26): source AC
|
|
3941
|
+
* said "When `.git/hooks/post-merge` is installed" — describing the runtime
|
|
3942
|
+
* install location of a hook the dev's installer script writes. The dev
|
|
3943
|
+
* correctly shipped `hooks/install-vault-hooks.sh` + `hooks/vault-conflict-resolver.sh`,
|
|
3944
|
+
* but the check flagged `.git/hooks/post-merge` as architectural drift and
|
|
3945
|
+
* VERIFICATION_FAILED'd the story across both review cycles.
|
|
3946
|
+
*
|
|
3947
|
+
* Patterns covered:
|
|
3948
|
+
* - `^\.git/...` git internals (vault hooks, repo-internal paths)
|
|
3949
|
+
* - `^/usr/...`, `^/etc/...`, `^/var/...`, `^/mnt/...`, `^/opt/...`,
|
|
3950
|
+
* `^/srv/...`, `^/tmp/...`, `^/run/...`, `^/sys/...`, `^/proc/...`,
|
|
3951
|
+
* `^/dev/...`, `^/home/...` Unix system / install destinations
|
|
3952
|
+
* - `^~/...` user home references (`~/.config/...`, `~/obsidian-vault-test/`)
|
|
3953
|
+
*
|
|
3954
|
+
* Out of scope for v1 (deferred to follow-up if real evidence accumulates):
|
|
3955
|
+
* - HTTP routes (`/api/embeddings`) — distinguishing a route from a system
|
|
3956
|
+
* path requires extra signal (extension absence + plural-noun heuristic);
|
|
3957
|
+
* punt until a story actually trips on this.
|
|
3958
|
+
*/
|
|
3959
|
+
function isOperationalPath(pathClause) {
|
|
3960
|
+
const raw = pathClause.replace(/^`/, "").replace(/`$/, "");
|
|
3961
|
+
if (raw.startsWith(".git/")) return true;
|
|
3962
|
+
if (raw.startsWith("~/")) return true;
|
|
3963
|
+
const SYSTEM_ROOTS = [
|
|
3964
|
+
"usr",
|
|
3965
|
+
"etc",
|
|
3966
|
+
"var",
|
|
3967
|
+
"mnt",
|
|
3968
|
+
"opt",
|
|
3969
|
+
"srv",
|
|
3970
|
+
"tmp",
|
|
3971
|
+
"run",
|
|
3972
|
+
"sys",
|
|
3973
|
+
"proc",
|
|
3974
|
+
"dev",
|
|
3975
|
+
"home"
|
|
3976
|
+
];
|
|
3977
|
+
for (const root of SYSTEM_ROOTS) if (raw.startsWith(`/${root}/`)) return true;
|
|
3978
|
+
return false;
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3789
3981
|
* Return true if `base` (a filename like `discover.ts`) exists somewhere under
|
|
3790
3982
|
* `root` within MAX_WALK_DEPTH levels, skipping SKIP_DIRS. The walk is
|
|
3791
3983
|
* synchronous and bounded; finding a single match exits early.
|
|
@@ -3894,36 +4086,156 @@ function pathReferencedInModifiedFiles(workingDir, pathClause, modifiedFiles) {
|
|
|
3894
4086
|
/**
|
|
3895
4087
|
* Extract the story's section from the full epic content.
|
|
3896
4088
|
*
|
|
3897
|
-
* Uses the
|
|
3898
|
-
*
|
|
4089
|
+
* Uses the heading pattern `### Story <storyKey>:` or `### Story <storyKey>[whitespace]`.
|
|
4090
|
+
*
|
|
4091
|
+
* **Separator-tolerant matching** (Story 60-6, mirrors create-story.ts Story
|
|
4092
|
+
* 58-5 normalization): Substrate's canonical storyKey form is hyphen
|
|
4093
|
+
* (`1-10c`) — `seed-methodology-context.ts` normalizes any author convention
|
|
4094
|
+
* to hyphen before storing in `wg_stories`. But strata's `epics.md` uses
|
|
4095
|
+
* dot-form headings (`### Story 1.10c:`). When the supplied storyKey
|
|
4096
|
+
* (`1-10c`) doesn't textually match the heading separator (`.`), the
|
|
4097
|
+
* extraction must still find the right section — silently scanning the
|
|
4098
|
+
* whole epic and attributing every story's clauses to this one is far worse
|
|
4099
|
+
* than emitting a clear "could not isolate" signal.
|
|
3899
4100
|
*
|
|
3900
4101
|
* Returns the extracted section text (from the heading match through to the
|
|
3901
|
-
* next `### Story` heading or end of file), or
|
|
3902
|
-
*
|
|
4102
|
+
* next `### Story` heading or end of file), or `null` if no matching heading
|
|
4103
|
+
* is found. Callers MUST handle null explicitly — the previous silent-fallback
|
|
4104
|
+
* behavior (return-full-epic) inflated findings cross-story and is gone.
|
|
3903
4105
|
*/
|
|
3904
4106
|
function extractStorySection(epicContent, storyKey) {
|
|
3905
|
-
const
|
|
3906
|
-
const
|
|
4107
|
+
const parts = storyKey.split(/[-._ ]/);
|
|
4108
|
+
const normalized = parts.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[-._ ]");
|
|
4109
|
+
const headingPattern = new RegExp(`^###\\s+Story\\s+${normalized}[:\\s]`, "m");
|
|
3907
4110
|
const match = headingPattern.exec(epicContent);
|
|
3908
|
-
if (!match) return
|
|
4111
|
+
if (!match) return null;
|
|
3909
4112
|
const start = match.index;
|
|
3910
4113
|
const nextHeading = /\n### Story /m.exec(epicContent.slice(start + 1));
|
|
3911
4114
|
if (nextHeading) return epicContent.slice(start, start + 1 + nextHeading.index);
|
|
3912
4115
|
return epicContent.slice(start);
|
|
3913
4116
|
}
|
|
4117
|
+
const ALTERNATIVE_ITEM = /^\s*-\s+\*\*\(([a-zA-Z])\)/;
|
|
4118
|
+
/**
|
|
4119
|
+
* Scan section lines for alternative-option groups. A group requires at least
|
|
4120
|
+
* two consecutive lettered list items; isolated `- **(a)**` items are NOT
|
|
4121
|
+
* treated as alternatives because there is no second option to compare against.
|
|
4122
|
+
*
|
|
4123
|
+
* Returns a flat list of options (each item annotated with its group id) so
|
|
4124
|
+
* the caller can map any path-clause line back to its (group, option) bucket.
|
|
4125
|
+
*/
|
|
4126
|
+
function detectAlternativeOptions(lines) {
|
|
4127
|
+
const options = [];
|
|
4128
|
+
let i = 0;
|
|
4129
|
+
while (i < lines.length) {
|
|
4130
|
+
const start = lines[i];
|
|
4131
|
+
const m = start !== void 0 ? ALTERNATIVE_ITEM.exec(start) : null;
|
|
4132
|
+
if (m) {
|
|
4133
|
+
const groupStartLine = i;
|
|
4134
|
+
const items = [{
|
|
4135
|
+
letter: m[1].toLowerCase(),
|
|
4136
|
+
line: i
|
|
4137
|
+
}];
|
|
4138
|
+
let j = i + 1;
|
|
4139
|
+
while (j < lines.length) {
|
|
4140
|
+
const line = lines[j] ?? "";
|
|
4141
|
+
const am = ALTERNATIVE_ITEM.exec(line);
|
|
4142
|
+
if (am) {
|
|
4143
|
+
items.push({
|
|
4144
|
+
letter: am[1].toLowerCase(),
|
|
4145
|
+
line: j
|
|
4146
|
+
});
|
|
4147
|
+
j++;
|
|
4148
|
+
continue;
|
|
4149
|
+
}
|
|
4150
|
+
if (line.trim() === "" || /^\s+\S/.test(line)) {
|
|
4151
|
+
j++;
|
|
4152
|
+
continue;
|
|
4153
|
+
}
|
|
4154
|
+
break;
|
|
4155
|
+
}
|
|
4156
|
+
if (items.length >= 2) {
|
|
4157
|
+
const groupId = `alt-L${groupStartLine}`;
|
|
4158
|
+
for (let k = 0; k < items.length; k++) {
|
|
4159
|
+
const item = items[k];
|
|
4160
|
+
const next = k + 1 < items.length ? items[k + 1].line : j;
|
|
4161
|
+
options.push({
|
|
4162
|
+
group: groupId,
|
|
4163
|
+
option: item.letter,
|
|
4164
|
+
lineStart: item.line,
|
|
4165
|
+
lineEnd: next
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
i = j;
|
|
4170
|
+
} else i++;
|
|
4171
|
+
}
|
|
4172
|
+
return options;
|
|
4173
|
+
}
|
|
4174
|
+
/** Resolve the (group, option) for a path clause whose match appeared on
|
|
4175
|
+
* `lineIndex`, or undefined if the line is not inside any alternative option. */
|
|
4176
|
+
function findOptionForLine(lineIndex, options) {
|
|
4177
|
+
for (const opt of options) if (lineIndex >= opt.lineStart && lineIndex < opt.lineEnd) return {
|
|
4178
|
+
group: opt.group,
|
|
4179
|
+
option: opt.option
|
|
4180
|
+
};
|
|
4181
|
+
return void 0;
|
|
4182
|
+
}
|
|
4183
|
+
/**
|
|
4184
|
+
* Story 60-5: compute the "taken" option per alternative group.
|
|
4185
|
+
*
|
|
4186
|
+
* For each group of alternative options:
|
|
4187
|
+
* - Each option owns one or more path clauses (tagged with the same `group`
|
|
4188
|
+
* and the option's letter).
|
|
4189
|
+
* - An option is satisfied when every path clause it owns exists in code
|
|
4190
|
+
* (pathSatisfiedByCode === true). Missing paths in code make the option
|
|
4191
|
+
* unsatisfied — the dev did not take this option.
|
|
4192
|
+
* - The group's taken-option is the alphabetically-first satisfied letter,
|
|
4193
|
+
* for deterministic selection when multiple options happen to be
|
|
4194
|
+
* satisfied (uncommon, but possible if both options' paths exist from
|
|
4195
|
+
* prior unrelated work).
|
|
4196
|
+
*
|
|
4197
|
+
* Returns a map: group-id → option-letter that was taken. Groups with no
|
|
4198
|
+
* satisfied option are absent from the map (caller falls back to existing
|
|
4199
|
+
* per-path error-severity drift detection).
|
|
4200
|
+
*/
|
|
4201
|
+
function computeTakenOptionPerGroup(hardClauses, workingDir) {
|
|
4202
|
+
const optionState = new Map();
|
|
4203
|
+
for (const clause of hardClauses) {
|
|
4204
|
+
if (clause.type !== "path" || !clause.alternative) continue;
|
|
4205
|
+
const { group, option } = clause.alternative;
|
|
4206
|
+
if (!optionState.has(group)) optionState.set(group, new Map());
|
|
4207
|
+
const groupMap = optionState.get(group);
|
|
4208
|
+
const exists = pathSatisfiedByCode(workingDir, clause.text);
|
|
4209
|
+
if (!groupMap.has(option)) groupMap.set(option, exists);
|
|
4210
|
+
else if (!exists) groupMap.set(option, false);
|
|
4211
|
+
}
|
|
4212
|
+
const taken = new Map();
|
|
4213
|
+
for (const [group, opts] of optionState) {
|
|
4214
|
+
const sorted = [...opts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
4215
|
+
for (const [letter, satisfied] of sorted) if (satisfied) {
|
|
4216
|
+
taken.set(group, letter);
|
|
4217
|
+
break;
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
return taken;
|
|
4221
|
+
}
|
|
3914
4222
|
/**
|
|
3915
4223
|
* Extract hard clauses from a story section of an epic file.
|
|
3916
4224
|
*
|
|
3917
4225
|
* Hard clauses:
|
|
3918
4226
|
* 1. Lines containing MUST NOT / MUST / SHALL NOT / SHALL as standalone keywords (case-sensitive)
|
|
3919
|
-
* 2. Backtick-wrapped paths with at least one `/` (excludes bare filenames)
|
|
4227
|
+
* 2. Backtick-wrapped paths with at least one `/` (excludes bare filenames).
|
|
4228
|
+
* Story 60-5: paths inside `- **(letter)**` list items belonging to a
|
|
4229
|
+
* multi-option alternative group are tagged with `{group, option}` so
|
|
4230
|
+
* the verification phase can OR satisfaction across options.
|
|
3920
4231
|
* 3. The presence of `## Runtime Probes` heading followed by a fenced yaml block
|
|
3921
4232
|
* (represented as a single "runtime-probes-section" clause)
|
|
3922
4233
|
*/
|
|
3923
4234
|
function extractHardClauses(sectionContent) {
|
|
3924
4235
|
const clauses = [];
|
|
3925
|
-
const mustPattern = /\b(MUST NOT|MUST|SHALL NOT|SHALL)\b/;
|
|
3926
4236
|
const lines = sectionContent.split("\n");
|
|
4237
|
+
const alternativeOptions = detectAlternativeOptions(lines);
|
|
4238
|
+
const mustPattern = /\b(MUST NOT|MUST|SHALL NOT|SHALL)\b/;
|
|
3927
4239
|
for (const line of lines) {
|
|
3928
4240
|
const match = mustPattern.exec(line);
|
|
3929
4241
|
if (match) {
|
|
@@ -3935,11 +4247,19 @@ function extractHardClauses(sectionContent) {
|
|
|
3935
4247
|
}
|
|
3936
4248
|
}
|
|
3937
4249
|
const pathPattern = /`([a-zA-Z0-9_./-]+\/[a-zA-Z0-9_./-]+)`/g;
|
|
3938
|
-
let
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
4250
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
4251
|
+
const line = lines[lineIdx] ?? "";
|
|
4252
|
+
pathPattern.lastIndex = 0;
|
|
4253
|
+
let pathMatch;
|
|
4254
|
+
while ((pathMatch = pathPattern.exec(line)) !== null) {
|
|
4255
|
+
const alt = findOptionForLine(lineIdx, alternativeOptions);
|
|
4256
|
+
clauses.push({
|
|
4257
|
+
type: "path",
|
|
4258
|
+
text: `\`${pathMatch[1]}\``,
|
|
4259
|
+
...alt ? { alternative: alt } : {}
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
3943
4263
|
const probesPattern = /^##\s+Runtime Probes[\s\S]*?```yaml/m;
|
|
3944
4264
|
if (probesPattern.test(sectionContent)) clauses.push({
|
|
3945
4265
|
type: "runtime-probes-section",
|
|
@@ -3966,9 +4286,23 @@ var SourceAcFidelityCheck = class {
|
|
|
3966
4286
|
};
|
|
3967
4287
|
}
|
|
3968
4288
|
const storySection = extractStorySection(context.sourceEpicContent, context.storyKey);
|
|
4289
|
+
if (storySection === null) {
|
|
4290
|
+
const findings$1 = [{
|
|
4291
|
+
category: "source-ac-section-not-found",
|
|
4292
|
+
severity: "warn",
|
|
4293
|
+
message: `could not locate "### Story ${context.storyKey}" heading in source epic content — skipping fidelity check (the heading may use a separator convention (e.g. dot vs hyphen vs underscore) the matcher does not recognize, or the story may not exist in this epic file)`
|
|
4294
|
+
}];
|
|
4295
|
+
return {
|
|
4296
|
+
status: "pass",
|
|
4297
|
+
details: renderFindings(findings$1),
|
|
4298
|
+
duration_ms: Date.now() - start,
|
|
4299
|
+
findings: findings$1
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
3969
4302
|
const hardClauses = extractHardClauses(storySection);
|
|
3970
4303
|
const findings = [];
|
|
3971
4304
|
const storyContent = context.storyContent ?? "";
|
|
4305
|
+
const takenOption = computeTakenOptionPerGroup(hardClauses, context.workingDir);
|
|
3972
4306
|
for (const clause of hardClauses) if (clause.type === "runtime-probes-section") {
|
|
3973
4307
|
if (!storyContent.includes("## Runtime Probes")) {
|
|
3974
4308
|
const truncated = clause.text.length > 120 ? clause.text.slice(0, 120) : clause.text;
|
|
@@ -3981,6 +4315,26 @@ var SourceAcFidelityCheck = class {
|
|
|
3981
4315
|
} else if (!storyContent.includes(clause.text)) {
|
|
3982
4316
|
const truncated = clause.text.length > 120 ? clause.text.slice(0, 120) : clause.text;
|
|
3983
4317
|
if (clause.type === "path") {
|
|
4318
|
+
if (isOperationalPath(clause.text)) {
|
|
4319
|
+
findings.push({
|
|
4320
|
+
category: "source-ac-operational-path-reference",
|
|
4321
|
+
severity: "info",
|
|
4322
|
+
message: `path: "${truncated}" referenced in source AC as a runtime / install / system location (matches operational-path heuristic) — treated as informational, not a deliverable file path`
|
|
4323
|
+
});
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
if (clause.alternative) {
|
|
4327
|
+
const { group, option } = clause.alternative;
|
|
4328
|
+
const taken = takenOption.get(group);
|
|
4329
|
+
if (taken !== void 0 && taken !== option) {
|
|
4330
|
+
findings.push({
|
|
4331
|
+
category: "source-ac-alternative-not-taken",
|
|
4332
|
+
severity: "info",
|
|
4333
|
+
message: `path: "${truncated}" not implemented — source AC offered this as alternative option (${option}); story implemented option (${taken}) instead`
|
|
4334
|
+
});
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
3984
4338
|
const existsInCode = pathSatisfiedByCode(context.workingDir, clause.text);
|
|
3985
4339
|
const modifiedFiles = context.devStoryResult?.files_modified ?? [];
|
|
3986
4340
|
const referencedByStory = pathReferencedInModifiedFiles(context.workingDir, clause.text, modifiedFiles);
|
|
@@ -4226,6 +4580,37 @@ const StoredVerificationSummarySchema = z.object({
|
|
|
4226
4580
|
duration_ms: z.number().nonnegative()
|
|
4227
4581
|
});
|
|
4228
4582
|
|
|
4583
|
+
//#endregion
|
|
4584
|
+
//#region packages/sdlc/dist/run-model/dev-story-signals.js
|
|
4585
|
+
/**
|
|
4586
|
+
* Persisted shape of the normalized dev-story signals.
|
|
4587
|
+
*
|
|
4588
|
+
* All fields optional because:
|
|
4589
|
+
* - Different dev-story dispatches surface different subsets of fields
|
|
4590
|
+
* depending on the agent's YAML output (some omit `tests`, some omit
|
|
4591
|
+
* `ac_failures` when none failed, etc.).
|
|
4592
|
+
* - `result` uses the open extensible-union pattern (v0.19.6 convention)
|
|
4593
|
+
* so future result strings (e.g. 'partial-checkpoint') don't break
|
|
4594
|
+
* deserialization.
|
|
4595
|
+
*/
|
|
4596
|
+
const StoredDevStorySignalsSchema = z.object({
|
|
4597
|
+
result: z.union([
|
|
4598
|
+
z.literal("completed"),
|
|
4599
|
+
z.literal("failed"),
|
|
4600
|
+
z.literal("partial"),
|
|
4601
|
+
z.string()
|
|
4602
|
+
]).optional(),
|
|
4603
|
+
ac_met: z.array(z.string()).optional(),
|
|
4604
|
+
ac_failures: z.array(z.string()).optional(),
|
|
4605
|
+
files_modified: z.array(z.string()).optional(),
|
|
4606
|
+
tests: z.union([
|
|
4607
|
+
z.literal("pass"),
|
|
4608
|
+
z.literal("fail"),
|
|
4609
|
+
z.literal("unknown"),
|
|
4610
|
+
z.string()
|
|
4611
|
+
]).optional()
|
|
4612
|
+
});
|
|
4613
|
+
|
|
4229
4614
|
//#endregion
|
|
4230
4615
|
//#region packages/sdlc/dist/run-model/per-story-state.js
|
|
4231
4616
|
/**
|
|
@@ -4271,7 +4656,8 @@ const PerStoryStateSchema = z.object({
|
|
|
4271
4656
|
cost_usd: z.number().nonnegative().optional(),
|
|
4272
4657
|
review_cycles: z.number().int().nonnegative().optional(),
|
|
4273
4658
|
dispatches: z.number().int().nonnegative().optional(),
|
|
4274
|
-
retry_count: z.number().int().nonnegative().optional()
|
|
4659
|
+
retry_count: z.number().int().nonnegative().optional(),
|
|
4660
|
+
dev_story_signals: StoredDevStorySignalsSchema.optional()
|
|
4275
4661
|
});
|
|
4276
4662
|
|
|
4277
4663
|
//#endregion
|
|
@@ -5681,4 +6067,4 @@ function registerHealthCommand(program, _version = "0.0.0", projectRoot = proces
|
|
|
5681
6067
|
|
|
5682
6068
|
//#endregion
|
|
5683
6069
|
export { BMAD_BASELINE_TOKENS_FULL, DEFAULT_STALL_THRESHOLD_SECONDS, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN$1 as STORY_KEY_PATTERN, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDING_COUNTS, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter$1 as createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, createStateStore, detectCycles, extractTargetFilesFromStoryContent, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, isOrchestratorProcessLine, parseDbTimestampAsUtc, registerHealthCommand, renderFindings, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, runHealthAction, validateStoryKey };
|
|
5684
|
-
//# sourceMappingURL=health-
|
|
6070
|
+
//# sourceMappingURL=health-Dx9hm9x1.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectCycles, extractTargetFilesFromStoryContent, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, renderFindings, resolveGraphPath, resolveMainRepoRoot, validateStoryKey } from "./health-
|
|
1
|
+
import { BMAD_BASELINE_TOKENS_FULL, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, WorkGraphRepository, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectCycles, extractTargetFilesFromStoryContent, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, renderFindings, resolveGraphPath, resolveMainRepoRoot, validateStoryKey } from "./health-Dx9hm9x1.js";
|
|
2
2
|
import { createLogger } from "./logger-KeHncl-f.js";
|
|
3
3
|
import { TypedEventBusImpl, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-CElYrONe.js";
|
|
4
4
|
import { ADVISORY_NOTES, Categorizer, ConsumerAnalyzer, DEFAULT_GLOBAL_SETTINGS, DispatcherImpl, DoltClient, ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, EfficiencyScorer, IngestionServer, LogTurnAnalyzer, OPERATIONAL_FINDING, Recommender, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, STORY_METRICS, STORY_OUTCOME, SubstrateConfigSchema, TEST_EXPANSION_FINDING, TEST_PLAN, TelemetryNormalizer, TelemetryPipeline, TurnAnalyzer, addTokenUsage, aggregateTokenUsageForRun, aggregateTokenUsageForStory, callLLM, createConfigSystem, createDatabaseAdapter$1, createDecision, createPipelineRun, createRequirement, detectInterfaceChanges, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunMetrics, getRunningPipelineRuns, getStoryMetricsForRun, getTokenUsageSummary, initSchema, listRequirements, loadModelRoutingConfig, registerArtifact, updatePipelineRun, updatePipelineRunConfig, upsertDecision, writeRunMetrics, writeStoryMetrics } from "./dist-CqtWS9wF.js";
|
|
@@ -10729,6 +10729,36 @@ function persistVerificationResult(storyKey, summary, runManifest) {
|
|
|
10729
10729
|
}, "manifest verification_result write failed — pipeline continues"));
|
|
10730
10730
|
}
|
|
10731
10731
|
/**
|
|
10732
|
+
* Non-fatally persist dev-story signals to the run manifest.
|
|
10733
|
+
*
|
|
10734
|
+
* Called right before each verification dispatch so the signals that fed
|
|
10735
|
+
* into the verification context are durably recorded. Closes a manifest-as-
|
|
10736
|
+
* source-of-truth gap (Epic 52 design contract): Story 60-3's under-delivery
|
|
10737
|
+
* detection in source-ac-fidelity reads `context.devStoryResult.files_modified`,
|
|
10738
|
+
* which the orchestrator passes in-memory at dispatch time but never wrote
|
|
10739
|
+
* to the manifest. Resume / retry-escalated / supervisor-restart / post-mortem
|
|
10740
|
+
* paths read state from the manifest and saw `dev_story_signals: undefined`,
|
|
10741
|
+
* forcing the under-delivery check into "benefit of doubt" warn mode rather
|
|
10742
|
+
* than the intended error.
|
|
10743
|
+
*
|
|
10744
|
+
* Surfaced strata Run a880f201 (2026-04-26): manifest's per_story_state["1-12"]
|
|
10745
|
+
* had no `dev_story_signals` field even though dev-story shipped 3 files.
|
|
10746
|
+
*
|
|
10747
|
+
* Same non-fatal / fire-and-forget semantics as persistVerificationResult.
|
|
10748
|
+
*
|
|
10749
|
+
* @param storyKey - Story key being verified
|
|
10750
|
+
* @param signals - Normalized DevStorySignals from the orchestrator's
|
|
10751
|
+
* replaceDevStorySignals / mergeDevStorySignals helpers
|
|
10752
|
+
* @param runManifest - RunManifest instance to write to, or null/undefined to skip
|
|
10753
|
+
*/
|
|
10754
|
+
function persistDevStorySignals(storyKey, signals, runManifest) {
|
|
10755
|
+
if (runManifest == null || signals === void 0) return Promise.resolve();
|
|
10756
|
+
return runManifest.patchStoryState(storyKey, { dev_story_signals: signals }).catch((err) => _logger.warn({
|
|
10757
|
+
err,
|
|
10758
|
+
storyKey
|
|
10759
|
+
}, "manifest dev_story_signals write failed — pipeline continues"));
|
|
10760
|
+
}
|
|
10761
|
+
/**
|
|
10732
10762
|
* Flatten every finding from a VerificationSummary's checks into a single
|
|
10733
10763
|
* prompt-ready string. Returns '' when the summary is undefined, contains
|
|
10734
10764
|
* no checks, or every check emits zero findings (e.g. every check passed).
|
|
@@ -13807,6 +13837,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
13807
13837
|
const section = extractStorySection(epicFull, storyKey);
|
|
13808
13838
|
if (section) sourceEpicContent = section;
|
|
13809
13839
|
} catch {}
|
|
13840
|
+
await persistDevStorySignals(storyKey, devStorySignals, runManifest);
|
|
13810
13841
|
const verifContext = assembleVerificationContext({
|
|
13811
13842
|
storyKey,
|
|
13812
13843
|
workingDir: projectRoot ?? process.cwd(),
|
|
@@ -14079,6 +14110,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
14079
14110
|
const section2 = extractStorySection(epicFull2, storyKey);
|
|
14080
14111
|
if (section2) sourceEpicContent2 = section2;
|
|
14081
14112
|
} catch {}
|
|
14113
|
+
await persistDevStorySignals(storyKey, devStorySignals, runManifest);
|
|
14082
14114
|
const verifContext = assembleVerificationContext({
|
|
14083
14115
|
storyKey,
|
|
14084
14116
|
workingDir: projectRoot ?? process.cwd(),
|
|
@@ -14151,7 +14183,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
14151
14183
|
updateStory(storyKey, { phase: "NEEDS_FIXES" });
|
|
14152
14184
|
startPhase(storyKey, "fix");
|
|
14153
14185
|
const taskType = verdict === "NEEDS_MINOR_FIXES" ? "minor-fixes" : "major-rework";
|
|
14154
|
-
const fixModel = taskType === "major-rework" ? "claude-opus-4-
|
|
14186
|
+
const fixModel = taskType === "major-rework" ? "claude-opus-4-7" : void 0;
|
|
14155
14187
|
try {
|
|
14156
14188
|
let fixPrompt;
|
|
14157
14189
|
const isMajorRework = taskType === "major-rework";
|
|
@@ -44456,4 +44488,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
44456
44488
|
|
|
44457
44489
|
//#endregion
|
|
44458
44490
|
export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveMaxReviewCycles, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict, wireNdjsonEmitter };
|
|
44459
|
-
//# sourceMappingURL=run-
|
|
44491
|
+
//# sourceMappingURL=run-BmRu588B.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import "./health-
|
|
1
|
+
import "./health-Dx9hm9x1.js";
|
|
2
2
|
import "./logger-KeHncl-f.js";
|
|
3
3
|
import "./helpers-CElYrONe.js";
|
|
4
4
|
import "./dist-CqtWS9wF.js";
|
|
5
|
-
import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, runRunAction, wireNdjsonEmitter } from "./run-
|
|
5
|
+
import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, runRunAction, wireNdjsonEmitter } from "./run-BmRu588B.js";
|
|
6
6
|
import "./routing-CcBOCuC9.js";
|
|
7
7
|
import "./decisions-C0pz9Clx.js";
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -118,9 +118,13 @@ Declare probes as a YAML list inside a single fenced `yaml` block directly under
|
|
|
118
118
|
command: <shell command line(s)> # required
|
|
119
119
|
timeout_ms: 60000 # optional; defaults to 60000
|
|
120
120
|
description: <optional context> # optional
|
|
121
|
+
expect_stdout_no_regex: # optional; stdout must NOT match any of these
|
|
122
|
+
- '<regex pattern>'
|
|
123
|
+
expect_stdout_regex: # optional; stdout must match each of these
|
|
124
|
+
- '<regex pattern>'
|
|
121
125
|
```
|
|
122
126
|
|
|
123
|
-
Required fields: `name`, `sandbox`, `command`. `timeout_ms` and `
|
|
127
|
+
Required fields: `name`, `sandbox`, `command`. `timeout_ms`, `description`, `expect_stdout_no_regex`, and `expect_stdout_regex` are optional. Probe names must be unique within one story.
|
|
124
128
|
|
|
125
129
|
### Sandbox choice
|
|
126
130
|
|
|
@@ -134,6 +138,74 @@ For stories with multiple runtime concerns (install + start + connect), declare
|
|
|
134
138
|
|
|
135
139
|
Probe names are hyphen-separated identifiers, not sentences: `dolt-image-pullable`, not `verify that the dolt image can be pulled`.
|
|
136
140
|
|
|
141
|
+
### Asserting success-shape on structured-output probes
|
|
142
|
+
|
|
143
|
+
Exit-code success is necessary but **not sufficient** for probes calling tools that return structured payloads (MCP, REST, JSON-RPC, A2A). Many such tools respond HTTP 200 with an error envelope (`{"isError": true}`, `{"status": "error"}`, `{"error": {...}}`) — exit-0 hides the failure. Strata Run 12 shipped four broken MCP tools under SHIP_IT because probes only asserted "tool advertised", not "tool returned a success-shaped response."
|
|
144
|
+
|
|
145
|
+
**Use** `expect_stdout_no_regex` (forbidden patterns) and/or `expect_stdout_regex` (required patterns) when the probe hits MCP / REST / JSON-RPC / A2A. **Skip** for commands that exit non-zero on logical failure (`systemctl`, `podman pull`, `docker compose config`).
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
- name: mcp-semantic-search-returns-results
|
|
149
|
+
sandbox: host
|
|
150
|
+
command: |
|
|
151
|
+
mcp-client call strata_semantic_search '{"query": "auth"}'
|
|
152
|
+
expect_stdout_no_regex:
|
|
153
|
+
- '"isError"\s*:\s*true'
|
|
154
|
+
- '"status"\s*:\s*"error"'
|
|
155
|
+
expect_stdout_regex:
|
|
156
|
+
- '"similarity_score"'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Patterns are JavaScript regex (`new RegExp`). Evaluated only when exit code is 0; non-zero exits emit `runtime-probe-fail` and assertions are skipped to avoid redundant findings.
|
|
160
|
+
|
|
161
|
+
### Probes for event-driven mechanisms must invoke the production trigger
|
|
162
|
+
|
|
163
|
+
When the source AC describes a hook, timer, signal, webhook, or other event-driven mechanism, the probe MUST invoke the **production trigger** that fires the implementation in real usage — NOT call the implementation script directly. Calling the implementation directly verifies it produces correct outputs given synthetic inputs; it does NOT verify the implementation is wired to the right trigger and will actually fire when the AC's user-facing event occurs.
|
|
164
|
+
|
|
165
|
+
Strata Run 13 (Story 1-12, post-merge git hook) shipped SHIP_IT after the dev's probe ran the resolver script directly with conflict-marker fixtures. The resolver was correct; the wiring was not. `git`'s `post-merge` hook is **not executed when a merge fails due to conflicts** (per `githooks(5)`) — and the AC's whole point was conflict resolution. The hook never fired in production. Direct invocation hid this entirely.
|
|
166
|
+
|
|
167
|
+
**Rule**: if the AC describes "when X happens, Y runs", the probe must MAKE X HAPPEN and assert Y ran. Synthesized inputs to Y skip the wiring layer.
|
|
168
|
+
|
|
169
|
+
| AC describes | Production trigger to invoke | Common wrong shape (DO NOT use) |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `post-merge` / `post-commit` / `post-rewrite` git hook | `git merge <branch>` (with the conflict scenario the AC describes) | `bash .git/hooks/post-merge` |
|
|
172
|
+
| `pre-push` git hook | `git push` against a local fixture remote | `bash .git/hooks/pre-push` |
|
|
173
|
+
| systemd unit / timer | `systemctl --user start <unit>` or `systemctl --user start <timer>.timer` then assert `<unit>.service` ran | direct call to the binary the unit invokes |
|
|
174
|
+
| systemd path / inotify trigger | touch / create / modify the watched path; assert the unit fires within N seconds | direct call to the script |
|
|
175
|
+
| cron job | invoke `crontab` to install + run-once via `run-parts` OR shorten the schedule to `* * * * *` and wait | direct call to the script |
|
|
176
|
+
| Signal handler | `kill -<SIGNAL> <pid>` against the running process | direct call to the handler function |
|
|
177
|
+
| Webhook receiver | `curl -X POST <endpoint>` with the actual payload shape | direct call to the handler with synthetic payload |
|
|
178
|
+
|
|
179
|
+
**Example: post-merge hook probe (the strata 1-12 case, fixed)**
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
- name: post-merge-hook-fires-and-resolves-conflict
|
|
183
|
+
sandbox: twin
|
|
184
|
+
command: |
|
|
185
|
+
set -e
|
|
186
|
+
REPO=$(mktemp -d)
|
|
187
|
+
cd "$REPO" && git init -q
|
|
188
|
+
git config user.email t@example.com && git config user.name test
|
|
189
|
+
bash <REPO_ROOT>/hooks/install-vault-hooks.sh "$REPO"
|
|
190
|
+
echo "human content" > note.md && git add . && git commit -qm initial
|
|
191
|
+
git checkout -qb branch-jarvis
|
|
192
|
+
GIT_AUTHOR_NAME=jarvis-bot GIT_AUTHOR_EMAIL=jarvis@bot \
|
|
193
|
+
bash -c 'echo "jarvis content" > note.md && git commit -aqm "jarvis edit"'
|
|
194
|
+
git checkout -q main
|
|
195
|
+
echo "human content edit" > note.md && git commit -aqm "human edit"
|
|
196
|
+
git merge --no-edit branch-jarvis || true # produces conflict
|
|
197
|
+
# If post-merge fired correctly via the production trigger, the conflict is resolved.
|
|
198
|
+
# If it did NOT fire (because it can't, by design — see githooks(5)), the working
|
|
199
|
+
# tree still has conflict markers and this assertion catches it.
|
|
200
|
+
expect_stdout_no_regex:
|
|
201
|
+
- '<{7}|>{7}' # conflict markers must NOT remain in tree after resolution
|
|
202
|
+
expect_stdout_regex:
|
|
203
|
+
- 'human content' # human side preserved per "Jarvis yields to human" rule
|
|
204
|
+
description: real git merge fires (or fails to fire) post-merge — assertion catches both
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Note this example, taken to production, would have caught the strata 1-12 bug at runtime-probe phase rather than only at e2e smoke pass. That's the standard 60-10 sets.
|
|
208
|
+
|
|
137
209
|
### Examples by artifact class
|
|
138
210
|
|
|
139
211
|
**Systemd unit:**
|