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
@@ -7,6 +7,8 @@ const fs_1 = __importDefault(require("fs"));
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const os_1 = __importDefault(require("os"));
9
9
  const status_1 = require("../../../src/commands/sensors/status");
10
+ const checks_1 = require("../../../src/commands/preflight/checks");
11
+ const run_1 = require("../../../src/commands/sensors/run");
10
12
  // `resolveOnPath` resuelve PATH en proceso (ya no invoca un shell — ver
11
13
  // core/paths.ts). Por eso estos tests controlan un PATH aislado en vez de
12
14
  // mockear `execSync`: ademas de reflejar el mecanismo real, los vuelve
@@ -36,8 +38,8 @@ describe('computeSensorStatus', () => {
36
38
  fs_1.default.writeFileSync(p, '#!/bin/sh\nexit 0\n');
37
39
  fs_1.default.chmodSync(p, 0o755);
38
40
  }
39
- it('returns NOT_CONFIGURED when .awm/sensors.json missing', () => {
40
- const result = (0, status_1.computeSensorStatus)(tmpDir);
41
+ it('returns NOT_CONFIGURED when .awm/sensors.json missing', async () => {
42
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
41
43
  expect(result.overall).toBe('NOT_CONFIGURED');
42
44
  expect(result.pack).toBeNull();
43
45
  });
@@ -47,42 +49,42 @@ describe('computeSensorStatus', () => {
47
49
  fs_1.default.mkdirSync(binDir, { recursive: true });
48
50
  fs_1.default.writeFileSync(path_1.default.join(binDir, tool), '');
49
51
  }
50
- it('returns HEALTHY when an npx tool is installed locally', () => {
52
+ it('keeps an operational legacy manifest DEGRADED even when its npx tool is installed locally', async () => {
51
53
  installLocalBin('tsc');
52
54
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
53
55
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
54
56
  pack: 'js-ts',
55
57
  sensors: { typecheck: { cmd: 'npx tsc --noEmit', fast: true } }
56
58
  }));
57
- const result = (0, status_1.computeSensorStatus)(tmpDir);
58
- expect(result.overall).toBe('HEALTHY');
59
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
60
+ expect(result.overall).toBe('DEGRADED');
59
61
  expect(result.pack).toBe('js-ts');
60
62
  expect(result.checks.typecheck.ok).toBe(true);
61
63
  });
62
- it('marks an npx sensor DEGRADED when the tool is NOT installed locally (npx would fetch a remote package)', () => {
64
+ it('marks an npx sensor DEGRADED when the tool is NOT installed locally (npx would fetch a remote package)', async () => {
63
65
  // No node_modules/.bin/depcruise → status must NOT report ✔ just because npx exists.
64
66
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
65
67
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
66
68
  pack: 'js-ts',
67
69
  sensors: { depcheck: { cmd: 'npx depcruise --config .dep-cruiser.awm.js app', fast: false } }
68
70
  }));
69
- const result = (0, status_1.computeSensorStatus)(tmpDir);
71
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
70
72
  expect(result.overall).toBe('DEGRADED');
71
73
  expect(result.checks.depcheck.ok).toBe(false);
72
74
  expect(result.checks.depcheck.detail).toMatch(/not installed locally/i);
73
75
  });
74
- it('marks a sensor DEGRADED when its --config file is missing', () => {
76
+ it('marks a sensor DEGRADED when its --config file is missing', async () => {
75
77
  installLocalBin('eslint');
76
78
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
77
79
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
78
80
  pack: 'js-ts',
79
81
  sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs --format json', fast: true } }
80
82
  }));
81
- const result = (0, status_1.computeSensorStatus)(tmpDir);
83
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
82
84
  expect(result.checks.lint.ok).toBe(false);
83
85
  expect(result.checks.lint.detail).toMatch(/missing config/i);
84
86
  });
85
- it('is HEALTHY when the npx tool is installed and the --config file exists', () => {
87
+ it('is HEALTHY when the npx tool is installed and the --config file exists', async () => {
86
88
  installLocalBin('eslint');
87
89
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'), 'export default []');
88
90
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
@@ -90,17 +92,17 @@ describe('computeSensorStatus', () => {
90
92
  pack: 'js-ts',
91
93
  sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs --format json', fast: true } }
92
94
  }));
93
- const result = (0, status_1.computeSensorStatus)(tmpDir);
95
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
94
96
  expect(result.checks.lint.ok).toBe(true);
95
97
  });
96
- it('returns DEGRADED when a binary is missing', () => {
98
+ it('returns DEGRADED when a binary is missing', async () => {
97
99
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
98
100
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
99
101
  pack: 'js-ts',
100
102
  sensors: { security: { cmd: 'semgrep --json .', fast: false } }
101
103
  }));
102
104
  // PATH aislado y vacio => semgrep no resuelve.
103
- const result = (0, status_1.computeSensorStatus)(tmpDir);
105
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
104
106
  expect(result.overall).toBe('DEGRADED');
105
107
  expect(result.checks.security.ok).toBe(false);
106
108
  });
@@ -112,19 +114,19 @@ describe('computeSensorStatus', () => {
112
114
  afterEach(() => {
113
115
  Object.defineProperty(process, 'platform', { value: originalPlatform });
114
116
  });
115
- it('resuelve un binario instalado recorriendo PATH, sin invocar un shell', () => {
117
+ it('resuelve un binario instalado recorriendo PATH, sin invocar un shell', async () => {
116
118
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
117
119
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
118
120
  pack: 'js-ts',
119
121
  sensors: { security: { cmd: 'semgrep --json .', fast: false } }
120
122
  }));
121
123
  installOnPath('semgrep');
122
- const result = (0, status_1.computeSensorStatus)(tmpDir);
123
- expect(result.overall).toBe('HEALTHY');
124
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
125
+ expect(result.overall).toBe('DEGRADED');
124
126
  expect(result.checks.security.ok).toBe(true);
125
127
  });
126
128
  });
