agentic-workflow-manager 8.1.4 → 8.1.6
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/src/commands/preflight/checks.js +70 -1
- package/dist/src/commands/preflight/index.js +6 -5
- package/dist/src/commands/sensors/changed.js +15 -0
- package/dist/src/commands/sensors/compatibility/contract.js +17 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -4
- package/dist/src/commands/sensors/compatibility/timeout.js +21 -0
- package/dist/src/commands/sensors/exec.js +3 -1
- package/dist/src/commands/sensors/index.js +5 -13
- package/dist/src/commands/sensors/init.js +1 -0
- package/dist/src/commands/sensors/prepare.js +140 -0
- package/dist/src/commands/sensors/result.js +140 -0
- package/dist/src/commands/sensors/run.js +56 -335
- package/dist/src/commands/sensors/status.js +93 -21
- package/dist/src/commands/sensors/verdict.js +27 -0
- package/dist/tests/commands/preflight/preflight.test.js +50 -0
- package/dist/tests/commands/sensors/baseline.test.js +14 -0
- package/dist/tests/commands/sensors/changed-windows.test.js +3 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +51 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +27 -11
- package/dist/tests/commands/sensors/compatibility/probe.test.js +13 -3
- package/dist/tests/commands/sensors/exec-fixtures.js +1 -1
- package/dist/tests/commands/sensors/exec.test.js +36 -0
- package/dist/tests/commands/sensors/index.test.js +46 -8
- package/dist/tests/commands/sensors/init.test.js +30 -1
- package/dist/tests/commands/sensors/prepare.test.js +88 -0
- package/dist/tests/commands/sensors/router.test.js +15 -1
- package/dist/tests/commands/sensors/run-changed.test.js +4 -4
- package/dist/tests/commands/sensors/run.test.js +29 -0
- package/dist/tests/commands/sensors/status-windows.test.js +1 -1
- package/dist/tests/commands/sensors/status.test.js +52 -20
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +79 -3
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +40 -1
- package/dist/tests/structural/sensor-documentation-contract.test.js +33 -5
- package/dist/tests/structural/support-matrix-is-current.test.js +25 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkSensorExecution = checkSensorExecution;
|
|
6
7
|
exports.preflight = preflight;
|
|
7
8
|
const child_process_1 = require("child_process");
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -10,6 +11,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
10
11
|
const status_1 = require("../sensors/status");
|
|
11
12
|
const init_1 = require("../sensors/init");
|
|
12
13
|
const baseline_1 = require("../sensors/baseline");
|
|
14
|
+
const run_1 = require("../sensors/run");
|
|
13
15
|
const paths_1 = require("../../core/paths");
|
|
14
16
|
const MANIFEST = path_1.default.join('.awm', 'sensors.json');
|
|
15
17
|
/**
|
|
@@ -193,6 +195,67 @@ function checkSensorsBaseline(cwd, manifest) {
|
|
|
193
195
|
+ 'so the gate only chases new problems',
|
|
194
196
|
};
|
|
195
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Empirical diagnostics intentionally name only stable execution facts. Sensor output
|
|
200
|
+
* can include source, environment values, or tool-specific unbounded text; preflight
|
|
201
|
+
* is a phase gate, not a log transport, so none of that crosses this boundary.
|
|
202
|
+
*/
|
|
203
|
+
function executionReason(sensor) {
|
|
204
|
+
const reason = sensor.skipReason ?? sensor.incomplete ?? '';
|
|
205
|
+
if (/^timeout after \d+ms/.test(reason))
|
|
206
|
+
return 'timeout';
|
|
207
|
+
if (/^output exceeded \d+ bytes/.test(reason))
|
|
208
|
+
return 'output limit exceeded';
|
|
209
|
+
const exit = /^exit (\d+):/.exec(reason);
|
|
210
|
+
if (exit)
|
|
211
|
+
return `exit ${exit[1]} without parseable findings`;
|
|
212
|
+
if (sensor.status === 'fail')
|
|
213
|
+
return 'reported findings or an actionable execution failure';
|
|
214
|
+
if (sensor.status === 'skipped')
|
|
215
|
+
return 'not applicable to this execution';
|
|
216
|
+
return 'execution did not establish a conclusive result';
|
|
217
|
+
}
|
|
218
|
+
function renderExecutionFailure(output) {
|
|
219
|
+
const failed = output.sensors.filter(sensor => sensor.status !== 'pass');
|
|
220
|
+
if (failed.length === 0)
|
|
221
|
+
return `sensor verdict was ${output.overall}; no sensor established an empirical pass`;
|
|
222
|
+
return failed.map(sensor => {
|
|
223
|
+
const evidence = sensor.execution;
|
|
224
|
+
if (!evidence)
|
|
225
|
+
return `${sensor.name} (${sensor.status}): no bounded execution evidence; ${executionReason(sensor)}`;
|
|
226
|
+
return `${sensor.name} (${sensor.status}): timeout ${evidence.timeoutMs}ms (${evidence.timeoutSource}), `
|
|
227
|
+
+ `elapsed ${evidence.elapsedMs}ms; ${executionReason(sensor)}`;
|
|
228
|
+
}).join('; ');
|
|
229
|
+
}
|
|
230
|
+
function executionRemedy(output) {
|
|
231
|
+
if (output.sensors.every(sensor => sensor.status === 'pass')) {
|
|
232
|
+
return 'configure at least one runnable sensor with `awm sensors init`, then rerun `awm preflight --verify-sensors`';
|
|
233
|
+
}
|
|
234
|
+
return 'diagnose the named sensor; if a healthy progressing run needs longer, set a finite sensor timeout and rerun `awm preflight --verify-sensors`';
|
|
235
|
+
}
|
|
236
|
+
/** Run the full read-only sensor gate and reject every verdict except `pass`. */
|
|
237
|
+
async function checkSensorExecution(cwd) {
|
|
238
|
+
try {
|
|
239
|
+
const output = await (0, run_1.runSensors)({ cwd, all: true });
|
|
240
|
+
if (output.overall === 'pass') {
|
|
241
|
+
return { id: 'sensors-execution', ok: true, detail: 'all selected sensors completed with pass' };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
id: 'sensors-execution',
|
|
245
|
+
ok: false,
|
|
246
|
+
detail: renderExecutionFailure(output),
|
|
247
|
+
remedy: executionRemedy(output),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Fail closed without relaying an arbitrary tool/configuration error to JSON.
|
|
252
|
+
return {
|
|
253
|
+
id: 'sensors-execution', ok: false,
|
|
254
|
+
detail: 'sensor execution could not establish an empirical verdict',
|
|
255
|
+
remedy: 'repair the sensor configuration, then rerun `awm preflight --verify-sensors`',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
196
259
|
/**
|
|
197
260
|
* Extract just the hostname portion of a git remote URL — never match against the
|
|
198
261
|
* full URL string. A bare substring check against the whole remote (`remote.includes
|
|
@@ -276,7 +339,10 @@ function checkHost(cwd) {
|
|
|
276
339
|
// Bitbucket, Azure DevOps, an internal git server, etc. — don't overclaim support.
|
|
277
340
|
return { id: 'host', ok: true, detail: 'git host not recognized (github/gitlab) — PR/MR automation not applicable' };
|
|
278
341
|
}
|
|
279
|
-
async function preflight(cwd = process.cwd()) {
|
|
342
|
+
async function preflight(cwd = process.cwd(), opts = {}) {
|
|
343
|
+
if (!opts || typeof opts !== 'object' || Array.isArray(opts) || (opts.verifySensors !== undefined && typeof opts.verifySensors !== 'boolean')) {
|
|
344
|
+
throw new Error('preflight options must contain an optional boolean verifySensors');
|
|
345
|
+
}
|
|
280
346
|
const manifest = readManifest(cwd);
|
|
281
347
|
const manifestExists = fs_1.default.existsSync(path_1.default.join(cwd, MANIFEST));
|
|
282
348
|
const checks = [
|
|
@@ -286,6 +352,9 @@ async function preflight(cwd = process.cwd()) {
|
|
|
286
352
|
// a baseline that has nothing to snapshot) on a repo that was never set up
|
|
287
353
|
// buries the one thing the operator needs to read.
|
|
288
354
|
...(manifestExists ? [await checkTools(cwd), checkPack(cwd, manifest), checkSensorsBaseline(cwd, manifest)] : []),
|
|
355
|
+
// The empirical mode is intentionally opt-in: ordinary preflight remains a
|
|
356
|
+
// quick static inspection and must not dispatch project software.
|
|
357
|
+
...(opts.verifySensors === true ? [await checkSensorExecution(cwd)] : []),
|
|
289
358
|
// Runs unconditionally — orthogonal to sensor configuration entirely, this is
|
|
290
359
|
// about PR/MR tooling, not sensors.
|
|
291
360
|
checkHost(cwd),
|
|
@@ -11,10 +11,10 @@ const checks_1 = require("./checks");
|
|
|
11
11
|
/**
|
|
12
12
|
* Exit code. Anything but `ready` exits 1.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* `awm sensors run` exits zero only for an empirical `pass`; preflight mirrors that
|
|
15
|
+
* binary-gate rule for its own `ready` status. It is invoked explicitly by a phase
|
|
16
|
+
* gate, so the exit code carries the verdict and the caller need not infer it from
|
|
17
|
+
* a field in JSON.
|
|
18
18
|
*/
|
|
19
19
|
function exitCodeFor(report) {
|
|
20
20
|
return report.status === 'ready' ? 0 : 1;
|
|
@@ -44,9 +44,10 @@ function registerPreflightCommand(program) {
|
|
|
44
44
|
.command('preflight')
|
|
45
45
|
.description('verify the project harness can actually gate before development starts')
|
|
46
46
|
.option('--json', 'emit the report as JSON')
|
|
47
|
+
.option('--verify-sensors', 'run the complete sensor gate before unattended execution')
|
|
47
48
|
.option('--cwd <path>', 'project directory to check (default: current)')
|
|
48
49
|
.action(async (opts) => {
|
|
49
|
-
const report = await (0, checks_1.preflight)(opts.cwd ?? process.cwd());
|
|
50
|
+
const report = await (0, checks_1.preflight)(opts.cwd ?? process.cwd(), { verifySensors: opts.verifySensors === true });
|
|
50
51
|
process.stdout.write(opts.json ? JSON.stringify(report, null, 2) + '\n' : formatReport(report));
|
|
51
52
|
const code = exitCodeFor(report);
|
|
52
53
|
// `process.exit()` may truncate the JSON written immediately above when
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.changedFiles = changedFiles;
|
|
7
7
|
exports.hasUnsafeWin32Chars = hasUnsafeWin32Chars;
|
|
8
|
+
exports.changedScopeError = changedScopeError;
|
|
8
9
|
exports.applyChangedCmd = applyChangedCmd;
|
|
9
10
|
exports.filterByExtension = filterByExtension;
|
|
10
11
|
const child_process_1 = require("child_process");
|
|
@@ -80,6 +81,20 @@ const WIN32_UNSAFE_CHARS = /[&|<>^%\r\n]/;
|
|
|
80
81
|
function hasUnsafeWin32Chars(file) {
|
|
81
82
|
return WIN32_UNSAFE_CHARS.test(file);
|
|
82
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Explain why a resolved diff cannot safely be interpolated into a legacy shell
|
|
86
|
+
* command. Structured v2 commands never need this check: their file names remain
|
|
87
|
+
* literal argv entries and never meet a shell.
|
|
88
|
+
*/
|
|
89
|
+
function changedScopeError(changed) {
|
|
90
|
+
if (changed.error)
|
|
91
|
+
return changed.error;
|
|
92
|
+
if ((0, paths_1.isWindowsNative)() && changed.files.some(hasUnsafeWin32Chars)) {
|
|
93
|
+
return 'a changed filename contains a cmd.exe metacharacter (& | < > ^ % or newline/CR) '
|
|
94
|
+
+ 'that quoting cannot reliably neutralize on native Windows — refusing to interpolate it';
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
83
98
|
/**
|
|
84
99
|
* CommandLineToArgvW-safe quoting (the algorithm behind Python's
|
|
85
100
|
* `subprocess.list2cmdline`, Rust's `std::process::Command` on Windows, and .NET's
|
|
@@ -12,6 +12,7 @@ const semver_1 = __importDefault(require("semver"));
|
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const contract_1 = require("../coverage/contract");
|
|
15
|
+
const timeout_1 = require("./timeout");
|
|
15
16
|
const PACK_SCHEMA_VERSION = 2;
|
|
16
17
|
const SHELL_EXECUTABLES = new Set(['sh', 'bash', 'cmd', 'powershell']);
|
|
17
18
|
const PACKAGE_MANAGERS = new Set(['npm', 'pnpm', 'yarn', 'bun']);
|
|
@@ -193,7 +194,7 @@ function parseStructuredCommand(input, source) {
|
|
|
193
194
|
}
|
|
194
195
|
function parseVariant(input, source, location) {
|
|
195
196
|
const value = record(input, source, location);
|
|
196
|
-
fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'assets', 'formatter', 'probe', 'policyRef'], source, location);
|
|
197
|
+
fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'changedCommand', 'assets', 'formatter', 'probe', 'policyRef'], source, location);
|
|
197
198
|
const certifiedRange = text(value.certifiedRange, source, `${location}.certifiedRange`);
|
|
198
199
|
if (semver_1.default.validRange(certifiedRange) === null)
|
|
199
200
|
invalid(source, `${location}.certifiedRange must be a valid semver range`);
|
|
@@ -217,6 +218,9 @@ function parseVariant(input, source, location) {
|
|
|
217
218
|
if (typeof probe.kind !== 'string' || !ALLOWED_PROBES.has(probe.kind))
|
|
218
219
|
invalid(source, `${location}.probe.kind must be an allowed probe`);
|
|
219
220
|
}
|
|
221
|
+
const changedCommand = 'changedCommand' in value ? parseStructuredCommand(value.changedCommand, source) : undefined;
|
|
222
|
+
if (changedCommand && !changedCommand.fileInput)
|
|
223
|
+
invalid(source, `${location}.changedCommand must declare fileInput`);
|
|
220
224
|
return {
|
|
221
225
|
id: id(value.id, source, `${location}.id`),
|
|
222
226
|
priority: value.priority,
|
|
@@ -230,6 +234,7 @@ function parseVariant(input, source, location) {
|
|
|
230
234
|
probe: { kind: policy?.probe ?? probe.kind },
|
|
231
235
|
...(policy ? { policyRef: SEMGREP_POLICY_REF } : {}),
|
|
232
236
|
command: parseStructuredCommand(value.command, source),
|
|
237
|
+
...(changedCommand ? { changedCommand } : {}),
|
|
233
238
|
};
|
|
234
239
|
}
|
|
235
240
|
function parseHardening(input, source) {
|
|
@@ -265,7 +270,7 @@ function assertNoEqualPriorityOverlap(variants) {
|
|
|
265
270
|
}
|
|
266
271
|
function parseSensor(input, source, location, variantIds) {
|
|
267
272
|
const value = record(input, source, location);
|
|
268
|
-
fields(value, ['applicability', 'variants', 'fast'], source, location);
|
|
273
|
+
fields(value, ['applicability', 'variants', 'fast', 'timeout'], source, location);
|
|
269
274
|
if (!Array.isArray(value.variants) || value.variants.length === 0)
|
|
270
275
|
invalid(source, `${location}.variants must be a nonempty array`);
|
|
271
276
|
const variants = value.variants.map((variant, index) => parseVariant(variant, source, `${location}.variants[${index}]`));
|
|
@@ -293,7 +298,16 @@ function parseSensor(input, source, location, variantIds) {
|
|
|
293
298
|
invalid(source, `${location}.applicability must declare a condition`);
|
|
294
299
|
if ('fast' in value && typeof value.fast !== 'boolean')
|
|
295
300
|
invalid(source, `${location}.fast must be a boolean`);
|
|
296
|
-
|
|
301
|
+
let timeout;
|
|
302
|
+
if ('timeout' in value) {
|
|
303
|
+
try {
|
|
304
|
+
timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
invalid(source, error instanceof Error ? error.message : `${location}.timeout must be a positive safe integer`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return { applicability, variants, ...('fast' in value ? { fast: value.fast } : {}), ...(timeout !== undefined ? { timeout } : {}) };
|
|
297
311
|
}
|
|
298
312
|
function legacyCompatibility() {
|
|
299
313
|
return {
|
|
@@ -7,6 +7,7 @@ exports.legacyCompatibility = legacyCompatibility;
|
|
|
7
7
|
exports.parseSensorManifest = parseSensorManifest;
|
|
8
8
|
exports.serializeManifestV2 = serializeManifestV2;
|
|
9
9
|
const contract_1 = require("./contract");
|
|
10
|
+
const timeout_1 = require("./timeout");
|
|
10
11
|
const semver_1 = __importDefault(require("semver"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
12
13
|
function isRecord(value) {
|
|
@@ -106,9 +107,12 @@ function parseLegacySensor(input, source, location) {
|
|
|
106
107
|
sensor.enabled = value.enabled;
|
|
107
108
|
}
|
|
108
109
|
if ('timeout' in value) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
try {
|
|
111
|
+
sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
invalid(source, error instanceof Error ? error.message : `${location}.timeout must be a positive safe integer`);
|
|
115
|
+
}
|
|
112
116
|
}
|
|
113
117
|
if ('changedCmd' in value)
|
|
114
118
|
sensor.changedCmd = text(value.changedCmd, source, `${location}.changedCmd`);
|
|
@@ -135,7 +139,7 @@ function parseLegacyManifest(value, source) {
|
|
|
135
139
|
}
|
|
136
140
|
function parseV2Sensor(input, source, location) {
|
|
137
141
|
const value = record(input, source, location);
|
|
138
|
-
fields(value, ['enabled', 'fast', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
|
|
142
|
+
fields(value, ['enabled', 'fast', 'timeout', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
|
|
139
143
|
if (typeof value.enabled !== 'boolean')
|
|
140
144
|
invalid(source, `${location}.enabled must be a boolean`);
|
|
141
145
|
const sensor = {
|
|
@@ -163,6 +167,14 @@ function parseV2Sensor(input, source, location) {
|
|
|
163
167
|
invalid(source, `${location}.fast must be a boolean`);
|
|
164
168
|
sensor.fast = value.fast;
|
|
165
169
|
}
|
|
170
|
+
if ('timeout' in value) {
|
|
171
|
+
try {
|
|
172
|
+
sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
invalid(source, error instanceof Error ? error.message : `${location}.timeout must be a positive safe integer`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
166
178
|
return sensor;
|
|
167
179
|
}
|
|
168
180
|
function provenanceRoot(value, source) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.positiveTimeout = positiveTimeout;
|
|
4
|
+
exports.resolveTimeout = resolveTimeout;
|
|
5
|
+
function positiveTimeout(value, location) {
|
|
6
|
+
if (typeof location !== 'string' || location.trim() === '')
|
|
7
|
+
throw new Error('timeout location must be a nonempty string');
|
|
8
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
|
|
9
|
+
throw new Error(`${location} must be a positive safe integer`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function resolveTimeout(input) {
|
|
14
|
+
if (!input || typeof input !== 'object' || typeof input.fast !== 'boolean')
|
|
15
|
+
throw new Error('timeout resolution input is invalid');
|
|
16
|
+
if (input.project !== undefined)
|
|
17
|
+
return { timeoutMs: positiveTimeout(input.project, 'project timeout'), source: 'project' };
|
|
18
|
+
if (input.pack !== undefined)
|
|
19
|
+
return { timeoutMs: positiveTimeout(input.pack, 'pack timeout'), source: 'pack' };
|
|
20
|
+
return { timeoutMs: input.fast ? 10_000 : 120_000, source: 'fallback' };
|
|
21
|
+
}
|
|
@@ -73,6 +73,7 @@ function collectSpawn(input, opts) {
|
|
|
73
73
|
const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
74
74
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
75
75
|
return new Promise((resolve) => {
|
|
76
|
+
const startedAt = process.hrtime.bigint();
|
|
76
77
|
let stdout = '';
|
|
77
78
|
let stderr = '';
|
|
78
79
|
let timedOut = false;
|
|
@@ -106,7 +107,8 @@ function collectSpawn(input, opts) {
|
|
|
106
107
|
return;
|
|
107
108
|
settled = true;
|
|
108
109
|
timers.forEach(clearTimeout);
|
|
109
|
-
|
|
110
|
+
const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1000000n);
|
|
111
|
+
resolve({ stdout, stderr, code: null, signal: null, timedOut, overflowed, elapsedMs, ...extra });
|
|
110
112
|
};
|
|
111
113
|
/** Cut the run short: kill the tree, escalate, and never hang waiting for it. */
|
|
112
114
|
const cutShort = () => {
|
|
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.exitCodeFor = exitCodeFor;
|
|
7
6
|
exports.parsePositiveSafeInteger = parsePositiveSafeInteger;
|
|
8
7
|
exports.registerSensorsCommand = registerSensorsCommand;
|
|
9
8
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
@@ -16,12 +15,7 @@ const baseline_1 = require("./baseline");
|
|
|
16
15
|
const coverage_1 = require("./coverage");
|
|
17
16
|
const render_1 = require("./coverage/render");
|
|
18
17
|
const registries_1 = require("../../core/registries");
|
|
19
|
-
|
|
20
|
-
* not_certified intentionally exits 0: its signal lives in `overall`, because
|
|
21
|
-
* exit code 2 is a blocking error in Claude Code hooks. */
|
|
22
|
-
function exitCodeFor(output) {
|
|
23
|
-
return output.overall === 'fail' ? 1 : 0;
|
|
24
|
-
}
|
|
18
|
+
const verdict_1 = require("./verdict");
|
|
25
19
|
/** Commander coercion for coverage recurrence emphasis. It deliberately runs
|
|
26
20
|
* before the action, so an invalid value cannot trigger ledger I/O. */
|
|
27
21
|
function parsePositiveSafeInteger(value) {
|
|
@@ -65,9 +59,7 @@ function registerSensorsCommand(program) {
|
|
|
65
59
|
// Emit the verdict ALWAYS — an empty `sensors` with overall:'not_certified'
|
|
66
60
|
// must be visible, never a silent exit-0 that reads as "clean".
|
|
67
61
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
68
|
-
|
|
69
|
-
if (code !== 0)
|
|
70
|
-
process.exit(code);
|
|
62
|
+
process.exitCode = (0, verdict_1.exitCodeForVerdict)(output.overall);
|
|
71
63
|
});
|
|
72
64
|
sensors
|
|
73
65
|
.command('init')
|
|
@@ -110,10 +102,10 @@ function registerSensorsCommand(program) {
|
|
|
110
102
|
});
|
|
111
103
|
sensors
|
|
112
104
|
.command('status')
|
|
113
|
-
.description('check sensor
|
|
105
|
+
.description('check static sensor readiness for the current project')
|
|
114
106
|
.action(async () => {
|
|
115
107
|
const status = await (0, status_1.computeSensorStatus)();
|
|
116
|
-
const icon = status.overall === '
|
|
108
|
+
const icon = status.overall === 'READY' ? picocolors_1.default.green('✔') : picocolors_1.default.yellow('⚠');
|
|
117
109
|
console.log(`\nPack: ${status.pack ?? 'none'}`);
|
|
118
110
|
console.log(`Overall: ${icon} ${status.overall}\n`);
|
|
119
111
|
for (const [name, check] of Object.entries(status.checks)) {
|
|
@@ -121,7 +113,7 @@ function registerSensorsCommand(program) {
|
|
|
121
113
|
console.log(` ${mark} ${name.padEnd(12)} ${check.detail}`);
|
|
122
114
|
}
|
|
123
115
|
console.log('');
|
|
124
|
-
if (status.overall !== '
|
|
116
|
+
if (status.overall !== 'READY')
|
|
125
117
|
process.exit(1);
|
|
126
118
|
});
|
|
127
119
|
sensors
|
|
@@ -272,6 +272,7 @@ async function initSensors(opts = {}) {
|
|
|
272
272
|
sensors[name] = {
|
|
273
273
|
enabled: prior?.enabled ?? true,
|
|
274
274
|
fast: prior?.fast ?? sensor.fast ?? false,
|
|
275
|
+
...(prior?.timeout !== undefined ? { timeout: prior.timeout } : {}),
|
|
275
276
|
variantId: variant.id,
|
|
276
277
|
command: variant.command,
|
|
277
278
|
assets: variant.assets,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateRunOptions = validateRunOptions;
|
|
4
|
+
exports.expandFileInput = expandFileInput;
|
|
5
|
+
exports.prepareLegacySensor = prepareLegacySensor;
|
|
6
|
+
exports.prepareV2Sensor = prepareV2Sensor;
|
|
7
|
+
const changed_1 = require("./changed");
|
|
8
|
+
const timeout_1 = require("./compatibility/timeout");
|
|
9
|
+
function resolveRequestedScope(value) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return 'full';
|
|
12
|
+
if (value === 'full' || value === 'changed')
|
|
13
|
+
return value;
|
|
14
|
+
throw new Error('requested scope must be "full" or "changed"');
|
|
15
|
+
}
|
|
16
|
+
function validateRunOptions(opts) {
|
|
17
|
+
if (!opts || typeof opts !== 'object' || Array.isArray(opts))
|
|
18
|
+
throw new Error('run options must be an object');
|
|
19
|
+
if (opts.fast !== undefined && typeof opts.fast !== 'boolean')
|
|
20
|
+
throw new Error('run option fast must be a boolean');
|
|
21
|
+
if (opts.slow !== undefined && typeof opts.slow !== 'boolean')
|
|
22
|
+
throw new Error('run option slow must be a boolean');
|
|
23
|
+
if (opts.all !== undefined && typeof opts.all !== 'boolean')
|
|
24
|
+
throw new Error('run option all must be a boolean');
|
|
25
|
+
if (opts.cwd !== undefined && (typeof opts.cwd !== 'string' || opts.cwd.trim() === ''))
|
|
26
|
+
throw new Error('run option cwd must be a nonempty string');
|
|
27
|
+
if (opts.changed !== undefined && typeof opts.changed !== 'boolean')
|
|
28
|
+
throw new Error('run option changed must be a boolean');
|
|
29
|
+
if (opts.ignoreBaseline !== undefined && typeof opts.ignoreBaseline !== 'boolean')
|
|
30
|
+
throw new Error('run option ignoreBaseline must be a boolean');
|
|
31
|
+
if (opts.base !== undefined && (typeof opts.base !== 'string' || opts.base.trim() === ''))
|
|
32
|
+
throw new Error('run option base must be a nonempty string');
|
|
33
|
+
if (opts.changed && opts.ignoreBaseline) {
|
|
34
|
+
throw new Error('refusing to combine --changed with a baseline capture: a partial run cannot define the accepted set');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Expand the one declared file placeholder into literal structured argv entries. */
|
|
38
|
+
function expandFileInput(command, files) {
|
|
39
|
+
if (!command.fileInput)
|
|
40
|
+
throw new Error('changed command requires fileInput');
|
|
41
|
+
const index = command.args.indexOf(command.fileInput.placeholder);
|
|
42
|
+
if (index < 0 || command.args.lastIndexOf(command.fileInput.placeholder) !== index) {
|
|
43
|
+
throw new Error('changed command requires exactly one standalone {files} argument');
|
|
44
|
+
}
|
|
45
|
+
// `fileInput` describes the unexpanded registry template. Leaving it on the
|
|
46
|
+
// materialized command makes the execution boundary (correctly) demand a
|
|
47
|
+
// placeholder that has already been replaced with literal argv entries.
|
|
48
|
+
const { fileInput: _templateInput, ...materialized } = command;
|
|
49
|
+
return { ...materialized, args: [...command.args.slice(0, index), ...files, ...command.args.slice(index + 1)] };
|
|
50
|
+
}
|
|
51
|
+
function timeout(project, pack, fast) {
|
|
52
|
+
const resolved = (0, timeout_1.resolveTimeout)({ project, pack, fast });
|
|
53
|
+
return { timeoutMs: resolved.timeoutMs, timeoutSource: resolved.source };
|
|
54
|
+
}
|
|
55
|
+
function fullLegacy(input, scopeReason) {
|
|
56
|
+
const requestedScope = resolveRequestedScope(input.requestedScope);
|
|
57
|
+
return {
|
|
58
|
+
name: input.name,
|
|
59
|
+
...(input.config.cmd ? { command: { kind: 'legacy', value: input.config.cmd } } : { syntheticStatus: 'inconclusive', syntheticReason: 'no cmd configured' }),
|
|
60
|
+
...(input.config.formatter ? { formatter: input.config.formatter } : {}),
|
|
61
|
+
...timeout(input.config.timeout, input.packTimeout, input.config.fast ?? false),
|
|
62
|
+
requestedScope,
|
|
63
|
+
effectiveScope: 'full',
|
|
64
|
+
...(scopeReason ? { scopeReason } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Prepare one legacy command without dispatching it. */
|
|
68
|
+
function prepareLegacySensor(input) {
|
|
69
|
+
if (!input || typeof input !== 'object' || !input.config || typeof input.name !== 'string' || input.name === '')
|
|
70
|
+
throw new Error('legacy preparation input is invalid');
|
|
71
|
+
const requestedScope = resolveRequestedScope(input.requestedScope);
|
|
72
|
+
if (requestedScope !== 'changed' || !input.changed)
|
|
73
|
+
return fullLegacy(input);
|
|
74
|
+
const scopeError = (0, changed_1.changedScopeError)(input.changed);
|
|
75
|
+
if (scopeError)
|
|
76
|
+
return fullLegacy(input, `changed scope could not be resolved safely: ${scopeError}`);
|
|
77
|
+
if (!input.config.changedCmd)
|
|
78
|
+
return fullLegacy(input, 'sensor does not support changed scope');
|
|
79
|
+
if (!input.config.changedCmd.includes('{files}')) {
|
|
80
|
+
const { command: _command, ...prepared } = fullLegacy(input);
|
|
81
|
+
return { ...prepared, effectiveScope: 'changed', syntheticStatus: 'inconclusive', syntheticReason: 'changedCmd has no {files} placeholder' };
|
|
82
|
+
}
|
|
83
|
+
const files = (0, changed_1.filterByExtension)(input.changed.files, input.config.changedExtensions);
|
|
84
|
+
if (files.length === 0) {
|
|
85
|
+
const { command: _command, ...prepared } = fullLegacy(input);
|
|
86
|
+
return { ...prepared, effectiveScope: 'changed', files: 0, syntheticStatus: 'pass', syntheticReason: 'no changed files in scope' };
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
name: input.name,
|
|
90
|
+
command: { kind: 'legacy', value: (0, changed_1.applyChangedCmd)(input.config.changedCmd, files) },
|
|
91
|
+
...(input.config.formatter ? { formatter: input.config.formatter } : {}),
|
|
92
|
+
...timeout(input.config.timeout, input.packTimeout, input.config.fast ?? false),
|
|
93
|
+
requestedScope,
|
|
94
|
+
effectiveScope: 'changed',
|
|
95
|
+
files: files.length,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function v2Synthetic(input, reason) {
|
|
99
|
+
const requestedScope = resolveRequestedScope(input.requestedScope);
|
|
100
|
+
return {
|
|
101
|
+
name: input.name,
|
|
102
|
+
...timeout(input.projectTimeout ?? input.sensor.timeout, input.packTimeout ?? input.liveSensor?.timeout, input.sensor.fast ?? input.liveSensor?.fast ?? false),
|
|
103
|
+
requestedScope,
|
|
104
|
+
effectiveScope: 'full',
|
|
105
|
+
syntheticStatus: 'inconclusive',
|
|
106
|
+
syntheticReason: reason,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Prepare a v2 command from the freshly resolved pack. The manifest command is
|
|
111
|
+
* deliberately never read: variantId is only a selector for live authority.
|
|
112
|
+
*/
|
|
113
|
+
function prepareV2Sensor(input) {
|
|
114
|
+
if (!input || typeof input !== 'object' || !input.sensor || typeof input.name !== 'string' || input.name === '')
|
|
115
|
+
throw new Error('v2 preparation input is invalid');
|
|
116
|
+
const requestedScope = resolveRequestedScope(input.requestedScope);
|
|
117
|
+
if (!input.liveState || input.liveState.state !== 'certified')
|
|
118
|
+
return v2Synthetic(input, input.liveState ? `${input.liveState.state}: ${input.liveState.reason}` : 'compatibility could not be revalidated');
|
|
119
|
+
if (input.liveState.variantId !== input.sensor.variantId)
|
|
120
|
+
return v2Synthetic(input, `variant-drift: manifest ${input.sensor.variantId}, live ${input.liveState.variantId ?? 'none'}; run \`awm sensors init\``);
|
|
121
|
+
const variant = input.liveSensor?.variants.find(candidate => candidate.id === input.sensor.variantId);
|
|
122
|
+
if (!variant)
|
|
123
|
+
return v2Synthetic(input, 'variant-drift: selected live variant has no command; run `awm sensors init`');
|
|
124
|
+
const common = {
|
|
125
|
+
name: input.name,
|
|
126
|
+
...(variant.formatter ? { formatter: variant.formatter } : {}),
|
|
127
|
+
...timeout(input.projectTimeout ?? input.sensor.timeout, input.packTimeout ?? input.liveSensor?.timeout, input.sensor.fast ?? input.liveSensor?.fast ?? false),
|
|
128
|
+
requestedScope,
|
|
129
|
+
};
|
|
130
|
+
if (requestedScope !== 'changed' || !input.changed)
|
|
131
|
+
return { ...common, command: { kind: 'structured', value: variant.command }, effectiveScope: 'full' };
|
|
132
|
+
if (input.changed.error)
|
|
133
|
+
return { ...common, command: { kind: 'structured', value: variant.command }, effectiveScope: 'full', scopeReason: `changed scope could not be resolved: ${input.changed.error}` };
|
|
134
|
+
if (!variant.changedCommand)
|
|
135
|
+
return { ...common, command: { kind: 'structured', value: variant.command }, effectiveScope: 'full', scopeReason: 'sensor does not support changed scope' };
|
|
136
|
+
const files = (0, changed_1.filterByExtension)(input.changed.files, variant.changedCommand.fileInput?.extensions);
|
|
137
|
+
if (files.length === 0)
|
|
138
|
+
return { ...common, effectiveScope: 'changed', files: 0, syntheticStatus: 'pass', syntheticReason: 'no changed files in scope' };
|
|
139
|
+
return { ...common, command: { kind: 'structured', value: expandFileInput(variant.changedCommand, files) }, effectiveScope: 'changed', files: files.length };
|
|
140
|
+
}
|