@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.1-fork.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 (94) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/src/agent-execution.js +114 -87
  3. package/dist/src/background-widget.d.ts +61 -0
  4. package/dist/src/background-widget.js +23 -16
  5. package/dist/src/decoders.js +28 -1
  6. package/dist/src/execution.d.ts +4 -2
  7. package/dist/src/execution.js +43 -2
  8. package/dist/src/host-delivery.js +1 -0
  9. package/dist/src/host-navigator.d.ts +18 -0
  10. package/dist/src/host-navigator.js +71 -13
  11. package/dist/src/host-recovery.d.ts +5 -2
  12. package/dist/src/host-recovery.js +3 -3
  13. package/dist/src/host-view.d.ts +21 -1
  14. package/dist/src/host-view.js +138 -47
  15. package/dist/src/host.d.ts +2 -2
  16. package/dist/src/host.js +246 -60
  17. package/dist/src/index.d.ts +3 -1
  18. package/dist/src/index.js +1346 -622
  19. package/dist/src/index.js.map +3 -3
  20. package/dist/src/paths.d.ts +2 -0
  21. package/dist/src/paths.js +2 -0
  22. package/dist/src/registry.js +3 -2
  23. package/dist/src/roles.d.ts +70 -0
  24. package/dist/src/roles.js +326 -0
  25. package/dist/src/script-tools.d.ts +27 -0
  26. package/dist/src/script-tools.js +87 -0
  27. package/dist/src/settings.d.ts +26 -0
  28. package/dist/src/settings.js +277 -0
  29. package/dist/src/trajectory.d.ts +39 -4
  30. package/dist/src/trajectory.js +82 -5
  31. package/dist/src/types.d.ts +30 -3
  32. package/dist/src/types.js +1 -1
  33. package/dist/src/utils.d.ts +21 -1
  34. package/dist/src/utils.js +40 -0
  35. package/dist/src/validation.d.ts +10 -17
  36. package/dist/src/validation.js +29 -458
  37. package/dist/starter/index.js +0 -2
  38. package/dist/starter/index.js.map +1 -1
  39. package/dist/starter/review-loop.d.ts +0 -13
  40. package/dist/starter/review-loop.js +0 -2
  41. package/dist/subagents/index.js +1344 -798
  42. package/dist/subagents/index.js.map +3 -3
  43. package/dist/subagents/src/contracts.d.ts +49 -29
  44. package/dist/subagents/src/contracts.js +18 -7
  45. package/dist/subagents/src/index.d.ts +1 -1
  46. package/dist/subagents/src/index.js +15 -7
  47. package/dist/subagents/src/manager.js +32 -13
  48. package/dist/subagents/src/navigator.js +396 -247
  49. package/dist/subagents/src/view.d.ts +24 -2
  50. package/dist/subagents/src/view.js +107 -80
  51. package/dist/trajectory/assets/index.html +370 -232
  52. package/dist/trajectory/assets/prism.min.js +13 -0
  53. package/dist/trajectory/index.js +445 -277
  54. package/dist/trajectory/index.js.map +3 -3
  55. package/dist/trajectory/src/assets/index.html +370 -232
  56. package/dist/trajectory/src/assets/prism.min.js +13 -0
  57. package/dist/trajectory/src/export.js +4 -2
  58. package/dist/trajectory/src/index.d.ts +2 -0
  59. package/dist/trajectory/src/index.js +151 -60
  60. package/dist/trajectory/src/server.js +138 -11
  61. package/dist/trajectory/src/server.js.map +2 -2
  62. package/package.json +7 -6
  63. package/skills/pi-extensible-workflows/SKILL.md +2 -0
  64. package/src/agent-execution.ts +110 -80
  65. package/src/background-widget.ts +26 -19
  66. package/src/decoders.ts +22 -1
  67. package/src/execution.ts +41 -4
  68. package/src/host-delivery.ts +1 -0
  69. package/src/host-navigator.ts +58 -14
  70. package/src/host-recovery.ts +8 -7
  71. package/src/host-view.ts +116 -42
  72. package/src/host.ts +223 -55
  73. package/src/index.ts +3 -1
  74. package/src/paths.ts +3 -0
  75. package/src/registry.ts +3 -2
  76. package/src/roles.ts +327 -0
  77. package/src/script-tools.ts +76 -0
  78. package/src/settings.ts +224 -0
  79. package/src/trajectory.ts +97 -9
  80. package/src/types.ts +12 -5
  81. package/src/utils.ts +36 -1
  82. package/src/validation.ts +30 -356
  83. package/starter/review-loop.ts +0 -2
  84. package/subagents/README.md +3 -3
  85. package/subagents/src/contracts.ts +22 -7
  86. package/subagents/src/index.ts +18 -7
  87. package/subagents/src/manager.ts +31 -13
  88. package/subagents/src/navigator.ts +336 -180
  89. package/subagents/src/view.ts +108 -76
  90. package/trajectory/src/assets/index.html +370 -232
  91. package/trajectory/src/assets/prism.min.js +13 -0
  92. package/trajectory/src/export.ts +4 -2
  93. package/trajectory/src/index.ts +124 -47
  94. package/trajectory/src/server.ts +114 -14
