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
@@ -3,7 +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.REGISTRY_MANIFEST_NAME = exports.CONTENT_DIR_NAMES = void 0;
6
+ exports.REGISTRY_METADATA_FILE_NAMES = exports.REGISTRY_MANIFEST_NAME = exports.REGISTRY_DIR_NAMES = exports.CAPABILITY_DIR_NAMES = exports.CONTENT_DIR_NAMES = void 0;
7
7
  exports.registriesDir = registriesDir;
8
8
  exports.registriesConfigPath = registriesConfigPath;
9
9
  exports.registryContentRoot = registryContentRoot;
@@ -13,7 +13,10 @@ exports.seedBaselineRegistry = seedBaselineRegistry;
13
13
  exports.listRegistries = listRegistries;
14
14
  exports.contentRoots = contentRoots;
15
15
  exports.capabilityRoot = capabilityRoot;
16
+ exports.assertRegularRegistryFile = assertRegularRegistryFile;
17
+ exports.isSafeRegistryRoot = isSafeRegistryRoot;
16
18
  exports.validateRegistryLayout = validateRegistryLayout;
19
+ exports.registriesNeedSync = registriesNeedSync;
17
20
  exports.syncRegistries = syncRegistries;
18
21
  exports.registrySyncErrors = registrySyncErrors;
19
22
  exports.describeRegistrySyncErrors = describeRegistrySyncErrors;
@@ -44,9 +47,13 @@ function registriesConfigPath() {
44
47
  return path_1.default.join((0, paths_1.awmHome)(), 'registries.json');
45
48
  }
46
49
  exports.CONTENT_DIR_NAMES = ['skills', 'bundles', 'workflows', 'agents'];
