devlyn-cli 4.3.3 → 4.3.4

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "source": "scripts/update-cleanup-history.js: LF-normalized Git blobs from first-parent config/agents-config/optional-skills history and all tags; excludes optional-addon hashes and third-party web-design-guidelines",
2
+ "source": "scripts/update-cleanup-history.js: LF-normalized Git blobs from first-parent config/agents-config/optional-skills history and release tags; excludes optional-addon hashes and third-party web-design-guidelines; commands: devlyn.*.md from config/commands, .claude/commands, commands and optional-commands/* in the same commits via skill-ownership.contentHash",
3
3
  "files": {
4
4
  ".claude/agents/evaluator.md": [
5
5
  "146a229b0218938bca2de6b426cda642ae9826dc2dfa8c3687c6fd5de9160cf6",
@@ -41,6 +41,81 @@
41
41
  "d5615e73af2401ea7418e02fc76d850506e59a64fff00f084ad272e474e138ba"
42
42
  ]
43
43
  },
44
+ "commands": {
45
+ "devlyn.clean.md": [
46
+ "918c6ae867c57a4d9748237adae0df8c78a1447c7c028a0af988b59a5fbb0d9a"
47
+ ],
48
+ "devlyn.design-system.md": [
49
+ "f81166b5ba08d9ee1f5cff33c911ea387e5cb50141a9712a96fc4434c60a673f"
50
+ ],
51
+ "devlyn.design-ui.md": [
52
+ "0496a6b2dd8c6bb0474b71ef9a319a6b95bb76972d0f07a01c33f8860fe83dfb",
53
+ "4e2aafcc0de6ce7f3022a353aaa9fb4330e76a50a91ab970b9b9a0f192f9f1f2"
54
+ ],
55
+ "devlyn.discover-product.md": [
56
+ "1e66aa35bf300ec6167e1b888f43a92ebd9e60d501397ff0334c9a7809d433f5",
57
+ "a609401d29cd2bdad7a48c7c8f096b442fe03a17c6b2c3151936c47b4a16e6c6"
58
+ ],
59
+ "devlyn.evaluate.md": [
60
+ "8f17ccc639d54adab24c49dcf471376bab97511675d00554056a4c1f67a14115"
61
+ ],
62
+ "devlyn.feature-spec.md": [
63
+ "d442ed7ae8d14a255c37f1b120b758f69794be88e2970a8a0c0e51519344b6c7"
64
+ ],
65
+ "devlyn.handoff.md": [
66
+ "91d143c6305e95e4af6992eda0d127e94a394c2e91a2d409b4d41ca03c23f470"
67
+ ],
68
+ "devlyn.implement-ui.md": [
69
+ "a8d0813781b2f7a3df5a4751c92755bf7c84c53428cf63cea34c965939b8c478"
70
+ ],
71
+ "devlyn.pencil-pull.md": [
72
+ "dfdd3d19ca676558bfa2bb3ce398181b0a081ab88d1feaf69c54c706a9671631"
73
+ ],
74
+ "devlyn.pencil-push.md": [
75
+ "5f2e8b29609cbb2af72941579823bec598265ebf29cd3a375d6aa47d95b74354"
76
+ ],
77
+ "devlyn.product-spec.md": [
78
+ "7a92d2df2586b44e2c4d9182eeb3d61f7adc2a13bed4d2cd26abcfe086585895"
79
+ ],
80
+ "devlyn.recommend-features.md": [
81
+ "2b92523017e1da10c299dafb19d84736de5084b5b46693de7fd2d6df51dc56b4"
82
+ ],
83
+ "devlyn.resolve.md": [
84
+ "24595766e1a81a1fcacb78261b03c3665bc686bcac8b1805d1c4db379584879f",
85
+ "48c0c3ee553f87f1f0733190effc7220c01c82837a5300ef68c9ffdf30c5d3a1",
86
+ "eaad5354cfc20ed378ffd2a2e1ca6bade1310bd2a47ab93e5942e9599c3ba9a8"
87
+ ],
88
+ "devlyn.review.md": [
89
+ "22f532f587f6d2fbf2662e38d5073b85a030141573f86d92ad40b9d292836fb7",
90
+ "3a511fc678e599ff4b4c37fd9191399a599e1ba9ea9c40fc4afcbe29cb188d2b",
91
+ "55888e08bf31ead1d359f218277cfa7efc3c6cb0f96fadc665d5a2406ff115d3",
92
+ "7f9ce1db4ffd3cdaf9fd949e4b07de04e0931c91cd846d4ab220ff6114a45b17"
93
+ ],
94
+ "devlyn.team-design-ui.md": [
95
+ "02485911588e9f52afb5a5082fd7804ee7a41bad44323960a2afe1a16a49d628"
96
+ ],
97
+ "devlyn.team-resolve.md": [
98
+ "09fc163d34dfe8906dd8d22501a028bfbbad639499acab01e5e60f06fc520382",
99
+ "54c52c3f27998998ed55cc648d071bbb10444761b819c59ed118b795596a4aee",
100
+ "72f66be3c4c27a46e76d56d13a0d381880c054a03243188f98da2faf6dc77295",
101
+ "8f11fa0c3203c0dd5bf09b5243dcff5968a36275682c54b3a012262fc545859a",
102
+ "ccfc5083813ee50d407109616ccbc2509aa7526316cebbf7a271537abdfff3b7"
103
+ ],
104
+ "devlyn.team-review.md": [
105
+ "58b36cfe2749968d697d486a104a1b139e4312730b0ac2933550634f6ece48db",
106
+ "7e5c2d93a23d51af7939dae29c70095dbd8a4a681cc06d4d578f2de29df346a3",
107
+ "c8e145955a303141af4082dae174e3b3f1681e4c9698e25ae337eadd31fb883b"
108
+ ],
109
+ "devlyn.team-ui.md": [
110
+ "d0b10b922b0936d8826a1bbcf62c0f9ea4cc3ed79e6d7c6af0116e134de73aad"
111
+ ],
112
+ "devlyn.ui.md": [
113
+ "3cb63dcbe3c455c97eca8b30057aefba8959936fce79c3d854adeb3c07ec8959"
114
+ ],
115
+ "devlyn.update-docs.md": [
116
+ "86945afccf24762a88d26eef3703fbf6c5a90a3343fd3de72a9cb26b6f62d3b5"
117
+ ]
118
+ },
44
119
  "skills": {
45
120
  "code-health-standards": [
46
121
  "962a82b5ebeafac515f81f5c68494c67dbf31c3e3e453f23c4452d4725e21355",
package/bin/devlyn.js CHANGED
@@ -1347,7 +1347,7 @@ function showHelp() {
1347
1347
  // is read-only and lists each item with its proof. Apply plans again from what is on disk now and
1348
1348
  // acts only on items both plans name the same way, so a plan file can narrow a cleanup but never
1349
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.
1350
+ // Every removal needs shipped-content proof; names only identify candidates.
1351
1351
  // ---------------------------------------------------------------------------
1352
1352
 
1353
1353
  const CLEANUP_CORE = ['devlyn-ideate', 'devlyn-engines'];
@@ -1394,6 +1394,26 @@ function cleanupInUse(dir) {
1394
1394
  return cleanupProcesses === null || cleanupProcesses.some((line) => line.includes(dir + path.sep));
1395
1395
  }
1396
1396
 
1397
+ // Markers are generated, not bundled: require the exact writer format, a shipped version and
1398
+ // only fingerprints of shipped skills. Extra fields, arbitrary hashes and non-files stay untouched.
1399
+ function cleanupMarkerOwned(file, current) {
1400
+ if (!fs.lstatSync(file)?.isFile()) return false;
1401
+ const text = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
1402
+ let marker;
1403
+ try { marker = JSON.parse(text); } catch (error) { if (error instanceof SyntaxError) return false; throw error; }
1404
+ if (!marker || (marker.version !== PKG.version && !SKILL_HISTORY.versions.includes(marker.version))) return false;
1405
+ // 2.10.1 to 4.2.0 wrote no skills field.
1406
+ const [major, minor, patch] = marker.version.split('.').map(Number);
1407
+ if (major < 4 || (major === 4 && (minor < 2 || (minor === 2 && patch === 0)))) {
1408
+ return text === JSON.stringify({ schemaVersion: 1, package: PKG.name, version: marker.version }, null, 2) + '\n';
1409
+ }
1410
+ if (!marker.skills || typeof marker.skills !== 'object' || Array.isArray(marker.skills)) return false;
1411
+ const entries = Object.entries(marker.skills);
1412
+ if (!entries.length || entries.some(([name, hash]) => current[name] !== hash
1413
+ && !(Object.hasOwn(SKILL_HISTORY.skills, name) && SKILL_HISTORY.skills[name].includes(hash)))) return false;
1414
+ return text === JSON.stringify({ schemaVersion: 1, package: PKG.name, version: marker.version, skills: marker.skills }, null, 2) + '\n';
1415
+ }
1416
+
1397
1417
  // The legacy items of one skill root, each with its proof and the base its parents are checked from.
1398
1418
  // `_shared` and the install marker go only when every copy beside them goes too: a kept, edited
1399
1419
  // devlyn-ideate needs both.
@@ -1412,6 +1432,7 @@ function cleanupSkillRoot(base, root, current) {
1412
1432
  for (const spelling of legacySkillPaths(root, old)) renamed.set(path.basename(spelling).toLowerCase(), current);
1413
1433
  }
1414
1434
  const deprecated = new Set([...deprecatedSkillPaths(root)].map((file) => path.basename(file).toLowerCase()));
1435
+ const optional = new Set(OPTIONAL_ADDONS.map((addon) => addon.name.toLowerCase()));
1415
1436
  const support = [];
1416
1437
  let kept = 0;
1417
1438
  let marker = false;
@@ -1443,17 +1464,36 @@ function cleanupSkillRoot(base, root, current) {
1443
1464
  }
1444
1465
  if (renamed.has(lower)) {
1445
1466
  // 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: [] });
1467
+ if (CLEANUP_CORE.includes(renamed.get(lower))) {
1468
+ const proven = historicalSkill(file);
1469
+ items.push({ kind: 'renamed-skill', path: file, base, action: proven ? 'remove' : 'skip',
1470
+ reasons: proven ? [] : [cleanupReason('modified-copy')] });
1471
+ if (!proven) kept++;
1472
+ }
1447
1473
  else { items.push({ kind: 'renamed-skill', path: file, base, action: 'skip', reasons: [cleanupReason('optional-skill')] }); kept++; }
1448
1474
  continue;
1449
1475
  }
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: [] });
1476
+ if (deprecated.has(lower)) {
1477
+ const proven = historicalSkill(file);
1478
+ items.push({ kind: 'retired-skill', path: file, base, action: proven ? 'remove' : 'skip',
1479
+ reasons: proven ? [] : [cleanupReason('modified-copy')] });
1480
+ if (!proven) kept++;
1481
+ continue;
1482
+ }
1483
+ const proven = isShippedCopy(file, RETIRED_SKILL_MD_SHA256[lower]);
1484
+ // Under a current optional add-on's name, other bytes are that add-on, installed by choice.
1485
+ if (!proven && optional.has(lower)) continue;
1486
+ items.push({ kind: 'retired-standard', path: file, base, action: proven ? 'remove' : 'skip',
1487
+ reasons: proven ? [] : [cleanupReason('modified-copy')] });
1488
+ if (!proven) kept++;
1452
1489
  }
