@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.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 (128) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +442 -162
  5. package/dist/src/background-widget.d.ts +5 -1
  6. package/dist/src/background-widget.js +5 -10
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +59 -37
  9. package/dist/src/execution.d.ts +6 -2
  10. package/dist/src/execution.js +48 -7
  11. package/dist/src/host-delivery.js +5 -8
  12. package/dist/src/host-navigator.js +5 -2
  13. package/dist/src/host-phases.d.ts +1 -0
  14. package/dist/src/host-recovery.d.ts +11 -8
  15. package/dist/src/host-recovery.js +14 -22
  16. package/dist/src/host-runtime.js +56 -5
  17. package/dist/src/host-view.d.ts +6 -1
  18. package/dist/src/host-view.js +46 -20
  19. package/dist/src/host.d.ts +2 -2
  20. package/dist/src/host.js +246 -118
  21. package/dist/src/index.d.ts +1 -2
  22. package/dist/src/index.js +1985 -1690
  23. package/dist/src/index.js.map +3 -3
  24. package/dist/src/paths.d.ts +8 -5
  25. package/dist/src/paths.js +24 -13
  26. package/dist/src/persistence.d.ts +1 -0
  27. package/dist/src/persistence.js +1 -0
  28. package/dist/src/pi-runtime-runner.js +4 -1
  29. package/dist/src/registry.d.ts +9 -7
  30. package/dist/src/registry.js +29 -71
  31. package/dist/src/runtime/agent-runner.d.ts +2 -0
  32. package/dist/src/script-tools.d.ts +27 -0
  33. package/dist/src/script-tools.js +87 -0
  34. package/dist/src/session-lease.d.ts +1 -1
  35. package/dist/src/session-lease.js +45 -11
  36. package/dist/src/settings.d.ts +19 -3
  37. package/dist/src/settings.js +63 -50
  38. package/dist/src/store.d.ts +3 -1
  39. package/dist/src/store.js +17 -1
  40. package/dist/src/trajectory.d.ts +12 -3
  41. package/dist/src/trajectory.js +32 -11
  42. package/dist/src/types.d.ts +129 -45
  43. package/dist/src/types.js +5 -8
  44. package/dist/src/utils.d.ts +21 -1
  45. package/dist/src/utils.js +46 -6
  46. package/dist/src/validation.d.ts +14 -3
  47. package/dist/src/validation.js +143 -87
  48. package/dist/starter/index.js +8 -11
  49. package/dist/starter/index.js.map +1 -1
  50. package/dist/starter/review-loop.d.ts +0 -13
  51. package/dist/starter/review-loop.js +8 -9
  52. package/dist/subagents/index.js +904 -956
  53. package/dist/subagents/index.js.map +3 -3
  54. package/dist/subagents/src/contracts.d.ts +48 -31
  55. package/dist/subagents/src/contracts.js +20 -9
  56. package/dist/subagents/src/index.d.ts +1 -1
  57. package/dist/subagents/src/index.js +14 -8
  58. package/dist/subagents/src/manager.js +52 -73
  59. package/dist/subagents/src/navigator.js +3 -14
  60. package/dist/subagents/src/view.d.ts +1 -1
  61. package/dist/subagents/src/view.js +15 -68
  62. package/dist/subagents/src/worktree.js +1 -1
  63. package/dist/trajectory/assets/index.html +35 -13
  64. package/dist/trajectory/index.js +576 -932
  65. package/dist/trajectory/index.js.map +3 -3
  66. package/dist/trajectory/src/assets/index.html +35 -13
  67. package/dist/trajectory/src/index.js +134 -104
  68. package/dist/trajectory/src/server.js +268 -27
  69. package/dist/trajectory/src/server.js.map +3 -3
  70. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  71. package/dist/trajectory/src/startup-mutex.js +172 -0
  72. package/examples/workflow-extension-template/README.md +4 -7
  73. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  74. package/examples/workflow-extension-template/index.js +0 -3
  75. package/package.json +10 -10
  76. package/skills/pi-extensible-workflows/SKILL.md +12 -8
  77. package/src/agent-execution.ts +412 -164
  78. package/src/background-widget.ts +5 -11
  79. package/src/decoders.ts +51 -37
  80. package/src/execution.ts +46 -9
  81. package/src/host-delivery.ts +5 -8
  82. package/src/host-navigator.ts +4 -2
  83. package/src/host-phases.ts +1 -1
  84. package/src/host-recovery.ts +23 -28
  85. package/src/host-runtime.ts +44 -6
  86. package/src/host-view.ts +41 -16
  87. package/src/host.ts +229 -142
  88. package/src/index.ts +1 -2
  89. package/src/paths.ts +18 -9
  90. package/src/persistence.ts +1 -0
  91. package/src/pi-runtime-runner.ts +3 -1
  92. package/src/registry.ts +28 -64
  93. package/src/runtime/agent-runner.ts +2 -0
  94. package/src/script-tools.ts +76 -0
  95. package/src/session-lease.ts +34 -6
  96. package/src/settings.ts +56 -50
  97. package/src/store.ts +18 -2
  98. package/src/trajectory.ts +32 -15
  99. package/src/types.ts +82 -34
  100. package/src/utils.ts +42 -5
  101. package/src/validation.ts +121 -80
  102. package/starter/index.ts +0 -1
  103. package/starter/review-loop.ts +8 -9
  104. package/starter/roles/developer.md +0 -1
  105. package/starter/roles/oracle.md +0 -1
  106. package/starter/roles/researcher.md +0 -1
  107. package/starter/roles/reviewer.md +0 -1
  108. package/starter/roles/scout.md +0 -1
  109. package/subagents/README.md +10 -16
  110. package/subagents/src/contracts.ts +21 -11
  111. package/subagents/src/index.ts +17 -8
  112. package/subagents/src/manager.ts +48 -128
  113. package/subagents/src/navigator.ts +4 -13
  114. package/subagents/src/view.ts +16 -63
  115. package/subagents/src/worktree.ts +1 -1
  116. package/trajectory/src/assets/index.html +35 -13
  117. package/trajectory/src/index.ts +112 -91
  118. package/trajectory/src/server.ts +46 -10
  119. package/trajectory/src/startup-mutex.ts +127 -0
  120. package/dist/src/roles.d.ts +0 -70
  121. package/dist/src/roles.js +0 -325
  122. package/dist/starter/roles/developer.md +0 -6
  123. package/dist/starter/roles/oracle.md +0 -18
  124. package/dist/starter/roles/researcher.md +0 -20
  125. package/dist/starter/roles/reviewer.md +0 -19
  126. package/dist/starter/roles/scout.md +0 -18
  127. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  128. package/src/roles.ts +0 -326