127
- it('is DEGRADED (never HEALTHY) when the manifest has zero sensor entries', () => {
129
+ it('is DEGRADED (never HEALTHY) when the manifest has zero sensor entries', async () => {
128
130
  // `Object.values({}).every(...)` is vacuously true — guard against reading an
129
131
  // empty manifest (the honest floor when the registry had no pack.json for the
130
132
  // detected stack) as a clean run that found nothing wrong.
@@ -133,11 +135,11 @@ describe('computeSensorStatus', () => {
133
135
  pack: 'python',
134
136
  sensors: {},
135
137
  }));
136
- const result = (0, status_1.computeSensorStatus)(tmpDir);
138
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
137
139
  expect(result.overall).toBe('DEGRADED');
138
140
  expect(result.pack).toBe('python');
139
141
  });
140
- it('degrades gracefully (never throws) when sensors is null in a hand-edited manifest', () => {
142
+ it('degrades gracefully (never throws) when sensors is null in a hand-edited manifest', async () => {
141
143
  // Regression for Finding 3: `checkManifest` (preflight) already guards
142
144
  // `manifest.sensors ?? {}` — computeSensorStatus needs the same guard, or a
143
145
  // corrupted/hand-edited manifest with `"sensors": null` crashes
@@ -147,20 +149,69 @@ describe('computeSensorStatus', () => {
147
149
  pack: 'python',
148
150
  sensors: null,
149
151
  }));
150
- expect(() => (0, status_1.computeSensorStatus)(tmpDir)).not.toThrow();
151
- const result = (0, status_1.computeSensorStatus)(tmpDir);
152
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
152
153
  expect(result.overall).toBe('DEGRADED');
153
154
  expect(result.pack).toBe('python');
154
155
  expect(result.checks).toEqual({});
155
156
  });
156
- it('marks disabled sensors as ok', () => {
157
+ it('marks disabled sensors as ok', async () => {
157
158
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
158
159
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
159
160
  pack: 'js-ts',
160
161
  sensors: { mutation: { enabled: false } }
161
162
  }));
162
- const result = (0, status_1.computeSensorStatus)(tmpDir);
163
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
163
164
  expect(result.checks.mutation.ok).toBe(true);
164
165
  expect(result.checks.mutation.detail).toBe('disabled');
165
166
  });
167
+ it('does not trust the initialized v2 variant after local tool drift', async () => {
168
+ // The manifest records ESLint 9 at init time, but local evidence is now ESLint
169
+ // 10. Status must resolve the installed pack again, not certify the stale
170
+ // initializedCompatibility record.
171
+ const previousHome = process.env.AWM_HOME;
172
+ const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-home-'));
173
+ try {
174
+ process.env.AWM_HOME = home;
175
+ const registry = path_1.default.join(home, 'registries', 'baseline');
176
+ fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
177
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
178
+ const variant = (id, range) => ({
179
+ id, priority: 10, certifiedRange: range,
180
+ requirements: { tool: 'eslint', toolRange: range, runtime: 'node', runtimeRange: '>=0.0.0' },
181
+ assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'config-present' },
182
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
183
+ });
184
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
185
+ schemaVersion: 2, name: 'js-ts', description: 'test', detects: ['package.json'], coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix lint', command: 'awm sensors init --pack js-ts' } } } },
186
+ sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [variant('eslint-9', '>=9.0.0 <10.0.0'), variant('eslint-10', '>=10.0.0 <11.0.0')] } },
187
+ }));
188
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
189
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
190
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
191
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
192
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
193
+ schemaVersion: 2, pack: 'js-ts', sensors: { lint: {
194
+ enabled: true, variantId: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
195
+ initializedCompatibility: { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: process.versions.node, certifiedRange: '>=9.0.0 <10.0.0', evidence: [] },
196
+ } },
197
+ }));
198
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
199
+ expect(result.overall).toBe('DEGRADED');
200
+ expect(result.checks.lint).toMatchObject({ ok: false, detail: expect.stringContaining('variant-drift') });
201
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'AGENTS.md'), '# test\n');
202
+ const gate = await (0, checks_1.preflight)(tmpDir);
203
+ expect(gate.status).toBe('degraded');
204
+ expect(gate.checks.find(check => check.id === 'tools')).toMatchObject({ ok: false, detail: expect.stringContaining('variant-drift') });
205
+ const run = await (0, run_1.runSensors)({ cwd: tmpDir, all: true });
206
+ expect(run.overall).toBe('not_certified');
207
+ expect(run.sensors).toEqual([expect.objectContaining({ name: 'lint', status: 'inconclusive', skipReason: expect.stringContaining('variant-drift') })]);
208
+ }
209
+ finally {
210
+ if (previousHome === undefined)
211
+ delete process.env.AWM_HOME;
212
+ else
213
+ process.env.AWM_HOME = previousHome;
214
+ fs_1.default.rmSync(home, { recursive: true, force: true });
215
+ }
216
+ });
166
217
  });
