agentic-workflow-manager 9.14.1 → 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/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 +56 -2
- 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 +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,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). */
|
|
@@ -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
|
});
|
|
@@ -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 });
|