devlyn-cli 4.3.27 → 4.3.29
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/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +10 -10
- package/bin/devlyn.js +92 -58
- package/bin/engines.js +5 -3
- package/bin/harness-activation.js +212 -19
- package/bin/instruction-templates.json +2 -0
- package/bin/skill-history.json +11 -0
- package/config/skills/_shared/engine-doctor.sh +2 -2
- package/config/skills/_shared/engine-preflight.md +2 -2
- package/config/skills/_shared/managed-storage.md +80 -0
- package/config/skills/_shared/platform-support.py +67 -28
- package/config/skills/_shared/role-config.py +16 -7
- package/config/skills/_shared/runtime-principles.md +1 -1
- package/config/skills/_shared/storage-client.py +31 -7
- package/config/skills/_shared/task-complete.py +1 -1
- package/config/skills/_shared/task-completion.md +2 -76
- package/config/skills/devlyn-engines/SKILL.md +1 -1
- package/config/skills/devlyn-ideate/SKILL.md +2 -2
- package/config/skills/devlyn-ideate/references/executor.md +62 -0
- package/config/skills/devlyn-ideate/references/loop.md +2 -59
- package/config/skills/devlyn-ideate/references/package-format.md +1 -1
- package/config/skills/devlyn-ideate/scripts/acceptance.py +442 -21
- package/config/skills/devlyn-ideate/scripts/queue.py +58 -10
- package/harness/AGENTS.md +2 -2
- package/harness/CLAUDE.md +2 -2
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/engine-doctor.sh +2 -2
- package/native/claude/skills/_shared/engine-preflight.md +2 -2
- package/native/claude/skills/_shared/managed-storage.md +80 -0
- package/native/claude/skills/_shared/platform-support.py +67 -28
- package/native/claude/skills/_shared/role-config.py +16 -7
- package/native/claude/skills/_shared/runtime-principles.md +1 -1
- package/native/claude/skills/_shared/storage-client.py +31 -7
- package/native/claude/skills/_shared/task-complete.py +1 -1
- package/native/claude/skills/_shared/task-completion.md +2 -76
- package/native/claude/skills/devlyn-engines/SKILL.md +1 -1
- package/native/claude/skills/devlyn-ideate/SKILL.md +2 -2
- package/native/claude/skills/devlyn-ideate/references/executor.md +62 -0
- package/native/claude/skills/devlyn-ideate/references/loop.md +2 -59
- package/native/claude/skills/devlyn-ideate/references/package-format.md +1 -1
- package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +442 -21
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +58 -10
- 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/engine-doctor.sh +2 -2
- package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +2 -2
- package/native/codex/plugins/devlyn/skills/_shared/managed-storage.md +80 -0
- package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +67 -28
- package/native/codex/plugins/devlyn/skills/_shared/role-config.py +16 -7
- package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/storage-client.py +31 -7
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +2 -76
- package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +2 -2
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/executor.md +62 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +2 -59
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +442 -21
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +58 -10
- package/package.json +1 -11
- package/release.json +98 -68
- /package/config/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
- /package/config/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
- /package/config/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
- /package/native/claude/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
- /package/native/claude/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
- /package/native/claude/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
- /package/native/codex/plugins/devlyn/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
- /package/native/codex/plugins/devlyn/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
- /package/native/codex/plugins/devlyn/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
package/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ Seven rules govern every change. Cite them by name when a decision touches one.
|
|
|
15
15
|
3. **No guesswork** — verify with the actual files, logs, diffs, and run output before forming conclusions. State the falsifiable prediction BEFORE the experiment; record raw results AFTER. Retroactive prediction edits are dishonest.
|
|
16
16
|
4. **Worldclass** — zero CRITICAL, zero HIGH security/design findings on the shippable path. Any review is one pass in which every reviewer, including each one the user requires, reads the source the checks ran on; fix each finding or reject it with a reason, re-check the fixes and the invariants they touch, and repeat the full review only while a CRITICAL or HIGH finding remains.
|
|
17
17
|
5. **Best practice** — idiomatic for the language and framework. Use standard primitives; do not hand-roll what the library already provides.
|
|
18
|
-
6. **Optimized** —
|
|
18
|
+
6. **Optimized** — efficient on the resource that matters (the user's time, tokens, attention, runtime) without weakening correctness or requirements. An extra check or process step must beat the simpler path.
|
|
19
19
|
7. **Production ready** — error states are explicit and visible; behavior under failure is what the user expects, not silent corruption.
|
|
20
20
|
|
|
21
21
|
Three discipline rules govern HOW the principles are applied:
|
package/CLAUDE.md
CHANGED
|
@@ -15,7 +15,7 @@ Seven rules govern every change. Cite them by name when a decision touches one.
|
|
|
15
15
|
3. **No guesswork** — verify with the actual files, logs, diffs, and run output before forming conclusions. State the falsifiable prediction BEFORE the experiment; record raw results AFTER. Retroactive prediction edits are dishonest.
|
|
16
16
|
4. **Worldclass** — zero CRITICAL, zero HIGH security/design findings on the shippable path. Any review is one pass in which every reviewer, including each one the user requires, reads the source the checks ran on; fix each finding or reject it with a reason, re-check the fixes and the invariants they touch, and repeat the full review only while a CRITICAL or HIGH finding remains.
|
|
17
17
|
5. **Best practice** — idiomatic for the language and framework. Use standard primitives; do not hand-roll what the library already provides.
|
|
18
|
-
6. **Optimized** —
|
|
18
|
+
6. **Optimized** — efficient on the resource that matters (the user's time, tokens, attention, runtime) without weakening correctness or requirements. An extra check or process step must beat the simpler path.
|
|
19
19
|
7. **Production ready** — error states are explicit and visible; behavior under failure is what the user expects, not silent corruption.
|
|
20
20
|
|
|
21
21
|
Three discipline rules govern HOW the principles are applied:
|
package/README.md
CHANGED
|
@@ -43,17 +43,17 @@ Without prompts, `npx devlyn-cli -y` installs `.agents/skills/` plus every targe
|
|
|
43
43
|
|
|
44
44
|
## How It Works — Principles and Loops
|
|
45
45
|
|
|
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
|
|
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 session started any other way, as with a plain `npx devlyn-cli` install, gets the skills but not the defaults; ideate still reads its installed copy for the loops it plans and drains. 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
|
-
intent → direct work under the
|
|
49
|
+
intent → direct work under the Devlyn defaults, or an ideate loop → ship
|
|
50
50
|
```
|
|
51
51
|
|
|
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
|
|
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 any CLI other than Claude Code or Codex needs [a pinned executor](#executor--devlyn-engines).
|
|
53
53
|
|
|
54
54
|
### Terminal sessions — `devlyn run`
|
|
55
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.
|
|
56
|
+
`devlyn run claude`, `devlyn run codex` or `devlyn run omp` starts the engine in this terminal on macOS or Linux (not Windows) with the same principles and skills the Devlyn app delivers, from the selected release. While the harness is on for the project, only engine arguments known not to change the session's freshness, directory or planned settings pass through, and any other stops the launch; with it off, every argument passes. See [docs/harness-run.md](docs/harness-run.md).
|
|
57
57
|
|
|
58
58
|
### Plan and drain loops — `/devlyn-ideate`
|
|
59
59
|
|
|
@@ -66,7 +66,7 @@ Give ideate an intent or a document. It writes a loop package — a meta-prompt
|
|
|
66
66
|
| `/devlyn-ideate status` | Pending, active, accepted and failed counts; the next runnable task; delivery and recovery blockers. |
|
|
67
67
|
| `/devlyn-ideate drain` | Runs the queue serially and hands-free. |
|
|
68
68
|
|
|
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;
|
|
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; a CLI other than Claude Code or Codex needs 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 CLAUDE.md/AGENTS.md changes 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.
|
|
70
70
|
|
|
71
71
|
### Delivery
|
|
72
72
|
|
|
@@ -86,9 +86,9 @@ for allocation, acceptance, writer cessation and recovery.
|
|
|
86
86
|
|
|
87
87
|
### Executor — `/devlyn-engines`
|
|
88
88
|
|
|
89
|
-
The orchestrator is the CLI you opened (Claude Code, Codex, omp, Pi or Grok); the contract is symmetric (`CLAUDE.md` ↔ `AGENTS.md`), so the loop's file artifacts carry over if you switch. The executor does the implementation work, direct or drained: the engine pinned in machine-local `.devlyn/engines.json`, else the CLI you opened.
|
|
89
|
+
The orchestrator is the CLI you opened (Claude Code, Codex, omp, Pi or Grok); the contract is symmetric (`CLAUDE.md` ↔ `AGENTS.md`), so the loop's file artifacts carry over if you switch. The executor does the implementation work, direct or drained: the engine pinned in machine-local `.devlyn/engines.json`, else the CLI you opened. Only Claude and Codex can execute, so a drain from any other CLI needs `devlyn-engines executor <claude|codex>`.
|
|
90
90
|
|
|
91
|
-
`/devlyn-engines` with no arguments shows the executor and the engines detected on this machine; `executor <name>` pins one and `clear` removes the pin. A pin is a promise: an unavailable pinned engine stops dispatch with `BLOCKED:<engine>-unavailable`, and a name with no executor-eligible `_shared/adapters/<name>.md` stops with `BLOCKED:invalid-engine-config`. Adapters ship with devlyn-cli releases; a reinstall replaces `_shared`.
|
|
91
|
+
`/devlyn-engines` with no arguments shows the executor and the engines detected on this machine; `executor <name>` pins one and `clear` removes the pin. A pin is a promise: an unavailable pinned engine stops dispatch with `BLOCKED:<engine>-unavailable`, and a name with no executor-eligible `_shared/adapters/<name>.adapter.md` stops with `BLOCKED:invalid-engine-config`. Adapters ship with devlyn-cli releases; a reinstall replaces `_shared`.
|
|
92
92
|
|
|
93
93
|
Unattended children disable owner-desktop and account drivers: every monitored
|
|
94
94
|
Codex child and every `claude` drain executor (including `.exe`/`.cmd`/`.bat`)
|
|
@@ -171,11 +171,11 @@ Earlier versions of devlyn-cli shipped 16+ slash commands. The iter-0034 Phase 4
|
|
|
171
171
|
|
|
172
172
|
| Old command | Now use |
|
|
173
173
|
|---|---|
|
|
174
|
-
| `/devlyn:auto-resolve`, `/devlyn:preflight`, `/devlyn:evaluate`, `/devlyn:review`, `/devlyn:team-resolve`, `/devlyn:team-review`, `/devlyn:clean`, `/devlyn:update-docs`, `/devlyn:browser-validate`, `/devlyn:implement-ui` | Folded into resolve, now retired — work directly under the
|
|
174
|
+
| `/devlyn:auto-resolve`, `/devlyn:preflight`, `/devlyn:evaluate`, `/devlyn:review`, `/devlyn:team-resolve`, `/devlyn:team-review`, `/devlyn:clean`, `/devlyn:update-docs`, `/devlyn:browser-validate`, `/devlyn:implement-ui` | Folded into resolve, now retired — work directly under the Devlyn defaults |
|
|
175
175
|
| `/devlyn:product-spec`, `/devlyn:feature-spec`, `/devlyn:recommend-features`, `/devlyn:discover-product` | `/devlyn-ideate` |
|
|
176
176
|
| `/devlyn:design-system` | Removed 2026-05-14 — no replacement |
|
|
177
177
|
| `/devlyn:ideate`, `/devlyn:engines`, `devlyn:pencil-pull`, `devlyn:pencil-push`, `devlyn:reap` (3.x and earlier) | `/devlyn-ideate`, `/devlyn-engines`, `devlyn-pencil-pull`, `devlyn-pencil-push`, `devlyn-reap` (4.0.0) |
|
|
178
|
-
| `/devlyn:resolve` (3.x and earlier), `/devlyn-resolve` (4.0.0–4.1.0) | Retired — work directly under the
|
|
178
|
+
| `/devlyn:resolve` (3.x and earlier), `/devlyn-resolve` (4.0.0–4.1.0) | Retired — work directly under the Devlyn defaults; `/devlyn-ideate` plans and drains multi-task work |
|
|
179
179
|
| `/devlyn:queue` (3.x and earlier), `/devlyn-queue` (4.0.0–4.1.0) | `/devlyn-ideate add`, `status` and `drain` |
|
|
180
180
|
| `/devlyn-ideate --quick`, `--from-spec`, `--project` (4.1.0 and earlier) | `/devlyn-ideate plan`; `--autonomous` replaces `--quick` |
|
|
181
181
|
| `/devlyn:team-design-ui`, `/devlyn:design-ui` (3.x and earlier), `/devlyn-design-ui` (4.0.0–4.1.0) | Retired — no replacement |
|
|
@@ -257,7 +257,7 @@ Windows completion preserves the workspace, owned refs and recovery receipt when
|
|
|
257
257
|
|
|
258
258
|
## Contributing
|
|
259
259
|
|
|
260
|
-
- **Add a skill** — add it to `optional-skills/` and `OPTIONAL_ADDONS` in [`bin/devlyn.js`](bin/devlyn.js); the core skills are fixed by `DEVLYN_CORE_SKILLS` (lint
|
|
260
|
+
- **Add a skill** — add it to `optional-skills/` and `OPTIONAL_ADDONS` in [`bin/devlyn.js`](bin/devlyn.js); the core skills are fixed by `DEVLYN_CORE_SKILLS` (lint Check 5a)
|
|
261
261
|
- **Suggest a pack** — PR to the pack list
|
|
262
262
|
|
|
263
263
|
## Supercharge it — pair devlyn with persistent agent memory
|
package/bin/devlyn.js
CHANGED
|
@@ -358,7 +358,7 @@ function refreshRenamedSkills(skillsDir, installedSkills) {
|
|
|
358
358
|
for (const [oldName, newName] of Object.entries(RENAMED_SKILLS)) {
|
|
359
359
|
const found = legacySkillPaths(skillsDir, oldName).filter((fullPath) => fs.existsSync(fullPath));
|
|
360
360
|
if (found.length > 0 && optional.has(newName) && canRefreshRenamed(skillsDir, newName)) {
|
|
361
|
-
|
|
361
|
+
installSkillInto(OPTIONAL_SKILLS_SOURCE, skillsDir, newName, installedSkills);
|
|
362
362
|
refreshed.push(newName);
|
|
363
363
|
} else if (isShippedCopy(path.join(skillsDir, newName), PRE_STANDARD_SKILL_MD_SHA256)) {
|
|
364
364
|
refreshPreStandardCopy(skillsDir, newName);
|
|
@@ -610,21 +610,22 @@ function installLocalSkill(skillName, roots, installedSkills = new Map()) {
|
|
|
610
610
|
}
|
|
611
611
|
log(`\n🛠️ Installing ${skillName}...`, 'cyan');
|
|
612
612
|
for (const root of roots) {
|
|
613
|
-
|
|
613
|
+
installSkillInto(OPTIONAL_SKILLS_SOURCE, root, skillName, installedSkills);
|
|
614
614
|
writeInstallMarker(root, [skillName], readInstallManifest(root));
|
|
615
615
|
}
|
|
616
616
|
return true;
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
// One
|
|
620
|
-
// copy
|
|
621
|
-
|
|
619
|
+
// One skill from `source` into one skill-loader directory. The copy is staged and checked first, so
|
|
620
|
+
// a failed copy leaves any older copy of it in place for a plain rerun. A copy under its name
|
|
621
|
+
// before the 4.0.0 rename goes only once the new one is complete.
|
|
622
|
+
function installSkillInto(source, target, skillName, installedSkills) {
|
|
622
623
|
const dest = path.join(target, skillName);
|
|
623
624
|
const stage = fs.mkdtempSync(path.join(target, '.devlyn-skill-'));
|
|
624
625
|
const stagedSkill = path.join(stage, skillName);
|
|
625
626
|
try {
|
|
626
|
-
const files = copyRecursive(path.join(
|
|
627
|
-
assertCompleteSkillInstall(
|
|
627
|
+
const files = copyRecursive(path.join(source, skillName), stagedSkill, stage);
|
|
628
|
+
assertCompleteSkillInstall(source, stage, [skillName]);
|
|
628
629
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
629
630
|
fs.renameSync(stagedSkill, dest);
|
|
630
631
|
installedSkills.set(dest, files.map((file) => path.join(dest, path.relative(stagedSkill, file))));
|
|
@@ -692,12 +693,7 @@ function installCoreSkills(skillsDir, installedSkills = new Map()) {
|
|
|
692
693
|
const previous = readInstallManifest(skillsDir);
|
|
693
694
|
clearInstallMarker(skillsDir);
|
|
694
695
|
for (const skillName of DEVLYN_CORE_SKILLS) {
|
|
695
|
-
|
|
696
|
-
const dest = path.join(skillsDir, skillName);
|
|
697
|
-
if (!fs.existsSync(src)) continue;
|
|
698
|
-
// Full replace: copyRecursive is an overlay, so stale files would otherwise persist.
|
|
699
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
700
|
-
installedSkills.set(dest, copyRecursive(src, dest, skillsDir));
|
|
696
|
+
if (fs.existsSync(path.join(sourceSkillsDir, skillName))) installSkillInto(sourceSkillsDir, skillsDir, skillName, installedSkills);
|
|
701
697
|
}
|
|
702
698
|
assertCompleteSkillInstall(sourceSkillsDir, skillsDir, DEVLYN_CORE_SKILLS);
|
|
703
699
|
const refreshed = refreshRenamedSkills(skillsDir, installedSkills);
|
|
@@ -773,14 +769,14 @@ function installClaudeCore(installedSkills = new Map()) {
|
|
|
773
769
|
+ '.claude/ is unchanged; fix the file and rerun the same install command.');
|
|
774
770
|
const settingsStat = fs.lstatSync(settingsPath, { throwIfNoEntry: false });
|
|
775
771
|
if (settingsStat && !settingsStat.isFile()) throw unmergeable('not a regular file');
|
|
776
|
-
let
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
772
|
+
let text = '{}\n'; // A missing file starts as an empty object.
|
|
773
|
+
let settings;
|
|
774
|
+
try {
|
|
775
|
+
// Claude Code accepts a leading byte order mark; bytes that are not UTF-8 could not be written back as they are.
|
|
776
|
+
if (settingsStat) text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(settingsPath));
|
|
777
|
+
settings = JSON.parse(text.replace(/^\uFEFF/, ''));
|
|
778
|
+
} catch (error) {
|
|
779
|
+
throw unmergeable(error.message);
|
|
784
780
|
}
|
|
785
781
|
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
786
782
|
throw unmergeable('root must be a JSON object');
|
|
@@ -789,31 +785,24 @@ function installClaudeCore(installedSkills = new Map()) {
|
|
|
789
785
|
&& (!settings.env || typeof settings.env !== 'object' || Array.isArray(settings.env))) {
|
|
790
786
|
throw unmergeable('env must be a JSON object');
|
|
791
787
|
}
|
|
788
|
+
// The env edit finds the first env while JSON.parse keeps the last, so a repeated env has no single place to edit.
|
|
789
|
+
if (parseTree(text.replace(/^\uFEFF/, ' ')).children.filter((property) => property.children[0].value === 'env').length > 1) {
|
|
790
|
+
throw unmergeable('env appears more than once');
|
|
791
|
+
}
|
|
792
792
|
installCoreSkills(skillsDir, installedSkills);
|
|
793
793
|
updateProjectGitignore('.claude/skills/.devlyn-install.json');
|
|
794
794
|
|
|
795
795
|
// One-hour foreground Bash calls and one-hour prompt caching serve a drain host that waits in the foreground.
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
if (!Object.prototype.hasOwnProperty.call(settings.env, 'ENABLE_PROMPT_CACHING_1H')) {
|
|
802
|
-
settings.env.ENABLE_PROMPT_CACHING_1H = 'true';
|
|
803
|
-
settingsChanged = true;
|
|
804
|
-
}
|
|
805
|
-
if (!Object.prototype.hasOwnProperty.call(settings.env, 'BASH_MAX_TIMEOUT_MS')) {
|
|
806
|
-
settings.env.BASH_MAX_TIMEOUT_MS = '3600000';
|
|
807
|
-
settingsChanged = true;
|
|
808
|
-
}
|
|
809
|
-
settingsChanged = cleanRetiredSettings(settings) || settingsChanged;
|
|
810
|
-
if (settingsChanged) {
|
|
811
|
-
replaceFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
796
|
+
const env = settings.env || {};
|
|
797
|
+
const missing = Object.entries({ ENABLE_PROMPT_CACHING_1H: 'true', BASH_MAX_TIMEOUT_MS: '3600000' })
|
|
798
|
+
.filter(([key]) => !Object.prototype.hasOwnProperty.call(env, key));
|
|
799
|
+
if (missing.length) {
|
|
800
|
+
replaceFile(settingsPath, insertSettingsEnv(text, missing));
|
|
812
801
|
log(' → settings.json', 'dim');
|
|
813
802
|
}
|
|
814
803
|
// Their ownership cannot be established, so the other retired settings stay; name the ones present.
|
|
815
804
|
const retired = [
|
|
816
|
-
...RETIRED_ENV.filter((key) => Object.prototype.hasOwnProperty.call(
|
|
805
|
+
...RETIRED_ENV.filter((key) => Object.prototype.hasOwnProperty.call(env, key)),
|
|
817
806
|
...RETIRED_PERMISSIONS.filter((rule) => Array.isArray(settings.permissions?.allow) && settings.permissions.allow.includes(rule)),
|
|
818
807
|
];
|
|
819
808
|
if (retired.length > 0) {
|
|
@@ -833,7 +822,40 @@ function replaceFile(file, text) {
|
|
|
833
822
|
}
|
|
834
823
|
}
|
|
835
824
|
|
|
836
|
-
|
|
825
|
+
// Adds the env entries as new text laid out like the members they join, so every existing byte stays: the byte
|
|
826
|
+
// order mark, line endings, indentation, escapes and numbers beyond double precision. jsonc-parser's
|
|
827
|
+
// formattingOptions would lay them out too, but by reformatting the neighbouring lines.
|
|
828
|
+
function insertSettingsEnv(text, entries) {
|
|
829
|
+
const tree = () => parseTree(text.replace(/^\uFEFF/, ' '));
|
|
830
|
+
// The whitespace an object puts after its opening brace, before a later member and before its closing brace,
|
|
831
|
+
// and around its colons.
|
|
832
|
+
const layout = (object) => {
|
|
833
|
+
const first = object.children[0], last = object.children.at(-1), [key, value] = last.children;
|
|
834
|
+
const before = text.slice(0, last.offset);
|
|
835
|
+
return { open: text.slice(object.offset + 1, first.offset), lead: before.slice(before.trimEnd().length),
|
|
836
|
+
close: text.slice(last.offset + last.length, object.offset + object.length - 1),
|
|
837
|
+
colon: text.slice(key.offset + key.length, value.offset) };
|
|
838
|
+
};
|
|
839
|
+
const insert = (object, members, { open, lead, close }) => {
|
|
840
|
+
const last = object.children.at(-1);
|
|
841
|
+
text = applyEdits(text, [last
|
|
842
|
+
? { offset: last.offset + last.length, length: 0, content: members.map((member) => `,${lead}${member}`).join('') }
|
|
843
|
+
: { offset: object.offset + 1, length: 0, content: open + members.join(`,${lead}`) + (object.length === 2 ? close : '') }]);
|
|
844
|
+
};
|
|
845
|
+
const root = tree();
|
|
846
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
847
|
+
const outer = root.children.length ? layout(root) : { open: `${eol} `, lead: `${eol} `, close: eol, colon: ': ' };
|
|
848
|
+
// A new or empty env is one level deeper: the root's indentation once more.
|
|
849
|
+
const unit = outer.lead.includes('\n') ? outer.lead.slice(outer.lead.lastIndexOf('\n') + 1) : '';
|
|
850
|
+
if (!findNodeAtLocation(root, ['env'])) insert(root, [`"env"${outer.colon}{}`], outer);
|
|
851
|
+
const env = findNodeAtLocation(tree(), ['env']);
|
|
852
|
+
const inner = env.children.length ? layout(env)
|
|
853
|
+
: { open: outer.open + unit, lead: outer.lead + unit, close: outer.close + unit, colon: outer.colon };
|
|
854
|
+
insert(env, entries.map(([key, value]) => `${JSON.stringify(key)}${inner.colon}${JSON.stringify(value)}`), inner);
|
|
855
|
+
return text;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function cleanRetiredSettings(settings, hookScripts) {
|
|
837
859
|
let changed = cleanJarvisSettings(settings, hookScripts);
|
|
838
860
|
// Remove exactly the retired Stop hook command, and its entry only when nothing else is left in it.
|
|
839
861
|
const retiredHook = (hook) => hook?.type === 'command' && hook.command === RETIRED_STOP_HOOK;
|
|
@@ -930,8 +952,8 @@ function cleanupSettingsText(text, settings, scripts) {
|
|
|
930
952
|
const end = !previous && next ? next.offset : node.offset + node.length;
|
|
931
953
|
text = applyEdits(text, [{ offset, length: end - offset, content: '' }]);
|
|
932
954
|
}
|
|
933
|
-
|
|
934
|
-
return
|
|
955
|
+
JSON.parse(text.replace(/^\uFEFF/, '')); // Refuse an invalid edit before changing any bytes.
|
|
956
|
+
return text;
|
|
935
957
|
}
|
|
936
958
|
|
|
937
959
|
// Shared user roots need one writer from ownership checks through the last addon write.
|
|
@@ -1034,7 +1056,9 @@ function install(targets, global, optionalNames = [], plan, afterInstall = () =>
|
|
|
1034
1056
|
for (const name of optionalNames) installLocalSkill(name, roots, installedSkills);
|
|
1035
1057
|
afterInstall();
|
|
1036
1058
|
log(`\n✅ devlyn ${PKG.version} installed`, 'green');
|
|
1037
|
-
|
|
1059
|
+
// `devlyn run` does not start on native Windows.
|
|
1060
|
+
log(process.platform === 'win32' ? 'The Devlyn app gives every session the principles.'
|
|
1061
|
+
: 'The Devlyn app gives every session the principles; in a terminal, start with: npx devlyn-cli run <claude|codex|omp>');
|
|
1038
1062
|
noticeGlobalDrift(roots);
|
|
1039
1063
|
cleanupScope(global ? os.homedir() : projectDir(), global);
|
|
1040
1064
|
if (!global) noticeIgnoredInstallFiles(installedSkills);
|
|
@@ -1139,15 +1163,14 @@ function cleanupScope(base, global) {
|
|
|
1139
1163
|
}
|
|
1140
1164
|
const settingsFile = path.join(base, '.claude', 'settings.json');
|
|
1141
1165
|
attempt(settingsFile, () => {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
1166
|
+
const { safe, settings, text } = cleanupReadSettings(base);
|
|
1167
|
+
if (!safe || text === undefined) return;
|
|
1168
|
+
const after = cleanupSettingsText(text, settings, []);
|
|
1169
|
+
if (after === text) return;
|
|
1170
|
+
found = true;
|
|
1171
|
+
// Even an emptied file stays: only the reviewed cleanup, which records it first, deletes it.
|
|
1172
|
+
replaceFile(settingsFile, after);
|
|
1173
|
+
log(' ✕ .claude/settings.json retired entries (removed)', 'dim');
|
|
1151
1174
|
});
|
|
1152
1175
|
// CLAUDE.md and AGENTS.md stay as they are here: only a reviewed cleanup changes them.
|
|
1153
1176
|
const instructions = global ? [] : ['GEMINI.md', '.cursorrules', '.windsurfrules', '.github/copilot-instructions.md'];
|
|
@@ -1972,17 +1995,18 @@ function cleanupScopes(inputs, current, includedHooks = new Set(), baseViews = n
|
|
|
1972
1995
|
return scopes;
|
|
1973
1996
|
}
|
|
1974
1997
|
|
|
1975
|
-
|
|
1998
|
+
// A checkout-only plan is a count of the checkout as it stands: no fetch, base snapshot or checkout update.
|
|
1999
|
+
function cleanupPlan(inputs, checkoutOnly = false) {
|
|
1976
2000
|
const current = cleanupCurrentFingerprints();
|
|
1977
|
-
const baseViews = new Map();
|
|
2001
|
+
const baseViews = checkoutOnly ? null : new Map();
|
|
1978
2002
|
try {
|
|
1979
2003
|
// The review covers the bytes delivery acts on, including scopes sharing a repository.
|
|
1980
2004
|
const scopes = cleanupScopes(inputs, current, new Set(), baseViews);
|
|
1981
|
-
const plan = { schemaVersion: 1, version: PKG.version, inputs, scopes };
|
|
2005
|
+
const plan = { schemaVersion: 1, version: PKG.version, ...(checkoutOnly && { checkoutOnly }), inputs, scopes };
|
|
1982
2006
|
return { ...plan, digest: cleanupDigest(plan) };
|
|
1983
2007
|
} finally {
|
|
1984
2008
|
const errors = [];
|
|
1985
|
-
for (const view of baseViews
|
|
2009
|
+
for (const view of baseViews?.values() || []) {
|
|
1986
2010
|
if (!view.snapshot) continue;
|
|
1987
2011
|
try { cleanupRemoveSnapshot(view.root, view.snapshot); } catch (error) { errors.push(error); }
|
|
1988
2012
|
}
|
|
@@ -2032,8 +2056,9 @@ function cleanupApplyItem(item) {
|
|
|
2032
2056
|
const { safe, settings, text } = cleanupReadSettings(item.base);
|
|
2033
2057
|
if (!safe || text === undefined) return cleanupReason('stale-plan');
|
|
2034
2058
|
const after = cleanupSettingsText(text, settings, item.hookScripts);
|
|
2035
|
-
if (after ===
|
|
2036
|
-
|
|
2059
|
+
if (after === text) return null;
|
|
2060
|
+
if (Object.keys(JSON.parse(after.replace(/^\uFEFF/, ''))).length === 0) fs.unlinkSync(item.path);
|
|
2061
|
+
else replaceFile(item.path, after);
|
|
2037
2062
|
return null;
|
|
2038
2063
|
}
|
|
2039
2064
|
return cleanupReason('unknown-item');
|
|
@@ -2428,21 +2453,30 @@ function cleanupCommand(args) {
|
|
|
2428
2453
|
const { options, fail } = require('./release');
|
|
2429
2454
|
const opts = options(args, {
|
|
2430
2455
|
plan: { type: 'boolean' }, apply: { type: 'boolean' }, machine: { type: 'boolean' }, events: { type: 'boolean' },
|
|
2456
|
+
'checkout-only': { type: 'boolean' },
|
|
2431
2457
|
project: { type: 'string', multiple: true }, 'account-home': { type: 'string', multiple: true },
|
|
2432
2458
|
'plan-file': { type: 'string' }, digest: { type: 'string' }, include: { type: 'string', multiple: true },
|
|
2433
2459
|
});
|
|
2434
2460
|
if (opts.plan === opts.apply) fail('invalid-arguments', { argument: 'exactly one of --plan or --apply' }, 2);
|
|
2435
2461
|
if (opts.plan && opts.include) fail('invalid-arguments', { argument: '--include requires --apply' }, 2);
|
|
2462
|
+
if (opts.apply && opts['checkout-only']) fail('invalid-arguments', { argument: '--checkout-only requires --plan' }, 2);
|
|
2436
2463
|
for (const dir of [...(opts.project || []), ...(opts['account-home'] || [])]) {
|
|
2437
2464
|
if (!path.isAbsolute(dir)) fail('invalid-arguments', { argument: 'absolute paths required' }, 2);
|
|
2438
2465
|
}
|
|
2439
2466
|
// The installer's progress lines would corrupt this command's JSON on stdout.
|
|
2440
2467
|
console.log = (...parts) => process.stderr.write(`${parts.join(' ')}\n`);
|
|
2441
2468
|
if (opts.plan) {
|
|
2442
|
-
|
|
2469
|
+
// Every process a passive count starts inherits this, so no Git process, the queue inspection's
|
|
2470
|
+
// included, fetches missing objects or takes optional locks. Git before 2.45 ignores
|
|
2471
|
+
// GIT_NO_LAZY_FETCH; every Git refuses all transports under GIT_ALLOW_PROTOCOL=none.
|
|
2472
|
+
if (opts['checkout-only']) Object.assign(process.env, { GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: 'none', GIT_OPTIONAL_LOCKS: '0' });
|
|
2473
|
+
return cleanupPlan({ machine: Boolean(opts.machine), projects: opts.project || [], accountHomes: opts['account-home'] || [] },
|
|
2474
|
+
Boolean(opts['checkout-only']));
|
|
2443
2475
|
}
|
|
2444
2476
|
if (!opts['plan-file'] || !opts.digest) fail('invalid-arguments', { argument: '--plan-file and --digest' }, 2);
|
|
2445
2477
|
const plan = JSON.parse(fs.readFileSync(opts['plan-file'], 'utf8'));
|
|
2478
|
+
// A checkout-only plan never reviewed the base that delivery acts on.
|
|
2479
|
+
if (plan?.checkoutOnly !== undefined) fail('checkout-only-plan', {}, 2);
|
|
2446
2480
|
const inputs = plan?.inputs;
|
|
2447
2481
|
if (plan?.schemaVersion !== 1 || plan.digest !== opts.digest || cleanupDigest(plan) !== plan.digest
|
|
2448
2482
|
|| typeof inputs?.machine !== 'boolean' || !Array.isArray(inputs.projects) || !Array.isArray(inputs.accountHomes)
|
package/bin/engines.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Engine IDs
|
|
2
|
-
// CLI
|
|
1
|
+
// Engine IDs in the order of devlyn-os-v1/packages/contracts/src/engines.ts. An ID joins only once its
|
|
2
|
+
// channels are confirmed on the real CLI, with that examined version as its floor below; the planner
|
|
3
|
+
// refuses every other ID. CLI binary names (for example agy) are not engine IDs.
|
|
3
4
|
const ENGINE_IDS = Object.freeze([
|
|
4
|
-
'claude', 'codex', 'grok', 'omp', 'antigravity', 'cursor', 'opencode', 'kimi', 'copilot', 'qwen',
|
|
5
|
+
'claude', 'codex', 'grok', 'omp', 'antigravity', 'cursor', 'opencode', 'kimi', 'copilot', 'qwen', 'pi',
|
|
5
6
|
]);
|
|
6
7
|
|
|
7
8
|
// The versions each channel was examined on. Any newer release, a new major included, is taken as
|
|
@@ -20,6 +21,7 @@ const ENGINE_CAPABILITIES = Object.freeze({
|
|
|
20
21
|
kimi: { minimum: '2.3.0', executor: false },
|
|
21
22
|
copilot: { minimum: '1.0.98', executor: false },
|
|
22
23
|
qwen: { minimum: '0.26.0', executor: false },
|
|
24
|
+
pi: { minimum: '1.4.0', executor: false },
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
function readableVersion(engine, version) {
|