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.
@@ -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
- cursor: { ...UNKNOWN },
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
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
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
- try {
503
- const table = (0, child_process_1.execFileSync)('ps', ['-A', '-o', 'pid=,ppid=,time='], { encoding: 'utf8', stdio: exports.EXEC_STDIO, timeout: PS_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 });
504
- return descendantActivity(table, rootPid);
505
- }
506
- catch {
507
- return 'unknown';
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: 'cursor', executionMode: 'interactivo' }],
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: 'cursor', cwd: process.cwd(), enabledAgents: ['cursor'] });
143
- expect(report).toMatchObject({ state: 'blocked', executionMode: 'interactivo', provider: 'cursor', journal: 'not-required' });
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: 'cursor', cwd: process.cwd(), enabledAgents: ['cursor'], journalState: journal(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
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.cursor.unattendedController).toBe('unverified');
62
- expect((0, admission_1.unattendedCapabilities)('cursor', 'approval-free').unattendedController).toBe('unverified');
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(()=>{}, 4000)'], { detached: true, stdio: 'ignore' }).unref(); }, 400); setTimeout(()=>{}, 4000)";
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
- const before = (0, process_1.activitySnapshot)(ref);
213
- expect(before).not.toBeNull();
214
- let after = (0, process_1.activitySnapshot)(ref);
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
- after = (0, process_1.activitySnapshot)(ref);
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(before.groupSize);
223
- expect(after.descendants).not.toBe(before.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.15.1",
3
+ "version": "9.16.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file