agentic-workflow-manager 9.16.1 → 9.18.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 (54) hide show
  1. package/dist/src/commands/env/index.js +192 -0
  2. package/dist/src/commands/preflight/checks.js +63 -13
  3. package/dist/src/commands/registry/doctor.js +428 -0
  4. package/dist/src/commands/registry/index.js +52 -0
  5. package/dist/src/commands/registry/scaffold.js +140 -0
  6. package/dist/src/commands/sensors/bootstrap.js +2 -1
  7. package/dist/src/commands/sensors/compatibility/contract.js +30 -26
  8. package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
  9. package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
  10. package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
  11. package/dist/src/commands/sensors/compatibility/source.js +3 -2
  12. package/dist/src/commands/sensors/coverage/render.js +8 -2
  13. package/dist/src/commands/sensors/coverage/resolve.js +7 -0
  14. package/dist/src/commands/sensors/init.js +15 -2
  15. package/dist/src/commands/sensors/migrate.js +13 -10
  16. package/dist/src/commands/sensors/prepare.js +53 -6
  17. package/dist/src/commands/sensors/project.js +60 -6
  18. package/dist/src/commands/sensors/result.js +18 -3
  19. package/dist/src/commands/sensors/run.js +124 -16
  20. package/dist/src/commands/sensors/status.js +163 -18
  21. package/dist/src/commands/watch/index.js +3 -0
  22. package/dist/src/core/environments/resolve.js +122 -0
  23. package/dist/src/core/environments/store.js +323 -0
  24. package/dist/src/core/facts/catalog.js +138 -0
  25. package/dist/src/core/facts/needs.js +57 -0
  26. package/dist/src/core/facts/types.js +21 -0
  27. package/dist/src/core/init/steps.js +5 -1
  28. package/dist/src/index.js +2 -0
  29. package/dist/tests/commands/env/index.test.js +261 -0
  30. package/dist/tests/commands/preflight/preflight.test.js +121 -0
  31. package/dist/tests/commands/registry/doctor.test.js +283 -0
  32. package/dist/tests/commands/registry/scaffold.test.js +186 -0
  33. package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
  34. package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
  35. package/dist/tests/commands/sensors/init.test.js +107 -0
  36. package/dist/tests/commands/sensors/migrate.test.js +4 -3
  37. package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
  38. package/dist/tests/commands/sensors/prepare.test.js +62 -0
  39. package/dist/tests/commands/sensors/result.test.js +66 -0
  40. package/dist/tests/commands/sensors/run.test.js +331 -0
  41. package/dist/tests/commands/sensors/status.test.js +204 -0
  42. package/dist/tests/commands/watch/reopened-196.test.js +9 -2
  43. package/dist/tests/core/environments/resolve.test.js +347 -0
  44. package/dist/tests/core/environments/store.test.js +248 -0
  45. package/dist/tests/core/facts/catalog.test.js +179 -0
  46. package/dist/tests/core/facts/needs.test.js +88 -0
  47. package/dist/tests/structural/native-release-artifacts.test.js +4 -2
  48. package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
  49. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -0
  50. package/package.json +1 -1
  51. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  52. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  53. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  54. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -137,61 +137,65 @@ function resolveSemgrepPolicy(policyRef, source, location = 'policy') {
137
137
  }
138
138
  return parseSemgrepPolicy(parsed, candidate);
139
139
  }
