@sayansr26/agent-os 0.5.3 → 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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +59 -0
- package/README.md +27 -7
- package/package.json +1 -1
- package/plugins/agent-os/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-os/agents/architect.md +5 -0
- package/plugins/agent-os/agents/builder.md +7 -2
- package/plugins/agent-os/agents/documenter.md +6 -1
- package/plugins/agent-os/agents/feature-cartographer.md +18 -1
- package/plugins/agent-os/agents/orchestrator.md +5 -0
- package/plugins/agent-os/agents/reviewer.md +5 -0
- package/plugins/agent-os/agents/tester.md +5 -0
- package/plugins/agent-os/hooks/hooks.json +14 -1
- package/plugins/agent-os/hooks/pre-edit.mjs +83 -0
- package/plugins/agent-os/hooks/session-resume.mjs +75 -18
- package/plugins/agent-os/lib/features.mjs +154 -0
- package/plugins/agent-os/skills/init/SKILL.md +72 -32
- package/plugins/agent-os/skills/init/references/changing-a-feature.md +7 -0
- package/plugins/agent-os/skills/init/references/establishing.md +28 -0
- package/plugins/agent-os/skills/init/references/git-permissions.md +5 -2
- package/plugins/agent-os/skills/init/scripts/audit.mjs +73 -2
- package/plugins/agent-os/skills/init/scripts/settings.mjs +186 -0
- package/plugins/agent-os/skills/init/scripts/state.mjs +133 -0
- package/src/claude-setup.mjs +9 -78
- package/src/cli.mjs +71 -28
- package/src/plugin.mjs +74 -33
- package/src/selftest.mjs +62 -3
package/src/plugin.mjs
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Install the Claude Code half of agent-os during `init`.
|
|
2
|
+
* Install or update the Claude Code half of agent-os during `init`.
|
|
3
3
|
*
|
|
4
4
|
* The CLI compiles rules for every tool. Claude Code additionally gets the
|
|
5
|
-
* agents, skills, per-agent memory and
|
|
6
|
-
*
|
|
7
|
-
* hoping. `claude plugin ...` with `--yes` is the documented automation path.
|
|
5
|
+
* agents, skills, per-agent memory and hooks, which ship as a plugin. `init`
|
|
6
|
+
* brings that plugin to the latest version whatever state it finds:
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* not installed marketplace add + install, at project scope
|
|
9
|
+
* installed, disabled enable
|
|
10
|
+
* installed refresh the marketplace, then update
|
|
11
|
+
*
|
|
12
|
+
* An installed plugin is never left behind: Claude Code pins the cached
|
|
13
|
+
* version, so a project installed once stays on that version until something
|
|
14
|
+
* updates it — which is how a project ended up running 0.4.1 hooks.
|
|
15
|
+
*
|
|
16
|
+
* Every command runs with the project as its working directory, so project
|
|
17
|
+
* scope means *this* project even with --root. Best effort throughout: if the
|
|
18
|
+
* `claude` CLI is missing or a command fails, `init` reports it and carries
|
|
19
|
+
* on. The rules are the part that must not depend on this.
|
|
12
20
|
*/
|
|
13
21
|
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { realpathSync } from "node:fs";
|
|
23
|
+
import { bundledVersion, cmpVersion } from "../plugins/agent-os/skills/init/scripts/state.mjs";
|
|
14
24
|
|
|
15
25
|
export const MARKETPLACE = "sayansr26/agent-os";
|
|
16
26
|
export const MARKETPLACE_NAME = "sayan-plugins";
|
|
17
27
|
export const PLUGIN = "agent-os";
|
|
28
|
+
const ID = `${PLUGIN}@${MARKETPLACE_NAME}`;
|
|
18
29
|
|
|
19
|
-
const run = (args) =>
|
|
20
|
-
spawnSync("claude", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
30
|
+
const run = (args, cwd) =>
|
|
31
|
+
spawnSync("claude", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
|
|
32
|
+
const json = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
33
|
+
const real = (p) => { try { return realpathSync(p); } catch { return p; } };
|
|
34
|
+
const tail = (r) => (r.stderr || r.stdout || String(r.error || "")).trim().split("\n").slice(-3);
|
|
21
35
|
|
|
22
36
|
/** Is the Claude Code CLI usable from here? */
|
|
23
37
|
export function claudeAvailable() {
|
|
@@ -25,50 +39,77 @@ export function claudeAvailable() {
|
|
|
25
39
|
return !r.error && r.status === 0;
|
|
26
40
|
}
|
|
27
41
|
|
|
42
|
+
/** This project's install of the plugin, per `claude plugin list --json`, or null. */
|
|
43
|
+
export function installed(root) {
|
|
44
|
+
const list = json(run(["plugin", "list", "--json"], root));
|
|
45
|
+
if (!Array.isArray(list)) return null;
|
|
46
|
+
const mine = list.filter((p) => p.id === ID &&
|
|
47
|
+
(p.scope === "user" || (p.projectPath && real(p.projectPath) === real(root))));
|
|
48
|
+
return mine.find((p) => p.scope === "project" || p.scope === "local") || mine[0] || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
52
|
+
* Bring the plugin to the latest version for this project.
|
|
53
|
+
* Returns { ok, reason, from, to, done, hint } with reason one of
|
|
54
|
+
* "installed" | "updated" | "current" | "dry-run" | "no-cli" | "failed".
|
|
31
55
|
*/
|
|
32
|
-
export function
|
|
33
|
-
const steps = [
|
|
34
|
-
["marketplace", ["plugin", "marketplace", "add", MARKETPLACE, "--scope", scope]],
|
|
35
|
-
["plugin", ["plugin", "install", `${PLUGIN}@${MARKETPLACE_NAME}`, "--scope", scope, "--yes"]],
|
|
36
|
-
];
|
|
37
|
-
|
|
56
|
+
export function ensurePlugin({ root = process.cwd(), dry = false, scope = "project" } = {}) {
|
|
38
57
|
if (!claudeAvailable())
|
|
39
58
|
return {
|
|
40
|
-
ok: false,
|
|
41
|
-
reason: "no-cli",
|
|
42
|
-
done: [],
|
|
59
|
+
ok: false, reason: "no-cli", done: [],
|
|
43
60
|
hint: [
|
|
44
|
-
"The `claude` CLI is not on PATH, so the plugin was not installed.",
|
|
61
|
+
"The `claude` CLI is not on PATH, so the plugin was not installed or updated.",
|
|
45
62
|
"Inside Claude Code, run:",
|
|
46
63
|
` /plugin marketplace add ${MARKETPLACE}`,
|
|
47
|
-
` /plugin install ${
|
|
64
|
+
` /plugin install ${ID} (or /plugin update ${ID} if it is already there)`,
|
|
48
65
|
],
|
|
49
66
|
};
|
|
50
67
|
|
|
68
|
+
const before = installed(root);
|
|
69
|
+
const markets = json(run(["plugin", "marketplace", "list", "--json"], root));
|
|
70
|
+
const haveMarket = Array.isArray(markets) && markets.some((m) => m.name === MARKETPLACE_NAME);
|
|
71
|
+
|
|
72
|
+
const steps = [];
|
|
73
|
+
steps.push(haveMarket
|
|
74
|
+
? ["plugin", "marketplace", "update", MARKETPLACE_NAME]
|
|
75
|
+
: ["plugin", "marketplace", "add", MARKETPLACE, "--scope", scope]);
|
|
76
|
+
if (!before) steps.push(["plugin", "install", ID, "--scope", scope, "--yes"]);
|
|
77
|
+
else {
|
|
78
|
+
if (before.enabled === false) steps.push(["plugin", "enable", ID, "--scope", before.scope]);
|
|
79
|
+
steps.push(["plugin", "update", ID, "--scope", before.scope, "--yes"]);
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
if (dry)
|
|
52
|
-
return { ok: true, reason: "dry-run", done: steps.map((
|
|
83
|
+
return { ok: true, reason: "dry-run", from: before?.version || null, done: steps.map((a) => `claude ${a.join(" ")}`), hint: [] };
|
|
53
84
|
|
|
54
85
|
const done = [];
|
|
55
|
-
for (const
|
|
56
|
-
const r = run(args);
|
|
86
|
+
for (const args of steps) {
|
|
87
|
+
const r = run(args, root);
|
|
57
88
|
if (r.status !== 0) {
|
|
58
|
-
const err = (r.stderr || r.stdout || "").trim().split("\n").slice(-3).join("\n");
|
|
59
89
|
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
reason: what,
|
|
62
|
-
done,
|
|
90
|
+
ok: false, reason: "failed", from: before?.version || null, done,
|
|
63
91
|
hint: [
|
|
64
92
|
`\`claude ${args.join(" ")}\` failed:`,
|
|
65
|
-
...
|
|
66
|
-
"The rules above were still written.
|
|
67
|
-
`
|
|
93
|
+
...tail(r).map((l) => ` ${l}`),
|
|
94
|
+
"The rules and settings above were still written. Inside Claude Code:",
|
|
95
|
+
before ? ` /plugin update ${ID}, then /reload-plugins` : ` /plugin install ${ID}`,
|
|
68
96
|
],
|
|
69
97
|
};
|
|
70
98
|
}
|
|
71
99
|
done.push(`claude ${args.join(" ")}`);
|
|
72
100
|
}
|
|
73
|
-
|
|
101
|
+
|
|
102
|
+
const after = installed(root);
|
|
103
|
+
const to = after?.version || null;
|
|
104
|
+
const from = before?.version || null;
|
|
105
|
+
const want = bundledVersion();
|
|
106
|
+
const hint = [];
|
|
107
|
+
// The marketplace serves what is pushed to GitHub; this package may be newer.
|
|
108
|
+
if (to && want && cmpVersion(to, want) < 0)
|
|
109
|
+
hint.push(`Marketplace still serves ${to}; this agent-os ships ${want}. It updates once ${MARKETPLACE} is pushed.`);
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
reason: !before ? "installed" : from !== to ? "updated" : "current",
|
|
113
|
+
from, to, done, hint,
|
|
114
|
+
};
|
|
74
115
|
}
|
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("
|
|
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("
|
|
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 });
|
|
@@ -129,6 +136,20 @@ try {
|
|
|
129
136
|
ok(st.env.CLAUDE_CODE_ENABLE_TODO_TOOLS === "1", "todo tools enabled in project settings");
|
|
130
137
|
ok(st.env.FOO === "bar", "existing env keys preserved");
|
|
131
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");
|
|
132
153
|
|
|
133
154
|
const cm = readFileSync(join(r5, "CLAUDE.md"), "utf8");
|
|
134
155
|
ok(cm.includes("TaskCreate / TaskUpdate"), "task-tracking rule added to CLAUDE.md");
|
|
@@ -155,13 +176,51 @@ try {
|
|
|
155
176
|
rmSync(r6, { recursive: true, force: true });
|
|
156
177
|
}
|
|
157
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
|
+
|
|
158
217
|
console.log("\n merge, not overwrite");
|
|
159
218
|
writeFileSync(join(root, "opencode.json"), JSON.stringify({ model: "anthropic/x", instructions: ["KEEP.md"] }, null, 2));
|
|
160
219
|
cli(["sync"], root);
|
|
161
220
|
const oc = JSON.parse(read("opencode.json"));
|
|
162
221
|
ok(oc.model === "anthropic/x", "existing keys preserved");
|
|
163
222
|
ok(oc.instructions.includes("KEEP.md"), "existing instructions preserved");
|
|
164
|
-
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
223
|
+
} finally { rmSync(root, { recursive: true, force: true }); rmSync(HOME, { recursive: true, force: true }); }
|
|
165
224
|
|
|
166
225
|
console.log(`\n${fail ? `FAILED (${fail})` : "PASSED"}\n`);
|
|
167
226
|
process.exit(fail ? 1 : 0);
|