@marcoscale98/pi-extensible-workflows 5.19.1-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 (123) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +365 -127
  5. package/dist/src/background-widget.d.ts +0 -1
  6. package/dist/src/background-widget.js +4 -9
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +33 -37
  9. package/dist/src/execution.d.ts +2 -0
  10. package/dist/src/execution.js +5 -5
  11. package/dist/src/host-delivery.js +4 -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 +6 -6
  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 +1 -1
  18. package/dist/src/host-view.js +17 -15
  19. package/dist/src/host.js +62 -87
  20. package/dist/src/index.d.ts +1 -2
  21. package/dist/src/index.js +1523 -1630
  22. package/dist/src/index.js.map +3 -3
  23. package/dist/src/paths.d.ts +7 -6
  24. package/dist/src/paths.js +24 -15
  25. package/dist/src/persistence.d.ts +1 -0
  26. package/dist/src/persistence.js +1 -0
  27. package/dist/src/pi-runtime-runner.js +4 -1
  28. package/dist/src/registry.d.ts +9 -7
  29. package/dist/src/registry.js +28 -70
  30. package/dist/src/runtime/agent-runner.d.ts +2 -0
  31. package/dist/src/session-lease.d.ts +1 -1
  32. package/dist/src/session-lease.js +45 -11
  33. package/dist/src/settings.d.ts +6 -3
  34. package/dist/src/settings.js +36 -47
  35. package/dist/src/store.d.ts +3 -1
  36. package/dist/src/store.js +17 -1
  37. package/dist/src/trajectory.d.ts +0 -1
  38. package/dist/src/trajectory.js +2 -3
  39. package/dist/src/types.d.ts +100 -44
  40. package/dist/src/types.js +4 -7
  41. package/dist/src/utils.d.ts +3 -2
  42. package/dist/src/utils.js +20 -10
  43. package/dist/src/validation.d.ts +8 -2
  44. package/dist/src/validation.js +122 -86
  45. package/dist/starter/index.js +8 -9
  46. package/dist/starter/index.js.map +1 -1
  47. package/dist/starter/review-loop.js +8 -7
  48. package/dist/subagents/index.js +654 -814
  49. package/dist/subagents/index.js.map +3 -3
  50. package/dist/subagents/src/contracts.d.ts +4 -3
  51. package/dist/subagents/src/contracts.js +2 -2
  52. package/dist/subagents/src/index.d.ts +1 -1
  53. package/dist/subagents/src/index.js +1 -2
  54. package/dist/subagents/src/manager.js +49 -70
  55. package/dist/subagents/src/navigator.js +3 -14
  56. package/dist/subagents/src/view.d.ts +0 -1
  57. package/dist/subagents/src/view.js +2 -16
  58. package/dist/subagents/src/worktree.js +1 -1
  59. package/dist/trajectory/assets/index.html +8 -8
  60. package/dist/trajectory/index.js +487 -891
  61. package/dist/trajectory/index.js.map +3 -3
  62. package/dist/trajectory/src/assets/index.html +8 -8
  63. package/dist/trajectory/src/index.js +126 -86
  64. package/dist/trajectory/src/server.js +264 -25
  65. package/dist/trajectory/src/server.js.map +3 -3
  66. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  67. package/dist/trajectory/src/startup-mutex.js +172 -0
  68. package/examples/workflow-extension-template/README.md +4 -7
  69. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  70. package/examples/workflow-extension-template/index.js +0 -3
  71. package/package.json +5 -5
  72. package/skills/pi-extensible-workflows/SKILL.md +10 -8
  73. package/src/agent-execution.ts +339 -127
  74. package/src/background-widget.ts +4 -10
  75. package/src/decoders.ts +31 -37
  76. package/src/execution.ts +5 -5
  77. package/src/host-delivery.ts +4 -8
  78. package/src/host-navigator.ts +4 -2
  79. package/src/host-phases.ts +1 -1
  80. package/src/host-recovery.ts +20 -26
  81. package/src/host-runtime.ts +44 -6
  82. package/src/host-view.ts +18 -11
  83. package/src/host.ts +62 -113
  84. package/src/index.ts +1 -2
  85. package/src/paths.ts +18 -12
  86. package/src/persistence.ts +1 -0
  87. package/src/pi-runtime-runner.ts +3 -1
  88. package/src/registry.ts +27 -63
  89. package/src/runtime/agent-runner.ts +2 -0
  90. package/src/session-lease.ts +34 -6
  91. package/src/settings.ts +35 -47
  92. package/src/store.ts +18 -2
  93. package/src/trajectory.ts +2 -4
  94. package/src/types.ts +71 -30
  95. package/src/utils.ts +18 -9
  96. package/src/validation.ts +102 -79
  97. package/starter/index.ts +0 -1
  98. package/starter/review-loop.ts +8 -7
  99. package/starter/roles/developer.md +0 -1
  100. package/starter/roles/oracle.md +0 -1
  101. package/starter/roles/researcher.md +0 -1
  102. package/starter/roles/reviewer.md +0 -1
  103. package/starter/roles/scout.md +0 -1
  104. package/subagents/README.md +8 -14
  105. package/subagents/src/contracts.ts +3 -4
  106. package/subagents/src/index.ts +2 -3
  107. package/subagents/src/manager.ts +45 -125
  108. package/subagents/src/navigator.ts +4 -13
  109. package/subagents/src/view.ts +4 -14
  110. package/subagents/src/worktree.ts +1 -1
  111. package/trajectory/src/assets/index.html +8 -8
  112. package/trajectory/src/index.ts +106 -82
  113. package/trajectory/src/server.ts +44 -8
  114. package/trajectory/src/startup-mutex.ts +127 -0
  115. package/dist/src/roles.d.ts +0 -70
  116. package/dist/src/roles.js +0 -326
  117. package/dist/starter/roles/developer.md +0 -6
  118. package/dist/starter/roles/oracle.md +0 -18
  119. package/dist/starter/roles/researcher.md +0 -20
  120. package/dist/starter/roles/reviewer.md +0 -19
  121. package/dist/starter/roles/scout.md +0 -18
  122. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  123. package/src/roles.ts +0 -327
