devlyn-cli 4.2.26 → 4.3.0
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/bin/devlyn.js +510 -0
- package/bin/engines.js +20 -15
- package/bin/harness-activation.js +1 -2
- package/bin/release.js +16 -3
- package/bin/skill-history.json +2 -0
- package/config/skills/_shared/harness-store.py +201 -0
- package/config/skills/devlyn-ideate/SKILL.md +5 -1
- package/config/skills/devlyn-ideate/references/loop.md +12 -2
- package/config/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +5 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +156 -15
- package/package.json +3 -2
package/bin/devlyn.js
CHANGED
|
@@ -1341,6 +1341,513 @@ function showHelp() {
|
|
|
1341
1341
|
log('');
|
|
1342
1342
|
}
|
|
1343
1343
|
|
|
1344
|
+
// ---------------------------------------------------------------------------
|
|
1345
|
+
// Legacy cleanup for the built-in harness: Devlyn delivers devlyn-cli per session, so every copy an
|
|
1346
|
+
// earlier install left in a skill root, and the files those installs added, become legacy. A plan
|
|
1347
|
+
// is read-only and lists each item with its proof. Apply plans again from what is on disk now and
|
|
1348
|
+
// acts only on items both plans name the same way, so a plan file can narrow a cleanup but never
|
|
1349
|
+
// widen it: untracked copies go in place, tracked ones through the project's delivery policy.
|
|
1350
|
+
// Nothing is removed by name alone except what the installer itself already removes.
|
|
1351
|
+
// ---------------------------------------------------------------------------
|
|
1352
|
+
|
|
1353
|
+
const CLEANUP_CORE = ['devlyn-ideate', 'devlyn-engines'];
|
|
1354
|
+
|
|
1355
|
+
function cleanupReason(code, params = {}) { return { code, params }; }
|
|
1356
|
+
|
|
1357
|
+
// What a path is now, so apply can refuse an item that changed after it was reviewed.
|
|
1358
|
+
function cleanupPreimage(file) {
|
|
1359
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
1360
|
+
if (!stat) return { type: 'missing', sha256: null };
|
|
1361
|
+
if (stat.isSymbolicLink()) return { type: 'link', sha256: null };
|
|
1362
|
+
if (stat.isDirectory()) {
|
|
1363
|
+
try { return { type: 'directory', sha256: fingerprint(file) }; }
|
|
1364
|
+
catch { return { type: 'directory', sha256: null }; }
|
|
1365
|
+
}
|
|
1366
|
+
if (stat.isFile()) return { type: 'file', sha256: crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') };
|
|
1367
|
+
return { type: 'special', sha256: null };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Whether every directory from `base` down to `file`'s parent is a real directory (no link is followed).
|
|
1371
|
+
function cleanupRealParents(base, file) {
|
|
1372
|
+
let dir = base;
|
|
1373
|
+
if (!fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory()) return false;
|
|
1374
|
+
for (const part of path.relative(base, path.dirname(file)).split(path.sep).filter(Boolean)) {
|
|
1375
|
+
if (part === '..') return false;
|
|
1376
|
+
dir = path.join(dir, part);
|
|
1377
|
+
if (!fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory()) return false;
|
|
1378
|
+
}
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
let cleanupProcesses;
|
|
1383
|
+
// Whether a running process names `dir` on its command line (a drain started from a global copy runs
|
|
1384
|
+
// its queue.py and acceptance.py by absolute path). Read once per command; unreadable means unknown.
|
|
1385
|
+
function cleanupInUse(dir) {
|
|
1386
|
+
if (cleanupProcesses === undefined) {
|
|
1387
|
+
const listing = process.platform === 'win32'
|
|
1388
|
+
? spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command',
|
|
1389
|
+
'Get-CimInstance Win32_Process | ForEach-Object { $_.CommandLine }'], { encoding: 'utf8', timeout: 20000 })
|
|
1390
|
+
: spawnSync('ps', ['-axww', '-o', 'command='], { encoding: 'utf8', timeout: 20000 });
|
|
1391
|
+
cleanupProcesses = listing.status === 0 ? listing.stdout.split(/\r?\n/).filter((line) => line && !line.includes(' cleanup --')) : null;
|
|
1392
|
+
}
|
|
1393
|
+
// A list that cannot be read cannot clear anything: the roots stay until it can.
|
|
1394
|
+
return cleanupProcesses === null || cleanupProcesses.some((line) => line.includes(dir + path.sep));
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// The legacy items of one skill root, each with its proof and the base its parents are checked from.
|
|
1398
|
+
// `_shared` and the install marker go only when every copy beside them goes too: a kept, edited
|
|
1399
|
+
// devlyn-ideate needs both.
|
|
1400
|
+
function cleanupSkillRoot(base, root, current) {
|
|
1401
|
+
const items = [];
|
|
1402
|
+
const rootStat = fs.lstatSync(root, { throwIfNoEntry: false });
|
|
1403
|
+
if (!rootStat) return items;
|
|
1404
|
+
if (!cleanupRealParents(base, root) || rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
1405
|
+
if (rootStat.isSymbolicLink() || rootStat.isDirectory()) {
|
|
1406
|
+
items.push({ kind: 'core-skill', path: root, base, action: 'skip', reasons: [cleanupReason('outside-link')] });
|
|
1407
|
+
}
|
|
1408
|
+
return items;
|
|
1409
|
+
}
|
|
1410
|
+
const renamed = new Map();
|
|
1411
|
+
for (const [old, current] of Object.entries(RENAMED_SKILLS)) {
|
|
1412
|
+
for (const spelling of legacySkillPaths(root, old)) renamed.set(path.basename(spelling).toLowerCase(), current);
|
|
1413
|
+
}
|
|
1414
|
+
const deprecated = new Set([...deprecatedSkillPaths(root)].map((file) => path.basename(file).toLowerCase()));
|
|
1415
|
+
const support = [];
|
|
1416
|
+
let kept = 0;
|
|
1417
|
+
let marker = false;
|
|
1418
|
+
for (const name of fs.readdirSync(root).sort()) {
|
|
1419
|
+
const file = path.join(root, name);
|
|
1420
|
+
const stat = fs.lstatSync(file);
|
|
1421
|
+
const lower = name.toLowerCase();
|
|
1422
|
+
if (name === DEVLYN_INSTALL_MARKER) { marker = true; continue; }
|
|
1423
|
+
if (name === DEVLYN_INSTALL_LOCK) {
|
|
1424
|
+
items.push({ kind: 'install-lock', path: file, base, action: 'skip', reasons: [cleanupReason('occupied-lock')] });
|
|
1425
|
+
kept++;
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
const core = CLEANUP_CORE.includes(name) || name === '_shared';
|
|
1429
|
+
if (!core && !deprecated.has(lower) && !renamed.has(lower) && !RETIRED_SKILL_MD_SHA256[lower]) continue;
|
|
1430
|
+
if (stat.isSymbolicLink()) {
|
|
1431
|
+
items.push({ kind: 'core-skill', path: file, base, action: 'skip', reasons: [cleanupReason('outside-link')] });
|
|
1432
|
+
kept++;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (core) {
|
|
1436
|
+
let proven = false;
|
|
1437
|
+
try { proven = fingerprint(file) === current[name] || historicalSkill(file); }
|
|
1438
|
+
catch { /* A link or special file inside is not an unedited copy. */ }
|
|
1439
|
+
if (name === '_shared') { if (proven) support.push({ kind: 'core-skill', path: file, base, action: 'remove', reasons: [] }); continue; }
|
|
1440
|
+
if (proven) items.push({ kind: 'core-skill', path: file, base, action: 'remove', reasons: [] });
|
|
1441
|
+
else { items.push({ kind: 'core-skill', path: file, base, action: 'skip', reasons: [cleanupReason('modified-copy')] }); kept++; }
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (renamed.has(lower)) {
|
|
1445
|
+
// An optional skill under its pre-4.0 name: the installer moves it to its new name; cleanup keeps it.
|
|
1446
|
+
if (CLEANUP_CORE.includes(renamed.get(lower))) items.push({ kind: 'renamed-skill', path: file, base, action: 'remove', reasons: [] });
|
|
1447
|
+
else { items.push({ kind: 'renamed-skill', path: file, base, action: 'skip', reasons: [cleanupReason('optional-skill')] }); kept++; }
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (deprecated.has(lower)) { items.push({ kind: 'retired-skill', path: file, base, action: 'remove', reasons: [] }); continue; }
|
|
1451
|
+
if (isShippedCopy(file, RETIRED_SKILL_MD_SHA256[lower])) items.push({ kind: 'retired-standard', path: file, base, action: 'remove', reasons: [] });
|
|
1452
|
+
}
|
|
1453
|
+
if (kept === 0) {
|
|
1454
|
+
items.push(...support);
|
|
1455
|
+
if (marker && items.some((item) => item.action === 'remove')) {
|
|
1456
|
+
items.push({ kind: 'install-marker', path: path.join(root, DEVLYN_INSTALL_MARKER), base, action: 'remove', reasons: [] });
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (cleanupInUse(root)) {
|
|
1460
|
+
for (const item of items) if (item.action === 'remove') Object.assign(item, { action: 'skip', reasons: [cleanupReason('in-use')] });
|
|
1461
|
+
}
|
|
1462
|
+
return items;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function cleanupCommands(base) {
|
|
1466
|
+
const commands = path.join(base, '.claude', 'commands');
|
|
1467
|
+
if (!cleanupRealParents(base, path.join(commands, 'x')) || !fs.lstatSync(commands, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
1468
|
+
const names = new Set(DEPRECATED_FILES.map((file) => path.basename(file).toLowerCase()));
|
|
1469
|
+
return fs.readdirSync(commands).sort().filter((name) => names.has(name.toLowerCase())
|
|
1470
|
+
&& fs.lstatSync(path.join(commands, name)).isFile())
|
|
1471
|
+
.map((name) => ({ kind: 'legacy-command', path: path.join(commands, name), base, action: 'remove', reasons: [] }));
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function cleanupProjectFiles(base) {
|
|
1475
|
+
const items = [];
|
|
1476
|
+
const hash = (file) => crypto.createHash('sha256')
|
|
1477
|
+
.update(fs.readFileSync(file).toString('latin1').replace(/\r\n/g, '\n'), 'latin1').digest('hex');
|
|
1478
|
+
for (const [name, hashes] of Object.entries(CLEANUP_HISTORY.files)) {
|
|
1479
|
+
const file = path.join(base, name);
|
|
1480
|
+
if (cleanupRealParents(base, file) && fs.lstatSync(file, { throwIfNoEntry: false })?.isFile() && hashes.includes(hash(file))) {
|
|
1481
|
+
items.push({ kind: 'legacy-file', path: file, base, action: 'remove', reasons: [] });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const settingsFile = path.join(base, '.claude', 'settings.json');
|
|
1485
|
+
if (cleanupRealParents(base, settingsFile) && fs.lstatSync(settingsFile, { throwIfNoEntry: false })?.isFile()) {
|
|
1486
|
+
try {
|
|
1487
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8').replace(/^/, ''));
|
|
1488
|
+
const stop = Array.isArray(settings?.hooks?.Stop) ? settings.hooks.Stop : [];
|
|
1489
|
+
if (stop.some((entry) => Array.isArray(entry?.hooks) && entry.hooks.some((hook) => hook?.type === 'command' && hook.command === RETIRED_STOP_HOOK))) {
|
|
1490
|
+
items.push({ kind: 'settings-entry', path: settingsFile, base, action: 'edit', reasons: [] });
|
|
1491
|
+
}
|
|
1492
|
+
} catch (error) { if (!(error instanceof SyntaxError)) throw error; }
|
|
1493
|
+
}
|
|
1494
|
+
return items;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// An intact managed block from an earlier release is brought up to date; owner text around it stays
|
|
1498
|
+
// byte-for-byte (updateInstructions). A current block is the team's principles and stays.
|
|
1499
|
+
function cleanupInstructions(base) {
|
|
1500
|
+
const { inspectInstruction } = require('./harness-status');
|
|
1501
|
+
const items = [];
|
|
1502
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
1503
|
+
const file = path.join(base, name);
|
|
1504
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
1505
|
+
if (!stat?.isFile() || stat.nlink !== 1) continue;
|
|
1506
|
+
let text;
|
|
1507
|
+
try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(file)); }
|
|
1508
|
+
catch { continue; }
|
|
1509
|
+
if (!/<!-- devlyn:instructions:begin sha256=/.test(text)) continue;
|
|
1510
|
+
const template = fs.readFileSync(path.join(CONFIG_SOURCE, '..', name), 'utf8');
|
|
1511
|
+
if (inspectInstruction(text, template, name).ownership === 'outdated') {
|
|
1512
|
+
items.push({ kind: 'instructions', path: file, base, action: 'edit', reasons: [] });
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return items;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function cleanupCurrentFingerprints() {
|
|
1519
|
+
return Object.fromEntries([...CLEANUP_CORE, '_shared'].map((name) => [name, fingerprint(path.join(CONFIG_SOURCE, 'skills', name))]));
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function cleanupGit(root, args) {
|
|
1523
|
+
return spawnSync('git', ['-C', root, ...args], { env: { ...gitEnvironment(), GIT_OPTIONAL_LOCKS: '0' }, encoding: 'utf8' });
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
let cleanupPythonBinary;
|
|
1527
|
+
function cleanupPython() {
|
|
1528
|
+
if (cleanupPythonBinary === undefined) {
|
|
1529
|
+
cleanupPythonBinary = ['python3', 'python'].find((name) => {
|
|
1530
|
+
const probe = spawnSync(name, ['--version'], { encoding: 'utf8', timeout: 10000 });
|
|
1531
|
+
return probe.status === 0 && /Python 3\.(1[1-9]|[2-9]\d)/.test(probe.stdout + probe.stderr);
|
|
1532
|
+
}) || null;
|
|
1533
|
+
}
|
|
1534
|
+
return cleanupPythonBinary;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// The GitHub repository and base branch a project's tracked changes are delivered to, or null.
|
|
1538
|
+
function cleanupRemote(root) {
|
|
1539
|
+
const url = cleanupGit(root, ['remote', 'get-url', 'origin']);
|
|
1540
|
+
if (url.status !== 0) return null;
|
|
1541
|
+
const match = url.stdout.trim().match(/github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
|
|
1542
|
+
if (!match) return null;
|
|
1543
|
+
const head = cleanupGit(root, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD']);
|
|
1544
|
+
// origin/HEAD names the default; without it, the remote-tracking branch that exists. With neither
|
|
1545
|
+
// fetched, `main`: an allocation against a missing base fails visibly, before any commit.
|
|
1546
|
+
const base = head.status === 0 ? head.stdout.trim().replace(/^origin\//, '')
|
|
1547
|
+
: ['main', 'master'].find((name) => cleanupGit(root, ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${name}`]).status === 0)
|
|
1548
|
+
|| 'main';
|
|
1549
|
+
return { repository: `${match[1]}/${match[2]}`, base, named: head.status === 0 };
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Whether a task is in flight on the installed copies: an allocated task's packet runs their exact
|
|
1553
|
+
// paths, so those copies stay until it settles. Pending rows and legacy rows run on any release.
|
|
1554
|
+
function cleanupQueueBusy(root) {
|
|
1555
|
+
const python = cleanupPython();
|
|
1556
|
+
if (!python) return cleanupReason('requirement-missing', { name: 'Python 3.11' });
|
|
1557
|
+
const queue = path.join(CONFIG_SOURCE, 'skills', 'devlyn-ideate', 'scripts', 'queue.py');
|
|
1558
|
+
const result = spawnSync(python, ['-B', queue, 'inspect', '--repo', root, '--json'],
|
|
1559
|
+
{ encoding: 'utf8', env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' }, timeout: 60000 });
|
|
1560
|
+
let data;
|
|
1561
|
+
try { data = JSON.parse(result.stdout); } catch { return cleanupReason('queue-unreadable'); }
|
|
1562
|
+
if (data.status === 'BLOCKED') return cleanupReason('queue-unreadable');
|
|
1563
|
+
return (data.counts?.active || 0) > 0 ? cleanupReason('legacy-queue') : null;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function cleanupSameFile(a, b) {
|
|
1567
|
+
const x = fs.statSync(a, { throwIfNoEntry: false });
|
|
1568
|
+
const y = fs.statSync(b, { throwIfNoEntry: false });
|
|
1569
|
+
return Boolean(x && y && x.dev === y.dev && x.ino === y.ino);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function cleanupProjectScope(project, current) {
|
|
1573
|
+
const { repositoryIdentity } = require('./harness-status');
|
|
1574
|
+
const { physicalRoot } = require('./release');
|
|
1575
|
+
const root = physicalRoot(project);
|
|
1576
|
+
const identity = repositoryIdentity(root);
|
|
1577
|
+
const base = identity?.root || root;
|
|
1578
|
+
const scope = { kind: 'project', root: base, name: path.basename(base), remote: identity ? cleanupRemote(base) : null, items: [] };
|
|
1579
|
+
const whole = (code) => [{ kind: 'project', path: base, base, relative: '', route: 'local', action: 'skip', reasons: [cleanupReason(code)] }];
|
|
1580
|
+
if (identity?.sourceRepository) return { ...scope, items: whole('source-repository') };
|
|
1581
|
+
// The home folder's roots are this machine's: the machine scope cleans them under the user-root locks.
|
|
1582
|
+
if (cleanupSameFile(base, os.homedir())) return { ...scope, items: whole('home-folder') };
|
|
1583
|
+
const items = [
|
|
1584
|
+
...['.agents', '.claude'].flatMap((dir) => cleanupSkillRoot(base, path.join(base, dir, 'skills'), current)),
|
|
1585
|
+
...cleanupCommands(base), ...cleanupProjectFiles(base), ...cleanupInstructions(base),
|
|
1586
|
+
];
|
|
1587
|
+
const busy = identity && items.some((item) => item.action !== 'skip') ? cleanupQueueBusy(base) : null;
|
|
1588
|
+
for (const item of items) {
|
|
1589
|
+
if (busy && item.action !== 'skip') Object.assign(item, { action: 'skip', reasons: [busy] });
|
|
1590
|
+
const relative = path.relative(base, item.path).split(path.sep).join('/');
|
|
1591
|
+
const tracked = Boolean(identity && relative && cleanupGit(base, ['ls-files', '-z', '--', relative]).stdout.length > 0);
|
|
1592
|
+
scope.items.push({ ...item, relative, route: tracked ? 'delivery' : 'local' });
|
|
1593
|
+
}
|
|
1594
|
+
if (!scope.remote) {
|
|
1595
|
+
for (const item of scope.items) {
|
|
1596
|
+
if (item.route === 'delivery' && item.action !== 'skip') Object.assign(item, { action: 'skip', reasons: [cleanupReason('no-remote')] });
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return scope;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function cleanupMachineScope(home, accountHomes, current) {
|
|
1603
|
+
const roots = [...['.agents', '.codex', '.claude', '.grok'].map((dir) => [home, path.join(home, dir, 'skills')]),
|
|
1604
|
+
...accountHomes.map((account) => [path.dirname(account), path.join(account, 'skills')])];
|
|
1605
|
+
const seen = new Set();
|
|
1606
|
+
const items = [];
|
|
1607
|
+
for (const [base, root] of roots) {
|
|
1608
|
+
if (seen.has(root)) continue;
|
|
1609
|
+
seen.add(root);
|
|
1610
|
+
items.push(...cleanupSkillRoot(base, root, current));
|
|
1611
|
+
}
|
|
1612
|
+
items.push(...cleanupCommands(home));
|
|
1613
|
+
return { kind: 'machine', root: home, name: null, remote: null, items: items.map((item) => ({ ...item, relative: null, route: 'local' })) };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function cleanupScopes(inputs, current) {
|
|
1617
|
+
const { sha256 } = require('./release');
|
|
1618
|
+
const scopes = [];
|
|
1619
|
+
if (inputs.machine) scopes.push({ id: 'machine', ...cleanupMachineScope(os.homedir(), inputs.accountHomes, current) });
|
|
1620
|
+
for (const project of inputs.projects) {
|
|
1621
|
+
let scope;
|
|
1622
|
+
try { scope = cleanupProjectScope(project, current); }
|
|
1623
|
+
catch (error) {
|
|
1624
|
+
scope = { kind: 'project', root: project, name: path.basename(project), remote: null, items: [{ kind: 'project', path: project, base: project,
|
|
1625
|
+
relative: '', route: 'local', action: 'skip', reasons: [cleanupReason('project-unreadable', { detail: error.message })] }] };
|
|
1626
|
+
}
|
|
1627
|
+
scopes.push({ id: `project:${sha256(Buffer.from(project)).slice(0, 12)}`, ...scope });
|
|
1628
|
+
}
|
|
1629
|
+
for (const scope of scopes) {
|
|
1630
|
+
scope.items = scope.items.map((item, index) => ({ id: `${scope.id}:${index}`, ...item, before: cleanupPreimage(item.path) }));
|
|
1631
|
+
}
|
|
1632
|
+
return scopes;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function cleanupPlan(inputs) {
|
|
1636
|
+
const plan = { schemaVersion: 1, version: PKG.version, inputs, scopes: cleanupScopes(inputs, cleanupCurrentFingerprints()) };
|
|
1637
|
+
return { ...plan, digest: cleanupDigest(plan) };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function cleanupDigest(plan) {
|
|
1641
|
+
const { sha256 } = require('./release');
|
|
1642
|
+
const canonical = (value) => Array.isArray(value) ? `[${value.map(canonical).join(',')}]`
|
|
1643
|
+
: value && typeof value === 'object'
|
|
1644
|
+
? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`
|
|
1645
|
+
: JSON.stringify(value);
|
|
1646
|
+
const { digest, ...body } = plan;
|
|
1647
|
+
return sha256(Buffer.from(canonical(body)));
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Apply one item a fresh plan proves right now; returns a reason when it is not what was proven.
|
|
1651
|
+
function cleanupApplyItem(item) {
|
|
1652
|
+
const now = cleanupPreimage(item.path);
|
|
1653
|
+
if (now.type !== item.before.type || now.sha256 !== item.before.sha256) return cleanupReason('stale-plan');
|
|
1654
|
+
if (!cleanupRealParents(item.base, item.path)) return cleanupReason('outside-link');
|
|
1655
|
+
if (item.action === 'remove') {
|
|
1656
|
+
fs.rmSync(item.path, { recursive: true }); // lstat/realParents never follow a link; rm removes the entry itself.
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
if (item.kind === 'settings-entry') {
|
|
1660
|
+
const settings = JSON.parse(fs.readFileSync(item.path, 'utf8').replace(/^/, ''));
|
|
1661
|
+
if (cleanRetiredSettings(settings)) replaceFile(item.path, JSON.stringify(settings, null, 2) + '\n');
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
if (item.kind === 'instructions') {
|
|
1665
|
+
updateInstructions(path.basename(item.path), '', true, true, path.dirname(item.path));
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
return cleanupReason('unknown-item');
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// A reviewed item's counterpart in the fresh plan, when both name the same action on the same bytes.
|
|
1672
|
+
function cleanupProven(reviewed, fresh) {
|
|
1673
|
+
const key = (item) => `${item.kind}\u0000${item.path}`;
|
|
1674
|
+
const now = new Map(fresh.items.map((item) => [key(item), item]));
|
|
1675
|
+
const asked = new Set(reviewed.items.filter((item) => item.action === 'remove' || item.action === 'edit').map(key));
|
|
1676
|
+
// A copy left out of the review stays, so the `_shared` and marker beside it stay too.
|
|
1677
|
+
const narrowed = new Set(fresh.items.filter((item) => item.action === 'remove' && !cleanupSupport(item) && !asked.has(key(item)))
|
|
1678
|
+
.map((item) => path.dirname(item.path)));
|
|
1679
|
+
return reviewed.items.filter((item) => item.action === 'remove' || item.action === 'edit').map((item) => {
|
|
1680
|
+
const match = now.get(key(item));
|
|
1681
|
+
const same = match && match.action === item.action && match.route === item.route
|
|
1682
|
+
&& match.before.type === item.before.type && match.before.sha256 === item.before.sha256
|
|
1683
|
+
&& !(cleanupSupport(item) && narrowed.has(path.dirname(item.path)));
|
|
1684
|
+
return { reviewed: item, fresh: same ? match : null };
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Support entries (`_shared`, the marker) go last, and only when nothing beside them was refused.
|
|
1689
|
+
function cleanupSupport(item) {
|
|
1690
|
+
return path.basename(item.path) === '_shared' || item.kind === 'install-marker';
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function cleanupLocal(scope, items) {
|
|
1694
|
+
const results = new Map();
|
|
1695
|
+
const roots = [...new Set(items.map((item) => path.dirname(item.path)).filter((dir) => path.basename(dir) === 'skills'
|
|
1696
|
+
&& fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory()))];
|
|
1697
|
+
const act = (item) => {
|
|
1698
|
+
try { results.set(item.id, cleanupApplyItem(item)); }
|
|
1699
|
+
catch (error) { results.set(item.id, cleanupReason('apply-failed', { detail: error.message })); }
|
|
1700
|
+
};
|
|
1701
|
+
const run = () => {
|
|
1702
|
+
for (const item of items.filter((item) => !cleanupSupport(item))) act(item);
|
|
1703
|
+
for (const item of items.filter(cleanupSupport)) {
|
|
1704
|
+
const refused = items.some((other) => !cleanupSupport(other) && path.dirname(other.path) === path.dirname(item.path)
|
|
1705
|
+
&& results.get(other.id) !== null);
|
|
1706
|
+
if (refused) results.set(item.id, cleanupReason('stale-plan'));
|
|
1707
|
+
else act(item);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
if (scope.kind === 'machine' && roots.length) withUserSkillLocks(roots, run, { existingOnly: true });
|
|
1711
|
+
else run();
|
|
1712
|
+
// A root that vanished under its lock ran nothing: every item it held is refused, never reported done.
|
|
1713
|
+
for (const item of items) if (!results.has(item.id)) results.set(item.id, cleanupReason('stale-plan'));
|
|
1714
|
+
// After the locks are released, so a root holds nothing but what stayed.
|
|
1715
|
+
for (const root of new Set(items.filter((item) => results.get(item.id) === null && item.kind !== 'instructions')
|
|
1716
|
+
.map((item) => path.dirname(item.path)))) {
|
|
1717
|
+
if (path.basename(root) !== 'skills') continue;
|
|
1718
|
+
try { if (fs.lstatSync(root).isDirectory() && fs.readdirSync(root).length === 0) fs.rmdirSync(root); } catch {}
|
|
1719
|
+
}
|
|
1720
|
+
return results;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Tracked items go through the project's own delivery: a fresh worktree from the remote base, the
|
|
1724
|
+
// same items proven there, one commit, and task-complete's PR and merge by project policy. Anything
|
|
1725
|
+
// it allocated is released again when it stops before that.
|
|
1726
|
+
function cleanupDeliver(scope, items, current) {
|
|
1727
|
+
const helper = path.join(CONFIG_SOURCE, 'skills', '_shared', 'task-complete.py');
|
|
1728
|
+
const python = cleanupPython();
|
|
1729
|
+
if (!python) return { error: cleanupReason('requirement-missing', { name: 'Python 3.11' }) };
|
|
1730
|
+
const run = (args, cwd) => spawnSync(python, ['-B', helper, ...args], { cwd, encoding: 'utf8', env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
|
|
1731
|
+
const lastJson = (text) => { try { return JSON.parse(text.trim().split('\n').at(-1)); } catch { return null; } };
|
|
1732
|
+
// Without origin/HEAD the base was a guess: GitHub names the default branch before anything is committed.
|
|
1733
|
+
let base = scope.remote.base;
|
|
1734
|
+
if (!scope.remote.named) {
|
|
1735
|
+
const asked = spawnSync('gh', ['repo', 'view', scope.remote.repository, '--json', 'defaultBranchRef', '-q', '.defaultBranchRef.name'],
|
|
1736
|
+
{ encoding: 'utf8', timeout: 30000 });
|
|
1737
|
+
if (asked.status === 0 && asked.stdout.trim()) base = asked.stdout.trim();
|
|
1738
|
+
}
|
|
1739
|
+
const short = crypto.randomBytes(4).toString('hex');
|
|
1740
|
+
const branch = `devlyn/cleanup-${short}`;
|
|
1741
|
+
const worktree = path.join(path.dirname(scope.root), `${path.basename(scope.root)}.devlyn.noindex`, `devlyn-cleanup-${short}`);
|
|
1742
|
+
const allocated = lastJson(run(['allocate', '--repo', scope.root, '--task', `devlyn-cli ${PKG.version} legacy cleanup`,
|
|
1743
|
+
'--branch', branch, '--worktree', worktree, '--repository', scope.remote.repository,
|
|
1744
|
+
'--remote', 'origin', '--base', base], scope.root).stdout);
|
|
1745
|
+
if (allocated?.status !== 'ALLOCATED') return { error: cleanupReason('delivery-failed', { detail: allocated?.reason || 'allocation failed' }) };
|
|
1746
|
+
const release = (error) => {
|
|
1747
|
+
cleanupGit(scope.root, ['worktree', 'remove', '--force', worktree]);
|
|
1748
|
+
cleanupGit(scope.root, ['branch', '-D', branch]);
|
|
1749
|
+
try { fs.rmSync(path.dirname(allocated.receipt), { recursive: true, force: true }); } catch {}
|
|
1750
|
+
return { error };
|
|
1751
|
+
};
|
|
1752
|
+
const fresh = cleanupProjectScope(worktree, current);
|
|
1753
|
+
const wanted = new Map(items.map((item) => [`${item.kind}\u0000${item.relative}`, item]));
|
|
1754
|
+
const applied = [];
|
|
1755
|
+
for (const item of fresh.items) {
|
|
1756
|
+
const key = `${item.kind}\u0000${item.relative}`;
|
|
1757
|
+
if (item.action === 'skip' || item.route !== 'delivery' || !wanted.has(key)) continue;
|
|
1758
|
+
if (cleanupApplyItem({ ...item, before: cleanupPreimage(item.path) }) === null) { applied.push(item.relative); wanted.delete(key); }
|
|
1759
|
+
}
|
|
1760
|
+
// A reviewed item the remote base does not hold as reviewed (not pushed, already gone) is not delivered.
|
|
1761
|
+
if (wanted.size) return release(cleanupReason('stale-plan', { detail: [...wanted.values()].map((item) => item.relative).join(', ') }));
|
|
1762
|
+
for (const dir of ['.agents/skills', '.claude/skills']) {
|
|
1763
|
+
try { const full = path.join(worktree, dir); if (fs.readdirSync(full).length === 0) fs.rmdirSync(full); } catch {}
|
|
1764
|
+
}
|
|
1765
|
+
const add = cleanupGit(worktree, ['add', '-A', '--', ...applied]);
|
|
1766
|
+
const commit = add.status === 0 && cleanupGit(worktree, ['-c', 'user.useConfigOnly=true', 'commit', '-q', '-m',
|
|
1767
|
+
`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. The managed principles block stays for teammates.`]);
|
|
1768
|
+
if (!commit || commit.status !== 0) return release(cleanupReason('delivery-failed', { detail: (commit || add).stderr.trim() }));
|
|
1769
|
+
const check = spawnSync(process.execPath, [path.join(__dirname, 'devlyn.js'), 'cleanup', '--plan', '--project', worktree, '--json'], { encoding: 'utf8' });
|
|
1770
|
+
const evidence = path.join(worktree, '.devlyn', 'cleanup-check.json');
|
|
1771
|
+
fs.mkdirSync(path.dirname(evidence), { recursive: true });
|
|
1772
|
+
fs.writeFileSync(evidence, check.stdout);
|
|
1773
|
+
let left = [];
|
|
1774
|
+
try { left = JSON.parse(check.stdout.trim().split('\n').at(-1)).data.scopes[0].items.filter((item) => item.action !== 'skip' && applied.includes(item.relative)); }
|
|
1775
|
+
catch { return release(cleanupReason('delivery-failed', { detail: 'the check after cleanup could not run' })); }
|
|
1776
|
+
if (left.length) return release(cleanupReason('delivery-failed', { detail: 'items remain after cleanup' }));
|
|
1777
|
+
const acceptance = path.join(worktree, '.devlyn', 'acceptance.json');
|
|
1778
|
+
fs.writeFileSync(acceptance, JSON.stringify({ kind: 'direct', task: `devlyn-cli ${PKG.version} legacy cleanup`,
|
|
1779
|
+
source_sha: cleanupGit(worktree, ['rev-parse', 'HEAD']).stdout.trim(),
|
|
1780
|
+
checks: [{ command: `devlyn cleanup --plan --project ${worktree} --json`, evidence: '.devlyn/cleanup-check.json' }] }));
|
|
1781
|
+
const completed = lastJson(run(['complete', '--receipt', allocated.receipt, '--acceptance', acceptance, '--writers-stopped'], scope.root).stdout);
|
|
1782
|
+
const status = completed?.delivery_status || completed?.status;
|
|
1783
|
+
if (status === 'COMPLETE' || status === 'MERGED') return { delivery: { state: 'merged', url: completed.pr || null } };
|
|
1784
|
+
if (completed?.pr) return { delivery: { state: 'pr-open', url: completed.pr } };
|
|
1785
|
+
const reason = completed?.reason || 'delivery did not complete';
|
|
1786
|
+
const auth = /\b(authentication|unauthorized|gh auth login|could not read Username|HTTP 40[13])\b/i.test(reason);
|
|
1787
|
+
return { error: cleanupReason(auth ? 'auth-failed' : 'delivery-failed', { detail: reason, ...(completed?.resume ? { resume: completed.resume } : {}) }) };
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function cleanupApply(plan, { events = false } = {}) {
|
|
1791
|
+
const current = cleanupCurrentFingerprints();
|
|
1792
|
+
const fresh = new Map(cleanupScopes(plan.inputs, current).map((scope) => [scope.id, scope]));
|
|
1793
|
+
let seq = 0;
|
|
1794
|
+
const emit = (scope) => { if (events) process.stdout.write(JSON.stringify({ schemaVersion: 1, event: 'scope', seq: ++seq, scope }) + '\n'); };
|
|
1795
|
+
const scopes = [];
|
|
1796
|
+
for (const reviewed of plan.scopes) {
|
|
1797
|
+
const pairs = cleanupProven(reviewed, fresh.get(reviewed.id) || { items: [] });
|
|
1798
|
+
const progress = { scopeId: reviewed.id, kind: reviewed.kind, name: reviewed.name, total: pairs.length, state: 'running', delivery: null, error: null };
|
|
1799
|
+
if (!pairs.length) { scopes.push({ ...progress, state: 'skipped' }); emit(scopes.at(-1)); continue; }
|
|
1800
|
+
emit(progress);
|
|
1801
|
+
try {
|
|
1802
|
+
const refused = pairs.find((pair) => !pair.fresh);
|
|
1803
|
+
const proven = pairs.filter((pair) => pair.fresh).map((pair) => pair.fresh);
|
|
1804
|
+
const local = cleanupLocal(fresh.get(reviewed.id), proven.filter((item) => item.route === 'local'));
|
|
1805
|
+
const failedLocal = [...local.values()].find((reason) => reason);
|
|
1806
|
+
const deliverable = proven.filter((item) => item.route === 'delivery');
|
|
1807
|
+
const delivery = deliverable.length ? cleanupDeliver(fresh.get(reviewed.id), deliverable, current) : null;
|
|
1808
|
+
progress.delivery = delivery?.delivery || null;
|
|
1809
|
+
const error = delivery?.error || failedLocal || (refused && cleanupReason('stale-plan'));
|
|
1810
|
+
Object.assign(progress, error ? { state: 'failed', error } : { state: 'done' });
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
Object.assign(progress, { state: 'failed', error: error instanceof InstructionError
|
|
1813
|
+
? cleanupReason('occupied-lock', { detail: error.message }) : cleanupReason('apply-failed', { detail: error.message }) });
|
|
1814
|
+
}
|
|
1815
|
+
scopes.push(progress);
|
|
1816
|
+
emit(progress);
|
|
1817
|
+
}
|
|
1818
|
+
const failed = scopes.filter((scope) => scope.state === 'failed').length;
|
|
1819
|
+
const done = scopes.filter((scope) => scope.state === 'done').length;
|
|
1820
|
+
return { state: failed === 0 ? 'succeeded' : done > 0 ? 'partial' : 'failed', scopes };
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function cleanupCommand(args) {
|
|
1824
|
+
const { options, fail } = require('./release');
|
|
1825
|
+
const opts = options(args, {
|
|
1826
|
+
plan: { type: 'boolean' }, apply: { type: 'boolean' }, machine: { type: 'boolean' }, events: { type: 'boolean' },
|
|
1827
|
+
project: { type: 'string', multiple: true }, 'account-home': { type: 'string', multiple: true },
|
|
1828
|
+
'plan-file': { type: 'string' }, digest: { type: 'string' },
|
|
1829
|
+
});
|
|
1830
|
+
if (opts.plan === opts.apply) fail('invalid-arguments', { argument: 'exactly one of --plan or --apply' }, 2);
|
|
1831
|
+
for (const dir of [...(opts.project || []), ...(opts['account-home'] || [])]) {
|
|
1832
|
+
if (!path.isAbsolute(dir)) fail('invalid-arguments', { argument: 'absolute paths required' }, 2);
|
|
1833
|
+
}
|
|
1834
|
+
// The installer's progress lines would corrupt this command's JSON on stdout.
|
|
1835
|
+
console.log = (...parts) => process.stderr.write(`${parts.join(' ')}\n`);
|
|
1836
|
+
if (opts.plan) {
|
|
1837
|
+
return cleanupPlan({ machine: Boolean(opts.machine), projects: opts.project || [], accountHomes: opts['account-home'] || [] });
|
|
1838
|
+
}
|
|
1839
|
+
if (!opts['plan-file'] || !opts.digest) fail('invalid-arguments', { argument: '--plan-file and --digest' }, 2);
|
|
1840
|
+
const plan = JSON.parse(fs.readFileSync(opts['plan-file'], 'utf8'));
|
|
1841
|
+
const inputs = plan?.inputs;
|
|
1842
|
+
if (plan?.schemaVersion !== 1 || plan.digest !== opts.digest || cleanupDigest(plan) !== plan.digest
|
|
1843
|
+
|| typeof inputs?.machine !== 'boolean' || !Array.isArray(inputs.projects) || !Array.isArray(inputs.accountHomes)
|
|
1844
|
+
|| ![...inputs.projects, ...inputs.accountHomes].every((dir) => typeof dir === 'string' && path.isAbsolute(dir))) {
|
|
1845
|
+
fail('stale-plan', {}, 3);
|
|
1846
|
+
}
|
|
1847
|
+
if (plan.version !== PKG.version) fail('stale-plan', { version: plan.version }, 3);
|
|
1848
|
+
return cleanupApply(plan, { events: Boolean(opts.events) });
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1344
1851
|
// Main
|
|
1345
1852
|
const args = process.argv.slice(2);
|
|
1346
1853
|
|
|
@@ -1350,6 +1857,9 @@ async function main() {
|
|
|
1350
1857
|
const command = args[0];
|
|
1351
1858
|
|
|
1352
1859
|
switch (command) {
|
|
1860
|
+
case 'cleanup':
|
|
1861
|
+
require('./release').runJson(() => cleanupCommand(args.slice(1)));
|
|
1862
|
+
break;
|
|
1353
1863
|
case 'activation':
|
|
1354
1864
|
require('./release').runJson(() => require('./harness-activation').activationCommand(args.slice(1)));
|
|
1355
1865
|
break;
|
package/bin/engines.js
CHANGED
|
@@ -4,29 +4,34 @@ const ENGINE_IDS = Object.freeze([
|
|
|
4
4
|
'claude', 'codex', 'grok', 'omp', 'antigravity', 'cursor', 'opencode', 'kimi', 'copilot', 'qwen',
|
|
5
5
|
]);
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// The versions each channel was examined on. Newer releases of the same major are taken as
|
|
8
|
+
// compatible: these CLIs ship minor releases weekly, and gating on the examined minor would turn
|
|
9
|
+
// the harness off at every one of them. A new major (or a Cursor build older than the examined one)
|
|
10
|
+
// is refused, since any flag may change there.
|
|
9
11
|
const ENGINE_CAPABILITIES = Object.freeze({
|
|
10
|
-
claude: { minimum: '2.1.307',
|
|
11
|
-
codex: { minimum: '0.163.0',
|
|
12
|
-
grok: { minimum: '1.0.62',
|
|
13
|
-
omp: { minimum: '16.5.0',
|
|
14
|
-
antigravity: { minimum: '1.4.4',
|
|
15
|
-
cursor: { minimum: '2025.09.12-4852336',
|
|
16
|
-
opencode: { minimum: '1.19.2',
|
|
17
|
-
kimi: { minimum: '2.3.0',
|
|
18
|
-
copilot: { minimum: '1.0.98',
|
|
19
|
-
qwen: { minimum: '0.26.0',
|
|
12
|
+
claude: { minimum: '2.1.307', executor: true },
|
|
13
|
+
codex: { minimum: '0.163.0', executor: true },
|
|
14
|
+
grok: { minimum: '1.0.62', executor: false },
|
|
15
|
+
omp: { minimum: '16.5.0', executor: false },
|
|
16
|
+
antigravity: { minimum: '1.4.4', executor: false },
|
|
17
|
+
cursor: { minimum: '2025.09.12-4852336', executor: false },
|
|
18
|
+
opencode: { minimum: '1.19.2', executor: false },
|
|
19
|
+
kimi: { minimum: '2.3.0', executor: false },
|
|
20
|
+
copilot: { minimum: '1.0.98', executor: false },
|
|
21
|
+
qwen: { minimum: '0.26.0', executor: false },
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
function supportedVersion(engine, version) {
|
|
23
25
|
const capability = ENGINE_CAPABILITIES[engine];
|
|
24
|
-
if (engine === 'cursor')
|
|
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
|
+
}
|
|
25
30
|
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) return false;
|
|
26
|
-
if (!version.startsWith(`${capability.generation}.`)) return false;
|
|
27
31
|
const actual = version.split('.').map(Number);
|
|
28
32
|
const floor = capability.minimum.split('.').map(Number);
|
|
29
|
-
|
|
33
|
+
if (actual[0] !== floor[0]) return false;
|
|
34
|
+
for (let i = 1; i < 3; i++) {
|
|
30
35
|
if (actual[i] !== floor[i]) return actual[i] > floor[i];
|
|
31
36
|
}
|
|
32
37
|
return true;
|
|
@@ -268,8 +268,7 @@ function planActivation(releaseRoot, input, env = process.env) {
|
|
|
268
268
|
if (off) return finish(plan);
|
|
269
269
|
plan.release = release;
|
|
270
270
|
if (!supported) {
|
|
271
|
-
const reasons = [reason('engine-version-unsupported', { minimum: ENGINE_CAPABILITIES[context.engine].minimum
|
|
272
|
-
generation: ENGINE_CAPABILITIES[context.engine].generation })];
|
|
271
|
+
const reasons = [reason('engine-version-unsupported', { minimum: ENGINE_CAPABILITIES[context.engine].minimum })];
|
|
273
272
|
status.principles = channel('unavailable', null, reasons);
|
|
274
273
|
status.skills = channel('unavailable', null, reasons);
|
|
275
274
|
return finish(plan);
|
package/bin/release.js
CHANGED
|
@@ -174,8 +174,8 @@ function metadata(root) {
|
|
|
174
174
|
claudePlugin: 'native/claude', codexMarketplace: 'native/codex' },
|
|
175
175
|
stateCompat: {
|
|
176
176
|
loopManifest: { read: [1, 1], write: 1 }, addRecord: { read: [1, 1], write: 1 },
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
taskPacket: { inspect: [1, 2], execute: [1, 2], write: 2 },
|
|
178
|
+
queueBinding: { read: [1, 1], write: 1 },
|
|
179
179
|
submission: { read: [1, 1], write: 1 }, review: { read: [1, 1], write: 1 },
|
|
180
180
|
acceptanceResult: { read: [1, 1], write: 1 }, invocationReceipt: { read: [1, 1], write: 1 },
|
|
181
181
|
legacyFormats: ['completion-receipt-unversioned', 'direct-acceptance-unversioned',
|
|
@@ -235,6 +235,19 @@ function buildRelease(root = PACKAGE_ROOT) {
|
|
|
235
235
|
digest: manifest.digest.value, fileCount: manifest.files.length };
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// npm runs prepack in the source checkout and leaves its outputs there; postpack removes them, so a
|
|
239
|
+
// checkout never carries a release.json that would make its own skills read as a release.
|
|
240
|
+
function cleanGenerated(root = PACKAGE_ROOT) {
|
|
241
|
+
root = physicalRoot(root);
|
|
242
|
+
for (const name of ['harness', 'native', 'release.json']) {
|
|
243
|
+
const file = path.join(root, name);
|
|
244
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
245
|
+
if (!stat) continue;
|
|
246
|
+
if (stat.isSymbolicLink() || !(stat.isDirectory() || stat.isFile())) fail('release-unsafe-entry', { path: file });
|
|
247
|
+
fs.rmSync(file, { recursive: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
238
251
|
function checkNpmShims(root, files) {
|
|
239
252
|
const dir = path.join(root, 'node_modules/.bin');
|
|
240
253
|
if (!fs.lstatSync(dir, { throwIfNoEntry: false })) return;
|
|
@@ -363,6 +376,6 @@ function runJson(action) {
|
|
|
363
376
|
}
|
|
364
377
|
}
|
|
365
378
|
|
|
366
|
-
module.exports = { buildRelease, verifyRelease, materializeRelease, checkGenerated,
|
|
379
|
+
module.exports = { buildRelease, cleanGenerated, verifyRelease, materializeRelease, checkGenerated,
|
|
367
380
|
sha256, manifestDigest, physicalRoot, HarnessError, fail,
|
|
368
381
|
options, releaseCommand, runJson };
|
package/bin/skill-history.json
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"3be2ec1243eb28f779b170e469dfb65555a92dc08b3d927b8973cbd49308125d",
|
|
44
44
|
"3c58989be2f943896810ef709fb115171420a918dfaadc43ea47940fa8ebc7ed",
|
|
45
45
|
"3c81c575cc43d70488865b440204cb399091405d9b7f81dbf047a32d3d5ac045",
|
|
46
|
+
"3cb043970432281f3c2c3c7ec813c4721221ca18ff227e94ef204f2ef2178044",
|
|
46
47
|
"3cd398df22e666fdd2e0f253c5a7464bca62bceefe3737d28a5ef95b75cb3a33",
|
|
47
48
|
"3e39fbe29d0bd607f4ad940f4ca40f9abf82d468e418d0362fb0fd4fa6812475",
|
|
48
49
|
"3e5135877e733f1dcff3b747444228f5eece7cb047956c87a3af02a485cfaad9",
|
|
@@ -357,6 +358,7 @@
|
|
|
357
358
|
"d6433bf0f96dd9dd485c3623885255738654674b3e23cfcc1a2cfd24f3cda96e",
|
|
358
359
|
"db8d921aabf0d04811b55465a3757c62c4b39631aebaf6eecc994ca241cfbe56",
|
|
359
360
|
"e25da746dc37b2e82c658a28d9481fd69741151045683eee2119582ddaf731cd",
|
|
361
|
+
"e47a2ce766ae81fa0870e0a8dfbfe737311de69ce620e92217abbe4c532e3c8b",
|
|
360
362
|
"e929f76c15349f1b9bc1e84e0a3e9e48e916168d79270e0f4a36ea8eab9b5388",
|
|
361
363
|
"f2243cc4150e225961825f6097b0cf38166c291dcf58127516ceee841f7ed4c8",
|
|
362
364
|
"fa536198d539a3c85f54dda74d0d78716ac6e6849e733bdadecc8605511eac37",
|