package/src/settings.ts CHANGED
@@ -1,20 +1,23 @@
1
1
  import { atomicWriteFile } from "./persistence.js";
2
2
  import { mkdirSync, readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { getAgentDir, type ToolAnnotations } from "@earendil-works/pi-coding-agent";
5
+ import type { AgentResourcePolicy, AgentResourceSelectorSources, AgentResourceSelectors, AgentResourceSelectorSet, ContextFileScope, JsonValue, WorkflowExtensionSettings, WorkflowRetentionSettings, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowSettingsSources } from "./types.js";
3
6
  import { homedir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
7
+ import { resolve } from "node:path";
5
8
  import { fileURLToPath } from "node:url";
6
- import { getAgentDir } 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
9
  import { canonicalPath, workflowProjectSettingsPath } from "./paths.js";
11
10
  export { workflowProjectSettingsPath } from "./paths.js";
11
+ import { isContextFileScope } from "./types.js";
12
+ import { annotateModelAliasError, deepFreeze, errorText, fail, isNodeError, modelCapability, object, positiveInteger, unknownModel, validateModelAliases } from "./utils.js";
13
+ import { jsonValue, resourcePatternHasMagic, validateResourcePattern, validWorkflowExtensionNamespace } from "./utils.js";
12
14
 
13
- const ROLE_DIRECTORY = "pi-extensible-workflows";
15
+ const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
14
16
  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"); }
17
+ export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, WORKFLOW_DIRECTORY, "settings.json"); }
16
18
  function normalizedResourcePath(value: string, settingsPath: string): string {
17
- if (value === "*") return value;
19
+ // Built-in extensions are named `builtin:<name>`, not by a path.
20
+ if (value === "*" || value.startsWith("builtin:")) return value;
18
21
  let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
19
22
  if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
20
23
  const resolved = resolve(dirname(settingsPath), expanded);
@@ -63,18 +66,19 @@ function selectorsFromSettings(settings: Readonly<WorkflowSettings | WorkflowSet
63
66
  };
64
67
  }
