devlyn-cli 4.3.31 → 4.3.32
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 +3 -1
- package/bin/devlyn.js +33 -8
- package/bin/harness-activation.js +4 -1
- package/bin/harness-status.js +15 -8
- package/bin/skill-history.json +7 -0
- package/config/skills/_shared/bring-in.py +10 -0
- package/config/skills/_shared/task-complete.py +319 -54
- package/config/skills/_shared/task-completion.md +5 -5
- package/config/skills/_shared/warm-build-dirs.py +13 -0
- package/config/skills/devlyn-ideate/references/loop.md +1 -1
- package/config/skills/devlyn-ideate/scripts/acceptance.py +11 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +17 -1
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/bring-in.py +10 -0
- package/native/claude/skills/_shared/task-complete.py +319 -54
- package/native/claude/skills/_shared/task-completion.md +5 -5
- package/native/claude/skills/_shared/warm-build-dirs.py +13 -0
- package/native/claude/skills/devlyn-ideate/references/loop.md +1 -1
- package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +11 -0
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +17 -1
- 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/bring-in.py +10 -0
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +319 -54
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +5 -5
- package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +13 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +11 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +17 -1
- package/package.json +1 -1
- package/release.json +32 -32
package/README.md
CHANGED
|
@@ -72,7 +72,9 @@ A bare intent is planned and, when your request authorizes the work, added and d
|
|
|
72
72
|
|
|
73
73
|
Direct work edits your current checkout; run parallel sessions in separate worktrees (for example `claude -w`). Completed, verified direct requests with changes, and
|
|
74
74
|
every drained task, are delivered by project policy from their own linked worktrees. A commit request stays local and a PR request stops at the PR; otherwise accepted tasks default to
|
|
75
|
-
scoped commit → push → PR → merge when the repository allows it (otherwise the PR waits for a person
|
|
75
|
+
scoped commit → push → PR → merge when the repository allows it (otherwise the PR waits for a person; while required
|
|
76
|
+
checks or reviews have not passed it stays pending, and running the printed resume after they pass merges it, or a person
|
|
77
|
+
merges it; with GitHub's Allow auto-merge on, delivery requests the merge, so it lands by itself). After the
|
|
76
78
|
PR merges, the worktree and branch its session released are cleaned; anything in use is
|
|
77
79
|
kept. Set `git config --local devlyn.completionMode pr` to stop at the PR; `task-complete.py complete --mode auto|pr`
|
|
78
80
|
overrides one task. Local-only/no-push instructions take precedence: nothing is pushed or merged;
|
package/bin/devlyn.js
CHANGED
|
@@ -616,23 +616,46 @@ function installLocalSkill(skillName, roots, installedSkills = new Map()) {
|
|
|
616
616
|
return true;
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
// One skill from `source` into one skill-loader directory. The copy is staged and checked first,
|
|
620
|
-
//
|
|
619
|
+
// One skill from `source` into one skill-loader directory. The copy is staged and checked first, and
|
|
620
|
+
// the older copy is moved aside into the stage before the new one is renamed in, and back if that
|
|
621
|
+
// fails, so a failed copy or swap leaves the older copy in place for a plain rerun. If moving it back
|
|
622
|
+
// or removing it after the swap fails, the error names where it is kept. A copy under its name
|
|
621
623
|
// before the 4.0.0 rename goes only once the new one is complete.
|
|
622
624
|
function installSkillInto(source, target, skillName, installedSkills) {
|
|
623
625
|
const dest = path.join(target, skillName);
|
|
624
626
|
const stage = fs.mkdtempSync(path.join(target, '.devlyn-skill-'));
|
|
625
627
|
const stagedSkill = path.join(stage, skillName);
|
|
628
|
+
const previous = path.join(stage, 'previous');
|
|
629
|
+
let files;
|
|
630
|
+
let moved = false;
|
|
626
631
|
try {
|
|
627
|
-
|
|
632
|
+
files = copyRecursive(path.join(source, skillName), stagedSkill, stage);
|
|
628
633
|
assertCompleteSkillInstall(source, stage, [skillName]);
|
|
629
|
-
fs.
|
|
634
|
+
if (fs.lstatSync(dest, { throwIfNoEntry: false })) {
|
|
635
|
+
fs.renameSync(dest, previous);
|
|
636
|
+
moved = true;
|
|
637
|
+
}
|
|
630
638
|
fs.renameSync(stagedSkill, dest);
|
|
631
|
-
|
|
632
|
-
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (moved) {
|
|
641
|
+
try { fs.renameSync(previous, dest); }
|
|
642
|
+
catch (rollback) {
|
|
643
|
+
throw new AggregateError([error, rollback], `${error.message}; ${rollback.message}; the previous copy is kept at ${previous}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
try { fs.rmSync(stage, { recursive: true, force: true }); }
|
|
647
|
+
catch (cleanup) {
|
|
648
|
+
throw new AggregateError([error, cleanup], `${error.message}; ${dest} is as it was, but ${stage} could not be removed: ${cleanup.message}`);
|
|
649
|
+
}
|
|
650
|
+
throw new Error(`${error.message}; ${dest} is as it was`, { cause: error });
|
|
651
|
+
}
|
|
652
|
+
installedSkills.set(dest, files.map((file) => path.join(dest, path.relative(stagedSkill, file))));
|
|
653
|
+
try {
|
|
633
654
|
fs.rmSync(stage, { recursive: true, force: true });
|
|
655
|
+
} catch (error) {
|
|
656
|
+
const kept = fs.lstatSync(previous, { throwIfNoEntry: false }) ? `its previous copy at ${previous}` : stage;
|
|
657
|
+
throw new Error(`${dest} is installed, but ${kept} could not be removed: ${error.message}`);
|
|
634
658
|
}
|
|
635
|
-
|
|
636
659
|
}
|
|
637
660
|
|
|
638
661
|
// A 0.6.0-0.7.1 copy is one SKILL.md, the file that identifies it. A failed swap leaves
|
|
@@ -2256,7 +2279,9 @@ function cleanupDeliver(scope, items, current) {
|
|
|
2256
2279
|
if (!prepared.acceptance) return cleanupUpdateCheckout(scope);
|
|
2257
2280
|
const completed = lastJson(run(['complete', '--receipt', allocated.receipt, '--acceptance', prepared.acceptance, '--writers-stopped'], scope.root).stdout);
|
|
2258
2281
|
const status = completed?.delivery_status || completed?.status;
|
|
2259
|
-
|
|
2282
|
+
// A merge whose workspace or scratch the helper retained (CLEANUP_PENDING) is not a clean merge: report its resume.
|
|
2283
|
+
if (status === 'COMPLETE' || status === 'MERGED') return { ...cleanupUpdateCheckout(scope), delivery: completed.status === 'CLEANUP_PENDING'
|
|
2284
|
+
? { state: 'cleanup-pending', url: completed.pr || null, resume: completed.resume } : { state: 'merged', url: completed.pr || null } };
|
|
2260
2285
|
if (completed?.pr) return { delivery: { state: 'pr-open', url: completed.pr } };
|
|
2261
2286
|
const reason = completed?.reason || 'delivery did not complete';
|
|
2262
2287
|
return { error: cleanupReason(failureCode(reason), { detail: reason, ...(completed?.resume ? { resume: completed.resume } : {}) }) };
|
|
@@ -380,7 +380,10 @@ function inspectSkills(context, release, env, config, active) {
|
|
|
380
380
|
for (const name of SKILLS) {
|
|
381
381
|
const dir = path.join(root, name);
|
|
382
382
|
try {
|
|
383
|
-
|
|
383
|
+
// A lookup through a file finds no entry: POSIX says ENOTDIR where Windows says "not found".
|
|
384
|
+
let entry;
|
|
385
|
+
try { entry = fs.lstatSync(dir, { throwIfNoEntry: false }); } catch (error) { if (error.code !== 'ENOTDIR') throw error; }
|
|
386
|
+
if (!entry) continue;
|
|
384
387
|
const hash = fingerprint(dir);
|
|
385
388
|
if (!hashes.has(name)) hashes.set(name, fingerprint(path.join(release.root, 'config/skills', name)));
|
|
386
389
|
found.push({ name, path: path.join(dir, 'SKILL.md'), owned: hash === hashes.get(name) || (history.skills[name] || []).includes(hash) });
|
package/bin/harness-status.js
CHANGED
|
@@ -70,16 +70,23 @@ function requirements() {
|
|
|
70
70
|
const probes = [
|
|
71
71
|
['python', true, 'python3', ['-B', '--version']],
|
|
72
72
|
['git', true, 'git', ['--version']],
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// Git Bash is a bash whose builtin `pwd -W` (an MSYS2 patch) prints a Windows path; its version label no longer
|
|
74
|
+
// tells it from Cygwin's. -p reads no BASH_ENV or ENV file; --norc no ~/.bashrc, which a bash at shell level 1
|
|
75
|
+
// otherwise reads when SSH_CLIENT is set or stdin is a socket (Node's pipes are). Long options come first. macOS
|
|
76
|
+
// and Linux bashes refuse -W, so there this row (never required) is unavailable.
|
|
77
|
+
['git-bash', process.platform === 'win32', 'bash', ['--norc', '-p', '-c', 'echo "$BASH_VERSION"; cd / && pwd -W']],
|
|
78
|
+
// gh 2.91 and later writes a telemetry device id into its state folder on every command unless this is off.
|
|
79
|
+
['gh', false, 'gh', ['--version'], { GH_TELEMETRY: 'false' }],
|
|
75
80
|
];
|
|
76
|
-
return [node, ...probes.map(([name, required, binary, argv]) => {
|
|
77
|
-
const result = spawnSync(binary, argv, { encoding: 'utf8', timeout: 5000, env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
|
|
81
|
+
return [node, ...probes.map(([name, required, binary, argv, env]) => {
|
|
82
|
+
const result = spawnSync(binary, argv, { encoding: 'utf8', timeout: 5000, env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1', ...env } });
|
|
78
83
|
const output = (result.stdout || result.stderr || '').trim();
|
|
79
|
-
const
|
|
80
|
-
|
|
84
|
+
const bash = name === 'git-bash';
|
|
85
|
+
// A bash's version is its $BASH_VERSION line, never digits in the path line after it.
|
|
86
|
+
const match = output.match(bash ? /^\d+\.\d+\.\d+(?=(?:\(\d+\)-\w+)?\r?$)/m : /\b(\d+)\.(\d+)(?:\.\d+)?/);
|
|
87
|
+
let available = !result.error && result.status === 0
|
|
88
|
+
&& (bash ? /^(?:[A-Za-z]:\/|\/\/)/.test(output.split('\n').pop()) : Boolean(match));
|
|
81
89
|
if (name === 'python' && available) available = Number(match[1]) > 3 || (Number(match[1]) === 3 && Number(match[2]) >= 11);
|
|
82
|
-
if (name === 'git-bash' && required && available) available = /mingw|msys/i.test(output);
|
|
83
90
|
return { name, required, available, version: match?.[0] || null,
|
|
84
91
|
reasons: available ? [] : [{ code: 'requirement-unavailable', params: { name } }] };
|
|
85
92
|
})];
|
|
@@ -168,4 +175,4 @@ function statusCommand(args) {
|
|
|
168
175
|
return status;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
module.exports = { harnessStatus, statusCommand, repositoryIdentity, inspectInstruction };
|
|
178
|
+
module.exports = { harnessStatus, statusCommand, repositoryIdentity, inspectInstruction, requirements };
|
package/bin/skill-history.json
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"406b982f6ca0b9f5c0faa6cb50749b52aaf23545e06174ec39f831929dc929bd",
|
|
58
58
|
"40f50f3c3c0d030d1efeb6955c0c6f32f518e737d8dd3bec6610ff02badebbdb",
|
|
59
59
|
"430d156eb8c62c7edba3d660050f90b267f928976c3a3688e95d54f00dc34747",
|
|
60
|
+
"446e47fc34c81ed4cb9f4332d446a93025ae3dc1791155a4b80d6f195255958e",
|
|
60
61
|
"450d578ef177a9e8f0b693940ad64b41c1d9719975ba6305f62a7c82ac3a892f",
|
|
61
62
|
"45ef27af9ec27089e2861c13b882521fc6bf073dc6b7a356b326b4a27c5e6e24",
|
|
62
63
|
"4685be6a94ad5587ce53785c14a60cccf9a852d9d23391dba0a5bcd0c64931cf",
|
|
@@ -120,6 +121,7 @@
|
|
|
120
121
|
"8c80926ab03b2477e52264495b35c2116f65fc51b02e8c56e441b5705872392a",
|
|
121
122
|
"8fb73c0d2472b3643ac6e3f0ad87d818694afebaf68fcad596496e703893c43d",
|
|
122
123
|
"9162fc2fed6f4a611d299485be3a397080beb3304865ce59b95576ba7d00650e",
|
|
124
|
+
"92ffe78e156d8f11f666a92b26d21ae52e147ed9cad7eb79efbef334a9373281",
|
|
123
125
|
"94a14a1b87df1d7297d12949a24e47a0d6c16a8fe30495ee759fcabb2e1aa398",
|
|
124
126
|
"978e4210e49b42827c98cf6e40c868c87dc6e3533a2b779ca2fa05abfc16ad0f",
|
|
125
127
|
"97e7ad89c74c609b15dfbf6f9f7ac61df8b50e97e4bc318f72922e058ecffba4",
|
|
@@ -197,6 +199,7 @@
|
|
|
197
199
|
"f2bcfa0471aba81a10e3d0e6fd43bd6ec670601a4ba3a7d719e595edf9ab032a",
|
|
198
200
|
"f958bfabd2e056fea911526fba2196d2ed5a48828f576e5ce1657100b466db20",
|
|
199
201
|
"f9617e1034543f31ae2a3820301fbac6e4674e5d6fbc0d4b76751fe87b8e013e",
|
|
202
|
+
"fbca65e47854425acd80087e89024fb9d2f44f853d88abe582fbee107a3e8de3",
|
|
200
203
|
"fd68c7a74272929a14e685e447a51058ffcdd4c1d6bef83cbba18045c07aee55",
|
|
201
204
|
"fdb608b34a8600b4ce209d7f645ec5ad4475948be6ad95c941cc7ef02b5959df",
|
|
202
205
|
"fe357defe8ac6efd6bbd15b3bf35f79d79761c0f56d507488d9428536d4fb5f7",
|
|
@@ -365,6 +368,7 @@
|
|
|
365
368
|
"36b58dbdc26e51abc05a60872c4582faeebd67e9fe5626a3933467ba07c68dc4",
|
|
366
369
|
"40237cd5259dbcf7c490f130e844a9e32eea48a8edabd50800edee5029bf95a4",
|
|
367
370
|
"4213143689f7e7fe62ab8743c256a45492fafffc1757d64901b7b02725aee5c3",
|
|
371
|
+
"437659470cb978840f503c7d4a2af75c78beb2752cc3eed413bb33de346bd561",
|
|
368
372
|
"472d2ad853c96651324aa9a3de00cb61c0cecacb389e850ab1b380d535d22922",
|
|
369
373
|
"49d6e024b343dd77e4c0cea0e513e40fe679a7a8192953190d042b3a21bbf69c",
|
|
370
374
|
"5536f8391ec4442f1b793dba8dd5d7820d45f4b6dadf1111cbac9d54469a01ca",
|
|
@@ -384,6 +388,7 @@
|
|
|
384
388
|
"6e2b54571bfbc1fab571b81564fb0625ed83333fc12277c473fdf5a231f7d40c",
|
|
385
389
|
"6e7228610628fc4bca31dc0b0aaf5eb4445e1f123f2c54bd13921c5cd01129fe",
|
|
386
390
|
"75cecbce4d1f4adbba608cef498ae7e421ea75512c6bf9282b66cd94d821f672",
|
|
391
|
+
"78be1509ca8cf797f8c6e790d0d49ab608e50e0b828916c8c6ee790165ce792b",
|
|
387
392
|
"7f451e48e391643b34f6249244bc47d93d6303875e7b092771e3af0718343bc1",
|
|
388
393
|
"7fb3ceaf835230e1a75e510b046f4fe26e79815760354bec1949fa55515c6316",
|
|
389
394
|
"83e9de10d3c439af17dc4d7a603109d35136c5c8bd468ab2be962e1fad064c3c",
|
|
@@ -407,6 +412,7 @@
|
|
|
407
412
|
"db8d921aabf0d04811b55465a3757c62c4b39631aebaf6eecc994ca241cfbe56",
|
|
408
413
|
"e25da746dc37b2e82c658a28d9481fd69741151045683eee2119582ddaf731cd",
|
|
409
414
|
"e47a2ce766ae81fa0870e0a8dfbfe737311de69ce620e92217abbe4c532e3c8b",
|
|
415
|
+
"e67d3662836adf13c2e7177553bba6e7695484bd40438e8188cb9dd6d956cd2f",
|
|
410
416
|
"e929f76c15349f1b9bc1e84e0a3e9e48e916168d79270e0f4a36ea8eab9b5388",
|
|
411
417
|
"ebc693658da0b260c876f29cd6dfc2b12ae6690e3532622fa9c1cd4481162bd8",
|
|
412
418
|
"f2243cc4150e225961825f6097b0cf38166c291dcf58127516ceee841f7ed4c8",
|
|
@@ -829,6 +835,7 @@
|
|
|
829
835
|
"4.3.3",
|
|
830
836
|
"4.3.30",
|
|
831
837
|
"4.3.31",
|
|
838
|
+
"4.3.32",
|
|
832
839
|
"4.3.4",
|
|
833
840
|
"4.3.5",
|
|
834
841
|
"4.3.6",
|
|
@@ -363,6 +363,8 @@ class BringInTests(unittest.TestCase):
|
|
|
363
363
|
self.env = {key: value for key, value in os.environ.items()
|
|
364
364
|
if not key.startswith("GIT_")}
|
|
365
365
|
self.env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=str(self.root / "no-global"),
|
|
366
|
+
# A commit's detached automatic maintenance would still write into .git while a test copies it.
|
|
367
|
+
GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="maintenance.auto", GIT_CONFIG_VALUE_0="false",
|
|
366
368
|
GIT_AUTHOR_NAME="Fixture", GIT_AUTHOR_EMAIL="fixture@example.invalid",
|
|
367
369
|
GIT_COMMITTER_NAME="Fixture", GIT_COMMITTER_EMAIL="fixture@example.invalid",
|
|
368
370
|
LC_ALL="C", GIT_EDITOR=":", GIT_MERGE_AUTOEDIT="no")
|
|
@@ -415,6 +417,14 @@ class BringInTests(unittest.TestCase):
|
|
|
415
417
|
path.write_bytes(("#!/bin/sh\n" + body).encode("utf-8"))
|
|
416
418
|
path.chmod(0o755)
|
|
417
419
|
|
|
420
|
+
def test_fixture_commit_starts_no_automatic_maintenance(self):
|
|
421
|
+
# Its detached child would still write .git/objects/maintenance.lock while copytree or cleanup reads the tree.
|
|
422
|
+
trace = self.root / "trace.json"
|
|
423
|
+
self.env["GIT_TRACE2_EVENT"] = str(trace)
|
|
424
|
+
self.commit("traced", "traced\n")
|
|
425
|
+
children = [line for line in trace.read_text(encoding="utf-8").splitlines() if '"event":"child_start"' in line]
|
|
426
|
+
self.assertEqual(children, [])
|
|
427
|
+
|
|
418
428
|
def test_fast_forward_and_paths_with_spaces(self):
|
|
419
429
|
self.cli()
|
|
420
430
|
self.assertEqual(self.g("rev-parse", "HEAD").strip(), self.target)
|