Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devia/01_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .devia/02_SURFACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 8 additions & 3 deletions .devia/04_PERMISSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions .devia/11_GAPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
1 change: 1 addition & 0 deletions .devia/12_DEBT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
69 changes: 48 additions & 21 deletions src/commands/skills.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
44 changes: 27 additions & 17 deletions tests/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
Loading