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.
Files changed (35) hide show
  1. package/dist/src/commands/preflight/checks.js +70 -1
  2. package/dist/src/commands/preflight/index.js +6 -5
  3. package/dist/src/commands/sensors/changed.js +15 -0
  4. package/dist/src/commands/sensors/compatibility/contract.js +17 -3
  5. package/dist/src/commands/sensors/compatibility/manifest.js +16 -4
  6. package/dist/src/commands/sensors/compatibility/timeout.js +21 -0
  7. package/dist/src/commands/sensors/exec.js +3 -1
  8. package/dist/src/commands/sensors/index.js +5 -13
  9. package/dist/src/commands/sensors/init.js +1 -0
  10. package/dist/src/commands/sensors/prepare.js +140 -0
  11. package/dist/src/commands/sensors/result.js +140 -0
  12. package/dist/src/commands/sensors/run.js +56 -335
  13. package/dist/src/commands/sensors/status.js +93 -21
  14. package/dist/src/commands/sensors/verdict.js +27 -0
  15. package/dist/tests/commands/preflight/preflight.test.js +50 -0
  16. package/dist/tests/commands/sensors/baseline.test.js +14 -0
  17. package/dist/tests/commands/sensors/changed-windows.test.js +3 -0
  18. package/dist/tests/commands/sensors/compatibility/contract.test.js +51 -0
  19. package/dist/tests/commands/sensors/compatibility/manifest.test.js +27 -11
  20. package/dist/tests/commands/sensors/compatibility/probe.test.js +13 -3
  21. package/dist/tests/commands/sensors/exec-fixtures.js +1 -1
  22. package/dist/tests/commands/sensors/exec.test.js +36 -0
  23. package/dist/tests/commands/sensors/index.test.js +46 -8
  24. package/dist/tests/commands/sensors/init.test.js +30 -1
  25. package/dist/tests/commands/sensors/prepare.test.js +88 -0
  26. package/dist/tests/commands/sensors/router.test.js +15 -1
  27. package/dist/tests/commands/sensors/run-changed.test.js +4 -4
  28. package/dist/tests/commands/sensors/run.test.js +29 -0
  29. package/dist/tests/commands/sensors/status-windows.test.js +1 -1
  30. package/dist/tests/commands/sensors/status.test.js +52 -20
  31. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +79 -3
  32. package/dist/tests/integration/sensor-compatibility.e2e.test.js +40 -1
  33. package/dist/tests/structural/sensor-documentation-contract.test.js +33 -5
  34. package/dist/tests/structural/support-matrix-is-current.test.js +25 -0
  35. 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
- * Unlike `awm sensors run` which exits 0 on `not_certified` because exit 2 is a
15
- * blocking error in Claude Code hooks preflight is never a hook. It is invoked
16
- * explicitly by a phase gate, so the exit code can carry the verdict and the caller
17
- * does not have to remember to read a field out of JSON.
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
- return { applicability, variants, ...('fast' in value ? { fast: value.fast } : {}) };
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
- if (typeof value.timeout !== 'number' || !Number.isSafeInteger(value.timeout) || value.timeout <= 0)
110
- invalid(source, `${location}.timeout must be a positive safe integer`);
111
- sensor.timeout = value.timeout;
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
- resolve({ stdout, stderr, code: null, signal: null, timedOut, overflowed, ...extra });
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
- /** Map a sensor run verdict to a process exit code. fail → 1; everything else → 0.
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
- const code = exitCodeFor(output);
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 health for the current project')
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 === 'HEALTHY' ? picocolors_1.default.green('✔') : picocolors_1.default.yellow('⚠');
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 !== 'HEALTHY')
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
+ }