@sayansr26/agent-os 0.5.3 → 0.6.1
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 +12 -1
- package/CHANGELOG.md +77 -0
- package/README.md +49 -7
- package/package.json +2 -2
- package/plugins/agent-os/.claude-plugin/icon.png +0 -0
- package/plugins/agent-os/.claude-plugin/plugin.json +3 -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/plugins/auto-route/.claude-plugin/plugin.json +18 -0
- package/plugins/auto-route/hooks/hooks.json +1 -0
- package/plugins/auto-route/hooks/register.ts +143 -0
- package/plugins/auto-route/tests/route.test.ts +87 -0
- package/plugins/auto-route/tsconfig.json +3 -0
- package/plugins/auto-route/types/index.d.ts +12 -0
- package/src/claude-setup.mjs +9 -78
- package/src/cli.mjs +81 -30
- package/src/plugin.mjs +91 -35
- package/src/selftest.mjs +67 -3
package/src/claude-setup.mjs
CHANGED
|
@@ -1,81 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The Claude Code settings that are not rules and not the plugin: git write
|
|
3
|
+
* protection, the todo tools flag, and the task-tracking rule.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* that the setup travels with the repository.
|
|
5
|
+
* The logic lives in the plugin (`skills/init/scripts/settings.mjs`) so that
|
|
6
|
+
* `/agent-os:init` inside Claude Code and `agent-os init` in a terminal write
|
|
7
|
+
* exactly the same thing. This module is the CLI's door to it.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
}
|
|
9
|
+
export {
|
|
10
|
+
TODO_ENV, TASK_RULE, GIT_DENY, userDir, paths,
|
|
11
|
+
mergeSettings, addTaskRule, applyScope, describe,
|
|
12
|
+
} from "../plugins/agent-os/skills/init/scripts/settings.mjs";
|
package/src/cli.mjs
CHANGED
|
@@ -5,8 +5,10 @@ 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 {
|
|
9
|
-
import {
|
|
8
|
+
import { ensurePlugin, MARKETPLACE_NAME, PLUGIN, PLUGINS } from "./plugin.mjs";
|
|
9
|
+
import { setupState, describeState } from "../plugins/agent-os/skills/init/scripts/state.mjs";
|
|
10
|
+
import { applyScope, describe } from "./claude-setup.mjs";
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
10
12
|
import { compile, TARGETS } from "./targets.mjs";
|
|
11
13
|
|
|
12
14
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
@@ -15,12 +17,14 @@ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
|
15
17
|
const USAGE = `
|
|
16
18
|
${bold("agent-os")} — one source of truth for AI coding agent config
|
|
17
19
|
|
|
18
|
-
agent-os init
|
|
20
|
+
agent-os init set up a fresh project, or repair and update an existing one
|
|
19
21
|
agent-os sync recompile after editing .agent-os/
|
|
20
22
|
agent-os check verify nothing drifted (exit 1 if it has) — for CI
|
|
21
23
|
agent-os detect list which tools this project is set up for
|
|
22
24
|
agent-os audit inspect the project's context layer and report findings
|
|
23
25
|
agent-os memory inspect and health-check every memory store
|
|
26
|
+
agent-os settings preview git write protection + task tools for Claude Code
|
|
27
|
+
(--scope project|user|both, --apply to write)
|
|
24
28
|
|
|
25
29
|
Without a global install, prefix any of these with
|
|
26
30
|
${dim("npx @sayansr26/agent-os")}
|
|
@@ -29,7 +33,11 @@ Options
|
|
|
29
33
|
--root <dir> project directory (default: cwd)
|
|
30
34
|
--dry-run print what would change, write nothing
|
|
31
35
|
--force overwrite files agent-os did not generate (it refuses by default)
|
|
32
|
-
--no-plugin skip installing the Claude Code
|
|
36
|
+
--no-plugin skip installing or updating the Claude Code plugins during init
|
|
37
|
+
--no-auto-route
|
|
38
|
+
init: install agent-os but not the auto-route model router
|
|
39
|
+
--global init: also write ~/.claude settings without asking
|
|
40
|
+
--no-global init: leave ~/.claude alone
|
|
33
41
|
`;
|
|
34
42
|
|
|
35
43
|
function scaffold(root, found) {
|
|
@@ -116,6 +124,13 @@ export async function main(argv) {
|
|
|
116
124
|
return;
|
|
117
125
|
}
|
|
118
126
|
|
|
127
|
+
if (cmd === "settings") {
|
|
128
|
+
const i = argv.indexOf("--scope");
|
|
129
|
+
runScript("plugins/agent-os/skills/init/scripts/settings.mjs",
|
|
130
|
+
["--root", root, "--scope", i === -1 ? "both" : argv[i + 1], ...(argv.includes("--apply") ? ["--apply"] : [])]);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
if (cmd === "memory") {
|
|
120
135
|
runScript("plugins/agent-os/skills/memory/scripts/memory.mjs", argv.includes("--stale") ? [root, "--stale"] : [root]);
|
|
121
136
|
return;
|
|
@@ -123,14 +138,28 @@ export async function main(argv) {
|
|
|
123
138
|
|
|
124
139
|
let adopted = new Set();
|
|
125
140
|
|
|
141
|
+
let before = null;
|
|
126
142
|
if (cmd === "init") {
|
|
127
143
|
console.log(`\n${bold("agent-os init")} ${root}\n`);
|
|
144
|
+
// Decide fresh vs repair before touching anything, and say which, so a
|
|
145
|
+
// second run on a set-up project reads as a repair rather than a re-init.
|
|
146
|
+
before = setupState(root);
|
|
147
|
+
for (const l of describeState(before)) console.log(` ${l}`);
|
|
148
|
+
console.log({
|
|
149
|
+
fresh: `\n ${dim("Fresh setup: creating everything.")}`,
|
|
150
|
+
repair: `\n ${dim("Existing setup: fixing the items marked MISSING, leaving the rest as it is.")}`,
|
|
151
|
+
healthy: `\n ${dim("Setup is complete: re-syncing and checking the plugin for updates.")}`,
|
|
152
|
+
}[before.status]);
|
|
153
|
+
console.log("");
|
|
128
154
|
console.log(summarise(found));
|
|
155
|
+
const hadSource = existsSync(join(root, DIR));
|
|
129
156
|
const { present, taken } = scaffold(root, found);
|
|
130
157
|
adopted = taken.paths;
|
|
131
158
|
if (taken.from.length) {
|
|
132
159
|
console.log(`\n adopted into ${DIR}/ ${dim("— your existing files are now the source")}`);
|
|
133
160
|
for (const f of taken.from) console.log(` ${f}`);
|
|
161
|
+
} else if (hadSource) {
|
|
162
|
+
console.log(`\n kept ${DIR}/ ${dim("— already the source; nothing re-scaffolded")}`);
|
|
134
163
|
} else {
|
|
135
164
|
console.log(`\n created ${DIR}/ ${dim("(config.json, AGENTS.md, rules/example.md)")}`);
|
|
136
165
|
}
|
|
@@ -214,36 +243,58 @@ Pick one:
|
|
|
214
243
|
if (cmd === "init" && targets.includes("claude-code")) {
|
|
215
244
|
console.log(`\n${bold("Claude Code setup")}\n`);
|
|
216
245
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
246
|
+
// Project scope always: it travels with the repo. User scope protects
|
|
247
|
+
// every other project on the machine too, so it is asked once rather
|
|
248
|
+
// than assumed — unless --global / --no-global already answered.
|
|
249
|
+
for (const l of describe(applyScope("project", root, { dry }), { dry })) console.log(` ${l}`);
|
|
250
|
+
|
|
251
|
+
let global = argv.includes("--global") ? true : argv.includes("--no-global") ? false : null;
|
|
252
|
+
if (global === null && !dry && process.stdin.isTTY && process.stdout.isTTY) {
|
|
253
|
+
console.log("");
|
|
254
|
+
for (const l of describe(applyScope("user", root, { dry: true }), { dry: true })) console.log(` ${l}`);
|
|
255
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
256
|
+
const a = (await rl.question(`\n Apply the same to ~/.claude (every project on this machine)? [Y/n] `)).trim().toLowerCase();
|
|
257
|
+
rl.close();
|
|
258
|
+
global = a === "" || a === "y" || a === "yes";
|
|
259
|
+
}
|
|
260
|
+
if (global) {
|
|
261
|
+
console.log("");
|
|
262
|
+
for (const l of describe(applyScope("user", root, { dry }), { dry })) console.log(` ${l}`);
|
|
263
|
+
} else if (global === null) {
|
|
264
|
+
console.log(`\n ${dim("~/.claude left alone (not a terminal). To protect every project:")}`);
|
|
265
|
+
console.log(` ${dim("npx @sayansr26/agent-os settings --scope user --apply")}`);
|
|
266
|
+
}
|
|
233
267
|
}
|
|
234
268
|
|
|
235
269
|
if (cmd === "init" && targets.includes("claude-code") && !argv.includes("--no-plugin")) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
console.log(`\n
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
console.log(
|
|
244
|
-
|
|
270
|
+
// Both come from the same marketplace, so it is added or refreshed once:
|
|
271
|
+
// by the first plugin whose run gets that far.
|
|
272
|
+
let refresh = true;
|
|
273
|
+
for (const plugin of PLUGINS.filter((p) => p === PLUGIN || !argv.includes(`--no-${p}`))) {
|
|
274
|
+
console.log(`\n${bold("Claude Code plugin")} ${dim(`${plugin}@${MARKETPLACE_NAME}`)}\n`);
|
|
275
|
+
const r = ensurePlugin({ root, dry, plugin, refresh });
|
|
276
|
+
if (r.done.length || r.reason === "no-cli") refresh = false;
|
|
277
|
+
for (const line of r.done) console.log(` ${dim(line)}`);
|
|
278
|
+
if (r.ok) console.log({
|
|
279
|
+
installed: `\n installed ${r.to || ""} at project scope ${dim("— .claude/settings.json, so it travels with the repo")}`,
|
|
280
|
+
updated: `\n updated ${r.from} → ${r.to}`,
|
|
281
|
+
current: `\n already the latest (${r.to})`,
|
|
282
|
+
"dry-run": `\n ${dim(r.from ? `installed: ${r.from} — would refresh and update` : "not installed — would install")}`,
|
|
283
|
+
}[r.reason]);
|
|
284
|
+
for (const line of r.hint) console.log(` ${r.ok ? dim(line) : line}`);
|
|
285
|
+
if (r.reason === "installed" || r.reason === "updated")
|
|
286
|
+
console.log(` ${dim("restart Claude Code, or /reload-plugins, to load it")}`);
|
|
287
|
+
}
|
|
288
|
+
console.log(`\n ${dim("--no-plugin skips this; --no-auto-route skips only the model router")}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (cmd === "init" && !dry) {
|
|
292
|
+
const after = setupState(root);
|
|
293
|
+
console.log(`\n${bold("Result")} ${before.status} → ${after.status}`);
|
|
294
|
+
if (after.missing.length) {
|
|
295
|
+
console.log(dim(" still to do:"));
|
|
296
|
+
for (const i of after.missing) console.log(` ${i.label} → ${i.fix}`);
|
|
245
297
|
}
|
|
246
|
-
console.log(`\n ${dim("--no-plugin skips this")}`);
|
|
247
298
|
}
|
|
248
299
|
console.log("");
|
|
249
300
|
return;
|
package/src/plugin.mjs
CHANGED
|
@@ -1,23 +1,46 @@
|
|
|
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
|
-
*
|
|
5
|
+
* agents, skills, per-agent memory and hooks, which ship as a plugin, and
|
|
6
|
+
* auto-route, a mod that picks the model and effort per turn. Both live in
|
|
7
|
+
* this repo's marketplace. `init` brings each to the latest version whatever
|
|
8
|
+
* state it finds:
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* not installed marketplace add + install, at project scope
|
|
11
|
+
* installed, disabled enable
|
|
12
|
+
* installed refresh the marketplace, then update
|
|
13
|
+
*
|
|
14
|
+
* An installed plugin is never left behind: Claude Code pins the cached
|
|
15
|
+
* version, so a project installed once stays on that version until something
|
|
16
|
+
* updates it — which is how a project ended up running 0.4.1 hooks.
|
|
17
|
+
*
|
|
18
|
+
* Every command runs with the project as its working directory, so project
|
|
19
|
+
* scope means *this* project even with --root. Best effort throughout: if the
|
|
20
|
+
* `claude` CLI is missing or a command fails, `init` reports it and carries
|
|
21
|
+
* on. The rules are the part that must not depend on this.
|
|
12
22
|
*/
|
|
13
23
|
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
25
|
+
import { cmpVersion } from "../plugins/agent-os/skills/init/scripts/state.mjs";
|
|
14
26
|
|
|
15
27
|
export const MARKETPLACE = "sayansr26/agent-os";
|
|
16
28
|
export const MARKETPLACE_NAME = "sayan-plugins";
|
|
17
29
|
export const PLUGIN = "agent-os";
|
|
30
|
+
// Installed by `init` in this order; each can be skipped with --no-<name>.
|
|
31
|
+
export const PLUGINS = [PLUGIN, "auto-route"];
|
|
32
|
+
const idOf = (plugin) => `${plugin}@${MARKETPLACE_NAME}`;
|
|
18
33
|
|
|
19
|
-
const run = (args) =>
|
|
20
|
-
spawnSync("claude", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
34
|
+
const run = (args, cwd) =>
|
|
35
|
+
spawnSync("claude", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
|
|
36
|
+
const json = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
37
|
+
const real = (p) => { try { return realpathSync(p); } catch { return p; } };
|
|
38
|
+
/** The version this package ships for a plugin, from its plugin.json. */
|
|
39
|
+
const bundled = (plugin) => {
|
|
40
|
+
try { return JSON.parse(readFileSync(new URL(`../plugins/${plugin}/.claude-plugin/plugin.json`, import.meta.url), "utf8")).version || null; }
|
|
41
|
+
catch { return null; }
|
|
42
|
+
};
|
|
43
|
+
const tail = (r) => (r.stderr || r.stdout || String(r.error || "")).trim().split("\n").slice(-3);
|
|
21
44
|
|
|
22
45
|
/** Is the Claude Code CLI usable from here? */
|
|
23
46
|
export function claudeAvailable() {
|
|
@@ -25,50 +48,83 @@ export function claudeAvailable() {
|
|
|
25
48
|
return !r.error && r.status === 0;
|
|
26
49
|
}
|
|
27
50
|
|
|
51
|
+
/** This project's install of a plugin, per `claude plugin list --json`, or null. */
|
|
52
|
+
export function installed(root, plugin = PLUGIN) {
|
|
53
|
+
const list = json(run(["plugin", "list", "--json"], root));
|
|
54
|
+
if (!Array.isArray(list)) return null;
|
|
55
|
+
const mine = list.filter((p) => p.id === idOf(plugin) &&
|
|
56
|
+
(p.scope === "user" || (p.projectPath && real(p.projectPath) === real(root))));
|
|
57
|
+
return mine.find((p) => p.scope === "project" || p.scope === "local") || mine[0] || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
61
|
+
* Bring a plugin to the latest version for this project.
|
|
62
|
+
* Returns { ok, reason, from, to, done, hint } with reason one of
|
|
63
|
+
* "installed" | "updated" | "current" | "dry-run" | "no-cli" | "failed".
|
|
64
|
+
* `refresh: false` skips the marketplace step when an earlier call this run
|
|
65
|
+
* already added or updated it.
|
|
31
66
|
*/
|
|
32
|
-
export function
|
|
33
|
-
const
|
|
34
|
-
["marketplace", ["plugin", "marketplace", "add", MARKETPLACE, "--scope", scope]],
|
|
35
|
-
["plugin", ["plugin", "install", `${PLUGIN}@${MARKETPLACE_NAME}`, "--scope", scope, "--yes"]],
|
|
36
|
-
];
|
|
37
|
-
|
|
67
|
+
export function ensurePlugin({ root = process.cwd(), dry = false, scope = "project", plugin = PLUGIN, refresh = true } = {}) {
|
|
68
|
+
const ID = idOf(plugin);
|
|
38
69
|
if (!claudeAvailable())
|
|
39
70
|
return {
|
|
40
|
-
ok: false,
|
|
41
|
-
reason: "no-cli",
|
|
42
|
-
done: [],
|
|
71
|
+
ok: false, reason: "no-cli", done: [],
|
|
43
72
|
hint: [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
...(refresh ? [
|
|
74
|
+
"The `claude` CLI is not on PATH, so the plugin was not installed or updated.",
|
|
75
|
+
"Inside Claude Code, run:",
|
|
76
|
+
` /plugin marketplace add ${MARKETPLACE}`,
|
|
77
|
+
] : []),
|
|
78
|
+
` /plugin install ${ID} (or /plugin update ${ID} if it is already there)`,
|
|
48
79
|
],
|
|
49
80
|
};
|
|
50
81
|
|
|
82
|
+
const before = installed(root, plugin);
|
|
83
|
+
const steps = [];
|
|
84
|
+
if (refresh) {
|
|
85
|
+
const markets = json(run(["plugin", "marketplace", "list", "--json"], root));
|
|
86
|
+
const haveMarket = Array.isArray(markets) && markets.some((m) => m.name === MARKETPLACE_NAME);
|
|
87
|
+
steps.push(haveMarket
|
|
88
|
+
? ["plugin", "marketplace", "update", MARKETPLACE_NAME]
|
|
89
|
+
: ["plugin", "marketplace", "add", MARKETPLACE, "--scope", scope]);
|
|
90
|
+
}
|
|
91
|
+
if (!before) steps.push(["plugin", "install", ID, "--scope", scope, "--yes"]);
|
|
92
|
+
else {
|
|
93
|
+
if (before.enabled === false) steps.push(["plugin", "enable", ID, "--scope", before.scope]);
|
|
94
|
+
steps.push(["plugin", "update", ID, "--scope", before.scope, "--yes"]);
|
|
95
|
+
}
|
|
96
|
+
|
|
51
97
|
if (dry)
|
|
52
|
-
return { ok: true, reason: "dry-run", done: steps.map((
|
|
98
|
+
return { ok: true, reason: "dry-run", from: before?.version || null, done: steps.map((a) => `claude ${a.join(" ")}`), hint: [] };
|
|
53
99
|
|
|
54
100
|
const done = [];
|
|
55
|
-
for (const
|
|
56
|
-
const r = run(args);
|
|
101
|
+
for (const args of steps) {
|
|
102
|
+
const r = run(args, root);
|
|
57
103
|
if (r.status !== 0) {
|
|
58
|
-
const err = (r.stderr || r.stdout || "").trim().split("\n").slice(-3).join("\n");
|
|
59
104
|
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
reason: what,
|
|
62
|
-
done,
|
|
105
|
+
ok: false, reason: "failed", from: before?.version || null, done,
|
|
63
106
|
hint: [
|
|
64
107
|
`\`claude ${args.join(" ")}\` failed:`,
|
|
65
|
-
...
|
|
66
|
-
"The rules above were still written.
|
|
67
|
-
`
|
|
108
|
+
...tail(r).map((l) => ` ${l}`),
|
|
109
|
+
"The rules and settings above were still written. Inside Claude Code:",
|
|
110
|
+
before ? ` /plugin update ${ID}, then /reload-plugins` : ` /plugin install ${ID}`,
|
|
68
111
|
],
|
|
69
112
|
};
|
|
70
113
|
}
|
|
71
114
|
done.push(`claude ${args.join(" ")}`);
|
|
72
115
|
}
|
|
73
|
-
|
|
116
|
+
|
|
117
|
+
const after = installed(root, plugin);
|
|
118
|
+
const to = after?.version || null;
|
|
119
|
+
const from = before?.version || null;
|
|
120
|
+
const want = bundled(plugin);
|
|
121
|
+
const hint = [];
|
|
122
|
+
// The marketplace serves what is pushed to GitHub; this package may be newer.
|
|
123
|
+
if (to && want && cmpVersion(to, want) < 0)
|
|
124
|
+
hint.push(`Marketplace still serves ${plugin} ${to}; this agent-os ships ${want}. It updates once ${MARKETPLACE} is pushed.`);
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
reason: !before ? "installed" : from !== to ? "updated" : "current",
|
|
128
|
+
from, to, done, hint,
|
|
129
|
+
};
|
|
74
130
|
}
|
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,15 +91,21 @@ 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");
|
|
104
|
+
ok(pi.stdout.includes("auto-route@sayan-plugins"), "init sets up auto-route by default");
|
|
105
|
+
ok((pi.stdout.match(/marketplace add/g) || []).length <= 1, "the marketplace is added once, not per plugin");
|
|
106
|
+
const na = cli(["init", "--dry-run", "--no-auto-route"], r4);
|
|
107
|
+
ok(!/install auto-route@sayan-plugins/.test(na.stdout) && /install agent-os@sayan-plugins/.test(na.stdout),
|
|
108
|
+
"--no-auto-route keeps agent-os and skips auto-route");
|
|
97
109
|
rmSync(r4, { recursive: true, force: true });
|
|
98
110
|
|
|
99
111
|
// A hand-written file at a generated path must survive a sync.
|
|
@@ -129,6 +141,20 @@ try {
|
|
|
129
141
|
ok(st.env.CLAUDE_CODE_ENABLE_TODO_TOOLS === "1", "todo tools enabled in project settings");
|
|
130
142
|
ok(st.env.FOO === "bar", "existing env keys preserved");
|
|
131
143
|
ok(st.permissions?.deny?.[0] === "Bash(git push *)", "existing permissions preserved");
|
|
144
|
+
ok(st.permissions.deny.length === 42, "git write protection merged into project settings, no duplicate push rule");
|
|
145
|
+
ok(!existsSync(join(HOME, "settings.json")), "without --global (and no terminal) ~/.claude is left alone");
|
|
146
|
+
ok(!existsSync(join(r5, ".claude/settings.json.agent-os.bak")), "no backup litter inside the project");
|
|
147
|
+
|
|
148
|
+
const again = cli(["init", "--no-plugin", "--global"], r5).stdout;
|
|
149
|
+
ok(again.includes("SETUP REPAIR") && again.includes("MISSING ~/.claude/settings.json git write protection"),
|
|
150
|
+
"init on a set-up project reports a repair and names what is missing");
|
|
151
|
+
ok(again.includes("kept .agent-os/"), "repair keeps the existing source instead of re-scaffolding");
|
|
152
|
+
const us = JSON.parse(readFileSync(join(HOME, "settings.json"), "utf8"));
|
|
153
|
+
ok(us.permissions.deny.includes("Bash(git commit *)") && us.env.CLAUDE_CODE_ENABLE_TODO_TOOLS === "1",
|
|
154
|
+
"--global writes git protection and task tools to ~/.claude/settings.json");
|
|
155
|
+
ok(readFileSync(join(HOME, "CLAUDE.md"), "utf8").includes("TaskCreate"), "--global adds the task rule to ~/.claude/CLAUDE.md");
|
|
156
|
+
cli(["init", "--no-plugin", "--global"], r5);
|
|
157
|
+
ok(JSON.parse(readFileSync(join(HOME, "settings.json"), "utf8")).permissions.deny.length === 42, "second --global run adds nothing");
|
|
132
158
|
|
|
133
159
|
const cm = readFileSync(join(r5, "CLAUDE.md"), "utf8");
|
|
134
160
|
ok(cm.includes("TaskCreate / TaskUpdate"), "task-tracking rule added to CLAUDE.md");
|
|
@@ -155,13 +181,51 @@ try {
|
|
|
155
181
|
rmSync(r6, { recursive: true, force: true });
|
|
156
182
|
}
|
|
157
183
|
|
|
184
|
+
console.log("\n plugin hooks");
|
|
185
|
+
{
|
|
186
|
+
const r7 = mkdtempSync(join(tmpdir(), "agent-os-hooks-"));
|
|
187
|
+
const mem = join(r7, ".claude/agent-memory/agent-os-feature-cartographer");
|
|
188
|
+
mkdirSync(mem, { recursive: true });
|
|
189
|
+
for (const f of ["billing", "orders", "search"]) mkdirSync(join(r7, "src/features", f), { recursive: true });
|
|
190
|
+
writeFileSync(join(mem, "_architecture.md"), "# Architecture\n");
|
|
191
|
+
writeFileSync(join(mem, "billing.md"), "---\nmapped: 2026-01-01\nentry: src/features/billing/index.ts\n---\n# billing\n");
|
|
192
|
+
writeFileSync(join(mem, "MEMORY.md"), "- billing — src/features/billing — mapped 2026-01-01\n");
|
|
193
|
+
mkdirSync(join(r7, ".agent-os/rules"), { recursive: true });
|
|
194
|
+
writeFileSync(join(r7, ".agent-os/rules/api.md"), '---\npaths:\n - "src/**"\n---\n\nSource body.\n');
|
|
195
|
+
mkdirSync(join(r7, ".claude/rules"), { recursive: true });
|
|
196
|
+
writeFileSync(join(r7, ".claude/rules/api.md"), "---\n---\n\n<!-- agent-os: generated from .agent-os/ — edit the source -->\nEdited in place.\n");
|
|
197
|
+
const env = { ...process.env, CLAUDE_PROJECT_DIR: r7 };
|
|
198
|
+
|
|
199
|
+
const start = node("hooks/session-resume.mjs", [], { cwd: r7, env }).stdout;
|
|
200
|
+
ok(start.includes("agent-os is active") && start.includes("agent-os:feature-cartographer"), "session start announces the agents");
|
|
201
|
+
ok(start.includes("Mapped: 1 of 3 features") && start.includes("billing"), "session start says what is mapped");
|
|
202
|
+
|
|
203
|
+
const run = `selftest-${process.pid}-${Date.now()}`;
|
|
204
|
+
const edit = (file, session = run) => node("hooks/pre-edit.mjs", [], {
|
|
205
|
+
env, input: JSON.stringify({ session_id: session, cwd: r7, tool_input: { file_path: join(r7, file) } }),
|
|
206
|
+
}).stdout;
|
|
207
|
+
const denied = edit(".claude/rules/api.md");
|
|
208
|
+
ok(denied.includes('"deny"') && denied.includes(".agent-os/rules/api.md"), "edit to a generated rule is blocked, source named");
|
|
209
|
+
ok(edit("src/features/orders/a.ts").includes("no cartographer map"), "first edit to an unmapped feature reminds");
|
|
210
|
+
ok(edit("src/features/orders/b.ts") === "", "reminder shows once per feature per session");
|
|
211
|
+
ok(edit("src/features/billing/a.ts") === "", "mapped feature stays silent");
|
|
212
|
+
ok(edit("src/lib/x.ts") === "", "non-feature path stays silent");
|
|
213
|
+
|
|
214
|
+
mkdirSync(join(mem, ".claude/agent-memory"), { recursive: true });
|
|
215
|
+
const audit = node("skills/init/scripts/audit.mjs", [r7]).stdout;
|
|
216
|
+
ok(/api\.md was edited in place/.test(audit), "audit flags a generated rule that drifted from its source");
|
|
217
|
+
ok(audit.includes("folder inside agent memory"), "audit flags a nested folder in agent memory");
|
|
218
|
+
ok(audit.includes("1 of 3 (33%)"), "audit reports map coverage, not just that a folder exists");
|
|
219
|
+
rmSync(r7, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
|
|
158
222
|
console.log("\n merge, not overwrite");
|
|
159
223
|
writeFileSync(join(root, "opencode.json"), JSON.stringify({ model: "anthropic/x", instructions: ["KEEP.md"] }, null, 2));
|
|
160
224
|
cli(["sync"], root);
|
|
161
225
|
const oc = JSON.parse(read("opencode.json"));
|
|
162
226
|
ok(oc.model === "anthropic/x", "existing keys preserved");
|
|
163
227
|
ok(oc.instructions.includes("KEEP.md"), "existing instructions preserved");
|
|
164
|
-
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
228
|
+
} finally { rmSync(root, { recursive: true, force: true }); rmSync(HOME, { recursive: true, force: true }); }
|
|
165
229
|
|
|
166
230
|
console.log(`\n${fail ? `FAILED (${fail})` : "PASSED"}\n`);
|
|
167
231
|
process.exit(fail ? 1 : 0);
|