agentic-workflow-manager 6.5.2 → 8.1.0

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 (95) hide show
  1. package/dist/scripts/sensor-support-matrix.js +103 -0
  2. package/dist/scripts/support-matrix.js +40 -17
  3. package/dist/src/commands/init.js +1 -3
  4. package/dist/src/commands/ledger/index.js +6 -0
  5. package/dist/src/commands/preflight/checks.js +4 -4
  6. package/dist/src/commands/preflight/index.js +2 -2
  7. package/dist/src/commands/registry/add.js +2 -2
  8. package/dist/src/commands/registry/index.js +5 -0
  9. package/dist/src/commands/registry/status.js +7 -0
  10. package/dist/src/commands/sensors/compatibility/contract.js +382 -0
  11. package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
  12. package/dist/src/commands/sensors/compatibility/live.js +92 -0
  13. package/dist/src/commands/sensors/compatibility/manifest.js +207 -0
  14. package/dist/src/commands/sensors/compatibility/materialize.js +134 -0
  15. package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
  16. package/dist/src/commands/sensors/compatibility/probe.js +47 -0
  17. package/dist/src/commands/sensors/compatibility/resolve.js +99 -0
  18. package/dist/src/commands/sensors/compatibility/types.js +2 -0
  19. package/dist/src/commands/sensors/coverage/contract.js +0 -50
  20. package/dist/src/commands/sensors/coverage/empirical.js +131 -0
  21. package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
  22. package/dist/src/commands/sensors/coverage/evidence.js +5 -1
  23. package/dist/src/commands/sensors/coverage/index.js +44 -7
  24. package/dist/src/commands/sensors/coverage/render.js +214 -14
  25. package/dist/src/commands/sensors/coverage/resolve.js +24 -38
  26. package/dist/src/commands/sensors/exec.js +244 -9
  27. package/dist/src/commands/sensors/index.js +18 -6
  28. package/dist/src/commands/sensors/init.js +95 -6
  29. package/dist/src/commands/sensors/run.js +71 -0
  30. package/dist/src/commands/sensors/status.js +48 -4
  31. package/dist/src/commands/sync.js +7 -0
  32. package/dist/src/core/bundles.js +38 -3
  33. package/dist/src/core/init/steps.js +7 -4
  34. package/dist/src/core/ledger/scan.js +228 -0
  35. package/dist/src/core/ledger/store.js +89 -25
  36. package/dist/src/core/ledger/types.js +54 -0
  37. package/dist/src/core/paths.js +10 -9
  38. package/dist/src/core/registries.js +134 -14
  39. package/dist/src/index.js +24 -1
  40. package/dist/tests/commands/ledger/index.test.js +11 -0
  41. package/dist/tests/commands/preflight/preflight.test.js +64 -64
  42. package/dist/tests/commands/registry/add.test.js +69 -2
  43. package/dist/tests/commands/registry/status.test.js +14 -0
  44. package/dist/tests/commands/sensors/compatibility/contract.test.js +193 -0
  45. package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
  46. package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
  47. package/dist/tests/commands/sensors/compatibility/manifest.test.js +77 -0
  48. package/dist/tests/commands/sensors/compatibility/materialize.test.js +72 -0
  49. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
  50. package/dist/tests/commands/sensors/compatibility/probe.test.js +37 -0
  51. package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
  52. package/dist/tests/commands/sensors/compatibility/resolve.test.js +83 -0
  53. package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
  54. package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
  55. package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
  56. package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
  57. package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
  58. package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
  59. package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
  60. package/dist/tests/commands/sensors/exec-windows.test.js +140 -2
  61. package/dist/tests/commands/sensors/exec.test.js +115 -5
  62. package/dist/tests/commands/sensors/index.test.js +8 -0
  63. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
  64. package/dist/tests/commands/sensors/init.test.js +204 -48
  65. package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
  66. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  67. package/dist/tests/commands/sensors/run.test.js +66 -3
  68. package/dist/tests/commands/sensors/status-windows.test.js +7 -7
  69. package/dist/tests/commands/sensors/status.test.js +74 -23
  70. package/dist/tests/commands/update.test.js +1 -1
  71. package/dist/tests/core/bundles.test.js +37 -0
  72. package/dist/tests/core/discovery.test.js +4 -0
  73. package/dist/tests/core/init/orchestrator.test.js +5 -1
  74. package/dist/tests/core/init/steps.test.js +20 -4
  75. package/dist/tests/core/ledger/scan.test.js +217 -0
  76. package/dist/tests/core/ledger/store.test.js +108 -0
  77. package/dist/tests/core/profile-pins.test.js +2 -1
  78. package/dist/tests/core/project-skill-links.test.js +22 -0
  79. package/dist/tests/core/registries-capability.test.js +10 -0
  80. package/dist/tests/core/registries-sync.test.js +16 -0
  81. package/dist/tests/core/registries.test.js +129 -1
  82. package/dist/tests/core/registry-manifest.test.js +7 -0
  83. package/dist/tests/core/same-existing-path.test.js +35 -0
  84. package/dist/tests/core/sync-gates.test.js +2 -1
  85. package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
  86. package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
  87. package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
  88. package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
  89. package/dist/tests/structural/active-documentation.test.js +128 -0
  90. package/dist/tests/structural/async-entrypoint.test.js +15 -0
  91. package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
  92. package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
  93. package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
  94. package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
  95. package/package.json +8 -2