package/src/registry.ts CHANGED
@@ -1,14 +1,9 @@
1
- import { readFileSync } from "node:fs";
2
- import { basename, dirname, isAbsolute, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
1
  import { Value } from "typebox/value";
5
- import type { AgentAttemptAction, JsonSchema, JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration } from "./types.js";
2
+ import type { AgentAttemptAction, JsonSchema, JsonValue, RegisteredAgentSetupHook, RegisteredAgentPreparationHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext } from "./types.js";
6
3
  import type { SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
7
4
  import { byPriorityThenName, deepFreeze, errorCode, errorText, fail, jsonValue, MODEL_ALIAS_NAME, object, validWorkflowExtensionNamespace } from "./utils.js";
8
5
  import { loadSettings, resolveWorkflowSettings } from "./settings.js";
9
6
  import { validateSchema } from "./utils.js";
10
- import { canonicalPath } from "./paths.js";
11
- import { isCorePackageName } from "./package-identity.js";
12
7
 
13
8
  const RESERVED_GLOBALS = new Set(["agent", "shell", "tools", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
14
9
  const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
@@ -16,33 +11,6 @@ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-
16
11
  const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
17
12
  export type SubagentStatusObserver = (status: Readonly<SubagentStatus>, request: Readonly<SubagentRunRequest>) => void;
18
13
 
19
- function normalizeRoleDirectory(value: unknown): string {
20
- try {
21
- if (value instanceof URL) {
22
- if (value.protocol !== "file:") fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
23
- return fileURLToPath(value);
24
- }
25
- if (typeof value === "string") {
26
- if (value.startsWith("file:")) return fileURLToPath(value);
27
- if (isAbsolute(value)) return value;
28
- }
29
- } catch (error) {
30
- fail("INVALID_METADATA", `Invalid workflow role directory: ${errorText(error)}`);
31
- }
32
- fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
33
- }
34
- function isBuiltinRoleDirectory(path: string): boolean {
35
- const canonical = canonicalPath(path);
36
- const starterDirectory = dirname(canonical);
37
- if (basename(canonical) !== "roles" || basename(starterDirectory) !== "starter") return false;
38
- const distributionDirectory = dirname(starterDirectory);
39
- const packageRoot = basename(distributionDirectory) === "dist" ? dirname(distributionDirectory) : distributionDirectory;
40
- try {
41
- const metadata: unknown = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
42
- return object(metadata) && isCorePackageName(metadata.name);
43
- } catch { return false; }
44
- }
45
-
46
14
  function catalogSchema(schema: unknown, at: string): JsonSchema {
47
15
  validateSchema(schema, at);
48
16
  return structuredClone(schema);
@@ -51,9 +19,9 @@ export class WorkflowRegistry {
51
19
  readonly #extensions = new Set<Readonly<WorkflowExtension>>();
52
20
  readonly #functionSources = new Map<string, WorkflowFunctionSource>();
53
21
  readonly #globals = new Map<string, string>();
22
+ readonly #preparationHooks = new Map<string, RegisteredAgentPreparationHook>();
54
23
  readonly #hooks = new Map<string, RegisteredAgentSetupHook>();
55
24
  readonly #agentAttemptActions = new Map<string, AgentAttemptAction>();
56
- readonly #roleDirectories = new Map<string, WorkflowRoleDirectoryRegistration>();
57
25
  readonly #modelAliases = new Map<string, { name: string; version: string; headline: string; resolve: WorkflowModelAlias["resolve"] }>();
58
26
  readonly #settingsValidators = new Set<{ headline: string; validate: NonNullable<WorkflowExtension["validateSettings"]> }>();
59
27
  #frozen = false;
@@ -67,9 +35,10 @@ export class WorkflowRegistry {
67
35
  register(extension: WorkflowExtension): void {
68
36
  if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
69
37
  if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows")) fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
70
- if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "agentSetupHooks", "agentAttemptActions", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim()) fail("INVALID_METADATA", "Workflow extensions require a semantic version and non-empty headline");
38
+ if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "agentPreparationHooks", "agentSetupHooks", "agentAttemptActions"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim()) fail("INVALID_METADATA", "Workflow extensions require a semantic version and non-empty headline");
71
39
  const functions = extension.functions ?? {};
72
40
  const modelAliases = extension.modelAliases ?? {};
41
+ const agentPreparationHooks = extension.agentPreparationHooks ?? {};
73
42
  const agentSetupHooks = extension.agentSetupHooks ?? {};
74
43
  const agentAttemptActions = extension.agentAttemptActions ?? {};
75
44
  const source = extension.source;
@@ -79,10 +48,7 @@ export class WorkflowRegistry {
79
48
  if (validateSettings !== undefined && typeof validateSettings !== "function") fail("INVALID_METADATA", "Workflow extension validateSettings must be a function");
80
49
  if (source !== undefined && (typeof source !== "string" || !source.trim())) fail("INVALID_METADATA", "Workflow extension source must be a non-empty module URL");
81
50
  if (dependencyValues !== undefined && (!Array.isArray(dependencyValues) || dependencyValues.some((dependency) => typeof dependency !== "string" || dependency.trim() !== dependency || !PACKAGE_NAME.test(dependency)) || new Set(dependencyValues).size !== dependencyValues.length)) fail("INVALID_METADATA", "Workflow extension dependencies must be unique, non-empty package names");
82
- const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
83
- if (!Array.isArray(roleDirectoryValues)) fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
84
- const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
85
- if (!object(functions) || !object(modelAliases) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && roleDirectories.length === 0 && validateSettings === undefined)) fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent setup hooks, agent attempt actions, role directories, or a settings validator");
51
+ if (!object(functions) || !object(modelAliases) || !object(agentPreparationHooks) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentPreparationHooks).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && validateSettings === undefined)) fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent preparation hooks, agent setup hooks, agent attempt actions, or a settings validator");
86
52
  const names = Object.keys(functions);
87
53
  if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
88
54
  for (const name of names) {
@@ -101,6 +67,11 @@ export class WorkflowRegistry {
101
67
  if (!object(alias) || Object.keys(alias).some((key) => key !== "resolve") || typeof alias.resolve !== "function") fail("INVALID_METADATA", `Invalid model alias resolver: ${name}`);
102
68
  if (this.#modelAliases.has(name)) fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
103
69
  }
70
+ for (const [name, hook] of Object.entries(agentPreparationHooks)) {
71
+ if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "optionsSchema", "prepare"].includes(key)) || typeof hook.prepare !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority))) fail("INVALID_METADATA", `Invalid agent preparation hook: ${name}`);
72
+ if (this.#preparationHooks.has(name)) fail("DUPLICATE_NAME", `Agent preparation hook already registered: ${name}`);
73
+ if (hook.optionsSchema !== undefined) validateSchema(hook.optionsSchema, `${name} options schema`);
74
+ }
104
75
  for (const [name, hook] of Object.entries(agentSetupHooks)) {
105
76
  if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority))) fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
