@sayansr26/agent-os 0.5.2 → 0.6.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/src/selftest.mjs CHANGED
@@ -8,6 +8,12 @@ import { spawnSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
10
  const PKG = join(fileURLToPath(new URL(".", import.meta.url)), "..");
11
+ // Every child inherits this, so nothing here can ever touch the real ~/.claude.
12
+ const HOME = mkdtempSync(join(tmpdir(), "agent-os-home-"));
13
+ process.env.CLAUDE_CONFIG_DIR = HOME;
14
+ const PLUGIN = join(PKG, "plugins/agent-os");
15
+ const node = (script, args, opts = {}) =>
16
+ spawnSync(process.execPath, [join(PLUGIN, script), ...args], { encoding: "utf8", ...opts });
11
17
  const cli = (args, cwd) => spawnSync(process.execPath, [join(PKG, "bin/agent-os.mjs"), ...args, "--root", cwd], { encoding: "utf8" });
12
18
  let fail = 0;
13
19
  const ok = (c, m) => { console.log(` ${c ? "ok " : "FAIL"} ${m}`); if (!c) fail++; };
@@ -85,13 +91,14 @@ try {
85
91
  ok(existsSync(join(r2, ".agent-os/rules/theming.md")), "existing .claude/rules/ are adopted");
86
92
  ok(!existsSync(join(r2, ".agent-os/rules/example.md")), "no example.md when real rules were adopted");
87
93
  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");
94
+ ok(!i.stdout.includes("--no-plugin skips this"), "--no-plugin skips the plugin install");
89
95
 
90
96
  // init offers the plugin when not told otherwise; --dry-run proves the
91
97
  // commands without running them against the machine's real config.
92
98
  const r4 = mkdtempSync(join(tmpdir(), "agent-os-plug-"));
93
99
  const pi = cli(["init", "--dry-run"], r4);
94
- ok(pi.stdout.includes("Claude Code plugin"), "init sets up the Claude Code plugin by default");
100
+ ok(pi.stdout.includes("--no-plugin skips this"), "init sets up the Claude Code plugin by default");
101
+ ok(pi.stdout.includes("SETUP FRESH"), "init on an empty project reports a fresh setup");
95
102
  ok(/marketplace add sayansr26\/agent-os|plugin marketplace add|not on PATH/.test(pi.stdout),
96
103
  "init names the marketplace step or says why it could not run it");
97
104
  rmSync(r4, { recursive: true, force: true });
@@ -114,13 +121,106 @@ try {
114
121
  rmSync(r3, { recursive: true, force: true });
115
122
  }
116
123
 
124
+ console.log("\n Claude Code project setup");
125
+ {
126
+ const r5 = mkdtempSync(join(tmpdir(), "agent-os-cc-"));
127
+ mkdirSync(join(r5, ".claude"), { recursive: true });
128
+ // An existing settings.json with unrelated keys must survive untouched.
129
+ writeFileSync(join(r5, ".claude/settings.json"),
130
+ JSON.stringify({ permissions: { deny: ["Bash(git push *)"] }, env: { FOO: "bar" } }, null, 2));
131
+ writeFileSync(join(r5, "CLAUDE.md"),
132
+ "# CLAUDE.md\n\nStack notes.\n\n## Operator preferences\n\n- Never run git unasked.\n\n## Verification\n\nRun make test.\n");
133
+
134
+ cli(["init", "--no-plugin"], r5);
135
+ const st = JSON.parse(readFileSync(join(r5, ".claude/settings.json"), "utf8"));
136
+ ok(st.env.CLAUDE_CODE_ENABLE_TODO_TOOLS === "1", "todo tools enabled in project settings");
137
+ ok(st.env.FOO === "bar", "existing env keys preserved");
138
+ ok(st.permissions?.deny?.[0] === "Bash(git push *)", "existing permissions preserved");
139
+ ok(st.permissions.deny.length === 42, "git write protection merged into project settings, no duplicate push rule");
140
+ ok(!existsSync(join(HOME, "settings.json")), "without --global (and no terminal) ~/.claude is left alone");
141
+ ok(!existsSync(join(r5, ".claude/settings.json.agent-os.bak")), "no backup litter inside the project");
142
+
143
+ const again = cli(["init", "--no-plugin", "--global"], r5).stdout;
144
+ ok(again.includes("SETUP REPAIR") && again.includes("MISSING ~/.claude/settings.json git write protection"),
145
+ "init on a set-up project reports a repair and names what is missing");
146
+ ok(again.includes("kept .agent-os/"), "repair keeps the existing source instead of re-scaffolding");
147
+ const us = JSON.parse(readFileSync(join(HOME, "settings.json"), "utf8"));
148
+ ok(us.permissions.deny.includes("Bash(git commit *)") && us.env.CLAUDE_CODE_ENABLE_TODO_TOOLS === "1",
149
+ "--global writes git protection and task tools to ~/.claude/settings.json");
150
+ ok(readFileSync(join(HOME, "CLAUDE.md"), "utf8").includes("TaskCreate"), "--global adds the task rule to ~/.claude/CLAUDE.md");
151
+ cli(["init", "--no-plugin", "--global"], r5);
152
+ ok(JSON.parse(readFileSync(join(HOME, "settings.json"), "utf8")).permissions.deny.length === 42, "second --global run adds nothing");
153
+
154
+ const cm = readFileSync(join(r5, "CLAUDE.md"), "utf8");
155
+ ok(cm.includes("TaskCreate / TaskUpdate"), "task-tracking rule added to CLAUDE.md");
156
+ ok(cm.indexOf("TaskCreate") > cm.indexOf("## Operator preferences") &&
157
+ cm.indexOf("TaskCreate") < cm.indexOf("## Verification"),
158
+ "rule lands inside Operator preferences, not at the end");
159
+ ok(cm.includes("Never run git unasked."), "existing CLAUDE.md content preserved");
160
+
161
+ // Idempotent: a second init must not duplicate either.
162
+ cli(["init", "--no-plugin"], r5);
163
+ const cm2 = readFileSync(join(r5, "CLAUDE.md"), "utf8");
164
+ ok(cm2.split("TaskCreate").length - 1 === 1, "second init does not duplicate the rule");
165
+
166
+ // A user who set the flag their own way keeps their value.
167
+ const r6 = mkdtempSync(join(tmpdir(), "agent-os-cc2-"));
168
+ mkdirSync(join(r6, ".claude"), { recursive: true });
169
+ writeFileSync(join(r6, ".claude/settings.json"),
170
+ JSON.stringify({ env: { CLAUDE_CODE_ENABLE_TODO_TOOLS: "true" } }, null, 2));
171
+ cli(["init", "--no-plugin"], r6);
172
+ ok(JSON.parse(readFileSync(join(r6, ".claude/settings.json"), "utf8"))
173
+ .env.CLAUDE_CODE_ENABLE_TODO_TOOLS === "true", "an existing truthy value is left as the user set it");
174
+
175
+ rmSync(r5, { recursive: true, force: true });
176
+ rmSync(r6, { recursive: true, force: true });
177
+ }
178
+
179
+ console.log("\n plugin hooks");
180
+ {
181
+ const r7 = mkdtempSync(join(tmpdir(), "agent-os-hooks-"));
182
+ const mem = join(r7, ".claude/agent-memory/agent-os-feature-cartographer");
183
+ mkdirSync(mem, { recursive: true });
184
+ for (const f of ["billing", "orders", "search"]) mkdirSync(join(r7, "src/features", f), { recursive: true });
185
+ writeFileSync(join(mem, "_architecture.md"), "# Architecture\n");
186
+ writeFileSync(join(mem, "billing.md"), "---\nmapped: 2026-01-01\nentry: src/features/billing/index.ts\n---\n# billing\n");
187
+ writeFileSync(join(mem, "MEMORY.md"), "- billing — src/features/billing — mapped 2026-01-01\n");
188
+ mkdirSync(join(r7, ".agent-os/rules"), { recursive: true });
189
+ writeFileSync(join(r7, ".agent-os/rules/api.md"), '---\npaths:\n - "src/**"\n---\n\nSource body.\n');
190
+ mkdirSync(join(r7, ".claude/rules"), { recursive: true });
191
+ writeFileSync(join(r7, ".claude/rules/api.md"), "---\n---\n\n<!-- agent-os: generated from .agent-os/ — edit the source -->\nEdited in place.\n");
192
+ const env = { ...process.env, CLAUDE_PROJECT_DIR: r7 };
193
+
194
+ const start = node("hooks/session-resume.mjs", [], { cwd: r7, env }).stdout;
195
+ ok(start.includes("agent-os is active") && start.includes("agent-os:feature-cartographer"), "session start announces the agents");
196
+ ok(start.includes("Mapped: 1 of 3 features") && start.includes("billing"), "session start says what is mapped");
197
+
198
+ const run = `selftest-${process.pid}-${Date.now()}`;
199
+ const edit = (file, session = run) => node("hooks/pre-edit.mjs", [], {
200
+ env, input: JSON.stringify({ session_id: session, cwd: r7, tool_input: { file_path: join(r7, file) } }),
201
+ }).stdout;
202
+ const denied = edit(".claude/rules/api.md");
203
+ ok(denied.includes('"deny"') && denied.includes(".agent-os/rules/api.md"), "edit to a generated rule is blocked, source named");
204
+ ok(edit("src/features/orders/a.ts").includes("no cartographer map"), "first edit to an unmapped feature reminds");
205
+ ok(edit("src/features/orders/b.ts") === "", "reminder shows once per feature per session");
206
+ ok(edit("src/features/billing/a.ts") === "", "mapped feature stays silent");
207
+ ok(edit("src/lib/x.ts") === "", "non-feature path stays silent");
208
+
209
+ mkdirSync(join(mem, ".claude/agent-memory"), { recursive: true });
210
+ const audit = node("skills/init/scripts/audit.mjs", [r7]).stdout;
211
+ ok(/api\.md was edited in place/.test(audit), "audit flags a generated rule that drifted from its source");
212
+ ok(audit.includes("folder inside agent memory"), "audit flags a nested folder in agent memory");
213
+ ok(audit.includes("1 of 3 (33%)"), "audit reports map coverage, not just that a folder exists");
214
+ rmSync(r7, { recursive: true, force: true });
215
+ }
216
+
117
217
  console.log("\n merge, not overwrite");
118
218
  writeFileSync(join(root, "opencode.json"), JSON.stringify({ model: "anthropic/x", instructions: ["KEEP.md"] }, null, 2));
119
219
  cli(["sync"], root);
120
220
  const oc = JSON.parse(read("opencode.json"));
121
221
  ok(oc.model === "anthropic/x", "existing keys preserved");
122
222
  ok(oc.instructions.includes("KEEP.md"), "existing instructions preserved");
123
- } finally { rmSync(root, { recursive: true, force: true }); }
223
+ } finally { rmSync(root, { recursive: true, force: true }); rmSync(HOME, { recursive: true, force: true }); }
124
224
 
125
225
  console.log(`\n${fail ? `FAILED (${fail})` : "PASSED"}\n`);
126
226
  process.exit(fail ? 1 : 0);