agentic-workflow-manager 9.14.0 → 9.14.1

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.
@@ -166,7 +166,7 @@ function sanitizeAdmissionReport(report) {
166
166
  invalidAdmission();
167
167
  if (value.cliCurrentness !== undefined && !CLI_CURRENTNESS.has(value.cliCurrentness))
168
168
  invalidAdmission();
169
- if (value.sensorEvidence !== undefined && value.sensorEvidence !== 'generation-bound')
169
+ if (value.sensorEvidence !== undefined && value.sensorEvidence !== 'generation-bound' && value.sensorEvidence !== 'r8-registry-opt-out')
170
170
  invalidAdmission();
171
171
  if (value.planDigest !== undefined && (typeof value.planDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.planDigest)))
172
172
  invalidAdmission();
@@ -225,12 +225,18 @@ async function admitPlan(input) {
225
225
  if (input.compatibilityDiagnostics?.length)
226
226
  return blocked(input, input.compatibilityDiagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: 'unverifiable', cliCurrentness });
227
227
  }
228
+ let sensorsReported = 'not-required';
229
+ let sensorEvidence = {};
228
230
  if (input.verifySensors) {
229
231
  if (!input.sensors)
230
232
  return blocked(input, [diagnostic('ADMISSION_SENSORS_REQUIRED', 'Sensor evidence was required but not supplied.')], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness });
231
233
  const sensors = sensorVerdict(input.sensors);
232
- if (sensors !== 'pass')
234
+ const registryOptOut = sensors === 'not-certified' && input.sensorClosurePolicy === 'r8-registry-opt-out' && input.sensors.mode === 'opt-out';
235
+ if (sensors !== 'pass' && !registryOptOut)
233
236
  return blocked(input, [diagnostic('ADMISSION_SENSORS_BLOCKED', `Sensor verdict is ${sensors}.`)], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors });
237
+ sensorsReported = sensors;
238
+ if (registryOptOut)
239
+ sensorEvidence = { sensorEvidence: 'r8-registry-opt-out' };
234
240
  }
235
241
  if (mode === 'desatendido') {
236
242
  // The durable controller (journal + awm watch + jobs) is opt-in, as it
@@ -241,7 +247,7 @@ async function admitPlan(input) {
241
247
  const observed = journalStatus(input, plan);
242
248
  const journal = observed === 'missing' && plan.schema === 'compact-slices/v1' ? 'not-required' : observed;
243
249
  if (journal !== 'current' && journal !== 'not-required')
244
- return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: input.verifySensors ? 'pass' : 'not-required' });
250
+ return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: sensorsReported, ...sensorEvidence });
245
251
  const capabilities = unattendedCapabilities(provider, input.controllerAutonomy);
