agentic-workflow-manager 9.14.0 → 9.15.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/context-budget/budget.js +50 -4
- package/dist/src/commands/doctor.js +4 -1
- package/dist/src/commands/hooks/cursor.js +359 -0
- package/dist/src/commands/hooks/install.js +3 -0
- package/dist/src/commands/hooks/resync.js +10 -0
- package/dist/src/commands/hooks/status.js +3 -0
- package/dist/src/commands/hooks/uninstall.js +3 -0
- package/dist/src/commands/update.js +4 -0
- package/dist/src/core/admission/index.js +12 -6
- package/dist/src/core/admission/registry-contracts.js +14 -1
- package/dist/src/core/context/strategies/codex-agents.js +6 -2
- package/dist/src/core/cursor-budgets.js +8 -0
- package/dist/src/core/dashboard/sanitize.js +2 -2
- package/dist/src/core/diagnostics/provider-checks.js +11 -2
- package/dist/src/core/init/migrate-cursor-mdc.js +72 -0
- package/dist/src/core/init/steps.js +4 -0
- package/dist/src/providers/index.js +18 -4
- package/dist/tests/commands/add.test.js +5 -7
- package/dist/tests/commands/context-budget/budget.test.js +68 -0
- package/dist/tests/commands/doctor.test.js +2 -2
- package/dist/tests/commands/hooks/cursor.test.js +257 -0
- package/dist/tests/commands/hooks/resync.test.js +8 -0
- package/dist/tests/commands/init.test.js +23 -22
- package/dist/tests/commands/plan/index.test.js +82 -2
- package/dist/tests/commands/sensors/exec.test.js +18 -9
- package/dist/tests/core/admission/index.test.js +39 -0
- package/dist/tests/core/bundle-install.test.js +8 -14
- package/dist/tests/core/context/strategies/codex-agents.test.js +31 -12
- package/dist/tests/core/diagnostics/provider-tier.test.js +46 -96
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +6 -4
- package/dist/tests/core/init/migrate-cursor-mdc.test.js +74 -0
- package/dist/tests/core/install-planner.test.js +16 -12
- package/dist/tests/integration/cursor-subset-gate.test.js +123 -0
- package/dist/tests/providers/index.test.js +34 -21
- package/dist/tests/structural/provider-paths-honor-config-home.test.js +3 -2
- package/package.json +2 -2
- 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
|
@@ -65,6 +65,7 @@ describe('resyncInstalledHooks', () => {
|
|
|
65
65
|
expect(results).toEqual([
|
|
66
66
|
{ agent: 'claude-code', action: 'resynced' },
|
|
67
67
|
{ agent: 'codex', action: 'not-installed' },
|
|
68
|
+
{ agent: 'cursor', action: 'not-installed' },
|
|
68
69
|
]);
|
|
69
70
|
const settings = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.claude/settings.json'), 'utf8'));
|
|
70
71
|
expect(settings.hooks.SubagentStart[0].hooks[0].command).toBe('awm model-policy hook-event --event start');
|
|
@@ -85,6 +86,7 @@ describe('resyncInstalledHooks', () => {
|
|
|
85
86
|
expect(results).toEqual([
|
|
86
87
|
{ agent: 'claude-code', action: 'not-installed' },
|
|
87
88
|
{ agent: 'codex', action: 'not-installed' },
|
|
89
|
+
{ agent: 'cursor', action: 'not-installed' },
|
|
88
90
|
]);
|
|
89
91
|
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/hooks/session-start'))).toBe(false);
|
|
90
92
|
});
|
|
@@ -117,6 +119,7 @@ describe('resyncInstalledHooks', () => {
|
|
|
117
119
|
expect(results).toEqual([
|
|
118
120
|
{ agent: 'claude-code', action: 'resynced' },
|
|
119
121
|
{ agent: 'codex', action: 'not-installed' },
|
|
122
|
+
{ agent: 'cursor', action: 'not-installed' },
|
|
120
123
|
]);
|
|
121
124
|
expect(fs_1.default.lstatSync(skillDest).isSymbolicLink()).toBe(false);
|
|
122
125
|
const content = fs_1.default.readFileSync(skillDest, 'utf-8');
|
|
@@ -135,6 +138,7 @@ describe('resyncInstalledHooks', () => {
|
|
|
135
138
|
expect(results).toEqual([
|
|
136
139
|
{ agent: 'claude-code', action: 'resynced' },
|
|
137
140
|
{ agent: 'codex', action: 'not-installed' },
|
|
141
|
+
{ agent: 'cursor', action: 'not-installed' },
|
|
138
142
|
]);
|
|
139
143
|
expect(fs_1.default.lstatSync(path_1.default.join(scriptsDir, 'session-start')).isSymbolicLink()).toBe(true);
|
|
140
144
|
// Task 6: using-awm.md is materialized regardless of the scripts' install method —
|
|
@@ -151,6 +155,7 @@ describe('resyncInstalledHooks', () => {
|
|
|
151
155
|
expect(results).toEqual([
|
|
152
156
|
{ agent: 'claude-code', action: 'registry-missing' },
|
|
153
157
|
{ agent: 'codex', action: 'not-installed' },
|
|
158
|
+
{ agent: 'cursor', action: 'not-installed' },
|
|
154
159
|
]);
|
|
155
160
|
expect(fs_1.default.readFileSync(path_1.default.join(scriptsDir, 'session-start'), 'utf-8')).toContain('OLD');
|
|
156
161
|
});
|
|
@@ -166,6 +171,7 @@ describe('resyncInstalledHooks', () => {
|
|
|
166
171
|
expect(results).toEqual([
|
|
167
172
|
{ agent: 'claude-code', action: 'resynced' },
|
|
168
173
|
{ agent: 'codex', action: 'not-installed' },
|
|
174
|
+
{ agent: 'cursor', action: 'not-installed' },
|
|
169
175
|
]);
|
|
170
176
|
expect(fs_1.default.existsSync(path_1.default.join(scriptsDir, 'session-start'))).toBe(true);
|
|
171
177
|
// detectInstallMethod fell back to copy since lstatSync threw
|
|
@@ -189,6 +195,7 @@ describe('resyncInstalledHooks', () => {
|
|
|
189
195
|
expect(results).toEqual([
|
|
190
196
|
{ agent: 'claude-code', action: 'registry-missing' },
|
|
191
197
|
{ agent: 'codex', action: 'not-installed' },
|
|
198
|
+
{ agent: 'cursor', action: 'not-installed' },
|
|
192
199
|
]);
|
|
193
200
|
// old script left intact — never leave user without hook
|
|
194
201
|
expect(fs_1.default.readFileSync(path_1.default.join(scriptsDir, 'session-start'), 'utf-8')).toContain('OLD');
|
|
@@ -219,6 +226,7 @@ describe('resyncInstalledHooks', () => {
|
|
|
219
226
|
expect(results).toEqual([
|
|
220
227
|
{ agent: 'claude-code', action: 'not-installed' },
|
|
221
228
|
{ agent: 'codex', action: 'resynced' },
|
|
229
|
+
{ agent: 'cursor', action: 'not-installed' },
|
|
222
230
|
]);
|
|
223
231
|
expect(fs_1.default.readFileSync(path_1.default.join(codexScriptsDir, 'session-start'), 'utf-8')).toContain('NEW CODEX');
|
|
224
232
|
});
|
|
@@ -395,20 +395,22 @@ describe('runInit', () => {
|
|
|
395
395
|
expect(JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf8'))).toEqual({ pristine: true, unrelated: 'keep' });
|
|
396
396
|
});
|
|
397
397
|
// -----------------------------------------------------------------------
|
|
398
|
-
// Gap A (QA panel) — GLOBAL-scope
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
// --agent cursor` through
|
|
402
|
-
// (
|
|
403
|
-
// real network clone) and asserts a real rendered .mdc file lands in the
|
|
404
|
-
// GLOBAL ~/.cursor/rules directory with the expected frontmatter shape.
|
|
398
|
+
// Gap A (QA panel) — GLOBAL-scope Cursor skill delivery. Plan B installs
|
|
399
|
+
// Cursor skills as links under ~/.agents/skills (shared with Codex/
|
|
400
|
+
// OpenCode) plus native hooks.json entries (registry floor >= 4.9.0).
|
|
401
|
+
// Drives a real `awm init --agent cursor` through installBundle/
|
|
402
|
+
// applyInstallPlan (only syncCache stubbed) and asserts the link + hooks.
|
|
405
403
|
// -----------------------------------------------------------------------
|
|
406
|
-
it('Gap A — real global-scope
|
|
404
|
+
it('Gap A — real global-scope link: awm init --agent cursor links using-awm into ~/.agents/skills', async () => {
|
|
405
|
+
const { spawnSync } = require('child_process');
|
|
407
406
|
const contentRoot = path_1.default.join(process.env.AWM_HOME, 'registries', 'baseline');
|
|
408
|
-
// A 'hooks' dir
|
|
409
|
-
//
|
|
410
|
-
//
|
|
407
|
+
// A 'hooks' dir is what makes capabilityRoot('hooks') resolve to this content
|
|
408
|
+
// root — registryRoot for stepContextInjection / stepHook. Plan B also needs
|
|
409
|
+
// the three Cursor hook scripts and an exact v4.9.0+ tag for the registry floor.
|
|
411
410
|
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'hooks'), { recursive: true });
|
|
411
|
+
for (const name of ['cursor-session-start', 'cursor-pre-compact', 'cursor-post-tool-use']) {
|
|
412
|
+
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'hooks', name), '#!/usr/bin/env node\nconsole.log("{}")\n', { mode: 0o755 });
|
|
413
|
+
}
|
|
412
414
|
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'skills', 'using-awm'), { recursive: true });
|
|
413
415
|
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\ndescription: Use when starting any development conversation\n---\n\nMUST invoke skills per the tiered policy.\n');
|
|
414
416
|
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'bundles', 'dev-core'), { recursive: true });
|
|
@@ -420,6 +422,10 @@ describe('runInit', () => {
|
|
|
420
422
|
name: 'dev-core', version: '1.0.0', description: 'Baseline', scope: 'baseline',
|
|
421
423
|
dependsOn: [], skills: ['using-awm'], workflows: [], agents: [],
|
|
422
424
|
}));
|
|
425
|
+
spawnSync('git', ['init', '-q', '-b', 'main'], { cwd: contentRoot, stdio: 'ignore' });
|
|
426
|
+
spawnSync('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'add', '.'], { cwd: contentRoot, stdio: 'ignore' });
|
|
427
|
+
spawnSync('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'fixture'], { cwd: contentRoot, stdio: 'ignore' });
|
|
428
|
+
spawnSync('git', ['tag', 'v4.9.0'], { cwd: contentRoot, stdio: 'ignore' });
|
|
423
429
|
const projectCwd = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-cursor-cwd-'));
|
|
424
430
|
try {
|
|
425
431
|
const { runInit } = require('../../src/commands/init');
|
|
@@ -431,20 +437,15 @@ describe('runInit', () => {
|
|
|
431
437
|
// seedBaselineRegistry() writes registries.json pointing at 'baseline'
|
|
432
438
|
// (our manually-seeded content root above) since none exists yet — no
|
|
433
439
|
// real network clone happens because that content root already exists
|
|
434
|
-
// on disk. Only syncCache is stubbed
|
|
435
|
-
// content root above has no `.git`, so it reads as "not yet cloned")
|
|
436
|
-
// doesn't try to shell out to git either.
|
|
440
|
+
// on disk. Only syncCache is stubbed.
|
|
437
441
|
actions: { syncCache: async () => { } },
|
|
438
442
|
});
|
|
439
443
|
expect(code).toBeLessThanOrEqual(1);
|
|
440
|
-
const
|
|
441
|
-
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.cursor', 'rules', 'using-awm'))).toBe(false);
|
|
442
|
-
expect(fs_1.default.existsSync(
|
|
443
|
-
|
|
444
|
-
expect(
|
|
445
|
-
expect(rendered).toContain('globs:');
|
|
446
|
-
expect(rendered).toContain('alwaysApply: false');
|
|
447
|
-
expect(rendered).toContain('MUST invoke skills per the tiered policy.');
|
|
444
|
+
const linked = path_1.default.join(tmpHome, '.agents', 'skills', 'using-awm');
|
|
445
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.cursor', 'rules', 'using-awm.mdc'))).toBe(false);
|
|
446
|
+
expect(fs_1.default.lstatSync(linked).isSymbolicLink() || fs_1.default.existsSync(path_1.default.join(linked, 'SKILL.md'))).toBe(true);
|
|
447
|
+
expect(fs_1.default.readFileSync(path_1.default.join(linked, 'SKILL.md'), 'utf8')).toContain('MUST invoke skills per the tiered policy.');
|
|
448
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.cursor', 'hooks.json'))).toBe(true);
|
|
448
449
|
}
|
|
449
450
|
finally {
|
|
450
451
|
fs_1.default.rmSync(projectCwd, { recursive: true, force: true });
|
|
@@ -309,6 +309,32 @@ describe('plan admit Commander wiring', () => {
|
|
|
309
309
|
process.exitCode = undefined;
|
|
310
310
|
}
|
|
311
311
|
});
|
|
312
|
+
it.each([
|
|
313
|
+
['a content registry', true, 'admitted'],
|
|
314
|
+
['an ordinary project', false, 'blocked'],
|
|
315
|
+
])('applies the R8 sensor opt-out exception only to %s', async (_name, isRegistry, expected) => {
|
|
316
|
+
const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-r8-')));
|
|
317
|
+
const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
318
|
+
if (isRegistry)
|
|
319
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
|
|
320
|
+
const sensors = jest.fn().mockResolvedValue({ overall: 'not_certified', sensors: [], mode: 'opt-out', reason: 'opt-out-declared', projectRoot: root, manifestPath: path_1.default.join(root, '.awm', 'sensors.json') });
|
|
321
|
+
const program = new commander_1.Command();
|
|
322
|
+
(0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, listRegistries: () => [], readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }), runSensors: sensors });
|
|
323
|
+
try {
|
|
324
|
+
await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'interactivo', '--verify-sensors', '--json']);
|
|
325
|
+
const report = JSON.parse(String(output.mock.calls.at(-1)[0]));
|
|
326
|
+
expect(report).toMatchObject({ state: expected, sensors: 'not-certified' });
|
|
327
|
+
if (isRegistry)
|
|
328
|
+
expect(report.sensorEvidence).toBe('r8-registry-opt-out');
|
|
329
|
+
else
|
|
330
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_SENSORS_BLOCKED');
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
output.mockRestore();
|
|
334
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
335
|
+
process.exitCode = undefined;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
312
338
|
it('derives desatendido from the canonical plan header; a native v1 plan needs no journal (opt-in durable custody)', async () => {
|
|
313
339
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-header-'));
|
|
314
340
|
const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
@@ -462,7 +488,11 @@ describe('plan admit Commander wiring', () => {
|
|
|
462
488
|
process.exitCode = undefined;
|
|
463
489
|
}
|
|
464
490
|
});
|
|
465
|
-
|
|
491
|
+
// Cursor skills are link-installed under ~/.agents/skills (same as Codex); only
|
|
492
|
+
// content renderers still need this "rendered copy ≠ ownership" proof. Link
|
|
493
|
+
// unowned cases are covered by the runtime-unowned scenarios above.
|
|
494
|
+
it('does not prove absence for actual unowned rendered copilot local runtime contracts', async () => {
|
|
495
|
+
const target = 'copilot';
|
|
466
496
|
const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-rendered-admission-')));
|
|
467
497
|
const oldHome = process.env.HOME;
|
|
468
498
|
process.env.HOME = path_1.default.join(root, 'home');
|
|
@@ -474,7 +504,7 @@ describe('plan admit Commander wiring', () => {
|
|
|
474
504
|
fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
|
|
475
505
|
fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
|
|
476
506
|
const provider = (0, providers_1.providerFor)(target);
|
|
477
|
-
const directory =
|
|
507
|
+
const directory = path_1.default.resolve(root, provider.skill.local);
|
|
478
508
|
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
479
509
|
const installedFile = path_1.default.join(directory, (0, registry_1.renderedFilename)('using-awm', provider.skill.renderer));
|
|
480
510
|
fs_1.default.writeFileSync(installedFile, (0, registry_1.renderArtifact)(provider.skill.renderer, skillSource));
|
|
@@ -514,6 +544,56 @@ describe('plan admit Commander wiring', () => {
|
|
|
514
544
|
process.exitCode = undefined;
|
|
515
545
|
}
|
|
516
546
|
});
|
|
547
|
+
it.each([['cursor', 'global'], ['cursor', 'local']])('does not prove absence for actual unowned linked %s %s runtime contracts', async (target, scope) => {
|
|
548
|
+
const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-linked-admission-')));
|
|
549
|
+
const oldHome = process.env.HOME;
|
|
550
|
+
process.env.HOME = path_1.default.join(root, 'home');
|
|
551
|
+
const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
552
|
+
const registryRoot = path_1.default.join(root, 'registry');
|
|
553
|
+
fs_1.default.mkdirSync(path_1.default.join(registryRoot, 'skills'), { recursive: true });
|
|
554
|
+
fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
|
|
555
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
|
|
556
|
+
const unowned = path_1.default.join(root, 'unowned', 'using-awm');
|
|
557
|
+
fs_1.default.mkdirSync(unowned, { recursive: true });
|
|
558
|
+
fs_1.default.writeFileSync(path_1.default.join(unowned, 'SKILL.md'), '---\nname: using-awm\ndescription: contract\n---\nRuntime contract.\n');
|
|
559
|
+
const provider = (0, providers_1.providerFor)(target);
|
|
560
|
+
const directory = scope === 'global' ? provider.skill.global : path_1.default.resolve(root, provider.skill.local);
|
|
561
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
562
|
+
const artifact = path_1.default.join(directory, (0, registry_1.renderedFilename)('using-awm', provider.skill.renderer));
|
|
563
|
+
fs_1.default.symlinkSync(unowned, artifact, process.platform === 'win32' ? 'junction' : 'dir');
|
|
564
|
+
const sensors = jest.fn();
|
|
565
|
+
const program = new commander_1.Command();
|
|
566
|
+
(0, plan_1.registerPlanCommand)(program, {
|
|
567
|
+
validatePlanFile: () => ({ ...valid, manifest: { ...valid.manifest, sources: [{ id: 'source', path: 'source.md', locator: 'contract', fact: 'contract' }] } }),
|
|
568
|
+
listRegistries: () => [{ name: 'fixture', remote: 'https://example.invalid/fixture.git', contentRoot: registryRoot }],
|
|
569
|
+
readPreferences: () => ({ defaultAgent: target, enabledAgents: [target], installMethod: 'symlink', defaultScope: 'local' }),
|
|
570
|
+
checkCurrentness: async () => ({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [{ component: 'cli', installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'fixture', checkedAt: 'x', status: 'current', detail: 'ok', remedy: 'none' }] }),
|
|
571
|
+
runSensors: sensors,
|
|
572
|
+
});
|
|
573
|
+
try {
|
|
574
|
+
await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', target, '--cwd', root, '--execution-mode', 'interactivo', '--require-current', '--verify-sensors', '--json']);
|
|
575
|
+
const output = JSON.parse(String(outputSpy.mock.calls.at(-1)[0]));
|
|
576
|
+
expect(output.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED');
|
|
577
|
+
expect(output.state).toBe('blocked');
|
|
578
|
+
const reported = /^Runtime artifact (.+) has no unique physical registry owner/.exec(output.diagnostics[0].message);
|
|
579
|
+
expect(reported).not.toBeNull();
|
|
580
|
+
const reportedIdentity = fs_1.default.lstatSync(reported[1], { bigint: true });
|
|
581
|
+
const installedIdentity = fs_1.default.lstatSync(artifact, { bigint: true });
|
|
582
|
+
expect(reportedIdentity.isSymbolicLink() || reportedIdentity.isDirectory()).toBe(true);
|
|
583
|
+
expect(reportedIdentity.ino).not.toBe(0n);
|
|
584
|
+
expect([reportedIdentity.dev, reportedIdentity.ino]).toEqual([installedIdentity.dev, installedIdentity.ino]);
|
|
585
|
+
expect(sensors).not.toHaveBeenCalled();
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
if (oldHome === undefined)
|
|
589
|
+
delete process.env.HOME;
|
|
590
|
+
else
|
|
591
|
+
process.env.HOME = oldHome;
|
|
592
|
+
outputSpy.mockRestore();
|
|
593
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
594
|
+
process.exitCode = undefined;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
517
597
|
it.each(['symlink', 'directory', 'oversized', 'duplicate-floor', 'unsafe-floor', 'stale-before-compatibility'])('keeps registry manifest %s noncertifying before sensors', async (scenario) => {
|
|
518
598
|
const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-manifest-admission-')));
|
|
519
599
|
const registryRoot = path_1.default.join(root, 'registry');
|
|
@@ -161,21 +161,30 @@ describe('runCommand — exit codes and output', () => {
|
|
|
161
161
|
});
|
|
162
162
|
itLinux('never lets a structured capture file grow beyond maxBuffer', async () => {
|
|
163
163
|
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-cap-'));
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// The overflow watcher kills the child and deletes the capture as soon as
|
|
165
|
+
// the cap is reached, so polling for the file races that cleanup. Capture
|
|
166
|
+
// files only grow, so their size at deletion is the largest they ever were.
|
|
167
|
+
const captureSizes = [];
|
|
168
|
+
const rmSync = fs_1.default.rmSync.bind(fs_1.default);
|
|
169
|
+
const removal = jest.spyOn(fs_1.default, 'rmSync').mockImplementation((target, options) => {
|
|
170
|
+
if (typeof target === 'string' && path_1.default.basename(target).startsWith('awm-sensor-output-')) {
|
|
171
|
+
captureSizes.push(fs_1.default.statSync(path_1.default.join(target, 'stdout')).size);
|
|
172
|
+
}
|
|
173
|
+
return rmSync(target, options);
|
|
174
|
+
});
|
|
166
175
|
try {
|
|
167
|
-
const result = (0, exec_1.runStructuredCommand)({
|
|
176
|
+
const result = await (0, exec_1.runStructuredCommand)({
|
|
168
177
|
executable: 'node',
|
|
169
178
|
resolution: 'path',
|
|
170
|
-
args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {}
|
|
179
|
+
args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {} setTimeout(() => {}, 1000);`],
|
|
171
180
|
}, { timeout: 5_000, cwd: dir, maxBuffer: 1_024 });
|
|
172
|
-
expect(
|
|
173
|
-
|
|
174
|
-
expect(
|
|
175
|
-
expect(
|
|
176
|
-
await expect(result).resolves.toMatchObject({ overflowed: true });
|
|
181
|
+
expect(result).toMatchObject({ overflowed: true });
|
|
182
|
+
expect(captureSizes).toHaveLength(1);
|
|
183
|
+
expect(captureSizes[0]).toBeGreaterThan(0);
|
|
184
|
+
expect(captureSizes[0]).toBeLessThanOrEqual(1_024);
|
|
177
185
|
}
|
|
178
186
|
finally {
|
|
187
|
+
removal.mockRestore();
|
|
179
188
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
180
189
|
}
|
|
181
190
|
});
|
|
@@ -178,6 +178,45 @@ describe('admitPlan', () => {
|
|
|
178
178
|
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
|
+
// R8: a content registry whose sensors are ALL deliberately disabled may close
|
|
182
|
+
// work; its release proof lives in CI. The local verdict stays not-certified —
|
|
183
|
+
// never reported as pass — and the exception is named in the evidence.
|
|
184
|
+
describe('the R8 registry opt-out closure exception', () => {
|
|
185
|
+
const optOut = { overall: 'not_certified', sensors: [], mode: 'opt-out', reason: 'opt-out-declared', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json' };
|
|
186
|
+
const unattended = () => ({ ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } });
|
|
187
|
+
const base = () => ({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], verifySensors: true, sensors: optOut, sensorClosurePolicy: 'r8-registry-opt-out' });
|
|
188
|
+
it('admits interactive work without reporting the opt-out as a pass', async () => {
|
|
189
|
+
const report = await (0, admission_1.admitPlan)(base());
|
|
190
|
+
expect(report).toMatchObject({ state: 'admitted', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
|
|
191
|
+
});
|
|
192
|
+
it('admits journal-less unattended work under the same exception', async () => {
|
|
193
|
+
const report = await (0, admission_1.admitPlan)({ ...base(), plan: unattended(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
|
|
194
|
+
expect(report).toMatchObject({ state: 'admitted', executionMode: 'desatendido', journal: 'not-required', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
|
|
195
|
+
});
|
|
196
|
+
it('reports the preserved verdict when a later gate blocks', async () => {
|
|
197
|
+
const report = await (0, admission_1.admitPlan)({ ...base(), plan: unattended(), planPath: 'docs/plan.md' });
|
|
198
|
+
expect(report).toMatchObject({ state: 'blocked', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
|
|
199
|
+
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
|
|
200
|
+
});
|
|
201
|
+
it.each([
|
|
202
|
+
['no declared policy', { sensorClosurePolicy: undefined }],
|
|
203
|
+
['a failing sensor', { sensors: { ...optOut, overall: 'fail', mode: 'project-sensors' } }],
|
|
204
|
+
['an absent manifest', { sensors: { ...optOut, mode: 'missing', reason: 'manifest-absent' } }],
|
|
205
|
+
['a native gate', { sensors: { ...optOut, mode: 'native-gate', reason: 'native-gate-declared' } }],
|
|
206
|
+
['an unavailable sensor source', { sensors: { ...optOut, mode: 'source-unavailable' } }],
|
|
207
|
+
['all-disabled legacy sensors', { sensors: { ...optOut, overall: 'skipped', mode: 'project-sensors' } }],
|
|
208
|
+
])('keeps blocking with %s', async (_name, fields) => {
|
|
209
|
+
const report = await (0, admission_1.admitPlan)({ ...base(), ...fields });
|
|
210
|
+
expect(report.state).toBe('blocked');
|
|
211
|
+
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_SENSORS_BLOCKED' });
|
|
212
|
+
expect(report.sensorEvidence).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
it('accepts only the known sensor evidence values at the public sanitizer', () => {
|
|
215
|
+
const report = { state: 'admitted', planState: 'valid', journal: 'not-required', currentness: 'not-checked', sensors: 'not-certified', diagnostics: [] };
|
|
216
|
+
expect((0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'r8-registry-opt-out' }).sensorEvidence).toBe('r8-registry-opt-out');
|
|
217
|
+
expect(() => (0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'opt-out' })).toThrow('invalid report');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
181
220
|
it('admits unattended work only with an exact schema-2 plan binding', async () => {
|
|
182
221
|
const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
|
|
183
222
|
const journal = { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } };
|
|
@@ -284,12 +284,8 @@ describe('installBundle', () => {
|
|
|
284
284
|
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.awm/profile.json'))).toBe(true);
|
|
285
285
|
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.gitignore'))).toBe(true);
|
|
286
286
|
});
|
|
287
|
-
it('materializes a skill as a
|
|
287
|
+
it('materializes a Cursor skill as a link under .agents/skills via the real default applyInstallPlan (Plan B e2e)', () => {
|
|
288
288
|
const { content, projectRoot, bundles } = makeFixture();
|
|
289
|
-
// s-base's fixture SKILL.md (makeFixture, above) has no `description`
|
|
290
|
-
// field — real for a bare skill fixture, but cursor-mdc.ts's renderer
|
|
291
|
-
// requires one (parseSkillSource). Seed a real description here so
|
|
292
|
-
// this is a genuine end-to-end render, not just a source-existence check.
|
|
293
289
|
fs_1.default.writeFileSync(path_1.default.join(content, 'skills', 's-base', 'SKILL.md'), '---\nname: s-base\ndescription: Base skill for bundle-install fixtures\n---\n\nDo the base thing.\n');
|
|
294
290
|
const base = bundles.find((bundle) => bundle.name === 'base');
|
|
295
291
|
const result = (0, bundle_install_1.installBundle)({
|
|
@@ -300,15 +296,12 @@ describe('installBundle', () => {
|
|
|
300
296
|
projectRoot,
|
|
301
297
|
contentDir: content,
|
|
302
298
|
});
|
|
303
|
-
const
|
|
304
|
-
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base'))).toBe(false);
|
|
305
|
-
expect(fs_1.default.existsSync(
|
|
306
|
-
|
|
307
|
-
expect(rendered).toContain('description: Base skill for bundle-install fixtures');
|
|
308
|
-
expect(rendered).toContain('alwaysApply: false');
|
|
309
|
-
expect(rendered).toContain('Do the base thing.');
|
|
299
|
+
const linked = path_1.default.join(projectRoot, '.agents/skills/s-base');
|
|
300
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base.mdc'))).toBe(false);
|
|
301
|
+
expect(fs_1.default.lstatSync(linked).isSymbolicLink() || fs_1.default.existsSync(path_1.default.join(linked, 'SKILL.md'))).toBe(true);
|
|
302
|
+
expect(fs_1.default.readFileSync(path_1.default.join(linked, 'SKILL.md'), 'utf8')).toContain('Do the base thing.');
|
|
310
303
|
expect(result.transactionId).toBeTruthy();
|
|
311
|
-
expect(result.modifiedFiles).toContain(
|
|
304
|
+
expect(result.modifiedFiles).toContain(linked);
|
|
312
305
|
});
|
|
313
306
|
it('materializes a skill as rendered Copilot .instructions.md via the real default applyInstallPlan (Task 4.3 e2e)', () => {
|
|
314
307
|
const { content, projectRoot, bundles } = makeFixture();
|
|
@@ -338,7 +331,8 @@ describe('installBundle', () => {
|
|
|
338
331
|
bundleName: 'base', bundles, agents: ['cursor'],
|
|
339
332
|
method: 'symlink', projectRoot, contentDir: content,
|
|
340
333
|
});
|
|
341
|
-
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.
|
|
334
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.agents/skills/s-base'))).toBe(true);
|
|
335
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base.mdc'))).toBe(false);
|
|
342
336
|
expect(cursorResult.recordedExtension).toBe('base');
|
|
343
337
|
const copilotResult = (0, bundle_install_1.addBundle)({
|
|
344
338
|
bundleName: 'base', bundles, agents: ['copilot'],
|
|
@@ -162,14 +162,8 @@ describe('CodexAgentsStrategy', () => {
|
|
|
162
162
|
expect(fs_1.default.existsSync(path_1.default.join(project, 'AGENTS.md'))).toBe(false);
|
|
163
163
|
expect(fs_1.default.existsSync(path_1.default.join(project, '.cursor'))).toBe(false);
|
|
164
164
|
});
|
|
165
|
-
it('
|
|
166
|
-
|
|
167
|
-
// the SAME single-slot managed block in <projectRoot>/AGENTS.md for a local-scope
|
|
168
|
-
// provider (Cursor/Copilot) — whichever ran second silently discarded the other's
|
|
169
|
-
// content. stepContextInjection runs before stepConstitutionInjection in the real
|
|
170
|
-
// init orchestrator (init/orchestrator.ts), so constitution always won, and the real
|
|
171
|
-
// AWM skill/context guidance never survived a real `awm init --agent cursor` run.
|
|
172
|
-
const project = path_1.default.join(tmpWork, 'collision-repo');
|
|
165
|
+
it('Cursor inject writes PROJECT_GUIDANCE only (using-awm lives in the hook — Plan B R4/R10)', () => {
|
|
166
|
+
const project = path_1.default.join(tmpWork, 'cursor-guidance-only');
|
|
173
167
|
fs_1.default.mkdirSync(project, { recursive: true });
|
|
174
168
|
const materialized = path_1.default.join(project, '.awm/context/awm-context.md');
|
|
175
169
|
const contextMarkdown = '# AWM\n\nUse `development-process`. MUST invoke skills per policy.';
|
|
@@ -186,15 +180,40 @@ describe('CodexAgentsStrategy', () => {
|
|
|
186
180
|
projectRoot: project,
|
|
187
181
|
};
|
|
188
182
|
expect(strategy.inject(input, provider)).toBe('injected');
|
|
189
|
-
expect(strategy.injectProject(project, provider, 'cursor')).toBe('injected'); // carrier
|
|
183
|
+
expect(strategy.injectProject(project, provider, 'cursor')).toBe('injected'); // awm.mdc carrier
|
|
190
184
|
const written = fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8');
|
|
191
|
-
expect(written).toContain('MUST invoke skills per policy');
|
|
192
185
|
expect(written).toContain('Read and obey `CONSTITUTION.md` before work');
|
|
186
|
+
expect(written).not.toContain('MUST invoke skills');
|
|
187
|
+
expect(written).not.toContain('# AWM');
|
|
193
188
|
expect(strategy.status(input, provider)).toBe('injected');
|
|
194
|
-
|
|
189
|
+
const mdc = fs_1.default.readFileSync(path_1.default.join(project, '.cursor/rules/awm.mdc'), 'utf8');
|
|
190
|
+
expect(mdc).toContain('alwaysApply: true');
|
|
191
|
+
expect(mdc).toContain('Read and obey `CONSTITUTION.md` before work');
|
|
195
192
|
expect(strategy.inject(input, provider)).toBe('unchanged');
|
|
196
193
|
expect(strategy.injectProject(project, provider, 'cursor')).toBe('unchanged');
|
|
197
|
-
|
|
194
|
+
});
|
|
195
|
+
it('Copilot inject still writes full using-awm plus PROJECT_GUIDANCE (sole carrier)', () => {
|
|
196
|
+
const project = path_1.default.join(tmpWork, 'copilot-full-context');
|
|
197
|
+
fs_1.default.mkdirSync(project, { recursive: true });
|
|
198
|
+
const materialized = path_1.default.join(project, '.awm/context/awm-context.md');
|
|
199
|
+
const contextMarkdown = '# AWM\n\nUse `development-process`. MUST invoke skills per policy.';
|
|
200
|
+
fs_1.default.mkdirSync(path_1.default.dirname(materialized), { recursive: true });
|
|
201
|
+
fs_1.default.writeFileSync(materialized, contextMarkdown);
|
|
202
|
+
const provider = copilotProvider();
|
|
203
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
204
|
+
const input = {
|
|
205
|
+
ref: { absPath: materialized, scope: 'local', contentHash: (0, provider_1.sha256)(contextMarkdown) },
|
|
206
|
+
registryRoot: '/registry',
|
|
207
|
+
installMethod: 'copy',
|
|
208
|
+
agent: 'copilot',
|
|
209
|
+
scope: 'local',
|
|
210
|
+
projectRoot: project,
|
|
211
|
+
};
|
|
212
|
+
expect(strategy.inject(input, provider)).toBe('injected');
|
|
213
|
+
const written = fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8');
|
|
214
|
+
expect(written).toContain('MUST invoke skills per policy');
|
|
215
|
+
expect(written).toContain('Read and obey `CONSTITUTION.md` before work');
|
|
216
|
+
expect(strategy.status(input, provider)).toBe('injected');
|
|
198
217
|
});
|
|
199
218
|
it('implements global status and remove while preserving user bytes', () => {
|
|
200
219
|
const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|