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
@@ -6,6 +6,7 @@ exports.prepareLegacySensor = prepareLegacySensor;
6
6
  exports.prepareV2Sensor = prepareV2Sensor;
7
7
  const changed_1 = require("./changed");
8
8
  const timeout_1 = require("./compatibility/timeout");
9
+ const manifest_1 = require("./compatibility/manifest");
9
10
  function resolveRequestedScope(value) {
10
11
  if (value === undefined)
11
12
  return 'full';
@@ -108,13 +109,59 @@ function v2Synthetic(input, reason) {
108
109
  syntheticReason: reason,
109
110
  };
110
111
  }
112
+ function projectApplicabilityMet(sensor, paths) {
113
+ const rule = sensor.applicability;
114
+ if (!rule)
115
+ return true;
116
+ if (rule.kind === 'explicit-or-supported-language' || rule.kind === 'explicit-opt-in') {
117
+ // Project sensors do not receive pack-selection signals; treat these kinds
118
+ // as met when no path markers were declared.
119
+ if (!rule.allFiles?.length && !rule.anyFiles?.length)
120
+ return true;
121
+ }
122
+ const present = new Set(paths ?? []);
123
+ return (!rule.allFiles || rule.allFiles.every(file => present.has(file)))
124
+ && (!rule.anyFiles || rule.anyFiles.some(file => present.has(file)));
125
+ }
111
126
  /**
112
- * Prepare a v2 command from the freshly resolved pack. The manifest command is
113
- * deliberately never read: variantId is only a selector for live authority.
127
+ * Prepare a project-declared sensor from its manifest entry. No live pack variant
128
+ * re-resolution: the structured command on the entry is authoritative.
129
+ */
130
+ function prepareProjectSensor(input) {
131
+ const sensor = input.sensor;
132
+ const requestedScope = resolveRequestedScope(input.requestedScope);
133
+ if (!projectApplicabilityMet(sensor, input.applicabilityPaths)) {
134
+ return {
135
+ name: input.name,
136
+ ...timeout(input.projectTimeout ?? sensor.timeout, input.packTimeout, sensor.fast ?? false),
137
+ requestedScope,
138
+ effectiveScope: 'full',
139
+ syntheticStatus: 'inconclusive',
140
+ syntheticReason: 'not-applicable: applicability-not-met',
141
+ certification: 'project-declared',
142
+ };
143
+ }
144
+ return {
145
+ name: input.name,
146
+ command: { kind: 'structured', value: sensor.command },
147
+ formatter: sensor.formatter || 'exit-code',
148
+ ...timeout(input.projectTimeout ?? sensor.timeout, input.packTimeout, sensor.fast ?? false),
149
+ requestedScope,
150
+ effectiveScope: 'full',
151
+ certification: 'project-declared',
152
+ };
153
+ }
154
+ /**
155
+ * Prepare a v2/v3 command. Pack-bound sensors re-resolve from the live pack
156
+ * (manifest command is never authority). Project-declared sensors use the
157
+ * entry command and carry `project-declared` provenance.
114
158
  */
