agentic-workflow-manager 6.5.2 → 8.0.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 (93) 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 +261 -0
  11. package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
  12. package/dist/src/commands/sensors/compatibility/live.js +59 -0
  13. package/dist/src/commands/sensors/compatibility/manifest.js +197 -0
  14. package/dist/src/commands/sensors/compatibility/materialize.js +126 -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 +96 -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 +150 -9
  27. package/dist/src/commands/sensors/index.js +18 -6
  28. package/dist/src/commands/sensors/init.js +92 -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 +98 -0
  45. package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
  46. package/dist/tests/commands/sensors/compatibility/manifest.test.js +68 -0
  47. package/dist/tests/commands/sensors/compatibility/materialize.test.js +53 -0
  48. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
  49. package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
  50. package/dist/tests/commands/sensors/compatibility/resolve.test.js +75 -0
  51. package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
  52. package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
  53. package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
  54. package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
  55. package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
  56. package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
  57. package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
  58. package/dist/tests/commands/sensors/exec-windows.test.js +92 -2
  59. package/dist/tests/commands/sensors/exec.test.js +67 -5
  60. package/dist/tests/commands/sensors/index.test.js +8 -0
  61. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
  62. package/dist/tests/commands/sensors/init.test.js +132 -48
  63. package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
  64. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  65. package/dist/tests/commands/sensors/run.test.js +66 -3
  66. package/dist/tests/commands/sensors/status-windows.test.js +7 -7
  67. package/dist/tests/commands/sensors/status.test.js +74 -23
  68. package/dist/tests/commands/update.test.js +1 -1
  69. package/dist/tests/core/bundles.test.js +37 -0
  70. package/dist/tests/core/discovery.test.js +4 -0
  71. package/dist/tests/core/init/orchestrator.test.js +5 -1
  72. package/dist/tests/core/init/steps.test.js +20 -4
  73. package/dist/tests/core/ledger/scan.test.js +217 -0
  74. package/dist/tests/core/ledger/store.test.js +108 -0
  75. package/dist/tests/core/profile-pins.test.js +2 -1
  76. package/dist/tests/core/project-skill-links.test.js +22 -0
  77. package/dist/tests/core/registries-capability.test.js +10 -0
  78. package/dist/tests/core/registries-sync.test.js +16 -0
  79. package/dist/tests/core/registries.test.js +129 -1
  80. package/dist/tests/core/registry-manifest.test.js +7 -0
  81. package/dist/tests/core/same-existing-path.test.js +35 -0
  82. package/dist/tests/core/sync-gates.test.js +2 -1
  83. package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
  84. package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
  85. package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
  86. package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
  87. package/dist/tests/structural/active-documentation.test.js +128 -0
  88. package/dist/tests/structural/async-entrypoint.test.js +15 -0
  89. package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
  90. package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
  91. package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
  92. package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
  93. package/package.json +8 -2
@@ -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,66 @@ 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 compatibility = (await (0, live_1.resolveParsedPackCompatibility)(cwd, resolvedV2.pack)).sensors;
257
+ const sensors = {};
258
+ for (const [name, sensor] of Object.entries(resolvedV2.pack.sensors)) {
259
+ const resolved = compatibility[name];
260
+ const variant = resolved.variantId === null ? null : sensor.variants.find(candidate => candidate.id === resolved.variantId) ?? null;
261
+ // Unresolved states do not receive an arbitrary command. They remain
262
+ // represented in the manifest so status/coverage can explain them,
263
+ // but run cannot accidentally dispatch a mismatched executable.
264
+ if (variant) {
265
+ const prior = existing?.sensors[name] ?? existingV2?.sensors[name];
266
+ // Legacy string commands cannot be translated into a shell-free
267
+ // structured command without guessing. Preserve the operator's
268
+ // enabled/fast intent but make that migration non-certified.
269
+ const migratedOverride = existing?.sensors[name]?.cmd !== undefined;
270
+ sensors[name] = {
271
+ enabled: prior?.enabled ?? true,
272
+ fast: prior?.fast ?? sensor.fast ?? false,
273
+ variantId: variant.id,
274
+ command: variant.command,
275
+ assets: variant.assets,
276
+ initializedCompatibility: migratedOverride
277
+ ? { ...resolved, state: 'compatible-unverified', reason: 'legacy custom command requires explicit v2 migration' }
278
+ : resolved,
279
+ };
280
+ }
281
+ }
282
+ const materialized = (0, materialize_1.materializeResolvedSensors)({
283
+ projectRoot: cwd, packRoot: resolvedV2.packRoot,
284
+ pack: resolvedPack, registryRoot: opts.registryRoot, sensors, configure,
285
+ });
286
+ return { detection, ...materialized,
287
+ compatibility, ...(unavailablePack ? { unavailablePack } : {}) };
288
+ }
289
+ }
204
290
  const manifest = buildManifest(resolvedPack, existing, opts.registryRoot, cwd);
