substrate-ai 0.20.3 → 0.20.5

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.
@@ -4,6 +4,7 @@ import { createRequire } from "module";
4
4
  import { dirname, join } from "path";
5
5
  import { readFile } from "fs/promises";
6
6
  import { EventEmitter } from "node:events";
7
+ import { YAMLException, load } from "js-yaml";
7
8
  import { existsSync, promises, readFileSync } from "node:fs";
8
9
  import { spawn, spawnSync } from "node:child_process";
9
10
  import { dirname as dirname$1, join as join$1, resolve as resolve$1 } from "node:path";
@@ -2904,6 +2905,961 @@ function applyConfigToGraph(graph, options) {
2904
2905
  graph.defaultMaxRetries = options.maxReviewCycles;
2905
2906
  }
2906
2907
 
2908
+ //#endregion
2909
+ //#region packages/sdlc/dist/verification/findings.js
2910
+ /**
2911
+ * VerificationFinding — structured per-issue payload emitted by verification checks.
2912
+ *
2913
+ * Replaces the ad-hoc "stuff everything into VerificationResult.details" pattern
2914
+ * that preceded it: every downstream consumer (retry prompts, run manifest,
2915
+ * post-run analysis) used to string-parse a free-form blob that the emitting
2916
+ * check never promised a schema for. With findings, each issue is an
2917
+ * addressable record the pipeline can act on individually.
2918
+ *
2919
+ * The {command, exitCode, stdoutTail, stderrTail} optional fields are reserved
2920
+ * primarily for Phase 2 runtime probes — they cost nothing on the current four
2921
+ * Tier A checks (which leave them undefined) but let probe output flow through
2922
+ * the same shape without a second refactor.
2923
+ */
2924
+ const SEVERITY_PREFIX = {
2925
+ error: "ERROR",
2926
+ warn: "WARN",
2927
+ info: "INFO"
2928
+ };
2929
+ /**
2930
+ * Render a list of findings into the multi-line human-readable string that
2931
+ * populates VerificationResult.details. One line per finding:
2932
+ *
2933
+ * `${PREFIX} [${category}] ${message}`
2934
+ *
2935
+ * Checks that migrate to the findings-first pattern call this helper to derive
2936
+ * `details` from the findings they emit, guaranteeing the two stay in sync.
2937
+ */
2938
+ function renderFindings(findings) {
2939
+ if (findings.length === 0) return "";
2940
+ return findings.map((f) => `${SEVERITY_PREFIX[f.severity]} [${f.category}] ${f.message}`).join("\n");
2941
+ }
2942
+
2943
+ //#endregion
2944
+ //#region packages/sdlc/dist/verification/checks/phantom-review-check.js
2945
+ /**
2946
+ * Detects phantom reviews — dispatches that failed or produced no output but
2947
+ * were recorded as passing verdicts.
2948
+ *
2949
+ * AC1: dispatch failed (non-zero exit, timeout, crash) → fail
2950
+ * AC2: empty or null rawOutput → fail
2951
+ * AC3: schema_validation_failed error → fail
2952
+ * AC5: valid review (non-empty rawOutput, no dispatchFailed) → pass
2953
+ * AC6: name='phantom-review', tier='A'
2954
+ */
2955
+ var PhantomReviewCheck = class {
2956
+ name = "phantom-review";
2957
+ tier = "A";
2958
+ async run(context) {
2959
+ const start = Date.now();
2960
+ const review = context.reviewResult;
2961
+ if (!review) return {
2962
+ status: "pass",
2963
+ details: "phantom-review: no review result in context — skipping check",
2964
+ duration_ms: Date.now() - start,
2965
+ findings: []
2966
+ };
2967
+ if (review.dispatchFailed === true) {
2968
+ const reason = review.error === "schema_validation_failed" ? "schema validation failed" : `dispatch failed${review.error ? ` — ${review.error}` : ""}`;
2969
+ const findings = [{
2970
+ category: "phantom-review",
2971
+ severity: "error",
2972
+ message: reason
2973
+ }];
2974
+ return {
2975
+ status: "fail",
2976
+ details: renderFindings(findings),
2977
+ duration_ms: Date.now() - start,
2978
+ findings
2979
+ };
2980
+ }
2981
+ if (review.rawOutput !== void 0 && review.rawOutput.trim().length === 0) {
2982
+ const findings = [{
2983
+ category: "phantom-review",
2984
+ severity: "error",
2985
+ message: "empty review output"
2986
+ }];
2987
+ return {
2988
+ status: "fail",
2989
+ details: renderFindings(findings),
2990
+ duration_ms: Date.now() - start,
2991
+ findings
2992
+ };
2993
+ }
2994
+ return {
2995
+ status: "pass",
2996
+ details: "phantom-review: review output is valid",
2997
+ duration_ms: Date.now() - start,
2998
+ findings: []
2999
+ };
3000
+ }
3001
+ };
3002
+
3003
+ //#endregion
3004
+ //#region packages/sdlc/dist/verification/checks/trivial-output-check.js
3005
+ /**
3006
+ * Default minimum output-token count a story must produce to be
3007
+ * considered non-trivial. Configurable via trivialOutputThreshold config field.
3008
+ */
3009
+ const DEFAULT_TRIVIAL_OUTPUT_THRESHOLD = 100;
3010
+ /**
3011
+ * Checks that a completed story dispatch produced at least `threshold` output
3012
+ * tokens. Dispatches that produced fewer tokens are flagged as failures with
3013
+ * an actionable suggestion to re-run with increased maxTurns.
3014
+ *
3015
+ * AC1: fail when outputTokenCount < threshold.
3016
+ * AC2: details string includes "Re-run with increased maxTurns".
3017
+ * AC3: pass when outputTokenCount >= threshold.
3018
+ * AC4: threshold is configurable via trivialOutputThreshold config field.
3019
+ * AC5: warn (not fail) when outputTokenCount is undefined.
3020
+ * AC6: implements VerificationCheck with name='trivial-output', tier='A'.
3021
+ */
3022
+ var TrivialOutputCheck = class {
3023
+ name = "trivial-output";
3024
+ tier = "A";
3025
+ threshold;
3026
+ constructor(config) {
3027
+ this.threshold = config?.trivialOutputThreshold ?? DEFAULT_TRIVIAL_OUTPUT_THRESHOLD;
3028
+ }
3029
+ async run(context) {
3030
+ const start = Date.now();
3031
+ if (context.outputTokenCount === void 0) {
3032
+ const findings = [{
3033
+ category: "trivial-output",
3034
+ severity: "warn",
3035
+ message: "output token count unavailable — skipping check"
3036
+ }];
3037
+ return {
3038
+ status: "warn",
3039
+ details: renderFindings(findings),
3040
+ duration_ms: Date.now() - start,
3041
+ findings
3042
+ };
3043
+ }
3044
+ const count = context.outputTokenCount;
3045
+ if (count < this.threshold) {
3046
+ const findings = [{
3047
+ category: "trivial-output",
3048
+ severity: "error",
3049
+ message: `output token count ${count} is below threshold ${this.threshold} — Re-run with increased maxTurns`
3050
+ }];
3051
+ return {
3052
+ status: "fail",
3053
+ details: renderFindings(findings),
3054
+ duration_ms: Date.now() - start,
3055
+ findings
3056
+ };
3057
+ }
3058
+ return {
3059
+ status: "pass",
3060
+ details: `output token count ${count} meets threshold ${this.threshold}`,
3061
+ duration_ms: Date.now() - start,
3062
+ findings: []
3063
+ };
3064
+ }
3065
+ };
3066
+
3067
+ //#endregion
3068
+ //#region packages/sdlc/dist/verification/checks/acceptance-criteria-evidence-check.js
3069
+ const EXPLICIT_AC_REF = /\bAC\s*:?\s*#?\s*(\d+)\b/gi;
3070
+ const NUMBERED_CRITERION = /^\s*(?:[-*]\s*)?(?:\[[ xX]\]\s*)?(\d+)[.)]\s+\S/;
3071
+ function normalizeAcId(value) {
3072
+ const parsed = Number.parseInt(value, 10);
3073
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
3074
+ return `AC${parsed}`;
3075
+ }
3076
+ function sortAcIds(ids) {
3077
+ return Array.from(ids).sort((a, b) => {
3078
+ const aNum = Number.parseInt(a.replace(/^AC/i, ""), 10);
3079
+ const bNum = Number.parseInt(b.replace(/^AC/i, ""), 10);
3080
+ return aNum - bNum;
3081
+ });
3082
+ }
3083
+ function addExplicitAcRefs(text, ids) {
3084
+ EXPLICIT_AC_REF.lastIndex = 0;
3085
+ let match;
3086
+ while ((match = EXPLICIT_AC_REF.exec(text)) !== null) {
3087
+ const id = normalizeAcId(match[1] ?? "");
3088
+ if (id !== void 0) ids.add(id);
3089
+ }
3090
+ }
3091
+ function extractAcceptanceSection(storyContent) {
3092
+ const lines = storyContent.split(/\r?\n/);
3093
+ const start = lines.findIndex((line) => /^##\s+Acceptance Criteria\s*$/i.test(line.trim()));
3094
+ if (start === -1) return void 0;
3095
+ let end = lines.length;
3096
+ for (let i = start + 1; i < lines.length; i += 1) if (/^##\s+\S/.test(lines[i] ?? "")) {
3097
+ end = i;
3098
+ break;
3099
+ }
3100
+ return lines.slice(start + 1, end).join("\n");
3101
+ }
3102
+ /**
3103
+ * Extract normalized AC ids from story markdown.
3104
+ *
3105
+ * Supports the BMAD default format (`### AC1:`), explicit references such as
3106
+ * `AC: #1`, and plain numbered criteria inside the Acceptance Criteria section.
3107
+ */
3108
+ function extractAcceptanceCriteriaIds(storyContent) {
3109
+ const ids = new Set();
3110
+ const acceptanceSection = extractAcceptanceSection(storyContent);
3111
+ const textToScan = acceptanceSection ?? storyContent;
3112
+ addExplicitAcRefs(textToScan, ids);
3113
+ if (acceptanceSection !== void 0) for (const line of acceptanceSection.split(/\r?\n/)) {
3114
+ const match = line.match(NUMBERED_CRITERION);
3115
+ if (match?.[1] !== void 0) {
3116
+ const id = normalizeAcId(match[1]);
3117
+ if (id !== void 0) ids.add(id);
3118
+ }
3119
+ }
3120
+ return sortAcIds(ids);
3121
+ }
3122
+ function extractClaimedAcceptanceCriteriaIds(values) {
3123
+ const ids = new Set();
3124
+ for (const value of values ?? []) {
3125
+ addExplicitAcRefs(value, ids);
3126
+ const bareNumber = value.trim().match(/^#?(\d+)\b/);
3127
+ if (bareNumber?.[1] !== void 0) {
3128
+ const id = normalizeAcId(bareNumber[1]);
3129
+ if (id !== void 0) ids.add(id);
3130
+ }
3131
+ }
3132
+ return sortAcIds(ids);
3133
+ }
3134
+ function normalizeTestOutcome(value) {
3135
+ if (value === void 0) return void 0;
3136
+ return value.toLowerCase().includes("fail") ? "fail" : "pass";
3137
+ }
3138
+ function formatIds(ids) {
3139
+ return ids.join(", ");
3140
+ }
3141
+ var AcceptanceCriteriaEvidenceCheck = class {
3142
+ name = "acceptance-criteria-evidence";
3143
+ tier = "A";
3144
+ async run(context) {
3145
+ const start = Date.now();
3146
+ const storyContent = context.storyContent?.trim();
3147
+ if (!storyContent) {
3148
+ const findings = [{
3149
+ category: "ac-context-missing",
3150
+ severity: "warn",
3151
+ message: "story content unavailable - skipping AC evidence check"
3152
+ }];
3153
+ return {
3154
+ status: "warn",
3155
+ details: renderFindings(findings),
3156
+ duration_ms: Date.now() - start,
3157
+ findings
3158
+ };
3159
+ }
3160
+ const expectedIds = extractAcceptanceCriteriaIds(storyContent);
3161
+ if (expectedIds.length === 0) {
3162
+ const findings = [{
3163
+ category: "ac-context-missing",
3164
+ severity: "warn",
3165
+ message: "no numbered acceptance criteria found in story"
3166
+ }];
3167
+ return {
3168
+ status: "warn",
3169
+ details: renderFindings(findings),
3170
+ duration_ms: Date.now() - start,
3171
+ findings
3172
+ };
3173
+ }
3174
+ const devResult = context.devStoryResult;
3175
+ if (devResult === void 0) {
3176
+ const findings = [{
3177
+ category: "ac-context-missing",
3178
+ severity: "warn",
3179
+ message: `dev-story result unavailable for ${formatIds(expectedIds)}`
3180
+ }];
3181
+ return {
3182
+ status: "warn",
3183
+ details: renderFindings(findings),
3184
+ duration_ms: Date.now() - start,
3185
+ findings
3186
+ };
3187
+ }
3188
+ const acFailures = devResult.ac_failures ?? [];
3189
+ if (acFailures.length > 0) {
3190
+ const findings = acFailures.map((failure) => ({
3191
+ category: "ac-explicit-failure",
3192
+ severity: "error",
3193
+ message: `dev-story reported AC failure: ${failure}`
3194
+ }));
3195
+ return {
3196
+ status: "fail",
3197
+ details: renderFindings(findings),
3198
+ duration_ms: Date.now() - start,
3199
+ findings
3200
+ };
3201
+ }
3202
+ const testOutcome = normalizeTestOutcome(devResult.tests);
3203
+ if (testOutcome === "fail") {
3204
+ const findings = [{
3205
+ category: "ac-test-failure",
3206
+ severity: "error",
3207
+ message: "dev-story reported failing tests"
3208
+ }];
3209
+ return {
3210
+ status: "fail",
3211
+ details: renderFindings(findings),
3212
+ duration_ms: Date.now() - start,
3213
+ findings
3214
+ };
3215
+ }
3216
+ const claimedIds = new Set(extractClaimedAcceptanceCriteriaIds(devResult.ac_met));
3217
+ const missingIds = expectedIds.filter((id) => !claimedIds.has(id));
3218
+ if (missingIds.length > 0) {
3219
+ const claimedSummary = formatIds(sortAcIds(claimedIds)) || "none";
3220
+ const findings = missingIds.map((id) => ({
3221
+ category: "ac-missing-evidence",
3222
+ severity: "error",
3223
+ message: `missing dev-story AC evidence for ${id} (expected ${formatIds(expectedIds)}, claimed ${claimedSummary})`
3224
+ }));
3225
+ return {
3226
+ status: "fail",
3227
+ details: renderFindings(findings),
3228
+ duration_ms: Date.now() - start,
3229
+ findings
3230
+ };
3231
+ }
3232
+ if (testOutcome === void 0) {
3233
+ const findings = [{
3234
+ category: "ac-test-outcome-missing",
3235
+ severity: "warn",
3236
+ message: `AC evidence covers ${formatIds(expectedIds)} but test outcome is unavailable`
3237
+ }];
3238
+ return {
3239
+ status: "warn",
3240
+ details: renderFindings(findings),
3241
+ duration_ms: Date.now() - start,
3242
+ findings
3243
+ };
3244
+ }
3245
+ return {
3246
+ status: "pass",
3247
+ details: `acceptance-criteria-evidence: AC evidence covers ${formatIds(expectedIds)}; tests=${testOutcome}`,
3248
+ duration_ms: Date.now() - start,
3249
+ findings: []
3250
+ };
3251
+ }
3252
+ };
3253
+
3254
+ //#endregion
3255
+ //#region packages/sdlc/dist/verification/checks/build-check.js
3256
+ /** Hard timeout for the build command in milliseconds (FR-V11). */
3257
+ const BUILD_CHECK_TIMEOUT_MS = 6e4;
3258
+ /** Maximum characters to include in details string from build output. */
3259
+ const MAX_OUTPUT_CHARS = 2e3;
3260
+ /** Per-stream tail size cap for structured findings (story 55-1 convention). */
3261
+ const TAIL_BYTES = 4 * 1024;
3262
+ /** Return the last N bytes of a UTF-8 string, sliced by string length for simplicity. */
3263
+ function tail$1(text, bytes = TAIL_BYTES) {
3264
+ return text.length <= bytes ? text : text.slice(text.length - bytes);
3265
+ }
3266
+ /**
3267
+ * Detect the build command for a project based on files present in `workingDir`.
3268
+ *
3269
+ * Returns an empty string when no recognized build system is found, which
3270
+ * causes BuildCheck to return a 'warn' result without blocking the pipeline.
3271
+ *
3272
+ * NOTE: Do NOT import from src/modules/agent-dispatch/dispatcher-impl.ts —
3273
+ * that would create a circular dependency from packages/sdlc/ → monolith src/.
3274
+ * This function inlines the detection logic independently.
3275
+ */
3276
+ function detectBuildCommand(workingDir) {
3277
+ if (existsSync(join$1(workingDir, "turbo.json"))) return "turbo build";
3278
+ if (existsSync(join$1(workingDir, "pnpm-lock.yaml"))) return "pnpm run build";
3279
+ if (existsSync(join$1(workingDir, "yarn.lock"))) return "yarn build";
3280
+ if (existsSync(join$1(workingDir, "bun.lockb"))) return "bun run build";
3281
+ if (existsSync(join$1(workingDir, "package.json"))) return "npm run build";
3282
+ const nonNodeMarkers = [
3283
+ "pyproject.toml",
3284
+ "poetry.lock",
3285
+ "setup.py",
3286
+ "Cargo.toml",
3287
+ "go.mod"
3288
+ ];
3289
+ for (const marker of nonNodeMarkers) if (existsSync(join$1(workingDir, marker))) return "";
3290
+ return "";
3291
+ }
3292
+ /**
3293
+ * Runs the project's build command and returns pass/warn/fail based on exit code.
3294
+ *
3295
+ * AC1: exit code 0 → pass
3296
+ * AC2: non-zero exit code → fail with truncated output in details
3297
+ * AC3: timeout → kill process group, return fail with timeout message
3298
+ * AC4: no recognized build system → warn without blocking
3299
+ * AC5: explicit buildCommand override respected; empty string → warn (skip)
3300
+ * AC6: name === 'build', tier === 'A'
3301
+ */
3302
+ var BuildCheck = class {
3303
+ name = "build";
3304
+ tier = "A";
3305
+ async run(context) {
3306
+ const start = Date.now();
3307
+ const cmd = context.buildCommand !== void 0 ? context.buildCommand : detectBuildCommand(context.workingDir);
3308
+ if (cmd === "") {
3309
+ const findings = [{
3310
+ category: "build-skip",
3311
+ severity: "warn",
3312
+ message: `no build command detected for project at ${context.workingDir}`
3313
+ }];
3314
+ return {
3315
+ status: "warn",
3316
+ details: renderFindings(findings),
3317
+ duration_ms: Date.now() - start,
3318
+ findings
3319
+ };
3320
+ }
3321
+ return new Promise((resolve$2) => {
3322
+ const child = spawn(cmd, [], {
3323
+ cwd: context.workingDir,
3324
+ detached: true,
3325
+ shell: true,
3326
+ stdio: [
3327
+ "ignore",
3328
+ "pipe",
3329
+ "pipe"
3330
+ ]
3331
+ });
3332
+ let stdout = "";
3333
+ let stderr = "";
3334
+ let output = "";
3335
+ child.stdout?.on("data", (chunk) => {
3336
+ const s = chunk.toString();
3337
+ stdout += s;
3338
+ output += s;
3339
+ });
3340
+ child.stderr?.on("data", (chunk) => {
3341
+ const s = chunk.toString();
3342
+ stderr += s;
3343
+ output += s;
3344
+ });
3345
+ const timeoutHandle = setTimeout(() => {
3346
+ try {
3347
+ process.kill(-child.pid, "SIGKILL");
3348
+ } catch {}
3349
+ const duration = Date.now() - start;
3350
+ const findings = [{
3351
+ category: "build-timeout",
3352
+ severity: "error",
3353
+ message: `command exceeded ${BUILD_CHECK_TIMEOUT_MS}ms`,
3354
+ command: cmd,
3355
+ stdoutTail: tail$1(stdout),
3356
+ stderrTail: tail$1(stderr),
3357
+ durationMs: duration
3358
+ }];
3359
+ resolve$2({
3360
+ status: "fail",
3361
+ details: renderFindings(findings),
3362
+ duration_ms: duration,
3363
+ findings
3364
+ });
3365
+ }, BUILD_CHECK_TIMEOUT_MS);
3366
+ child.on("close", (code) => {
3367
+ clearTimeout(timeoutHandle);
3368
+ const duration = Date.now() - start;
3369
+ if (code === 0) resolve$2({
3370
+ status: "pass",
3371
+ details: "build passed",
3372
+ duration_ms: duration,
3373
+ findings: []
3374
+ });
3375
+ else {
3376
+ const truncated = output.length > MAX_OUTPUT_CHARS ? output.slice(0, MAX_OUTPUT_CHARS) + "... (truncated)" : output;
3377
+ const findings = [{
3378
+ category: "build-error",
3379
+ severity: "error",
3380
+ message: `build failed (exit ${code}): ${truncated}`,
3381
+ command: cmd,
3382
+ ...code !== null ? { exitCode: code } : {},
3383
+ stdoutTail: tail$1(stdout),
3384
+ stderrTail: tail$1(stderr),
3385
+ durationMs: duration
3386
+ }];
3387
+ resolve$2({
3388
+ status: "fail",
3389
+ details: renderFindings(findings),
3390
+ duration_ms: duration,
3391
+ findings
3392
+ });
3393
+ }
3394
+ });
3395
+ });
3396
+ }
3397
+ };
3398
+
3399
+ //#endregion
3400
+ //#region packages/sdlc/dist/verification/probes/types.js
3401
+ /**
3402
+ * Execution sandbox for a runtime probe.
3403
+ *
3404
+ * - `host`: probe runs directly on the operator's machine. Explicit opt-in.
3405
+ * Cheapest; most dangerous. Authors choosing `host` acknowledge the probe
3406
+ * may touch host state (ports, systemd units, filesystem) and take
3407
+ * responsibility for cleanup.
3408
+ * - `twin`: probe runs inside an ephemeral sandbox brokered by the Digital
3409
+ * Twin subsystem (Epic 47). Twin integration is **deferred to Phase 3** —
3410
+ * probes with `sandbox: twin` currently emit a `probe-deferred` warn
3411
+ * finding rather than executing. Authors can declare twin-scoped probes
3412
+ * today and they will execute transparently once Phase 3 lands.
3413
+ */
3414
+ const RuntimeProbeSandboxSchema = z.enum(["host", "twin"]);
3415
+ /**
3416
+ * Default per-probe timeout in milliseconds. Matches the existing
3417
+ * BuildCheck ceiling (60 s) — deliberate, so probe timeouts are bounded
3418
+ * by the same policy the pipeline already uses for long-running checks.
3419
+ */
3420
+ const DEFAULT_PROBE_TIMEOUT_MS = 6e4;
3421
+ /** Hard upper bound on per-probe stdout/stderr retention (≤ 4 KiB — the
3422
+ * same convention as VerificationFinding.{stdoutTail,stderrTail}). */
3423
+ const PROBE_TAIL_BYTES = 4 * 1024;
3424
+ /**
3425
+ * Zod schema for one runtime probe declared in a story's
3426
+ * `## Runtime Probes` section.
3427
+ *
3428
+ * Required fields (`name`, `sandbox`, `command`) force authors to make
3429
+ * intent explicit — no silent defaults that could mask a miswritten probe.
3430
+ * Optional fields cover operational knobs with sensible fallbacks.
3431
+ */
3432
+ const RuntimeProbeSchema = z.object({
3433
+ name: z.string().min(1, "probe name is required"),
3434
+ sandbox: RuntimeProbeSandboxSchema,
3435
+ command: z.string().min(1, "probe command is required"),
3436
+ timeout_ms: z.number().int().positive().optional(),
3437
+ description: z.string().optional()
3438
+ });
3439
+ /** Zod schema for the full list (wrapping the per-probe schema). */
3440
+ const RuntimeProbeListSchema = z.array(RuntimeProbeSchema);
3441
+
3442
+ //#endregion
3443
+ //#region packages/sdlc/dist/verification/probes/parser.js
3444
+ const SECTION_HEADING = /^##\s+Runtime\s+Probes\s*$/i;
3445
+ /**
3446
+ * Return the raw text of the story's `## Runtime Probes` section (excluding
3447
+ * the heading line itself), or `undefined` if the section is not present.
3448
+ *
3449
+ * The section ends at the next `##` heading or end-of-file. Sub-headings
3450
+ * (`###`, `####`) remain part of the section body.
3451
+ */
3452
+ function extractRuntimeProbesSection(storyContent) {
3453
+ const lines = storyContent.split(/\r?\n/);
3454
+ const start = lines.findIndex((line) => SECTION_HEADING.test(line.trim()));
3455
+ if (start === -1) return void 0;
3456
+ let end = lines.length;
3457
+ for (let i = start + 1; i < lines.length; i += 1) if (/^##\s+\S/.test(lines[i] ?? "")) {
3458
+ end = i;
3459
+ break;
3460
+ }
3461
+ return lines.slice(start + 1, end).join("\n");
3462
+ }
3463
+ /**
3464
+ * Extract the body of the first ```yaml (or ```yml) fenced block in the
3465
+ * given section text. Returns `undefined` if no yaml fence is present.
3466
+ *
3467
+ * The opening fence is recognized case-insensitively and may carry an
3468
+ * arbitrary trailing info string (e.g. ```yaml title=...). The closing
3469
+ * fence is any line whose first non-whitespace run is exactly three
3470
+ * backticks.
3471
+ */
3472
+ function extractYamlFence(section) {
3473
+ const lines = section.split(/\r?\n/);
3474
+ let inside = false;
3475
+ let collected;
3476
+ for (const line of lines) {
3477
+ if (!inside) {
3478
+ if (/^\s*```\s*(yaml|yml)\b/i.test(line)) {
3479
+ inside = true;
3480
+ collected = [];
3481
+ }
3482
+ continue;
3483
+ }
3484
+ if (/^\s*```\s*$/.test(line)) return (collected ?? []).join("\n");
3485
+ collected?.push(line);
3486
+ }
3487
+ return void 0;
3488
+ }
3489
+ /**
3490
+ * Parse the `## Runtime Probes` section of a story's markdown content.
3491
+ *
3492
+ * Outcomes:
3493
+ * - section missing → { kind: 'absent' }
3494
+ * - section present, no yaml fence → { kind: 'invalid' }
3495
+ * - section present, yaml fence malformed → { kind: 'invalid' }
3496
+ * - section present, yaml root is not a list → { kind: 'invalid' }
3497
+ * - section present, entry fails RuntimeProbeSchema → { kind: 'invalid' }
3498
+ * - section present, yaml valid, all entries valid → { kind: 'parsed' }
3499
+ *
3500
+ * Duplicate names within a single story are surfaced as `invalid` so that
3501
+ * finding messages can unambiguously reference a probe by name.
3502
+ */
3503
+ function parseRuntimeProbes(storyContent) {
3504
+ const section = extractRuntimeProbesSection(storyContent);
3505
+ if (section === void 0) return { kind: "absent" };
3506
+ const yamlBody = extractYamlFence(section);
3507
+ if (yamlBody === void 0) return {
3508
+ kind: "invalid",
3509
+ error: "## Runtime Probes section is present but contains no terminated ```yaml fenced block"
3510
+ };
3511
+ let parsed;
3512
+ try {
3513
+ parsed = load(yamlBody) ?? [];
3514
+ } catch (err) {
3515
+ const detail = err instanceof YAMLException ? err.message : String(err);
3516
+ return {
3517
+ kind: "invalid",
3518
+ error: `YAML parse error: ${detail}`
3519
+ };
3520
+ }
3521
+ if (!Array.isArray(parsed)) return {
3522
+ kind: "invalid",
3523
+ error: `probe block root must be a YAML list; got ${typeof parsed}`
3524
+ };
3525
+ const validation = RuntimeProbeListSchema.safeParse(parsed);
3526
+ if (!validation.success) {
3527
+ const first = validation.error.issues[0];
3528
+ const path$1 = first?.path.join(".") ?? "";
3529
+ const message = first?.message ?? "schema validation failed";
3530
+ return {
3531
+ kind: "invalid",
3532
+ error: `probe list is malformed at ${path$1 || "<root>"}: ${message}`
3533
+ };
3534
+ }
3535
+ const probes = validation.data;
3536
+ const seen = new Set();
3537
+ for (const probe of probes) {
3538
+ if (seen.has(probe.name)) return {
3539
+ kind: "invalid",
3540
+ error: `duplicate probe name: ${probe.name}`
3541
+ };
3542
+ seen.add(probe.name);
3543
+ }
3544
+ return {
3545
+ kind: "parsed",
3546
+ probes
3547
+ };
3548
+ }
3549
+
3550
+ //#endregion
3551
+ //#region packages/sdlc/dist/verification/probes/executor.js
3552
+ /** Return the last N bytes of a UTF-8 string (sliced by length for simplicity). */
3553
+ function tail(text, bytes = PROBE_TAIL_BYTES) {
3554
+ return text.length <= bytes ? text : text.slice(text.length - bytes);
3555
+ }
3556
+ /**
3557
+ * Execute one probe on the host and return a structured ProbeResult.
3558
+ *
3559
+ * Behavior notes:
3560
+ * - The shell used is `/bin/sh -c '<probe.command>'` inside a detached
3561
+ * process group (so the entire tree is killed on timeout).
3562
+ * - stdout and stderr are captured independently; each is returned
3563
+ * tailed to PROBE_TAIL_BYTES (≤ 4 KiB) so published tarballs of the
3564
+ * run manifest stay small.
3565
+ * - Timeout defaults to `probe.timeout_ms ?? DEFAULT_PROBE_TIMEOUT_MS`
3566
+ * (60 s). When the timeout fires, the process group is SIGKILL'd and
3567
+ * the returned result has `outcome: 'timeout'`, `exitCode` undefined.
3568
+ * - Never throws. Spawn errors (e.g. exec format error) are returned as
3569
+ * `outcome: 'fail'` with exitCode -1 and the error message captured on
3570
+ * stderrTail, so the caller can emit a deterministic finding.
3571
+ */
3572
+ function executeProbeOnHost(probe, options = {}) {
3573
+ const timeoutMs = probe.timeout_ms ?? DEFAULT_PROBE_TIMEOUT_MS;
3574
+ const cwd = options.cwd ?? process.cwd();
3575
+ const env = options.env ?? process.env;
3576
+ const start = Date.now();
3577
+ return new Promise((resolve$2) => {
3578
+ let stdout = "";
3579
+ let stderr = "";
3580
+ let settled = false;
3581
+ const child = spawn(probe.command, [], {
3582
+ cwd,
3583
+ env,
3584
+ detached: true,
3585
+ shell: true,
3586
+ stdio: [
3587
+ "ignore",
3588
+ "pipe",
3589
+ "pipe"
3590
+ ]
3591
+ });
3592
+ const finalize = (result) => {
3593
+ if (settled) return;
3594
+ settled = true;
3595
+ resolve$2(result);
3596
+ };
3597
+ child.on("error", (err) => {
3598
+ finalize({
3599
+ outcome: "fail",
3600
+ command: probe.command,
3601
+ exitCode: -1,
3602
+ stdoutTail: tail(stdout),
3603
+ stderrTail: tail(stderr + (stderr.length > 0 && !stderr.endsWith("\n") ? "\n" : "") + `spawn error: ${err.message}\n`),
3604
+ durationMs: Date.now() - start
3605
+ });
3606
+ });
3607
+ child.stdout?.on("data", (chunk) => {
3608
+ stdout += chunk.toString();
3609
+ });
3610
+ child.stderr?.on("data", (chunk) => {
3611
+ stderr += chunk.toString();
3612
+ });
3613
+ const timeoutHandle = setTimeout(() => {
3614
+ try {
3615
+ if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
3616
+ } catch {}
3617
+ finalize({
3618
+ outcome: "timeout",
3619
+ command: probe.command,
3620
+ stdoutTail: tail(stdout),
3621
+ stderrTail: tail(stderr),
3622
+ durationMs: Date.now() - start
3623
+ });
3624
+ }, timeoutMs);
3625
+ child.on("close", (code) => {
3626
+ clearTimeout(timeoutHandle);
3627
+ const duration = Date.now() - start;
3628
+ finalize({
3629
+ outcome: code === 0 ? "pass" : "fail",
3630
+ command: probe.command,
3631
+ ...code !== null ? { exitCode: code } : {},
3632
+ stdoutTail: tail(stdout),
3633
+ stderrTail: tail(stderr),
3634
+ durationMs: duration
3635
+ });
3636
+ });
3637
+ });
3638
+ }
3639
+
3640
+ //#endregion
3641
+ //#region packages/sdlc/dist/verification/checks/runtime-probe-check.js
3642
+ const CATEGORY_PARSE = "runtime-probe-parse-error";
3643
+ const CATEGORY_SKIP = "runtime-probe-skip";
3644
+ const CATEGORY_DEFERRED = "runtime-probe-deferred";
3645
+ const CATEGORY_FAIL = "runtime-probe-fail";
3646
+ const CATEGORY_TIMEOUT = "runtime-probe-timeout";
3647
+ const defaultExecutors = { host: (probe) => executeProbeOnHost(probe, { cwd: process.cwd() }) };
3648
+ var RuntimeProbeCheck = class {
3649
+ name = "runtime-probes";
3650
+ tier = "A";
3651
+ _executors;
3652
+ constructor(executors) {
3653
+ this._executors = {
3654
+ ...defaultExecutors,
3655
+ ...executors ?? {}
3656
+ };
3657
+ }
3658
+ async run(context) {
3659
+ const start = Date.now();
3660
+ if (context.storyContent === void 0) {
3661
+ const findings$1 = [{
3662
+ category: CATEGORY_SKIP,
3663
+ severity: "warn",
3664
+ message: "story content unavailable — skipping runtime probe check"
3665
+ }];
3666
+ return {
3667
+ status: "warn",
3668
+ details: renderFindings(findings$1),
3669
+ duration_ms: Date.now() - start,
3670
+ findings: findings$1
3671
+ };
3672
+ }
3673
+ const parsed = parseRuntimeProbes(context.storyContent);
3674
+ if (parsed.kind === "absent") return {
3675
+ status: "pass",
3676
+ details: "runtime-probes: no ## Runtime Probes section declared — skipping",
3677
+ duration_ms: Date.now() - start,
3678
+ findings: []
3679
+ };
3680
+ if (parsed.kind === "invalid") {
3681
+ const findings$1 = [{
3682
+ category: CATEGORY_PARSE,
3683
+ severity: "error",
3684
+ message: parsed.error
3685
+ }];
3686
+ return {
3687
+ status: "fail",
3688
+ details: renderFindings(findings$1),
3689
+ duration_ms: Date.now() - start,
3690
+ findings: findings$1
3691
+ };
3692
+ }
3693
+ if (parsed.probes.length === 0) return {
3694
+ status: "pass",
3695
+ details: "runtime-probes: 0 probes declared — skipping",
3696
+ duration_ms: Date.now() - start,
3697
+ findings: []
3698
+ };
3699
+ const findings = [];
3700
+ for (const probe of parsed.probes) {
3701
+ if (probe.sandbox === "twin") {
3702
+ findings.push({
3703
+ category: CATEGORY_DEFERRED,
3704
+ severity: "warn",
3705
+ message: `probe "${probe.name}" uses sandbox=twin which is deferred until Phase 3 (Digital Twin integration); skipping`
3706
+ });
3707
+ continue;
3708
+ }
3709
+ const result = await this._executors.host(probe);
3710
+ if (result.outcome === "pass") continue;
3711
+ const category = result.outcome === "timeout" ? CATEGORY_TIMEOUT : CATEGORY_FAIL;
3712
+ const descriptor = probe.description ? ` (${probe.description})` : "";
3713
+ const message = result.outcome === "timeout" ? `probe "${probe.name}"${descriptor} timed out after ${result.durationMs}ms` : `probe "${probe.name}"${descriptor} failed with exit ${result.exitCode ?? "unknown"}`;
3714
+ findings.push({
3715
+ category,
3716
+ severity: "error",
3717
+ message,
3718
+ command: result.command,
3719
+ ...result.exitCode !== void 0 ? { exitCode: result.exitCode } : {},
3720
+ stdoutTail: result.stdoutTail,
3721
+ stderrTail: result.stderrTail,
3722
+ durationMs: result.durationMs
3723
+ });
3724
+ }
3725
+ const status = findings.some((f) => f.severity === "error") ? "fail" : findings.some((f) => f.severity === "warn") ? "warn" : "pass";
3726
+ return {
3727
+ status,
3728
+ details: findings.length > 0 ? renderFindings(findings) : `runtime-probes: ${parsed.probes.length} probe(s) passed`,
3729
+ duration_ms: Date.now() - start,
3730
+ findings
3731
+ };
3732
+ }
3733
+ };
3734
+
3735
+ //#endregion
3736
+ //#region packages/sdlc/dist/verification/verification-pipeline.js
3737
+ /**
3738
+ * Compute the worst-case aggregate status across a list of check results.
3739
+ * Precedence: fail > warn > pass.
3740
+ */
3741
+ function aggregateStatus(checks) {
3742
+ let result = "pass";
3743
+ for (const c of checks) {
3744
+ if (c.status === "fail") return "fail";
3745
+ if (c.status === "warn") result = "warn";
3746
+ }
3747
+ return result;
3748
+ }
3749
+ /**
3750
+ * Runs an ordered chain of VerificationCheck implementations after each story dispatch.
3751
+ *
3752
+ * Checks are stored in registration order. When `run()` is called with `tier: 'A'`
3753
+ * only Tier A checks execute; when called with `tier: 'B'` only Tier B checks execute.
3754
+ * (Story 51-5 will invoke both tiers at the appropriate orchestration points.)
3755
+ */
3756
+ var VerificationPipeline = class {
3757
+ _bus;
3758
+ _checks = [];
3759
+ /**
3760
+ * @param bus Typed event bus for emitting verification events.
3761
+ * @param checks Optional initial list of checks to register at construction time.
3762
+ */
3763
+ constructor(bus, checks = []) {
3764
+ this._bus = bus;
3765
+ for (const check of checks) this.register(check);
3766
+ }
3767
+ /**
3768
+ * Register a VerificationCheck.
3769
+ *
3770
+ * Checks are stored in insertion order within their tier.
3771
+ * Tier A checks always run before Tier B checks regardless of registration order.
3772
+ */
3773
+ register(check) {
3774
+ this._checks.push(check);
3775
+ }
3776
+ /**
3777
+ * Execute all checks matching the specified tier sequentially.
3778
+ *
3779
+ * AC2: Tier A checks execute in registration order.
3780
+ * AC4: Results are aggregated into a VerificationSummary.
3781
+ * AC5: verification:check-complete and verification:story-complete events are emitted.
3782
+ * AC6: Unhandled exceptions are caught and recorded as warn.
3783
+ *
3784
+ * @param context Verification context for the story being verified.
3785
+ * @param tier Which tier of checks to execute ('A' | 'B'). Defaults to 'A'.
3786
+ */
3787
+ async run(context, tier = "A") {
3788
+ const pipelineStart = Date.now();
3789
+ const checks = this._checks.filter((c) => c.tier === tier);
3790
+ const checkResults = [];
3791
+ for (const check of checks) {
3792
+ const checkStart = Date.now();
3793
+ let result;
3794
+ try {
3795
+ const runResult = await check.run(context);
3796
+ result = {
3797
+ checkName: check.name,
3798
+ status: runResult.status,
3799
+ details: runResult.details,
3800
+ duration_ms: runResult.duration_ms,
3801
+ ...runResult.findings !== void 0 ? { findings: runResult.findings } : {}
3802
+ };
3803
+ } catch (err) {
3804
+ const elapsed = Date.now() - checkStart;
3805
+ const message = err instanceof Error ? err.message : String(err);
3806
+ process.stderr.write(`[verification-pipeline] check "${check.name}" threw an unhandled exception: ${message}\n`);
3807
+ result = {
3808
+ checkName: check.name,
3809
+ status: "warn",
3810
+ details: message,
3811
+ duration_ms: elapsed,
3812
+ findings: [{
3813
+ category: "check-exception",
3814
+ severity: "warn",
3815
+ message
3816
+ }]
3817
+ };
3818
+ }
3819
+ checkResults.push(result);
3820
+ this._bus.emit("verification:check-complete", {
3821
+ storyKey: context.storyKey,
3822
+ checkName: result.checkName,
3823
+ status: result.status,
3824
+ details: result.details,
3825
+ duration_ms: result.duration_ms
3826
+ });
3827
+ }
3828
+ const summary = {
3829
+ storyKey: context.storyKey,
3830
+ checks: checkResults,
3831
+ status: aggregateStatus(checkResults),
3832
+ duration_ms: Date.now() - pipelineStart
3833
+ };
3834
+ this._bus.emit("verification:story-complete", summary);
3835
+ return summary;
3836
+ }
3837
+ };
3838
+ /**
3839
+ * Create a VerificationPipeline pre-loaded with the canonical check set.
3840
+ *
3841
+ * Canonical Tier A check order:
3842
+ * 1. PhantomReviewCheck — story 51-2 (runs first: unreviewed stories skipped)
3843
+ * 2. TrivialOutputCheck — story 51-3
3844
+ * 3. AcceptanceCriteriaEvidenceCheck
3845
+ * 4. BuildCheck — story 51-4
3846
+ * 5. RuntimeProbeCheck — Epic 55 Phase 2: runtime behavior gate; runs last
3847
+ * in Tier A because probes may depend on built artifacts
3848
+ *
3849
+ * @param bus Typed event bus for verification events.
3850
+ * @param config Optional config (used to forward threshold to TrivialOutputCheck).
3851
+ */
3852
+ function createDefaultVerificationPipeline(bus, config) {
3853
+ const checks = [
3854
+ new PhantomReviewCheck(),
3855
+ new TrivialOutputCheck(config),
3856
+ new AcceptanceCriteriaEvidenceCheck(),
3857
+ new BuildCheck(),
3858
+ new RuntimeProbeCheck()
3859
+ ];
3860
+ return new VerificationPipeline(bus, checks);
3861
+ }
3862
+
2907
3863
  //#endregion
2908
3864
  //#region packages/sdlc/dist/run-model/cli-flags.js
2909
3865
  /**
@@ -2928,6 +3884,27 @@ const CliFlagsSchema = z.object({
2928
3884
  //#endregion
2929
3885
  //#region packages/sdlc/dist/run-model/verification-result.js
2930
3886
  /**
3887
+ * Schema for a single structured verification finding (story 55-1 / 55-3).
3888
+ *
3889
+ * Mirrors the VerificationFinding interface in
3890
+ * packages/sdlc/src/verification/findings.ts without importing from that
3891
+ * module to keep run-model free of a dependency on verification.
3892
+ */
3893
+ const StoredVerificationFindingSchema = z.object({
3894
+ category: z.string(),
3895
+ severity: z.enum([
3896
+ "error",
3897
+ "warn",
3898
+ "info"
3899
+ ]),
3900
+ message: z.string(),
3901
+ command: z.string().optional(),
3902
+ exitCode: z.number().int().optional(),
3903
+ stdoutTail: z.string().optional(),
3904
+ stderrTail: z.string().optional(),
3905
+ durationMs: z.number().nonnegative().optional()
3906
+ });
3907
+ /**
2931
3908
  * Schema for a single per-check verification result stored in the manifest.
2932
3909
  *
2933
3910
  * Mirrors VerificationCheckResult from packages/sdlc/src/verification/types.ts
@@ -2941,7 +3918,8 @@ const StoredVerificationCheckResultSchema = z.object({
2941
3918
  "fail"
2942
3919
  ]),
2943
3920
  details: z.string(),
2944
- duration_ms: z.number().nonnegative()
3921
+ duration_ms: z.number().nonnegative(),
3922
+ findings: z.array(StoredVerificationFindingSchema).optional()
2945
3923
  });
2946
3924
  /**
2947
3925
  * Schema for the aggregated verification pipeline summary stored in the manifest.
@@ -4252,5 +5230,5 @@ function registerHealthCommand(program, _version = "0.0.0", projectRoot = proces
4252
5230
  }
4253
5231
 
4254
5232
  //#endregion
4255
- 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, __commonJS, __require, __toESM, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter$1 as createDatabaseAdapter, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, createStateStore, detectCycles, extractTargetFilesFromStoryContent, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, isOrchestratorProcessLine, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, runHealthAction, validateStoryKey };
4256
- //# sourceMappingURL=health-BfeoutPu.js.map
5233
+ 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, __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, runHealthAction, validateStoryKey };
5234
+ //# sourceMappingURL=health-DHLR9Iz1.js.map