agentic-workflow-manager 9.4.1 → 9.6.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 (57) hide show
  1. package/dist/src/commands/preflight/checks.js +58 -27
  2. package/dist/src/commands/sensors/bootstrap.js +171 -0
  3. package/dist/src/commands/sensors/compatibility/contract.js +18 -15
  4. package/dist/src/commands/sensors/compatibility/manifest.js +75 -2
  5. package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
  6. package/dist/src/commands/sensors/compatibility/pack-source.js +61 -17
  7. package/dist/src/commands/sensors/compatibility/safe-file.js +286 -0
  8. package/dist/src/commands/sensors/compatibility/source.js +75 -0
  9. package/dist/src/commands/sensors/coverage/index.js +2 -2
  10. package/dist/src/commands/sensors/coverage/resolve.js +35 -29
  11. package/dist/src/commands/sensors/detection.js +31 -0
  12. package/dist/src/commands/sensors/exec.js +116 -11
  13. package/dist/src/commands/sensors/index.js +51 -10
  14. package/dist/src/commands/sensors/init.js +17 -168
  15. package/dist/src/commands/sensors/migrate.js +209 -0
  16. package/dist/src/commands/sensors/project.js +107 -0
  17. package/dist/src/commands/sensors/run.js +66 -47
  18. package/dist/src/commands/sensors/status.js +97 -56
  19. package/dist/src/core/init/steps.js +24 -11
  20. package/dist/src/core/secure-fs/native-bridge.js +209 -0
  21. package/dist/tests/commands/preflight/preflight.test.js +31 -0
  22. package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
  23. package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
  24. package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
  25. package/dist/tests/commands/sensors/compatibility/manifest.test.js +90 -0
  26. package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
  27. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +249 -0
  28. package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
  29. package/dist/tests/commands/sensors/compatibility/source.test.js +121 -0
  30. package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
  31. package/dist/tests/commands/sensors/coverage/resolve.test.js +36 -34
  32. package/dist/tests/commands/sensors/exec.test.js +86 -0
  33. package/dist/tests/commands/sensors/index.test.js +54 -0
  34. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
  35. package/dist/tests/commands/sensors/init.test.js +56 -490
  36. package/dist/tests/commands/sensors/migrate.test.js +513 -0
  37. package/dist/tests/commands/sensors/project.test.js +102 -0
  38. package/dist/tests/commands/sensors/run.test.js +107 -4
  39. package/dist/tests/commands/sensors/status.test.js +106 -2
  40. package/dist/tests/core/init/orchestrator.test.js +3 -1
  41. package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
  42. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +5 -0
  43. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
  44. package/dist/tests/integration/sensor-compatibility.e2e.test.js +14 -23
  45. package/dist/tests/integration/sensor-coverage.e2e.test.js +15 -24
  46. package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
  47. package/dist/tests/structural/native-release-artifacts.test.js +22 -0
  48. package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
  49. package/dist/tests/structural/release-registry-provenance.test.js +4 -0
  50. package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
  51. package/package.json +6 -2
  52. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  53. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  54. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  55. package/prebuilds/linux-x64/secure_fs.node +0 -0
  56. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  57. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -7,12 +7,42 @@ exports.runStructuredCommand = runStructuredCommand;
7
7
  exports.runCommand = runCommand;
8
8
  const child_process_1 = require("child_process");
9
9
  const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  const paths_1 = require("../../core/paths");
12
13
  const DEFAULT_MAX_BUFFER = 64 * 1024 * 1024;
13
14
  const DEFAULT_KILL_GRACE_MS = 2_000;
14
15
  /** After SIGKILL, resolve regardless. A sensor must never hang the gate. */
15
16
  const POST_KILL_GRACE_MS = 1_000;