package/src/validation.ts CHANGED
@@ -1,20 +1,19 @@
1
- import { atomicWriteFile } from "./persistence.js";
2
- import { mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { basename, dirname, extname, join, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
6
3
  import * as acorn from "acorn";
7
4
  import { Script } from "node:vm";
8
- import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
9
- import type { AgentDefinition, AgentResourceSelectors, AgentResourceSelectorSet, AgentResourcePolicy, CheckpointInput, ContextFileScope, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowExtensionMetadata, WorkflowExtensionSettings, WorkflowMetadata, WorkflowRetentionSettings, WorkflowRoleDirectoryRegistration, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowSettingsSources, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
5
+ import type { AgentDefinition, CheckpointInput, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
10
6
  import type { WorkflowRegistryApi } from "./registry.js";
11
- import { registeredWorkflowRoleDirectoryRegistrations } from "./registry.js";
12
- import { annotateModelAliasError, assertModelThinking, deepFreeze, errorText, fail, isNodeError, jsonObject, jsonValue, mergeWorkflowExtensionSettings, modelAliasName, modelCapability, object, positiveInteger, resolveModelReference, resourcePatternHasMagic, unknownModel, validateModelAliases, validateResourcePattern, validWorkflowExtensionNamespace } from "./utils.js";
13
- import { WORKFLOW_CALL_KINDS, isContextFileScope } from "./types.js";
14
- import { canonicalPath, workflowProjectSettingsPath } from "./paths.js";
15
- export { workflowProjectSettingsPath } from "./paths.js";
7
+ import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue, mergeWorkflowExtensionSettings, modelAliasName, modelCapability, object, positiveInteger, resolveModelReference, resourcePatternHasMagic, unknownModel } from "./utils.js";
8
+ import { WORKFLOW_CALL_KINDS } from "./types.js";
9
+ export { validateSchema } from "./utils.js";
10
+ import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
11
+ export { loadAgentDefinitions, loadProjectAgentDefinitions, parseRoleMarkdown, workflowRoleDirectories } from "./roles.js";
12
+ export type { WorkflowRoleDirectoryInput } from "./roles.js";
13
+
14
+ import { validateContextFileScopes, validateSelectorList } from "./settings.js";
15
+ export { DEFAULT_SETTINGS, loadCodemodeToolsSetting, loadSettings, loadSettingsOverrides, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateContextFileScopes, validateModelAliasAvailability, validateSelectorList, validateWorkflowExtensionSettings, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./settings.js";
16
16
 
17
- export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ concurrency: 8, backgroundWidget: true });
18
17
  export function validateCheckpoint(value: unknown): CheckpointInput {
19
18
  if (!object(value) || Object.keys(value).some((key) => !["name", "prompt", "context"].includes(key)) || typeof value.name !== "string" || value.name.trim() === "" || typeof value.prompt !== "string" || !jsonValue(value.context)) fail("INVALID_METADATA", "checkpoint requires only name, prompt, and JSON context");
20
19
  if (Buffer.byteLength(value.prompt) > 1024) fail("INVALID_METADATA", "checkpoint prompt exceeds 1024 UTF-8 bytes");
@@ -22,342 +21,6 @@ export function validateCheckpoint(value: unknown): CheckpointInput {
22
21
  return { name: value.name, prompt: value.prompt, context: value.context };
23
22
  }
24
23
 
25
- export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
26
- function normalizedResourcePath(value: string, settingsPath: string): string {
27
- if (value === "*") return value;
28
- let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
29
- if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
30
- const resolved = resolve(dirname(settingsPath), expanded);
31
- if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\")) return expanded;
32
- if (resourcePatternHasMagic(expanded)) {
33
- const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
34
- const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
35
- const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
36
- const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
37
- const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
38
- return `${canonicalPath(prefix)}${suffix}`;
39
- }
40
- return canonicalPath(resolved);
41
- }
42
- function validateSelectorList(value: unknown, path: string, kind: "skills" | "extensions" | "tools", errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS", normalizeExtensions = true): readonly string[] | undefined {
43
- if (value === undefined) return undefined;
44
- if (!Array.isArray(value)) fail(errorCode, `${path}.${kind} must be an array`);
45
- const normalized: string[] = [];
46
- for (const [index, entry] of value.entries()) {
47
- if (typeof entry !== "string" || !entry.trim()) fail(errorCode, `${path}.${kind}[${String(index)}] must be a non-empty string`);
48
- let selector = entry.trim();
49
- if (kind === "extensions" && normalizeExtensions) {
50
- const negated = selector.startsWith("!");
51
- const body = negated ? selector.slice(1) : selector;
52
- if (!body) fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
53
- try { selector = `${negated ? "!" : ""}${normalizedResourcePath(body, path)}`; } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`); }
54
- }
55
- try { validateResourcePattern(selector); } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`); }
56
- normalized.push(selector);
57
- }
58
- return Object.freeze(normalized);
59
- }
60
- function validateWorktreePostCreateCommand(value: unknown, settingsPath: string): readonly string[] | undefined {
61
- if (value === undefined) return undefined;
62
- if (!Array.isArray(value) || value.length === 0) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand must be a non-empty array`);
63
- return Object.freeze(value.map((entry, index) => {
64
- if (typeof entry !== "string" || !entry.trim()) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand[${String(index)}] must be a non-empty string`);
65
- return entry;
66
- }));
67
- }
68
- function selectorsFromSettings(settings: Readonly<WorkflowSettings | WorkflowSettingsOverrides>): AgentResourceSelectors {
69
- return {
70
- ...(settings.skills === undefined ? {} : { skills: settings.skills }),
71
- ...(settings.extensions === undefined ? {} : { extensions: settings.extensions }),
72
- ...(settings.tools === undefined ? {} : { tools: settings.tools }),
73
- };
74
- }
75
- function selectorSet(value: AgentResourceSelectors | undefined): AgentResourceSelectorSet { return { skills: [...(value?.skills ?? [])], extensions: [...(value?.extensions ?? [])], ...(value?.tools === undefined ? {} : { tools: [...value.tools] }) }; }
76
- function validateContextFileScopes(value: unknown, rolePath: string): readonly ContextFileScope[] | undefined {
77
- if (value === undefined) return undefined;
78
- if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${rolePath}.contextFiles must be an array containing only global, project, or cwd`);
79
- return [...value];
80
- }
81
- export function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS", applyDefaults = true): WorkflowExtensionSettings | undefined {
82
- if (value === undefined) return undefined;
83
- const base = `${settingsPath}.extensionSettings`;
84
- if (!object(value)) fail(errorCode, `${base} must be an object`);
85
- const normalized: Record<string, JsonValue> = {};
86
- for (const [namespace, raw] of Object.entries(value)) {
87
- if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${base} contains an invalid namespace: ${namespace}`);
88
- if (namespace === "herdr") {
89
- if (!object(raw)) fail(errorCode, `${base}.herdr must be an object`);
90
- if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode, `${base}.herdr contains an unsupported setting`);
91
- if (raw.enableFullyInspectableMode !== undefined && typeof raw.enableFullyInspectableMode !== "boolean") fail(errorCode, `${base}.herdr.enableFullyInspectableMode must be a boolean`);
92
- normalized.herdr = Object.freeze({ ...(raw.enableFullyInspectableMode === undefined ? {} : { enableFullyInspectableMode: raw.enableFullyInspectableMode }) });
93
- continue;
94
- }
95
- if (namespace === "trajectory") {
96
- if (!object(raw)) fail(errorCode, `${base}.trajectory must be an object`);
97
- if (Object.keys(raw).some((key) => key !== "port" && key !== "themes")) fail(errorCode, `${base}.trajectory contains an unsupported setting`);
98
- if (raw.port !== undefined && (!positiveInteger(raw.port) || raw.port > 65535)) fail(errorCode, `${base}.trajectory.port must be an integer from 1 to 65535`);
99
- if (raw.themes !== undefined && typeof raw.themes !== "boolean") fail(errorCode, `${base}.trajectory.themes must be a boolean`);
100
- normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }), ...(raw.themes === undefined ? (applyDefaults ? { themes: false } : {}) : { themes: raw.themes }) });
101
- continue;
102
- }
103
- if (!jsonValue(raw)) fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
104
- normalized[namespace] = structuredClone(raw);
105
- }
106
- return deepFreeze(normalized as WorkflowExtensionSettings);
107
- }
108
- function positiveRetentionInteger(value: unknown): value is number { return positiveInteger(value) && Number.isSafeInteger(value); }
109
- function validateRetention(value: unknown, settingsPath: string): Readonly<WorkflowRetentionSettings> | undefined {
110
- if (value === undefined) return undefined;
111
- const base = `${settingsPath}.retention`;
112
- if (!object(value)) fail("INVALID_SETTINGS", `${base} must be an object`);
113
- const unknown = Object.keys(value).find((key) => key !== "olderThanDays" && key !== "maxTerminalRuns");
114
- if (unknown) fail("INVALID_SETTINGS", `Unknown retention setting at ${base}: ${unknown}`);
115
- if (value.olderThanDays !== undefined && !positiveRetentionInteger(value.olderThanDays)) fail("INVALID_SETTINGS", `${base}.olderThanDays must be a positive integer`);
116
- if (value.maxTerminalRuns !== undefined && !positiveRetentionInteger(value.maxTerminalRuns)) fail("INVALID_SETTINGS", `${base}.maxTerminalRuns must be a positive integer`);
117
- return Object.freeze({ ...(value.olderThanDays === undefined ? {} : { olderThanDays: value.olderThanDays }), ...(value.maxTerminalRuns === undefined ? {} : { maxTerminalRuns: value.maxTerminalRuns }) });
118
- }
119
- function parseSettings(path: string, partial: false): Readonly<WorkflowSettings>;
120
- function parseSettings(path: string, partial: true): Readonly<WorkflowSettingsOverrides>;
121
- function parseSettings(path: string, partial: boolean): Readonly<WorkflowSettings | WorkflowSettingsOverrides> {
122
- let parsed: unknown;
123
- try { parsed = JSON.parse(readFileSync(path, "utf8")); }
124
- catch (error) {
125
- if (isNodeError(error, "ENOENT")) return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
126
- fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
127
- }
128
- if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
129
- const allowed = new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...(partial ? [] : ["backgroundWidget"]) ]);
130
- const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
131
- if (Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources")) fail("INVALID_SETTINGS", `disabledAgentResources is no longer supported; use skills, extensions, and tools selectors (settings: ${path})`);
132
- if (unknown) fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
133
- const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
134
- if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16)) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
135
- const backgroundWidget = parsed.backgroundWidget === undefined ? (partial ? undefined : DEFAULT_SETTINGS.backgroundWidget) : parsed.backgroundWidget;
136
- if (backgroundWidget !== undefined && typeof backgroundWidget !== "boolean") fail("INVALID_SETTINGS", `${path}.backgroundWidget must be a boolean`);
137
- const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
138
- const skills = validateSelectorList(parsed.skills, path, "skills");
139
- const tools = validateSelectorList(parsed.tools, path, "tools");
140
- const extensions = validateSelectorList(parsed.extensions, path, "extensions");
141
- const extensionSettings = parsed.extensionSettings === undefined ? undefined : validateWorkflowExtensionSettings(parsed.extensionSettings, path, "INVALID_SETTINGS", !partial);
142
- const retention = validateRetention(parsed.retention, path);
143
- const worktreePostCreateCommand = validateWorktreePostCreateCommand(parsed.worktreePostCreateCommand, path);
144
- return Object.freeze({
145
- ...(concurrency === undefined ? {} : { concurrency }), ...(backgroundWidget === undefined ? {} : { backgroundWidget }), ...(modelAliases === undefined ? {} : { modelAliases }),
146
- ...(skills === undefined ? {} : { skills }), ...(extensions === undefined ? {} : { extensions }),
147
- ...(extensionSettings === undefined ? {} : { extensionSettings }), ...(tools === undefined ? {} : { tools }), ...(retention === undefined ? {} : { retention }),
148
- ...(worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand }),
149
- });
150
- }
151
- export function loadSettings(path = workflowSettingsPath()): Readonly<WorkflowSettings> { return parseSettings(path, false); }
152
- export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
153
- export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
154
- const projectSettingsPath = workflowProjectSettingsPath(cwd);
155
- const global = loadSettings(globalSettingsPath);
156
- const project: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
157
- const projectHas = (key: keyof WorkflowSettingsOverrides): boolean => Object.prototype.hasOwnProperty.call(project, key);
158
- const sourceFor = (key: "skills" | "extensions" | "tools"): string => projectHas(key) ? projectSettingsPath : globalSettingsPath;
159
- const globalSelectors = selectorsFromSettings(global);
160
- const projectSelectors = selectorsFromSettings(project);
161
- const effectiveSelectors = selectorSet({
162
- skills: [...(globalSelectors.skills ?? []), ...(projectSelectors.skills ?? [])],
163
- extensions: [...(globalSelectors.extensions ?? []), ...(projectSelectors.extensions ?? [])],
164
- ...(globalSelectors.tools === undefined && projectSelectors.tools === undefined ? {} : { tools: [...(globalSelectors.tools ?? []), ...(projectSelectors.tools ?? [])] }),
165
- });
166
- const hasExtensionSelectors = global.extensions !== undefined || project.extensions !== undefined;
167
- const extensionSettings = projectHas("extensionSettings") ? project.extensionSettings : global.extensionSettings;
168
- const sources: WorkflowSettingsSources = {
169
- concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
170
- modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
171
- skills: sourceFor("skills"), extensions: sourceFor("extensions"), tools: sourceFor("tools"),
172
- ...(extensionSettings === undefined ? {} : { extensionSettings: projectHas("extensionSettings") ? projectSettingsPath : globalSettingsPath }),
173
- ...(project.retention === undefined && global.retention === undefined ? {} : { retention: project.retention === undefined ? globalSettingsPath : projectSettingsPath }),
174
- ...(projectHas("worktreePostCreateCommand") || global.worktreePostCreateCommand !== undefined ? { worktreePostCreateCommand: projectHas("worktreePostCreateCommand") ? projectSettingsPath : globalSettingsPath } : {}),
175
- };
176
- const effective = Object.freeze({
177
- concurrency: project.concurrency ?? global.concurrency,
178
- backgroundWidget: global.backgroundWidget ?? true,
179
- ...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
180
- ...(effectiveSelectors.skills.length ? { skills: effectiveSelectors.skills } : global.skills !== undefined || project.skills !== undefined ? { skills: effectiveSelectors.skills } : {}),
181
- ...(hasExtensionSelectors ? { extensions: effectiveSelectors.extensions } : {}),
182
- ...(extensionSettings === undefined ? {} : { extensionSettings }),
183
- ...(effectiveSelectors.tools?.length ? { tools: effectiveSelectors.tools } : global.tools !== undefined || project.tools !== undefined ? { tools: effectiveSelectors.tools } : {}),
184
- ...((project.retention ?? global.retention) === undefined ? {} : { retention: project.retention ?? global.retention }),
185
- ...(projectHas("worktreePostCreateCommand") ? { worktreePostCreateCommand: project.worktreePostCreateCommand } : global.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: global.worktreePostCreateCommand }),
186
- });
187
- return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
188
- }
189
- export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
190
- for (const name of names) {
191
- try {
192
- const target = modelCapability(name, aliases, knownModels, settingsPath);
193
- if (!availableModels.has(target)) unknownModel(name, target, settingsPath);
194
- } catch (error) { throw annotateModelAliasError(error, name); }
195
- }
196
- }
197
- export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): AgentResourcePolicy {
198
- const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
199
- const global = selectorSet(selectorsFromSettings(resolved.global));
200
- const project = selectorSet(selectorsFromSettings(resolved.project));
201
- const effective = selectorSet(selectorsFromSettings(resolved.effective));
202
- return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: selectorsFromSettings(resolved.global), project: selectorsFromSettings(resolved.project) } };
203
- }
204
- export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
205
- const normalized = validateModelAliases(aliases, path);
206
- let parsed: unknown = {};
207
- try {
208
- loadSettings(path);
209
- parsed = JSON.parse(readFileSync(path, "utf8"));
210
- } catch (error) {
211
- if (!isNodeError(error, "ENOENT")) throw error;
212
- }
213
- if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
214
- mkdirSync(dirname(path), { recursive: true });
215
- atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
216
- }
217
-
218
- export function parseRoleMarkdown(content: string, strict = false, rolePath?: string): AgentDefinition {
219
- if (!strict) {
220
- if (!content.startsWith("---\n")) return { prompt: content };
221
- const end = content.indexOf("\n---", 4);
222
- if (end < 0) return { prompt: content };
223
- const meta: Record<string, string> = {};
224
- for (const line of content.slice(4, end).split("\n")) {
225
- const match = /^(model|tools|skills|extensions|description|overrideSystemPrompt|override_system_prompt|is_system_prompt|contextFiles|extensionSettings|disabledAgentResources|thinking)\s*:\s*(.+)$/.exec(line.trim());
226
- if (match?.[1] === "disabledAgentResources") fail("INVALID_METADATA", "disabledAgentResources is no longer supported; use skills, extensions, and tools selectors");
227
- if (match?.[1] === "thinking") fail("INVALID_METADATA", "Role thinking is not supported; put it on model as provider/model:thinking");
228
- if (match?.[1] && match[2]) meta[match[1]] = match[2].trim();
229
- }
230
- const unquote = (v: string) => v.replace(/^['"]|['"]$/g, "");
231
- const parseList = (value: string | undefined): string[] | undefined => value === undefined ? undefined : value.replace(/^\[|\]$/g, "").split(",").map((entry) => unquote(entry.trim())).filter(Boolean);
232
- const tools = parseList(meta.tools);
233
- const skills = parseList(meta.skills);
234
- const extensions = parseList(meta.extensions);
235
- let extensionSettings: unknown;
236
- if (meta.extensionSettings !== undefined) {
237
- try { extensionSettings = JSON.parse(meta.extensionSettings) as unknown; }
238
- catch (error) { fail("INVALID_METADATA", `Invalid role extensionSettings: ${errorText(error)}`); }
239
- }
240
- const rolePathValue = rolePath ?? "<role>";
241
- const normalizedExtensionSettings = validateWorkflowExtensionSettings(extensionSettings, rolePathValue, "INVALID_METADATA", false);
242
- const definition: AgentDefinition = { prompt: content.slice(end + 4).replace(/^\n/, "") };
243
- if (meta.model) {
244
- const model = unquote(meta.model);
245
- assertModelThinking(model, "Role model");
246
- definition.model = model;
247
- }
248
- if (meta.description) definition.description = unquote(meta.description);
249
- if (tools) definition.tools = tools;
250
- if (skills) definition.skills = skills;
251
- if (extensions) definition.extensions = extensions;
252
- if (normalizedExtensionSettings !== undefined) definition.extensionSettings = normalizedExtensionSettings;
253
- const overrideSystemPrompt = meta.overrideSystemPrompt ?? meta.override_system_prompt ?? meta.is_system_prompt;
254
- const contextFiles = meta.contextFiles ? meta.contextFiles.replace(/^\[|\]$/g, "").split(",").map((scope) => unquote(scope.trim())).filter(Boolean) : undefined;
255
- const normalizedContextFiles = validateContextFileScopes(contextFiles, "role");
256
- if (overrideSystemPrompt) definition.overrideSystemPrompt = overrideSystemPrompt === "true";
257
- if (normalizedContextFiles) definition.contextFiles = normalizedContextFiles;
258
- return definition;
259
- }
260
- const normalized = content.replace(/\r\n?/g, "\n");
261
- if (normalized.startsWith("---\n") && normalized.indexOf("\n---", 3) < 0) fail("INVALID_METADATA", "Role frontmatter is missing its closing delimiter");
262
- let parsed: ReturnType<typeof parseFrontmatter>;
263
- try { parsed = parseFrontmatter(content); }
264
- catch (error) { fail("INVALID_METADATA", `Invalid role frontmatter: ${errorText(error)}`); }
265
- if (!object(parsed.frontmatter)) fail("INVALID_METADATA", "Role frontmatter must be an object");
266
- const { model, tools, skills, extensions, extensionSettings, description, contextFiles } = parsed.frontmatter;
267
- if (Object.prototype.hasOwnProperty.call(parsed.frontmatter, "disabledAgentResources")) fail("INVALID_METADATA", "disabledAgentResources is no longer supported; use skills, extensions, and tools selectors");
268
- if (Object.prototype.hasOwnProperty.call(parsed.frontmatter, "thinking")) fail("INVALID_METADATA", "Role thinking is not supported; put it on model as provider/model:thinking");
269
- const overrideSystemPrompt = parsed.frontmatter.overrideSystemPrompt ?? parsed.frontmatter.override_system_prompt ?? parsed.frontmatter.is_system_prompt;
270
- if (model !== undefined && (typeof model !== "string" || model.trim() === "")) fail("INVALID_METADATA", "Role model must be a non-empty string");
271
- if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description))) fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
272
- if (overrideSystemPrompt !== undefined && typeof overrideSystemPrompt !== "boolean") fail("INVALID_METADATA", "Role overrideSystemPrompt must be a boolean");
273
- const normalizedContextFiles = validateContextFileScopes(contextFiles, rolePath ?? "<role>");
274
- const rolePathValue = rolePath ?? "<role>";
275
- const normalizedTools = validateSelectorList(tools, rolePathValue, "tools", "INVALID_METADATA");
276
- const normalizedSkills = validateSelectorList(skills, rolePathValue, "skills", "INVALID_METADATA");
277
- const normalizedExtensions = validateSelectorList(extensions, rolePathValue, "extensions", "INVALID_METADATA");
278
- const normalizedExtensionSettings = validateWorkflowExtensionSettings(extensionSettings, rolePathValue, "INVALID_METADATA", false);
279
- const normalizedDescription = typeof description === "string" ? description.trim() : undefined;
280
- const normalizedModel = typeof model === "string" ? model.trim() : undefined;
281
- if (normalizedModel !== undefined) assertModelThinking(normalizedModel, "Role model");
282
- const definition: AgentDefinition = { prompt: parsed.body };
283
- if (normalizedDescription !== undefined) definition.description = normalizedDescription;
284
- if (normalizedModel !== undefined) definition.model = normalizedModel;
285
- if (normalizedTools !== undefined) definition.tools = normalizedTools;
286
- if (normalizedSkills !== undefined) definition.skills = normalizedSkills;
287
- if (normalizedExtensions !== undefined) definition.extensions = normalizedExtensions;
288
- if (normalizedExtensionSettings !== undefined) definition.extensionSettings = normalizedExtensionSettings;
289
- if (typeof overrideSystemPrompt === "boolean") definition.overrideSystemPrompt = overrideSystemPrompt;
290
- if (normalizedContextFiles !== undefined) definition.contextFiles = normalizedContextFiles;
291
- return definition;
292
- }
293
-
294
- const ROLE_DIRECTORY = "pi-extensible-workflows";
295
-
296
- export function workflowRoleDirectories(agentDir = getAgentDir()): readonly string[] {
297
- return [join(agentDir, ROLE_DIRECTORY, "roles")];
298
- }
299
-
300
- function projectRoleDirectories(root: string): readonly string[] {
301
- return [join(root, ROLE_DIRECTORY, "roles")];
302
- }
303
-
304
- type RoleDirectorySource = { path: string; extension?: WorkflowExtensionMetadata; builtin?: true };
305
- export type WorkflowRoleDirectoryInput = string | WorkflowRoleDirectoryRegistration;
306
- type RoleFile = { name: string; path: string; source: RoleDirectorySource };
307
- function roleDirectorySources(dirs: readonly WorkflowRoleDirectoryInput[]): RoleDirectorySource[] {
308
- const seen = new Set<string>();
309
- return dirs.flatMap((value) => {
310
- const source = typeof value === "string" ? { path: value } : { path: value.path, extension: value.extension, ...(value.builtin === true ? { builtin: true as const } : {}) };
311
- if (seen.has(source.path)) return [];
312
- seen.add(source.path);
313
- return [source];
314
- });
315
- }
316
- function roleDirectoryLabel(source: RoleDirectorySource, extension = true): string {
317
- return source.extension ? `Extension "${source.extension.headline}" (${source.extension.version})` : extension ? "Registered workflow extension" : "Standard workflow";
318
- }
319
- function isRoleFile(dir: string, entry: import("node:fs").Dirent): boolean {
320
- if (extname(entry.name) !== ".md") return false;
321
- if (entry.isFile()) return true;
322
- if (!entry.isSymbolicLink()) return false;
323
- try { return statSync(join(dir, entry.name)).isFile(); }
324
- catch (error) { if (isNodeError(error, "ENOENT")) return false; throw error; }
325
- }
326
- function scanRoleFiles(dirs: readonly WorkflowRoleDirectoryInput[], extension: boolean): RoleFile[] {
327
- const files: RoleFile[] = [];
328
- for (const source of roleDirectorySources(dirs)) {
329
- let entries: import("node:fs").Dirent[];
330
- try { entries = readdirSync(source.path, { withFileTypes: true }); }
331
- catch (error) {
332
- if (!extension && isNodeError(error, "ENOENT")) continue;
333
- fail("INVALID_METADATA", `${roleDirectoryLabel(source, extension)} role directory "${source.path}" could not be scanned: ${errorText(error)}`);
334
- }
335
- for (const entry of entries) if (isRoleFile(source.path, entry)) files.push({ name: basename(entry.name, ".md"), path: join(source.path, entry.name), source });
336
- }
337
- return files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
338
- }
339
- function readRoleDefinitions(dirs: readonly WorkflowRoleDirectoryInput[], extension = false): Record<string, AgentDefinition> {
340
- const files = scanRoleFiles(dirs, extension);
341
- const starterFiles = files.filter(({ source }) => source.builtin === true);
342
- const regularFiles = files.filter(({ source }) => source.builtin !== true);
343
- if (extension) {
344
- const byName = new Map<string, RoleFile[]>();
345
- for (const file of regularFiles) byName.set(file.name, [...(byName.get(file.name) ?? []), file]);
346
- for (const [name, matches] of byName) if (matches.length > 1) fail("INVALID_METADATA", `Duplicate extension role "${name}": ${matches.map(({ path, source }) => `${roleDirectoryLabel(source)} role directory "${source.path}" (${path})`).join("; ")}`);
347
- }
348
- const read = (roleFiles: readonly RoleFile[]): Record<string, AgentDefinition> => Object.fromEntries(roleFiles.map(({ name, path, source }) => {
349
- try { return [name, parseRoleMarkdown(readFileSync(path, "utf8"), true, path)]; }
350
- catch (error) {
351
- if (extension) fail("INVALID_METADATA", `${roleDirectoryLabel(source)} role directory "${source.path}" contains invalid role "${name}" at "${path}": ${errorText(error)}`);
352
- throw error;
353
- }
354
- }));
355
- return extension ? { ...read(starterFiles), ...read(regularFiles) } : read(files);
356
- }
357
- export function loadProjectAgentDefinitions(cwd: string): Readonly<Record<string, AgentDefinition>> { return readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))); }
358
- export function loadAgentDefinitions(cwd: string, agentDir = getAgentDir(), projectTrusted = true, extensionRoleDirectories: readonly WorkflowRoleDirectoryInput[] = registeredWorkflowRoleDirectoryRegistrations()): Readonly<Record<string, AgentDefinition>> {
359
- return deepFreeze({ ...readRoleDefinitions(extensionRoleDirectories, true), ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
360
- }
361
24
  function validateRolePolicies(definitions: Readonly<Record<string, AgentDefinition>>, roles: readonly string[], availableModels: ReadonlySet<string>, aliases: Readonly<Record<string, string>> = {}, knownModels = availableModels, settingsPath?: string): void {
362
25
  for (const role of roles) {
363
26
  const definition = definitions[role];
@@ -556,6 +219,24 @@ function callHasTrailingComma(source: string, call: WorkflowCall): boolean {
556
219
  return current?.type.label === ")" && previous?.type.label === ",";
557
220
  }
558
221
 
222
+ /**
223
+ * The `tools.<id>` members a workflow script names statically. Any other use of `tools`, such as a
224
+ * local declaration that shadows the global or passing it around, makes the set unknowable: undefined.
225
+ */
226
+ export function scriptToolReferences(script: string): string[] | undefined {
227
+ const body = workflowBody(script);
228
+ if (!body.trim()) return [];
229
+ const names = new Set<string>();
230
+ const opaque = (node: acorn.AnyNode, parent?: acorn.AnyNode): boolean => {
231
+ if (node.type === "Identifier" && node.name === "tools") {
232
+ if (parent?.type === "MemberExpression" && parent.object === node && !parent.computed && parent.property.type === "Identifier") names.add(parent.property.name);
233
+ else if (!(parent?.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand || parent?.type === "MemberExpression" && parent.property === node && !parent.computed)) return true;
234
+ }
235
+ return astChildren(node).some((child) => opaque(child, node));
236
+ };
237
+ return opaque(parseWorkflow(body)) ? undefined : [...names];
238
+ }
239
+
559
240
  export function instrumentWorkflow(script: string): string {
560
241
  const body = workflowBody(script);
561
242
  if (!body.trim()) return body;
@@ -628,13 +309,6 @@ export function workflowPrompt(template: string, values: Readonly<Record<string,
628
309
  });
629
310
  }
630
311
 
631
- export function validateSchema(schema: unknown, at = "schema"): asserts schema is JsonSchema {
632
- if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema)) fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
633
- if (typeof schema.type !== "string" && !Array.isArray(schema.type) && schema.$ref === undefined && schema.anyOf === undefined && schema.oneOf === undefined && schema.allOf === undefined && schema.const === undefined && schema.enum === undefined) fail("INVALID_SCHEMA", `${at} has no JSON Schema shape`);
634
- if (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string"))) fail("INVALID_SCHEMA", `${at}.required must be an array of strings`);
635
- if (schema.properties !== undefined && !object(schema.properties)) fail("INVALID_SCHEMA", `${at}.properties must be an object`);
636
- }
637
-
638
312
  const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "skills", "extensions", "contextFiles", "role", "outputSchema", "retries", "timeoutMs"]);
