agentic-workflow-manager 9.7.1 → 9.9.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 (107) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/job/exec-wrapper.js +53 -3
  3. package/dist/src/commands/job/gate.js +18 -0
  4. package/dist/src/commands/job/index.js +61 -0
  5. package/dist/src/commands/model-policy/index.js +73 -0
  6. package/dist/src/commands/plan/index.js +182 -7
  7. package/dist/src/commands/sensors/bootstrap.js +36 -5
  8. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  9. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  10. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  11. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  12. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  13. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  14. package/dist/src/commands/sensors/coverage/index.js +11 -2
  15. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  16. package/dist/src/commands/sensors/exec.js +16 -1
  17. package/dist/src/commands/sensors/migrate.js +62 -2
  18. package/dist/src/commands/sensors/prepare.js +2 -0
  19. package/dist/src/commands/sensors/result.js +2 -0
  20. package/dist/src/commands/sensors/run.js +64 -1
  21. package/dist/src/commands/sensors/status.js +6 -4
  22. package/dist/src/commands/watch/apply.js +32 -0
  23. package/dist/src/commands/watch/archive-unused.js +138 -0
  24. package/dist/src/commands/watch/generations.js +32 -9
  25. package/dist/src/commands/watch/index.js +108 -2
  26. package/dist/src/commands/watch/init.js +149 -18
  27. package/dist/src/commands/watch/supervisor.js +167 -13
  28. package/dist/src/commands/watch/teardown-driver.js +13 -13
  29. package/dist/src/commands/watch/track-state.js +59 -0
  30. package/dist/src/commands/watch/tracks.js +38 -102
  31. package/dist/src/core/admission/index.js +231 -0
  32. package/dist/src/core/admission/registry-contracts.js +290 -0
  33. package/dist/src/core/journal/fingerprint.js +35 -1
  34. package/dist/src/core/journal/paths.js +11 -0
  35. package/dist/src/core/journal/requests.js +1 -1
  36. package/dist/src/core/journal/store.js +146 -4
  37. package/dist/src/core/journal/types.js +57 -2
  38. package/dist/src/core/migration/historical-provenance.js +18 -0
  39. package/dist/src/core/migration/index.js +322 -0
  40. package/dist/src/core/model-policy/canonical.js +18 -0
  41. package/dist/src/core/model-policy/capabilities.js +197 -0
  42. package/dist/src/core/model-policy/capability-types.js +2 -0
  43. package/dist/src/core/model-policy/journal.js +124 -0
  44. package/dist/src/core/model-policy/paths.js +12 -0
  45. package/dist/src/core/model-policy/resolve.js +116 -0
  46. package/dist/src/core/model-policy/store.js +182 -0
  47. package/dist/src/core/model-policy/types.js +2 -0
  48. package/dist/src/core/model-policy/validate.js +112 -0
  49. package/dist/src/core/plan/identity.js +77 -0
  50. package/dist/src/core/plan/validate.js +191 -78
  51. package/dist/src/index.js +2 -0
  52. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  53. package/dist/tests/commands/job/gate-reconcile.test.js +40 -10
  54. package/dist/tests/commands/job/gate.test.js +9 -0
  55. package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
  56. package/dist/tests/commands/job/routing.test.js +11 -0
  57. package/dist/tests/commands/model-policy/index.test.js +57 -0
  58. package/dist/tests/commands/pin.test.js +23 -23
  59. package/dist/tests/commands/plan/index.test.js +682 -14
  60. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  61. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  62. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  63. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  64. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  65. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  66. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  67. package/dist/tests/commands/sensors/exec.test.js +112 -0
  68. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  69. package/dist/tests/commands/sensors/run.test.js +15 -2
  70. package/dist/tests/commands/sensors/status.test.js +3 -3
  71. package/dist/tests/commands/watch/apply.test.js +12 -0
  72. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  73. package/dist/tests/commands/watch/generations.test.js +113 -4
  74. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  75. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  76. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  77. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  78. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  79. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  80. package/dist/tests/core/admission/index.test.js +140 -0
  81. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  82. package/dist/tests/core/journal/store.test.js +53 -0
  83. package/dist/tests/core/journal/types.test.js +7 -0
  84. package/dist/tests/core/migration/facts.test.js +289 -0
  85. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  86. package/dist/tests/core/model-policy/capabilities.test.js +365 -0
  87. package/dist/tests/core/model-policy/journal.test.js +46 -0
  88. package/dist/tests/core/model-policy/policy.test.js +47 -0
  89. package/dist/tests/core/model-policy/resolve.test.js +111 -0
  90. package/dist/tests/core/model-policy/store.test.js +177 -0
  91. package/dist/tests/core/plan/validate.test.js +360 -8
  92. package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
  93. package/dist/tests/integration/pack-e2e.test.js +4 -1
  94. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  95. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  96. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  97. package/dist/tests/providers/injection-config.test.js +14 -6
  98. package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
  99. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  100. package/dist/tests/structural/native-release-artifacts.test.js +30 -0
  101. package/package.json +4 -3
  102. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  103. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  104. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  105. package/prebuilds/linux-x64/secure_fs.node +0 -0
  106. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  107. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -51,7 +51,7 @@ function declaration(mode, reason) {
51
51
  return { schemaVersion: 3, mode, reason };
52
52
  }
53
53
  function digest(bytes) { return (0, crypto_1.createHash)('sha256').update(bytes).digest('hex'); }
