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
@@ -0,0 +1,278 @@
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.cacheFileName = cacheFileName;
7
+ exports.canCacheRawResult = canCacheRawResult;
8
+ exports.computeSensorFingerprint = computeSensorFingerprint;
9
+ exports.toCacheRaw = toCacheRaw;
10
+ exports.writeResultCache = writeResultCache;
11
+ exports.readResultCache = readResultCache;
12
+ const crypto_1 = __importDefault(require("crypto"));
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const atomic_file_1 = require("../../core/atomic-file");
16
+ const text_safety_1 = require("../../core/changes/text-safety");
17
+ const MAX_CACHE_BYTES = 1024 * 1024;
18
+ const CACHE_SCHEMA = 1;
19
+ function runsDir(root) {
20
+ return path_1.default.join(root, '.awm', 'sensors', 'runs');
21
+ }
22
+ function assertNoSymlinkChain(root) {
23
+ let current = root;
24
+ for (const segment of ['.awm', 'sensors', 'runs']) {
25
+ current = path_1.default.join(current, segment);
26
+ try {
27
+ if (fs_1.default.lstatSync(current).isSymbolicLink())
28
+ throw new Error(`refusing to use symlinked path ${(0, text_safety_1.esc)(current)}`);
29
+ }
30
+ catch (error) {
31
+ if (error.code === 'ENOENT')
32
+ return;
33
+ throw error;
34
+ }
35
+ }
36
+ }
37
+ function ensureRunsIgnore(root) {
38
+ const file = path_1.default.join(runsDir(root), '.gitignore');
39
+ try {
40
+ const stat = fs_1.default.lstatSync(file);
41
+ if (stat.isFile() && fs_1.default.readFileSync(file, 'utf8') === '*\n')
42
+ return;
43
+ }
44
+ catch (error) {
45
+ if (error.code !== 'ENOENT')
46
+ throw error;
47
+ }
48
+ (0, atomic_file_1.writeFileAtomic)(file, '*\n', 0o644);
49
+ }
50
+ function cacheKeyHash(lookup) {
51
+ return crypto_1.default.createHash('sha256')
52
+ .update([lookup.projectKey, lookup.sensorId, lookup.sensorFingerprint, lookup.inputDigest].join('\0'))
53
+ .digest('hex');
54
+ }
55
+ function cacheFileName(lookup) {
56
+ return `cache-${cacheKeyHash(lookup)}.json`;
57
+ }
58
+ /** RF-2.4: store only clean pass, or fail+findings with errors.length > 0 (and not incomplete). */
59
+ function canCacheRawResult(result) {
60
+ if (!result || typeof result !== 'object')
61
+ return false;
62
+ if (result.incomplete)
63
+ return false;
64
+ if (!Array.isArray(result.errors))
65
+ return false;
66
+ if (result.status === 'pass') {
67
+ // Planted pass+errors / pass+failureKind must never cache (or read as a hit).
68
+ return result.errors.length === 0 && result.failureKind === undefined;
69
+ }
70
+ if (result.status !== 'fail')
71
+ return false;
72
+ if (result.failureKind !== 'findings')
73
+ return false;
74
+ return result.errors.length > 0;
75
+ }
76
+ function computeSensorFingerprint(parts, opts) {
77
+ if (!parts || typeof parts !== 'object')
78
+ throw new Error('fingerprint parts must be an object');
79
+ if (typeof parts.cliVersion !== 'string' || parts.cliVersion.length === 0)
80
+ throw new Error('cliVersion must be a nonempty string');
81
+ if (!Array.isArray(parts.inputs) || parts.inputs.some((g) => typeof g !== 'string'))
82
+ throw new Error('inputs must be string[]');
83
+ const allowMissing = opts?.allowMissingLiveVersions === true;
84
+ const toolOk = typeof parts.toolVersion === 'string' && parts.toolVersion.length > 0;
85
+ const runtimeOk = typeof parts.runtimeVersion === 'string' && parts.runtimeVersion.length > 0;
86
+ if (!allowMissing && (!toolOk || !runtimeOk))
87
+ return null;
88
+ const canonical = JSON.stringify({
89
+ variantId: parts.variantId,
90
+ command: parts.command,
91
+ changedCommand: parts.changedCommand ?? null,
92
+ cliVersion: parts.cliVersion,
93
+ inputs: [...parts.inputs].sort(),
94
+ toolVersion: toolOk ? parts.toolVersion : null,
95
+ runtimeVersion: runtimeOk ? parts.runtimeVersion : null,
96
+ });
97
+ return `sha256:${crypto_1.default.createHash('sha256').update(canonical).digest('hex')}`;
98
+ }
99
+ function isSensorError(value) {
100
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
101
+ return false;
102
+ const e = value;
103
+ if (typeof e.message !== 'string' || !(0, text_safety_1.isSingleLine)(e.message))
104
+ return false;
105
+ if (e.file !== undefined && (typeof e.file !== 'string' || !(0, text_safety_1.isSingleLine)(e.file)))
106
+ return false;
107
+ if (e.line !== undefined && (!Number.isSafeInteger(e.line) || e.line < 0))
108
+ return false;
109
+ if (e.column !== undefined && (!Number.isSafeInteger(e.column) || e.column < 0))
110
+ return false;
111
+ if (e.rule !== undefined && typeof e.rule !== 'string')
112
+ return false;
113
+ return true;
114
+ }
115
+ function validateRaw(raw) {
116
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
117
+ return 'raw must be an object';
118
+ const r = raw;
119
+ if (typeof r.name !== 'string' || r.name.length === 0)
120
+ return 'raw.name invalid';
121
+ if (r.status !== 'pass' && r.status !== 'fail')
122
+ return 'raw.status invalid';
123
+ if (!Array.isArray(r.errors) || !r.errors.every(isSensorError))
124
+ return 'raw.errors invalid';
125
+ if (r.failureKind !== undefined
126
+ && r.failureKind !== 'findings'
127
+ && r.failureKind !== 'missing-binary'
128
+ && r.failureKind !== 'exit-code')
129
+ return 'raw.failureKind invalid';
130
+ if (r.status === 'pass') {
131
+ if (r.errors.length !== 0)
132
+ return 'raw.pass must have errors.length===0';
133
+ if (r.failureKind !== undefined)
134
+ return 'raw.pass must not carry failureKind';
135
+ }
136
+ if (r.incomplete !== undefined && typeof r.incomplete !== 'string')
137
+ return 'raw.incomplete invalid';
138
+ if (r.skipReason !== undefined && typeof r.skipReason !== 'string')
139
+ return 'raw.skipReason invalid';
140
+ if (r.certification !== undefined
141
+ && r.certification !== 'certified'
142
+ && r.certification !== 'operational-unverified'
143
+ && r.certification !== 'project-declared')
144
+ return 'raw.certification invalid';
145
+ return null;
146
+ }
147
+ function cacheProblem(x) {
148
+ if (x === null || typeof x !== 'object' || Array.isArray(x))
149
+ return 'cache must be an object';
150
+ const r = x;
151
+ if (r.schema !== CACHE_SCHEMA)
152
+ return 'schema must be 1';
153
+ if (typeof r.sensorId !== 'string' || r.sensorId.length === 0)
154
+ return 'sensorId invalid';
155
+ if (typeof r.projectKey !== 'string' || r.projectKey.length === 0)
156
+ return 'projectKey invalid';
157
+ if (typeof r.sensorFingerprint !== 'string' || !r.sensorFingerprint.startsWith('sha256:'))
158
+ return 'sensorFingerprint invalid';
159
+ if (typeof r.inputDigest !== 'string' || !r.inputDigest.startsWith('sha256:'))
160
+ return 'inputDigest invalid';
161
+ if (r.originCommit !== null && (typeof r.originCommit !== 'string' || !/^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(r.originCommit))) {
162
+ return 'originCommit invalid';
163
+ }
164
+ if (typeof r.originDirty !== 'boolean')
165
+ return 'originDirty invalid';
166
+ return validateRaw(r.raw);
167
+ }
168
+ function toCacheRaw(result) {
169
+ if (!result || typeof result !== 'object')
170
+ throw new Error('cache raw requires a sensor result');
171
+ if (result.status !== 'pass' && result.status !== 'fail') {
172
+ throw new Error(`toCacheRaw refuses non-pass/fail status (${result.status}); only pass|fail may be cached`);
173
+ }
174
+ if (!canCacheRawResult(result)) {
175
+ throw new Error('toCacheRaw refuses uncacheable raw (RF-2.4)');
176
+ }
177
+ return {
178
+ name: result.name,
179
+ status: result.status,
180
+ errors: result.errors,
181
+ ...(result.failureKind ? { failureKind: result.failureKind } : {}),
182
+ ...(result.incomplete ? { incomplete: result.incomplete } : {}),
183
+ ...(result.certification ? { certification: result.certification } : {}),
184
+ ...(result.skipReason ? { skipReason: result.skipReason } : {}),
185
+ };
186
+ }
187
+ function writeResultCache(gitRoot, record) {
188
+ if (typeof gitRoot !== 'string' || gitRoot.length === 0)
189
+ throw new Error('cache root must be a nonempty string');
190
+ const problem = cacheProblem(record);
191
+ if (problem !== null)
192
+ throw new Error(`invalid result cache: ${problem}`);
193
+ if (record.raw.name !== record.sensorId) {
194
+ throw new Error('refusing to write result cache: raw.name must match sensorId');
195
+ }
196
+ const asResult = {
197
+ name: record.raw.name,
198
+ status: record.raw.status,
199
+ errors: record.raw.errors,
200
+ ...(record.raw.failureKind ? { failureKind: record.raw.failureKind } : {}),
201
+ ...(record.raw.incomplete ? { incomplete: record.raw.incomplete } : {}),
202
+ ...(record.raw.certification ? { certification: record.raw.certification } : {}),
203
+ ...(record.raw.skipReason ? { skipReason: record.raw.skipReason } : {}),
204
+ };
205
+ if (!canCacheRawResult(asResult)) {
206
+ throw new Error('refusing to write uncacheable result cache entry (RF-2.4)');
207
+ }
208
+ const lookup = {
209
+ sensorId: record.sensorId,
210
+ projectKey: record.projectKey,
211
+ sensorFingerprint: record.sensorFingerprint,
212
+ inputDigest: record.inputDigest,
213
+ };
214
+ const content = JSON.stringify(record, null, 2) + '\n';
215
+ if (Buffer.byteLength(content) > MAX_CACHE_BYTES)
216
+ throw new Error('invalid result cache: larger than the size read accepts');
217
+ assertNoSymlinkChain(gitRoot);
218
+ fs_1.default.mkdirSync(runsDir(gitRoot), { recursive: true, mode: 0o700 });
219
+ assertNoSymlinkChain(gitRoot);
220
+ ensureRunsIgnore(gitRoot);
221
+ const file = path_1.default.join(runsDir(gitRoot), cacheFileName(lookup));
222
+ (0, atomic_file_1.writeFileAtomic)(file, content, 0o600);
223
+ fs_1.default.chmodSync(file, 0o600);
224
+ return file;
225
+ }
226
+ /** Validated cache read (RF-2.15). Any invalid/tampered entry is a miss. */
227
+ function readResultCache(gitRoot, lookup) {
228
+ if (typeof gitRoot !== 'string' || gitRoot.length === 0)
229
+ return null;
230
+ if (!lookup || typeof lookup !== 'object')
231
+ return null;
232
+ if (typeof lookup.sensorId !== 'string' || typeof lookup.projectKey !== 'string')
233
+ return null;
234
+ if (typeof lookup.sensorFingerprint !== 'string' || typeof lookup.inputDigest !== 'string')
235
+ return null;
236
+ let raw;
237
+ try {
238
+ assertNoSymlinkChain(gitRoot);
239
+ const file = path_1.default.join(runsDir(gitRoot), cacheFileName(lookup));
240
+ const stat = fs_1.default.lstatSync(file);
241
+ if (!stat.isFile() || stat.size > MAX_CACHE_BYTES)
242
+ return null;
243
+ raw = fs_1.default.readFileSync(file, 'utf8');
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ let parsed;
249
+ try {
250
+ parsed = JSON.parse(raw);
251
+ }
252
+ catch {
253
+ return null;
254
+ }
255
+ if (cacheProblem(parsed) !== null)
256
+ return null;
257
+ const record = parsed;
258
+ if (record.sensorId !== lookup.sensorId
259
+ || record.projectKey !== lookup.projectKey
260
+ || record.sensorFingerprint !== lookup.sensorFingerprint
261
+ || record.inputDigest !== lookup.inputDigest)
262
+ return null;
263
+ if (record.raw.name !== record.sensorId || record.raw.name !== lookup.sensorId)
264
+ return null;
265
+ // RF-2.15/2.4: structural validity is not enough — reject uncacheable raw payloads.
266
+ const asResult = {
267
+ name: record.raw.name,
268
+ status: record.raw.status,
269
+ errors: record.raw.errors,
270
+ ...(record.raw.failureKind ? { failureKind: record.raw.failureKind } : {}),
271
+ ...(record.raw.incomplete ? { incomplete: record.raw.incomplete } : {}),
272
+ ...(record.raw.certification ? { certification: record.raw.certification } : {}),
273
+ ...(record.raw.skipReason ? { skipReason: record.raw.skipReason } : {}),
274
+ };
275
+ if (!canCacheRawResult(asResult))
276
+ return null;
277
+ return record;
278
+ }
@@ -77,6 +77,9 @@ function scoped(result, prepared) {
77
77
  ...(prepared.effectiveScope === 'changed' ? { scope: 'changed' } : {}),
78
78
  };
79
79
  }