65
68
  function selectorSet(value: AgentResourceSelectors | undefined): AgentResourceSelectorSet { return { skills: [...(value?.skills ?? [])], extensions: [...(value?.extensions ?? [])], ...(value?.tools === undefined ? {} : { tools: [...value.tools] }) }; }
66
- export function validateContextFileScopes(value: unknown, rolePath: string): readonly ContextFileScope[] | undefined {
69
+ export function validateContextFileScopes(value: unknown, sourcePath: string): readonly ContextFileScope[] | undefined {
67
70
  if (value === undefined) return undefined;
68
- if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${rolePath}.contextFiles must be an array containing only global, project, or cwd`);
71
+ if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${sourcePath}.contextFiles must be an array containing only global, project, or cwd`);
69
72
  return [...value];
70
73
  }
71
74
  export function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS"): WorkflowExtensionSettings | undefined {
72
75
  if (value === undefined) return undefined;
76
+ if (!object(value) || !jsonValue(value)) fail(errorCode, `${settingsPath}.extensionSettings must be a JSON object`);
77
+ for (const namespace of Object.keys(value)) if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${settingsPath}.extensionSettings contains an invalid namespace: ${namespace}`);
78
+ const generic = value;
73
79
  const base = `${settingsPath}.extensionSettings`;
74
- if (!object(value)) fail(errorCode, `${base} must be an object`);
75
80
  const normalized: Record<string, JsonValue> = {};
76
- for (const [namespace, raw] of Object.entries(value)) {
77
- if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${base} contains an invalid namespace: ${namespace}`);
81
+ for (const [namespace, raw] of Object.entries(generic)) {
78
82
  if (namespace === "herdr") {
79
83
  if (!object(raw)) fail(errorCode, `${base}.herdr must be an object`);
80
84
  if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode, `${base}.herdr contains an unsupported setting`);
@@ -89,7 +93,6 @@ export function validateWorkflowExtensionSettings(value: unknown, settingsPath:
89
93
  normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }) });
90
94
  continue;
91
95
  }
92
- if (!jsonValue(raw)) fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
93
96
  normalized[namespace] = structuredClone(raw);
94
97
  }
95
98
  return deepFreeze(normalized as WorkflowExtensionSettings);
@@ -115,7 +118,7 @@ function parseSettings(path: string, partial: boolean): Readonly<WorkflowSetting
115
118
  fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
116
119
  }
117
120
  if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
118
- const allowed = new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...(partial ? [] : ["backgroundWidget"]) ]);
121
+ const allowed = new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...(partial ? [] : ["backgroundWidget", "codemodeTools"]) ]);
119
122
  const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
120
123
  if (Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources")) fail("INVALID_SETTINGS", `disabledAgentResources is no longer supported; use skills, extensions, and tools selectors (settings: ${path})`);
121
124
  if (unknown) fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
@@ -123,6 +126,8 @@ function parseSettings(path: string, partial: boolean): Readonly<WorkflowSetting
123
126
  if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16)) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
124
127
  const backgroundWidget = parsed.backgroundWidget === undefined ? (partial ? undefined : DEFAULT_SETTINGS.backgroundWidget) : parsed.backgroundWidget;
125
128
  if (backgroundWidget !== undefined && typeof backgroundWidget !== "boolean") fail("INVALID_SETTINGS", `${path}.backgroundWidget must be a boolean`);
129
+ const codemodeTools = parsed.codemodeTools;
130
+ if (codemodeTools !== undefined && codemodeTools !== "all" && codemodeTools !== "read-only" && codemodeTools !== "none") fail("INVALID_SETTINGS", `${path}.codemodeTools must be "all", "read-only", or "none"`);
126
131
  const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
127
132
  const skills = validateSelectorList(parsed.skills, path, "skills");
128
133
  const tools = validateSelectorList(parsed.tools, path, "tools");
@@ -131,49 +136,45 @@ function parseSettings(path: string, partial: boolean): Readonly<WorkflowSetting
131
136
  const retention = validateRetention(parsed.retention, path);
132
137
  const worktreePostCreateCommand = validateWorktreePostCreateCommand(parsed.worktreePostCreateCommand, path);