106
77
  if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
@@ -110,17 +81,22 @@ export class WorkflowRegistry {
110
81
  if ((action.visibleStandalone !== undefined) !== (action.runStandalone !== undefined)) fail("INVALID_METADATA", `Standalone agent attempt actions require visibleStandalone and runStandalone: ${name}`);
111
82
  if (this.#agentAttemptActions.has(name)) fail("DUPLICATE_NAME", `Agent attempt action already registered: ${name}`);
112
83
  }
113
- const stored = deepFreeze({ ...extension, functions, modelAliases, agentSetupHooks, agentAttemptActions, ...(roleDirectories.length ? { roleDirectories } : {}) });
84
+ const stored = deepFreeze({ ...extension, functions, modelAliases, agentPreparationHooks, agentSetupHooks, agentAttemptActions });
114
85
  if (source !== undefined) for (const name of names) this.#functionSources.set(name, Object.freeze({ module: source, export: "default", dependencies: Object.freeze([...dependencies]) }));
115
86
  this.#extensions.add(stored);
116
- for (const directory of roleDirectories) if (!this.#roleDirectories.has(directory)) this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline }, ...(isBuiltinRoleDirectory(directory) ? { builtin: true as const } : {}) }));
117
87
  for (const name of names) this.#globals.set(name, name);
118
88
  for (const [name, alias] of Object.entries(modelAliases)) this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, resolve: alias.resolve });