17
+ const PRLIMIT_PATHS = ['/usr/bin/prlimit', '/bin/prlimit'];
18
+ /** Decode a byte-bounded capture without allowing replacement characters to
19
+ * expand it. Invalid input bytes become one-byte `?` markers. */
20
+ function decodeBoundedOutput(buffer) {
21
+ const decoded = Buffer.allocUnsafe(buffer.length);
22
+ let read = 0;
23
+ let written = 0;
24
+ const continuation = (index) => index < buffer.length && (buffer[index] & 0b1100_0000) === 0b1000_0000;
25
+ while (read < buffer.length) {
26
+ const first = buffer[read];
27
+ let length = 1;
28
+ if (first >= 0xc2 && first <= 0xdf && continuation(read + 1))
29
+ length = 2;
30
+ else if (first >= 0xe0 && first <= 0xef && continuation(read + 1) && continuation(read + 2)
31
+ && !(first === 0xe0 && buffer[read + 1] < 0xa0) && !(first === 0xed && buffer[read + 1] > 0x9f))
32
+ length = 3;
33
+ else if (first >= 0xf0 && first <= 0xf4 && continuation(read + 1) && continuation(read + 2) && continuation(read + 3)
34
+ && !(first === 0xf0 && buffer[read + 1] < 0x90) && !(first === 0xf4 && buffer[read + 1] > 0x8f))
35
+ length = 4;
36
+ else if (first >= 0x80)
37
+ decoded[written++] = 0x3f;
38
+ if (length > 1 || first < 0x80) {
39
+ buffer.copy(decoded, written, read, read + length);
40
+ written += length;
41
+ }
42
+ read += length;
43
+ }
44
+ return decoded.subarray(0, written).toString('utf8');
45
+ }
16
46
  /**
17
47
  * Kill an entire process tree, not just its root.
18
48
  *
@@ -76,10 +106,59 @@ function collectSpawn(input, opts) {
76
106
  const startedAt = process.hrtime.bigint();
77
107
  let stdout = '';
78
108
  let stderr = '';
109
+ const stdoutChunks = [];
110
+ const stderrChunks = [];
111
+ let capturedBytes = 0;
79
112
  let timedOut = false;
80
113
  let overflowed = false;
81
114
  let settled = false;
82
115
  const timers = [];
116
+ const prlimit = !input.shell && process.platform === 'linux'
117
+ ? PRLIMIT_PATHS.find(candidate => regularFile(candidate))
118
+ : undefined;
119
+ const streamMaxBuffer = maxBuffer;
120
+ // Some structured Node CLIs call process.exit() after writing their
121
+ // report. Node drops an asynchronous pipe write in that case, but a
122
+ // regular file descriptor is flushed before exit. Keep the report on
123
+ // disk only until settlement. Each channel is hard-capped at maxBuffer
124
+ // while a shared decode budget caps the returned combined output.
125
+ // Linux enforces the per-file cap before either child stream can grow
126
+ // unbounded; polling alone can only discover excess after it happens.
127
+ const captureDir = prlimit ? fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensor-output-')) : undefined;
128
+ const stdoutPath = captureDir ? path_1.default.join(captureDir, 'stdout') : undefined;
129
+ const stderrPath = captureDir ? path_1.default.join(captureDir, 'stderr') : undefined;
130
+ const stdoutFd = stdoutPath ? fs_1.default.openSync(stdoutPath, 'w') : undefined;
131
+ const stderrFd = stderrPath ? fs_1.default.openSync(stderrPath, 'w') : undefined;
132
+ const readCaptured = (file, fd, budget) => {
133
+ fs_1.default.closeSync(fd);
134
+ const size = fs_1.default.statSync(file).size;
135
+ if (size >= streamMaxBuffer)
136
+ overflowed = true;
137
+ if (size > budget)
138
+ overflowed = true;
139
+ const bytes = Math.min(size, budget);
140
+ const buffer = Buffer.allocUnsafe(bytes);
141
+ if (buffer.length > 0) {
142
+ const readFd = fs_1.default.openSync(file, 'r');
143
+ try {
144
+ fs_1.default.readSync(readFd, buffer, 0, buffer.length, 0);
145
+ }
146
+ finally {
147
+ fs_1.default.closeSync(readFd);
148
+ }
149
+ }
150
+ return { output: decodeBoundedOutput(buffer), bytes };
151
+ };
152
+ const captureSize = (file) => {
153
+ if (!file)
154
+ return 0;
155
+ try {
156
+ return fs_1.default.statSync(file).size;
157
+ }
158
+ catch {
159
+ return 0;
160
+ }
161
+ };
83
162
  const child = input.shell
84
163
  ? (0, child_process_1.spawn)(input.executable, {
85
164
  shell: true,
@@ -87,14 +166,16 @@ function collectSpawn(input, opts) {
87
166
  detached: !(0, paths_1.isWindowsNative)(),
88
167
  stdio: ['ignore', 'pipe', 'pipe'],
89
168
  })
90
- : (0, child_process_1.spawn)(input.executable, input.args, {
169
+ : (0, child_process_1.spawn)(prlimit ?? input.executable, prlimit
170
+ ? [`--fsize=${streamMaxBuffer}`, '--', input.executable, ...input.args]
171
+ : input.args, {
91
172
  shell: false,
92
173
  cwd: opts.cwd,
93
174
  detached: !(0, paths_1.isWindowsNative)(),
94
175
  ...(input.environment ? { env: { ...process.env, ...input.environment } } : {}),
95
176
  // stdin closed: a sensor must never block waiting for input, and the
96
177
  // EOF also tells watch-mode-capable tools (vitest, jest) to run once.
97
- stdio: ['ignore', 'pipe', 'pipe'],
178
+ stdio: prlimit ? ['ignore', stdoutFd, stderrFd] : ['ignore', 'pipe', 'pipe'],
98
179
  });
99
180
  const later = (fn, ms) => {
100
181
  const t = setTimeout(fn, ms);
@@ -107,6 +188,20 @@ function collectSpawn(input, opts) {
107
188
  return;
108
189
  settled = true;
109
190
  timers.forEach(clearTimeout);
191
+ if (stdoutPath && stderrPath && stdoutFd !== undefined && stderrFd !== undefined) {
192
+ try {
193
+ const capturedStdout = readCaptured(stdoutPath, stdoutFd, maxBuffer);
194
+ stdout = capturedStdout.output;
195
+ stderr = readCaptured(stderrPath, stderrFd, maxBuffer - capturedStdout.bytes).output;
196
+ }
197
+ finally {
198
+ fs_1.default.rmSync(captureDir, { recursive: true, force: true });
199
+ }
200
+ }
201
+ else {
202
+ stdout = decodeBoundedOutput(Buffer.concat(stdoutChunks));
203
+ stderr = decodeBoundedOutput(Buffer.concat(stderrChunks));
204
+ }
110
205
  const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1000000n);
111
206
  resolve({ stdout, stderr, code: null, signal: null, timedOut, overflowed, elapsedMs, ...extra });
112
207
  };
@@ -124,15 +219,13 @@ function collectSpawn(input, opts) {
124
219
  const collect = (into) => (chunk) => {
125
220
  if (settled || overflowed)
126
221
  return;
127
- const text = String(chunk);
128
- const current = into === 'out' ? stdout : stderr;
129
- const room = maxBuffer - current.length;
130
- const next = current + (text.length > room ? text.slice(0, room) : text);
131
- if (into === 'out')
132
- stdout = next;
133
- else
134
- stderr = next;
135
- if (next.length >= maxBuffer) {
222
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
223
+ const room = maxBuffer - capturedBytes;
224
+ const kept = bytes.subarray(0, Math.max(0, room));
225
+ if (kept.length > 0)
226
+ (into === 'out' ? stdoutChunks : stderrChunks).push(kept);
227
+ capturedBytes += kept.length;
228
+ if (bytes.length > kept.length || capturedBytes >= maxBuffer) {
136
229
  overflowed = true;
137
230
  cutShort();
138
231
  }
@@ -143,6 +236,18 @@ function collectSpawn(input, opts) {
143
236
  child.stderr?.on('error', () => { });
144
237
  child.on('error', (err) => finish({ spawnError: err }));
145
238
  child.on('close', (code, signal) => finish({ code, signal }));
239
+ if (stdoutPath || stderrPath) {
240
+ const outputWatch = setInterval(() => {
241
+ const stdoutSize = captureSize(stdoutPath);
242
+ const stderrSize = captureSize(stderrPath);
243
+ if (!overflowed && (stdoutSize >= streamMaxBuffer || stderrSize >= streamMaxBuffer || stdoutSize + stderrSize >= maxBuffer)) {
244
+ overflowed = true;
245
+ cutShort();
246
+ }
247
+ }, 25);
248
+ outputWatch.unref?.();
249
+ timers.push(outputWatch);
250
+ }
146
251
  later(() => { timedOut = true; cutShort(); }, opts.timeout);
147
252
  });
148
253
  }
@@ -8,7 +8,6 @@ exports.registerSensorsCommand = registerSensorsCommand;
8
8
  const picocolors_1 = __importDefault(require("picocolors"));
9
9
  const prompts_1 = require("@clack/prompts");
10
10
  const run_1 = require("./run");
11
- const init_1 = require("./init");
12
11
  const status_1 = require("./status");
13
12
  const install_1 = require("./install");
14
13
  const baseline_1 = require("./baseline");
@@ -16,6 +15,7 @@ const coverage_1 = require("./coverage");
16
15
  const render_1 = require("./coverage/render");
17
16
  const registries_1 = require("../../core/registries");
18
17
  const verdict_1 = require("./verdict");
18
+ const bootstrap_1 = require("./bootstrap");
19
19
  /** Commander coercion for coverage recurrence emphasis. It deliberately runs
20
20
  * before the action, so an invalid value cannot trigger ledger I/O. */