205
291
  fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
206
292
  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);
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') {
@@ -7,6 +7,8 @@ exports.computeSensorStatus = computeSensorStatus;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const paths_1 = require("../../core/paths");
10
+ const manifest_1 = require("./compatibility/manifest");
11
+ const live_1 = require("./compatibility/live");
10
12
  /** First non-flag token after `npx` — the tool the command actually runs. */
11
13
  function npxTool(parts) {
12
14
  for (let i = 1; i < parts.length; i++) {
@@ -53,16 +55,57 @@ function checkCmd(cmd, cwd) {
53
55
  }
54
56
  return configCheck(parts, cwd) ?? { ok: true, detail: bin };
55
57
  }
56
- function computeSensorStatus(cwd = process.cwd()) {
58
+ async function computeSensorStatus(cwd = process.cwd()) {
57
59
  const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
58
60
  if (!fs_1.default.existsSync(manifestPath)) {
59
61
  return { overall: 'NOT_CONFIGURED', pack: null, checks: {} };
60
62
  }
61
63
  let manifest;
62
64
  try {
63
- manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
65
+ const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
66
+ const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
67
+ if (parsed.kind === 'v2') {
68
+ const checks = {};
69
+ let live;
70
+ try {
71
+ live = await (0, live_1.resolveLiveCompatibility)(cwd, parsed.pack.pack, parsed.pack.registryRoot);
72
+ }
73
+ catch (error) {
74
+ const detail = `compatibility revalidation failed: ${error instanceof Error ? error.message : String(error)}`;
75
+ for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
76
+ checks[name] = sensor.enabled === false ? { ok: true, detail: 'disabled' } : { ok: false, detail };
77
+ }
78
+ return { overall: 'DEGRADED', pack: parsed.pack.pack, checks };
79
+ }
80
+ for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
81
+ const state = live.sensors[name];
82
+ checks[name] = sensor.enabled === false
83
+ ? { ok: true, detail: 'disabled' }
84
+ : !state
85
+ ? { ok: false, detail: 'variant-drift: sensor no longer exists in the live pack; run `awm sensors init`' }
86
+ : state.variantId !== sensor.variantId
87
+ ? { ok: false, detail: `variant-drift: manifest ${sensor.variantId}, live ${state.variantId ?? 'none'}; run \`awm sensors init\`` }
88
+ : state.state === 'certified'
89
+ ? { ok: true, detail: `certified (${state.variantId})` }
90
+ : state.state === 'not-applicable'
91
+ ? { ok: true, detail: 'not applicable' }
92
+ : { ok: false, detail: `${state.state}: ${state.reason}` };
93
+ }
94
+ return { overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'HEALTHY' : 'DEGRADED', pack: parsed.pack.pack, checks };
95
+ }
96
+ manifest = parsed.pack;
64
97
  }
65
98
  catch {
99
+ // Preserve the historic diagnostic path for a hand-edited legacy manifest:
100
+ // malformed v2 is fail-closed, while a legacy `sensors:null` is surfaced as
101
+ // degraded rather than crashing the status command.
102
+ try {
103
+ const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
104
+ if (raw && typeof raw === 'object' && !Array.isArray(raw) && !('schemaVersion' in raw) && typeof raw.pack === 'string') {
105
+ return { overall: 'DEGRADED', pack: raw.pack, checks: {} };
106
+ }
107
+ }
108
+ catch { /* malformed JSON is not configured */ }
66
109
  return { overall: 'NOT_CONFIGURED', pack: null, checks: {} };
67
110
  }
68
111
  const checks = {};
@@ -84,6 +127,7 @@ function computeSensorStatus(cwd = process.cwd()) {
84
127
  if (Object.keys(manifest.sensors ?? {}).length === 0) {
85
128
  return { overall: 'DEGRADED', pack: manifest.pack, checks };
86
129
  }
87
- const allOk = Object.values(checks).every(c => c.ok);
88
- return { overall: allOk ? 'HEALTHY' : 'DEGRADED', pack: manifest.pack, checks };
130
+ // A legacy manifest may be operational (the checks are still useful), but it
131
+ // has no versioned/structured contract and must never present as certified.
132
+ return { overall: 'DEGRADED', pack: manifest.pack, checks };
89
133
  }
@@ -77,6 +77,13 @@ async function runSyncCore(options, deps = {}) {
77
77
  if (r.action === 'error')
78
78
  console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
79
79
  }
80
+ try {
81
+ (0, registries_1.assertSyncedRegistriesUsable)(syncResults);
82
+ }
83
+ catch (e) {
84
+ console.error(picocolors_1.default.red(e.message));
85
+ return { code: 1, selectedAgents };
86
+ }
80
87
  // Gate minCliVersion (WS-4) before pins (contract gates first — CONSTITUTION).
81
88
  try {
82
89
  (0, registries_1.assertRegistryGates)(d.verifyMinCliVersions());
@@ -16,6 +16,41 @@ const registries_1 = require("./registries");
16
16
  function catalogPath(contentDir) {
17
17
  return path_1.default.join(contentDir, 'catalog.json');
18
18
  }
19
+ function bundleManifestPath(contentDir, source) {
20
+ const root = path_1.default.resolve(contentDir);
21
+ const manifest = path_1.default.resolve(root, source, 'bundle.json');
22
+ const relative = path_1.default.relative(root, manifest);
23
+ if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) {
24
+ throw new Error(`Invalid bundle source "${source}" in ${catalogPath(contentDir)}: it must remain inside the registry content root.`);
25
+ }
26
+ let rootStat;
27
+ try {
28
+ rootStat = fs_1.default.lstatSync(root);
29
+ }
30
+ catch (error) {
31
+ throw new Error(`Cannot inspect registry content root ${root}: ${error instanceof Error ? error.message : String(error)}`);
32
+ }
33
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
34
+ throw new Error(`Registry content root ${root} must be a regular directory`);
35
+ }
36
+ let current = root;
37
+ for (const segment of relative.split(path_1.default.sep)) {
38
+ current = path_1.default.join(current, segment);
39
+ let stat;
40
+ try {
41
+ stat = fs_1.default.lstatSync(current);
42
+ }
43
+ catch (error) {
44
+ if (error.code === 'ENOENT')
45
+ break;
46
+ throw new Error(`Cannot inspect registry bundle path ${current}: ${error instanceof Error ? error.message : String(error)}`);
47
+ }
48
+ if (stat.isSymbolicLink()) {
49
+ throw new Error(`Registry bundle path ${current} must not be a symbolic link`);
50
+ }
51
+ }
52
+ return manifest;
53
+ }
19
54
  /** Lee `catalog.json` de un registry.
20
55
  *
21
56
  * El contenido de un registry es INPUT NO CONFIABLE (un registry de equipo,
@@ -28,7 +63,7 @@ function catalogPath(contentDir) {
28
63
  * dos eran los que faltaban. */
