@xingular/harness 1.2.0 → 1.3.1-unstable.1

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 (186) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +26 -17
  3. package/builtin/skills/plugin-authoring/SKILL.md +34 -0
  4. package/dist/cli.js +7362 -6239
  5. package/dist/sdk/agents/builtin.js +3 -3
  6. package/dist/sdk/agents/planResearch.d.ts +23 -1
  7. package/dist/sdk/agents/planResearch.js +71 -8
  8. package/dist/sdk/agents/profiles.d.ts +16 -4
  9. package/dist/sdk/agents/profiles.js +75 -12
  10. package/dist/sdk/annotations/bridge.d.ts +2 -0
  11. package/dist/sdk/annotations/bridge.js +4 -0
  12. package/dist/sdk/annotations/capture.d.ts +44 -2
  13. package/dist/sdk/annotations/capture.js +104 -3
  14. package/dist/sdk/annotations/delivery.d.ts +24 -17
  15. package/dist/sdk/annotations/delivery.js +82 -49
  16. package/dist/sdk/annotations/inspect.js +7 -6
  17. package/dist/sdk/annotations/pageScript.js +4 -4
  18. package/dist/sdk/annotations/part.d.ts +0 -2
  19. package/dist/sdk/annotations/part.js +4 -33
  20. package/dist/sdk/annotations/queue.d.ts +2 -2
  21. package/dist/sdk/annotations/queue.js +12 -7
  22. package/dist/sdk/annotations/serverMethods.js +11 -7
  23. package/dist/sdk/annotations/store.d.ts +13 -21
  24. package/dist/sdk/annotations/store.js +47 -37
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/commands/list.js +2 -0
  27. package/dist/sdk/commands/registry.d.ts +11 -3
  28. package/dist/sdk/commands/registry.js +3 -2
  29. package/dist/sdk/core/links.d.ts +51 -0
  30. package/dist/sdk/core/links.js +155 -0
  31. package/dist/sdk/core/shell.d.ts +20 -0
  32. package/dist/sdk/core/shell.js +73 -0
  33. package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
  34. package/dist/sdk/core/workspaceBashTool.js +16 -2
  35. package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
  36. package/dist/sdk/core/workspaceSandbox.js +52 -12
  37. package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
  38. package/dist/sdk/core/workspaceWriteOperations.js +26 -8
  39. package/dist/sdk/doctor/checks.js +71 -12
  40. package/dist/sdk/harness.d.ts +5 -0
  41. package/dist/sdk/harness.js +11 -6
  42. package/dist/sdk/hub/hub.js +5 -3
  43. package/dist/sdk/index.d.ts +3 -2
  44. package/dist/sdk/index.js +2 -1
  45. package/dist/sdk/plugins/builtin.js +14 -9
  46. package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
  47. package/dist/sdk/plugins/builtinRegistry.js +9 -3
  48. package/dist/sdk/plugins/discover.d.ts +7 -1
  49. package/dist/sdk/plugins/discover.js +41 -2
  50. package/dist/sdk/plugins/identity.js +1 -1
  51. package/dist/sdk/plugins/install.d.ts +6 -2
  52. package/dist/sdk/plugins/install.js +29 -5
  53. package/dist/sdk/plugins/inventory.js +3 -3
  54. package/dist/sdk/plugins/manifest.d.ts +2 -0
  55. package/dist/sdk/plugins/manifest.js +21 -4
  56. package/dist/sdk/plugins/plugins.d.ts +2 -0
  57. package/dist/sdk/plugins/plugins.js +1 -0
  58. package/dist/sdk/plugins/proposals.d.ts +17 -0
  59. package/dist/sdk/plugins/proposals.js +147 -0
  60. package/dist/sdk/plugins/types.d.ts +1 -1
  61. package/dist/sdk/plugins/types.js +1 -1
  62. package/dist/sdk/prompt/compile.d.ts +13 -4
  63. package/dist/sdk/prompt/compile.js +46 -20
  64. package/dist/sdk/prompt/standard.d.ts +1 -1
  65. package/dist/sdk/prompt/standard.js +1 -1
  66. package/dist/sdk/server/methods.js +14 -2
  67. package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
  68. package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
  69. package/dist/sdk/sessions/continuation.d.ts +4 -2
  70. package/dist/sdk/sessions/continuation.js +7 -6
  71. package/dist/sdk/sessions/contracts.d.ts +22 -4
  72. package/dist/sdk/sessions/contracts.js +12 -7
  73. package/dist/sdk/sessions/planMode.js +2 -2
  74. package/dist/sdk/sessions/runStore.js +3 -1
  75. package/dist/sdk/sessions/runner.js +2 -2
  76. package/dist/sdk/sessions/sessionState.d.ts +13 -0
  77. package/dist/sdk/sessions/sessionState.js +31 -0
  78. package/dist/sdk/sessions/sessions.d.ts +7 -0
  79. package/dist/sdk/sessions/sessions.js +73 -15
  80. package/dist/sdk/sessions/subagentService.d.ts +15 -0
  81. package/dist/sdk/sessions/subagentService.js +195 -55
  82. package/dist/sdk/sessions/subagents.d.ts +10 -1
  83. package/dist/sdk/sessions/subagents.js +4 -0
  84. package/dist/sdk/sessions/workSlice.d.ts +8 -5
  85. package/dist/sdk/sessions/workSlice.js +77 -45
  86. package/dist/sdk/terminal/controller.d.ts +20 -0
  87. package/dist/sdk/terminal/controller.js +43 -18
  88. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  89. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  90. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  91. package/dist/sdk/terminal/opentui/model.js +482 -0
  92. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  93. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  94. package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
  95. package/dist/sdk/terminal/opentui/theme.js +2 -0
  96. package/dist/sdk/terminal/piExtension.js +98 -15
  97. package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
  98. package/dist/sdk/terminal/subagentMonitor.js +29 -8
  99. package/dist/sdk/terminal/terminals.d.ts +16 -0
  100. package/dist/sdk/terminal/terminals.js +130 -26
  101. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  102. package/dist/sdk/terminal/trajectory.js +8 -0
  103. package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
  104. package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
  105. package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
  106. package/dist/sdk/terminal/tui/annotateSend.js +40 -0
  107. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  108. package/dist/sdk/terminal/tui/chrome.js +13 -0
  109. package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
  110. package/dist/sdk/terminal/tui/commandPages.js +16 -11
  111. package/dist/sdk/terminal/tui/commands.d.ts +4 -0
  112. package/dist/sdk/terminal/tui/commands.js +12 -2
  113. package/dist/sdk/terminal/tui/dialogs.js +1 -0
  114. package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
  115. package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
  116. package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
  117. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
  118. package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
  119. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  120. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  121. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  122. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +14 -1
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
  124. package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
  125. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
  126. package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
  127. package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
  128. package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
  129. package/dist/sdk/tools/defaultSkills.d.ts +4 -0
  130. package/dist/sdk/tools/defaultSkills.js +20 -0
  131. package/dist/sdk/tools/definitions.d.ts +3 -1
  132. package/dist/sdk/tools/definitions.js +17 -1
  133. package/dist/sdk/tools/dispatch.js +12 -0
  134. package/dist/sdk/tools/harnessTools.js +1 -1
  135. package/dist/sdk/tools/sessionName.d.ts +9 -0
  136. package/dist/sdk/tools/sessionName.js +58 -11
  137. package/dist/sdk/tools/spawnSubagent.js +9 -7
  138. package/dist/sdk/tools/todo.d.ts +15 -0
  139. package/dist/sdk/tools/todo.js +64 -0
  140. package/dist/sdk/world/world.d.ts +0 -2
  141. package/dist/terminal-opentui.js +3331 -1584
  142. package/dist/terminal-pi.js +421 -133
  143. package/docs/README.md +3 -0
  144. package/docs/architecture/00-overview.md +1 -1
  145. package/docs/architecture/01-extension-points.md +1 -1
  146. package/docs/architecture/02-sessions-and-runs.md +73 -36
  147. package/docs/architecture/03-variants.md +1 -3
  148. package/docs/architecture/04-prompt.md +24 -13
  149. package/docs/architecture/06-plugins-and-capabilities.md +46 -10
  150. package/docs/architecture/07-tools-and-gate.md +49 -8
  151. package/docs/architecture/09-accounts-and-connectors.md +8 -2
  152. package/docs/architecture/10-server-protocol.md +68 -16
  153. package/docs/architecture/11-terminal.md +94 -45
  154. package/docs/architecture/12-storage.md +1 -0
  155. package/docs/architecture/13-world-tools.md +8 -6
  156. package/docs/architecture/14-surfaces-and-parity.md +4 -1
  157. package/docs/architecture/README.md +20 -18
  158. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  159. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
  160. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
  161. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  162. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  163. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  164. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  165. package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
  166. package/docs/architecture/proposals/README.md +11 -6
  167. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  168. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  169. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  170. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  171. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  172. package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
  173. package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
  174. package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
  175. package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
  176. package/docs/doctor.md +14 -1
  177. package/docs/parity.md +16 -13
  178. package/docs/releases/1.3.0.md +21 -0
  179. package/docs/releases/1.3.1-unstable.1.md +8 -0
  180. package/docs/releases/README.md +9 -6
  181. package/docs/terminal-tui.md +127 -65
  182. package/package.json +1 -1
  183. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
  184. package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
  185. package/dist/sdk/tools/checklist.d.ts +0 -9
  186. package/dist/sdk/tools/checklist.js +0 -55
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * `context` is an instruction file Pi reads on its own: `AGENTS.md`, `SYSTEM.md` and their kin.
3
3
  * `hook` is a package's hooks file, which the harness runs itself; Pi never loads it.