1453
1490
  if (kept === 0) {
1454
1491
  items.push(...support);
1455
1492
  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: [] });
1493
+ const file = path.join(root, DEVLYN_INSTALL_MARKER);
1494
+ const proven = cleanupMarkerOwned(file, current);
1495
+ items.push({ kind: 'install-marker', path: file, base, action: proven ? 'remove' : 'skip',
1496
+ reasons: proven ? [] : [cleanupReason('modified-copy')] });
1457
1497
  }
1458
1498
  }
1459
1499
  if (cleanupInUse(root)) {
@@ -1468,7 +1508,12 @@ function cleanupCommands(base) {
1468
1508
  const names = new Set(DEPRECATED_FILES.map((file) => path.basename(file).toLowerCase()));
1469
1509
  return fs.readdirSync(commands).sort().filter((name) => names.has(name.toLowerCase())
1470
1510
  && fs.lstatSync(path.join(commands, name)).isFile())
1471
- .map((name) => ({ kind: 'legacy-command', path: path.join(commands, name), base, action: 'remove', reasons: [] }));
1511
+ .map((name) => {
1512
+ const file = path.join(commands, name);
1513
+ const proven = CLEANUP_HISTORY.commands[name.toLowerCase()]?.includes(contentHash(file));
1514
+ return { kind: 'legacy-command', path: file, base, action: proven ? 'remove' : 'skip',
1515
+ reasons: proven ? [] : [cleanupReason('modified-copy')] };
1516
+ });
1472
1517
  }
1473
1518
 
1474
1519
  function cleanupProjectFiles(base) {
@@ -1536,17 +1581,25 @@ function cleanupPython() {
1536
1581
 
1537
1582
  // The GitHub repository and base branch a project's tracked changes are delivered to, or null.
1538
1583
  function cleanupRemote(root) {
1539
- const url = cleanupGit(root, ['remote', 'get-url', 'origin']);
1584
+ const { fail } = require('./release');
1585
+ const query = (args, absent) => {
1586
+ const result = cleanupGit(root, args);
1587
+ if (result.error || (result.status !== 0 && result.status !== absent)) {
1588
+ fail('repository-unreadable', { detail: result.error?.message || result.stderr.trim() });
1589
+ }
1590
+ return result;
1591
+ };
1592
+ const url = query(['remote', 'get-url', 'origin'], 2);
1540
1593
  if (url.status !== 0) return null;
1541
1594
  const match = url.stdout.trim().match(/github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
1542
1595
  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.
1596
+ const head = query(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], 1);
1597
+ // With no default ref, the reviewed base is an existing main/master tracking branch or main.
1598
+ // Delivery uses exactly this choice; completion refuses a base that is not the remote default.
1546
1599
  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)
1600
+ : ['main', 'master'].find((name) => query(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${name}`], 1).status === 0)
1548
1601
  || 'main';
1549
- return { repository: `${match[1]}/${match[2]}`, base, named: head.status === 0 };
1602
+ return { repository: `${match[1]}/${match[2]}`, base };
1550
1603
  }
1551
1604
 
1552
1605
  // Whether a task is in flight on the installed copies: an allocated task's packet runs their exact
@@ -1569,6 +1622,31 @@ function cleanupSameFile(a, b) {
1569
1622
  return Boolean(x && y && x.dev === y.dev && x.ino === y.ino);
1570
1623
  }
1571
1624
 
1625
+ // Ownership is unknown if Git cannot read the work tree or index. Only an explicit non-repository
1626
+ // result or a successful empty index query permits local deletion, including machine/dotfiles roots.
1627
+ function cleanupTracked(file) {
1628
+ const root = path.dirname(file);
1629
+ const repo = cleanupGit(root, ['rev-parse', '--show-toplevel']);
1630
+ if (!repo.error && repo.status === 128 && repo.stderr.includes('not a git repository')) return false;
1631
+ if (repo.error || repo.status !== 0) throw new Error(repo.error?.message || repo.stderr.trim());
1632
+ const files = cleanupGit(root, ['ls-files', '-z', '--', `:(icase,literal)${file}`]);
1633
+ if (files.error || files.status !== 0) throw new Error(files.error?.message || files.stderr.trim());
1634
+ return files.stdout.length > 0;
1635
+ }
1636
+
1637
+ // A tracked copy remains in the caller's checkout even after PR delivery. Decide retention across
1638
+ // the whole root, before splitting routes; support in the delivery tree is considered separately.
1639
+ function cleanupRetainSupport(items) {
1640
+ const remaining = new Set(items.filter((item) => !cleanupSupport(item)
1641
+ && (item.action === 'skip' || item.route === 'delivery')).map((item) => path.dirname(item.path)));
1642
+ for (const item of items) {
1643
+ if (cleanupSupport(item) && item.route === 'local' && item.action !== 'skip' && remaining.has(path.dirname(item.path))) {
1644
+ Object.assign(item, { action: 'skip', reasons: [cleanupReason('dependent-copy')] });
1645
+ }
1646
+ }
1647
+ return items;
1648
+ }
1649
+
1572
1650
  function cleanupProjectScope(project, current) {
1573
1651
  const { repositoryIdentity } = require('./harness-status');
1574
1652
  const { physicalRoot } = require('./release');
@@ -1588,7 +1666,9 @@ function cleanupProjectScope(project, current) {
1588
1666
  for (const item of items) {
1589
1667
  if (busy && item.action !== 'skip') Object.assign(item, { action: 'skip', reasons: [busy] });
1590
1668
  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);
1669
+ let tracked = false;
1670
+ try { tracked = cleanupTracked(item.path); }
1671
+ catch (error) { Object.assign(item, { action: 'skip', reasons: [cleanupReason('repository-unreadable', { detail: error.message })] }); }
1592
1672
  scope.items.push({ ...item, relative, route: tracked ? 'delivery' : 'local' });
1593
1673
  }
1594
1674
  if (!scope.remote) {
@@ -1596,6 +1676,7 @@ function cleanupProjectScope(project, current) {
1596
1676
  if (item.route === 'delivery' && item.action !== 'skip') Object.assign(item, { action: 'skip', reasons: [cleanupReason('no-remote')] });
1597
1677
  }
1598
1678
  }
1679
+ cleanupRetainSupport(scope.items);
1599
1680
  return scope;
1600
1681
  }
1601
1682
 
@@ -1610,7 +1691,15 @@ function cleanupMachineScope(home, accountHomes, current) {
1610
1691
  items.push(...cleanupSkillRoot(base, root, current));
1611
1692
  }
1612
1693
  items.push(...cleanupCommands(home));
1613
- return { kind: 'machine', root: home, name: null, remote: null, items: items.map((item) => ({ ...item, relative: null, route: 'local' })) };
1694
+ for (const item of items) {
1695
+ try {
1696
+ if (cleanupTracked(item.path)) Object.assign(item, { action: 'skip', reasons: [cleanupReason('tracked-copy')] });
1697
+ } catch (error) {
1698
+ Object.assign(item, { action: 'skip', reasons: [cleanupReason('repository-unreadable', { detail: error.message })] });
1699
+ }
1700
+ Object.assign(item, { relative: null, route: 'local' });
1701
+ }
1702
+ return { kind: 'machine', root: home, name: null, remote: null, items: cleanupRetainSupport(items) };
1614
1703
  }
1615
1704
 
1616
1705
  function cleanupScopes(inputs, current) {
@@ -1622,7 +1711,7 @@ function cleanupScopes(inputs, current) {
1622
1711
  try { scope = cleanupProjectScope(project, current); }
1623
1712
  catch (error) {
1624
1713
  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 })] }] };
1714
+ relative: '', route: 'local', action: 'skip', reasons: [cleanupReason(error.reason?.code === 'repository-unreadable' ? 'repository-unreadable' : 'project-unreadable', { detail: error.message })] }] };
1626
1715
  }
1627
1716
  scopes.push({ id: `project:${sha256(Buffer.from(project)).slice(0, 12)}`, ...scope });
1628
1717
  }
@@ -1637,14 +1726,18 @@ function cleanupPlan(inputs) {
1637
1726
  return { ...plan, digest: cleanupDigest(plan) };
1638
1727
  }
1639
1728
 
1640
- function cleanupDigest(plan) {
1641
- const { sha256 } = require('./release');
1642
- const canonical = (value) => Array.isArray(value) ? `[${value.map(canonical).join(',')}]`
1729
+ // JSON text with sorted keys: a plan the app stored with its own key order still reads the same.
1730
+ function cleanupCanonical(value) {
1731
+ return Array.isArray(value) ? `[${value.map(cleanupCanonical).join(',')}]`
1643
1732
  : value && typeof value === 'object'
1644
- ? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`
1733
+ ? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${cleanupCanonical(value[key])}`).join(',')}}`
1645
1734
  : JSON.stringify(value);
1735
+ }
1736
+
1737
+ function cleanupDigest(plan) {
1738
+ const { sha256 } = require('./release');
1646
1739
  const { digest, ...body } = plan;
1647
- return sha256(Buffer.from(canonical(body)));
1740
+ return sha256(Buffer.from(cleanupCanonical(body)));
1648
1741
  }
1649
1742
 
1650
1743
  // Apply one item a fresh plan proves right now; returns a reason when it is not what was proven.
@@ -1695,6 +1788,11 @@ function cleanupLocal(scope, items) {
1695
1788
  const roots = [...new Set(items.map((item) => path.dirname(item.path)).filter((dir) => path.basename(dir) === 'skills'
1696
1789
  && fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory()))];
1697
1790
  const act = (item) => {
1791
+ try {
1792
+ if (cleanupTracked(item.path)) { results.set(item.id, cleanupReason('tracked-copy')); return; }
1793
+ } catch (error) {
1794
+ results.set(item.id, cleanupReason('repository-unreadable', { detail: error.message })); return;
1795
+ }
1698
1796
  try { results.set(item.id, cleanupApplyItem(item)); }
1699
1797
  catch (error) { results.set(item.id, cleanupReason('apply-failed', { detail: error.message })); }
1700
1798
  };
@@ -1729,36 +1827,54 @@ function cleanupDeliver(scope, items, current) {
1729
1827
  if (!python) return { error: cleanupReason('requirement-missing', { name: 'Python 3.11' }) };
1730
1828
  const run = (args, cwd) => spawnSync(python, ['-B', helper, ...args], { cwd, encoding: 'utf8', env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
1731
1829
  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
1830
  const short = crypto.randomBytes(4).toString('hex');
1740
1831
  const branch = `devlyn/cleanup-${short}`;
1741
1832
  const worktree = path.join(path.dirname(scope.root), `${path.basename(scope.root)}.devlyn.noindex`, `devlyn-cleanup-${short}`);
1742
1833
  const allocated = lastJson(run(['allocate', '--repo', scope.root, '--task', `devlyn-cli ${PKG.version} legacy cleanup`,
1743
1834
  '--branch', branch, '--worktree', worktree, '--repository', scope.remote.repository,
1744
- '--remote', 'origin', '--base', base], scope.root).stdout);
1835
+ '--remote', 'origin', '--base', scope.remote.base], scope.root).stdout);
1745
1836
  if (allocated?.status !== 'ALLOCATED') return { error: cleanupReason('delivery-failed', { detail: allocated?.reason || 'allocation failed' }) };
1746
1837
  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 {}
1838
+ for (const args of [['worktree', 'remove', '--force', worktree], ['branch', '-D', branch]]) {
1839
+ const result = cleanupGit(scope.root, args);
1840
+ if (result.error || result.status !== 0) return { error: cleanupReason(error.code, { ...error.params,
1841
+ releaseError: result.error?.message || result.stderr.trim(), worktree, receipt: allocated.receipt }) };
1842
+ }
1843
+ fs.rmSync(path.dirname(allocated.receipt), { recursive: true, force: true });
1750
1844
  return { error };
1751
1845
  };
1846
+ let prepared;
1847
+ try { prepared = cleanupDeliveryCommit(worktree, items, current, release); }
1848
+ catch (error) { return release(cleanupReason('apply-failed', { detail: error.message })); }
1849
+ if (prepared.error) return prepared;
1850
+ const completed = lastJson(run(['complete', '--receipt', allocated.receipt, '--acceptance', prepared.acceptance, '--writers-stopped'], scope.root).stdout);
1851
+ const status = completed?.delivery_status || completed?.status;
1852
+ if (status === 'COMPLETE' || status === 'MERGED') return { delivery: { state: 'merged', url: completed.pr || null } };
1853
+ if (completed?.pr) return { delivery: { state: 'pr-open', url: completed.pr } };
1854
+ const reason = completed?.reason || 'delivery did not complete';
1855
+ const auth = /\b(authentication|unauthorized|gh auth login|could not read Username|HTTP 40[13])\b/i.test(reason);
1856
+ return { error: cleanupReason(auth ? 'auth-failed' : 'delivery-failed', { detail: reason, ...(completed?.resume ? { resume: completed.resume } : {}) }) };
1857
+ }
1858
+
1859
+ // Inside the allocated worktree: prove every reviewed item, remove them, commit, and write the
1860
+ // acceptance completion binds. A failure releases the worktree; so does a throw, in the caller.
1861
+ function cleanupDeliveryCommit(worktree, items, current, release) {
1752
1862
  const fresh = cleanupProjectScope(worktree, current);
1753
- const wanted = new Map(items.map((item) => [`${item.kind}\u0000${item.relative}`, item]));
1863
+ const available = new Map(fresh.items.map((item) => [`${item.kind}\u0000${item.relative}`, item]));
1864
+ const pairs = items.map((reviewed) => {
1865
+ const fresh = available.get(`${reviewed.kind}\u0000${reviewed.relative}`);
1866
+ return { reviewed, fresh, before: fresh && cleanupPreimage(fresh.path) };
1867
+ });
1868
+ const stale = pairs.filter(({ reviewed, fresh, before }) => !fresh || fresh.action !== reviewed.action || fresh.route !== 'delivery'
1869
+ || before.type !== reviewed.before.type || before.sha256 !== reviewed.before.sha256);
1870
+ // Check the entire batch before mutating even one item; the remote base cannot replace the reviewed preimage.
1871
+ if (stale.length) return release(cleanupReason('stale-plan', { detail: stale.map(({ reviewed }) => reviewed.relative).join(', ') }));
1754
1872
  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); }
1873
+ for (const { reviewed, fresh } of pairs) {
1874
+ const error = cleanupApplyItem({ ...fresh, before: reviewed.before });
1875
+ if (error) return release(cleanupReason(error.code, { ...error.params, detail: reviewed.relative }));
1876
+ applied.push(fresh.relative);
1759
1877
  }
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
1878
  for (const dir of ['.agents/skills', '.claude/skills']) {
1763
1879
  try { const full = path.join(worktree, dir); if (fs.readdirSync(full).length === 0) fs.rmdirSync(full); } catch {}
1764
1880
  }
@@ -1774,17 +1890,13 @@ function cleanupDeliver(scope, items, current) {
1774
1890
  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
1891
  catch { return release(cleanupReason('delivery-failed', { detail: 'the check after cleanup could not run' })); }
1776
1892
  if (left.length) return release(cleanupReason('delivery-failed', { detail: 'items remain after cleanup' }));
1893
+ const head = cleanupGit(worktree, ['rev-parse', 'HEAD']);
1894
+ if (head.error || head.status !== 0) return release(cleanupReason('repository-unreadable', { detail: head.error?.message || head.stderr.trim() }));
1777
1895
  const acceptance = path.join(worktree, '.devlyn', 'acceptance.json');
1778
1896
  fs.writeFileSync(acceptance, JSON.stringify({ kind: 'direct', task: `devlyn-cli ${PKG.version} legacy cleanup`,
1779
- source_sha: cleanupGit(worktree, ['rev-parse', 'HEAD']).stdout.trim(),
1897
+ source_sha: head.stdout.trim(),
1780
1898
  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 } : {}) }) };
1899
+ return { acceptance };
1788
1900
  }
