@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
@@ -1,11 +1,11 @@
1
1
  // trajectory/src/index.ts
2
2
  import { spawn } from "node:child_process";
3
3
  import { createHash as createHash3 } from "node:crypto";
4
- import { mkdir as mkdir3, open as open3, readFile as readFile6, rm as rm5 } from "node:fs/promises";
4
+ import { mkdir as mkdir4, readFile as readFile7, rm as rm6 } from "node:fs/promises";
5
5
  import { existsSync } from "node:fs";
6
- import { dirname as dirname5, join as join8 } from "node:path";
7
- import { fileURLToPath as fileURLToPath3 } from "node:url";
8
- import { getAgentDir as getAgentDir4 } from "@earendil-works/pi-coding-agent";
6
+ import { dirname as dirname3, join as join7 } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { getAgentDir as getAgentDir3 } from "@earendil-works/pi-coding-agent";
9
9
 
10
10
  // src/trajectory-host-handle.ts
11
11
  var TRAJECTORY_HOST_KEY = /* @__PURE__ */ Symbol.for("pi-extensible-workflows.trajectory-host");
@@ -17,10 +17,12 @@ function clearTrajectoryHost(host) {
17
17
  if (globalTrajectoryHosts[TRAJECTORY_HOST_KEY] === host) globalTrajectoryHosts[TRAJECTORY_HOST_KEY] = void 0;
18
18
  }
19
19
 
20
- // src/session-lease.ts
21
- import { link, mkdir, open, readFile as readFile2, readdir, rename as rename2, rm as rm2, stat } from "node:fs/promises";
22
- import { homedir as homedir2 } from "node:os";
23
- import { join as join2 } from "node:path";
20
+ // src/io.ts
21
+ import { execFile } from "node:child_process";
22
+ import { randomUUID } from "node:crypto";
23
+ import { renameSync, rmSync, writeFileSync } from "node:fs";
24
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
25
+ import { promisify } from "node:util";
24
26
 
25
27
  // src/types.ts
26
28
  var RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