29
64
  function readCatalog(contentDir) {
30
65
  const file = catalogPath(contentDir);
31
- if (!fs_1.default.existsSync(file))
66
+ if (!(0, registries_1.assertRegularRegistryFile)(file))
32
67
  return [];
33
68
  let parsed;
34
69
  try {
@@ -74,8 +109,8 @@ function discoverBundles(contentDir) {
74
109
  const entries = readCatalog(contentDir);
75
110
  const bundles = [];
76
111
  for (const entry of entries) {
77
- const manifestPath = path_1.default.join(contentDir, entry.source, 'bundle.json');
78
- if (!fs_1.default.existsSync(manifestPath))
112
+ const manifestPath = bundleManifestPath(contentDir, entry.source);
113
+ if (!(0, registries_1.assertRegularRegistryFile)(manifestPath))
79
114
  continue;
80
115
  let raw;
81
116
  try {
@@ -65,8 +65,8 @@ exports.defaultActions = {
65
65
  method: o.method,
66
66
  contentDir: o.contentDir,
67
67
  }),
68
- initSensors: (o) => {
69
- const result = (0, init_1.initSensors)({ cwd: o.cwd, registryRoot: o.registryRoot, configure: o.configure });
68
+ initSensors: async (o) => {
69
+ const result = await (0, init_1.initSensors)({ cwd: o.cwd, registryRoot: o.registryRoot, configure: o.configure });
70
70
  // `unavailablePack`/`manifest` viajan a proposito: `stepSensors` los reporta.
71
71
  // Este wrapper proyectaba solo `detection`, asi que cualquier hallazgo nuevo de
72
72
  // initSensors moria aca en silencio antes de llegar al step que lo muestra.
@@ -216,6 +216,9 @@ function stepDevCore(d) {
216
216
  // `awm doctor` decia `healthy`. Su propio `globalUnsupportedReason` ya dice
217
217
  // que hay que instalar por proyecto, asi que eso es lo que se hace.
218
218
  const localOnly = (0, providers_1.providerFor)(d.agent).skill.global === null;
219
+ if (localOnly && d.machineOnly) {
220
+ return ok('machine.devCore', 'machine', 'skipped', `${(0, providers_1.providerFor)(d.agent).label} provides skills at project scope — deferred until project initialization`);
221
+ }
219
222
  for (const bundleName of toInstall) {
220
223
  d.actions.installBundle({
221
224
  bundleName,
@@ -316,13 +319,13 @@ function stepActivation(d) {
316
319
  return ok('project.activation', 'project', 'applied');
317
320
  }
318
321
  /** Step 7 – Initialize the sensor manifest if absent. */
319
- function stepSensors(d) {
322
+ async function stepSensors(d) {
320
323
  const proj = d.ctx.project;
321
324
  if (!proj)
322
325
  return ok('project.sensors', 'project', 'skipped', 'no project');
323
326
  if (proj.sensors.present)
324
327
  return ok('project.sensors', 'project', 'skipped');
325
- const res = d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
328
+ const res = await d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
326
329
  // A registry without the detected pack yields a fallback (or empty) manifest. The
327
330
  // step still succeeded — a manifest was written — but reporting a bare 'applied'
328
331
  // would let `awm init` hand back a quality gate that measures less than the
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_LEDGER_SCAN_LIMITS = void 0;
7
+ exports.scanProjectLedgers = scanProjectLedgers;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const types_1 = require("./types");
11
+ exports.DEFAULT_LEDGER_SCAN_LIMITS = {
12
+ maxFiles: 256,
13
+ maxFileBytes: 4 * 1024 * 1024,
14
+ maxEntries: 20_000,
15
+ maxLineBytes: 64 * 1024,
16
+ maxJsonDepth: 16,
17
+ maxRefsPerClass: 128,
18
+ };
19
+ function isWithin(root, candidate) {
20
+ const relative = path_1.default.relative(root, candidate);
21
+ return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative));
22
+ }
23
+ function assertDirectoryInside(root, directory) {
24
+ if (!fs_1.default.existsSync(directory))
25
+ return false;
26
+ const stat = fs_1.default.lstatSync(directory);
27
+ if (stat.isSymbolicLink() || !stat.isDirectory())
28
+ throw new Error(`ledger directory is unsafe: ${directory}`);
29
+ return isWithin(root, fs_1.default.realpathSync(directory));
30
+ }
31
+ function isJsonDepthWithinLimit(raw, maxDepth) {
32
+ let depth = 0;
33
+ let quoted = false;
34
+ let escaped = false;
35
+ for (const char of raw) {
36
+ if (quoted) {
37
+ if (escaped)
38
+ escaped = false;
39
+ else if (char === '\\')
40
+ escaped = true;
41
+ else if (char === '"')
42
+ quoted = false;
43
+ continue;
44
+ }
45
+ if (char === '"')
46
+ quoted = true;
47
+ else if (char === '{' || char === '[') {
48
+ depth += 1;
49
+ if (depth > maxDepth)
50
+ return false;
51
+ }
52
+ else if (char === '}' || char === ']')
53
+ depth -= 1;
54
+ }
55
+ return true;
56
+ }
57
+ function boundedLimits(overrides) {
58
+ if (overrides === null || typeof overrides !== 'object' || Array.isArray(overrides)) {
59
+ throw new Error('ledger scan limits must be an object');
60
+ }
61
+ const limits = { ...exports.DEFAULT_LEDGER_SCAN_LIMITS, ...overrides };
62
+ for (const value of Object.values(limits)) {
63
+ if (!Number.isSafeInteger(value) || value < 1)
64
+ throw new Error('ledger scan limits must be positive safe integers');
65
+ }
66
+ return limits;
67
+ }
68
+ function relativeSource(root, target, line) {
69
+ return `${relativePath(root, target)}:${line}`;
70
+ }
71
+ function relativePath(root, target) {
72
+ return path_1.default.relative(root, target).split(path_1.default.sep).join('/');
73
+ }
74
+ function compareCandidates(a, b) {
75
+ return a.sourcePath < b.sourcePath ? -1 : a.sourcePath > b.sourcePath ? 1 : 0;
76
+ }
77
+ /**
78
+ * Reads one global bounded window across the direct ledger directories, then
79
+ * sorts only the retained candidates. This deliberately does not claim to
80
+ * find a globally lexical prefix in an unbounded directory: the extra
81
+ * candidate is a truncation witness, and no later candidate is opened or
82
+ * retained from either active or archive storage.
83
+ */
84
+ function collectBoundedCandidates(root, directories, maxFiles) {
85
+ const candidates = [];
86
+ const capacity = maxFiles + 1;
87
+ let truncated = false;
88
+ for (const { directory, archive } of directories) {
89
+ if (candidates.length >= capacity)
90
+ break;
91
+ const handle = fs_1.default.opendirSync(directory);
92
+ try {
93
+ let item;
94
+ while ((item = handle.readSync()) !== null) {
95
+ if (!item.name.endsWith('.jsonl'))
96
+ continue;
97
+ const target = path_1.default.join(directory, item.name);
98
+ candidates.push({ isArchive: archive, target, sourcePath: relativePath(root, target) });
99
+ if (candidates.length === capacity) {
100
+ truncated = true;
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ finally {
106
+ handle.closeSync();
107
+ }
108
+ }
109
+ candidates.sort(compareCandidates);
110
+ return { candidates, truncated: truncated || candidates.length > maxFiles };
111
+ }
112
+ /** Reads only direct .jsonl children of active/archive ledger roots; never follows links. */
113
+ function scanProjectLedgers(projectRoot, overrides = {}) {
114
+ if (typeof projectRoot !== 'string' || !path_1.default.isAbsolute(projectRoot))
115
+ throw new Error('projectRoot must be an absolute path');
116
+ const limits = boundedLimits(overrides);
117
+ const root = fs_1.default.realpathSync(projectRoot);
118
+ const ledgerRoot = path_1.default.join(root, '.awm', 'ledger');
119
+ const sources = {
120
+ activeFiles: 0, archivedFiles: 0, validEntries: 0, validFindings: 0, skippedFindings: 0, skippedByReason: {},
121
+ };
122
+ const entries = [];
123
+ const skip = (reason) => {
124
+ sources.skippedFindings += 1;
125
+ sources.skippedByReason[reason] = (sources.skippedByReason[reason] ?? 0) + 1;
126
+ };
127
+ if (!fs_1.default.existsSync(ledgerRoot))
128
+ return { entries, sources, omittedEvidenceRefs: 0 };
129
+ if (!assertDirectoryInside(root, path_1.default.join(root, '.awm')) || !assertDirectoryInside(root, ledgerRoot)) {
130
+ throw new Error('ledger directory escapes project root');
131
+ }
132
+ const directories = [{ directory: ledgerRoot, archive: false }];
133
+ const archive = path_1.default.join(ledgerRoot, 'archive');
134
+ if (fs_1.default.existsSync(archive)) {
135
+ if (!assertDirectoryInside(root, archive))
136
+ throw new Error('ledger archive directory escapes project root');
137
+ directories.push({ directory: archive, archive: true });
138
+ }
139
+ let filesSeen = 0;
140
+ let linesSeen = 0;
141
+ let entryLimitReached = false;
142
+ let omittedEvidenceRefs = 0;
143
+ const evidenceRefsByClass = new Map();
144
+ const collected = collectBoundedCandidates(root, directories, limits.maxFiles);
145
+ for (const { isArchive, target } of collected.candidates.slice(0, limits.maxFiles)) {
146
+ if (entryLimitReached)
147
+ break;
148
+ filesSeen += 1;
149
+ const stat = fs_1.default.lstatSync(target);
150
+ if (stat.isSymbolicLink()) {
151
+ skip('symlink-entry');
152
+ continue;
153
+ }
154
+ if (!stat.isFile()) {
155
+ skip('nonregular-entry');
156
+ continue;
157
+ }
158
+ const real = fs_1.default.realpathSync(target);
159
+ if (!isWithin(root, real)) {
160
+ skip('path-escape');
161
+ continue;
162
+ }
163
+ if (stat.size > limits.maxFileBytes) {
164
+ skip('file-too-large');
165
+ continue;
166
+ }
167
+ if (isArchive)
168
+ sources.archivedFiles += 1;
169
+ else
170
+ sources.activeFiles += 1;
171
+ const lines = fs_1.default.readFileSync(target, 'utf-8').split('\n');
172
+ for (let index = 0; index < lines.length; index += 1) {
173
+ const raw = lines[index];
174
+ if (!raw.trim())
175
+ continue;
176
+ if (linesSeen >= limits.maxEntries) {
177
+ skip('entry-limit');
178
+ entryLimitReached = true;
179
+ break;
180
+ }
181
+ linesSeen += 1;
182
+ if (Buffer.byteLength(raw, 'utf-8') > limits.maxLineBytes) {
183
+ skip('line-too-large');
184
+ continue;
185
+ }
186
+ if (!isJsonDepthWithinLimit(raw, limits.maxJsonDepth)) {
187
+ skip('json-too-deep');
188
+ continue;
189
+ }
190
+ let value;
191
+ try {
192
+ value = JSON.parse(raw);
193
+ }
194
+ catch {
195
+ skip('invalid-json');
196
+ continue;
197
+ }
198
+ const source = relativeSource(root, target, index + 1);
199
+ const parsed = (0, types_1.parseLedgerEntry)(value, source);
200
+ if (!parsed.ok) {
201
+ skip(parsed.reason);
202
+ continue;
203
+ }
204
+ sources.validEntries += 1;
205
+ if (parsed.entry.polarity === 'finding') {
206
+ sources.validFindings += 1;
207
+ const defectClass = parsed.entry.defectClass ?? 'unclassified';
208
+ const count = evidenceRefsByClass.get(defectClass) ?? 0;
209
+ const evidenceRef = count < limits.maxRefsPerClass ? source : null;
210
+ if (evidenceRef === null) {
211
+ omittedEvidenceRefs += 1;
212
+ // The finding remains available for recurrence analysis, but
213
+ // its public evidence is deliberately omitted. Record the
214
+ // truncation through the same typed source contract as every
215
+ // other scan bound so downstream renderers cannot call it a
216
+ // complete report.
217
+ skip('evidence-ref-limit');
218
+ }
219
+ else
220
+ evidenceRefsByClass.set(defectClass, count + 1);
221
+ entries.push({ entry: parsed.entry, source, evidenceRef });
222
+ }
223
+ }
224
+ }
225
+ if (!entryLimitReached && collected.truncated)
226
+ skip('file-limit');
227
+ return { entries, sources, omittedEvidenceRefs };
228
+ }