115
159
  function prepareV2Sensor(input) {
116
160
  if (!input || typeof input !== 'object' || !input.sensor || typeof input.name !== 'string' || input.name === '')
117
161
  throw new Error('v2 preparation input is invalid');
162
+ if ((0, manifest_1.isProjectDeclaredSensor)(input.sensor))
163
+ return prepareProjectSensor(input);
164
+ const packSensor = input.sensor;
118
165
  const requestedScope = resolveRequestedScope(input.requestedScope);
119
166
  // Un sensor se ejecuta cuando la herramienta ESTA y funciona aca: `certified` es
120
167
  // la version exacta que el registry congelo, y `compatible-unverified` es una que
@@ -133,16 +180,16 @@ function prepareV2Sensor(input) {
133
180
  if (!input.liveState || (input.liveState.state !== 'certified' && input.liveState.state !== 'compatible-unverified')) {
134
181
  return v2Synthetic(input, input.liveState ? `${input.liveState.state}: ${input.liveState.reason}` : 'compatibility could not be revalidated');
135
182
  }
136
- if (input.liveState.variantId !== input.sensor.variantId)
137
- return v2Synthetic(input, `variant-drift: manifest ${input.sensor.variantId}, live ${input.liveState.variantId ?? 'none'}; run \`awm sensors init\``);
138
- const variant = input.liveSensor?.variants.find(candidate => candidate.id === input.sensor.variantId);
183
+ if (input.liveState.variantId !== packSensor.variantId)
184
+ return v2Synthetic(input, `variant-drift: manifest ${packSensor.variantId}, live ${input.liveState.variantId ?? 'none'}; run \`awm sensors init\``);
185
+ const variant = input.liveSensor?.variants.find(candidate => candidate.id === packSensor.variantId);
139
186
  if (!variant)
140
187
  return v2Synthetic(input, 'variant-drift: selected live variant has no command; run `awm sensors init`');
141
188
  const common = {
142
189
  name: input.name,
143
190
  certification: input.liveState.state === 'certified' ? 'certified' : 'operational-unverified',
144
191
  ...(variant.formatter ? { formatter: variant.formatter } : {}),
145
- ...timeout(input.projectTimeout ?? input.sensor.timeout, input.packTimeout ?? input.liveSensor?.timeout, input.sensor.fast ?? input.liveSensor?.fast ?? false),
192
+ ...timeout(input.projectTimeout ?? packSensor.timeout, input.packTimeout ?? input.liveSensor?.timeout, packSensor.fast ?? input.liveSensor?.fast ?? false),
146
193
  requestedScope,
147
194
  };
148
195
  if (requestedScope !== 'changed' || !input.changed)
@@ -3,6 +3,8 @@ 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.resolveContainedPackageRoot = resolveContainedPackageRoot;
7
+ exports.softRecoverInvalidProject = softRecoverInvalidProject;
6
8
  exports.resolveSensorProject = resolveSensorProject;
7
9
  const fs_1 = __importDefault(require("fs"));
8
10
  const path_1 = __importDefault(require("path"));
@@ -17,6 +19,60 @@ function regularManifest(manifestPath) {
17
19
  return false;
18
20
  }
19
21
  }
22
+ /**
23
+ * Resolve packageRoot under projectRoot with the same realpath containment
24
+ * check used by hard authority selection. Returns null when the path is
25
+ * missing, not a directory, or escapes the project root (including via symlink).
26
+ */
27
+ function resolveContainedPackageRoot(projectRoot, configuredPackageRoot) {
28
+ const packageRoot = configuredPackageRoot ? path_1.default.resolve(projectRoot, configuredPackageRoot) : projectRoot;
29
+ if (!fs_1.default.existsSync(packageRoot) || !fs_1.default.statSync(packageRoot).isDirectory())
30
+ return null;
31
+ try {
32
+ const realProjectRoot = fs_1.default.realpathSync(projectRoot);
33
+ const realPackageRoot = fs_1.default.realpathSync(packageRoot);
34
+ const relative = path_1.default.relative(realProjectRoot, realPackageRoot);
35
+ if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative))
36
+ return null;
37
+ return packageRoot;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Soft-recover schema-entry failures after a successful JSON.parse of a
45
+ * *regular* sensors.json. Refuses symlink/non-regular manifests and any
46
+ * packageRoot that escapes the project root — same trust boundary as hard parse.
47
+ */
48
+ function softRecoverInvalidProject(project) {
49
+ if (!regularManifest(project.manifestPath))
50
+ return null;
51
+ if (/regular file/i.test(project.reason))
52
+ return null;
53
+ try {
54
+ const text = new util_1.TextDecoder('utf-8', { fatal: true }).decode(fs_1.default.readFileSync(project.manifestPath));
55
+ const soft = (0, manifest_1.parseSensorManifestWithIssues)(JSON.parse(text), project.manifestPath);
56
+ if (soft.kind !== 'v3' || soft.pack.mode !== 'project-sensors')
57
+ return null;
58
+ if (Object.keys(soft.pack.sensors).length === 0 && soft.invalidEntries.length === 0)
59
+ return null;
60
+ const packageRoot = resolveContainedPackageRoot(project.projectRoot, soft.pack.packageRoot);
61
+ if (packageRoot === null)
62
+ return null;
63
+ const { invalidEntries, ...manifest } = soft;
64
+ return {
65
+ projectRoot: project.projectRoot,
66
+ manifestPath: project.manifestPath,
67
+ packageRoot,
68
+ manifest,
69
+ invalidEntries,
70
+ };
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
20
76
  function gitMarker(directory) {
21
77
  try {
22
78
  const stat = fs_1.default.lstatSync(path_1.default.join(directory, '.git'));
@@ -52,14 +108,12 @@ function manifestResolution(projectRoot) {
52
108
  configuredPackageRoot = manifest.pack.packageRoot;
53
109
  if (manifest.kind === 'v3' && manifest.pack.mode === 'project-sensors')
54
110
  configuredPackageRoot = manifest.pack.packageRoot;
55
- const packageRoot = configuredPackageRoot ? path_1.default.resolve(projectRoot, configuredPackageRoot) : projectRoot;
56
- if (!fs_1.default.existsSync(packageRoot) || !fs_1.default.statSync(packageRoot).isDirectory()) {
111
+ const candidate = configuredPackageRoot ? path_1.default.resolve(projectRoot, configuredPackageRoot) : projectRoot;
112
+ if (!fs_1.default.existsSync(candidate) || !fs_1.default.statSync(candidate).isDirectory()) {
57
113
  return { state: 'invalid', projectRoot, manifestPath, reason: 'packageRoot must name an existing directory' };
58
114
  }
59
- const realProjectRoot = fs_1.default.realpathSync(projectRoot);
60
- const realPackageRoot = fs_1.default.realpathSync(packageRoot);
61
- const relative = path_1.default.relative(realProjectRoot, realPackageRoot);
62
- if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) {
115
+ const packageRoot = resolveContainedPackageRoot(projectRoot, configuredPackageRoot);
116
+ if (packageRoot === null) {
63
117
  return { state: 'invalid', projectRoot, manifestPath, reason: 'packageRoot escapes the manifest directory' };
64
118
  }
65
119
  return { state: 'configured', projectRoot, manifestPath, packageRoot, manifest };
@@ -14,6 +14,13 @@ const mypy_1 = require("./formatters/mypy");
14
14
  const ruff_1 = require("./formatters/ruff");
15
15
  const shellcheck_1 = require("./formatters/shellcheck");
16
16
  const MAX_BUFFER = 64 * 1024 * 1024;
17
+ /** Exit-code sensors signal solely via process status — never invent per-file findings. */
18
+ function parseExitCodeOutput(_raw) {
19
+ return [];
20
+ }
21
+ function truncateEvidence(raw, max = 200) {
22
+ return raw.length <= max ? raw : raw.slice(0, max);
23
+ }
17
24
  function formatterFor(name, formatter) {
18
25
  if (formatter !== undefined) {
19
26
  switch (formatter) {
@@ -25,6 +32,7 @@ function formatterFor(name, formatter) {
25
32
  case 'ruff': return ruff_1.parseRuffOutput;
26
33
  case 'shellcheck': return shellcheck_1.parseShellcheckOutput;
27
34
  case 'generic': return generic_1.parseGenericOutput;
35
+ case 'exit-code': return parseExitCodeOutput;
28
36
  default: return generic_1.parseGenericOutput;
29
37
  }
30
38
  }
@@ -92,12 +100,18 @@ function interpretResult(prepared, raw) {
92
100
  return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: reason });
93
101
  }
94
102
  if (raw.code === 0) {
95
- if (format === generic_1.parseGenericOutput)
103
+ if (format === generic_1.parseGenericOutput || format === parseExitCodeOutput) {
96
104
  return withEvidence({ name: prepared.name, status: 'pass', errors: [] });
105
+ }
97
106
  const errors = format(raw.stdout);
98
107
  return withEvidence({ name: prepared.name, status: errors.length ? 'fail' : 'pass', errors });
99
108
  }
100
109
  const output = raw.stdout + raw.stderr;
110
+ if (prepared.formatter === 'exit-code' || format === parseExitCodeOutput) {
111
+ const combined = output.trim();
112
+ const evidence = truncateEvidence(combined.length > 0 ? combined : `exit ${raw.code}`);
113
+ return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: evidence }] });
114
+ }
101
115
  const errors = format(output);
102
116
  if (errors.length > 0)
103
117
  return withEvidence({ name: prepared.name, status: 'fail', errors });
@@ -108,10 +122,10 @@ function interpretResult(prepared, raw) {
108
122
  || lower.includes('enoent')
109
123
  || lower.includes('could not determine executable');
110
124
  if (toolMissing)
111
- return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `sensor tool not available: ${output.slice(0, 200)}` }] });
125
+ return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `sensor tool not available: ${truncateEvidence(output)}` }] });
112
126
  if (prepared.name === 'test')