@@ -35,7 +37,7 @@ var ERROR_CODES = [
35
37
  "INVALID_SCHEMA",
36
38
  "UNKNOWN_MODEL",
37
39
  "UNKNOWN_TOOL",
38
- "UNKNOWN_AGENT_TYPE",
40
+ "UNKNOWN_AGENT",
39
41
  "RUN_OWNED",
40
42
  "RUN_NOT_FOUND",
41
43
  "REGISTRY_FROZEN",
@@ -56,13 +58,14 @@ var ERROR_CODES = [
56
58
  "INTERNAL_ERROR"
57
59
  ];
58
60
  var THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
59
- var BUDGET_DIMENSIONS = ["tokens", "costUsd", "durationMs", "agentLaunches"];
60
- var BUDGET_EVENT_TYPES = ["soft_crossed", "hard_overrun", "hard_exhausted", "adjustment_requested", "adjustment_approved", "adjustment_rejected"];
61
61
  var CONTEXT_FILE_SCOPES = ["global", "project", "cwd"];
62
62
  function isContextFileScope(value) {
63
63
  return CONTEXT_FILE_SCOPES.some((scope) => scope === value);
64
64
  }
65
+ var BUDGET_DIMENSIONS = ["tokens", "costUsd", "durationMs", "agentLaunches"];
66
+ var BUDGET_EVENT_TYPES = ["soft_crossed", "hard_overrun", "hard_exhausted", "adjustment_requested", "adjustment_approved", "adjustment_rejected"];
65
67
  var WORKFLOW_AGENT_STALL_THRESHOLD_MS = 10 * 60 * 1e3;
68
+ var LAUNCH_SNAPSHOT_IDENTITY_VERSION = 6;
66
69
  var WORKFLOW_ERROR_BRAND = /* @__PURE__ */ Symbol.for("pi-extensible-workflows.workflow-error");
67
70
  var WorkflowError = class extends Error {
68
71
  constructor(code, message) {
@@ -140,90 +143,13 @@ function errorText(error) {
140
143
  function coerceWorkflowError(code, error) {
141
144
  return error instanceof WorkflowError && error.code === code ? error : new WorkflowError(code, errorText(error));
142
145
  }
143
- function errorCode(error) {
144
- if (error instanceof WorkflowError) return isWorkflowErrorCode(error.code) ? error.code : void 0;
145
- if (!object(error)) return void 0;
146
- return isWorkflowErrorCode(error.code) ? error.code : void 0;
147
- }
148
- function fail(code, message) {
149
- throw new WorkflowError(code, message);
150
- }
151
- function byPriorityThenName(left, right) {
152
- return left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
153
- }
154
146
  function isThinkingLevel(value) {
155
147
  return THINKING_LEVELS.some((level) => level === value);
156
148
  }
157
- var MODEL_ALIAS_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
158
149
  var WORKFLOW_EXTENSION_NAMESPACE = /^[A-Za-z][A-Za-z0-9_-]*$/;
159
150
  function validWorkflowExtensionNamespace(value) {
160
151
  return WORKFLOW_EXTENSION_NAMESPACE.test(value) && value !== "__proto__" && value !== "constructor" && value !== "prototype";
161
152
  }
162
- function parseModelReference(value) {
163
- const match = /^([^/:\s]+)\/([^:\s]+)(?::([^:\s]+))?$/.exec(value);
164
- if (!match?.[1] || !match[2]) fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
165
- const thinking = match[3];
166
- if (thinking !== void 0 && !isThinkingLevel(thinking)) fail("UNKNOWN_MODEL", `Invalid thinking level: ${thinking}`);
167
- return { provider: match[1], model: match[2], ...thinking !== void 0 ? { thinking } : {} };
168
- }
169
- var MODEL_ALIAS_ERROR_NAME = /* @__PURE__ */ Symbol("modelAliasErrorName");
170
- function aliasError(message, settingsPath, name) {
171
- const error = new WorkflowError("CONFIG_ERROR", `${message} (settings: ${settingsPath})`);
172
- if (name) Object.defineProperty(error, MODEL_ALIAS_ERROR_NAME, { value: name, configurable: true });
173
- throw error;
174
- }
175
- function modelAliasName(value, aliases) {
176
- const name = /^([^/:\s]+)(?::[^:\s]+)?$/.exec(value)?.[1];
177
- return name && Object.prototype.hasOwnProperty.call(aliases, name) ? name : void 0;
178
- }
179
- function validateModelAliases(value, settingsPath = "workflow settings") {
180
- if (!object(value)) aliasError("modelAliases must be an object", settingsPath);
181
- const aliases = {};
182
- for (const [name, target] of Object.entries(value)) {
183
- if (!MODEL_ALIAS_NAME.test(name)) aliasError(`Invalid model alias name: ${name}`, settingsPath, name);
184
- if (typeof target !== "string" || !target.trim()) aliasError(`Invalid model alias target for ${name}`, settingsPath, name);
185
- aliases[name] = target;
186
- }
187
- for (const name of Object.keys(aliases)) {
188
- try {
189
- resolveModelReference(name, aliases);
190
- } catch (error) {
191
- aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath, name);
192
- }
193
- }
194
- return Object.freeze(aliases);
195
- }
196
- function unknownModel(value, target, settingsPath) {
197
- const resolved = target ? ` resolved to ${target}` : "";
198
- const path = settingsPath ? ` (settings: ${settingsPath})` : "";
199
- fail("UNKNOWN_MODEL", `Unknown model${target ? " alias" : ""} ${value}${resolved}${path}`);
200
- }
201
- function resolveModelReference(value, aliases = {}, knownModels, settingsPath) {
202
- const resolveReference = (reference, chain) => {
203
- if (reference.includes("/")) return parseModelReference(reference);
204
- const match = /^([^:\s]+)(?::([^:\s]+))?$/.exec(reference);
205
- const thinking = match?.[2];
206
- if (!match?.[1] || thinking !== void 0 && !isThinkingLevel(thinking)) unknownModel(reference, void 0, settingsPath);
207
- const alias = modelAliasName(reference, aliases);
208
- if (alias) {
209
- if (chain.includes(alias)) fail("UNKNOWN_MODEL", `Circular model alias: ${[...chain, alias].join(" -> ")}${settingsPath ? ` (settings: ${settingsPath})` : ""}`);
210
- const { [alias]: target } = aliases;
211
- if (typeof target !== "string") unknownModel(reference, void 0, settingsPath);
212
- const parsed = resolveReference(target, [...chain, alias]);
213
- return thinking !== void 0 ? { ...parsed, thinking } : parsed;
214
- }
215
- const candidates = [...knownModels ?? []].filter((model) => model.slice(model.indexOf("/") + 1) === match[1]);
216
- if (candidates.length === 1) {
217
- const [candidate] = candidates;
218
- if (candidate !== void 0) {
219
- const parsed = parseModelReference(candidate);
220
- return thinking !== void 0 ? { ...parsed, thinking } : parsed;
221
- }
222
- }
223
- unknownModel(reference, void 0, settingsPath);
224
- };
225
- return resolveReference(value, []);
226
- }
227
153
  var RESOURCE_PATTERN_OPTIONS = { dot: true, nonegate: true, nocomment: true };
228
154
  function resourcePatternBody(pattern) {
229
155
  return pattern.startsWith("!") ? pattern.slice(1) : pattern;
@@ -231,12 +157,6 @@ function resourcePatternBody(pattern) {
231
157
  function resourcePatternPath(value) {
232
158
  return value.replaceAll("\\", "/");
233
159
  }
234
- function validateResourcePattern(pattern) {
235
- const body = resourcePatternBody(pattern);
236
- if (!body) throw new Error(`Empty minimatch pattern ${JSON.stringify(pattern)}`);
237
- const matcher = new Minimatch(resourcePatternPath(body), RESOURCE_PATTERN_OPTIONS);
238
- if (matcher.makeRe() === false) throw new Error(`Invalid minimatch pattern ${JSON.stringify(pattern)}`);
239
- }
240
160
  function resourcePatternMatches(resource, pattern) {
241
161
  const body = resourcePatternBody(pattern);
242
162
  if (body === "*") return true;
@@ -261,13 +181,137 @@ function resourcePatternHasMagic(pattern) {
261
181
  function loadLaunchSnapshot(input) {
262
182
  return deepFreeze(structuredClone(input));
263
183
  }
264
- function validateSchema(schema, at = "schema") {
265
- if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema)) fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
266
- if (typeof schema.type !== "string" && !Array.isArray(schema.type) && schema.$ref === void 0 && schema.anyOf === void 0 && schema.oneOf === void 0 && schema.allOf === void 0 && schema.const === void 0 && schema.enum === void 0) fail("INVALID_SCHEMA", `${at} has no JSON Schema shape`);
267
- if (schema.required !== void 0 && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string"))) fail("INVALID_SCHEMA", `${at}.required must be an array of strings`);
268
- if (schema.properties !== void 0 && !object(schema.properties)) fail("INVALID_SCHEMA", `${at}.properties must be an object`);
184
+
185
+ // src/io.ts
186
+ var execute = promisify(execFile);
187
+ var RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 400, 800];
188
+ var syncSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
189
+ function retryableRenameError(error) {
190
+ return isNodeError(error, "EPERM") || isNodeError(error, "EBUSY") || isNodeError(error, "EACCES");
191
+ }
192
+ async function renameWithRetry(from, to) {
193
+ for (let attempt = 0; ; attempt += 1) {
194
+ try {
195
+ await rename(from, to);
196
+ return;
197
+ } catch (error) {
198
+ const delayMs = RENAME_RETRY_DELAYS_MS[attempt];
199
+ if (!retryableRenameError(error) || delayMs === void 0) throw error;
200
+ await new Promise((resolve4) => setTimeout(resolve4, delayMs));
201
+ }
202
+ }
203
+ }
204
+ function renameSyncWithRetry(from, to) {
205
+ for (let attempt = 0; ; attempt += 1) {
206
+ try {
207
+ renameSync(from, to);
208
+ return;
209
+ } catch (error) {
210
+ const delayMs = RENAME_RETRY_DELAYS_MS[attempt];
211
+ if (!retryableRenameError(error) || delayMs === void 0) throw error;
212
+ Atomics.wait(syncSleepBuffer, 0, 0, delayMs);
213
+ }
214
+ }
215
+ }
216
+ var gitIdentity = {
217
+ GIT_AUTHOR_NAME: "pi-extensible-workflows",
218
+ GIT_AUTHOR_EMAIL: "pi-extensible-workflows@localhost",
219
+ GIT_COMMITTER_NAME: "pi-extensible-workflows",
220
+ GIT_COMMITTER_EMAIL: "pi-extensible-workflows@localhost",
221
+ GIT_AUTHOR_DATE: "2000-01-01T00:00:00Z",
222
+ GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z"
223
+ };
224
+ function atomicWriteFile(path, content, sync = false) {
225
+ const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
226
+ if (sync) {
227
+ try {
228
+ writeFileSync(temporary, content, { encoding: "utf8", mode: 384 });
229
+ renameSyncWithRetry(temporary, path);
230
+ } catch (error) {
231
+ try {
232
+ rmSync(temporary, { force: true });
233
+ } catch {
234
+ }
235
+ throw error;
236
+ }
237
+ return;
238
+ }
239
+ return writeFile(temporary, content, { encoding: "utf8", mode: 384 }).then(() => renameWithRetry(temporary, path)).catch(async (error) => {
240
+ try {
241
+ await rm(temporary, { force: true });
242
+ } catch {
243
+ }
244
+ throw error;
245
+ });
246
+ }
247
+ async function atomicJson(path, value) {
248
+ const serialized = JSON.stringify(value);
249
+ if (typeof serialized !== "string") throw new WorkflowError("INTERNAL_ERROR", `Cannot serialize JSON for ${path}`);
250
+ await atomicWriteFile(path, `${serialized}
251
+ `);
252
+ }
253
+ async function atomicPrettyJson(path, value) {
254
+ await atomicWriteFile(path, `${JSON.stringify(value, null, 2)}
255
+ `);
256
+ }
257
+ async function json(path) {
258
+ return JSON.parse(await readFile(path, "utf8"));
259
+ }
260
+ async function git(cwd, args, extraEnv = {}) {
261
+ const { stdout } = await execute("git", ["-c", "core.hooksPath=/dev/null", "-c", "commit.gpgSign=false", ...args], { cwd, env: { ...process.env, ...extraEnv }, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
262
+ return stdout;
269
263
  }
270
264
 
265
+ // src/trajectory.ts
266
+ import { open as open2, readFile as readFile4, readdir as readdir3, stat as stat3 } from "node:fs/promises";
267
+ import { homedir as homedir4 } from "node:os";
268
+ import { basename as basename2, dirname as dirname2, join as join4, resolve as resolve3 } from "node:path";
269
+ import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, DefaultPackageManager, getAgentDir as getAgentDir2, SettingsManager, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
270
+
271
+ // src/paths.ts
272
+ import { createHash } from "node:crypto";
273
+ import { realpathSync } from "node:fs";
274
+ import { homedir } from "node:os";
275
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
276
+ function safePart(value) {
277
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
278
+ }
279
+ function canonicalPath(path) {
280
+ const absolute = resolve(path);
281
+ try {
282
+ return realpathSync(absolute);
283
+ } catch {
284
+ return absolute;
285
+ }
286
+ }
287
+ function sameFilesystemPath(left, right) {
288
+ return canonicalPath(left) === canonicalPath(right);
289
+ }
290
+ function workflowProjectSettingsPath(cwd) {
291
+ return join(cwd, ".pi", "pi-extensible-workflows", "settings.json");
292
+ }
293
+ function projectStorageKey(cwd) {
294
+ const exact = resolve(cwd);
295
+ const slug = safePart(basename(exact)) || "root";
296
+ return `${slug}-${createHash("sha256").update(exact).digest("hex").slice(0, 12)}`;
297
+ }
298
+ function projectSessionsDirectory(cwd, home = homedir()) {
299
+ return join(home, ".pi", "workflows", "projects", projectStorageKey(cwd), "sessions");
300
+ }
301
+ function runsDirectory(cwd, sessionId, home = homedir()) {
302
+ return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
303
+ }
304
+ function structuralPath(...names) {
305
+ if (names.length === 0 || names.some((name) => name.trim() === "")) throw new WorkflowError("INVALID_METADATA", "Structural paths require non-empty explicit names");
306
+ return names.map((name) => encodeURIComponent(name)).join("/");
307
+ }
308
+
309
+ // src/session-lease.ts
310
+ import { execFileSync } from "node:child_process";
311
+ import { link, mkdir, open, readFile as readFile2, readdir, rename as rename2, rm as rm2, stat } from "node:fs/promises";
312
+ import { homedir as homedir2 } from "node:os";
313
+ import { join as join2 } from "node:path";
314
+
271
315
  // src/decoders.ts
272
316
  var INVALID_PERSISTED_VALUE = /* @__PURE__ */ Symbol("invalid persisted value");
273
317
  function integer(value) {
@@ -356,32 +400,19 @@ function decodeAgentResourceSelectors(value) {
356
400
  function decodeContextFileScopes(value) {
357
401
  return decodeArray(value, (scope) => isContextFileScope(scope) ? scope : void 0);
358
402
  }
359
- function decodeAgentDefinition(value) {
360
- if (!object(value)) return void 0;
361
- const prompt = optionalString(value.prompt);
362
- const description = optionalString(value.description);
363
- const model = optionalString(value.model);
364
- const thinking = value.thinking;
365
- const tools = value.tools === void 0 ? void 0 : decodeStringArray(value.tools);
366
- const skills = value.skills === void 0 ? void 0 : decodeStringArray(value.skills);
367
- const extensions = value.extensions === void 0 ? void 0 : decodeStringArray(value.extensions);
368
- const overrideSystemPrompt = optionalBoolean(value.overrideSystemPrompt);
403
+ function decodePreparedAgentConfiguration(value) {
404
+ if (!object(value) || !jsonValue(value) || typeof value.projectTrusted !== "boolean" || typeof value.systemPromptAppend !== "string") return void 0;
405
+ const model = decodeModelSpec(value.model);
406
+ const tools = decodeStringArray(value.tools);
407
+ const skills = decodeStringArray(value.skills);
408
+ const extensions = decodeStringArray(value.extensions);
409
+ const settings = decodeWorkflowExtensions(value.settings);
369
410
  const contextFiles = value.contextFiles === void 0 ? void 0 : decodeContextFileScopes(value.contextFiles);
370
- const extensionSettings = value.extensionSettings === void 0 ? void 0 : decodeWorkflowExtensions(value.extensionSettings);
371
- if (prompt === INVALID_PERSISTED_VALUE || description === INVALID_PERSISTED_VALUE || model === INVALID_PERSISTED_VALUE || overrideSystemPrompt === INVALID_PERSISTED_VALUE) return void 0;
372
- if (thinking !== void 0 && !isThinkingLevel(thinking) || value.tools !== void 0 && !tools || value.skills !== void 0 && !skills || value.extensions !== void 0 && !extensions || value.contextFiles !== void 0 && !contextFiles || value.extensionSettings !== void 0 && !extensionSettings) return void 0;
373
- const foldedModel = typeof model === "string" && thinking !== void 0 && !model.includes(":") ? `${model}:${thinking}` : model;
374
- return {
375
- ...prompt === void 0 ? {} : { prompt },
376
- ...description === void 0 ? {} : { description },
377
- ...foldedModel === void 0 ? {} : { model: foldedModel },
378
- ...tools === void 0 ? {} : { tools },
379
- ...skills === void 0 ? {} : { skills },
380
- ...extensions === void 0 ? {} : { extensions },
381
- ...overrideSystemPrompt === void 0 ? {} : { overrideSystemPrompt },
382
- ...contextFiles === void 0 ? {} : { contextFiles },
383
- ...extensionSettings === void 0 ? {} : { extensionSettings }
384
- };
411
+ const label = optionalString(value.label);
412
+ const systemPrompt = optionalString(value.systemPrompt);
413
+ const excludeTools = value.excludeTools === void 0 ? void 0 : decodeStringArray(value.excludeTools);
414
+ if (!model || !tools || !skills || !extensions || !settings || label === INVALID_PERSISTED_VALUE || systemPrompt === INVALID_PERSISTED_VALUE || value.contextFiles !== void 0 && !contextFiles || value.excludeTools !== void 0 && !excludeTools) return void 0;
415
+ return { model, tools, skills, extensions, settings, projectTrusted: value.projectTrusted, systemPromptAppend: value.systemPromptAppend, ...contextFiles === void 0 ? {} : { contextFiles }, ...excludeTools === void 0 ? {} : { excludeTools }, ...label === void 0 ? {} : { label }, ...systemPrompt === void 0 ? {} : { systemPrompt } };
385
416
  }
386
417
  function decodeWorkflowMetadata(value) {
387
418
  if (!object(value) || typeof value.name !== "string") return void 0;
@@ -490,17 +521,22 @@ function decodeScheduledAgentOptions(value) {
490
521
  const parentBreadcrumb = optionalString(value.parentBreadcrumb);
491
522
  const worktreeOwner = optionalString(value.worktreeOwner);
492
523
  const model = optionalString(value.model);
493
- const role = typeof value.role === "string" ? value.role : void 0;
494
524
  const contextFiles = value.contextFiles === void 0 ? void 0 : decodeContextFileScopes(value.contextFiles);
495
525
  const schema = value.schema === void 0 ? void 0 : decodeJsonObject(value.schema);
496
526
  const retries = optionalNumber(value.retries);
497
527
  const timeoutMs = value.timeoutMs;
528
+ if (value.projectTrusted !== void 0 && typeof value.projectTrusted !== "boolean") return void 0;
529
+ const rawCapabilities = value.capabilities;
530
+ const capabilities = rawCapabilities === void 0 ? void 0 : object(rawCapabilities) ? { tools: decodeStringArray(rawCapabilities.tools), skills: decodeStringArray(rawCapabilities.skills), extensions: decodeStringArray(rawCapabilities.extensions) } : void 0;
531
+ if (rawCapabilities !== void 0 && (!capabilities?.tools || !capabilities.skills || !capabilities.extensions)) return void 0;
532
+ const configuration = value.configuration === void 0 ? void 0 : decodePreparedAgentConfiguration(value.configuration);
533
+ if (value.configuration !== void 0 && configuration === void 0) return void 0;
498
534
  const agentOptions = value.agentOptions === void 0 ? void 0 : decodeJsonObject(value.agentOptions);
499
535
  const agentIdentity = value.agentIdentity === void 0 ? void 0 : decodeIdentity(value.agentIdentity);
500
536
  const sessionPath = optionalString(value.sessionPath);
501
537
  const continuity = value.continuity === void 0 ? void 0 : isAgentContinuity(value.continuity) ? value.continuity : INVALID_PERSISTED_VALUE;
502
538
  const extensionSettings = value.extensionSettings === void 0 ? void 0 : decodeWorkflowExtensions(value.extensionSettings);
503
- if (sessionPath === INVALID_PERSISTED_VALUE || continuity === INVALID_PERSISTED_VALUE || requestedLabel === INVALID_PERSISTED_VALUE || parentBreadcrumb === INVALID_PERSISTED_VALUE || worktreeOwner === INVALID_PERSISTED_VALUE || model === INVALID_PERSISTED_VALUE || value.skills !== void 0 && !skills || value.extensions !== void 0 && !extensions || value.role !== void 0 && typeof value.role !== "string" || value.contextFiles !== void 0 && !contextFiles || value.schema !== void 0 && !schema || retries === INVALID_PERSISTED_VALUE || retries !== void 0 && !integer(retries) || timeoutMs !== void 0 && timeoutMs !== null && !finiteNumber(timeoutMs) || value.agentOptions !== void 0 && !agentOptions || value.agentIdentity !== void 0 && !agentIdentity || value.extensionSettings !== void 0 && !extensionSettings) return void 0;
539
+ if (sessionPath === INVALID_PERSISTED_VALUE || continuity === INVALID_PERSISTED_VALUE || requestedLabel === INVALID_PERSISTED_VALUE || parentBreadcrumb === INVALID_PERSISTED_VALUE || worktreeOwner === INVALID_PERSISTED_VALUE || model === INVALID_PERSISTED_VALUE || value.skills !== void 0 && !skills || value.extensions !== void 0 && !extensions || value.contextFiles !== void 0 && !contextFiles || value.schema !== void 0 && !schema || retries === INVALID_PERSISTED_VALUE || retries !== void 0 && !integer(retries) || timeoutMs !== void 0 && timeoutMs !== null && !finiteNumber(timeoutMs) || value.agentOptions !== void 0 && !agentOptions || value.agentIdentity !== void 0 && !agentIdentity || value.extensionSettings !== void 0 && !extensionSettings) return void 0;
504
540
  return {
505
541
  label: value.label,
506
542
  ...requestedLabel === void 0 ? {} : { requestedLabel },
@@ -511,7 +547,6 @@ function decodeScheduledAgentOptions(value) {
511
547
  ...extensions === void 0 ? {} : { extensions },
512
548
  ...worktreeOwner === void 0 ? {} : { worktreeOwner },
513
549
  ...model === void 0 ? {} : { model },
514
- ...role === void 0 ? {} : { role },
515
550
  ...contextFiles === void 0 ? {} : { contextFiles },
516
551
  ...schema === void 0 ? {} : { schema },
517
552
  ...retries === void 0 ? {} : { retries },
@@ -519,6 +554,9 @@ function decodeScheduledAgentOptions(value) {
519
554
  ...sessionPath === void 0 ? {} : { sessionPath },
520
555
  ...continuity === void 0 ? {} : { continuity },
521
556
  ...agentOptions === void 0 ? {} : { agentOptions },
557
+ ...configuration === void 0 ? {} : { configuration },
558
+ ...value.projectTrusted === void 0 ? {} : { projectTrusted: value.projectTrusted },
559
+ ...capabilities?.tools && capabilities.skills && capabilities.extensions ? { capabilities: { tools: capabilities.tools, skills: capabilities.skills, extensions: capabilities.extensions } } : {},
522
560
  ...agentIdentity === void 0 ? {} : { agentIdentity },
523
561
  ...extensionSettings === void 0 ? {} : { extensionSettings }
524
562
  };
@@ -559,11 +597,10 @@ function decodeAgentResourceInspection(value) {
559
597
  const sources = sourceRecord === void 0 ? void 0 : {
560
598
  global: decodeAgentResourceSelectors(sourceRecord.global),
561
599
  project: decodeAgentResourceSelectors(sourceRecord.project),
562
- ...sourceRecord.role === void 0 ? {} : { role: decodeAgentResourceSelectors(sourceRecord.role) },
563
600
  ...sourceRecord.call === void 0 ? {} : { call: decodeAgentResourceSelectors(sourceRecord.call) }
564
601
  };
565
- if (!selectors || !skills || !extensions || !tools || !unmatchedSkills || !unmatchedExtensions || !unmatchedTools || rawSources !== void 0 && (!sources || !sources.global || !sources.project || sources.role === void 0 && sourceRecord?.role !== void 0 || sources.call === void 0 && sourceRecord?.call !== void 0)) return void 0;
566
- return { selectors: { skills: [...selectors.skills ?? []], extensions: [...selectors.extensions ?? []], tools: [...selectors.tools ?? []] }, skills, extensions, tools, unmatchedSkills, unmatchedExtensions, unmatchedTools, ...sources === void 0 ? {} : { selectorSources: { global: sources.global ?? {}, project: sources.project ?? {}, ...sources.role === void 0 ? {} : { role: sources.role }, ...sources.call === void 0 ? {} : { call: sources.call } } } };
602
+ if (!selectors || !skills || !extensions || !tools || !unmatchedSkills || !unmatchedExtensions || !unmatchedTools || rawSources !== void 0 && (!sources || !sources.global || !sources.project || sources.call === void 0 && sourceRecord?.call !== void 0)) return void 0;
603
+ return { selectors: { skills: [...selectors.skills ?? []], extensions: [...selectors.extensions ?? []], tools: [...selectors.tools ?? []] }, skills, extensions, tools, unmatchedSkills, unmatchedExtensions, unmatchedTools, ...sources === void 0 ? {} : { selectorSources: { global: sources.global ?? {}, project: sources.project ?? {}, ...sources.call === void 0 ? {} : { call: sources.call } } } };
567
604
  }
568
605
  function decodeAgentSetupSummary(value) {
569
606
  if (!object(value) || typeof value.cwd !== "string") return void 0;
@@ -601,7 +638,6 @@ function decodeAgent(value) {
601
638
  const worktreeOwner = optionalString(value.worktreeOwner);
602
639
  const handle = optionalString(value.handle);
603
640
  const continuity = value.continuity === void 0 ? void 0 : isAgentContinuity(value.continuity) ? value.continuity : INVALID_PERSISTED_VALUE;
604
- const role = optionalString(value.role);
605
641
  const requestedModel = optionalString(value.requestedModel);
606
642
  const startedAt = optionalNumber(value.startedAt);
607
643
  const durationMs = optionalNumber(value.durationMs);
@@ -610,7 +646,7 @@ function decodeAgent(value) {
610
646
  const toolCalls = value.toolCalls === void 0 ? void 0 : decodeArray(value.toolCalls, decodeAgentToolCall);
611
647
  const activity = value.activity === void 0 ? void 0 : decodeAgentActivity(value.activity);
612
648
  const lastEventAt = optionalNumber(value.lastEventAt);
613
- if (systemPrompt === INVALID_PERSISTED_VALUE || prompt === INVALID_PERSISTED_VALUE || label === INVALID_PERSISTED_VALUE || parentId === INVALID_PERSISTED_VALUE || resultPath === INVALID_PERSISTED_VALUE || parentBreadcrumb === INVALID_PERSISTED_VALUE || worktreeOwner === INVALID_PERSISTED_VALUE || role === INVALID_PERSISTED_VALUE || requestedModel === INVALID_PERSISTED_VALUE || startedAt === INVALID_PERSISTED_VALUE || durationMs === INVALID_PERSISTED_VALUE || lastEventAt === INVALID_PERSISTED_VALUE || handle === INVALID_PERSISTED_VALUE || continuity === INVALID_PERSISTED_VALUE || handle !== void 0 && !positiveInteger(value.turn)) return void 0;
649
+ if (systemPrompt === INVALID_PERSISTED_VALUE || prompt === INVALID_PERSISTED_VALUE || label === INVALID_PERSISTED_VALUE || parentId === INVALID_PERSISTED_VALUE || resultPath === INVALID_PERSISTED_VALUE || parentBreadcrumb === INVALID_PERSISTED_VALUE || worktreeOwner === INVALID_PERSISTED_VALUE || requestedModel === INVALID_PERSISTED_VALUE || startedAt === INVALID_PERSISTED_VALUE || durationMs === INVALID_PERSISTED_VALUE || lastEventAt === INVALID_PERSISTED_VALUE || handle === INVALID_PERSISTED_VALUE || continuity === INVALID_PERSISTED_VALUE || handle !== void 0 && !positiveInteger(value.turn)) return void 0;
614
650
  if (!model || !tools || value.structuralPath !== void 0 && !structuralPath2 || value.attemptDetails !== void 0 && !attemptDetails || value.accounting !== void 0 && !accounting || value.toolCalls !== void 0 && !toolCalls || value.activity !== void 0 && !activity) return void 0;
615
651
  return {
616
652
  ...systemPrompt === void 0 ? {} : { systemPrompt },
@@ -627,7 +663,6 @@ function decodeAgent(value) {
627
663
  ...worktreeOwner === void 0 ? {} : { worktreeOwner },
628
664
  ...handle === void 0 ? {} : { handle, turn: value.turn },
629
665
  ...continuity === void 0 ? {} : { continuity },
630
- ...role === void 0 ? {} : { role },
631
666
  ...requestedModel === void 0 ? {} : { requestedModel },
632
667
  model,
633
668
  tools,
@@ -775,13 +810,11 @@ function decodeLaunchSnapshot(value) {
775
810
  const phases = value.phases === void 0 ? void 0 : decodeStringArray(value.phases);
776
811
  const models = decodeStringArray(value.models);
777
812
  const tools = decodeStringArray(value.tools);
778
- const agentTypes = decodeStringArray(value.agentTypes);
779
- const roles = value.roles === void 0 ? void 0 : decodeRecord(value.roles, decodeAgentDefinition);
780
- const projectRoles = value.projectRoles === void 0 ? void 0 : decodeStringArray(value.projectRoles);
813
+ const agentConfigurations = decodeRecord(value.agentConfigurations, decodePreparedAgentConfiguration);
781
814
  const schemas = decodeArray(value.schemas, decodeJsonObject);
782
- if (identityVersion === INVALID_PERSISTED_VALUE || value.identityVersion !== void 0 && !integer(identityVersion) || launchMode !== void 0 && !isLaunchMode(launchMode) || args === void 0 || !metadata || !settings || value.settingsSources !== void 0 && !settingsSources || value.budget !== void 0 && !budget || settingsPath === INVALID_PERSISTED_VALUE || value.modelAliases !== void 0 && !modelAliases || value.phases !== void 0 && !phases || !models || !tools || !agentTypes || value.roles !== void 0 && !roles || value.projectRoles !== void 0 && !projectRoles || !schemas) return void 0;
815
+ if (identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION || launchMode !== void 0 && !isLaunchMode(launchMode) || args === void 0 || !metadata || !settings || value.settingsSources !== void 0 && !settingsSources || value.budget !== void 0 && !budget || settingsPath === INVALID_PERSISTED_VALUE || value.modelAliases !== void 0 && !modelAliases || value.phases !== void 0 && !phases || !models || !tools || !agentConfigurations || !schemas) return void 0;
783
816
  return {
784
- ...identityVersion === void 0 ? {} : { identityVersion },
817
+ identityVersion: LAUNCH_SNAPSHOT_IDENTITY_VERSION,
785
818
  ...launchMode === void 0 ? {} : { launchMode },
786
819
  script: value.script,
787
820
  args,
@@ -794,9 +827,7 @@ function decodeLaunchSnapshot(value) {
794
827
  ...phases === void 0 ? {} : { phases },
795
828
  models,
796
829
  tools,
797
- agentTypes,
798
- ...roles === void 0 ? {} : { roles },
799
- ...projectRoles === void 0 ? {} : { projectRoles },
830
+ agentConfigurations,
800
831
  schemas
801
832
  };
802
833
  }
@@ -876,143 +907,45 @@ function decodeSystemPromptArtifact(value) {
876
907
  return { version: value.version, ...value.format === void 0 ? {} : { format: value.format }, ...value.storage === void 0 ? {} : { storage: value.storage }, ...entries === void 0 ? {} : { entries } };
877
908
  }
878
909
 
879
- // src/io.ts
880
- import { execFile } from "node:child_process";
881
- import { randomUUID } from "node:crypto";
882
- import { renameSync, rmSync, writeFileSync } from "node:fs";
883
- import { readFile, rename, rm, writeFile } from "node:fs/promises";
884
- import { promisify } from "node:util";
885
- var execute = promisify(execFile);
886
- var RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 400, 800];
887
- var syncSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
888
- function retryableRenameError(error) {
889
- return isNodeError(error, "EPERM") || isNodeError(error, "EBUSY") || isNodeError(error, "EACCES");
890
- }
891
- async function renameWithRetry(from, to) {
892
- for (let attempt = 0; ; attempt += 1) {
893
- try {
894
- await rename(from, to);
895
- return;
896
- } catch (error) {
897
- const delayMs = RENAME_RETRY_DELAYS_MS[attempt];
898
- if (!retryableRenameError(error) || delayMs === void 0) throw error;
899
- await new Promise((resolve5) => setTimeout(resolve5, delayMs));
900
- }
901
- }
902
- }
903
- function renameSyncWithRetry(from, to) {
904
- for (let attempt = 0; ; attempt += 1) {
905
- try {
906
- renameSync(from, to);
907
- return;
908
- } catch (error) {
909
- const delayMs = RENAME_RETRY_DELAYS_MS[attempt];
910
- if (!retryableRenameError(error) || delayMs === void 0) throw error;
911
- Atomics.wait(syncSleepBuffer, 0, 0, delayMs);
912
- }
913
- }
914
- }
915
- var gitIdentity = {
916
- GIT_AUTHOR_NAME: "pi-extensible-workflows",
917
- GIT_AUTHOR_EMAIL: "pi-extensible-workflows@localhost",
918
- GIT_COMMITTER_NAME: "pi-extensible-workflows",
919
- GIT_COMMITTER_EMAIL: "pi-extensible-workflows@localhost",
920
- GIT_AUTHOR_DATE: "2000-01-01T00:00:00Z",
921
- GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z"
922
- };
923
- function atomicWriteFile(path, content, sync = false) {
924
- const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
925
- if (sync) {
926
- try {
927
- writeFileSync(temporary, content, { encoding: "utf8", mode: 384 });
928
- renameSyncWithRetry(temporary, path);
929
- } catch (error) {
930
- try {
931
- rmSync(temporary, { force: true });
932
- } catch {
933
- }
934
- throw error;
935
- }
936
- return;
910
+ // src/session-lease.ts
911
+ var RUN_CREATE_TEMP = /^\.([a-zA-Z0-9._-]+)\.(\d+)\.[0-9a-f-]+\.tmp$/;
912
+ var START_MARGIN_MS = 6e4;
913
+ var clockTicks;
914
+ function clockTickRate() {
915
+ if (clockTicks !== void 0) return clockTicks;
916
+ try {
917
+ const rate = Number(execFileSync("getconf", ["CLK_TCK"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
918
+ clockTicks = Number.isInteger(rate) && rate > 0 ? rate : 100;
919
+ } catch {
920
+ clockTicks = 100;
937
921
  }
938
- return writeFile(temporary, content, { encoding: "utf8", mode: 384 }).then(() => renameWithRetry(temporary, path)).catch(async (error) => {
939
- try {
940
- await rm(temporary, { force: true });
941
- } catch {
942
- }
943
- throw error;
944
- });
945
- }
946
- async function atomicJson(path, value) {
947
- const serialized = JSON.stringify(value);
948
- if (typeof serialized !== "string") throw new WorkflowError("INTERNAL_ERROR", `Cannot serialize JSON for ${path}`);
949
- await atomicWriteFile(path, `${serialized}
950
- `);
951
- }
952
- async function atomicPrettyJson(path, value) {
953
- await atomicWriteFile(path, `${JSON.stringify(value, null, 2)}
954
- `);
955
- }
956
- async function json(path) {
957
- return JSON.parse(await readFile(path, "utf8"));
958
- }
959
- async function git(cwd, args, extraEnv = {}) {
960
- const { stdout } = await execute("git", ["-c", "core.hooksPath=/dev/null", "-c", "commit.gpgSign=false", ...args], { cwd, env: { ...process.env, ...extraEnv }, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
961
- return stdout;
962
- }
963
-
964
- // src/paths.ts
965
- import { createHash } from "node:crypto";
966
- import { realpathSync } from "node:fs";
967
- import { homedir } from "node:os";
968
- import { basename, join, resolve } from "node:path";
969
- function safePart(value) {
970
- return value.replace(/[^a-zA-Z0-9._-]/g, "_");
922
+ return clockTicks;
971
923
  }
972
- function canonicalPath(path) {
973
- const absolute = resolve(path);
924
+ async function linuxProcessStart(pid, procRoot) {
974
925
  try {
975
- return realpathSync(absolute);
926
+ const text = await readFile2(join2(procRoot, String(pid), "stat"), "utf8");
927
+ const close = text.lastIndexOf(")");
928
+ const ticks = close < 0 ? Number.NaN : Number(text.slice(close + 2).split(" ")[19]);
929
+ const btime = /^btime (\d+)$/m.exec(await readFile2(join2(procRoot, "stat"), "utf8"))?.[1];
930
+ if (btime === void 0 || !Number.isInteger(ticks) || ticks < 0) return void 0;
931
+ return Number(btime) * 1e3 + ticks / clockTickRate() * 1e3;
976
932
  } catch {
977
- return absolute;
933
+ return void 0;
978
934
  }
979
935
  }
980
- function sameFilesystemPath(left, right) {
981
- return canonicalPath(left) === canonicalPath(right);
982
- }
983
- function workflowProjectSettingsPath(cwd) {
984
- return join(cwd, ".pi", "pi-extensible-workflows", "settings.json");
985
- }
986
- function projectStorageKey(cwd) {
987
- const exact = resolve(cwd);
988
- const slug = safePart(basename(exact)) || "root";
989
- return `${slug}-${createHash("sha256").update(exact).digest("hex").slice(0, 12)}`;
990
- }
991
- function projectSessionsDirectory(cwd, home = homedir()) {
992
- return join(home, ".pi", "workflows", "projects", projectStorageKey(cwd), "sessions");
993
- }
994
- function runsDirectory(cwd, sessionId, home = homedir()) {
995
- return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
996
- }
997
- function structuralPath(...names) {
998
- if (names.length === 0 || names.some((name) => name.trim() === "")) throw new WorkflowError("INVALID_METADATA", "Structural paths require non-empty explicit names");
999
- return names.map((name) => encodeURIComponent(name)).join("/");
1000
- }
1001
-
1002
- // src/session-lease.ts
1003
- var RUN_CREATE_TEMP = /^\.([a-zA-Z0-9._-]+)\.(\d+)\.[0-9a-f-]+\.tmp$/;
1004
- async function processAlive(pid, startedAt) {
936
+ async function processAlive(pid, startedAt, procRoot = "/proc") {
1005
937
  try {
1006
938
  process.kill(pid, 0);
1007
939
  } catch (error) {
1008
940
  return !isNodeError(error, "ESRCH");
1009
941
  }
1010
- if (startedAt !== void 0 && process.platform === "linux") {
1011
- try {
1012
- if ((await stat(`/proc/${String(pid)}`)).ctimeMs > startedAt) return false;
1013
- } catch (error) {
1014
- if (isNodeError(error, "ENOENT")) return false;
1015
- }
942
+ if (startedAt === void 0 || process.platform !== "linux") return true;
943
+ const start = await linuxProcessStart(pid, procRoot);
944
+ if (start !== void 0) return start <= startedAt + START_MARGIN_MS;
945
+ try {
946
+ process.kill(pid, 0);
947
+ } catch (error) {
948
+ return !isNodeError(error, "ESRCH");
1016
949
  }
1017
950
  return true;
1018
951
  }
@@ -1035,18 +968,12 @@ async function listRunIds(cwd, sessionId, home = homedir2(), cleanTemps = true)
1035
968
  }
1036
969
  }
1037
970
 
1038
- // src/trajectory.ts
1039
- import { open as open2, readFile as readFile4, readdir as readdir3, stat as stat3 } from "node:fs/promises";
1040
- import { homedir as homedir5 } from "node:os";
1041
- import { basename as basename3, dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
1042
- import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, DefaultPackageManager, getAgentDir as getAgentDir3, SettingsManager, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
1043
-
1044
971
  // src/store.ts
1045
972
  import { execFile as execFile2 } from "node:child_process";
1046
973
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
1047
974
  import { access, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, realpath, rename as rename3, rm as rm3, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
1048
975
  import { homedir as homedir3 } from "node:os";
1049
- import { dirname, join as join3, relative, resolve as resolve2, sep } from "node:path";
976
+ import { dirname, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
1050
977
  import { promisify as promisify2 } from "node:util";
1051
978
 
1052
979
  // src/budget.ts
@@ -1071,7 +998,7 @@ function summaryFromRun(run, directory, journal, previous, fallbackCreatedAt, no
1071
998
  const failedAt = run.failedAt ?? run.error?.failedAt;
1072
999
  const replayablePaths = [.../* @__PURE__ */ new Set([...run.retry?.completedPaths ?? [], ...Object.keys(journal.completed)])];
1073
1000
  const incompletePaths = [.../* @__PURE__ */ new Set([...run.retry?.incompletePaths ?? [], ...failedAt ? [failedAt] : []])];
1074
- 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) };
1001
+ 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) };
1075
1002
  }
1076
1003
  function systemPromptStoragePath(directory) {
1077
1004
  return join3(directory, SYSTEM_PROMPT_STORAGE);
@@ -1219,6 +1146,20 @@ var RunStore = class _RunStore {
1219
1146
  });
1220
1147
  return write;
1221
1148
  }
1149
+ async saveAgentConfiguration(key, configuration) {
1150
+ await this.launchSnapshotLane.run(async () => {
1151
+ const snapshot = decodeLaunchSnapshot(await json(join3(this.directory, "snapshot.json")));
1152
+ if (!snapshot) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
1153
+ await atomicJson(join3(this.directory, "snapshot.json"), { ...snapshot, agentConfigurations: { ...snapshot.agentConfigurations, [key]: configuration } });
1154
+ });
1155
+ }
1156
+ async setLaunchMode(launchMode) {
1157
+ await this.launchSnapshotLane.run(async () => {
1158
+ const snapshot = decodeLaunchSnapshot(await json(join3(this.directory, "snapshot.json")));
1159
+ if (!snapshot) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
1160
+ await atomicJson(join3(this.directory, "snapshot.json"), { ...snapshot, launchMode });
1161
+ });
1162
+ }
1222
1163
  async saveSnapshot(snapshot) {
1223
1164
  const write = this.launchSnapshotLane.run(() => atomicJson(join3(this.directory, "snapshot.json"), snapshot));
1224
1165
  await write;
@@ -1493,9 +1434,9 @@ var RunStore = class _RunStore {
1493
1434
  }
1494
1435
  structuralWorktree(owner, record) {
1495
1436
  const expected = this.expectedWorktree(owner);
1496
- const relativePath = relative(this.directory, record.path);
1497
- const relativeCwd = relative(record.path, record.cwd);
1498
- if (record.owner !== owner || resolve2(record.path) !== expected.path || record.branch !== expected.branch || relativePath === ".." || relativePath.startsWith(`..${sep}`) || relativeCwd === ".." || relativeCwd.startsWith(`..${sep}`)) throw new Error(`Invalid worktree record for ${owner}`);
1437
+ const relativePath = relative2(this.directory, record.path);
1438
+ const relativeCwd = relative2(record.path, record.cwd);
1439
+ if (record.owner !== owner || resolve2(record.path) !== expected.path || record.branch !== expected.branch || relativePath === ".." || relativePath.startsWith(`..${sep2}`) || relativeCwd === ".." || relativeCwd.startsWith(`..${sep2}`)) throw new Error(`Invalid worktree record for ${owner}`);
1499
1440
  return record;
1500
1441
  }
1501
1442
  async #loadWorktreeRecords(missingOk = true) {
@@ -1733,8 +1674,8 @@ var RunStore = class _RunStore {
1733
1674
  try {
1734
1675
  const root = (await git(this.cwd, ["rev-parse", "--show-toplevel"])).trim();
1735
1676
  const [canonicalRoot, canonicalCwd] = await Promise.all([realpath(root), realpath(this.cwd)]);
1736
- const launchRelative = relative(canonicalRoot, canonicalCwd);
1737
- if (launchRelative === ".." || launchRelative.startsWith(`..${sep}`)) throw new Error("launch cwd is outside the repository");
1677
+ const launchRelative = relative2(canonicalRoot, canonicalCwd);
1678
+ if (launchRelative === ".." || launchRelative.startsWith(`..${sep2}`)) throw new Error("launch cwd is outside the repository");
1738
1679
  await this.cleanupMarker(markerPath);
1739
1680
  const dirty = (await git(root, ["status", "--porcelain", "--untracked-files=normal"])).trim();
1740
1681
  if (dirty) {
@@ -1920,520 +1861,12 @@ import { Type } from "@earendil-works/pi-ai";
1920
1861
  // src/validation.ts
1921
1862
  import * as acorn from "acorn";
1922
1863
 
1923
- // src/roles.ts
1924
- import { getAgentDir as getAgentDir2, parseFrontmatter } from "@earendil-works/pi-coding-agent";
1925
-
1926
- // src/registry.ts
1927
- import { readFileSync as readFileSync2 } from "node:fs";
1928
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join5 } from "node:path";
1929
- import { fileURLToPath as fileURLToPath2 } from "node:url";
1930
- import { Value } from "typebox/value";
1931
-
1932
1864
  // src/settings.ts
1933
- import { mkdirSync, readFileSync } from "node:fs";
1934
- import { homedir as homedir4 } from "node:os";
1935
- import { dirname as dirname2, join as join4, resolve as resolve3 } from "node:path";
1936
- import { fileURLToPath } from "node:url";
1937
1865
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
1938
- var ROLE_DIRECTORY = "pi-extensible-workflows";
1939
1866
  var DEFAULT_SETTINGS = Object.freeze({ concurrency: 8, backgroundWidget: true });
1940
- function workflowSettingsPath(agentDir = getAgentDir()) {
1941
- return join4(agentDir, ROLE_DIRECTORY, "settings.json");
1942
- }
1943
- function normalizedResourcePath(value, settingsPath) {
1944
- if (value === "*" || value.startsWith("builtin:")) return value;
1945
- let expanded = value === "~" ? homedir4() : value.startsWith("~/") || value.startsWith("~\\") ? join4(homedir4(), value.slice(2)) : value;
1946
- if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
1947
- const resolved = resolve3(dirname2(settingsPath), expanded);
1948
- if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\")) return expanded;
1949
- if (resourcePatternHasMagic(expanded)) {
1950
- const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
1951
- const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
1952
- const rootBoundary = separatorIndex === 0 || separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved);
1953
- const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
1954
- const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
1955
- return `${canonicalPath(prefix)}${suffix}`;
1956
- }
1957
- return canonicalPath(resolved);
1958
- }
1959
- function validateSelectorList(value, path, kind, errorCode2 = "INVALID_SETTINGS", normalizeExtensions = true) {
1960
- if (value === void 0) return void 0;
1961
- if (!Array.isArray(value)) fail(errorCode2, `${path}.${kind} must be an array`);
1962
- const normalized = [];
1963
- for (const [index, entry] of value.entries()) {
1964
- if (typeof entry !== "string" || !entry.trim()) fail(errorCode2, `${path}.${kind}[${String(index)}] must be a non-empty string`);
1965
- let selector = entry.trim();
1966
- if (kind === "extensions" && normalizeExtensions) {
1967
- const negated = selector.startsWith("!");
1968
- const body = negated ? selector.slice(1) : selector;
1969
- if (!body) fail(errorCode2, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
1970
- try {
1971
- selector = `${negated ? "!" : ""}${normalizedResourcePath(body, path)}`;
1972
- } catch (error) {
1973
- fail(errorCode2, `${path}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`);
1974
- }
1975
- }
1976
- try {
1977
- validateResourcePattern(selector);
1978
- } catch (error) {
1979
- fail(errorCode2, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`);
1980
- }
1981
- normalized.push(selector);
1982
- }
1983
- return Object.freeze(normalized);
1984
- }
1985
- function validateWorktreePostCreateCommand(value, settingsPath) {
1986
- if (value === void 0) return void 0;
1987
- if (!Array.isArray(value) || value.length === 0) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand must be a non-empty array`);
1988
- return Object.freeze(value.map((entry, index) => {
1989
- if (typeof entry !== "string" || !entry.trim()) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand[${String(index)}] must be a non-empty string`);
1990
- return entry;
1991
- }));
1992
- }
1993
- function selectorsFromSettings(settings) {
1994
- return {
1995
- ...settings.skills === void 0 ? {} : { skills: settings.skills },
1996
- ...settings.extensions === void 0 ? {} : { extensions: settings.extensions },
1997
- ...settings.tools === void 0 ? {} : { tools: settings.tools }
1998
- };
1999
- }
2000
- function selectorSet(value) {
2001
- return { skills: [...value?.skills ?? []], extensions: [...value?.extensions ?? []], ...value?.tools === void 0 ? {} : { tools: [...value.tools] } };
2002
- }
2003
- function validateWorkflowExtensionSettings(value, settingsPath, errorCode2 = "INVALID_SETTINGS") {
2004
- if (value === void 0) return void 0;
2005
- const base = `${settingsPath}.extensionSettings`;
2006
- if (!object(value)) fail(errorCode2, `${base} must be an object`);
2007
- const normalized = {};
2008
- for (const [namespace, raw] of Object.entries(value)) {
2009
- if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode2, `${base} contains an invalid namespace: ${namespace}`);
2010
- if (namespace === "herdr") {
2011
- if (!object(raw)) fail(errorCode2, `${base}.herdr must be an object`);
2012
- if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode2, `${base}.herdr contains an unsupported setting`);
2013
- if (raw.enableFullyInspectableMode !== void 0 && typeof raw.enableFullyInspectableMode !== "boolean") fail(errorCode2, `${base}.herdr.enableFullyInspectableMode must be a boolean`);
2014
- normalized.herdr = Object.freeze({ ...raw.enableFullyInspectableMode === void 0 ? {} : { enableFullyInspectableMode: raw.enableFullyInspectableMode } });
2015
- continue;
2016
- }
2017
- if (namespace === "trajectory") {
2018
- if (!object(raw)) fail(errorCode2, `${base}.trajectory must be an object`);
2019
- if (Object.keys(raw).some((key) => key !== "port")) fail(errorCode2, `${base}.trajectory contains an unsupported setting`);
2020
- if (raw.port !== void 0 && (!positiveInteger(raw.port) || raw.port > 65535)) fail(errorCode2, `${base}.trajectory.port must be an integer from 1 to 65535`);
2021
- normalized.trajectory = Object.freeze({ ...raw.port === void 0 ? {} : { port: raw.port } });
2022
- continue;
2023
- }
2024
- if (!jsonValue(raw)) fail(errorCode2, `${base}.${namespace} must be JSON-compatible`);
2025
- normalized[namespace] = structuredClone(raw);
2026
- }
2027
- return deepFreeze(normalized);
2028
- }
2029
- function positiveRetentionInteger(value) {
2030
- return positiveInteger(value) && Number.isSafeInteger(value);
2031
- }
2032
- function validateRetention(value, settingsPath) {
2033
- if (value === void 0) return void 0;
2034
- const base = `${settingsPath}.retention`;
2035
- if (!object(value)) fail("INVALID_SETTINGS", `${base} must be an object`);
2036
- const unknown = Object.keys(value).find((key) => key !== "olderThanDays" && key !== "maxTerminalRuns");
2037
- if (unknown) fail("INVALID_SETTINGS", `Unknown retention setting at ${base}: ${unknown}`);
2038
- if (value.olderThanDays !== void 0 && !positiveRetentionInteger(value.olderThanDays)) fail("INVALID_SETTINGS", `${base}.olderThanDays must be a positive integer`);
2039
- if (value.maxTerminalRuns !== void 0 && !positiveRetentionInteger(value.maxTerminalRuns)) fail("INVALID_SETTINGS", `${base}.maxTerminalRuns must be a positive integer`);
2040
- return Object.freeze({ ...value.olderThanDays === void 0 ? {} : { olderThanDays: value.olderThanDays }, ...value.maxTerminalRuns === void 0 ? {} : { maxTerminalRuns: value.maxTerminalRuns } });
2041
- }
2042
- function parseSettings(path, partial) {
2043
- let parsed;
2044
- try {
2045
- parsed = JSON.parse(readFileSync(path, "utf8"));
2046
- } catch (error) {
2047
- if (isNodeError(error, "ENOENT")) return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
2048
- fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
2049
- }
2050
- if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
2051
- const allowed = /* @__PURE__ */ new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...partial ? [] : ["backgroundWidget", "codemodeTools"]]);
2052
- const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
2053
- if (Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources")) fail("INVALID_SETTINGS", `disabledAgentResources is no longer supported; use skills, extensions, and tools selectors (settings: ${path})`);
2054
- if (unknown) fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
2055
- const concurrency = parsed.concurrency === void 0 ? partial ? void 0 : DEFAULT_SETTINGS.concurrency : parsed.concurrency;
2056
- if (concurrency !== void 0 && (!positiveInteger(concurrency) || concurrency > 16)) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
2057
- const backgroundWidget = parsed.backgroundWidget === void 0 ? partial ? void 0 : DEFAULT_SETTINGS.backgroundWidget : parsed.backgroundWidget;
2058
- if (backgroundWidget !== void 0 && typeof backgroundWidget !== "boolean") fail("INVALID_SETTINGS", `${path}.backgroundWidget must be a boolean`);
2059
- const codemodeTools = parsed.codemodeTools;
2060
- if (codemodeTools !== void 0 && codemodeTools !== "all" && codemodeTools !== "read-only" && codemodeTools !== "none") fail("INVALID_SETTINGS", `${path}.codemodeTools must be "all", "read-only", or "none"`);
2061
- const modelAliases = parsed.modelAliases === void 0 ? void 0 : validateModelAliases(parsed.modelAliases, path);
2062
- const skills = validateSelectorList(parsed.skills, path, "skills");
2063
- const tools = validateSelectorList(parsed.tools, path, "tools");
2064
- const extensions = validateSelectorList(parsed.extensions, path, "extensions");
2065
- const extensionSettings = parsed.extensionSettings === void 0 ? void 0 : validateWorkflowExtensionSettings(parsed.extensionSettings, path, "INVALID_SETTINGS");
2066
- const retention = validateRetention(parsed.retention, path);
2067
- const worktreePostCreateCommand = validateWorktreePostCreateCommand(parsed.worktreePostCreateCommand, path);
2068
- return Object.freeze({
2069
- ...concurrency === void 0 ? {} : { concurrency },
2070
- ...backgroundWidget === void 0 ? {} : { backgroundWidget },
2071
- ...codemodeTools === void 0 ? {} : { codemodeTools },
2072
- ...modelAliases === void 0 ? {} : { modelAliases },
2073
- ...skills === void 0 ? {} : { skills },
2074
- ...extensions === void 0 ? {} : { extensions },
2075
- ...extensionSettings === void 0 ? {} : { extensionSettings },
2076
- ...tools === void 0 ? {} : { tools },
2077
- ...retention === void 0 ? {} : { retention },
2078
- ...worktreePostCreateCommand === void 0 ? {} : { worktreePostCreateCommand }
2079
- });
2080
- }
2081
- function loadSettings(path = workflowSettingsPath()) {
2082
- return parseSettings(path, false);
2083
- }
2084
- function loadSettingsOverrides(path) {
2085
- return parseSettings(path, true);
2086
- }
2087
- function resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
2088
- const projectSettingsPath = workflowProjectSettingsPath(cwd);
2089
- const global = loadSettings(globalSettingsPath);
2090
- const project = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
2091
- const projectHas = (key) => Object.prototype.hasOwnProperty.call(project, key);
2092
- const sourceFor = (key) => projectHas(key) ? projectSettingsPath : globalSettingsPath;
2093
- const globalSelectors = selectorsFromSettings(global);
2094
- const projectSelectors = selectorsFromSettings(project);
2095
- const effectiveSelectors = selectorSet({
2096
- skills: [...globalSelectors.skills ?? [], ...projectSelectors.skills ?? []],
2097
- extensions: [...globalSelectors.extensions ?? [], ...projectSelectors.extensions ?? []],
2098
- ...globalSelectors.tools === void 0 && projectSelectors.tools === void 0 ? {} : { tools: [...globalSelectors.tools ?? [], ...projectSelectors.tools ?? []] }
2099
- });
2100
- const hasExtensionSelectors = global.extensions !== void 0 || project.extensions !== void 0;
2101
- const extensionSettings = projectHas("extensionSettings") ? project.extensionSettings : global.extensionSettings;
2102
- const sources = {
2103
- concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
2104
- modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
2105
- skills: sourceFor("skills"),
2106
- extensions: sourceFor("extensions"),
2107
- tools: sourceFor("tools"),
2108
- ...extensionSettings === void 0 ? {} : { extensionSettings: projectHas("extensionSettings") ? projectSettingsPath : globalSettingsPath },
2109
- ...project.retention === void 0 && global.retention === void 0 ? {} : { retention: project.retention === void 0 ? globalSettingsPath : projectSettingsPath },
2110
- ...projectHas("worktreePostCreateCommand") || global.worktreePostCreateCommand !== void 0 ? { worktreePostCreateCommand: projectHas("worktreePostCreateCommand") ? projectSettingsPath : globalSettingsPath } : {}
2111
- };
2112
- const effective = Object.freeze({
2113
- concurrency: project.concurrency ?? global.concurrency,
2114
- backgroundWidget: global.backgroundWidget ?? true,
2115
- ...projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === void 0 ? {} : { modelAliases: global.modelAliases },
2116
- ...effectiveSelectors.skills.length ? { skills: effectiveSelectors.skills } : global.skills !== void 0 || project.skills !== void 0 ? { skills: effectiveSelectors.skills } : {},
2117
- ...hasExtensionSelectors ? { extensions: effectiveSelectors.extensions } : {},
2118
- ...extensionSettings === void 0 ? {} : { extensionSettings },
2119
- ...effectiveSelectors.tools?.length ? { tools: effectiveSelectors.tools } : global.tools !== void 0 || project.tools !== void 0 ? { tools: effectiveSelectors.tools } : {},
2120
- ...(project.retention ?? global.retention) === void 0 ? {} : { retention: project.retention ?? global.retention },
2121
- ...projectHas("worktreePostCreateCommand") ? { worktreePostCreateCommand: project.worktreePostCreateCommand } : global.worktreePostCreateCommand === void 0 ? {} : { worktreePostCreateCommand: global.worktreePostCreateCommand }
2122
- });
2123
- return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
2124
- }
2125
-
2126
- // src/package-identity.ts
2127
- var CORE_PACKAGE_NAME = "@marcoscale98/pi-extensible-workflows";
2128
- var CORE_PACKAGE_COMPATIBILITY_NAME = "pi-extensible-workflows";
2129
- var CORE_PACKAGE_NAMES = [CORE_PACKAGE_NAME, CORE_PACKAGE_COMPATIBILITY_NAME];
2130
- function isCorePackageName(value) {
2131
- return typeof value === "string" && CORE_PACKAGE_NAMES.includes(value);
2132
- }
2133
-
2134
- // src/registry.ts
2135
- var RESERVED_GLOBALS = /* @__PURE__ */ 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"]);
2136
- var IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
2137
- var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
2138
- var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
2139
- function normalizeRoleDirectory(value) {
2140
- try {
2141
- if (value instanceof URL) {
2142
- if (value.protocol !== "file:") fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
2143
- return fileURLToPath2(value);
2144
- }
2145
- if (typeof value === "string") {
2146
- if (value.startsWith("file:")) return fileURLToPath2(value);
2147
- if (isAbsolute(value)) return value;
2148
- }
2149
- } catch (error) {
2150
- fail("INVALID_METADATA", `Invalid workflow role directory: ${errorText(error)}`);
2151
- }
2152
- fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
2153
- }
2154
- function isBuiltinRoleDirectory(path) {
2155
- const canonical = canonicalPath(path);
2156
- const starterDirectory = dirname3(canonical);
2157
- if (basename2(canonical) !== "roles" || basename2(starterDirectory) !== "starter") return false;
2158
- const distributionDirectory = dirname3(starterDirectory);
2159
- const packageRoot = basename2(distributionDirectory) === "dist" ? dirname3(distributionDirectory) : distributionDirectory;
2160
- try {
2161
- const metadata = JSON.parse(readFileSync2(join5(packageRoot, "package.json"), "utf8"));
2162
- return object(metadata) && isCorePackageName(metadata.name);
2163
- } catch {
2164
- return false;
2165
- }
2166
- }
2167
- function catalogSchema(schema, at) {
2168
- validateSchema(schema, at);
2169
- return structuredClone(schema);
2170
- }
2171
- var WorkflowRegistry = class {
2172
- #extensions = /* @__PURE__ */ new Set();
2173
- #functionSources = /* @__PURE__ */ new Map();
2174
- #globals = /* @__PURE__ */ new Map();
2175
- #hooks = /* @__PURE__ */ new Map();
2176
- #agentAttemptActions = /* @__PURE__ */ new Map();
2177
- #roleDirectories = /* @__PURE__ */ new Map();
2178
- #modelAliases = /* @__PURE__ */ new Map();
2179
- #settingsValidators = /* @__PURE__ */ new Set();
2180
- #frozen = false;
2181
- #subagentStatusObserver;
2182
- get frozen() {
2183
- return this.#frozen;
2184
- }
2185
- freeze() {
2186
- this.#frozen = true;
2187
- }
2188
- setSubagentStatusObserver(observer) {
2189
- this.#subagentStatusObserver = observer;
2190
- }
2191
- observeSubagentStatus(status, request) {
2192
- this.#subagentStatusObserver?.(status, request);
2193
- }
2194
- register(extension2) {
2195
- if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
2196
- if (object(extension2) && Object.prototype.hasOwnProperty.call(extension2, "workflows")) fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
2197
- if (!object(extension2) || Object.keys(extension2).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "agentSetupHooks", "agentAttemptActions", "roleDirectories"].includes(key)) || typeof extension2.version !== "string" || !SEMVER.test(extension2.version) || typeof extension2.headline !== "string" || !extension2.headline.trim()) fail("INVALID_METADATA", "Workflow extensions require a semantic version and non-empty headline");
2198
- const functions = extension2.functions ?? {};
2199
- const modelAliases = extension2.modelAliases ?? {};
2200
- const agentSetupHooks = extension2.agentSetupHooks ?? {};
2201
- const agentAttemptActions = extension2.agentAttemptActions ?? {};
2202
- const source = extension2.source;
2203
- const dependencyValues = extension2.dependencies;
2204
- const dependencies = dependencyValues ?? [];
2205
- const validateSettings = extension2.validateSettings;
2206
- if (validateSettings !== void 0 && typeof validateSettings !== "function") fail("INVALID_METADATA", "Workflow extension validateSettings must be a function");
2207
- if (source !== void 0 && (typeof source !== "string" || !source.trim())) fail("INVALID_METADATA", "Workflow extension source must be a non-empty module URL");
2208
- if (dependencyValues !== void 0 && (!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");
2209
- const roleDirectoryValues = extension2.roleDirectories === void 0 ? [] : extension2.roleDirectories;
2210
- if (!Array.isArray(roleDirectoryValues)) fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
2211
- const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
2212
- 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 === void 0) fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent setup hooks, agent attempt actions, role directories, or a settings validator");
2213
- const names = Object.keys(functions);
2214
- if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
2215
- for (const name of names) {
2216
- if (!IDENTIFIER.test(name) || name.startsWith("__pi_extensible_workflows_")) fail("INVALID_METADATA", `Invalid global name: ${name}`);
2217
- if (RESERVED_GLOBALS.has(name)) fail("GLOBAL_COLLISION", `Global name is reserved: ${name}`);
2218
- if (this.#globals.has(name)) fail("GLOBAL_COLLISION", `Global name is already registered: ${name}`);
2219
- }
2220
- for (const [name, fn] of Object.entries(functions)) {
2221
- if (!object(fn) || Object.keys(fn).some((key) => !["description", "input", "output", "run"].includes(key)) || typeof fn.description !== "string" || !fn.description.trim() || typeof fn.run !== "function") fail("INVALID_METADATA", `Invalid workflow function: ${name}`);
2222
- validateSchema(fn.input, `${name} input`);
2223
- validateSchema(fn.output, `${name} output`);
2224
- if (fn.input.type !== "object") fail("INVALID_SCHEMA", `${name} input must describe one object`);
2225
- }
2226
- for (const [name, alias] of Object.entries(modelAliases)) {
2227
- if (!MODEL_ALIAS_NAME.test(name)) fail("INVALID_METADATA", `Invalid model alias name: ${name}`);
2228
- if (!object(alias) || Object.keys(alias).some((key) => key !== "resolve") || typeof alias.resolve !== "function") fail("INVALID_METADATA", `Invalid model alias resolver: ${name}`);
2229
- if (this.#modelAliases.has(name)) fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
2230
- }
2231
- for (const [name, hook] of Object.entries(agentSetupHooks)) {
2232
- if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== void 0 && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority))) fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
2233
- if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
2234
- }
2235
- for (const [name, action] of Object.entries(agentAttemptActions)) {
2236
- if (!IDENTIFIER.test(name) || !object(action) || Object.keys(action).some((key) => !["label", "visible", "run", "visibleStandalone", "runStandalone"].includes(key)) || typeof action.label !== "string" || !action.label.trim() || typeof action.visible !== "function" || typeof action.run !== "function" || action.visibleStandalone !== void 0 && typeof action.visibleStandalone !== "function" || action.runStandalone !== void 0 && typeof action.runStandalone !== "function") fail("INVALID_METADATA", `Invalid agent attempt action: ${name}`);
2237
- if (action.visibleStandalone !== void 0 !== (action.runStandalone !== void 0)) fail("INVALID_METADATA", `Standalone agent attempt actions require visibleStandalone and runStandalone: ${name}`);
2238
- if (this.#agentAttemptActions.has(name)) fail("DUPLICATE_NAME", `Agent attempt action already registered: ${name}`);
2239
- }
2240
- const stored = deepFreeze({ ...extension2, functions, modelAliases, agentSetupHooks, agentAttemptActions, ...roleDirectories.length ? { roleDirectories } : {} });
2241
- if (source !== void 0) for (const name of names) this.#functionSources.set(name, Object.freeze({ module: source, export: "default", dependencies: Object.freeze([...dependencies]) }));
2242
- this.#extensions.add(stored);
2243
- for (const directory of roleDirectories) if (!this.#roleDirectories.has(directory)) this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension2.version, headline: extension2.headline }, ...isBuiltinRoleDirectory(directory) ? { builtin: true } : {} }));
2244
- for (const name of names) this.#globals.set(name, name);
2245
- for (const [name, alias] of Object.entries(modelAliases)) this.#modelAliases.set(name, { name, version: extension2.version, headline: extension2.headline, resolve: alias.resolve });
2246
- for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
2247
- for (const [name, action] of Object.entries(agentAttemptActions)) this.#agentAttemptActions.set(name, action);
2248
- if (validateSettings !== void 0) this.#settingsValidators.add({ headline: extension2.headline, validate: validateSettings });
2249
- }
2250
- function(name) {
2251
- if (!IDENTIFIER.test(name)) fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
2252
- const fn = [...this.#extensions].find((extension2) => extension2.functions?.[name])?.functions?.[name];
2253
- if (!fn) fail("MISSING_WORKFLOW", `Registered function is unavailable: ${name}; the separate registered-workflow format was removed`);
2254
- return fn;
2255
- }
2256
- functions() {
2257
- return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension2) => Object.entries(extension2.functions ?? {}))));
2258
- }
2259
- functionSources() {
2260
- return Object.freeze(Object.fromEntries(this.#functionSources));
2261
- }
2262
- catalog(context) {
2263
- const functions = [];
2264
- for (const extension2 of this.#extensions) {
2265
- for (const [name, fn] of Object.entries(extension2.functions ?? {})) functions.push({ name, version: extension2.version, headline: extension2.headline, description: fn.description, input: catalogSchema(fn.input, `${name} input`), output: catalogSchema(fn.output, `${name} output`) });
2266
- }
2267
- let aliases;
2268
- let settings;
2269
- let source = "global settings";
2270
- try {
2271
- const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : void 0;
2272
- aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
2273
- if (resolved) {
2274
- 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";
2275
- 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 === void 0 ? {} : { extensionSettings: resolved.effective.extensionSettings }, ...resolved.effective.worktreePostCreateCommand === void 0 ? {} : { worktreePostCreateCommand: resolved.effective.worktreePostCreateCommand }, globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted: resolved.projectTrusted, sources: resolved.sources };
2276
- }
2277
- } catch {
2278
- aliases = void 0;
2279
- }
2280
- const staticEntries = Object.keys(aliases ?? {}).map((name) => ({ name, kind: "static", provenance: source }));
2281
- const dynamicEntries = [...this.#modelAliases.values()].map(({ name, version, headline }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline }));
2282
- const modelAliasEntries = [...staticEntries, ...dynamicEntries].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind) || left.provenance.localeCompare(right.provenance));
2283
- const sort = (left, right) => left.name.localeCompare(right.name);
2284
- const catalog = { functions: functions.sort(sort), ...modelAliasEntries.length ? { modelAliasEntries } : {} };
2285
- if (aliases && Object.keys(aliases).length) Object.defineProperty(catalog, "modelAliases", { value: Object.freeze(structuredClone(aliases)), enumerable: false });
2286
- if (settings) {
2287
- const publicSettings = { ...settings, modelAliases: Object.keys(aliases ?? {}).length ? {} : settings.modelAliases };
2288
- Object.defineProperty(catalog, "settings", { value: deepFreeze(publicSettings), enumerable: true });
2289
- }
2290
- return deepFreeze(catalog);
2291
- }
2292
- catalogIndex(context) {
2293
- const catalog = this.catalog(context);
2294
- let modelAliasEntries;
2295
- if (catalog.modelAliasEntries !== void 0) {
2296
- const winningEntries = /* @__PURE__ */ new Map();
2297
- for (const entry of catalog.modelAliasEntries) {
2298
- if (!winningEntries.has(entry.name) || entry.kind === "static") winningEntries.set(entry.name, entry);
2299
- }
2300
- modelAliasEntries = [...winningEntries.values()];
2301
- }
2302
- const index = {
2303
- functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
2304
- ...modelAliasEntries ? { modelAliasEntries: structuredClone(modelAliasEntries) } : {}
2305
- };
2306
- if (catalog.modelAliases) Object.defineProperty(index, "modelAliases", { value: Object.freeze(structuredClone(catalog.modelAliases)), enumerable: false });
2307
- if (catalog.settings) Object.defineProperty(index, "settings", { value: deepFreeze(structuredClone(catalog.settings)), enumerable: true });
2308
- return deepFreeze(index);
2309
- }
2310
- catalogDetail(name, context) {
2311
- const catalog = this.catalog(context);
2312
- const entry = catalog.functions.find((candidate) => candidate.name === name);
2313
- if (entry) return entry;
2314
- const alias = catalog.modelAliasEntries?.find((candidate) => candidate.name === name && candidate.kind === "static") ?? catalog.modelAliasEntries?.find((candidate) => candidate.name === name);
2315
- if (alias) return alias;
2316
- return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function is available: ${name}` } });
2317
- }
2318
- globals() {
2319
- return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension2) => Object.keys(extension2.functions ?? {}).map((name) => [name, { name }]))));
2320
- }
2321
- validateExtensionSettings(settings, context) {
2322
- const effectiveSettings = settings ?? Object.freeze({});
2323
- if (!jsonValue(effectiveSettings) || !object(effectiveSettings)) fail("INVALID_SETTINGS", "Workflow extension settings must be a JSON object");
2324
- const unsafeNamespace = Object.keys(effectiveSettings).find((namespace) => !validWorkflowExtensionNamespace(namespace));
2325
- if (unsafeNamespace !== void 0) fail("INVALID_SETTINGS", `Workflow extension settings contains an invalid namespace: ${unsafeNamespace}`);
2326
- const frozenSettings = deepFreeze(structuredClone(effectiveSettings));
2327
- for (const { headline, validate } of this.#settingsValidators) {
2328
- try {
2329
- validate(frozenSettings, Object.freeze({ ...context }));
2330
- } catch (error) {
2331
- const location = context.source === "role" && context.role ? `role ${context.role}` : context.source;
2332
- fail("INVALID_SETTINGS", `Invalid extension settings (${headline}; ${location}): ${errorText(error)}`);
2333
- }
2334
- }
2335
- }
2336
- async invokeFunction(name, input, context, path, journal) {
2337
- const fn = this.function(name);
2338
- if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input)) fail("RESULT_INVALID", `Invalid input for ${name}`);
2339
- const replayed = journal.get(path);
2340
- if (replayed !== void 0) {
2341
- if (!jsonValue(replayed) || !Value.Check(fn.output, replayed)) fail("RESULT_INVALID", `Invalid replay for ${name}`);
2342
- return structuredClone(replayed);
2343
- }
2344
- const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, settings: deepFreeze(structuredClone(context.settings)), invoke: context.invoke, agent: context.agent, shell: context.shell, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
2345
- if (!jsonValue(result) || !Value.Check(fn.output, result)) fail("RESULT_INVALID", `Invalid output from ${name}`);
2346
- const stored = structuredClone(result);
2347
- journal.put(path, stored);
2348
- return structuredClone(stored);
2349
- }
2350
- agentSetupHooks() {
2351
- return [...this.#hooks.values()].sort(byPriorityThenName);
2352
- }
2353
- agentAttemptActions() {
2354
- return Object.freeze(Object.fromEntries(this.#agentAttemptActions.entries()));
2355
- }
2356
- roleDirectories() {
2357
- return [...this.#roleDirectories.keys()];
2358
- }
2359
- roleDirectoryRegistrations() {
2360
- return [...this.#roleDirectories.values()];
2361
- }
2362
- modelAliases() {
2363
- return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name));
2364
- }
2365
- async resolveModelAliases(context, shadowed = /* @__PURE__ */ new Set()) {
2366
- const resolved = {};
2367
- const isAborted = () => context.signal.aborted;
2368
- for (const alias of this.modelAliases()) {
2369
- if (shadowed.has(alias.name)) continue;
2370
- if (isAborted()) fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
2371
- let target;
2372
- try {
2373
- target = await alias.resolve(Object.freeze({ cwd: context.cwd, projectTrusted: context.projectTrusted, rootModel: Object.freeze({ ...context.rootModel }), knownModels: new Set(context.knownModels), availableModels: new Set(context.availableModels), signal: context.signal }));
2374
- } catch (error) {
2375
- if (isAborted() || error instanceof Error && error.name === "AbortError" || errorCode(error) === "CANCELLED") fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
2376
- fail("CONFIG_ERROR", `Model alias resolver failed for ${alias.name} (${alias.headline}): ${errorText(error)}`);
2377
- }
2378
- if (isAborted()) fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
2379
- if (typeof target !== "string" || !target.trim()) fail("CONFIG_ERROR", `Model alias resolver returned an invalid target for ${alias.name} (${alias.headline})`);
2380
- resolved[alias.name] = target.trim();
2381
- }
2382
- return Object.freeze(resolved);
2383
- }
2384
- };
2385
- var WORKFLOW_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("pi-extensible-workflows.workflow-registry");
2386
- var globalRegistry = globalThis;
2387
- function createWorkflowRegistryApi(registry) {
2388
- return {
2389
- get frozen() {
2390
- return registry.frozen;
2391
- },
2392
- freeze: () => {
2393
- registry.freeze();
2394
- },
2395
- setSubagentStatusObserver: (observer) => {
2396
- registry.setSubagentStatusObserver(observer);
2397
- },
2398
- observeSubagentStatus: (status, request) => {
2399
- registry.observeSubagentStatus(status, request);
2400
- },
2401
- register: (extension2) => {
2402
- registry.register(extension2);
2403
- },
2404
- function: (name) => registry.function(name),
2405
- functions: () => registry.functions(),
2406
- functionSources: () => registry.functionSources(),
2407
- catalog: (context) => registry.catalog(context),
2408
- catalogIndex: (context) => registry.catalogIndex(context),
2409
- catalogDetail: (name, context) => registry.catalogDetail(name, context),
2410
- globals: () => registry.globals(),
2411
- invokeFunction: (...args) => registry.invokeFunction(...args),
2412
- validateExtensionSettings: (...args) => {
2413
- registry.validateExtensionSettings(...args);
2414
- },
2415
- modelAliases: () => registry.modelAliases(),
2416
- resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
2417
- roleDirectories: () => registry.roleDirectories(),
2418
- roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
2419
- agentSetupHooks: () => registry.agentSetupHooks(),
2420
- agentAttemptActions: () => registry.agentAttemptActions()
2421
- };
2422
- }
2423
- function workflowRegistryHost() {
2424
- return globalRegistry[WORKFLOW_REGISTRY_KEY] ??= { api: createWorkflowRegistryApi(new WorkflowRegistry()), activeHosts: 0 };
2425
- }
2426
- function resetWorkflowRegistry() {
2427
- workflowRegistryHost().api = createWorkflowRegistryApi(new WorkflowRegistry());
2428
- }
2429
- function beginWorkflowExtensionLoading() {
2430
- const host = workflowRegistryHost();
2431
- if (host.api.frozen && host.activeHosts === 0) resetWorkflowRegistry();
2432
- }
2433
- beginWorkflowExtensionLoading();
2434
1867
 
2435
1868
  // subagents/src/contracts.ts
2436
- import { Value as Value2 } from "typebox/value";
1869
+ import { Value } from "typebox/value";
2437
1870
  var SUBAGENT_MAX_RETRIES = 255;
2438
1871
  var SUBAGENT_SYSTEM_PROMPT_LIMIT = 64 * 1024;
2439
1872
  var SUBAGENTS_MODE = Type.Union([
@@ -2446,15 +1879,15 @@ var SUBAGENTS_RUN_PARAMETERS = Type.Object({
2446
1879
  label: Type.Optional(Type.String({ description: "Optional display label for the subagent" })),
2447
1880
  model: Type.Optional(Type.String({ description: "Optional model as provider/model:thinking or alias[:thinking]" })),
2448
1881
  tools: Type.Optional(Type.Array(Type.String(), { description: "Optional ordered tool selectors; candidates start enabled and !* restricts the set" })),
1882
+ excludeTools: Type.Optional(Type.Array(Type.String(), { description: "Optional exact tool names removed after preparation; no patterns, cannot remove workflow_result" })),
2449
1883
  skills: Type.Optional(Type.Array(Type.String(), { description: "Optional ordered skill selectors; candidates start enabled and !* restricts the set" })),
2450
1884
  extensions: Type.Optional(Type.Array(Type.String(), { description: "Optional ordered extension selectors; candidates start enabled and !* restricts the set" })),
2451
1885
  contextFiles: Type.Optional(Type.Array(Type.String(), { description: "Optional context-file scopes: global, project, cwd" })),
2452
- role: Type.Optional(Type.String({ description: "Workflow role name" })),
2453
1886
  worktree: Type.Optional(Type.String({ description: "Optional named worktree; requires a clean working tree" })),
2454
1887
  outputSchema: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional JSON schema for the result" })),
2455
1888
  retries: Type.Optional(Type.Integer({ minimum: 0, maximum: SUBAGENT_MAX_RETRIES, description: "Optional retry count; at most 255 retries" })),
2456
1889
  timeoutMs: Type.Optional(Type.Union([Type.Integer({ minimum: 1, description: "Optional execution timeout in milliseconds" }), Type.Null()]))
2457
- }, { additionalProperties: false });
1890
+ }, { additionalProperties: true });
2458
1891
  var SUBAGENTS_INSPECT_PARAMETERS = Type.Object({
2459
1892
  id: Type.Optional(Type.String({ description: "Subagent ID; omit to list ordered run summaries" })),
2460
1893
  scope: Type.Optional(Type.Union([Type.Literal("session"), Type.Literal("all")], { description: "List scope when id is omitted: session (default) lists runs from the current Pi session; all lists every stored run" }))
@@ -2644,20 +2077,20 @@ function canonicalExtensionSelector(selector, base) {
2644
2077
  const negated = selector.startsWith("!");
2645
2078
  const body = negated ? selector.slice(1) : selector;
2646
2079
  if (body === "*" || body === "**" || body.startsWith("**/") || body.startsWith("builtin:")) return selector;
2647
- const resolved = resolve4(base, body);
2080
+ const resolved = resolve3(base, body);
2648
2081
  if (resourcePatternHasMagic(body)) return `${negated ? "!" : ""}${resolved}`;
2649
2082
  return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
2650
2083
  }
2651
2084
  function skillNameFromPath(path) {
2652
- const file = basename3(path);
2653
- return file.toLowerCase() === "skill.md" ? basename3(dirname4(path)) : file;
2085
+ const file = basename2(path);
2086
+ return file.toLowerCase() === "skill.md" ? basename2(dirname2(path)) : file;
2654
2087
  }
2655
2088
  var discoveredResourcesByCwd = /* @__PURE__ */ new Map();
2656
2089
  async function discoveredResources(cwd) {
2657
2090
  const cached = discoveredResourcesByCwd.get(cwd);
2658
2091
  if (cached) return cached;
2659
2092
  const pending = (async () => {
2660
- const agentDir = getAgentDir3();
2093
+ const agentDir = getAgentDir2();
2661
2094
  const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
2662
2095
  const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
2663
2096
  const resolved = await packageManager.resolve();
@@ -2679,8 +2112,8 @@ async function resolveResourceInspection(inspection, cwd, selectorCwd = cwd) {
2679
2112
  return inspection;
2680
2113
  }
2681
2114
  const sources = inspection.selectorSources ?? { global: inspection.selectors, project: {} };
2682
- const skills = inspection.skills.length ? inspection.skills : selectResourcesByLayers([sources.global.skills, sources.project.skills, sources.role?.skills, sources.call?.skills], discovered.skills);
2683
- 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);
2115
+ const skills = inspection.skills.length ? inspection.skills : selectResourcesByLayers([sources.global.skills, sources.project.skills, sources.call?.skills], discovered.skills);
2116
+ 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);
2684
2117
  return skills === inspection.skills && extensions === inspection.extensions ? inspection : { ...inspection, skills, extensions };
2685
2118
  }
2686
2119
  async function withResolvedResources(run, cwd) {
@@ -2762,7 +2195,7 @@ async function loadTrajectoryRun(store, includeTranscripts = true) {
2762
2195
  const run = applyTrajectoryAgentOutputs(prepared, operations);
2763
2196
  return { run, snapshot: value.snapshot, awaiting: await store.awaitingCheckpoints(), ...summary?.createdAt === void 0 ? {} : { createdAt: summary.createdAt }, transcripts: includeTranscripts ? await runTranscripts(run) : {} };
2764
2197
  }
2765
- async function loadTrajectoryRuns(cwd, sessionId, home = homedir5(), overlay) {
2198
+ async function loadTrajectoryRuns(cwd, sessionId, home = homedir4(), overlay) {
2766
2199
  const loaded = [];
2767
2200
  for (const runId of await listRunIds(cwd, sessionId, home, false)) {
2768
2201
  try {
@@ -2777,15 +2210,15 @@ async function loadTrajectoryRuns(cwd, sessionId, home = homedir5(), overlay) {
2777
2210
  // trajectory/src/export.ts
2778
2211
  import { execFile as execFile3 } from "node:child_process";
2779
2212
  import { mkdtemp, readFile as readFile5, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
2780
- import { homedir as homedir6, tmpdir } from "node:os";
2781
- import { join as join7 } from "node:path";
2213
+ import { homedir as homedir5, tmpdir } from "node:os";
2214
+ import { join as join5 } from "node:path";
2782
2215
  var DEFAULT_SHARE_VIEWER_URL = "https://vekexasia.github.io/pi-extensible-workflows/run.html";
2783
2216
  function inlineAsset(html, tag, replacement) {
2784
2217
  if (!html.includes(tag)) throw new Error(`Trajectory UI is missing the expected ${tag} reference`);
2785
2218
  return html.replace(tag, () => replacement);
2786
2219
  }
2787
2220
  async function exportTrajectoryRunHtml(options) {
2788
- const home = options.home ?? homedir6();
2221
+ const home = options.home ?? homedir5();
2789
2222
  const runs = await loadTrajectoryRuns(options.cwd, options.sessionId, home);
2790
2223
  const run = runs.find((candidate) => candidate.run.id === options.runId);
2791
2224
  if (!run) throw new Error(`Workflow run ${options.runId} was not found for session ${options.sessionId}`);
@@ -2813,22 +2246,22 @@ async function exportTrajectoryRunHtml(options) {
2813
2246
  return output;
2814
2247
  }
2815
2248
  function runGh(ghPath, args) {
2816
- return new Promise((resolve5, reject) => {
2249
+ return new Promise((resolve4, reject) => {
2817
2250
  execFile3(ghPath, [...args], { encoding: "utf8", maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
2818
2251
  if (error) {
2819
2252
  const notFound = error.code === "ENOENT";
2820
2253
  reject(new Error(notFound ? "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/ and run `gh auth login`." : stderr.trim() || error.message));
2821
2254
  return;
2822
2255
  }
2823
- resolve5(stdout);
2256
+ resolve4(stdout);
2824
2257
  });
2825
2258
  });
2826
2259
  }
2827
2260
  async function shareTrajectoryRun(options) {
2828
2261
  const html = await exportTrajectoryRunHtml(options);
2829
- const directory = await mkdtemp(join7(tmpdir(), "piewf-share-"));
2262
+ const directory = await mkdtemp(join5(tmpdir(), "piewf-share-"));
2830
2263
  try {
2831
- const file = join7(directory, "trajectory.html");
2264
+ const file = join5(directory, "trajectory.html");
2832
2265
  await writeFile3(file, html);
2833
2266
  const stdout = await runGh(options.ghPath ?? "gh", ["gist", "create", "--public=false", file]);
2834
2267
  const gistUrl = stdout.trim().split("\n").pop() ?? "";
@@ -2841,21 +2274,158 @@ async function shareTrajectoryRun(options) {
2841
2274
  }
2842
2275
  }
2843
2276
 
2277
+ // trajectory/src/startup-mutex.ts
2278
+ import { randomUUID as randomUUID3 } from "node:crypto";
2279
+ import { readFileSync } from "node:fs";
2280
+ import { mkdir as mkdir3, readdir as readdir4, readFile as readFile6, rm as rm5 } from "node:fs/promises";
2281
+ import { join as join6 } from "node:path";
2282
+ var bootId;
2283
+ function processStart(pid) {
2284
+ if (process.platform !== "linux") return void 0;
2285
+ try {
2286
+ bootId ??= readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
2287
+ const stat4 = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
2288
+ const startTicks = stat4.slice(stat4.lastIndexOf(")") + 2).split(" ")[19];
2289
+ return startTicks === void 0 || !/^\d+$/.test(startTicks) ? void 0 : `${bootId}:${startTicks}`;
2290
+ } catch {
2291
+ return void 0;
2292
+ }
2293
+ }
2294
+ function processLive(identity) {
2295
+ try {
2296
+ process.kill(identity.pid, 0);
2297
+ } catch (error) {
2298
+ if (isNodeError(error, "ESRCH")) return false;
2299
+ }
2300
+ if (identity.start === void 0) return true;
2301
+ const current = processStart(identity.pid);
2302
+ return current === void 0 || current === identity.start;
2303
+ }
2304
+ function processConfirmed(identity) {
2305
+ return identity.start !== void 0 && processStart(identity.pid) === identity.start;
2306
+ }
2307
+ function holderDirectory(lockPath) {
2308
+ return `${lockPath}.startup`;
2309
+ }
2310
+ function holderPath(lockPath, token) {
2311
+ return join6(holderDirectory(lockPath), `${token}.json`);
2312
+ }
2313
+ function identityOf(pid) {
2314
+ const start = processStart(pid);
2315
+ return start === void 0 ? { pid } : { pid, start };
2316
+ }
2317
+ function processIdentity(value) {
2318
+ if (!object(value) || !positiveInteger(value.pid)) return void 0;
2319
+ return typeof value.start === "string" ? { pid: value.pid, start: value.start } : { pid: value.pid };
2320
+ }
2321
+ function decodeHolder(text) {
2322
+ try {
2323
+ const parsed = JSON.parse(text);
2324
+ const holder = processIdentity(parsed);
2325
+ if (holder === void 0 || !object(parsed) || typeof parsed.token !== "string") return void 0;
2326
+ const server = processIdentity(parsed.server);
2327
+ return { ...holder, token: parsed.token, ...server === void 0 ? {} : { server } };
2328
+ } catch {
2329
+ return void 0;
2330
+ }
2331
+ }
2332
+ function readStartupHolder(lockPath, token) {
2333
+ try {
2334
+ return decodeHolder(readFileSync(holderPath(lockPath, token), "utf8"));
2335
+ } catch (error) {
2336
+ if (isNodeError(error, "ENOENT")) return void 0;
2337
+ throw error;
2338
+ }
2339
+ }
2340
+ async function serverPending(lockPath, server) {
2341
+ if (!processLive(server)) return false;
2342
+ let text;
2343
+ try {
2344
+ text = await readFile6(lockPath, "utf8");
2345
+ } catch (error) {
2346
+ if (isNodeError(error, "ENOENT")) return true;
2347
+ throw error;
2348
+ }
2349
+ try {
2350
+ const lock = JSON.parse(text);
2351
+ return !(object(lock) && lock.pid === server.pid);
2352
+ } catch {
2353
+ return true;
2354
+ }
2355
+ }
2356
+ async function holderLive(lockPath, path, read) {
2357
+ if (processLive(read)) return true;
2358
+ let holder;
2359
+ try {
2360
+ holder = decodeHolder(await readFile6(path, "utf8"));
2361
+ } catch (error) {
2362
+ if (isNodeError(error, "ENOENT")) return false;
2363
+ throw error;
2364
+ }
2365
+ if (holder?.server === void 0 || !await serverPending(lockPath, holder.server)) return false;
2366
+ if (!processConfirmed(holder.server)) return true;
2367
+ try {
2368
+ process.kill(holder.server.pid, "SIGKILL");
2369
+ } catch (error) {
2370
+ if (!isNodeError(error, "ESRCH")) throw error;
2371
+ }
2372
+ return true;
2373
+ }
2374
+ async function tryAcquireStartupMutex(lockPath) {
2375
+ const directory = holderDirectory(lockPath);
2376
+ await mkdir3(directory, { recursive: true, mode: 448 });
2377
+ const token = randomUUID3();
2378
+ const own = `${token}.json`;
2379
+ await atomicWriteFile(join6(directory, own), `${JSON.stringify({ ...identityOf(process.pid), token })}
2380
+ `);
2381
+ let contended = false;
2382
+ try {
2383
+ for (const name of await readdir4(directory)) {
2384
+ if (name === own || !name.endsWith(".json")) continue;
2385
+ const path = join6(directory, name);
2386
+ let holder;
2387
+ try {
2388
+ holder = decodeHolder(await readFile6(path, "utf8"));
2389
+ } catch (error) {
2390
+ if (isNodeError(error, "ENOENT")) continue;
2391
+ throw error;
2392
+ }
2393
+ if (holder !== void 0 && await holderLive(lockPath, path, holder)) contended = true;
2394
+ else await rm5(path, { force: true });
2395
+ }
2396
+ } catch (error) {
2397
+ await rm5(join6(directory, own), { force: true });
2398
+ throw error;
2399
+ }
2400
+ if (!contended) return token;
2401
+ await rm5(join6(directory, own), { force: true });
2402
+ return void 0;
2403
+ }
2404
+ function recordStartupServer(lockPath, token, pid) {
2405
+ const holder = readStartupHolder(lockPath, token);
2406
+ if (holder === void 0) throw new Error("Trajectory startup mutex was lost");
2407
+ atomicWriteFile(holderPath(lockPath, token), `${JSON.stringify({ ...holder, server: identityOf(pid) })}
2408
+ `, true);
2409
+ }
2410
+ async function releaseStartupMutex(lockPath, token) {
2411
+ await rm5(holderPath(lockPath, token), { force: true });
2412
+ }
2413
+
2844
2414
  // trajectory/src/index.ts
2845
2415
  var DEFAULT_TRAJECTORY_PORT = 7432;
2846
2416
  var TRAJECTORY_IDLE_EXIT_MS = 5 * 60 * 1e3;
2847
2417
  var TRAJECTORY_LOCK_NAME = "trajectory.lock";
2848
2418
  function trajectoryLockPath(agentDir) {
2849
- return join8(agentDir, "pi-extensible-workflows", TRAJECTORY_LOCK_NAME);
2419
+ return join7(agentDir, "pi-extensible-workflows", TRAJECTORY_LOCK_NAME);
2850
2420
  }
2851
- function trajectoryServerPath(moduleDirectory = dirname5(fileURLToPath3(import.meta.url))) {
2852
- const candidates = [join8(moduleDirectory, "server.js"), join8(moduleDirectory, "../../dist/trajectory/src/server.js")];
2421
+ function trajectoryServerPath(moduleDirectory = dirname3(fileURLToPath(import.meta.url))) {
2422
+ const candidates = [join7(moduleDirectory, "server.js"), join7(moduleDirectory, "../../dist/trajectory/src/server.js")];
2853
2423
  const path = candidates.find((candidate) => existsSync(candidate));
2854
2424
  if (!path) throw new Error("Trajectory server implementation is unavailable");
2855
2425
  return path;
2856
2426
  }
2857
2427
  async function trajectoryFingerprint(serverPath) {
2858
- const [serverBytes, htmlBytes] = await Promise.all([readFile6(serverPath), readFile6(join8(dirname5(serverPath), "assets/index.html"))]);
2428
+ const [serverBytes, htmlBytes] = await Promise.all([readFile7(serverPath), readFile7(join7(dirname3(serverPath), "assets/index.html"))]);
2859
2429
  return `${createHash3("sha256").update(serverBytes).digest("hex")}:${createHash3("sha256").update(htmlBytes).digest("hex")}`;
2860
2430
  }
2861
2431
  function publisherId(cwd, sessionId) {
@@ -2866,7 +2436,7 @@ function trajectoryPort(value) {
2866
2436
  return positiveInteger(value) && value <= 65535 ? value : DEFAULT_TRAJECTORY_PORT;
2867
2437
  }
2868
2438
  function delay(ms) {
2869
- return new Promise((resolve5) => setTimeout(resolve5, ms));
2439
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
2870
2440
  }
2871
2441
  async function serverHealth(port) {
2872
2442
  try {
@@ -2889,107 +2459,133 @@ function signalProcess(pid, signal) {
2889
2459
  if (!isNodeError(error, "ESRCH")) throw error;
2890
2460
  }
2891
2461
  }
2892
- async function stopStaleServer(lock) {
2893
- if (lock.pid === process.pid) return;
2894
- if (!await processAlive(lock.pid, lock.startedAt)) return;
2895
- signalProcess(lock.pid, "SIGTERM");
2462
+ async function stopStaleServer(pid, port) {
2463
+ const server = identityOf(pid);
2464
+ if (!processLive(server)) return;
2465
+ signalProcess(pid, "SIGTERM");
2896
2466
  const deadline = Date.now() + 3e3;
2897
2467
  while (Date.now() < deadline) {
2898
- if (!await serverHealthy(lock.port)) break;
2468
+ if (!await serverHealthy(port)) break;
2899
2469
  const remaining = deadline - Date.now();
2900
2470
  if (remaining <= 0) break;
2901
2471
  await delay(Math.min(50, remaining));
2902
2472
  }
2903
- if (await processAlive(lock.pid, lock.startedAt)) signalProcess(lock.pid, "SIGKILL");
2473
+ if (processConfirmed(server)) signalProcess(pid, "SIGKILL");
2474
+ for (let attempt = 0; attempt < 50 && processLive(server); attempt += 1) await delay(20);
2475
+ if (processLive(server)) throw new Error(`Stale Trajectory server ${String(pid)} on port ${String(port)} did not stop; stop it and retry`);
2904
2476
  }
2905
2477
  async function readLock(path) {
2478
+ let text;
2906
2479
  try {
2907
- const parsed = JSON.parse(await readFile6(path, "utf8"));
2908
- if (!object(parsed) || !positiveInteger(parsed.pid) || !positiveInteger(parsed.port) || parsed.port > 65535) return void 0;
2909
- const fingerprint = typeof parsed.fingerprint === "string" ? parsed.fingerprint : void 0;
2910
- const startedAt = positiveInteger(parsed.startedAt) ? parsed.startedAt : void 0;
2911
- return { pid: parsed.pid, port: parsed.port, ...fingerprint === void 0 ? {} : { fingerprint }, ...startedAt === void 0 ? {} : { startedAt } };
2480
+ text = await readFile7(path, "utf8");
2912
2481
  } catch (error) {
2913
2482
  if (isNodeError(error, "ENOENT")) return void 0;
2483
+ throw error;
2484
+ }
2485
+ let parsed;
2486
+ try {
2487
+ parsed = JSON.parse(text);
2488
+ } catch {
2914
2489
  return void 0;
2915
2490
  }
2491
+ if (!object(parsed) || !positiveInteger(parsed.pid) || !positiveInteger(parsed.port) || parsed.port > 65535) return void 0;
2492
+ const fingerprint = typeof parsed.fingerprint === "string" ? parsed.fingerprint : void 0;
2493
+ const startedAt = positiveInteger(parsed.startedAt) ? parsed.startedAt : void 0;
2494
+ const start = typeof parsed.start === "string" ? parsed.start : void 0;
2495
+ return { pid: parsed.pid, port: parsed.port, ...fingerprint === void 0 ? {} : { fingerprint }, ...startedAt === void 0 ? {} : { startedAt }, ...start === void 0 ? {} : { start } };
2496
+ }
2497
+ var STARTUP_MUTEX_WAIT_MS = 2 * 60 * (300 + 50) + 3e3 + 1e3 + 1e3;
2498
+ async function acquireStartupMutex(lockPath) {
2499
+ const deadline = Date.now() + STARTUP_MUTEX_WAIT_MS;
2500
+ for (; ; ) {
2501
+ const token = await tryAcquireStartupMutex(lockPath);
2502
+ if (token !== void 0) return token;
2503
+ if (Date.now() >= deadline) throw new Error("Another Pi is still starting Trajectory; retry");
2504
+ await delay(25 + Math.floor(Math.random() * 50));
2505
+ }
2916
2506
  }
2917
- async function waitForServer(port, fingerprint) {
2918
- let foreign = false;
2919
- for (let attempt = 0; attempt < 60; attempt += 1) {
2507
+ function isTrajectoryServer(health) {
2508
+ return health?.pid !== void 0 && health.fingerprint !== void 0;
2509
+ }
2510
+ function ownsLock(health, lock) {
2511
+ return isTrajectoryServer(health) && health.pid === lock.pid && (lock.startedAt === void 0 || lock.startedAt === health.startedAt);
2512
+ }
2513
+ async function awaitHealth(port, accept, stopped = () => false) {
2514
+ for (let attempt = 0; attempt < 60 && !stopped(); attempt += 1) {
2920
2515
  const health = await serverHealth(port);
2921
- if (health?.fingerprint === fingerprint) return;
2922
- foreign = health !== void 0;
2516
+ if (health !== void 0 && accept(health)) return health;
2923
2517
  await delay(50);
2924
2518
  }
2925
- throw new Error(foreign ? `Trajectory port ${String(port)} is held by another Trajectory server; stop it and retry` : `Trajectory server did not start on port ${String(port)}`);
2519
+ return void 0;
2926
2520
  }
2927
- async function resolveExistingServer(lockPath, existing, fingerprint) {
2928
- const health = await serverHealth(existing.port);
2929
- if (health) {
2930
- if (existing.fingerprint === fingerprint && health.fingerprint === fingerprint) return existing;
2931
- await stopStaleServer(existing);
2932
- await rm5(lockPath, { force: true });
2933
- return void 0;
2521
+ async function startServer(lockPath, serverPath, fingerprint, port, startupToken) {
2522
+ const child = spawn(process.versions.bun ? "node" : process.execPath, [serverPath, "--port", String(port), "--lock", lockPath, "--fingerprint", fingerprint, "--startup", startupToken], { detached: true, stdio: "ignore" });
2523
+ const startup = { ended: false };
2524
+ const exited = new Promise((resolve4) => {
2525
+ child.once("error", (error) => {
2526
+ startup.error = error;
2527
+ startup.ended = true;
2528
+ resolve4();
2529
+ });
2530
+ child.once("exit", () => {
2531
+ startup.ended = true;
2532
+ resolve4();
2533
+ });
2534
+ });
2535
+ try {
2536
+ if (child.pid !== void 0) recordStartupServer(lockPath, startupToken, child.pid);
2537
+ const health = await awaitHealth(port, (candidate) => candidate.pid === child.pid && candidate.fingerprint === fingerprint, () => startup.ended);
2538
+ if (startup.error !== void 0) throw startup.error;
2539
+ const lock = health === void 0 ? void 0 : await readLock(lockPath);
2540
+ if (lock !== void 0 && ownsLock(health, lock) && lock.port === port && lock.fingerprint === fingerprint) {
2541
+ child.unref();
2542
+ return { port };
2543
+ }
2544
+ throw new Error(`Trajectory server did not start on port ${String(port)}`);
2545
+ } catch (error) {
2546
+ if (!startup.ended) {
2547
+ child.kill("SIGKILL");
2548
+ await exited;
2549
+ }
2550
+ if ((await readLock(lockPath))?.pid === child.pid) await rm6(lockPath, { force: true });
2551
+ throw error;
2934
2552
  }
2935
- if (await processAlive(existing.pid, existing.startedAt)) {
2936
- try {
2937
- await waitForServer(existing.port, fingerprint);
2938
- return existing;
2939
- } catch {
2940
- await stopStaleServer(existing);
2941
- await rm5(lockPath, { force: true });
2942
- return void 0;
2553
+ }
2554
+ async function startOrReuseServer(lockPath, serverPath, fingerprint, configuredPort, startupToken) {
2555
+ const existing = await readLock(lockPath);
2556
+ if (existing) {
2557
+ let health = await serverHealth(existing.port);
2558
+ if (health === void 0 && processLive(existing)) health = await awaitHealth(existing.port, (candidate) => candidate.pid === existing.pid);
2559
+ const lock = health === void 0 ? existing : await readLock(lockPath) ?? existing;
2560
+ if (ownsLock(health, lock)) {
2561
+ if (lock.fingerprint === fingerprint && health.fingerprint === fingerprint) return { port: lock.port };
2562
+ await stopStaleServer(health.pid, lock.port);
2563
+ } else if (!isTrajectoryServer(health) && lock.pid !== process.pid && processLive(lock)) {
2564
+ throw new Error(health === void 0 ? `Trajectory server ${String(lock.pid)} named by ${lockPath} is alive but not answering on port ${String(lock.port)}; retry once it answers, or remove the lock if that process is not a Trajectory server` : `Trajectory port ${String(lock.port)} is held by another Trajectory server; stop it and retry`);
2943
2565
  }
2566
+ await rm6(lockPath, { force: true });
2944
2567
  }
2945
- await rm5(lockPath, { force: true });
2946
- return void 0;
2568
+ const occupant = await serverHealth(configuredPort);
2569
+ if (occupant !== void 0) {
2570
+ if (!isTrajectoryServer(occupant)) throw new Error(`Trajectory port ${String(configuredPort)} is held by another Trajectory server; stop it and retry`);
2571
+ if (occupant.fingerprint === fingerprint && occupant.startedAt !== void 0) {
2572
+ await atomicJson(lockPath, { pid: occupant.pid, port: configuredPort, fingerprint, startedAt: occupant.startedAt, start: processStart(occupant.pid) });
2573
+ return { port: configuredPort };
2574
+ }
2575
+ await stopStaleServer(occupant.pid, configuredPort);
2576
+ }
2577
+ return startServer(lockPath, serverPath, fingerprint, configuredPort, startupToken);
2947
2578
  }
2948
2579
  async function ensureTrajectoryServer(agentDir, configuredPort) {
2949
2580
  const lockPath = trajectoryLockPath(agentDir);
2950
2581
  const serverPath = trajectoryServerPath();
2951
2582
  const fingerprint = await trajectoryFingerprint(serverPath);
2952
- await mkdir3(dirname5(lockPath), { recursive: true, mode: 448 });
2953
- const existing = await readLock(lockPath);
2954
- if (existing) {
2955
- const reused = await resolveExistingServer(lockPath, existing, fingerprint);
2956
- if (reused) return reused;
2957
- }
2958
- let lockHandle;
2583
+ await mkdir4(dirname3(lockPath), { recursive: true, mode: 448 });
2584
+ const startupToken = await acquireStartupMutex(lockPath);
2959
2585
  try {
2960
- lockHandle = await open3(lockPath, "wx", 384);
2961
- await lockHandle.writeFile(`${JSON.stringify({ pid: process.pid, port: configuredPort, fingerprint, startedAt: Date.now() })}
2962
- `, "utf8");
2963
- } catch (error) {
2964
- if (isNodeError(error, "EEXIST")) {
2965
- const raced = await readLock(lockPath);
2966
- if (raced) {
2967
- const reused = await resolveExistingServer(lockPath, raced, fingerprint);
2968
- if (reused) return reused;
2969
- }
2970
- await rm5(lockPath, { force: true });
2971
- lockHandle = await open3(lockPath, "wx", 384);
2972
- await lockHandle.writeFile(`${JSON.stringify({ pid: process.pid, port: configuredPort, fingerprint, startedAt: Date.now() })}
2973
- `, "utf8");
2974
- } else {
2975
- throw error;
2976
- }
2586
+ return await startOrReuseServer(lockPath, serverPath, fingerprint, configuredPort, startupToken);
2977
2587
  } finally {
2978
- await lockHandle?.close();
2979
- }
2980
- const occupant = await serverHealth(configuredPort);
2981
- if (occupant?.pid !== void 0 && occupant.fingerprint !== fingerprint) await stopStaleServer({ pid: occupant.pid, port: configuredPort, ...occupant.startedAt === void 0 ? {} : { startedAt: occupant.startedAt } });
2982
- try {
2983
- const child = spawn(process.versions.bun ? "node" : process.execPath, [serverPath, "--port", String(configuredPort), "--lock", lockPath, "--fingerprint", fingerprint], { detached: true, stdio: "ignore" });
2984
- const startupError = new Promise((_resolve, reject) => {
2985
- child.once("error", reject);
2986
- });
2987
- child.unref();
2988
- await Promise.race([waitForServer(configuredPort, fingerprint), startupError]);
2989
- return { port: configuredPort };
2990
- } catch (error) {
2991
- await rm5(lockPath, { force: true });
2992
- throw error;
2588
+ await releaseStartupMutex(lockPath, startupToken);
2993
2589
  }
2994
2590
  }
2995
2591
  function trajectoryWebSocket() {
@@ -3460,13 +3056,13 @@ function createTrajectoryController(agentDir) {
3460
3056
  } catch {
3461
3057
  }
3462
3058
  });
3463
- await new Promise((resolve5, reject) => {
3059
+ await new Promise((resolve4, reject) => {
3464
3060
  let settled = false;
3465
3061
  const finish = (error) => {
3466
3062
  if (settled) return;
3467
3063
  settled = true;
3468
3064
  if (error) reject(error);
3469
- else resolve5();
3065
+ else resolve4();
3470
3066
  };
3471
3067
  next.addEventListener("open", () => {
3472
3068
  finish();
@@ -3486,11 +3082,11 @@ function createTrajectoryController(agentDir) {
3486
3082
  next.send(JSON.stringify({ type: "publisher:attach", publisherId: publisherId(input.cwd, input.sessionId) }));
3487
3083
  };
3488
3084
  let reconnectCancel;
3489
- const waitReconnect = (ms) => new Promise((resolve5) => {
3085
+ const waitReconnect = (ms) => new Promise((resolve4) => {
3490
3086
  const timer = setTimeout(() => {
3491
3087
  if (reconnectTimer === timer) reconnectTimer = void 0;
3492
3088
  reconnectCancel = void 0;
3493
- resolve5();
3089
+ resolve4();
3494
3090
  }, ms);
3495
3091
  timer.unref();
3496
3092
  reconnectTimer = timer;
@@ -3498,7 +3094,7 @@ function createTrajectoryController(agentDir) {
3498
3094
  clearTimeout(timer);
3499
3095
  if (reconnectTimer === timer) reconnectTimer = void 0;
3500
3096
  reconnectCancel = void 0;
3501
- resolve5();
3097
+ resolve4();
3502
3098
  };
3503
3099
  });
3504
3100
  const scheduleReconnect = () => {
@@ -3565,7 +3161,7 @@ function createTrajectoryController(agentDir) {
3565
3161
  };
3566
3162
  }
3567
3163
  function registerTrajectoryExtension(pi, options = {}) {
3568
- const controller = options.controller ?? createTrajectoryController(options.agentDir ?? getAgentDir4());
3164
+ const controller = options.controller ?? createTrajectoryController(options.agentDir ?? getAgentDir3());
3569
3165
  const openUrl = options.openUrl ?? openTrajectoryUrl;
3570
3166
  let autoAttached = false;
3571
3167
  const notify = (context, message, level) => {
@@ -3581,7 +3177,7 @@ function registerTrajectoryExtension(pi, options = {}) {
3581
3177
  return void 0;
3582
3178
  }
3583
3179
  };
3584
- const open4 = async (provider, context) => {
3180
+ const open3 = async (provider, context) => {
3585
3181
  const server = await attach(provider, context);
3586
3182
  if (server === void 0) return;
3587
3183
  const url = trajectoryUrl(server.port);
@@ -3589,7 +3185,7 @@ function registerTrajectoryExtension(pi, options = {}) {
3589
3185
  notify(context, `Trajectory opened at ${url}`, "info");
3590
3186
  };
3591
3187
  const host = {
3592
- open: open4,
3188
+ open: open3,
3593
3189
  autoAttach(provider, context) {
3594
3190
  if (autoAttached || !context.hasUI) return;
3595
3191
  autoAttached = true;