21
21
  function parsePositiveSafeInteger(value) {
@@ -71,23 +71,64 @@ function registerSensorsCommand(program) {
71
71
  .action(async (opts) => {
72
72
  const registryRoot = opts.registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs') ?? undefined;
73
73
  try {
74
- const result = await (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack, packageRoot: opts.packageRoot });
75
- prompts_1.log.success(`Detected: ${result.detection.pack} (${result.detection.indicators.join(', ') || 'fallback'})`);
74
+ const plan = await (0, bootstrap_1.planSensorBootstrap)(process.cwd(), { mode: 'project-sensors', registryRoot, configure: opts.configure, pack: opts.pack, packageRoot: opts.packageRoot });
75
+ if (plan.kind === 'blocked')
76
+ throw new Error(`${plan.reason}: ${plan.remedy}`);
77
+ if (plan.kind === 'noop') {
78
+ prompts_1.log.info('already-configured');
79
+ return;
80
+ }
81
+ if (plan.kind === 'migrate')
82
+ throw new Error('sensors init does not migrate an existing v2 manifest; run awm sensors bootstrap');
83
+ const result = (0, bootstrap_1.applySensorBootstrap)(plan);
84
+ const detected = plan.manifest.mode === 'project-sensors' ? plan.manifest.pack : 'none';
85
+ prompts_1.log.success(`Detected: ${detected}`);
76
86
  // Said BEFORE "Wrote .awm/sensors.json": the manifest about to be
77
87
  // reported as written is not the one the detection implied.
78
- if (result.unavailablePack) {
79
- prompts_1.log.warn(`No '${result.unavailablePack}' sensor-pack in the registry — wrote the `
80
- + `'${result.manifest.pack}' pack instead (${Object.keys(result.manifest.sensors).length} sensors). `
81
- + 'Run `awm update`, or add a registry that ships it, then re-run `awm sensors init`.');
82
- }
83
- prompts_1.log.success('Wrote .awm/sensors.json');
84
- result.configured.forEach((f) => prompts_1.log.info(` Installed ${f}`));
88
+ prompts_1.log.success(`${result} .awm/sensors.json`);
85
89
  }
86
90
  catch (e) {
87
91
  prompts_1.log.error(e instanceof Error ? e.message : String(e));
88
92
  process.exit(1);
89
93
  }
90
94
  });
95
+ sensors
96
+ .command('bootstrap')
97
+ .description('explicitly create or migrate portable project sensor configuration')
98
+ .option('--mode <mode>', 'project-sensors, native-gate, or opt-out')
99
+ .option('--reason <text>', 'versioned reason required by native-gate and opt-out')
100
+ .option('--dry-run', 'report exact project changes without writing files')
101
+ .action(async (opts) => {
102
+ try {
103
+ const plan = await (0, bootstrap_1.planSensorBootstrap)(process.cwd(), { mode: opts.mode, reason: opts.reason, dryRun: opts.dryRun });
104
+ if (plan.kind === 'blocked') {
105
+ prompts_1.log.error(`${plan.reason}: ${plan.remedy}`);
106
+ process.exitCode = 1;
107
+ return;
108
+ }
109
+ if (plan.kind === 'noop') {
110
+ prompts_1.log.info('already-configured');
111
+ return;
112
+ }
113
+ const summary = plan.changes.map(change => change.path).join(', ');
114
+ if (plan.dryRun) {
115
+ const pack = plan.kind === 'create' && plan.manifest.mode === 'project-sensors'
116
+ ? `; pack=${plan.manifest.pack}`
117
+ : '';
118
+ prompts_1.log.info(`dry-run: ${plan.kind}${pack}; ${summary}`);
119
+ return;
120
+ }
121
+ const result = (0, bootstrap_1.applySensorBootstrap)(plan);
122
+ const pack = plan.kind === 'create' && plan.manifest.mode === 'project-sensors'
123
+ ? `; pack=${plan.manifest.pack}`
124
+ : '';
125
+ prompts_1.log.success(`${result}${pack}: ${summary}`);
126
+ }
127
+ catch (error) {
128
+ prompts_1.log.error(error instanceof Error ? error.message : String(error));
129
+ process.exitCode = 1;
130
+ }
131
+ });
91
132
  sensors
92
133
  .command('baseline')
93
134
  .description('snapshot current findings as accepted — sensors then fail only on NEW ones')
@@ -9,11 +9,8 @@ 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
12
  const pack_source_1 = require("./compatibility/pack-source");
13
+ const bootstrap_1 = require("./bootstrap");
17
14
  /** Read a pack only through the shared source resolver. Init is a registry
18
15
  * boundary: a pack path that is absent is harmless, but a symlink or escaping
19
16
  * claimed source is never silently downgraded to an empty manifest. */
@@ -30,20 +27,6 @@ function readResolvedPack(pack, registryRoot) {
30
27
  throw error;
31
28
  }
32
29
  }
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
- }
47
30
  const STACK_DETECTORS = [
48
31
  { pack: 'js-ts', files: ['package.json'] },
49
32
  { pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'] },
@@ -165,157 +148,23 @@ function availablePacks(registryRoot) {
165
148
  .map(e => e.name)
166
149
  .sort();
167
150
  }
168
- /** The last-resort pack, used when the detected one is absent from the registry. */
169
- const FALLBACK_PACK = 'generic';
170
- /**
171
- * Resolve the detected pack against what the registry actually ships.
172
- *
173
- * `--pack <name>` has always thrown here (`assertPackExists`), but auto-detection had
174
- * no such check: it wrote `{"pack":"python","sensors":{}}` and said nothing, so the
175
- * operator learned the gate was empty from an unrelated command days later. The two
176
- * paths now reach the same conclusion; only the remedy differs, because an explicit
177
- * `--pack` is a typo to correct while a detection is a fact about the tree that the
178
- * registry simply cannot serve yet.
179
- *
180
- * Falling back to `generic` keeps the gate measuring *something* real rather than
181
- * nothing. When the registry has no `generic` either, the detected pack is kept: the
182
- * manifest is then honestly empty, and preflight's `manifest`/`tools` checks both fail
183
- * on `total === 0` with a remedy pointing at the registry.
184
- */
185
- function resolvePack(detected, registryRoot) {
186
- if (!registryRoot)
187
- return { pack: detected }; // nothing to validate against
188
- const available = availablePacks(registryRoot);
189
- if (available.length === 0 || available.includes(detected))
190
- return { pack: detected };
191
- return {
192
- pack: available.includes(FALLBACK_PACK) ? FALLBACK_PACK : detected,
193
- unavailablePack: detected,
194
- };
195
- }
196
- /**
197
- * Validate that `pack` exists as a directory under `<registryRoot>/sensor-packs/`.
198
- * Throws (not a swallow-and-return) so `awm sensors init --pack bogus` actually stops
199
- * instead of silently writing a manifest for a pack that doesn't exist. Lists every
200
- * pack directory actually present, sorted, so the user immediately sees valid options.
201
- */
202
- function assertPackExists(pack, registryRoot) {
203
- const packsDir = path_1.default.join(registryRoot, 'sensor-packs');
204
- if (!fs_1.default.existsSync(packsDir) || !fs_1.default.statSync(packsDir).isDirectory()) {
205
- throw new Error('registry has no sensor-packs directory');
206
- }
207
- const available = availablePacks(registryRoot);
208
- if (!available.includes(pack)) {
209
- throw new Error(`pack '${pack}' not found in registry (available: ${available.join(', ')})`);
210
- }
211
- }
212
151
  async function initSensors(opts = {}) {
213
152
  const cwd = opts.cwd ?? process.cwd();
214
- const configure = opts.configure ?? true; // configure (copy pack config files) by default
215
- const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
216
- // Monorepo support: detection and evidence both need to see the real project,
217
- // not the (possibly unrelated) directory the manifest lives in. The manifest
218
- // write itself stays pinned to cwd regardless — see manifestPath above.
219
153
  const detectionCwd = opts.packageRoot ? path_1.default.resolve(cwd, opts.packageRoot) : cwd;
220
- // --pack skips the heuristic entirely. Only validate against the registry when a
221
- // registryRoot was actually given — same tolerance pattern as readPackDefaults /
222
- // buildManifest elsewhere in this file for a missing registry: nothing to validate
223
- // against, so nothing is validated.
224
- let detection;
225
- if (opts.pack) {
226
- if (opts.registryRoot)
227
- assertPackExists(opts.pack, opts.registryRoot);
228
- detection = { pack: opts.pack, indicators: ['--pack override'] };
229
- }
230
- else {
231
- detection = detectStack(detectionCwd);
232
- }
233
- let existing;
234
- let existingV2;
235
- if (fs_1.default.existsSync(manifestPath)) {
236
- let raw;
237
- try {
238
- raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
239
- }
240
- catch {
241
- throw new Error(`cannot parse existing sensor manifest ${manifestPath}`);
242
- }
243
- // Never overwrite a corrupt or future-version manifest during init. Legacy
244
- // manifests remain a supported migration input; v2 is validated before a new
245
- // materialized selection becomes the commit point.
246
- const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
247
- if (parsed.kind === 'legacy')
248
- existing = parsed.pack;
249
- else
250
- existingV2 = parsed.pack;
251
- }
252
- // `detection` keeps saying what the tree IS; `pack` is what the registry can serve
253
- // for it. They diverge only when the registry lacks the detected pack.
254
- const { pack: resolvedPack, unavailablePack } = resolvePack(detection.pack, opts.registryRoot);
255
- // v2 packs own the executable contract. Legacy packs deliberately retain the
256
- // old string-command path below until a registry author publishes a v2 contract.
257
- if (opts.registryRoot) {
258
- const resolvedV2 = readV2Pack(resolvedPack, opts.registryRoot);
259
- if (resolvedV2) {
260
- const packSelection = opts.pack ? 'explicit' : undefined;
261
- const live = await (0, live_1.resolveParsedPackCompatibility)(detectionCwd, resolvedV2.pack, { packSelection });
262
- const compatibility = live.sensors;
263
- const sensors = {};
264
- for (const [name, sensor] of Object.entries(live.pack.sensors)) {
265
- const resolved = compatibility[name];
266
- const variant = resolved.variantId === null ? null : sensor.variants.find(candidate => candidate.id === resolved.variantId) ?? null;
267
- // Unresolved states do not receive an arbitrary command. They remain
268
- // represented in the manifest so status/coverage can explain them,
269
- // but run cannot accidentally dispatch a mismatched executable.
270
- if (variant) {
271
- const prior = existing?.sensors[name] ?? existingV2?.sensors[name];
272
- // Legacy string commands cannot be translated into a shell-free
273
- // structured command without guessing. Preserve the operator's
274
- // enabled/fast intent but make that migration non-certified.
275
- const migratedOverride = existing?.sensors[name]?.cmd !== undefined;
276
- sensors[name] = {
277
- enabled: prior?.enabled ?? true,
278
- fast: prior?.fast ?? sensor.fast ?? false,
279
- ...(prior?.timeout !== undefined ? { timeout: prior.timeout } : {}),
280
- variantId: variant.id,
281
- command: variant.command,
282
- assets: variant.assets,
283
- ...(variant.policyRef ? { policyRef: variant.policyRef } : {}),
284
- initializedCompatibility: migratedOverride
285
- ? { ...resolved, state: 'compatible-unverified', reason: 'legacy custom command requires explicit v2 migration' }
286
- : resolved,
287
- };
288
- }
289
- }
290
- const materialized = (0, materialize_1.materializeResolvedSensors)({
291
- projectRoot: cwd, packRoot: resolvedV2.packRoot,
292
- pack: resolvedPack, ...(packSelection ? { packSelection } : {}), registryRoot: opts.registryRoot,
293
- ...(opts.packageRoot ? { packageRoot: opts.packageRoot } : {}), sensors, configure,
294
- });
295
- return { detection, ...materialized,
296
- compatibility, ...(unavailablePack ? { unavailablePack } : {}) };
297
- }
298
- }
299
- const manifest = buildManifest(resolvedPack, existing, opts.registryRoot, cwd);
300
- fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
301
- const tmpPath = manifestPath + '.tmp';
302
- fs_1.default.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2), 'utf-8');
303
- fs_1.default.renameSync(tmpPath, manifestPath);
304
- const configured = [];
305
- if (configure && opts.registryRoot) {
306
- // The pack whose defaults the manifest was actually built from — copying the
307
- // detected pack's config files here would drop files for sensors that are not
308
- // in the manifest, and miss the ones that are.
309
- const packDir = path_1.default.join(opts.registryRoot, 'sensor-packs', resolvedPack);
310
- if (fs_1.default.existsSync(packDir)) {
311
- for (const file of fs_1.default.readdirSync(packDir).filter(f => f !== 'pack.json')) {
312
- const dst = path_1.default.join(cwd, file);
313
- if (!fs_1.default.existsSync(dst)) {
314
- fs_1.default.copyFileSync(path_1.default.join(packDir, file), dst);
315
- configured.push(file);
316
- }
317
- }
318
- }
319
- }
320
- return { manifest, detection, configured, ...(unavailablePack ? { unavailablePack } : {}) };
154
+ const plan = await (0, bootstrap_1.planSensorBootstrap)(cwd, {
155
+ mode: 'project-sensors', registryRoot: opts.registryRoot, configure: opts.configure,
156
+ pack: opts.pack, packageRoot: opts.packageRoot,
157
+ });
158
+ if (plan.kind === 'blocked')
159
+ throw new Error(`${plan.reason}: ${plan.remedy}`);
160
+ if (plan.kind === 'migrate')
161
+ throw new Error('initSensors does not migrate existing v2 manifests; run awm sensors bootstrap');
162
+ if (plan.kind === 'noop')
163
+ return {
164
+ // No manifest is written or synthesized here. The compatibility API only
165
+ // retains its historical result shape for callers that inspect detection.
166
+ manifest: {}, detection: detectStack(detectionCwd), configured: [], status: 'already-configured',
167
+ };
168
+ (0, bootstrap_1.applySensorBootstrap)(plan);
169
+ return { manifest: plan.manifest, detection: detectStack(detectionCwd), configured: [], status: 'created' };
321
170
  }