639
313
  function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
640
314
  switch (key) {
@@ -69,5 +69,3 @@ export const reviewLoop = defineWorkflowFunction({
69
69
  ),
70
70
  });
71
71
 
72
- export const reviewLoopInputSchema = reviewLoop.input;
73
- export const reviewLoopOutputSchema = reviewLoop.output;
@@ -44,7 +44,7 @@ subagents_run({ prompt: "Summarize README.md.", mode: "foreground" })
44
44
 
45
45
  ## Interactive inspection
46
46
 
47
- In Pi's TUI, `/subagents` opens a picker of durable standalone runs for the current session. Select a run to inspect the same activity, stall warning, state, model, role, tools, attempts, duration, cumulative token accounting, cost, and error fields used by `/workflow`; prompt, request failure, and result details remain bounded, and requests without a role remain `role=none`. The detail view also exposes registered standalone agent actions plus `Steer` and `Stop` while running, `Retry` for failed or stopped runs, and copy/editor controls where applicable. Inspection does not launch a new LLM call; lifecycle actions may steer, stop, or retry a run.
47
+ In Pi's TUI, `/subagents` opens the `/workflow` dashboard layout for the durable standalone runs of the current session: the run list sits beside the selected run's details at 80 columns or wider, and narrower terminals drill down from the list to the details. The details show the run ID and the same activity, stall warning, state, model, role, tools, attempts, duration, cumulative token accounting, cost, and error fields as a `/workflow` agent; a running run shows its live activity, which `subagents_inspect` leaves out. Requests without a role show `Role: (none)`. While a run is active the view refreshes the active runs and the selection every second; runs launched while it is open appear after an action or when it reopens. `a` or `enter` opens registered standalone agent actions, editor controls for the prompt, the live system prompt, and the result, `Steer` and `Stop` while running, `Retry` for failed or stopped runs, and the `/workflow` run actions that apply: `Delete`, `Delete all completed`, `Delete all failed`, `Copy run path`, and `Copy agent ID`. Stop and every deletion ask for confirmation, and a run whose worktree still awaits cleanup cannot be deleted. Outside the TUI, `/subagents` opens a picker with the same run summary and bulk deletions as the `/workflow` picker. Background runs appear below the editor in the workflow widget's frame, one row per run with its model, tokens, cost, elapsed time, and quiet or stalled warning, and each leaves a receipt in the transcript when it settles. Inspection does not launch a new LLM call; lifecycle actions may steer, stop, retry, or delete a run.
48
48
 
