agentic-workflow-manager 9.15.0 → 9.16.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/watch/index.js +10 -0
- package/dist/src/core/admission/index.js +12 -4
- package/dist/src/core/journal/durable-custody.js +19 -0
- 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 +45 -4
- package/dist/tests/core/journal/controller-autonomy.test.js +9 -2
- package/dist/tests/core/journal/durable-custody.test.js +44 -0
- 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
|
@@ -13,6 +13,7 @@ const capabilities_1 = require("../../core/model-policy/capabilities");
|
|
|
13
13
|
const adapter_1 = require("../../core/journal/adapter");
|
|
14
14
|
const process_1 = require("../../core/journal/process");
|
|
15
15
|
const adapter_2 = require("../../core/journal/adapter");
|
|
16
|
+
const durable_custody_1 = require("../../core/journal/durable-custody");
|
|
16
17
|
const context_1 = require("../../core/tracks/context");
|
|
17
18
|
const concurrency_1 = require("../../core/tracks/concurrency");
|
|
18
19
|
const validate_1 = require("../../core/plan/validate");
|
|
@@ -20,6 +21,12 @@ const path_1 = __importDefault(require("path"));
|
|
|
20
21
|
const archive_unused_1 = require("./archive-unused");
|
|
21
22
|
const store_1 = require("../../core/journal/store");
|
|
22
23
|
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
24
|
+
function refuseIfDurableCustodySuspended() {
|
|
25
|
+
if (!(0, durable_custody_1.isDurableCustodySuspended)())
|
|
26
|
+
return;
|
|
27
|
+
process.stderr.write(`${durable_custody_1.DURABLE_CUSTODY_SUSPENDED_MESSAGE}\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
23
30
|
function currentBranch(cwd) {
|
|
24
31
|
// stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
|
|
25
32
|
// default de execFileSync del stderr de git hacia el stderr del llamante,
|
|
@@ -61,6 +68,9 @@ function registerWatchCommand(program) {
|
|
|
61
68
|
// #168: sin esto el controller desatendido arranca y se cuelga en la
|
|
62
69
|
// primera herramienta pidiendo aprobación. Se declara, no se adivina.
|
|
63
70
|
.option('--controller-autonomy <postura>', `postura del controller desatendido (${adapter_1.CONTROLLER_AUTONOMIES.join(' | ')})`)
|
|
71
|
+
// Fail-closed for every watch verb (parent + subcommands) while durable
|
|
72
|
+
// custody is suspended. Help still works; only actions are refused.
|
|
73
|
+
.hook('preAction', () => { refuseIfDurableCustodySuspended(); })
|
|
64
74
|
.action(async (opts) => {
|
|
65
75
|
const repo = process.cwd();
|
|
66
76
|
const branch = currentBranch(repo);
|
|
@@ -6,6 +6,7 @@ exports.sanitizeAdmissionReport = sanitizeAdmissionReport;
|
|
|
6
6
|
exports.admitPlan = admitPlan;
|
|
7
7
|
const providers_1 = require("../../providers");
|
|
8
8
|
const paths_1 = require("../journal/paths");
|
|
9
|
+
const durable_custody_1 = require("../journal/durable-custody");
|
|
9
10
|
const resolve_1 = require("../model-policy/resolve");
|
|
10
11
|
const capabilities_1 = require("../model-policy/capabilities");
|
|
11
12
|
const UNKNOWN = {
|
|
@@ -24,7 +25,10 @@ exports.PROVIDER_EXECUTION_CAPABILITIES = {
|
|
|
24
25
|
opencode: { ...UNKNOWN },
|
|
25
26
|
'claude-code': { ...UNKNOWN, interactiveExecution: 'supported', unattendedController: 'supported', durableResume: 'supported' },
|
|
26
27
|
codex: { ...UNKNOWN, interactiveExecution: 'supported', unattendedController: 'supported', durableResume: 'supported' },
|
|
27
|
-
|
|
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' },
|
|
28
32
|
copilot: { ...UNKNOWN },
|
|
29
33
|
};
|
|
30
34
|
/** #168: the controller can only act if it was launched with an autonomy
|
|
@@ -242,10 +246,14 @@ async function admitPlan(input) {
|
|
|
242
246
|
// The durable controller (journal + awm watch + jobs) is opt-in, as it
|
|
243
247
|
// was before 9.8.0: native v1 dispatch with no journal on its branch
|
|
244
248
|
// runs as one provider session. Creating a journal is the opt-in, and
|
|
245
|
-
// from then on it must be current.
|
|
246
|
-
//
|
|
249
|
+
// from then on it must be current. While durable custody is suspended
|
|
250
|
+
// (see durable-custody.ts), leftover journals must not trap native v1
|
|
251
|
+
// unattended work. v2 routing custody lives in the journal, so
|
|
252
|
+
// awm-routed work still requires it when that path is active.
|
|
247
253
|
const observed = journalStatus(input, plan);
|
|
248
|
-
const journal =
|
|
254
|
+
const journal = plan.schema === 'compact-slices/v1' && (observed === 'missing' || (0, durable_custody_1.isDurableCustodySuspended)())
|
|
255
|
+
? 'not-required'
|
|
256
|
+
: observed;
|
|
249
257
|
if (journal !== 'current' && journal !== 'not-required')
|
|
250
258
|
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 });
|
|
251
259
|
const capabilities = unattendedCapabilities(provider, input.controllerAutonomy);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Durable custody = `awm watch` + journal-bound supervisor/controller relaunch.
|
|
4
|
+
*
|
|
5
|
+
* Suspended for every provider until routing/custody work resumes. The supported
|
|
6
|
+
* unattended path is the native provider session (compact-slices/v1, no journal).
|
|
7
|
+
*
|
|
8
|
+
* The Jest suite sets `AWM_ALLOW_DURABLE_CUSTODY=1` so the machinery stays tested
|
|
9
|
+
* for when this suspension is lifted. Operators never set that variable.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DURABLE_CUSTODY_SUSPENDED_MESSAGE = void 0;
|
|
13
|
+
exports.isDurableCustodySuspended = isDurableCustodySuspended;
|
|
14
|
+
exports.DURABLE_CUSTODY_SUSPENDED_MESSAGE = 'Durable custody (awm watch) is suspended for all providers until routing/custody work resumes. '
|
|
15
|
+
+ 'Use native provider-session unattended (compact-slices/v1, no journal).';
|
|
16
|
+
/** True for operators; false only when tests explicitly re-enable the machinery. */
|
|
17
|
+
function isDurableCustodySuspended() {
|
|
18
|
+
return process.env.AWM_ALLOW_DURABLE_CUSTODY !== '1';
|
|
19
|
+
}
|
|
@@ -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
|
|
@@ -177,6 +199,25 @@ describe('admitPlan', () => {
|
|
|
177
199
|
expect(report.state).toBe('blocked');
|
|
178
200
|
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
|
|
179
201
|
});
|
|
202
|
+
// While Camino B is suspended, a leftover journal on the branch must not
|
|
203
|
+
// trap native v1 unattended admission. Jest sets AWM_ALLOW_DURABLE_CUSTODY=1
|
|
204
|
+
// for the rest of the suite; this case restores the operator default.
|
|
205
|
+
it('ignores leftover journals for native v1 while durable custody is suspended', async () => {
|
|
206
|
+
const previous = process.env.AWM_ALLOW_DURABLE_CUSTODY;
|
|
207
|
+
delete process.env.AWM_ALLOW_DURABLE_CUSTODY;
|
|
208
|
+
try {
|
|
209
|
+
const corrupt = await (0, admission_1.admitPlan)({ ...base(), journalCorrupt: true });
|
|
210
|
+
expect(corrupt).toMatchObject({ state: 'admitted', executionMode: 'desatendido', journal: 'not-required' });
|
|
211
|
+
const stale = await (0, admission_1.admitPlan)({ ...base(), journalState: { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'b'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } } });
|
|
212
|
+
expect(stale).toMatchObject({ state: 'admitted', journal: 'not-required' });
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
if (previous === undefined)
|
|
216
|
+
delete process.env.AWM_ALLOW_DURABLE_CUSTODY;
|
|
217
|
+
else
|
|
218
|
+
process.env.AWM_ALLOW_DURABLE_CUSTODY = previous;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
180
221
|
});
|
|
181
222
|
// R8: a content registry whose sensors are ALL deliberately disabled may close
|
|
182
223
|
// work; its release proof lives in CI. The local verdict stays not-certified —
|
|
@@ -247,7 +288,7 @@ describe('admitPlan', () => {
|
|
|
247
288
|
expect(report.state).toBe('admitted');
|
|
248
289
|
});
|
|
249
290
|
it('keeps the original diagnostic for a provider that never had the capability', async () => {
|
|
250
|
-
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' });
|
|
251
292
|
expect(report.state).toBe('blocked');
|
|
252
293
|
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CAPABILITY_UNVERIFIED' });
|
|
253
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)', () => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const commander_1 = require("commander");
|
|
4
|
+
const durable_custody_1 = require("../../../src/core/journal/durable-custody");
|
|
5
|
+
const watch_1 = require("../../../src/commands/watch");
|
|
6
|
+
describe('durable custody suspension', () => {
|
|
7
|
+
const original = process.env.AWM_ALLOW_DURABLE_CUSTODY;
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (original === undefined)
|
|
10
|
+
delete process.env.AWM_ALLOW_DURABLE_CUSTODY;
|
|
11
|
+
else
|
|
12
|
+
process.env.AWM_ALLOW_DURABLE_CUSTODY = original;
|
|
13
|
+
});
|
|
14
|
+
it('is suspended for operators (env unset) and only re-enabled by the Jest harness', () => {
|
|
15
|
+
delete process.env.AWM_ALLOW_DURABLE_CUSTODY;
|
|
16
|
+
expect((0, durable_custody_1.isDurableCustodySuspended)()).toBe(true);
|
|
17
|
+
process.env.AWM_ALLOW_DURABLE_CUSTODY = '0';
|
|
18
|
+
expect((0, durable_custody_1.isDurableCustodySuspended)()).toBe(true);
|
|
19
|
+
process.env.AWM_ALLOW_DURABLE_CUSTODY = '1';
|
|
20
|
+
expect((0, durable_custody_1.isDurableCustodySuspended)()).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
it('refuses every awm watch entry point while suspended', async () => {
|
|
23
|
+
delete process.env.AWM_ALLOW_DURABLE_CUSTODY;
|
|
24
|
+
const program = new commander_1.Command();
|
|
25
|
+
(0, watch_1.registerWatchCommand)(program);
|
|
26
|
+
const err = [];
|
|
27
|
+
const exit = jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
28
|
+
throw Object.assign(new Error(`exit:${code ?? 0}`), { code });
|
|
29
|
+
}));
|
|
30
|
+
program.configureOutput({ writeErr: (s) => { err.push(String(s)); } });
|
|
31
|
+
// stderr write in the refusal helper bypasses configureOutput; capture it too.
|
|
32
|
+
const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(((chunk) => {
|
|
33
|
+
err.push(String(chunk));
|
|
34
|
+
return true;
|
|
35
|
+
}));
|
|
36
|
+
await expect(program.parseAsync(['node', 'awm', 'watch', 'journal-status'])).rejects.toThrow(/exit:1/);
|
|
37
|
+
expect(err.join('')).toContain(durable_custody_1.DURABLE_CUSTODY_SUSPENDED_MESSAGE);
|
|
38
|
+
err.length = 0;
|
|
39
|
+
await expect(program.parseAsync(['node', 'awm', 'watch', '--init', '--plan', 'docs/plan.md'])).rejects.toThrow(/exit:1/);
|
|
40
|
+
expect(err.join('')).toContain(durable_custody_1.DURABLE_CUSTODY_SUSPENDED_MESSAGE);
|
|
41
|
+
exit.mockRestore();
|
|
42
|
+
stderr.mockRestore();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -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
|