4
+ * `agent` is a subagent profile a package brings; the harness reads it, Pi never does.
4
5
  */
5
- export type FoundKind = 'extension' | 'skill' | 'prompt' | 'theme' | 'context' | 'hook';
6
+ export type FoundKind = 'extension' | 'skill' | 'prompt' | 'theme' | 'context' | 'hook' | 'agent';
6
7
  /** Where a found item comes from. `builtin` is one of the Pi command's own extensions. */
7
8
  export type FoundOrigin = 'shared' | 'package' | 'project' | 'home' | 'builtin';
8
9
  export interface Found {
@@ -33,6 +34,11 @@ export type Resolver = (input: ResolveInput) => Promise<Found[]>;
33
34
  /** The extensions built into the `pi` command. A terminal loads them; an SDK session has none. */
34
35
  export declare const PI_BUILTINS: readonly ["llama.cpp", "codemode", "tool-search", "mcp"];
35
36
  export declare const BUILTIN_PREFIX = "builtin:";
37
+ /**
38
+ * The subagent profiles in a package's agents folder: Markdown files whose frontmatter has a
39
+ * `name` and a `description`. Anything else there (notes, a README) is not a profile.
40
+ */
41
+ export declare function packageProfiles(root: string, folder: string): string[];
36
42
  /**
37
43
  * Runs an install with the package manager's install scripts switched off, so fetching a
38
44
  * package runs none of its code. A package that builds itself on install then fails to load,
@@ -1,14 +1,50 @@
1
1
  // What Pi would load for a session, found with Pi's own resolver so the list matches Pi's
2
2
  // rules. Resolving reads folders and manifests; it never runs an extension.
3
- import { existsSync } from 'node:fs';
3
+ import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
5
  import { join, sep } from 'node:path';
6
6
  import { log } from "../core/bus.js";
7
+ import * as frontmatter from "../core/frontmatter.js";
7
8
  import { loadPi } from "../pi/runtime.js";
8
9
  import { packageManifest } from "./manifest.js";
9
10
  /** The extensions built into the `pi` command. A terminal loads them; an SDK session has none. */
10
11
  export const PI_BUILTINS = ['llama.cpp', 'codemode', 'tool-search', 'mcp'];
11
12
  export const BUILTIN_PREFIX = 'builtin:';
13
+ /** At most this many profiles are read from one package. */
14
+ const MAX_PACKAGE_PROFILES = 50;
15
+ /**
16
+ * The subagent profiles in a package's agents folder: Markdown files whose frontmatter has a
17
+ * `name` and a `description`. Anything else there (notes, a README) is not a profile.
18
+ */
19
+ export function packageProfiles(root, folder) {
20
+ const dir = join(root, folder);
21
+ let names;
22
+ try {
23
+ if (!lstatSync(dir).isDirectory())
24
+ return [];
25
+ names = readdirSync(dir).filter((name) => name.endsWith('.md') && !name.startsWith('.')).sort();
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ const profiles = [];
31
+ for (const name of names) {
32
+ const path = join(dir, name);
33
+ try {
34
+ if (!lstatSync(path).isFile())
35
+ continue;
36
+ const front = frontmatter.parse(readFileSync(path, 'utf8'))?.front;
37
+ if (typeof front?.name === 'string' && front.name.trim() && typeof front.description === 'string' && front.description.trim())
38
+ profiles.push(path);
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ if (profiles.length >= MAX_PACKAGE_PROFILES)
44
+ break;
45
+ }
46
+ return profiles;
47
+ }
12
48
  const inside = (path, parent) => path === parent || path.startsWith(parent.endsWith(sep) ? parent : `${parent}${sep}`);
13
49
  function origin(path, metadata) {
14
50
  if (path.startsWith(BUILTIN_PREFIX))
@@ -92,7 +128,10 @@ export const piResolver = async ({ cwd, agentDir, install, packages }) => {
92
128
  for (const folder of packages ?? [])
93
129
  roots.set(folder, folder);
94
130
  for (const [root, source] of roots) {
95
- const hooks = packageManifest(root).hooks;
131
+ const manifest = packageManifest(root);
132
+ for (const path of packageProfiles(root, manifest.agents))
133
+ found.push({ kind: 'agent', path, origin: 'package', project: false, packageRoot: root, source, enabled: true });
134
+ const hooks = manifest.hooks;
96
135
  if (!hooks || !existsSync(join(root, hooks)))
97
136
  continue;
98
137
  found.push({ kind: 'hook', path: join(root, hooks), origin: 'package', project: false, packageRoot: root, source, enabled: true });
@@ -6,7 +6,7 @@ import * as frontmatter from "../core/frontmatter.js";
6
6
  import { BUILTIN_PREFIX } from "./discover.js";
7
7
  import { packageManifest } from "./manifest.js";
8
8
  export { packageManifest } from "./manifest.js";
9
- export const FOUND_KINDS = { extension: 'code', hook: 'code', skill: 'skill', prompt: 'command', theme: 'theme', context: 'instruction' };
9
+ export const FOUND_KINDS = { extension: 'code', hook: 'code', skill: 'skill', prompt: 'command', agent: 'agent', theme: 'theme', context: 'instruction' };
10
10
  /** The id of the plugin a found item is listed under. */
11
11
  export function pluginIdOf(item) {
12
12
  if (item.origin === 'builtin')
@@ -10,11 +10,15 @@ export declare function normalizeSource(input: string): string;
10
10
  export declare function install(harness: Harness, params: PluginScope & {
11
11
  source: string;
12
12
  }): Promise<Plugin[]>;
13
- /** Removes an installed package. One kept as a folder beside a person's data is moved to the trash there. */
13
+ /** Removes an installed package without consulting Hub. Hub calls this while it owns the receipt transaction. */
14
+ export declare function removeDirect(harness: Harness, params: PluginScope & {
15
+ pluginId: string;
16
+ }): Promise<Plugin[]>;
17
+ /** Removes an installed package, delegating Hub-owned packages to their receipt owner. */
14
18
  export declare function remove(harness: Harness, params: PluginScope & {
15
19
  pluginId: string;
16
20
  }): Promise<Plugin[]>;
17
- /** Fetches a newer version of one installed package, or of all of them. Changed code waits for trust again. */
21
+ /** Fetches newer versions of local packages. Hub-owned packages remain pinned to catalog versions. */
18
22
  export declare function update(harness: Harness, params: PluginScope & {
19
23
  pluginId?: string | null;
20
24
  }): Promise<Plugin[]>;
@@ -75,8 +75,8 @@ function installed(plugins, pluginId) {
75
75
  throw invalid(`${plugin.title} was not installed as a package, so it cannot be removed or updated here.`);
76
76
  return plugin;
77
77
  }
78
- /** Removes an installed package. One kept as a folder beside a person's data is moved to the trash there. */
79
- export async function remove(harness, params) {
78
+ /** Removes an installed package without consulting Hub. Hub calls this while it owns the receipt transaction. */
79
+ export async function removeDirect(harness, params) {
80
80
  const plugin = installed(await list(harness, params), params.pluginId);
81
81
  if (plugin.install.kind === 'folder') {
82
82
  const trash = join(harness.env.root, 'trash', 'plugins');
@@ -98,13 +98,37 @@ export async function remove(harness, params) {
98
98
  changed(harness);
99
99
  return list(harness, params);
100
100
  }
101
- /** Fetches a newer version of one installed package, or of all of them. Changed code waits for trust again. */
101
+ async function hubReceiptForPackage(harness, pluginId) {
102
+ const receipts = (await harness.hub.list()).installed.filter((receipt) => receipt.entry.delivery.kind === 'pi-package' && receipt.resourceIds.includes(pluginId));
103
+ if (receipts.length > 1)
104
+ throw invalid('More than one Hub receipt owns this package. Review the Hub receipts before changing it.');
105
+ return receipts[0];
106
+ }
107
+ /** Removes an installed package, delegating Hub-owned packages to their receipt owner. */
108
+ export async function remove(harness, params) {
109
+ const receipt = await hubReceiptForPackage(harness, params.pluginId);
110
+ if (receipt) {
111
+ await harness.hub.uninstall(receipt.entry.id, receipt.entry.version);
112
+ return list(harness, params);
113
+ }
114
+ return removeDirect(harness, params);
115
+ }
116
+ /** Fetches newer versions of local packages. Hub-owned packages remain pinned to catalog versions. */
102
117
  export async function update(harness, params) {
103
- const plugin = params.pluginId ? installed(await list(harness, params), params.pluginId) : null;
118
+ const plugins = await list(harness, params);
119
+ const plugin = params.pluginId ? installed(plugins, params.pluginId) : null;
104
120
  if (plugin?.install.kind === 'folder')
105
121
  throw invalid(`${plugin.title} is a folder on this machine. Change its files to update it.`);
122
+ const receipts = (await harness.hub.list()).installed.filter((receipt) => receipt.entry.delivery.kind === 'pi-package');
123
+ const hubOwned = new Set(receipts.flatMap((receipt) => receipt.resourceIds));
124
+ if (plugin && hubOwned.has(plugin.id))
125
+ throw invalid(`${plugin.title} is managed by Hub. Uninstall its receipt before installing another exact catalog version.`);
126
+ const sources = plugin
127
+ ? [plugin.install.source]
128
+ : [...new Set(plugins.filter((candidate) => candidate.install && candidate.install.kind !== 'folder' && !hubOwned.has(candidate.id)).map((candidate) => candidate.install.source))];
106
129
  const { packages } = await manager(harness);
107
- await withoutInstallScripts(() => packages.update(plugin?.install.source));
130
+ for (const source of sources)
131
+ await withoutInstallScripts(() => packages.update(source));
108
132
  changed(harness);
109
133
  return list(harness, params);
110
134
  }
@@ -6,7 +6,7 @@ import { charCount } from "../core/text.js";
6
6
  import { allowed } from "../mcp/connections.js";
7
7
  import { BUILT_IN_DYNAMIC, compile, toolGroups } from "../prompt/compile.js";
8
8
  import { ASK_USER } from "../tools/askUser.js";
9
- import { CHECKLIST_WRITE } from "../tools/checklist.js";
9
+ import { TODO_WRITE } from "../tools/todo.js";
10
10
  import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
11
11
  import { VARIANT_PLUGIN } from "./builtin.js";
12
12
  const size = (path) => {
@@ -68,7 +68,7 @@ export function inventory(input) {
68
68
  }
69
69
  const names = [...input.tools];
70
70
  const extensionToolNames = new Set(input.plugins.filter((plugin) => ['project', 'package', 'shared', 'home'].includes(plugin.source)).flatMap((plugin) => plugin.contributions.filter((entry) => entry.kind === 'tool' && entry.state === 'on').map((entry) => entry.name)));
71
- for (const name of [ASK_USER, CHECKLIST_WRITE, ...DEFAULT_TOOL_NAMES, ...(input.connections ? ['mcp_list_tools', 'mcp_call_tool'] : [])])
71
+ for (const name of [ASK_USER, TODO_WRITE, ...DEFAULT_TOOL_NAMES, ...(input.connections ? ['mcp_list_tools', 'mcp_call_tool'] : [])])
72
72
  if (!names.includes(name))
73
73
  names.push(name);
74
74
  for (const name of names) {
@@ -86,7 +86,7 @@ export function inventory(input) {
86
86
  }
87
87
  // What the harness found on disk and lets load. The pi command's own extensions load in no
88
88
  // Xingular session, so a terminal and a host have the same (PROP-0039).
89
- const delivered = { code: 'code', skill: 'on-demand', command: 'on-demand', instruction: 'system', theme: 'code', tool: 'tool', connection: 'tool' };
89
+ const delivered = { code: 'code', skill: 'on-demand', command: 'on-demand', agent: 'on-demand', instruction: 'system', theme: 'code', tool: 'tool', connection: 'tool' };
90
90
  const terminal = context.surface === 'cli';
91
91
  for (const plugin of input.plugins) {
92
92
  if (!['project', 'package', 'shared', 'home'].includes(plugin.source))
@@ -9,6 +9,8 @@ export interface PackageManifest {
9
9
  commandPrefix: string | null;
10
10
  /** The hooks file, inside the package, or `null` when it has none. */
11
11
  hooks: string | null;
12
+ /** The folder of subagent profiles, inside the package. `agents` unless the manifest names another. */
13
+ agents: string;
12
14
  /** Data-only remote connector forms. Invalid templates are ignored; none can carry credentials. */
13
15
  connectorTemplates: ConnectorTemplate[];
14
16
  }
@@ -1,5 +1,7 @@
1
1
  // What a package says about itself in its `package.json`: Pi's own fields, and under `harness`
2
- // which part of the harness it extends, its command namespace and the file of hooks it runs.
2
+ // which part of the harness it extends, its command namespace, the file of hooks it runs and the
3
+ // folder of subagent profiles it brings. A package written for other agent harnesses may have no
4
+ // `package.json`; its `plugin.json` then names and describes it.
3
5
  import { basename, join } from 'node:path';
4
6
  import { z } from 'zod';
5
7
  import { readJsonOr, safeRelative } from "../core/fs.js";
@@ -39,16 +41,31 @@ function inside(path) {
39
41
  return null;
40
42
  }
41
43
  }
44
+ /** The plugin manifests other agent harnesses read, in the order they are tried. */
45
+ const PLUGIN_MANIFESTS = ['plugin.json', '.claude-plugin/plugin.json', '.codex-plugin/plugin.json'];
46
+ const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
47
+ function pluginJson(packageRoot) {
48
+ for (const file of PLUGIN_MANIFESTS) {
49
+ const found = readJsonOr(join(packageRoot, file), null);
50
+ if (found && typeof found === 'object' && !Array.isArray(found))
51
+ return found;
52
+ }
53
+ return {};
54
+ }
42
55
  export function packageManifest(packageRoot) {
43
56
  const manifest = readJsonOr(join(packageRoot, 'package.json'), {});
57
+ // Only a package without a name of its own is described by its plugin.json.
58
+ const plugin = text(manifest.name) ? {} : pluginJson(packageRoot);
44
59
  const own = (manifest.harness && typeof manifest.harness === 'object' && !Array.isArray(manifest.harness) ? manifest.harness : {});
60
+ const pluginName = text(plugin.name);
45
61
  return {
46
- name: typeof manifest.name === 'string' && manifest.name ? manifest.name : basename(packageRoot),
47
- description: typeof manifest.description === 'string' ? manifest.description : '',
48
- version: typeof manifest.version === 'string' ? manifest.version : null,
62
+ name: text(manifest.name) ?? (pluginName && /^[a-z0-9][a-z0-9._-]*$/i.test(pluginName) ? pluginName : basename(packageRoot)),
63
+ description: text(manifest.description) ?? text(plugin.description) ?? '',
64
+ version: text(manifest.version) ?? text(plugin.version),
49
65
  extends: (Array.isArray(own.extends) ? own.extends : typeof own.extends === 'string' ? [own.extends] : []).filter((id) => typeof id === 'string' && !!id),
50
66
  commandPrefix: typeof own.commandPrefix === 'string' && /^[a-z][a-z0-9-]*$/i.test(own.commandPrefix) && !['plugin', 'skill'].includes(own.commandPrefix.toLowerCase()) ? own.commandPrefix.toLowerCase() : null,
51
67
  hooks: inside(own.hooks),
68
+ agents: inside(own.agents) ?? 'agents',
52
69
  connectorTemplates: (Array.isArray(own.connectors) ? own.connectors : []).flatMap((value) => {
53
70
  const parsed = connectorTemplateSchema.safeParse(value);
54
71
  return parsed.success ? [parsed.data] : [];
@@ -9,6 +9,8 @@ export interface Loading {
9
9
  skillPaths: string[];
10
10
  promptPaths: string[];
11
11
  themePaths: string[];
12
+ /** Subagent profile files packages bring. The harness reads them; Pi is never given them. */
13
+ profilePaths: string[];
12
14
  /** The `pi` command's own extensions left on, as `builtin:<name>`. Only a terminal has them. */
13
15
  builtins: string[];
14
16
  /** Hooks files the harness runs itself. Pi is never given them. */
@@ -87,6 +87,7 @@ export class Plugins {
87
87
  skillPaths: paths('skill', false),
88
88
  promptPaths: paths('prompt', false),
89
89
  themePaths: paths('theme', false),
90
+ profilePaths: paths('agent', false),
90
91
  builtins: paths('extension', true),
91
92
  hooks: chosen.filter((item) => item.kind === 'hook' && item.packageRoot).map((item) => ({ path: item.path, packageRoot: item.packageRoot, hash: packageCodeState[item.packageRoot]?.hash ?? '' })),
92
93
  found,
@@ -0,0 +1,17 @@
1
+ import type { Harness } from "../harness.js";
2
+ export interface Proposal {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ files: Record<string, string>;
7
+ manifest: string;
8
+ hash: string;
9
+ createdAt: string;
10
+ status: 'pending' | 'accepted' | 'rejected';
11
+ decidedAt?: string;
12
+ }
13
+ export declare function create(root: string, name: unknown, description: unknown, files: unknown): Proposal;
14
+ export declare function preview(root: string, id: string): Proposal;
15
+ export declare function list(root: string): Proposal[];
16
+ export declare function reject(root: string, id: string, hash: string): Proposal;
17
+ export declare function accept(harness: Harness, id: string, hash: string): Promise<Proposal>;
@@ -0,0 +1,147 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { invalid, notFound } from "../core/errors.js";
5
+ import * as frontmatter from "../core/frontmatter.js";
6
+ import { now, readJson, safeId, safeRelative, writeJson } from "../core/fs.js";
7
+ import { ensureAdopted, list as listPlugins } from "./api.js";
8
+ import * as attachments from "./attachments.js";
9
+ import { packageCode } from "./trust.js";
10
+ const MAX_FILES = 100;
11
+ const MAX_FILE_BYTES = 256 * 1024;
12
+ const MAX_TOTAL_BYTES = 1024 * 1024;
13
+ const dir = (root) => join(root, 'plugin-proposals');
14
+ const pathOf = (root, id) => join(dir(root), `${safeId(id)}.json`);
15
+ function validate(name, description, input) {
16
+ if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name))
17
+ throw invalid('Plugin name must be lowercase letters, digits, underscores or hyphens.');
18
+ if (typeof description !== 'string' || !description.trim() || Buffer.byteLength(description, 'utf8') > 4096 || /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(description))
19
+ throw invalid('Description must be non-empty valid text of at most 4096 bytes.');
20
+ if (!input || typeof input !== 'object' || Array.isArray(input))
21
+ throw invalid('Files must be a path to text object.');
22
+ const entries = Object.entries(input);
23
+ if (!entries.length || entries.length > MAX_FILES)
24
+ throw invalid('A proposal must contain between 1 and 100 files.');
25
+ const files = Object.create(null);
26
+ let total = 0;
27
+ for (const [path, content] of entries) {
28
+ if (typeof content !== 'string' || /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(content))
29
+ throw invalid('Each file must be valid UTF-8 text.');
30
+ if (path.includes('\\') || path.length > 240 || /[\x00-\x1f\x7f]/.test(path) || safeRelative(path) !== path || path.split('/').some((part) => part === 'node_modules' || part === '.git'))
31
+ throw invalid('Invalid proposal file path.');
32
+ if (!/^(?:README\.md|extensions\/[A-Za-z0-9_./-]+\.(?:ts|js|mjs)|skills\/[A-Za-z0-9_./-]+\.md|prompts\/[A-Za-z0-9_./-]+\.md|agents\/[A-Za-z0-9_./-]+\.md|themes\/[A-Za-z0-9_./-]+\.json)$/.test(path))
33
+ throw invalid('Unsupported proposal file type or location.');
34
+ if (path.startsWith('skills/') && path.endsWith('/SKILL.md')) {
35
+ if (!/^skills\/[A-Za-z0-9_-]+\/SKILL\.md$/.test(path))
36
+ throw invalid('Skill files must be in skills/<name>/SKILL.md.');
37
+ const front = frontmatter.parse(content)?.front;
38
+ if (front?.name !== path.split('/')[1] || typeof front.description !== 'string' || !front.description.trim())
39
+ throw invalid('Skill frontmatter needs a matching name and a non-empty description.');
40
+ }
41
+ const bytes = Buffer.byteLength(content, 'utf8');
42
+ if (bytes > MAX_FILE_BYTES || (total += bytes) > MAX_TOTAL_BYTES)
43
+ throw invalid('Proposal files exceed the size limit.');
44
+ files[path] = content;
45
+ }
46
+ for (const path of Object.keys(files))
47
+ if (Object.keys(files).some((other) => other.startsWith(`${path}/`)))
48
+ throw invalid('A file cannot also be a directory.');
49
+ const manifest = `${JSON.stringify({ name, version: '1.0.0', description }, null, 2)}\n`;
50
+ const hash = createHash('sha256').update(JSON.stringify([name, description, manifest, Object.entries(files).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)])).digest('hex');
51
+ return { name, description, files, manifest, hash };
52
+ }
53
+ export function create(root, name, description, files) {
54
+ const valid = validate(name, description, files);
55
+ const proposal = { id: randomUUID(), ...valid, createdAt: now(), status: 'pending' };
56
+ writeJson(pathOf(root, proposal.id), proposal);
57
+ return proposal;
58
+ }
59
+ export function preview(root, id) {
60
+ const proposal = readJson(pathOf(root, id));
61
+ if (!proposal)
62
+ throw notFound('Proposal');
63
+ const valid = validate(proposal.name, proposal.description, proposal.files);
64
+ if (proposal.id !== id || !['pending', 'accepted', 'rejected'].includes(proposal.status))
65
+ throw invalid('Invalid stored proposal.');
66
+ return { ...proposal, ...valid };
67
+ }
68
+ export function list(root) {
69
+ let names;
70
+ try {
71
+ names = readdirSync(dir(root));
72
+ }
73
+ catch (error) {
74
+ if (error.code === 'ENOENT')
75
+ return [];
76
+ throw error;
77
+ }
78
+ return names.filter((name) => /^[a-f0-9-]+\.json$/.test(name)).sort().map((name) => preview(root, name.slice(0, -5)));
79
+ }
80
+ function reviewed(proposal, hash) {
81
+ if (proposal.status !== 'pending')
82
+ throw invalid('Proposal is no longer pending.');
83
+ if (!/^[a-f0-9]{64}$/.test(hash) || proposal.hash !== hash)
84
+ throw invalid('Stale proposal review; preview it again.');
85
+ }
86
+ export function reject(root, id, hash) {
87
+ const proposal = preview(root, id);
88
+ reviewed(proposal, hash);
89
+ const rejected = { ...proposal, status: 'rejected', decidedAt: now() };
90
+ writeJson(pathOf(root, id), rejected);
91
+ return rejected;
92
+ }
93
+ export async function accept(harness, id, hash) {
94
+ const root = harness.env.root;
95
+ const initial = preview(root, id);
96
+ reviewed(initial, hash);
97
+ await ensureAdopted(harness);
98
+ const existing = await listPlugins(harness, {});
99
+ const proposal = preview(root, id);
100
+ reviewed(proposal, hash);
101
+ const pluginId = `package:${proposal.name}`;
102
+ if (existing.some((plugin) => plugin.id === pluginId))
103
+ throw invalid('A plugin with this name already exists.');
104
+ const parent = join(root, 'plugins');
105
+ const destination = join(parent, proposal.name);
106
+ mkdirSync(parent, { recursive: true });
107
+ const stage = join(parent, `.proposal-${randomUUID()}`);
108
+ mkdirSync(stage);
109
+ let reserved = false;
110
+ try {
111
+ for (const [path, content] of Object.entries(proposal.files)) {
112
+ const target = join(stage, path);
113
+ mkdirSync(dirname(target), { recursive: true });
114
+ writeFileSync(target, content, { flag: 'wx' });
115
+ }
116
+ writeFileSync(join(stage, 'package.json'), proposal.manifest, { flag: 'wx' });
117
+ const found = { kind: 'extension', path: stage, origin: 'package', project: false, packageRoot: stage, source: stage, enabled: true };
118
+ const code = packageCode([found]).get(stage);
119
+ if (harness.plugins.trust.has(code.hash))
120
+ throw invalid('Identical package code is already trusted; change it before accepting.');
121
+ // Reserve the destination exclusively. Renaming over our empty directory publishes the whole package.
122
+ mkdirSync(destination);
123
+ reserved = true;
124
+ const choices = attachments.load(root);
125
+ for (const scope of [choices.global, ...Object.values(choices.agents), ...Object.values(choices.projects)]) {
126
+ scope.on = scope.on.filter((entry) => entry !== pluginId && !entry.startsWith(`${pluginId}/`));
127
+ scope.off = scope.off.filter((entry) => entry !== pluginId && !entry.startsWith(`${pluginId}/`));
128
+ }
129
+ attachments.set(choices, [pluginId], {}, false, true);
130
+ attachments.save(root, choices);
131
+ renameSync(stage, destination);
132
+ reserved = false;
133
+ const accepted = { ...proposal, status: 'accepted', decidedAt: now() };
134
+ writeJson(pathOf(root, id), accepted);
135
+ harness.bus.emit({ type: 'plugins.changed' });
136
+ return accepted;
137
+ }
138
+ finally {
139
+ rmSync(stage, { recursive: true, force: true });
140
+ if (reserved) {
141
+ try {
142
+ rmdirSync(destination);
143
+ }
144
+ catch { /* Do not remove another writer's files. */ }
145
+ }
146
+ }
147
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Attachment } from "./attachments.js";
2
2
  import type { Registered } from "./plugins.js";
3
3
  import type { ProjectCode } from "./trust.js";
4
- export declare const CONTRIBUTION_KINDS: readonly ["instruction", "skill", "command", "tool", "connection", "code", "theme"];
4
+ export declare const CONTRIBUTION_KINDS: readonly ["instruction", "skill", "command", "agent", "tool", "connection", "code", "theme"];
5
5
  export type ContributionKind = (typeof CONTRIBUTION_KINDS)[number];
6
6
  /**
7
7
  * Where a plugin comes from: the harness itself, the embedding application, Pi, a person's own
@@ -1,4 +1,4 @@
1
- export const CONTRIBUTION_KINDS = ['instruction', 'skill', 'command', 'tool', 'connection', 'code', 'theme'];
1
+ export const CONTRIBUTION_KINDS = ['instruction', 'skill', 'command', 'agent', 'tool', 'connection', 'code', 'theme'];
2
2
  /**
3
3
  * Where a plugin comes from: the harness itself, the embedding application, Pi, a person's own
4
4
  * registry, the folder every account shares, an installed package, the working folder, or the
@@ -1,13 +1,15 @@
1
1
  import { type CallerContext } from "../core/caller.js";
2
2
  import type { HarnessEnv } from "../core/env.js";
3
3
  import type { Materialised } from "../variant/materialise.js";
4
- import type { ChecklistItem } from "../sessions/contracts.js";
4
+ import type { TodoItem } from "../sessions/contracts.js";
5
+ import type { SessionState } from "../sessions/sessionState.js";
5
6
  import { type StandardSession } from "./standard.js";
6
7
  export declare const REFERENCES_BUDGET = 12000;
7
8
  /** Where the harness's own sections sit; an application's sections are placed among them by `order`. */
8
9
  export declare const SECTION_ORDER: {
9
10
  readonly harnessIdentity: 5;
10
11
  readonly system: 25;
12
+ readonly sessionState: 30;
11
13
  readonly guardrails: 40;
12
14
  readonly memory: 45;
13
15
  readonly references: 50;
@@ -24,7 +26,7 @@ export interface Source {
24
26
  revision?: string | null;
25
27
  }
26
28
  export interface Section {
27
- /** `harness-identity`, `identity`, `role`, `system`, `guardrails`, `memory`, `references`, `subagents`, `scripts`, `wiki`, `tools`, or an application's own. */
29
+ /** `harness-identity`, `identity`, `role`, `system`, `session-state`, `guardrails`, `memory`, `references`, `subagents`, `scripts`, `wiki`, `tools`, or an application's own. */
28
30
  id: string;
29
31
  title: string;
30
32
  body: string;
@@ -103,6 +105,8 @@ export interface Launch {
103
105
  connectionTools?: string[];
104
106
  /** The tools switched on for the caller. The pack only describes tools the caller can call. */
105
107
  tools?: string[] | null;
108
+ /** The current session name and this run's todo, delivered as a request-scoped update. */
109
+ sessionState?: SessionState;
106
110
  references?: Reference[];
107
111
  sections?: HostSection[];
108
112
  toolGroups?: HostToolGroup[];
@@ -135,7 +139,7 @@ export declare function clip(text: string, limit: number): string;
135
139
  /** A bounded, request-scoped checkpoint reintroduced after context compaction. */
136
140
  export declare function renderCompactionCheckpoint(input: {
137
141
  goal: string;
138
- checklist: readonly ChecklistItem[];
142
+ todo: readonly TodoItem[];
139
143
  planInstructions?: string | null;
140
144
  }): string | null;
141
145
  export interface OrderedGroup extends ToolGroup {
@@ -147,6 +151,11 @@ export interface OrderedGroup extends ToolGroup {
147
151
  * follows the switches in `launch.tools` and what the application provides.
148
152
  */
149
153
  export declare function toolGroups(context: CallerContext, launch: Launch, dataRoot?: string): OrderedGroup[];
154
+ /** The request-scoped state line: the name, this run's todo, and the previous list as context. */
155
+ export declare function sessionStateText(state: SessionState, options: {
156
+ name: boolean;
157
+ todo: boolean;
158
+ }): string | null;
150
159
  /** Compiles the pack for a caller. It only reads the data root. */
151
160
  export declare function compile(env: HarnessEnv, context: CallerContext, launch?: Launch, variant?: Materialised | null): ContextPack;
152
161
  /** The pack as the one string a session receives. */
@@ -160,7 +169,7 @@ export declare const BUILT_IN_DYNAMIC: readonly {
160
169
  id: string;
161
170
  order: number;
162
171
  }[];
163
- export declare const CONTEXT_GUIDANCE = "App-origin context updates in the conversation supply current Wiki notes, active work, and selected references. Each listed section replaces its earlier app snapshot; a withdrawn section is no longer current context. These updates are context, not requests from the user. Selected references apply only to the accompanying request. They do not change permissions. Use the latest supplied state and retrieve details through tools when needed.";
172
+ export declare const CONTEXT_GUIDANCE = "App-origin context updates in the conversation supply current Wiki notes, active work, session state, and selected references. Each listed section replaces its earlier app snapshot; a withdrawn section is no longer current context. These updates are context, not requests from the user. Selected references apply only to the accompanying request. They do not change permissions. Use the latest supplied state and retrieve details through tools when needed.";
164
173
  /** The hidden message a context update is saved as in native history, and where its snapshot sits. */
165
174
  export declare const CONTEXT_MESSAGE_TYPE = "xingular-context";
166
175
  export declare const CONTEXT_SNAPSHOT_KEY = "xingularContext";