agentic-workflow-manager 9.12.4 → 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.
- package/dist/src/commands/plan/index.js +6 -1
- package/dist/src/commands/watch/apply.js +10 -3
- package/dist/src/core/model-policy/resolve.js +2 -0
- package/dist/src/core/plan/validate.js +7 -0
- package/dist/tests/commands/plan/index.test.js +6 -0
- package/dist/tests/commands/watch/reopened-196.test.js +42 -3
- package/dist/tests/core/plan/validate.test.js +37 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -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
|
|
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);
|
|
@@ -542,11 +542,18 @@ function applyRequestToState(s, env, digest, repoRoot, eventScope) {
|
|
|
542
542
|
{ taskId, entity: 'dispatch', dispatchId: existing.id },
|
|
543
543
|
{ taskId, dispatchId: existing.id, entity: 'dispatch' },
|
|
544
544
|
].map(payload => (0, requests_1.digestOf)(payload));
|
|
545
|
-
const
|
|
545
|
+
const appliedAcks = Object.values(s.appliedRequests);
|
|
546
|
+
const oldAckIndex = appliedAcks.findIndex(ack => ack.outcome === 'applied'
|
|
546
547
|
&& ack.resultRef === existing.id && legacyDigests.includes(ack.payloadDigest));
|
|
548
|
+
const reservationAckIndex = appliedAcks.findIndex(ack => ack.requestId === attempt.reservationRequestId);
|
|
549
|
+
// The single writer appends ACKs to appliedRequests and persists
|
|
550
|
+
// their insertion order. Equal millisecond timestamps need this
|
|
551
|
+
// additional durable ordering evidence, not an inferred child.
|
|
552
|
+
const legacyPrecedesReservation = oldAt < reservedAt
|
|
553
|
+
|| (oldAt === reservedAt && oldAckIndex >= 0 && reservationAckIndex > oldAckIndex);
|
|
547
554
|
const safeLegacy = reservedForNewDispatch && Number.isFinite(oldAt) && Number.isFinite(reservedAt)
|
|
548
|
-
&&
|
|
549
|
-
&&
|
|
555
|
+
&& legacyPrecedesReservation
|
|
556
|
+
&& oldAckIndex >= 0
|
|
550
557
|
&& s.dispatches.filter(d => d.taskId === taskId).length === 1
|
|
551
558
|
&& s.tasks.find(t => t.id === taskId)?.attempts === 1
|
|
552
559
|
&& s.tasks.find(t => t.id === taskId)?.status === 'in-progress'
|
|
@@ -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();
|
|
@@ -224,6 +224,37 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
224
224
|
.toThrow(/conflictivo/);
|
|
225
225
|
expect(state.dispatches).toHaveLength(2);
|
|
226
226
|
});
|
|
227
|
+
test('equal-millisecond legacy timestamps reconcile only when the durable dispatch ACK precedes the reservation ACK', () => {
|
|
228
|
+
const state = v2State();
|
|
229
|
+
const at = state.routingAttempts[0].reservedAt;
|
|
230
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1', at });
|
|
231
|
+
state.tasks[0].attempts = 1;
|
|
232
|
+
state.tasks[0].status = 'in-progress';
|
|
233
|
+
const reservationAck = state.appliedRequests['reserve-1'];
|
|
234
|
+
const historicalAck = { requestId: 'old-dispatch-request', idempotencyKey: 'old-dispatch-key',
|
|
235
|
+
payloadDigest: legacyDispatchDigest(), outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
236
|
+
state.appliedRequests = { 'old-dispatch-request': historicalAck, 'reserve-1': reservationAck };
|
|
237
|
+
const durable = JSON.parse(JSON.stringify(state));
|
|
238
|
+
expect(Object.keys(durable.appliedRequests)).toEqual(['old-dispatch-request', 'reserve-1']);
|
|
239
|
+
dispatch(durable, { routingAttemptId: 'route-1' });
|
|
240
|
+
expect(durable.dispatches).toHaveLength(2);
|
|
241
|
+
expect(durable.dispatches[0]).toEqual({ id: 'dispatch-S1', taskId: 'S1', at });
|
|
242
|
+
expect(durable.appliedRequests['old-dispatch-request']).toEqual(historicalAck);
|
|
243
|
+
expect(durable.tasks[0].attempts).toBe(1);
|
|
244
|
+
expect((0, types_1.isWellFormedState)(durable)).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
test('equal-millisecond legacy timestamps do not infer order from a later dispatch ACK', () => {
|
|
247
|
+
const state = v2State();
|
|
248
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1', at: state.routingAttempts[0].reservedAt });
|
|
249
|
+
state.tasks[0].attempts = 1;
|
|
250
|
+
state.tasks[0].status = 'in-progress';
|
|
251
|
+
state.appliedRequests['old-dispatch-request'] = { requestId: 'old-dispatch-request',
|
|
252
|
+
idempotencyKey: 'old-dispatch-key', payloadDigest: legacyDispatchDigest(),
|
|
253
|
+
outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
254
|
+
expect(() => dispatch(state, { routingAttemptId: 'route-1' })).toThrow(/legacy handoff/);
|
|
255
|
+
expect(state.dispatches).toHaveLength(1);
|
|
256
|
+
expect(state.appliedRequests['dispatch-request']).toBeUndefined();
|
|
257
|
+
});
|
|
227
258
|
test('historical ACK matching is independent of JSON key order but not payload identity', () => {
|
|
228
259
|
for (const rawJson of [
|
|
229
260
|
'{"taskId":"S1","dispatchId":"dispatch-S1"}',
|
|
@@ -972,10 +1003,10 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
972
1003
|
(0, apply_1.consumePendingRequests)(repo, 'main', generation.token, scope);
|
|
973
1004
|
expect((0, store_1.readJournal)(repo, 'main').state.requestProblems).toEqual([]);
|
|
974
1005
|
expect(cli('ack', task.requestId).state).toBe('applied');
|
|
975
|
-
// Durable 9.12.2 intent
|
|
976
|
-
//
|
|
1006
|
+
// Durable 9.12.2 intent and ACK precede the route reservation.
|
|
1007
|
+
// Equal timestamp granularity alone must not imply a child.
|
|
977
1008
|
const historical = (0, store_1.readJournal)(repo, 'main').state;
|
|
978
|
-
|
|
1009
|
+
let historicalAt = new Date(Date.now() - 60_000).toISOString();
|
|
979
1010
|
historical.dispatches.push({ id: 'dispatch-S1', taskId: 'S1', at: historicalAt });
|
|
980
1011
|
historical.tasks[0].attempts = 1;
|
|
981
1012
|
historical.tasks[0].status = 'in-progress';
|
|
@@ -989,6 +1020,14 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
989
1020
|
const reservation = cli('ack', reserve.requestId);
|
|
990
1021
|
expect(reservation.state).toBe('applied');
|
|
991
1022
|
const attemptId = reservation.resultRef;
|
|
1023
|
+
// Reproduce the real journal's millisecond tie without changing
|
|
1024
|
+
// the durable ACK order or adding any native evidence.
|
|
1025
|
+
const tied = (0, store_1.readJournal)(repo, 'main').state;
|
|
1026
|
+
historicalAt = tied.routingAttempts.find(attempt => attempt.id === attemptId).reservedAt;
|
|
1027
|
+
tied.dispatches[0].at = historicalAt;
|
|
1028
|
+
expect(Object.keys(tied.appliedRequests).indexOf('old-dispatch-request'))
|
|
1029
|
+
.toBeLessThan(Object.keys(tied.appliedRequests).indexOf(reserve.requestId));
|
|
1030
|
+
(0, store_1.writeJournal)(repo, 'main', tied);
|
|
992
1031
|
const dispatchPayload = JSON.stringify({ dispatchId: 'dispatch-S1', taskId: 'S1', routingAttemptId: attemptId });
|
|
993
1032
|
const queuedDispatch = cli('register', '--generation', generation.token, '--entity', 'dispatch', '--json', dispatchPayload);
|
|
994
1033
|
expect(cli('ack', queuedDispatch.requestId).state).toBe('pending');
|
|
@@ -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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|