49
49
  ## Tools
50
50
 
@@ -53,14 +53,14 @@ Every tool schema is a closed object. Unknown properties are rejected. The model
53
53
  | Tool | Input schema |
54
54
  | --- | --- |
55
55
  | `subagents_run` | `{ prompt: string, mode?: "background" \| "foreground", label?: string, model?: string, tools?: string[], skills?: string[], extensions?: string[], contextFiles?: string[], role?: string, worktree?: string, outputSchema?: object, retries?: integer 0..255, timeoutMs?: positive integer \| null }` |
56
- | `subagents_inspect` | `{ id?: string }` |
56
+ | `subagents_inspect` | `{ id?: string, scope?: "session" \| "all" }` |
57
57
  | `subagents_steer` | `{ id: string, message: string }` |
58
58
  | `subagents_stop` | `{ id: string }` |
59
59
  | `subagents_retry` | `{ id: string }` |
60
60
 
61
61
  `prompt` is the only required `subagents_run` property. `mode` defaults to "background". Resource candidates start enabled, so plain positive top-level selector lists are additive and top-level `[]` adds no matches. Use `["!*", "read", "grep"]` to restrict a selector to those resources, or `["!*"]` to select none. A `role` string selects an existing workflow role. Model, tools, skills, extensions, and contextFiles are top-level `AgentOptions` overrides applied after the role file. Concrete models are `provider/model:thinking`. Capability selectors use ordered minimatch rules and are applied after global, trusted-project, and role selectors. Use `tools: ["*"]` to re-enable all tools after a role restriction.