140
- function parseStructuredCommand(input, source) {
141
- const value = record(input, source, 'command');
142
- fields(value, ['executable', 'resolution', 'args', 'packageManager', 'environment', 'fileInput', 'pythonEnvironmentRoot'], source, 'command');
143
- const executable = text(value.executable, source, 'command.executable');
140
+ function parseStructuredCommand(input, source, location = 'command') {
141
+ const value = record(input, source, location);
142
+ fields(value, ['executable', 'resolution', 'args', 'packageManager', 'environment', 'fileInput', 'pythonEnvironmentRoot'], source, location);
143
+ const executable = text(value.executable, source, `${location}.executable`);
144
144
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(executable) || SHELL_EXECUTABLES.has(executable.toLowerCase().replace(/\.exe$/, ''))) {
145
- invalid(source, 'command.executable must not be a shell or path');
145
+ invalid(source, `${location}.executable must not be a shell or path`);
146
146
  }
147
147
  if (value.resolution !== 'node-modules-bin' && value.resolution !== 'python-environment' && value.resolution !== 'path') {
148
- invalid(source, 'command.resolution must be a supported resolution');
148
+ invalid(source, `${location}.resolution must be a supported resolution`);
149
149
  }
150
- const args = stringArray(value.args, source, 'command.args');
150
+ const args = stringArray(value.args, source, `${location}.args`);
151
151
  for (const [index, arg] of args.entries()) {
152
152
  if (arg.includes('{files}') && arg !== '{files}')
153
- invalid(source, `command.args[${index}] must not embed {files}`);
153
+ invalid(source, `${location}.args[${index}] must not embed {files}`);
154
154
  }
155
155
  const command = { executable, resolution: value.resolution, args };
156
156
  if ('pythonEnvironmentRoot' in value) {
157
- if (value.resolution !== 'python-environment' || (value.pythonEnvironmentRoot !== '.venv' && value.pythonEnvironmentRoot !== 'venv'))
158
- invalid(source, 'command.pythonEnvironmentRoot must name the selected .venv or venv Python environment');
157
+ if (value.resolution !== 'python-environment' || (value.pythonEnvironmentRoot !== '.venv' && value.pythonEnvironmentRoot !== 'venv')) {
158
+ invalid(source, `${location}.pythonEnvironmentRoot must name the selected .venv or venv Python environment`);
159
+ }
159
160
  command.pythonEnvironmentRoot = value.pythonEnvironmentRoot;
160
161
  }
161
162
  const executablePackageManager = normalizedPackageManager(executable);
162
- if (PACKAGE_MANAGERS.has(executablePackageManager) && !('packageManager' in value))
163
- invalid(source, 'command.packageManager is required for a package-manager executable');
163
+ if (PACKAGE_MANAGERS.has(executablePackageManager) && !('packageManager' in value)) {
164
+ invalid(source, `${location}.packageManager is required for a package-manager executable`);
165
+ }
164
166
  if ('packageManager' in value) {
165
- if (typeof value.packageManager !== 'string' || !PACKAGE_MANAGERS.has(normalizedPackageManager(value.packageManager)))
166
- invalid(source, 'command.packageManager must be npm, pnpm, yarn, or bun');
167
+ if (typeof value.packageManager !== 'string' || !PACKAGE_MANAGERS.has(normalizedPackageManager(value.packageManager))) {
168
+ invalid(source, `${location}.packageManager must be npm, pnpm, yarn, or bun`);
169
+ }
167
170
  const packageManager = normalizedPackageManager(value.packageManager);
168
171
  if (packageManager !== executablePackageManager)
169
- invalid(source, 'command.packageManager must match executable');
172
+ invalid(source, `${location}.packageManager must match executable`);
170
173
  command.packageManager = packageManager;
171
174
  }
172
175
  if ('environment' in value) {
173
- const environment = record(value.environment, source, 'command.environment');
174
- fields(environment, ['ESLINT_USE_FLAT_CONFIG'], source, 'command.environment');
175
- if ((environment.ESLINT_USE_FLAT_CONFIG !== 'true' && environment.ESLINT_USE_FLAT_CONFIG !== 'false') || Object.keys(environment).length !== 1)
176
- invalid(source, 'command.environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
176
+ const environment = record(value.environment, source, `${location}.environment`);
177
+ fields(environment, ['ESLINT_USE_FLAT_CONFIG'], source, `${location}.environment`);
178
+ if ((environment.ESLINT_USE_FLAT_CONFIG !== 'true' && environment.ESLINT_USE_FLAT_CONFIG !== 'false') || Object.keys(environment).length !== 1) {
179
+ invalid(source, `${location}.environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping`);
180
+ }
177
181
  command.environment = { ESLINT_USE_FLAT_CONFIG: environment.ESLINT_USE_FLAT_CONFIG };
178
182
  }
179
183
  if ('fileInput' in value) {
180
- const fileInput = record(value.fileInput, source, 'command.fileInput');
181
- fields(fileInput, ['placeholder', 'extensions'], source, 'command.fileInput');
184
+ const fileInput = record(value.fileInput, source, `${location}.fileInput`);
185
+ fields(fileInput, ['placeholder', 'extensions'], source, `${location}.fileInput`);
182
186
  if (fileInput.placeholder !== '{files}')
183
- invalid(source, 'command.fileInput.placeholder must be {files}');
184
- const extensions = stringArray(fileInput.extensions, source, 'command.fileInput.extensions');
187
+ invalid(source, `${location}.fileInput.placeholder must be {files}`);
188
+ const extensions = stringArray(fileInput.extensions, source, `${location}.fileInput.extensions`);
185
189
  for (const [index, extension] of extensions.entries()) {
186
190
  if (!/^\.[A-Za-z0-9]+$/.test(extension))
187
- invalid(source, `command.fileInput.extensions[${index}] must be an extension`);
191
+ invalid(source, `${location}.fileInput.extensions[${index}] must be an extension`);
188
192
  }
189
193
  if (args.filter(arg => arg === '{files}').length !== 1)
190
- invalid(source, 'command.fileInput requires exactly one {files} argument');
194
+ invalid(source, `${location}.fileInput requires exactly one {files} argument`);
191
195
  command.fileInput = { placeholder: '{files}', extensions };
192
196
  }
193
197
  else if (args.includes('{files}')) {
194
- invalid(source, 'command {files} argument requires fileInput');
198
+ invalid(source, `${location} {files} argument requires fileInput`);
195
199
  }
196
200
  return command;
197
201
  }
@@ -3,6 +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.discoverPresentMarkers = discoverPresentMarkers;
6
7
  exports.discoverProjectEvidence = discoverProjectEvidence;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
@@ -21,6 +22,26 @@ function safeFile(root, relative) {
21
22
  return false;
22
23
  }
23
24
  }