133
138
  return Object.freeze({
134
- ...(concurrency === undefined ? {} : { concurrency }), ...(backgroundWidget === undefined ? {} : { backgroundWidget }), ...(modelAliases === undefined ? {} : { modelAliases }),
139
+ ...(concurrency === undefined ? {} : { concurrency }), ...(backgroundWidget === undefined ? {} : { backgroundWidget }), ...(codemodeTools === undefined ? {} : { codemodeTools }), ...(modelAliases === undefined ? {} : { modelAliases }),
135
140
  ...(skills === undefined ? {} : { skills }), ...(extensions === undefined ? {} : { extensions }),
136
141
  ...(extensionSettings === undefined ? {} : { extensionSettings }), ...(tools === undefined ? {} : { tools }), ...(retention === undefined ? {} : { retention }),
137
142
  ...(worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand }),
138
143
  });
139
144
  }
140
145
  export function loadSettings(path = workflowSettingsPath()): Readonly<WorkflowSettings> { return parseSettings(path, false); }
146
+ const READ_ONLY_WORKFLOW_TOOLS: ReadonlySet<string> = new Set(["workflow_status", "workflow_catalog", "subagents_inspect"]);
147
+ /**
148
+ * Exposure and annotations of a workflow or subagent tool. Under the global `codemodeTools` setting,
149
+ * `model-only` keeps the model's direct access and stops codemode scripts from calling the tool.
150
+ * Tools are registered once at load, so the setting is global and applies after a reload.
151
+ * Read-only tools carry `readOnlyHint` so permission extensions need not confirm them.
152
+ */
153
+ export function workflowToolExposure(name: string, codemodeTools: WorkflowSettings["codemodeTools"]): { exposure?: "model-only"; annotations?: ToolAnnotations } {
154
+ const readOnly = READ_ONLY_WORKFLOW_TOOLS.has(name);
155
+ return { ...(codemodeTools === "none" || (codemodeTools === "read-only" && !readOnly) ? { exposure: "model-only" as const } : {}), ...(readOnly ? { annotations: { readOnlyHint: true } } : {}) };
156
+ }
157
+ /** The `codemodeTools` setting at load time. Invalid settings keep the default; launches report them. */
158
+ export function loadCodemodeToolsSetting(agentDir?: string): WorkflowSettings["codemodeTools"] {
159
+ try { return loadSettings(workflowSettingsPath(agentDir)).codemodeTools; } catch { return undefined; }
160
+ }
141
161
  export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