54
- async function projectSensors(projectRoot, registryRoot, requestedPack, packageRoot) {
54
+ async function projectSensors(projectRoot, registryRoot, requestedPack, packageRoot, explicitPackSelection = requestedPack !== undefined) {
55
55
  const detectionRoot = packageRoot ? path_1.default.resolve(projectRoot, packageRoot) : projectRoot;
56
56
  const detected = (0, detection_1.detectStack)(detectionRoot);
57
57
  const pack = requestedPack ?? detected.pack;
@@ -76,7 +76,9 @@ async function projectSensors(projectRoot, registryRoot, requestedPack, packageR
76
76
  return { reason: 'source-unsupported', remedy: 'install-a-v2-sensor-pack' };
77
77
  let live;
78
78
  try {
79
- live = await (0, live_1.resolveParsedPackCompatibility)(detectionRoot, parsed.pack);
79
+ live = explicitPackSelection
80
+ ? await (0, live_1.resolveParsedPackCompatibility)(detectionRoot, parsed.pack, { packSelection: 'explicit' })
81
+ : await (0, live_1.resolveParsedPackCompatibility)(detectionRoot, parsed.pack);
80
82
  }
81
83
  catch {
82
84
  return { reason: 'compatibility-unresolvable', remedy: 'repair-project-tools-or-select-another-mode' };
@@ -84,6 +86,8 @@ async function projectSensors(projectRoot, registryRoot, requestedPack, packageR
84
86
  const sensors = {};
85
87
  for (const [name, packSensor] of Object.entries(live.pack.sensors)) {
86
88
  const evidence = live.sensors[name];
89
+ if (evidence.state === 'not-applicable')
90
+ continue;
87
91
  const variant = evidence.variantId === null ? undefined : packSensor.variants.find(item => item.id === evidence.variantId);
88
92
  if (!variant)
89
93
  return { reason: 'sensor-variant-unresolvable', remedy: 'repair-project-tools-or-select-another-mode' };
@@ -109,8 +113,32 @@ async function planSensorBootstrap(cwd = process.cwd(), input = {}) {
109
113
  }
110
114
  return { kind: 'noop', projectRoot: project.projectRoot, manifestPath: project.manifestPath, changes: [], dryRun: opts.dryRun };
111
115
  }
112
- if (project.manifest.kind === 'legacy')
113
- return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'legacy-v1-preserved', 'migrate-the-manifest-explicitly');
116
+ if (project.manifest.kind === 'legacy') {
117
+ // Legacy manifests can carry arbitrary commands and per-sensor
118
+ // semantics that a v3 pack cannot prove equivalent. Replacing
119
+ // them is therefore a deliberate opt-in, never an automatic
120
+ // side effect of inspection or an unqualified bootstrap.
121
+ if (opts.mode !== 'project-sensors') {
122
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'legacy-replacement-requires-project-sensors-mode', 'rerun-bootstrap-with-mode-project-sensors');
123
+ }
124
+ // The legacy pack name identifies the source to migrate, but must not
125
+ // activate v2 sensors which require an explicit project opt-in.
126
+ const planned = await projectSensors(project.projectRoot, opts.registryRoot, project.manifest.pack.pack, undefined, false);
127
+ if ('reason' in planned)
128
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, planned.reason, planned.remedy, 'candidates' in planned ? planned.candidates : undefined);
129
+ try {
130
+ const original = fs_1.default.readFileSync(project.manifestPath);
131
+ return {
132
+ kind: 'migrate', projectRoot: project.projectRoot, manifestPath: project.manifestPath,
133
+ changes: planned.changes.map((change, index) => index === 0 ? { ...change, action: 'replace' } : change),
134
+ dryRun: opts.dryRun, migration: { kind: 'legacy-v1', manifest: planned.manifest }, source: planned.source,
135
+ originalDigest: digest(original),
136
+ };
137
+ }
138
+ catch {
139
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'legacy-manifest-unreadable', 'repair-the-manifest-before-bootstrap');
140
+ }
141
+ }
114
142
  let source;
115
143
  try {
116
144
  source = (0, source_1.resolveSensorSource)(project.manifest, { registries: (0, registries_1.listRegistries)() });
@@ -152,7 +180,10 @@ function applySensorBootstrap(plan) {
152
180
  const current = fs_1.default.readFileSync(plan.manifestPath);
153
181
  if (digest(current) !== plan.originalDigest)
154
182
  throw new Error('bootstrap migration plan is stale: manifest changed after planning');
155
- (0, migrate_2.replaceV2ManifestWithV3)(plan.manifestPath, plan.migration.candidate, plan.source);
183
+ if ('kind' in plan.migration)
184
+ (0, migrate_1.replaceLegacyManifestWithV3)(plan.manifestPath, plan.migration.manifest, plan.source);
185
+ else
186
+ (0, migrate_2.replaceV2ManifestWithV3)(plan.manifestPath, plan.migration.candidate, plan.source);
156
187
  return 'migrated';
157
188
  }
158
189
  if (plan.manifest.mode === 'project-sensors') {
@@ -216,10 +216,21 @@ function parseVariant(input, source, location) {
216
216
  if (semver_1.default.validRange(toolRange) === null || semver_1.default.validRange(runtimeRange) === null)
217
217
  invalid(source, `${location}.requirements ranges must be valid semver ranges`);
218
218
  const probe = policy ? undefined : record(value.probe, source, `${location}.probe`);
219
+ let probeScript;
219
220
  if (probe) {
220
- fields(probe, ['kind'], source, `${location}.probe`);
221
+ fields(probe, ['kind', 'script'], source, `${location}.probe`);
221
222
  if (typeof probe.kind !== 'string' || !ALLOWED_PROBES.has(probe.kind))
222
223
  invalid(source, `${location}.probe.kind must be an allowed probe`);
224
+ // Historical schema-v2 packs remain readable. An unnamed probe cannot
225
+ // certify compatibility; the probe runner rejects it as unverifiable.
226
+ if (probe.kind === 'package-script-present' && 'script' in probe) {
227
+ probeScript = text(probe.script, source, `${location}.probe.script`);
228
+ if (!/^[A-Za-z0-9][A-Za-z0-9:_-]*$/.test(probeScript))
229
+ invalid(source, `${location}.probe.script must be a package script name`);
230
+ }
231
+ else if (probe.kind !== 'package-script-present' && 'script' in probe) {
232
+ invalid(source, `${location}.probe.script is only valid for package-script-present`);
233
+ }
223
234
  }
224
235
  const changedCommand = 'changedCommand' in value ? parseStructuredCommand(value.changedCommand, source) : undefined;
225
236
  if (changedCommand && !changedCommand.fileInput)
@@ -234,7 +245,7 @@ function parseVariant(input, source, location) {
234
245
  invalid(source, `${location}.requirements.packageJsonFields[${index}] must be a stable package.json field`); return field; }) } : {}) },
