agentic-workflow-manager 9.12.5 → 9.13.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.
@@ -106,6 +106,7 @@ function reportPayload(report, planPath) {
106
106
  case 'valid':
107
107
  return {
108
108
  state: report.state, path: planPath, schema: report.schema, planId: report.manifest.planId,
109
+ ...(report.dispatchMode ? { dispatchMode: report.dispatchMode } : {}),
109
110
  planDigest: report.planDigest,
110
111
  requirements: report.manifest.requirements.length, sources: report.manifest.sources.length,
111
112
  commands: report.manifest.commands.length, slices: report.manifest.slices.length, completeOwnership: true,
@@ -259,11 +260,15 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
259
260
  .description('validate a compact plan without modifying it')
260
261
  .option('--json', 'emit one stable JSON report')
261
262
  .option('--cwd <path>', 'repository root for plan containment and source resolution')
263
+ .option('--require-dispatch-mode', 'require an explicit dispatch choice for a newly authored plan')
262
264
  .action((planPath, options) => {
263
265
  assertText(planPath, 'plan path');
264
266
  const cwd = options.cwd ?? process.cwd();
265
267
  assertText(cwd, '--cwd');
266
- const report = deps.validatePlanFile(planPath, cwd);
268
+ const validated = deps.validatePlanFile(planPath, cwd);
269
+ const report = options.requireDispatchMode && validated.state === 'valid' && !validated.dispatchMode
270
+ ? { state: 'invalid', diagnostics: [{ code: 'PLAN_DISPATCH_MODE', message: 'new plans must declare Modo de despacho before the manifest' }] }
271
+ : validated;
267
272
  const output = options.json === true
268
273
  ? `${JSON.stringify(reportPayload(report, planPath))}\n`
269
274
  : formatReport(report, planPath);
@@ -152,6 +152,8 @@ function resolveV1Dispatch(input) {
152
152
  if (!input.plan || input.plan.schema !== 'compact-slices/v1')
153
153
  throw new Error('resolveV1Dispatch requires an authenticated v1 plan report');
154
154
  (0, validate_2.assertVerifiedValidPlanReport)(input.plan);
155
+ if (input.plan.dispatchMode === 'proveedor-nativo' && input.optInV1)
156
+ return blocked('ROUTING_DISPATCH_MODE', 'Provider-native plan forbids v1 routing opt-in; create a separately approved routed plan.');
155
157
  if (!input.optInV1)
156
158
  return { state: 'not-required', reason: 'v1-without-opt-in' };
157
159
  return resolveConfiguredSelection({ ...input, requestedProfile: 'full' });
@@ -395,6 +395,12 @@ function validatePlan(planPath, cwd, snapshot) {
395
395
  return diagnostic('PLAN_LIMIT', 'plan id exceeds maximum string size');
396
396
  if ((raw.schema !== 'compact-slices/v1' && raw.schema !== 'compact-slices/v2') || (raw.schema === 'compact-slices/v1' && start !== START) || (raw.schema === 'compact-slices/v2' && start !== START_V2) || typeof raw.planId !== 'string' || !PLAN_ID.test(raw.planId) || !allStrings(raw.requirements) || new Set(raw.requirements).size !== raw.requirements.length)
397
397
  return diagnostic('PLAN_SHAPE', 'manifest scalar fields are invalid');
398
+ const dispatchHeaders = Array.from(text.slice(0, text.indexOf(start)).matchAll(/^[ \t]*\*\*Modo de despacho:\*\*[ \t]*([^\r\n]*)$/gm), match => match[1].trim());
399
+ if (dispatchHeaders.length > 1 || dispatchHeaders.some(value => value !== 'proveedor-nativo' && value !== 'awm-routed'))
400
+ return diagnostic('PLAN_DISPATCH_MODE', 'dispatch mode must occur once and be proveedor-nativo or awm-routed');
401
+ const dispatchMode = dispatchHeaders[0];
402
+ if (dispatchMode && ((dispatchMode === 'proveedor-nativo') !== (raw.schema === 'compact-slices/v1')))
403
+ return diagnostic('PLAN_DISPATCH_MODE', 'dispatch mode does not match compact plan schema');
398
404
  if (raw.requirements.some((id) => !validRequirementId(id)) || !allStrings(raw.closureCommands) || new Set(raw.closureCommands).size !== raw.closureCommands.length)
399
405
  return diagnostic('PLAN_SHAPE', 'manifest arrays or identifiers are invalid');
400
406
  const sourceIds = new Set();
@@ -488,6 +494,7 @@ function validatePlan(planPath, cwd, snapshot) {
488
494
  planDigest: crypto_1.default.createHash('sha256').update(text, 'utf8').digest('hex'),
489
495
  executionDigest: (0, identity_1.executionPlanDigest)(text),
490
496
  manifest: raw, executionMode: executionModeFromValidatedText(text),
497
+ ...(dispatchMode ? { dispatchMode } : {}),
491
498
  };
492
499
  freezeJson(report);
493
500
  verifiedValidReports.add(report);
@@ -57,6 +57,12 @@ describe('plan validate Commander wiring', () => {
57
57
  stdoutWrite.mockRestore();
58
58
  process.exitCode = previousExitCode;
59
59
  });
60
+ it('requires an explicit dispatch header for strict new-plan validation', async () => {
61
+ const program = commandFor(valid);
62
+ await program.parseAsync(['node', 'awm', 'plan', 'validate', 'plans/r4.md', '--cwd', repositoryRoot, '--require-dispatch-mode', '--json']);
63
+ expect(JSON.parse(String(stdoutWrite.mock.calls.at(-1)[0]))).toMatchObject({ state: 'invalid', diagnostics: [{ code: 'PLAN_DISPATCH_MODE' }] });
64
+ expect(process.exitCode).toBe(2);
65
+ });
60
66
  it('returns v1 not-required before reading personal policy or capability state', async () => {
61
67
  const policy = jest.fn();
62
68
  const capabilities = jest.fn();
@@ -10,6 +10,7 @@ const path_1 = __importDefault(require("path"));
10
10
  const crypto_1 = __importDefault(require("crypto"));
11
11
  const json_1 = require("../../../src/core/plan/json");
12
12
  const validate_1 = require("../../../src/core/plan/validate");
13
+ const resolve_1 = require("../../../src/core/model-policy/resolve");
13
14
  jest.mock('child_process', () => ({ exec: jest.fn(), execFileSync: jest.fn(), execSync: jest.fn(), spawn: jest.fn(), spawnSync: jest.fn() }));
14
15
  const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
15
16
  const END = '<!-- AWM:COMPACT-SLICES:END v1 -->';
@@ -99,6 +100,42 @@ describe('validatePlanFile', () => {
99
100
  const report = (0, validate_1.validatePlanFile)(v2Fixture(root, profile), root);
100
101
  expect(report).toMatchObject({ state: 'valid', schema: 'compact-slices/v2', manifest: { slices: [expect.objectContaining({ implementerProfile: profile })] } });
101
102
  });
103
+ test.each([
104
+ ['proveedor-nativo', false],
105
+ ['awm-routed', true],
106
+ ])('accepts an explicit %s dispatch mode only with its matching schema', (mode, v2) => {
107
+ const plan = v2 ? v2Fixture(root) : fixture(root);
108
+ const before = fs_1.default.readFileSync(plan, 'utf8');
109
+ fs_1.default.writeFileSync(plan, `**Modo de despacho:** ${mode}\n${before}`);
110
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'valid', dispatchMode: mode });
111
+ fs_1.default.writeFileSync(plan, `**Modo de despacho:** ${v2 ? 'proveedor-nativo' : 'awm-routed'}\n${before}`);
112
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [{ code: 'PLAN_DISPATCH_MODE' }] });
113
+ });
114
+ test.each(['indefinido', 'proveedor-nativo\n**Modo de despacho:** proveedor-nativo'])('rejects an invalid or duplicated dispatch header: %s', value => {
115
+ const plan = fixture(root);
116
+ fs_1.default.writeFileSync(plan, `**Modo de despacho:** ${value}\n${fs_1.default.readFileSync(plan, 'utf8')}`);
117
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [{ code: 'PLAN_DISPATCH_MODE' }] });
118
+ });
119
+ test('binds an explicit dispatch choice into plan and execution identity', () => {
120
+ const plan = fixture(root);
121
+ const historical = (0, validate_1.validatePlanFile)(plan, root);
122
+ fs_1.default.writeFileSync(plan, `**Modo de despacho:** proveedor-nativo\n${fs_1.default.readFileSync(plan, 'utf8')}`);
123
+ const explicit = (0, validate_1.validatePlanFile)(plan, root);
124
+ expect(historical).toMatchObject({ state: 'valid' });
125
+ expect(explicit).toMatchObject({ state: 'valid', dispatchMode: 'proveedor-nativo' });
126
+ if (historical.state !== 'valid' || explicit.state !== 'valid')
127
+ throw new Error('both v1 plans must validate');
128
+ expect(explicit.planDigest).not.toBe(historical.planDigest);
129
+ expect(explicit.executionDigest).not.toBe(historical.executionDigest);
130
+ });
131
+ test('does not let v1 opt-in override an explicit provider-native plan', () => {
132
+ const plan = fixture(root);
133
+ fs_1.default.writeFileSync(plan, `**Modo de despacho:** proveedor-nativo\n${fs_1.default.readFileSync(plan, 'utf8')}`);
134
+ const report = (0, validate_1.validatePlanFile)(plan, root);
135
+ if (report.state !== 'valid')
136
+ throw new Error('provider-native fixture must validate');
137
+ expect((0, resolve_1.resolveV1Dispatch)({ plan: report, optInV1: true, role: 'controller', runtime: { target: 'codex', kind: 'native', version: '1.0.0', accountScopeDigest: 'a'.repeat(64) }, now: new Date() })).toMatchObject({ state: 'blocked', diagnostics: [{ code: 'ROUTING_DISPATCH_MODE' }] });
138
+ });
102
139
  test('rejects v1 profile without weakening exact keys', () => {
103
140
  expect((0, validate_1.validatePlanFile)(fixture(root, manifest => { (manifest.slices[0]).implementerProfile = 'mechanical'; }), root)).toMatchObject({ state: 'invalid' });
104
141
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.12.5",
3
+ "version": "9.13.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file