142
- export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
162
+ export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution & { selectorSources: AgentResourceSelectorSources } {
143
163
  const projectSettingsPath = workflowProjectSettingsPath(cwd);
144
- const global = loadSettings(globalSettingsPath);
145
- const project: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
146
- const projectHas = (key: keyof WorkflowSettingsOverrides): boolean => Object.prototype.hasOwnProperty.call(project, key);
147
- const sourceFor = (key: "skills" | "extensions" | "tools"): string => projectHas(key) ? projectSettingsPath : globalSettingsPath;
148
- const globalSelectors = selectorsFromSettings(global);
149
- const projectSelectors = selectorsFromSettings(project);
150
- const effectiveSelectors = selectorSet({
151
- skills: [...(globalSelectors.skills ?? []), ...(projectSelectors.skills ?? [])],
152
- extensions: [...(globalSelectors.extensions ?? []), ...(projectSelectors.extensions ?? [])],
153
- ...(globalSelectors.tools === undefined && projectSelectors.tools === undefined ? {} : { tools: [...(globalSelectors.tools ?? []), ...(projectSelectors.tools ?? [])] }),
164
+ const consumerGlobal = loadSettings(globalSettingsPath);
165
+ const consumerProject: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
166
+ const global = consumerGlobal;
167
+ const project = consumerProject;
168
+ const selectorSources = { global: selectorsFromSettings(global), project: selectorsFromSettings(project) };
169
+ const effective: Readonly<WorkflowSettings> = Object.freeze({
170
+ ...global, ...project,
171
+ skills: [...(global.skills ?? []), ...(project.skills ?? [])],
172
+ extensions: [...(global.extensions ?? []), ...(project.extensions ?? [])],
173
+ ...(global.tools === undefined && project.tools === undefined ? {} : { tools: [...(global.tools ?? []), ...(project.tools ?? [])] }),
154
174
  });
155
- const hasExtensionSelectors = global.extensions !== undefined || project.extensions !== undefined;
156
- const extensionSettings = projectHas("extensionSettings") ? project.extensionSettings : global.extensionSettings;
157
- const sources: WorkflowSettingsSources = {
158
- concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
159
- modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
160
- skills: sourceFor("skills"), extensions: sourceFor("extensions"), tools: sourceFor("tools"),
161
- ...(extensionSettings === undefined ? {} : { extensionSettings: projectHas("extensionSettings") ? projectSettingsPath : globalSettingsPath }),
162
- ...(project.retention === undefined && global.retention === undefined ? {} : { retention: project.retention === undefined ? globalSettingsPath : projectSettingsPath }),
163
- ...(projectHas("worktreePostCreateCommand") || global.worktreePostCreateCommand !== undefined ? { worktreePostCreateCommand: projectHas("worktreePostCreateCommand") ? projectSettingsPath : globalSettingsPath } : {}),
164
- };
165
- const effective = Object.freeze({
166
- concurrency: project.concurrency ?? global.concurrency,
167
- backgroundWidget: global.backgroundWidget ?? true,
168
- ...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
169
- ...(effectiveSelectors.skills.length ? { skills: effectiveSelectors.skills } : global.skills !== undefined || project.skills !== undefined ? { skills: effectiveSelectors.skills } : {}),
170
- ...(hasExtensionSelectors ? { extensions: effectiveSelectors.extensions } : {}),
171
- ...(extensionSettings === undefined ? {} : { extensionSettings }),
172
- ...(effectiveSelectors.tools?.length ? { tools: effectiveSelectors.tools } : global.tools !== undefined || project.tools !== undefined ? { tools: effectiveSelectors.tools } : {}),
173
- ...((project.retention ?? global.retention) === undefined ? {} : { retention: project.retention ?? global.retention }),
174
- ...(projectHas("worktreePostCreateCommand") ? { worktreePostCreateCommand: project.worktreePostCreateCommand } : global.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: global.worktreePostCreateCommand }),
175
- });
176
- return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
175
+ const sources: WorkflowSettingsSources = { concurrency: project.concurrency === undefined ? globalSettingsPath : projectSettingsPath, modelAliases: project.modelAliases === undefined ? globalSettingsPath : projectSettingsPath };
176
+ for (const key of ["skills", "extensions", "tools", "extensionSettings", "retention", "worktreePostCreateCommand"] as const) if (effective[key] !== undefined) sources[key] = project[key] === undefined ? globalSettingsPath : projectSettingsPath;
177
+ return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources, selectorSources };
177
178
  }
178
179
  export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
179
180
  for (const name of names) {
@@ -188,7 +189,7 @@ export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean,
188
189
  const global = selectorSet(selectorsFromSettings(resolved.global));
189
190
  const project = selectorSet(selectorsFromSettings(resolved.project));
190
191
  const effective = selectorSet(selectorsFromSettings(resolved.effective));
191
- return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: selectorsFromSettings(resolved.global), project: selectorsFromSettings(resolved.project) } };
192
+ return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: resolved.selectorSources };
192
193
  }
193
194
  export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
194
195
  const normalized = validateModelAliases(aliases, path);
@@ -204,3 +205,8 @@ export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonl
204
205
  atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
205
206
  }
206
207
 