62
62
 
63
- `subagents_inspect({})` returns all accessible run summaries ordered by start time. `subagents_inspect({ id })` returns the detailed lifecycle record, including state, start and finish timestamps, and the live snapshot under `progress`: `state`, cumulative `accounting`, `toolCalls`, `activity`, and `lastEventAt`. The snapshot state never includes the effective system prompt, and inspection has no `usage` field; token totals are derived from accounting. Materialized worktree path and branch are included when available. For completed runs it also includes `value`; for failed runs it includes `error`. A running run has no terminal value yet. Unknown IDs fail with `RUN_NOT_FOUND`.
63
+ `subagents_inspect({})` returns the current Pi session's run summaries ordered by start time; `subagents_inspect({ scope: "all" })` lists every stored run. `subagents_inspect({ id })` returns the detailed lifecycle record, including state, start and finish timestamps, and the live snapshot under `progress`: `state`, cumulative `accounting`, `toolCalls`, `activity`, and `lastEventAt`. The snapshot state never includes the effective system prompt, and inspection has no `usage` field; token totals are derived from accounting. Materialized worktree path and branch are included when available. For completed runs it also includes `value`; for failed runs it includes `error`. A running run has no terminal value yet. Unknown IDs fail with `RUN_NOT_FOUND`.
64
64
 
