devlyn-cli 4.3.32 → 4.3.33
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 +1 -1
- package/bin/devlyn.js +76 -14
- package/bin/skill-history.json +3 -0
- package/config/skills/_shared/task-complete.py +472 -37
- package/config/skills/_shared/task-completion.md +1 -1
- package/config/skills/devlyn-ideate/SKILL.md +3 -2
- package/config/skills/devlyn-ideate/references/loop.md +1 -1
- package/config/skills/devlyn-ideate/scripts/queue.py +668 -9
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/task-complete.py +472 -37
- package/native/claude/skills/_shared/task-completion.md +1 -1
- package/native/claude/skills/devlyn-ideate/SKILL.md +3 -2
- package/native/claude/skills/devlyn-ideate/references/loop.md +1 -1
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +668 -9
- 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/task-complete.py +472 -37
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +3 -2
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +668 -9
- package/package.json +1 -1
- package/release.json +24 -24
package/README.md
CHANGED
|
@@ -255,7 +255,7 @@ On native Windows, use native Node/npm and Python plus Git for Windows Bash for
|
|
|
255
255
|
|
|
256
256
|
Drain worktrees default to `<repo parent>/.devlyn-worktrees/<repo name>/<loop-id>/<task-id>`. On Windows, a newly created shared container is marked hidden, and each worktree disables content indexing for new children; names stay indexed and Microsoft Defender scanning is unaffected. For Defender performance, use a Dev Drive via `--worktree-root` or an administrator's exclusion for the root; see [loop options](config/skills/devlyn-ideate/references/loop.md).
|
|
257
257
|
|
|
258
|
-
Windows completion
|
|
258
|
+
Windows completion keeps a merged task's workspace, with its owned refs and recovery receipt, while a program has a file open in it or runs in it, and removes it once that program lets go; a workspace a program only watches may be removed. A managed workspace is kept the same way and removed by hand as the reason says; scratch that holds files is kept too. Resume guidance and delivery status remain separate from product verification. The portability workflow checks out the repository on native Windows and tests the POSIX-packed npm artifact there. A POSIX pass alone does not establish Windows support: acceptance requires the passing Windows job for the exact source SHA/artifact hashes.
|
|
259
259
|
|
|
260
260
|
## Contributing
|
|
261
261
|
|
package/bin/devlyn.js
CHANGED
|
@@ -1463,19 +1463,78 @@ function cleanupRealParents(base, file) {
|
|
|
1463
1463
|
return true;
|
|
1464
1464
|
}
|
|
1465
1465
|
|
|
1466
|
-
|
|
1467
|
-
//
|
|
1468
|
-
//
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1466
|
+
// The process list that decides whether a copy is in use (a drain started from a copy runs its queue.py and
|
|
1467
|
+
// acceptance.py by absolute path). A path is compared only with text the lister itself printed for that very
|
|
1468
|
+
// path, in a list whose rows are all rendered the same way. A list that cannot be read clears nothing.
|
|
1469
|
+
// POSIX: under the C locale `ps` renders each byte by itself, alike in every row, and the shell it runs from
|
|
1470
|
+
// holds each path between random tokens in its own row, the witness. Each shell's arguments fit one page,
|
|
1471
|
+
// which every kernel accepts and prints whole.
|
|
1472
|
+
const CLEANUP_LIST_BYTES = 4096;
|
|
1473
|
+
// Windows: each command line travels as its UTF-16 code units in decimal, which no code page changes, and this
|
|
1474
|
+
// marker in PowerShell's own command line must come back intact.
|
|
1475
|
+
const CLEANUP_LIST_MARKER = '✓한';
|
|
1476
|
+
const CLEANUP_LIST_COMMAND = `Get-CimInstance Win32_Process | ForEach-Object { ([int[]][char[]]"$($_.CommandLine)") -join ',' } # ${CLEANUP_LIST_MARKER}`;
|
|
1477
|
+
|
|
1478
|
+
// A Windows list, one command line per row; null when a row is malformed or the marker did not come back in
|
|
1479
|
+
// PowerShell's own row beside the command text before it (Node writes each `"` of the command there as `\"`).
|
|
1480
|
+
function cleanupWindowsRows(text) {
|
|
1481
|
+
const rows = [];
|
|
1482
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1483
|
+
const units = line ? line.split(',') : [];
|
|
1484
|
+
if (!units.every((unit) => /^\d{1,5}$/.test(unit) && Number(unit) <= 0xffff)) return null;
|
|
1485
|
+
rows.push(String.fromCharCode(...units.map(Number)));
|
|
1486
|
+
}
|
|
1487
|
+
const own = CLEANUP_LIST_COMMAND.slice(CLEANUP_LIST_COMMAND.lastIndexOf('"') + 1);
|
|
1488
|
+
return rows.some((row) => row.includes(own)) ? rows : null;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// A drain started from Git Bash may name a copy C:/… or /c/…: compared lower-cased, with slashes.
|
|
1492
|
+
function cleanupWindowsNames(row, dir) {
|
|
1493
|
+
const text = row.toLowerCase().replaceAll('\\', '/');
|
|
1494
|
+
const name = `${dir}\\`.toLowerCase().replaceAll('\\', '/');
|
|
1495
|
+
return text.includes(name) || (/^[a-z]:\//.test(name) && text.includes(`/${name[0]}${name.slice(2)}`));
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Which of `dirs` a running process names on its command line.
|
|
1499
|
+
function cleanupInUse(dirs) {
|
|
1500
|
+
const options = { timeout: 20000, maxBuffer: 64 * 1024 * 1024 };
|
|
1501
|
+
const others = (rows) => rows.filter((row) => !row.includes(' cleanup --'));
|
|
1502
|
+
if (process.platform === 'win32') {
|
|
1503
|
+
const listing = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', CLEANUP_LIST_COMMAND], options);
|
|
1504
|
+
const rows = !listing.error && listing.status === 0 ? cleanupWindowsRows(listing.stdout.toString('latin1')) : null;
|
|
1505
|
+
return new Set(dirs.filter((dir) => !rows || others(rows).some((row) => cleanupWindowsNames(row, dir))));
|
|
1506
|
+
}
|
|
1507
|
+
const script = 'ps -axww -o command=; exit $?';
|
|
1508
|
+
// The shell's arguments with 16-letter tokens around the paths, each argument with its NUL.
|
|
1509
|
+
const size = (batch) => Buffer.byteLength(['sh', '-c', script, ...batch].join('\0')) + 1 + 17 * (batch.length + 1);
|
|
1510
|
+
const used = new Set();
|
|
1511
|
+
const batches = [];
|
|
1512
|
+
for (const name of new Set(dirs.map((dir) => dir + path.sep))) {
|
|
1513
|
+
if (batches.length && size([...batches.at(-1), name]) <= CLEANUP_LIST_BYTES) batches.at(-1).push(name);
|
|
1514
|
+
else if (size([name]) <= CLEANUP_LIST_BYTES) batches.push([name]);
|
|
1515
|
+
else used.add(name);
|
|
1516
|
+
}
|
|
1517
|
+
for (const batch of batches) {
|
|
1518
|
+
let tokens;
|
|
1519
|
+
do {
|
|
1520
|
+
tokens = [...batch, ''].map(() => crypto.randomBytes(8).toString('hex'));
|
|
1521
|
+
} while (new Set(tokens).size < tokens.length || tokens.some((token) => batch.some((name) => name.includes(token))));
|
|
1522
|
+
const listing = spawnSync('sh', ['-c', script, ...batch.flatMap((name, i) => [tokens[i], name]), tokens.at(-1)],
|
|
1523
|
+
{ ...options, env: { ...process.env, LC_ALL: 'C' } });
|
|
1524
|
+
const rows = !listing.error && listing.status === 0 ? listing.stdout.toString('latin1').split('\n') : [];
|
|
1525
|
+
// The witness holds every token once, in order; `ps` joins arguments with one space, dropped on each side.
|
|
1526
|
+
const marks = (row) => tokens.map((token) => (row.split(token).length === 2 ? row.indexOf(token) : -1));
|
|
1527
|
+
const witnesses = rows.filter((row) => marks(row).every((mark, i, all) => mark >= 0 && (i === 0 || all[i - 1] < mark)));
|
|
1528
|
+
const [witness] = witnesses, at = witnesses.length === 1 && marks(witness);
|
|
1529
|
+
const between = at ? batch.map((_, i) => witness.slice(at[i] + tokens[i].length, at[i + 1])) : [];
|
|
1530
|
+
const framed = between.length && between.every((text) => text.length > 2 && text.startsWith(' ') && text.endsWith(' '));
|
|
1531
|
+
// The witness alone is no user: any other row naming a path is one, another cleanup's lister shell included.
|
|
1532
|
+
const listed = others(rows.filter((row) => row !== witness));
|
|
1533
|
+
batch.forEach((name, i) => {
|
|
1534
|
+
if (!framed || listed.some((row) => row.includes(between[i].slice(1, -1)))) used.add(name);
|
|
1535
|
+
});
|
|
1476
1536
|
}
|
|
1477
|
-
|
|
1478
|
-
return cleanupProcesses === null || cleanupProcesses.some((line) => line.includes(dir + path.sep));
|
|
1537
|
+
return new Set(dirs.filter((dir) => used.has(dir + path.sep)));
|
|
1479
1538
|
}
|
|
1480
1539
|
|
|
1481
1540
|
// Markers are generated, not bundled: require the exact writer format, a shipped version and
|
|
@@ -1501,7 +1560,7 @@ function cleanupMarkerOwned(file, current) {
|
|
|
1501
1560
|
function cleanupSkillRoots(roots, current, callerBase = null) {
|
|
1502
1561
|
const resolved = new Map();
|
|
1503
1562
|
const linked = new Set();
|
|
1504
|
-
const
|
|
1563
|
+
const spellings = [];
|
|
1505
1564
|
for (const [base, root] of roots) {
|
|
1506
1565
|
let target = root;
|
|
1507
1566
|
const stat = fs.lstatSync(root, { throwIfNoEntry: false });
|
|
@@ -1521,8 +1580,11 @@ function cleanupSkillRoots(roots, current, callerBase = null) {
|
|
|
1521
1580
|
} else if (parents && stat?.isDirectory()) target = fs.realpathSync.native(root);
|
|
1522
1581
|
if (!resolved.has(target)) resolved.set(target, base);
|
|
1523
1582
|
const callerPath = (file) => callerBase ? path.join(callerBase, path.relative(base, file)) : file;
|
|
1524
|
-
|
|
1583
|
+
spellings.push([target, callerPath(root), callerPath(target)]);
|
|
1525
1584
|
}
|
|
1585
|
+
// Every path is known before the list is taken: the lister prints each of them beside the rows it is sought in.
|
|
1586
|
+
const used = cleanupInUse(spellings.flatMap(([, ...names]) => names));
|
|
1587
|
+
const active = new Set(spellings.filter(([, ...names]) => names.some((name) => used.has(name))).map(([target]) => target));
|
|
1526
1588
|
return [...resolved].flatMap(([root, base]) => {
|
|
1527
1589
|
const items = cleanupSkillRoot(base, root, current);
|
|
1528
1590
|
const callerRoot = callerBase ? path.join(callerBase, path.relative(base, root)) : root;
|
package/bin/skill-history.json
CHANGED
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"9e4d06fd466fa896040109bac0018fcfb2f1604d55abb4180ff1f1a0b665bbc8",
|
|
133
133
|
"9f14ca629e41730b1a9fb5d0ad1a39cff558fe34758d6ceccbb9de498f9f384e",
|
|
134
134
|
"a11950a5880346f03257c688fd8bc6f8c29fc0b7c05c90e8f5211d5fb7665774",
|
|
135
|
+
"a1d5a9505c527af88bcac9305ec8bce0ee01d472ec383e7ffacbcccf9137565b",
|
|
135
136
|
"a405a99b988f6edd2eabb0504b36309bb4cf0c3b97eab26bb7ef257e4515dbf3",
|
|
136
137
|
"a463693773934b6de78f5326fb17acac34e8159f459b89c21aa66f3ac0874a57",
|
|
137
138
|
"a66080b0ff25f35e8574a24ee01d87de9f579a59796977f3cc59f1865063f6f6",
|
|
@@ -353,6 +354,7 @@
|
|
|
353
354
|
"05e824f63a6381e02042afb1df0f1437584e25a80de8e9db0bd9456aaeb7aab8",
|
|
354
355
|
"0601ee535419b7039d5da34bd9c8e1c2b3631307214e4afaf394dd47494090c7",
|
|
355
356
|
"062fde2838c0edc0c20e6716948a2ae9fe9d8a98b01a8e8c544c5801641c7cdb",
|
|
357
|
+
"0c54ebc6c95f04ee0ec2f21aa4ebe34f4d2cfb9697b45a2b02999b963bbbdbca",
|
|
356
358
|
"0cef055580e00f9ed439eacb2dff9d238bb11619457472016095a8915162fa4b",
|
|
357
359
|
"0dc69c20434966015176d83baa6f95b3421f9e13d0b1bd608a22f0560eae1049",
|
|
358
360
|
"0fe9b270d670f70da9d2535969b6117684b7c1517806b3029ebaf9624da8006c",
|
|
@@ -836,6 +838,7 @@
|
|
|
836
838
|
"4.3.30",
|
|
837
839
|
"4.3.31",
|
|
838
840
|
"4.3.32",
|
|
841
|
+
"4.3.33",
|
|
839
842
|
"4.3.4",
|
|
840
843
|
"4.3.5",
|
|
841
844
|
"4.3.6",
|