@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
@@ -0,0 +1,224 @@
1
+ import { atomicWriteFile } from "./persistence.js";
2
+ import { mkdirSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { getAgentDir, type ToolAnnotations } from "@earendil-works/pi-coding-agent";
7
+ import type { AgentResourcePolicy, AgentResourceSelectors, AgentResourceSelectorSet, ContextFileScope, JsonValue, WorkflowExtensionSettings, WorkflowRetentionSettings, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowSettingsSources } from "./types.js";
8
+ import { isContextFileScope } from "./types.js";
9
+ import { annotateModelAliasError, deepFreeze, errorText, fail, isNodeError, jsonValue, modelCapability, object, positiveInteger, resourcePatternHasMagic, unknownModel, validateModelAliases, validateResourcePattern, validWorkflowExtensionNamespace } from "./utils.js";
10
+ import { canonicalPath, workflowProjectSettingsPath } from "./paths.js";
11
+ export { workflowProjectSettingsPath } from "./paths.js";
12
+
13
+ const ROLE_DIRECTORY = "pi-extensible-workflows";
14
+ export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ concurrency: 8, backgroundWidget: true });
15
+ export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
16
+ function normalizedResourcePath(value: string, settingsPath: string): string {
17
+ // Built-in extensions are named `builtin:<name>`, not by a path.
18
+ if (value === "*" || value.startsWith("builtin:")) return value;
19
+ let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
20
+ if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
21
+ const resolved = resolve(dirname(settingsPath), expanded);
22
+ if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\")) return expanded;
23
+ if (resourcePatternHasMagic(expanded)) {
24
+ const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
25
+ const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
26
+ const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
27
+ const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
28
+ const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
29
+ return `${canonicalPath(prefix)}${suffix}`;
30
+ }
31
+ return canonicalPath(resolved);
32
+ }
33
+ export function validateSelectorList(value: unknown, path: string, kind: "skills" | "extensions" | "tools", errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS", normalizeExtensions = true): readonly string[] | undefined {
34
+ if (value === undefined) return undefined;
35
+ if (!Array.isArray(value)) fail(errorCode, `${path}.${kind} must be an array`);
36
+ const normalized: string[] = [];
37
+ for (const [index, entry] of value.entries()) {
38
+ if (typeof entry !== "string" || !entry.trim()) fail(errorCode, `${path}.${kind}[${String(index)}] must be a non-empty string`);
39
+ let selector = entry.trim();
40
+ if (kind === "extensions" && normalizeExtensions) {
41
+ const negated = selector.startsWith("!");
42
+ const body = negated ? selector.slice(1) : selector;
43
+ if (!body) fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
44
+ try { selector = `${negated ? "!" : ""}${normalizedResourcePath(body, path)}`; } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`); }
45
+ }
46
+ try { validateResourcePattern(selector); } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`); }
47
+ normalized.push(selector);
48
+ }
49
+ return Object.freeze(normalized);
50
+ }
51
+ function validateWorktreePostCreateCommand(value: unknown, settingsPath: string): readonly string[] | undefined {
52
+ if (value === undefined) return undefined;
53
+ if (!Array.isArray(value) || value.length === 0) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand must be a non-empty array`);
54
+ return Object.freeze(value.map((entry, index) => {
55
+ if (typeof entry !== "string" || !entry.trim()) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand[${String(index)}] must be a non-empty string`);
56
+ return entry;
57
+ }));
58
+ }
59
+ function selectorsFromSettings(settings: Readonly<WorkflowSettings | WorkflowSettingsOverrides>): AgentResourceSelectors {
60
+ return {
61
+ ...(settings.skills === undefined ? {} : { skills: settings.skills }),
62
+ ...(settings.extensions === undefined ? {} : { extensions: settings.extensions }),
63
+ ...(settings.tools === undefined ? {} : { tools: settings.tools }),
64
+ };
65
+ }
66
+ function selectorSet(value: AgentResourceSelectors | undefined): AgentResourceSelectorSet { return { skills: [...(value?.skills ?? [])], extensions: [...(value?.extensions ?? [])], ...(value?.tools === undefined ? {} : { tools: [...value.tools] }) }; }
67
+ export function validateContextFileScopes(value: unknown, rolePath: string): readonly ContextFileScope[] | undefined {
68
+ if (value === undefined) return undefined;
69
+ if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${rolePath}.contextFiles must be an array containing only global, project, or cwd`);
70
+ return [...value];
71
+ }
72
+ export function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS"): WorkflowExtensionSettings | undefined {
73
+ if (value === undefined) return undefined;
74
+ const base = `${settingsPath}.extensionSettings`;
75
+ if (!object(value)) fail(errorCode, `${base} must be an object`);
76
+ const normalized: Record<string, JsonValue> = {};
77
+ for (const [namespace, raw] of Object.entries(value)) {
78
+ if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${base} contains an invalid namespace: ${namespace}`);
79
+ if (namespace === "herdr") {
80
+ if (!object(raw)) fail(errorCode, `${base}.herdr must be an object`);
81
+ if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode, `${base}.herdr contains an unsupported setting`);
82
+ if (raw.enableFullyInspectableMode !== undefined && typeof raw.enableFullyInspectableMode !== "boolean") fail(errorCode, `${base}.herdr.enableFullyInspectableMode must be a boolean`);
83
+ normalized.herdr = Object.freeze({ ...(raw.enableFullyInspectableMode === undefined ? {} : { enableFullyInspectableMode: raw.enableFullyInspectableMode }) });
84
+ continue;
85
+ }
86
+ if (namespace === "trajectory") {
87
+ if (!object(raw)) fail(errorCode, `${base}.trajectory must be an object`);
88
+ if (Object.keys(raw).some((key) => key !== "port")) fail(errorCode, `${base}.trajectory contains an unsupported setting`);
89
+ if (raw.port !== undefined && (!positiveInteger(raw.port) || raw.port > 65535)) fail(errorCode, `${base}.trajectory.port must be an integer from 1 to 65535`);
90
+ normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }) });
91
+ continue;
92
+ }
93
+ if (!jsonValue(raw)) fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
94
+ normalized[namespace] = structuredClone(raw);
95
+ }
96
+ return deepFreeze(normalized as WorkflowExtensionSettings);
97
+ }
98
+ function positiveRetentionInteger(value: unknown): value is number { return positiveInteger(value) && Number.isSafeInteger(value); }
99
+ function validateRetention(value: unknown, settingsPath: string): Readonly<WorkflowRetentionSettings> | undefined {
100
+ if (value === undefined) return undefined;
101
+ const base = `${settingsPath}.retention`;
102
+ if (!object(value)) fail("INVALID_SETTINGS", `${base} must be an object`);
103
+ const unknown = Object.keys(value).find((key) => key !== "olderThanDays" && key !== "maxTerminalRuns");
104
+ if (unknown) fail("INVALID_SETTINGS", `Unknown retention setting at ${base}: ${unknown}`);
105
+ if (value.olderThanDays !== undefined && !positiveRetentionInteger(value.olderThanDays)) fail("INVALID_SETTINGS", `${base}.olderThanDays must be a positive integer`);
106
+ if (value.maxTerminalRuns !== undefined && !positiveRetentionInteger(value.maxTerminalRuns)) fail("INVALID_SETTINGS", `${base}.maxTerminalRuns must be a positive integer`);
107
+ return Object.freeze({ ...(value.olderThanDays === undefined ? {} : { olderThanDays: value.olderThanDays }), ...(value.maxTerminalRuns === undefined ? {} : { maxTerminalRuns: value.maxTerminalRuns }) });
108
+ }
109
+ function parseSettings(path: string, partial: false): Readonly<WorkflowSettings>;
110
+ function parseSettings(path: string, partial: true): Readonly<WorkflowSettingsOverrides>;
111
+ function parseSettings(path: string, partial: boolean): Readonly<WorkflowSettings | WorkflowSettingsOverrides> {
112
+ let parsed: unknown;
113
+ try { parsed = JSON.parse(readFileSync(path, "utf8")); }
114
+ catch (error) {
115
+ if (isNodeError(error, "ENOENT")) return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
116
+ fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
117
+ }
118
+ if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
119
+ const allowed = new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...(partial ? [] : ["backgroundWidget", "codemodeTools"]) ]);
120
+ const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
121
+ if (Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources")) fail("INVALID_SETTINGS", `disabledAgentResources is no longer supported; use skills, extensions, and tools selectors (settings: ${path})`);
122
+ if (unknown) fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
123
+ const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
124
+ if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16)) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
125
+ const backgroundWidget = parsed.backgroundWidget === undefined ? (partial ? undefined : DEFAULT_SETTINGS.backgroundWidget) : parsed.backgroundWidget;
126
+ if (backgroundWidget !== undefined && typeof backgroundWidget !== "boolean") fail("INVALID_SETTINGS", `${path}.backgroundWidget must be a boolean`);
127
+ const codemodeTools = parsed.codemodeTools;
128
+ if (codemodeTools !== undefined && codemodeTools !== "all" && codemodeTools !== "read-only" && codemodeTools !== "none") fail("INVALID_SETTINGS", `${path}.codemodeTools must be "all", "read-only", or "none"`);
129
+ const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
130
+ const skills = validateSelectorList(parsed.skills, path, "skills");
131
+ const tools = validateSelectorList(parsed.tools, path, "tools");
132
+ const extensions = validateSelectorList(parsed.extensions, path, "extensions");
133
+ const extensionSettings = parsed.extensionSettings === undefined ? undefined : validateWorkflowExtensionSettings(parsed.extensionSettings, path, "INVALID_SETTINGS");
134
+ const retention = validateRetention(parsed.retention, path);
135
+ const worktreePostCreateCommand = validateWorktreePostCreateCommand(parsed.worktreePostCreateCommand, path);
136
+ return Object.freeze({
137
+ ...(concurrency === undefined ? {} : { concurrency }), ...(backgroundWidget === undefined ? {} : { backgroundWidget }), ...(codemodeTools === undefined ? {} : { codemodeTools }), ...(modelAliases === undefined ? {} : { modelAliases }),
138
+ ...(skills === undefined ? {} : { skills }), ...(extensions === undefined ? {} : { extensions }),
139
+ ...(extensionSettings === undefined ? {} : { extensionSettings }), ...(tools === undefined ? {} : { tools }), ...(retention === undefined ? {} : { retention }),
140
+ ...(worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand }),
141
+ });
142
+ }
143
+ export function loadSettings(path = workflowSettingsPath()): Readonly<WorkflowSettings> { return parseSettings(path, false); }
144
+ const READ_ONLY_WORKFLOW_TOOLS: ReadonlySet<string> = new Set(["workflow_status", "workflow_catalog", "subagents_inspect"]);
145
+ /**
146
+ * Exposure and annotations of a workflow or subagent tool. Under the global `codemodeTools` setting,
147
+ * `model-only` keeps the model's direct access and stops codemode scripts from calling the tool.
148
+ * Tools are registered once at load, so the setting is global and applies after a reload.
149
+ * Read-only tools carry `readOnlyHint` so permission extensions need not confirm them.
150
+ */
151
+ export function workflowToolExposure(name: string, codemodeTools: WorkflowSettings["codemodeTools"]): { exposure?: "model-only"; annotations?: ToolAnnotations } {
152
+ const readOnly = READ_ONLY_WORKFLOW_TOOLS.has(name);
153
+ return { ...(codemodeTools === "none" || (codemodeTools === "read-only" && !readOnly) ? { exposure: "model-only" as const } : {}), ...(readOnly ? { annotations: { readOnlyHint: true } } : {}) };
154
+ }
155
+ /** The `codemodeTools` setting at load time. Invalid settings keep the default; launches report them. */
156
+ export function loadCodemodeToolsSetting(agentDir?: string): WorkflowSettings["codemodeTools"] {
157
+ try { return loadSettings(workflowSettingsPath(agentDir)).codemodeTools; } catch { return undefined; }
158
+ }
159
+ export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
160
+ export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
161
+ const projectSettingsPath = workflowProjectSettingsPath(cwd);
162
+ const global = loadSettings(globalSettingsPath);
163
+ const project: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
164
+ const projectHas = (key: keyof WorkflowSettingsOverrides): boolean => Object.prototype.hasOwnProperty.call(project, key);
165
+ const sourceFor = (key: "skills" | "extensions" | "tools"): string => projectHas(key) ? projectSettingsPath : globalSettingsPath;
166
+ const globalSelectors = selectorsFromSettings(global);
167
+ const projectSelectors = selectorsFromSettings(project);
168
+ const effectiveSelectors = selectorSet({
169
+ skills: [...(globalSelectors.skills ?? []), ...(projectSelectors.skills ?? [])],
170
+ extensions: [...(globalSelectors.extensions ?? []), ...(projectSelectors.extensions ?? [])],
171
+ ...(globalSelectors.tools === undefined && projectSelectors.tools === undefined ? {} : { tools: [...(globalSelectors.tools ?? []), ...(projectSelectors.tools ?? [])] }),
172
+ });
173
+ const hasExtensionSelectors = global.extensions !== undefined || project.extensions !== undefined;
174
+ const extensionSettings = projectHas("extensionSettings") ? project.extensionSettings : global.extensionSettings;
175
+ const sources: WorkflowSettingsSources = {
176
+ concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
177
+ modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
178
+ skills: sourceFor("skills"), extensions: sourceFor("extensions"), tools: sourceFor("tools"),
179
+ ...(extensionSettings === undefined ? {} : { extensionSettings: projectHas("extensionSettings") ? projectSettingsPath : globalSettingsPath }),
180
+ ...(project.retention === undefined && global.retention === undefined ? {} : { retention: project.retention === undefined ? globalSettingsPath : projectSettingsPath }),
181
+ ...(projectHas("worktreePostCreateCommand") || global.worktreePostCreateCommand !== undefined ? { worktreePostCreateCommand: projectHas("worktreePostCreateCommand") ? projectSettingsPath : globalSettingsPath } : {}),
182
+ };
183
+ const effective = Object.freeze({
184
+ concurrency: project.concurrency ?? global.concurrency,
185
+ backgroundWidget: global.backgroundWidget ?? true,
186
+ ...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
187
+ ...(effectiveSelectors.skills.length ? { skills: effectiveSelectors.skills } : global.skills !== undefined || project.skills !== undefined ? { skills: effectiveSelectors.skills } : {}),
188
+ ...(hasExtensionSelectors ? { extensions: effectiveSelectors.extensions } : {}),
189
+ ...(extensionSettings === undefined ? {} : { extensionSettings }),
190
+ ...(effectiveSelectors.tools?.length ? { tools: effectiveSelectors.tools } : global.tools !== undefined || project.tools !== undefined ? { tools: effectiveSelectors.tools } : {}),
191
+ ...((project.retention ?? global.retention) === undefined ? {} : { retention: project.retention ?? global.retention }),
192
+ ...(projectHas("worktreePostCreateCommand") ? { worktreePostCreateCommand: project.worktreePostCreateCommand } : global.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: global.worktreePostCreateCommand }),
193
+ });
194
+ return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
195
+ }
196
+ export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
197
+ for (const name of names) {
198
+ try {
199
+ const target = modelCapability(name, aliases, knownModels, settingsPath);
200
+ if (!availableModels.has(target)) unknownModel(name, target, settingsPath);
201
+ } catch (error) { throw annotateModelAliasError(error, name); }
202
+ }
203
+ }
204
+ export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): AgentResourcePolicy {
205
+ const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
206
+ const global = selectorSet(selectorsFromSettings(resolved.global));
207
+ const project = selectorSet(selectorsFromSettings(resolved.project));
208
+ const effective = selectorSet(selectorsFromSettings(resolved.effective));
209
+ return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: selectorsFromSettings(resolved.global), project: selectorsFromSettings(resolved.project) } };
210
+ }
211
+ export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
212
+ const normalized = validateModelAliases(aliases, path);
213
+ let parsed: unknown = {};
214
+ try {
215
+ loadSettings(path);
216
+ parsed = JSON.parse(readFileSync(path, "utf8"));
217
+ } catch (error) {
218
+ if (!isNodeError(error, "ENOENT")) throw error;
219
+ }
220
+ if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
221
+ mkdirSync(dirname(path), { recursive: true });
222
+ atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
223
+ }
224
+
package/src/trajectory.ts CHANGED
@@ -3,20 +3,30 @@ import { open, readFile, readdir, stat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
5
  import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, DefaultPackageManager, getAgentDir, SettingsManager, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
6
- import { listRunIds, RunStore, type AwaitingCheckpoint, type EffectiveSystemPrompt, type PersistedRun } from "./persistence.js";
6
+ import { listRunIds, RunStore, type AwaitingCheckpoint, type CompletedOperation, type EffectiveSystemPrompt, type PersistedRun } from "./persistence.js";
7
7
  import { navigatorAttentionSortByState } from "./host-view.js";
8
- import type { AgentAttemptSummary, JsonValue, LaunchSnapshot, WorkflowAgentSessionReference } from "./types.js";
8
+ import type { AgentAttemptSummary, AgentRecord, JsonValue, LaunchSnapshot, WorkflowAgentSessionReference, WorkflowErrorCode, WorkflowScriptCall } from "./types.js";
9
9
  import { normalizeSubagentRunRequest, type SubagentProgress, type SubagentRunRequest, type SubagentStatus } from "../subagents/src/contracts.js";
10
10
  import { statusValue, subagentErrorValue } from "../subagents/src/decode.js";
11
11
  import { isNodeError, jsonValue, object, resourcePatternHasMagic, selectResourcesByLayers } from "./utils.js";
12
12
  import type { TrajectoryAction, TrajectoryTarget } from "./trajectory-contracts.js";
13
13
  import { canonicalPath, sameFilesystemPath } from "./paths.js";
14
+ import { TOOL_TIMING_ENTRY_TYPE } from "./tool-timing.js";
14
15
 
15
16
  export const TRAJECTORY_MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
16
17
  const TRAJECTORY_MAX_NON_TIMING_ENTRIES = 400;
17
18
 
19
+ export type TrajectoryAgentOutput =
20
+ | { readonly status: "available"; readonly value: JsonValue; readonly bytes: number }
21
+ | { readonly status: "pending" }
22
+ | { readonly status: "failed" | "cancelled"; readonly code: string; readonly message: string }
23
+ | { readonly status: "truncated"; readonly kind: "result" | "failure"; readonly bytes: number; readonly path?: string }
24
+ | { readonly status: "unavailable" };
25
+ export type TrajectoryAgent = AgentRecord & { readonly output: TrajectoryAgentOutput };
26
+ export type TrajectoryScriptCall = WorkflowScriptCall & { readonly output: TrajectoryAgentOutput | { readonly status: "completed" } | { readonly status: "failed" | "cancelled"; readonly code: WorkflowErrorCode } };
27
+ export type TrajectoryRunRecord = Omit<PersistedRun, "agents" | "scriptCalls"> & { readonly agents: readonly TrajectoryAgent[]; readonly scriptCalls?: readonly TrajectoryScriptCall[] };
18
28
  export type TrajectoryRun = {
19
- run: PersistedRun;
29
+ run: TrajectoryRunRecord;
20
30
  snapshot: Readonly<LaunchSnapshot>;
21
31
  awaiting: readonly AwaitingCheckpoint[];
22
32
  createdAt?: string;
@@ -26,6 +36,31 @@ export type TrajectoryTranscriptStatus = "available" | "empty" | "missing" | "fa
26
36
  export type TrajectoryTranscriptMetadata = { readonly revision: number; readonly status: TrajectoryTranscriptStatus; readonly bytes?: number; readonly timing?: readonly unknown[]; };
27
37
  export type TrajectoryRunMetadata = Omit<TrajectoryRun, "transcripts"> & { transcripts: Readonly<Record<string, TrajectoryTranscriptMetadata>> };
28
38
  export type TrajectorySubagentArtifact = { readonly truncated: true; readonly path: string; readonly bytes: number };
39
+ type SubagentOutputOptions = {
40
+ readonly state: SubagentStatus["state"];
41
+ readonly statusError: SubagentStatus["error"];
42
+ readonly attempt: AgentAttemptSummary | undefined;
43
+ readonly result: JsonValue | undefined;
44
+ readonly resultTruncated: boolean;
45
+ readonly resultPath: string;
46
+ readonly resultBytes: number | undefined;
47
+ readonly failure: { readonly code: string; readonly message: string } | undefined;
48
+ readonly failureTruncated: boolean;
49
+ readonly failurePath: string;
50
+ readonly failureBytes: number | undefined;
51
+ };
52
+ function subagentOutput(options: SubagentOutputOptions): TrajectoryAgentOutput {
53
+ if (options.resultTruncated) return { status: "truncated", kind: "result", path: options.resultPath, bytes: options.resultBytes ?? 0 };
54
+ if (options.result !== undefined) return { status: "available", value: options.result, bytes: serializedJsonBytes(options.result) };
55
+ if (options.failureTruncated) return { status: "truncated", kind: "failure", path: options.failurePath, bytes: options.failureBytes ?? 0 };
56
+ if (options.failure !== undefined) return { status: "failed", code: options.failure.code, message: options.failure.message };
57
+ if (options.state === "failed" || options.state === "stopped") {
58
+ const error = options.statusError ?? options.attempt?.error;
59
+ return { status: options.state === "stopped" ? "cancelled" : "failed", code: error?.code ?? (options.state === "stopped" ? "CANCELLED" : "AGENT_FAILED"), message: error?.message ?? `Subagent ${options.state}` };
60
+ }
61
+ if (options.state === "completed") return { status: "unavailable" };
62
+ return { status: "pending" };
63
+ }
29
64
  export type TrajectorySubagent = {
30
65
  id: string;
31
66
  sessionId: string;
@@ -47,6 +82,7 @@ export type TrajectorySubagent = {
47
82
  attempt?: AgentAttemptSummary;
48
83
  result?: JsonValue | TrajectorySubagentArtifact;
49
84
  failure?: { readonly code: string; readonly message: string } | TrajectorySubagentArtifact;
85
+ output: TrajectoryAgentOutput;
50
86
  transcript: readonly unknown[];
51
87
  };
52
88
  export type TrajectorySubagentMetadata = Omit<TrajectorySubagent, "transcript"> & { transcript: TrajectoryTranscriptMetadata };
@@ -69,7 +105,6 @@ export type TrajectoryPublisherInput = {
69
105
  cwd: string;
70
106
  sessionId: string;
71
107
  port?: number;
72
- themes: boolean;
73
108
  loadRuns: TrajectoryRunLoader;
74
109
  loadSubagents: TrajectorySubagentLoader;
75
110
  loadMetadata: TrajectoryPublisherMetadataLoader;
@@ -91,7 +126,7 @@ function transcriptToolCallId(value: unknown): string | undefined {
91
126
  for (const part of message.content) if (object(part) && typeof part.id === "string") return part.id;
92
127
  return undefined;
93
128
  }
94
- function isTimingTranscriptEntry(value: unknown): boolean { return object(value) && value.type === "custom" && value.customType === "pi-workflows:tool-timing"; }
129
+ export function isTimingTranscriptEntry(value: unknown): boolean { return object(value) && value.type === "custom" && value.customType === TOOL_TIMING_ENTRY_TYPE; }
95
130
  function timingToolCallId(value: unknown): string | undefined {
96
131
  if (!object(value) || !isTimingTranscriptEntry(value) || !object(value.data)) return undefined;
97
132
  return typeof value.data.toolCallId === "string" ? value.data.toolCallId : undefined;
@@ -210,7 +245,8 @@ export function withPiToolDescriptionsForTools(tools: readonly string[], cwd: st
210
245
  function canonicalExtensionSelector(selector: string, base: string): string {
211
246
  const negated = selector.startsWith("!");
212
247
  const body = negated ? selector.slice(1) : selector;
213
- if (body === "*" || body === "**" || body.startsWith("**/")) return selector;
248
+ // Built-in extensions are named `builtin:<name>`, not by a path.
249
+ if (body === "*" || body === "**" || body.startsWith("**/") || body.startsWith("builtin:")) return selector;
214
250
  const resolved = resolve(base, body);
215
251
  if (resourcePatternHasMagic(body)) return `${negated ? "!" : ""}${resolved}`;
216
252
  return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
@@ -277,11 +313,62 @@ function withoutAgentActivities(run: PersistedRun): PersistedRun {
277
313
  return agents.every((agent, index) => agent === run.agents[index]) ? run : { ...run, agents };
278
314
  }
279
315
 
316
+ function serializedJsonBytes(value: JsonValue): number {
317
+ const serialized = JSON.stringify(value);
318
+ return Buffer.byteLength(serialized, "utf8");
319
+ }
320
+ function failedAgentOutput(agent: PersistedRun["agents"][number]): TrajectoryAgentOutput {
321
+ const error = [...(agent.attemptDetails ?? [])].reverse().find((attempt) => attempt.error)?.error;
322
+ const status = agent.state === "cancelled" ? "cancelled" : "failed";
323
+ return { status, code: error?.code ?? (status === "cancelled" ? "CANCELLED" : "AGENT_FAILED"), message: error?.message ?? `Agent ${agent.state}` };
324
+ }
325
+ function operationOutput(operation: CompletedOperation): TrajectoryAgentOutput {
326
+ const bytes = serializedJsonBytes(operation.value);
327
+ return bytes > DEFAULT_MAX_BYTES ? { status: "truncated", kind: "result", bytes } : { status: "available", value: operation.value, bytes };
328
+ }
329
+ function outputForCall(call: WorkflowScriptCall, operation: CompletedOperation | undefined): TrajectoryScriptCall["output"] {
330
+ if (operation !== undefined) return call.kind === "tool" ? { status: "completed" } : operationOutput(operation);
331
+ if (call.error) return { status: call.error.code === "CANCELLED" ? "cancelled" : "failed", code: call.error.code, ...(call.kind === "shell" ? { message: call.error.message ?? "Shell failed" } : {}) };
332
+ return call.finishedAt === undefined ? { status: "pending" } : { status: "unavailable" };
333
+ }
334
+ function outputForAgent(agent: PersistedRun["agents"][number], operation: CompletedOperation | undefined): TrajectoryAgentOutput {
335
+ if (operation !== undefined) return operationOutput(operation);
336
+ if (agent.state === "failed" || agent.state === "cancelled") return failedAgentOutput(agent);
337
+ if (agent.state === "completed") return { status: "unavailable" };
338
+ return { status: "pending" };
339
+ }
340
+ export function applyTrajectoryAgentOutputs(run: PersistedRun, operations: readonly CompletedOperation[]): TrajectoryRunRecord {
341
+ const byPath = new Map(operations.map((operation) => [operation.path, operation]));
342
+ const agents: readonly TrajectoryAgent[] = run.agents.map((agent) => ({ ...agent, output: outputForAgent(agent, agent.resultPath === undefined ? undefined : byPath.get(agent.resultPath)) }));
343
+ const scriptCalls = run.scriptCalls?.map((call): TrajectoryScriptCall => {
344
+ const recorded = { ...call, output: outputForCall(call, byPath.get(call.path)) };
345
+ if (call.kind === "tool") {
346
+ delete recorded.input;
347
+ delete recorded.inputBytes;
348
+ if (call.error) recorded.error = { code: call.error.code };
349
+ }
350
+ return recorded;
351
+ });
352
+ const rest: Omit<PersistedRun, "scriptCalls"> = run;
353
+ return { ...rest, agents, ...(scriptCalls === undefined ? {} : { scriptCalls }) };
354
+ }
355
+ function overlayTrajectoryRun(run: TrajectoryRunRecord, overlay: (run: PersistedRun) => PersistedRun): TrajectoryRunRecord {
356
+ const overlaid: Omit<PersistedRun, "scriptCalls"> = overlay(run);
357
+ const outputs = new Map(run.agents.map((agent) => [agent.id, agent.output]));
358
+ const agents: readonly TrajectoryAgent[] = overlaid.agents.map((agent) => {
359
+ const previous = outputs.get(agent.id);
360
+ const output = previous?.status === "available" || previous?.status === "truncated" ? previous : outputForAgent(agent, undefined);
361
+ return { ...agent, output };
362
+ });
363
+ return { ...overlaid, agents, ...(run.scriptCalls === undefined ? {} : { scriptCalls: run.scriptCalls }) };
364
+ }
280
365
  async function loadTrajectoryRun(store: RunStore, includeTranscripts = true): Promise<TrajectoryRun> {
281
366
  const value = await store.load();
282
367
  const summary = await store.loadSummary().catch(() => undefined);
283
368
  const prompts = await store.systemPrompts().catch(() => []);
284
- const run = withoutAgentActivities(await withResolvedResources(withPiToolDescriptions(applySystemPrompts(value.run, prompts)), store.cwd));
369
+ const prepared = withoutAgentActivities(await withResolvedResources(withPiToolDescriptions(applySystemPrompts(value.run, prompts)), store.cwd));
370
+ const operations = await store.replayableOperations().catch(() => []);
371
+ const run = applyTrajectoryAgentOutputs(prepared, operations);
285
372
  return { run, snapshot: value.snapshot, awaiting: await store.awaitingCheckpoints(), ...(summary?.createdAt === undefined ? {} : { createdAt: summary.createdAt }), transcripts: includeTranscripts ? await runTranscripts(run) : {} };
286
373
  }
287
374
 
@@ -290,7 +377,7 @@ export async function loadTrajectoryRuns(cwd: string, sessionId: string, home =
290
377
  for (const runId of await listRunIds(cwd, sessionId, home, false)) {
291
378
  try {
292
379
  const value = await loadTrajectoryRun(new RunStore(cwd, sessionId, runId, home));
293
- loaded.push(overlay ? { ...value, run: overlay(value.run) } : value);
380
+ loaded.push(overlay ? { ...value, run: overlayTrajectoryRun(value.run, overlay) } : value);
294
381
  } catch { /* Ignore corrupt or concurrently removed runs. */ }
295
382
  }
296
383
  return loaded;
@@ -323,7 +410,7 @@ function createCachedRunLoader<Value extends { run: PersistedRun }, Signature>(c
323
410
  entry = { value, stateMtimeMs: (await fileMtime(join(store.directory, "state.json"))) ?? 0, journalMtimeMs: (await fileMtime(join(store.directory, "journal.json"))) ?? 0, transcripts };
324
411
  cache.set(runId, entry);
325
412
  }
326
- loaded.push(overlay ? { ...entry.value, run: overlay(entry.value.run) } : entry.value);
413
+ loaded.push(overlay ? { ...entry.value, run: overlayTrajectoryRun(entry.value.run as TrajectoryRunRecord, overlay) } : entry.value);
327
414
  } catch { cache.delete(runId); /* Ignore corrupt or concurrently removed runs. */ }
328
415
  }
329
416
  return loaded;
@@ -490,6 +577,7 @@ async function loadTrajectorySubagent(directory: string, cwd: string, sessionId:
490
577
  ...(attempt === undefined ? {} : { attempt }),
491
578
  ...(result === undefined ? {} : { result }),
492
579
  ...(failure === undefined ? {} : { failure }),
580
+ output: subagentOutput({ state: status.state, statusError: status.error, attempt, result: resultValue === undefined ? undefined : boundedSubagentJson(resultValue), resultTruncated, resultPath, resultBytes, failure: failureValue === undefined ? undefined : subagentErrorValue(failureValue), failureTruncated, failurePath, failureBytes }),
493
581
  };
494
582
  } catch { return undefined; }
495
583
  }
package/src/types.ts CHANGED
@@ -29,7 +29,7 @@ export const WORKFLOW_BLOCKED_EVENT = "workflow:blocked";
29
29
  export const ERROR_CODES = [
30
30
  "CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
31
31
  "RUN_OWNED", "RUN_NOT_FOUND", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "AGENT_RESULT_COLLECTED", "RESULT_INVALID",
32
- "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
32
+ "TOOL_FAILED", "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
33
33
  ] as const;
34
34
 
35
35
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
@@ -90,15 +90,16 @@ export interface WorkflowModelAliasResolverContext { cwd: string; projectTrusted
90
90
  export interface WorkflowModelAlias { resolve: (context: Readonly<WorkflowModelAliasResolverContext>) => string | Promise<string> }
91
91
  export interface WorkflowMetadata { name: string; description?: string }
92
92
  export interface HerdrExtensionSettings { enableFullyInspectableMode?: boolean }
93
- export interface TrajectoryExtensionSettings { port?: number; themes?: boolean }
93
+ export interface TrajectoryExtensionSettings { port?: number }
94
94
  export interface WorkflowExtensionSettings { readonly [namespace: string]: JsonValue; readonly herdr?: Readonly<HerdrExtensionSettings>; readonly trajectory?: Readonly<TrajectoryExtensionSettings> }
95
95
  export type WorkflowExtensionSettingsSource = "global" | "project" | "role" | "effective";
96
96
  export interface WorkflowExtensionSettingsValidatorContext { source: WorkflowExtensionSettingsSource; cwd: string; projectTrusted: boolean; settingsPath?: string; role?: string }
97
97
  export type WorkflowExtensionSettingsValidator = (settings: Readonly<WorkflowExtensionSettings>, context: Readonly<WorkflowExtensionSettingsValidatorContext>) => void;
98
98
  export interface WorkflowSessionStartEvent { type: "session_start"; reason: "startup" | "reload" | "new" | "resume" | "fork"; previousSessionFile?: string; settings: Readonly<WorkflowExtensionSettings> }
99
99
  export interface WorkflowRetentionSettings { olderThanDays?: number; maxTerminalRuns?: number }
100
- export interface WorkflowSettings { concurrency: number; backgroundWidget?: boolean; modelAliases?: Readonly<Record<string, string>>; skills?: readonly string[]; extensions?: readonly string[]; extensionSettings?: Readonly<WorkflowExtensionSettings>; tools?: readonly string[]; retention?: Readonly<WorkflowRetentionSettings>; worktreePostCreateCommand?: readonly string[] }
101
- export type WorkflowSettingsOverrides = Partial<Omit<WorkflowSettings, "backgroundWidget">>;
100
+ export type CodemodeToolsSetting = "all" | "read-only" | "none";
101
+ export interface WorkflowSettings { concurrency: number; backgroundWidget?: boolean; codemodeTools?: CodemodeToolsSetting; modelAliases?: Readonly<Record<string, string>>; skills?: readonly string[]; extensions?: readonly string[]; extensionSettings?: Readonly<WorkflowExtensionSettings>; tools?: readonly string[]; retention?: Readonly<WorkflowRetentionSettings>; worktreePostCreateCommand?: readonly string[] }
102
+ export type WorkflowSettingsOverrides = Partial<Omit<WorkflowSettings, "backgroundWidget" | "codemodeTools">>;
102
103
  export interface WorkflowSettingsSources { concurrency: string; modelAliases: string; skills?: string; extensions?: string; tools?: string; extensionSettings?: string; retention?: string; worktreePostCreateCommand?: string }
103
104
  export interface WorkflowSettingsResolution { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: Readonly<WorkflowSettings>; project: Readonly<WorkflowSettingsOverrides>; effective: Readonly<WorkflowSettings>; sources: Readonly<WorkflowSettingsSources> }
104
105
  export interface AgentResourceSelectors { skills?: readonly string[]; extensions?: readonly string[]; tools?: readonly string[] }
@@ -178,6 +179,8 @@ export interface WorkflowRunEvent { type: string; message: string; timestamp?: n
178
179
  export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
179
180
  export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
180
181
  export interface WorkflowPhaseShellActivity { phaseIndex: number; active: number; startedAt: number }
182
+ /** A script call recorded for inspection. Tool payloads stay in the replay journal, not this record. Oversized shell inputs are JSON prefixes, with `inputBytes` their full size. */
183
+ export interface WorkflowScriptCall { kind: "shell" | "tool"; name: string; path: string; input?: JsonValue; inputBytes?: number; phase?: string; startedAt: number; finishedAt?: number; error?: { code: WorkflowErrorCode; message?: string } }
181
184
  export interface RunRecord {
182
185
  id: string;
183
186
  workflowName: string;
@@ -194,6 +197,8 @@ export interface RunRecord {
194
197
  activeShells?: number;
195
198
  activeShellStartedAt?: number;
196
199
  activeShellsByPhase?: readonly WorkflowPhaseShellActivity[];
200
+ activeTools?: readonly { name: string; startedAt: number }[]; // Script tool calls in flight: a live overlay for rendering, never persisted.
201
+ scriptCalls?: readonly WorkflowScriptCall[];
197
202
  error?: WorkflowErrorShape;
198
203
  failedAt?: string;
199
204
  budget?: WorkflowBudget;
@@ -356,7 +361,9 @@ export interface FunctionIdentity { path: string; structuralPath: readonly strin
356
361
  export type AgentContinuity = "fresh" | "continued";
357
362
  export interface AgentIdentity { structuralPath: readonly string[]; callSite: string; occurrence: number; parentBreadcrumb?: string; worktreeOwner?: string; handle?: string; turn?: number }
358
363
  export interface ShellIdentity { structuralPath: readonly string[]; callSite: string; occurrence: number; worktreeOwner?: string }
359
- export interface WorkflowBridge { agent?: (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>; shell?: (command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>; checkpoint?: (input: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => boolean | Promise<boolean>; function?: (name: string, input: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: FunctionIdentity) => Promise<JsonValue>; worktree?: (owner: string, signal: AbortSignal) => Promise<Readonly<WorkflowWorktreeReference>>; functions?: Readonly<Record<string, { name: string }>>; phase?: (name: string) => void | Promise<void>; log?: (message: string) => void | Promise<void> }
364
+ /** A script `tools.<id>(args)` call: the n-th call of that tool within one structural scope. */
365
+ export interface ToolIdentity { structuralPath: readonly string[]; occurrence: number }
366
+ export interface WorkflowBridge { agent?: (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>; shell?: (command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>; checkpoint?: (input: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => boolean | Promise<boolean>; function?: (name: string, input: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: FunctionIdentity) => Promise<JsonValue>; worktree?: (owner: string, signal: AbortSignal) => Promise<Readonly<WorkflowWorktreeReference>>; functions?: Readonly<Record<string, { name: string }>>; phase?: (name: string) => void | Promise<void>; log?: (message: string) => void | Promise<void>; tool?: (identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity) => Promise<JsonValue> }
360
367
  export interface WorkflowExecution { result: Promise<JsonValue>; cancel: () => void }
361
368
  export interface StaticWorkflowScope { kind: "parallel" | "pipeline"; name: string | null; key: string | null }
362
369
  export type StaticWorkflowExecution = "parallel" | "sequential";
package/src/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WorkflowError, type JsonValue, type ModelSpec, type ThinkingLevel, type WorkflowErrorCode, type WorkflowExtensionSettings } from "./types.js";
1
+ import { createHash } from "node:crypto";
2
+ import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WorkflowError, type JsonSchema, type JsonValue, type ModelSpec, type ThinkingLevel, type WorkflowErrorCode, type WorkflowExtensionSettings } from "./types.js";
2
3
  import { Minimatch } from "minimatch";
3
4
  export class SerialLane {
4
5
  #tail: Promise<void> = Promise.resolve();
@@ -18,6 +19,15 @@ export function sanitizeDisplayText(value: string): string { return value.replac
18
19
  export { object as isObject };
19
20
  function isStringKey(key: PropertyKey): key is string { return typeof key === "string"; }
20
21
  function stringKeyValue(value: object, key: string): unknown { return object(value) ? value[key] : undefined; }
22
+ function sortedJson(value: JsonValue): JsonValue {
23
+ if (Array.isArray(value)) return value.map(sortedJson);
24
+ if (value === null || typeof value !== "object") return value;
25
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortedJson(value[key] as JsonValue)]));
26
+ }
27
+ /** A short digest of a JSON value that ignores object key order. */
28
+ export function jsonDigest(value: JsonValue): string {
29
+ return createHash("sha256").update(JSON.stringify(sortedJson(value))).digest("hex").slice(0, 16);
30
+ }
21
31
  export function jsonValue(value: unknown, seen = new Set<object>()): value is JsonValue {
22
32
  if (value === null || typeof value === "boolean" || typeof value === "string") return true;
23
33
  if (typeof value === "number") return Number.isFinite(value);
@@ -153,6 +163,24 @@ export function resolveModelReference(value: string, aliases: Readonly<Record<st
153
163
  };
154
164
  return resolveReference(value, []);
155
165
  }
166
+ type ToolSource = { getActiveTools(): string[]; getAllTools?(): readonly { name: string; exposure?: string }[] };
167
+ /**
168
+ * Tools a session can reach, and so the ceiling for its agents: the declared tools plus the ones
169
+ * only codemode scripts call or tool_search loads, such as MCP tools with the default exposure.
170
+ * Agent sessions allow tools by name, so leaving these out would make them unreachable there.
171
+ */
172
+ export function reachableTools(pi: ToolSource): string[] {
173
+ const undeclared = pi.getAllTools?.().filter(({ exposure }) => exposure === "codemode" || exposure === "deferred").map(({ name }) => name) ?? [];
174
+ return [...new Set([...pi.getActiveTools(), ...undeclared])];
175
+ }
176
+ /** Provider of the virtual models that expose workflow model aliases in `/model`. */
177
+ export const VIRTUAL_MODEL_PROVIDER = "workflow";
178
+ /** Child sessions do not load the workflow extension, so they receive the alias target instead of its virtual model. */
179
+ export function physicalModel(spec: ModelSpec, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
180
+ if (spec.provider !== VIRTUAL_MODEL_PROVIDER || !modelAliasName(spec.model, aliases)) return spec;
181
+ const target = resolveModelReference(spec.model, aliases, knownModels, settingsPath);
182
+ return spec.thinking === undefined ? target : { ...target, thinking: spec.thinking };
183
+ }
156
184
  export function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string {
157
185
  const parsed = typeof value === "string" ? resolveModelReference(value, aliases, knownModels, settingsPath) : value;
158
186
  return `${parsed.provider}/${parsed.model}`;
@@ -190,3 +218,10 @@ export function resourcePatternHasMagic(pattern: string): boolean { return /[*?\
190
218
  export function unmatchedResourcePatterns(patterns: readonly string[], resources: readonly string[]): string[] { return patterns.filter((pattern) => !resources.some((resource) => resourcePatternMatches(resource, pattern))); }
191
219
  export function createLaunchSnapshot(input: Omit<import("./types.js").LaunchSnapshot, "identityVersion"> & { identityVersion?: number }): Readonly<import("./types.js").LaunchSnapshot> { return deepFreeze(structuredClone({ ...input, identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION })); }
192
220
  export function loadLaunchSnapshot(input: import("./types.js").LaunchSnapshot): Readonly<import("./types.js").LaunchSnapshot> { return deepFreeze(structuredClone(input)); }
221
+
222
+ export function validateSchema(schema: unknown, at = "schema"): asserts schema is JsonSchema {
223
+ if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema)) fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
224
+ 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`);
225
+ 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`);
226
+ if (schema.properties !== undefined && !object(schema.properties)) fail("INVALID_SCHEMA", `${at}.properties must be an object`);
227
+ }