thoth-agents 0.4.1 → 0.5.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.
Files changed (117) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +86 -58
  3. package/dist/agents/prompt-sections.d.ts +5 -3
  4. package/dist/agents/prompt-utils.d.ts +1 -1
  5. package/dist/agents/worker.d.ts +2 -0
  6. package/dist/{chunk-STCURXAQ.js → chunk-MRFCA3U2.js} +33 -67
  7. package/dist/{chunk-EFGSGPYM.js → chunk-OW6I7ZUU.js} +391 -416
  8. package/dist/chunk-V7HSGRMB.js +1016 -0
  9. package/dist/{chunk-2D7E7GMP.js → chunk-VUNYH263.js} +1650 -380
  10. package/dist/cli/claude-code-paths.d.ts +2 -2
  11. package/dist/cli/codex-install.d.ts +5 -2
  12. package/dist/cli/codex-paths.d.ts +2 -2
  13. package/dist/cli/index.js +47 -11
  14. package/dist/cli/model-defaults.d.ts +4 -0
  15. package/dist/cli/operations/pi.d.ts +1 -0
  16. package/dist/cli/pi-effort.d.ts +1 -1
  17. package/dist/cli/pi-external-package.d.ts +24 -0
  18. package/dist/cli/pi-git-source.d.ts +1 -0
  19. package/dist/cli/pi-install.d.ts +92 -15
  20. package/dist/cli/pi-managed-write.d.ts +3 -0
  21. package/dist/cli/pi-model-config.d.ts +17 -0
  22. package/dist/cli/pi-native-probe.d.ts +2 -0
  23. package/dist/cli/pi-paths.d.ts +2 -0
  24. package/dist/cli/pi-resources.d.ts +3 -1
  25. package/dist/cli/pi-tool-config.d.ts +32 -0
  26. package/dist/cli/tui/index.js +32 -7
  27. package/dist/cli/tui/operations.d.ts +3 -1
  28. package/dist/cli/types.d.ts +2 -0
  29. package/dist/config/constants.d.ts +4 -8
  30. package/dist/config/schema.d.ts +7 -36
  31. package/dist/harness/adapters/claude-code.d.ts +2 -4
  32. package/dist/harness/core/agent-pack.d.ts +52 -42
  33. package/dist/harness/core/memory-governance.d.ts +2 -2
  34. package/dist/harness/core/sdd.d.ts +159 -319
  35. package/dist/harness/writers/pi-agent.d.ts +3 -1
  36. package/dist/index.js +12 -6
  37. package/dist/pi/tools-panel.d.ts +51 -0
  38. package/dist/pi.d.ts +44 -2
  39. package/dist/pi.js +471 -9
  40. package/dist/utils/subprocess.d.ts +1 -0
  41. package/package.json +18 -4
  42. package/pi/.thoth-agents-assets.json +5 -6
  43. package/pi/agents/thoth-designer.md +23 -8
  44. package/pi/agents/thoth-explorer.md +29 -9
  45. package/pi/agents/thoth-librarian.md +30 -8
  46. package/pi/agents/thoth-oracle.md +18 -10
  47. package/pi/agents/thoth-worker.md +96 -0
  48. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +7 -12
  49. package/plugin/.claude-plugin/plugin.json +2 -2
  50. package/plugin/.codex-plugin/plugin.json +2 -2
  51. package/plugin/agents/designer.md +13 -5
  52. package/plugin/agents/explorer.md +16 -5
  53. package/plugin/agents/librarian.md +15 -4
  54. package/plugin/agents/oracle.md +13 -7
  55. package/plugin/agents/orchestrator.md +82 -91
  56. package/plugin/agents/worker.md +76 -0
  57. package/plugin/skills/plan-reviewer/SKILL.md +39 -141
  58. package/plugin/skills/thoth-archive/SKILL.md +50 -47
  59. package/plugin/skills/thoth-archive/scripts/archive.mjs +287 -654
  60. package/plugin/skills/thoth-constitution/SKILL.md +23 -42
  61. package/plugin/skills/thoth-constitution/templates/constitution.md +54 -44
  62. package/plugin/skills/thoth-init/SKILL.md +19 -29
  63. package/plugin/skills/thoth-init/scripts/init.mjs +73 -66
  64. package/plugin/skills/thoth-sdd/SKILL.md +164 -117
  65. package/plugin/skills/thoth-sdd/references/phases/checklist.md +2 -22
  66. package/plugin/skills/thoth-sdd/references/phases/clarify.md +14 -12
  67. package/plugin/skills/thoth-sdd/references/phases/converge.md +2 -13
  68. package/plugin/skills/thoth-sdd/references/phases/explore.md +3 -8
  69. package/plugin/skills/thoth-sdd/references/phases/implement.md +58 -57
  70. package/plugin/skills/thoth-sdd/references/phases/plan.md +2 -18
  71. package/plugin/skills/thoth-sdd/references/phases/specify.md +2 -33
  72. package/plugin/skills/thoth-sdd/references/phases/tasks.md +4 -45
  73. package/plugin/skills/thoth-sdd/references/phases/verify.md +4 -22
  74. package/plugin/skills/thoth-sdd/scripts/validate.mjs +610 -1232
  75. package/plugin/skills/thoth-sdd/templates/change.md +94 -0
  76. package/skills/README.md +11 -13
  77. package/skills/plan-reviewer/SKILL.md +39 -141
  78. package/skills/thoth-archive/SKILL.md +50 -47
  79. package/skills/thoth-archive/scripts/archive.mjs +287 -654
  80. package/skills/thoth-constitution/SKILL.md +23 -42
  81. package/skills/thoth-constitution/templates/constitution.md +54 -44
  82. package/skills/thoth-init/SKILL.md +19 -29
  83. package/skills/thoth-init/scripts/init.mjs +73 -66
  84. package/skills/thoth-sdd/SKILL.md +164 -117
  85. package/skills/thoth-sdd/references/phases/checklist.md +2 -22
  86. package/skills/thoth-sdd/references/phases/clarify.md +14 -12
  87. package/skills/thoth-sdd/references/phases/converge.md +2 -13
  88. package/skills/thoth-sdd/references/phases/explore.md +3 -8
  89. package/skills/thoth-sdd/references/phases/implement.md +58 -57
  90. package/skills/thoth-sdd/references/phases/plan.md +2 -18
  91. package/skills/thoth-sdd/references/phases/specify.md +2 -33
  92. package/skills/thoth-sdd/references/phases/tasks.md +4 -45
  93. package/skills/thoth-sdd/references/phases/verify.md +4 -22
  94. package/skills/thoth-sdd/scripts/validate.mjs +610 -1232
  95. package/skills/thoth-sdd/templates/change.md +94 -0
  96. package/thoth-agents.schema.json +9 -77
  97. package/dist/agents/deep.d.ts +0 -2
  98. package/dist/agents/quick.d.ts +0 -2
  99. package/dist/chunk-2U5MBLLV.js +0 -312
  100. package/pi/agents/thoth-deep.md +0 -81
  101. package/pi/agents/thoth-quick.md +0 -81
  102. package/plugin/agents/deep.md +0 -68
  103. package/plugin/agents/quick.md +0 -68
  104. package/plugin/skills/plan-reviewer/templates/plan-review.md +0 -65
  105. package/plugin/skills/thoth-sdd/templates/archive-report.md +0 -25
  106. package/plugin/skills/thoth-sdd/templates/checklist.md +0 -27
  107. package/plugin/skills/thoth-sdd/templates/plan.md +0 -46
  108. package/plugin/skills/thoth-sdd/templates/spec.md +0 -62
  109. package/plugin/skills/thoth-sdd/templates/tasks.md +0 -58
  110. package/plugin/skills/thoth-sdd/templates/verify-report.md +0 -28
  111. package/skills/plan-reviewer/templates/plan-review.md +0 -65
  112. package/skills/thoth-sdd/templates/archive-report.md +0 -25
  113. package/skills/thoth-sdd/templates/checklist.md +0 -27
  114. package/skills/thoth-sdd/templates/plan.md +0 -46
  115. package/skills/thoth-sdd/templates/spec.md +0 -62
  116. package/skills/thoth-sdd/templates/tasks.md +0 -58
  117. package/skills/thoth-sdd/templates/verify-report.md +0 -28
@@ -4,13 +4,19 @@ import {
4
4
  exa
5
5
  } from "./chunk-62L3ORXN.js";