246
252
  const resolution = { outcome: capabilities.unattendedController === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
247
253
  if (resolution.outcome === 'blocked') {
@@ -252,17 +258,17 @@ async function admitPlan(input) {
252
258
  const reason = exports.PROVIDER_EXECUTION_CAPABILITIES[provider].unattendedController !== 'supported'
253
259
  ? diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified unattended execution capability.`)
254
260
  : diagnostic('ADMISSION_CONTROLLER_AUTONOMY_REQUIRED', `Unattended dispatch on ${provider} requires an explicit controller autonomy posture; without it the controller blocks on its first tool call.`);
255
- return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution });
261
+ return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: sensorsReported, ...sensorEvidence, capabilityResolution: resolution });
256
262
  }
257
263
  const routing = plan.schema === 'compact-slices/v2' ? input.routing ?? await input.routingReader?.() : input.routing;
258
- return completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution, cliCurrentness);
264
+ return { ...completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', sensorsReported, resolution, cliCurrentness), ...sensorEvidence };
259
265
  }
260
266
  const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
261
267
  const resolution = { outcome: capabilities.interactiveExecution === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
262
268
  if (resolution.outcome === 'blocked')
263
269
  return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified interactive execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, capabilityResolution: resolution });
264
270
  const routing = plan.schema === 'compact-slices/v2' ? input.routing ?? await input.routingReader?.() : input.routing;
265
- return completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution, cliCurrentness);
271
+ return { ...completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked', sensorsReported, resolution, cliCurrentness), ...sensorEvidence };
266
272
  }
267
273
  function completeAdmission(input, plan, provider, executionMode, journal, currentness, sensors, capabilityResolution, cliCurrentness = 'not-checked') {
268
274
  if (plan.schema === 'compact-slices/v1')
@@ -271,6 +271,17 @@ function hasOwnedGenerationAdmission(input) {
271
271
  || (generation.wrapperRef !== undefined && generation.wrapperRef.spawnNonce === generation.spawnNonce
272
272
  && (0, process_1.refIsAlive)(generation.wrapperRef));
273
273
  }
274
+ /** R8: only a content registry (its manifest at the sensor project root) may close on a deliberate opt-out. */
275
+ function registryOptOutPolicy(sensors) {
276
+ if (sensors?.mode !== 'opt-out' || !sensors.projectRoot)
277
+ return undefined;
278
+ try {
279
+ return fs_1.default.statSync(path_1.default.join(sensors.projectRoot, registries_1.REGISTRY_MANIFEST_NAME)).isFile() ? 'r8-registry-opt-out' : undefined;
280
+ }
281
+ catch {
282
+ return undefined;
283
+ }
284
+ }
274
285
  /** One admission authority for the public command AND actual watch dispatch.
275
286
  * No empirical execution occurs before provenance/currentness/CLI-floor gates. */
276
287
  async function admitRegistryPlan(input, dependencies = {}) {
@@ -308,8 +319,9 @@ async function admitRegistryPlan(input, dependencies = {}) {
308
319
  return scope.provenance === 'proven';
309
320
  };
310
321
  let generationBound = false;
322
+ let sensorClosurePolicy;
311
323
  const compose = async (fields) => {
312
- const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...fields });
324
+ const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...(sensorClosurePolicy ? { sensorClosurePolicy } : {}), ...fields });
313
325
  const withEvidence = generationBound && report.sensors === 'pass' ? { ...report, sensorEvidence: 'generation-bound' } : report;
314
326
  if (scope.provenanceDiagnostic && report.diagnostics.some(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED')) {
315
327
  return { ...withEvidence, diagnostics: report.diagnostics.map(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED' ? scope.provenanceDiagnostic : diagnostic) };
@@ -337,6 +349,7 @@ async function admitRegistryPlan(input, dependencies = {}) {
337
349
  const sensors = input.verifySensors ? input.sensors ?? (generationBound
338
350
  ? { sensors: [], overall: 'pass', reason: 'generation-bound-admission' }
339
351
  : await (dependencies.runSensors ?? run_1.runSensors)({ cwd: input.cwd, all: true, readOnly: true })) : undefined;
352
+ sensorClosurePolicy = registryOptOutPolicy(sensors);
340
353
  if (input.requireCurrent && input.verifySensors) {
341
354
  if (!refresh())
342
355
  return compose({ currentness, sensors });
@@ -309,6 +309,32 @@ describe('plan admit Commander wiring', () => {
309
309
  process.exitCode = undefined;
310
310
  }
311
311
  });
312
+ it.each([
313
+ ['a content registry', true, 'admitted'],
314
+ ['an ordinary project', false, 'blocked'],
315
+ ])('applies the R8 sensor opt-out exception only to %s', async (_name, isRegistry, expected) => {
316
+ const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-r8-')));
317
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
318
+ if (isRegistry)
319
+ fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
320
+ const sensors = jest.fn().mockResolvedValue({ overall: 'not_certified', sensors: [], mode: 'opt-out', reason: 'opt-out-declared', projectRoot: root, manifestPath: path_1.default.join(root, '.awm', 'sensors.json') });
321
+ const program = new commander_1.Command();
322
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, listRegistries: () => [], readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }), runSensors: sensors });
323
+ try {
324
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'interactivo', '--verify-sensors', '--json']);
325
+ const report = JSON.parse(String(output.mock.calls.at(-1)[0]));
326
+ expect(report).toMatchObject({ state: expected, sensors: 'not-certified' });
327
+ if (isRegistry)
328
+ expect(report.sensorEvidence).toBe('r8-registry-opt-out');
329
+ else
330
+ expect(report.diagnostics[0].code).toBe('ADMISSION_SENSORS_BLOCKED');
331
+ }
332
+ finally {
333
+ output.mockRestore();
334
+ fs_1.default.rmSync(root, { recursive: true, force: true });
335
+ process.exitCode = undefined;
336
+ }
337
+ });
312
338
  it('derives desatendido from the canonical plan header; a native v1 plan needs no journal (opt-in durable custody)', async () => {
313
339
  const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-header-'));
314
340
  const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
@@ -161,21 +161,30 @@ describe('runCommand — exit codes and output', () => {
161
161
  });
162
162
  itLinux('never lets a structured capture file grow beyond maxBuffer', async () => {
163
163
  const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-cap-'));
164
- const marker = path_1.default.join(dir, 'written');
165
- const existing = new Set(fs_1.default.readdirSync(os_1.default.tmpdir()).filter(name => name.startsWith('awm-sensor-output-')));
164
+ // The overflow watcher kills the child and deletes the capture as soon as
165
+ // the cap is reached, so polling for the file races that cleanup. Capture
166
+ // files only grow, so their size at deletion is the largest they ever were.
167
+ const captureSizes = [];
168
+ const rmSync = fs_1.default.rmSync.bind(fs_1.default);
169
+ const removal = jest.spyOn(fs_1.default, 'rmSync').mockImplementation((target, options) => {
170
+ if (typeof target === 'string' && path_1.default.basename(target).startsWith('awm-sensor-output-')) {
171
+ captureSizes.push(fs_1.default.statSync(path_1.default.join(target, 'stdout')).size);
172
+ }
173
+ return rmSync(target, options);
174
+ });
166
175
  try {
167
- const result = (0, exec_1.runStructuredCommand)({
176
+ const result = await (0, exec_1.runStructuredCommand)({
168
177
  executable: 'node',
169
178
  resolution: 'path',
170
- args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {} fs.writeFileSync(${JSON.stringify(marker)}, 'written'); setTimeout(() => {}, 1000);`],
179
+ args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {} setTimeout(() => {}, 1000);`],
171
180
  }, { timeout: 5_000, cwd: dir, maxBuffer: 1_024 });
172
- expect(await until(() => fs_1.default.existsSync(marker))).toBe(true);
173
- const capture = fs_1.default.readdirSync(os_1.default.tmpdir()).find(name => name.startsWith('awm-sensor-output-') && !existing.has(name));
174
- expect(capture).toBeDefined();
175
- expect(fs_1.default.statSync(path_1.default.join(os_1.default.tmpdir(), capture, 'stdout')).size).toBeLessThanOrEqual(1_024);
176
- await expect(result).resolves.toMatchObject({ overflowed: true });
181
+ expect(result).toMatchObject({ overflowed: true });
182
+ expect(captureSizes).toHaveLength(1);
183
+ expect(captureSizes[0]).toBeGreaterThan(0);
184
+ expect(captureSizes[0]).toBeLessThanOrEqual(1_024);
177
185
  }
178
186
  finally {
187
+ removal.mockRestore();
179
188
  fs_1.default.rmSync(dir, { recursive: true, force: true });
180
189
  }
181
190
  });
@@ -178,6 +178,45 @@ describe('admitPlan', () => {
178
178
  expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
179
179
  });
180
180
  });
181
+ // R8: a content registry whose sensors are ALL deliberately disabled may close
182
+ // work; its release proof lives in CI. The local verdict stays not-certified —
183
+ // never reported as pass — and the exception is named in the evidence.
184
+ describe('the R8 registry opt-out closure exception', () => {
185
+ const optOut = { overall: 'not_certified', sensors: [], mode: 'opt-out', reason: 'opt-out-declared', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json' };
186
+ const unattended = () => ({ ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } });
187
+ const base = () => ({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], verifySensors: true, sensors: optOut, sensorClosurePolicy: 'r8-registry-opt-out' });
188
+ it('admits interactive work without reporting the opt-out as a pass', async () => {
189
+ const report = await (0, admission_1.admitPlan)(base());
190
+ expect(report).toMatchObject({ state: 'admitted', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
191
+ });
192
+ it('admits journal-less unattended work under the same exception', async () => {
193
+ const report = await (0, admission_1.admitPlan)({ ...base(), plan: unattended(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
194
+ expect(report).toMatchObject({ state: 'admitted', executionMode: 'desatendido', journal: 'not-required', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
195
+ });
196
+ it('reports the preserved verdict when a later gate blocks', async () => {
197
+ const report = await (0, admission_1.admitPlan)({ ...base(), plan: unattended(), planPath: 'docs/plan.md' });
198
+ expect(report).toMatchObject({ state: 'blocked', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
199
+ expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
200
+ });
201
+ it.each([
202
+ ['no declared policy', { sensorClosurePolicy: undefined }],
203
+ ['a failing sensor', { sensors: { ...optOut, overall: 'fail', mode: 'project-sensors' } }],
204
+ ['an absent manifest', { sensors: { ...optOut, mode: 'missing', reason: 'manifest-absent' } }],
205
+ ['a native gate', { sensors: { ...optOut, mode: 'native-gate', reason: 'native-gate-declared' } }],
206
+ ['an unavailable sensor source', { sensors: { ...optOut, mode: 'source-unavailable' } }],
207
+ ['all-disabled legacy sensors', { sensors: { ...optOut, overall: 'skipped', mode: 'project-sensors' } }],
208
+ ])('keeps blocking with %s', async (_name, fields) => {
209
+ const report = await (0, admission_1.admitPlan)({ ...base(), ...fields });
210
+ expect(report.state).toBe('blocked');
211
+ expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_SENSORS_BLOCKED' });
212
+ expect(report.sensorEvidence).toBeUndefined();
213
+ });
214
+ it('accepts only the known sensor evidence values at the public sanitizer', () => {
215
+ const report = { state: 'admitted', planState: 'valid', journal: 'not-required', currentness: 'not-checked', sensors: 'not-certified', diagnostics: [] };
216
+ expect((0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'r8-registry-opt-out' }).sensorEvidence).toBe('r8-registry-opt-out');
217
+ expect(() => (0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'opt-out' })).toThrow('invalid report');
218
+ });
219
+ });
181
220
  it('admits unattended work only with an exact schema-2 plan binding', async () => {
182
221
  const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
183
222
  const journal = { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.14.0",
3
+ "version": "9.14.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
@@ -65,7 +65,7 @@
65
65
  "dependency-cruiser": "16.10.4",
66
66
  "eslint": "10.8.1",
67
67
  "jest": "^30.2.0",
68
- "node-gyp": "^11.5.0",
68
+ "node-gyp": "^12.4.0",
69
69
  "ts-jest": "^29.4.6",
70
70
  "ts-node": "^10.9.2",
71
71
  "typescript": "^5.9.3"
Binary file
Binary file