235
246
  assets: assetArray(value.assets, source, `${location}.assets`, true),
236
247
  formatter: text(value.formatter, source, `${location}.formatter`),
237
- probe: { kind: policy?.probe ?? probe.kind },
248
+ probe: { kind: policy?.probe ?? probe.kind, ...(probeScript ? { script: probeScript } : {}) },
238
249
  ...(policy ? { policyRef: SEMGREP_POLICY_REF } : {}),
239
250
  command: parseStructuredCommand(value.command, source),
240
251
  ...(changedCommand ? { changedCommand } : {}),
@@ -295,8 +306,12 @@ function parseSensor(input, source, location, variantIds) {
295
306
  applicability.allFiles = stringArray(applicabilityInput.allFiles, source, `${location}.applicability.allFiles`).map((file, index) => asset(file, source, `${location}.applicability.allFiles[${index}]`));
296
307
  if ('anyFiles' in applicabilityInput)
297
308
  applicability.anyFiles = stringArray(applicabilityInput.anyFiles, source, `${location}.applicability.anyFiles`).map((file, index) => asset(file, source, `${location}.applicability.anyFiles[${index}]`));
298
- if ('kind' in applicabilityInput)
299
- applicability.kind = text(applicabilityInput.kind, source, `${location}.applicability.kind`);
309
+ if ('kind' in applicabilityInput) {
310
+ const kind = text(applicabilityInput.kind, source, `${location}.applicability.kind`);
311
+ if (kind !== 'explicit-or-supported-language' && kind !== 'explicit-opt-in')
312
+ invalid(source, `${location}.applicability.kind must be a supported applicability kind`);
313
+ applicability.kind = kind;
314
+ }
300
315
  if (Object.keys(applicability).length === 0)
301
316
  invalid(source, `${location}.applicability must declare a condition`);
302
317
  if ('fast' in value && typeof value.fast !== 'boolean')
@@ -176,6 +176,7 @@ function discoverProjectEvidence(cwd, pack, dependencies = {}) {
176
176
  const declaredManager = typeof pkg?.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
177
177
  if (declaredManager)
178
178
  lockManagers.add(declaredManager);
179
+ const packageManager = declaredManager ?? (lockManagers.size === 1 ? [...lockManagers][0] : null);
179
180
  const configCandidates = new Set(COMMON_CONFIGS);
180
181
  if ('schemaVersion' in pack) {
181
182
  for (const marker of pack.detects)
@@ -193,16 +194,35 @@ function discoverProjectEvidence(cwd, pack, dependencies = {}) {
193
194
  const packageJsonFields = Object.keys(pkg ?? {}).filter(field => /^[A-Za-z][A-Za-z0-9]*$/.test(field)).sort();
194
195
  const declaredToolRanges = { ...stringMap(pkg?.dependencies), ...stringMap(pkg?.devDependencies), ...stringMap(pkg?.peerDependencies) };
195
196
  const tools = new Set();
197
+ const pathTools = new Set();
196
198
  if ('schemaVersion' in pack)
197
199
  for (const sensor of Object.values(pack.sensors))
198
- for (const variant of sensor.variants)
200
+ for (const variant of sensor.variants) {
199
201
  tools.add(variant.requirements.tool);
202
+ if (variant.command?.resolution === 'path' && variant.command.packageManager === variant.requirements.tool)
203
+ pathTools.add(variant.requirements.tool);
204
+ }
200
205
  const environment = pythonEnvironment(root);
201
206
  const sitePackages = environment ? pythonSitePackages(root, environment.rootParts, targetPlatform) : [];
202
- const toolVersions = Object.fromEntries([...tools].sort().map(tool => [tool, pythonToolVersion(root, sitePackages, tool) ?? installedPackageVersion(root, tool)]));
207
+ const toolVersions = {};
208
+ const toolProvenance = {};
209
+ for (const tool of [...tools].sort()) {
210
+ if (pathTools.has(tool) && packageManager === tool) {
211
+ // Discovery/status is read-only. A PATH executable is not inspected or
212
+ // executed here; callers that already possess trusted runtime evidence
213
+ // may inject it explicitly for an execution-time compatibility check.
214
+ toolVersions[tool] = exactVersion(dependencies.pathToolVersion?.(tool) ?? null);
215
+ toolProvenance[tool] = toolVersions[tool] === null ? null : 'path';
216
+ }
217
+ else {
218
+ const pythonVersion = pythonToolVersion(root, sitePackages, tool);
219
+ toolVersions[tool] = pythonVersion ?? installedPackageVersion(root, tool);
220
+ toolProvenance[tool] = pythonVersion ? 'python-environment' : toolVersions[tool] ? 'node-modules-bin' : null;
221
+ }
222
+ }
203
223
  return {
204
224
  cwd: root, os: targetPlatform, runtimeVersions: { node: process.versions.node ?? null, ...(environment ? { python: environment.runtimeVersion } : {}) }, pythonEnvironmentRoot: environment?.rootParts[0] ?? null, declaredToolRanges, toolVersions,
205
- packageManager: declaredManager ?? (lockManagers.size === 1 ? [...lockManagers][0] : null), packageManagerConflict: lockManagers.size > 1,
225
+ toolProvenance, packageManager, packageManagerConflict: lockManagers.size > 1,
206
226
  scripts, configFiles, packageJsonFields, paths: [...new Set([...(safeFile(root, 'package.json') ? ['package.json'] : []), ...locks, ...configFiles])].sort(),
207
227
  };
208
228
  }
@@ -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.bindContainedRuntimeCommands = bindContainedRuntimeCommands;
6
7
  exports.resolveLiveCompatibility = resolveLiveCompatibility;
7
8
  exports.resolveParsedPackCompatibility = resolveParsedPackCompatibility;
8
9
  const pack_source_1 = require("./pack-source");
@@ -11,6 +12,22 @@ const discovery_1 = require("./discovery");
11
12
  const resolve_1 = require("./resolve");
12
13
  const probe_1 = require("./probe");
13
14
  const path_1 = __importDefault(require("path"));
15
+ const child_process_1 = require("child_process");
16
+ const semver_1 = __importDefault(require("semver"));
17
+ /** Execution-time evidence only. Status/discovery never invokes this resolver. */
18
+ function pathPackageManagerVersion(tool) {
19
+ if (!['npm', 'pnpm', 'yarn', 'bun'].includes(tool))
20
+ return null;
21
+ try {
22
+ const result = (0, child_process_1.spawnSync)(tool, ['--version'], { cwd: process.cwd(), shell: false, encoding: 'utf8', timeout: 5_000, maxBuffer: 1024, windowsHide: true, stdio: 'pipe' });
23
+ if (result.error || result.status !== 0 || result.signal || typeof result.stdout !== 'string' || result.stdout.length > 256)
24
+ return null;
25
+ return semver_1.default.valid(result.stdout.trim());
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
14
31
  /**
15
32
  * Python package metadata is evidence only for a contained virtual environment.
16
33
  * A v2 command that names a Python-runtime tool must therefore resolve through
@@ -68,7 +85,7 @@ async function resolveParsedPackCompatibility(cwd, pack, options = {}) {
68
85
  throw new Error('cwd must be a non-empty path');
69
86
  if (!pack || typeof pack !== 'object' || pack.schemaVersion !== 2)
70
87
  throw new Error('pack must be a parsed v2 sensor pack');
71
- const evidence = (0, discovery_1.discoverProjectEvidence)(cwd, pack);
88
+ const evidence = (0, discovery_1.discoverProjectEvidence)(cwd, pack, { pathToolVersion: pathPackageManagerVersion });
72
89
  const executionPack = bindContainedRuntimeCommands(pack, evidence.pythonEnvironmentRoot);
73
90
  const resolutionEvidence = { ...evidence, ...(options.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}) };
74
91
  const initial = (0, resolve_1.resolveProjectCompatibility)(executionPack, resolutionEvidence).sensors;
@@ -48,8 +48,14 @@ async function runCompatibilityProbe(probe, evidence, executor = exec_1.runStruc
48
48
  throw new Error('compatibility probe kind must be allowed');
49
49
  if (!evidence || typeof evidence.cwd !== 'string' || evidence.cwd.trim() === '')
50
50
  throw new Error('probe evidence requires cwd');
51
- if (probe.kind === 'package-script-present')
52
- return { status: (evidence.scripts?.length ?? 0) > 0 ? 'matched' : 'not-matched', reason: 'package-script' };
51
+ if (probe.kind === 'package-script-present') {
52
+ const script = probe.script;
53
+ if (!Object.prototype.hasOwnProperty.call(probe, 'script'))
54
+ return { status: 'unverifiable', reason: 'package-script-name-required' };
55
+ if (typeof script !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:_-]*$/.test(script))
56
+ throw new Error('package-script-present probe requires a named package script');
57
+ return { status: evidence.scripts?.includes(script) ? 'matched' : 'not-matched', reason: 'package-script' };
58
+ }
53
59
  if (probe.kind === 'config-present')
54
60
  return { status: (evidence.configFiles?.length ?? 0) > 0 ? 'matched' : 'not-matched', reason: 'config-file' };
55
61
  try {
@@ -26,7 +26,7 @@ function result(state, reason, variant, evidence) {
26
26
  return { state, reason, variantId: variant?.id ?? null, toolVersion, runtimeVersion, certifiedRange: variant?.certifiedRange ?? null, evidence: refs.slice(0, 32) };
27
27
  }
28
28
  function applies(sensor, evidence) {
29
- if (sensor.applicability.kind === 'explicit-or-supported-language') {
29
+ if (sensor.applicability.kind === 'explicit-or-supported-language' || sensor.applicability.kind === 'explicit-opt-in') {
30
30
  return evidence.applicable === true || evidence.packSelection === 'explicit';
31
31
  }
32
32
  if (evidence.applicable === false)
@@ -40,7 +40,10 @@ function specificity(variant) {
40
40
  }
41
41
  function validVersion(value) { return typeof value === 'string' && semver_1.default.valid(value) !== null; }
42
42
  function toolFor(variant, evidence) {
43
- return evidence.toolVersions === undefined ? evidence.toolVersion : evidence.toolVersions[variant.requirements.tool];
43
+ const tool = variant.requirements.tool;
44
+ const version = evidence.toolVersions === undefined ? evidence.toolVersion : evidence.toolVersions[tool];
45
+ const provenance = evidence.toolProvenance?.[tool];
46
+ return provenance !== undefined && provenance !== null && provenance !== variant.command.resolution ? null : version;
44
47
  }
45
48
  function runtimeFor(variant, evidence) {
46
49
  return evidence.runtimeVersions === undefined ? evidence.runtimeVersion : evidence.runtimeVersions[variant.requirements.runtime];
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.observeDetector = observeDetector;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const native_bridge_1 = require("../../../core/secure-fs/native-bridge");
9
10
  const contract_1 = require("./contract");
10
11
  const realIo = {
11
12
  lstatSync: fs_1.default.lstatSync,
@@ -39,23 +40,31 @@ function inspectFile(root, relative, markers, io) {
39
40
  return { status: 'unverifiable', evidence: [{ kind: 'file', path: relative, status: 'unverifiable' }] };
40
41
  }
41
42
  const noFollow = fs_1.default.constants.O_NOFOLLOW;
42
- if (typeof noFollow !== 'number') {
43
+ if (typeof noFollow !== 'number' && io !== realIo) {
43
44
  return { status: 'unverifiable', evidence: [{ kind: 'file', path: relative, status: 'unverifiable' }] };
44
45
  }
45
46
  let fd;
46
47
  let content;
47
48
  try {
48
- fd = (io.openSync ?? realIo.openSync)(absolute, fs_1.default.constants.O_RDONLY | noFollow);
49
- const opened = (io.fstatSync ?? realIo.fstatSync)(fd);
50
- if (!opened.isFile() || opened.size > contract_1.MAX_COVERAGE_FILE_BYTES) {
51
- return { status: 'unverifiable', evidence: [{ kind: 'file', path: relative, status: 'unverifiable' }] };
49
+ if (typeof noFollow !== 'number') {
50
+ // Windows has no O_NOFOLLOW. Its native descriptor-anchored read
51
+ // rejects reparse points in every ancestor and preserves identity.
52
+ // Never replace an injected IO boundary with this native trust.
53
+ content = native_bridge_1.secureFs.readRegularFile(absolute, contract_1.MAX_COVERAGE_FILE_BYTES).bytes.toString('utf8');
52
54
  }
53
- const buffer = Buffer.allocUnsafe(contract_1.MAX_COVERAGE_FILE_BYTES + 1);
54
- const bytesRead = (io.readSync ?? realIo.readSync)(fd, buffer, 0, buffer.length, null);
55
- if (!Number.isSafeInteger(bytesRead) || bytesRead < 0 || bytesRead > contract_1.MAX_COVERAGE_FILE_BYTES) {
56
- return { status: 'unverifiable', evidence: [{ kind: 'file', path: relative, status: 'unverifiable' }] };
55
+ else {
56
+ fd = (io.openSync ?? realIo.openSync)(absolute, fs_1.default.constants.O_RDONLY | noFollow);
57
+ const opened = (io.fstatSync ?? realIo.fstatSync)(fd);
58
+ if (!opened.isFile() || opened.size > contract_1.MAX_COVERAGE_FILE_BYTES) {
59
+ return { status: 'unverifiable', evidence: [{ kind: 'file', path: relative, status: 'unverifiable' }] };
60
+ }
61
+ const buffer = Buffer.allocUnsafe(contract_1.MAX_COVERAGE_FILE_BYTES + 1);
62
+ const bytesRead = (io.readSync ?? realIo.readSync)(fd, buffer, 0, buffer.length, null);
63
+ if (!Number.isSafeInteger(bytesRead) || bytesRead < 0 || bytesRead > contract_1.MAX_COVERAGE_FILE_BYTES) {
64
+ return { status: 'unverifiable', evidence: [{ kind: 'file', path: relative, status: 'unverifiable' }] };
65
+ }
66
+ content = buffer.subarray(0, bytesRead).toString('utf8');
57
67
  }
58
- content = buffer.subarray(0, bytesRead).toString('utf8');
59
68
  }
60
69
  catch {
61
70
  return { status: 'unverifiable', evidence: [{ kind: 'file', path: relative, status: 'unverifiable' }] };
@@ -12,6 +12,7 @@ const manifest_1 = require("../compatibility/manifest");
12
12
  const scan_1 = require("../../../core/ledger/scan");
13
13
  const empirical_1 = require("./empirical");
14
14
  const path_1 = __importDefault(require("path"));
15
+ const project_1 = require("../project");
15
16
  const defaults = { resolve: resolve_1.resolveCoverageInputs, observe: evidence_1.observeDetector, resolveLive: live_1.resolveLiveCompatibility, scan: scan_1.scanProjectLedgers };
16
17
  function normalizedMin(value) {
17
18
  if (value === undefined)
@@ -34,13 +35,21 @@ async function runCoverage(cwd, dependencies = {}, options = {}) {
34
35
  return { schemaVersion: 2, pack: null, registry: null, overall: 'inconclusive', static: { status: 'inconclusive', reason: 'not_configured', classes: [] },
35
36
  empirical: empirical(deps, path_1.default.resolve(cwd), {}, min, 'unavailable') };
36
37
  }
38
+ const packageRoot = input.manifest.kind !== 'legacy' && input.manifest.pack.packageRoot
39
+ ? (() => {
40
+ const project = (0, project_1.resolveSensorProject)(input.projectRoot);
41
+ if (project.state !== 'configured')
42
+ throw new Error(`runCoverage: packageRoot cannot be resolved: ${project.state === 'invalid' ? project.reason : 'manifest missing'}`);
43
+ return project.packageRoot;
44
+ })()
45
+ : input.projectRoot;
37
46
  if (input.kind === 'no_reference') {
38
47
  return { schemaVersion: 2, pack: input.pack, registry: input.registry, overall: 'inconclusive', static: { status: 'inconclusive', reason: 'no_reference', classes: [] },
39
48
  empirical: empirical(deps, input.projectRoot, {}, min, 'unavailable') };
40
49
  }
41
50
  let live;
42
51
  if (input.manifest.kind !== 'legacy') {
43
- live = (await deps.resolveLive(input.projectRoot, input.pack, input.registryRoot)).sensors;
52
+ live = (await deps.resolveLive(packageRoot, input.pack, input.registryRoot)).sensors;
44
53
  }
45
54
  else {
46
55
  live = Object.fromEntries(Object.keys(input.manifest.pack.sensors).map((name) => [name, (0, manifest_1.legacyCompatibility)('legacy manifest without schemaVersion')]));
@@ -48,7 +57,7 @@ async function runCoverage(cwd, dependencies = {}, options = {}) {
48
57
  const observations = [];
49
58
  for (const [classId, coverageClass] of Object.entries(input.contract.classes)) {
50
59
  coverageClass.detectors.forEach((detector, detectorIndex) => {
51
- const observed = deps.observe(input.projectRoot, classId, detectorIndex, detector, input.manifest.pack.sensors[detector.sensor]);
60
+ const observed = deps.observe(packageRoot, classId, detectorIndex, detector, input.manifest.pack.sensors[detector.sensor]);
52
61
  observations.push({
53
62
  ...observed,
54
63
  compatibility: live[detector.sensor] ?? {
@@ -70,6 +70,16 @@ function findManifestDirNoFollow(startCwd) {
70
70
  if (error.code !== 'ENOENT')
71
71
  throw readFailure(manifestPath, error);
72
72
  }
73
+ const gitMarker = path_1.default.join(dir, '.git');
74
+ try {
75
+ const marker = fs_1.default.lstatSync(gitMarker);
76
+ if (marker.isFile() || marker.isDirectory())
77
+ return null;
78
+ }
79
+ catch (error) {
80
+ if (error.code !== 'ENOENT')
81
+ throw readFailure(gitMarker, error);
82
+ }
73
83
  const parent = path_1.default.dirname(dir);
74
84
  if (parent === dir)
75
85
  return null;
@@ -98,6 +98,20 @@ function validateOptions(opts) {
98
98
  if (opts.killGraceMs !== undefined && (!Number.isSafeInteger(opts.killGraceMs) || opts.killGraceMs < 0))
99
99
  throw new Error('exec options killGraceMs must be a non-negative safe integer');
100
100
  }
101
+ /** Run sensor commands with the same Node runtime that launched AWM. Package
102
+ * manager wrappers resolve `node` through PATH, so an older inherited Node can
103
+ * otherwise run a sensor even when the CLI itself is running on a supported
104
+ * runtime. Keep every inherited entry after the runtime directory: tools and
105
+ * project-local shims remain available to the child. */
106
+ function sensorEnvironment(inputEnvironment) {
107
+ const runtimeDir = path_1.default.dirname(process.execPath);
108
+ const inheritedPath = process.env.PATH;
109
+ return {
110
+ ...process.env,
111
+ PATH: inheritedPath ? `${runtimeDir}${path_1.default.delimiter}${inheritedPath}` : runtimeDir,
112
+ ...inputEnvironment,
113
+ };
114
+ }
101
115
  function collectSpawn(input, opts) {
102
116
  validateOptions(opts);
103
117
  const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
@@ -164,6 +178,7 @@ function collectSpawn(input, opts) {
164
178
  shell: true,
165
179
  cwd: opts.cwd,
166
180
  detached: !(0, paths_1.isWindowsNative)(),
181
+ env: sensorEnvironment(input.environment),
167
182
  stdio: ['ignore', 'pipe', 'pipe'],
168
183
  })
169
184
  : (0, child_process_1.spawn)(prlimit ?? input.executable, prlimit
@@ -172,7 +187,7 @@ function collectSpawn(input, opts) {
172
187
  shell: false,
173
188
  cwd: opts.cwd,
174
189
  detached: !(0, paths_1.isWindowsNative)(),
175
- ...(input.environment ? { env: { ...process.env, ...input.environment } } : {}),
190
+ env: sensorEnvironment(input.environment),
176
191
  // stdin closed: a sensor must never block waiting for input, and the
177
192
  // EOF also tells watch-mode-capable tools (vitest, jest) to run once.
178
193
  stdio: prlimit ? ['ignore', stdoutFd, stderrFd] : ['ignore', 'pipe', 'pipe'],
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.planV2Migration = planV2Migration;
7
7
  exports.replaceV2ManifestWithV3 = replaceV2ManifestWithV3;
8
+ exports.replaceLegacyManifestWithV3 = replaceLegacyManifestWithV3;
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const path_1 = __importDefault(require("path"));
10
11
  const contract_1 = require("./compatibility/contract");
@@ -67,7 +68,7 @@ function exactLogicalSource(input, pack) {
67
68
  throw new Error('v2 migration source must contain an exact resolved pack');
68
69
  let parsedSource;
69
70
  try {
70
- parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), 'v2 migration resolved source');
71
+ parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
71
72
  }
72
73
  catch {
73
74
  throw new Error('v2 migration source exact v2 pack is invalid');
@@ -79,7 +80,7 @@ function exactLogicalSource(input, pack) {
79
80
  function assertSourceCompatibleWithManifest(source, manifest) {
80
81
  let parsedSource;
81
82
  try {
82
- parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), 'v2 migration resolved source');
83
+ parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
83
84
  }
84
85
  catch {
85
86
  throw new Error('v2 migration source exact v2 pack is invalid');
@@ -94,6 +95,23 @@ function assertSourceCompatibleWithManifest(source, manifest) {
94
95
  }
95
96
  }
96
97
  }
98
+ function assertSourceCompatibleWithCandidate(source, candidate) {
99
+ let parsedSource;
100
+ try {
101
+ parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
102
+ }
103
+ catch {
104
+ throw new Error('legacy migration source exact v2 pack is invalid');
105
+ }
106
+ if (parsedSource.kind !== 'v2' || parsedSource.pack.name !== candidate.pack) {
107
+ throw new Error('legacy migration source must contain the exact v2 pack');
108
+ }
109
+ for (const [name, sensor] of Object.entries(candidate.sensors)) {
110
+ if (!parsedSource.pack.sensors[name]?.variants.some(variant => variant.id === sensor.variantId)) {
111
+ throw new Error('legacy migration source is not compatible with selected manifest sensors');
112
+ }
113
+ }
114
+ }
97
115
  function hasPhysicalSensorPath(value, registryRoots) {
98
116
  if (typeof value === 'string') {
99
117
  const normalized = path_1.default.posix.normalize(value.replace(/\\/g, '/')).toLowerCase();
@@ -207,3 +225,45 @@ function replaceV2ManifestWithV3(manifestPath, candidate, source) {
207
225
  });
208
226
  });
209
227
  }
228
+ /**
229
+ * Replace an explicitly bootstrapped legacy manifest with the v3 declaration
230
+ * selected from one current logical pack. Legacy commands have no structured
231
+ * variant identity, so equivalence cannot be asserted; the caller must have
232
+ * re-probed every selected v3 sensor before this fenced publication.
233
+ */
234
+ function replaceLegacyManifestWithV3(manifestPath, candidate, source) {
235
+ if (typeof manifestPath !== 'string' || !path_1.default.isAbsolute(manifestPath) || path_1.default.normalize(manifestPath) !== manifestPath
236
+ || manifestPath.includes('\0') || path_1.default.basename(manifestPath) !== 'sensors.json'
237
+ || path_1.default.basename(path_1.default.dirname(manifestPath)) !== '.awm') {
238
+ throw new Error('legacy migration manifest path must be the canonical project manifest <project>/.awm/sensors.json');
239
+ }
240
+ const manifestDirectory = path_1.default.dirname(manifestPath);
241
+ const projectRoot = path_1.default.dirname(manifestDirectory);
242
+ (0, safe_file_1.withProjectLease)(projectRoot, () => {
243
+ const originalFailure = (_reason) => new Error('legacy migration original manifest must be readable JSON');
244
+ let inspected;
245
+ let original;
246
+ try {
247
+ inspected = inspectManifest(manifestPath, originalFailure);
248
+ original = JSON.parse(inspected.content.toString('utf8'));
249
+ }
250
+ catch {
251
+ throw new Error('legacy migration original manifest must be readable JSON');
252
+ }
253
+ const before = (0, manifest_1.parseSensorManifest)(original, manifestPath);
254
+ const after = (0, manifest_1.parseSensorManifest)(candidate, 'legacy migration candidate');
255
+ if (before.kind !== 'legacy' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
256
+ throw new Error('legacy migration candidate semantic mismatch');
257
+ }
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, [])) {
262
+ throw new Error('legacy migration candidate contains an unsafe source or physical path');
263
+ }
264
+ assertManifestUnchanged(manifestPath, inspected);
265
+ (0, safe_file_1.writeProjectFile)(projectRoot, '.awm/sensors.json', Buffer.from((0, manifest_1.serializeManifestV3)(after.pack), 'utf8'), {
266
+ mode: 'replace', expected: inspected.content, expectedIdentity: inspected.identity, createParents: false,
267
+ });
268
+ });
269
+ }
@@ -30,6 +30,8 @@ function validateRunOptions(opts) {
30
30
  throw new Error('run option ignoreBaseline must be a boolean');
31
31
  if (opts.base !== undefined && (typeof opts.base !== 'string' || opts.base.trim() === ''))
32
32
  throw new Error('run option base must be a nonempty string');
33
+ if (opts.readOnly !== undefined && typeof opts.readOnly !== 'boolean')
34
+ throw new Error('run option readOnly must be a boolean');
33
35
  if (opts.changed && opts.ignoreBaseline) {
34
36
  throw new Error('refusing to combine --changed with a baseline capture: a partial run cannot define the accepted set');
35
37
  }
@@ -91,6 +91,8 @@ function interpretResult(prepared, raw) {
91
91
  return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: reason });
92
92
  }
93
93
  if (raw.code === 0) {
94
+ if (format === generic_1.parseGenericOutput)
95
+ return withEvidence({ name: prepared.name, status: 'pass', errors: [] });
94
96
  const errors = format(raw.stdout);
95
97
  return withEvidence({ name: prepared.name, status: errors.length ? 'fail' : 'pass', errors });
96
98
  }
@@ -8,8 +8,10 @@ exports.findManifestDir = findManifestDir;
8
8
  exports.detectPackDrift = detectPackDrift;
9
9
  exports.resolveConcurrency = resolveConcurrency;
10
10
  exports.runSensors = runSensors;
11
+ const fs_1 = __importDefault(require("fs"));
11
12
  const os_1 = __importDefault(require("os"));
12
13
  const path_1 = __importDefault(require("path"));
14
+ const crypto_1 = __importDefault(require("crypto"));
13
15
  const baseline_1 = require("./baseline");
14
16
  const result_1 = require("./result");
15
17
  Object.defineProperty(exports, "applyBaseline", { enumerable: true, get: function () { return result_1.applyBaseline; } });
@@ -30,6 +32,59 @@ const project_1 = require("./project");
30
32
  const MAX_BUFFER = 64 * 1024 * 1024;
31
33
  /** Hard ceiling on parallel sensors: past this, they only contend for the same cores. */
32
34
  const MAX_CONCURRENCY = 4;
35
+ const READ_ONLY_MAX_ENTRIES = 250_000;
36
+ const READ_ONLY_MAX_BYTES = 1024 * 1024 * 1024;
37
+ /**
38
+ * A git status only sees tracked and non-ignored paths, while a sensor can
39
+ * mutate ignored runtime files (notably `.awm/`) just as easily. Hash the
40
+ * actual project tree instead. The scan is deliberately bounded: inability to
41
+ * observe the whole declared project means no admission certificate.
42
+ */
43
+ function filesystemSnapshot(root) {
44
+ try {
45
+ const digest = crypto_1.default.createHash('sha256');
46
+ let entriesSeen = 0;
47
+ let bytesSeen = 0;
48
+ const visit = (directory, relative) => {
49
+ const entries = fs_1.default.readdirSync(directory, { withFileTypes: true })
50
+ .sort((left, right) => left.name.localeCompare(right.name));
51
+ for (const entry of entries) {
52
+ // Git's internal bookkeeping changes as a consequence of normal
53
+ // observation, not project sensor execution. Everything else,
54
+ // including ignored/untracked `.awm`, is evidence-relevant.
55
+ if (relative === '' && entry.name === '.git')
56
+ continue;
57
+ if (++entriesSeen > READ_ONLY_MAX_ENTRIES)
58
+ throw new Error('snapshot-entry-limit');
59
+ const child = path_1.default.join(directory, entry.name);
60
+ const childRelative = relative === '' ? entry.name : `${relative}/${entry.name}`;
61
+ const stat = fs_1.default.lstatSync(child);
62
+ digest.update(`${childRelative}\0${stat.mode}\0${stat.size}\0`);
63
+ if (stat.isDirectory()) {
64
+ digest.update('directory\0');
65
+ visit(child, childRelative);
66
+ }
67
+ else if (stat.isFile()) {
68
+ if ((bytesSeen += stat.size) > READ_ONLY_MAX_BYTES)
69
+ throw new Error('snapshot-byte-limit');
70
+ digest.update('file\0');
71
+ digest.update(fs_1.default.readFileSync(child));
72
+ }
73
+ else if (stat.isSymbolicLink()) {
74
+ digest.update(`symlink\0${fs_1.default.readlinkSync(child)}\0`);
75
+ }
76
+ else {
77
+ digest.update(`special\0${stat.rdev}\0`);
78
+ }
79
+ }
80
+ };
81
+ visit(root, '');
82
+ return digest.digest('hex');
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
33
88
  async function resolveLiveFromSource(cwd, source, packSelection) {
34
89
  try {
35
90
  const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
@@ -141,6 +196,10 @@ async function runSensors(opts = {}) {
141
196
  return { sensors: [], overall: 'not_certified', mode: 'invalid', reason, projectRoot: project.projectRoot, manifestPath: project.manifestPath, remedy: 'repair-sensor-manifest' };
142
197
  }
143
198
  const manifestDir = project.projectRoot;
199
+ const beforeReadOnly = opts.readOnly ? filesystemSnapshot(manifestDir) : undefined;
200
+ if (opts.readOnly && beforeReadOnly === null) {
201
+ 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' };
202
+ }
144
203
  const parsed = project.manifest;
145
204
  const authority = { projectRoot: project.projectRoot, manifestPath: project.manifestPath };
146
205
  if (parsed.kind === 'v3' && (parsed.pack.mode === 'native-gate' || parsed.pack.mode === 'opt-out')) {
@@ -205,7 +264,7 @@ async function runSensors(opts = {}) {
205
264
  overall = 'not_certified';
206
265
  if (overall === 'skipped' && drift && drift.detection.pack !== 'generic')
207
266
  overall = 'not_certified';
208
- return {
267
+ const output = {
209
268
  sensors: results,
210
269
  overall,
211
270
  ...authority,
@@ -216,4 +275,8 @@ async function runSensors(opts = {}) {
216
275
  ...(drift?.drift ? { packDrift: drift.drift } : {}),
217
276
  ...(changed ? { changedScope: { files: changed.files.length, ...(changed.error ? { error: changed.error } : {}) } } : {}),
218
277
  };
278
+ if (opts.readOnly && filesystemSnapshot(manifestDir) !== beforeReadOnly) {
279
+ return { ...output, overall: 'not_certified', reason: 'read-only-mutation-detected', remedy: 'sensor execution changed the worktree; repair it before using this evidence for admission' };
280
+ }
281
+ return output;
219
282
  }