6
6
  import {
7
+ PI_EFFORT_VALUES,
7
8
  PI_MANAGED_OWNER,
8
9
  PI_ROOT_END,
9
10
  PI_ROOT_START,
11
+ assertSafePiManagedPath,
10
12
  piAdapter,
13
+ readPiModelConfig,
11
14
  renderConfiguredRolePrompt,
12
- syncPiSpecialists
13
- } from "./chunk-2U5MBLLV.js";
15
+ resolvePiEffort,
16
+ savePiModelConfig,
17
+ syncPiSpecialists,
18
+ writePiManagedText
19
+ } from "./chunk-V7HSGRMB.js";
14
20
  import {
15
21
  ALL_AGENT_NAMES,
16
22
  CLAUDE_CODE_PROMPT_DIALECT,
@@ -19,6 +25,7 @@ import {
19
25
  CONFIRMED_OPENAI_SUBAGENT_PRESET,
20
26
  DEFAULT_MODELS,
21
27
  PI_SPECIALIST_ROLES,
28
+ SUBAGENT_NAMES,
22
29
  claudeCodeSubagentType,
23
30
  ensureConfigDir,
24
31
  findPackageRoot,
@@ -40,7 +47,7 @@ import {
40
47
  updateOpenCodeMainConfig,
41
48
  writeConfig,
42
49
  writeLiteConfig
43
- } from "./chunk-EFGSGPYM.js";
50
+ } from "./chunk-OW6I7ZUU.js";
44
51
 
45
52
  // src/cli/codex-paths.ts
46
53
  import { homedir } from "os";
@@ -50,8 +57,7 @@ var CODEX_ROLE_NAMES = [
50
57
  "librarian",
51
58
  "oracle",
52
59
  "designer",
53
- "quick",
54
- "deep"
60
+ "worker"
55
61
  ];
56
62
  function getCodexHome(options = {}) {
57
63
  const explicit = options.codexHome ?? process.env.CODEX_HOME?.trim();
@@ -92,8 +98,9 @@ function isExplicitEffort(selection) {
92
98
  }
93
99
 
94
100
  // src/cli/codex-install.ts
95
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
96
- import { basename } from "path";
101
+ import { createHash as createHash2 } from "crypto";
102
+ import { existsSync as existsSync4, lstatSync, readFileSync as readFileSync4, rmSync } from "fs";
103
+ import { basename, dirname as dirname4, parse, resolve as resolve2 } from "path";
97
104
  import { fileURLToPath as fileURLToPath2 } from "url";
98
105
 
99
106
  // src/harness/adapters/codex.ts
@@ -1019,7 +1026,7 @@ function createCodexPluginPackageManifest(context2) {
1019
1026
  return {
1020
1027
  name: "thoth-agents",
1021
1028
  version: readRootPackageVersion(context2),
1022
- description: "Bundled Spec Kit-compatible SDD skills and MCP configuration; the seven-role global Codex layer requires thoth-agents CLI setup."
1029
+ description: "Bundled AI-first work skills and MCP configuration; the six-role global Codex layer requires thoth-agents CLI setup."
1023
1030
  };
1024
1031
  }
1025
1032
  function stableJson2(value) {
@@ -1064,7 +1071,7 @@ function codexRuntimeGuidance() {
1064
1071
  return [
1065
1072
  "<codex-runtime>",
1066
1073
  "- The ambient Codex session is the adaptive root; no orchestrator child TOML is generated.",
1067
- "- Delegate with `collaboration.spawn_agent` only when the root determines that specialization, context isolation, review, or independent parallel work creates a net gain.",
1074
+ "- When delegation is selected under the shared ownership policy, use `collaboration.spawn_agent`; honor explicit direct-work instructions.",
1068
1075
  "- Collaboration tools are direct tools and must not be called from inside `functions.exec`.",
1069
1076
  "- When the active `collaboration.spawn_agent` schema exposes `agent_type`, set it to the selected canonical role. When it does not, use a role-prefixed `task_name` plus a self-contained bounded envelope; that fallback is instruction-only.",
1070
1077
  "- Keep maximum depth 1: children do not delegate. Use one writer per mutable surface and parallelize only independent work.",
@@ -1496,6 +1503,90 @@ function readManagedModelState(path, version) {
1496
1503
  var ROOT_START = "<!-- thoth-agents:codex-root:start -->";
1497
1504
  var ROOT_END = "<!-- thoth-agents:codex-root:end -->";
1498
1505
  var MANAGED_MODEL_STATE_VERSION = 1;
1506
+ var OBSOLETE_CODEX_ROLES = ["quick", "deep"];
1507
+ var RECOVERY_FILE = ".thoth-agents-install-recovery.json";
1508
+ function lstatIfPresent(path) {
1509
+ try {
1510
+ return lstatSync(path);
1511
+ } catch (error) {
1512
+ if (error.code === "ENOENT") return void 0;
1513
+ throw error;
1514
+ }
1515
+ }
1516
+ function fingerprint(content) {
1517
+ return createHash2("sha256").update(content).digest("hex");
1518
+ }
1519
+ function assertSafeCodexRolePath(path, agentsDir) {
1520
+ const absolute = resolve2(path);
1521
+ const expectedParent = resolve2(agentsDir);
1522
+ if (dirname4(absolute) !== expectedParent)
1523
+ throw new Error(
1524
+ `Unsafe Codex role path outside managed agents directory: ${path}`
1525
+ );
1526
+ const root = parse(absolute).root;
1527
+ const ancestors = [];
1528
+ let cursor = dirname4(absolute);
1529
+ while (cursor !== root) {
1530
+ ancestors.push(cursor);
1531
+ cursor = dirname4(cursor);
1532
+ }
1533
+ ancestors.push(root);
1534
+ for (const ancestor of ancestors.reverse()) {
1535
+ const stat2 = lstatIfPresent(ancestor);
1536
+ if (!stat2) continue;
1537
+ if (stat2.isSymbolicLink())
1538
+ throw new Error(
1539
+ `Managed Codex role path has a symlinked parent: ${ancestor}`
1540
+ );
1541
+ if (!stat2.isDirectory())
1542
+ throw new Error(
1543
+ `Managed Codex role path parent is not a directory: ${ancestor}`
1544
+ );
1545
+ }
1546
+ const stat = lstatIfPresent(absolute);
1547
+ if (!stat) return;
1548
+ if (stat.isSymbolicLink())
1549
+ throw new Error(`Managed Codex role target is a symlink: ${absolute}`);
1550
+ if (!stat.isFile())
1551
+ throw new Error(
1552
+ `Managed Codex role target is not a regular file: ${absolute}`
1553
+ );
1554
+ }
1555
+ function addApplyGuard(item) {
1556
+ if (!lstatIfPresent(item.targetPath))
1557
+ return { ...item, expectedAbsent: true };
1558
+ return { ...item, expectedContent: readFileSync4(item.targetPath, "utf8") };
1559
+ }
1560
+ function assertApplyGuard(item) {
1561
+ if (item.expectedAbsent) {
1562
+ if (lstatIfPresent(item.targetPath))
1563
+ throw new Error(
1564
+ `Managed Codex target appeared after planning: ${item.targetPath}`
1565
+ );
1566
+ return;
1567
+ }
1568
+ if (item.expectedContent !== void 0) {
1569
+ if (!lstatIfPresent(item.targetPath) || readFileSync4(item.targetPath, "utf8") !== item.expectedContent)
1570
+ throw new Error(
1571
+ `Managed Codex target changed after planning: ${item.targetPath}`
1572
+ );
1573
+ }
1574
+ }
1575
+ function readInstallRecovery(path) {
1576
+ if (!lstatIfPresent(path)) return void 0;
1577
+ const raw = JSON.parse(readFileSync4(path, "utf8"));
1578
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
1579
+ throw new Error(`Corrupt Codex install recovery state: ${path}`);
1580
+ const value = raw;
1581
+ if (value.version !== 1 || !value.roles || typeof value.roles !== "object" || Array.isArray(value.roles))
1582
+ throw new Error(`Corrupt Codex install recovery state: ${path}`);
1583
+ const roles = Object.fromEntries(Object.entries(value.roles));
1584
+ if (Object.values(roles).some(
1585
+ (hash) => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)
1586
+ ))
1587
+ throw new Error(`Corrupt Codex install recovery state: ${path}`);
1588
+ return { version: 1, roles };
1589
+ }
1499
1590
  function mergeManagedBlock(existing, managedBlock) {
1500
1591
  const start = existing.indexOf(ROOT_START);
1501
1592
  const end = existing.indexOf(ROOT_END);
@@ -1713,8 +1804,82 @@ function buildCodexSetupPlan(config) {
1713
1804
  ...packageRoot ? { packageRoot } : {}
1714
1805
  });
1715
1806
  const rootBlock = renderCodexRootInstructions();
1807
+ const agentsDir = dirname4(
1808
+ targets.roleAgentPaths[0]?.path ?? targets.managedModelsPath
1809
+ );
1810
+ const recoveryPath = resolve2(agentsDir, RECOVERY_FILE);
1811
+ assertSafeCodexRolePath(targets.managedModelsPath, agentsDir);
1812
+ assertSafeCodexRolePath(recoveryPath, agentsDir);
1813
+ for (const target of targets.roleAgentPaths)
1814
+ assertSafeCodexRolePath(target.path, agentsDir);
1716
1815
  const managedModelState2 = readManagedModelState2(targets.managedModelsPath);
1816
+ const recovery = readInstallRecovery(recoveryPath);
1717
1817
  const nextManagedModelState = emptyManagedModelState2();
1818
+ const roleItems = targets.roleAgentPaths.map(
1819
+ (target) => addApplyGuard({
1820
+ kind: "role-subagent-toml",
1821
+ action: "write-role-toml",
1822
+ targetPath: target.path,
1823
+ description: `Materialize Codex role subagent ${target.role}.`,
1824
+ requiresBackup: Boolean(lstatIfPresent(target.path)),
1825
+ role: target.role,
1826
+ renderedModel: parseRoleTomlModel(
1827
+ roleArtifactContent(target.role, render.artifacts)
1828
+ ),
1829
+ content: resolveRoleTomlContent({
1830
+ renderedContent: roleArtifactContent(target.role, render.artifacts),
1831
+ targetPath: target.path,
1832
+ state: managedModelState2,
1833
+ nextState: nextManagedModelState,
1834
+ reset: config.reset
1835
+ })
1836
+ })
1837
+ );
1838
+ const recoveryRoles = Object.fromEntries(
1839
+ roleItems.map((item) => [
1840
+ roleManagedModelStateKey(item.targetPath),
1841
+ fingerprint(item.content ?? "")
1842
+ ])
1843
+ );
1844
+ for (const item of roleItems) {
1845
+ if (item.role !== "worker" || !lstatIfPresent(item.targetPath)) continue;
1846
+ const key = roleManagedModelStateKey(item.targetPath);
1847
+ const ownedByLedger = key in managedModelState2.models;
1848
+ const contentHash = fingerprint(readFileSync4(item.targetPath, "utf8"));
1849
+ const recoverable = recovery?.roles[key] === contentHash && recoveryRoles[key] === contentHash;
1850
+ if (!ownedByLedger && !recoverable)
1851
+ throw new Error(
1852
+ `Existing Worker Codex role lacks thoth-agents ownership state or a trustworthy recovery fingerprint: ${item.targetPath}. Move or remove it explicitly before retrying.`
1853
+ );
1854
+ }
1855
+ if (recovery) {
1856
+ const expectedKeys = Object.keys(recoveryRoles).sort();
1857
+ if (JSON.stringify(Object.keys(recovery.roles).sort()) !== JSON.stringify(expectedKeys) || expectedKeys.some((key) => recovery.roles[key] !== recoveryRoles[key]))
1858
+ throw new Error(
1859
+ `Codex install recovery state does not match this package: ${recoveryPath}`
1860
+ );
1861
+ }
1862
+ const obsoleteItems = OBSOLETE_CODEX_ROLES.flatMap(
1863
+ (role) => {
1864
+ const targetPath = resolve2(agentsDir, `thoth-agents-${role}.toml`);
1865
+ assertSafeCodexRolePath(targetPath, agentsDir);
1866
+ if (!lstatIfPresent(targetPath)) return [];
1867
+ const key = roleManagedModelStateKey(targetPath);
1868
+ if (!(key in managedModelState2.models))
1869
+ throw new Error(
1870
+ `Obsolete Codex role lacks thoth-agents ownership state and was preserved: ${targetPath}. Remove it explicitly before retrying.`
1871
+ );
1872
+ return [
1873
+ addApplyGuard({
1874
+ kind: "role-subagent-toml",
1875
+ action: "retire-obsolete-role-toml",
1876
+ targetPath,
1877
+ description: `Retire attributable obsolete Codex role ${role}.`,
1878
+ requiresBackup: false
1879
+ })
1880
+ ];
1881
+ }
1882
+ );
1718
1883
  const items = [
1719
1884
  {
1720
1885
  kind: "root-instructions",
@@ -1724,34 +1889,24 @@ function buildCodexSetupPlan(config) {
1724
1889
  requiresBackup: true,
1725
1890
  content: rootBlock
1726
1891
  },
1727
- ...targets.roleAgentPaths.map(
1728
- (target) => ({
1729
- kind: "role-subagent-toml",
1730
- action: "write-role-toml",
1731
- targetPath: target.path,
1732
- description: `Materialize Codex role subagent ${target.role}.`,
1733
- requiresBackup: existsSync4(target.path),
1734
- role: target.role,
1735
- renderedModel: parseRoleTomlModel(
1736
- roleArtifactContent(target.role, render.artifacts)
1737
- ),
1738
- content: resolveRoleTomlContent({
1739
- renderedContent: roleArtifactContent(target.role, render.artifacts),
1740
- targetPath: target.path,
1741
- state: managedModelState2,
1742
- nextState: nextManagedModelState,
1743
- reset: config.reset
1744
- })
1745
- })
1746
- ),
1747
- {
1892
+ addApplyGuard({
1893
+ kind: "install-recovery",
1894
+ action: "write-install-recovery",
1895
+ targetPath: recoveryPath,
1896
+ description: "Persist exact role fingerprints for interrupted-install recovery.",
1897
+ requiresBackup: Boolean(lstatIfPresent(recoveryPath)),
1898
+ content: stableJson3({ version: 1, roles: recoveryRoles })
1899
+ }),
1900
+ ...roleItems,
1901
+ ...obsoleteItems,
1902
+ addApplyGuard({
1748
1903
  kind: "managed-model-state",
1749
1904
  action: "write-managed-model-state",
1750
1905
  targetPath: targets.managedModelsPath,
1751
1906
  description: "Record thoth-agents-managed Codex role model ownership state.",
1752
1907
  requiresBackup: existsSync4(targets.managedModelsPath),
1753
1908
  content: stableJson3(nextManagedModelState)
1754
- },
1909
+ }),
1755
1910
  {
1756
1911
  kind: "user-config",
1757
1912
  action: "merge-toml",
@@ -1801,8 +1956,32 @@ function applyCodexSetup(plan) {
1801
1956
  ]);
1802
1957
  if (plan.dryRun) return { success: true, changed, diagnostics };
1803
1958
  try {
1959
+ const stateItem = plan.items.find(
1960
+ (item) => item.action === "write-managed-model-state"
1961
+ );
1962
+ if (!stateItem)
1963
+ throw new Error("Codex managed model state item is missing from plan.");
1964
+ const agentsDir = dirname4(stateItem.targetPath);
1965
+ for (const item of plan.items) {
1966
+ if (item.action === "write-role-toml" || item.action === "retire-obsolete-role-toml" || item.action === "write-install-recovery" || item.action === "write-managed-model-state") {
1967
+ assertSafeCodexRolePath(item.targetPath, agentsDir);
1968
+ assertApplyGuard(item);
1969
+ }
1970
+ }
1804
1971
  for (const item of plan.items) {
1805
1972
  if (item.action === "diagnose-only") continue;
1973
+ if (item.action === "retire-obsolete-role-toml") {
1974
+ const agentsDir2 = dirname4(item.targetPath);
1975
+ assertSafeCodexRolePath(item.targetPath, agentsDir2);
1976
+ assertApplyGuard(item);
1977
+ assertSafeCodexRolePath(stateItem.targetPath, agentsDir2);
1978
+ assertApplyGuard(stateItem);
1979
+ if (existsSync4(item.targetPath)) {
1980
+ rmSync(item.targetPath);
1981
+ changed.push(item.targetPath);
1982
+ }
1983
+ continue;
1984
+ }
1806
1985
  if (item.action === "merge-toml") {
1807
1986
  const result = writeCodexConfigMerge({
1808
1987
  configPath: item.targetPath,
@@ -1815,12 +1994,29 @@ function applyCodexSetup(plan) {
1815
1994
  continue;
1816
1995
  }
1817
1996
  if (item.content === void 0) continue;
1997
+ if (item.action === "write-role-toml" || item.action === "write-install-recovery" || item.action === "write-managed-model-state") {
1998
+ assertSafeCodexRolePath(item.targetPath, agentsDir);
1999
+ assertApplyGuard(item);
2000
+ }
1818
2001
  const content = item.action === "merge-managed-block" ? mergeManagedBlock(
1819
2002
  existsSync4(item.targetPath) ? readFileSync4(item.targetPath, "utf8") : "",
1820
2003
  item.content
1821
2004
  ) : item.content;
1822
2005
  if (writeTextWithBackup(item.targetPath, content))
1823
2006
  changed.push(item.targetPath);
2007
+ if (item.action === "write-managed-model-state") {
2008
+ const recoveryItem = plan.items.find(
2009
+ (candidate) => candidate.action === "write-install-recovery"
2010
+ );
2011
+ if (!recoveryItem?.content)
2012
+ throw new Error("Codex install recovery item is missing from plan.");
2013
+ assertSafeCodexRolePath(recoveryItem.targetPath, agentsDir);
2014
+ if (!lstatIfPresent(recoveryItem.targetPath) || readFileSync4(recoveryItem.targetPath, "utf8") !== recoveryItem.content)
2015
+ throw new Error(
2016
+ `Codex install recovery state changed before completion: ${recoveryItem.targetPath}`
2017
+ );
2018
+ rmSync(recoveryItem.targetPath);
2019
+ }
1824
2020
  }
1825
2021
  return { success: true, changed, diagnostics: uniqueMessages(diagnostics) };
1826
2022
  } catch (error) {
@@ -1840,7 +2036,7 @@ import { existsSync as existsSync7 } from "fs";
1840
2036
  import { fileURLToPath as fileURLToPath3 } from "url";
1841
2037
 
1842
2038
  // src/harness/writers/claude-code-plugin-package.ts
1843
- import { createHash as createHash2 } from "crypto";
2039
+ import { createHash as createHash3 } from "crypto";
1844
2040
  var CLAUDE_PLUGIN_MANIFEST_FIELDS = [
1845
2041
  "name",
1846
2042
  "version",
@@ -1857,7 +2053,7 @@ function stableJson4(value) {
1857
2053
  `;
1858
2054
  }
1859
2055
  function sha256Hash(content) {
1860
- return `sha256:${createHash2("sha256").update(content).digest("hex")}`;
2056
+ return `sha256:${createHash3("sha256").update(content).digest("hex")}`;
1861
2057
  }
1862
2058
  function withoutProviderAsset(artifact) {
1863
2059
  if (!normalizePath2(artifact.path).endsWith(".mcp.json")) return artifact;
@@ -1951,14 +2147,12 @@ var CLAUDE_CODE_SUBAGENT_DEFAULT_MODELS = {
1951
2147
  librarian: "sonnet",
1952
2148
  oracle: "opus",
1953
2149
  designer: "sonnet",
1954
- quick: "haiku",
1955
- deep: "sonnet"
2150
+ worker: "sonnet"
1956
2151
  };
1957
2152
  var CLAUDE_CODE_SUBAGENT_DEFAULT_EFFORTS = {
1958
2153
  explorer: "low",
1959
- quick: "low",
1960
2154
  designer: "medium",
1961
- deep: "medium",
2155
+ worker: "medium",
1962
2156
  librarian: "high",
1963
2157
  oracle: "high"
1964
2158
  };
@@ -2012,7 +2206,7 @@ function renderClaudeCodeRootInstructions(config) {
2012
2206
  rootPrompt,
2013
2207
  "<claude-code-runtime>",
2014
2208
  "- You are the Claude Code adaptive root activated by plugin settings.json.",
2015
- `- Delegate only for net gain through Agent with \`subagent_type\` set to one of these plugin-namespaced specialists: ${specialists}. Always keep the ${CLAUDE_CODE_SUBAGENT_NAMESPACE}: prefix.`,
2209
+ `- When delegation is selected, use Agent with \`subagent_type\`: ${specialists}. Honor shared ownership and explicit direct-work instructions. Keep the ${CLAUDE_CODE_SUBAGENT_NAMESPACE}: prefix.`,
2016
2210
  "- Subagents cannot delegate further. Parallelize only independent work and maintain one writer per mutable surface.",
2017
2211
  "- Read-only roles deny Write and Edit while retaining other inherited tools, including MCP tools. Coordination-agent path scope remains instruction-level.",
2018
2212
  "- Use AskUserQuestion only for blocking material choices and TodoWrite only for genuine multi-step progress.",
@@ -2055,7 +2249,7 @@ function createPluginManifest(context2) {
2055
2249
  return {
2056
2250
  name: "thoth-agents",
2057
2251
  version: readRootPackageVersion2(context2),
2058
- description: "Adaptive multi-harness agent pack with seven roles and a runtime-autonomous Spec Kit-compatible SDD bundle for Claude Code.",
2252
+ description: "Adaptive multi-harness agent pack with six roles and proportional SDD governance for Claude Code.",
2059
2253
  author: { name: "thoth-agents" }
2060
2254
  };
2061
2255
  }
@@ -2088,7 +2282,7 @@ function renderOrchestratorArtifact(config) {
2088
2282
  );
2089
2283
  const content = renderClaudeCodeSubagent({
2090
2284
  name: "orchestrator",
2091
- description: orchestrator?.responsibility ?? "Adaptive root coordinator for direct work, SDD routing, and specialist dispatch.",
2285
+ description: orchestrator?.responsibility ?? "Adaptive root coordinator for human agreement, SDD classification, and specialist dispatch.",
2092
2286
  model: "inherit",
2093
2287
  instructions: renderClaudeCodeRootInstructions(config)
2094
2288
  });
@@ -2145,8 +2339,7 @@ var CLAUDE_CODE_ROLE_NAMES = [
2145
2339
  "librarian",
2146
2340
  "oracle",
2147
2341
  "designer",
2148
- "quick",
2149
- "deep"
2342
+ "worker"
2150
2343
  ];
2151
2344
 
2152
2345
  // src/cli/claude-code-install.ts
@@ -2452,11 +2645,11 @@ import {
2452
2645
  mkdirSync as mkdirSync3,
2453
2646
  readFileSync as readFileSync5,
2454
2647
  renameSync as renameSync2,
2455
- rmSync,
2648
+ rmSync as rmSync2,
2456
2649
  writeFileSync as writeFileSync3
2457
2650
  } from "fs";
2458
2651
  import { homedir as homedir2 } from "os";
2459
- import { dirname as dirname4, join as join2 } from "path";
2652
+ import { dirname as dirname5, join as join2 } from "path";
2460
2653
  var INSTALL_LEDGER_SCHEMA_VERSION = 1;
2461
2654
  var INSTALL_HARNESSES = /* @__PURE__ */ new Set([
2462
2655
  "opencode",
@@ -2536,7 +2729,7 @@ function recordCompletedInstall(options) {
2536
2729
  const repairedInvalidState = current.status === "invalid";
2537
2730
  const backupPath = repairedInvalidState ? `${path}.bak` : void 0;
2538
2731
  try {
2539
- mkdirSync3(dirname4(path), { recursive: true });
2732
+ mkdirSync3(dirname5(path), { recursive: true });
2540
2733
  writeFileSync3(temporaryPath, `${JSON.stringify(ledger, null, 2)}
2541
2734
  `);
2542
2735
  if (backupPath) copyFileSync3(path, backupPath);
@@ -2550,7 +2743,7 @@ function recordCompletedInstall(options) {
2550
2743
  };
2551
2744
  } catch (error) {
2552
2745
  try {
2553
- rmSync(temporaryPath, { recursive: true, force: true });
2746
+ rmSync2(temporaryPath, { recursive: true, force: true });
2554
2747
  } catch {
2555
2748
  }
2556
2749
  return {
@@ -2616,6 +2809,7 @@ function parseProviderSetupResult(stdout, expectedHarness) {
2616
2809
  }
2617
2810
  function defaultCommandExecutor2(command, args, options) {
2618
2811
  const result = spawnSync2(command, [...args], {
2812
+ windowsHide: true,
2619
2813
  cwd: options.cwd,
2620
2814
  encoding: "utf8",
2621
2815
  stdio: ["ignore", "pipe", "pipe"],
@@ -2901,7 +3095,10 @@ function installRequiredSkill(skill, harness, options = {}) {
2901
3095
  options
2902
3096
  );
2903
3097
  try {
2904
- const result = spawnSync3(command, args, { stdio: "inherit" });
3098
+ const result = spawnSync3(command, args, {
3099
+ windowsHide: true,
3100
+ stdio: "inherit"
3101
+ });
2905
3102
  if (result.status === 0) {
2906
3103
  return {
2907
3104
  skill,
@@ -3613,13 +3810,13 @@ import { basename as basename2 } from "path";
3613
3810
  // src/cli/codex-plugin-install.ts
3614
3811
  import { spawnSync as spawnSync4 } from "child_process";
3615
3812
  import {
3616
- lstatSync,
3813
+ lstatSync as lstatSync2,
3617
3814
  readdirSync,
3618
3815
  readFileSync as readFileSync6,
3619
3816
  realpathSync,
3620
- rmSync as rmSync2
3817
+ rmSync as rmSync3
3621
3818
  } from "fs";
3622
- import { isAbsolute, join as join4, relative, resolve as resolve2, sep } from "path";
3819
+ import { isAbsolute, join as join4, relative, resolve as resolve3, sep } from "path";
3623
3820
  var PLUGIN_NAME2 = "thoth-agents";
3624
3821
  var MARKETPLACE_NAME2 = "thoth-plugins";
3625
3822
  var MARKETPLACE_SOURCE2 = "https://github.com/EremesNG/thoth-plugins.git";
@@ -3693,9 +3890,9 @@ function parseManagerRecord(content) {
3693
3890
  function normalizeMarketplaceSource2(value) {
3694
3891
  return value.trim().replace(/#.*$/u, "").replace(/^git@github\.com:/i, "").replace(/^(?:https?|ssh):\/\/(?:git@)?github\.com\//i, "").replace(/^github\.com\//i, "").replace(/\/?\.git\/?$/i, "").replace(/\/$/, "").toLowerCase();
3695
3892
  }
3696
- function lstatIfPresent(path) {
3893
+ function lstatIfPresent2(path) {
3697
3894
  try {
3698
- return lstatSync(path);
3895
+ return lstatSync2(path);
3699
3896
  } catch (error) {
3700
3897
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
3701
3898
  return null;
@@ -3708,7 +3905,7 @@ function strictDescendant(base, candidate) {
3708
3905
  return suffix !== "" && suffix !== ".." && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix);
3709
3906
  }
3710
3907
  function parseObjectFile(path, label) {
3711
- const file = lstatIfPresent(path);
3908
+ const file = lstatIfPresent2(path);
3712
3909
  if (!file || file.isSymbolicLink() || !file.isFile()) {
3713
3910
  throw new Error(`${label} must be a regular non-link file`);
3714
3911
  }
@@ -3731,7 +3928,7 @@ function validateCacheManifest(root) {
3731
3928
  );
3732
3929
  }
3733
3930
  const productRoot = join4(root, PLUGIN_NAME2);
3734
- const product = lstatIfPresent(productRoot);
3931
+ const product = lstatIfPresent2(productRoot);
3735
3932
  if (!product || product.isSymbolicLink() || !product.isDirectory()) {
3736
3933
  throw new Error(`Legacy cache product root ${productRoot} is unsafe`);
3737
3934
  }
@@ -3741,7 +3938,7 @@ function validateCacheManifest(root) {
3741
3938
  }
3742
3939
  for (const entry of versions) {
3743
3940
  const versionRoot = join4(productRoot, entry.name);
3744
- const version = lstatIfPresent(versionRoot);
3941
+ const version = lstatIfPresent2(versionRoot);
3745
3942
  if (!entry.isDirectory() || !version || version.isSymbolicLink() || !version.isDirectory()) {
3746
3943
  throw new Error(`Legacy cache version root ${versionRoot} is unsafe`);
3747
3944
  }
@@ -3782,22 +3979,22 @@ function validateSnapshotManifest(root, marketplaceName) {
3782
3979
  }
3783
3980
  }
3784
3981
  function validateLegacyRoot(codexHome, definition, expectedRealPath) {
3785
- const path = resolve2(codexHome, ...definition.relativePath.split("/"));
3982
+ const path = resolve3(codexHome, ...definition.relativePath.split("/"));
3786
3983
  if (!strictDescendant(codexHome, path)) {
3787
3984
  throw new Error(
3788
3985
  `Legacy cleanup target escapes CODEX_HOME: ${definition.relativePath}`
3789
3986
  );
3790
3987
  }
3791
- const target = lstatIfPresent(path);
3988
+ const target = lstatIfPresent2(path);
3792
3989
  if (!target) return null;
3793
- const home = lstatIfPresent(codexHome);
3990
+ const home = lstatIfPresent2(codexHome);
3794
3991
  if (!home || !home.isDirectory()) {
3795
3992
  throw new Error(`Resolved CODEX_HOME is not a directory: ${codexHome}`);
3796
3993
  }
3797
3994
  let boundary = codexHome;
3798
3995
  for (const segment of definition.relativePath.split("/")) {
3799
3996
  boundary = join4(boundary, segment);
3800
- const value = lstatIfPresent(boundary);
3997
+ const value = lstatIfPresent2(boundary);
3801
3998
  if (!value || value.isSymbolicLink() || !value.isDirectory()) {
3802
3999
  throw new Error(
3803
4000
  `Legacy cleanup boundary is not a real directory: ${boundary}`
@@ -3830,7 +4027,7 @@ function preflightLegacyRoots(codexHomeInput) {
3830
4027
  if (typeof codexHomeInput !== "string" || codexHomeInput.trim() === "") {
3831
4028
  throw new Error("CODEX_HOME could not be resolved");
3832
4029
  }
3833
- const codexHome = resolve2(codexHomeInput);
4030
+ const codexHome = resolve3(codexHomeInput);
3834
4031
  const roots = LEGACY_ROOTS.map(
3835
4032
  (definition) => validateLegacyRoot(codexHome, definition)
3836
4033
  ).filter((root) => root !== null);
@@ -3965,7 +4162,7 @@ function legacyManagerClean(inspection) {
3965
4162
  }
3966
4163
  function legacyRootsAbsent(codexHome) {
3967
4164
  return LEGACY_ROOTS.every(
3968
- ({ relativePath }) => !lstatIfPresent(resolve2(codexHome, ...relativePath.split("/")))
4165
+ ({ relativePath }) => !lstatIfPresent2(resolve3(codexHome, ...relativePath.split("/")))
3969
4166
  );
3970
4167
  }
3971
4168
  function legacyPluginCommandItem(pluginId, projectRoot) {
@@ -4193,11 +4390,11 @@ function applyCodexPluginSetup(plan) {
4193
4390
  error: `Central ${PLUGIN_ID2} remains installed, but the fixed legacy cleanup plan is invalid; close Codex and retry.`
4194
4391
  };
4195
4392
  }
4196
- const exactPath = resolve2(
4393
+ const exactPath = resolve3(
4197
4394
  plan.codexHome,
4198
4395
  ...definition.relativePath.split("/")
4199
4396
  );
4200
- if (resolve2(root.path) !== exactPath || resolve2(item.targetPath) !== exactPath || root.kind !== definition.kind || root.marketplaceName !== definition.marketplaceName) {
4397
+ if (resolve3(root.path) !== exactPath || resolve3(item.targetPath) !== exactPath || root.kind !== definition.kind || root.marketplaceName !== definition.marketplaceName) {
4201
4398
  return {
4202
4399
  success: false,
4203
4400
  changed,
@@ -4205,7 +4402,7 @@ function applyCodexPluginSetup(plan) {
4205
4402
  error: `Central ${PLUGIN_ID2} remains installed, but the fixed legacy cleanup target changed; close Codex and retry.`
4206
4403
  };
4207
4404
  }
4208
- if (!lstatIfPresent(exactPath)) continue;
4405
+ if (!lstatIfPresent2(exactPath)) continue;
4209
4406
  try {
4210
4407
  const validated = validateLegacyRoot(
4211
4408
  plan.codexHome,
@@ -4213,8 +4410,8 @@ function applyCodexPluginSetup(plan) {
4213
4410
  root.realPath
4214
4411
  );
4215
4412
  if (!validated) continue;
4216
- rmSync2(validated.path, { recursive: true, force: false });
4217
- if (lstatIfPresent(validated.path)) {
4413
+ rmSync3(validated.path, { recursive: true, force: false });
4414
+ if (lstatIfPresent2(validated.path)) {
4218
4415
  throw new Error(
4219
4416
  `Legacy cleanup target still exists: ${root.relativePath}`
4220
4417
  );
@@ -4439,6 +4636,14 @@ function classifyItem(item) {
4439
4636
  if (item.action === "write-managed-model-state") {
4440
4637
  return managedModelState(item);
4441
4638
  }
4639
+ if (item.action === "write-install-recovery") {
4640
+ if (!existsSync8(item.targetPath))
4641
+ return { state: "installed", observed: "no interrupted install" };
4642
+ return {
4643
+ state: "drift",
4644
+ observed: "recoverable interrupted install journal present"
4645
+ };
4646
+ }
4442
4647
  if (item.action === "merge-toml") return userConfigState(item);
4443
4648
  return contentState(item);
4444
4649
  }
@@ -4546,6 +4751,14 @@ function getCodexStatus(context2 = { cwd: process.cwd() }, evidence = {}) {
4546
4751
  code: "codex-diagnostic"
4547
4752
  }));
4548
4753
  diagnostics.push(...requiredSkills.diagnostics);
4754
+ if (classified.some(
4755
+ ({ item, state: state2 }) => item.action === "write-install-recovery" && state2 === "drift"
4756
+ ))
4757
+ diagnostics.push({
4758
+ severity: "important",
4759
+ code: "codex-interrupted-install-recoverable",
4760
+ message: "An exact thoth-agents recovery journal indicates an interrupted Codex install; run sync or update to resume safely."
4761
+ });
4549
4762
  return {
4550
4763
  harness: "codex",
4551
4764
  displayName: CODEX_DISPLAY_NAME,
@@ -5078,7 +5291,7 @@ function applyCodexPlan(plan) {
5078
5291
  }
5079
5292
 
5080
5293
  // src/cli/operations/opencode.ts
5081
- import { createHash as createHash3 } from "crypto";
5294
+ import { createHash as createHash4 } from "crypto";
5082
5295
  import { existsSync as existsSync10 } from "fs";
5083
5296
  import { join as join6 } from "path";
5084
5297
 
@@ -5088,7 +5301,8 @@ var OPENAI_GPT_RUNTIME_VARIANTS = /* @__PURE__ */ new Set([
5088
5301
  "low",
5089
5302
  "medium",
5090
5303
  "high",
5091
- "xhigh"
5304
+ "xhigh",
5305
+ "max"
5092
5306
  ]);
5093
5307
  function resolveOpenCodeEffort(input) {
5094
5308
  if (!input.effort || input.effort.kind === "inherit") {
@@ -5116,16 +5330,16 @@ function resolveOpenCodeEffort(input) {
5116
5330
  import {
5117
5331
  cpSync,
5118
5332
  existsSync as existsSync9,
5119
- lstatSync as lstatSync2,
5333
+ lstatSync as lstatSync3,
5120
5334
  mkdirSync as mkdirSync4,
5121
5335
  mkdtempSync,
5122
5336
  readFileSync as readFileSync8,
5123
5337
  renameSync as renameSync3,
5124
- rmSync as rmSync3,
5338
+ rmSync as rmSync4,
5125
5339
  statSync
5126
5340
  } from "fs";
5127
5341
  import { homedir as homedir4 } from "os";
5128
- import { dirname as dirname5, join as join5 } from "path";
5342
+ import { dirname as dirname6, join as join5 } from "path";
5129
5343
  import { fileURLToPath as fileURLToPath4 } from "url";
5130
5344
 
5131
5345
  // src/harness/core/owned-skills.ts
@@ -5169,7 +5383,7 @@ function inspectPiPackageSkill(entry) {
5169
5383
  state: "missing",
5170
5384
  message: `Package-declared Pi skill directory is missing: ${entry.name}`
5171
5385
  };
5172
- const root = lstatSync2(entry.sourcePath);
5386
+ const root = lstatSync3(entry.sourcePath);
5173
5387
  if (!root.isDirectory() || root.isSymbolicLink())
5174
5388
  return {
5175
5389
  name: entry.name,
@@ -5183,7 +5397,7 @@ function inspectPiPackageSkill(entry) {
5183
5397
  state: "missing",
5184
5398
  message: `Package-declared Pi skill contract is missing: ${entry.name}`
5185
5399
  };
5186
- const contract = lstatSync2(contractPath);
5400
+ const contract = lstatSync3(contractPath);
5187
5401
  if (!contract.isFile() || contract.isSymbolicLink())
5188
5402
  return {
5189
5403
  name: entry.name,
@@ -5201,7 +5415,7 @@ function inspectPiPackageSkill(entry) {
5201
5415
  return void 0;
5202
5416
  }
5203
5417
  function resolveOwnedSkillPackageRoot(moduleUrl) {
5204
- const packageRoot = findPackageRoot(dirname5(fileURLToPath4(moduleUrl)));
5418
+ const packageRoot = findPackageRoot(dirname6(fileURLToPath4(moduleUrl)));
5205
5419
  if (!packageRoot) {
5206
5420
  throw new Error(
5207
5421
  "Unable to locate the thoth-agents package root for bundled owned skills."
@@ -5243,7 +5457,7 @@ function validateCanonicalSkill(entry) {
5243
5457
  function installOwnedSkills(skills) {
5244
5458
  const firstSkill = skills[0];
5245
5459
  if (!firstSkill) return;
5246
- const globalRoot = dirname5(firstSkill.destinationPath);
5460
+ const globalRoot = dirname6(firstSkill.destinationPath);
5247
5461
  mkdirSync4(globalRoot, { recursive: true });
5248
5462
  const transactionRoot = mkdtempSync(join5(globalRoot, ".thoth-agents-owned-"));
5249
5463
  const stagedRoot = join5(transactionRoot, "staged");
@@ -5268,7 +5482,7 @@ function installOwnedSkills(skills) {
5268
5482
  renameSync3(stagedPath, skill.destinationPath);
5269
5483
  } catch (error) {
5270
5484
  if (existsSync9(skill.destinationPath)) {
5271
- rmSync3(skill.destinationPath, { recursive: true, force: true });
5485
+ rmSync4(skill.destinationPath, { recursive: true, force: true });
5272
5486
  }
5273
5487
  if (hadExistingDestination && existsSync9(backupPath)) {
5274
5488
  renameSync3(backupPath, skill.destinationPath);
@@ -5276,11 +5490,11 @@ function installOwnedSkills(skills) {
5276
5490
  throw error;
5277
5491
  }
5278
5492
  if (existsSync9(backupPath)) {
5279
- rmSync3(backupPath, { recursive: true, force: true });
5493
+ rmSync4(backupPath, { recursive: true, force: true });
5280
5494
  }
5281
5495
  }
5282
5496
  } finally {
5283
- rmSync3(transactionRoot, { recursive: true, force: true });
5497
+ rmSync4(transactionRoot, { recursive: true, force: true });
5284
5498
  }
5285
5499
  }
5286
5500
  function syncOpenCodeOwnedSkills(options = {}) {
@@ -5434,7 +5648,7 @@ function canonicalDigest(value) {
5434
5648
  const serialized = canonicalData(value, /* @__PURE__ */ new Set());
5435
5649
  return {
5436
5650
  ok: true,
5437
- digest: createHash3("sha256").update(serialized).digest("hex")
5651
+ digest: createHash4("sha256").update(serialized).digest("hex")
5438
5652
  };
5439
5653
  } catch {
5440
5654
  return { ok: false };
@@ -5558,7 +5772,7 @@ function targetForLiteConfig(state) {
5558
5772
  path: getExistingLiteConfigPath(),
5559
5773
  label: "thoth-agents config",
5560
5774
  ...state ? { state } : {},
5561
- expected: `seven-role ${OPENAI_PRESET} roster`
5775
+ expected: `six-role ${OPENAI_PRESET} roster`
5562
5776
  };
5563
5777
  }
5564
5778
  function titleCaseSkillName(value) {
@@ -6036,7 +6250,7 @@ function getOpenCodeManagedStatus(context2 = { cwd: process.cwd() }) {
6036
6250
  harness: "opencode",
6037
6251
  displayName: "OpenCode",
6038
6252
  state: "drift",
6039
- summary: selectedNamedPreset ? "A valid named OpenCode preset is active outside the managed seven-role roster." : "thoth-agents config does not match the expected seven-role roster.",
6253
+ summary: selectedNamedPreset ? "A valid named OpenCode preset is active outside the managed six-role roster." : "thoth-agents config does not match the expected six-role roster.",
6040
6254
  targets: [
6041
6255
  { ...mainTarget, state: "installed" },
6042
6256
  { ...liteTarget, state: "drift" },
@@ -6240,7 +6454,7 @@ function buildOpenCodeCompletePlan(action, context2) {
6240
6454
  backup: defaultBackup(getExistingConfigPath())
6241
6455
  },
6242
6456
  {
6243
- title: "Write thoth-agents seven-role config",
6457
+ title: "Write thoth-agents six-role config",
6244
6458
  target: targetForLiteConfig(),
6245
6459
  preview: JSON.stringify(generatedConfig, null, 2),
6246
6460
  backup: defaultBackup(getExistingLiteConfigPath())
@@ -6341,7 +6555,7 @@ function buildOpenCodeSyncPlan(context2 = { cwd: process.cwd() }) {
6341
6555
  backup: defaultBackup(getExistingConfigPath())
6342
6556
  },
6343
6557
  {
6344
- title: "Write thoth-agents seven-role config",
6558
+ title: "Write thoth-agents six-role config",
6345
6559
  target: targetForLiteConfig(),
6346
6560
  preview: JSON.stringify(generatedConfig, null, 2),
6347
6561
  backup: defaultBackup(litePath)
@@ -6864,7 +7078,7 @@ function applyOpenCodePlan(plan) {
6864
7078
  }
6865
7079
  changedTargets.push({
6866
7080
  ...targetForLiteConfig("installed"),
6867
- observed: "seven-role roster written"
7081
+ observed: "six-role roster written"
6868
7082
  });
6869
7083
  if (existsSync10(`${liteResult.configPath}.bak`)) {
6870
7084
  backups.push({
@@ -6956,56 +7170,289 @@ function applyOpenCodePlan(plan) {
6956
7170
 
6957
7171
  // src/cli/operations/pi.ts
6958
7172
  import { spawnSync as spawnSync7 } from "child_process";
6959
- import { createHash as createHash5 } from "crypto";
6960
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
6961
- import { basename as basename3, isAbsolute as isAbsolute4, join as join12 } from "path";
7173
+ import { createHash as createHash6 } from "crypto";
7174
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
7175
+ import { basename as basename4, dirname as dirname11, isAbsolute as isAbsolute5, join as join13 } from "path";
6962
7176
 
6963
- // src/cli/pi-effort.ts
6964
- var PI_EFFORT_VALUES = [
6965
- "off",
6966
- "minimal",
6967
- "low",
6968
- "medium",
6969
- "high",
6970
- "xhigh"
6971
- ];
6972
- var PI_EFFORTS = new Set(PI_EFFORT_VALUES);
6973
- function resolvePiEffort(input) {
6974
- if (!input.effort || input.effort.kind === "inherit") {
6975
- return { ok: true, effort: void 0 };
7177
+ // src/cli/pi-external-package.ts
7178
+ import { readFileSync as readFileSync9, realpathSync as realpathSync2 } from "fs";
7179
+ import { isAbsolute as isAbsolute2, join as join7 } from "path";
7180
+ import { satisfies, valid } from "semver";
7181
+ function npmPackageName(source) {
7182
+ if (!source.startsWith("npm:")) return void 0;
7183
+ const spec = source.slice("npm:".length);
7184
+ const separator = spec.lastIndexOf("@");
7185
+ if (separator <= 0) return spec || void 0;
7186
+ return spec.slice(0, separator);
7187
+ }
7188
+ function installedPackageName(path) {
7189
+ if (!path) return void 0;
7190
+ try {
7191
+ const manifest = JSON.parse(
7192
+ readFileSync9(join7(path, "package.json"), "utf8")
7193
+ );
7194
+ if (typeof manifest === "object" && manifest !== null && !Array.isArray(manifest)) {
7195
+ const { name } = manifest;
7196
+ if (typeof name === "string") return name;
7197
+ }
7198
+ } catch {
7199
+ return void 0;
7200
+ }
7201
+ return void 0;
7202
+ }
7203
+ function piExternalSourceMatches(candidate, expectedSource) {
7204
+ if (candidate.source === expectedSource) return true;
7205
+ if (!isAbsolute2(expectedSource) || !candidate.installedPath) return false;
7206
+ try {
7207
+ return realpathSync2(candidate.installedPath) === realpathSync2(expectedSource);
7208
+ } catch {
7209
+ return false;
6976
7210
  }
6977
- const effort = input.effort.value;
6978
- if (!PI_EFFORTS.has(effort)) {
7211
+ }
7212
+ function inspectPiExternalPackage(packages, spec, requireManagedSource = true) {
7213
+ const matching = packages.filter(
7214
+ ({ source, installedPath }) => source === spec.source || npmPackageName(source) === spec.packageName || installedPackageName(installedPath) === spec.packageName
7215
+ );
7216
+ if (matching.some(({ scope }) => scope === "project"))
6979
7217
  return {
6980
- ok: false,
6981
- message: `Pi does not support effort ${effort}; expected ${PI_EFFORT_VALUES.join(", ")}, or inherit.`
7218
+ state: "drift",
7219
+ reason: "project package shadows the required global package"
6982
7220
  };
6983
- }
6984
- if (input.availableEfforts !== void 0 && !input.availableEfforts.includes(effort)) {
7221
+ const candidates = matching.filter(({ scope }) => scope === "user");
7222
+ if (candidates.length === 0)
7223
+ return { state: "missing", reason: "package is not configured globally" };
7224
+ if (candidates.length !== 1)
7225
+ return { state: "drift", reason: "package configuration is ambiguous" };
7226
+ const candidate = candidates[0];
7227
+ if (!candidate?.installedPath)
6985
7228
  return {
6986
- ok: false,
6987
- message: `Effort ${effort} is not available for ${input.model} in the selected model catalog entry.`
7229
+ state: "drift",
7230
+ source: candidate?.source,
7231
+ reason: "Pi did not report an installed package directory"
7232
+ };
7233
+ let manifest;
7234
+ try {
7235
+ manifest = JSON.parse(
7236
+ readFileSync9(join7(candidate.installedPath, "package.json"), "utf8")
7237
+ );
7238
+ } catch {
7239
+ return {
7240
+ state: "drift",
7241
+ source: candidate.source,
7242
+ installedPath: candidate.installedPath,
7243
+ reason: "installed package manifest is missing or malformed"
6988
7244
  };
6989
7245
  }
6990
- return { ok: true, effort };
7246
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
7247
+ return {
7248
+ state: "drift",
7249
+ source: candidate.source,
7250
+ installedPath: candidate.installedPath,
7251
+ reason: "installed package manifest is not an object"
7252
+ };
7253
+ const { name, version } = manifest;
7254
+ if (name !== spec.packageName)
7255
+ return {
7256
+ state: "drift",
7257
+ source: candidate.source,
7258
+ installedPath: candidate.installedPath,
7259
+ reason: `installed package name does not match ${spec.packageName}`
7260
+ };
7261
+ if (typeof version !== "string" || valid(version) === null)
7262
+ return {
7263
+ state: "drift",
7264
+ source: candidate.source,
7265
+ installedPath: candidate.installedPath,
7266
+ reason: "installed package version is not valid SemVer"
7267
+ };
7268
+ if (!satisfies(version, `>=${spec.version}`))
7269
+ return {
7270
+ state: "drift",
7271
+ source: candidate.source,
7272
+ installedPath: candidate.installedPath,
7273
+ version,
7274
+ reason: `installed package version does not satisfy >=${spec.version}`
7275
+ };
7276
+ if (requireManagedSource && !piExternalSourceMatches(candidate, spec.source))
7277
+ return {
7278
+ state: "drift",
7279
+ source: candidate.source,
7280
+ installedPath: candidate.installedPath,
7281
+ version,
7282
+ reason: `configured source must resolve to ${spec.source}`
7283
+ };
7284
+ return {
7285
+ state: "installed",
7286
+ source: candidate.source,
7287
+ installedPath: candidate.installedPath,
7288
+ version
7289
+ };
6991
7290
  }
6992
7291
 
6993
7292
  // src/cli/pi-install.ts
6994
7293
  import { spawnSync as spawnSync6 } from "child_process";
6995
- import { createHash as createHash4 } from "crypto";
7294
+ import { createHash as createHash5 } from "crypto";
6996
7295
  import {
6997
- copyFileSync as copyFileSync5,
6998
7296
  existsSync as existsSync14,
6999
- lstatSync as lstatSync3,
7000
- mkdirSync as mkdirSync7,
7297
+ lstatSync as lstatSync4,
7001
7298
  readdirSync as readdirSync3,
7002
- readFileSync as readFileSync11,
7003
- renameSync as renameSync6,
7004
- rmSync as rmSync6,
7005
- writeFileSync as writeFileSync7
7299
+ readFileSync as readFileSync13,
7300
+ realpathSync as realpathSync3,
7301
+ statSync as statSync3
7006
7302
  } from "fs";
7007
- import { dirname as dirname9, isAbsolute as isAbsolute3, join as join11 } from "path";
7008
- import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
7303
+ import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute4, join as join12, resolve as resolve8, sep as sep3 } from "path";
7304
+ import { fileURLToPath as fileURLToPath5 } from "url";
7305
+ import { lt } from "semver";
7306
+
7307
+ // src/cli/pi-git-source.ts
7308
+ import { resolve as resolve4, sep as sep2 } from "path";
7309
+ import hostedGitInfo from "hosted-git-info";
7310
+ function splitRef(url) {
7311
+ const scpLikeMatch = url.match(/^git@([^:]+):(.+)$/);
7312
+ if (scpLikeMatch) {
7313
+ const pathWithMaybeRef2 = scpLikeMatch[2] ?? "";
7314
+ const refSeparator2 = pathWithMaybeRef2.indexOf("@");
7315
+ if (refSeparator2 < 0) return { repo: url };
7316
+ const repoPath2 = pathWithMaybeRef2.slice(0, refSeparator2);
7317
+ const ref2 = pathWithMaybeRef2.slice(refSeparator2 + 1);
7318
+ if (!repoPath2 || !ref2) return { repo: url };
7319
+ return { repo: `git@${scpLikeMatch[1] ?? ""}:${repoPath2}`, ref: ref2 };
7320
+ }
7321
+ if (url.includes("://")) {
7322
+ try {
7323
+ const parsed = new URL(url);
7324
+ const pathWithMaybeRef2 = parsed.pathname.replace(/^\/+/, "");
7325
+ const refSeparator2 = pathWithMaybeRef2.indexOf("@");
7326
+ if (refSeparator2 < 0) return { repo: url };
7327
+ const repoPath2 = pathWithMaybeRef2.slice(0, refSeparator2);
7328
+ const ref2 = pathWithMaybeRef2.slice(refSeparator2 + 1);
7329
+ if (!repoPath2 || !ref2) return { repo: url };
7330
+ parsed.pathname = `/${repoPath2}`;
7331
+ return { repo: parsed.toString().replace(/\/$/, ""), ref: ref2 };
7332
+ } catch {
7333
+ return { repo: url };
7334
+ }
7335
+ }
7336
+ const slashIndex = url.indexOf("/");
7337
+ if (slashIndex < 0) return { repo: url };
7338
+ const host = url.slice(0, slashIndex);
7339
+ const pathWithMaybeRef = url.slice(slashIndex + 1);
7340
+ const refSeparator = pathWithMaybeRef.indexOf("@");
7341
+ if (refSeparator < 0) return { repo: url };
7342
+ const repoPath = pathWithMaybeRef.slice(0, refSeparator);
7343
+ const ref = pathWithMaybeRef.slice(refSeparator + 1);
7344
+ if (!repoPath || !ref) return { repo: url };
7345
+ return { repo: `${host}/${repoPath}`, ref };
7346
+ }
7347
+ function hasUnsafeGitInstallPart(value, allowSlash) {
7348
+ let decoded;
7349
+ try {
7350
+ decoded = decodeURIComponent(value);
7351
+ } catch {
7352
+ return true;
7353
+ }
7354
+ for (const candidate of [value, decoded]) {
7355
+ if (candidate.includes("\0") || candidate.includes("\\") || candidate.startsWith("/") || !allowSlash && candidate.includes("/") || candidate.split("/").includes(".."))
7356
+ return true;
7357
+ }
7358
+ return false;
7359
+ }
7360
+ function buildGitSource(args) {
7361
+ if (args.path.startsWith("/")) return null;
7362
+ const normalizedPath = args.path.replace(/\.git$/, "").replace(/^\/+/, "");
7363
+ if (!args.host || !normalizedPath || normalizedPath.split("/").length < 2 || hasUnsafeGitInstallPart(args.host, false) || hasUnsafeGitInstallPart(normalizedPath, true))
7364
+ return null;
7365
+ return {
7366
+ type: "git",
7367
+ repo: args.repo,
7368
+ host: args.host,
7369
+ path: normalizedPath,
7370
+ ref: args.ref,
7371
+ pinned: Boolean(args.ref)
7372
+ };
7373
+ }
7374
+ function parseGenericGitUrl(url) {
7375
+ const { repo: repoWithoutRef, ref } = splitRef(url);
7376
+ let repo = repoWithoutRef;
7377
+ let host = "";
7378
+ let path = "";
7379
+ const scpLikeMatch = repoWithoutRef.match(/^git@([^:]+):(.+)$/);
7380
+ if (scpLikeMatch) {
7381
+ host = scpLikeMatch[1] ?? "";
7382
+ path = scpLikeMatch[2] ?? "";
7383
+ } else if (repoWithoutRef.startsWith("https://") || repoWithoutRef.startsWith("http://") || repoWithoutRef.startsWith("ssh://") || repoWithoutRef.startsWith("git://")) {
7384
+ try {
7385
+ const parsed = new URL(repoWithoutRef);
7386
+ host = parsed.hostname;
7387
+ path = parsed.pathname.replace(/^\/+/, "");
7388
+ } catch {
7389
+ return null;
7390
+ }
7391
+ } else {
7392
+ const slashIndex = repoWithoutRef.indexOf("/");
7393
+ if (slashIndex < 0) return null;
7394
+ host = repoWithoutRef.slice(0, slashIndex);
7395
+ path = repoWithoutRef.slice(slashIndex + 1);
7396
+ if (!host.includes(".") && host !== "localhost") return null;
7397
+ repo = `https://${repoWithoutRef}`;
7398
+ }
7399
+ return buildGitSource({ repo, host, path, ref });
7400
+ }
7401
+ function parseGitUrl(source) {
7402
+ const trimmed = source.trim();
7403
+ const hasGitPrefix = trimmed.startsWith("git:");
7404
+ const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed;
7405
+ if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) return null;
7406
+ const split = splitRef(url);
7407
+ const hostedCandidates = [
7408
+ split.ref ? `${split.repo}#${split.ref}` : void 0,
7409
+ url
7410
+ ].filter((value) => Boolean(value));
7411
+ for (const candidate of hostedCandidates) {
7412
+ const info = hostedGitInfo.fromUrl(candidate);
7413
+ if (info) {
7414
+ if (split.ref && info.project?.includes("@")) continue;
7415
+ const useHttpsPrefix = !split.repo.startsWith("http://") && !split.repo.startsWith("https://") && !split.repo.startsWith("ssh://") && !split.repo.startsWith("git://") && !split.repo.startsWith("git@");
7416
+ return buildGitSource({
7417
+ repo: useHttpsPrefix ? `https://${split.repo}` : split.repo,
7418
+ host: info.domain || "",
7419
+ path: `${info.user}/${info.project}`,
7420
+ ref: info.committish || split.ref || void 0
7421
+ });
7422
+ }
7423
+ }
7424
+ const httpsCandidates = [
7425
+ split.ref ? `https://${split.repo}#${split.ref}` : void 0,
7426
+ `https://${url}`
7427
+ ].filter((value) => Boolean(value));
7428
+ for (const candidate of httpsCandidates) {
7429
+ const info = hostedGitInfo.fromUrl(candidate);
7430
+ if (info) {
7431
+ if (split.ref && info.project?.includes("@")) continue;
7432
+ return buildGitSource({
7433
+ repo: `https://${split.repo}`,
7434
+ host: info.domain || "",
7435
+ path: `${info.user}/${info.project}`,
7436
+ ref: info.committish || split.ref || void 0
7437
+ });
7438
+ }
7439
+ }
7440
+ return parseGenericGitUrl(url);
7441
+ }
7442
+ function projectGitPackagePath(source, projectRoot) {
7443
+ const trimmed = source.trim();
7444
+ if (!["npm:", "git:", "github:", "http:", "https:", "ssh:", "builtin:"].some(
7445
+ (prefix) => trimmed.startsWith(prefix)
7446
+ ))
7447
+ return void 0;
7448
+ const parsed = parseGitUrl(source);
7449
+ if (!parsed) return void 0;
7450
+ const installRoot = resolve4(projectRoot, "git");
7451
+ const installedPath = resolve4(installRoot, parsed.host, parsed.path);
7452
+ if (installedPath !== installRoot && !installedPath.startsWith(`${installRoot}${sep2}`))
7453
+ return void 0;
7454
+ return installedPath;
7455
+ }
7009
7456
 
7010
7457
  // src/cli/pi-migration.ts
7011
7458
  import {
@@ -7013,16 +7460,16 @@ import {
7013
7460
  existsSync as existsSync11,
7014
7461
  mkdirSync as mkdirSync5,
7015
7462
  readdirSync as readdirSync2,
7016
- readFileSync as readFileSync9,
7463
+ readFileSync as readFileSync10,
7017
7464
  renameSync as renameSync4,
7018
7465
  statSync as statSync2,
7019
7466
  writeFileSync as writeFileSync4
7020
7467
  } from "fs";
7021
- import { dirname as dirname6, join as join7 } from "path";
7468
+ import { dirname as dirname7, join as join8 } from "path";
7022
7469
  function files(root, current = root) {
7023
7470
  if (!existsSync11(root)) return [];
7024
7471
  return readdirSync2(current, { withFileTypes: true }).flatMap((entry) => {
7025
- const path = join7(current, entry.name);
7472
+ const path = join8(current, entry.name);
7026
7473
  return entry.isDirectory() ? files(root, path) : [path.slice(root.length + 1).replaceAll("\\", "/")];
7027
7474
  }).sort();
7028
7475
  }
@@ -7030,7 +7477,7 @@ function identicalTrees(left, right) {
7030
7477
  const leftFiles = files(left);
7031
7478
  const rightFiles = files(right);
7032
7479
  return leftFiles.length === rightFiles.length && leftFiles.every(
7033
- (name, index) => name === rightFiles[index] && readFileSync9(join7(left, name)).equals(readFileSync9(join7(right, name)))
7480
+ (name, index) => name === rightFiles[index] && readFileSync10(join8(left, name)).equals(readFileSync10(join8(right, name)))
7034
7481
  );
7035
7482
  }
7036
7483
  function removeRootBlock(content) {
@@ -7045,19 +7492,19 @@ function removeRootBlock(content) {
7045
7492
  function migrateLegacyPiResources(options) {
7046
7493
  const changed = [];
7047
7494
  const manualActions = [];
7048
- const appendPath = join7(options.piRoot, "APPEND_SYSTEM.md");
7495
+ const appendPath = join8(options.piRoot, "APPEND_SYSTEM.md");
7049
7496
  let nextRoot;
7050
7497
  try {
7051
7498
  if (existsSync11(appendPath))
7052
- nextRoot = removeRootBlock(readFileSync9(appendPath, "utf8"));
7499
+ nextRoot = removeRootBlock(readFileSync10(appendPath, "utf8"));
7053
7500
  } catch (error) {
7054
7501
  manualActions.push(error instanceof Error ? error.message : String(error));
7055
7502
  }
7056
7503
  const retire = [];
7057
7504
  for (const name of THOTH_OWNED_SKILL_NAMES) {
7058
- const source = join7(options.piRoot, "skills", name);
7505
+ const source = join8(options.piRoot, "skills", name);
7059
7506
  if (!existsSync11(source)) continue;
7060
- const canonical = join7(options.packageRoot, "skills", name);
7507
+ const canonical = join8(options.packageRoot, "skills", name);
7061
7508
  const backup = `${source}.thoth-agents-legacy.bak`;
7062
7509
  if (!statSync2(source).isDirectory() || !identicalTrees(source, canonical) || existsSync11(backup)) {
7063
7510
  manualActions.push(
@@ -7078,7 +7525,7 @@ function migrateLegacyPiResources(options) {
7078
7525
  };
7079
7526
  try {
7080
7527
  if (nextRoot !== void 0) {
7081
- mkdirSync5(dirname6(appendPath), { recursive: true });
7528
+ mkdirSync5(dirname7(appendPath), { recursive: true });
7082
7529
  copyFileSync4(appendPath, `${appendPath}.thoth-agents-legacy.bak`);
7083
7530
  const temporary = `${appendPath}.tmp-${process.pid}`;
7084
7531
  writeFileSync4(temporary, nextRoot);
@@ -7110,29 +7557,63 @@ function migrateLegacyPiResources(options) {
7110
7557
 
7111
7558
  // src/cli/pi-native-probe.ts
7112
7559
  import { spawnSync as spawnSync5 } from "child_process";
7113
- import { existsSync as existsSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
7560
+ import {
7561
+ existsSync as existsSync12,
7562
+ mkdtempSync as mkdtempSync2,
7563
+ readFileSync as readFileSync11,
7564
+ rmSync as rmSync5,
7565
+ writeFileSync as writeFileSync5
7566
+ } from "fs";
7114
7567
  import { tmpdir } from "os";
7115
- import { dirname as dirname7, join as join8, resolve as resolve3 } from "path";
7568
+ import { dirname as dirname8, join as join9, resolve as resolve5, win32 } from "path";
7569
+ function resolvePiWindowsCliFromShim(shimPath, shimContents) {
7570
+ const target = shimContents.match(
7571
+ /"([^"\r\n]*@earendil-works[\\/]+pi-coding-agent[\\/]+dist[\\/]+bundle[\\/]+cli\.js)"\s+%\*/i
7572
+ )?.[1];
7573
+ if (!target) return void 0;
7574
+ const expanded = target.replace(
7575
+ /%~dp0|%dp0%/gi,
7576
+ `${win32.dirname(shimPath)}\\`
7577
+ );
7578
+ return win32.isAbsolute(expanded) ? win32.normalize(expanded) : void 0;
7579
+ }
7580
+ function findPiWindowsCli() {
7581
+ const located = spawnSync5("where.exe", ["pi.cmd"], {
7582
+ windowsHide: true,
7583
+ encoding: "utf8"
7584
+ }).stdout?.split(/\r?\n/).find(Boolean);
7585
+ if (!located) return void 0;
7586
+ try {
7587
+ const cli = resolvePiWindowsCliFromShim(
7588
+ located,
7589
+ readFileSync11(located, "utf8")
7590
+ );
7591
+ if (cli && existsSync12(cli)) return cli;
7592
+ } catch {
7593
+ }
7594
+ const adjacentCli = join9(
7595
+ dirname8(located),
7596
+ "node_modules",
7597
+ "@earendil-works",
7598
+ "pi-coding-agent",
7599
+ "dist",
7600
+ "bundle",
7601
+ "cli.js"
7602
+ );
7603
+ return existsSync12(adjacentCli) ? adjacentCli : void 0;
7604
+ }
7116
7605
  function defaultExecutor(command, args, env) {
7117
7606
  let executable = command;
7118
7607
  let executableArgs = [...args];
7119
7608
  if (process.platform === "win32" && command === "pi") {
7120
- const located = spawnSync5("where.exe", ["pi.cmd"], { encoding: "utf8" }).stdout?.split(/\r?\n/).find(Boolean);
7121
- const cli = located ? join8(
7122
- dirname7(located),
7123
- "node_modules",
7124
- "@earendil-works",
7125
- "pi-coding-agent",
7126
- "dist",
7127
- "bundle",
7128
- "cli.js"
7129
- ) : void 0;
7130
- if (cli && existsSync12(cli)) {
7609
+ const cli = findPiWindowsCli();
7610
+ if (cli) {
7131
7611
  executable = process.execPath;
7132
7612
  executableArgs = [cli, ...args];
7133
7613
  }
7134
7614
  }
7135
7615
  const result = spawnSync5(executable, executableArgs, {
7616
+ windowsHide: true,
7136
7617
  encoding: "utf8",
7137
7618
  timeout: 3e4,
7138
7619
  env: { ...process.env, ...env }
@@ -7149,8 +7630,8 @@ function occurrences(text, marker) {
7149
7630
  }
7150
7631
  function observePiNativeRoot(options) {
7151
7632
  const ownsIsolatedRoot = options.piHome === void 0;
7152
- const isolatedRoot = options.piHome ?? mkdtempSync2(join8(tmpdir(), "thoth-pi-observer-"));
7153
- const observerPath = join8(
7633
+ const isolatedRoot = options.piHome ?? mkdtempSync2(join9(tmpdir(), "thoth-pi-observer-"));
7634
+ const observerPath = join9(
7154
7635
  isolatedRoot,
7155
7636
  `observer-${process.pid}-${Date.now()}.mjs`
7156
7637
  );
@@ -7159,7 +7640,7 @@ function observePiNativeRoot(options) {
7159
7640
  observerPath,
7160
7641
  `import { existsSync, readdirSync, readFileSync } from 'node:fs';
7161
7642
  import { isAbsolute, join, relative, resolve } from 'node:path';
7162
- import { createAssistantMessageEventStream } from '@earendil-works/pi-ai';
7643
+ import { createAssistantMessageEventStream, getCurrentSystemPrompt } from '@earendil-works/pi-ai';
7163
7644
  const manifestSha256=${JSON.stringify(options.manifestSha256)};
7164
7645
  const extensionSha256=${JSON.stringify(options.extensionSha256)};
7165
7646
  const packageRoot=${JSON.stringify(options.packageRoot)};
@@ -7204,12 +7685,16 @@ export default function observer(pi) {
7204
7685
  const stream=createAssistantMessageEventStream();
7205
7686
  queueMicrotask(async () => {
7206
7687
  try {
7207
- skills=discoveredSkills(context.systemPrompt);
7208
- const raw={systemPrompt:context.systemPrompt,messages:context.messages};
7688
+ const systemPrompt=getCurrentSystemPrompt(context.messages);
7689
+ skills=discoveredSkills(systemPrompt);
7690
+ const raw={systemPrompt};
7209
7691
  if (streamOptions?.onPayload) await streamOptions.onPayload(raw);
7210
7692
  const message={role:'assistant',content:[{type:'text',text:'probe'}],api:model.api,provider:model.provider,model:model.id,usage:{input:0,output:1,cacheRead:0,cacheWrite:0,totalTokens:1,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}},stopReason:'stop',timestamp:Date.now()};
7211
- stream.push({type:'done',message});
7212
- } catch (error) { stream.push({type:'error',error}); }
7693
+ stream.push({type:'start',partial:message});
7694
+ stream.push({type:'done',reason:'stop',message});
7695
+ } catch (error) {
7696
+ stream.push({type:'error',reason:'error',error:{role:'assistant',content:[],api:model.api,provider:model.provider,model:model.id,usage:{input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}},stopReason:'error',errorMessage:String(error),timestamp:Date.now()}});
7697
+ } finally { stream.end(); }
7213
7698
  });
7214
7699
  return stream;
7215
7700
  }
@@ -7225,7 +7710,7 @@ export default function observer(pi) {
7225
7710
  "--offline",
7226
7711
  "--no-extensions",
7227
7712
  "--extension",
7228
- resolve3(options.extensionPath),
7713
+ resolve5(options.extensionPath),
7229
7714
  "--extension",
7230
7715
  observerPath,
7231
7716
  "--provider",
@@ -7278,8 +7763,8 @@ ${result.stderr}`.split(/\r?\n/).flatMap((line) => {
7278
7763
  ) : void 0;
7279
7764
  const orchestratorChild = typeof record?.orchestratorChild === "boolean" ? record.orchestratorChild : void 0;
7280
7765
  const sessionStartCount = typeof record?.sessionStartCount === "number" ? record.sessionStartCount : void 0;
7281
- const valid = record?.manifestSha256 === options.manifestSha256 && record?.extensionSha256 === options.extensionSha256 && record?.hookCount === 1 && occurrences(prompt, PI_ROOT_START) === 1 && occurrences(prompt, PI_ROOT_END) === 1 && prompt.indexOf(PI_ROOT_START) < prompt.indexOf(PI_ROOT_END);
7282
- return valid ? {
7766
+ const valid2 = record?.manifestSha256 === options.manifestSha256 && record?.extensionSha256 === options.extensionSha256 && record?.hookCount === 1 && occurrences(prompt, PI_ROOT_START) === 1 && occurrences(prompt, PI_ROOT_END) === 1 && prompt.indexOf(PI_ROOT_START) < prompt.indexOf(PI_ROOT_END);
7767
+ return valid2 ? {
7283
7768
  state: "observed-at-install",
7284
7769
  basis: [
7285
7770
  "Real Pi final provider request contained exactly one receipt-bound root marker."
@@ -7298,8 +7783,8 @@ ${result.stderr}`.split(/\r?\n/).flatMap((line) => {
7298
7783
  };
7299
7784
  } finally {
7300
7785
  if (ownsIsolatedRoot)
7301
- rmSync4(isolatedRoot, { recursive: true, force: true });
7302
- else rmSync4(observerPath, { force: true });
7786
+ rmSync5(isolatedRoot, { recursive: true, force: true });
7787
+ else rmSync5(observerPath, { force: true });
7303
7788
  }
7304
7789
  }
7305
7790
 
@@ -7307,13 +7792,13 @@ ${result.stderr}`.split(/\r?\n/).flatMap((line) => {
7307
7792
  import {
7308
7793
  existsSync as existsSync13,
7309
7794
  mkdirSync as mkdirSync6,
7310
- readFileSync as readFileSync10,
7795
+ readFileSync as readFileSync12,
7311
7796
  renameSync as renameSync5,
7312
- rmSync as rmSync5,
7797
+ rmSync as rmSync6,
7313
7798
  writeFileSync as writeFileSync6
7314
7799
  } from "fs";
7315
7800
  import { homedir as homedir5 } from "os";
7316
- import { dirname as dirname8, isAbsolute as isAbsolute2, join as join9, normalize, resolve as resolve4 } from "path";
7801
+ import { dirname as dirname9, isAbsolute as isAbsolute3, join as join10, normalize, resolve as resolve6 } from "path";
7317
7802
  var keys = [
7318
7803
  "schemaVersion",
7319
7804
  "owner",
@@ -7328,16 +7813,16 @@ var keys = [
7328
7813
  var semver = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
7329
7814
  var digest = /^[0-9a-f]{64}$/;
7330
7815
  function piPackagePathsEqual(left, right) {
7331
- const normalizedLeft = normalize(resolve4(left));
7332
- const normalizedRight = normalize(resolve4(right));
7816
+ const normalizedLeft = normalize(resolve6(left));
7817
+ const normalizedRight = normalize(resolve6(right));
7333
7818
  return process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight;
7334
7819
  }
7335
7820
  function getPiPackageReceiptPath(options = {}) {
7336
7821
  const env = options.env ?? process.env;
7337
- const root = options.configRoot ?? env.XDG_CONFIG_HOME?.trim() ?? join9(options.homeDir ?? homedir5(), ".config");
7338
- return join9(root, "thoth-agents", "pi-package.json");
7822
+ const root = options.configRoot ?? env.XDG_CONFIG_HOME?.trim() ?? join10(options.homeDir ?? homedir5(), ".config");
7823
+ return join10(root, "thoth-agents", "pi-package.json");
7339
7824
  }
7340
- function parse(value) {
7825
+ function parse2(value) {
7341
7826
  if (!value || typeof value !== "object" || Array.isArray(value))
7342
7827
  throw new Error("Receipt must be a JSON object.");
7343
7828
  const record = value;
@@ -7349,7 +7834,7 @@ function parse(value) {
7349
7834
  throw new Error("Receipt version is invalid.");
7350
7835
  const npmSource = `npm:thoth-agents@${record.version}`;
7351
7836
  const npmIdentity = record.source === npmSource && record.installSource === npmSource;
7352
- const localIdentity = typeof record.source === "string" && record.source.length > 0 && !isAbsolute2(record.source) && !/^[a-z][a-z+.-]*:/i.test(record.source) && typeof record.installSource === "string" && isAbsolute2(record.installSource);
7837
+ const localIdentity = typeof record.source === "string" && record.source.length > 0 && !isAbsolute3(record.source) && !/^[a-z][a-z+.-]*:/i.test(record.source) && typeof record.installSource === "string" && isAbsolute3(record.installSource);
7353
7838
  if (!npmIdentity && !localIdentity)
7354
7839
  throw new Error("Receipt source is not canonical.");
7355
7840
  if (typeof record.manifestSha256 !== "string" || !digest.test(record.manifestSha256) || typeof record.extensionSha256 !== "string" || !digest.test(record.extensionSha256))
@@ -7363,7 +7848,7 @@ function readPiPackageReceipt(options = {}) {
7363
7848
  return {
7364
7849
  status: "valid",
7365
7850
  path,
7366
- receipt: parse(JSON.parse(readFileSync10(path, "utf8")))
7851
+ receipt: parse2(JSON.parse(readFileSync12(path, "utf8")))
7367
7852
  };
7368
7853
  } catch (error) {
7369
7854
  return {
@@ -7376,8 +7861,8 @@ function readPiPackageReceipt(options = {}) {
7376
7861
  function writePiPackageReceipt(receipt, options = {}) {
7377
7862
  const path = getPiPackageReceiptPath(options);
7378
7863
  try {
7379
- const validated = parse(receipt);
7380
- mkdirSync6(dirname8(path), { recursive: true });
7864
+ const validated = parse2(receipt);
7865
+ mkdirSync6(dirname9(path), { recursive: true });
7381
7866
  const temporary = `${path}.tmp-${process.pid}`;
7382
7867
  writeFileSync6(temporary, `${JSON.stringify(validated, null, 2)}
7383
7868
  `, {
@@ -7386,7 +7871,7 @@ function writePiPackageReceipt(receipt, options = {}) {
7386
7871
  try {
7387
7872
  renameSync5(temporary, path);
7388
7873
  } catch (error) {
7389
- rmSync5(temporary, { force: true });
7874
+ rmSync6(temporary, { force: true });
7390
7875
  throw error;
7391
7876
  }
7392
7877
  return { success: true, path };
@@ -7421,76 +7906,78 @@ function classifyPiPackageOwnership(input) {
7421
7906
 
7422
7907
  // src/cli/pi-paths.ts
7423
7908
  import { homedir as homedir6 } from "os";
7424
- import { join as join10, resolve as resolve5 } from "path";
7909
+ import { join as join11, resolve as resolve7 } from "path";
7425
7910
  function resolvePiPaths(options = {}) {
7426
7911
  const env = options.env ?? process.env;
7427
- const homeDir = resolve5(
7912
+ const homeDir = resolve7(
7428
7913
  options.homeDir ?? env.HOME ?? env.USERPROFILE ?? homedir6()
7429
7914
  );
7430
- const defaultPiRoot = join10(homeDir, ".pi", "agent");
7915
+ const defaultPiRoot = join11(homeDir, ".pi", "agent");
7431
7916
  const configured = env.PI_CODING_AGENT_DIR?.trim();
7432
- const piRoot = configured ? resolve5(configured) : defaultPiRoot;
7433
- const cwd = resolve5(options.cwd ?? process.cwd());
7434
- const configRoot = env.XDG_CONFIG_HOME?.trim() ? resolve5(env.XDG_CONFIG_HOME) : join10(homeDir, ".config");
7917
+ const piRoot = configured ? resolve7(configured) : defaultPiRoot;
7918
+ const cwd = resolve7(options.cwd ?? process.cwd());
7919
+ const configRoot = env.XDG_CONFIG_HOME?.trim() ? resolve7(env.XDG_CONFIG_HOME) : join11(homeDir, ".config");
7435
7920
  return {
7436
7921
  homeDir,
7437
7922
  piRoot,
7438
7923
  defaultPiRoot,
7439
7924
  customPiRoot: piRoot !== defaultPiRoot,
7440
7925
  skillsDestinationCompatible: piRoot === defaultPiRoot,
7441
- appendSystemPath: join10(piRoot, "APPEND_SYSTEM.md"),
7442
- agentsRoot: join10(piRoot, "agents"),
7443
- alternateAgentsRoot: join10(piRoot, "subagents"),
7444
- ownedSkillsRoot: join10(piRoot, "skills"),
7445
- mcpConfigPath: join10(configRoot, "mcp", "mcp.json"),
7926
+ appendSystemPath: join11(piRoot, "APPEND_SYSTEM.md"),
7927
+ agentsRoot: join11(piRoot, "agents"),
7928
+ alternateAgentsRoot: join11(piRoot, "subagents"),
7929
+ ownedSkillsRoot: join11(piRoot, "skills"),
7930
+ settingsPath: join11(piRoot, "settings.json"),
7931
+ subagentsConfigPath: join11(piRoot, "subagents.json"),
7932
+ mcpConfigPath: join11(configRoot, "mcp", "mcp.json"),
7446
7933
  projectAgentRoots: [
7447
- join10(cwd, ".pi", "agents"),
7448
- join10(cwd, ".pi", "subagents")
7934
+ join11(cwd, ".pi", "agents"),
7935
+ join11(cwd, ".pi", "subagents")
7449
7936
  ],
7450
- projectMcpPaths: [join10(cwd, ".mcp.json"), join10(cwd, ".pi", "mcp.json")]
7937
+ projectMcpPaths: [join11(cwd, ".mcp.json"), join11(cwd, ".pi", "mcp.json")]
7451
7938
  };
7452
7939
  }
7453
7940
 
7454
7941
  // src/cli/pi-install.ts
7455
- var PI_MINIMUM_VERSION = "0.84.4";
7942
+ var PI_MINIMUM_VERSION = "0.99.0";
7456
7943
  var PI_NODE_MINIMUM = "22.19.0";
7457
7944
  var PI_COMMAND_TIMEOUT_MS = 12e4;
7458
7945
  var PI_PACKAGE_SPECS = [
7459
7946
  {
7460
7947
  id: "delegation",
7461
- source: "npm:pi-subagents-j0k3r@1.5.9",
7462
- packageName: "pi-subagents-j0k3r",
7463
- version: "1.5.9"
7948
+ source: "npm:@thoth-agents/pi-subagents@>=0.1.0",
7949
+ packageName: "@thoth-agents/pi-subagents",
7950
+ version: "0.1.0"
7464
7951
  },
7465
7952
  {
7466
7953
  id: "context7",
7467
- source: "npm:@upstash/context7-pi@0.1.2",
7954
+ source: "npm:@upstash/context7-pi@>=0.1.2",
7468
7955
  packageName: "@upstash/context7-pi",
7469
7956
  version: "0.1.2"
7470
7957
  },
7471
7958
  {
7472
7959
  id: "web-access",
7473
- source: "npm:pi-web-access@0.27.0",
7960
+ source: "npm:pi-web-access@>=0.27.0",
7474
7961
  packageName: "pi-web-access",
7475
7962
  version: "0.27.0"
7476
7963
  },
7477
7964
  {
7478
7965
  id: "grep-adapter",
7479
- source: "npm:pi-mcp-adapter@2.32.1",
7966
+ source: "npm:pi-mcp-adapter@>=2.32.1",
7480
7967
  packageName: "pi-mcp-adapter",
7481
7968
  version: "2.32.1"
7482
7969
  },
7483
7970
  {
7484
7971
  id: "ask-user-question",
7485
- source: "npm:@juicesharp/rpiv-ask-user-question@2.9.0",
7486
- packageName: "@juicesharp/rpiv-ask-user-question",
7487
- version: "2.9.0"
7972
+ source: "npm:@thoth-agents/pi-questions-user@>=0.1.0",
7973
+ packageName: "@thoth-agents/pi-questions-user",
7974
+ version: "0.1.0"
7488
7975
  },
7489
7976
  {
7490
7977
  id: "todo",
7491
- source: "npm:@juicesharp/rpiv-todo@2.9.0",
7492
- packageName: "@juicesharp/rpiv-todo",
7493
- version: "2.9.0"
7978
+ source: "npm:@thoth-agents/pi-todo@>=0.1.0",
7979
+ packageName: "@thoth-agents/pi-todo",
7980
+ version: "0.1.0"
7494
7981
  }
7495
7982
  ];
7496
7983
  var PI_GREP_MCP_ENTRY = {
@@ -7502,7 +7989,7 @@ function getPiFirstPartySource(version) {
7502
7989
  return `npm:thoth-agents@${version}`;
7503
7990
  }
7504
7991
  function packageRootFor(options) {
7505
- const root = options.packageRoot ?? findPackageRoot(dirname9(fileURLToPath5(import.meta.url)));
7992
+ const root = options.packageRoot ?? findPackageRoot(dirname10(fileURLToPath5(import.meta.url)));
7506
7993
  if (!root)
7507
7994
  throw new Error(
7508
7995
  "Unable to locate the executing thoth-agents package root."
@@ -7511,63 +7998,56 @@ function packageRootFor(options) {
7511
7998
  }
7512
7999
  function packageVersionFor(root) {
7513
8000
  const value = JSON.parse(
7514
- readFileSync11(join11(root, "package.json"), "utf8")
8001
+ readFileSync13(join12(root, "package.json"), "utf8")
7515
8002
  );
7516
8003
  if (typeof value.version !== "string")
7517
8004
  throw new Error("Executing thoth-agents package version is invalid.");
7518
8005
  return value.version;
7519
8006
  }
7520
8007
  function sha2562(path) {
7521
- return createHash4("sha256").update(readFileSync11(path)).digest("hex");
8008
+ return createHash5("sha256").update(readFileSync13(path)).digest("hex");
7522
8009
  }
7523
8010
  function verifyPiFirstPartyPackage(input) {
7524
8011
  try {
7525
- const manifestPath = join11(input.packageRoot, "package.json");
7526
- const extensionPath = join11(input.packageRoot, "dist", "pi.js");
7527
- const assetsPath = join11(
8012
+ const manifestPath = join12(input.packageRoot, "package.json");
8013
+ const extensionPath = join12(input.packageRoot, "dist", "pi.js");
8014
+ const assetsPath = join12(
7528
8015
  input.packageRoot,
7529
8016
  "pi",
7530
8017
  ".thoth-agents-assets.json"
7531
8018
  );
7532
8019
  for (const path of [manifestPath, extensionPath, assetsPath])
7533
- if (!existsSync14(path) || !lstatSync3(path).isFile() || lstatSync3(path).isSymbolicLink())
8020
+ if (!existsSync14(path) || !lstatSync4(path).isFile() || lstatSync4(path).isSymbolicLink())
7534
8021
  throw new Error(
7535
8022
  `Required regular package asset is missing or symlinked: ${path}`
7536
8023
  );
7537
- const manifest = JSON.parse(readFileSync11(manifestPath, "utf8"));
8024
+ const manifest = JSON.parse(readFileSync13(manifestPath, "utf8"));
7538
8025
  if (manifest.name !== "thoth-agents" || manifest.version !== input.version || JSON.stringify(manifest.pi) !== JSON.stringify({ extensions: ["./dist/pi.js"], skills: ["./skills"] }))
7539
8026
  throw new Error(
7540
8027
  "Installed package identity or Pi manifest is inconsistent."
7541
8028
  );
7542
- const assets = JSON.parse(readFileSync11(assetsPath, "utf8"));
8029
+ const assets = JSON.parse(readFileSync13(assetsPath, "utf8"));
7543
8030
  const expectedAgentPaths = PI_SPECIALIST_ROLES.map(
7544
8031
  (role) => `agents/${piSpecialistName(role)}.md`
7545
8032
  );
7546
8033
  if (assets.schemaVersion !== 1 || assets.owner !== "thoth-agents" || !isRecord6(assets.files) || Object.keys(assets.files).sort().join("|") !== [...expectedAgentPaths].sort().join("|"))
7547
8034
  throw new Error("Installed Pi specialist provenance is invalid.");
7548
8035
  for (const relativePath of expectedAgentPaths) {
7549
- const path = join11(input.packageRoot, "pi", relativePath);
7550
- if (!existsSync14(path) || !lstatSync3(path).isFile() || lstatSync3(path).isSymbolicLink() || sha2562(path) !== assets.files[relativePath])
8036
+ const path = join12(input.packageRoot, "pi", relativePath);
8037
+ if (!existsSync14(path) || !lstatSync4(path).isFile() || lstatSync4(path).isSymbolicLink() || sha2562(path) !== assets.files[relativePath])
7551
8038
  throw new Error(
7552
8039
  `Installed Pi specialist asset is missing, symlinked, or stale: ${relativePath}`
7553
8040
  );
7554
8041
  }
7555
8042
  for (const skill of THOTH_OWNED_SKILL_NAMES) {
7556
- const path = join11(input.packageRoot, "skills", skill, "SKILL.md");
7557
- if (!existsSync14(path) || !lstatSync3(path).isFile() || lstatSync3(path).isSymbolicLink())
8043
+ const path = join12(input.packageRoot, "skills", skill, "SKILL.md");
8044
+ if (!existsSync14(path) || !lstatSync4(path).isFile() || lstatSync4(path).isSymbolicLink())
7558
8045
  throw new Error(
7559
8046
  `Installed Pi manifest skill is missing or symlinked: ${skill}`
7560
8047
  );
7561
8048
  }
7562
8049
  const manifestSha256 = sha2562(manifestPath);
7563
8050
  const extensionSha256 = sha2562(extensionPath);
7564
- const load = defaultCommandExecutor4("node", [
7565
- "--input-type=module",
7566
- "--eval",
7567
- `import(${JSON.stringify(pathToFileURL(extensionPath).href)}).then(m=>{if(typeof m.default!=="function")process.exit(2)})`
7568
- ]);
7569
- if (load.exitCode !== 0)
7570
- throw new Error(`Compiled Pi extension is not loadable: ${load.stderr}`);
7571
8051
  const observation = observePiNativeRoot({
7572
8052
  extensionPath,
7573
8053
  manifestSha256,
@@ -7600,11 +8080,264 @@ function isRecord6(value) {
7600
8080
  return typeof value === "object" && value !== null && !Array.isArray(value);
7601
8081
  }
7602
8082
  function readJsonObject(path) {
8083
+ assertSafePiManagedPath(path);
7603
8084
  if (!existsSync14(path)) return {};
7604
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
8085
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
7605
8086
  if (!isRecord6(parsed)) throw new Error(`${path} must contain a JSON object.`);
7606
8087
  return parsed;
7607
8088
  }
8089
+ function isPiIncumbentDelegationSource(source) {
8090
+ return /^npm:pi-subagents(?:-j0k3r)?(?:@|$)/.test(source);
8091
+ }
8092
+ function getPiExternalPackageSpecs(options = {}) {
8093
+ const runtimePackageRoot = options.runtimePackageRoot;
8094
+ if (!runtimePackageRoot) return PI_PACKAGE_SPECS;
8095
+ return PI_PACKAGE_SPECS.map(
8096
+ (spec) => spec.id === "delegation" ? { ...spec, source: runtimePackageRoot } : spec
8097
+ );
8098
+ }
8099
+ var PI_INCUMBENT_TODO_NAME = "@juicesharp/rpiv-todo";
8100
+ var PI_INCUMBENT_QUESTIONS_NAME = "@juicesharp/rpiv-ask-user-question";
8101
+ function piQuestionSourceName(source) {
8102
+ if (!source.startsWith("npm:")) return void 0;
8103
+ const spec = source.slice(4).trim();
8104
+ const alias = spec.indexOf("@npm:");
8105
+ const target = alias >= 0 ? spec.slice(alias + 5) : spec;
8106
+ return target.match(/^(@[^/@\s]+\/[^/@\s]+|[^/@\s]+)(?:@.+)?$/)?.[1];
8107
+ }
8108
+ function findPiIncumbentQuestions(packages) {
8109
+ return packages.filter(
8110
+ (candidate) => piQuestionSourceName(candidate.source) === PI_INCUMBENT_QUESTIONS_NAME || configuredPackageIdentity(candidate).packageName === PI_INCUMBENT_QUESTIONS_NAME
8111
+ );
8112
+ }
8113
+ function piQuestionIdentityBlockers(packages, plan) {
8114
+ return packages.flatMap((candidate) => {
8115
+ const manifest = configuredPackageManifest(candidate);
8116
+ if (typeof manifest?.name === "string" && manifest.name) return [];
8117
+ const npmSource = candidate.source.startsWith("npm:");
8118
+ const questionSource = /(?:^|[\\/:@?#._-])rpiv-ask-user-question(?:$|[\\/:@?#._-])/i.test(
8119
+ candidate.source
8120
+ );
8121
+ const unresolvedNpmLocation = npmSource && !candidate.installedPath;
8122
+ const unresolvedAlias = npmSource && candidate.source.includes("@npm:") && !piQuestionSourceName(candidate.source);
8123
+ const previewedQuestion = candidate.scope === "user" && plan?.items.some(
8124
+ ({ command }) => command?.command === "pi" && command.args[0] === "remove" && command.args[1] === candidate.source && command.args[2] === "--no-approve"
8125
+ );
8126
+ if (manifest && !previewedQuestion) return [];
8127
+ if (!questionSource && !unresolvedNpmLocation && !unresolvedAlias && !previewedQuestion)
8128
+ return [];
8129
+ const flags = candidate.scope === "project" ? "--local --approve" : "--no-approve";
8130
+ return [
8131
+ `Cannot verify configured Pi package ${candidate.source}: installed manifest identity is unavailable. Review its ownership and inspect its package.json using pi list; if it is ${PI_INCUMBENT_QUESTIONS_NAME}, manually run pi remove ${candidate.source} ${flags}. Verify removal, then rerun setup preview.`
8132
+ ];
8133
+ });
8134
+ }
8135
+ function piUnpreviewedQuestionRecovery(plan, packages) {
8136
+ const previewed = new Set(
8137
+ plan.items.flatMap(
8138
+ ({ command }) => command?.command === "pi" && command.args[0] === "remove" && command.args[2] === "--no-approve" ? [command.args[1]] : []
8139
+ )
8140
+ );
8141
+ const unpreviewed = packages.filter(
8142
+ ({ scope, source }) => scope === "user" && !previewed.has(source)
8143
+ );
8144
+ if (unpreviewed.length === 0) return void 0;
8145
+ return unpreviewed.map(
8146
+ ({ source }) => `Pi question migration blocked: incumbent ${source} was not previewed for removal. Review its ownership, then manually run: pi remove ${source} --no-approve. Verify with pi list, then rerun setup preview; no unpreviewed incumbent may be removed automatically.`
8147
+ ).join("\n");
8148
+ }
8149
+ function piProjectQuestionConflict(candidate) {
8150
+ const source = candidate.unmappedSource ? "<source>" : candidate.source;
8151
+ return `Incumbent Pi question package ${candidate.source} conflicts with @thoth-agents/pi-questions-user. ${candidate.identityLimitation ? `${candidate.identityLimitation} ` : ""}Review the project's ownership and trust first; --approve trusts project-local settings for this command only without saving a trust decision. ${candidate.unmappedSource ? "Find the matching project settings entry, then" : "Then"} run: pi remove ${source} --local --approve. Verify removal, then rerun setup.`;
8152
+ }
8153
+ function isPiIncumbentTodoSource(source) {
8154
+ return /^npm:@juicesharp\/rpiv-todo(?:@|$)/.test(source);
8155
+ }
8156
+ function piIncumbentTodoRecovery(source, scope, identityLimitation, unmappedSource = false) {
8157
+ const flags = scope === "project" ? "--local --approve" : "--no-approve";
8158
+ const review = scope === "project" ? "Review the project's ownership and trust first; --approve trusts project-local settings for this command only without saving a trust decision. Then run:" : "Review its ownership, then run:";
8159
+ const localRecovery = unmappedSource ? " Find the matching project settings entry; after reviewing its ownership, remove it with: pi remove <source> --local --approve." : isPiIncumbentTodoSource(source) ? "" : ` For the actual configured source, run: pi remove ${source} ${flags}.`;
8160
+ return `${identityLimitation ? `${identityLimitation} ` : ""}${review} pi remove npm:${PI_INCUMBENT_TODO_NAME} ${flags}.${localRecovery} Verify with pi list, then rerun setup.`;
8161
+ }
8162
+ function configuredPackageSources(settings) {
8163
+ if (!Array.isArray(settings.packages)) return [];
8164
+ return settings.packages.flatMap((entry) => {
8165
+ const source = typeof entry === "string" ? entry : isRecord6(entry) && typeof entry.source === "string" ? entry.source : void 0;
8166
+ return source ? [source] : [];
8167
+ });
8168
+ }
8169
+ function configuredPackageSource(settings, matches) {
8170
+ return configuredPackageSources(settings).find(matches);
8171
+ }
8172
+ function projectLocalPackagePath(source, projectRoot, homeDir) {
8173
+ let path = source.trim();
8174
+ if (process.platform === "win32" && !path.startsWith("//") && !path.includes("\\")) {
8175
+ const drivePath = path.match(
8176
+ /^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i
8177
+ );
8178
+ if (drivePath)
8179
+ path = `${drivePath[1]?.toUpperCase()}:\\${drivePath[2]?.replaceAll("/", "\\") ?? ""}`;
8180
+ }
8181
+ if (path === "~") path = homeDir;
8182
+ else if (path.startsWith("~/") || process.platform === "win32" && path.startsWith("~\\"))
8183
+ path = join12(homeDir, path.slice(2));
8184
+ else if (path.startsWith("file://")) path = fileURLToPath5(path);
8185
+ return resolve8(projectRoot, path);
8186
+ }
8187
+ function legacyPiNpmPackagePath(name, settings, execute) {
8188
+ try {
8189
+ const commandParts = settings.npmCommand === void 0 ? ["npm"] : settings.npmCommand;
8190
+ if (!Array.isArray(commandParts) || commandParts.length !== 1 || typeof commandParts[0] !== "string")
8191
+ return void 0;
8192
+ const command = commandParts[0];
8193
+ if (/[\0\r\n&|<>^%"'`$;!]/.test(command)) return void 0;
8194
+ const manager = basename3(command).replace(/\.(cmd|exe)$/i, "");
8195
+ if (!["npm", "pnpm", "bun"].includes(manager)) return void 0;
8196
+ if (/\s/.test(command) && !statSync3(command, { throwIfNoEntry: false })?.isFile())
8197
+ return void 0;
8198
+ const query = (args) => {
8199
+ const result = execute(command, args);
8200
+ if (result.error || result.exitCode !== 0)
8201
+ throw new Error("Package metadata unavailable");
8202
+ return (result.stdout || result.stderr).trim();
8203
+ };
8204
+ if (manager === "pnpm") {
8205
+ const entries = JSON.parse(query(["list", "-g", "--json"]));
8206
+ if (!Array.isArray(entries)) return void 0;
8207
+ for (const entry of entries) {
8208
+ const dependencies = isRecord6(entry) ? entry.dependencies : void 0;
8209
+ const dependency = isRecord6(dependencies) ? dependencies[name] : void 0;
8210
+ if (isRecord6(dependency) && typeof dependency.path === "string" && dependency.path)
8211
+ return dependency.path;
8212
+ }
8213
+ return void 0;
8214
+ }
8215
+ const root = manager === "bun" ? join12(
8216
+ dirname10(query(["pm", "bin", "-g"])),
8217
+ "install",
8218
+ "global",
8219
+ "node_modules"
8220
+ ) : query(["root", "-g"]);
8221
+ return root ? join12(root, name) : void 0;
8222
+ } catch {
8223
+ return void 0;
8224
+ }
8225
+ }
8226
+ function parsePiPackageSource(source, baseDir) {
8227
+ if (source.startsWith("npm:")) {
8228
+ const spec = source.slice(4).trim();
8229
+ const name = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/)?.[1] ?? spec;
8230
+ return { type: "npm", name };
8231
+ }
8232
+ const gitPath = projectGitPackagePath(source, baseDir);
8233
+ return gitPath ? { type: "git", path: gitPath } : { type: "local", path: source };
8234
+ }
8235
+ function resolvePiPackage(candidate, options, userSettings) {
8236
+ if (candidate.installedPath) return candidate;
8237
+ const paths = resolvePiPaths(options);
8238
+ const baseDir = candidate.scope === "user" ? paths.piRoot : join12(resolve8(options.cwd ?? process.cwd()), ".pi");
8239
+ const parsed = parsePiPackageSource(candidate.source, baseDir);
8240
+ const npmName = parsed.type === "npm" ? parsed.name : void 0;
8241
+ let path = npmName ? join12(baseDir, "npm", "node_modules", npmName) : parsed.type === "git" ? parsed.path : projectLocalPackagePath(candidate.source, baseDir, paths.homeDir);
8242
+ if (npmName && candidate.scope === "user" && !existsSync14(path))
8243
+ path = legacyPiNpmPackagePath(
8244
+ npmName,
8245
+ userSettings,
8246
+ options.commandExecutor ?? defaultCommandExecutor4
8247
+ ) ?? path;
8248
+ return {
8249
+ ...candidate,
8250
+ installedPath: existsSync14(path) ? path : candidate.installedPath
8251
+ };
8252
+ }
8253
+ function resolvePiConfiguredPackages(settings, scope, options) {
8254
+ return configuredPackageSources(settings).map(
8255
+ (source) => resolvePiPackage({ scope, source }, options, settings)
8256
+ );
8257
+ }
8258
+ function resolvePiListedPackages(output, options) {
8259
+ const packages = parsePiPackageList(output);
8260
+ const paths = resolvePiPaths(options);
8261
+ const settings = readJsonObject(paths.settingsPath);
8262
+ const projectRoot = join12(resolve8(options.cwd ?? process.cwd()), ".pi");
8263
+ let projectSettings;
8264
+ for (const { scope, source, installedPath } of packages) {
8265
+ const baseDir = scope === "user" ? paths.piRoot : projectRoot;
8266
+ if (parsePiPackageSource(source, baseDir).type !== "local") continue;
8267
+ let inventory = settings;
8268
+ if (scope === "project") {
8269
+ projectSettings ??= readJsonObject(join12(projectRoot, "settings.json"));
8270
+ inventory = projectSettings;
8271
+ }
8272
+ if (!configuredPackageSources(inventory).includes(source) && (Array.isArray(inventory.packages) || !installedPath || !isAbsolute4(installedPath)))
8273
+ throw new Error(
8274
+ `Pi package manifest identity cannot be verified: malformed pi list source ${source}. Rerun pi list --no-approve and inspect the configured sources before setup.`
8275
+ );
8276
+ }
8277
+ return packages.map(
8278
+ (candidate) => resolvePiPackage(candidate, options, settings)
8279
+ );
8280
+ }
8281
+ function readPiProjectPackages(options) {
8282
+ const projectRoot = join12(resolve8(options.cwd ?? process.cwd()), ".pi");
8283
+ return resolvePiConfiguredPackages(
8284
+ readJsonObject(join12(projectRoot, "settings.json")),
8285
+ "project",
8286
+ options
8287
+ );
8288
+ }
8289
+ function scanPiProjectInstalledIncumbents(options, configuredPackages) {
8290
+ const projectRoot = join12(resolve8(options.cwd ?? process.cwd()), ".pi");
8291
+ const incumbents = [];
8292
+ const visited = /* @__PURE__ */ new Set();
8293
+ const pathKey = (path) => process.platform === "win32" ? path.toLowerCase() : path;
8294
+ const knownPaths = new Set(
8295
+ configuredPackages.flatMap(
8296
+ ({ installedPath }) => installedPath ? [pathKey(realpathSync3(installedPath))] : []
8297
+ )
8298
+ );
8299
+ for (const root of [join12(projectRoot, "git"), join12(projectRoot, "npm")]) {
8300
+ if (!existsSync14(root)) continue;
8301
+ const rootKey = pathKey(realpathSync3(root));
8302
+ const pending = [root];
8303
+ while (pending.length > 0) {
8304
+ const path = pending.pop();
8305
+ if (!path || !statSync3(path, { throwIfNoEntry: false })?.isDirectory())
8306
+ continue;
8307
+ const key = pathKey(realpathSync3(path));
8308
+ if (visited.has(key)) continue;
8309
+ visited.add(key);
8310
+ const manifestPath = join12(path, "package.json");
8311
+ if (existsSync14(manifestPath) && statSync3(manifestPath).isFile()) {
8312
+ const candidate = {
8313
+ scope: "project",
8314
+ source: path,
8315
+ installedPath: path,
8316
+ unmappedSource: true
8317
+ };
8318
+ const identity = configuredPackageIdentity(candidate);
8319
+ if ((identity.packageName === PI_INCUMBENT_TODO_NAME || identity.packageName === PI_INCUMBENT_QUESTIONS_NAME) && !knownPaths.has(key))
8320
+ incumbents.push({
8321
+ ...candidate,
8322
+ ...identity,
8323
+ identityLimitation: `Installed manifest at ${path} identifies ${identity.packageName}, but no configured settings source maps to this directory.`
8324
+ });
8325
+ }
8326
+ if (key !== rootKey && !key.startsWith(`${rootKey}${sep3}`)) continue;
8327
+ for (const entry of readdirSync3(path, { withFileTypes: true }))
8328
+ if (entry.isDirectory() || entry.isSymbolicLink())
8329
+ pending.push(join12(path, entry.name));
8330
+ }
8331
+ }
8332
+ return incumbents;
8333
+ }
8334
+ function mergePiSubagentsConfig(current) {
8335
+ return {
8336
+ ...current,
8337
+ session_resources: "lean",
8338
+ enable_continue: false
8339
+ };
8340
+ }
7608
8341
  function mergePiGrepMcpConfig(current) {
7609
8342
  const rawServers = current.mcpServers;
7610
8343
  if (rawServers !== void 0 && !isRecord6(rawServers)) {
@@ -7645,8 +8378,8 @@ function findAgentConflicts(paths) {
7645
8378
  if (!existsSync14(root)) continue;
7646
8379
  for (const name of readdirSync3(root)) {
7647
8380
  if (!name.endsWith(".md")) continue;
7648
- const path = join11(root, name);
7649
- const content = readFileSync11(path, "utf8");
8381
+ const path = join12(root, name);
8382
+ const content = readFileSync13(path, "utf8");
7650
8383
  const role = frontmatterValue(content, "name");
7651
8384
  const filenameRole = name.slice(0, -".md".length).toLowerCase();
7652
8385
  const canonicalRole = role && canonical.has(role) ? role : canonical.has(filenameRole) ? filenameRole : void 0;
@@ -7678,8 +8411,57 @@ function buildPiSetupPlan(options = {}) {
7678
8411
  );
7679
8412
  }
7680
8413
  blockers.push(...findAgentConflicts(paths));
8414
+ if (options.runtimePackageRoot) {
8415
+ const runtimeRoot = options.runtimePackageRoot;
8416
+ const spec = getPiExternalPackageSpecs(options)[0];
8417
+ const sourceIsNormalizedAbsolutePath = isAbsolute4(runtimeRoot) && resolve8(runtimeRoot) === runtimeRoot;
8418
+ const inspection = sourceIsNormalizedAbsolutePath ? inspectPiExternalPackage(
8419
+ [{ scope: "user", source: runtimeRoot, installedPath: runtimeRoot }],
8420
+ spec
8421
+ ) : void 0;
8422
+ if (inspection?.state !== "installed") {
8423
+ blockers.push(
8424
+ `Local Pi delegation runtime root ${runtimeRoot} is invalid: ${inspection?.reason ?? "the source must be a normalized absolute path"}.`
8425
+ );
8426
+ }
8427
+ }
8428
+ let plannedQuestionRemovals = [];
8429
+ let subagentsConfigContent;
7681
8430
  let mcpContent;
7682
8431
  try {
8432
+ const userSettings = readJsonObject(paths.settingsPath);
8433
+ const incumbentSource = configuredPackageSource(
8434
+ userSettings,
8435
+ isPiIncumbentDelegationSource
8436
+ );
8437
+ const incumbentBlockers = [];
8438
+ if (incumbentSource)
8439
+ incumbentBlockers.push(
8440
+ `Incumbent delegation runtime ${incumbentSource} is configured in ${paths.settingsPath}. Review its ownership, then run: pi remove ${incumbentSource} --no-approve. Rerun setup after removing it; ${PI_PACKAGE_SPECS[0].source} cannot be loaded beside it.`
8441
+ );
8442
+ const incumbentTodoSource = configuredPackageSource(
8443
+ userSettings,
8444
+ isPiIncumbentTodoSource
8445
+ );
8446
+ if (incumbentTodoSource)
8447
+ incumbentBlockers.push(
8448
+ `Incumbent Pi task-list package ${incumbentTodoSource} is configured in ${paths.settingsPath} and conflicts with @thoth-agents/pi-todo. ${piIncumbentTodoRecovery(incumbentTodoSource, "user")}`
8449
+ );
8450
+ if (incumbentBlockers.length > 0)
8451
+ throw new Error(incumbentBlockers.join("\n"));
8452
+ const userPackages = resolvePiConfiguredPackages(
8453
+ userSettings,
8454
+ "user",
8455
+ options
8456
+ );
8457
+ blockers.push(...piQuestionIdentityBlockers(userPackages));
8458
+ plannedQuestionRemovals = findPiIncumbentQuestions(userPackages);
8459
+ subagentsConfigContent = `${JSON.stringify(
8460
+ mergePiSubagentsConfig(readJsonObject(paths.subagentsConfigPath)),
8461
+ null,
8462
+ 2
8463
+ )}
8464
+ `;
7683
8465
  mcpContent = `${JSON.stringify(
7684
8466
  mergePiGrepMcpConfig(readJsonObject(paths.mcpConfigPath)),
7685
8467
  null,
@@ -7689,6 +8471,40 @@ function buildPiSetupPlan(options = {}) {
7689
8471
  } catch (error) {
7690
8472
  blockers.push(error instanceof Error ? error.message : String(error));
7691
8473
  }
8474
+ const projectSettingsPath = join12(
8475
+ resolve8(options.cwd ?? process.cwd()),
8476
+ ".pi",
8477
+ "settings.json"
8478
+ );
8479
+ let projectPackages = [];
8480
+ try {
8481
+ projectPackages = readPiProjectPackages(options);
8482
+ } catch (error) {
8483
+ blockers.push(error instanceof Error ? error.message : String(error));
8484
+ }
8485
+ try {
8486
+ projectPackages.push(
8487
+ ...scanPiProjectInstalledIncumbents(options, projectPackages)
8488
+ );
8489
+ } catch (error) {
8490
+ blockers.push(error instanceof Error ? error.message : String(error));
8491
+ }
8492
+ blockers.push(...piQuestionIdentityBlockers(projectPackages));
8493
+ const projectIncumbentTodos = findPiIncumbentTodos(projectPackages);
8494
+ for (const incumbent of projectIncumbentTodos) {
8495
+ const location = incumbent.unmappedSource ? "was found in the project install roots" : `is configured in ${projectSettingsPath}`;
8496
+ const recovery = piIncumbentTodoRecovery(
8497
+ incumbent.source,
8498
+ "project",
8499
+ incumbent.identityLimitation,
8500
+ incumbent.unmappedSource
8501
+ );
8502
+ blockers.push(
8503
+ `Incumbent Pi task-list package ${incumbent.source} ${location} and conflicts with @thoth-agents/pi-todo. ${recovery}`
8504
+ );
8505
+ }
8506
+ for (const incumbent of findPiIncumbentQuestions(projectPackages))
8507
+ blockers.push(piProjectQuestionConflict(incumbent));
7692
8508
  for (const path of [...paths.projectAgentRoots, ...paths.projectMcpPaths]) {
7693
8509
  if (existsSync14(path))
7694
8510
  diagnostics.push(
@@ -7706,7 +8522,7 @@ function buildPiSetupPlan(options = {}) {
7706
8522
  const items = [
7707
8523
  {
7708
8524
  kind: "preflight",
7709
- description: "Verify Node.js >=22.19 and Pi >=0.84.4 before mutation",
8525
+ description: `Verify Node.js >=22.19 and Pi >=${PI_MINIMUM_VERSION} before mutation`,
7710
8526
  target: "node/pi runtime"
7711
8527
  },
7712
8528
  {
@@ -7718,12 +8534,26 @@ function buildPiSetupPlan(options = {}) {
7718
8534
  args: ["install", firstPartySource, "--no-approve"]
7719
8535
  }
7720
8536
  },
7721
- ...PI_PACKAGE_SPECS.map((pkg) => ({
8537
+ ...plannedQuestionRemovals.map(({ source }) => ({
8538
+ kind: "preflight",
8539
+ description: "Remove the conflicting user question provider through native Pi removal after root-package verification",
8540
+ target: source,
8541
+ command: { command: "pi", args: ["remove", source, "--no-approve"] }
8542
+ })),
8543
+ ...getPiExternalPackageSpecs(options).map((pkg) => ({
7722
8544
  kind: "package",
7723
8545
  description: `Install and verify Pi package ${pkg.source}`,
7724
8546
  target: pkg.source,
7725
8547
  command: { command: "pi", args: ["install", pkg.source, "--no-approve"] }
7726
8548
  })),
8549
+ ...subagentsConfigContent === void 0 ? [] : [
8550
+ {
8551
+ kind: "settings",
8552
+ description: "Configure global subagent lean resources and disable continuation",
8553
+ target: paths.subagentsConfigPath,
8554
+ content: subagentsConfigContent
8555
+ }
8556
+ ],
7727
8557
  ...mcpContent === void 0 ? [] : [
7728
8558
  {
7729
8559
  kind: "mcp",
@@ -7735,7 +8565,7 @@ function buildPiSetupPlan(options = {}) {
7735
8565
  ...rendered.artifacts.filter((artifact) => artifact.kind === "agent-config").map((artifact) => ({
7736
8566
  kind: "agent",
7737
8567
  description: artifact.description ?? `Install ${artifact.path}`,
7738
- target: join11(paths.piRoot, artifact.path),
8568
+ target: join12(paths.piRoot, artifact.path),
7739
8569
  content: String(artifact.content)
7740
8570
  }))
7741
8571
  ];
@@ -7746,10 +8576,12 @@ function buildPiSetupPlan(options = {}) {
7746
8576
  items,
7747
8577
  blockers,
7748
8578
  diagnostics,
8579
+ projectIncumbentTodos,
7749
8580
  disclaimers: [
7750
- "Pi extensions execute with the invoking user's system permissions; package pins and tool allowlists are not a security sandbox.",
8581
+ "Pi extensions execute with the invoking user's system permissions; package minimums and tool allowlists are not a security sandbox.",
7751
8582
  "Context7 and web access are native Pi extensions; only grep.app uses pi-mcp-adapter and directTools is intentionally omitted.",
7752
- "Project-local resources require Pi trust and may shadow global resources."
8583
+ "Project-local resources require Pi trust and may shadow global resources.",
8584
+ "Global subagents.json requests lean session resources; project-local subagents.json may override it, and full child resource mode is unsupported."
7753
8585
  ],
7754
8586
  options: { ...options, expectedVersion, packageRoot, firstPartySource }
7755
8587
  };
@@ -7800,33 +8632,108 @@ function parsePiPackageList(output) {
7800
8632
  }
7801
8633
  return packages;
7802
8634
  }
8635
+ function npmPackageName2(source) {
8636
+ if (!source.startsWith("npm:")) return void 0;
8637
+ const spec = source.slice("npm:".length);
8638
+ const separator = spec.lastIndexOf("@");
8639
+ return separator <= 0 ? spec || void 0 : spec.slice(0, separator);
8640
+ }
8641
+ var incumbentDelegationNames = /* @__PURE__ */ new Set([
8642
+ "pi-subagents",
8643
+ "pi-subagents-j0k3r"
8644
+ ]);
8645
+ function isPiLocalOrGitSource(source) {
8646
+ return /^(?:git\+|https?:\/\/|ssh:\/\/|git@|github:|file:|\.\.?[\\/]|[a-z]:[\\/]|\/)/i.test(
8647
+ source
8648
+ );
8649
+ }
8650
+ function sourceSuggestsIncumbentDelegation(source) {
8651
+ if (!isPiLocalOrGitSource(source)) return false;
8652
+ return /(?:^|[\\/:@?#._-])pi-subagents(?:-j0k3r)?(?:$|[\\/:@?#._-])/i.test(
8653
+ source
8654
+ );
8655
+ }
8656
+ function findPiIncumbentDelegation(packages) {
8657
+ for (const candidate of packages) {
8658
+ const sourceName = npmPackageName2(candidate.source);
8659
+ const { packageName: installedName } = configuredPackageIdentity(candidate);
8660
+ if (installedName && incumbentDelegationNames.has(installedName)) {
8661
+ return {
8662
+ candidate,
8663
+ classification: "confirmed",
8664
+ reason: `installed manifest identifies ${installedName}`
8665
+ };
8666
+ }
8667
+ if (sourceName && incumbentDelegationNames.has(sourceName)) {
8668
+ return {
8669
+ candidate,
8670
+ classification: "ambiguous",
8671
+ reason: installedName ? `source names ${sourceName}, but its installed manifest identifies ${installedName}` : `source names ${sourceName}, but its installed manifest is unavailable`
8672
+ };
8673
+ }
8674
+ if (!installedName && !sourceName && sourceSuggestsIncumbentDelegation(candidate.source)) {
8675
+ return {
8676
+ candidate,
8677
+ classification: "ambiguous",
8678
+ reason: "the source suggests an incumbent, but its manifest is unavailable"
8679
+ };
8680
+ }
8681
+ }
8682
+ return void 0;
8683
+ }
8684
+ function findPiIncumbentTodos(packages) {
8685
+ const incumbents = [];
8686
+ for (const candidate of packages) {
8687
+ const { packageName: installedName } = configuredPackageIdentity(candidate);
8688
+ const matches = installedName === PI_INCUMBENT_TODO_NAME || isPiIncumbentTodoSource(candidate.source) || !installedName && !npmPackageName2(candidate.source) && (isPiLocalOrGitSource(candidate.source) || candidate.scope === "project") && /(?:^|[\\/:@?#._-])rpiv-todo(?:$|[\\/:@?#._-])/i.test(
8689
+ candidate.source
8690
+ );
8691
+ if (!matches) continue;
8692
+ incumbents.push(
8693
+ candidate.scope === "project" && !installedName ? {
8694
+ ...candidate,
8695
+ identityLimitation: "Project package manifest identity is unavailable; detection relies on its source and read-only inspection cannot confirm its installed identity."
8696
+ } : candidate
8697
+ );
8698
+ }
8699
+ return incumbents;
8700
+ }
8701
+ function piIncumbentDelegationRecovery(incumbent) {
8702
+ const source = incumbent.candidate.source;
8703
+ const inspectManifest = incumbent.candidate.installedPath ? ` Inspect the installed manifest at ${incumbent.candidate.installedPath}.` : " Inspect the package manifest through Pi\u2019s package directory.";
8704
+ return incumbent.classification === "confirmed" ? `Review its ownership, then run: pi remove ${source} --no-approve. Rerun setup after verifying it is removed.` : `${incumbent.reason}.${inspectManifest} If it is an incumbent delegation runtime, run: pi remove ${source} --no-approve. Verify with pi list, then rerun setup.`;
8705
+ }
7803
8706
  function isThothPackageLocation(candidate, knownSource) {
7804
8707
  if (candidate.source === knownSource || /^npm:thoth-agents@/.test(candidate.source))
7805
8708
  return true;
7806
8709
  if (!candidate.installedPath) return false;
7807
8710
  try {
7808
8711
  const manifest = JSON.parse(
7809
- readFileSync11(join11(candidate.installedPath, "package.json"), "utf8")
8712
+ readFileSync13(join12(candidate.installedPath, "package.json"), "utf8")
7810
8713
  );
7811
8714
  return manifest.name === "thoth-agents";
7812
8715
  } catch {
7813
8716
  return false;
7814
8717
  }
7815
8718
  }
7816
- function configuredPackageIdentity(candidate) {
7817
- if (!candidate.installedPath) return { packageName: "", packageVersion: "" };
8719
+ function configuredPackageManifest(candidate) {
8720
+ if (!candidate.installedPath) return void 0;
7818
8721
  try {
7819
8722
  const manifest = JSON.parse(
7820
- readFileSync11(join11(candidate.installedPath, "package.json"), "utf8")
8723
+ readFileSync13(join12(candidate.installedPath, "package.json"), "utf8")
7821
8724
  );
7822
- return {
7823
- packageName: typeof manifest.name === "string" ? manifest.name : "",
7824
- packageVersion: typeof manifest.version === "string" ? manifest.version : ""
7825
- };
8725
+ return isRecord6(manifest) ? manifest : void 0;
7826
8726
  } catch {
7827
- return { packageName: "", packageVersion: "" };
8727
+ return void 0;
7828
8728
  }
7829
8729
  }
8730
+ function configuredPackageIdentity(candidate) {
8731
+ const manifest = configuredPackageManifest(candidate);
8732
+ return {
8733
+ packageName: typeof manifest?.name === "string" ? manifest.name : "",
8734
+ packageVersion: typeof manifest?.version === "string" ? manifest.version : ""
8735
+ };
8736
+ }
7830
8737
  function getPiFirstPartyPackages(packages, knownSource) {
7831
8738
  return packages.filter((candidate) => isThothPackageLocation(candidate, knownSource)).map((candidate) => ({
7832
8739
  ...candidate,
@@ -7834,30 +8741,25 @@ function getPiFirstPartyPackages(packages, knownSource) {
7834
8741
  }));
7835
8742
  }
7836
8743
  function matchesInstallSource(candidate, installSource) {
7837
- return isAbsolute3(installSource) ? candidate.installedPath !== void 0 && piPackagePathsEqual(candidate.installedPath, installSource) : candidate.source === installSource;
8744
+ return isAbsolute4(installSource) ? candidate.installedPath !== void 0 && piPackagePathsEqual(candidate.installedPath, installSource) : candidate.source === installSource;
7838
8745
  }
7839
8746
  function defaultCommandExecutor4(command, args) {
7840
8747
  let executable = command;
7841
8748
  let executableArgs = [...args];
7842
8749
  if (process.platform === "win32" && command === "pi") {
7843
- const located = spawnSync6("where.exe", ["pi.cmd"], { encoding: "utf8" }).stdout?.split(/\r?\n/).find(Boolean);
7844
- const cli = located ? join11(
7845
- dirname9(located),
7846
- "node_modules",
7847
- "@earendil-works",
7848
- "pi-coding-agent",
7849
- "dist",
7850
- "bundle",
7851
- "cli.js"
7852
- ) : void 0;
7853
- if (cli && existsSync14(cli)) {
8750
+ const cli = findPiWindowsCli();
8751
+ if (cli) {
7854
8752
  executable = process.execPath;
7855
8753
  executableArgs = [cli, ...args];
7856
8754
  }
7857
8755
  }
7858
8756
  const result = spawnSync6(executable, executableArgs, {
8757
+ windowsHide: true,
7859
8758
  encoding: "utf8",
7860
- timeout: PI_COMMAND_TIMEOUT_MS
8759
+ timeout: PI_COMMAND_TIMEOUT_MS,
8760
+ // Like the SDK's cross-spawn, support npm/pnpm's Windows command shims
8761
+ // for the offline metadata queries used by package resolution.
8762
+ shell: process.platform === "win32" && /^(npm|pnpm)(\.cmd)?$/i.test(basename3(executable))
7861
8763
  });
7862
8764
  return {
7863
8765
  exitCode: result.status,
@@ -7866,21 +8768,6 @@ function defaultCommandExecutor4(command, args) {
7866
8768
  error: result.error
7867
8769
  };
7868
8770
  }
7869
- function writePiManagedText(path, content) {
7870
- if (existsSync14(path) && readFileSync11(path, "utf8") === content) return false;
7871
- mkdirSync7(dirname9(path), { recursive: true });
7872
- const temporaryPath = `${path}.tmp`;
7873
- const backupPath = `${path}.bak`;
7874
- writeFileSync7(temporaryPath, content);
7875
- try {
7876
- if (existsSync14(path)) copyFileSync5(path, backupPath);
7877
- renameSync6(temporaryPath, path);
7878
- } catch (error) {
7879
- rmSync6(temporaryPath, { force: true });
7880
- throw error;
7881
- }
7882
- return true;
7883
- }
7884
8771
  function applyPiSetup(plan) {
7885
8772
  const diagnostics = [...plan.diagnostics];
7886
8773
  if (!plan.ready)
@@ -7899,7 +8786,14 @@ function applyPiSetup(plan) {
7899
8786
  const installedPackages = [];
7900
8787
  let receiptCommitted = false;
7901
8788
  let configuredPackageRoot;
8789
+ let manualRecovery;
7902
8790
  try {
8791
+ for (const path of [
8792
+ plan.paths.settingsPath,
8793
+ plan.paths.subagentsConfigPath,
8794
+ plan.paths.mcpConfigPath
8795
+ ])
8796
+ assertSafePiManagedPath(path);
7903
8797
  const node = execute("node", ["--version"]);
7904
8798
  if (node.exitCode !== 0 || !isVersionAtLeast(node.stdout.trim(), PI_NODE_MINIMUM))
7905
8799
  throw new Error(
@@ -7922,7 +8816,104 @@ function applyPiSetup(plan) {
7922
8816
  ...plan.options.receiptOptions
7923
8817
  };
7924
8818
  const receipt = readPiPackageReceipt(receiptOptions);
7925
- const configuredBefore = parsePiPackageList(before.stdout);
8819
+ let configuredBefore;
8820
+ try {
8821
+ configuredBefore = resolvePiListedPackages(before.stdout, plan.options);
8822
+ } catch (error) {
8823
+ return {
8824
+ success: false,
8825
+ changed,
8826
+ diagnostics,
8827
+ error: error instanceof Error ? error.message : String(error),
8828
+ failedStep: "preflight",
8829
+ installedPackages
8830
+ };
8831
+ }
8832
+ const incumbentDelegation = findPiIncumbentDelegation(configuredBefore);
8833
+ let configuredProjectPackages;
8834
+ try {
8835
+ configuredProjectPackages = readPiProjectPackages(plan.options);
8836
+ configuredProjectPackages.push(
8837
+ ...scanPiProjectInstalledIncumbents(
8838
+ plan.options,
8839
+ configuredProjectPackages
8840
+ )
8841
+ );
8842
+ } catch (error) {
8843
+ return {
8844
+ success: false,
8845
+ changed,
8846
+ diagnostics,
8847
+ error: error instanceof Error ? error.message : String(error),
8848
+ failedStep: "preflight",
8849
+ installedPackages
8850
+ };
8851
+ }
8852
+ const projectQuestions = findPiIncumbentQuestions([
8853
+ ...configuredBefore.filter(({ scope }) => scope === "project"),
8854
+ ...configuredProjectPackages
8855
+ ]);
8856
+ if (projectQuestions.length > 0) {
8857
+ const recovery = projectQuestions.map(piProjectQuestionConflict).join("\n");
8858
+ return {
8859
+ success: false,
8860
+ changed,
8861
+ diagnostics,
8862
+ error: recovery,
8863
+ failedStep: "preflight",
8864
+ installedPackages,
8865
+ manualRecovery: recovery
8866
+ };
8867
+ }
8868
+ const unpreviewedQuestions = piUnpreviewedQuestionRecovery(
8869
+ plan,
8870
+ findPiIncumbentQuestions(configuredBefore)
8871
+ );
8872
+ if (unpreviewedQuestions)
8873
+ return {
8874
+ success: false,
8875
+ changed,
8876
+ diagnostics,
8877
+ error: unpreviewedQuestions,
8878
+ failedStep: "preflight",
8879
+ installedPackages,
8880
+ manualRecovery: unpreviewedQuestions
8881
+ };
8882
+ const incumbentTodos = findPiIncumbentTodos([
8883
+ ...configuredBefore,
8884
+ ...configuredProjectPackages
8885
+ ]);
8886
+ if (incumbentDelegation || incumbentTodos.length > 0) {
8887
+ const incumbentBlockers = [];
8888
+ const recovery = [];
8889
+ if (incumbentDelegation) {
8890
+ incumbentBlockers.push(
8891
+ `Pi delegation runtime preflight blocked ${incumbentDelegation.candidate.source}: ${incumbentDelegation.reason}. Loading it beside ${PI_PACKAGE_SPECS[0].source} is unsupported.`
8892
+ );
8893
+ recovery.push(piIncumbentDelegationRecovery(incumbentDelegation));
8894
+ }
8895
+ for (const incumbentTodo of incumbentTodos) {
8896
+ const todoRecovery = piIncumbentTodoRecovery(
8897
+ incumbentTodo.source,
8898
+ incumbentTodo.scope,
8899
+ incumbentTodo.identityLimitation,
8900
+ incumbentTodo.unmappedSource
8901
+ );
8902
+ incumbentBlockers.push(
8903
+ `Pi task-list preflight blocked ${incumbentTodo.source}: it conflicts with @thoth-agents/pi-todo. ${todoRecovery}`
8904
+ );
8905
+ recovery.push(todoRecovery);
8906
+ }
8907
+ return {
8908
+ success: false,
8909
+ changed,
8910
+ diagnostics,
8911
+ error: incumbentBlockers.join("\n"),
8912
+ failedStep: "preflight",
8913
+ installedPackages,
8914
+ manualRecovery: `Manual recovery: ${recovery.join(" ")}`
8915
+ };
8916
+ }
7926
8917
  const knownReceiptSource = receipt.status === "valid" ? receipt.receipt.source : void 0;
7927
8918
  const firstPartyBefore = getPiFirstPartyPackages(
7928
8919
  configuredBefore,
@@ -7943,6 +8934,20 @@ function applyPiSetup(plan) {
7943
8934
  throw new Error(
7944
8935
  `First-party Pi package ownership conflict: ${ownership.reason ?? ownership.state}`
7945
8936
  );
8937
+ const identityRecovery = piQuestionIdentityBlockers(
8938
+ [...configuredBefore, ...configuredProjectPackages],
8939
+ plan
8940
+ ).join("\n");
8941
+ if (identityRecovery)
8942
+ return {
8943
+ success: false,
8944
+ changed,
8945
+ diagnostics,
8946
+ error: identityRecovery,
8947
+ failedStep: "preflight",
8948
+ installedPackages,
8949
+ manualRecovery: identityRecovery
8950
+ };
7946
8951
  const installed = execute("pi", ["install", desiredSource, "--no-approve"]);
7947
8952
  if (installed.exitCode !== 0)
7948
8953
  throw new Error(
@@ -7972,7 +8977,7 @@ function applyPiSetup(plan) {
7972
8977
  `Pi did not verify the exact installed package source ${desiredSource}.`
7973
8978
  );
7974
8979
  const resolvedPackageRoot = candidates[0]?.installedPath;
7975
- if (!resolvedPackageRoot || !isAbsolute3(resolvedPackageRoot) || !existsSync14(resolvedPackageRoot) || !lstatSync3(resolvedPackageRoot).isDirectory() || lstatSync3(resolvedPackageRoot).isSymbolicLink())
8980
+ if (!resolvedPackageRoot || !isAbsolute4(resolvedPackageRoot) || !existsSync14(resolvedPackageRoot) || !lstatSync4(resolvedPackageRoot).isDirectory() || lstatSync4(resolvedPackageRoot).isSymbolicLink())
7976
8981
  throw new Error(
7977
8982
  `Pi did not report one absolute regular installed directory for ${desiredSource}.`
7978
8983
  );
@@ -8050,25 +9055,130 @@ function applyPiSetup(plan) {
8050
9055
  if (!migration.success)
8051
9056
  throw new Error(migration.error ?? "Pi legacy migration failed.");
8052
9057
  changed.push(...migration.changed);
8053
- for (const pkg of PI_PACKAGE_SPECS) {
8054
- const result = execute("pi", ["install", pkg.source, "--no-approve"]);
8055
- if (result.exitCode !== 0)
9058
+ const questionListing = execute("pi", ["list", "--no-approve"]);
9059
+ if (questionListing.exitCode !== 0)
9060
+ throw new Error(
9061
+ `Unable to inspect conflicting Pi question packages: ${questionListing.stderr.trim() || "pi list unavailable"}`
9062
+ );
9063
+ const questionPackages = resolvePiListedPackages(
9064
+ questionListing.stdout,
9065
+ plan.options
9066
+ );
9067
+ const freshProjectQuestions = findPiIncumbentQuestions(
9068
+ questionPackages
9069
+ ).filter(({ scope }) => scope === "project");
9070
+ const questionRecovery = freshProjectQuestions.length > 0 ? freshProjectQuestions.map(piProjectQuestionConflict).join("\n") : piUnpreviewedQuestionRecovery(
9071
+ plan,
9072
+ findPiIncumbentQuestions(questionPackages)
9073
+ ) ?? (piQuestionIdentityBlockers(questionPackages, plan).join("\n") || void 0);
9074
+ if (questionRecovery) {
9075
+ manualRecovery = questionRecovery;
9076
+ throw new Error(questionRecovery);
9077
+ }
9078
+ const incumbentQuestions = findPiIncumbentQuestions(
9079
+ questionPackages
9080
+ ).filter(({ scope }) => scope === "user");
9081
+ for (const incumbent of incumbentQuestions) {
9082
+ const unpreviewed = piUnpreviewedQuestionRecovery(plan, [incumbent]);
9083
+ if (unpreviewed) {
9084
+ manualRecovery = unpreviewed;
9085
+ throw new Error(unpreviewed);
9086
+ }
9087
+ const removed = execute("pi", [
9088
+ "remove",
9089
+ incumbent.source,
9090
+ "--no-approve"
9091
+ ]);
9092
+ if (removed.exitCode !== 0)
9093
+ throw new Error(
9094
+ `Failed to remove conflicting Pi question package ${incumbent.source}: ${removed.stderr.trim() || "unknown Pi error"}`
9095
+ );
9096
+ }
9097
+ if (incumbentQuestions.length > 0) {
9098
+ const afterRemoval = execute("pi", ["list", "--no-approve"]);
9099
+ const remaining = resolvePiListedPackages(
9100
+ afterRemoval.stdout,
9101
+ plan.options
9102
+ );
9103
+ const identityRecovery2 = piQuestionIdentityBlockers(remaining, plan).join(
9104
+ "\n"
9105
+ );
9106
+ if (identityRecovery2) {
9107
+ manualRecovery = identityRecovery2;
9108
+ throw new Error(identityRecovery2);
9109
+ }
9110
+ if (afterRemoval.exitCode !== 0 || findPiIncumbentQuestions(remaining).some(
9111
+ ({ scope }) => scope === "user"
9112
+ ) || remaining.some(
9113
+ ({ scope, source }) => scope === "user" && incumbentQuestions.some((incumbent) => incumbent.source === source)
9114
+ ))
8056
9115
  throw new Error(
8057
- `Failed to install ${pkg.source}: ${result.stderr.trim() || "unknown Pi error"}`
9116
+ "Pi did not verify removal of the conflicting user question package; rerun pi list and remove it before retrying setup."
8058
9117
  );
9118
+ }
9119
+ for (const pkg of getPiExternalPackageSpecs(plan.options)) {
9120
+ const beforeInstall = execute("pi", ["list", "--no-approve"]);
9121
+ if (beforeInstall.exitCode !== 0)
9122
+ throw new Error(
9123
+ `Unable to inspect ${pkg.packageName} before installation: ${beforeInstall.stderr.trim() || "pi list unavailable"}`
9124
+ );
9125
+ const prior = inspectPiExternalPackage(
9126
+ parsePiPackageList(beforeInstall.stdout),
9127
+ pkg,
9128
+ false
9129
+ );
9130
+ const priorSourceMatches = prior.state === "installed" && prior.source !== void 0 && prior.installedPath !== void 0 && piExternalSourceMatches(
9131
+ {
9132
+ scope: "user",
9133
+ source: prior.source,
9134
+ installedPath: prior.installedPath
9135
+ },
9136
+ pkg.source
9137
+ );
9138
+ if (!priorSourceMatches) {
9139
+ const result = execute("pi", ["install", pkg.source, "--no-approve"]);
9140
+ if (result.exitCode !== 0)
9141
+ throw new Error(
9142
+ `Failed to install ${pkg.source}: ${result.stderr.trim() || "unknown Pi error"}`
9143
+ );
9144
+ }
8059
9145
  const listed = execute("pi", ["list", "--no-approve"]);
8060
- if (listed.exitCode !== 0 || !hasExactInstalledPiPackage(listed.stdout, pkg.source)) {
9146
+ const verified = listed.exitCode === 0 ? inspectPiExternalPackage(parsePiPackageList(listed.stdout), pkg) : {
9147
+ state: "drift",
9148
+ version: void 0,
9149
+ reason: listed.stderr.trim() || "pi list unavailable"
9150
+ };
9151
+ if (prior.state === "installed" && verified.version !== void 0 && lt(verified.version, prior.version)) {
9152
+ const restoreSource = prior.source ?? `npm:${pkg.packageName}@${prior.version}`;
9153
+ const restored = execute("pi", [
9154
+ "install",
9155
+ restoreSource,
9156
+ "--no-approve"
9157
+ ]);
9158
+ const restoredListing = execute("pi", ["list", "--no-approve"]);
9159
+ const restoredEvidence = restored.exitCode === 0 && restoredListing.exitCode === 0 ? inspectPiExternalPackage(
9160
+ parsePiPackageList(restoredListing.stdout),
9161
+ pkg,
9162
+ false
9163
+ ) : void 0;
9164
+ const recoverySucceeded = restoredEvidence?.state === "installed" && restoredEvidence.source === prior.source && restoredEvidence.version === prior.version;
9165
+ const recoveryGuidance = `Manual recovery: run pi install ${restoreSource} --no-approve, then verify with pi list and the installed package manifest.`;
9166
+ manualRecovery = recoveryGuidance;
8061
9167
  throw new Error(
8062
- `Pi did not verify the exact installed package source ${pkg.source}.`
9168
+ `Pi attempted to downgrade ${pkg.packageName} from ${prior.version} to ${verified.version}; exact-version recovery ${recoverySucceeded ? "verified" : "failed or unverifiable"}. ${recoveryGuidance}`
8063
9169
  );
8064
9170
  }
9171
+ if (verified.state !== "installed")
9172
+ throw new Error(`Pi did not verify ${pkg.source}: ${verified.reason}.`);
8065
9173
  installedPackages.push(pkg.source);
8066
9174
  }
8067
9175
  for (const item of plan.items.filter(
8068
- (candidate) => candidate.content !== void 0 && candidate.kind === "mcp"
9176
+ (candidate) => candidate.content !== void 0 && (candidate.kind === "settings" || candidate.kind === "mcp")
8069
9177
  )) {
8070
- if (writePiManagedText(item.target, item.content ?? ""))
8071
- changed.push(item.target);
9178
+ const content = item.target === plan.paths.subagentsConfigPath ? `${JSON.stringify(mergePiSubagentsConfig(readJsonObject(item.target)), null, 2)}
9179
+ ` : item.target === plan.paths.mcpConfigPath ? `${JSON.stringify(mergePiGrepMcpConfig(readJsonObject(item.target)), null, 2)}
9180
+ ` : item.content ?? "";
9181
+ if (writePiManagedText(item.target, content)) changed.push(item.target);
8072
9182
  }
8073
9183
  const resources = syncPiSpecialists({
8074
9184
  packageRoot: configuredPackageRoot,
@@ -8097,7 +9207,8 @@ function applyPiSetup(plan) {
8097
9207
  error: error instanceof Error ? error.message : String(error),
8098
9208
  failedStep: installedPackages.length < plan.items.filter((item) => item.kind === "package").length ? "package" : "managed-surface",
8099
9209
  installedPackages,
8100
- receiptCommitted
9210
+ receiptCommitted,
9211
+ manualRecovery
8101
9212
  };
8102
9213
  }
8103
9214
  }
@@ -8177,7 +9288,11 @@ function disclaimers() {
8177
9288
  },
8178
9289
  {
8179
9290
  code: "pi-runtime-owned",
8180
- message: "Pi and pi-subagents own execution, concurrency, task/history storage, trust, and lifecycle."
9291
+ message: `Pi and ${PI_PACKAGE_SPECS[0].packageName} own execution, concurrency, task/history storage, trust, and lifecycle.`
9292
+ },
9293
+ {
9294
+ code: "pi-lean-resources-global-only",
9295
+ message: "Global subagents.json requests lean session resources; project-local subagents.json may override it, and full child resource mode is unsupported."
8181
9296
  },
8182
9297
  {
8183
9298
  code: "pi-research-independent",
@@ -8237,6 +9352,12 @@ function runtimeTarget(provider, evidence) {
8237
9352
  description: evidence.basis.join("; ")
8238
9353
  };
8239
9354
  }
9355
+ function incumbentDelegationRecovery(incumbent) {
9356
+ return `Incumbent Pi delegation runtime ${incumbent.candidate.source} conflicts with ${PI_PACKAGE_SPECS[0].source}. ${piIncumbentDelegationRecovery(incumbent)}`;
9357
+ }
9358
+ function incumbentTodoRecovery(incumbent) {
9359
+ return `Incumbent Pi task-list package ${incumbent.source} conflicts with @thoth-agents/pi-todo. ${piIncumbentTodoRecovery(incumbent.source, incumbent.scope, incumbent.identityLimitation, incumbent.unmappedSource)}`;
9360
+ }
8240
9361
  function runtimeDiagnostic(provider, evidence) {
8241
9362
  if (evidence.state === "ready") return void 0;
8242
9363
  const code = `pi-${provider}-runtime-${evidence.state}`;
@@ -8252,12 +9373,13 @@ function statusFromPlan(plan, context2, evidence = {}) {
8252
9373
  kind: item.kind === "package" ? "package" : item.kind === "agent" ? "file" : item.kind === "mcp" ? "config" : "file",
8253
9374
  path: item.target,
8254
9375
  label: item.description,
8255
- state: item.kind === "package" ? "unknown" : existsSync15(item.target) ? item.content === void 0 || readFileSync12(item.target, "utf8") === item.content ? "installed" : "drift" : "missing",
9376
+ state: item.kind === "package" ? "unknown" : existsSync15(item.target) ? item.content === void 0 || readFileSync14(item.target, "utf8") === item.content ? "installed" : "drift" : "missing",
8256
9377
  expected: item.kind === "package" ? item.target : "attributable managed content",
8257
- description: item.kind === "package" ? "Exact installed-package evidence; this does not prove live tool availability." : void 0
9378
+ description: item.kind === "package" ? "Validated installed-package manifest evidence; this does not prove live tool availability." : void 0
8258
9379
  }));
8259
9380
  const execute = context2.piCommandExecutor ?? ((command, args) => {
8260
9381
  const result = spawnSync7(command, [...args], {
9382
+ windowsHide: true,
8261
9383
  encoding: "utf8",
8262
9384
  timeout: 5e3,
8263
9385
  env: {
@@ -8293,19 +9415,64 @@ function statusFromPlan(plan, context2, evidence = {}) {
8293
9415
  observed: pi.stdout.trim() || pi.stderr.trim() || "unavailable"
8294
9416
  }
8295
9417
  );
9418
+ const configuredPackages = packages.exitCode === 0 ? parsePiPackageList(packages.stdout) : [];
9419
+ const incumbentDelegation = findPiIncumbentDelegation(configuredPackages);
9420
+ const incumbentTodos = [
9421
+ ...findPiIncumbentTodos(configuredPackages),
9422
+ ...plan.projectIncumbentTodos ?? []
9423
+ ];
9424
+ const packageSpecs = getPiExternalPackageSpecs(plan.options);
8296
9425
  for (const target of targets.filter(
8297
- (candidate) => candidate.kind === "package" && candidate.path?.startsWith("npm:")
9426
+ (candidate) => candidate.kind === "package" && packageSpecs.some(({ source }) => source === candidate.path)
8298
9427
  )) {
8299
- const packageName = target.path?.replace(/^npm:/, "").replace(/@[^@]+$/, "");
8300
- target.state = packages.exitCode !== 0 ? "unknown" : hasExactInstalledPiPackage(packages.stdout, target.path ?? "") ? "installed" : packageName && packages.stdout.includes(packageName) ? "drift" : "missing";
8301
- target.observed = packages.exitCode === 0 ? target.state : packages.stderr.trim() || "pi list unavailable";
9428
+ const externalSpec = packageSpecs.find(
9429
+ ({ source }) => source === target.path
9430
+ );
9431
+ if (packages.exitCode !== 0) {
9432
+ target.state = "unknown";
9433
+ target.observed = packages.stderr.trim() || "pi list unavailable";
9434
+ } else if (externalSpec) {
9435
+ const inspected = inspectPiExternalPackage(
9436
+ configuredPackages,
9437
+ externalSpec,
9438
+ externalSpec.id !== "delegation" || plan.options.runtimePackageRoot !== void 0
9439
+ );
9440
+ target.state = inspected.state;
9441
+ target.observed = inspected.state === "installed" ? inspected.version : inspected.reason;
9442
+ } else {
9443
+ const packageName = target.path?.replace(/^npm:/, "").replace(/@[^@]+$/, "");
9444
+ target.state = hasExactInstalledPiPackage(
9445
+ packages.stdout,
9446
+ target.path ?? ""
9447
+ ) ? "installed" : packageName && packages.stdout.includes(packageName) ? "drift" : "missing";
9448
+ target.observed = target.state;
9449
+ }
8302
9450
  }
9451
+ if (incumbentDelegation)
9452
+ targets.push({
9453
+ kind: "package",
9454
+ path: incumbentDelegation.candidate.source,
9455
+ label: "Pi incumbent delegation runtime",
9456
+ state: "drift",
9457
+ expected: `not configured alongside ${PI_PACKAGE_SPECS[0].packageName}`,
9458
+ observed: incumbentDelegation.candidate.source,
9459
+ description: incumbentDelegationRecovery(incumbentDelegation)
9460
+ });
9461
+ for (const incumbentTodo of incumbentTodos)
9462
+ targets.push({
9463
+ kind: "package",
9464
+ path: incumbentTodo.source,
9465
+ label: "Pi incumbent task-list package",
9466
+ state: "drift",
9467
+ expected: "not configured alongside @thoth-agents/pi-todo",
9468
+ observed: incumbentTodo.source,
9469
+ description: incumbentTodoRecovery(incumbentTodo)
9470
+ });
8303
9471
  const receiptOptions = context2.installLedgerOptions ?? {
8304
9472
  env: context2.env,
8305
9473
  homeDir: context2.homeDir
8306
9474
  };
8307
9475
  const receipt = readPiPackageReceipt(receiptOptions);
8308
- const configuredPackages = packages.exitCode === 0 ? parsePiPackageList(packages.stdout) : [];
8309
9476
  const firstPartyPackages = getPiFirstPartyPackages(
8310
9477
  configuredPackages,
8311
9478
  receipt.status === "valid" ? receipt.receipt.source : void 0
@@ -8340,7 +9507,7 @@ function statusFromPlan(plan, context2, evidence = {}) {
8340
9507
  const configuredPackage = configuredCandidates[0];
8341
9508
  const configuredPath = configuredPackage?.installedPath;
8342
9509
  const localPathMatches = receipt.receipt.source === receipt.receipt.installSource || configuredPath !== void 0 && piPackagePathsEqual(configuredPath, receipt.receipt.installSource);
8343
- const configured = ownership.state === "owned-current" && configuredCandidates.length === 1 && configuredPath !== void 0 && isAbsolute4(configuredPath) && localPathMatches;
9510
+ const configured = ownership.state === "owned-current" && configuredCandidates.length === 1 && configuredPath !== void 0 && isAbsolute5(configuredPath) && localPathMatches;
8344
9511
  if (configured) validatedPackageRoot = configuredPath;
8345
9512
  targets.push({
8346
9513
  kind: "surface",
@@ -8350,11 +9517,11 @@ function statusFromPlan(plan, context2, evidence = {}) {
8350
9517
  expected: `${receipt.receipt.source} -> ${receipt.receipt.installSource}`,
8351
9518
  observed: configuredPackage ? `${configuredPackage.source} -> ${configuredPath ?? "unavailable"}` : "unavailable"
8352
9519
  });
8353
- const extensionPath = configuredPath ? join12(configuredPath, "dist", "pi.js") : void 0;
8354
- const manifestPath = configuredPath ? join12(configuredPath, "package.json") : void 0;
9520
+ const extensionPath = configuredPath ? join13(configuredPath, "dist", "pi.js") : void 0;
9521
+ const manifestPath = configuredPath ? join13(configuredPath, "package.json") : void 0;
8355
9522
  let receiptMatches = false;
8356
9523
  if (extensionPath && manifestPath && existsSync15(extensionPath) && existsSync15(manifestPath))
8357
- receiptMatches = createHash5("sha256").update(readFileSync12(extensionPath)).digest("hex") === receipt.receipt.extensionSha256 && createHash5("sha256").update(readFileSync12(manifestPath)).digest("hex") === receipt.receipt.manifestSha256;
9524
+ receiptMatches = createHash6("sha256").update(readFileSync14(extensionPath)).digest("hex") === receipt.receipt.extensionSha256 && createHash6("sha256").update(readFileSync14(manifestPath)).digest("hex") === receipt.receipt.manifestSha256;
8358
9525
  targets.push({
8359
9526
  kind: "surface",
8360
9527
  path: extensionPath,
@@ -8381,7 +9548,7 @@ function statusFromPlan(plan, context2, evidence = {}) {
8381
9548
  } : void 0);
8382
9549
  return {
8383
9550
  kind: "skill",
8384
- path: join12(skill.destinationPath, "SKILL.md"),
9551
+ path: join13(skill.destinationPath, "SKILL.md"),
8385
9552
  label: `Pi package-declared skill: ${skill.name}`,
8386
9553
  state: issue?.state ?? "installed",
8387
9554
  ...issue ? { observed: issue.message } : {}
@@ -8417,6 +9584,33 @@ function statusFromPlan(plan, context2, evidence = {}) {
8417
9584
  ...plan.blockers.map(
8418
9585
  (message) => warning3(message, "pi-preflight-blocked", "critical")
8419
9586
  ),
9587
+ ...incumbentDelegation && !plan.blockers.some(
9588
+ (message) => message.includes(incumbentDelegation.candidate.source)
9589
+ ) ? [
9590
+ warning3(
9591
+ incumbentDelegationRecovery(incumbentDelegation),
9592
+ "pi-incumbent-delegation-conflict",
9593
+ "critical"
9594
+ )
9595
+ ] : [],
9596
+ ...incumbentTodos.filter(
9597
+ (incumbentTodo) => !plan.blockers.some(
9598
+ (message) => message.includes(incumbentTodo.source) && message.includes(
9599
+ piIncumbentTodoRecovery(
9600
+ incumbentTodo.source,
9601
+ incumbentTodo.scope,
9602
+ incumbentTodo.identityLimitation,
9603
+ incumbentTodo.unmappedSource
9604
+ )
9605
+ )
9606
+ )
9607
+ ).map(
9608
+ (incumbentTodo) => warning3(
9609
+ incumbentTodoRecovery(incumbentTodo),
9610
+ "pi-incumbent-todo-conflict",
9611
+ "critical"
9612
+ )
9613
+ ),
8420
9614
  ...plan.diagnostics.map(
8421
9615
  (message) => warning3(message, "pi-resource-shadowing")
8422
9616
  ),
@@ -8457,6 +9651,7 @@ function contextPlan(context2, dryRun = true) {
8457
9651
  homeDir: context2.homeDir,
8458
9652
  commandExecutor: context2.piCommandExecutor,
8459
9653
  packageRoot: context2.packageRoot ?? (version.ok ? version.packageRoot : void 0),
9654
+ runtimePackageRoot: context2.runtimePackageRoot,
8460
9655
  expectedVersion: version.ok ? version.version : void 0,
8461
9656
  receiptOptions: context2.installLedgerOptions
8462
9657
  });
@@ -8476,7 +9671,7 @@ function configuredPackageRootFromStatus(status) {
8476
9671
  const configured = status.targets.find(
8477
9672
  ({ label }) => label === "First-party Pi configured source"
8478
9673
  );
8479
- return ownership?.observed === "owned-current" && configured?.state === "installed" && configured.path && isAbsolute4(configured.path) ? configured.path : void 0;
9674
+ return ownership?.observed === "owned-current" && configured?.state === "installed" && configured.path && isAbsolute5(configured.path) ? configured.path : void 0;
8480
9675
  }
8481
9676
  function piPlan(action, context2) {
8482
9677
  const setup = contextPlan(context2);
@@ -8502,7 +9697,7 @@ function piPlan(action, context2) {
8502
9697
  items.push({
8503
9698
  title: "Inspect five package-declared Pi skills",
8504
9699
  target: { kind: "skill", label: "Pi package-declared skills" },
8505
- preview: configuredPackageRoot ? getPiOwnedSkillEntries({ packageRoot: configuredPackageRoot }).map((entry) => join12(entry.destinationPath, "SKILL.md")).join(", ") : "unavailable until the first-party Pi package is receipt-validated"
9700
+ preview: configuredPackageRoot ? getPiOwnedSkillEntries({ packageRoot: configuredPackageRoot }).map((entry) => join13(entry.destinationPath, "SKILL.md")).join(", ") : "unavailable until the first-party Pi package is receipt-validated"
8506
9701
  });
8507
9702
  if (complete) {
8508
9703
  items.push(
@@ -8553,6 +9748,25 @@ function piPlan(action, context2) {
8553
9748
  observed: `${ownershipState}; remove the configured first-party package manually or restore its matching receipt before applying ${action}.`
8554
9749
  }
8555
9750
  ] : [];
9751
+ const incumbentDelegation = status.targets.find(
9752
+ ({ label }) => label === "Pi incumbent delegation runtime"
9753
+ );
9754
+ const delegationBlockers = incumbentDelegation ? [
9755
+ {
9756
+ kind: "package",
9757
+ path: incumbentDelegation.path,
9758
+ label: "Pi delegation runtime blocker",
9759
+ state: "drift",
9760
+ observed: incumbentDelegation.description ?? `${incumbentDelegation.observed} conflicts with ${PI_PACKAGE_SPECS[0].source}. Remove it manually before applying ${action}.`
9761
+ }
9762
+ ] : [];
9763
+ const todoBlockers = status.targets.filter(({ label }) => label === "Pi incumbent task-list package").map((incumbentTodo) => ({
9764
+ kind: "package",
9765
+ path: incumbentTodo.path,
9766
+ label: "Pi task-list package blocker",
9767
+ state: "drift",
9768
+ observed: incumbentTodo.description ?? `${incumbentTodo.observed} conflicts with @thoth-agents/pi-todo. Remove it manually before applying ${action}.`
9769
+ }));
8556
9770
  const syncSkillsUnavailable = action === "sync" && (!configuredPackageRoot || status.targets.some(
8557
9771
  ({ label, state }) => label?.startsWith("Pi package-declared skill:") && state !== "installed"
8558
9772
  ));
@@ -8571,7 +9785,7 @@ function piPlan(action, context2) {
8571
9785
  title: `${action === "install" ? "Install" : action === "update" ? "Update" : "Sync"} Pi`,
8572
9786
  summary: complete ? "Preview complete pinned Pi packages, owned resources, skills, provider setup, and final ledger commit." : "Preview only attributable Pi root, specialist, package-skill evidence, and grep.app surfaces.",
8573
9787
  dryRun: true,
8574
- canApply: setup.ready && (!complete || version.ok) && ownershipBlockers.length === 0 && syncSkillBlockers.length === 0,
9788
+ canApply: setup.ready && (!complete || version.ok) && ownershipBlockers.length === 0 && delegationBlockers.length === 0 && todoBlockers.length === 0 && syncSkillBlockers.length === 0,
8575
9789
  targets: status.targets,
8576
9790
  blockerTargets: [
8577
9791
  ...setup.blockers.map((message) => ({
@@ -8581,6 +9795,8 @@ function piPlan(action, context2) {
8581
9795
  observed: message
8582
9796
  })),
8583
9797
  ...ownershipBlockers,
9798
+ ...delegationBlockers,
9799
+ ...todoBlockers,
8584
9800
  ...syncSkillBlockers
8585
9801
  ],
8586
9802
  surfaces: setup.items.filter((item) => item.kind !== "preflight").map((item) => ({
@@ -8628,17 +9844,18 @@ function defaultPiModelRoles(context2 = { cwd: process.cwd() }) {
8628
9844
  (role) => isPiSpecialistRole(role.name)
8629
9845
  ).map((role) => {
8630
9846
  const path = plan.items.find(
8631
- (item) => item.kind === "agent" && basename3(item.target) === `${piSpecialistName(role.name)}.md`
9847
+ (item) => item.kind === "agent" && basename4(item.target) === `${piSpecialistName(role.name)}.md`
8632
9848
  )?.target;
8633
- const content = path && existsSync15(path) ? readFileSync12(path, "utf8") : "";
8634
- const model = /^model:\s*["']?([^"'\r\n]+)/m.exec(content)?.[1]?.trim();
9849
+ if (path && existsSync15(path)) {
9850
+ const configured = readPiModelConfig(dirname11(dirname11(path)), [
9851
+ role.name
9852
+ ]).roles[0];
9853
+ if (configured) return configured;
9854
+ }
8635
9855
  return {
8636
9856
  role: role.name,
8637
- model: model && model !== "default" ? model : "inherit",
8638
- effort: (() => {
8639
- const value = /^effort:\s*["']?([^"'\r\n]+)/m.exec(content)?.[1]?.trim();
8640
- return value && value !== "default" && value !== "inherit" ? { kind: "effort", value } : { kind: "inherit" };
8641
- })()
9857
+ model: "inherit",
9858
+ effort: { kind: "inherit" }
8642
9859
  };
8643
9860
  });
8644
9861
  }
@@ -8656,7 +9873,7 @@ function buildPiModelPlan(input, context2 = { cwd: process.cwd() }) {
8656
9873
  target: {
8657
9874
  kind: "file",
8658
9875
  path: specialist ? setup.items.find(
8659
- (item) => item.kind === "agent" && basename3(item.target) === `${specialist}.md`
9876
+ (item) => item.kind === "agent" && basename4(item.target) === `${specialist}.md`
8660
9877
  )?.target : void 0,
8661
9878
  label: `Pi ${role.role} specialist`
8662
9879
  },
@@ -8696,26 +9913,35 @@ function buildPiModelPlan(input, context2 = { cwd: process.cwd() }) {
8696
9913
  ],
8697
9914
  disclaimers: disclaimers()
8698
9915
  };
8699
- planSources.set(plan, { setup, context: context2, model: input });
9916
+ let modelSnapshot;
9917
+ if (!invalid && input.roles.length > 0) {
9918
+ const path = items[0]?.target.path;
9919
+ if (path && items.every((item) => item.target.path && existsSync15(item.target.path))) {
9920
+ try {
9921
+ modelSnapshot = readPiModelConfig(
9922
+ dirname11(dirname11(path)),
9923
+ input.roles.map(({ role }) => role)
9924
+ );
9925
+ } catch (error) {
9926
+ plan.canApply = false;
9927
+ plan.warnings.push(
9928
+ warning3(
9929
+ error instanceof Error ? error.message : String(error),
9930
+ "pi-model-config-invalid",
9931
+ "critical"
9932
+ )
9933
+ );
9934
+ }
9935
+ }
9936
+ }
9937
+ planSources.set(plan, {
9938
+ setup,
9939
+ context: context2,
9940
+ model: structuredClone(input),
9941
+ modelSnapshot
9942
+ });
8700
9943
  return plan;
8701
9944
  }
8702
- function replaceFrontmatterField(content, field, value) {
8703
- const newline = content.includes("\r\n") ? "\r\n" : "\n";
8704
- const lines = content.split(/\r?\n/);
8705
- if (lines[0]?.trim() !== "---") return content;
8706
- const end = lines.findIndex(
8707
- (line, index2) => index2 > 0 && line.trim() === "---"
8708
- );
8709
- if (end === -1) return content;
8710
- const prefix = `${field}:`;
8711
- const index = lines.slice(1, end).findIndex((line) => line.startsWith(prefix));
8712
- const absoluteIndex = index === -1 ? -1 : index + 1;
8713
- const nativeValue = value === void 0 || value === "inherit" ? "default" : value;
8714
- const replacement = `${field}: ${JSON.stringify(nativeValue)}`;
8715
- if (absoluteIndex === -1) lines.splice(1, 0, replacement);
8716
- else lines[absoluteIndex] = replacement;
8717
- return lines.join(newline);
8718
- }
8719
9945
  function applyPiPlan(plan) {
8720
9946
  const source = planSources.get(plan);
8721
9947
  const fileTargets = (paths) => paths.map((path) => ({ kind: "file", path, state: "installed" }));
@@ -8737,28 +9963,23 @@ function applyPiPlan(plan) {
8737
9963
  "Pi operation plan is not applicable or was not built in this process."
8738
9964
  );
8739
9965
  if (plan.action === "model-config" && source.model) {
8740
- const changedTargets = [];
8741
- for (const role of source.model.roles) {
8742
- const target = plan.items.find((item) => item.title.endsWith(role.role))?.target.path;
8743
- if (!target || !existsSync15(target))
8744
- return reject(
8745
- `Owned Pi specialist definition is missing: ${role.role}.`
8746
- );
8747
- let content = readFileSync12(target, "utf8");
8748
- content = replaceFrontmatterField(content, "model", role.model);
8749
- content = replaceFrontmatterField(
8750
- content,
8751
- "effort",
8752
- role.effort?.kind === "effort" ? role.effort.value : void 0
9966
+ if (!source.modelSnapshot)
9967
+ return reject(
9968
+ "Owned Pi specialist definitions were missing at preview. Reopen model configuration."
9969
+ );
9970
+ const snapshot = source.modelSnapshot;
9971
+ const result = savePiModelConfig(snapshot, source.model.roles);
9972
+ const changedTargets = fileTargets(
9973
+ result.changedRoles.map(
9974
+ (role) => join13(snapshot.piRoot, "agents", `${piSpecialistName(role)}.md`)
9975
+ )
9976
+ );
9977
+ source.modelSnapshot = result.snapshot;
9978
+ if (!result.success)
9979
+ return reject(
9980
+ result.error ?? "Pi model configuration failed.",
9981
+ changedTargets
8753
9982
  );
8754
- writePiManagedText(target, content);
8755
- changedTargets.push({
8756
- kind: "file",
8757
- path: target,
8758
- label: `Pi ${role.role} specialist`,
8759
- state: "installed"
8760
- });
8761
- }
8762
9983
  return {
8763
9984
  harness: "pi",
8764
9985
  action: plan.action,
@@ -8832,7 +10053,7 @@ function applyPiPlan(plan) {
8832
10053
  changedTargets: fileTargets(changed),
8833
10054
  diagnosticTargets: packageSkills.skills.map((skill) => ({
8834
10055
  kind: "skill",
8835
- path: join12(skill.destinationPath, "SKILL.md"),
10056
+ path: join13(skill.destinationPath, "SKILL.md"),
8836
10057
  label: `Pi package-declared skill: ${skill.name}`,
8837
10058
  state: "installed"
8838
10059
  })),
@@ -8919,7 +10140,7 @@ function applyPiPlan(plan) {
8919
10140
  ],
8920
10141
  diagnosticTargets: packageSkills.skills.map((skill) => ({
8921
10142
  kind: "skill",
8922
- path: join12(skill.destinationPath, "SKILL.md"),
10143
+ path: join13(skill.destinationPath, "SKILL.md"),
8923
10144
  label: `Pi package-declared skill: ${skill.name}`,
8924
10145
  state: "installed"
8925
10146
  })),
@@ -8956,14 +10177,14 @@ import { execFileSync } from "child_process";
8956
10177
  // src/cli/model-catalog/cache.ts
8957
10178
  import {
8958
10179
  existsSync as existsSync16,
8959
- mkdirSync as mkdirSync8,
8960
- readFileSync as readFileSync13,
8961
- renameSync as renameSync7,
10180
+ mkdirSync as mkdirSync7,
10181
+ readFileSync as readFileSync15,
10182
+ renameSync as renameSync6,
8962
10183
  unlinkSync,
8963
- writeFileSync as writeFileSync8
10184
+ writeFileSync as writeFileSync7
8964
10185
  } from "fs";
8965
10186
  import { homedir as homedir7 } from "os";
8966
- import { dirname as dirname10, posix, win32 } from "path";
10187
+ import { dirname as dirname12, posix, win32 as win322 } from "path";
8967
10188
  import { z as z2 } from "zod";
8968
10189
  var modelCatalogSourceSchema = z2.enum(["remote", "lkg", "manual"]);
8969
10190
  var cachedModelOptionSchema = z2.object({
@@ -8985,7 +10206,7 @@ function resolveModelsDevCachePath(options = {}) {
8985
10206
  const env = options.env ?? process.env;
8986
10207
  const home = options.homeDir ?? homedir7();
8987
10208
  if (platform === "win32") {
8988
- const path = win32;
10209
+ const path = win322;
8989
10210
  const base2 = env.LOCALAPPDATA ?? env.APPDATA ?? path.join(home, "AppData", "Local");
8990
10211
  return path.join(base2, "thoth-agents", "models-dev-v1.json");
8991
10212
  }
@@ -8995,7 +10216,7 @@ function resolveModelsDevCachePath(options = {}) {
8995
10216
  function readModelsDevCache(cachePath) {
8996
10217
  if (!existsSync16(cachePath)) return void 0;
8997
10218
  try {
8998
- const input = JSON.parse(readFileSync13(cachePath, "utf8"));
10219
+ const input = JSON.parse(readFileSync15(cachePath, "utf8"));
8999
10220
  const parsed = modelsDevCacheSchema.safeParse(input);
9000
10221
  return parsed.success ? parsed.data : void 0;
9001
10222
  } catch {
@@ -9007,15 +10228,15 @@ function writeModelsDevCacheAtomic(cachePath, cache) {
9007
10228
  if (!validated.success) {
9008
10229
  throw new Error(`Invalid models.dev cache: ${validated.error.message}`);
9009
10230
  }
9010
- mkdirSync8(dirname10(cachePath), { recursive: true });
10231
+ mkdirSync7(dirname12(cachePath), { recursive: true });
9011
10232
  const temporaryPath = `${cachePath}.${process.pid}.${Date.now()}.tmp`;
9012
10233
  try {
9013
- writeFileSync8(
10234
+ writeFileSync7(
9014
10235
  temporaryPath,
9015
10236
  `${JSON.stringify(validated.data, null, 2)}
9016
10237
  `
9017
10238
  );
9018
- renameSync7(temporaryPath, cachePath);
10239
+ renameSync6(temporaryPath, cachePath);
9019
10240
  } catch (error) {
9020
10241
  if (existsSync16(temporaryPath)) unlinkSync(temporaryPath);
9021
10242
  throw error;
@@ -9210,7 +10431,7 @@ var CODEX_DOCUMENTED_EFFORTS2 = /* @__PURE__ */ new Set([
9210
10431
  "ultra"
9211
10432
  ]);
9212
10433
  var CLAUDE_CODE_EFFORTS2 = ["low", "medium", "high", "xhigh", "max"];
9213
- var PI_EFFORTS2 = new Set(PI_EFFORT_VALUES);
10434
+ var PI_EFFORTS = new Set(PI_EFFORT_VALUES);
9214
10435
  function parseOpenCodeModels(output) {
9215
10436
  const seen = /* @__PURE__ */ new Set();
9216
10437
  const options = [];
@@ -9247,7 +10468,7 @@ function writableEfforts(harness, option) {
9247
10468
  );
9248
10469
  }
9249
10470
  if (harness === "pi") {
9250
- return option.efforts.filter((effort) => PI_EFFORTS2.has(effort));
10471
+ return option.efforts.filter((effort) => PI_EFFORTS.has(effort));
9251
10472
  }
9252
10473
  return option.efforts.filter(
9253
10474
  (effort) => resolveOpenCodeEffort({
@@ -9280,7 +10501,10 @@ function getNativeOpenCodeOptions() {
9280
10501
  try {
9281
10502
  const invocation = getOpenCodeModelsInvocation();
9282
10503
  return parseOpenCodeModels(
9283
- execFileSync(invocation.command, invocation.args, invocation.options)
10504
+ execFileSync(invocation.command, invocation.args, {
10505
+ ...invocation.options,
10506
+ windowsHide: true
10507
+ })
9284
10508
  );
9285
10509
  } catch {
9286
10510
  return [];
@@ -9340,7 +10564,38 @@ async function getModelOptions(harness) {
9340
10564
  }
9341
10565
 
9342
10566
  // src/cli/tui/operations.ts
9343
- import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
10567
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
10568
+
10569
+ // src/cli/model-defaults.ts
10570
+ function getShippedModelRoles(harness) {
10571
+ if (harness === "opencode") {
10572
+ return ALL_AGENT_NAMES.map((role) => ({
10573
+ role,
10574
+ model: getDefaultOpenCodeModel(role),
10575
+ effort: { kind: "effort", value: getDefaultOpenCodeVariant(role) }
10576
+ }));
10577
+ }
10578
+ return SUBAGENT_NAMES.map((role) => {
10579
+ if (harness === "claude") {
10580
+ return {
10581
+ role,
10582
+ model: CLAUDE_CODE_SUBAGENT_DEFAULT_MODELS[role],
10583
+ effort: {
10584
+ kind: "effort",
10585
+ value: CLAUDE_CODE_SUBAGENT_DEFAULT_EFFORTS[role]
10586
+ }
10587
+ };
10588
+ }
10589
+ const preset = CONFIRMED_OPENAI_SUBAGENT_PRESET[role];
10590
+ return {
10591
+ role,
10592
+ model: harness === "pi" ? `openai-codex/${preset.model}` : preset.model,
10593
+ effort: { kind: "effort", value: preset.effort }
10594
+ };
10595
+ });
10596
+ }
10597
+
10598
+ // src/cli/tui/operations.ts
9344
10599
  var context = { cwd: process.cwd() };
9345
10600
  var codexContext = { cwd: process.cwd() };
9346
10601
  var claudeCodeContext = {
@@ -9354,15 +10609,7 @@ var opencodeModelRoles = ALL_AGENT_NAMES.map(
9354
10609
  effort: { kind: "effort", value: getDefaultOpenCodeVariant(role) }
9355
10610
  })
9356
10611
  );
9357
- var codexModelRoles = CODEX_ROLE_NAMES.map(
9358
- (role) => ({
9359
- role,
9360
- model: "gpt-5.4-mini"
9361
- })
9362
- );
9363
- var codexDefaultModels = new Map(
9364
- codexModelRoles.map((role) => [role.role, role.model])
9365
- );
10612
+ var codexModelRoles = getShippedModelRoles("codex");
9366
10613
  function codexInstallConfig(source, dryRun) {
9367
10614
  return {
9368
10615
  dryRun,
@@ -9378,20 +10625,22 @@ function codexInstallConfig(source, dryRun) {
9378
10625
  function getCodexModelRoles(source = codexContext) {
9379
10626
  try {
9380
10627
  const plan = buildCodexSetupPlan(codexInstallConfig(source, true));
9381
- return CODEX_ROLE_NAMES.map((role) => {
9382
- const item = plan.items.find(
9383
- (candidate) => candidate.action === "write-role-toml" && candidate.role === role
9384
- );
9385
- const content = item && existsSync17(item.targetPath) ? readFileSync14(item.targetPath, "utf8") : item?.content;
9386
- const effort = content ? parseRoleTomlEffort(content) : void 0;
9387
- return {
9388
- role,
9389
- model: (content ? parseRoleTomlModel(content) : void 0) ?? codexDefaultModels.get(role) ?? "gpt-5.4-mini",
9390
- effort: effort ? { kind: "effort", value: effort } : { kind: "inherit" }
9391
- };
9392
- });
10628
+ return getShippedModelRoles("codex").map(
10629
+ ({ role, model: defaultModel }) => {
10630
+ const item = plan.items.find(
10631
+ (candidate) => candidate.action === "write-role-toml" && candidate.role === role
10632
+ );
10633
+ const content = item && existsSync17(item.targetPath) ? readFileSync16(item.targetPath, "utf8") : item?.content;
10634
+ const effort = content ? parseRoleTomlEffort(content) : void 0;
10635
+ return {
10636
+ role,
10637
+ model: (content ? parseRoleTomlModel(content) : void 0) ?? defaultModel,
10638
+ effort: effort ? { kind: "effort", value: effort } : { kind: "inherit" }
10639
+ };
10640
+ }
10641
+ );
9393
10642
  } catch {
9394
- return codexModelRoles.map((role) => ({ ...role }));
10643
+ return getShippedModelRoles("codex");
9395
10644
  }
9396
10645
  }
9397
10646
  function getClaudeCodeModelRoles(_source = claudeCodeContext) {
@@ -9432,20 +10681,32 @@ function getOpenCodeModelRoles() {
9432
10681
  };
9433
10682
  });
9434
10683
  }
9435
- function buildTuiModelPlan(harness, roles) {
10684
+ function buildTuiModelPlan(harness, roles, source = context) {
9436
10685
  if (harness === "opencode") {
9437
- return buildOpenCodeModelPlan({ harness, dryRun: true, roles }, context);
10686
+ return buildOpenCodeModelPlan({ harness, dryRun: true, roles }, source);
9438
10687
  }
9439
10688
  if (harness === "claude") {
9440
- return buildClaudeCodeModelPlan(
9441
- { harness, dryRun: true, roles },
9442
- claudeCodeContext
9443
- );
10689
+ return buildClaudeCodeModelPlan({ harness, dryRun: true, roles }, source);
9444
10690
  }
9445
10691
  if (harness === "pi") {
9446
- return buildPiModelPlan({ harness, dryRun: true, roles }, context);
10692
+ return buildPiModelPlan({ harness, dryRun: true, roles }, source);
9447
10693
  }
9448
- return buildCodexModelPlan({ harness, dryRun: true, roles }, codexContext);
10694
+ return buildCodexModelPlan({ harness, dryRun: true, roles }, source);
10695
+ }
10696
+ function buildRestoreModelPlan(harness, options, source = context) {
10697
+ const roles = getShippedModelRoles(harness).map((role) => {
10698
+ const catalogId = harness === "codex" ? `openai/${role.model}` : harness === "pi" ? role.model.replace(/^openai-codex\//, "openai/") : role.model;
10699
+ const option = options.find((candidate) => candidate.id === role.model) ?? options.find((candidate) => candidate.catalogId === catalogId);
10700
+ return {
10701
+ ...role,
10702
+ ...option ? {
10703
+ provider: option.provider,
10704
+ catalogId: option.catalogId,
10705
+ availableEfforts: [...option.efforts]
10706
+ } : {}
10707
+ };
10708
+ });
10709
+ return buildTuiModelPlan(harness, roles, source);
9449
10710
  }
9450
10711
  var defaultTuiOperations = {
9451
10712
  status(harness, evidence) {
@@ -9501,6 +10762,9 @@ var defaultTuiOperations = {
9501
10762
  modelPlan(harness, roles) {
9502
10763
  return buildTuiModelPlan(harness, roles);
9503
10764
  },
10765
+ restoreModelPlan(harness, options) {
10766
+ return buildRestoreModelPlan(harness, options);
10767
+ },
9504
10768
  apply(plan) {
9505
10769
  if (plan.harness === "opencode") return applyOpenCodePlan(plan);
9506
10770
  if (plan.harness === "claude") return applyClaudeCodePlan(plan);
@@ -9566,3 +10830,9 @@ export {
9566
10830
  getOpenCodeModelRoles,
9567
10831
  defaultTuiOperations
9568
10832
  };
10833
+ /*! @license
10834
+ * Portions ported from @earendil-works/pi-coding-agent 1.0.2
10835
+ * (dist/utils/git.js:parseGitUrl and its helpers).
10836
+ * Copyright (c) 2025 Mario Zechner. MIT License; see the third-party notice
10837
+ * in this package's LICENSE file.
10838
+ */