@@ -1,7 +1,13 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runStructuredCommand = runStructuredCommand;
3
7
  exports.runCommand = runCommand;
4
8
  const child_process_1 = require("child_process");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
5
11
  const paths_1 = require("../../core/paths");
6
12
  const DEFAULT_MAX_BUFFER = 64 * 1024 * 1024;
7
13
  const DEFAULT_KILL_GRACE_MS = 2_000;
@@ -53,7 +59,17 @@ function killTree(pid, signal) {
53
59
  * throws away 60s of eslint output costs double: the wall clock, and then
54
60
  * the re-run the caller has to do to learn anything at all.
55
61
  */
56
- function runCommand(cmd, opts) {
62
+ function validateOptions(opts) {
63
+ if (!opts || typeof opts !== 'object' || typeof opts.cwd !== 'string' || opts.cwd.trim() === '' || !Number.isSafeInteger(opts.timeout) || opts.timeout <= 0) {
64
+ throw new Error('exec options require a non-empty cwd and positive safe-integer timeout');
65
+ }
66
+ if (opts.maxBuffer !== undefined && (!Number.isSafeInteger(opts.maxBuffer) || opts.maxBuffer <= 0))
67
+ throw new Error('exec options maxBuffer must be a positive safe integer');
68
+ if (opts.killGraceMs !== undefined && (!Number.isSafeInteger(opts.killGraceMs) || opts.killGraceMs < 0))
69
+ throw new Error('exec options killGraceMs must be a non-negative safe integer');
70
+ }
71
+ function collectSpawn(input, opts) {
72
+ validateOptions(opts);
57
73
  const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
58
74
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
59
75
  return new Promise((resolve) => {
@@ -63,14 +79,22 @@ function runCommand(cmd, opts) {
63
79
  let overflowed = false;
64
80
  let settled = false;
65
81
  const timers = [];
66
- const child = (0, child_process_1.spawn)(cmd, {
67
- shell: true,
68
- cwd: opts.cwd,
69
- detached: !(0, paths_1.isWindowsNative)(),
70
- // stdin closed: a sensor must never block waiting for input, and the
71
- // EOF also tells watch-mode-capable tools (vitest, jest) to run once.
72
- stdio: ['ignore', 'pipe', 'pipe'],
73
- });
82
+ const child = input.shell
83
+ ? (0, child_process_1.spawn)(input.executable, {
84
+ shell: true,
85
+ cwd: opts.cwd,
86
+ detached: !(0, paths_1.isWindowsNative)(),
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ })
89
+ : (0, child_process_1.spawn)(input.executable, input.args, {
90
+ shell: false,
91
+ cwd: opts.cwd,
92
+ detached: !(0, paths_1.isWindowsNative)(),
93
+ ...(input.environment ? { env: { ...process.env, ...input.environment } } : {}),
94
+ // stdin closed: a sensor must never block waiting for input, and the
95
+ // EOF also tells watch-mode-capable tools (vitest, jest) to run once.
96
+ stdio: ['ignore', 'pipe', 'pipe'],
97
+ });
74
98
  const later = (fn, ms) => {
75
99
  const t = setTimeout(fn, ms);
76
100
  t.unref?.();
@@ -120,3 +144,214 @@ function runCommand(cmd, opts) {
120
144
  later(() => { timedOut = true; cutShort(); }, opts.timeout);
121
145
  });
122
146
  }
147
+ function validateStructuredCommand(command) {
148
+ if (!command || typeof command !== 'object' || typeof command.executable !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(command.executable)) {
149
+ throw new Error('structured command executable must be a safe executable name');
150
+ }
151
+ const normalizedExecutable = command.executable.toLowerCase().replace(/\.exe$/, '');
152
+ if (new Set(['sh', 'bash', 'cmd', 'powershell']).has(normalizedExecutable)) {
153
+ throw new Error('structured command executable must not be a shell');
154
+ }
155
+ if (!Array.isArray(command.args) || command.args.length === 0) {
156
+ throw new Error('structured command args must be a nonempty array');
157
+ }
158
+ for (const [index, arg] of command.args.entries()) {
159
+ if (typeof arg !== 'string' || arg.trim() === '' || /[\0\r\n]/.test(arg)) {
160
+ throw new Error(`structured command args[${index}] must be a nonempty single-line string without NUL`);
161
+ }
162
+ if (arg.includes('{files}') && arg !== '{files}') {
163
+ throw new Error(`structured command args[${index}] must not embed {files}`);
164
+ }
165
+ }
166
+ if (!['node-modules-bin', 'python-environment', 'path'].includes(command.resolution))
167
+ throw new Error('structured command resolution is unsupported');
168
+ if (command.pythonEnvironmentRoot !== undefined && (command.resolution !== 'python-environment' || (command.pythonEnvironmentRoot !== '.venv' && command.pythonEnvironmentRoot !== 'venv'))) {
169
+ throw new Error('structured command pythonEnvironmentRoot must name the selected .venv or venv Python environment');
170
+ }
171
+ if (command.resolution === 'python-environment' && command.pythonEnvironmentRoot === undefined)
172
+ throw new Error('python environment command requires a discovery-bound contained local environment root');
173
+ const packageManagers = new Set(['npm', 'pnpm', 'yarn', 'bun']);
174
+ const normalizedPackageManager = typeof command.packageManager === 'string' ? command.packageManager.toLowerCase().replace(/\.exe$/, '') : undefined;
175
+ if (packageManagers.has(normalizedExecutable) && normalizedPackageManager !== normalizedExecutable)
176
+ throw new Error('structured command packageManager must explicitly match its executable');
177
+ if (normalizedPackageManager !== undefined && !packageManagers.has(normalizedPackageManager))
178
+ throw new Error('structured command packageManager is unsupported');
179
+ if (normalizedPackageManager !== undefined && normalizedPackageManager !== normalizedExecutable)
180
+ throw new Error('structured command packageManager must explicitly match its executable');
181
+ if (command.environment !== undefined) {
182
+ const environment = command.environment;
183
+ if (!environment || typeof environment !== 'object' || Array.isArray(environment) || Object.keys(environment).length !== 1 || !Object.prototype.hasOwnProperty.call(environment, 'ESLINT_USE_FLAT_CONFIG')) {
184
+ throw new Error('structured command environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
185
+ }
186
+ const flatConfig = environment.ESLINT_USE_FLAT_CONFIG;
187
+ if (flatConfig !== 'true' && flatConfig !== 'false')
188
+ throw new Error('structured command environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
189
+ }
190
+ const fileArguments = command.args.filter(arg => arg === '{files}').length;
191
+ if (command.fileInput === undefined) {
192
+ if (fileArguments !== 0)
193
+ throw new Error('structured command {files} argument requires fileInput');
194
+ return;
195
+ }
196
+ const fileInput = command.fileInput;
197
+ if (!fileInput || typeof fileInput !== 'object' || Array.isArray(fileInput) || Object.keys(fileInput).length !== 2 || !Object.prototype.hasOwnProperty.call(fileInput, 'placeholder') || !Object.prototype.hasOwnProperty.call(fileInput, 'extensions')) {
198
+ throw new Error('structured command fileInput must contain only placeholder and extensions');
199
+ }
200
+ const { placeholder, extensions } = fileInput;
201
+ if (placeholder !== '{files}')
202
+ throw new Error('structured command fileInput.placeholder must be {files}');
203
+ if (!Array.isArray(extensions) || extensions.length === 0 || extensions.some(extension => typeof extension !== 'string' || extension.trim() === '' || /[\0\r\n]/.test(extension) || !/^\.[A-Za-z0-9]+$/.test(extension))) {
204
+ throw new Error('structured command fileInput.extensions must be a nonempty array of extensions');
205
+ }
206
+ if (fileArguments !== 1)
207
+ throw new Error('structured command fileInput requires exactly one {files} argument');
208
+ }
209
+ function safeWindowsExecutableExtensions() {
210
+ return (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
211
+ .split(';')
212
+ .map(ext => ext.toLowerCase())
213
+ .filter(ext => ext === '.exe' || ext === '.com');
214
+ }
215
+ function regularFile(candidate) {
216
+ try {
217
+ const stat = fs_1.default.lstatSync(candidate);
218
+ return stat.isFile() && !stat.isSymbolicLink();
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ }
224
+ /** Resolve a Python environment executable only when every selected path
225
+ * component is a non-symbolic-link local entry. A leaf-only lstat follows a
226
+ * linked .venv/venv ancestor and can otherwise execute outside the project. */
227
+ function localPythonEnvironmentExecutable(cwd, environmentRoot, executable) {
228
+ const parts = [environmentRoot, (0, paths_1.isWindowsNative)() ? 'Scripts' : 'bin', executable];
229
+ let candidate = cwd;
230
+ try {
231
+ for (const part of parts) {
232
+ candidate = path_1.default.join(candidate, part);
233
+ const stat = fs_1.default.lstatSync(candidate);
234
+ if (stat.isSymbolicLink())
235
+ return null;
236
+ }
237
+ return fs_1.default.lstatSync(candidate).isFile() ? candidate : null;
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
243
+ function containedPath(root, candidate) {
244
+ const relative = path_1.default.relative(root, candidate);
245
+ return relative !== '' && !relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative);
246
+ }
247
+ /** Resolve a local npm shim to its real regular-file target. npm commonly uses
248
+ * symlinks in .bin on POSIX, so rejecting every symlink would reject valid local
249
+ * installs. The real target must remain inside the project's node_modules. */
250
+ function localNodeModulesExecutable(candidate, modulesRoot) {
251
+ try {
252
+ const shim = fs_1.default.lstatSync(candidate);
253
+ if (!shim.isFile() && !shim.isSymbolicLink())
254
+ return null;
255
+ const real = fs_1.default.realpathSync(candidate);
256
+ const target = fs_1.default.statSync(real);
257
+ return target.isFile() && containedPath(modulesRoot, real) ? real : null;
258
+ }
259
+ catch {
260
+ return null;
261
+ }
262
+ }
263
+ /** Find a real executable without a shell. Windows .cmd/.bat shims are deliberately
264
+ * excluded: CreateProcess cannot execute them safely without cmd.exe. */
265
+ function resolveStructuredExecutable(command, cwd) {
266
+ if (command.resolution === 'node-modules-bin') {
267
+ let modulesRoot;
268
+ try {
269
+ modulesRoot = fs_1.default.realpathSync(path_1.default.join(cwd, 'node_modules'));
270
+ if (!fs_1.default.statSync(modulesRoot).isDirectory())
271
+ throw new Error();
272
+ }
273
+ catch {
274
+ throw new Error('node_modules executable not found locally');
275
+ }
276
+ const bin = path_1.default.join(modulesRoot, '.bin');
277
+ if (!(0, paths_1.isWindowsNative)()) {
278
+ const local = localNodeModulesExecutable(path_1.default.join(bin, command.executable), modulesRoot);
279
+ if (local)
280
+ return local;
281
+ throw new Error('node_modules executable is not a contained local file');
282
+ }
283
+ const extensions = safeWindowsExecutableExtensions();
284
+ if (extensions.length === 0)
285
+ throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
286
+ const candidates = [path_1.default.join(bin, command.executable), ...extensions.map(extension => path_1.default.join(bin, command.executable + extension))];
287
+ for (const candidate of candidates) {
288
+ const lower = candidate.toLowerCase();
289
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
290
+ throw new Error('structured commands cannot execute Windows command wrappers');
291
+ const local = localNodeModulesExecutable(candidate, modulesRoot);
292
+ if (local && extensions.some(extension => local.toLowerCase().endsWith(extension)))
293
+ return local;
294
+ }
295
+ throw new Error('node_modules executable is not a contained local file');
296
+ }
297
+ const candidates = [];
298
+ if (command.resolution === 'python-environment') {
299
+ if (path_1.default.isAbsolute(command.executable))
300
+ throw new Error('python environment executable must be a contained local name');
301
+ if (!(0, paths_1.isWindowsNative)()) {
302
+ const local = localPythonEnvironmentExecutable(cwd, command.pythonEnvironmentRoot, command.executable);
303
+ if (local)
304
+ return local;
305
+ throw new Error('python environment executable is not a contained local regular file');
306
+ }
307
+ const extensions = safeWindowsExecutableExtensions();
308
+ if (extensions.length === 0)
309
+ throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
310
+ for (const executable of [command.executable, ...extensions.map(extension => command.executable + extension)]) {
311
+ const lower = executable.toLowerCase();
312
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
313
+ throw new Error('structured commands cannot execute Windows command wrappers');
314
+ const local = localPythonEnvironmentExecutable(cwd, command.pythonEnvironmentRoot, executable);
315
+ if (local && extensions.some(extension => lower.endsWith(extension)))
316
+ return local;
317
+ }
318
+ throw new Error('python environment executable is not a contained local regular file');
319
+ }
320
+ else if (path_1.default.isAbsolute(command.executable))
321
+ candidates.push(command.executable);
322
+ else {
323
+ for (const entry of (process.env.PATH ?? '').split(path_1.default.delimiter).filter(Boolean))
324
+ candidates.push(path_1.default.join(entry, command.executable));
325
+ }
326
+ if (!(0, paths_1.isWindowsNative)()) {
327
+ const found = candidates.find(regularFile);
328
+ if (found)
329
+ return found;
330
+ return command.executable;
331
+ }
332
+ const extensions = safeWindowsExecutableExtensions();
333
+ if (extensions.length === 0)
334
+ throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
335
+ for (const candidate of candidates) {
336
+ const lower = candidate.toLowerCase();
337
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
338
+ throw new Error('structured commands cannot execute Windows command wrappers');
339
+ if (regularFile(candidate) && extensions.some(ext => lower.endsWith(ext)))
340
+ return candidate;
341
+ for (const extension of extensions)
342
+ if (regularFile(candidate + extension))
343
+ return candidate + extension;
344
+ }
345
+ return command.executable;
346
+ }
347
+ /** Execute a v2 command as an executable plus literal argv; it never starts a shell. */
348
+ function runStructuredCommand(command, opts) {
349
+ validateStructuredCommand(command);
350
+ return collectSpawn({ executable: resolveStructuredExecutable(command, opts.cwd), args: command.args, shell: false, environment: command.environment }, opts);
351
+ }
352
+ /** Legacy sensor strings intentionally retain their documented shell semantics. */
353
+ function runCommand(cmd, opts) {
354
+ if (typeof cmd !== 'string' || cmd.trim() === '' || /[\0\r\n]/.test(cmd))
355
+ throw new Error('runCommand: cmd must be a non-empty single-line legacy string without NUL');
356
+ return collectSpawn({ executable: cmd, args: [], shell: true }, opts);
357
+ }
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.exitCodeFor = exitCodeFor;
7
+ exports.parsePositiveSafeInteger = parsePositiveSafeInteger;
7
8
  exports.registerSensorsCommand = registerSensorsCommand;
8
9
  const picocolors_1 = __importDefault(require("picocolors"));
9
10
  const prompts_1 = require("@clack/prompts");
@@ -21,15 +22,26 @@ const registries_1 = require("../../core/registries");
21
22
  function exitCodeFor(output) {
22
23
  return output.overall === 'fail' ? 1 : 0;
23
24
  }
25
+ /** Commander coercion for coverage recurrence emphasis. It deliberately runs
26
+ * before the action, so an invalid value cannot trigger ledger I/O. */
27
+ function parsePositiveSafeInteger(value) {
28
+ if (typeof value !== 'string' || !/^[1-9][0-9]*$/.test(value))
29
+ throw new Error('--min must be a positive safe integer');
30
+ const parsed = Number(value);
31
+ if (!Number.isSafeInteger(parsed))
32
+ throw new Error('--min must be a positive safe integer');
33
+ return parsed;
34
+ }
24
35
  function registerSensorsCommand(program) {
25
36
  const sensors = program.command('sensors').description('manage computational sensors for the current project');
26
37
  sensors
27
38
  .command('coverage')
28
39
  .description('report static gaps between configured sensors and the pack reference')
29
40
  .option('--json', 'emit the versioned machine-readable envelope')
30
- .action((opts) => {
41
+ .option('--min <count>', 'recurrence emphasis threshold', parsePositiveSafeInteger, 2)
42
+ .action(async (opts) => {
31
43
  try {
32
- const report = (0, coverage_1.runCoverage)(process.cwd());
44
+ const report = await (0, coverage_1.runCoverage)(process.cwd(), {}, { min: opts.min });
33
45
  process.stdout.write(opts.json ? (0, render_1.renderCoverageJson)(report) : (0, render_1.renderCoverageHuman)(report));
34
46
  }
35
47
  catch (error) {
@@ -63,10 +75,10 @@ function registerSensorsCommand(program) {
63
75
  .option('--no-configure', 'skip copying sensor pack config files into the project')
64
76
  .option('--registry-root <path>', 'path to AWM registry root')
65
77
  .option('--pack <name>', 'skip auto-detection, use this pack explicitly')
66
- .action((opts) => {
78
+ .action(async (opts) => {
67
79
  const registryRoot = opts.registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs') ?? undefined;
68
80
  try {
69
- const result = (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack });
81
+ const result = await (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack });
70
82
  prompts_1.log.success(`Detected: ${result.detection.pack} (${result.detection.indicators.join(', ') || 'fallback'})`);
71
83
  // Said BEFORE "Wrote .awm/sensors.json": the manifest about to be
72
84
  // reported as written is not the one the detection implied.
@@ -99,8 +111,8 @@ function registerSensorsCommand(program) {
99
111
  sensors
100
112
  .command('status')
101
113
  .description('check sensor health for the current project')
102
- .action(() => {
103
- const status = (0, status_1.computeSensorStatus)();
114
+ .action(async () => {
115
+ const status = await (0, status_1.computeSensorStatus)();
104
116
  const icon = status.overall === 'HEALTHY' ? picocolors_1.default.green('✔') : picocolors_1.default.yellow('⚠');
105
117
  console.log(`\nPack: ${status.pack ?? 'none'}`);
106
118
  console.log(`Overall: ${icon} ${status.overall}\n`);
@@ -9,6 +9,41 @@ exports.buildManifest = buildManifest;
9
9
  exports.initSensors = initSensors;
10
10
  const fs_1 = __importDefault(require("fs"));
11
11
  const path_1 = __importDefault(require("path"));
12
+ const contract_1 = require("./compatibility/contract");
13
+ const materialize_1 = require("./compatibility/materialize");
14
+ const live_1 = require("./compatibility/live");
15
+ const manifest_1 = require("./compatibility/manifest");
16
+ const pack_source_1 = require("./compatibility/pack-source");
17
+ /** Read a pack only through the shared source resolver. Init is a registry
18
+ * boundary: a pack path that is absent is harmless, but a symlink or escaping
19
+ * claimed source is never silently downgraded to an empty manifest. */
20
+ function readResolvedPack(pack, registryRoot) {
21
+ try {
22
+ const source = (0, pack_source_1.resolvePackSource)(pack, {
23
+ registries: [{ name: 'init-registry', remote: 'local', contentRoot: registryRoot }],
24
+ });
25
+ return { path: source.path, content: source.content };
26
+ }
27
+ catch (error) {
28
+ if (error instanceof Error && error.message.includes('was not found in configured registries'))
29
+ return null;
30
+ throw error;
31
+ }
32
+ }
33
+ function readV2Pack(pack, registryRoot) {
34
+ const source = readResolvedPack(pack, registryRoot);
35
+ if (!source)
36
+ return null;
37
+ let raw;
38
+ try {
39
+ raw = JSON.parse(source.content);
40
+ }
41
+ catch {
42
+ throw new Error(`cannot parse sensor pack ${source.path}`);
43
+ }
44
+ const parsed = (0, contract_1.parseSensorPack)(raw, source.path);
45
+ return parsed.kind === 'v2' ? { pack: parsed.pack, packRoot: path_1.default.dirname(source.path) } : null;
46
+ }
12
47
  const STACK_DETECTORS = [
13
48
  { pack: 'js-ts', files: ['package.json'] },
14
49
  { pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'] },
@@ -60,12 +95,12 @@ function detectSourceDirs(cwd) {
60
95
  * with the project's actual source dirs. Returns null if the pack has no pack.json.
61
96
  */
62
97
  function readPackDefaults(pack, registryRoot, cwd) {
63
- const packJsonPath = path_1.default.join(registryRoot, 'sensor-packs', pack, 'pack.json');
64
- if (!fs_1.default.existsSync(packJsonPath))
98
+ const source = readResolvedPack(pack, registryRoot);
99
+ if (!source)
65
100
  return null;
66
101
  let parsed;
67
102
  try {
68
- parsed = JSON.parse(fs_1.default.readFileSync(packJsonPath, 'utf-8'));
103
+ parsed = JSON.parse(source.content);
69
104
  }
70
105
  catch {
71
106
  return null;
@@ -174,7 +209,7 @@ function assertPackExists(pack, registryRoot) {
174
209
  throw new Error(`pack '${pack}' not found in registry (available: ${available.join(', ')})`);
175
210
  }
176
211
  }
177
- function initSensors(opts = {}) {
212
+ async function initSensors(opts = {}) {
178
213
  const cwd = opts.cwd ?? process.cwd();
179
214
  const configure = opts.configure ?? true; // configure (copy pack config files) by default
180
215
  const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
@@ -192,15 +227,69 @@ function initSensors(opts = {}) {
192
227
  detection = detectStack(cwd);
193
228
  }
194
229
  let existing;
230
+ let existingV2;
195
231
  if (fs_1.default.existsSync(manifestPath)) {
232
+ let raw;
196
233
  try {
197
- existing = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
234
+ raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
198
235
  }
199
- catch { /* ignore corrupt manifest */ }
236
+ catch {
237
+ throw new Error(`cannot parse existing sensor manifest ${manifestPath}`);
238
+ }
239
+ // Never overwrite a corrupt or future-version manifest during init. Legacy
240
+ // manifests remain a supported migration input; v2 is validated before a new
241
+ // materialized selection becomes the commit point.
242
+ const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
243
+ if (parsed.kind === 'legacy')
244
+ existing = parsed.pack;
245
+ else
246
+ existingV2 = parsed.pack;
200
247
  }
201
248
  // `detection` keeps saying what the tree IS; `pack` is what the registry can serve
202
249
  // for it. They diverge only when the registry lacks the detected pack.
203
250
  const { pack: resolvedPack, unavailablePack } = resolvePack(detection.pack, opts.registryRoot);
251
+ // v2 packs own the executable contract. Legacy packs deliberately retain the
252
+ // old string-command path below until a registry author publishes a v2 contract.
253
+ if (opts.registryRoot) {
254
+ const resolvedV2 = readV2Pack(resolvedPack, opts.registryRoot);
255
+ if (resolvedV2) {
256
+ const packSelection = opts.pack ? 'explicit' : undefined;
257
+ const live = await (0, live_1.resolveParsedPackCompatibility)(cwd, resolvedV2.pack, { packSelection });
258
+ const compatibility = live.sensors;
259
+ const sensors = {};
260
+ for (const [name, sensor] of Object.entries(live.pack.sensors)) {
261
+ const resolved = compatibility[name];
262
+ const variant = resolved.variantId === null ? null : sensor.variants.find(candidate => candidate.id === resolved.variantId) ?? null;
263
+ // Unresolved states do not receive an arbitrary command. They remain
264
+ // represented in the manifest so status/coverage can explain them,
265
+ // but run cannot accidentally dispatch a mismatched executable.
266
+ if (variant) {
267
+ const prior = existing?.sensors[name] ?? existingV2?.sensors[name];
268
+ // Legacy string commands cannot be translated into a shell-free
269
+ // structured command without guessing. Preserve the operator's
270
+ // enabled/fast intent but make that migration non-certified.
271
+ const migratedOverride = existing?.sensors[name]?.cmd !== undefined;
272
+ sensors[name] = {
273
+ enabled: prior?.enabled ?? true,
274
+ fast: prior?.fast ?? sensor.fast ?? false,
275
+ variantId: variant.id,
276
+ command: variant.command,
277
+ assets: variant.assets,
278
+ ...(variant.policyRef ? { policyRef: variant.policyRef } : {}),
279
+ initializedCompatibility: migratedOverride
280
+ ? { ...resolved, state: 'compatible-unverified', reason: 'legacy custom command requires explicit v2 migration' }
281
+ : resolved,
282
+ };
283
+ }
284
+ }
285
+ const materialized = (0, materialize_1.materializeResolvedSensors)({
286
+ projectRoot: cwd, packRoot: resolvedV2.packRoot,
287
+ pack: resolvedPack, ...(packSelection ? { packSelection } : {}), registryRoot: opts.registryRoot, sensors, configure,
288
+ });
289
+ return { detection, ...materialized,
290
+ compatibility, ...(unavailablePack ? { unavailablePack } : {}) };
291
+ }
292
+ }
204
293
  const manifest = buildManifest(resolvedPack, existing, opts.registryRoot, cwd);
205
294
  fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
206
295
  const tmpPath = manifestPath + '.tmp';
@@ -26,6 +26,8 @@ const changed_1 = require("./changed");
26
26
  // verbo de lectura: no debe tener a mano ninguna funcion capaz de mutar el proyecto.
27
27
  const init_1 = require("./init");
28
28
  const paths_1 = require("../../core/paths");
29
+ const manifest_1 = require("./compatibility/manifest");
30
+ const live_1 = require("./compatibility/live");
29
31
  const MANIFEST_FILE = '.awm/sensors.json';
30
32
  const DEFAULT_FAST_TIMEOUT = 10_000;
31
33
  const DEFAULT_SLOW_TIMEOUT = 120_000;
@@ -67,6 +69,28 @@ function readManifest(cwd) {
67
69
  return null;
68
70
  }
69
71
  }
72
+ async function resolveLiveV2(cwd, manifest) {
73
+ if (manifest.kind !== 'v2')
74
+ return null;
75
+ try {
76
+ return await (0, live_1.resolveLiveCompatibility)(cwd, manifest.pack.pack, manifest.pack.registryRoot, { packSelection: manifest.pack.packSelection });
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ async function runV2Sensor(name, command, timeout, cwd, formatter) {
83
+ const res = await (0, exec_1.runStructuredCommand)(command, { timeout, cwd, maxBuffer: MAX_BUFFER });
84
+ const format = getFormatter(name, formatter);
85
+ if (res.spawnError)
86
+ return { name, status: 'fail', errors: [{ message: `sensor could not be started: ${res.spawnError.message}` }] };
87
+ if (res.timedOut || res.overflowed)
88
+ return { name, status: 'inconclusive', errors: [], skipReason: res.timedOut ? `timeout after ${timeout}ms` : `output exceeded ${MAX_BUFFER} bytes` };
89
+ const errors = format(res.stdout + res.stderr);
90
+ if (res.code === 0)
91
+ return { name, status: errors.length ? 'fail' : 'pass', errors };
92
+ return errors.length ? { name, status: 'fail', errors } : { name, status: 'inconclusive', errors: [], skipReason: `exit ${res.code}` };
93
+ }
70
94
  /**
71
95
  * Detect — and only detect — that the manifest's pack no longer describes the tree:
72
96
  * it sits on the `generic` fallback while real stack indicators (package.json,
@@ -105,6 +129,10 @@ function findManifestDir(startCwd) {
105
129
  while (true) {
106
130
  if (fs_1.default.existsSync(path_1.default.join(dir, MANIFEST_FILE)))
107
131
  return dir;
132
+ // Temp directories are test/scratch boundaries, never project ancestors.
133
+ // This prevents an unrelated /tmp/.awm from being adopted by a fresh project.
134
+ if (dir === path_1.default.resolve(os_1.default.tmpdir()))
135
+ return null;
108
136
  const parent = path_1.default.dirname(dir);
109
137
  if (parent === dir)
110
138
  return null; // reached filesystem root
@@ -282,6 +310,45 @@ async function runSensors(opts = {}) {
282
310
  const manifest = readManifest(manifestDir);
283
311
  if (!manifest)
284
312
  return { sensors: [], overall: 'not_certified' };
313
+ let parsedManifest;
314
+ try {
315
+ parsedManifest = (0, manifest_1.parseSensorManifest)(JSON.parse(fs_1.default.readFileSync(path_1.default.join(manifestDir, MANIFEST_FILE), 'utf8')), path_1.default.join(manifestDir, MANIFEST_FILE));
316
+ }
317
+ catch {
318
+ return { sensors: [], overall: 'not_certified' };
319
+ }
320
+ if (parsedManifest.kind === 'v2') {
321
+ const live = await resolveLiveV2(manifestDir, parsedManifest);
322
+ const tasks = [];
323
+ for (const [name, sensor] of Object.entries(parsedManifest.pack.sensors)) {
324
+ const state = live?.sensors[name];
325
+ const liveSensor = live?.pack.sensors[name];
326
+ if (!shouldRun(sensor.fast ?? liveSensor?.fast ?? false, opts))
327
+ continue;
328
+ if (sensor.enabled === false) {
329
+ tasks.push(() => Promise.resolve({ name, status: 'skipped', errors: [], skipReason: 'disabled' }));
330
+ continue;
331
+ }
332
+ if (!state || state.state !== 'certified') {
333
+ tasks.push(() => Promise.resolve({ name, status: 'inconclusive', errors: [], skipReason: state ? `${state.state}: ${state.reason}` : 'compatibility could not be revalidated' }));
334
+ continue;
335
+ }
336
+ if (state.variantId !== sensor.variantId) {
337
+ tasks.push(() => Promise.resolve({ name, status: 'inconclusive', errors: [], skipReason: `variant-drift: manifest ${sensor.variantId}, live ${state.variantId ?? 'none'}; run \`awm sensors init\`` }));
338
+ continue;
339
+ }
340
+ const selected = liveSensor?.variants.find(candidate => candidate.id === state.variantId);
341
+ if (!selected) {
342
+ tasks.push(() => Promise.resolve({ name, status: 'inconclusive', errors: [], skipReason: 'variant-drift: selected live variant has no command; run `awm sensors init`' }));
343
+ continue;
344
+ }
345
+ // Variant IDs are stable selectors, not immutable command snapshots.
346
+ // Only the just re-resolved pack command is authorized to execute.
347
+ tasks.push(() => runV2Sensor(name, selected.command, (sensor.fast ?? liveSensor?.fast ?? false) ? DEFAULT_FAST_TIMEOUT : DEFAULT_SLOW_TIMEOUT, manifestDir, selected.formatter));
348
+ }
349
+ const sensors = await pooled(tasks, Math.min(MAX_CONCURRENCY, Math.max(1, tasks.length)));
350
+ return { sensors, overall: sensors.some(sensor => sensor.status === 'fail') ? 'fail' : sensors.some(sensor => sensor.status === 'inconclusive') ? 'not_certified' : sensors.length ? 'pass' : 'skipped' };
351
+ }
285
352
  const drift = detectPackDrift(manifestDir, manifest);
286
353
  const activeManifest = manifest; // el manifest COMITEADO es lo que se corre; `run` no lo reescribe
287
354
  const cwd = manifestDir; // ejecutar sensores y baseline desde donde vive el manifest
@@ -382,6 +449,10 @@ async function runSensors(opts = {}) {
382
449
  : results.length > 0 && results.every(r => r.status === 'skipped') ? 'skipped'
383
450
  : results.length === 0 ? 'skipped'
384
451
  : 'pass';
452
+ // Legacy commands retain operational compatibility but their unversioned,
453
+ // shell-backed contract can never certify a run.
454
+ if (parsedManifest.kind === 'legacy' && overall === 'pass')
455
+ overall = 'not_certified';
385
456
  // Honest floor: a benign-green 'skipped' over a tree that clearly HAS a stack
386
457
  // (indicators present) is a false green — the gate ran nothing real. Never green.
387
458
  if (overall === 'skipped' && drift.detection.pack !== 'generic') {