50
+ exports.CAPABILITY_DIR_NAMES = ['hooks', 'sensor-packs'];
51
+ exports.REGISTRY_DIR_NAMES = [...exports.CONTENT_DIR_NAMES, ...exports.CAPABILITY_DIR_NAMES];
52
+ exports.REGISTRY_MANIFEST_NAME = 'awm-registry.json';
53
+ exports.REGISTRY_METADATA_FILE_NAMES = [exports.REGISTRY_MANIFEST_NAME, 'catalog.json'];
47
54
  function registryContentRoot(name) {
48
55
  const root = path_1.default.join(registriesDir(), name);
49
- if (!path_1.default.resolve(root).startsWith(path_1.default.resolve(registriesDir()) + path_1.default.sep)) {
56
+ if (!name || name === '.' || name.includes('..') || /[/\\]/.test(name) || !path_1.default.resolve(root).startsWith(path_1.default.resolve(registriesDir()) + path_1.default.sep)) {
50
57
  throw new Error(`Invalid registry name "${name}" — must not contain path separators`);
51
58
  }
52
59
  return root;
@@ -68,7 +75,7 @@ function readRegistriesConfig() {
68
75
  if (typeof entry?.name !== 'string' || typeof entry?.remote !== 'string') {
69
76
  throw new Error(`Invalid registries config at ${registriesConfigPath()}: malformed entry ${JSON.stringify(entry)}`);
70
77
  }
71
- if (entry.name === '.' || entry.name.includes('/') || entry.name.includes('\\') || entry.name.includes('..')) {
78
+ if (!entry.name || entry.name === '.' || entry.name.includes('/') || entry.name.includes('\\') || entry.name.includes('..')) {
72
79
  throw new Error(`Invalid registries config at ${registriesConfigPath()}: malformed entry name "${entry.name}" (path traversal)`);
73
80
  }
74
81
  }
@@ -95,7 +102,7 @@ function listRegistries() {
95
102
  function contentRoots() {
96
103
  const roots = [];
97
104
  for (const reg of listRegistries()) {
98
- if (fs_1.default.existsSync(reg.contentRoot))
105
+ if (isSafeRegistryRoot(reg.contentRoot))
99
106
  roots.push(reg.contentRoot);
100
107
  }
101
108
  return roots;
@@ -104,14 +111,121 @@ function contentRoots() {
104
111
  * directorio pedido ('hooks', 'sensor-packs', 'skills'…). null si ninguno. */
105
112
  function capabilityRoot(dirName) {
106
113
  for (const root of contentRoots()) {
107
- if (fs_1.default.existsSync(path_1.default.join(root, dirName)))
114
+ if (isRegularDirectory(path_1.default.join(root, dirName)))
108
115
  return root;
109
116
  }
110
117
  return null;
111
118
  }
112
- /** Un registry válido tiene ≥1 dir de contenido en su raíz. */
119
+ /** Todo symlink bajo directorios consumidos de un registry es no confiable: discovery,
120
+ * hooks y sensores no pueden consumir contenido que podría resolver fuera del clone. */
121
+ function containsManagedSymlink(dir) {
122
+ let entries;
123
+ try {
124
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
125
+ }
126
+ catch {
127
+ return true;
128
+ }
129
+ for (const entry of entries) {
130
+ const candidate = path_1.default.join(dir, entry.name);
131
+ let stat;
132
+ try {
133
+ stat = fs_1.default.lstatSync(candidate);
134
+ }
135
+ catch {
136
+ return true;
137
+ }
138
+ if (stat.isSymbolicLink())
139
+ return true;
140
+ if (stat.isDirectory() && containsManagedSymlink(candidate))
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+ function isMissing(error) {
146
+ return error?.code === 'ENOENT';
147
+ }
148
+ function isRegularDirectory(candidate) {
149
+ try {
150
+ const stat = fs_1.default.lstatSync(candidate);
151
+ return stat.isDirectory() && !stat.isSymbolicLink();
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ }
157
+ function isSafeOptionalRegistryFile(candidate) {
158
+ try {
159
+ const stat = fs_1.default.lstatSync(candidate);
160
+ return stat.isFile() && !stat.isSymbolicLink();
161
+ }
162
+ catch (error) {
163
+ return isMissing(error);
164
+ }
165
+ }
166
+ /** Returns false when absent; throws when an existing registry metadata file cannot be
167
+ * safely read as a regular file. Kept public so catalog and manifest readers share the
168
+ * same trust boundary instead of relying only on an earlier layout scan. */
169
+ function assertRegularRegistryFile(file) {
170
+ let stat;
171
+ try {
172
+ stat = fs_1.default.lstatSync(file);
173
+ }
174
+ catch (error) {
175
+ if (isMissing(error))
176
+ return false;
177
+ throw new Error(`Cannot inspect registry metadata at ${file}: ${error instanceof Error ? error.message : String(error)}`);
178
+ }
179
+ if (stat.isSymbolicLink())
180
+ throw new Error(`Registry metadata at ${file} must not be a symbolic link`);
181
+ if (!stat.isFile())
182
+ throw new Error(`Registry metadata at ${file} must be a regular file`);
183
+ return true;
184
+ }
185
+ /** Verifica que la raíz y cada directorio que AWM consume sean regulares y no tengan
186
+ * symlinks anidados. Los registries capability-only siguen siendo válidos. */
187
+ function inspectRegistrySafety(root) {
188
+ if (!isRegularDirectory(root))
189
+ return { safe: false, hasManagedDirectory: false };
190
+ let hasManagedDirectory = false;
191
+ for (const d of exports.REGISTRY_DIR_NAMES) {
192
+ const candidate = path_1.default.join(root, d);
193
+ try {
194
+ const stat = fs_1.default.lstatSync(candidate);
195
+ if (stat.isSymbolicLink())
196
+ return { safe: false, hasManagedDirectory: false };
197
+ if (!stat.isDirectory())
198
+ continue;
199
+ hasManagedDirectory = true;
200
+ if (containsManagedSymlink(candidate))
201
+ return { safe: false, hasManagedDirectory: false };
202
+ }
203
+ catch (error) {
204
+ if (isMissing(error))
205
+ continue;
206
+ return { safe: false, hasManagedDirectory: false };
207
+ }
208
+ }
209
+ for (const file of exports.REGISTRY_METADATA_FILE_NAMES) {
210
+ if (!isSafeOptionalRegistryFile(path_1.default.join(root, file))) {
211
+ return { safe: false, hasManagedDirectory: false };
212
+ }
213
+ }
214
+ return { safe: true, hasManagedDirectory };
215
+ }
216
+ function isSafeRegistryRoot(root) {
217
+ return inspectRegistrySafety(root).safe;
218
+ }
219
+ /** Un registry válido tiene ≥1 directorio de contenido o capability regular y ningún
220
+ * symlink en las rutas que AWM consume. */
113
221
  function validateRegistryLayout(root) {
114
- return exports.CONTENT_DIR_NAMES.some((d) => fs_1.default.existsSync(path_1.default.join(root, d)));
222
+ const state = inspectRegistrySafety(root);
223
+ return state.safe && state.hasManagedDirectory;
224
+ }
225
+ /** Whether machine setup must synchronize before any registry-backed discovery.
226
+ * An empty root is safe to keep configured, but cannot yet supply AWM content. */
227
+ function registriesNeedSync() {
228
+ return listRegistries().some((registry) => !validateRegistryLayout(registry.contentRoot));
115
229
  }
116
230
  /** Sincroniza cada registry configurado al ref resuelto (pin > último tag > HEAD);
117
231
  * re-clona si falta el dir. Errores por-registry NO fatales: se reportan en el resultado. */
@@ -120,6 +234,9 @@ async function syncRegistries() {
120
234
  for (const reg of listRegistries()) {
121
235
  try {
122
236
  const freshClone = !fs_1.default.existsSync(reg.contentRoot);
237
+ if (!freshClone && !isSafeRegistryRoot(reg.contentRoot)) {
238
+ throw new Error(`Unsafe registry layout at ${reg.contentRoot}`);
239
+ }
123
240
  if (freshClone) {
124
241
  fs_1.default.mkdirSync(registriesDir(), { recursive: true });
125
242
  try {
@@ -140,6 +257,9 @@ async function syncRegistries() {
140
257
  await git.checkout(resolved.ref);
141
258
  if (resolved.kind !== 'tag')
142
259
  await git.pull('origin', resolved.ref);
260
+ if (!validateRegistryLayout(reg.contentRoot)) {
261
+ throw new Error(`Unsafe registry layout at ${reg.contentRoot}`);
262
+ }
143
263
  }
144
264
  catch (e) {
145
265
  if (freshClone)
@@ -169,13 +289,14 @@ function describeRegistrySyncErrors(errors) {
169
289
  return errors.map((e) => `${e.name}: ${e.error}`).join('; ');
170
290
  }
171
291
  /**
172
- * Registries that both errored during sync AND have no content on disk
173
- * afterwards — i.e. genuinely unusable, as opposed to merely stale.
292
+ * Registries that both errored during sync AND no longer have a usable content
293
+ * layout afterwards — i.e. genuinely unusable, as opposed to merely stale.
174
294
  *
175
295
  * `syncRegistries()` deliberately reports per-registry failures as results
176
296
  * rather than throwing, so a flaky secondary registry never aborts a whole
177
297
  * run. Callers that go on to READ registry content (init) still need to know
178
- * whether what they are about to read exists: otherwise a missing registry
298
+ * whether what they are about to read remains usable: otherwise a missing,
299
+ * empty, or unsafe registry
179
300
  * resurfaces much later as an unrelated step's failure, with its real cause
180
301
  * already discarded. Note that "usable" is deliberately about content on disk,
181
302
  * not about being a healthy git clone — a seeded content root with no `.git`
@@ -188,7 +309,7 @@ function unusableSyncedRegistries(results) {
188
309
  const roots = new Map(listRegistries().map((r) => [r.name, r.contentRoot]));
189
310
  return errors.filter((e) => {
190
311
  const root = roots.get(e.name);
191
- return root === undefined || !fs_1.default.existsSync(root);
312
+ return root === undefined || !validateRegistryLayout(root);
192
313
  });
193
314
  }
194
315
  /** `unusableSyncedRegistries` as a guard: throws naming every unusable registry. */
@@ -196,12 +317,11 @@ function assertSyncedRegistriesUsable(results) {
196
317
  const unusable = unusableSyncedRegistries(results);
197
318
  if (unusable.length === 0)
198
319
  return;
199
- throw new Error(`registry sync failed and left no content on disk — ${describeRegistrySyncErrors(unusable)}`);
320
+ throw new Error(`registry sync failed and left no usable content on disk — ${describeRegistrySyncErrors(unusable)}`);
200
321
  }
201
- exports.REGISTRY_MANIFEST_NAME = 'awm-registry.json';
202
322
  function readRegistryManifest(root) {
203
323
  const file = path_1.default.join(root, exports.REGISTRY_MANIFEST_NAME);
204
- if (!fs_1.default.existsSync(file))
324
+ if (!assertRegularRegistryFile(file))
205
325
  return { overrides: new Set() };
206
326
  let raw;
207
327
  try {
package/dist/src/index.js CHANGED
@@ -118,6 +118,15 @@ program.command('add [name]')
118
118
  if (r.action === 'error')
119
119
  console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
120
120
  }
121
+ try {
122
+ (0, registries_1.assertSyncedRegistriesUsable)(results);
123
+ }
124
+ catch (e) {
125
+ s.stop('Registry sync failed.');
126
+ console.error(picocolors_1.default.red(e.message));
127
+ process.exitCode = 1;
128
+ return;
129
+ }
121
130
  // 1b. If `name` matches a bundle, run the bundle-activation flow and exit.
122
131
  if (name) {
123
132
  const allBundles = (0, bundles_1.discoverAllBundles)();
@@ -437,6 +446,14 @@ program.command('list [package]')
437
446
  if (r.action === 'error')
438
447
  console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
439
448
  }
449
+ try {
450
+ (0, registries_1.assertSyncedRegistriesUsable)(listSyncResults);
451
+ }
452
+ catch (e) {
453
+ console.error(picocolors_1.default.red(e.message));
454
+ process.exitCode = 1;
455
+ return;
456
+ }
440
457
  const fullView = (0, registry_view_1.buildPackageView)((0, discovery_1.discoverSkills)(), (0, discovery_1.discoverWorkflows)(), (0, discovery_1.discoverAgents)(), (0, bundles_1.discoverAllBundles)());
441
458
  const view = options.all ? fullView : fullView.filter((p) => p.visibility !== 'private');
442
459
  if (view.length === 0) {
@@ -716,4 +733,10 @@ miroCmd.command('sync <storyMapPath>')
716
733
  (0, job_1.registerJobCommand)(program);
717
734
  (0, watch_1.registerWatchCommand)(program);
718
735
  (0, track_1.registerTrackCommand)(program);
719
- program.parse();
736
+ // Commander only waits for async action handlers through parseAsync(). The CLI has
737
+ // async commands (including `sensors coverage`), so returning its promise keeps the
738
+ // process alive until their JSON/output contract has been completed.
739
+ program.parseAsync().catch((error) => {
740
+ console.error(error instanceof Error ? error.message : String(error));
741
+ process.exitCode = 1;
742
+ });
@@ -40,6 +40,17 @@ describe('awm ledger CLI', () => {
40
40
  expect(entries[0]).toMatchObject({ signature: 'sig-1', polarity: 'finding', class: 'logica' });
41
41
  expect(entries[0].ts).toMatch(/\d{4}-\d{2}-\d{2}T/);
42
42
  });
43
+ test('add persists an optional reusable defect class', () => {
44
+ run(['add', '--branch', 'feat-x', '--polarity', 'finding', '--class', 'logica',
45
+ '--signature', 'sig-1', '--severity', 'blocker', '--desc', 'boom', '--defect-class', 'lint-errors'], cwd);
46
+ expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([expect.objectContaining({ defectClass: 'lint-errors' })]);
47
+ });
48
+ test.each(['', 'Bad_ID', '../escape', 'a b', '-leading', 'trailing-'])('add rejects invalid defect class %p before writing', (defectClass) => {
49
+ expect(() => run(['add', '--branch', 'feat-x', '--polarity', 'finding', '--class', 'logica',
50
+ '--signature', 'sig-1', '--severity', 'blocker', '--desc', 'boom', '--defect-class', defectClass], cwd))
51
+ .toThrow(/defect-class.*kebab-case/i);
52
+ expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([]);
53
+ });
43
54
  test('list emits the branch entries as JSON', () => {
44
55
  run(['add', '--branch', 'feat-x', '--polarity', 'win', '--class', 'proceso',
45
56
  '--signature', 'good', '--severity', 'info', '--desc', 'nice'], cwd);