@@ -137,7 +137,7 @@ describe('awm update — honest outcome', () => {
137
137
  // `unusableSyncedRegistries` mira contenido EN DISCO: se siembra el content root
138
138
  // y el registries.json para que `docs` cuente como stale, no como roto.
139
139
  const registriesDir = path_1.default.join(process.env.AWM_HOME, 'registries');
140
- fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs'), { recursive: true });
140
+ fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs', 'skills'), { recursive: true });
141
141
  fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'docs', remote: 'https://example.test/docs.git' }]));
142
142
  const { runUpdateCore, updateOutro } = require('../../src/commands/update');
143
143
  const result = await runUpdateCore({}, deps({
@@ -37,6 +37,14 @@ describe('readCatalog', () => {
37
37
  expect(entries.map((e) => e.name).sort()).toEqual(['dev', 'frontend']);
38
38
  expect(entries.find((e) => e.name === 'dev').scope).toBe('baseline');
39
39
  });
40
+ it('rejects a catalog symlink instead of reading outside the registry', () => {
41
+ const content = makeFixture();
42
+ const outside = path_1.default.join(path_1.default.dirname(content), 'outside-catalog.json');
43
+ fs_1.default.writeFileSync(outside, JSON.stringify({ bundles: [] }));
44
+ fs_1.default.rmSync(path_1.default.join(content, 'catalog.json'));
45
+ fs_1.default.symlinkSync(outside, path_1.default.join(content, 'catalog.json'));
46
+ expect(() => (0, bundles_1.readCatalog)(content)).toThrow(/symbolic link/);
47
+ });
40
48
  });
41
49
  describe('discoverBundles', () => {
42
50
  it('loads each bundle and normalizes skill refs (string | object)', () => {
@@ -54,6 +62,35 @@ describe('discoverBundles', () => {
54
62
  const empty = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-'));
55
63
  expect((0, bundles_1.discoverBundles)(empty)).toEqual([]);
56
64
  });
65
+ it('rejects catalog sources that escape the registry content root', () => {
66
+ const content = makeFixture();
67
+ const outside = path_1.default.join(path_1.default.dirname(content), 'outside');
68
+ fs_1.default.mkdirSync(outside, { recursive: true });
69
+ fs_1.default.writeFileSync(path_1.default.join(outside, 'bundle.json'), JSON.stringify({ name: 'outside' }));
70
+ fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({
71
+ bundles: [{ name: 'outside', source: '../outside', version: '1.0.0', scope: 'project' }],
72
+ }));
73
+ expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/invalid bundle source/i);
74
+ });
75
+ it('rejects a bundle manifest symlink instead of reading outside the registry', () => {
76
+ const content = makeFixture();
77
+ const outside = path_1.default.join(path_1.default.dirname(content), 'outside-bundle.json');
78
+ fs_1.default.writeFileSync(outside, JSON.stringify({ name: 'outside' }));
79
+ fs_1.default.rmSync(path_1.default.join(content, 'bundles', 'dev', 'bundle.json'));
80
+ fs_1.default.symlinkSync(outside, path_1.default.join(content, 'bundles', 'dev', 'bundle.json'));
81
+ expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/symbolic link/);
82
+ });
83
+ it('rejects a catalog source that reaches a bundle through an intermediate symlink', () => {
84
+ const content = makeFixture();
85
+ const outside = path_1.default.join(path_1.default.dirname(content), 'outside-bundle');
86
+ fs_1.default.mkdirSync(outside, { recursive: true });
87
+ fs_1.default.writeFileSync(path_1.default.join(outside, 'bundle.json'), JSON.stringify({ name: 'outside' }));
88
+ fs_1.default.symlinkSync(outside, path_1.default.join(content, 'outside'));
89
+ fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({
90
+ bundles: [{ name: 'outside', source: 'outside', version: '1.0.0', scope: 'project' }],
91
+ }));
92
+ expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/symbolic link/);
93
+ });
57
94
  });
