From c99222bde76f86104d3c33e4006b9a692acecbfc Mon Sep 17 00:00:00 2001 From: Schneider <224583183+schneiderjoseph@users.noreply.github.com> Date: Wed, 9 Sep 2026 07:29:22 -0400 Subject: [PATCH] devia is for every agent 0.2.0 shipped --global serving Claude Code alone. Two of the four SKIP reasons were wrong: they stated an absence that had never been verified. Checking the machine instead of assuming: - ~/.cursor/rules/ exists and holds user-level .mdc rules, with the same frontmatter the devia adapter already ships - ~/.codex/skills/ uses the same SKILL.md convention as Claude Code - ~/.gemini/GEMINI.md is a single global instruction file So --global now writes, each in the format the agent actually reads: the skill pack for Claude Code and Codex, the rules adapter for Cursor, the universal contract for Gemini. CLAUDE_CONFIG_DIR and CODEX_HOME are honoured. Copilot and Windsurf still report SKIP with the reason -- their user-level configuration is editor settings rather than a file devia can place -- and that is now debt D8 with what must be established before building, not a silent omission. Ownership decides how a path is written. A directory the agent owns (~/.claude/skills, ~/.codex/skills, ~/.cursor/rules) is added to freely. A file the user owns (~/.gemini/GEMINI.md) is written only when absent or empty; otherwise SKIP says why and the content stays. --force overrides both and names every path it took. Registries, recorded with the tool itself: - G6 opened and closed: devia is for every agent, no agent is privileged - D8 opened: no user-level install for Copilot and Windsurf - 01_ARCHITECTURE carries the decision, so a future surface serving one agent has to say why the others are not served Version: package 0.3.0, standard unchanged at 0.1.0. Verified: 33 tests with and without FORCE_COLOR, including one that writes into a temporary home and asserts a plain install touches nothing outside the repository, that each agent receives its own format, and that an edited file survives without --force. validate clean, check P0 clear, memory validate clean. --- .devia/01_ARCHITECTURE.md | 1 + .devia/02_SURFACES.md | 2 +- .devia/04_PERMISSIONS.md | 11 +++++-- .devia/11_GAPS.md | 1 + .devia/12_DEBT.md | 1 + CHANGELOG.md | 25 ++++++++++++++ package.json | 2 +- src/commands/skills.mjs | 69 +++++++++++++++++++++++++++------------ tests/cli.test.mjs | 44 +++++++++++++++---------- 9 files changed, 113 insertions(+), 43 deletions(-) diff --git a/.devia/01_ARCHITECTURE.md b/.devia/01_ARCHITECTURE.md index c15353c..cab0f50 100644 --- a/.devia/01_ARCHITECTURE.md +++ b/.devia/01_ARCHITECTURE.md @@ -58,6 +58,7 @@ Content is data. Code reads it; code never encodes what a rule says. | `check` scans what git carries, not what the disk holds | A P0 failure on an ignored build artefact is a false positive that teaches people to ignore the gate | `src/lib/git.mjs` | | Design rule IDs carried over unchanged | Consolidation must not invalidate existing citations | `MIGRATION.md` | | A check that cannot answer returns SKIP | `PASS` must mean verified, never assumed | `src/commands/check.mjs` | +| devia is for every agent | No agent is privileged: a surface that serves one must say why the others are not served, and record the gap. Absence of evidence about an agent is reported as SKIP, never as "unsupported" | `.devia/11_GAPS.md` G6, `src/commands/skills.mjs` | | The npm package is scoped, the command is not | npm refused the bare name `devia` as too similar to `degit`, `dexie` and `dva`; scoped names skip that filter. Docs say `npm i -D @schneiderjoseph/devia`, then `npx devia` | `package.json` | ## Current vs target diff --git a/.devia/02_SURFACES.md b/.devia/02_SURFACES.md index cf369b3..41a472e 100644 --- a/.devia/02_SURFACES.md +++ b/.devia/02_SURFACES.md @@ -37,7 +37,7 @@ say where, or `--yes` to accept it. Nothing is written before that question is s | `init` | `.devia/` (memory, `devia.json`, `impact-map.yaml`, `standard/`) | | `init`, `skills install` | `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/devia.mdc`, `.github/copilot-instructions.md`, `.windsurfrules` | | `skills install --skill` | `.cursor/skills/devia/SKILL.md`, `.claude/skills/devia/SKILL.md` | -| `skills install --global` | Outside the repository: the agent's own skills directory, so the contract applies to every project | +| `skills install --global` | Outside the repository, in each agent's own configuration: `~/.claude/skills/devia/`, `~/.codex/skills/devia/`, `~/.cursor/rules/devia.mdc`, `~/.gemini/GEMINI.md` when empty. Copilot and Windsurf report `SKIP` (`12_DEBT.md` D8) | `files` in `package.json` decides what npm ships. Adding a directory the CLI reads at runtime without adding it there ships a broken package — see `12_DEBT.md` before assuming it is covered. diff --git a/.devia/04_PERMISSIONS.md b/.devia/04_PERMISSIONS.md index 1c85768..9caf568 100644 --- a/.devia/04_PERMISSIONS.md +++ b/.devia/04_PERMISSIONS.md @@ -15,12 +15,17 @@ `init` keeps every existing memory file unless `--force` is passed, because those files hold decisions the tool did not make. -`skills install --global` is the single exception to the boundary: it installs the skill in the -agent's own configuration directory so it applies to every project. It is off by default, it -prints every path it writes, it keeps an edited file without `--force`, and for agents whose +`skills install --global` is the single exception to the boundary: it installs the contract in +each agent's own configuration directory so it applies to every project. It is off by default, +it prints every path it writes, it keeps an edited file without `--force`, and for agents whose user-level location cannot be determined it reports `SKIP` with the reason rather than guessing a path inside someone's home directory. +A file the agent owns and devia adds to (`~/.claude/skills/`, `~/.codex/skills/`, +`~/.cursor/rules/`) is written freely. A file the **user** owns and devia would replace +(`~/.gemini/GEMINI.md`) is written only when absent or empty; otherwise `SKIP` says so and the +content stays. `--force` overrides both, and says which paths it took. + ## Destructive operations | Operation | Where | Guard | diff --git a/.devia/11_GAPS.md b/.devia/11_GAPS.md index 99ad007..cbcfad4 100644 --- a/.devia/11_GAPS.md +++ b/.devia/11_GAPS.md @@ -20,3 +20,4 @@ Add one with `npx devia gap add "question"`. | ID | Question | Decided by | |---|---|---| +| G6 | Should devia install its contract at user level for every agent, or stay per-repository outside Claude Code? | devia is for every agent — user-level install built for Claude Code, Codex, Cursor and Gemini in 0.3.0 | diff --git a/.devia/12_DEBT.md b/.devia/12_DEBT.md index 96fc499..6aacdde 100644 --- a/.devia/12_DEBT.md +++ b/.devia/12_DEBT.md @@ -17,6 +17,7 @@ Add one with `npx devia debt add "what is missing"`. | D4 | GOVERNANCE | `rules/LIFECYCLE.md` | No rule has ever been superseded, so the `deprecated → superseded → removed` path is enforced by `validate-rules.mjs` but never exercised | P3 | 2026-09-03 | | D5 | SEC-002 | `src/commands/check.mjs` | The secret scanner is a pattern list, not entropy analysis; it will miss a novel key format and `SECURITY.md` says so rather than the check itself | P2 | 2026-09-03 | | D7 | OPS-001 | `package.json` | No linter or formatter is configured, so `devia check` reports the missing lint gate on this repository itself; adding one means accepting a devDependency under `ARC-004` | P2 | 2026-09-03 | +| D8 | AGT-001 | src/commands/skills.mjs | No user-level install for Copilot and Windsurf: their global configuration is editor settings rather than a file devia can place, so both report SKIP. Establish the real location before building | P2 | 2026-09-09 | ## Discharged diff --git a/CHANGELOG.md b/CHANGELOG.md index e3152da..dc9fb7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 0.3.0 — 2026-09-09 + +The standard is unchanged: `VERSION` stays at 0.1.0, no adopter needs `devia sync`. + +### devia is for every agent + +0.2.0 shipped `--global` serving Claude Code alone and reported the other agents as SKIP. Two of +those reasons were wrong: they came from an absence never verified. `~/.cursor/rules/` holds +user-level `.mdc` rules, and `~/.codex/skills/` uses the same `SKILL.md` convention as Claude +Code. The decision is recorded as G6: no agent is privileged. + +`devia skills install --global` now writes, each in the format the agent actually reads: + +| Agent | Path | File | +|---|---|---| +| Claude Code | `~/.claude/skills/devia/SKILL.md` | skill pack | +| Codex | `~/.codex/skills/devia/SKILL.md` | skill pack | +| Cursor | `~/.cursor/rules/devia.mdc` | rules adapter | +| Gemini | `~/.gemini/GEMINI.md` | universal contract, only when absent or empty | +| Copilot, Windsurf | — | `SKIP`: user-level configuration is editor settings, not a file devia can place (`12_DEBT.md` D8) | + +`CLAUDE_CONFIG_DIR` and `CODEX_HOME` are honoured when set. A directory the agent owns is +written to freely; a file the **user** owns is written only when absent or empty, and otherwise +skipped with the reason rather than replaced. `--force` overrides both and names every path. + ## 0.2.0 — 2026-09-09 The standard is unchanged: `VERSION` stays at 0.1.0 and no adopter needs `devia sync`. This diff --git a/package.json b/package.json index 5f326c5..183d0c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@schneiderjoseph/devia", - "version": "0.2.0", + "version": "0.3.0", "description": "One standard, one memory: engineering and design rules plus living project memory for AI coding agents", "type": "module", "license": "MIT", diff --git a/src/commands/skills.mjs b/src/commands/skills.mjs index e3a4085..3e61391 100644 --- a/src/commands/skills.mjs +++ b/src/commands/skills.mjs @@ -38,57 +38,84 @@ export function installAdapters(root, { force = false, agents = Object.keys(ADAP return { written, kept }; } +const SKILL_PACK = path.join("skills", "devia", "SKILL.md"); +const adapter = (file) => path.join("templates", "agents", file); + /** - * Where an agent keeps skills for every project, not one. + * Where each agent keeps a contract that applies to every project, not one. * - * Only agents whose user-level location devia can actually determine are listed. The rest are - * reported as SKIP with the reason: guessing a path in someone's home directory and writing to - * it is exactly the kind of confident wrong answer this tool exists to prevent. + * devia is for every agent, so an agent is listed here as soon as its user-level location is + * known — and reported as SKIP with the reason when it is not. Each entry carries the file the + * agent actually reads: a skill pack where the agent loads skills, its own rules format + * otherwise. Guessing a path inside someone's home directory is the confident wrong answer this + * tool exists to prevent, so absence of evidence is reported, never rounded up. */ -function globalSkillTargets() { +function globalTargets() { const home = os.homedir(); - const claudeDir = process.env.CLAUDE_CONFIG_DIR - ? path.resolve(process.env.CLAUDE_CONFIG_DIR) - : path.join(home, ".claude"); + const configDir = (envVar, fallback) => + process.env[envVar] ? path.resolve(process.env[envVar]) : path.join(home, fallback); + return { claude: { - target: path.join(claudeDir, "skills", "devia", "SKILL.md"), + target: path.join(configDir("CLAUDE_CONFIG_DIR", ".claude"), "skills", "devia", "SKILL.md"), + source: SKILL_PACK, + }, + codex: { + target: path.join(configDir("CODEX_HOME", ".codex"), "skills", "devia", "SKILL.md"), + source: SKILL_PACK, }, cursor: { - reason: "no user-level skill directory — use the per-project .cursor/rules/devia.mdc", + target: path.join(home, ".cursor", "rules", "devia.mdc"), + source: adapter("cursor.mdc"), + }, + gemini: { + // One file the user owns, not a directory devia can add to: written only when it is + // absent or empty, so a global instruction file is never silently replaced. + target: path.join(home, ".gemini", "GEMINI.md"), + source: adapter("AGENTS.md"), + onlyWhenEmpty: true, }, copilot: { - reason: "instructions are per-repository — .github/copilot-instructions.md", + reason: "user-level instructions live in the editor's settings, not a file devia can place", }, windsurf: { - reason: "rules are per-repository — .windsurfrules", + reason: "no user-level rules file — .windsurfrules is per repository", }, }; } /** - * Install the skill once for every project. This is the only path that writes outside `--root`, - * it happens only behind `--global`, and it prints every path it touches (04_PERMISSIONS.md). + * Install the contract once for every project. This is the only path that writes outside + * `--root`: it happens behind `--global`, and it prints every path it touches + * (`04_PERMISSIONS.md`). */ export function installGlobalSkill({ force = false } = {}) { - const skill = read(path.join(packageRoot, "skills", "devia", "SKILL.md")); const written = []; const kept = []; const skipped = []; - for (const [key, entry] of Object.entries(globalSkillTargets())) { + + for (const [key, entry] of Object.entries(globalTargets())) { if (!entry.target) { skipped.push([key, entry.reason]); continue; } - if (skill === null) { - skipped.push([key, "skill pack missing from the installed package"]); + const content = read(path.join(packageRoot, entry.source)); + if (content === null) { + skipped.push([key, `${entry.source} missing from the installed package`]); continue; } if (exists(entry.target) && !force) { - kept.push([key, entry.target]); - continue; + const current = read(entry.target) || ""; + if (entry.onlyWhenEmpty && current.trim()) { + skipped.push([key, `${path.basename(entry.target)} already has content — add the contract yourself`]); + continue; + } + if (current.trim()) { + kept.push([key, entry.target]); + continue; + } } - writeFile(entry.target, skill); + writeFile(entry.target, content); written.push([key, entry.target]); } return { written, kept, skipped }; diff --git a/tests/cli.test.mjs b/tests/cli.test.mjs index aa969c9..859fa15 100644 --- a/tests/cli.test.mjs +++ b/tests/cli.test.mjs @@ -207,27 +207,37 @@ test("closing a line keeps the open table contiguous", () => { test("skills install writes outside the project only behind --global", () => { const dir = scratch(); const home = fs.mkdtempSync(path.join(os.tmpdir(), "devia-home-")); - const claudeDir = path.join(home, ".claude"); - const skill = path.join(claudeDir, "skills", "devia", "SKILL.md"); + // os.homedir() reads USERPROFILE on Windows and HOME elsewhere: no real home is touched. + const at = { USERPROFILE: home, HOME: home }; + const claude = path.join(home, ".claude", "skills", "devia", "SKILL.md"); + const codex = path.join(home, ".codex", "skills", "devia", "SKILL.md"); + const cursor = path.join(home, ".cursor", "rules", "devia.mdc"); + const gemini = path.join(home, ".gemini", "GEMINI.md"); try { // Without the flag, nothing outside --root may be touched (04_PERMISSIONS.md). - devia(["skills", "install", "--root", dir], dir, { env: { CLAUDE_CONFIG_DIR: claudeDir } }); - assert.ok(!fs.existsSync(claudeDir), "a plain install must stay inside the repository"); + devia(["skills", "install", "--root", dir], dir, { env: at }); + assert.ok(!fs.existsSync(path.join(home, ".claude")), "a plain install stays in the repository"); - const res = devia(["skills", "install", "--global", "--root", dir], dir, { - env: { CLAUDE_CONFIG_DIR: claudeDir }, - }); - assert.ok(fs.existsSync(skill), "the user-level skill must be written"); - assert.match(fs.readFileSync(skill, "utf8"), /^---\nname: devia/); - // Every path it touches is printed, and agents it cannot place are SKIP with a reason. - assert.match(res.out, /SKILL\.md/); - assert.match(res.out, /cursor/); + const res = devia(["skills", "install", "--global", "--root", dir], dir, { env: at }); - fs.writeFileSync(skill, "edited by hand\n"); - devia(["skills", "install", "--global", "--root", dir], dir, { - env: { CLAUDE_CONFIG_DIR: claudeDir }, - }); - assert.match(fs.readFileSync(skill, "utf8"), /edited by hand/, "no overwrite without --force"); + // Each agent gets the file it actually reads: a skill pack where skills are loaded, the + // agent's own rules format otherwise. + for (const f of [claude, codex]) { + assert.match(fs.readFileSync(f, "utf8"), /^---\nname: devia/, `${f} must be the skill pack`); + } + assert.match(fs.readFileSync(cursor, "utf8"), /^---\ndescription: devia/); + assert.match(fs.readFileSync(gemini, "utf8"), /devia/); + // Every path is printed, and an agent devia cannot place is SKIP with the reason. + assert.match(res.out, /SKIP\s+copilot/); + assert.match(res.out, /SKIP\s+windsurf/); + + // A file the user has edited is never replaced without --force. + fs.writeFileSync(claude, "edited by hand\n"); + fs.writeFileSync(gemini, "my own global instructions\n"); + const again = devia(["skills", "install", "--global", "--root", dir], dir, { env: at }); + assert.match(fs.readFileSync(claude, "utf8"), /edited by hand/); + assert.match(fs.readFileSync(gemini, "utf8"), /my own global instructions/); + assert.match(again.out, /already has content/, "the user-owned file says why it was skipped"); } finally { fs.rmSync(dir, { recursive: true, force: true }); fs.rmSync(home, { recursive: true, force: true });