65
65
  ## Launching and concurrency
66
66
 
@@ -41,6 +41,7 @@ export const SUBAGENTS_RUN_PARAMETERS = Type.Object({
41
41
 
42
42
  export const SUBAGENTS_INSPECT_PARAMETERS = Type.Object({
43
43
  id: Type.Optional(Type.String({ description: "Subagent ID; omit to list ordered run summaries" })),
44
+ scope: Type.Optional(Type.Union([Type.Literal("session"), Type.Literal("all")], { description: "List scope when id is omitted: session (default) lists runs from the current Pi session; all lists every stored run" })),
44
45
  }, { additionalProperties: false });
45
46
 
46
47
  export const SUBAGENTS_ID_PARAMETERS = Type.Object({
@@ -60,13 +61,23 @@ export const SUBAGENTS_RETRY_PARAMETERS = Type.Object({
60
61
  id: Type.String({ description: "Failed or stopped subagent ID to retry" }),
61
62
  }, { additionalProperties: false });
62
63
 
63
- export const SUBAGENTS_TOOL_SCHEMAS = {
64
- subagents_run: SUBAGENTS_RUN_PARAMETERS,
65
- subagents_inspect: SUBAGENTS_INSPECT_PARAMETERS,
66
- subagents_steer: SUBAGENTS_STEER_PARAMETERS,
67
- subagents_stop: SUBAGENTS_STOP_PARAMETERS,
68
- subagents_retry: SUBAGENTS_RETRY_PARAMETERS,
69
- } as const;
64
+ // Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
65
+ export const SUBAGENT_STATUS_OUTPUT = Type.Object({
66
+ id: Type.String(),
67
+ state: Type.String(),
68
+ sessionId: Type.Optional(Type.String()),
69
+ startedAt: Type.Optional(Type.Number()),
70
+ finishedAt: Type.Optional(Type.Number()),
71
+ attempts: Type.Optional(Type.Number()),
72
+ attemptDetails: Type.Optional(Type.Array(Type.Unknown())),
73
+ worktree: Type.Optional(Type.Object({ path: Type.String(), branch: Type.String() })),
74
+ error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String() })),
75
+ progress: Type.Optional(Type.Unknown()),
76
+ value: Type.Optional(Type.Unknown()),
77
+ });
78
+ // One run when id is given, otherwise the ordered run summaries; the summaries stay short in the codemode description.
79
+ export const SUBAGENTS_INSPECT_OUTPUT = Type.Union([SUBAGENT_STATUS_OUTPUT, Type.Array(Type.Object({ id: Type.String(), state: Type.String() }))]);
80
+ export const SUBAGENTS_STEER_OUTPUT = Type.Object({ id: Type.String(), accepted: Type.Boolean() });
70
81
 
71
82
  export type SubagentRunRequest = Static<typeof SUBAGENTS_RUN_PARAMETERS>;
72
83
  export function normalizeSubagentRunRequest(value: unknown): SubagentRunRequest {
@@ -114,6 +125,8 @@ export interface SubagentManagerContext {
114
125
  readonly extensionContext: ExtensionContext;
115
126
  readonly waitForForeground?: boolean;
116
127
  readonly includeAttemptMetadata?: boolean;
128
+ /** Adds the live progress and activity of an active run; tool results leave streamed activity out. */
129
+ readonly includeActivity?: boolean;
117
130
  }
118
131
  export interface SubagentAttemptActionData {
119
132
  readonly attempt: AgentAttemptSummary;
@@ -160,6 +173,8 @@ export interface SubagentManager {
160
173
  steer(request: Readonly<SubagentSteerRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
161
174
  stop(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
162
175
  retry(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
176
+ /** Removes a settled run record; used by the /subagents navigator only. */
177
+ delete?(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
163
178
  getAttemptActionData?(id: string): Readonly<SubagentAttemptActionData> | undefined;
164
179
  dispose?(): Promise<void>;
165
180
  }
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { defineTool, getAgentDir, type AgentToolResult, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
3
3
  import { Value } from "typebox/value";
4
- import { WorkflowError, loadingRegistry } from "../../src/index.js";
4
+ import { WorkflowError, loadCodemodeToolsSetting, loadingRegistry, reachableTools, workflowToolExposure, type JsonValue } from "../../src/index.js";
5
5
  import { clearSubagentManager, setSubagentManager } from "../../src/subagent-manager-handle.js";
6
6
  import type { SubagentIdRequest, SubagentInspectRequest, SubagentManager, SubagentManagerContext, SubagentNotification, SubagentsExtension, SubagentsExtensionOptions, SubagentRunRequest, SubagentStatus, SubagentSteerRequest } from "./contracts.js";
7
7
  import { createSubagentManager } from "./manager.js";
@@ -9,10 +9,13 @@ import { registerSubagentNavigator } from "./navigator.js";
9
9
  import { createSubagentBackgroundWidget, renderSubagentCall, renderSubagentControlCall, renderSubagentControlResult, renderSubagentInspectCall, renderSubagentInspectResult, renderSubagentResult } from "./view.js";
10
10
  import {
11
11
  normalizeSubagentRunRequest,
12
+ SUBAGENT_STATUS_OUTPUT,
12
13
  SUBAGENTS_ID_PARAMETERS,
14
+ SUBAGENTS_INSPECT_OUTPUT,
13
15
  SUBAGENTS_INSPECT_PARAMETERS,
14
16
  SUBAGENTS_RETRY_PARAMETERS,
15
17
  SUBAGENTS_RUN_PARAMETERS,
18
+ SUBAGENTS_STEER_OUTPUT,
16
19
  SUBAGENTS_STEER_PARAMETERS,
17
20
  SUBAGENTS_STOP_PARAMETERS,
18
21
  } from "./contracts.js";
@@ -23,7 +26,7 @@ export { createSubagentManager, createUnavailableSubagentManager } from "./manag
23
26
  export { createRunStoreWorktreeAdapter, defaultWorktreeHome } from "./worktree.js";
24
27
  export type { SubagentWorktreeAdapter, SubagentWorktreeContext, SubagentWorktreeHandle, SubagentWorktreeRunStore } from "./worktree.js";
25
28
 
26
- type SubagentsExtensionAPI = Pick<ExtensionAPI, "registerTool"> & Partial<Pick<ExtensionAPI, "getActiveTools" | "on" | "sendMessage" | "registerCommand">>;
29
+ type SubagentsExtensionAPI = Pick<ExtensionAPI, "registerTool"> & Partial<Pick<ExtensionAPI, "getActiveTools" | "getAllTools" | "on" | "sendMessage" | "registerCommand" | "appendEntry" | "registerEntryRenderer">>;
27
30
 
28
31
  function validateSubagentRunRequest(value: unknown): SubagentRunRequest {
29
32
  return normalizeSubagentRunRequest(value);
@@ -55,7 +58,7 @@ function serialize(value: unknown): string {
55
58
  }
56
59
 
57
60
  function toolResult(value: unknown): AgentToolResult<unknown> {
58
- return { content: [{ type: "text", text: serialize(value) }], details: value };
61
+ return { content: [{ type: "text", text: serialize(value) }], details: value, structuredContent: value as JsonValue };
59
62
  }
60
63
 
61
64
  function managerContext(toolCallId: string, signal: AbortSignal | undefined, onUpdate: ((value: AgentToolResult<unknown>) => void) | undefined, context: ExtensionContext): SubagentManagerContext {
@@ -78,6 +81,7 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
78
81
  "Do not poll a running ID; use subagents_inspect({ id }) when you need its current status or terminal result.",
79
82
  ],
80
83
  parameters: SUBAGENTS_RUN_PARAMETERS,
84
+ outputSchema: SUBAGENT_STATUS_OUTPUT,
81
85
  async execute(toolCallId, params, signal, onUpdate, context) {
82
86
  return toolResult(await manager.run(validateSubagentRunRequest(params), managerContext(toolCallId, signal, onUpdate, context)));
83
87
  },
@@ -87,8 +91,9 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
87
91
  defineTool({
88
92
  name: "subagents_inspect",
89
93
  label: "Subagents Inspect",
90
- description: "Inspect durable subagent runs. Omit id for ordered run summaries, or provide id for detailed status, progress, activity, accounting, tool calls, timestamps, worktree metadata, and a terminal value or error when available.",
94
+ description: "Inspect durable subagent runs. Omit id for ordered run summaries of the current session (scope all for every session), or provide id for detailed status, progress, activity, accounting, tool calls, timestamps, worktree metadata, and a terminal value or error when available.",
91
95
  parameters: SUBAGENTS_INSPECT_PARAMETERS,
96
+ outputSchema: SUBAGENTS_INSPECT_OUTPUT,
92
97
  async execute(toolCallId, params, signal, onUpdate, context) {
93
98
  return toolResult(await manager.inspect(validateSubagentInspectRequest(params), managerContext(toolCallId, signal, onUpdate, context)));
94
99
  },
@@ -100,6 +105,7 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
100
105
  label: "Subagents Steer",
101
106
  description: "Send a message to a running subagent. The message is queued safely if its steering handler is not ready; settled runs cannot be steered.",
102
107
  parameters: SUBAGENTS_STEER_PARAMETERS,
108
+ outputSchema: SUBAGENTS_STEER_OUTPUT,
103
109
  async execute(toolCallId, params, signal, onUpdate, context) {
104
110
  return toolResult(await manager.steer(validateSubagentSteerRequest(params), managerContext(toolCallId, signal, onUpdate, context)));
105
111
  },
@@ -111,6 +117,7 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
111
117
  label: "Subagents Stop",
112
118
  description: "Stop one running subagent, abort its active session, persist state \"stopped\", and clean its worktree without affecting sibling runs.",
113
119
  parameters: SUBAGENTS_STOP_PARAMETERS,
120
+ outputSchema: SUBAGENT_STATUS_OUTPUT,
114
121
  async execute(toolCallId, params, signal, onUpdate, context) {
115
122
  return toolResult(await manager.stop(validateSubagentIdRequest(params, "subagents_stop"), managerContext(toolCallId, signal, onUpdate, context)));
116
123
  },
@@ -122,6 +129,7 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
122
129
  label: "Subagents Retry",
123
130
  description: "Start a fresh run from a failed or stopped subagent's persisted request. The new run gets a new ID and preserves its original background or foreground mode.",
124
131
  parameters: SUBAGENTS_RETRY_PARAMETERS,
132
+ outputSchema: SUBAGENT_STATUS_OUTPUT,
125
133
  async execute(toolCallId, params, signal, onUpdate, context) {
126
134
  return toolResult(await manager.retry(validateSubagentIdRequest(params, "subagents_retry"), managerContext(toolCallId, signal, onUpdate, context)));
127
135
  },
@@ -162,7 +170,8 @@ export function createSubagentsExtension(options: SubagentsExtensionOptions = {}
162
170
 
163
171
  export function registerSubagentsExtension(pi: SubagentsExtensionAPI, options: SubagentsExtensionOptions = {}): SubagentsExtension {
164
172
  const getActiveTools = pi.getActiveTools;
165
- const activeTools = getActiveTools === undefined ? undefined : () => getActiveTools.call(pi);
173
+ const getAllTools = pi.getAllTools;
174
+ const activeTools = getActiveTools === undefined ? undefined : () => reachableTools({ getActiveTools: () => getActiveTools.call(pi), ...(getAllTools === undefined ? {} : { getAllTools: () => getAllTools.call(pi) }) });
166
175
  const sendMessage = pi.sendMessage;
167
176
  const notify = sendMessage === undefined ? undefined : (notification: SubagentNotification): void => {
168
177
  sendMessage.call(pi, { customType: "subagents", content: notificationContent(notification), display: true, details: notification }, { deliverAs: "steer", triggerTurn: true });
@@ -170,9 +179,11 @@ export function registerSubagentsExtension(pi: SubagentsExtensionAPI, options: S
170
179
  const onResourceWarning = sendMessage === undefined ? undefined : (message: string): void => {
171
180
  sendMessage.call(pi, { customType: "subagents", content: `Warning: ${message}`, display: true }, { deliverAs: "steer" });
172
181
  };
173
- const widget = createSubagentBackgroundWidget();
182
+ const appendEntry = pi.appendEntry;
183
+ const widget = createSubagentBackgroundWidget({ ...(appendEntry === undefined ? {} : { appendEntry: (customType, data) => { appendEntry.call(pi, customType, data); } }), ...(pi.registerEntryRenderer === undefined ? {} : { registerEntryRenderer: pi.registerEntryRenderer.bind(pi) }) });
174
184
  const extension = createSubagentsExtension(options, activeTools, notify, (status, request) => { widget.update(status, request); const registry = loadingRegistry(); if (typeof registry.observeSubagentStatus === "function") registry.observeSubagentStatus(status, request); }, onResourceWarning);
175
- for (const tool of extension.tools) pi.registerTool(tool);
185
+ const codemodeTools = loadCodemodeToolsSetting(options.managerDependencies?.agentDir);
186
+ for (const tool of extension.tools) pi.registerTool({ ...tool, ...workflowToolExposure(tool.name, codemodeTools) });
176
187
  if (pi.registerCommand !== undefined) registerSubagentNavigator(pi.registerCommand.bind(pi), extension.manager, storageDirectory(options), options.clipboard);
177
188
  if (pi.on !== undefined) {
178
189
  setSubagentManager(extension.manager);