1789
1901
 
1790
1902
  function cleanupApply(plan, { events = false } = {}) {
@@ -1799,11 +1911,23 @@ function cleanupApply(plan, { events = false } = {}) {
1799
1911
  if (!pairs.length) { scopes.push({ ...progress, state: 'skipped' }); emit(scopes.at(-1)); continue; }
1800
1912
  emit(progress);
1801
1913
  try {
1914
+ const observed = fresh.get(reviewed.id);
1915
+ if (reviewed.kind !== observed?.kind
1916
+ || (observed.kind === 'project' && cleanupCanonical(reviewed.remote) !== cleanupCanonical(observed.remote))) {
1917
+ Object.assign(progress, { state: 'failed', error: cleanupReason('stale-plan', { detail: 'delivery identity changed' }) });
1918
+ scopes.push(progress); emit(progress); continue;
1919
+ }
1802
1920
  const refused = pairs.find((pair) => !pair.fresh);
1803
- const proven = pairs.filter((pair) => pair.fresh).map((pair) => pair.fresh);
1921
+ // A reviewed copy refused now stays on disk, so the support beside it stays too.
1922
+ const held = new Set(pairs.filter((pair) => !pair.fresh && !cleanupSupport(pair.reviewed))
1923
+ .map((pair) => path.dirname(pair.reviewed.path)));
1924
+ const proven = pairs.filter((pair) => pair.fresh && !(cleanupSupport(pair.fresh) && held.has(path.dirname(pair.fresh.path))))
1925
+ .map((pair) => pair.fresh);
1804
1926
  const local = cleanupLocal(fresh.get(reviewed.id), proven.filter((item) => item.route === 'local'));
1805
1927
  const failedLocal = [...local.values()].find((reason) => reason);
1806
- const deliverable = proven.filter((item) => item.route === 'delivery');
1928
+ // A local copy that could not go keeps the tracked support beside it, as cleanupLocal keeps local support.
1929
+ const keptRoots = new Set(proven.filter((item) => item.route === 'local' && local.get(item.id)).map((item) => path.dirname(item.path)));
1930
+ const deliverable = proven.filter((item) => item.route === 'delivery' && !(cleanupSupport(item) && keptRoots.has(path.dirname(item.path))));
1807
1931
  const delivery = deliverable.length ? cleanupDeliver(fresh.get(reviewed.id), deliverable, current) : null;
1808
1932
  progress.delivery = delivery?.delivery || null;
1809
1933
  const error = delivery?.error || failedLocal || (refused && cleanupReason('stale-plan'));
@@ -1,5 +1,5 @@
1
1
  {
2
- "source": "skills: config/skills/* and optional-skills/* trees from every first-parent HEAD commit touching those paths and every local tag, derived by scripts/update-skill-history.js using bin/skill-ownership.js normalization (LF, install-path stamps, OS metadata and Python bytecode)",
2
+ "source": "skills: config/skills/* and optional-skills/* trees from every first-parent HEAD commit touching those paths and every release tag, derived by scripts/update-skill-history.js using bin/skill-ownership.js normalization (LF, install-path stamps, OS metadata and Python bytecode)",
3
3
  "skills": {
4
4
  "_shared": [
5
5
  "015eea12cc07cfb9eb318c30855f6deacb5bf43bd508a511dffd2e48c9d82964",
@@ -89,6 +89,7 @@
89
89
  "7403aa2911ac89180c4abe2c15e4037a8023af134e2fe70ed35ae1b14c92a04c",
90
90
  "74ad540635331051e8a32b6d7aa08c1926b9433298d1a1f699efe294b0c96d53",
91
91
  "74d650102836e72f0ab6cec05760ced6e2e047c62c1fe403837879e91d2e1cc4",
92
+ "75578439ec7edad038ff429071b7625f1bc1fc81cf6a715b3d4f2c05afa0e07e",
92
93
  "75cdbe50f909866a69cd7f7fa6ed104f471a6386e19fcf82e695c001edeafe13",
93
94
  "7a0f6c692bd1f74759d44f1a145899c157e1787635db546ea8a192461421e353",
94
95
  "7ca704d818bf9c044fed59fdd4107bd0463f9b0514d44729003e3e52aa796353",
@@ -635,5 +636,132 @@
635
636
  "711cd2633df2062104f12d50037a07a1012a90ab18c422cb5fdf665fcb9dc987",
636
637
  "d6a98c105a0d1952de90c39ca0734cb70cbdd116e6b4a4d45507d796bd2a9e88"
637
638
  ]
638
- }
639
+ },
640
+ "versions": [
641
+ "0.2.1",
642
+ "0.3.0",
643
+ "0.4.0",
644
+ "0.5.0",
645
+ "0.5.1",
646
+ "0.5.2",
647
+ "0.5.3",
648
+ "0.5.4",
649
+ "0.5.5",
650
+ "0.5.7",
651
+ "0.5.8",
652
+ "0.6.0",
653
+ "0.7.0",
654
+ "0.7.1",
655
+ "0.7.2",
656
+ "1.0.0",
657
+ "1.0.1",
658
+ "1.1.0",
659
+ "1.1.1",
660
+ "1.1.2",
661
+ "1.10.0",
662
+ "1.11.0",
663
+ "1.12.0",
664
+ "1.12.1",
665
+ "1.12.2",
666
+ "1.12.3",
667
+ "1.12.4",
668
+ "1.12.5",
669
+ "1.13.0",
670
+ "1.14.0",
671
+ "1.15.0",
672
+ "1.2.0",
673
+ "1.2.1",
674
+ "1.3.0",
675
+ "1.3.1",
676
+ "1.3.2",
677
+ "1.4.0",
678
+ "1.5.0",
679
+ "1.5.1",
680
+ "1.5.2",
681
+ "1.5.3",
682
+ "1.5.4",
683
+ "1.6.0",
684
+ "1.7.0",
685
+ "1.7.1",
686
+ "1.7.2",
687
+ "1.7.3",
688
+ "1.8.0",
689
+ "1.8.1",
690
+ "1.9.0",
691
+ "1.9.1",
692
+ "2.0.0",
693
+ "2.1.0",
694
+ "2.10.0",
695
+ "2.10.1",
696
+ "2.2.0",
697
+ "2.2.1",
698
+ "2.2.2",
699
+ "2.3.0",
700
+ "2.3.1",
701
+ "2.3.2",
702
+ "2.4.0",
703
+ "2.4.1",
704
+ "2.5.0",
705
+ "2.5.1",
706
+ "2.5.2",
707
+ "2.5.3",
708
+ "2.5.4",
709
+ "2.5.5",
710
+ "2.5.6",
711
+ "2.5.7",
712
+ "2.6.0",
713
+ "2.6.1",
714
+ "2.7.0",
715
+ "2.8.0",
716
+ "2.8.1",
717
+ "2.9.0",
718
+ "2.9.1",
719
+ "2.9.2",
720
+ "2.9.3",
721
+ "3.0.0",
722
+ "3.0.1",
723
+ "3.1.0",
724
+ "3.1.1",
725
+ "3.1.2",
726
+ "3.1.3",
727
+ "3.2.0",
728
+ "3.2.1",
729
+ "3.3.0",
730
+ "3.3.1",
731
+ "4.0.0",
732
+ "4.0.1",
733
+ "4.1.0",
734
+ "4.2.0",
735
+ "4.2.1",
736
+ "4.2.10",
737
+ "4.2.11",
738
+ "4.2.12",
739
+ "4.2.13",
740
+ "4.2.14",
741
+ "4.2.15",
742
+ "4.2.16",
743
+ "4.2.17",
744
+ "4.2.18",
745
+ "4.2.19",
746
+ "4.2.2",
747
+ "4.2.20",
748
+ "4.2.21",
749
+ "4.2.22",
750
+ "4.2.23",
751
+ "4.2.24",
752
+ "4.2.25",
753
+ "4.2.26",
754
+ "4.2.3",
755
+ "4.2.4",
756
+ "4.2.5",
757
+ "4.2.6",
758
+ "4.2.7",
759
+ "4.2.8",
760
+ "4.2.9",
761
+ "4.3.0",
762
+ "4.3.1",
763
+ "4.3.2",
764
+ "4.3.3",
765
+ "4.3.4"
766
+ ]
639
767
  }
@@ -12,6 +12,7 @@ import json
12
12
  import os
13
13
  from pathlib import Path
14
14
  import re
15
+ import runpy
15
16
  import subprocess
16
17
  import sys
17
18
 
@@ -54,7 +55,11 @@ def verify_release(root):
54
55
  root = Path(root)
55
56
  # As harness-status verifies it: an npm-installed package keeps npm's own .bin shims, which are checked.
56
57
  script = "const r=require(process.argv[1]);r.runJson(()=>r.verifyRelease(process.argv[2],{npmInstall:true}));"
57
- result = subprocess.run(["node", "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
58
+ try:
59
+ node = runpy.run_path(str(Path(__file__).with_name("platform-support.py")))["managed_node"]() or "node"
60
+ result = subprocess.run([node, "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
61
+ except (OSError, ValueError) as exc:
62
+ raise HarnessError(f"release verifier could not start: {exc}") from exc
58
63
  require(result.returncode == 0, f"release verification failed: {root}: {result.stderr.strip()}")
59
64
  try:
60
65
  release = release_ref(json.loads(result.stdout)["data"]["release"])
@@ -15,6 +15,18 @@ import sys
15
15
  import time
16
16
 
17
17
 
18
+ def managed_node():
19
+ """The Node that runs devlyn-cli's own scripts: the app's override is authoritative, and an invalid one never
20
+ falls back to PATH. Project and engine commands keep the session's own Node."""
21
+ value = os.environ.get("DEVLYN_NODE")
22
+ if value is None:
23
+ return None
24
+ node = Path(value)
25
+ if not node.is_absolute() or not node.is_file() or not os.access(node, os.X_OK):
26
+ raise OSError("DEVLYN_NODE must be an absolute path to an executable file")
27
+ return value
28
+
29
+
18
30
  def configure_utf8():
19
31
  for stream in (sys.stdin, sys.stdout, sys.stderr):
20
32
  if hasattr(stream, "reconfigure"):
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn",
3
- "version": "4.3.3",
3
+ "version": "4.3.4",
4
4
  "description": "Devlyn harness skills",
5
5
  "skills": "./skills/"
6
6
  }
@@ -12,6 +12,7 @@ import json
12
12
  import os
13
13
  from pathlib import Path
14
14
  import re
15
+ import runpy
15
16
  import subprocess
16
17
  import sys
17
18
 
@@ -54,7 +55,11 @@ def verify_release(root):
54
55
  root = Path(root)
55
56
  # As harness-status verifies it: an npm-installed package keeps npm's own .bin shims, which are checked.
56
57
  script = "const r=require(process.argv[1]);r.runJson(()=>r.verifyRelease(process.argv[2],{npmInstall:true}));"
57
- result = subprocess.run(["node", "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
58
+ try:
59
+ node = runpy.run_path(str(Path(__file__).with_name("platform-support.py")))["managed_node"]() or "node"
60
+ result = subprocess.run([node, "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
61
+ except (OSError, ValueError) as exc:
62
+ raise HarnessError(f"release verifier could not start: {exc}") from exc
58
63
  require(result.returncode == 0, f"release verification failed: {root}: {result.stderr.strip()}")
59
64
  try:
60
65
  release = release_ref(json.loads(result.stdout)["data"]["release"])
@@ -15,6 +15,18 @@ import sys
15
15
  import time
16
16
 
17
17
 
18
+ def managed_node():
19
+ """The Node that runs devlyn-cli's own scripts: the app's override is authoritative, and an invalid one never
20
+ falls back to PATH. Project and engine commands keep the session's own Node."""
21
+ value = os.environ.get("DEVLYN_NODE")
22
+ if value is None:
23
+ return None
24
+ node = Path(value)
25
+ if not node.is_absolute() or not node.is_file() or not os.access(node, os.X_OK):
26
+ raise OSError("DEVLYN_NODE must be an absolute path to an executable file")
27
+ return value
28
+
29
+
18
30
  def configure_utf8():
19
31
  for stream in (sys.stdin, sys.stdout, sys.stderr):
20
32
  if hasattr(stream, "reconfigure"):
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "devlyn-harness-v4-3-3",
2
+ "name": "devlyn-harness-v4-3-4",
3
3
  "plugins": [
4
4
  {
5
5
  "name": "devlyn",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn",
3
- "version": "4.3.3",
3
+ "version": "4.3.4",
4
4
  "description": "Devlyn harness skills",
5
5
  "skills": "./skills/"
6
6
  }
@@ -12,6 +12,7 @@ import json
12
12
  import os
13
13
  from pathlib import Path
14
14
  import re
15
+ import runpy
15
16
  import subprocess
16
17
  import sys
17
18
 
@@ -54,7 +55,11 @@ def verify_release(root):
54
55
  root = Path(root)
55
56
  # As harness-status verifies it: an npm-installed package keeps npm's own .bin shims, which are checked.
56
57
  script = "const r=require(process.argv[1]);r.runJson(()=>r.verifyRelease(process.argv[2],{npmInstall:true}));"
57
- result = subprocess.run(["node", "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
58
+ try:
59
+ node = runpy.run_path(str(Path(__file__).with_name("platform-support.py")))["managed_node"]() or "node"
60
+ result = subprocess.run([node, "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
61
+ except (OSError, ValueError) as exc:
62
+ raise HarnessError(f"release verifier could not start: {exc}") from exc
58
63
  require(result.returncode == 0, f"release verification failed: {root}: {result.stderr.strip()}")
59
64
  try:
60
65
  release = release_ref(json.loads(result.stdout)["data"]["release"])
@@ -15,6 +15,18 @@ import sys
15
15
  import time
16
16
 
17
17
 
18
+ def managed_node():
19
+ """The Node that runs devlyn-cli's own scripts: the app's override is authoritative, and an invalid one never
20
+ falls back to PATH. Project and engine commands keep the session's own Node."""
21
+ value = os.environ.get("DEVLYN_NODE")
22
+ if value is None:
23
+ return None
24
+ node = Path(value)
25
+ if not node.is_absolute() or not node.is_file() or not os.access(node, os.X_OK):
26
+ raise OSError("DEVLYN_NODE must be an absolute path to an executable file")
27
+ return value
28
+
29
+
18
30
  def configure_utf8():
19
31
  for stream in (sys.stdin, sys.stdout, sys.stderr):
20
32
  if hasattr(stream, "reconfigure"):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "4.3.3",
3
+ "version": "4.3.4",
4
4
  "description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — core engineering principles for CLAUDE.md/AGENTS.md and a loop designer that splits one intent into tasks agents drain hands-free",
5
5
  "homepage": "https://github.com/fysoul17/devlyn-cli#readme",
6
6
  "bin": {
package/release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "devlyn-cli",
4
- "version": "4.3.3",
4
+ "version": "4.3.4",
5
5
  "principles": {
6
6
  "defaults": "config/skills/_shared/runtime-principles.md",
7
7
  "memory": {
@@ -119,7 +119,7 @@
119
119
  },
120
120
  {
121
121
  "path": "bin/cleanup-history.json",
122
- "sha256": "e2d6319a8a22be94188a8c004edeb596052da3b1fd441565b273416c82d49e2a",
122
+ "sha256": "d239acfc06c30a2c2de2210d79ebff630829d2d04f133267c7d91e1f8edd7b72",
123
123
  "executable": false
124
124
  },
125
125
  {
@@ -129,7 +129,7 @@
129
129
  },
130
130
  {
131
131
  "path": "bin/devlyn.js",
132
- "sha256": "1b20470ea02060b2dae52192199f1ce0694cb1ed26dd45494485f6b75a9a1e1a",
132
+ "sha256": "8ba08fe52c9529dee1bf13498ab3237fc25830f7f7305337fd832f32d158be7e",
133
133
  "executable": true
134
134
  },
135
135
  {
@@ -164,7 +164,7 @@
164
164
  },
165
165
  {
166
166
  "path": "bin/skill-history.json",
167
- "sha256": "17f3fc938be662c8a27e123df12390054cecee65e93de5c7157f772acf27adae",
167
+ "sha256": "714337eb0686097292fb464a053a7645bc1e44b15fa7c34077fd714d0ad4739e",
168
168
  "executable": false
169
169
  },
170
170
  {
@@ -219,7 +219,7 @@
219
219
  },
220
220
  {
221
221
  "path": "config/skills/_shared/harness-store.py",
222
- "sha256": "9b8b9a8fba6d90df7cd02e3d7664520fb4536eee78bc26229ba2614ed618c894",
222
+ "sha256": "04505c53b0aa3006c700382cb444492d5a4f7c7c0780124dd229caa78eaaacfb",
223
223
  "executable": false
224
224
  },
225
225
  {
@@ -229,7 +229,7 @@
229
229
  },
230
230
  {
231
231
  "path": "config/skills/_shared/platform-support.py",
232
- "sha256": "0e1a86dc02871744abec5aa0d0789e62f2aba4d6e8ee79b3001d4eb15e3be7f6",
232
+ "sha256": "419e9ddf2e11c8e770dca469e89b88c7f6e2c6941836482fb6072bfce49d14b8",
233
233
  "executable": false
234
234
  },
235
235
  {
@@ -309,7 +309,7 @@
309
309
  },
310
310
  {
311
311
  "path": "native/claude/.claude-plugin/plugin.json",
312
- "sha256": "62873f1bc35e7badbe5f6669d19c01fc5f72a4120624e2b1560faad1b2e385d8",
312
+ "sha256": "c5925d81d5a6d26b54271f6d85b4856dfccd2333dfbe5c8bf3e7c60a288a25fd",
313
313
  "executable": false
314
314
  },
315
315
  {
@@ -359,7 +359,7 @@
359
359
  },
360
360
  {
361
361
  "path": "native/claude/skills/_shared/harness-store.py",
362
- "sha256": "9b8b9a8fba6d90df7cd02e3d7664520fb4536eee78bc26229ba2614ed618c894",
362
+ "sha256": "04505c53b0aa3006c700382cb444492d5a4f7c7c0780124dd229caa78eaaacfb",
363
363
  "executable": false
364
364
  },
365
365
  {
@@ -369,7 +369,7 @@
369
369
  },
370
370
  {
371
371
  "path": "native/claude/skills/_shared/platform-support.py",
372
- "sha256": "0e1a86dc02871744abec5aa0d0789e62f2aba4d6e8ee79b3001d4eb15e3be7f6",
372
+ "sha256": "419e9ddf2e11c8e770dca469e89b88c7f6e2c6941836482fb6072bfce49d14b8",
373
373
  "executable": false
374
374
  },
375
375
  {
@@ -439,12 +439,12 @@
439
439
  },
440
440
  {
441
441
  "path": "native/codex/.agents/plugins/marketplace.json",
442
- "sha256": "d5250ae7c116d8a6f8cd6277f00b0fa14b78099cf7818d0635e5347d85fff360",
442
+ "sha256": "8b6d5165236fd2c0e6bdbe58b1f1b2659cf6852c268db8483f7a2e25f870002c",
443
443
  "executable": false
444
444
  },
445
445
  {
446
446
  "path": "native/codex/plugins/devlyn/.codex-plugin/plugin.json",
447
- "sha256": "62873f1bc35e7badbe5f6669d19c01fc5f72a4120624e2b1560faad1b2e385d8",
447
+ "sha256": "c5925d81d5a6d26b54271f6d85b4856dfccd2333dfbe5c8bf3e7c60a288a25fd",
448
448
  "executable": false
449
449
  },
450
450
  {
@@ -494,7 +494,7 @@
494
494
  },
495
495
  {
496
496
  "path": "native/codex/plugins/devlyn/skills/_shared/harness-store.py",
497
- "sha256": "9b8b9a8fba6d90df7cd02e3d7664520fb4536eee78bc26229ba2614ed618c894",
497
+ "sha256": "04505c53b0aa3006c700382cb444492d5a4f7c7c0780124dd229caa78eaaacfb",
498
498
  "executable": false
499
499
  },
500
500
  {
@@ -504,7 +504,7 @@
504
504
  },
505
505
  {
506
506
  "path": "native/codex/plugins/devlyn/skills/_shared/platform-support.py",
507
- "sha256": "0e1a86dc02871744abec5aa0d0789e62f2aba4d6e8ee79b3001d4eb15e3be7f6",
507
+ "sha256": "419e9ddf2e11c8e770dca469e89b88c7f6e2c6941836482fb6072bfce49d14b8",
508
508
  "executable": false
509
509
  },
510
510
  {
@@ -1619,12 +1619,12 @@
1619
1619
  },
1620
1620
  {
1621
1621
  "path": "package.json",
1622
- "sha256": "3e212b7eb21db1ce909afbf38bb9f9375f31f7d135f1f9a354c80678d644f6a3",
1622
+ "sha256": "c222c0658671710f90c77884f1582a860abf3e5f074ef8e5f485648ab17d6871",
1623
1623
  "executable": false
1624
1624
  }
1625
1625
  ],
1626
1626
  "digest": {
1627
1627
  "algorithm": "sha256",
1628
- "value": "18a02f41c27a19c5268d5b9f8ab82ed7f6fadb6531dadc382d679b1cffc33fc2"
1628
+ "value": "dfb6e72ab508aa4e0a9929e812d8881c186689c37b08e6340eb40cf1865015af"
1629
1629
  }
1630
1630
  }