80
+ function withFailureKind(result, kind) {
81
+ return result.status === 'fail' ? { ...result, failureKind: kind } : result;
82
+ }
80
83
  /** Interpret one raw bounded process result without knowing its manifest format. */
81
84
  function interpretResult(prepared, raw) {
82
85
  validatePrepared(prepared);
@@ -90,13 +93,15 @@ function interpretResult(prepared, raw) {
90
93
  execution,
91
94
  });
92
95
  const format = formatterFor(prepared.name, prepared.formatter);
93
- if (raw.spawnError)
94
- return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `sensor could not be started: ${raw.spawnError.message}` }] });
96
+ if (raw.spawnError) {
97
+ return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors: [{ message: `sensor could not be started: ${raw.spawnError.message}` }] }, 'missing-binary'));
98
+ }
95
99
  if (raw.timedOut || raw.overflowed) {
96
100
  const reason = raw.timedOut ? `timeout after ${prepared.timeoutMs}ms` : `output exceeded ${MAX_BUFFER} bytes`;
97
101
  const errors = format(raw.stdout + raw.stderr);
98
- if (errors.length > 0)
99
- return withEvidence({ name: prepared.name, status: 'fail', errors, incomplete: `${reason} — findings below are from partial output; the run did not finish` });
102
+ if (errors.length > 0) {
103
+ return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors, incomplete: `${reason} — findings below are from partial output; the run did not finish` }, 'findings'));
104
+ }
100
105
  return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: reason });
