agentic-workflow-manager 9.14.1 → 9.15.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/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/commands/watch/index.js +10 -0
- package/dist/src/core/admission/index.js +8 -3
- 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/core/journal/durable-custody.js +19 -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 +56 -2
- package/dist/tests/core/admission/index.test.js +19 -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/core/journal/durable-custody.test.js +44 -0
- 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 +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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CURSOR_REANCHOR_PIN = exports.CURSOR_SESSION_START_PIN = exports.CURSOR_REANCHOR_MAX_BYTES = exports.CURSOR_SESSION_START_MAX_BYTES = void 0;
|
|
4
|
+
/** Cursor hook payload caps (Plan A / Plan B R5). Must match baseline registry scripts. */
|
|
5
|
+
exports.CURSOR_SESSION_START_MAX_BYTES = 24 * 1024;
|
|
6
|
+
exports.CURSOR_REANCHOR_MAX_BYTES = 4 * 1024;
|
|
7
|
+
exports.CURSOR_SESSION_START_PIN = 'cursor.sessionStartMaxBytes';
|
|
8
|
+
exports.CURSOR_REANCHOR_PIN = 'cursor.reanchorMaxBytes';
|
|
@@ -15,9 +15,9 @@ const CANONICAL_FINDING_IDS = new Set([
|
|
|
15
15
|
'machine.preferences.missing', 'machine.registries.stale', 'project.profile.missing',
|
|
16
16
|
'project.sensors.unavailable', 'project.preflight.degraded', 'planning.source.unavailable', 'execution.source.unavailable',
|
|
17
17
|
]);
|
|
18
|
-
const PROVIDER_FINDING_ID = /^machine\.provider\.(?:claude-code|codex|opencode|cursor|copilot|antigravity)\.(?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
18
|
+
const PROVIDER_FINDING_ID = /^machine\.provider\.(?:claude-code|codex|opencode|cursor|copilot|antigravity)\.(?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|context\.overlap|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
19
19
|
const PROJECT_FINDING_ID = /^project\.(?:profile\.present|extensions\.configured|registry-pins\.present|bundles\.coherent|context\.present|constitution\.present|sensors\.present|preflight\.not_collected)$/;
|
|
20
|
-
const PROVIDER_LABEL = /^Provider (?:claude-code|codex|opencode|cursor|copilot|antigravity): (?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
20
|
+
const PROVIDER_LABEL = /^Provider (?:claude-code|codex|opencode|cursor|copilot|antigravity): (?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|context\.overlap|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
21
21
|
// El slug ya lo validó PROCESS_NAME en core/process/model.ts. Repetir la forma
|
|
22
22
|
// acá es deliberado: este módulo es la frontera de render y no puede confiar en
|
|
23
23
|
// que su input pasó por aquel validador — un adapter nuevo podría no hacerlo.
|
|
@@ -375,11 +375,12 @@ function hookTrustCheck(agent) {
|
|
|
375
375
|
return { id: 'hook.trust', state: 'absent', remediationCode: 'awm-init' };
|
|
376
376
|
}
|
|
377
377
|
if (status.trust) {
|
|
378
|
-
// Codex: 'pending-trust' | 'healthy' | 'stale'
|
|
378
|
+
// Codex: 'pending-trust' | 'healthy' | 'stale'; Cursor: 'pending-first-run' | …
|
|
379
379
|
return {
|
|
380
380
|
id: 'hook.trust',
|
|
381
381
|
state: status.trust,
|
|
382
|
-
remediationCode: status.trust === 'pending-trust' ? 'open-hooks-trust'
|
|
382
|
+
remediationCode: status.trust === 'pending-trust' ? 'open-hooks-trust'
|
|
383
|
+
: status.trust === 'pending-first-run' ? 'open-cursor-session' : undefined,
|
|
383
384
|
};
|
|
384
385
|
}
|
|
385
386
|
return { id: 'hook.trust', state: status.overall === 'HEALTHY' ? 'healthy' : 'broken' };
|
|
@@ -470,6 +471,14 @@ function gatherProviderChecks(agents, scanSkills, projectRoot) {
|
|
|
470
471
|
hookTrustCheck(agent),
|
|
471
472
|
contextGlobalCheck(agent, projectRoot),
|
|
472
473
|
].filter((check) => check !== null);
|
|
474
|
+
// R19: informational only — never overall-red by itself (state: pending).
|
|
475
|
+
if (agent === 'cursor' && agents.includes('claude-code') && agents.includes('cursor')) {
|
|
476
|
+
checks.push({
|
|
477
|
+
id: 'context.overlap',
|
|
478
|
+
state: 'pending',
|
|
479
|
+
detail: 'Claude Code may import Cursor hooks as a third-party path; R15 dual-delivery suppression requires the Cursor session-start hook installed',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
473
482
|
return { id: agent, label: provider.label, tier: providerTier(provider), checks };
|
|
474
483
|
});
|
|
475
484
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// cli/src/core/init/migrate-cursor-mdc.ts
|
|
3
|
+
//
|
|
4
|
+
// Plan B S3 / R8: after Cursor skills move to link under ~/.agents/skills, remove
|
|
5
|
+
// only artifact-state records that were rendered with cursor-mdc under Cursor rules
|
|
6
|
+
// dirs. Never touch unrecorded files; never remove awm.mdc (project guidance carrier).
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.migrateCursorMdcSkills = migrateCursorMdcSkills;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const paths_1 = require("../paths");
|
|
15
|
+
const artifact_state_1 = require("../artifact-state");
|
|
16
|
+
function isUnderCursorRules(targetPath, projectRoot) {
|
|
17
|
+
const resolved = path_1.default.resolve(targetPath);
|
|
18
|
+
const globalRules = path_1.default.resolve((0, paths_1.homeDir)(), '.cursor', 'rules');
|
|
19
|
+
if (resolved === globalRules || resolved.startsWith(globalRules + path_1.default.sep))
|
|
20
|
+
return true;
|
|
21
|
+
if (!projectRoot)
|
|
22
|
+
return false;
|
|
23
|
+
const localRules = path_1.default.resolve(projectRoot, '.cursor', 'rules');
|
|
24
|
+
return resolved === localRules || resolved.startsWith(localRules + path_1.default.sep);
|
|
25
|
+
}
|
|
26
|
+
function isAwmMdcCarrier(record) {
|
|
27
|
+
return record.name === 'awm' || path_1.default.basename(record.targetPath) === 'awm.mdc';
|
|
28
|
+
}
|
|
29
|
+
function safeUnlinkManaged(targetPath) {
|
|
30
|
+
try {
|
|
31
|
+
const stat = fs_1.default.lstatSync(targetPath);
|
|
32
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
33
|
+
return false;
|
|
34
|
+
fs_1.default.unlinkSync(targetPath);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === 'ENOENT')
|
|
39
|
+
return false;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Drop historical cursor-mdc skill renders under Cursor rules dirs once link
|
|
45
|
+
* skills are the delivery path. Idempotent; safe on machines without cursor.
|
|
46
|
+
*/
|
|
47
|
+
function migrateCursorMdcSkills(opts) {
|
|
48
|
+
const stateFile = opts.stateFile ?? (0, artifact_state_1.artifactStateFile)();
|
|
49
|
+
const existing = (0, artifact_state_1.readArtifactState)(stateFile);
|
|
50
|
+
const removed = [];
|
|
51
|
+
const kept = [];
|
|
52
|
+
for (const record of existing) {
|
|
53
|
+
if (record.renderer !== 'cursor-mdc') {
|
|
54
|
+
kept.push(record);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isAwmMdcCarrier(record)) {
|
|
58
|
+
kept.push(record);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!isUnderCursorRules(record.targetPath, opts.projectRoot)) {
|
|
62
|
+
kept.push(record);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
safeUnlinkManaged(record.targetPath);
|
|
66
|
+
removed.push(record.targetPath);
|
|
67
|
+
}
|
|
68
|
+
if (removed.length > 0) {
|
|
69
|
+
(0, artifact_state_1.writeArtifactState)(kept, stateFile);
|
|
70
|
+
}
|
|
71
|
+
return { removed, kept: kept.length };
|
|
72
|
+
}
|
|
@@ -35,6 +35,7 @@ const skill_integrity_1 = require("../skill-integrity");
|
|
|
35
35
|
const registries_2 = require("../registries");
|
|
36
36
|
const project_constitution_inject_1 = require("../context/project-constitution-inject");
|
|
37
37
|
const codex_agents_1 = require("../context/strategies/codex-agents");
|
|
38
|
+
const migrate_cursor_mdc_1 = require("./migrate-cursor-mdc");
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// defaultActions — bridges the real functions to the InitActions interface
|
|
40
41
|
// ---------------------------------------------------------------------------
|
|
@@ -231,6 +232,9 @@ function stepDevCore(d) {
|
|
|
231
232
|
...(localOnly ? { scopeOverride: 'local' } : {}),
|
|
232
233
|
});
|
|
233
234
|
}
|
|
235
|
+
if (d.enabledAgents.includes('cursor') || d.agent === 'cursor') {
|
|
236
|
+
(0, migrate_cursor_mdc_1.migrateCursorMdcSkills)({ projectRoot: d.ctx.project?.root ?? d.cwd });
|
|
237
|
+
}
|
|
234
238
|
return ok('machine.devCore', 'machine', 'applied', localOnly ? 'installed at project scope (no global skill dir)' : undefined);
|
|
235
239
|
}
|
|
236
240
|
/** Step 3.5 – Repair broken global skill symlinks (orphans outside the baseline). */
|
|
@@ -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,13 +163,27 @@ function providers() {
|
|
|
163
163
|
cursor: {
|
|
164
164
|
label: 'Cursor',
|
|
165
165
|
configHome: configHomeFor('cursor'),
|
|
166
|
+
// Skills join the Codex/OpenCode shared physical dirs (~/.agents/skills +
|
|
167
|
+
// .agents/skills, renderer link). cursor-mdc remains registered for S3
|
|
168
|
+
// migration reads of historical ~/.cursor/rules installs.
|
|
166
169
|
skill: {
|
|
167
|
-
global: path_1.default.join(
|
|
168
|
-
local: '.
|
|
169
|
-
renderer: '
|
|
170
|
+
global: path_1.default.join(home, '.agents/skills'),
|
|
171
|
+
local: '.agents/skills',
|
|
172
|
+
renderer: 'link',
|
|
170
173
|
},
|
|
171
174
|
workflow: null,
|
|
172
|
-
agent:
|
|
175
|
+
agent: {
|
|
176
|
+
global: path_1.default.join(root('cursor'), 'agents'),
|
|
177
|
+
local: '.cursor/agents',
|
|
178
|
+
renderer: 'link',
|
|
179
|
+
},
|
|
180
|
+
hooks: {
|
|
181
|
+
type: 'cursor-hooks-json',
|
|
182
|
+
settingsPath: path_1.default.join(root('cursor'), 'hooks.json'),
|
|
183
|
+
scriptsDir: path_1.default.join(awm, 'hooks/cursor'),
|
|
184
|
+
matcher: '',
|
|
185
|
+
eventName: '',
|
|
186
|
+
},
|
|
173
187
|
injection: {
|
|
174
188
|
type: 'managed-agents-md',
|
|
175
189
|
// Cursor has no confirmed user-level/global AGENTS.md-equivalent file — its
|
|
@@ -65,18 +65,16 @@ function makeProjectRoot() {
|
|
|
65
65
|
fs_1.default.writeFileSync(path_1.default.join(projectRoot, 'package.json'), JSON.stringify({ name: 'fixture' }));
|
|
66
66
|
return projectRoot;
|
|
67
67
|
}
|
|
68
|
-
it('awm add demo materializes a
|
|
68
|
+
it('awm add demo materializes a Cursor skill link under .agents/skills end-to-end', () => {
|
|
69
69
|
const content = makeContentFixture();
|
|
70
70
|
const projectRoot = makeProjectRoot();
|
|
71
71
|
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
72
72
|
const outcome = (0, add_1.runAddBundleCore)({ name: 'demo', agent: 'cursor', cwd: projectRoot }, prefsFor('cursor'), bundles);
|
|
73
73
|
expect(outcome.code).toBe(0);
|
|
74
|
-
const
|
|
75
|
-
expect(fs_1.default.existsSync(
|
|
76
|
-
|
|
77
|
-
expect(
|
|
78
|
-
expect(rendered).toContain('alwaysApply: false');
|
|
79
|
-
expect(rendered).toContain('Follow the demo skill body.');
|
|
74
|
+
const linked = path_1.default.join(projectRoot, '.agents/skills/demo-skill');
|
|
75
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/demo-skill.mdc'))).toBe(false);
|
|
76
|
+
expect(fs_1.default.lstatSync(linked).isSymbolicLink() || fs_1.default.existsSync(path_1.default.join(linked, 'SKILL.md'))).toBe(true);
|
|
77
|
+
expect(fs_1.default.readFileSync(path_1.default.join(linked, 'SKILL.md'), 'utf8')).toContain('Follow the demo skill body.');
|
|
80
78
|
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
81
79
|
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
82
80
|
});
|
|
@@ -134,3 +134,71 @@ describe('pinning refuses to happen when there is nothing to measure', () => {
|
|
|
134
134
|
expect(fs_1.default.existsSync(path_1.default.join(cwd, budget_1.CONFIG_FILE))).toBe(true);
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
|
+
describe('Cursor hook budget pins (Plan B R5)', () => {
|
|
138
|
+
let tmpHome;
|
|
139
|
+
let originalHome;
|
|
140
|
+
let originalAwmHome;
|
|
141
|
+
const dirs = [];
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cursor-budget-home-'));
|
|
144
|
+
originalHome = process.env.HOME;
|
|
145
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
146
|
+
process.env.HOME = tmpHome;
|
|
147
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
148
|
+
jest.resetModules();
|
|
149
|
+
});
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
152
|
+
dirs.forEach((d) => fs_1.default.rmSync(d, { recursive: true, force: true }));
|
|
153
|
+
if (originalHome === undefined)
|
|
154
|
+
delete process.env.HOME;
|
|
155
|
+
else
|
|
156
|
+
process.env.HOME = originalHome;
|
|
157
|
+
if (originalAwmHome === undefined)
|
|
158
|
+
delete process.env.AWM_HOME;
|
|
159
|
+
else
|
|
160
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
161
|
+
});
|
|
162
|
+
it('skips cursor pins when hooks are not installed', () => {
|
|
163
|
+
const { checkBudget } = require('../../../src/commands/context-budget/budget');
|
|
164
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-budget-'));
|
|
165
|
+
dirs.push(dir);
|
|
166
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), 'a'.repeat(100));
|
|
167
|
+
const report = checkBudget(dir);
|
|
168
|
+
expect(report.cursorHooks).toBeUndefined();
|
|
169
|
+
expect(report.status).toBe('pinned');
|
|
170
|
+
});
|
|
171
|
+
it('marks over when installed script caps exceed pin or disagree with module constants', () => {
|
|
172
|
+
const { checkBudget, writeConfig } = require('../../../src/commands/context-budget/budget');
|
|
173
|
+
const { CURSOR_SESSION_START_MAX_BYTES, CURSOR_REANCHOR_MAX_BYTES, } = require('../../../src/core/cursor-budgets');
|
|
174
|
+
expect(CURSOR_SESSION_START_MAX_BYTES).toBe(24 * 1024);
|
|
175
|
+
expect(CURSOR_REANCHOR_MAX_BYTES).toBe(4 * 1024);
|
|
176
|
+
const scripts = path_1.default.join(tmpHome, '.awm/hooks/cursor');
|
|
177
|
+
fs_1.default.mkdirSync(scripts, { recursive: true });
|
|
178
|
+
fs_1.default.writeFileSync(path_1.default.join(scripts, 'session-start'), 'const MAX_SESSION_BYTES = 24 * 1024;\nconst MAX_REANCHOR_BYTES = 4 * 1024;\n');
|
|
179
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-budget-'));
|
|
180
|
+
dirs.push(dir);
|
|
181
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), 'a'.repeat(100));
|
|
182
|
+
writeConfig(dir, {
|
|
183
|
+
files: ['AGENTS.md'],
|
|
184
|
+
maxBytes: 1000,
|
|
185
|
+
'cursor.sessionStartMaxBytes': 1024, // below observed 24KiB
|
|
186
|
+
'cursor.reanchorMaxBytes': 4096,
|
|
187
|
+
});
|
|
188
|
+
const report = checkBudget(dir);
|
|
189
|
+
expect(report.cursorHooks?.status).toBe('over');
|
|
190
|
+
expect(report.status).toBe('over');
|
|
191
|
+
});
|
|
192
|
+
it('is within when script literals match defaults', () => {
|
|
193
|
+
const { checkBudget } = require('../../../src/commands/context-budget/budget');
|
|
194
|
+
const scripts = path_1.default.join(tmpHome, '.awm/hooks/cursor');
|
|
195
|
+
fs_1.default.mkdirSync(scripts, { recursive: true });
|
|
196
|
+
fs_1.default.writeFileSync(path_1.default.join(scripts, 'session-start'), 'const MAX_SESSION_BYTES = 24 * 1024;\nconst MAX_REANCHOR_BYTES = 4 * 1024;\n');
|
|
197
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-budget-'));
|
|
198
|
+
dirs.push(dir);
|
|
199
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), 'a'.repeat(100));
|
|
200
|
+
const report = checkBudget(dir);
|
|
201
|
+
expect(report.cursorHooks?.status).toBe('within');
|
|
202
|
+
expect(report.cursorHooks?.observedSessionStartMaxBytes).toBe(24 * 1024);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -229,13 +229,13 @@ describe('renderProviderReport — capability tier (Task 4.4)', () => {
|
|
|
229
229
|
overall: 'healthy',
|
|
230
230
|
providers: [
|
|
231
231
|
{ id: 'claude-code', label: 'Claude Code', tier: 'hooks-native', checks: [] },
|
|
232
|
-
{ id: 'cursor', label: 'Cursor', tier: '
|
|
232
|
+
{ id: 'cursor', label: 'Cursor', tier: 'hooks-native', checks: [] },
|
|
233
233
|
{ id: 'antigravity', label: 'Antigravity', tier: 'context-only', checks: [] },
|
|
234
234
|
],
|
|
235
235
|
};
|
|
236
236
|
const out = (0, doctor_1.renderProviderReport)(report);
|
|
237
237
|
expect(out).toContain('Provider: Claude Code (hooks-native)');
|
|
238
|
-
expect(out).toContain('Provider: Cursor (
|
|
238
|
+
expect(out).toContain('Provider: Cursor (hooks-native)');
|
|
239
239
|
expect(out).toContain('Provider: Antigravity (context-only)');
|
|
240
240
|
});
|
|
241
241
|
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
describe('installHook / computeHookStatus / uninstallHook — Cursor adapter', () => {
|
|
11
|
+
let tmpHome;
|
|
12
|
+
let tmpRegistry;
|
|
13
|
+
let hooksJson;
|
|
14
|
+
let cursorScriptsDir;
|
|
15
|
+
let originalHome;
|
|
16
|
+
let originalAwmHome;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cursor-hooks-'));
|
|
19
|
+
tmpRegistry = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cursor-registry-'));
|
|
20
|
+
hooksJson = path_1.default.join(tmpHome, '.cursor/hooks.json');
|
|
21
|
+
cursorScriptsDir = path_1.default.join(tmpHome, '.awm/hooks/cursor');
|
|
22
|
+
originalHome = process.env.HOME;
|
|
23
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
24
|
+
process.env.HOME = tmpHome;
|
|
25
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
30
|
+
fs_1.default.rmSync(tmpRegistry, { recursive: true, force: true });
|
|
31
|
+
if (originalHome === undefined)
|
|
32
|
+
delete process.env.HOME;
|
|
33
|
+
else
|
|
34
|
+
process.env.HOME = originalHome;
|
|
35
|
+
if (originalAwmHome === undefined)
|
|
36
|
+
delete process.env.AWM_HOME;
|
|
37
|
+
else
|
|
38
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
39
|
+
});
|
|
40
|
+
function writeRegistry(content = '#!/usr/bin/env node\nconsole.log("{}")') {
|
|
41
|
+
const regHooks = path_1.default.join(tmpRegistry, 'hooks');
|
|
42
|
+
const regSkill = path_1.default.join(tmpRegistry, 'skills/using-awm');
|
|
43
|
+
fs_1.default.mkdirSync(regHooks, { recursive: true });
|
|
44
|
+
fs_1.default.mkdirSync(regSkill, { recursive: true });
|
|
45
|
+
for (const name of ['cursor-session-start', 'cursor-pre-compact', 'cursor-post-tool-use']) {
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(regHooks, name), content, { mode: 0o755 });
|
|
47
|
+
}
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(regSkill, 'SKILL.md'), '---\nname: using-awm\n---\nMUST invoke skills.\n');
|
|
49
|
+
}
|
|
50
|
+
function installOpts(over = {}) {
|
|
51
|
+
return {
|
|
52
|
+
agent: 'cursor',
|
|
53
|
+
registryRoot: tmpRegistry,
|
|
54
|
+
installMethod: 'copy',
|
|
55
|
+
baselineRegistryVersion: '4.9.0',
|
|
56
|
+
...over,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function awmEntries() {
|
|
60
|
+
return {
|
|
61
|
+
sessionStart: [{ command: path_1.default.join(cursorScriptsDir, 'session-start') }],
|
|
62
|
+
preCompact: [{ command: path_1.default.join(cursorScriptsDir, 'pre-compact') }],
|
|
63
|
+
postToolUse: [{ command: path_1.default.join(cursorScriptsDir, 'post-tool-use') }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
it('merges three AWM events and preserves non-AWM entries', () => {
|
|
67
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
68
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({
|
|
69
|
+
version: 1,
|
|
70
|
+
hooks: {
|
|
71
|
+
sessionStart: [{ command: '/user/custom-start' }],
|
|
72
|
+
stop: [{ command: 'echo stop' }],
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
writeRegistry();
|
|
76
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
77
|
+
installHook(installOpts());
|
|
78
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
79
|
+
expect(cfg.version).toBe(1);
|
|
80
|
+
expect(cfg.hooks.stop).toEqual([{ command: 'echo stop' }]);
|
|
81
|
+
expect(cfg.hooks.sessionStart).toEqual([
|
|
82
|
+
{ command: '/user/custom-start' },
|
|
83
|
+
{ command: path_1.default.join(cursorScriptsDir, 'session-start') },
|
|
84
|
+
]);
|
|
85
|
+
expect(cfg.hooks.preCompact).toEqual([{ command: path_1.default.join(cursorScriptsDir, 'pre-compact') }]);
|
|
86
|
+
expect(cfg.hooks.postToolUse).toEqual([{ command: path_1.default.join(cursorScriptsDir, 'post-tool-use') }]);
|
|
87
|
+
expect(fs_1.default.existsSync(path_1.default.join(cursorScriptsDir, 'session-start'))).toBe(true);
|
|
88
|
+
expect(fs_1.default.readFileSync(path_1.default.join(cursorScriptsDir, 'using-awm.md'), 'utf8')).toContain('MUST invoke skills.');
|
|
89
|
+
});
|
|
90
|
+
it('installs cleanly with no pre-existing hooks.json and backs up on replace', () => {
|
|
91
|
+
writeRegistry();
|
|
92
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
93
|
+
const first = installHook(installOpts({ installMethod: 'symlink' }));
|
|
94
|
+
expect(first.status).toBe('installed');
|
|
95
|
+
expect(first.backupPath).toBeNull();
|
|
96
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
97
|
+
expect(cfg).toEqual({ version: 1, hooks: awmEntries() });
|
|
98
|
+
expect(fs_1.default.lstatSync(path_1.default.join(cursorScriptsDir, 'session-start')).isSymbolicLink()).toBe(true);
|
|
99
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 1, hooks: { sessionStart: [] } }, null, 2));
|
|
100
|
+
const second = installHook(installOpts());
|
|
101
|
+
expect(second.status).toBe('installed');
|
|
102
|
+
expect(second.backupPath).toMatch(/hooks\.json\..*\.bak$/);
|
|
103
|
+
});
|
|
104
|
+
it('fails when registry lacks cursor-session-start', () => {
|
|
105
|
+
const regHooks = path_1.default.join(tmpRegistry, 'hooks');
|
|
106
|
+
fs_1.default.mkdirSync(regHooks, { recursive: true });
|
|
107
|
+
fs_1.default.writeFileSync(path_1.default.join(regHooks, 'cursor-pre-compact'), 'x', { mode: 0o755 });
|
|
108
|
+
fs_1.default.writeFileSync(path_1.default.join(regHooks, 'cursor-post-tool-use'), 'x', { mode: 0o755 });
|
|
109
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpRegistry, 'skills/using-awm'), { recursive: true });
|
|
110
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpRegistry, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\n---\n');
|
|
111
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
112
|
+
expect(() => installHook(installOpts())).toThrow(/Cursor hook source missing/);
|
|
113
|
+
expect(fs_1.default.existsSync(hooksJson)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
it('refuses invalid JSON, unknown version, and duplicate AWM entries without writing', () => {
|
|
116
|
+
writeRegistry();
|
|
117
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
118
|
+
fs_1.default.writeFileSync(hooksJson, '{not json');
|
|
119
|
+
const beforeBad = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
120
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
121
|
+
expect(() => installHook(installOpts())).toThrow(/not valid JSON/);
|
|
122
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(beforeBad);
|
|
123
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 2, hooks: {} }));
|
|
124
|
+
const beforeVer = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
125
|
+
expect(() => installHook(installOpts())).toThrow(/unsupported hooks\.json version/);
|
|
126
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(beforeVer);
|
|
127
|
+
const dup = {
|
|
128
|
+
version: 1,
|
|
129
|
+
hooks: {
|
|
130
|
+
sessionStart: [
|
|
131
|
+
{ command: path_1.default.join(cursorScriptsDir, 'session-start') },
|
|
132
|
+
{ command: path_1.default.join(cursorScriptsDir, 'session-start') },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify(dup, null, 2));
|
|
137
|
+
const beforeDup = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
138
|
+
expect(() => installHook(installOpts())).toThrow(/multiple AWM sessionStart/);
|
|
139
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(beforeDup);
|
|
140
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/backups'))).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
it('aborts when an event key is present but not an array', () => {
|
|
143
|
+
writeRegistry();
|
|
144
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
145
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 1, hooks: { sessionStart: { command: 'x' } } }));
|
|
146
|
+
const before = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
147
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
148
|
+
expect(() => installHook(installOpts())).toThrow(/must be an array/);
|
|
149
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(before);
|
|
150
|
+
});
|
|
151
|
+
it('prunes dead AWM entries from another AWM_HOME and preserves live parallels', () => {
|
|
152
|
+
writeRegistry();
|
|
153
|
+
const dead = '/tmp/awm-gone-never/hooks/cursor/session-start';
|
|
154
|
+
const liveOther = path_1.default.join(tmpHome, '.awm-other/hooks/cursor/session-start');
|
|
155
|
+
fs_1.default.mkdirSync(path_1.default.dirname(liveOther), { recursive: true });
|
|
156
|
+
fs_1.default.writeFileSync(liveOther, '#!/usr/bin/env node\n', { mode: 0o755 });
|
|
157
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
158
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({
|
|
159
|
+
version: 1,
|
|
160
|
+
hooks: {
|
|
161
|
+
sessionStart: [
|
|
162
|
+
{ command: dead },
|
|
163
|
+
{ command: liveOther },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
}, null, 2));
|
|
167
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
168
|
+
installHook(installOpts());
|
|
169
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
170
|
+
expect(cfg.hooks.sessionStart.map((e) => e.command)).toEqual([
|
|
171
|
+
liveOther,
|
|
172
|
+
path_1.default.join(cursorScriptsDir, 'session-start'),
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
it('is idempotent when nothing changed', () => {
|
|
176
|
+
writeRegistry();
|
|
177
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
178
|
+
const first = installHook(installOpts());
|
|
179
|
+
expect(first.status).toBe('installed');
|
|
180
|
+
const contentBefore = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
181
|
+
const mtimeBefore = fs_1.default.statSync(hooksJson).mtimeMs;
|
|
182
|
+
const second = installHook(installOpts());
|
|
183
|
+
expect(second.status).toBe('already-up-to-date');
|
|
184
|
+
expect(second.backupPath).toBeNull();
|
|
185
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(contentBefore);
|
|
186
|
+
expect(fs_1.default.statSync(hooksJson).mtimeMs).toBe(mtimeBefore);
|
|
187
|
+
});
|
|
188
|
+
it.each([
|
|
189
|
+
[false, 'pending-first-run'],
|
|
190
|
+
[true, 'healthy'],
|
|
191
|
+
])('derives trust from heartbeat (present=%s → %s)', (heartbeat, expected) => {
|
|
192
|
+
fs_1.default.mkdirSync(cursorScriptsDir, { recursive: true });
|
|
193
|
+
const scriptPath = path_1.default.join(cursorScriptsDir, 'session-start');
|
|
194
|
+
fs_1.default.writeFileSync(scriptPath, '#!/usr/bin/env node\n', { mode: 0o755 });
|
|
195
|
+
fs_1.default.writeFileSync(path_1.default.join(cursorScriptsDir, 'using-awm.md'), '# using-awm\n');
|
|
196
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
197
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 1, hooks: awmEntries() }, null, 2));
|
|
198
|
+
if (heartbeat) {
|
|
199
|
+
fs_1.default.writeFileSync(path_1.default.join(cursorScriptsDir, 'heartbeat.json'), JSON.stringify({ hash: crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(scriptPath)).digest('hex') }));
|
|
200
|
+
}
|
|
201
|
+
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
202
|
+
expect(computeHookStatus('cursor').trust).toBe(expected);
|
|
203
|
+
});
|
|
204
|
+
it('reports stale trust when heartbeat hash mismatches', () => {
|
|
205
|
+
fs_1.default.mkdirSync(cursorScriptsDir, { recursive: true });
|
|
206
|
+
const scriptPath = path_1.default.join(cursorScriptsDir, 'session-start');
|
|
207
|
+
fs_1.default.writeFileSync(scriptPath, '#!/usr/bin/env node\n', { mode: 0o755 });
|
|
208
|
+
fs_1.default.writeFileSync(path_1.default.join(cursorScriptsDir, 'using-awm.md'), '# using-awm\n');
|
|
209
|
+
fs_1.default.writeFileSync(path_1.default.join(cursorScriptsDir, 'heartbeat.json'), JSON.stringify({ hash: 'deadbeef' }));
|
|
210
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
211
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 1, hooks: awmEntries() }, null, 2));
|
|
212
|
+
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
213
|
+
const status = computeHookStatus('cursor');
|
|
214
|
+
expect(status.trust).toBe('stale');
|
|
215
|
+
expect(status.overall).toBe('DEGRADED');
|
|
216
|
+
});
|
|
217
|
+
it('refuses install when baseline registry is below 4.9.0 and accepts 4.9.0', () => {
|
|
218
|
+
writeRegistry();
|
|
219
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
220
|
+
expect(() => installHook(installOpts({ baselineRegistryVersion: '4.8.1' })))
|
|
221
|
+
.toThrow("Cursor hooks require baseline registry >= 4.9.0; installed is 4.8.1. Run 'awm update -y'.");
|
|
222
|
+
expect(fs_1.default.existsSync(hooksJson)).toBe(false);
|
|
223
|
+
expect(installHook(installOpts({ baselineRegistryVersion: '4.9.0' })).status).toBe('installed');
|
|
224
|
+
});
|
|
225
|
+
it('shouldDisableDeferredReanchor follows R16.2 thresholds', () => {
|
|
226
|
+
const { shouldDisableDeferredReanchor } = require('../../../src/commands/hooks/cursor');
|
|
227
|
+
expect(shouldDisableDeferredReanchor({ deliveryVerified: false, p95Ms: 10 })).toBe(true);
|
|
228
|
+
expect(shouldDisableDeferredReanchor({ deliveryVerified: true, p95Ms: 51 })).toBe(true);
|
|
229
|
+
expect(shouldDisableDeferredReanchor({ deliveryVerified: true, p95Ms: 50 })).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
it('omits deferred events when disableDeferredReanchor is set', () => {
|
|
232
|
+
writeRegistry();
|
|
233
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
234
|
+
installHook(installOpts({ disableDeferredReanchor: true }));
|
|
235
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
236
|
+
expect(cfg.hooks.sessionStart).toEqual([{ command: path_1.default.join(cursorScriptsDir, 'session-start') }]);
|
|
237
|
+
expect(cfg.hooks.preCompact).toBeUndefined();
|
|
238
|
+
expect(cfg.hooks.postToolUse).toBeUndefined();
|
|
239
|
+
expect(fs_1.default.existsSync(path_1.default.join(cursorScriptsDir, 'session-start'))).toBe(true);
|
|
240
|
+
expect(fs_1.default.existsSync(path_1.default.join(cursorScriptsDir, 'pre-compact'))).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
it('uninstall removes only AWM Cursor entries', () => {
|
|
243
|
+
writeRegistry();
|
|
244
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
245
|
+
const { uninstallHook } = require('../../../src/commands/hooks/uninstall');
|
|
246
|
+
installHook(installOpts());
|
|
247
|
+
const current = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
248
|
+
current.hooks.sessionStart.unshift({ command: '/user/keep' });
|
|
249
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify(current, null, 2));
|
|
250
|
+
const result = uninstallHook({ agent: 'cursor' });
|
|
251
|
+
expect(result.status).toBe('uninstalled');
|
|
252
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
253
|
+
expect(cfg.hooks.sessionStart).toEqual([{ command: '/user/keep' }]);
|
|
254
|
+
expect(cfg.hooks.preCompact).toBeUndefined();
|
|
255
|
+
expect(cfg.hooks.postToolUse).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -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
|
});
|