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
3 changes: 2 additions & 1 deletion .devia/02_SURFACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
| `devia doctor` | Adoption, drift, staleness | `src/commands/doctor.mjs` | 1 when there is no `.devia/` |
| `devia rules` | Query the registry | `src/commands/rules.mjs` | 1 when `--id` is unknown |
| `devia sync` | Refresh the vendored standard | `src/commands/sync.mjs` | 1 without `.devia/` |
| `devia skills` | Install adapters and the skill pack | `src/commands/skills.mjs` | 2 on a bad action |
| `devia skills` | Install adapters and the skill pack, per repository or `--global` | `src/commands/skills.mjs` | 2 on a bad action |
| `devia gap` / `devia debt` | Registry lines | `src/commands/registry.mjs` | 1 when the id is unknown |

Global flags: `--root`, `--json`, `--help`, `--version` (prints the CLI **and** standard
Expand All @@ -37,6 +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 |

`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
8 changes: 7 additions & 1 deletion .devia/04_PERMISSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@

| Allowed | Never |
|---|---|
| Create and update files under `<root>/.devia/` | Write outside `--root` |
| Create and update files under `<root>/.devia/` | Write outside `--root`, except `skills install --global` |
| Write the agent adapters at the repository root | Overwrite a file the user has edited, without `--force` |
| Read files in the target repository to produce evidence | Send anything over the network |
| Replace `.devia/standard/` wholesale on `sync` | Touch the project's own memory content on `sync` |

`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
user-level location cannot be determined it reports `SKIP` with the reason rather than guessing
a path inside someone's home directory.

## Destructive operations

| Operation | Where | Guard |
Expand Down
5 changes: 3 additions & 2 deletions .devia/10_NEVER_ALWAYS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
liability (`ARC-004`) cannot ship a tree of its own. Node built-ins, or argue it in the PR.
- **Never let a check return `PASS` when it could not determine the answer.** `SKIP` with the
reason. A false pass is worse than a missing check, exactly like a false registry line.
- **Never write outside `--root`.** Every path a command touches is derived from the context,
never from `process.cwd()` inside a command.
- **Never write outside `--root`** — the one exception is `skills install --global`, which is
off by default and prints every path it writes. Every other path a command touches is derived
from the context, never from `process.cwd()` inside a command.
- **Never overwrite an adopter's memory file without `--force`.** Those files hold decisions the
tool did not make.
- **Never add a directory the CLI reads at runtime without adding it to `files` in
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## 0.2.0 — 2026-09-09

The standard is unchanged: `VERSION` stays at 0.1.0 and no adopter needs `devia sync`. This
release is the CLI only.

### The skill, once for every project

- `devia skills install --global` installs the skill pack in the agent's own configuration
directory instead of one repository, so the contract applies everywhere. Claude Code is
supported (`~/.claude/skills/devia/SKILL.md`, or `CLAUDE_CONFIG_DIR` when set); Cursor,
Copilot and Windsurf report `SKIP` with the reason, because devia will not guess a path
inside someone's home directory
- It is the only command that writes outside `--root`: off by default, every path printed, an
edited file kept without `--force` (`04_PERMISSIONS.md`, `10_NEVER_ALWAYS.md`)

### Bootstrap, fixed

- The skill and every agent adapter told an agent to run `npx devia init`. Since the package is
scoped, that resolves to nothing in a repository that has not installed devia: `404 devia@*`.
They now say `npm i -D @schneiderjoseph/devia && npx devia init`, which is what a cold start
actually needs. Found while installing the skill system-wide, where the cold start is the
normal case rather than the exception

## 0.1.0 — 2026-09-03

First release. Devia consolidates three bodies of work into one maintained standard plus a
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.1.0",
"version": "0.2.0",
"description": "One standard, one memory: engineering and design rules plus living project memory for AI coding agents",
"type": "module",
"license": "MIT",
Expand Down
3 changes: 3 additions & 0 deletions skills/devia/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ ls .devia
**No `.devia/`** → initialise before writing any code:

```bash
npm i -D @schneiderjoseph/devia # the package is scoped, the command is not
npx devia init
```

Install first: `npx devia` only resolves once the package is a dependency of the project.

Then fill `.devia/00_OVERVIEW.md` from what the repository actually contains — stack, modules,
where the truth lives. Read the code to fill it; do not invent it. This is not paperwork: it is
the difference between a task and a guess (`AGT-002`).
Expand Down
75 changes: 75 additions & 0 deletions src/commands/skills.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { packageRoot, exists, read, writeFile } from "../lib/fs.mjs";
import { color, heading, status, line } from "../lib/ui.mjs";

Expand Down Expand Up @@ -36,6 +38,62 @@ export function installAdapters(root, { force = false, agents = Object.keys(ADAP
return { written, kept };
}

/**
* Where an agent keeps skills for 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.
*/
function globalSkillTargets() {
const home = os.homedir();
const claudeDir = process.env.CLAUDE_CONFIG_DIR
? path.resolve(process.env.CLAUDE_CONFIG_DIR)
: path.join(home, ".claude");
return {
claude: {
target: path.join(claudeDir, "skills", "devia", "SKILL.md"),
},
cursor: {
reason: "no user-level skill directory — use the per-project .cursor/rules/devia.mdc",
},
copilot: {
reason: "instructions are per-repository — .github/copilot-instructions.md",
},
windsurf: {
reason: "rules are per-repository — .windsurfrules",
},
};
}

/**
* 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).
*/
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())) {
if (!entry.target) {
skipped.push([key, entry.reason]);
continue;
}
if (skill === null) {
skipped.push([key, "skill pack missing from the installed package"]);
continue;
}
if (exists(entry.target) && !force) {
kept.push([key, entry.target]);
continue;
}
writeFile(entry.target, skill);
written.push([key, entry.target]);
}
return { written, kept, skipped };
}