89
+ for (const [name, hook] of Object.entries(agentPreparationHooks)) this.#preparationHooks.set(name, { name, priority: hook.priority ?? 10, prepare: hook.prepare, ...(hook.optionsSchema === undefined ? {} : { optionsSchema: structuredClone(hook.optionsSchema) }), ...(source === undefined ? {} : { source }) });
119
90
  for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
120
91
  for (const [name, action] of Object.entries(agentAttemptActions)) this.#agentAttemptActions.set(name, action);
121
92
  if (validateSettings !== undefined) this.#settingsValidators.add({ headline: extension.headline, validate: validateSettings });
122
93
  }
123
94
 
95
+ /** Every registration's headline and declared module provenance, for hosts that must prove which extension made it. */
96
+ registrations(): ReadonlyArray<Readonly<{ headline: string; source?: string }>> {
97
+ return [...this.#extensions].map(({ headline, source }) => Object.freeze({ headline, ...(source === undefined ? {} : { source }) }));
98
+ }
99
+
124
100
  function(name: string): WorkflowFunction {
125
101
  if (!IDENTIFIER.test(name)) fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
126
102
  const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
@@ -145,7 +121,7 @@ export class WorkflowRegistry {
145
121
  let source = "global settings";
146
122
  try {
147
123
  const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : undefined;
148
- aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
124
+ aliases = resolved?.effective.modelAliases ?? (context ? loadSettings().modelAliases : resolveWorkflowSettings(process.cwd(), false).effective.modelAliases);
149
125
  if (resolved) { source = resolved.projectTrusted && [resolved.sources.modelAliases, resolved.sources.skills, resolved.sources.extensions, resolved.sources.tools].some((path) => path === resolved.projectSettingsPath) ? "trusted project settings" : "global settings"; settings = { concurrency: resolved.effective.concurrency, backgroundWidget: resolved.effective.backgroundWidget ?? true, modelAliases: resolved.effective.modelAliases ?? {}, skills: resolved.effective.skills ?? [], extensions: Array.isArray(resolved.effective.extensions) ? resolved.effective.extensions : [], tools: resolved.effective.tools ?? [], ...(resolved.effective.extensionSettings === undefined ? {} : { extensionSettings: resolved.effective.extensionSettings }), ...(resolved.effective.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: resolved.effective.worktreePostCreateCommand }), globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted: resolved.projectTrusted, sources: resolved.sources }; }
150
126
  } catch { aliases = undefined; }
151
127
  const staticEntries: WorkflowCatalogModelAlias[] = Object.keys(aliases ?? {}).map((name) => ({ name, kind: "static", provenance: source }));
@@ -197,7 +173,7 @@ export class WorkflowRegistry {
197
173
  for (const { headline, validate } of this.#settingsValidators) {
198
174
  try { validate(frozenSettings, Object.freeze({ ...context })); }
199
175
  catch (error) {
200
- const location = context.source === "role" && context.role ? `role ${context.role}` : context.source;
176
+ const location = context.source;
201
177
  fail("INVALID_SETTINGS", `Invalid extension settings (${headline}; ${location}): ${errorText(error)}`);
202
178
  }
203
179
  }
@@ -217,16 +193,11 @@ export class WorkflowRegistry {
217
193
  return structuredClone(stored);
218
194
  }
219
195
 
196
+ agentPreparationHooks(): readonly RegisteredAgentPreparationHook[] { return [...this.#preparationHooks.values()].sort(byPriorityThenName); }
220
197
  agentSetupHooks(): readonly RegisteredAgentSetupHook[] {
221
198
  return [...this.#hooks.values()].sort(byPriorityThenName);
222
199
  }
223
200
  agentAttemptActions(): Readonly<Record<string, AgentAttemptAction>> { return Object.freeze(Object.fromEntries(this.#agentAttemptActions.entries())); }
224
- roleDirectories(): readonly string[] {
225
- return [...this.#roleDirectories.keys()];
226
- }
227
- roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[] {
228
- return [...this.#roleDirectories.values()];
229
- }
230
201
  modelAliases(): readonly { name: string; version: string; headline: string; resolve: WorkflowModelAlias["resolve"] }[] { return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name)); }
231
202
  async resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed: ReadonlySet<string> = new Set()): Promise<Readonly<Record<string, string>>> {
232
203
  const resolved: Record<string, string> = {};
@@ -247,7 +218,7 @@ export class WorkflowRegistry {
247
218
  return Object.freeze(resolved);
248
219
  }
249
220
  }
250
- export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "functionSources" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "validateExtensionSettings" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "agentAttemptActions" | "roleDirectories" | "roleDirectoryRegistrations" | "setSubagentStatusObserver" | "observeSubagentStatus">;
221
+ export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "functionSources" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "validateExtensionSettings" | "modelAliases" | "resolveModelAliases" | "agentPreparationHooks" | "agentSetupHooks" | "agentAttemptActions" | "setSubagentStatusObserver" | "observeSubagentStatus" | "registrations">;
251
222
  interface WorkflowRegistryHost { api: WorkflowRegistryApi; activeHosts: number }
252
223
  const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
253
224
  const globalRegistry = globalThis as typeof globalThis & Record<symbol, WorkflowRegistryHost | undefined>;
@@ -269,10 +240,10 @@ function createWorkflowRegistryApi(registry: WorkflowRegistry): WorkflowRegistry
269
240
  validateExtensionSettings: (...args) => { registry.validateExtensionSettings(...args); },
270
241
  modelAliases: () => registry.modelAliases(),
271
242
  resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
272
- roleDirectories: () => registry.roleDirectories(),
273
- roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
243
+ agentPreparationHooks: () => registry.agentPreparationHooks(),
274
244
  agentSetupHooks: () => registry.agentSetupHooks(),
275
245
  agentAttemptActions: () => registry.agentAttemptActions(),
246
+ registrations: () => registry.registrations(),
276
247
  };
277
248
  }
278
249
  function workflowRegistryHost(): WorkflowRegistryHost {
@@ -301,7 +272,9 @@ export function retainWorkflowRegistry(): () => void {
301
272
  }
302
273
  export function loadingRegistry(): WorkflowRegistryApi { return workflowRegistryHost().api; }
303
274
  beginWorkflowExtensionLoading();
304
- export function registerWorkflowExtension(extension: WorkflowExtension): void { loadingRegistry().register(extension); }
275
+ export function registerWorkflowExtension(extension: WorkflowExtension): void {
276
+ loadingRegistry().register(extension);
277
+ }
305
278
  export function workflowCatalog(context?: WorkflowCatalogContext): WorkflowCatalog { return loadingRegistry().catalog(context); }
306
279
  export function workflowCatalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex { return loadingRegistry().catalogIndex(context); }
307
280
  export function workflowCatalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogModelAlias | WorkflowCatalogError { return loadingRegistry().catalogDetail(name, context); }
@@ -310,13 +283,4 @@ export function registeredWorkflowFunctionSources(): Readonly<Record<string, Wor
310
283
  const sources = loadingRegistry().functionSources;
311
284
  return typeof sources === "function" ? sources() : {};
312
285
  }
313
- export function registeredWorkflowRoleDirectories(): readonly string[] {
314
- const directories = loadingRegistry().roleDirectories;
315
- return typeof directories === "function" ? directories() : [];
316
- }
317
- export function registeredWorkflowRoleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[] {
318
- const registrations = loadingRegistry().roleDirectoryRegistrations;
319
- return typeof registrations === "function" ? registrations() : [];
320
- }
321
-
322
- export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowRoleDirectoryRegistration } from "./types.js";
286
+ export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogModelAlias, WorkflowCatalogSettings } from "./types.js";
@@ -121,6 +121,8 @@ export interface RuntimeAgentRunRequest {
121
121
  readonly customTools: readonly RuntimeTool[];
122
122
  /** Explicit result schema; omitted agents use the default string workflow_result schema. */
123
123
  readonly resultSchema?: RuntimeJsonSchema;
124
+ /** Transient retry or budget text delivered as a separate steered message after the task, never inside it. */
125
+ readonly turnControl?: string;
124
126
  readonly run: RuntimeRunIdentity;
125
127
  readonly agent: RuntimeAgentIdentity;
126
128
  readonly signal: AbortSignal;
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { link, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
@@ -12,12 +13,39 @@ const SESSION_OWNER_FILE = "owner.json";
12
13
  const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
13
14
  const RUN_CREATE_TEMP = /^\.([a-zA-Z0-9._-]+)\.(\d+)\.[0-9a-f-]+\.tmp$/;
14
15
 
15
- export async function processAlive(pid: number, startedAt?: number): Promise<boolean> {
16
+ const START_MARGIN_MS = 60_000;
17
+ let clockTicks: number | undefined;
18
+
19
+ function clockTickRate(): number {
20
+ if (clockTicks !== undefined) return clockTicks;
21
+ try {
22
+ const rate = Number(execFileSync("getconf", ["CLK_TCK"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
23
+ clockTicks = Number.isInteger(rate) && rate > 0 ? rate : 100;
24
+ } catch { clockTicks = 100; }
25
+ return clockTicks;
26
+ }
27
+
28
+ // Process start in epoch ms from /proc/<pid>/stat field 22; undefined when unreadable or unparsable.
29
+ async function linuxProcessStart(pid: number, procRoot: string): Promise<number | undefined> {
30
+ try {
31
+ const text = await readFile(join(procRoot, String(pid), "stat"), "utf8");
32
+ // Field 2 (command name) may contain spaces and parentheses, so count fields after the last ")".
33
+ const close = text.lastIndexOf(")");
34
+ const ticks = close < 0 ? Number.NaN : Number(text.slice(close + 2).split(" ")[19]);
35
+ const btime = /^btime (\d+)$/m.exec(await readFile(join(procRoot, "stat"), "utf8"))?.[1];
36
+ if (btime === undefined || !Number.isInteger(ticks) || ticks < 0) return undefined;
37
+ return Number(btime) * 1000 + (ticks / clockTickRate()) * 1000;
38
+ } catch { return undefined; }
39
+ }
40
+
41
+ // The /proc/<pid> ctime is not a start time: procfs rebuilds the inode after cache reclaim.
42
+ // A live owner never starts after its stamp; a reused pid starts later.
43
+ export async function processAlive(pid: number, startedAt?: number, procRoot = "/proc"): Promise<boolean> {
44
+ try { process.kill(pid, 0); } catch (error) { return !isNodeError(error, "ESRCH"); }
45
+ if (startedAt === undefined || process.platform !== "linux") return true;
46
+ const start = await linuxProcessStart(pid, procRoot);
47
+ if (start !== undefined) return start <= startedAt + START_MARGIN_MS;
16
48
  try { process.kill(pid, 0); } catch (error) { return !isNodeError(error, "ESRCH"); }
17
- if (startedAt !== undefined && process.platform === "linux") {
18
- try { if ((await stat(`/proc/${String(pid)}`)).ctimeMs > startedAt) return false; }
19
- catch (error) { if (isNodeError(error, "ENOENT")) return false; }
20
- }
21
49
  return true;
22
50
  }
23
51
  export async function hasLiveSessionLease(cwd: string, sessionId: string, home = homedir()): Promise<boolean> {
@@ -73,7 +101,7 @@ export async function acquireSessionLease(cwd: string, sessionId: string, home =
73
101
  const path = join(directory, SESSION_OWNER_FILE);
74
102
  for (;;) {
75
103
  const token = randomUUID();
76
- const owner: SessionOwner = { pid: process.pid, token, startedAt: process.platform === "linux" ? (await stat(`/proc/${String(process.pid)}`)).ctimeMs : Date.now() };
104
+ const owner: SessionOwner = { pid: process.pid, token, startedAt: Date.now() };
77
105
  try {
78
106
  const handle = await open(path, "wx", 0o600);
79
107
  try { await handle.writeFile(`${JSON.stringify(owner)}\n`, "utf8"); } finally { await handle.close(); }
package/src/settings.ts CHANGED
@@ -1,18 +1,20 @@
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, 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
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
19
  // Built-in extensions are named `builtin:<name>`, not by a path.
18
20
  if (value === "*" || value.startsWith("builtin:")) return value;
@@ -64,18 +66,19 @@ function selectorsFromSettings(settings: Readonly<WorkflowSettings | WorkflowSet
64
66
  };
65
67
  }
66
68
  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 {
69
+ export function validateContextFileScopes(value: unknown, sourcePath: string): readonly ContextFileScope[] | undefined {
68
70
  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`);
71
+ if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${sourcePath}.contextFiles must be an array containing only global, project, or cwd`);
70
72
  return [...value];
71
73
  }
72
74
  export function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS"): WorkflowExtensionSettings | undefined {
73
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;
74
79
  const base = `${settingsPath}.extensionSettings`;
75
- if (!object(value)) fail(errorCode, `${base} must be an object`);
76
80
  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}`);
81
+ for (const [namespace, raw] of Object.entries(generic)) {
79
82
  if (namespace === "herdr") {
80
83
  if (!object(raw)) fail(errorCode, `${base}.herdr must be an object`);
81
84
  if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode, `${base}.herdr contains an unsupported setting`);
@@ -90,7 +93,6 @@ export function validateWorkflowExtensionSettings(value: unknown, settingsPath:
90
93
  normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }) });
91
94
  continue;
92
95
  }
93
- if (!jsonValue(raw)) fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
94
96
  normalized[namespace] = structuredClone(raw);
95
97
  }
96
98
  return deepFreeze(normalized as WorkflowExtensionSettings);
@@ -157,41 +159,22 @@ export function loadCodemodeToolsSetting(agentDir?: string): WorkflowSettings["c
157
159
  try { return loadSettings(workflowSettingsPath(agentDir)).codemodeTools; } catch { return undefined; }
158
160
  }
159
161
  export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
160
- export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
162
+ export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution & { selectorSources: AgentResourceSelectorSources } {
161
163
  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 ?? [])] }),
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 ?? [])] }),
172
174
  });
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 };
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 };
195
178
  }
196
179
  export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
197
180
  for (const name of names) {
@@ -206,7 +189,7 @@ export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean,
206
189
  const global = selectorSet(selectorsFromSettings(resolved.global));
207
190
  const project = selectorSet(selectorsFromSettings(resolved.project));
208
191
  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) } };
192
+ return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: resolved.selectorSources };
210
193
  }
211
194
  export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
212
195
  const normalized = validateModelAliases(aliases, path);
@@ -222,3 +205,8 @@ export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonl
222
205
  atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
223
206
  }
224
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
@@ -66,7 +66,6 @@ export type TrajectorySubagent = {
66
66
  sessionId: string;
67
67
  cwd: string;
68
68
  label?: string;
69
- role?: string;
70
69
  mode: "background" | "foreground";
71
70
  state: SubagentStatus["state"];
72
71
  startedAt?: number;
@@ -282,8 +281,8 @@ async function resolveResourceInspection(inspection: ResourceInspection | undefi
282
281
  let discovered: DiscoveredResources;
283
282
  try { discovered = await discoveredResources(cwd); } catch { return inspection; }
284
283
  const sources = inspection.selectorSources ?? { global: inspection.selectors, project: {} };
285
- const skills = inspection.skills.length ? inspection.skills : selectResourcesByLayers([sources.global.skills, sources.project.skills, sources.role?.skills, sources.call?.skills], discovered.skills);
286
- 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);
287
286
  return skills === inspection.skills && extensions === inspection.extensions ? inspection : { ...inspection, skills, extensions };
288
287
  }
289
288
  export async function withResolvedAttemptResources(attempt: AgentAttemptSummary, cwd: string): Promise<AgentAttemptSummary> {
@@ -565,7 +564,6 @@ async function loadTrajectorySubagent(directory: string, cwd: string, sessionId:
565
564
  return {
566
565
  id, sessionId, cwd, mode: request.mode ?? "background", state: status.state, request, tools, transcript,
567
566
  ...(request.label === undefined ? {} : { label: request.label }),
568
- ...(request.role === undefined ? {} : { role: request.role }),
569
567
  ...(status.startedAt === undefined ? {} : { startedAt: status.startedAt }),
570
568
  ...(status.finishedAt === undefined ? {} : { finishedAt: status.finishedAt }),
571
569
  ...(status.attempts === undefined ? {} : { attempts: status.attempts }),