208
+
209
+ export function canonicalExtensionSelector(selector: string, cwd: string): string {
210
+ const negated = selector.startsWith("!");
211
+ return `${negated ? "!" : ""}${normalizedResourcePath(negated ? selector.slice(1) : selector, join(cwd, "settings.json"))}`;
212
+ }
package/src/store.ts CHANGED
@@ -4,7 +4,7 @@ import { access, mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { promisify } from "node:util";
7
- import { HARD_TERMINAL_RUN_STATES, WorkflowError, type JsonValue, type LaunchSnapshot, type WorkflowErrorCode, type WorkflowRunEvent } from "./types.js";
7
+ import { HARD_TERMINAL_RUN_STATES, WorkflowError, type PreparedAgentConfiguration, type JsonValue, type LaunchSnapshot, type WorkflowErrorCode, type WorkflowLaunchMode, type WorkflowRunEvent } from "./types.js";
8
8
  import { coerceWorkflowError, errorText, isNodeError, loadLaunchSnapshot, object, positiveInteger, SerialLane } from "./utils.js";
9
9
  import { budgetUsage } from "./budget.js";
10
10
  import {
@@ -32,7 +32,7 @@ function summaryFromRun(run: PersistedRun, directory: string, journal: Journal,
32
32
  const failedAt = run.failedAt ?? run.error?.failedAt;
33
33
  const replayablePaths = [...new Set([...(run.retry?.completedPaths ?? []), ...Object.keys(journal.completed)])];
34
34
  const incompletePaths = [...new Set([...(run.retry?.incompletePaths ?? []), ...(failedAt ? [failedAt] : [])])];
35
- return { schemaVersion: 1, runId: run.id, sessionId: run.sessionId, workflowName: run.workflowName, state: run.state, createdAt, updatedAt: now, ...(previous?.terminalAt || HARD_TERMINAL_RUN_STATES.has(run.state) ? { terminalAt: previous?.terminalAt ?? now } : {}), usage: budgetUsage(run.usage), agents: run.agents.map(({ id, name, label, state, role, attempts }) => ({ id, name, ...(label ? { label } : {}), state, ...(role ? { role } : {}), attempts })), ...(run.error ? { error: run.error } : {}), ...(failedAt ? { failedAt } : {}), replayablePaths, incompletePaths, artifacts: summaryArtifacts(directory) };
35
+ return { schemaVersion: 1, runId: run.id, sessionId: run.sessionId, workflowName: run.workflowName, state: run.state, createdAt, updatedAt: now, ...(previous?.terminalAt || HARD_TERMINAL_RUN_STATES.has(run.state) ? { terminalAt: previous?.terminalAt ?? now } : {}), usage: budgetUsage(run.usage), agents: run.agents.map(({ id, name, label, state, attempts }) => ({ id, name, ...(label ? { label } : {}), state, attempts })), ...(run.error ? { error: run.error } : {}), ...(failedAt ? { failedAt } : {}), replayablePaths, incompletePaths, artifacts: summaryArtifacts(directory) };
36
36
  }
37
37
  function systemPromptStoragePath(directory: string): string { return join(directory, SYSTEM_PROMPT_STORAGE); }
38
38
  function systemPromptRecordsPath(directory: string): string { return join(systemPromptStoragePath(directory), SYSTEM_PROMPT_RECORDS); }
@@ -163,6 +163,22 @@ export class RunStore {
163
163
  return write;
164
164
  }
165
165
 
166
+ async saveAgentConfiguration(key: string, configuration: PreparedAgentConfiguration): Promise<void> {
167
+ await this.launchSnapshotLane.run(async () => {
168
+ const snapshot = decodeLaunchSnapshot(await json(join(this.directory, "snapshot.json")));
169
+ if (!snapshot) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
170
+ await atomicJson(join(this.directory, "snapshot.json"), { ...snapshot, agentConfigurations: { ...snapshot.agentConfigurations, [key]: configuration } });
171
+ });
172
+ }
173
+
174
+ async setLaunchMode(launchMode: WorkflowLaunchMode): Promise<void> {
175
+ await this.launchSnapshotLane.run(async () => {
176
+ const snapshot = decodeLaunchSnapshot(await json(join(this.directory, "snapshot.json")));
177
+ if (!snapshot) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
178
+ await atomicJson(join(this.directory, "snapshot.json"), { ...snapshot, launchMode });
179
+ });
180
+ }
181
+
166
182
  async saveSnapshot(snapshot: Readonly<LaunchSnapshot>): Promise<void> {
167
183
  const write = this.launchSnapshotLane.run(() => atomicJson(join(this.directory, "snapshot.json"), snapshot));
168
184
  await write;
package/src/trajectory.ts CHANGED
@@ -5,12 +5,13 @@ 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
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, AgentRecord, 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;
@@ -22,7 +23,8 @@ export type TrajectoryAgentOutput =
22
23
  | { readonly status: "truncated"; readonly kind: "result" | "failure"; readonly bytes: number; readonly path?: string }
23
24
  | { readonly status: "unavailable" };
24
25
  export type TrajectoryAgent = AgentRecord & { readonly output: TrajectoryAgentOutput };
25
- export type TrajectoryRunRecord = Omit<PersistedRun, "agents"> & { readonly agents: readonly TrajectoryAgent[] };
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[] };
26
28
  export type TrajectoryRun = {
27
29
  run: TrajectoryRunRecord;
28
30
  snapshot: Readonly<LaunchSnapshot>;
@@ -64,7 +66,6 @@ export type TrajectorySubagent = {
64
66
  sessionId: string;
65
67
  cwd: string;
66
68
  label?: string;
67
- role?: string;
68
69
  mode: "background" | "foreground";
69
70
  state: SubagentStatus["state"];
70
71
  startedAt?: number;
@@ -124,7 +125,7 @@ function transcriptToolCallId(value: unknown): string | undefined {
124
125
  for (const part of message.content) if (object(part) && typeof part.id === "string") return part.id;
125
126
  return undefined;
126
127
  }
127
- function isTimingTranscriptEntry(value: unknown): boolean { return object(value) && value.type === "custom" && value.customType === "pi-workflows:tool-timing"; }
128
+ export function isTimingTranscriptEntry(value: unknown): boolean { return object(value) && value.type === "custom" && value.customType === TOOL_TIMING_ENTRY_TYPE; }
128
129
  function timingToolCallId(value: unknown): string | undefined {
129
130
  if (!object(value) || !isTimingTranscriptEntry(value) || !object(value.data)) return undefined;
130
131
  return typeof value.data.toolCallId === "string" ? value.data.toolCallId : undefined;
@@ -243,7 +244,8 @@ export function withPiToolDescriptionsForTools(tools: readonly string[], cwd: st
243
244
  function canonicalExtensionSelector(selector: string, base: string): string {
244
245
  const negated = selector.startsWith("!");
245
246
  const body = negated ? selector.slice(1) : selector;
246
- if (body === "*" || body === "**" || body.startsWith("**/")) return selector;
247
+ // Built-in extensions are named `builtin:<name>`, not by a path.
248
+ if (body === "*" || body === "**" || body.startsWith("**/") || body.startsWith("builtin:")) return selector;
247
249
  const resolved = resolve(base, body);
248
250
  if (resourcePatternHasMagic(body)) return `${negated ? "!" : ""}${resolved}`;
249
251
  return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
@@ -279,8 +281,8 @@ async function resolveResourceInspection(inspection: ResourceInspection | undefi
279
281
  let discovered: DiscoveredResources;
280
282
  try { discovered = await discoveredResources(cwd); } catch { return inspection; }
281
283
  const sources = inspection.selectorSources ?? { global: inspection.selectors, project: {} };
282
- const skills = inspection.skills.length ? inspection.skills : selectResourcesByLayers([sources.global.skills, sources.project.skills, sources.role?.skills, sources.call?.skills], discovered.skills);
283
- const extensions = inspection.extensions.length ? inspection.extensions : selectResourcesByLayers([sources.global.extensions, sources.project.extensions, sources.role?.extensions, sources.call?.extensions].map((layer) => layer?.map((selector) => canonicalExtensionSelector(selector, selectorCwd))), discovered.extensions);
284
+ const skills = inspection.skills.length ? inspection.skills : selectResourcesByLayers([sources.global.skills, sources.project.skills, sources.call?.skills], discovered.skills);
285
+ const extensions = inspection.extensions.length ? inspection.extensions : selectResourcesByLayers([sources.global.extensions, sources.project.extensions, sources.call?.extensions].map((layer) => layer?.map((selector) => canonicalExtensionSelector(selector, selectorCwd))), discovered.extensions);
284
286
  return skills === inspection.skills && extensions === inspection.extensions ? inspection : { ...inspection, skills, extensions };
285
287
  }
286
288
  export async function withResolvedAttemptResources(attempt: AgentAttemptSummary, cwd: string): Promise<AgentAttemptSummary> {
@@ -319,11 +321,17 @@ function failedAgentOutput(agent: PersistedRun["agents"][number]): TrajectoryAge
319
321
  const status = agent.state === "cancelled" ? "cancelled" : "failed";
320
322
  return { status, code: error?.code ?? (status === "cancelled" ? "CANCELLED" : "AGENT_FAILED"), message: error?.message ?? `Agent ${agent.state}` };
321
323
  }
324
+ function operationOutput(operation: CompletedOperation): TrajectoryAgentOutput {
325
+ const bytes = serializedJsonBytes(operation.value);
326
+ return bytes > DEFAULT_MAX_BYTES ? { status: "truncated", kind: "result", bytes } : { status: "available", value: operation.value, bytes };
327
+ }
328
+ function outputForCall(call: WorkflowScriptCall, operation: CompletedOperation | undefined): TrajectoryScriptCall["output"] {
329
+ if (operation !== undefined) return call.kind === "tool" ? { status: "completed" } : operationOutput(operation);
330
+ if (call.error) return { status: call.error.code === "CANCELLED" ? "cancelled" : "failed", code: call.error.code, ...(call.kind === "shell" ? { message: call.error.message ?? "Shell failed" } : {}) };
331
+ return call.finishedAt === undefined ? { status: "pending" } : { status: "unavailable" };
332
+ }
322
333
  function outputForAgent(agent: PersistedRun["agents"][number], operation: CompletedOperation | undefined): TrajectoryAgentOutput {
323
- if (operation !== undefined) {
324
- const bytes = serializedJsonBytes(operation.value);
325
- return bytes > DEFAULT_MAX_BYTES ? { status: "truncated", kind: "result", bytes } : { status: "available", value: operation.value, bytes };
326
- }
334
+ if (operation !== undefined) return operationOutput(operation);
327
335
  if (agent.state === "failed" || agent.state === "cancelled") return failedAgentOutput(agent);
328
336
  if (agent.state === "completed") return { status: "unavailable" };
329
337
  return { status: "pending" };
@@ -331,17 +339,27 @@ function outputForAgent(agent: PersistedRun["agents"][number], operation: Comple
331
339
  export function applyTrajectoryAgentOutputs(run: PersistedRun, operations: readonly CompletedOperation[]): TrajectoryRunRecord {
332
340
  const byPath = new Map(operations.map((operation) => [operation.path, operation]));
333
341
  const agents: readonly TrajectoryAgent[] = run.agents.map((agent) => ({ ...agent, output: outputForAgent(agent, agent.resultPath === undefined ? undefined : byPath.get(agent.resultPath)) }));
334
- return { ...run, agents };
342
+ const scriptCalls = run.scriptCalls?.map((call): TrajectoryScriptCall => {
343
+ const recorded = { ...call, output: outputForCall(call, byPath.get(call.path)) };
344
+ if (call.kind === "tool") {
345
+ delete recorded.input;
346
+ delete recorded.inputBytes;
347
+ if (call.error) recorded.error = { code: call.error.code };
348
+ }
349
+ return recorded;
350
+ });
351
+ const rest: Omit<PersistedRun, "scriptCalls"> = run;
352
+ return { ...rest, agents, ...(scriptCalls === undefined ? {} : { scriptCalls }) };
335
353
  }
336
354
  function overlayTrajectoryRun(run: TrajectoryRunRecord, overlay: (run: PersistedRun) => PersistedRun): TrajectoryRunRecord {
337
- const overlaid = overlay(run);
355
+ const overlaid: Omit<PersistedRun, "scriptCalls"> = overlay(run);
338
356
  const outputs = new Map(run.agents.map((agent) => [agent.id, agent.output]));
339
357
  const agents: readonly TrajectoryAgent[] = overlaid.agents.map((agent) => {
340
358
  const previous = outputs.get(agent.id);
341
359
  const output = previous?.status === "available" || previous?.status === "truncated" ? previous : outputForAgent(agent, undefined);
342
360
  return { ...agent, output };
343
361
  });
344
- return { ...overlaid, agents };
362
+ return { ...overlaid, agents, ...(run.scriptCalls === undefined ? {} : { scriptCalls: run.scriptCalls }) };
345
363
  }
346
364
  async function loadTrajectoryRun(store: RunStore, includeTranscripts = true): Promise<TrajectoryRun> {
347
365
  const value = await store.load();
@@ -546,7 +564,6 @@ async function loadTrajectorySubagent(directory: string, cwd: string, sessionId:
546
564
  return {
547
565
  id, sessionId, cwd, mode: request.mode ?? "background", state: status.state, request, tools, transcript,
548
566
  ...(request.label === undefined ? {} : { label: request.label }),
549
- ...(request.role === undefined ? {} : { role: request.role }),
550
567
  ...(status.startedAt === undefined ? {} : { startedAt: status.startedAt }),
551
568
  ...(status.finishedAt === undefined ? {} : { finishedAt: status.finishedAt }),
552
569
  ...(status.attempts === undefined ? {} : { attempts: status.attempts }),