101
106
  }
102
107
  if (raw.code === 0) {
@@ -104,27 +109,31 @@ function interpretResult(prepared, raw) {
104
109
  return withEvidence({ name: prepared.name, status: 'pass', errors: [] });
105
110
  }
106
111
  const errors = format(raw.stdout);
107
- return withEvidence({ name: prepared.name, status: errors.length ? 'fail' : 'pass', errors });
112
+ if (errors.length === 0)
113
+ return withEvidence({ name: prepared.name, status: 'pass', errors: [] });
114
+ return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors }, 'findings'));
108
115
  }
109
116
  const output = raw.stdout + raw.stderr;
110
117
  if (prepared.formatter === 'exit-code' || format === parseExitCodeOutput) {
111
118
  const combined = output.trim();
112
119
  const evidence = truncateEvidence(combined.length > 0 ? combined : `exit ${raw.code}`);
113
- return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: evidence }] });
120
+ return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors: [{ message: evidence }] }, 'exit-code'));
114
121
  }
115
122
  const errors = format(output);
116
123
  if (errors.length > 0)
117
- return withEvidence({ name: prepared.name, status: 'fail', errors });
124
+ return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors }, 'findings'));
118
125
  const lower = output.toLowerCase();
119
126
  const toolMissing = raw.code === 127
