agentic-workflow-manager 9.15.1 → 9.16.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.
- package/dist/src/core/admission/index.js +4 -1
- package/dist/src/core/journal/process.js +22 -8
- package/dist/src/providers/index.js +9 -0
- package/dist/tests/commands/init.test.js +4 -0
- package/dist/tests/core/admission/index.test.js +26 -4
- package/dist/tests/core/journal/controller-autonomy.test.js +9 -2
- package/dist/tests/core/journal/process.test.js +33 -10
- package/dist/tests/core/provider-version.test.js +20 -0
- package/dist/tests/providers/index.test.js +8 -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
|
@@ -25,7 +25,10 @@ exports.PROVIDER_EXECUTION_CAPABILITIES = {
|
|
|
25
25
|
opencode: { ...UNKNOWN },
|
|
26
26
|
'claude-code': { ...UNKNOWN, interactiveExecution: 'supported', unattendedController: 'supported', durableResume: 'supported' },
|
|
27
27
|
codex: { ...UNKNOWN, interactiveExecution: 'supported', unattendedController: 'supported', durableResume: 'supported' },
|
|
28
|
-
|
|
28
|
+
// Plan C (native session only): interactive + journal-less unattended v1.
|
|
29
|
+
// durableResume stays unverified — Camino B / awm watch is suspended (D-023)
|
|
30
|
+
// and Cursor is not in WATCH_PROVIDERS. Evidence: docs/testing/cursor-execution-evidence.md
|
|
31
|
+
cursor: { ...UNKNOWN, interactiveExecution: 'supported', unattendedController: 'supported' },
|
|
29
32
|
copilot: { ...UNKNOWN },
|
|
30
33
|
};
|
|
31
34
|
/** #168: the controller can only act if it was launched with an autonomy
|
|
@@ -470,7 +470,10 @@ function descendantActivity(psTable, rootPid) {
|
|
|
470
470
|
const children = new Map();
|
|
471
471
|
let rows = 0;
|
|
472
472
|
for (const line of psTable.split('\n')) {
|
|
473
|
-
|
|
473
|
+
// time may be empty on some Darwin samples for a just-spawned process;
|
|
474
|
+
// treat blank as "0" so the pid still joins the descendant tree (otherwise
|
|
475
|
+
// a healthy detached grandchild is invisible and activity reads as frozen).
|
|
476
|
+
const match = /^\s*(\d+)\s+(\d+)(?:\s+(\S+))?\s*$/.exec(line);
|
|
474
477
|
if (match === null)
|
|
475
478
|
continue;
|
|
476
479
|
if (++rows > MAX_PS_ROWS)
|
|
@@ -478,7 +481,7 @@ function descendantActivity(psTable, rootPid) {
|
|
|
478
481
|
const pid = Number(match[1]);
|
|
479
482
|
const ppid = Number(match[2]);
|
|
480
483
|
const siblings = children.get(ppid) ?? [];
|
|
481
|
-
siblings.push({ pid, time: match[3] });
|
|
484
|
+
siblings.push({ pid, time: match[3] && match[3].length > 0 ? match[3] : '0' });
|
|
482
485
|
children.set(ppid, siblings);
|
|
483
486
|
}
|
|
484
487
|
const seen = new Set([rootPid]);
|
|
@@ -499,13 +502,24 @@ function descendantActivity(psTable, rootPid) {
|
|
|
499
502
|
return `${found.length}:${crypto_1.default.createHash('sha256').update(found.join('\n')).digest('hex')}`;
|
|
500
503
|
}
|
|
501
504
|
function observeDescendants(rootPid) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
505
|
+
// One transient `ps` failure under CI load used to freeze the snapshot at
|
|
506
|
+
// `unknown`, which made the Darwin integration test (and a real supervisor
|
|
507
|
+
// tick) treat a later concrete `0:none` as "activity changed" without ever
|
|
508
|
+
// seeing the grandchild. Retry once before degrading.
|
|
509
|
+
let lastError;
|
|
510
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
511
|
+
try {
|
|
512
|
+
const table = (0, child_process_1.execFileSync)('ps', ['-A', '-o', 'pid=,ppid=,time='], {
|
|
513
|
+
encoding: 'utf8', stdio: exports.EXEC_STDIO, timeout: PS_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024,
|
|
514
|
+
});
|
|
515
|
+
return descendantActivity(table, rootPid);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
lastError = error;
|
|
519
|
+
}
|
|
508
520
|
}
|
|
521
|
+
void lastError;
|
|
522
|
+
return 'unknown';
|
|
509
523
|
}
|
|
510
524
|
/** Llamada UNA VEZ POR TICK por el supervisor (ver superviseController en
|
|
511
525
|
* commands/watch/supervisor.ts) mientras un controlador este activo — el
|
|
@@ -163,6 +163,15 @@ function providers() {
|
|
|
163
163
|
cursor: {
|
|
164
164
|
label: 'Cursor',
|
|
165
165
|
configHome: configHomeFor('cursor'),
|
|
166
|
+
// Date-based CLI line (YYYY.MM.DD-<hash>). compareSemver orders the
|
|
167
|
+
// date triple as [YYYY, M, D]. Floor from owner-machine capture
|
|
168
|
+
// 2026.09.26-dd393fe (docs/testing/cursor-execution-evidence.md).
|
|
169
|
+
minimumVersion: '2026.09.26',
|
|
170
|
+
versionCommand: {
|
|
171
|
+
command: 'agent',
|
|
172
|
+
args: ['--version'],
|
|
173
|
+
versionPattern: /^(\d{4}\.\d{2}\.\d{2})-[0-9a-f]+$/,
|
|
174
|
+
},
|
|
166
175
|
// Skills join the Codex/OpenCode shared physical dirs (~/.agents/skills +
|
|
167
176
|
// .agents/skills, renderer link). cursor-mdc remains registered for S3
|
|
168
177
|
// migration reads of historical ~/.cursor/rules installs.
|
|
@@ -434,6 +434,10 @@ describe('runInit', () => {
|
|
|
434
434
|
agent: 'cursor',
|
|
435
435
|
yes: true,
|
|
436
436
|
machineOnly: true,
|
|
437
|
+
// Plan C adds a Cursor version gate; CI has no `agent` binary.
|
|
438
|
+
// Stub the same way Codex init tests do — the gate itself is
|
|
439
|
+
// covered in provider-version.test.ts.
|
|
440
|
+
assertProviderSupported: () => ({ provider: 'cursor', version: '2026.09.26' }),
|
|
437
441
|
// seedBaselineRegistry() writes registries.json pointing at 'baseline'
|
|
438
442
|
// (our manually-seeded content root above) since none exists yet — no
|
|
439
443
|
// real network clone happens because that content root already exists
|
|
@@ -27,7 +27,7 @@ describe('admitPlan', () => {
|
|
|
27
27
|
});
|
|
28
28
|
it.each([
|
|
29
29
|
['journal', { provider: 'codex', executionMode: 'desatendido' }],
|
|
30
|
-
['provider capability', { provider: '
|
|
30
|
+
['provider capability', { provider: 'copilot', executionMode: 'interactivo' }],
|
|
31
31
|
])('does not invoke deferred v2 routing facts when %s gate blocks', async (_name, fields) => {
|
|
32
32
|
const reader = jest.fn(() => { throw new Error('routing reader must not run'); });
|
|
33
33
|
const report = await (0, admission_1.admitPlan)({ plan: v2(), cwd: process.cwd(), enabledAgents: [fields.provider], routingReader: reader, ...fields });
|
|
@@ -139,10 +139,32 @@ describe('admitPlan', () => {
|
|
|
139
139
|
expect(report.currentness).toBe('not-checked');
|
|
140
140
|
});
|
|
141
141
|
it('does not let an unverified execution capability admit interactive work', async () => {
|
|
142
|
-
const report = await (0, admission_1.admitPlan)({ plan: valid, provider: '
|
|
143
|
-
expect(report).toMatchObject({ state: 'blocked', executionMode: 'interactivo', provider: '
|
|
142
|
+
const report = await (0, admission_1.admitPlan)({ plan: valid, provider: 'copilot', cwd: process.cwd(), enabledAgents: ['copilot'] });
|
|
143
|
+
expect(report).toMatchObject({ state: 'blocked', executionMode: 'interactivo', provider: 'copilot', journal: 'not-required' });
|
|
144
144
|
expect(report.diagnostics[0].code).toBe('ADMISSION_CAPABILITY_UNVERIFIED');
|
|
145
145
|
});
|
|
146
|
+
it('admits Cursor interactive work once Plan C marks interactiveExecution supported', async () => {
|
|
147
|
+
const report = await (0, admission_1.admitPlan)({ plan: valid, provider: 'cursor', cwd: process.cwd(), enabledAgents: ['cursor'] });
|
|
148
|
+
expect(report).toMatchObject({ state: 'admitted', executionMode: 'interactivo', provider: 'cursor', journal: 'not-required' });
|
|
149
|
+
expect(report.capabilityResolution).toMatchObject({ outcome: 'native', capabilities: { interactiveExecution: 'supported' } });
|
|
150
|
+
});
|
|
151
|
+
it('admits Cursor journal-less unattended v1 with an explicit autonomy posture', async () => {
|
|
152
|
+
const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
|
|
153
|
+
const report = await (0, admission_1.admitPlan)({
|
|
154
|
+
plan: unattended, provider: 'cursor', cwd: process.cwd(), enabledAgents: ['cursor'],
|
|
155
|
+
planPath: 'docs/plan.md', controllerAutonomy: 'approval-free',
|
|
156
|
+
});
|
|
157
|
+
expect(report).toMatchObject({ state: 'admitted', executionMode: 'desatendido', provider: 'cursor', journal: 'not-required' });
|
|
158
|
+
expect(report.capabilityResolution).toMatchObject({ outcome: 'native', capabilities: { unattendedController: 'supported' } });
|
|
159
|
+
});
|
|
160
|
+
it('still requires the autonomy posture for Cursor unattended work', async () => {
|
|
161
|
+
const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
|
|
162
|
+
const report = await (0, admission_1.admitPlan)({
|
|
163
|
+
plan: unattended, provider: 'cursor', cwd: process.cwd(), enabledAgents: ['cursor'], planPath: 'docs/plan.md',
|
|
164
|
+
});
|
|
165
|
+
expect(report.state).toBe('blocked');
|
|
166
|
+
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
|
|
167
|
+
});
|
|
146
168
|
// The durable controller (journal + awm watch + jobs) is opt-in again for
|
|
147
169
|
// native v1 dispatch, as it was before 9.8.0: a v1 unattended plan with no
|
|
148
170
|
// journal on its branch runs as one native provider session. Creating a
|
|
@@ -266,7 +288,7 @@ describe('admitPlan', () => {
|
|
|
266
288
|
expect(report.state).toBe('admitted');
|
|
267
289
|
});
|
|
268
290
|
it('keeps the original diagnostic for a provider that never had the capability', async () => {
|
|
269
|
-
const report = await (0, admission_1.admitPlan)({ plan: unattended(), provider: '
|
|
291
|
+
const report = await (0, admission_1.admitPlan)({ plan: unattended(), provider: 'copilot', cwd: process.cwd(), enabledAgents: ['copilot'], journalState: journal(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
|
|
270
292
|
expect(report.state).toBe('blocked');
|
|
271
293
|
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CAPABILITY_UNVERIFIED' });
|
|
272
294
|
});
|
|
@@ -58,8 +58,15 @@ describe('unattendedController stops being asserted for free (#168)', () => {
|
|
|
58
58
|
expect((0, admission_1.unattendedCapabilities)('claude-code', 'approval-free').unattendedController).toBe('supported');
|
|
59
59
|
});
|
|
60
60
|
test('never promotes a provider that never had the capability', () => {
|
|
61
|
-
expect(admission_1.PROVIDER_EXECUTION_CAPABILITIES.
|
|
62
|
-
expect((0, admission_1.unattendedCapabilities)('
|
|
61
|
+
expect(admission_1.PROVIDER_EXECUTION_CAPABILITIES.copilot.unattendedController).toBe('unverified');
|
|
62
|
+
expect((0, admission_1.unattendedCapabilities)('copilot', 'approval-free').unattendedController).toBe('unverified');
|
|
63
|
+
});
|
|
64
|
+
test('Cursor gains unattendedController only with a declared posture (Plan C)', () => {
|
|
65
|
+
expect(admission_1.PROVIDER_EXECUTION_CAPABILITIES.cursor.unattendedController).toBe('supported');
|
|
66
|
+
expect((0, admission_1.unattendedCapabilities)('cursor', undefined).unattendedController).toBe('unverified');
|
|
67
|
+
expect((0, admission_1.unattendedCapabilities)('cursor', 'approval-free').unattendedController).toBe('supported');
|
|
68
|
+
// Watch/custody resume is still unverified and suspended (D-023).
|
|
69
|
+
expect(admission_1.PROVIDER_EXECUTION_CAPABILITIES.cursor.durableResume).toBe('unverified');
|
|
63
70
|
});
|
|
64
71
|
});
|
|
65
72
|
describe('awm watch surfaces the posture on the compiled binary (#168)', () => {
|
|
@@ -202,28 +202,51 @@ describe('process identity', () => {
|
|
|
202
202
|
test('ignores malformed ps lines instead of inventing processes', () => {
|
|
203
203
|
expect((0, process_1.descendantActivity)('garbage\n 20 10 00:00:01\n\n x y z', 10)).toMatch(/^1:/);
|
|
204
204
|
});
|
|
205
|
+
test('keeps Darwin-style rows with blank or dotted cputime in the tree', () => {
|
|
206
|
+
// macOS `ps -o time=` is often `0:00.01`; a brand-new process can also
|
|
207
|
+
// omit the third column entirely on some samples.
|
|
208
|
+
expect((0, process_1.descendantActivity)(' 10 1 0:00.05\n 20 10 0:00.01\n 30 20\n', 10)).toMatch(/^2:/);
|
|
209
|
+
expect((0, process_1.descendantActivity)(' 10 1\n 20 10\n', 10)).toMatch(/^1:/);
|
|
210
|
+
});
|
|
205
211
|
});
|
|
206
212
|
test('activitySnapshot ve un nieto fuera del process group del controlador', async () => {
|
|
207
213
|
if ((0, paths_1.isWindowsNative)())
|
|
208
214
|
return; // win32 degrada activity sin ps (ver activitySnapshot)
|
|
209
|
-
const script = "setTimeout(() => { require('child_process').spawn(process.execPath, ['-e', 'setTimeout(()=>{},
|
|
215
|
+
const script = "setTimeout(() => { require('child_process').spawn(process.execPath, ['-e', 'setTimeout(()=>{}, 15000)'], { detached: true, stdio: 'ignore' }).unref(); }, 200); setTimeout(()=>{}, 15000)";
|
|
210
216
|
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', script], process.cwd(), 'n-desc');
|
|
211
217
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
let
|
|
215
|
-
for (let i = 0; i < 40 && after !== null && after.descendants === before.descendants; i++) {
|
|
218
|
+
// Wait for a concrete baseline (not `unknown` from a transient ps miss).
|
|
219
|
+
let before = (0, process_1.activitySnapshot)(ref);
|
|
220
|
+
for (let i = 0; i < 50 && (before === null || before.descendants === 'unknown'); i++) {
|
|
216
221
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
217
|
-
|
|
222
|
+
before = (0, process_1.activitySnapshot)(ref);
|
|
223
|
+
}
|
|
224
|
+
expect(before).not.toBeNull();
|
|
225
|
+
expect(before.descendants).not.toBe('unknown');
|
|
226
|
+
const beforeGroup = before.groupSize;
|
|
227
|
+
let after = before;
|
|
228
|
+
const deadline = Date.now() + 8000;
|
|
229
|
+
while (Date.now() < deadline) {
|
|
230
|
+
const snap = (0, process_1.activitySnapshot)(ref);
|
|
231
|
+
if (snap !== null)
|
|
232
|
+
after = snap;
|
|
233
|
+
// Success: same process group, at least one PPID-descendant appeared.
|
|
234
|
+
if (after.groupSize === beforeGroup && /^[1-9]\d*:/.test(after.descendants))
|
|
235
|
+
break;
|
|
236
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
218
237
|
}
|
|
219
|
-
expect(after).not.toBeNull();
|
|
220
238
|
// The detached grandchild lives in its own process group: the group
|
|
221
239
|
// signal cannot see it, the descendant signal must.
|
|
222
|
-
expect(after.groupSize).toBe(
|
|
223
|
-
expect(after.descendants).
|
|
224
|
-
expect(after.descendants).toMatch(/^1:/);
|
|
240
|
+
expect(after.groupSize).toBe(beforeGroup);
|
|
241
|
+
expect(after.descendants).toMatch(/^[1-9]\d*:/);
|
|
225
242
|
}
|
|
226
243
|
finally {
|
|
244
|
+
// Detached grandchildren are outside `-PGID`; reap by parent pid too.
|
|
245
|
+
try {
|
|
246
|
+
const { execFileSync } = require('child_process');
|
|
247
|
+
execFileSync('pkill', ['-P', String(ref.pid)], { stdio: 'ignore', timeout: 1000 });
|
|
248
|
+
}
|
|
249
|
+
catch { /* ya ausente */ }
|
|
227
250
|
try {
|
|
228
251
|
process.kill(-ref.processGroup, 'SIGKILL');
|
|
229
252
|
}
|
|
@@ -75,6 +75,26 @@ describe('assertProviderSupported', () => {
|
|
|
75
75
|
});
|
|
76
76
|
expect(exec).not.toHaveBeenCalled();
|
|
77
77
|
});
|
|
78
|
+
it('accepts the Cursor date-based agent line (Plan C)', () => {
|
|
79
|
+
const exec = jest.fn(() => Buffer.from('2026.09.26-dd393fe\n'));
|
|
80
|
+
expect((0, provider_version_1.assertProviderSupported)('cursor', exec)).toEqual({
|
|
81
|
+
provider: 'cursor',
|
|
82
|
+
version: '2026.09.26',
|
|
83
|
+
});
|
|
84
|
+
expect(exec).toHaveBeenCalledWith('agent', ['--version'], expect.any(Object));
|
|
85
|
+
});
|
|
86
|
+
it.each([
|
|
87
|
+
[Buffer.from('2026.09.25-abc1234\n'), 'requires Cursor >= 2026.09.26'],
|
|
88
|
+
[Buffer.from('2026.09.26\n'), 'could not parse Cursor version'],
|
|
89
|
+
[Buffer.from('v2026.09.26-dd393fe\n'), 'could not parse Cursor version'],
|
|
90
|
+
])('rejects unsupported Cursor output without mutation', (output, message) => {
|
|
91
|
+
expect(() => (0, provider_version_1.assertProviderSupported)('cursor', () => output)).toThrow(message);
|
|
92
|
+
});
|
|
93
|
+
it('reports a missing Cursor agent binary distinctly', () => {
|
|
94
|
+
const missing = Object.assign(new Error('spawnSync agent ENOENT'), { code: 'ENOENT' });
|
|
95
|
+
expect(() => (0, provider_version_1.assertProviderSupported)('cursor', () => { throw missing; }))
|
|
96
|
+
.toThrow('Cursor is not installed or not available on PATH');
|
|
97
|
+
});
|
|
78
98
|
it('reports a missing Codex binary distinctly', () => {
|
|
79
99
|
const missing = Object.assign(new Error('spawnSync codex ENOENT'), { code: 'ENOENT' });
|
|
80
100
|
expect(() => (0, provider_version_1.assertProviderSupported)('codex', () => { throw missing; }))
|
|
@@ -210,6 +210,14 @@ describe('Providers Routing', () => {
|
|
|
210
210
|
});
|
|
211
211
|
expect((0, providers_1.providerFor)('copilot').hooks).toBeUndefined();
|
|
212
212
|
});
|
|
213
|
+
it('declares Cursor agent version gate (Plan C R26)', () => {
|
|
214
|
+
expect((0, providers_1.providerFor)('cursor').minimumVersion).toBe('2026.09.26');
|
|
215
|
+
expect((0, providers_1.providerFor)('cursor').versionCommand).toEqual({
|
|
216
|
+
command: 'agent',
|
|
217
|
+
args: ['--version'],
|
|
218
|
+
versionPattern: /^(\d{4}\.\d{2}\.\d{2})-[0-9a-f]+$/,
|
|
219
|
+
});
|
|
220
|
+
});
|
|
213
221
|
it('assigns Copilot instructions renderer; Cursor skills use link (cursor-mdc kept for migration)', () => {
|
|
214
222
|
expect((0, providers_1.providerFor)('cursor').skill.renderer).toBe('link');
|
|
215
223
|
expect((0, providers_1.providerFor)('copilot').skill.renderer).toBe('copilot-instructions');
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|