agentic-workflow-manager 9.20.4 → 9.22.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 (42) hide show
  1. package/dist/src/commands/change/index.js +14 -3
  2. package/dist/src/commands/preflight/checks.js +31 -0
  3. package/dist/src/commands/sensors/compatibility/contract.js +56 -3
  4. package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
  5. package/dist/src/commands/sensors/index.js +10 -1
  6. package/dist/src/commands/sensors/init.js +7 -1
  7. package/dist/src/commands/sensors/inputs-digest.js +315 -0
  8. package/dist/src/commands/sensors/prepare.js +10 -0
  9. package/dist/src/commands/sensors/result-cache.js +278 -0
  10. package/dist/src/commands/sensors/result.js +31 -12
  11. package/dist/src/commands/sensors/run.js +265 -8
  12. package/dist/src/commands/sensors/status.js +66 -7
  13. package/dist/src/commands/watch/init.js +3 -15
  14. package/dist/src/core/awm-gitignore.js +211 -0
  15. package/dist/src/core/changes/receipt.js +219 -9
  16. package/dist/src/core/diagnostics/checks.js +11 -0
  17. package/dist/tests/commands/change/new.test.js +1 -1
  18. package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
  19. package/dist/tests/commands/preflight/awm-gitignore.test.js +52 -0
  20. package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
  21. package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
  22. package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
  23. package/dist/tests/commands/sensors/init-gitignore.test.js +59 -0
  24. package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
  25. package/dist/tests/commands/sensors/result-cache.test.js +352 -0
  26. package/dist/tests/commands/sensors/run-cache.test.js +434 -0
  27. package/dist/tests/commands/sensors/run-slice.test.js +171 -0
  28. package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
  29. package/dist/tests/commands/watch/init-gitignore.test.js +34 -0
  30. package/dist/tests/commands/watch/track-freeze.test.js +11 -7
  31. package/dist/tests/commands/watch/watch-init.test.js +3 -1
  32. package/dist/tests/core/awm-gitignore.test.js +58 -0
  33. package/dist/tests/core/changes/receipt.test.js +261 -2
  34. package/dist/tests/core/diagnostics/awm-gitignore.test.js +101 -0
  35. package/dist/tests/core/ledger/gitignore.test.js +6 -1
  36. package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +102 -0
  37. package/dist/tests/structural/sensor-slice-decision-contract.test.js +63 -0
  38. package/package.json +2 -1
  39. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  40. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  41. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  42. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -23,7 +23,8 @@ const record_io_1 = require("../../core/changes/record-io");
23
23
  const text_safety_1 = require("../../core/changes/text-safety");
24
24
  const types_1 = require("../../core/changes/types");
25
25
  const validate_shape_1 = require("../../core/changes/validate-shape");
26
- const REMEDY_RECEIPT = 'commit your work, then run awm sensors run';
26
+ /** RF-4.7: full-gate run; cache may reuse when inputs unchanged — never claim every binary must re-exec. */
27
+ const REMEDY_RECEIPT = 'commit your work, then run full-gate awm sensors run (cache may reuse unchanged sensors)';
27
28
  const REMEDY_SPLIT = 'split the change into smaller ones';
28
29
  const REMEDY_SENSORS = 'reduce the sensors listed in .awm/sensors.json (the sensor receipt makes the record too large)';
29
30
  /** R3.4: the remedy that names the cause of an oversized record: the sensor receipt when it outweighs the module list, else the change itself. */