25
+ /**
26
+ * Present contained marker files under cwd — same `safeFile` rules pack discovery
27
+ * uses when evaluating applicability allFiles/anyFiles.
28
+ */
29
+ function discoverPresentMarkers(cwd, markers) {
30
+ if (typeof cwd !== 'string' || cwd.trim() === '')
31
+ throw new Error('cwd must be a non-empty path');
32
+ if (!Array.isArray(markers))
33
+ throw new Error('markers must be an array');
34
+ let root;
35
+ try {
36
+ root = fs_1.default.realpathSync(cwd);
37
+ if (!fs_1.default.statSync(root).isDirectory())
38
+ throw new Error();
39
+ }
40
+ catch {
41
+ throw new Error(`cwd is not a readable project directory: ${cwd}`);
42
+ }
43
+ return [...new Set(markers.filter((marker) => typeof marker === 'string' && safeFile(root, marker)))].sort();
44
+ }
24
45
  function safeParts(parts) {
25
46
  return parts.length > 0 && parts.every(part => Boolean(part) && part !== '.' && part !== '..' && !part.includes('/') && !part.includes('\\'));
26
47
  }
@@ -3,7 +3,11 @@ 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.isProjectDeclaredSensor = isProjectDeclaredSensor;
7
+ exports.isPackBoundManifestSensor = isPackBoundManifestSensor;
8
+ exports.asPackBoundProjectSensors = asPackBoundProjectSensors;
6
9
  exports.legacyCompatibility = legacyCompatibility;
10
+ exports.parseSensorManifestWithIssues = parseSensorManifestWithIssues;
7
11
  exports.parseSensorManifest = parseSensorManifest;
8
12
  exports.serializeManifestV2 = serializeManifestV2;
9
13
  exports.serializeManifestV3 = serializeManifestV3;
@@ -11,6 +15,28 @@ const contract_1 = require("./contract");
11
15
  const timeout_1 = require("./timeout");
12
16
  const semver_1 = __importDefault(require("semver"));
13
17
  const path_1 = __importDefault(require("path"));
18
+ /** Registered CLI formatter ids plus exit-code (default for project-declared sensors). */
19
+ const PROJECT_FORMATTERS = new Set([
20
+ 'tsc', 'eslint-llm', 'semgrep', 'test', 'mypy', 'ruff', 'shellcheck', 'generic', 'exit-code',
21
+ ]);
22
+ function isProjectDeclaredSensor(sensor) {
23
+ return 'source' in sensor && sensor.source === 'project';
24
+ }
25
+ function isPackBoundManifestSensor(sensor) {
26
+ return !isProjectDeclaredSensor(sensor);
27
+ }
28
+ /** Narrow a parsed v3 manifest to the pack-bound shape expected by migrate/materialize. */
29
+ function asPackBoundProjectSensors(manifest) {
30
+ if (typeof manifest.pack !== 'string' || manifest.pack.length === 0 || !manifest.source) {
31
+ throw new Error('project-sensors manifest requires a nonempty pack and source.registry');
32
+ }
33
+ for (const [name, sensor] of Object.entries(manifest.sensors)) {
34
+ if (isProjectDeclaredSensor(sensor)) {
35
+ throw new Error(`sensors.${name} is project-declared; pack-bound operation required`);
36
+ }
37
+ }
38
+ return manifest;
39
+ }
14
40
  function isRecord(value) {
15
41
  return typeof value === 'object' && value !== null && !Array.isArray(value);
16
42
  }
@@ -93,6 +119,9 @@ function parseLegacySensor(input, source, location) {
93
119
  if (typeof input === 'string')
94
120
  return { cmd: text(input, source, `${location}.cmd`) };
95
121
  const value = record(input, source, location);
122
+ if (value.source === 'project') {
123
+ invalid(source, `${location}.source "project" requires schemaVersion 3; migrate-to-v3`);
124
+ }
96
125
  fields(value, ['cmd', 'fast', 'enabled', 'timeout', 'changedCmd', 'changedExtensions', 'formatter'], source, location);
97
126
  const sensor = {};
98
127
  if ('cmd' in value)
@@ -138,14 +167,96 @@ function parseLegacyManifest(value, source) {
138
167
  }
139
168
  return manifest;
140
169
  }