120
127
  || lower.includes('command not found')
121
128
  || lower.includes('is not recognized as an internal or external command')
122
129
  || lower.includes('enoent')
123
130
  || lower.includes('could not determine executable');
124
- if (toolMissing)
125
- return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `sensor tool not available: ${truncateEvidence(output)}` }] });
126
- if (prepared.name === 'test')
127
- return withEvidence({ name: prepared.name, status: 'fail', errors: [{ message: `SENSOR[${prepared.name}] failed (exit ${raw.code})` }] });
131
+ if (toolMissing) {
132
+ return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors: [{ message: `sensor tool not available: ${truncateEvidence(output)}` }] }, 'missing-binary'));
133
+ }
134
+ if (prepared.name === 'test') {
135
+ return withEvidence(withFailureKind({ name: prepared.name, status: 'fail', errors: [{ message: `SENSOR[${prepared.name}] failed (exit ${raw.code})` }] }, 'exit-code'));
136
+ }
128
137
  return withEvidence({ name: prepared.name, status: 'inconclusive', errors: [], skipReason: `exit ${raw.code}: ${truncateEvidence(output)}` });
129
138
  }
130
139
  /** Execute a validated prepared command, or render its deliberate synthetic result. */
@@ -154,5 +163,15 @@ function applyBaseline(result, accepted) {
154
163
  const { newErrors, suppressed } = (0, baseline_1.partition)(result.name, result.errors, accepted);
155
164
  if (suppressed === 0)
156
165
  return result;
157
- return { ...result, errors: newErrors, status: newErrors.length > 0 ? 'fail' : 'pass', newCount: newErrors.length, baselineCount: suppressed };
166
+ const status = newErrors.length > 0 ? 'fail' : 'pass';
167
+ const { failureKind: _drop, ...rest } = result;
168
+ return {
169
+ ...rest,
170
+ errors: newErrors,
171
+ status,
172
+ // failureKind only applies to fail; a baseline-suppressed pass must not keep it (QA-4).
173
+ ...(status === 'fail' && result.failureKind ? { failureKind: result.failureKind } : {}),
174
+ newCount: newErrors.length,
175
+ baselineCount: suppressed,
176
+ };
158
177
  }