devlyn-cli 4.3.24 → 4.3.27
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/README.md +27 -27
- package/bin/devlyn.js +42 -94
- package/bin/engines.js +14 -12
- package/bin/harness-activation.js +4 -2
- package/bin/harness-context.js +395 -0
- package/bin/harness-run.js +148 -0
- package/bin/instructions.js +71 -159
- package/bin/skill-history.json +7 -0
- package/config/skills/_shared/storage-checkout.py +1 -0
- package/config/skills/_shared/storage-client.py +349 -14
- package/config/skills/_shared/task-complete.py +99 -24
- package/config/skills/_shared/task-completion.md +7 -3
- package/config/skills/devlyn-ideate/SKILL.md +2 -2
- package/config/skills/devlyn-ideate/references/loop.md +2 -2
- package/config/skills/devlyn-ideate/scripts/queue.py +10 -3
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/storage-checkout.py +1 -0
- package/native/claude/skills/_shared/storage-client.py +349 -14
- package/native/claude/skills/_shared/task-complete.py +99 -24
- package/native/claude/skills/_shared/task-completion.md +7 -3
- package/native/claude/skills/devlyn-ideate/SKILL.md +2 -2
- package/native/claude/skills/devlyn-ideate/references/loop.md +2 -2
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +10 -3
- package/native/codex/.agents/plugins/marketplace.json +1 -1
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/storage-checkout.py +1 -0
- package/native/codex/plugins/devlyn/skills/_shared/storage-client.py +349 -14
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +99 -24
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +7 -3
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +2 -2
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +2 -2
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +10 -3
- package/package.json +2 -2
- package/release.json +43 -33
package/README.md
CHANGED
|
@@ -29,13 +29,13 @@ npx devlyn-cli
|
|
|
29
29
|
|
|
30
30
|
That's it. The installer asks:
|
|
31
31
|
|
|
32
|
-
1. **What** — `
|
|
32
|
+
1. **What** — `Codex · omp · Pi · Grok` (checked) and `Claude Code` (checked when this project already has a devlyn Claude install). Space toggles, Enter confirms.
|
|
33
33
|
2. **Where** — `This project` (default) or `Global — every project on this machine`.
|
|
34
|
-
3. **Optional skills & packs** — none checked; see [Optional Add-ons](#optional-add-ons) below. MCP servers are offered only with
|
|
34
|
+
3. **Optional skills & packs** — none checked; see [Optional Add-ons](#optional-add-ons) below. MCP servers are offered only with Claude Code.
|
|
35
35
|
|
|
36
|
-
In a project,
|
|
36
|
+
In a project, Codex, omp, Pi and Grok load the skills from `.agents/skills/`; Claude Code loads only `.claude/skills/`. The installer never writes `CLAUDE.md` or `AGENTS.md`: those files stay yours. Global installs skills only: `~/.agents/skills/` (omp, Pi, Grok) and `~/.codex/skills/` (Codex), plus `~/.claude/skills/` for Claude Code. Every target gets the `devlyn-ideate` skill and the `devlyn-engines` utility. In Codex / omp / Pi, invoke them as skills (`$devlyn-ideate`); in Claude Code and Grok they're slash commands (`/devlyn-ideate`). Rerunning refreshes skills. See [Migration from earlier versions](#migration-from-earlier-versions) for projects that carry an earlier copy of the principles.
|
|
37
37
|
|
|
38
|
-
Without prompts, `npx devlyn-cli -y` installs
|
|
38
|
+
Without prompts, `npx devlyn-cli -y` installs `.agents/skills/` plus every target this project already has; add `--claude` for Claude Code. With `--global` it installs for every project on this machine, plus `~/.claude/skills/` with `--claude` or when it already has a devlyn install. The installer leaves Git to you: it prints no commit guidance after a project install or update. When Git ignores some, but not all, files of an installed skill folder, it prints one line with their count and an example ignore rule.
|
|
39
39
|
|
|
40
40
|
`npx devlyn-cli update` — Update every devlyn install already here (this project and your global skill folders) to this version; installs nothing new.
|
|
41
41
|
|
|
@@ -43,13 +43,17 @@ Without prompts, `npx devlyn-cli -y` installs AGENTS.md + `.agents/skills/` plus
|
|
|
43
43
|
|
|
44
44
|
## How It Works — Principles and Loops
|
|
45
45
|
|
|
46
|
-
The
|
|
46
|
+
The Devlyn defaults carry the North Star, seven principles (no workaround, no overengineering, no guesswork, worldclass, best practice, optimized, production ready) and three discipline rules, plus short pointers to the skills. Each session gets them from the release, through the Devlyn app or `devlyn run`, so projects hold no copy and a new release reaches every project at once. A clone without either gets no Devlyn defaults. Agents do the work directly under them; for work that splits into tasks, ideate designs a loop and drains it:
|
|
47
47
|
|
|
48
48
|
```
|
|
49
49
|
intent → direct work under the installed principles, or an ideate loop → ship
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
Non-Claude agents (Codex / omp / Pi / Grok): the
|
|
52
|
+
Non-Claude agents (Codex / omp / Pi / Grok): the default choice installs the skills for them. In Codex / omp / Pi, use `$devlyn-ideate`; in Grok, use `/devlyn-ideate`, the same slash-command form as Claude Code. A drain from Pi or Grok needs [a pinned executor](#executor--devlyn-engines).
|
|
53
|
+
|
|
54
|
+
### Terminal sessions — `devlyn run`
|
|
55
|
+
|
|
56
|
+
`devlyn run claude`, `devlyn run codex` or `devlyn run omp` starts the engine in this terminal with the same principles and skills the Devlyn app delivers, from the selected release. Arguments after the engine name go to the engine. See [docs/harness-run.md](docs/harness-run.md).
|
|
53
57
|
|
|
54
58
|
### Plan and drain loops — `/devlyn-ideate`
|
|
55
59
|
|
|
@@ -62,7 +66,7 @@ Give ideate an intent or a document. It writes a loop package — a meta-prompt
|
|
|
62
66
|
| `/devlyn-ideate status` | Pending, active, accepted and failed counts; the next runnable task; delivery and recovery blockers. |
|
|
63
67
|
| `/devlyn-ideate drain` | Runs the queue serially and hands-free. |
|
|
64
68
|
|
|
65
|
-
A bare intent is planned and, when your request authorizes the work, added and drained without another confirmation; no arguments shows status. Questions come only when the answer changes behavior, scope, data semantics, acceptance or delivery, each with a recommended answer; `--autonomous` plans without questions, recording ordinary low-consequence, reversible defaults within the request, including user-visible details; material ambiguity in intended outcomes, persistent data or state semantics, or public surface beyond the request stops planning with a concrete question. Each task runs in its own worktree with the executor pinned by `/devlyn-engines` (default: the CLI you opened; Pi and Grok need a pin) under your
|
|
69
|
+
A bare intent is planned and, when your request authorizes the work, added and drained without another confirmation; no arguments shows status. Questions come only when the answer changes behavior, scope, data semantics, acceptance or delivery, each with a recommended answer; `--autonomous` plans without questions, recording ordinary low-consequence, reversible defaults within the request, including user-visible details; material ambiguity in intended outcomes, persistent data or state semantics, or public surface beyond the request stops planning with a concrete question. Each task runs in its own worktree with the executor pinned by `/devlyn-engines` (default: the CLI you opened; Pi and Grok need a pin) under the Devlyn defaults and your project's CLAUDE.md/AGENTS.md. Tasks start from committed state, the branch you added the loop on or the remote base for `auto`/`pr`, so a task's worktree holds only what is committed there. A task would run without installed CLAUDE.md/AGENTS.md that exist only in this checkout, so add stops while they differ from HEAD, and drain waits while they differ from the task's start commit. It is marked `[x]` only when its declared checks pass on its committed source and its required reviews cover it: the checks run through the drain's runner (a wholly clean run the executor lists for that source is reused; the drain runs any other check itself), and review coverage is attested by records the executor submits; a failed task becomes `[F]` with its reason and blocks only its dependents. An interrupted drain resumes without repeating accepted work. `--local-only` (or `--no-push`) keeps delivery local: each accepted task stays on its `devlyn/<loop-id>/<task-id>` branch in a retained worktree, nothing is merged or pushed, and the drain report's `Bring into` command brings the result into your branch, stopping if your files cannot be preserved.
|
|
66
70
|
|
|
67
71
|
### Delivery
|
|
68
72
|
|
|
@@ -107,29 +111,25 @@ An account server you never list remains available.
|
|
|
107
111
|
|
|
108
112
|
### Migration from earlier versions
|
|
109
113
|
|
|
110
|
-
Reinstall with the new version to refresh
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
is kept conservatively, even when it may contain obsolete defaults.
|
|
124
|
-
Malformed or duplicate managed markers still stop with an exact `.backup`, new
|
|
125
|
-
defaults in `.incoming`, and a `.merge.md` guide; the original remains unchanged.
|
|
114
|
+
Reinstall with the new version to refresh skills for the selected targets. Install and
|
|
115
|
+
update leave `CLAUDE.md` and `AGENTS.md` byte-identical.
|
|
116
|
+
|
|
117
|
+
Earlier installs put the principles in those files, inside a checksum-marked block or as
|
|
118
|
+
a whole template. [Cleanup](docs/harness-cleanup.md) removes an intact copy, keeps the text
|
|
119
|
+
around it byte-for-byte and deletes a file that held only the copy; a `@AGENTS.md` import
|
|
120
|
+
left without its file is harmless. Exact pre-removal backups are saved in
|
|
121
|
+
`.devlyn/instructions/`; pre-existing symlinks or non-directory entries at `.devlyn/` or
|
|
122
|
+
`.devlyn/instructions/` stop the removal without touching the file.
|
|
123
|
+
|
|
124
|
+
A copy someone edited stays exactly as it is, as does any text Devlyn cannot prove it wrote:
|
|
125
|
+
a quoted example, or malformed or duplicate markers. Devlyn's defaults replace a remaining
|
|
126
|
+
block for each session.
|
|
126
127
|
Portability refreshes the offline fingerprints from Git history using
|
|
127
128
|
`node scripts/update-instruction-templates.js` before testing; publish ships that tested tarball.
|
|
128
129
|
Installation needs no Git or network.
|
|
129
|
-
Use 3.1.3 or newer consistently: older installers still overwrite `CLAUDE.md`.
|
|
130
130
|
|
|
131
131
|
Upgrading to 4.1.0: `npx devlyn-cli -y` in a 4.x project keeps its Claude Code install
|
|
132
|
-
current and adds
|
|
132
|
+
current and adds `.agents/skills/`. Earlier releases put the Codex, omp, Pi
|
|
133
133
|
and Grok skills in `~/.codex/skills`, `~/.agents/skills` and `~/.grok/skills`; a project
|
|
134
134
|
install no longer refreshes them and names each one it finds. Refresh them with `--global`
|
|
135
135
|
or delete them; global installs and `update` clean Devlyn’s retired `~/.grok/skills` entries automatically.
|
|
@@ -224,7 +224,7 @@ Selected during install. Run `npx devlyn-cli` again to add more.
|
|
|
224
224
|
</details>
|
|
225
225
|
|
|
226
226
|
<details>
|
|
227
|
-
<summary><strong>MCP Servers</strong> — installed via <code>claude mcp add</code>, offered with the
|
|
227
|
+
<summary><strong>MCP Servers</strong> — installed via <code>claude mcp add</code>, offered with the Claude Code choice</summary>
|
|
228
228
|
|
|
229
229
|
| Server | Description |
|
|
230
230
|
|---|---|
|
|
@@ -246,7 +246,7 @@ Only `__pycache__` directories containing exclusively regular `*.pyc` files coun
|
|
|
246
246
|
|
|
247
247
|
- **Node.js 20 (20.17+), 22 (22.13+) or 23.5+** and npm
|
|
248
248
|
- **Python 3.11+** available as `python3` for the harness, and **Git** for safe installation and the harness
|
|
249
|
-
- **An agent CLI** installed and configured: Codex, omp, Pi or Grok
|
|
249
|
+
- **An agent CLI** installed and configured: Codex, omp, Pi or Grok, or [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
|
|
250
250
|
- **For `auto`/`pr` delivery**, an `origin` remote naming one GitHub repository and an authenticated [`gh`](https://cli.github.com/)
|
|
251
251
|
|
|
252
252
|
On native Windows, use native Node/npm and Python plus Git for Windows Bash for the shipped shell wrapper. Run `npx devlyn-cli -y` in the project (add `--claude` for Claude Code). Skill folders follow the Agent Skills naming standard (for example `devlyn-ideate`), so a project that commits them checks out on Windows. Harness text is UTF-8 without requiring `PYTHONUTF8`.
|
package/bin/devlyn.js
CHANGED
|
@@ -10,7 +10,7 @@ const { parseTree, findNodeAtLocation, applyEdits } = require('jsonc-parser');
|
|
|
10
10
|
const CONFIG_SOURCE = path.join(__dirname, '..', 'config');
|
|
11
11
|
const OPTIONAL_SKILLS_SOURCE = path.join(__dirname, '..', 'optional-skills');
|
|
12
12
|
const PKG = require('../package.json');
|
|
13
|
-
const {
|
|
13
|
+
const { removeInstructions, InstructionError, holdsDevlynDefaults } = require('./instructions');
|
|
14
14
|
const { contentHash, fingerprint, gitEnvironment, isIgnoredEntry, trackedPaths } = require('./skill-ownership');
|
|
15
15
|
const SKILL_HISTORY = require('./skill-history.json');
|
|
16
16
|
const CLEANUP_HISTORY = require('./cleanup-history.json');
|
|
@@ -105,18 +105,13 @@ function hasDevlynInstall(root) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// A devlyn Claude install in this scope. Globally only the marker a `--global --claude` run
|
|
108
|
-
// writes counts; in a project also a devlyn skill (0.6.0 and later)
|
|
109
|
-
//
|
|
110
|
-
// while ignoring .claude/. An optional devlyn skill is not one: 4.0.1 put those into
|
|
111
|
-
// .claude/skills without the Claude target. A CLAUDE.md link (often to AGENTS.md) is not the
|
|
112
|
-
// Claude target's file: updateInstructions refuses links.
|
|
108
|
+
// writes counts; in a project also a devlyn skill (0.6.0 and later) or command (0.2-0.5). An
|
|
109
|
+
// optional devlyn skill is not one: 4.0.1 put those into .claude/skills without the Claude target.
|
|
113
110
|
function hasDevlynClaude(global) {
|
|
114
111
|
const claudeDir = path.dirname(skillRoots('claude', global)[0]);
|
|
115
112
|
if (global) return fs.existsSync(path.join(claudeDir, 'skills', DEVLYN_INSTALL_MARKER));
|
|
116
113
|
const names = (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir) : []);
|
|
117
114
|
const optional = new Set(OPTIONAL_ADDONS.map((addon) => addon.name));
|
|
118
|
-
const instructions = path.join(path.dirname(claudeDir), 'CLAUDE.md');
|
|
119
|
-
const claude = fs.lstatSync(instructions, { throwIfNoEntry: false })?.isFile() ? fs.readFileSync(instructions, 'utf8') : '';
|
|
120
115
|
const root = path.join(claudeDir, 'skills');
|
|
121
116
|
const deprecated = new Set([...deprecatedSkillPaths(root)].map((full) => path.basename(full).toLowerCase()));
|
|
122
117
|
return names(root).some((name) => {
|
|
@@ -124,8 +119,7 @@ function hasDevlynClaude(global) {
|
|
|
124
119
|
return name === DEVLYN_INSTALL_MARKER
|
|
125
120
|
|| (name.startsWith('devlyn') && !optional.has(name.replace(/[:\uF03A]/g, '-')));
|
|
126
121
|
})
|
|
127
|
-
|| names(path.join(claudeDir, 'commands')).some((name) => name.startsWith('devlyn.'))
|
|
128
|
-
|| holdsDevlynDefaults('CLAUDE.md', claude) || importsAgentsDefaults(claude);
|
|
122
|
+
|| names(path.join(claudeDir, 'commands')).some((name) => name.startsWith('devlyn.'));
|
|
129
123
|
}
|
|
130
124
|
|
|
131
125
|
// Exact Devlyn command names removed by previous versions, in either scope.
|
|
@@ -764,67 +758,19 @@ function updateProjectGitignore(markerEntry) {
|
|
|
764
758
|
|
|
765
759
|
// With the Claude target in the same run, an AGENTS.md that links to this project's CLAUDE.md (a
|
|
766
760
|
// common Claude-first setup) gets its devlyn block through CLAUDE.md. Any other link is refused.
|
|
767
|
-
|
|
768
|
-
// file Git for Windows checks out in a symlink's place when core.symlinks is off (its whole
|
|
769
|
-
// content is the link target). Compared by real path: a link to another hard link of CLAUDE.md
|
|
770
|
-
// would go stale once CLAUDE.md is replaced.
|
|
771
|
-
function agentsMdIsClaudeMd() {
|
|
772
|
-
const agents = path.join(projectDir(), 'AGENTS.md');
|
|
773
|
-
const claude = path.join(projectDir(), 'CLAUDE.md');
|
|
774
|
-
const stat = fs.lstatSync(agents, { throwIfNoEntry: false });
|
|
775
|
-
if (!stat || !fs.lstatSync(claude, { throwIfNoEntry: false })?.isFile()) return false;
|
|
776
|
-
const target = stat.isSymbolicLink() ? agents
|
|
777
|
-
: stat.isFile() && stat.size < 256 ? path.resolve(projectDir(), fs.readFileSync(agents, 'utf8'))
|
|
778
|
-
: null;
|
|
779
|
-
return target !== null && fs.existsSync(target) && fs.realpathSync.native(target) === fs.realpathSync.native(claude);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function installAgentsProject(withClaude, installedSkills = new Map()) {
|
|
783
|
-
if (!agentsMdIsClaudeMd()) {
|
|
784
|
-
updateInstructions('AGENTS.md');
|
|
785
|
-
} else if (withClaude) {
|
|
786
|
-
log(' → AGENTS.md is CLAUDE.md here, so it gets the devlyn block written there', 'dim');
|
|
787
|
-
} else {
|
|
788
|
-
throw new InstructionError('AGENTS.md is CLAUDE.md under another name here. Choose CLAUDE.md as well '
|
|
789
|
-
+ '(npx devlyn-cli -y --claude) so the devlyn block is written once, into CLAUDE.md.');
|
|
790
|
-
}
|
|
761
|
+
function installAgentsProject(installedSkills = new Map()) {
|
|
791
762
|
installCoreSkills(skillRoots('agents', false)[0], installedSkills);
|
|
792
763
|
updateProjectGitignore('.agents/skills/.devlyn-install.json');
|
|
793
764
|
}
|
|
794
765
|
|
|
795
|
-
//
|
|
796
|
-
// An AGENTS.md that is CLAUDE.md under another name holds CLAUDE.md's own defaults.
|
|
797
|
-
function importsAgentsDefaults(claude) {
|
|
798
|
-
const agents = path.join(projectDir(), 'AGENTS.md');
|
|
799
|
-
return importsAgentsMd(claude) && !agentsMdIsClaudeMd() && fs.statSync(agents, { throwIfNoEntry: false })?.isFile() === true
|
|
800
|
-
&& holdsDevlynDefaults('AGENTS.md', fs.readFileSync(agents, 'utf8'));
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// Claude Code reads AGENTS.md only where no CLAUDE.md exists, or through a CLAUDE.md that imports it. So a new
|
|
804
|
-
// CLAUDE.md imports an existing AGENTS.md, and a CLAUDE.md importing an AGENTS.md that holds devlyn defaults holds no
|
|
805
|
-
// copy of its own, not even one it got while AGENTS.md lacked the block. Only the AGENTS.md target updates those defaults,
|
|
806
|
-
// together with .agents/skills, so the Claude target stops while they need an update.
|
|
807
|
-
function installClaudeInstructions() {
|
|
808
|
-
const file = path.join(projectDir(), 'CLAUDE.md');
|
|
809
|
-
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
810
|
-
const initial = !stat && fs.statSync(path.join(projectDir(), 'AGENTS.md'), { throwIfNoEntry: false })?.isFile() ? '@AGENTS.md\n' : '';
|
|
811
|
-
const imported = importsAgentsDefaults(stat?.isFile() ? fs.readFileSync(file, 'utf8') : initial);
|
|
812
|
-
if (imported && updateInstructions('AGENTS.md', '', true, false)) {
|
|
813
|
-
throw new InstructionError('Claude Code reads AGENTS.md here, and its devlyn defaults need an update. Codex, omp, Pi and Grok '
|
|
814
|
-
+ 'read them too, with .agents/skills, so choose AGENTS.md as well (npx devlyn-cli -y --claude) to update both together.');
|
|
815
|
-
}
|
|
816
|
-
updateInstructions('CLAUDE.md', initial, !imported);
|
|
817
|
-
if (imported) log(' → CLAUDE.md imports AGENTS.md, which holds the devlyn block', 'dim');
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// Project CLAUDE.md and .claude/: skills and settings.
|
|
766
|
+
// Project .claude/: skills and settings.
|
|
821
767
|
function installClaudeCore(installedSkills = new Map()) {
|
|
822
768
|
const skillsDir = skillRoots('claude', false)[0];
|
|
823
769
|
const targetDir = path.dirname(skillsDir);
|
|
824
770
|
// Read before any write, so settings this install cannot merge leave the Claude target as it was.
|
|
825
771
|
const settingsPath = path.join(targetDir, 'settings.json');
|
|
826
772
|
const unmergeable = (reason) => new InstructionError(`Cannot merge .claude/settings.json: ${reason}. `
|
|
827
|
-
+ '
|
|
773
|
+
+ '.claude/ is unchanged; fix the file and rerun the same install command.');
|
|
828
774
|
const settingsStat = fs.lstatSync(settingsPath, { throwIfNoEntry: false });
|
|
829
775
|
if (settingsStat && !settingsStat.isFile()) throw unmergeable('not a regular file');
|
|
830
776
|
let settings = {};
|
|
@@ -843,7 +789,6 @@ function installClaudeCore(installedSkills = new Map()) {
|
|
|
843
789
|
&& (!settings.env || typeof settings.env !== 'object' || Array.isArray(settings.env))) {
|
|
844
790
|
throw unmergeable('env must be a JSON object');
|
|
845
791
|
}
|
|
846
|
-
installClaudeInstructions();
|
|
847
792
|
installCoreSkills(skillsDir, installedSkills);
|
|
848
793
|
updateProjectGitignore('.claude/skills/.devlyn-install.json');
|
|
849
794
|
|
|
@@ -1073,34 +1018,32 @@ function install(targets, global, optionalNames = [], plan, afterInstall = () =>
|
|
|
1073
1018
|
const home = fs.statSync(os.homedir(), { bigint: true, throwIfNoEntry: false });
|
|
1074
1019
|
const here = fs.statSync(projectDir(), { bigint: true });
|
|
1075
1020
|
if (!global && home?.dev === here.dev && home?.ino === here.ino) {
|
|
1076
|
-
throw new InstructionError('This project is your home folder, so its
|
|
1077
|
-
+ '
|
|
1021
|
+
throw new InstructionError('This project is your home folder, so its .claude/settings.json would apply to every project. '
|
|
1022
|
+
+ 'Run from a project folder, or use --global for skills only.');
|
|
1078
1023
|
}
|
|
1079
1024
|
const roots = targets.flatMap((target) => skillRoots(target, global));
|
|
1080
1025
|
return withUserSkillLocks(global ? roots : [], () => {
|
|
1081
1026
|
const installedSkills = new Map();
|
|
1082
1027
|
if (!global) readProjectGitignore();
|
|
1083
1028
|
plan ??= preflightSkills(roots, optionalNames);
|
|
1084
|
-
// AGENTS.md first, whatever order the menu gave: a new CLAUDE.md holds the block only when AGENTS.md does not.
|
|
1085
1029
|
for (const target of ['agents', 'claude'].filter((name) => targets.includes(name))) {
|
|
1086
1030
|
if (global) skillRoots(target, true).forEach((root) => installCoreSkills(root, installedSkills));
|
|
1087
|
-
else if (target === 'agents') installAgentsProject(
|
|
1031
|
+
else if (target === 'agents') installAgentsProject(installedSkills);
|
|
1088
1032
|
else installClaudeCore(installedSkills);
|
|
1089
1033
|
}
|
|
1090
1034
|
for (const name of optionalNames) installLocalSkill(name, roots, installedSkills);
|
|
1091
1035
|
afterInstall();
|
|
1092
1036
|
log(`\n✅ devlyn ${PKG.version} installed`, 'green');
|
|
1093
|
-
|
|
1037
|
+
log('The Devlyn app gives every session the principles; in a terminal, start with: npx devlyn-cli run <claude|codex|omp>');
|
|
1094
1038
|
noticeGlobalDrift(roots);
|
|
1095
|
-
cleanupScope(global ? os.homedir() : projectDir(), global
|
|
1096
|
-
global ? [] : targets.map((target) => target === 'agents' ? 'AGENTS.md' : 'CLAUDE.md'));
|
|
1039
|
+
cleanupScope(global ? os.homedir() : projectDir(), global);
|
|
1097
1040
|
if (!global) noticeIgnoredInstallFiles(installedSkills);
|
|
1098
1041
|
return roots;
|
|
1099
1042
|
});
|
|
1100
1043
|
}
|
|
1101
1044
|
|
|
1102
1045
|
// Cleanup visits existing paths in the command's scope; it never creates an install.
|
|
1103
|
-
function cleanupScope(base, global
|
|
1046
|
+
function cleanupScope(base, global) {
|
|
1104
1047
|
const warned = new Set();
|
|
1105
1048
|
let found = false;
|
|
1106
1049
|
const warning = (file, error) => {
|
|
@@ -1206,15 +1149,15 @@ function cleanupScope(base, global, activeInstructions = []) {
|
|
|
1206
1149
|
log(' ✕ .claude/settings.json retired entries (removed)', 'dim');
|
|
1207
1150
|
}
|
|
1208
1151
|
});
|
|
1209
|
-
|
|
1210
|
-
|
|
1152
|
+
// CLAUDE.md and AGENTS.md stay as they are here: only a reviewed cleanup changes them.
|
|
1153
|
+
const instructions = global ? [] : ['GEMINI.md', '.cursorrules', '.windsurfrules', '.github/copilot-instructions.md'];
|
|
1154
|
+
for (const name of instructions) {
|
|
1211
1155
|
const file = path.join(base, name);
|
|
1212
1156
|
attempt(file, () => {
|
|
1213
1157
|
if (!realParents(file) || !fs.lstatSync(file, { throwIfNoEntry: false })?.isFile()) return;
|
|
1214
|
-
|
|
1215
|
-
if (!updateInstructions(name, '', false, false, base, evaluatorOnly)) return;
|
|
1158
|
+
if (!removeInstructions(name, false, base)) return;
|
|
1216
1159
|
found = true;
|
|
1217
|
-
|
|
1160
|
+
removeInstructions(name, true, base);
|
|
1218
1161
|
});
|
|
1219
1162
|
}
|
|
1220
1163
|
return found;
|
|
@@ -1360,7 +1303,7 @@ async function init({ yes, claude, global }) {
|
|
|
1360
1303
|
install(targets, global);
|
|
1361
1304
|
log('\n💡 Add optional addons later: run `npx devlyn-cli` without -y', 'dim');
|
|
1362
1305
|
const hints = [
|
|
1363
|
-
...(targets.includes('claude') ? [] : [`${global ? '~/.claude/skills' : '
|
|
1306
|
+
...(targets.includes('claude') ? [] : [`${global ? '~/.claude/skills' : '.claude/'} for Claude Code: add --claude`]),
|
|
1364
1307
|
...(global ? [] : ['every project on this machine: add --global']),
|
|
1365
1308
|
];
|
|
1366
1309
|
if (hints.length > 0) log(` ${hints.join(' · ')}`, 'dim');
|
|
@@ -1370,8 +1313,8 @@ async function init({ yes, claude, global }) {
|
|
|
1370
1313
|
}
|
|
1371
1314
|
|
|
1372
1315
|
const targetOptions = [
|
|
1373
|
-
{ key: 'agents', name: '
|
|
1374
|
-
{ key: 'claude', name: '
|
|
1316
|
+
{ key: 'agents', name: 'Codex · omp · Pi · Grok', desc: '.agents/skills' },
|
|
1317
|
+
{ key: 'claude', name: 'Claude Code', desc: '.claude/ (skills, settings)' },
|
|
1375
1318
|
];
|
|
1376
1319
|
const preselected = claude || hasDevlynClaude(false) || (global && hasDevlynClaude(true)) ? [0, 1] : [0];
|
|
1377
1320
|
const targets = (await multiSelect('What to install', targetOptions, preselected)).map((option) => option.key);
|
|
@@ -1405,12 +1348,13 @@ async function init({ yes, claude, global }) {
|
|
|
1405
1348
|
function showHelp() {
|
|
1406
1349
|
showLogo();
|
|
1407
1350
|
log('Usage:', 'green');
|
|
1408
|
-
log(' npx devlyn-cli Install/update devlyn: choose
|
|
1351
|
+
log(' npx devlyn-cli Install/update devlyn skills: choose which agents and where (this project or global)');
|
|
1409
1352
|
log(' npx devlyn-cli update Update every devlyn install already here (this project and your global skill folders) to this version; installs nothing new');
|
|
1410
|
-
log(' npx devlyn-cli -y Without prompts:
|
|
1411
|
-
log(' npx devlyn-cli -y --claude Also install
|
|
1353
|
+
log(' npx devlyn-cli -y Without prompts: .agents/skills, plus .claude/ if this project has it');
|
|
1354
|
+
log(' npx devlyn-cli -y --claude Also install .claude/ for Claude Code');
|
|
1412
1355
|
log(' npx devlyn-cli -y --global Skills only, for every project on this machine (~/.agents/skills, ~/.codex/skills; ~/.claude/skills with --claude, or when it already has a devlyn install)');
|
|
1413
1356
|
log(' npx devlyn-cli list List available skills');
|
|
1357
|
+
log(' npx devlyn-cli run <engine> Start claude, codex or omp in this terminal with the devlyn principles and skills');
|
|
1414
1358
|
log(' npx devlyn-cli --help Show this help\n');
|
|
1415
1359
|
log('Optional skills (select during install):', 'green');
|
|
1416
1360
|
OPTIONAL_ADDONS.filter((a) => a.type === 'local').forEach((skill) => {
|
|
@@ -1704,10 +1648,10 @@ function cleanupProjectFiles(base, includedHooks = new Set(), callerBase = base)
|
|
|
1704
1648
|
return items;
|
|
1705
1649
|
}
|
|
1706
1650
|
|
|
1707
|
-
//
|
|
1708
|
-
// byte-for-byte (
|
|
1651
|
+
// The release gives each session the principles (docs/harness-run.md), so an intact copy in a project
|
|
1652
|
+
// file is removed, owner text around it kept byte-for-byte (removeInstructions). A copy someone
|
|
1653
|
+
// edited stays exactly as it is: only recognized content is ever removed.
|
|
1709
1654
|
function cleanupInstructions(base) {
|
|
1710
|
-
const { inspectInstruction } = require('./harness-status');
|
|
1711
1655
|
const items = [];
|
|
1712
1656
|
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
1713
1657
|
const file = path.join(base, name);
|
|
@@ -1716,10 +1660,10 @@ function cleanupInstructions(base) {
|
|
|
1716
1660
|
let text;
|
|
1717
1661
|
try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(file)); }
|
|
1718
1662
|
catch { continue; }
|
|
1719
|
-
if (
|
|
1720
|
-
|
|
1721
|
-
if (
|
|
1722
|
-
items.push({ kind: 'instructions', path: file, base, action: '
|
|
1663
|
+
if (removeInstructions(name, false, base)) {
|
|
1664
|
+
items.push({ kind: 'instructions', path: file, base, action: 'remove', reasons: [] });
|
|
1665
|
+
} else if (holdsDevlynDefaults(name, text)) {
|
|
1666
|
+
items.push({ kind: 'instructions', path: file, base, action: 'skip', reasons: [cleanupReason('modified-copy')] });
|
|
1723
1667
|
}
|
|
1724
1668
|
}
|
|
1725
1669
|
return items;
|
|
@@ -2071,6 +2015,11 @@ function cleanupValidateItem(item) {
|
|
|
2071
2015
|
function cleanupApplyItem(item) {
|
|
2072
2016
|
const invalid = cleanupValidateItem(item);
|
|
2073
2017
|
if (invalid) return invalid;
|
|
2018
|
+
// Before the generic removal: only the proven copy goes, never the owner's file.
|
|
2019
|
+
if (item.kind === 'instructions') {
|
|
2020
|
+
removeInstructions(path.basename(item.path), true, path.dirname(item.path));
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2074
2023
|
if (cleanupAction(item) === 'remove') {
|
|
2075
2024
|
if (item.kind === 'legacy-hook') {
|
|
2076
2025
|
const { safe, settings } = cleanupReadSettings(item.base);
|
|
@@ -2087,10 +2036,6 @@ function cleanupApplyItem(item) {
|
|
|
2087
2036
|
else if (after !== text) replaceFile(item.path, after);
|
|
2088
2037
|
return null;
|
|
2089
2038
|
}
|
|
2090
|
-
if (item.kind === 'instructions') {
|
|
2091
|
-
updateInstructions(path.basename(item.path), '', true, true, path.dirname(item.path));
|
|
2092
|
-
return null;
|
|
2093
|
-
}
|
|
2094
2039
|
return cleanupReason('unknown-item');
|
|
2095
2040
|
}
|
|
2096
2041
|
|
|
@@ -2169,7 +2114,7 @@ function cleanupSupport(item) {
|
|
|
2169
2114
|
}
|
|
2170
2115
|
|
|
2171
2116
|
function cleanupCanInclude(item) {
|
|
2172
|
-
return !cleanupSupport(item) && item.action === 'skip'
|
|
2117
|
+
return !cleanupSupport(item) && item.kind !== 'instructions' && item.action === 'skip'
|
|
2173
2118
|
&& item.reasons.length === 1 && item.reasons[0].code === 'modified-copy';
|
|
2174
2119
|
}
|
|
2175
2120
|
|
|
@@ -2391,7 +2336,7 @@ function cleanupDeliveryCommit(worktree, items, current, release, scope, base) {
|
|
|
2391
2336
|
}
|
|
2392
2337
|
const add = cleanupGit(worktree, ['add', '-u', '--', ...applied]);
|
|
2393
2338
|
const commit = add.status === 0 && cleanupGit(worktree, ['-c', 'user.useConfigOnly=true', 'commit', '-q', '-m',
|
|
2394
|
-
`chore(devlyn): remove legacy devlyn-cli copies\n\nDevlyn now delivers devlyn-cli ${PKG.version} to each session it starts, so the copies earlier\ninstalls committed here are no longer used
|
|
2339
|
+
`chore(devlyn): remove legacy devlyn-cli copies\n\nDevlyn now delivers devlyn-cli ${PKG.version} to each session it starts, so the copies earlier\ninstalls committed here, the principles block included, are no longer used.`]);
|
|
2395
2340
|
if (!commit || commit.status !== 0) return release(cleanupReason('delivery-failed', { detail: (commit || add).stderr.trim() }));
|
|
2396
2341
|
// This check must observe the committed delivery worktree, not project the still-unclean remote.
|
|
2397
2342
|
const inputs = { machine: false, projects: [worktree], accountHomes: [] };
|
|
@@ -2527,6 +2472,9 @@ async function main() {
|
|
|
2527
2472
|
const command = args[0];
|
|
2528
2473
|
|
|
2529
2474
|
switch (command) {
|
|
2475
|
+
case 'run':
|
|
2476
|
+
await require('./harness-run').runMain(args.slice(1));
|
|
2477
|
+
break;
|
|
2530
2478
|
case 'cleanup':
|
|
2531
2479
|
require('./release').runJson(() => cleanupCommand(args.slice(1)));
|
|
2532
2480
|
break;
|
package/bin/engines.js
CHANGED
|
@@ -4,10 +4,11 @@ const ENGINE_IDS = Object.freeze([
|
|
|
4
4
|
'claude', 'codex', 'grok', 'omp', 'antigravity', 'cursor', 'opencode', 'kimi', 'copilot', 'qwen',
|
|
5
5
|
]);
|
|
6
6
|
|
|
7
|
-
// The versions each channel was examined on.
|
|
8
|
-
// compatible:
|
|
9
|
-
//
|
|
10
|
-
// is refused
|
|
7
|
+
// The versions each channel was examined on. Any newer release, a new major included, is taken as
|
|
8
|
+
// compatible: a version number is not evidence that a channel broke (omp 18 kept both flags devlyn
|
|
9
|
+
// uses), and refusing unexamined majors turned working engines off. Only a version below the examined
|
|
10
|
+
// floor (or a Cursor build older than the examined one) is refused; an unreadable version is refused
|
|
11
|
+
// with its own reason.
|
|
11
12
|
const ENGINE_CAPABILITIES = Object.freeze({
|
|
12
13
|
claude: { minimum: '2.1.307', executor: true },
|
|
13
14
|
codex: { minimum: '0.163.0', executor: true },
|
|
@@ -21,20 +22,21 @@ const ENGINE_CAPABILITIES = Object.freeze({
|
|
|
21
22
|
qwen: { minimum: '0.26.0', executor: false },
|
|
22
23
|
});
|
|
23
24
|
|
|
25
|
+
function readableVersion(engine, version) {
|
|
26
|
+
return typeof version === 'string'
|
|
27
|
+
&& (engine === 'cursor' ? /^\d{4}\.\d{2}\.\d{2}-[0-9a-f]+$/ : /^\d+\.\d+\.\d+$/).test(version);
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
function supportedVersion(engine, version) {
|
|
31
|
+
if (!readableVersion(engine, version)) return false;
|
|
25
32
|
const capability = ENGINE_CAPABILITIES[engine];
|
|
26
|
-
if (engine === 'cursor')
|
|
27
|
-
const build = typeof version === 'string' && version.match(/^(\d{4})\.(\d{2})\.(\d{2})-[0-9a-f]+$/);
|
|
28
|
-
return Boolean(build) && build.slice(1).join('') >= capability.minimum.slice(0, 10).replace(/\./g, '');
|
|
29
|
-
}
|
|
30
|
-
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) return false;
|
|
33
|
+
if (engine === 'cursor') return version.slice(0, 10).replace(/\./g, '') >= capability.minimum.slice(0, 10).replace(/\./g, '');
|
|
31
34
|
const actual = version.split('.').map(Number);
|
|
32
35
|
const floor = capability.minimum.split('.').map(Number);
|
|
33
|
-
|
|
34
|
-
for (let i = 1; i < 3; i++) {
|
|
36
|
+
for (let i = 0; i < 3; i++) {
|
|
35
37
|
if (actual[i] !== floor[i]) return actual[i] > floor[i];
|
|
36
38
|
}
|
|
37
39
|
return true;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
module.exports = { ENGINE_IDS, ENGINE_CAPABILITIES, supportedVersion };
|
|
42
|
+
module.exports = { ENGINE_IDS, ENGINE_CAPABILITIES, readableVersion, supportedVersion };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { ENGINE_IDS, ENGINE_CAPABILITIES, supportedVersion } = require('./engines');
|
|
4
|
+
const { ENGINE_IDS, ENGINE_CAPABILITIES, readableVersion, supportedVersion } = require('./engines');
|
|
5
5
|
const { verifyRelease, sha256, fail, options, HarnessError } = require('./release');
|
|
6
6
|
const { fingerprint } = require('./skill-ownership');
|
|
7
7
|
const history = require('./skill-history.json');
|
|
@@ -301,7 +301,9 @@ function planActivation(releaseRoot, input, env = process.env) {
|
|
|
301
301
|
if (off) return finish(plan);
|
|
302
302
|
plan.release = release;
|
|
303
303
|
if (!supported) {
|
|
304
|
-
const reasons = [
|
|
304
|
+
const reasons = [readableVersion(context.engine, context.version)
|
|
305
|
+
? reason('engine-version-unsupported', { minimum: ENGINE_CAPABILITIES[context.engine].minimum })
|
|
306
|
+
: reason('engine-version-unreadable')];
|
|
305
307
|
status.principles = channel('unavailable', null, reasons);
|
|
306
308
|
status.skills = channel('unavailable', null, reasons);
|
|
307
309
|
return finish(plan);
|