113
127
  return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `SENSOR[${prepared.name}] failed (exit ${raw.code})` }] });
114
- return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: `exit ${raw.code}: ${output.slice(0, 200)}` });
128
+ return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: `exit ${raw.code}: ${truncateEvidence(output)}` });
115
129
  }
116
130
  /** Execute a validated prepared command, or render its deliberate synthetic result. */
117
131
  async function executePrepared(prepared, cwd = process.cwd()) {
@@ -123,6 +137,7 @@ async function executePrepared(prepared, cwd = process.cwd()) {
123
137
  status,
124
138
  errors: [],
125
139
  ...(prepared.syntheticReason ? { skipReason: prepared.syntheticReason } : {}),
140
+ ...(prepared.certification ? { certification: prepared.certification } : {}),
126
141
  execution: executionEvidence(prepared, 0),
127
142
  }, prepared);
128
143
  }
@@ -22,6 +22,8 @@ const detection_1 = require("./detection");
22
22
  const live_1 = require("./compatibility/live");
23
23
  const contract_1 = require("./compatibility/contract");
24
24
  const source_1 = require("./compatibility/source");
25
+ const manifest_1 = require("./compatibility/manifest");
26
+ const discovery_1 = require("./compatibility/discovery");
25
27
  const registries_1 = require("../../core/registries");
