@sayansr26/agent-os 0.5.1 → 0.5.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.4.0"
9
+ "version": "0.4.1"
10
10
  },
11
11
  "plugins": [
12
12
  {
package/CHANGELOG.md CHANGED
@@ -5,6 +5,57 @@ 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.5.3] — 2026-09-15
9
+
10
+ ### Added
11
+ - **`init` finishes the Claude Code project setup.** Two things that are
12
+ neither rules nor plugin, both written at project scope only:
13
+ `.claude/settings.json` gets `env.CLAUDE_CODE_ENABLE_TODO_TOOLS = "1"`,
14
+ merged into the existing keys rather than replacing the file; and `CLAUDE.md`
15
+ gets the task-tracking rule, inserted under `## Operator preferences` when
16
+ the project keeps that heading. Both are idempotent, an existing truthy value
17
+ for the flag is left as the user set it, and `CLAUDE.md` is never created — a
18
+ project's always-loaded context should not be invented by a CLI. Nothing
19
+ writes to `~/.claude/`; a project setup tool has no business editing the
20
+ machine's global config.
21
+ - **The validator checks release hygiene.** `package.json`'s version must have
22
+ a matching `## [x.y.z]` changelog section, and it must be the newest one.
23
+ 0.5.2 shipped with no entry of its own; a deterministic check belongs in a
24
+ script rather than in a habit.
25
+
26
+ ### Changed
27
+ - Plugin 0.4.1: the audit no longer misreports a generated rules directory as a
28
+ legacy store to fold into `CLAUDE.md` and delete.
29
+
30
+ ## [0.5.2] — 2026-09-15
31
+
32
+ ### Added
33
+ - **`init` installs the Claude Code plugin.** When Claude Code is detected it
34
+ runs `claude plugin marketplace add` and `claude plugin install --yes` at
35
+ project scope, so the plugin travels with the repository rather than living
36
+ on one machine. If the `claude` CLI is not on PATH it prints the two slash
37
+ commands instead. `--no-plugin` skips it, and the rules are written either
38
+ way — the compiler never depends on the plugin step succeeding.
39
+
40
+ ## [0.5.1] — 2026-09-15
41
+
42
+ Found by running 0.5.0 against a real repository that already had six
43
+ path-scoped rules and a hand-written `AGENTS.md`. It overwrote the `AGENTS.md`.
44
+
45
+ ### Fixed
46
+ - **`init` scaffolded over projects that already had rules.** It wrote a
47
+ placeholder `AGENTS.md` and an `example.md` rule regardless of what was
48
+ there, and the next `sync` compiled the placeholder on top of the project's
49
+ real `AGENTS.md`. `init` now adopts what it finds — the existing `AGENTS.md`
50
+ and the first rules directory it recognises, `.cursor/rules/*.mdc` converted
51
+ back to `paths:` — and seeds `example.md` only when there was nothing to
52
+ adopt. A tool for stopping rule drift must not cause it.
53
+ - **`sync` now refuses to overwrite a file it did not generate.** Generated
54
+ files carry a banner; anything else at a generated path is the user's own
55
+ work. `sync` names those files, leaves them alone and exits non-zero, with
56
+ `--force` as the explicit opt-out. `AGENTS.md` carries the banner too — it
57
+ previously did not, which is why nothing could tell it apart.
58
+
8
59
  ## [0.5.0] — 2026-09-15
9
60
 
10
61
  `agent-os` becomes a cross-tool CLI. The Claude Code plugin is now one target
@@ -40,18 +91,6 @@ among several rather than the whole product.
40
91
  - Documentation is generic throughout; examples are invented.
41
92
 
42
93
  ### Fixed
43
- - **`init` scaffolded over projects that already had rules.** It wrote a
44
- placeholder `AGENTS.md` and an `example.md` rule regardless of what was
45
- there, and the next `sync` compiled the placeholder on top of the project's
46
- real `AGENTS.md`. `init` now adopts what it finds — the existing `AGENTS.md`
47
- and the first rules directory it recognises, `.cursor/rules/*.mdc` converted
48
- back to `paths:` — and seeds `example.md` only when there was nothing to
49
- adopt. A tool for stopping rule drift must not cause it.
50
- - **`sync` now refuses to overwrite a file it did not generate.** Generated
51
- files carry a banner; anything else at a generated path is the user's own
52
- work. `sync` names those files, leaves them alone and exits non-zero, with
53
- `--force` as the explicit opt-out. `AGENTS.md` carries the banner too — it
54
- previously did not, which is why nothing could tell it apart.
55
94
  - `check` reported every skill file as drifted immediately after a `sync`. Skill
56
95
  files are read as buffers so a skill can ship a binary asset, rule files are
57
96
  generated as strings, and the two were compared with `!==`. Comparison is now
package/README.md CHANGED
@@ -76,6 +76,16 @@ Every command below is `npx @sayansr26/agent-os <command>`. Install it once —
76
76
 
77
77
  `--root <dir>` to target another directory, `--dry-run` to preview.
78
78
 
79
+ **`init` sets up Claude Code completely.** Beyond the rules it installs the
80
+ plugin — agents, skills, per-agent memory, session hook — with
81
+ `claude plugin marketplace add` and `claude plugin install --yes`, enables the
82
+ task tools in `.claude/settings.json`, and adds the task-tracking rule to
83
+ `CLAUDE.md` if it is not already there. All of it at **project scope**, so the
84
+ setup travels with the repo and nothing writes to `~/.claude/`. `--no-plugin`
85
+ skips the plugin step. Every other detected tool gets its rules in its own
86
+ schema; the plugin layer is Claude Code only because no other tool has anywhere
87
+ to put it.
88
+
79
89
  **On an existing project, `init` adopts rather than scaffolds.** It takes your
80
90
  current `AGENTS.md` and the first rules directory it recognises as the source,
81
91
  so the first `sync` regenerates what you already had. **`sync` never overwrites
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayansr26/agent-os",
3
- "version": "0.5.1",
3
+ "version": "0.5.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",
@@ -1,7 +1,7 @@
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.4.0",
4
+ "version": "0.4.1",
5
5
  "author": {
6
6
  "name": "Sayan Choudhury",
7
7
  "email": "sayan.choudhury.in@gmail.com"
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The two Claude Code settings that are not rules and not a plugin.
3
+ *
4
+ * Both are written at PROJECT scope only — `.claude/settings.json` and the
5
+ * project's `CLAUDE.md`. Nothing here touches `~/.claude/`: a project setup
6
+ * tool that edits the machine's global config is overreach, and the point is
7
+ * that the setup travels with the repository.
8
+ */
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { write } from "./source.mjs";
12
+
13
+ export const TODO_ENV = "CLAUDE_CODE_ENABLE_TODO_TOOLS";
14
+
15
+ export const TASK_RULE = `- **Always track work with the task tool (TaskCreate / TaskUpdate).** Any request
16
+ with more than one step gets a task list before work starts: one task per
17
+ deliverable, marked \`in_progress\` when started and \`completed\` only when
18
+ verified. Keep it current as scope changes, including work delegated to
19
+ subagents, so I can see what is done, running and left at any moment.`;
20
+
21
+ /**
22
+ * Turn on the todo tools in project settings, preserving every other key.
23
+ * Returns "added" | "present" | "conflict" | "invalid".
24
+ */
25
+ export function ensureTodoEnv(root, { dry = false } = {}) {
26
+ const rel = ".claude/settings.json";
27
+ const p = join(root, rel);
28
+ let settings = {};
29
+ if (existsSync(p)) {
30
+ try { settings = JSON.parse(readFileSync(p, "utf8")); }
31
+ catch { return { status: "invalid", rel }; }
32
+ }
33
+ const env = settings.env || {};
34
+ const current = env[TODO_ENV];
35
+
36
+ // "true" and "1" are both truthy to Claude Code; an existing truthy value is
37
+ // the user's choice and is left alone rather than normalised.
38
+ if (current !== undefined) {
39
+ const on = current === "1" || current === "true" || current === true;
40
+ return { status: on ? "present" : "conflict", rel, current };
41
+ }
42
+
43
+ settings.env = { ...env, [TODO_ENV]: "1" };
44
+ if (!dry) write(root, rel, JSON.stringify(settings, null, 2) + "\n");
45
+ return { status: "added", rel };
46
+ }
47
+
48
+ /**
49
+ * Put the task-tracking rule in CLAUDE.md if it is not already there.
50
+ * Never creates CLAUDE.md — that file is the project's own always-loaded
51
+ * context, and inventing one from a CLI is how a project ends up with a stub
52
+ * nobody owns. Returns "added" | "present" | "no-file".
53
+ */
54
+ export function ensureTaskRule(root, { dry = false } = {}) {
55
+ const rel = "CLAUDE.md";
56
+ const p = join(root, rel);
57
+ if (!existsSync(p)) return { status: "no-file", rel };
58
+
59
+ const text = readFileSync(p, "utf8");
60
+ if (/TaskCreate/.test(text)) return { status: "present", rel };
61
+
62
+ // Prefer the operator-preferences section if the project keeps one, so the
63
+ // rule lands with the other instructions about how to work rather than
64
+ // orphaned at the bottom of the file.
65
+ const heading = text.match(/^##\s+Operator preferences\s*$/m);
66
+ let next;
67
+ if (heading) {
68
+ const start = heading.index + heading[0].length;
69
+ const rest = text.slice(start);
70
+ const nextHeading = rest.search(/^##\s+/m);
71
+ const end = nextHeading === -1 ? text.length : start + nextHeading;
72
+ const before = text.slice(0, end).replace(/\s*$/, "");
73
+ next = `${before}\n\n${TASK_RULE}\n\n${text.slice(end)}`;
74
+ } else {
75
+ next = `${text.replace(/\s*$/, "")}\n\n## Working agreement\n\n${TASK_RULE}\n`;
76
+ }
77
+ next = next.replace(/\n{4,}/g, "\n\n\n");
78
+
79
+ if (!dry) write(root, rel, next);
80
+ return { status: "added", rel, section: heading ? "Operator preferences" : "Working agreement" };
81
+ }
package/src/cli.mjs CHANGED
@@ -5,6 +5,8 @@ import { join } from "node:path";
5
5
  import { detect, summarise, TOOLS } from "./detect.mjs";
6
6
  import { load, write, readIfExists, matches, BANNER, DIR } from "./source.mjs";
7
7
  import { adopt } from "./adopt.mjs";
8
+ import { installPlugin, MARKETPLACE, MARKETPLACE_NAME, PLUGIN } from "./plugin.mjs";
9
+ import { ensureTodoEnv, ensureTaskRule, TODO_ENV } from "./claude-setup.mjs";
8
10
  import { compile, TARGETS } from "./targets.mjs";
9
11
 
10
12
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
@@ -27,6 +29,7 @@ Options
27
29
  --root <dir> project directory (default: cwd)
28
30
  --dry-run print what would change, write nothing
29
31
  --force overwrite files agent-os did not generate (it refuses by default)
32
+ --no-plugin skip installing the Claude Code plugin during init
30
33
  `;
31
34
 
32
35
  function scaffold(root, found) {
@@ -203,7 +206,46 @@ Pick one:
203
206
  return;
204
207
  }
205
208
 
206
- console.log(`\n${dim("Generated files carry a banner. Edit .agent-os/ and re-run sync; never edit them directly.")}\n`);
209
+ console.log(`\n${dim("Generated files carry a banner. Edit .agent-os/ and re-run sync; never edit them directly.")}`);
210
+
211
+ // The rules are done and safe at this point. The Claude Code plugin is the
212
+ // other half — agents, skills, per-agent memory, the session hook — and it
213
+ // installs from the same repo. Only on `init`, never on `sync`.
214
+ if (cmd === "init" && targets.includes("claude-code")) {
215
+ console.log(`\n${bold("Claude Code setup")}\n`);
216
+
217
+ const e = ensureTodoEnv(root, { dry });
218
+ const eMsg = {
219
+ added: ` ${e.rel} ${dim(`env.${TODO_ENV} = "1"`)}`,
220
+ present: ` ${e.rel} ${dim(`env.${TODO_ENV} already set`)}`,
221
+ conflict: ` ${e.rel} ${dim(`env.${TODO_ENV} is "${e.current}" — left as you set it`)}`,
222
+ invalid: ` ${e.rel} ${dim("is not valid JSON — left alone, fix it and re-run")}`,
223
+ }[e.status];
224
+ console.log(eMsg);
225
+
226
+ const t = ensureTaskRule(root, { dry });
227
+ const tMsg = {
228
+ added: ` ${t.rel} ${dim(`task-tracking rule added under ${t.section}`)}`,
229
+ present: ` ${t.rel} ${dim("task-tracking rule already there")}`,
230
+ "no-file": ` ${t.rel} ${dim("absent — run /agent-os:init inside Claude Code to build it")}`,
231
+ }[t.status];
232
+ console.log(tMsg);
233
+ }
234
+
235
+ if (cmd === "init" && targets.includes("claude-code") && !argv.includes("--no-plugin")) {
236
+ console.log(`\n${bold("Claude Code plugin")} ${dim(`${PLUGIN}@${MARKETPLACE_NAME}`)}\n`);
237
+ const r = installPlugin({ dry });
238
+ for (const line of r.done) console.log(` ${dim(line)}`);
239
+ if (r.ok && r.reason === "installed") {
240
+ console.log(`\n installed at project scope ${dim("— .claude/settings.json, so it travels with the repo")}`);
241
+ console.log(` ${dim("restart Claude Code, or /reload-plugins, then run /agent-os:init")}`);
242
+ } else if (!r.ok) {
243
+ console.log("");
244
+ for (const line of r.hint) console.log(` ${line}`);
245
+ }
246
+ console.log(`\n ${dim("--no-plugin skips this")}`);
247
+ }
248
+ console.log("");
207
249
  return;
208
250
  }
209
251
 
package/src/plugin.mjs ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Install the Claude Code half of agent-os during `init`.
3
+ *
4
+ * The CLI compiles rules for every tool. Claude Code additionally gets the
5
+ * agents, skills, per-agent memory and session hook, which ship as a plugin —
6
+ * so `init` should install it rather than printing two slash commands and
7
+ * hoping. `claude plugin ...` with `--yes` is the documented automation path.
8
+ *
9
+ * Everything here is best effort and reversible: if the `claude` CLI is not on
10
+ * PATH, or a command fails, `init` reports it and carries on. The rules are the
11
+ * part that must not depend on this.
12
+ */
13
+ import { spawnSync } from "node:child_process";
14
+
15
+ export const MARKETPLACE = "sayansr26/agent-os";
16
+ export const MARKETPLACE_NAME = "sayan-plugins";
17
+ export const PLUGIN = "agent-os";
18
+
19
+ const run = (args) =>
20
+ spawnSync("claude", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
21
+
22
+ /** Is the Claude Code CLI usable from here? */
23
+ export function claudeAvailable() {
24
+ const r = spawnSync("claude", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
25
+ return !r.error && r.status === 0;
26
+ }
27
+
28
+ /**
29
+ * Add the marketplace and install the plugin at project scope, so the setup
30
+ * travels with the repository instead of living on one machine.
31
+ */
32
+ export function installPlugin({ dry = false, scope = "project" } = {}) {
33
+ const steps = [
34
+ ["marketplace", ["plugin", "marketplace", "add", MARKETPLACE, "--scope", scope]],
35
+ ["plugin", ["plugin", "install", `${PLUGIN}@${MARKETPLACE_NAME}`, "--scope", scope, "--yes"]],
36
+ ];
37
+
38
+ if (!claudeAvailable())
39
+ return {
40
+ ok: false,
41
+ reason: "no-cli",
42
+ done: [],
43
+ hint: [
44
+ "The `claude` CLI is not on PATH, so the plugin was not installed.",
45
+ "Inside Claude Code, run:",
46
+ ` /plugin marketplace add ${MARKETPLACE}`,
47
+ ` /plugin install ${PLUGIN}@${MARKETPLACE_NAME}`,
48
+ ],
49
+ };
50
+
51
+ if (dry)
52
+ return { ok: true, reason: "dry-run", done: steps.map(([, a]) => `claude ${a.join(" ")}`), hint: [] };
53
+
54
+ const done = [];
55
+ for (const [what, args] of steps) {
56
+ const r = run(args);
57
+ if (r.status !== 0) {
58
+ const err = (r.stderr || r.stdout || "").trim().split("\n").slice(-3).join("\n");
59
+ return {
60
+ ok: false,
61
+ reason: what,
62
+ done,
63
+ hint: [
64
+ `\`claude ${args.join(" ")}\` failed:`,
65
+ ...err.split("\n").map((l) => ` ${l}`),
66
+ "The rules above were still written. Install the plugin by hand inside",
67
+ `Claude Code: /plugin install ${PLUGIN}@${MARKETPLACE_NAME}`,
68
+ ],
69
+ };
70
+ }
71
+ done.push(`claude ${args.join(" ")}`);
72
+ }
73
+ return { ok: true, reason: "installed", done, hint: [] };
74
+ }
package/src/selftest.mjs CHANGED
@@ -77,7 +77,7 @@ try {
77
77
  writeFileSync(join(r2, ".claude/rules/theming.md"),
78
78
  '---\ndescription: Theming\npaths:\n - "src/**/*.tsx"\n---\n\nUse the token set.\n');
79
79
 
80
- const i = cli(["init"], r2);
80
+ const i = cli(["init", "--no-plugin"], r2);
81
81
  ok(i.status === 0, "init exits 0 on a project that already has rules");
82
82
  ok(readFileSync(join(r2, "AGENTS.md"), "utf8") === realAgents ||
83
83
  readFileSync(join(r2, ".agent-os/AGENTS.md"), "utf8") === realAgents,
@@ -85,6 +85,16 @@ try {
85
85
  ok(existsSync(join(r2, ".agent-os/rules/theming.md")), "existing .claude/rules/ are adopted");
86
86
  ok(!existsSync(join(r2, ".agent-os/rules/example.md")), "no example.md when real rules were adopted");
87
87
  ok(!existsSync(join(r2, ".claude/rules/example.md")), "no example.md compiled into the project");
88
+ ok(!i.stdout.includes("Claude Code plugin"), "--no-plugin skips the plugin install");
89
+
90
+ // init offers the plugin when not told otherwise; --dry-run proves the
91
+ // commands without running them against the machine's real config.
92
+ const r4 = mkdtempSync(join(tmpdir(), "agent-os-plug-"));
93
+ const pi = cli(["init", "--dry-run"], r4);
94
+ ok(pi.stdout.includes("Claude Code plugin"), "init sets up the Claude Code plugin by default");
95
+ ok(/marketplace add sayansr26\/agent-os|plugin marketplace add|not on PATH/.test(pi.stdout),
96
+ "init names the marketplace step or says why it could not run it");
97
+ rmSync(r4, { recursive: true, force: true });
88
98
 
89
99
  // A hand-written file at a generated path must survive a sync.
90
100
  const r3 = mkdtempSync(join(tmpdir(), "agent-os-guard-"));
@@ -104,6 +114,47 @@ try {
104
114
  rmSync(r3, { recursive: true, force: true });
105
115
  }
106
116
 
117
+ console.log("\n Claude Code project setup");
118
+ {
119
+ const r5 = mkdtempSync(join(tmpdir(), "agent-os-cc-"));
120
+ mkdirSync(join(r5, ".claude"), { recursive: true });
121
+ // An existing settings.json with unrelated keys must survive untouched.
122
+ writeFileSync(join(r5, ".claude/settings.json"),
123
+ JSON.stringify({ permissions: { deny: ["Bash(git push *)"] }, env: { FOO: "bar" } }, null, 2));
124
+ writeFileSync(join(r5, "CLAUDE.md"),
125
+ "# CLAUDE.md\n\nStack notes.\n\n## Operator preferences\n\n- Never run git unasked.\n\n## Verification\n\nRun make test.\n");
126
+
127
+ cli(["init", "--no-plugin"], r5);
128
+ const st = JSON.parse(readFileSync(join(r5, ".claude/settings.json"), "utf8"));
129
+ ok(st.env.CLAUDE_CODE_ENABLE_TODO_TOOLS === "1", "todo tools enabled in project settings");
130
+ ok(st.env.FOO === "bar", "existing env keys preserved");
131
+ ok(st.permissions?.deny?.[0] === "Bash(git push *)", "existing permissions preserved");
132
+
133
+ const cm = readFileSync(join(r5, "CLAUDE.md"), "utf8");
134
+ ok(cm.includes("TaskCreate / TaskUpdate"), "task-tracking rule added to CLAUDE.md");
135
+ ok(cm.indexOf("TaskCreate") > cm.indexOf("## Operator preferences") &&
136
+ cm.indexOf("TaskCreate") < cm.indexOf("## Verification"),
137
+ "rule lands inside Operator preferences, not at the end");
138
+ ok(cm.includes("Never run git unasked."), "existing CLAUDE.md content preserved");
139
+
140
+ // Idempotent: a second init must not duplicate either.
141
+ cli(["init", "--no-plugin"], r5);
142
+ const cm2 = readFileSync(join(r5, "CLAUDE.md"), "utf8");
143
+ ok(cm2.split("TaskCreate").length - 1 === 1, "second init does not duplicate the rule");
144
+
145
+ // A user who set the flag their own way keeps their value.
146
+ const r6 = mkdtempSync(join(tmpdir(), "agent-os-cc2-"));
147
+ mkdirSync(join(r6, ".claude"), { recursive: true });
148
+ writeFileSync(join(r6, ".claude/settings.json"),
149
+ JSON.stringify({ env: { CLAUDE_CODE_ENABLE_TODO_TOOLS: "true" } }, null, 2));
150
+ cli(["init", "--no-plugin"], r6);
151
+ ok(JSON.parse(readFileSync(join(r6, ".claude/settings.json"), "utf8"))
152
+ .env.CLAUDE_CODE_ENABLE_TODO_TOOLS === "true", "an existing truthy value is left as the user set it");
153
+
154
+ rmSync(r5, { recursive: true, force: true });
155
+ rmSync(r6, { recursive: true, force: true });
156
+ }
157
+
107
158
  console.log("\n merge, not overwrite");
108
159
  writeFileSync(join(root, "opencode.json"), JSON.stringify({ model: "anthropic/x", instructions: ["KEEP.md"] }, null, 2));
109
160
  cli(["sync"], root);