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.
- package/dist/cli/index.js +4 -4
- package/dist/{health-Cs2x975P.js → health-CQTK6ltK.js} +1 -1
- package/dist/{health-BfeoutPu.js → health-DHLR9Iz1.js} +981 -3
- package/dist/{run-8UnjRlkK.js → run-ofO9AWFc.js} +44 -476
- package/dist/{run-COw1z5NJ.js → run-s6bRK0LF.js} +2 -2
- package/package.json +1 -1
- package/packs/bmad/prompts/fix-story.md +3 -0
- package/packs/bmad/prompts/rework-story.md +3 -0
|
@@ -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-
|
|
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
|