export function installSkill(root, { force = false, agents = Object.keys(SKILL_TARGETS) } = {}) {
const skill = read(path.join(packageRoot, "skills", "devia", "SKILL.md"));
const written = [];
Expand Down Expand Up @@ -64,12 +122,29 @@ ${color.bold("devia skills")} — the same contract for every coding agent

devia skills list
devia skills install [--agent all|${Object.keys(ADAPTERS).join("|")}] [--force] [--skill]
devia skills install --global [--force]

--skill also install skills/devia/SKILL.md for Cursor and Claude Code
--global install the skill for every project, in the agent's own configuration
directory. The only command that writes outside --root; it prints
every path it touches
`.trim());
return flags.help ? 0 : 2;
}

if (flags.global) {
const res = installGlobalSkill({ force: Boolean(flags.force) });
heading("devia skills install --global");
for (const [key, target] of res.written) status("PASS", key, target);
for (const [key, target] of res.kept) status("SKIP", `${key} — already there`, target);
for (const [key, reason] of res.skipped) status("SKIP", key, reason);
line("");
line(color.dim(" The skill is now available in every project. It still expects each"));
line(color.dim(" repository to carry its own .devia/ — the skill says how to create one."));
line("");
return 0;
}

if (action === "list") {
heading("Adapters");
for (const [key, [, target]] of Object.entries(ADAPTERS)) {
Expand Down
3 changes: 2 additions & 1 deletion templates/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ This repository uses **devia**: a standard plus a living project memory in `.dev
4. Read the memory file for the surface you are touching — see
[`.devia/14_INDEX.md`](.devia/14_INDEX.md)

If `.devia/` is missing, run `npx devia init` and fill `00_OVERVIEW.md` before writing code.
If `.devia/` is missing, run `npm i -D @schneiderjoseph/devia && npx devia init`, then fill
`00_OVERVIEW.md` before writing code.

## While working

Expand Down
3 changes: 2 additions & 1 deletion templates/agents/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ work, updated with every change.
3. `.devia/00_OVERVIEW.md` — what this project is
4. The memory file for the surface you are touching (`.devia/14_INDEX.md`)

No `.devia/`? Run `npx devia init` and fill `00_OVERVIEW.md` before writing code.
No `.devia/`? Run `npm i -D @schneiderjoseph/devia && npx devia init`, then fill
`00_OVERVIEW.md` before writing code.

## Rules that override default behaviour

Expand Down
2 changes: 1 addition & 1 deletion templates/agents/cursor.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This repository runs on devia. `.devia/` is the project memory and it is not opt

1. Before the first edit, read `.devia/AGENTS.md`, `.devia/10_NEVER_ALWAYS.md`,
`.devia/00_OVERVIEW.md`, and the memory file for the surface you are touching
(`.devia/14_INDEX.md`). If `.devia/` does not exist, run `npx devia init` first (AGT-001,
(`.devia/14_INDEX.md`). If `.devia/` does not exist, run `npm i -D @schneiderjoseph/devia && npx devia init` first (AGT-001,
AGT-002).
2. Never invent an endpoint, field, config key or business rule. Unknown goes to
`.devia/11_GAPS.md` or to the human — never into the code as a quiet default (AGT-004,
Expand Down
2 changes: 1 addition & 1 deletion templates/agents/windsurfrules.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This repository runs on devia. `.devia/` is the project memory.

1. Read `.devia/AGENTS.md`, `.devia/10_NEVER_ALWAYS.md`, `.devia/00_OVERVIEW.md` and the memory
file for the surface you are touching before editing anything. No `.devia/`? Run
`npx devia init` first.
`npm i -D @schneiderjoseph/devia && npx devia init` first.
2. Never invent an endpoint, field, config key or business rule — record the unknown in
`.devia/11_GAPS.md` or ask.
3. Decided but not built goes to `.devia/12_DEBT.md`; never delete a line you did not discharge.
Expand Down
34 changes: 32 additions & 2 deletions tests/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const { FORCE_COLOR, ...cleanEnv } = process.env;

// stdout is a contract: `--json` is parsed from it. Merging stderr into it on failure turned a
// harmless runtime warning into unparseable JSON, and only for whoever had FORCE_COLOR set.
function devia(args, cwd, { allowFailure = false } = {}) {
const options = { cwd, encoding: "utf8", env: { ...cleanEnv, NO_COLOR: "1" } };
function devia(args, cwd, { allowFailure = false, env = {} } = {}) {
const options = { cwd, encoding: "utf8", env: { ...cleanEnv, NO_COLOR: "1", ...env } };
try {
return { code: 0, out: execFileSync(process.execPath, [bin, ...args], options), err: "" };
} catch (e) {
Expand Down Expand Up @@ -204,6 +204,36 @@ 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");
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");

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/);

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");
} finally {
fs.rmSync(dir, { recursive: true, force: true });
fs.rmSync(home, { recursive: true, force: true });
}
});

test("rules can be queried by id and by filter", () => {
const dir = scratch();
try {
Expand Down
Loading