170
+ function parseApplicability(input, source, location) {
171
+ const applicabilityInput = record(input, source, location);
172
+ fields(applicabilityInput, ['allFiles', 'anyFiles', 'kind'], source, location);
173
+ const applicability = {};
174
+ if ('allFiles' in applicabilityInput) {
175
+ applicability.allFiles = stringArray(applicabilityInput.allFiles, source, `${location}.allFiles`, false)
176
+ .map((file, index) => asset(file, source, `${location}.allFiles[${index}]`));
177
+ }
178
+ if ('anyFiles' in applicabilityInput) {
179
+ applicability.anyFiles = stringArray(applicabilityInput.anyFiles, source, `${location}.anyFiles`, false)
180
+ .map((file, index) => asset(file, source, `${location}.anyFiles[${index}]`));
181
+ }
182
+ if ('kind' in applicabilityInput) {
183
+ const kind = text(applicabilityInput.kind, source, `${location}.kind`);
184
+ if (kind !== 'explicit-or-supported-language' && kind !== 'explicit-opt-in') {
185
+ invalid(source, `${location}.kind must be a supported applicability kind`);
186
+ }
187
+ applicability.kind = kind;
188
+ }
189
+ if (Object.keys(applicability).length === 0)
190
+ invalid(source, `${location} must declare a condition`);
191
+ return applicability;
192
+ }
193
+ function parseProjectSensor(input, source, location) {
194
+ const value = record(input, source, location);
195
+ if ('variantId' in value)
196
+ invalid(source, `${location}.variantId is forbidden on source:"project" entries`);
197
+ if ('initializedCompatibility' in value)
198
+ invalid(source, `${location}.initializedCompatibility is forbidden on source:"project" entries`);
199
+ fields(value, ['source', 'enabled', 'command', 'formatter', 'fast', 'timeout', 'assets', 'applicability', 'description'], source, location);
200
+ if (value.source !== 'project')
201
+ invalid(source, `${location}.source must be "project"`);
202
+ if (typeof value.enabled !== 'boolean')
203
+ invalid(source, `${location}.enabled must be a boolean`);
204
+ const sensor = {
205
+ source: 'project',
206
+ enabled: value.enabled,
207
+ command: (0, contract_1.parseStructuredCommand)(value.command, source, `${location}.command`),
208
+ formatter: 'exit-code',
209
+ };
210
+ if ('formatter' in value) {
211
+ const formatter = text(value.formatter, source, `${location}.formatter`);
212
+ if (!PROJECT_FORMATTERS.has(formatter))
213
+ invalid(source, `${location}.formatter is not a registered formatter id`);
214
+ sensor.formatter = formatter;
215
+ }
216
+ if ('assets' in value) {
217
+ sensor.assets = stringArray(value.assets, source, `${location}.assets`, true)
218
+ .map((entry, index) => asset(entry, source, `${location}.assets[${index}]`));
219
+ }
220
+ if ('applicability' in value)
221
+ sensor.applicability = parseApplicability(value.applicability, source, `${location}.applicability`);
222
+ if ('description' in value)
223
+ sensor.description = text(value.description, source, `${location}.description`);
224
+ if ('fast' in value) {
225
+ if (typeof value.fast !== 'boolean')
226
+ invalid(source, `${location}.fast must be a boolean`);
227
+ sensor.fast = value.fast;
228
+ }
229
+ if ('timeout' in value) {
230
+ try {
231
+ sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
232
+ }
233
+ catch (error) {
234
+ invalid(source, error instanceof Error ? error.message : `${location}.timeout must be a positive safe integer`);
235
+ }
236
+ }
237
+ return sensor;
238
+ }
239
+ function parseV3SensorEntry(input, source, location) {
240
+ const value = record(input, source, location);
241
+ if (value.source === 'project')
242
+ return parseProjectSensor(value, source, location);
243
+ const unmarkedCustom = !('variantId' in value) && !('initializedCompatibility' in value) && 'command' in value;
244
+ if (unmarkedCustom) {
245
+ invalid(source, `${location}.source is required for project-declared sensors; add source:"project"`);
246
+ }
247
+ return parseV2Sensor(input, source, location);
248
+ }
141
249
  function parseV2Sensor(input, source, location) {
142
250
  const value = record(input, source, location);
251
+ if (value.source === 'project') {
252
+ invalid(source, `${location}.source "project" requires schemaVersion 3; migrate-to-v3`);
253
+ }
143
254
  fields(value, ['enabled', 'fast', 'timeout', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
144
255
  if (typeof value.enabled !== 'boolean')
145
256
  invalid(source, `${location}.enabled must be a boolean`);
146
257
  const sensor = {
147
258
  enabled: value.enabled, variantId: id(value.variantId, source, `${location}.variantId`),
148
- command: (0, contract_1.parseStructuredCommand)(value.command, source),
259
+ command: (0, contract_1.parseStructuredCommand)(value.command, source, `${location}.command`),
149
260
  initializedCompatibility: parseCompatibilityEvidence(value.initializedCompatibility, source, `${location}.initializedCompatibility`),
150
261
  };
151
262
  if (sensor.initializedCompatibility.variantId !== null && sensor.initializedCompatibility.variantId !== sensor.variantId)
@@ -226,24 +337,28 @@ function parseV2Manifest(value, source) {
226
337
  }
227
338
  return manifest;
228
339
  }
229
- function parseV3ProjectManifest(value, source) {
340
+ function parseV3ProjectManifestRoot(value, source) {
230
341
  fields(value, ['schemaVersion', 'mode', 'pack', 'packSelection', 'source', 'packageRoot', 'sensors', 'concurrency'], source, 'root');
231
- const sourceValue = record(value.source, source, 'source');
232
- fields(sourceValue, ['registry'], source, 'source');
233
- const sensorsInput = record(value.sensors, source, 'sensors');
234
- const sensors = {};
235
- for (const name of Object.keys(sensorsInput))
236
- sensors[id(name, source, 'sensor id')] = parseV2Sensor(sensorsInput[name], source, `sensors.${name}`);
342
+ const hasPackKey = 'pack' in value;
343
+ const pack = !hasPackKey ? undefined : value.pack === null ? null : id(value.pack, source, 'pack');
237
344
  const manifest = {
238
345
  schemaVersion: 3,
239
346
  mode: 'project-sensors',
240
- pack: id(value.pack, source, 'pack'),
241
- source: { registry: id(sourceValue.registry, source, 'source.registry') },
242
- sensors,
347
+ ...(hasPackKey ? { pack } : {}),
243
348
  };
349
+ if (typeof pack === 'string' && pack.length > 0) {
350
+ const sourceValue = record(value.source, source, 'source');
351
+ fields(sourceValue, ['registry'], source, 'source');
352
+ manifest.source = { registry: id(sourceValue.registry, source, 'source.registry') };
353
+ }
354
+ else if ('source' in value) {
355
+ invalid(source, 'source requires a nonempty pack id');
356
+ }
244
357
  if ('packSelection' in value) {
245
358
  if (value.packSelection !== 'explicit')
246
359
  invalid(source, 'packSelection must be "explicit" when present');
360
+ if (typeof pack !== 'string' || pack.length === 0)
361
+ invalid(source, 'packSelection requires a nonempty pack id');
247
362
  manifest.packSelection = 'explicit';
248
363
  }
249
364
  if ('packageRoot' in value)
@@ -255,26 +370,63 @@ function parseV3ProjectManifest(value, source) {
255
370
  }
256
371
  return manifest;
257
372
  }
373
+ function parseV3ProjectSensorsMap(sensorsInput, source, soft) {
374
+ const sensors = {};
375
+ const invalidEntries = [];
376
+ for (const name of Object.keys(sensorsInput)) {
377
+ const sensorName = id(name, source, 'sensor id');
378
+ try {
379
+ sensors[sensorName] = parseV3SensorEntry(sensorsInput[name], source, `sensors.${sensorName}`);
380
+ }
381
+ catch (error) {
382
+ const reason = error instanceof Error ? error.message : String(error);
383
+ if (!soft)
384
+ throw error instanceof Error ? error : new Error(reason);
385
+ invalidEntries.push({ name: sensorName, reason });
386
+ }
387
+ }
388
+ return { sensors, invalidEntries };
389
+ }
390
+ function parseV3ProjectManifest(value, source, soft) {
391
+ const root = parseV3ProjectManifestRoot(value, source);
392
+ const sensorsInput = record(value.sensors, source, 'sensors');
393
+ const { sensors, invalidEntries } = parseV3ProjectSensorsMap(sensorsInput, source, soft);
394
+ return { pack: { ...root, sensors }, invalidEntries };
395
+ }
258
396
  function parseV3Manifest(value, source) {
259
397
  if (value.schemaVersion !== 3)
260
398
  invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
261
399
  if (value.mode === 'project-sensors')
262
- return parseV3ProjectManifest(value, source);
400
+ return parseV3ProjectManifest(value, source, false).pack;
263
401
  if (value.mode !== 'native-gate' && value.mode !== 'opt-out')
264
402
  invalid(source, 'schemaVersion 3 mode must be "project-sensors", "native-gate", or "opt-out"');
265
403
  fields(value, ['schemaVersion', 'mode', 'reason'], source, 'root');
266
404
  return { schemaVersion: 3, mode: value.mode, reason: text(value.reason, source, 'reason') };
267
405
  }
268
- function parseSensorManifest(input, source) {
406
+ /** Soft per-entry parse for v3 project-sensors; document-level errors still throw. */
407
+ function parseSensorManifestWithIssues(input, source) {
269
408
  const value = record(input, source, 'root');
270
409
  if (!('schemaVersion' in value))
271
- return { kind: 'legacy', pack: parseLegacyManifest(value, source) };
410
+ return { kind: 'legacy', pack: parseLegacyManifest(value, source), invalidEntries: [] };
272
411
  if (value.schemaVersion === 2)
273
- return { kind: 'v2', pack: parseV2Manifest(value, source) };
274
- if (value.schemaVersion === 3)
275
- return { kind: 'v3', pack: parseV3Manifest(value, source) };
412
+ return { kind: 'v2', pack: parseV2Manifest(value, source), invalidEntries: [] };
413
+ if (value.schemaVersion === 3) {
414
+ if (value.mode === 'project-sensors') {
415
+ const parsed = parseV3ProjectManifest(value, source, true);
416
+ return { kind: 'v3', pack: parsed.pack, invalidEntries: parsed.invalidEntries };
417
+ }
418
+ return { kind: 'v3', pack: parseV3Manifest(value, source), invalidEntries: [] };
419
+ }
276
420
  invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2, 3; upgrade or migrate the manifest`);
277
421
  }
422
+ function parseSensorManifest(input, source) {
423
+ const withIssues = parseSensorManifestWithIssues(input, source);
424
+ if (withIssues.invalidEntries.length > 0) {
425
+ throw new Error(withIssues.invalidEntries[0].reason);
426
+ }
427
+ const { invalidEntries: _invalidEntries, ...parsed } = withIssues;
428
+ return parsed;
429
+ }
278
430
  function serializeManifestV2(input) {
279
431
  const parsed = parseSensorManifest(input, 'manifest serialization');
280
432
  if (parsed.kind !== 'v2')
@@ -217,6 +217,23 @@ function priorAssets(projectRoot) {
217
217
  return Object.values(parsedManifest.pack.sensors).flatMap(sensor => sensor.assets ?? []);
218
218
  return [];
219
219
  }
220
+ /** Existing `"source": "project"` entries to keep when regenerating pack sensors (RF-3.1). */
221
+ function priorProjectSensors(projectRoot) {
222
+ const manifestPath = path_1.default.join(projectRoot, '.awm', 'sensors.json');
223
+ try {
224
+ const stat = fs_1.default.lstatSync(manifestPath, { bigint: true });
225
+ if (!stat.isFile() || stat.isSymbolicLink())
226
+ return {};
227
+ // Soft-parse so one invalid sibling does not drop every valid project entry.
228
+ const parsed = (0, manifest_1.parseSensorManifestWithIssues)(JSON.parse((0, safe_file_1.readInspectedBoundedFile)(manifestPath, stat, MAX_ASSET_BYTES, () => new Error('prior manifest unreadable')).toString('utf8')), manifestPath);
229
+ if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
230
+ return {};
231
+ return Object.fromEntries(Object.entries(parsed.pack.sensors).filter(([, sensor]) => (0, manifest_1.isProjectDeclaredSensor)(sensor)));
232
+ }
233
+ catch {
234
+ return {};
235
+ }
236
+ }
220
237
  /** Materialize a previously selected logical registry source into a portable v3 manifest. */
221
238
  function materializePortableSensors(input) {
222
239
  if (!input || typeof input !== 'object')
@@ -237,18 +254,25 @@ function materializePortableSensors(input) {
237
254
  const parsed = (0, manifest_1.parseSensorManifest)(candidate, 'portable materialized manifest');
238
255
  if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
239
256
  throw new Error('portable materialized manifest must be v3 project-sensors');
240
- const manifest = parsed.pack;
241
- for (const [name, sensor] of Object.entries(manifest.sensors)) {
242
- if (sensor.policyRef)
257
+ const packBound = (0, manifest_1.asPackBoundProjectSensors)(parsed.pack);
258
+ for (const [name, sensor] of Object.entries(packBound.sensors)) {
259
+ if ((0, manifest_1.isPackBoundManifestSensor)(sensor) && sensor.policyRef)
243
260
  (0, contract_1.resolveSemgrepPolicy)(sensor.policyRef, path_1.default.join(packRoot, 'pack.json'), `sensors.${name}`);
244
261
  }
245
- const selected = [...new Set(Object.values(manifest.sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
262
+ const priorProject = priorProjectSensors(projectRoot);
263
+ // Project entries win on name collision so regen never drops them (RF-3.1).
264
+ const manifest = {
265
+ ...packBound,
266
+ sensors: { ...packBound.sensors, ...priorProject },
267
+ };
268
+ const selected = [...new Set(Object.values(packBound.sensors).flatMap(sensor => sensor.assets ?? []))].map((asset, index) => containedAsset(asset, `assets[${index}]`)).sort();
246
269
  // Establish every registry authority and read every selected byte before the
247
270
  // first project mutation. A bad later asset must not strand an earlier one.
248
271
  const publications = input.configure === false ? [] : selected.map(asset => {
249
272
  const selectedSource = selectedRegistryAsset(packRoot, asset);
250
273
  return { asset, content: readSelectedRegistryAsset(packRoot, selectedSource, asset) };
251
274
  });
275
+ const manifestExists = fs_1.default.existsSync(path_1.default.join(projectRoot, '.awm', 'sensors.json'));
252
276
  return (0, safe_file_1.withProjectLease)(projectRoot, () => {
253
277
  const configRoot = manifest.packageRoot ? containedPackageRoot(projectRoot, manifest.packageRoot) : projectRoot;
254
278
  const prior = priorAssets(projectRoot);
@@ -273,7 +297,8 @@ function materializePortableSensors(input) {
273
297
  preserved.push(publication.asset);
274
298
  }
275
299
  }
276
- atomicWrite(projectRoot, '.awm/sensors.json', (0, manifest_1.serializeManifestV3)(manifest), true);
300
+ // Replace when regenerating over an existing manifest so project entries can be re-merged.
301
+ atomicWrite(projectRoot, '.awm/sensors.json', (0, manifest_1.serializeManifestV3)(manifest), !manifestExists);
277
302
  }
278
303
  catch (error) {
279
304
  for (const entry of created.reverse()) {
@@ -49,10 +49,11 @@ function resolveSensorSource(manifest, deps) {
49
49
  || !v3.source || typeof v3.source.registry !== 'string') {
50
50
  throw new Error('sensor source resolution requires a valid project-sensors manifest');
51
51
  }
52
- if (!/^[a-z][a-z0-9-]*$/.test(v3.source.registry)) {
52
+ const registryName = v3.source.registry;
53
+ if (!/^[a-z][a-z0-9-]*$/.test(registryName)) {
53
54
  throw new Error('sensor source registry must be a stable lowercase id');
54
55
  }
55
- const matches = registries.filter(entry => entry?.name === v3.source.registry);
56
+ const matches = registries.filter(entry => entry?.name === registryName);
56
57
  if (matches.length !== 1)
57
58
  return unavailable('registry-not-installed');
58
59
  const source = (0, pack_source_1.listPackSources)(v3.pack, { registries: matches })[0];
@@ -225,8 +225,9 @@ function assertCoverageEnvelope(report, renderer) {
225
225
  return;
226
226
  }
227
227
  if (staticReport.reason === 'no_reference') {
228
- if (report.overall !== 'inconclusive' || !isNonBlankString(report.pack) || !isNonBlankString(report.registry)
229
- || staticReport.classes.length !== 0) {
228
+ const packReferenced = isNonBlankString(report.pack) && isNonBlankString(report.registry);
229
+ const onlyProject = report.pack === null && report.registry === null;
230
+ if (report.overall !== 'inconclusive' || staticReport.classes.length !== 0 || (!packReferenced && !onlyProject)) {
230
231
  invalidReport(renderer);
231
232
  }
232
233
  if (report.empirical !== undefined) {
@@ -303,6 +304,11 @@ function renderCoverageHuman(report) {
303
304
  return ['Sensor coverage', 'Overall: inconclusive', 'Reason: sensors are not configured', 'Run: awm sensors init', ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
304
305
  }
305
306
  if (report.static.reason === 'no_reference') {
307
+ if (report.pack === null && report.registry === null) {
308
+ return ['Sensor coverage', 'Pack: none', 'Overall: inconclusive',
309
+ 'No pack coverage reference (project-declared sensors only)',
310
+ ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
311
+ }
306
312
  return ['Sensor coverage', `Pack: ${safeHumanText(report.pack ?? 'unknown')}`, `Registry: ${safeHumanText(report.registry ?? 'unknown')}`,
307
313
  'Overall: inconclusive', `No coverage reference for pack '${safeHumanText(report.pack ?? 'unknown')}'`, ...(report.empirical ? [empiricalHuman(report)] : []), ''].join('\n');
308
314
  }
@@ -97,6 +97,13 @@ function resolveCoverageInputs(cwd) {
97
97
  if (manifest.kind === 'v3' && manifest.pack.mode !== 'project-sensors')
98
98
  return { kind: 'not_configured' };
99
99
  const coverageManifest = manifest;
100
+ // Only-project manifests have no pack coverage contract to evaluate (RF-2.9).
101
+ if (coverageManifest.kind === 'v3') {
102
+ const packId = coverageManifest.pack.pack;
103
+ if (typeof packId !== 'string' || packId.length === 0) {
104
+ return { kind: 'no_reference', projectRoot, pack: null, registry: null, manifest: coverageManifest };
105
+ }
106
+ }
100
107
  const source = coverageManifest.kind === 'v3'
101
108
  ? (() => {
102
109
  const resolution = (0, source_1.resolveSensorSource)(coverageManifest, { registries: (0, registries_1.listRegistries)() });
@@ -117,13 +117,15 @@ function readPackDefaults(pack, registryRoot, cwd) {
117
117
  return sensors;
118
118
  }
119
119
  function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
120
- const fromPack = registryRoot ? readPackDefaults(pack, registryRoot, cwd) : null;
120
+ const fromPack = typeof pack === 'string' && pack.length > 0 && registryRoot
121
+ ? readPackDefaults(pack, registryRoot, cwd)
122
+ : null;
121
123
  // No registry root, or the pack has no pack.json there → `{}` is the honest floor,
122
124
  // not a bug to paper over with CLI-hardcoded defaults. `checkManifest` (preflight)
123
125
  // and `computeSensorStatus` both surface a zero-sensor manifest as degraded, with a
124
126
  // remedy pointing at the registry — never silently inventing sensors here instead.
125
127
  const defaults = fromPack ?? {};
126
- const existingSensors = existing?.sensors ?? {};
128
+ const existingSensors = (existing?.sensors ?? {});
127
129
  // Per-FIELD merge, not whole-sensor-object replacement: if `existingSensors.foo`
128
130
  // exists at all, a naive `{ ...defaults, ...existingSensors }` would replace
129
131
  // `defaults.foo` wholesale, permanently dropping any field that only lives in the
@@ -132,11 +134,22 @@ function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
132
134
  // field-by-field within each sensor entry lets a user's hand-edited field (e.g. a
133
135
  // custom `cmd`) win, while still inheriting any field the existing manifest doesn't
134
136
  // specify.
137
+ //
138
+ // Exception — `source: "project"` entries are project-owned: never overlay pack
139
+ // defaults onto them (RF-3.1). Keep the entry byte-stable in meaning.
135
140
  const sensorNames = new Set([...Object.keys(defaults), ...Object.keys(existingSensors)]);
136
141
  const sensors = {};
137
142
  for (const name of sensorNames) {
143
+ const existingEntry = existingSensors[name];
144
+ if (existingEntry && existingEntry.source === 'project') {
145
+ sensors[name] = { ...existingEntry };
146
+ continue;
147
+ }
138
148
  sensors[name] = { ...defaults[name], ...existingSensors[name] };
139
149
  }
150
+ // Only-project write path: omit pack entirely — never invent `generic` (RF-3.2).
151
+ if (typeof pack !== 'string' || pack.length === 0)
152
+ return { sensors };
140
153
  return { pack, sensors };
141
154
  }
142
155
  async function initSensors(opts = {}) {
@@ -172,10 +172,11 @@ function planV2Migration(input) {
172
172
  if (validated.kind !== 'v3' || validated.pack.mode !== 'project-sensors') {
173
173
  throw new Error('v2 migration candidate semantic mismatch');
174
174
  }
175
- const equivalence = compareV2Semantics(parsed.pack, validated.pack, source.registry.name);
175
+ const packBound = (0, manifest_1.asPackBoundProjectSensors)(validated.pack);
176
+ const equivalence = compareV2Semantics(parsed.pack, packBound, source.registry.name);
176
177
  if (!allEquivalent(equivalence))
177
178
  throw new Error('v2 migration candidate semantic mismatch');
178
- return { candidate: validated.pack, equivalent: true, equivalence };
179
+ return { candidate: packBound, equivalent: true, equivalence };
179
180
  }
180
181
  /** Validate the old and new durable contracts, then atomically replace only the manifest. */
181
182
  function replaceV2ManifestWithV3(manifestPath, candidate, source) {
@@ -203,17 +204,18 @@ function replaceV2ManifestWithV3(manifestPath, candidate, source) {
203
204
  if (before.kind !== 'v2' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
204
205
  throw new Error('v2 migration candidate semantic mismatch');
205
206
  }
207
+ const afterPack = (0, manifest_1.asPackBoundProjectSensors)(after.pack);
206
208
  const logicalSource = exactLogicalSource(source, before.pack.pack);
207
209
  assertSourceCompatibleWithManifest(logicalSource, before.pack);
208
- if (after.pack.source.registry !== logicalSource.registry.name) {
210
+ if (afterPack.source.registry !== logicalSource.registry.name) {
209
211
  throw new Error('v2 migration candidate semantic mismatch');
210
212
  }
211
213
  if (hasPhysicalSensorPath(before.pack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
212
- || hasPhysicalSensorPath(after.pack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
213
- || hasPhysicalSensorPath(before.pack.packageRoot, []) || hasPhysicalSensorPath(after.pack.packageRoot, [])) {
214
+ || hasPhysicalSensorPath(afterPack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
215
+ || hasPhysicalSensorPath(before.pack.packageRoot, []) || hasPhysicalSensorPath(afterPack.packageRoot, [])) {
214
216
  throw new Error('v2 migration candidate contains a physical path');
215
217
  }
216
- if (!allEquivalent(compareV2Semantics(before.pack, after.pack, logicalSource.registry.name))) {
218
+ if (!allEquivalent(compareV2Semantics(before.pack, afterPack, logicalSource.registry.name))) {
217
219
  throw new Error('v2 migration candidate semantic mismatch');
218
220
  }
219
221
  // The native implementation reopens every ancestor without following links,
@@ -255,10 +257,11 @@ function replaceLegacyManifestWithV3(manifestPath, candidate, source) {
255
257
  if (before.kind !== 'legacy' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
256
258
  throw new Error('legacy migration candidate semantic mismatch');
257
259
  }
258
- const logicalSource = exactLogicalSource({ kind: 'logical', source }, after.pack.pack);
259
- assertSourceCompatibleWithCandidate(logicalSource, after.pack);
260
- if (after.pack.source.registry !== logicalSource.registry.name || hasPhysicalSensorPath(after.pack.sensors, [logicalSource.registry.contentRoot])
261
- || hasPhysicalSensorPath(after.pack.packageRoot, [])) {
260
+ const afterPack = (0, manifest_1.asPackBoundProjectSensors)(after.pack);
261
+ const logicalSource = exactLogicalSource({ kind: 'logical', source }, afterPack.pack);
262
+ assertSourceCompatibleWithCandidate(logicalSource, afterPack);
263
+ if (afterPack.source.registry !== logicalSource.registry.name || hasPhysicalSensorPath(afterPack.sensors, [logicalSource.registry.contentRoot])
264
+ || hasPhysicalSensorPath(afterPack.packageRoot, [])) {
262
265
  throw new Error('legacy migration candidate contains an unsafe source or physical path');
263
266
  }
264
267
  assertManifestUnchanged(manifestPath, inspected);