58
95
  describe('resolveBundleSkills', () => {
59
96
  it('follows dependsOn transitively and dedupes', () => {
@@ -18,6 +18,10 @@ const WORKFLOWS_DIR = path_1.default.join(CONTENT_ROOT, 'workflows');
18
18
  describe('Artifact Discovery', () => {
19
19
  beforeEach(() => {
20
20
  jest.clearAllMocks();
21
+ fs_1.default.lstatSync.mockImplementation(() => {
22
+ const error = Object.assign(new Error('missing'), { code: 'ENOENT' });
23
+ throw error;
24
+ });
21
25
  });
22
26
  describe('discoverSkills', () => {
23
27
  it('should return a list of skill directories that contain a SKILL.md', () => {
@@ -19,7 +19,11 @@ function seedRegistry(contentRoot) {
19
19
  fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'hooks', 'run-hook.cmd'), '#!/bin/sh\n');
20
20
  fs_1.default.chmodSync(path_1.default.join(contentRoot, 'hooks', 'run-hook.cmd'), 0o755);
21
21
  fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts'), { recursive: true });
22
- fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({ sensors: { lint: { defaultCmd: 'eslint {{SOURCE_DIRS}}', fast: true } } }));
22
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts', 'pack.json'),
23
+ // R3 validates even a legacy pack at the registry boundary. This fixture
24
+ // intentionally stays legacy (no schemaVersion) but must still carry its
25
+ // required stable identity, just like a real legacy registry pack.
26
+ JSON.stringify({ name: 'js-ts', sensors: { lint: { defaultCmd: 'eslint {{SOURCE_DIRS}}', fast: true } } }));
23
27
  // catalog + bundle dev (baseline)
24
28
  fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'catalog.json'), JSON.stringify({
25
29
  version: 1, bundles: [{ name: 'dev', source: './bundles/dev', version: '1.0.0', scope: 'baseline' }],
@@ -143,6 +143,22 @@ describe('stepHook / stepDevCore / stepAmbient', () => {
143
143
  expect((0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a)).action).toBe('applied');
144
144
  expect(a.installBundle).toHaveBeenCalled();
145
145
  });
146
+ it('defers local-only baseline bundles during --machine-only (R7)', () => {
147
+ const a = spies();
148
+ const m = machine();
149
+ m.devCore = { present: false, brokenLinks: [] };
150
+ const r = (0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a, {
151
+ agent: 'copilot',
152
+ enabledAgents: ['copilot'],
153
+ machineOnly: true,
154
+ }));
155
+ expect(r).toMatchObject({
156
+ action: 'skipped',
157
+ level: 'machine',
158
+ });
159
+ expect(r.detail).toMatch(/project scope.*deferred/i);
160
+ expect(a.installBundle).not.toHaveBeenCalled();
161
+ });
146
162
  // Regression for the confirmed production bug: `awm init -a copilot` crashed
147
163
  // 100% of the time with "machine.devCore: skill global scope is not
148
164
  // supported by Copilot...", rolling back the ENTIRE init transaction (even
@@ -386,13 +402,13 @@ describe('stepActivation', () => {
386
402
  });
387
403
  });
388
404
  describe('stepSensors', () => {
389
- it('skips when sensors present', () => {
405
+ it('skips when sensors present', async () => {
390
406
  const a = spies();
391
- expect((0, steps_1.stepSensors)(deps({ machine: machine(), project: project() }, a)).action).toBe('skipped');
407
+ expect((await (0, steps_1.stepSensors)(deps({ machine: machine(), project: project() }, a))).action).toBe('skipped');
392
408
  });
393
- it('inits sensors when absent', () => {
409
+ it('inits sensors when absent', async () => {
394
410
  const a = spies();
395
- const r = (0, steps_1.stepSensors)(deps({ machine: machine(), project: project({ sensors: { present: false } }) }, a));
411
+ const r = await (0, steps_1.stepSensors)(deps({ machine: machine(), project: project({ sensors: { present: false } }) }, a));
396
412
  expect(r.action).toBe('applied');
397
413
  expect(a.initSensors).toHaveBeenCalledWith({ cwd: '/repo', registryRoot: '/cache/registry', configure: true }); // sensorPacksRoot='/cache/registry' from deps()
398
414
  });
@@ -0,0 +1,217 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const scan_1 = require("../../../src/core/ledger/scan");
10
+ function mkTmp() {
11
+ return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ledger-scan-'));
12
+ }
13
+ function entry(over = {}) {
14
+ return {
15
+ ts: '2026-08-14T00:00:00.000Z', branch: 'feat-x', phase: 'qa', source_skill: 'post-implementation-qa',
16
+ polarity: 'finding', class: 'logica', signature: 'lint-error', severity: 'important', desc: 'private detail',
17
+ defectClass: 'lint-errors', ...over,
18
+ };
19
+ }
20
+ function write(root, relative, lines) {
21
+ const target = path_1.default.join(root, relative);
22
+ fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
23
+ fs_1.default.writeFileSync(target, lines.map(line => typeof line === 'string' ? line : JSON.stringify(line)).join('\n') + '\n');
24
+ }
25
+ describe('scanProjectLedgers', () => {
26
+ let root;
27
+ beforeEach(() => { root = mkTmp(); });
28
+ afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
29
+ test('reads only direct active and archive files in deterministic path and line order', () => {
30
+ write(root, '.awm/ledger/z.jsonl', [entry({ signature: 'z' })]);
31
+ write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'a' })]);
32
+ write(root, '.awm/ledger/archive/b.jsonl', [entry({ signature: 'b' })]);
33
+ write(root, '.awm/ledger/archive/nested/ignored.jsonl', [entry({ signature: 'ignored' })]);
34
+ const scan = (0, scan_1.scanProjectLedgers)(root);
35
+ expect(scan.entries.map(item => item.source)).toEqual([
36
+ '.awm/ledger/a.jsonl:1', '.awm/ledger/archive/b.jsonl:1', '.awm/ledger/z.jsonl:1',
37
+ ]);
38
+ expect(scan.sources).toMatchObject({ activeFiles: 2, archivedFiles: 1, validFindings: 3, skippedFindings: 0 });
39
+ });
40
+ test('orders direct active and archive sources together by their relative path', () => {
41
+ write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'active-b' })]);
42
+ write(root, '.awm/ledger/archive/a.jsonl', [entry({ signature: 'archive-a' })]);
43
+ const scan = (0, scan_1.scanProjectLedgers)(root);
44
+ expect(scan.entries.map(item => item.source)).toEqual([
45
+ '.awm/ledger/archive/a.jsonl:1', '.awm/ledger/b.jsonl:1',
46
+ ]);
47
+ });
48
+ test('counts wins as valid but excludes them from findings', () => {
49
+ write(root, '.awm/ledger/a.jsonl', [entry({ polarity: 'win' }), entry({ signature: 'finding' })]);
50
+ const scan = (0, scan_1.scanProjectLedgers)(root);
51
+ expect(scan.entries.map(item => item.entry.signature)).toEqual(['finding']);
52
+ expect(scan.sources).toMatchObject({ validEntries: 2, validFindings: 1 });
53
+ });
54
+ test.each([
55
+ ['invalid JSON', 'invalid-json', ['{not-json'], {}],
56
+ ['invalid persisted class', 'invalid-defect-class', [{ ...entry(), defectClass: 'Bad_ID' }], {}],
57
+ ['oversized line', 'line-too-large', [JSON.stringify(entry()) + 'x'.repeat(100)], { maxLineBytes: 64 }],
58
+ ])('reports %s as a typed skipped finding', (_name, reason, lines, limits) => {
59
+ write(root, '.awm/ledger/a.jsonl', lines);
60
+ const scan = (0, scan_1.scanProjectLedgers)(root, limits);
61
+ expect(scan.sources.skippedFindings).toBe(1);
62
+ expect(scan.sources.skippedByReason).toMatchObject({ [reason]: 1 });
63
+ });
64
+ test('rejects a symlink without reading its target', () => {
65
+ const outside = path_1.default.join(root, 'outside.jsonl');
66
+ fs_1.default.writeFileSync(outside, JSON.stringify(entry()));
67
+ const ledger = path_1.default.join(root, '.awm', 'ledger');
68
+ fs_1.default.mkdirSync(ledger, { recursive: true });
69
+ fs_1.default.symlinkSync(outside, path_1.default.join(ledger, 'linked.jsonl'));
70
+ const scan = (0, scan_1.scanProjectLedgers)(root);
71
+ expect(scan.entries).toEqual([]);
72
+ expect(scan.sources.skippedByReason).toMatchObject({ 'symlink-entry': 1 });
73
+ });
74
+ test('records direct non-regular entries and never descends into them', () => {
75
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm', 'ledger', 'directory.jsonl'), { recursive: true });
76
+ write(root, '.awm/ledger/directory.jsonl/hidden.jsonl', [entry({ signature: 'hidden' })]);
77
+ const scan = (0, scan_1.scanProjectLedgers)(root);
78
+ expect(scan.entries).toEqual([]);
79
+ expect(scan.sources.skippedByReason).toMatchObject({ 'nonregular-entry': 1 });
80
+ });
81
+ test('reports file and entry limits once without reading candidates beyond the bound', () => {
82
+ write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'a' })]);
83
+ write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'b' })]);
84
+ const files = (0, scan_1.scanProjectLedgers)(root, { maxFiles: 1 });
85
+ expect(files.entries.map(item => item.entry.signature)).toEqual(['a']);
86
+ expect(files.sources.skippedByReason).toMatchObject({ 'file-limit': 1 });
87
+ write(root, '.awm/ledger/entries.jsonl', [entry({ signature: 'first' }), entry({ signature: 'second' })]);
88
+ const entries = (0, scan_1.scanProjectLedgers)(root, { maxEntries: 1, maxFiles: 10 });
89
+ expect(entries.entries).toHaveLength(1);
90
+ expect(entries.sources.skippedByReason).toMatchObject({ 'entry-limit': 1 });
91
+ });
92
+ test('enumerates file candidates with bounded memory and never opens a source after maxFiles', () => {
93
+ write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'first' })]);
94
+ write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'must-not-open' })]);
95
+ write(root, '.awm/ledger/archive/c.jsonl', [entry({ signature: 'also-must-not-open' })]);
96
+ const readSpy = jest.spyOn(fs_1.default, 'readFileSync');
97
+ const readdirSpy = jest.spyOn(fs_1.default, 'readdirSync').mockImplementation(() => {
98
+ throw new Error('unbounded readdir must not be used for ledger candidates');
99
+ });
100
+ try {
101
+ const scan = (0, scan_1.scanProjectLedgers)(root, { maxFiles: 1 });
102
+ expect(scan.entries.map(item => item.entry.signature)).toEqual(['first']);
103
+ expect(scan.sources.skippedByReason).toMatchObject({ 'file-limit': 1 });
104
+ expect(readSpy.mock.calls.map(([candidate]) => String(candidate))).not.toContain(path_1.default.join(root, '.awm', 'ledger', 'b.jsonl'));
105
+ expect(readSpy.mock.calls.map(([candidate]) => String(candidate))).not.toContain(path_1.default.join(root, '.awm', 'ledger', 'archive', 'c.jsonl'));
106
+ }
107
+ finally {
108
+ readdirSpy.mockRestore();
109
+ readSpy.mockRestore();
110
+ }
111
+ });
112
+ test('sorts the retained candidate window instead of assuming host directory order', () => {
113
+ write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'first' })]);
114
+ write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'witness' })]);
115
+ write(root, '.awm/ledger/c.jsonl', [entry({ signature: 'must-not-enumerate' })]);
116
+ const ledgerDirectory = path_1.default.join(root, '.awm', 'ledger');
117
+ const originalOpen = fs_1.default.opendirSync;
118
+ let readCalls = 0;
119
+ const openSpy = jest.spyOn(fs_1.default, 'opendirSync').mockImplementation((directory, options) => {
120
+ if (String(directory) === ledgerDirectory) {
121
+ const names = ['b.jsonl', 'a.jsonl', 'c.jsonl'];
122
+ let index = 0;
123
+ return {
124
+ closeSync: jest.fn(),
125
+ readSync: jest.fn(() => {
126
+ readCalls += 1;
127
+ if (readCalls > 2)
128
+ throw new Error('candidate enumeration exceeded bounded window');
129
+ const name = names[index++];
130
+ return name === undefined ? null : { name };
131
+ }),
132
+ };
133
+ }
134
+ const handle = originalOpen(directory, options);
135
+ const originalRead = handle.readSync.bind(handle);
136
+ jest.spyOn(handle, 'readSync').mockImplementation(() => {
137
+ readCalls += 1;
138
+ if (readCalls > 2)
139
+ throw new Error('candidate enumeration exceeded bounded window');
140
+ return originalRead();
141
+ });
142
+ return handle;
143
+ });
144
+ try {
145
+ const scan = (0, scan_1.scanProjectLedgers)(root, { maxFiles: 1 });
146
+ expect(scan.entries.map(item => item.entry.signature)).toEqual(['first']);
147
+ expect(scan.sources.skippedByReason).toMatchObject({ 'file-limit': 1 });
148
+ expect(readCalls).toBe(2);
149
+ }
150
+ finally {
151
+ openSpy.mockRestore();
152
+ }
153
+ });
154
+ test('shares the candidate window across active and archive directories', () => {
155
+ write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'active-first' })]);
156
+ write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'active-witness' })]);
157
+ write(root, '.awm/ledger/archive/c.jsonl', [entry({ signature: 'archive-must-not-enumerate' })]);
158
+ const originalOpen = fs_1.default.opendirSync;
159
+ let candidateReads = 0;
160
+ const openSpy = jest.spyOn(fs_1.default, 'opendirSync').mockImplementation((directory, options) => {
161
+ const handle = originalOpen(directory, options);
162
+ const originalRead = handle.readSync.bind(handle);
163
+ jest.spyOn(handle, 'readSync').mockImplementation(() => {
164
+ const item = originalRead();
165
+ if (item?.name.endsWith('.jsonl')) {
166
+ candidateReads += 1;
167
+ if (candidateReads > 2)
168
+ throw new Error('candidate enumeration exceeded the global bounded window');
169
+ }
170
+ return item;
171
+ });
172
+ return handle;
173
+ });
174
+ try {
175
+ const scan = (0, scan_1.scanProjectLedgers)(root, { maxFiles: 1 });
176
+ expect(scan.entries.map(item => item.entry.signature)).toEqual(['active-first']);
177
+ expect(scan.sources.skippedByReason).toMatchObject({ 'file-limit': 1 });
178
+ expect(candidateReads).toBe(2);
179
+ }
180
+ finally {
181
+ openSpy.mockRestore();
182
+ }
183
+ });
184
+ test('reports a too-large file without parsing it', () => {
185
+ write(root, '.awm/ledger/a.jsonl', [entry()]);
186
+ const scan = (0, scan_1.scanProjectLedgers)(root, { maxFileBytes: 1 });
187
+ expect(scan.entries).toEqual([]);
188
+ expect(scan.sources.skippedByReason).toMatchObject({ 'file-too-large': 1 });
189
+ });
190
+ test('reports over-deep JSON before parsing it', () => {
191
+ write(root, '.awm/ledger/a.jsonl', ['{"x":{"x":{"x":{"x":{"x":1}}}}}']);
192
+ const scan = (0, scan_1.scanProjectLedgers)(root, { maxJsonDepth: 3 });
193
+ expect(scan.entries).toEqual([]);
194
+ expect(scan.sources.skippedByReason).toMatchObject({ 'json-too-deep': 1 });
195
+ });
196
+ test('keeps every finding for analysis while bounding renderable references per defect class', () => {
197
+ write(root, '.awm/ledger/a.jsonl', [
198
+ entry({ signature: 'first', defectClass: 'lint-errors' }),
199
+ entry({ signature: 'second', defectClass: 'lint-errors' }),
200
+ entry({ signature: 'other', defectClass: 'static-type-errors' }),
201
+ ]);
202
+ const scan = (0, scan_1.scanProjectLedgers)(root, { maxRefsPerClass: 1 });
203
+ expect(scan.entries.map(item => item.entry.signature)).toEqual(['first', 'second', 'other']);
204
+ expect(scan.entries.map(item => item.evidenceRef)).toEqual([
205
+ '.awm/ledger/a.jsonl:1', null, '.awm/ledger/a.jsonl:3',
206
+ ]);
207
+ expect(scan.omittedEvidenceRefs).toBe(1);
208
+ expect(scan.sources).toMatchObject({
209
+ validFindings: 3,
210
+ skippedFindings: 1,
211
+ skippedByReason: { 'evidence-ref-limit': 1 },
212
+ });
213
+ });
214
+ test('rejects non-object limit overrides instead of accepting an impossible public input', () => {
215
+ expect(() => (0, scan_1.scanProjectLedgers)(root, null)).toThrow(/limits.*object/i);
216
+ });
217
+ });