26
28
  const prepare_1 = require("./prepare");
27
29
  const verdict_1 = require("./verdict");
@@ -172,6 +174,12 @@ async function pooled(tasks, limit) {
172
174
  await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
173
175
  return results;
174
176
  }
177
+ function v3ProjectSensors(parsed) {
178
+ return parsed.kind === 'v3' && parsed.pack.mode === 'project-sensors' ? parsed.pack : null;
179
+ }
180
+ function hasSelectedPack(manifest) {
181
+ return typeof manifest.pack === 'string' && manifest.pack.length > 0;
182
+ }
175
183
  /**
176
184
  * One orchestration path for both manifest contracts. Preparation selects the
177
185
  * authorized command; execution, baseline handling, and verdict reduction are
@@ -190,22 +198,34 @@ async function runSensors(opts = {}) {
190
198
  if (project.state === 'missing') {
191
199
  return { sensors: [], overall: 'not_certified', mode: 'missing', reason: 'manifest-absent', projectRoot: project.projectRoot, manifestPath: project.manifestPath, remedy: 'run-awm-sensors-bootstrap' };
192
200
  }
201
+ let invalidEntries = [];
202
+ let configured;
193
203
  if (project.state === 'invalid') {
194
- const reason = project.reason.includes('unsupported manifest schemaVersion') ? 'schema-unsupported' : 'manifest-malformed';
195
- return { sensors: [], overall: 'not_certified', mode: 'invalid', reason, projectRoot: project.projectRoot, manifestPath: project.manifestPath, remedy: 'repair-sensor-manifest' };
204
+ const recovered = (0, project_1.softRecoverInvalidProject)(project);
205
+ if (!recovered) {
206
+ const reason = project.reason.includes('unsupported manifest schemaVersion') ? 'schema-unsupported' : 'manifest-malformed';
207
+ return { sensors: [], overall: 'not_certified', mode: 'invalid', reason, projectRoot: project.projectRoot, manifestPath: project.manifestPath, remedy: 'repair-sensor-manifest' };
208
+ }
209
+ invalidEntries = recovered.invalidEntries;
210
+ configured = recovered;
211
+ }
212
+ else {
213
+ configured = project;
196
214
  }
197
- const manifestDir = project.projectRoot;
215
+ const manifestDir = configured.projectRoot;
198
216
  const beforeReadOnly = opts.readOnly ? filesystemSnapshot(manifestDir) : undefined;
199
217
  if (opts.readOnly && beforeReadOnly === null) {
200
- return { sensors: [], overall: 'not_certified', projectRoot: project.projectRoot, manifestPath: project.manifestPath, mode: 'invalid', reason: 'read-only-observation-unavailable', remedy: 'run admission from a readable git worktree' };
218
+ return { sensors: [], overall: 'not_certified', projectRoot: configured.projectRoot, manifestPath: configured.manifestPath, mode: 'invalid', reason: 'read-only-observation-unavailable', remedy: 'run admission from a readable git worktree' };
201
219
  }
202
- const parsed = project.manifest;
203
- const authority = { projectRoot: project.projectRoot, manifestPath: project.manifestPath };
220
+ const parsed = configured.manifest;
221
+ const authority = { projectRoot: configured.projectRoot, manifestPath: configured.manifestPath };
204
222
  if (parsed.kind === 'v3' && (parsed.pack.mode === 'native-gate' || parsed.pack.mode === 'opt-out')) {
205
223
  return { sensors: [], overall: 'not_certified', ...authority, mode: parsed.pack.mode, reason: `${parsed.pack.mode}-declared`, declarationReason: parsed.pack.reason };
206
224
  }
225
+ const projectSensorsManifest = v3ProjectSensors(parsed);
226
+ const onlyProject = projectSensorsManifest !== null && !hasSelectedPack(projectSensorsManifest);
207
227
  let resolvedSource;
208
- if (parsed.kind === 'v2' || parsed.kind === 'v3') {
228
+ if (parsed.kind === 'v2' || (parsed.kind === 'v3' && projectSensorsManifest && hasSelectedPack(projectSensorsManifest))) {
209
229
  try {
210
230
  resolvedSource = (0, source_1.resolveSensorSource)(parsed, { registries: (0, registries_1.listRegistries)() });
211
231
  }
@@ -230,25 +250,108 @@ async function runSensors(opts = {}) {
230
250
  // manifest itself stays discoverable at the repo root via findManifestDir.
231
251
  // Legacy manifests never carry this field — manifestDir is always correct
232
252
  // for them, unchanged.
233
- const projectCwd = project.packageRoot;
253
+ const projectCwd = configured.packageRoot;
234
254
  const live = parsed.kind !== 'legacy' && resolvedSource && 'source' in resolvedSource
235
- ? await resolveLiveFromSource(projectCwd, resolvedSource.source, 'packSelection' in parsed.pack ? parsed.pack.packSelection : undefined)
255
+ ? await resolveLiveFromSource(projectCwd, resolvedSource.source, projectSensorsManifest && 'packSelection' in projectSensorsManifest ? projectSensorsManifest.packSelection : undefined)
236
256
  : null;
237
257
  const drift = parsed.kind === 'legacy' ? detectPackDrift(manifestDir, parsed.pack) : undefined;
238
258
  const requestedScope = opts.changed ? 'changed' : 'full';
239
259
  const prepared = [];
240
260
  const activeManifest = parsed.pack;
241
- for (const [name, sensor] of Object.entries(activeManifest.sensors)) {
261
+ const sensorsMap = activeManifest.sensors ?? {};
262
+ const packSelected = projectSensorsManifest !== null && hasSelectedPack(projectSensorsManifest);
263
+ for (const [name, sensor] of Object.entries(sensorsMap)) {
264
+ const entry = sensor;
265
+ const projectEntry = parsed.kind !== 'legacy' && (0, manifest_1.isProjectDeclaredSensor)(entry)
266
+ ? entry
267
+ : null;
268
+ // RF-1.7: collision when the live pack defines this id. When pack is selected
269
+ // but live cannot be resolved, fail closed — never run a project override that
270
+ // might be colliding with an unresolved pack sensor name.
271
+ const collidedWithPack = projectEntry !== null && (packSelected && live === null
272
+ ? true
273
+ : live?.pack.sensors[name] !== undefined);
274
+ if (collidedWithPack) {
275
+ invalidEntries.push({ name, reason: `project-sensor-name-collision: ${name}` });
276
+ // RF-1.7: keep the pack sensor when live is available; otherwise skip.
277
+ if (live === null || live.pack.sensors[name] === undefined)
278
+ continue;
279
+ // Fall through to pack-bound prepare using live.
280
+ }
281
+ else if (projectEntry !== null) {
282
+ const fast = projectEntry.fast ?? false;
283
+ if (!shouldRun(fast, opts))
284
+ continue;
285
+ const markers = [
286
+ ...(projectEntry.applicability?.allFiles ?? []),
287
+ ...(projectEntry.applicability?.anyFiles ?? []),
288
+ ];
289
+ const applicabilityPaths = markers.length > 0
290
+ ? (0, discovery_1.discoverPresentMarkers)(projectCwd, markers)
291
+ : undefined;
292
+ const execution = (0, prepare_1.prepareV2Sensor)({
293
+ name,
294
+ sensor: projectEntry,
295
+ requestedScope,
296
+ changed: changed ?? undefined,
297
+ projectTimeout: projectEntry.timeout,
298
+ applicabilityPaths,
299
+ });
300
+ prepared.push(projectEntry.enabled === false
301
+ ? { ...execution, command: undefined, syntheticStatus: 'skipped', syntheticReason: 'disabled' }
302
+ : execution);
303
+ continue;
304
+ }
242
305
  const liveSensor = parsed.kind !== 'legacy' ? live?.pack.sensors[name] : undefined;
306
+ const liveState = parsed.kind !== 'legacy' ? live?.sensors[name] : undefined;
307
+ let packBoundSensor = entry;
308
+ if (collidedWithPack && liveSensor) {
309
+ // Always reconstruct from live — never leave packBoundSensor as the project entry.
310
+ const fallbackVariant = liveSensor.variants[0];
311
+ if (!fallbackVariant)
312
+ continue;
313
+ const variantId = liveState?.variantId ?? fallbackVariant.id;
314
+ const variant = liveSensor.variants.find(candidate => candidate.id === variantId) ?? fallbackVariant;
315
+ const packSyntheticCompatibility = {
316
+ state: 'compatible-unverified',
317
+ reason: 'collision-pack-fallback',
318
+ variantId,
319
+ toolVersion: null,
320
+ runtimeVersion: null,
321
+ certifiedRange: null,
322
+ evidence: [],
323
+ };
324
+ const reconstructed = {
325
+ enabled: true,
326
+ ...(liveSensor.fast !== undefined ? { fast: liveSensor.fast } : {}),
327
+ ...(liveSensor.timeout !== undefined ? { timeout: liveSensor.timeout } : {}),
328
+ variantId,
329
+ command: variant.command,
330
+ initializedCompatibility: liveState ?? packSyntheticCompatibility,
331
+ };
332
+ packBoundSensor = reconstructed;
333
+ }
243
334
  const fast = parsed.kind !== 'legacy'
244
- ? sensor.fast ?? liveSensor?.fast ?? false
245
- : sensor.fast ?? false;
335
+ ? packBoundSensor.fast ?? liveSensor?.fast ?? false
336
+ : entry.fast ?? false;
246
337
  if (!shouldRun(fast, opts))
247
338
  continue;
248
339
  const execution = parsed.kind !== 'legacy'
249
- ? (0, prepare_1.prepareV2Sensor)({ name, sensor: sensor, liveSensor, liveState: live?.sensors[name], requestedScope, changed: changed ?? undefined })
250
- : (0, prepare_1.prepareLegacySensor)({ name, config: sensor, requestedScope, changed: changed ?? undefined });
251
- prepared.push(sensor.enabled === false
340
+ ? (0, prepare_1.prepareV2Sensor)({
341
+ name,
342
+ sensor: packBoundSensor,
343
+ liveSensor,
344
+ liveState,
345
+ requestedScope,
346
+ changed: changed ?? undefined,
347
+ })
348
+ : (0, prepare_1.prepareLegacySensor)({
349
+ name,
350
+ config: entry,
351
+ requestedScope,
352
+ changed: changed ?? undefined,
353
+ });
354
+ prepared.push(packBoundSensor.enabled === false
252
355
  ? { ...execution, command: undefined, syntheticStatus: 'skipped', syntheticReason: 'disabled' }
253
356
  : execution);
254
357
  }
@@ -268,9 +371,14 @@ async function runSensors(opts = {}) {
268
371
  overall,
269
372
  ...authority,
270
373
  mode: 'project-sensors',
271
- reason: parsed.kind === 'legacy' ? 'legacy-v1' : resolvedSource?.kind === 'logical' ? 'configured-v3' : resolvedSource?.kind,
374
+ reason: parsed.kind === 'legacy'
375
+ ? 'legacy-v1'
376
+ : onlyProject
377
+ ? 'configured-v3'
378
+ : resolvedSource?.kind === 'logical' ? 'configured-v3' : resolvedSource?.kind,
272
379
  ...(parsed.kind !== 'legacy' && resolvedSource && 'source' in resolvedSource
273
380
  ? { source: { kind: resolvedSource.kind, registry: resolvedSource.source.registry.name } } : {}),
381
+ ...(invalidEntries.length > 0 ? { invalidEntries } : {}),
274
382
  ...(drift?.drift ? { packDrift: drift.drift } : {}),
275
383
  ...(changed ? { changedScope: { files: changed.files.length, ...(changed.error ? { error: changed.error } : {}) } } : {}),
276
384
  };