@@ -257,8 +258,18 @@ function failure(json, code, message, remedy) {
257
258
  const deriveOnce = (run) => (run.derived ??= (0, derive_1.deriveFields)(run.root, run.mergeBaseSha));
258
259
  function lateFiles(run, commit) {
259
260
  if (!run.late.has(commit)) {
260
- const files = (0, git_1.changedFiles)(run.root, commit, run.head)?.filter((f) => !f.startsWith(`${types_1.RECORD_DIR}/`));
261
- run.late.set(commit, files === undefined ? null : { count: files.length, first: files.slice(0, MAX_LATE_PATHS) });
261
+ const raw = (0, git_1.changedFiles)(run.root, commit, run.head);
262
+ if (raw === null) {
263
+ run.late.set(commit, null);
264
+ }
265
+ else {
266
+ // RF-4.5/4.6: when a schema-2 receipt covers every sensor with authored inputs,
267
+ // measured ∪ always-measured ∪ inputs-glob rematch count; otherwise schema-1 rules.
268
+ const receiptRead = (0, receipt_1.readReceipt)(run.root, commit);
269
+ const receipt = receiptRead.ok ? receiptRead.receipt : null;
270
+ const files = (0, receipt_1.filterLateFilesForReceipt)(raw, receipt, types_1.RECORD_DIR);
271
+ run.late.set(commit, { count: files.length, first: files.slice(0, MAX_LATE_PATHS) });
272
+ }
262
273
  }
263
274
  return run.late.get(commit) ?? null;
264
275
  }
@@ -19,6 +19,7 @@ const registries_2 = require("../../core/registries");
19
19
  const check_1 = require("../../core/currentness/check");
20
20
  const store_1 = require("../../core/model-policy/store");
21
21
  const setup_readiness_1 = require("../../core/model-policy/setup-readiness");
22
+ const awm_gitignore_1 = require("../../core/awm-gitignore");
22
23
  const MANIFEST = path_1.default.join('.awm', 'sensors.json');
23
24
  function checkRoutingSetup(cwd) {
24
25
  const guidance = (0, setup_readiness_1.routingSetupGuidance)((0, store_1.readEffectivePolicy)(cwd));
@@ -325,6 +326,24 @@ function checkSensorsBaseline(cwd, status) {
325
326
  + 'so the gate only chases new problems',
326
327
  };
327
328
  }
329
+ /**
330
+ * RF-3.4 — advisory only: enabled sensors without effective `inputs` are not cacheable.
331
+ * Sourced from `computeSensorStatus().warnings` so status and preflight stay aligned.
332
+ * Omitted when there is nothing to warn about (including opt-out / native-gate).
333
+ */
334
+ function checkSensorsInputs(status) {
335
+ const warnings = status.warnings ?? [];
336
+ if (warnings.length === 0)
337
+ return null;
338
+ const names = warnings.map(entry => entry.sensor).join(', ');
339
+ return {
340
+ id: 'sensors-inputs',
341
+ ok: false,
342
+ advisory: true,
343
+ detail: `${names} lack effective inputs and are not cacheable`,
344
+ remedy: 'declare inputs on those sensors (project override or pack variant) so results can be reused',
345
+ };
346
+ }
328
347
  /**
329
348
  * Empirical diagnostics intentionally name only stable execution facts. Sensor output
330
349
  * can include source, environment values, or tool-specific unbounded text; preflight
@@ -441,6 +460,12 @@ function extractHost(remote) {
441
460
  * a PR/MR at all (merge locally, keep the branch as-is), so gating the whole harness on
442
461
  * missing tooling here would be wrong. It is FYI, never a blocker.
443
462
  */
463
+ function checkAwmGitignore(cwd) {
464
+ const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(path_1.default.resolve(cwd));
465
+ if (result.ok)
466
+ return { id: 'awm-gitignore', ok: true, detail: result.detail };
467
+ return { id: 'awm-gitignore', ok: false, detail: result.detail, remedy: result.remedy };
468
+ }
444
469
  function checkHost(cwd) {
445
470
  let remote;
446
471
  try {
@@ -521,6 +546,9 @@ async function preflight(cwd = process.cwd(), opts = {}) {
521
546
  }
522
547
  const sensorStatus = await (0, status_1.computeSensorStatus)(cwd);
523
548
  const manifestExists = sensorStatus.mode !== 'missing';
549
+ // RF-3.4: advisory inputs/cacheability warning (status.warnings is empty for
550
+ // opt-out / native-gate / missing, so this stays quiet there).
551
+ const sensorsInputs = checkSensorsInputs(sensorStatus);
524
552
  const contextKernel = checkContextKernel(cwd);
525
553
  const checks = [
526
554
  checkContext(cwd),
@@ -534,12 +562,15 @@ async function preflight(cwd = process.cwd(), opts = {}) {
534
562
  ...(sensorStatus.mode !== 'invalid' ? [await checkTools(cwd, sensorStatus), checkPack(cwd, sensorStatus)] : []),
535
563
  checkSensorsBaseline(cwd, sensorStatus),
536
564
  ] : []),
565
+ ...(sensorsInputs ? [sensorsInputs] : []),
537
566
  // The empirical mode is intentionally opt-in: ordinary preflight remains a
538
567
  // quick static inspection and must not dispatch project software.
539
568
  ...(opts.verifySensors === true ? [await checkSensorExecution(cwd)] : []),
540
569
  // Strict mode is the only path that contacts authoritative npm/Git sources.
541
570
  ...(opts.requireCurrent === true ? [checkCompatibility()] : []),
542
571
  ...checkRoutingSetup(cwd),
572
+ // Blocking .awm versioning policy (D-024 / #218).
573
+ checkAwmGitignore(cwd),
543
574
  // Runs unconditionally — orthogonal to sensor configuration entirely, this is
544
575
  // about PR/MR tooling, not sensors.
545
576
  checkHost(cwd),
@@ -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.parseSensorInputs = parseSensorInputs;
6
7
  exports.parseSemgrepPolicy = parseSemgrepPolicy;
7
8
  exports.resolveSemgrepPolicy = resolveSemgrepPolicy;
8
9
  exports.parseStructuredCommand = parseStructuredCommand;
@@ -77,6 +78,48 @@ function assetArray(value, source, location, allowEmpty = false) {
77
78
  invalid(source, `${location} must be a ${allowEmpty ? '' : 'nonempty '}array`);
78
79
  return value.map((item, index) => asset(item, source, `${location}[${index}]`));
79
80
  }
81
+ /**
82
+ * Validate authored sensor `inputs` globs (RF-1.5).
83
+ * Matching (later digests) uses pinned picomatch with options `{ dot: true, posix: true }`
84
+ * and without `nocase` (paths come from `git ls-files`, already normalized).
85
+ * Supported patterns: `*`, `**`, `?`, character classes. Brace expansion, `!` negation,
86
+ * and parentheses / picomatch extglobs (`@(a|b)`, `+(a)`, `*(a)`, `?(a)`) are rejected
87
+ * here so authors never depend on unsupported syntax.
88
+ *
89
+ * Throws field-scoped Error messages (no "Invalid sensor pack/manifest" prefix) so both
90
+ * pack and project parsers can soft-isolate or wrap without a misleading product noun.
91
+ * `source` is accepted for call-site parity with other parsers; it is unused.
92
+ */
93
+ function parseSensorInputs(value, _source, location) {
94
+ if (!Array.isArray(value) || value.length === 0) {
95
+ throw new Error(`${location} must be a nonempty array`);
96
+ }
97
+ return value.map((item, index) => inputGlob(item, `${location}[${index}]`));
98
+ }
99
+ function inputGlob(value, location) {
100
+ if (typeof value !== 'string' || value.trim().length === 0 || /[\0-\x1f\x7f]/.test(value)) {
101
+ throw new Error(`${location} must be a nonempty single-line string without NUL or control characters`);
102
+ }
103
+ const parsed = value;
104
+ if (parsed.startsWith('/') || /^[A-Za-z]:[\\/]/.test(parsed) || parsed.startsWith('\\\\')) {
105
+ throw new Error(`${location} must be a relative glob (absolute paths are invalid)`);
106
+ }
107
+ if (parsed.includes('\\'))
108
+ throw new Error(`${location} must use POSIX separators`);
109
+ if (parsed.includes('{') || parsed.includes('}')) {
110
+ throw new Error(`${location} must not use brace expansion`);
111
+ }
112
+ if (parsed.includes('!')) {
113
+ throw new Error(`${location} must not use negation (!)`);
114
+ }
115
+ if (parsed.includes('(') || parsed.includes(')')) {
116
+ throw new Error(`${location} must not use parentheses or picomatch extglobs`);
117
+ }
118
+ if (parsed.split('/').some(part => part === '' || part === '.' || part === '..')) {
119
+ throw new Error(`${location} must be a contained relative glob (no empty, ., or .. segments)`);
120
+ }
121
+ return parsed;
122
+ }
80
123
  const SEMGREP_POLICY_REF = 'shared/semgrep-policy.json';
81
124
  const MAX_POLICY_BYTES = 64 * 1024;
82
125
  function contained(root, candidate) {
@@ -201,7 +244,7 @@ function parseStructuredCommand(input, source, location = 'command') {
201
244
  }
202
245
  function parseVariant(input, source, location) {
203
246
  const value = record(input, source, location);
204
- fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'changedCommand', 'assets', 'formatter', 'probe', 'policyRef'], source, location);
247
+ fields(value, ['id', 'priority', 'requirements', 'certifiedRange', 'command', 'changedCommand', 'assets', 'formatter', 'probe', 'policyRef', 'inputs'], source, location);
205
248
  const certifiedRange = text(value.certifiedRange, source, `${location}.certifiedRange`);
206
249
  if (semver_1.default.validRange(certifiedRange) === null)
207
250
  invalid(source, `${location}.certifiedRange must be a valid semver range`);
@@ -239,6 +282,7 @@ function parseVariant(input, source, location) {
239
282
  const changedCommand = 'changedCommand' in value ? parseStructuredCommand(value.changedCommand, source) : undefined;
240
283
  if (changedCommand && !changedCommand.fileInput)
241
284
  invalid(source, `${location}.changedCommand must declare fileInput`);
285
+ const inputs = 'inputs' in value ? parseSensorInputs(value.inputs, source, `${location}.inputs`) : undefined;
242
286
  return {
243
287
  id: id(value.id, source, `${location}.id`),
244
288
  priority: value.priority,
@@ -253,6 +297,7 @@ function parseVariant(input, source, location) {
253
297
  ...(policy ? { policyRef: SEMGREP_POLICY_REF } : {}),
254
298
  command: parseStructuredCommand(value.command, source),
255
299
  ...(changedCommand ? { changedCommand } : {}),
300
+ ...(inputs ? { inputs } : {}),
256
301
  };
257
302
  }
258
303
  function parseHardening(input, source) {
@@ -288,7 +333,7 @@ function assertNoEqualPriorityOverlap(variants) {
288
333
  }
289
334
  function parseSensor(input, source, location, variantIds) {
290
335
  const value = record(input, source, location);
291
- fields(value, ['applicability', 'variants', 'fast', 'timeout'], source, location);
336
+ fields(value, ['applicability', 'variants', 'fast', 'timeout', 'closureOnly'], source, location);
292
337
  if (!Array.isArray(value.variants) || value.variants.length === 0)
293
338
  invalid(source, `${location}.variants must be a nonempty array`);
294
339
  const variants = value.variants.map((variant, index) => parseVariant(variant, source, `${location}.variants[${index}]`));
@@ -320,6 +365,8 @@ function parseSensor(input, source, location, variantIds) {
320
365
  invalid(source, `${location}.applicability must declare a condition`);
321
366
  if ('fast' in value && typeof value.fast !== 'boolean')
322
367
  invalid(source, `${location}.fast must be a boolean`);
368
+ if ('closureOnly' in value && typeof value.closureOnly !== 'boolean')
369
+ invalid(source, `${location}.closureOnly must be a boolean`);
323
370
  let timeout;
324
371
  if ('timeout' in value) {
325
372
  try {
@@ -329,7 +376,13 @@ function parseSensor(input, source, location, variantIds) {
329
376
  invalid(source, error instanceof Error ? error.message : `${location}.timeout must be a positive safe integer`);
330
377
  }
331
378
  }
332
- return { applicability, variants, ...('fast' in value ? { fast: value.fast } : {}), ...(timeout !== undefined ? { timeout } : {}) };
379
+ return {
380
+ applicability,
381
+ variants,
382
+ ...('fast' in value ? { fast: value.fast } : {}),
383
+ ...('closureOnly' in value ? { closureOnly: value.closureOnly } : {}),
384
+ ...(timeout !== undefined ? { timeout } : {}),
385
+ };
333
386
  }
334
387
  function legacyCompatibility() {
335
388
  return {
@@ -196,7 +196,7 @@ function parseProjectSensor(input, source, location) {
196
196
  invalid(source, `${location}.variantId is forbidden on source:"project" entries`);
197
197
  if ('initializedCompatibility' in value)
198
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);
199
+ fields(value, ['source', 'enabled', 'command', 'formatter', 'fast', 'timeout', 'assets', 'applicability', 'description', 'inputs', 'closureOnly'], source, location);
200
200
  if (value.source !== 'project')
201
201
  invalid(source, `${location}.source must be "project"`);
202
202
  if (typeof value.enabled !== 'boolean')
@@ -226,6 +226,13 @@ function parseProjectSensor(input, source, location) {
226
226
  invalid(source, `${location}.fast must be a boolean`);
227
227
  sensor.fast = value.fast;
228
228
  }
229
+ if ('closureOnly' in value) {
230
+ if (typeof value.closureOnly !== 'boolean')
231
+ invalid(source, `${location}.closureOnly must be a boolean`);
232
+ sensor.closureOnly = value.closureOnly;
233
+ }
234
+ if ('inputs' in value)
235
+ sensor.inputs = (0, contract_1.parseSensorInputs)(value.inputs, source, `${location}.inputs`);
229
236
  if ('timeout' in value) {
230
237
  try {
231
238
  sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
@@ -251,7 +258,7 @@ function parseV2Sensor(input, source, location) {
251
258
  if (value.source === 'project') {
252
259
  invalid(source, `${location}.source "project" requires schemaVersion 3; migrate-to-v3`);
253
260
  }
254
- fields(value, ['enabled', 'fast', 'timeout', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility'], source, location);
261
+ fields(value, ['enabled', 'fast', 'timeout', 'variantId', 'command', 'assets', 'policyRef', 'initializedCompatibility', 'inputs', 'closureOnly'], source, location);
255
262
  if (typeof value.enabled !== 'boolean')
256
263
  invalid(source, `${location}.enabled must be a boolean`);
257
264
  const sensor = {
@@ -279,6 +286,13 @@ function parseV2Sensor(input, source, location) {
279
286
  invalid(source, `${location}.fast must be a boolean`);
280
287
  sensor.fast = value.fast;
281
288
  }
289
+ if ('closureOnly' in value) {
290
+ if (typeof value.closureOnly !== 'boolean')
291
+ invalid(source, `${location}.closureOnly must be a boolean`);
292
+ sensor.closureOnly = value.closureOnly;
293
+ }
294
+ if ('inputs' in value)
295
+ sensor.inputs = (0, contract_1.parseSensorInputs)(value.inputs, source, `${location}.inputs`);
282
296
  if ('timeout' in value) {
283
297
  try {
284
298
  sensor.timeout = (0, timeout_1.positiveTimeout)(value.timeout, `${location}.timeout`);
@@ -51,14 +51,18 @@ function registerSensorsCommand(program) {
51
51
  .option('--fast', 'run fast sensors only (tsc, lint)')
52
52
  .option('--slow', 'run slow sensors only (semgrep, mutation)')
53
53
  .option('--all', 'run all sensors regardless of speed')
54
+ .option('--slice', 'run every selected sensor except pack/project closureOnly (slice DONE gate)')
54
55
  .option('--changed', 'scope sensors that support it to the files changed vs --base')
55
56
  .option('--base <ref>', 'comparison point for --changed (default: HEAD, i.e. uncommitted work)')
57
+ .option('--no-cache', 'ignore fingerprint result cache and execute every selected sensor')
56
58
  .action(async (opts) => {
57
59
  // R2.2b: the receipt belongs to the manifest that runs (the one `runSensors` resolves from this cwd), not to the cwd.
58
60
  const pre = (0, receipt_1.isFullSelection)(opts) ? (0, receipt_1.captureReceiptPrecondition)((0, run_1.findManifestDir)(process.cwd()) ?? process.cwd()) : null;
59
61
  const output = await (0, run_1.runSensors)({
60
- fast: opts.fast, slow: opts.slow, all: opts.all,
62
+ fast: opts.fast, slow: opts.slow, all: opts.all, slice: opts.slice,
61
63
  changed: opts.changed, base: opts.base,
64
+ // RF-2.11: only this CLI action enables the cache (unless --no-cache).
65
+ useCache: opts.cache !== false,
62
66
  });
63
67
  // Emit the verdict ALWAYS — an empty `sensors` with overall:'not_certified'
64
68
  // must be visible, never a silent exit-0 that reads as "clean".
@@ -177,6 +181,11 @@ function registerSensorsCommand(program) {
177
181
  for (const [name, gap] of Object.entries(status.uninitialized ?? {})) {
178
182
  console.log(` ${picocolors_1.default.yellow('–')} ${name.padEnd(12)} not initialized: ${gap.state}/${gap.reason}`);
179
183
  }
184
+ // RF-3.4: enabled sensors without effective inputs are not cacheable.
185
+ // Non-blocking — same attribution pattern as uninitialized; does not exit.
186
+ for (const warning of status.warnings ?? []) {
187
+ console.log(` ${picocolors_1.default.yellow('⚠')} ${warning.sensor.padEnd(12)} ${warning.detail}`);
188
+ }
180
189
  console.log('');
181
190
  if (status.overall !== 'READY')
182
191
  process.exit(1);
@@ -12,6 +12,7 @@ const path_1 = __importDefault(require("path"));
12
12
  const pack_source_1 = require("./compatibility/pack-source");
13
13
  const bootstrap_1 = require("./bootstrap");
14
14
  const unresolved_1 = require("./unresolved");
15
+ const awm_gitignore_1 = require("../../core/awm-gitignore");
15
16
  /** Read a pack only through the shared source resolver. Init is a registry
16
17
  * boundary: a pack path that is absent is harmless, but a symlink or escaping
17
18
  * claimed source is never silently downgraded to an empty manifest. */
@@ -165,12 +166,17 @@ async function initSensors(opts = {}) {
165
166
  }
166
167
  if (plan.kind === 'migrate')
167
168
  throw new Error('initSensors does not migrate existing v2 manifests; run awm sensors bootstrap');
168
- if (plan.kind === 'noop')
169
+ if (plan.kind === 'noop') {
170
+ // Repair path: existing project `.awm/` should still get the managed block (GI-1 remedy).
171
+ if (fs_1.default.existsSync(path_1.default.join(cwd, '.awm')))
172
+ (0, awm_gitignore_1.upsertAwmGitignore)(cwd);
169
173
  return {
170
174
  // No manifest is written or synthesized here. The compatibility API only
171
175
  // retains its historical result shape for callers that inspect detection.
172
176
  manifest: {}, detection: detectStack(detectionCwd), configured: [], status: 'already-configured',
173
177
  };
178
+ }
174
179
  (0, bootstrap_1.applySensorBootstrap)(plan);
180
+ (0, awm_gitignore_1.upsertAwmGitignore)(cwd);
175
181
  return { manifest: plan.manifest, detection: detectStack(detectionCwd), configured: [], status: 'created', ...(plan.unresolved && plan.unresolved.length > 0 ? { unresolved: plan.unresolved } : {}) };
176
182
  }
@@ -0,0 +1,315 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CACHE_DIGEST_BASELINE_REL = exports.MAX_DIGEST_FILE_BYTES = exports.ABSENT_MARKER = exports.PICOMATCH_DIGEST_OPTIONS = void 0;
7
+ exports.isSafeRepoRel = isSafeRepoRel;
8
+ exports.toSafeRepoRel = toSafeRepoRel;
9
+ exports.joinRepoRel = joinRepoRel;
10
+ exports.buildAlwaysMeasuredPaths = buildAlwaysMeasuredPaths;
11
+ exports.repoPathMatchesInputGlobs = repoPathMatchesInputGlobs;
12
+ exports.computeCacheInputDigest = computeCacheInputDigest;
13
+ /// <reference path="../../types/picomatch-posix.d.ts" />
14
+ const crypto_1 = __importDefault(require("crypto"));
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const posix_1 = __importDefault(require("picomatch/posix"));
18
+ const git_1 = require("../../core/changes/git");
19
+ /**
20
+ * Picomatch options for sensor input digests (RF-1.5 / RF-1.6).
21
+ * `dot: true` so patterns like `*` match `.env`; `posix: true` enables POSIX
22
+ * character classes. No `nocase` — paths come from `git ls-files` (already
23
+ * normalized). Callers force POSIX path separators via `picomatch/posix`.
24
+ */
25
+ exports.PICOMATCH_DIGEST_OPTIONS = { dot: true, posix: true };
26
+ /** Explicit marker contributed when a listed path is gone (ENOENT on lstat). */
27
+ exports.ABSENT_MARKER = 'ABSENT';
28
+ /** Hard ceiling on file content hashed into a digest contribution (QA-6). */
29
+ exports.MAX_DIGEST_FILE_BYTES = 1024 * 1024;
30
+ // Baseline is re-applied every run (RF-2.13) and must never enter the cache
31
+ // digest (RF-2.12), even when authored globs like **/*.json would match it.
32
+ /** Repo-root-relative path of the baseline file (never cache-digest measured). */
33
+ exports.CACHE_DIGEST_BASELINE_REL = '.awm/sensors.baseline.json';
34
+ function isExcludedFromCacheDigest(repoRel) {
35
+ const norm = repoRel.split(path_1.default.sep).join('/');
36
+ return norm === exports.CACHE_DIGEST_BASELINE_REL || norm.endsWith('/' + exports.CACHE_DIGEST_BASELINE_REL);
37
+ }
38
+ const LOCKFILE_NAMES = [
39
+ 'package-lock.json',
40
+ 'pnpm-lock.yaml',
41
+ 'yarn.lock',
42
+ 'bun.lockb',
43
+ 'Pipfile.lock',
44
+ 'poetry.lock',
45
+ 'uv.lock',
46
+ ];
47
+ /**
48
+ * Repo-root-relative path safety (QA-2): nonempty POSIX segments, no `.` / `..`,
49
+ * no absolute / drive / backslash forms.
50
+ */
51
+ function isSafeRepoRel(repoRel) {
52
+ if (typeof repoRel !== 'string' || repoRel.length === 0 || repoRel !== repoRel.trim())
53
+ return false;
54
+ if (repoRel.includes('\\') || repoRel.startsWith('/') || /^[A-Za-z]:/.test(repoRel))
55
+ return false;
56
+ return repoRel.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..');
57
+ }
58
+ function requireSafeRepoRel(repoRel, label) {
59
+ const norm = repoRel.split(path_1.default.sep).join('/');
60
+ if (!isSafeRepoRel(norm))
61
+ throw new Error(`${label} must be a safe relative path (no ., .., absolute, or escape)`);
62
+ return norm;
63
+ }
64
+ function resolveRealPath(p) {
65
+ const resolved = path_1.default.resolve(p);
66
+ try {
67
+ return fs_1.default.realpathSync.native(resolved);
68
+ }
69
+ catch {
70
+ try {
71
+ return fs_1.default.realpathSync(resolved);
72
+ }
73
+ catch {
74
+ return resolved;
75
+ }
76
+ }
77
+ }
78
+ function posixRel(from, to) {
79
+ return path_1.default.relative(from, to).split(path_1.default.sep).join('/');
80
+ }
81
+ /**
82
+ * Repo-root-relative POSIX path from an absolute (or cwd-relative) path.
83
+ * Realpaths both sides before `path.relative` so Windows drive-letter / casing /
84
+ * separator mismatches (git toplevel vs Node paths) cannot yield an absolute
85
+ * relative string that later fails `isSafeRepoRel`.
86
+ */
87
+ function toSafeRepoRel(gitRoot, absolutePath, label = 'path') {
88
+ if (typeof gitRoot !== 'string' || gitRoot.trim() === '')
89
+ throw new Error('gitRoot must be a nonempty string');
90
+ if (typeof absolutePath !== 'string' || absolutePath.trim() === '') {
91
+ throw new Error(`${label} must be a nonempty string`);
92
+ }
93
+ const from = resolveRealPath(gitRoot);
94
+ const to = resolveRealPath(absolutePath);
95
+ const rel = posixRel(from, to);
96
+ // Empty means the same path; sensors.json / package roots used here are always nested or equal.
97
+ // Equal packageRoot → '' is allowed only when callers opt in via allowEmpty.
98
+ if (rel === '')
99
+ return '';
100
+ if (path_1.default.win32.isAbsolute(rel) || path_1.default.posix.isAbsolute(rel) || rel.startsWith('..') || /^[A-Za-z]:/.test(rel)) {
101
+ throw new Error(`${label} must resolve under gitRoot`);
102
+ }
103
+ return requireSafeRepoRel(rel, label);
104
+ }
105
+ function staysUnderRoot(rootReal, candidate) {
106
+ const rel = path_1.default.relative(rootReal, candidate);
107
+ return rel === '' || (!rel.startsWith('..') && !path_1.default.isAbsolute(rel));
108
+ }
109
+ function joinRepoRel(packageRootRel, file) {
110
+ if (typeof packageRootRel !== 'string')
111
+ throw new Error('packageRootRel must be a string');
112
+ if (typeof file !== 'string' || file.length === 0)
113
+ throw new Error('file must be a nonempty string');
114
+ const cleaned = requireSafeRepoRel(file.replace(/^\/+/, ''), 'path');
115
+ if (packageRootRel === '')
116
+ return cleaned;
117
+ const rootRel = requireSafeRepoRel(packageRootRel.replace(/\/+$/, ''), 'packageRootRel');
118
+ return `${rootRel}/${cleaned}`;
119
+ }
120
+ /** Build the always-measured path set for the *cache* digest (RF-2.12). Never includes baseline.json. */
121
+ function buildAlwaysMeasuredPaths(input) {
122
+ if (!input || typeof input !== 'object')
123
+ throw new Error('always-measured input must be an object');
124
+ if (typeof input.includePackageJson !== 'boolean')
125
+ throw new Error('includePackageJson must be a boolean');
126
+ if (typeof input.packageRootRel !== 'string')
127
+ throw new Error('packageRootRel must be a string');
128
+ if (!Array.isArray(input.assetRels) || !Array.isArray(input.configFileRels)) {
129
+ throw new Error('assetRels and configFileRels must be arrays');
130
+ }
131
+ const out = new Set();
132
+ out.add(requireSafeRepoRel(input.sensorsJsonRel.split(path_1.default.sep).join('/'), 'sensorsJsonRel'));
133
+ for (const asset of input.assetRels) {
134
+ if (typeof asset !== 'string' || asset.length === 0)
135
+ throw new Error('asset path must be a nonempty string');
136
+ out.add(joinRepoRel(input.packageRootRel, asset));
137
+ }
138
+ for (const config of input.configFileRels) {
139
+ if (typeof config !== 'string' || config.length === 0)
140
+ throw new Error('configFile path must be a nonempty string');
141
+ out.add(joinRepoRel(input.packageRootRel, config));
142
+ }
143
+ if (input.includePackageJson) {
144
+ out.add(joinRepoRel(input.packageRootRel, 'package.json'));
145
+ for (const lock of LOCKFILE_NAMES)
146
+ out.add(joinRepoRel(input.packageRootRel, lock));
147
+ }
148
+ else {
149
+ for (const lock of LOCKFILE_NAMES)
150
+ out.add(joinRepoRel(input.packageRootRel, lock));
151
+ }
152
+ return [...out].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
153
+ }
154
+ function listGitFiles(gitRoot) {
155
+ const out = (0, git_1.runGit)(gitRoot, ['ls-files', '-co', '--exclude-standard', '-z']);
156
+ if (out === null)
157
+ return null;
158
+ return out.split('\0').filter((p) => p.length > 0);
159
+ }
160
+ function matchGlobs(repoRel, packageRootRel, matchers) {
161
+ let packageRel = repoRel;
162
+ if (packageRootRel) {
163
+ const prefix = packageRootRel.endsWith('/') ? packageRootRel : `${packageRootRel}/`;
164
+ if (repoRel !== packageRootRel && !repoRel.startsWith(prefix))
165
+ return false;
166
+ packageRel = repoRel === packageRootRel ? '' : repoRel.slice(prefix.length);
167
+ }
168
+ if (packageRel.length === 0)
169
+ return false;
170
+ return matchers.some((m) => m(packageRel));
171
+ }
172
+ /**
173
+ * Whether a repo-root POSIX path matches authored sensor `inputs` globs under
174
+ * `packageRootRel` (RF-1.5 picomatch options). Used by receipt late-path validate
175
+ * so new files matching inputs count as measured (RF-4.5), not only the frozen list.
176
+ */
177
+ function repoPathMatchesInputGlobs(repoRel, packageRootRel, inputGlobs) {
178
+ if (typeof repoRel !== 'string' || typeof packageRootRel !== 'string')
179
+ return false;
180
+ if (!Array.isArray(inputGlobs) || inputGlobs.some((g) => typeof g !== 'string'))
181
+ return false;
182
+ const norm = repoRel.split(path_1.default.sep).join('/');
183
+ if (!isSafeRepoRel(norm))
184
+ return false;
185
+ if (packageRootRel !== '' && !isSafeRepoRel(packageRootRel))
186
+ return false;
187
+ const matchers = inputGlobs.map((glob) => (0, posix_1.default)(glob, { ...exports.PICOMATCH_DIGEST_OPTIONS }));
188
+ return matchGlobs(norm, packageRootRel, matchers);
189
+ }
190
+ function absentContribution(repoRel, reason) {
191
+ return {
192
+ path: repoRel,
193
+ kind: 'absent',
194
+ marker: exports.ABSENT_MARKER,
195
+ hash: crypto_1.default.createHash('sha256').update(`ABSENT:${reason}:${repoRel}`).digest('hex'),
196
+ };
197
+ }
198
+ /**
199
+ * Hash one matched path without following intermediate symlinks (QA-1) and
200
+ * without reading files that escape gitRoot or exceed the size cap (QA-2/QA-6).
201
+ */
202
+ function hashContribution(gitRootReal, repoRel) {
203
+ if (!isSafeRepoRel(repoRel))
204
+ return absentContribution(repoRel, 'unsafe-rel');
205
+ const segments = repoRel.split('/');
206
+ let current = gitRootReal;
207
+ for (let i = 0; i < segments.length; i++) {
208
+ current = path_1.default.join(current, segments[i]);
209
+ if (!staysUnderRoot(gitRootReal, current))
210
+ return absentContribution(repoRel, 'escape');
211
+ let st;
212
+ try {
213
+ st = fs_1.default.lstatSync(current);
214
+ }
215
+ catch (error) {
216
+ if (error.code === 'ENOENT')
217
+ return absentContribution(repoRel, 'enoent');
218
+ throw error;
219
+ }
220
+ const isLast = i === segments.length - 1;
221
+ if (st.isSymbolicLink()) {
222
+ // Intermediate symlink → fail closed (never read through redirected parents).
223
+ if (!isLast)
224
+ return absentContribution(repoRel, 'intermediate-symlink');
225
+ const target = fs_1.default.readlinkSync(current);
226
+ return {
227
+ path: repoRel,
228
+ kind: 'symlink',
229
+ hash: crypto_1.default.createHash('sha256').update(`symlink:${target}`).digest('hex'),
230
+ };
231
+ }
232
+ if (!isLast) {
233
+ if (!st.isDirectory())
234
+ return absentContribution(repoRel, 'not-dir');
235
+ continue;
236
+ }
237
+ if (!st.isFile())
238
+ return absentContribution(repoRel, 'not-file');
239
+ if (st.size > exports.MAX_DIGEST_FILE_BYTES)
240
+ return absentContribution(repoRel, 'oversize');
241
+ const content = fs_1.default.readFileSync(current);
242
+ return {
243
+ path: repoRel,
244
+ kind: 'file',
245
+ hash: crypto_1.default.createHash('sha256').update(content).digest('hex'),
246
+ };
247
+ }
248
+ return absentContribution(repoRel, 'empty');
249
+ }
250
+ /**
251
+ * Compute the cache input digest (RF-2.1 / RF-2.12): git enumeration, picomatch
252
+ * filter, symlink readlink / ABSENT / file content hashing. Never follows links.
253
+ */
254
+ function computeCacheInputDigest(opts) {
255
+ if (!opts || typeof opts !== 'object')
256
+ throw new Error('digest options must be an object');
257
+ if (typeof opts.gitRoot !== 'string' || opts.gitRoot.trim() === '')
258
+ throw new Error('gitRoot must be a nonempty string');
259
+ if (typeof opts.packageRoot !== 'string' || opts.packageRoot.trim() === '')
260
+ throw new Error('packageRoot must be a nonempty string');
261
+ if (!Array.isArray(opts.inputGlobs) || opts.inputGlobs.some((g) => typeof g !== 'string')) {
262
+ throw new Error('inputGlobs must be an array of strings');
263
+ }
264
+ if (!Array.isArray(opts.alwaysMeasuredRepoRels) || opts.alwaysMeasuredRepoRels.some((p) => typeof p !== 'string')) {
265
+ throw new Error('alwaysMeasuredRepoRels must be an array of strings');
266
+ }
267
+ // Prefer realpathSync.native (same as toSafeRepoRel / run.ts). Mixing native vs
268
+ // non-native on Windows yields drive-absolute path.relative results and false
269
+ // "packageRoot must be inside gitRoot" after the sensorsJsonRel fix.
270
+ let gitRootReal;
271
+ try {
272
+ gitRootReal = fs_1.default.realpathSync.native(path_1.default.resolve(opts.gitRoot));
273
+ }
274
+ catch {
275
+ try {
276
+ gitRootReal = fs_1.default.realpathSync(path_1.default.resolve(opts.gitRoot));
277
+ }
278
+ catch {
279
+ return { ok: false, reason: 'git-failed' };
280
+ }
281
+ }
282
+ let packageRootRel;
283
+ try {
284
+ packageRootRel = toSafeRepoRel(gitRootReal, opts.packageRoot, 'packageRoot');
285
+ }
286
+ catch {
287
+ throw new Error('packageRoot must be inside gitRoot');
288
+ }
289
+ const listed = listGitFiles(gitRootReal);
290
+ if (listed === null)
291
+ return { ok: false, reason: 'git-failed' };
292
+ const matchers = opts.inputGlobs.map((glob) => (0, posix_1.default)(glob, { ...exports.PICOMATCH_DIGEST_OPTIONS }));
293
+ const matched = new Set();
294
+ for (const repoRel of listed) {
295
+ const norm = repoRel.split(path_1.default.sep).join('/');
296
+ if (!isSafeRepoRel(norm) || isExcludedFromCacheDigest(norm))
297
+ continue;
298
+ if (matchGlobs(norm, packageRootRel === '' ? '' : packageRootRel, matchers))
299
+ matched.add(norm);
300
+ }
301
+ for (const always of opts.alwaysMeasuredRepoRels) {
302
+ const norm = always.split(path_1.default.sep).join('/');
303
+ if (!isSafeRepoRel(norm)) {
304
+ throw new Error(`alwaysMeasuredRepoRels entry must be a safe relative path (no ., .., absolute, or escape): ${norm}`);
305
+ }
306
+ if (isExcludedFromCacheDigest(norm))
307
+ continue;
308
+ matched.add(norm);
309
+ }
310
+ const matchedPaths = [...matched].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
311
+ const contributions = matchedPaths.map((p) => hashContribution(gitRootReal, p));
312
+ const canonical = contributions.map((c) => `${c.path}\0${c.kind}\0${c.hash}`).join('\n');
313
+ const digest = `sha256:${crypto_1.default.createHash('sha256').update(canonical).digest('hex')}`;
314
+ return { ok: true, digest, matchedPaths, contributions };
315
+ }
@@ -33,9 +33,19 @@ function validateRunOptions(opts) {
33
33
  throw new Error('run option base must be a nonempty string');
34
34
  if (opts.readOnly !== undefined && typeof opts.readOnly !== 'boolean')
35
35
  throw new Error('run option readOnly must be a boolean');
36
+ if (opts.slice !== undefined && typeof opts.slice !== 'boolean')
37
+ throw new Error('run option slice must be a boolean');
38
+ if (opts.useCache !== undefined && typeof opts.useCache !== 'boolean')
39
+ throw new Error('run option useCache must be a boolean');
36
40
  if (opts.changed && opts.ignoreBaseline) {
37
41
  throw new Error('refusing to combine --changed with a baseline capture: a partial run cannot define the accepted set');
38
42
  }
43
+ if (opts.slice && opts.all) {
44
+ throw new Error('refusing to combine --slice with --all: slice already selects the non-closureOnly set');
45
+ }
46
+ if (opts.slice && opts.slow) {
47
+ throw new Error('refusing to combine --slice with --slow: use a full gate for closureOnly/slow sensors');
48
+ }
39
49
  }
40
50
  /** Expand the one declared file placeholder into literal structured argv entries. */
41
51
  function expandFileInput(command, files) {