@sayansr26/agent-os 0.6.2 → 0.6.3

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Context engineering and agent tooling for Claude Code.",
9
- "version": "0.5.0"
9
+ "version": "0.5.1"
10
10
  },
11
11
  "plugins": [
12
12
  {
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to this project are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
6
  this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.3] — 2026-10-31
9
+
10
+ agent-os plugin 0.5.1 (hooks gain a Cursor mode; Claude Code behaviour
11
+ unchanged), auto-route unchanged (0.1.0), auto-handoff unchanged (0.8.6).
12
+
13
+ ### Added
14
+ - **The agent-os layer for Cursor.** With `cursor` as a target, `init` and
15
+ `sync` compile the seven agents to `.cursor/agents/agent-os-<name>.md`, the
16
+ three skills to `/agent-os-init`, `/agent-os-map` and `/agent-os-memory`, the
17
+ session-start and pre-edit hooks plus git write protection to
18
+ `.cursor/hooks/agent-os/` (registered in `.cursor/hooks.json`, merged), and
19
+ the agent contract to an always-on `.cursor/rules/agent-os-agents.mdc`.
20
+ Subagent memory uses `.claude/agent-memory/agent-os-<name>/`, the directories
21
+ Claude Code's plugin agents use. `{ "cursor": { "agentOs": false } }` or
22
+ `{ "cursor": { "gitProtection": false } }` in `.agent-os/config.json` opt out.
23
+ - `session-resume.mjs --cursor` and `pre-edit.mjs --cursor` answer in Cursor's
24
+ hook protocol (`additional_context`; `permission` with an explicit `allow`).
25
+
26
+ ### Fixed
27
+ - **`init` in a project that does not target Claude Code** no longer prints
28
+ Claude Code's SETUP block or ends `fresh → repair` with a to-do list of
29
+ CLAUDE.md, `.claude/settings.json` and plugin items — including one telling
30
+ the user to run `init` again. It now ends with what it compiled for.
31
+
8
32
  ## [0.6.2] — 2026-10-22
9
33
 
10
34
  agent-os plugin unchanged (0.5.0), auto-route unchanged (0.1.0). New plugin
package/README.md CHANGED
@@ -110,14 +110,38 @@ Generated files carry a banner. Edit `.agent-os/`, run `sync`, never edit the ou
110
110
 
111
111
  Being straight about this matters more than the feature list.
112
112
 
113
- **Hooks don't port.** Every tool differs in events *and* control protocol — Cursor returns JSON `permission`, Windsurf uses exit codes, Claude Code uses JSON. There is no honest common denominator, so `agent-os` doesn't invent one.
113
+ **Hooks don't port generically.** Every tool differs in events *and* control protocol — Cursor returns JSON `permission`, Windsurf uses exit codes, Claude Code uses JSON. There is no honest common denominator, so `agent-os` doesn't invent one for your hooks. Its own hooks are written once per tool: Claude Code (the plugin) and Cursor (see below).
114
114
 
115
- **Memory doesn't port.** Only Claude Code (auto memory) and Windsurf have native per-session memory. Kilo deprecated its Memory Bank in favour of `AGENTS.md`; Cline's is a community methodology, not a feature. So the architecture map — the thing that makes *"change the login flow from email to OTP"* run off known structure — works in Claude Code and nowhere else yet.
115
+ **Memory doesn't port generically.** Only Claude Code (auto memory) and Windsurf have native per-session memory. Kilo deprecated its Memory Bank in favour of `AGENTS.md`; Cline's is a community methodology, not a feature. So the architecture map — the thing that makes *"change the login flow from email to OTP"* run off known structure — works in Claude Code and Cursor, whose subagents are told where their memory lives, and nowhere else yet.
116
116
 
117
117
  **Antigravity's rules target is best effort.** Its docs state a rule is "simply a Markdown file" and never show frontmatter; glob activation is configured in the Customizations panel. So `agent-os` writes valid Markdown and states the intended scope in a comment, rather than inventing a `globs:` key that may silently do nothing.
118
118
 
119
119
  ---
120
120
 
121
+ ## Cursor
122
+
123
+ When Cursor is a target, `init` and `sync` also compile the agent-os layer into the places Cursor reads it — the same agents, skills and hooks the Claude Code plugin ships:
124
+
125
+ | Claude Code (plugin) | Cursor (generated) |
126
+ |---|---|
127
+ | agents `agent-os:<name>` | subagents `.cursor/agents/agent-os-<name>.md` |
128
+ | `/agent-os:init`, `/agent-os:map`, `/agent-os:memory` | `/agent-os-init`, `/agent-os-map`, `/agent-os-memory` in `.cursor/skills/` |
129
+ | SessionStart hook | `sessionStart` hook → `additional_context` |
130
+ | PreToolUse guard on generated files + cartographer reminder | `preToolUse` hook on `Write` |
131
+ | git write protection in `permissions.deny` | `beforeShellExecution` hook refusing the same git commands |
132
+ | "Agents in this project" section of `CLAUDE.md` | `.cursor/rules/agent-os-agents.mdc`, always applied |
133
+ | per-agent memory, `.claude/agent-memory/agent-os-<name>/` | the same directories — one map for both tools |
134
+
135
+ Hook scripts live in `.cursor/hooks/agent-os/` and are registered in `.cursor/hooks.json`, merged so your own hooks stay. Everything carries the generated banner, so `check` catches drift and the pre-edit hook blocks hand edits; `sync` after upgrading the package is the Cursor equivalent of `claude plugin update`. Reload the Cursor window after `init`, then run `/agent-os-init` in Agent chat.
136
+
137
+ ```json
138
+ { "targets": ["cursor"], "cursor": { "agentOs": true, "gitProtection": true } }
139
+ ```
140
+
141
+ `agentOs: false` stops generating the layer and removes its hooks from `hooks.json` (delete the `agent-os-*` files yourself); `gitProtection: false` drops only the git hook. What does not carry over: Claude Code's auto memory, task tools (Cursor has its own to-do list), and the auto-route and auto-handoff mods.
142
+
143
+ ---
144
+
121
145
  ## The Claude Code plugin
122
146
 
123
147
  The deeper context-engineering work — the architecture map, seven coordinated agents, per-agent memory, and the hooks that make Claude use them — ships as a Claude Code plugin in this repo:
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cursor beforeShellExecution hook — git write protection.
4
+ *
5
+ * Claude Code gets this as `permissions.deny` rules in .claude/settings.json.
6
+ * Cursor has no deny list, so the same rules (GIT_DENY, generated next to this
7
+ * file from settings.mjs) are enforced here: any git command that changes the
8
+ * repository is denied, read-only inspection is allowed.
9
+ *
10
+ * The hook is registered with a `git` matcher, so it only sees commands that
11
+ * mention git. Cursor reads a missing or invalid response as a block, so every
12
+ * exit answers explicitly.
13
+ */
14
+ import { readFileSync } from "node:fs";
15
+ import { GIT_DENY } from "../lib/git-deny.mjs";
16
+
17
+ const answer = (o) => { process.stdout.write(JSON.stringify(o)); process.exit(0); };
18
+
19
+ // "Bash(git push *)" -> "git push" plus anything after a space;
20
+ // "Bash(git -C*)" -> anything starting "git -C".
21
+ const RULES = GIT_DENY.map((r) => r.replace(/^Bash\(/, "").replace(/\)$/, "")).map((r) =>
22
+ r.endsWith(" *") ? { prefix: r.slice(0, -2), spaced: true } : { prefix: r.replace(/\*$/, ""), spaced: false });
23
+
24
+ const denied = (segment) => {
25
+ const cmd = segment.trim().replace(/\s+/g, " ").replace(/^(?:\w+=\S*\s+)+/, "");
26
+ return RULES.find((r) => r.spaced ? cmd === r.prefix || cmd.startsWith(`${r.prefix} `) : cmd.startsWith(r.prefix));
27
+ };
28
+
29
+ let command = "";
30
+ try { command = String(JSON.parse(readFileSync(0, "utf8") || "{}").command ?? ""); } catch { answer({ permission: "allow" }); }
31
+
32
+ // Check every command in a chain: `cd x && git push` is still a push.
33
+ for (const segment of command.split(/&&|\|\||[;|\n]|\$\(|`/)) {
34
+ const hit = denied(segment);
35
+ if (!hit) continue;
36
+ const text = `agent-os blocks git commands that change the repository (${hit.prefix}${hit.spaced ? " …" : "…"}). ` +
37
+ "Read-only git (status, log, diff, show, blame) is allowed. Ask the user to run this one themselves.";
38
+ answer({ permission: "deny", user_message: text, agent_message: text });
39
+ }
40
+ answer({ permission: "allow" });
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: agent-os-init
3
+ description: Set up or repair this project's agent-os context layer in Cursor. Checks the generated rules, subagents and hooks against .agent-os/, inspects agent memory, then establishes, maps, migrates or maintains the layer. Use for "set up agent-os", "agent-os init", "initialise this project", "bootstrap my context setup", "migrate off memory-bank or .cursorrules", "my AGENTS.md is too big", or when starting work in a repo with no setup.
4
+ ---
5
+
6
+ # agent-os for Cursor — set up or repair
7
+
8
+ The Cursor counterpart of Claude Code's `/agent-os:init`. Same layout, same
9
+ agents, same memory — only the file names differ.
10
+
11
+ | Tier | Lives in | Startup cost | Holds |
12
+ |---|---|---|---|
13
+ | Always-loaded | `AGENTS.md` (from `.agent-os/AGENTS.md`), ≤200 lines | small, fixed | Only what is true in *every* session |
14
+ | On-demand | `.cursor/rules/*.mdc` with `globs:` (from `.agent-os/rules/*.md` with `paths:`) | **zero** | Durable facts scoped to some files |
15
+ | Explored | `.claude/agent-memory/agent-os-feature-cartographer/` | **zero** (subagent context) | How each feature is actually built |
16
+ | Task state | the project's own vault / handoff file | zero (read selectively) | What is in flight right now |
17
+
18
+ Agent memory stays under `.claude/agent-memory/` on purpose: Claude Code's
19
+ agent-os agents use the same directories, so a team on both tools shares one map.
20
+
21
+ **Storing a fact and loading a fact are different acts.** Anything that makes
22
+ them the same act — a handbook `AGENTS.md`, a memory bank read at every startup,
23
+ an always-apply rule that only matters for one directory — is the bug.
24
+
25
+ ## Arguments
26
+
27
+ | Argument | Do |
28
+ |---|---|
29
+ | *(none)* | The full pass below |
30
+ | `audit` | Steps 1 and 3 only. Change nothing. |
31
+
32
+ `/agent-os-map` builds the architecture map; `/agent-os-memory` inspects and
33
+ repairs what the project remembers.
34
+
35
+ ## Step 1 — Read the state
36
+
37
+ Three commands. Do not rediscover this with a dozen file reads — they are
38
+ deterministic:
39
+
40
+ ```bash
41
+ npx -y @sayansr26/agent-os detect
42
+ npx -y @sayansr26/agent-os check
43
+ npx -y @sayansr26/agent-os memory
44
+ ```
45
+
46
+ `check` lists every generated file — rules, `AGENTS.md`, the `agent-os-*`
47
+ subagents and skills, the hook scripts and `.cursor/hooks.json` — as `ok`,
48
+ `MISSING` or `DRIFTED`. `memory` lists every agent's memory, which features are
49
+ mapped, and a health list.
50
+
51
+ **Show the output to the user before you change anything.**
52
+
53
+ ## Step 2 — Fresh setup or repair?
54
+
55
+ | State | Do |
56
+ |---|---|
57
+ | no `.agent-os/` | `npx -y @sayansr26/agent-os init`, then carry on with Step 3 |
58
+ | `check` reports `MISSING` or `DRIFTED` | `npx -y @sayansr26/agent-os sync` — it rewrites only what it generated |
59
+ | `sync` names files it did not generate | stop and ask: move their content into `.agent-os/` (preferred), or `--force` after a commit |
60
+ | everything `ok` | nothing to repair |
61
+
62
+ Never edit a generated file by hand. They carry a banner, the pre-edit hook
63
+ blocks the edit, and the next `sync` would revert it. Edit `.agent-os/` and sync.
64
+
65
+ ## Step 3 — Decide what this run is for
66
+
67
+ Judge from what Step 1 printed and a look at the source tree:
68
+
69
+ | Mode | Evidence | Do |
70
+ |---|---|---|
71
+ | `TOO-EARLY` | Barely any source (under ~5 files) | **Build nothing.** A layer over an empty project is invented conventions. |
72
+ | `ESTABLISH` | `.agent-os/AGENTS.md` is still the placeholder, or the only rule is `example.md` | `references/establishing.md` — build the layer *from the code* |
73
+ | `MAP` | Layer real, but `memory` shows no `_architecture.md` | `references/establishing.md`, "Map the architecture" — or `/agent-os-map` |
74
+ | `MIGRATE` | `.cursorrules`, `memory-bank/`, `.serena/memories/`, or an always-apply rule that only concerns part of the tree | fix it; `references/migrating.md` for legacy stores |
75
+ | `MAINTAIN` | Real layer, architecture mapped | report and stop |
76
+
77
+ `references/changing-a-feature.md` is the workflow that pays all this back —
78
+ point the user at it once the layer exists.
79
+
80
+ The references were written for Claude Code. Read them through this table:
81
+
82
+ | They say | In Cursor |
83
+ |---|---|
84
+ | `CLAUDE.md` | `.agent-os/AGENTS.md` (compiled to `AGENTS.md`). Draft it yourself from the code — there is no `/init` to run first. |
85
+ | `.claude/rules/*.md` | `.agent-os/rules/*.md`, then `sync` (compiled to `.cursor/rules/*.mdc`) |
86
+ | `agent-os:<name>` | the `agent-os-<name>` subagent |
87
+ | `/agent-os:init`, `/agent-os:map`, `/agent-os:memory` | `/agent-os-init`, `/agent-os-map`, `/agent-os-memory` |
88
+ | `node "${CLAUDE_PLUGIN_ROOT}/…/<script>.mjs"` | `npx -y @sayansr26/agent-os <script>` |
89
+ | "Wire the agents into CLAUDE.md" (Step 4b) | already done: `.cursor/rules/agent-os-agents.mdc` is always applied — skip it |
90
+ | the settings pass, `permissions.deny`, task tools | see "Git write protection" below; Cursor has its own to-do list |
91
+
92
+ ## Step 4 — Verify by re-running
93
+
94
+ Run `check` and `memory` again and show before and after: generated files all
95
+ `ok`, features mapped, health findings. Do not declare success on vibes — the
96
+ commands produce the numbers, so quote them.
97
+
98
+ ## Git write protection
99
+
100
+ Claude Code gets git write protection as `permissions.deny` rules. Cursor has no
101
+ deny list, so `sync` registers a `beforeShellExecution` hook in
102
+ `.cursor/hooks.json` (`.cursor/hooks/agent-os/hooks/git-guard.mjs`) that refuses
103
+ the same commands: every git command that changes the repository. Read-only git
104
+ stays available. `references/git-permissions.md` explains each rule.
105
+
106
+ If the user wants it off: `{ "cursor": { "gitProtection": false } }` in
107
+ `.agent-os/config.json`, then `sync`. Say what that gives up.
108
+
109
+ ## What this skill will not do
110
+
111
+ - It will not write facts it has not verified in the repo. An invented rule is
112
+ worse than a missing one, because it will be trusted.
113
+ - It will not delete a legacy store before its content has landed in `.agent-os/`.
114
+ - It will not hand-edit generated files, or another agent's memory.
115
+ - It will not add a memory MCP server. The common failure is that nothing was
116
+ *written*, which a graph does not fix and the cartographer's memory does.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayansr26/agent-os",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "One source of truth for AI coding agent config. Write your rules once; compile them to Claude Code, Cursor, Cline, Windsurf, Antigravity, Gemini CLI, OpenCode and Kilo.",
5
5
  "keywords": [
6
6
  "ai",
@@ -34,6 +34,7 @@
34
34
  "bin",
35
35
  "src",
36
36
  "plugins",
37
+ "cursor",
37
38
  ".claude-plugin",
38
39
  "README.md",
39
40
  "LICENSE",
@@ -43,7 +44,6 @@
43
44
  "access": "public"
44
45
  },
45
46
  "scripts": {
46
- "test": "node scripts/validate-plugin.mjs && node src/selftest.mjs",
47
- "prepublishOnly": "npm test"
47
+ "test": "node scripts/validate-plugin.mjs && node src/selftest.mjs"
48
48
  }
49
- }
49
+ }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "agent-os",
3
3
  "description": "A context-engineered agent system for Claude Code. Ships a durable per-project memory layer, a cartographer that maps your codebase once and remembers it, and a coordinated agent set that reads your project's rules rather than hardcoding them.",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "author": {
6
6
  "name": "Sayan Choudhury",
7
7
  "email": "sayan.choudhury.in@gmail.com"
8
8
  },
9
9
  "homepage": "https://github.com/sayansr26/agent-os",
10
10
  "repository": "https://github.com/sayansr26/agent-os",
11
- "privacyPolicyUrl": "https://s3techpulse.com/privacy-policy",
12
- "supportUrl": "https://s3techpulse.com/support",
11
+ "privacyPolicyUrl": "https://github.com/sayansr26/agent-os/blob/master/PRIVACY.md",
12
+ "supportUrl": "https://github.com/sayansr26/agent-os/issues",
13
13
  "license": "MIT",
14
14
  "keywords": [
15
15
  "context-engineering",
@@ -10,11 +10,16 @@
10
10
  * Deny, and name the source to edit instead.
11
11
  * 2. Cartographer reminder (advisory, never blocks). The first edit to an
12
12
  * existing feature with no cartographer map adds a note telling Claude to
13
- * ask agent-os:feature-cartographer first. Once per feature per session.
13
+ * ask agent-os:feature-cartographer first (agent-os-feature-cartographer
14
+ * in Cursor). Once per feature per session.
14
15
  *
15
16
  * Stays silent for new (untracked) features, paths outside a feature
16
17
  * directory, and anything it cannot work out. Turn the reminder off with
17
18
  * `{ "claude": { "cartographerReminder": false } }` in `.agent-os/config.json`.
19
+ *
20
+ * `--cursor` is the same hook as Cursor's preToolUse on Write. Cursor reads a
21
+ * missing or empty response as a block, so in that mode every exit answers
22
+ * explicitly: `allow` (with the reminder as `agent_message`) or `deny`.
18
23
  */
19
24
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
25
  import { tmpdir } from "node:os";
@@ -24,17 +29,33 @@ import { claudeConfig, featureOf, findMap, mapFiles, tracked } from "../lib/feat
24
29
  // Must stay a substring of BANNER in src/source.mjs.
25
30
  const BANNER = "agent-os: generated from .agent-os/";
26
31
 
27
- const quiet = () => process.exit(0);
32
+ const CURSOR = process.argv.includes("--cursor");
33
+ const agent = (name) => `agent-os${CURSOR ? "-" : ":"}${name}`;
34
+
35
+ const quiet = () => {
36
+ if (CURSOR) process.stdout.write(JSON.stringify({ permission: "allow" }));
37
+ process.exit(0);
38
+ };
28
39
  const emit = (hookSpecificOutput) => {
29
- process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", ...hookSpecificOutput } }));
40
+ if (CURSOR) {
41
+ const deny = hookSpecificOutput.permissionDecision === "deny";
42
+ const text = deny ? hookSpecificOutput.permissionDecisionReason : hookSpecificOutput.additionalContext;
43
+ process.stdout.write(JSON.stringify(deny
44
+ ? { permission: "deny", user_message: text, agent_message: text }
45
+ : { permission: "allow", agent_message: text }));
46
+ } else
47
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", ...hookSpecificOutput } }));
30
48
  process.exit(0);
31
49
  };
32
50
 
33
51
  let payload;
34
52
  try { payload = JSON.parse(readFileSync(0, "utf8") || "{}"); } catch { quiet(); }
35
53
 
36
- const root = process.env.CLAUDE_PROJECT_DIR || payload?.cwd || process.cwd();
37
- const raw = String(payload?.tool_input?.file_path ?? "");
54
+ // Claude Code may run inside Cursor's terminal, so CURSOR_PROJECT_DIR counts only in --cursor mode.
55
+ const root = (CURSOR && process.env.CURSOR_PROJECT_DIR) || process.env.CLAUDE_PROJECT_DIR || payload?.cwd || process.cwd();
56
+ // Cursor does not document its Write tool's input, so take the usual names.
57
+ const input = payload?.tool_input || {};
58
+ const raw = String(input.file_path ?? input.path ?? input.target_file ?? input.filePath ?? "");
38
59
  if (!raw) quiet();
39
60
  const abs = isAbsolute(raw) ? raw : join(root, raw);
40
61
  const rel = relative(root, abs).replace(/\\/g, "/");
@@ -44,6 +65,14 @@ if (rel.startsWith("..")) quiet();
44
65
  try {
45
66
  if (existsSync(join(root, ".agent-os")) && existsSync(abs) && readFileSync(abs, "utf8").includes(BANNER)) {
46
67
  let source = ".agent-os/";
68
+ // Cursor's agent-os layer is compiled from the package, not from .agent-os/.
69
+ if (/^\.cursor\/(?:agents\/agent-os-|skills\/agent-os-|hooks\/agent-os\/|rules\/agent-os-agents\.mdc$)/.test(rel))
70
+ emit({
71
+ permissionDecision: "deny",
72
+ permissionDecisionReason:
73
+ `${rel} is part of the agent-os layer for Cursor and the next sync would revert this edit. ` +
74
+ `To customise it, set \`{ "cursor": { "agentOs": false } }\` in .agent-os/config.json and keep your own copy.`,
75
+ });
47
76
  const rule = rel.match(/^\.(?:claude|clinerules|cursor|windsurf|kilocode|gemini|agents?)\/(?:rules\/)?(.+?)\.(?:md|mdc)$/);
48
77
  if (rel === "AGENTS.md") source = ".agent-os/AGENTS.md";
49
78
  else if (rule && existsSync(join(root, ".agent-os/rules", `${basename(rule[1])}.md`)))
@@ -67,7 +96,7 @@ try {
67
96
 
68
97
  const dir = join(tmpdir(), "agent-os-reminders");
69
98
  mkdirSync(dir, { recursive: true });
70
- const session = String(payload?.session_id ?? "nosession").replace(/[^\w-]/g, "");
99
+ const session = String(payload?.session_id ?? payload?.conversation_id ?? "nosession").replace(/[^\w-]/g, "");
71
100
  const marker = join(dir, `${session}--${feature.dir.replace(/[^\w-]/g, "_")}`);
72
101
  if (existsSync(marker)) quiet();
73
102
  writeFileSync(marker, "");
@@ -75,7 +104,7 @@ try {
75
104
  emit({
76
105
  additionalContext:
77
106
  `agent-os (advisory): ${feature.dir}/ is an existing feature with no cartographer map. ` +
78
- `Before changing it further, ask agent-os:feature-cartographer how "${feature.name}" is built — ` +
107
+ `Before changing it further, ask ${agent("feature-cartographer")} how "${feature.name}" is built — ` +
79
108
  `it answers and files the map for every later session. This applies in plan mode too ` +
80
109
  `(ask it read-only; it writes the map after plan mode ends). Shown once per feature per session.`,
81
110
  });
@@ -16,6 +16,9 @@
16
16
  * - The snapshot is hard-capped (MAX_LINES); the contract is fixed-size.
17
17
  * - Fails silent: any error exits 0, so a broken hook never blocks a session.
18
18
  * - Read-only. Runs git for reading only; never writes, never mutates.
19
+ *
20
+ * `--cursor` is the same hook as Cursor's sessionStart: agent names in Cursor's
21
+ * `agent-os-<name>` form, and the text wrapped as `{ additional_context }`.
19
22
  */
20
23
 
21
24
  import { execSync } from "node:child_process";
@@ -24,7 +27,13 @@ import { join } from "node:path";
24
27
  import { coverage } from "../lib/features.mjs";
25
28
 
26
29
  const MAX_LINES = 40;
27
- const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
30
+ const CURSOR = process.argv.includes("--cursor");
31
+ // Claude Code may run inside Cursor's terminal, so CURSOR_PROJECT_DIR counts only in --cursor mode.
32
+ const cwd = (CURSOR && process.env.CURSOR_PROJECT_DIR) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
33
+ // Claude Code namespaces plugin agents and skills with `:`; Cursor names allow
34
+ // only letters and hyphens, so its generated copies use `-`.
35
+ const agent = (name) => `agent-os${CURSOR ? "-" : ":"}${name}`;
36
+ const skill = (name) => `/agent-os${CURSOR ? "-" : ":"}${name}`;
28
37
  const out = [];
29
38
 
30
39
  const sh = (cmd) => {
@@ -135,13 +144,13 @@ function snapshot() {
135
144
 
136
145
  // The agents and what triggers each. `subagent_type` is the namespaced name.
137
146
  const AGENTS = [
138
- ["agent-os:feature-cartographer", "BEFORE changing any existing feature — ask how it is built (files, state, API, blast radius). It answers from its map, or maps the feature and files the map."],
139
- ["agent-os:architect", "before a new subsystem, a cross-module change, or a data model others will depend on."],
140
- ["agent-os:builder", "to write the code once the shape is settled; it enforces the project's rules while writing."],
141
- ["agent-os:tester", "after building, to verify the change actually works."],
142
- ["agent-os:reviewer", "before calling any change done or opening a PR."],
143
- ["agent-os:documenter", "once work is verified, to update the changelog, task state and docs the change invalidated."],
144
- ["agent-os:orchestrator", "for work spanning several of the above, or that cannot be stated in one sentence."],
147
+ ["feature-cartographer", "BEFORE changing any existing feature — ask how it is built (files, state, API, blast radius). It answers from its map, or maps the feature and files the map."],
148
+ ["architect", "before a new subsystem, a cross-module change, or a data model others will depend on."],
149
+ ["builder", "to write the code once the shape is settled; it enforces the project's rules while writing."],
150
+ ["tester", "after building, to verify the change actually works."],
151
+ ["reviewer", "before calling any change done or opening a PR."],
152
+ ["documenter", "once work is verified, to update the changelog, task state and docs the change invalidated."],
153
+ ["orchestrator", "for work spanning several of the above, or that cannot be stated in one sentence."],
145
154
  ];
146
155
 
147
156
  function contract() {
@@ -153,15 +162,17 @@ function contract() {
153
162
  return [
154
163
  "## agent-os",
155
164
  "",
156
- "The agent-os plugin is installed but this project is not set up. If the user starts feature work, suggest `/agent-os:init` once.",
165
+ `agent-os is installed but this project is not set up. If the user starts feature work, suggest \`${skill("init")}\` once.`,
157
166
  ];
158
167
 
159
168
  const lines = [
160
169
  "## agent-os is active in this project",
161
170
  "",
162
- "These agents are how work is done here — use them without being asked (Agent tool, `subagent_type` as shown):",
171
+ CURSOR
172
+ ? "These subagents are how work is done here — delegate to them without being asked, by the names shown:"
173
+ : "These agents are how work is done here — use them without being asked (Agent tool, `subagent_type` as shown):",
163
174
  ];
164
- for (const [name, when] of AGENTS) lines.push(`- \`${name}\` — ${when}`);
175
+ for (const [name, when] of AGENTS) lines.push(`- \`${agent(name)}\` — ${when}`);
165
176
 
166
177
  const cov = coverage(cwd);
167
178
  lines.push("");
@@ -173,12 +184,14 @@ function contract() {
173
184
  } else if (cov.dir) {
174
185
  lines.push(`Cartographer maps are indexed in \`${cov.dir.slice(cwd.length + 1)}/MEMORY.md\`.`);
175
186
  }
176
- if (!cov.architecture) lines.push("No architecture map yet — `/agent-os:map` builds it.");
187
+ if (!cov.architecture) lines.push(`No architecture map yet — \`${skill("map")}\` builds it.`);
177
188
 
178
189
  lines.push("");
179
- lines.push("Plan mode: its \"Explore agents only\" phase does not replace the cartographer. Ask the cartographer read-only during planning (it will not write), and let it file its map once plan mode ends.");
190
+ lines.push(CURSOR
191
+ ? "Plan mode: ask the cartographer read-only during planning (it will not write), and let it file its map once plan mode ends."
192
+ : "Plan mode: its \"Explore agents only\" phase does not replace the cartographer. Ask the cartographer read-only during planning (it will not write), and let it file its map once plan mode ends.");
180
193
  if (existsSync(join(cwd, ".agent-os/rules")))
181
- lines.push("Rules: `.claude/rules/*` and other tool copies are generated. Edit `.agent-os/rules/`, then run `npx @sayansr26/agent-os sync`.");
194
+ lines.push(`Rules: \`${CURSOR ? ".cursor/rules/*" : ".claude/rules/*"}\` and other tool copies are generated. Edit \`.agent-os/rules/\`, then run \`npx @sayansr26/agent-os sync\`.`);
182
195
  return lines;
183
196
  }
184
197
 
@@ -186,7 +199,8 @@ try {
186
199
  const blocks = [];
187
200
  try { const s = snapshot(); if (s.length) blocks.push(s.join("\n")); } catch {}
188
201
  try { blocks.push(contract().join("\n")); } catch {}
189
- if (blocks.length) process.stdout.write(blocks.join("\n\n") + "\n");
202
+ if (CURSOR) process.stdout.write(JSON.stringify(blocks.length ? { additional_context: blocks.join("\n\n") } : {}));
203
+ else if (blocks.length) process.stdout.write(blocks.join("\n\n") + "\n");
190
204
  } catch {
191
205
  // Never let a resume hook break a session.
192
206
  }
package/src/cli.mjs CHANGED
@@ -10,6 +10,7 @@ import { setupState, describeState } from "../plugins/agent-os/skills/init/scrip
10
10
  import { applyScope, describe } from "./claude-setup.mjs";
11
11
  import { createInterface } from "node:readline/promises";
12
12
  import { compile, TARGETS } from "./targets.mjs";
13
+ import { enabled as cursorEnabled } from "./cursor.mjs";
13
14
 
14
15
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
15
16
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
@@ -42,6 +43,16 @@ Options
42
43
  --no-global init: leave ~/.claude alone
43
44
  `;
44
45
 
46
+ /** Will this init/sync compile for Claude Code? Mirrors how scaffold picks targets. */
47
+ function claudeTargeted(root, found) {
48
+ try {
49
+ const cfg = JSON.parse(readFileSync(join(root, DIR, "config.json"), "utf8"));
50
+ return (cfg.targets || []).includes("claude-code");
51
+ } catch {}
52
+ const present = found.filter((t) => t.present).map((t) => t.id);
53
+ return present.includes("claude-code") || !present.length;
54
+ }
55
+
45
56
  function scaffold(root, found) {
46
57
  const present = found.filter((t) => t.present).map((t) => t.id);
47
58
  mkdirSync(join(root, DIR, "rules"), { recursive: true });
@@ -95,6 +106,17 @@ export async function main(argv) {
95
106
 
96
107
  if (cmd === "help" || argv.includes("-h") || argv.includes("--help")) { console.log(USAGE); return; }
97
108
 
109
+ // A repository Rulegate owns: init would adopt Rulegate's generated CLAUDE.md and
110
+ // AGENTS.md as this tool's source, and every write after that fights `rulegate check`.
111
+ // That happened to Rulegate's own repository (its T112), so both writers stop here.
112
+ if ((cmd === "init" || cmd === "sync") && existsSync(join(root, ".rulegate"))) {
113
+ console.error(`\n This repository uses Rulegate (.rulegate/ exists), which replaces agent-os.`);
114
+ console.error(` agent-os ${cmd} would treat Rulegate's generated files as its source. Nothing was written.`);
115
+ console.error(`\n Use ${bold("npx rulegate sync")} here instead.\n`);
116
+ process.exitCode = 1;
117
+ return;
118
+ }
119
+
98
120
  const found = detect(root);
99
121
 
100
122
  if (cmd === "detect") {
@@ -141,18 +163,24 @@ export async function main(argv) {
141
163
  let adopted = new Set();
142
164
 
143
165
  let before = null;
166
+ // The SETUP state is Claude Code's: CLAUDE.md, .claude/settings.json, the
167
+ // plugin. In a project that does not target Claude Code every item reads
168
+ // MISSING and the verdict never leaves REPAIR, so it is not shown there.
169
+ const forClaude = claudeTargeted(root, found);
144
170
  if (cmd === "init") {
145
171
  console.log(`\n${bold("agent-os init")} ${root}\n`);
146
172
  // Decide fresh vs repair before touching anything, and say which, so a
147
173
  // second run on a set-up project reads as a repair rather than a re-init.
148
174
  before = setupState(root);
149
- for (const l of describeState(before)) console.log(` ${l}`);
150
- console.log({
151
- fresh: `\n ${dim("Fresh setup: creating everything.")}`,
152
- repair: `\n ${dim("Existing setup: fixing the items marked MISSING, leaving the rest as it is.")}`,
153
- healthy: `\n ${dim("Setup is complete: re-syncing and checking the plugin for updates.")}`,
154
- }[before.status]);
155
- console.log("");
175
+ if (forClaude) {
176
+ for (const l of describeState(before)) console.log(` ${l}`);
177
+ console.log({
178
+ fresh: `\n ${dim("Fresh setup: creating everything.")}`,
179
+ repair: `\n ${dim("Existing setup: fixing the items marked MISSING, leaving the rest as it is.")}`,
180
+ healthy: `\n ${dim("Setup is complete: re-syncing and checking the plugin for updates.")}`,
181
+ }[before.status]);
182
+ console.log("");
183
+ }
156
184
  console.log(summarise(found));
157
185
  const hadSource = existsSync(join(root, DIR));
158
186
  const { present, taken } = scaffold(root, found);
@@ -209,7 +237,7 @@ export async function main(argv) {
209
237
  const cur = readIfExists(root, f.path);
210
238
  if (cur === null) return true; // nothing there yet
211
239
  if (cur.includes(BANNER)) return true; // we wrote it
212
- if (TARGETS[f.target]?.merge) return true; // merged in place, nothing lost
240
+ if (f.merged) return true; // merged in place, nothing lost
213
241
  if (adopted.has(f.path)) return true; // init just took this as the source
214
242
  if (matches(root, f.path, f.content) === true) return true; // already identical
215
243
  return false;
@@ -290,7 +318,22 @@ Pick one:
290
318
  console.log(`\n ${dim("--no-plugin skips this; --no-auto-route / --no-auto-handoff skip one mod")}`);
291
319
  }
292
320
 
293
- if (cmd === "init" && !dry) {
321
+ if (cmd !== "check" && targets.includes("cursor") && cursorEnabled(src.config)) {
322
+ const n = (re) => files.filter((f) => re.test(f.path)).length;
323
+ console.log(`\n${bold("Cursor")} ${dim("agent-os layer, compiled from this package")}\n`);
324
+ console.log(` ${n(/^\.cursor\/agents\/agent-os-/)} subagents agent-os-feature-cartographer, -architect, -builder, -tester, -reviewer, …`);
325
+ console.log(` ${n(/^\.cursor\/skills\/agent-os-[^/]+\/SKILL\.md$/)} skills /agent-os-init, /agent-os-map, /agent-os-memory`);
326
+ console.log(` hooks session start, generated-file guard${src.config.cursor?.gitProtection === false ? "" : ", git write protection"} ${dim("(.cursor/hooks.json)")}`);
327
+ console.log(` agent memory .claude/agent-memory/agent-os-<agent>/ ${dim("— shared with Claude Code")}`);
328
+ if (cmd === "init") console.log(`\n ${dim("Reload the Cursor window, then run /agent-os-init in Agent chat.")}`);
329
+ }
330
+
331
+ if (cmd === "init" && !dry && !forClaude) {
332
+ const labels = targets.map((t) => TARGETS[t]?.label || t);
333
+ console.log(`\n${bold("Result")} ${labels.length ? `compiled for ${labels.join(", ")}` : "AGENTS.md only"}`);
334
+ }
335
+
336
+ if (cmd === "init" && !dry && forClaude) {
294
337
  const after = setupState(root);
295
338
  console.log(`\n${bold("Result")} ${before.status} → ${after.status}`);
296
339
  if (after.missing.length) {
package/src/cursor.mjs ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The agent-os layer for Cursor: the same agents, skills and hooks the Claude
3
+ * Code plugin ships, compiled into the places Cursor reads them.
4
+ *
5
+ * .cursor/agents/agent-os-<name>.md the seven agents, as subagents
6
+ * .cursor/skills/agent-os-{init,map,memory}/ the three skills
7
+ * .cursor/hooks/agent-os/ the session and pre-edit hooks, plus
8
+ * git write protection
9
+ * .cursor/hooks.json merged: user hooks are kept
10
+ * .cursor/rules/agent-os-agents.mdc when to use which agent, always on
11
+ *
12
+ * Everything is generated from this package, never from `.agent-os/`, so `sync`
13
+ * keeps it current the way `claude plugin update` does for Claude Code.
14
+ *
15
+ * Agent memory stays in `.claude/agent-memory/agent-os-<name>/`: the directory
16
+ * Claude Code's plugin agents use, so a project on both tools has one map.
17
+ */
18
+ import { readdirSync, readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { BANNER, parseFrontmatter } from "./source.mjs";
22
+ import { GIT_DENY } from "../plugins/agent-os/skills/init/scripts/settings.mjs";
23
+
24
+ const PKG = join(fileURLToPath(new URL(".", import.meta.url)), "..");
25
+ const PLUGIN = join(PKG, "plugins/agent-os");
26
+ const OWN = join(PKG, "cursor");
27
+ const HOOKS = ".cursor/hooks/agent-os";
28
+
29
+ const read = (p) => readFileSync(p, "utf8");
30
+ const md = (fields, body) => `---\n${fields}---\n\n<!-- ${BANNER} -->\n\n${body.trim()}\n`;
31
+ const js = (text) => text.startsWith("#!")
32
+ ? text.replace(/^(#!.*\n)/, `$1// ${BANNER}\n`)
33
+ : `// ${BANNER}\n${text}`;
34
+
35
+ /** `{ "cursor": { "agentOs": false } }` turns the whole layer off. */
36
+ export const enabled = (config) => config?.cursor?.agentOs !== false;
37
+ const gitGuard = (config) => config?.cursor?.gitProtection !== false;
38
+
39
+ /** Claude Code names and commands, rewritten to what this layer installs. */
40
+ export function translate(text) {
41
+ return text
42
+ .replace(/agent-os:([a-z][a-z-]*)/g, "agent-os-$1")
43
+ .replace(/node "\$\{CLAUDE_PLUGIN_ROOT\}\/skills\/\w+\/scripts\/(\w+)\.mjs"/g, "npx -y @sayansr26/agent-os $1");
44
+ }
45
+
46
+ /** A description is what the parent reads to pick a subagent, so it names Cursor's files. */
47
+ const describe = (d) => JSON.stringify(translate(String(d || ""))
48
+ .replace(/CLAUDE\.md/g, "AGENTS.md").replace(/\.claude\/rules\//g, ".cursor/rules/"));
49
+
50
+ const READ_AS = `> **Running in Cursor.** This text was written for Claude Code. Read it as:
51
+ > \`CLAUDE.md\` → \`AGENTS.md\` (edit \`.agent-os/AGENTS.md\`, then \`npx @sayansr26/agent-os sync\`);
52
+ > \`.claude/rules/\` → \`.cursor/rules/\` (edit \`.agent-os/rules/\`, then sync);
53
+ > an agent \`<name>\` → the \`agent-os-<name>\` subagent; \`/agent-os:<skill>\` → \`/agent-os-<skill>\`;
54
+ > "the Agent tool" or "dispatch" → delegate to that subagent.`;
55
+
56
+ function agents() {
57
+ const dir = join(PLUGIN, "agents");
58
+ return readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => {
59
+ const { fields, body } = parseFrontmatter(read(join(dir, f)));
60
+ const name = `agent-os-${fields.name}`;
61
+ // Claude Code creates the memory directory and preloads MEMORY.md for an
62
+ // agent with `memory: project`. Cursor does neither, so the agent is told.
63
+ const memory = `> **Your memory directory** is \`.claude/agent-memory/${name}/\` at the project root
64
+ > (shared with Claude Code's agent-os agents). Create it if it does not exist. Cursor does
65
+ > not load \`MEMORY.md\` for you — read it yourself before anything else.
66
+ >
67
+ ${READ_AS}`;
68
+ return {
69
+ path: `.cursor/agents/${name}.md`,
70
+ content: md(
71
+ `name: ${name}\ndescription: ${describe(fields.description)}\nmodel: inherit\n`,
72
+ `${memory}\n\n${translate(body)}`),
73
+ };
74
+ });
75
+ }
76
+
77
+ function skills() {
78
+ const out = [];
79
+ const refs = join(PLUGIN, "skills/init/references");
80
+ // init is rewritten for Cursor; its references are shared, read through the table.
81
+ {
82
+ const { fields, body } = parseFrontmatter(read(join(OWN, "skills/agent-os-init/SKILL.md")));
83
+ out.push({ path: ".cursor/skills/agent-os-init/SKILL.md",
84
+ content: md(`name: ${fields.name}\ndescription: ${JSON.stringify(fields.description)}\n`, body) });
85
+ for (const f of readdirSync(refs).filter((f) => f.endsWith(".md")).sort())
86
+ out.push({ path: `.cursor/skills/agent-os-init/references/${f}`,
87
+ content: `<!-- ${BANNER} -->\n\n${READ_AS}\n\n${translate(read(join(refs, f))).trim()}\n` });
88
+ }
89
+ // map and memory carry over as they are, renamed. Their scripts run through
90
+ // the CLI, which bundles the same files, so no script is copied.
91
+ for (const s of ["map", "memory"]) {
92
+ const { fields, body } = parseFrontmatter(read(join(PLUGIN, "skills", s, "SKILL.md")));
93
+ out.push({ path: `.cursor/skills/agent-os-${s}/SKILL.md`,
94
+ content: md(`name: agent-os-${s}\ndescription: ${describe(fields.description)}\n`, `${READ_AS}\n\n${translate(body)}`) });
95
+ }
96
+ return out;
97
+ }
98
+
99
+ function hookScripts(config) {
100
+ const out = [
101
+ { path: `${HOOKS}/hooks/session-resume.mjs`, content: js(read(join(PLUGIN, "hooks/session-resume.mjs"))) },
102
+ { path: `${HOOKS}/hooks/pre-edit.mjs`, content: js(read(join(PLUGIN, "hooks/pre-edit.mjs"))) },
103
+ { path: `${HOOKS}/lib/features.mjs`, content: js(read(join(PLUGIN, "lib/features.mjs"))) },
104
+ ];
105
+ if (gitGuard(config)) out.push(
106
+ { path: `${HOOKS}/hooks/git-guard.mjs`, content: js(read(join(OWN, "hooks/git-guard.mjs"))) },
107
+ { path: `${HOOKS}/lib/git-deny.mjs`,
108
+ content: js(`/** The same rules Claude Code gets as permissions.deny (settings.mjs). */\nexport const GIT_DENY = ${JSON.stringify(GIT_DENY, null, 2)};\n`) },
109
+ );
110
+ return out;
111
+ }
112
+
113
+ /** The agent contract, always on — the Cursor half of CLAUDE.md's Step 4b section. */
114
+ const contract = () => ({
115
+ path: ".cursor/rules/agent-os-agents.mdc",
116
+ content: md("description: When to use each agent-os subagent\nalwaysApply: true\n", `## Agents in this project (agent-os)
117
+
118
+ Delegate to these subagents without being asked:
119
+
120
+ - Before changing an existing feature: ask \`agent-os-feature-cartographer\` how
121
+ it is built. In plan mode ask it read-only, and have it file the map as the
122
+ first step after plan mode ends.
123
+ - New subsystem, cross-module change, shared data model: \`agent-os-architect\` first.
124
+ - Writing the change: \`agent-os-builder\`, given the map and the files to change.
125
+ - Done means: \`agent-os-tester\` verified it and \`agent-os-reviewer\` found no
126
+ rule violations.
127
+ - Once verified: \`agent-os-documenter\` records it.
128
+ - Work spanning several of these, or that cannot be stated in one sentence:
129
+ \`agent-os-orchestrator\`.
130
+
131
+ \`/agent-os-map\` builds the architecture map, \`/agent-os-memory\` inspects agent
132
+ memory, \`/agent-os-init\` sets up or repairs all of this.
133
+
134
+ Generated files carry a banner. Edit \`.agent-os/\`, then run
135
+ \`npx @sayansr26/agent-os sync\`; never edit the output.`),
136
+ });
137
+
138
+ /** Every file of the layer except hooks.json. */
139
+ export function cursorFiles(src) {
140
+ if (!enabled(src.config)) return [];
141
+ return [...agents(), ...skills(), ...hookScripts(src.config), contract()];
142
+ }
143
+
144
+ const OURS = (h) => String(h?.command || "").includes(`${HOOKS}/`);
145
+
146
+ /**
147
+ * Merge the agent-os hooks into `.cursor/hooks.json`. Hooks the user wrote are
148
+ * kept; earlier agent-os entries are replaced, so a removed hook goes away.
149
+ */
150
+ export function mergeHooks(src, existing) {
151
+ if (!existing && !enabled(src.config)) return [];
152
+ const cfg = existing ? JSON.parse(existing) : { version: 1, hooks: {} };
153
+ cfg.version ||= 1;
154
+ cfg.hooks ||= {};
155
+ for (const ev of Object.keys(cfg.hooks)) {
156
+ cfg.hooks[ev] = (cfg.hooks[ev] || []).filter((h) => !OURS(h));
157
+ if (!cfg.hooks[ev].length) delete cfg.hooks[ev];
158
+ }
159
+ if (enabled(src.config)) {
160
+ const add = (ev, h) => (cfg.hooks[ev] ||= []).push(h);
161
+ add("sessionStart", { command: `node ${HOOKS}/hooks/session-resume.mjs --cursor` });
162
+ add("preToolUse", { command: `node ${HOOKS}/hooks/pre-edit.mjs --cursor`, matcher: "Write" });
163
+ if (gitGuard(src.config))
164
+ add("beforeShellExecution", { command: `node ${HOOKS}/hooks/git-guard.mjs`, matcher: "\\bgit\\b" });
165
+ }
166
+ return [{ path: ".cursor/hooks.json", content: JSON.stringify(cfg, null, 2) + "\n" }];
167
+ }
package/src/selftest.mjs CHANGED
@@ -57,6 +57,39 @@ try {
57
57
  ok(read(".claude/skills/demo/SKILL.md") !== null, "skills -> .claude/skills");
58
58
  ok(read(".cline/skills/demo/SKILL.md") !== null, "skills -> .cline/skills");
59
59
 
60
+ console.log("\n Cursor gets the agent-os layer");
61
+ {
62
+ const agents = ["architect", "builder", "documenter", "feature-cartographer", "orchestrator", "reviewer", "tester"];
63
+ ok(agents.every((a) => read(`.cursor/agents/agent-os-${a}.md`)?.startsWith(`---\nname: agent-os-${a}\n`)), "7 subagents in .cursor/agents/, Cursor-legal names");
64
+ const carto = read(".cursor/agents/agent-os-feature-cartographer.md");
65
+ ok(carto.includes(".claude/agent-memory/agent-os-feature-cartographer/"), "subagent memory shared with Claude Code's directory");
66
+ ok(!/agent-os:[a-z]/.test(carto) && !/^memory:|^tools:/m.test(carto), "no Claude-only names or frontmatter left");
67
+ ok(["init", "map", "memory"].every((s) => read(`.cursor/skills/agent-os-${s}/SKILL.md`)?.includes(`name: agent-os-${s}\n`)), "skills /agent-os-init, -map, -memory");
68
+ ok(!read(".cursor/skills/agent-os-memory/SKILL.md").includes("CLAUDE_PLUGIN_ROOT"), "skill scripts run through the CLI");
69
+ ok(read(".cursor/rules/agent-os-agents.mdc")?.includes("alwaysApply: true"), "agent contract is an always-on rule");
70
+ const hooks = JSON.parse(read(".cursor/hooks.json"));
71
+ ok(hooks.version === 1 && hooks.hooks.sessionStart && hooks.hooks.preToolUse && hooks.hooks.beforeShellExecution, "hooks.json: sessionStart, preToolUse, beforeShellExecution");
72
+
73
+ const hook = (script, args, input) => spawnSync(process.execPath, [join(root, ".cursor/hooks/agent-os/hooks", script), ...args],
74
+ { cwd: root, encoding: "utf8", input, env: { ...process.env, CURSOR_PROJECT_DIR: root } }).stdout;
75
+ const start = JSON.parse(hook("session-resume.mjs", ["--cursor"], ""));
76
+ ok(start.additional_context?.includes("`agent-os-feature-cartographer`"), "session start returns additional_context with Cursor names");
77
+ const pre = (file) => JSON.parse(hook("pre-edit.mjs", ["--cursor"], JSON.stringify({ tool_input: { file_path: join(root, file) } })));
78
+ ok(pre(".cursor/rules/api.mdc").permission === "deny", "pre-edit denies an edit to a generated rule");
79
+ ok(pre(".cursor/agents/agent-os-builder.md").permission === "deny", "pre-edit denies an edit to a generated subagent");
80
+ ok(pre("src/new.ts").permission === "allow", "pre-edit answers allow, never empty, for everything else");
81
+ const git = (command) => JSON.parse(hook("git-guard.mjs", [], JSON.stringify({ command }))).permission;
82
+ ok(git("git status") === "allow" && git("git log --oneline") === "allow", "read-only git allowed");
83
+ ok(["git push", "git commit -m x", "cd a && git reset --hard", "git -C x add .", "A=1 git rebase main"].every((c) => git(c) === "deny"), "git writes denied, chained and flag forms too");
84
+
85
+ writeFileSync(join(root, ".cursor/hooks.json"), JSON.stringify({ version: 1, hooks: { afterFileEdit: [{ command: "./fmt.sh" }] } }));
86
+ cli(["sync"], root);
87
+ const merged = JSON.parse(read(".cursor/hooks.json")).hooks;
88
+ ok(merged.afterFileEdit?.[0]?.command === "./fmt.sh" && merged.sessionStart?.length === 1, "hooks.json merged: user hook kept, ours added once");
89
+ cli(["sync"], root);
90
+ ok(JSON.parse(read(".cursor/hooks.json")).hooks.preToolUse.length === 1, "re-sync does not duplicate hooks");
91
+ }
92
+
60
93
  console.log("\n drift");
61
94
  ok(cli(["check"], root).status === 0, "check passes when in sync");
62
95
  // Realistic drift: someone edits the body of a generated file and leaves the
@@ -229,7 +262,34 @@ try {
229
262
  const oc = JSON.parse(read("opencode.json"));
230
263
  ok(oc.model === "anthropic/x", "existing keys preserved");
231
264
  ok(oc.instructions.includes("KEEP.md"), "existing instructions preserved");
232
- } finally { rmSync(root, { recursive: true, force: true }); rmSync(HOME, { recursive: true, force: true }); }
265
+ } finally { rmSync(root, { recursive: true, force: true }); }
266
+
267
+ console.log("\n a Cursor-only init does not report Claude Code items");
268
+ const cu = mkdtempSync(join(tmpdir(), "agent-os-cursor-"));
269
+ try {
270
+ mkdirSync(join(cu, ".agent-os"), { recursive: true });
271
+ writeFileSync(join(cu, ".agent-os/config.json"), JSON.stringify({ targets: ["cursor"] }));
272
+ const r = cli(["init", "--no-global"], cu);
273
+ ok(r.status === 0, "init exits 0");
274
+ ok(!r.stdout.includes("SETUP") && !r.stdout.includes("still to do") && !r.stdout.includes("CLAUDE.md"), "no Claude Code SETUP or to-do list");
275
+ ok(/Result.*compiled for Cursor/.test(r.stdout) && r.stdout.includes("/agent-os-init"), "result names Cursor and the next step");
276
+ ok(!existsSync(join(cu, ".claude/settings.json")), "nothing written for Claude Code");
277
+ writeFileSync(join(cu, ".agent-os/config.json"), JSON.stringify({ targets: ["cursor"], cursor: { gitProtection: false } }));
278
+ cli(["sync"], cu);
279
+ ok(!JSON.parse(readFileSync(join(cu, ".cursor/hooks.json"), "utf8")).hooks.beforeShellExecution, "gitProtection: false removes the git hook");
280
+ } finally { rmSync(cu, { recursive: true, force: true }); }
281
+
282
+ console.log("\n a Rulegate repository is left alone");
283
+ const rg = mkdtempSync(join(tmpdir(), "agent-os-rulegate-"));
284
+ try {
285
+ mkdirSync(join(rg, ".rulegate/rules"), { recursive: true });
286
+ writeFileSync(join(rg, "CLAUDE.md"), "<!-- Generated by Rulegate -->\n# Project\n");
287
+ for (const cmd of ["init", "sync"]) {
288
+ const r = cli([cmd, "--no-plugin", "--no-global"], rg);
289
+ ok(r.status === 1 && /uses Rulegate/.test(r.stderr), `${cmd} refuses with .rulegate/ present`);
290
+ }
291
+ ok(!existsSync(join(rg, ".agent-os")) && !existsSync(join(rg, "AGENTS.md")), "nothing written");
292
+ } finally { rmSync(rg, { recursive: true, force: true }); rmSync(HOME, { recursive: true, force: true }); }
233
293
 
234
294
  console.log(`\n${fail ? `FAILED (${fail})` : "PASSED"}\n`);
235
295
  process.exit(fail ? 1 : 0);
package/src/targets.mjs CHANGED
@@ -7,6 +7,7 @@
7
7
  * existing JSON config receive the current contents and return the merged file.
8
8
  */
9
9
  import { BANNER } from "./source.mjs";
10
+ import { cursorFiles, mergeHooks } from "./cursor.mjs";
10
11
 
11
12
  const hdr = (comment) => `${comment} ${BANNER}\n`;
12
13
  const mdHeader = `<!-- ${BANNER} -->\n\n`;
@@ -54,13 +55,19 @@ export const TARGETS = {
54
55
  // ---- translate ----------------------------------------------------------
55
56
  cursor: {
56
57
  label: "Cursor",
57
- // .mdc, and a rule without frontmatter is ignored entirely.
58
- files: (src) => src.rules.map((r) => ({
59
- path: `.cursor/rules/${r.name}.mdc`,
60
- content: `---\ndescription: ${r.description || r.name}\n` +
61
- (r.always ? "alwaysApply: true\n" : `globs: ${r.paths.join(",")}\nalwaysApply: false\n`) +
62
- `---\n\n${mdHeader}${r.body}\n`,
63
- })),
58
+ // .mdc, and a rule without frontmatter is ignored entirely. Then the
59
+ // agent-os layer — subagents, skills, hooks — which src/cursor.mjs builds.
60
+ files: (src) => [
61
+ ...src.rules.map((r) => ({
62
+ path: `.cursor/rules/${r.name}.mdc`,
63
+ content: `---\ndescription: ${r.description || r.name}\n` +
64
+ (r.always ? "alwaysApply: true\n" : `globs: ${r.paths.join(",")}\nalwaysApply: false\n`) +
65
+ `---\n\n${mdHeader}${r.body}\n`,
66
+ })),
67
+ ...cursorFiles(src),
68
+ ],
69
+ merge: mergeHooks,
70
+ mergeFrom: ".cursor/hooks.json",
64
71
  },
65
72
 
66
73
  windsurf: {
@@ -179,10 +186,10 @@ export function compile(src, targetIds, readExisting) {
179
186
  for (const id of targetIds) {
180
187
  const t = TARGETS[id];
181
188
  if (!t) continue;
182
- const files = t.merge
183
- ? t.merge(src, readExisting(t.mergeFrom))
184
- : t.files(src);
185
- out.push(...files.map((f) => ({ ...f, target: id })));
189
+ // A target writes files, merges into one existing config, or both. Merged
190
+ // files are marked: they are rewritten in place with nothing of the user's lost.
191
+ if (t.files) out.push(...t.files(src).map((f) => ({ ...f, target: id })));
192
+ if (t.merge) out.push(...t.merge(src, readExisting(t.mergeFrom)).map((f) => ({ ...f, target: id, merged: true })));
186
193
  }
187
194
  return out;
188
195
  }