@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.1-fork.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +442 -162
  5. package/dist/src/background-widget.d.ts +5 -1
  6. package/dist/src/background-widget.js +5 -10
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +59 -37
  9. package/dist/src/execution.d.ts +6 -2
  10. package/dist/src/execution.js +48 -7
  11. package/dist/src/host-delivery.js +5 -8
  12. package/dist/src/host-navigator.js +5 -2
  13. package/dist/src/host-phases.d.ts +1 -0
  14. package/dist/src/host-recovery.d.ts +11 -8
  15. package/dist/src/host-recovery.js +14 -22
  16. package/dist/src/host-runtime.js +56 -5
  17. package/dist/src/host-view.d.ts +6 -1
  18. package/dist/src/host-view.js +46 -20
  19. package/dist/src/host.d.ts +2 -2
  20. package/dist/src/host.js +246 -118
  21. package/dist/src/index.d.ts +1 -2
  22. package/dist/src/index.js +1985 -1690
  23. package/dist/src/index.js.map +3 -3
  24. package/dist/src/paths.d.ts +8 -5
  25. package/dist/src/paths.js +24 -13
  26. package/dist/src/persistence.d.ts +1 -0
  27. package/dist/src/persistence.js +1 -0
  28. package/dist/src/pi-runtime-runner.js +4 -1
  29. package/dist/src/registry.d.ts +9 -7
  30. package/dist/src/registry.js +29 -71
  31. package/dist/src/runtime/agent-runner.d.ts +2 -0
  32. package/dist/src/script-tools.d.ts +27 -0
  33. package/dist/src/script-tools.js +87 -0
  34. package/dist/src/session-lease.d.ts +1 -1
  35. package/dist/src/session-lease.js +45 -11
  36. package/dist/src/settings.d.ts +19 -3
  37. package/dist/src/settings.js +63 -50
  38. package/dist/src/store.d.ts +3 -1
  39. package/dist/src/store.js +17 -1
  40. package/dist/src/trajectory.d.ts +12 -3
  41. package/dist/src/trajectory.js +32 -11
  42. package/dist/src/types.d.ts +129 -45
  43. package/dist/src/types.js +5 -8
  44. package/dist/src/utils.d.ts +21 -1
  45. package/dist/src/utils.js +46 -6
  46. package/dist/src/validation.d.ts +14 -3
  47. package/dist/src/validation.js +143 -87
  48. package/dist/starter/index.js +8 -11
  49. package/dist/starter/index.js.map +1 -1
  50. package/dist/starter/review-loop.d.ts +0 -13
  51. package/dist/starter/review-loop.js +8 -9
  52. package/dist/subagents/index.js +904 -956
  53. package/dist/subagents/index.js.map +3 -3
  54. package/dist/subagents/src/contracts.d.ts +48 -31
  55. package/dist/subagents/src/contracts.js +20 -9
  56. package/dist/subagents/src/index.d.ts +1 -1
  57. package/dist/subagents/src/index.js +14 -8
  58. package/dist/subagents/src/manager.js +52 -73
  59. package/dist/subagents/src/navigator.js +3 -14
  60. package/dist/subagents/src/view.d.ts +1 -1
  61. package/dist/subagents/src/view.js +15 -68
  62. package/dist/subagents/src/worktree.js +1 -1
  63. package/dist/trajectory/assets/index.html +35 -13
  64. package/dist/trajectory/index.js +576 -932
  65. package/dist/trajectory/index.js.map +3 -3
  66. package/dist/trajectory/src/assets/index.html +35 -13
  67. package/dist/trajectory/src/index.js +134 -104
  68. package/dist/trajectory/src/server.js +268 -27
  69. package/dist/trajectory/src/server.js.map +3 -3
  70. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  71. package/dist/trajectory/src/startup-mutex.js +172 -0
  72. package/examples/workflow-extension-template/README.md +4 -7
  73. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  74. package/examples/workflow-extension-template/index.js +0 -3
  75. package/package.json +10 -10
  76. package/skills/pi-extensible-workflows/SKILL.md +12 -8
  77. package/src/agent-execution.ts +412 -164
  78. package/src/background-widget.ts +5 -11
  79. package/src/decoders.ts +51 -37
  80. package/src/execution.ts +46 -9
  81. package/src/host-delivery.ts +5 -8
  82. package/src/host-navigator.ts +4 -2
  83. package/src/host-phases.ts +1 -1
  84. package/src/host-recovery.ts +23 -28
  85. package/src/host-runtime.ts +44 -6
  86. package/src/host-view.ts +41 -16
  87. package/src/host.ts +229 -142
  88. package/src/index.ts +1 -2
  89. package/src/paths.ts +18 -9
  90. package/src/persistence.ts +1 -0
  91. package/src/pi-runtime-runner.ts +3 -1
  92. package/src/registry.ts +28 -64
  93. package/src/runtime/agent-runner.ts +2 -0
  94. package/src/script-tools.ts +76 -0
  95. package/src/session-lease.ts +34 -6
  96. package/src/settings.ts +56 -50
  97. package/src/store.ts +18 -2
  98. package/src/trajectory.ts +32 -15
  99. package/src/types.ts +82 -34
  100. package/src/utils.ts +42 -5
  101. package/src/validation.ts +121 -80
  102. package/starter/index.ts +0 -1
  103. package/starter/review-loop.ts +8 -9
  104. package/starter/roles/developer.md +0 -1
  105. package/starter/roles/oracle.md +0 -1
  106. package/starter/roles/researcher.md +0 -1
  107. package/starter/roles/reviewer.md +0 -1
  108. package/starter/roles/scout.md +0 -1
  109. package/subagents/README.md +10 -16
  110. package/subagents/src/contracts.ts +21 -11
  111. package/subagents/src/index.ts +17 -8
  112. package/subagents/src/manager.ts +48 -128
  113. package/subagents/src/navigator.ts +4 -13
  114. package/subagents/src/view.ts +16 -63
  115. package/subagents/src/worktree.ts +1 -1
  116. package/trajectory/src/assets/index.html +35 -13
  117. package/trajectory/src/index.ts +112 -91
  118. package/trajectory/src/server.ts +46 -10
  119. package/trajectory/src/startup-mutex.ts +127 -0
  120. package/dist/src/roles.d.ts +0 -70
  121. package/dist/src/roles.js +0 -325
  122. package/dist/starter/roles/developer.md +0 -6
  123. package/dist/starter/roles/oracle.md +0 -18
  124. package/dist/starter/roles/researcher.md +0 -20
  125. package/dist/starter/roles/reviewer.md +0 -19
  126. package/dist/starter/roles/scout.md +0 -18
  127. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  128. package/src/roles.ts +0 -326
@@ -1,11 +1,14 @@
1
1
  export declare function safePart(value: string): string;
2
- /**
3
- * Resolves a path to its filesystem spelling, including symlink and platform aliases.
4
- * A missing path safely falls back to its absolute lexical spelling rather than
5
- * guessing at the identity of an object that does not exist.
6
- */
7
2
  export declare function canonicalPath(path: string): string;
3
+ export declare function extensionIdentity(path: string): string;
8
4
  export declare function sameFilesystemPath(left: string, right: string): boolean;
5
+ /**
6
+ * The path Pi's resource loader uses for a resource path an extension returns: trimmed, `~`, `~/` and `file://` expanded,
7
+ * relative to the loader cwd. Synthetic `<...>` and `builtin:` paths stay as they are.
8
+ */
9
+ export declare function piResourcePath(path: string, cwd: string): string;
10
+ /** Pi attributes a loaded resource to a contributed path by lexical containment of resolved paths, without following symlinks. */
11
+ export declare function piResourceContains(root: string, path: string): boolean;
9
12
  export declare function workflowProjectSettingsPath(cwd: string): string;
10
13
  export declare function projectStorageKey(cwd: string): string;
11
14
  export declare function projectSessionsDirectory(cwd: string, home?: string): string;
package/dist/src/paths.js CHANGED
@@ -1,26 +1,37 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { realpathSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { readdir } from "node:fs/promises";
4
5
  import { homedir } from "node:os";
5
- import { basename, join, resolve } from "node:path";
6
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
6
7
  import { WorkflowError } from "./types.js";
7
8
  import { isNodeError } from "./utils.js";
8
9
  export function safePart(value) { return value.replace(/[^a-zA-Z0-9._-]/g, "_"); }
10
+ export function canonicalPath(path) { const absolute = resolve(path); try {
11
+ return realpathSync(absolute);
12
+ }
13
+ catch {
14
+ return absolute;
15
+ } }
16
+ export function extensionIdentity(path) { return path.startsWith("builtin:") ? path : canonicalPath(path.startsWith("file:") ? fileURLToPath(path) : path); }
17
+ export function sameFilesystemPath(left, right) { return canonicalPath(left) === canonicalPath(right); }
9
18
  /**
10
- * Resolves a path to its filesystem spelling, including symlink and platform aliases.
11
- * A missing path safely falls back to its absolute lexical spelling rather than
12
- * guessing at the identity of an object that does not exist.
19
+ * The path Pi's resource loader uses for a resource path an extension returns: trimmed, `~`, `~/` and `file://` expanded,
20
+ * relative to the loader cwd. Synthetic `<...>` and `builtin:` paths stay as they are.
13
21
  */
14
- export function canonicalPath(path) {
15
- const absolute = resolve(path);
16
- try {
17
- return realpathSync(absolute);
18
- }
19
- catch {
20
- return absolute;
21
- }
22
+ export function piResourcePath(path, cwd) {
23
+ //NOTE: Pi also rewrites Windows shell paths and `~\` on win32; those forms are not mirrored here and fail closed.
24
+ if (path.startsWith("<") || path.startsWith("builtin:"))
25
+ return path;
26
+ const trimmed = path.trim();
27
+ const expanded = trimmed === "~" ? homedir() : trimmed.startsWith("~/") ? join(homedir(), trimmed.slice(2)) : /^file:\/\//.test(trimmed) ? fileURLToPath(trimmed) : trimmed;
28
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
29
+ }
30
+ /** Pi attributes a loaded resource to a contributed path by lexical containment of resolved paths, without following symlinks. */
31
+ export function piResourceContains(root, path) {
32
+ const relativePath = relative(resolve(root), resolve(path));
33
+ return relativePath === "" || (relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
22
34
  }
23
- export function sameFilesystemPath(left, right) { return canonicalPath(left) === canonicalPath(right); }
24
35
  export function workflowProjectSettingsPath(cwd) { return join(cwd, ".pi", "pi-extensible-workflows", "settings.json"); }
25
36
  export function projectStorageKey(cwd) {
26
37
  const exact = resolve(cwd);
@@ -2,4 +2,5 @@ export { atomicJson, atomicWriteFile, json } from "./io.js";
2
2
  export { projectStorageKey, projectSessionsDirectory, runsDirectory, listPersistedSessionIds, structuralPath, } from "./paths.js";
3
3
  export { hasLiveSessionLease, SessionLease, acquireSessionLease, listRunIds, processAlive } from "./session-lease.js";
4
4
  export { RunStore } from "./store.js";
5
+ export { decodePreparedAgentConfiguration } from "./decoders.js";
5
6
  export { EffectiveSystemPrompt, PersistedRun, RunSummaryAgent, RunSummaryArtifacts, RunSummary, CompletedOperation, AwaitingCheckpoint, PendingWorkflowDecision, PersistedOwnershipNode, WorktreeReference, BorrowedWorktreeBinding, isPersistedRun, } from "./decoders.js";
@@ -2,4 +2,5 @@ export { atomicJson, atomicWriteFile, json } from "./io.js";
2
2
  export { projectStorageKey, projectSessionsDirectory, runsDirectory, listPersistedSessionIds, structuralPath, } from "./paths.js";
3
3
  export { hasLiveSessionLease, SessionLease, acquireSessionLease, listRunIds, processAlive } from "./session-lease.js";
4
4
  export { RunStore } from "./store.js";
5
+ export { decodePreparedAgentConfiguration } from "./decoders.js";
5
6
  export { isPersistedRun, } from "./decoders.js";
@@ -164,7 +164,7 @@ export class PiRuntimeAgentRunner {
164
164
  const resultTool = prepared.resultTool;
165
165
  if (!resultTool)
166
166
  throw new WorkflowError("INTERNAL_ERROR", "Workflow agent session is missing its workflow_result tool");
167
- const preparedForSession = preparedForRuntimeRequest(prepared, request.customTools, request.signal, (value) => { structuredResult = value; }, resultTool);
167
+ const preparedForSession = Object.freeze({ ...preparedForRuntimeRequest(prepared, request.customTools, request.signal, (value) => { structuredResult = value; }, resultTool), ...(request.turnControl ? { turnControl: request.turnControl } : {}) });
168
168
  const callbacks = this.#options.callbacks;
169
169
  const handoff = this.#options.handoff;
170
170
  let adapter;
@@ -423,6 +423,9 @@ export class PiRuntimeAgentRunner {
423
423
  throwIfProgressFailed();
424
424
  beginTurn(false);
425
425
  throwIfTurnPolicyFailed();
426
+ // NOTE: A transport that already sent turnControl (Herdr, in its pane launch prompt) reports turnControlDelivered; steering its suspended local session would fail.
427
+ if (request.turnControl && !session.turnControlDelivered)
428
+ await activeSession.steer(request.turnControl);
426
429
  await promptAndRecover(request.task, true);
427
430
  completeTurn(hasResult() || !hasToolCall(lastAssistant), false, true);
428
431
  if (turnPolicyFailure)
@@ -1,4 +1,4 @@
1
- import type { AgentAttemptAction, JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration } from "./types.js";
1
+ import type { AgentAttemptAction, JsonValue, RegisteredAgentSetupHook, RegisteredAgentPreparationHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext } from "./types.js";
2
2
  import type { SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
3
3
  export type SubagentStatusObserver = (status: Readonly<SubagentStatus>, request: Readonly<SubagentRunRequest>) => void;
4
4
  export declare class WorkflowRegistry {
@@ -8,6 +8,11 @@ export declare class WorkflowRegistry {
8
8
  setSubagentStatusObserver(observer: SubagentStatusObserver | undefined): void;
9
9
  observeSubagentStatus(status: Readonly<SubagentStatus>, request: Readonly<SubagentRunRequest>): void;
10
10
  register(extension: WorkflowExtension): void;
11
+ /** Every registration's headline and declared module provenance, for hosts that must prove which extension made it. */
12
+ registrations(): ReadonlyArray<Readonly<{
13
+ headline: string;
14
+ source?: string;
15
+ }>>;
11
16
  function(name: string): WorkflowFunction;
12
17
  functions(): Readonly<Record<string, WorkflowFunction>>;
13
18
  functionSources(): Readonly<Record<string, WorkflowFunctionSource>>;
@@ -19,10 +24,9 @@ export declare class WorkflowRegistry {
19
24
  }>>;
20
25
  validateExtensionSettings(settings: Readonly<WorkflowExtensionSettings> | undefined, context: WorkflowExtensionSettingsValidatorContext): void;
21
26
  invokeFunction(name: string, input: unknown, context: Readonly<WorkflowFunctionContext>, path: string, journal: WorkflowJournal): Promise<JsonValue>;
27
+ agentPreparationHooks(): readonly RegisteredAgentPreparationHook[];
22
28
  agentSetupHooks(): readonly RegisteredAgentSetupHook[];
23
29
  agentAttemptActions(): Readonly<Record<string, AgentAttemptAction>>;
24
- roleDirectories(): readonly string[];
25
- roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
26
30
  modelAliases(): readonly {
27
31
  name: string;
28
32
  version: string;
@@ -31,7 +35,7 @@ export declare class WorkflowRegistry {
31
35
  }[];
32
36
  resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed?: ReadonlySet<string>): Promise<Readonly<Record<string, string>>>;
33
37
  }
34
- export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "functionSources" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "validateExtensionSettings" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "agentAttemptActions" | "roleDirectories" | "roleDirectoryRegistrations" | "setSubagentStatusObserver" | "observeSubagentStatus">;
38
+ export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "functionSources" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "validateExtensionSettings" | "modelAliases" | "resolveModelAliases" | "agentPreparationHooks" | "agentSetupHooks" | "agentAttemptActions" | "setSubagentStatusObserver" | "observeSubagentStatus" | "registrations">;
35
39
  export declare function resetWorkflowRegistry(): void;
36
40
  export declare function resetWorkflowRegistryIfIdle(): void;
37
41
  export declare function beginWorkflowExtensionLoading(): void;
@@ -43,6 +47,4 @@ export declare function workflowCatalogIndex(context?: WorkflowCatalogContext):
43
47
  export declare function workflowCatalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogModelAlias | WorkflowCatalogError;
44
48
  export declare function registeredWorkflowFunctions(): Readonly<Record<string, WorkflowFunction>>;
45
49
  export declare function registeredWorkflowFunctionSources(): Readonly<Record<string, WorkflowFunctionSource>>;
46
- export declare function registeredWorkflowRoleDirectories(): readonly string[];
47
- export declare function registeredWorkflowRoleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
48
- export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowRoleDirectoryRegistration } from "./types.js";
50
+ export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogModelAlias, WorkflowCatalogSettings } from "./types.js";
@@ -1,50 +1,11 @@
1
- import { readFileSync } from "node:fs";
2
- import { basename, dirname, isAbsolute, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
1
  import { Value } from "typebox/value";
5
2
  import { byPriorityThenName, deepFreeze, errorCode, errorText, fail, jsonValue, MODEL_ALIAS_NAME, object, validWorkflowExtensionNamespace } from "./utils.js";
6
3
  import { loadSettings, resolveWorkflowSettings } from "./settings.js";
7
4
  import { validateSchema } from "./utils.js";
8
- import { canonicalPath } from "./paths.js";
9
- import { isCorePackageName } from "./package-identity.js";
10
- const RESERVED_GLOBALS = new Set(["agent", "shell", "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"]);
5
+ const RESERVED_GLOBALS = new Set(["agent", "shell", "tools", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
11
6
  const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
12
7
  const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
13
8
  const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
14
- function normalizeRoleDirectory(value) {
15
- try {
16
- if (value instanceof URL) {
17
- if (value.protocol !== "file:")
18
- fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
19
- return fileURLToPath(value);
20
- }
21
- if (typeof value === "string") {
22
- if (value.startsWith("file:"))
23
- return fileURLToPath(value);
24
- if (isAbsolute(value))
25
- return value;
26
- }
27
- }
28
- catch (error) {
29
- fail("INVALID_METADATA", `Invalid workflow role directory: ${errorText(error)}`);
30
- }
31
- fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
32
- }
33
- function isBuiltinRoleDirectory(path) {
34
- const canonical = canonicalPath(path);
35
- const starterDirectory = dirname(canonical);
36
- if (basename(canonical) !== "roles" || basename(starterDirectory) !== "starter")
37
- return false;
38
- const distributionDirectory = dirname(starterDirectory);
39
- const packageRoot = basename(distributionDirectory) === "dist" ? dirname(distributionDirectory) : distributionDirectory;
40
- try {
41
- const metadata = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
42
- return object(metadata) && isCorePackageName(metadata.name);
43
- }
44
- catch {
45
- return false;
46
- }
47
- }
48
9
  function catalogSchema(schema, at) {
49
10
  validateSchema(schema, at);
50
11
  return structuredClone(schema);
@@ -53,9 +14,9 @@ export class WorkflowRegistry {
53
14
  #extensions = new Set();
54
15
  #functionSources = new Map();
55
16
  #globals = new Map();
17
+ #preparationHooks = new Map();
56
18
  #hooks = new Map();
57
19
  #agentAttemptActions = new Map();
58
- #roleDirectories = new Map();
59
20
  #modelAliases = new Map();
60
21
  #settingsValidators = new Set();
61
22
  #frozen = false;
@@ -69,10 +30,11 @@ export class WorkflowRegistry {
69
30
  fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
70
31
  if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
71
32
  fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
72
- if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "agentSetupHooks", "agentAttemptActions", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim())
33
+ if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "agentPreparationHooks", "agentSetupHooks", "agentAttemptActions"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim())
73
34
  fail("INVALID_METADATA", "Workflow extensions require a semantic version and non-empty headline");
74
35
  const functions = extension.functions ?? {};
75
36
  const modelAliases = extension.modelAliases ?? {};
37
+ const agentPreparationHooks = extension.agentPreparationHooks ?? {};
76
38
  const agentSetupHooks = extension.agentSetupHooks ?? {};
77
39
  const agentAttemptActions = extension.agentAttemptActions ?? {};
78
40
  const source = extension.source;
@@ -85,12 +47,8 @@ export class WorkflowRegistry {
85
47
  fail("INVALID_METADATA", "Workflow extension source must be a non-empty module URL");
86
48
  if (dependencyValues !== undefined && (!Array.isArray(dependencyValues) || dependencyValues.some((dependency) => typeof dependency !== "string" || dependency.trim() !== dependency || !PACKAGE_NAME.test(dependency)) || new Set(dependencyValues).size !== dependencyValues.length))
87
49
  fail("INVALID_METADATA", "Workflow extension dependencies must be unique, non-empty package names");
88
- const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
89
- if (!Array.isArray(roleDirectoryValues))
90
- fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
91
- const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
92
- if (!object(functions) || !object(modelAliases) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && roleDirectories.length === 0 && validateSettings === undefined))
93
- fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent setup hooks, agent attempt actions, role directories, or a settings validator");
50
+ if (!object(functions) || !object(modelAliases) || !object(agentPreparationHooks) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentPreparationHooks).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && validateSettings === undefined))
51
+ fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent preparation hooks, agent setup hooks, agent attempt actions, or a settings validator");
94
52
  const names = Object.keys(functions);
95
53
  if (new Set(names).size !== names.length)
96
54
  fail("GLOBAL_COLLISION", "Global name collision inside extension");
@@ -118,6 +76,14 @@ export class WorkflowRegistry {
118
76
  if (this.#modelAliases.has(name))
119
77
  fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
120
78
  }
79
+ for (const [name, hook] of Object.entries(agentPreparationHooks)) {
80
+ if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "optionsSchema", "prepare"].includes(key)) || typeof hook.prepare !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority)))
81
+ fail("INVALID_METADATA", `Invalid agent preparation hook: ${name}`);
82
+ if (this.#preparationHooks.has(name))
83
+ fail("DUPLICATE_NAME", `Agent preparation hook already registered: ${name}`);
84
+ if (hook.optionsSchema !== undefined)
85
+ validateSchema(hook.optionsSchema, `${name} options schema`);
86
+ }
121
87
  for (const [name, hook] of Object.entries(agentSetupHooks)) {
122
88
  if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority)))
123
89
  fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
@@ -132,18 +98,17 @@ export class WorkflowRegistry {
132
98
  if (this.#agentAttemptActions.has(name))
133
99
  fail("DUPLICATE_NAME", `Agent attempt action already registered: ${name}`);
134
100
  }
135
- const stored = deepFreeze({ ...extension, functions, modelAliases, agentSetupHooks, agentAttemptActions, ...(roleDirectories.length ? { roleDirectories } : {}) });
101
+ const stored = deepFreeze({ ...extension, functions, modelAliases, agentPreparationHooks, agentSetupHooks, agentAttemptActions });
136
102
  if (source !== undefined)
137
103
  for (const name of names)
138
104
  this.#functionSources.set(name, Object.freeze({ module: source, export: "default", dependencies: Object.freeze([...dependencies]) }));
139
105
  this.#extensions.add(stored);
140
- for (const directory of roleDirectories)
141
- if (!this.#roleDirectories.has(directory))
142
- this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline }, ...(isBuiltinRoleDirectory(directory) ? { builtin: true } : {}) }));
143
106
  for (const name of names)
144
107
  this.#globals.set(name, name);
145
108
  for (const [name, alias] of Object.entries(modelAliases))
146
109
  this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, resolve: alias.resolve });
110
+ for (const [name, hook] of Object.entries(agentPreparationHooks))
111
+ this.#preparationHooks.set(name, { name, priority: hook.priority ?? 10, prepare: hook.prepare, ...(hook.optionsSchema === undefined ? {} : { optionsSchema: structuredClone(hook.optionsSchema) }), ...(source === undefined ? {} : { source }) });
147
112
  for (const [name, hook] of Object.entries(agentSetupHooks))
148
113
  this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
149
114
  for (const [name, action] of Object.entries(agentAttemptActions))
@@ -151,6 +116,10 @@ export class WorkflowRegistry {
151
116
  if (validateSettings !== undefined)
152
117
  this.#settingsValidators.add({ headline: extension.headline, validate: validateSettings });
153
118
  }
119
+ /** Every registration's headline and declared module provenance, for hosts that must prove which extension made it. */
120
+ registrations() {
121
+ return [...this.#extensions].map(({ headline, source }) => Object.freeze({ headline, ...(source === undefined ? {} : { source }) }));
122
+ }
154
123
  function(name) {
155
124
  if (!IDENTIFIER.test(name))
156
125
  fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
@@ -176,7 +145,7 @@ export class WorkflowRegistry {
176
145
  let source = "global settings";
177
146
  try {
178
147
  const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : undefined;
179
- aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
148
+ aliases = resolved?.effective.modelAliases ?? (context ? loadSettings().modelAliases : resolveWorkflowSettings(process.cwd(), false).effective.modelAliases);
180
149
  if (resolved) {
181
150
  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";
182
151
  settings = { concurrency: resolved.effective.concurrency, backgroundWidget: resolved.effective.backgroundWidget ?? true, modelAliases: resolved.effective.modelAliases ?? {}, skills: resolved.effective.skills ?? [], extensions: Array.isArray(resolved.effective.extensions) ? resolved.effective.extensions : [], tools: resolved.effective.tools ?? [], ...(resolved.effective.extensionSettings === undefined ? {} : { extensionSettings: resolved.effective.extensionSettings }), ...(resolved.effective.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: resolved.effective.worktreePostCreateCommand }), globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted: resolved.projectTrusted, sources: resolved.sources };
@@ -246,7 +215,7 @@ export class WorkflowRegistry {
246
215
  validate(frozenSettings, Object.freeze({ ...context }));
247
216
  }
248
217
  catch (error) {
249
- const location = context.source === "role" && context.role ? `role ${context.role}` : context.source;
218
+ const location = context.source;
250
219
  fail("INVALID_SETTINGS", `Invalid extension settings (${headline}; ${location}): ${errorText(error)}`);
251
220
  }
252
221
  }
@@ -268,16 +237,11 @@ export class WorkflowRegistry {
268
237
  journal.put(path, stored);
269
238
  return structuredClone(stored);
270
239
  }
240
+ agentPreparationHooks() { return [...this.#preparationHooks.values()].sort(byPriorityThenName); }
271
241
  agentSetupHooks() {
272
242
  return [...this.#hooks.values()].sort(byPriorityThenName);
273
243
  }
274
244
  agentAttemptActions() { return Object.freeze(Object.fromEntries(this.#agentAttemptActions.entries())); }
275
- roleDirectories() {
276
- return [...this.#roleDirectories.keys()];
277
- }
278
- roleDirectoryRegistrations() {
279
- return [...this.#roleDirectories.values()];
280
- }
281
245
  modelAliases() { return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name)); }
282
246
  async resolveModelAliases(context, shadowed = new Set()) {
283
247
  const resolved = {};
@@ -325,10 +289,10 @@ function createWorkflowRegistryApi(registry) {
325
289
  validateExtensionSettings: (...args) => { registry.validateExtensionSettings(...args); },
326
290
  modelAliases: () => registry.modelAliases(),
327
291
  resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
328
- roleDirectories: () => registry.roleDirectories(),
329
- roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
292
+ agentPreparationHooks: () => registry.agentPreparationHooks(),
330
293
  agentSetupHooks: () => registry.agentSetupHooks(),
331
294
  agentAttemptActions: () => registry.agentAttemptActions(),
295
+ registrations: () => registry.registrations(),
332
296
  };
333
297
  }
334
298
  function workflowRegistryHost() {
@@ -361,7 +325,9 @@ export function retainWorkflowRegistry() {
361
325
  }
362
326
  export function loadingRegistry() { return workflowRegistryHost().api; }
363
327
  beginWorkflowExtensionLoading();
364
- export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
328
+ export function registerWorkflowExtension(extension) {
329
+ loadingRegistry().register(extension);
330
+ }
365
331
  export function workflowCatalog(context) { return loadingRegistry().catalog(context); }
366
332
  export function workflowCatalogIndex(context) { return loadingRegistry().catalogIndex(context); }
367
333
  export function workflowCatalogDetail(name, context) { return loadingRegistry().catalogDetail(name, context); }
@@ -370,11 +336,3 @@ export function registeredWorkflowFunctionSources() {
370
336
  const sources = loadingRegistry().functionSources;
371
337
  return typeof sources === "function" ? sources() : {};
372
338
  }
373
- export function registeredWorkflowRoleDirectories() {
374
- const directories = loadingRegistry().roleDirectories;
375
- return typeof directories === "function" ? directories() : [];
376
- }
377
- export function registeredWorkflowRoleDirectoryRegistrations() {
378
- const registrations = loadingRegistry().roleDirectoryRegistrations;
379
- return typeof registrations === "function" ? registrations() : [];
380
- }
@@ -109,6 +109,8 @@ export interface RuntimeAgentRunRequest {
109
109
  readonly customTools: readonly RuntimeTool[];
110
110
  /** Explicit result schema; omitted agents use the default string workflow_result schema. */
111
111
  readonly resultSchema?: RuntimeJsonSchema;
112
+ /** Transient retry or budget text delivered as a separate steered message after the task, never inside it. */
113
+ readonly turnControl?: string;
112
114
  readonly run: RuntimeRunIdentity;
113
115
  readonly agent: RuntimeAgentIdentity;
114
116
  readonly signal: AbortSignal;
@@ -0,0 +1,27 @@
1
+ import type { ExtensionToolContext, ToolLoadout, ToolLoadoutChanges } from "@earendil-works/pi-coding-agent";
2
+ import { type JsonValue } from "./types.js";
3
+ type ScriptTool = ExtensionToolContext["tools"][number];
4
+ type ScriptToolOutcome = Awaited<ReturnType<ExtensionToolContext["executeTool"]>>;
5
+ /** Workflow and subagent tools stay out of scripts, so a run cannot launch or control runs. Codemode is model-only and never callable. */
6
+ export declare function scriptToolExcluded(name: string): boolean;
7
+ /** The identifier scripts use for a tool, the same as Pi codemode's, so its `tools.<id>(args)` lines apply to workflow scripts. */
8
+ export declare function scriptToolIdentifier(name: string): string;
9
+ /** The context of a Pi tool call, whose `executeTool()` runs session tools; absent for commands, session events, and headless launches. */
10
+ export declare function scriptToolContext(ctx: unknown): ExtensionToolContext | undefined;
11
+ export declare function scriptToolIdentifiers(context: ExtensionToolContext | undefined): string[];
12
+ export declare function scriptTool(context: ExtensionToolContext, identifier: string): ScriptTool | undefined;
13
+ /**
14
+ * What a script call resolves to, as in codemode: the structured content of a tool with an output
15
+ * schema, also for error results that carry one, otherwise its text; other failures throw. The value
16
+ * is normalized through JSON and size-checked before it is journaled, so a replay returns the same value.
17
+ */
18
+ export declare function scriptToolValue(tool: ScriptTool, outcome: ScriptToolOutcome): JsonValue;
19
+ /** Fails a launch whose script names a tool this session cannot call, before any agent runs. */
20
+ export declare function validateScriptToolReferences(script: string, context: ExtensionToolContext | undefined): void;
21
+ /**
22
+ * Tells the model what `tools.<id>(args)` resolves to for declared tools with an output schema; the
23
+ * workflow description says that the others resolve to their text. With codemode active its own
24
+ * identical `Codemode:` lines say this, and pi keeps only one description per tool, so this defers.
25
+ */
26
+ export declare function prepareScriptToolLoadout(loadout: ToolLoadout): ToolLoadoutChanges | undefined;
27
+ export {};
@@ -0,0 +1,87 @@
1
+ import { WorkflowError } from "./types.js";
2
+ import { errorText, jsonValue, object } from "./utils.js";
3
+ import { RPC_LIMIT_BYTES } from "./execution.js";
4
+ import { scriptToolReferences } from "./validation.js";
5
+ /** Workflow and subagent tools stay out of scripts, so a run cannot launch or control runs. Codemode is model-only and never callable. */
6
+ export function scriptToolExcluded(name) {
7
+ return name === "workflow" || name.startsWith("workflow_") || name.startsWith("subagents_");
8
+ }
9
+ /** The identifier scripts use for a tool, the same as Pi codemode's, so its `tools.<id>(args)` lines apply to workflow scripts. */
10
+ export function scriptToolIdentifier(name) {
11
+ let identifier = "";
12
+ for (const char of name)
13
+ identifier += (identifier === "" ? /^[A-Za-z_$]$/ : /^[A-Za-z0-9_$]$/).test(char) ? char : "_";
14
+ return identifier || "_";
15
+ }
16
+ /** The context of a Pi tool call, whose `executeTool()` runs session tools; absent for commands, session events, and headless launches. */
17
+ export function scriptToolContext(ctx) {
18
+ return object(ctx) && typeof ctx.executeTool === "function" && Array.isArray(ctx.tools) ? ctx : undefined;
19
+ }
20
+ export function scriptToolIdentifiers(context) {
21
+ return (context?.tools ?? []).filter(({ name }) => !scriptToolExcluded(name)).map(({ name }) => scriptToolIdentifier(name));
22
+ }
23
+ // NOTE: a launch rejects identifiers that several tools share; a later tool set may still collide, and the first match wins.
24
+ export function scriptTool(context, identifier) {
25
+ return context.tools.find(({ name }) => !scriptToolExcluded(name) && scriptToolIdentifier(name) === identifier);
26
+ }
27
+ /**
28
+ * What a script call resolves to, as in codemode: the structured content of a tool with an output
29
+ * schema, also for error results that carry one, otherwise its text; other failures throw. The value
30
+ * is normalized through JSON and size-checked before it is journaled, so a replay returns the same value.
31
+ */
32
+ export function scriptToolValue(tool, outcome) {
33
+ const structured = tool.outputSchema === undefined ? undefined : outcome.result.structuredContent;
34
+ const text = outcome.result.content.flatMap((item) => item.type === "text" ? [item.text] : []).join("\n");
35
+ if (structured === undefined && outcome.isError)
36
+ throw new WorkflowError("TOOL_FAILED", `${tool.name}: ${text || "failed"}`);
37
+ let value;
38
+ try {
39
+ value = JSON.parse(JSON.stringify(structured ?? text));
40
+ }
41
+ catch (error) {
42
+ throw new WorkflowError("TOOL_FAILED", `${tool.name} returned a result that is not JSON: ${errorText(error)}`);
43
+ }
44
+ if (!jsonValue(value))
45
+ throw new WorkflowError("TOOL_FAILED", `${tool.name} returned a result that is not JSON`);
46
+ if (Buffer.byteLength(JSON.stringify(value)) > RPC_LIMIT_BYTES)
47
+ throw new WorkflowError("TOOL_FAILED", `${tool.name} returned more than the 10 MB workflow RPC limit`);
48
+ return value;
49
+ }
50
+ /** Fails a launch whose script names a tool this session cannot call, before any agent runs. */
51
+ export function validateScriptToolReferences(script, context) {
52
+ const references = scriptToolReferences(script);
53
+ if (!references?.length)
54
+ return;
55
+ if (!context)
56
+ throw new WorkflowError("UNKNOWN_TOOL", `Workflow scripts can call session tools only when launched by the workflow tool; this launch has none for ${references.map((name) => `tools.${name}`).join(", ")}`);
57
+ const identifiers = scriptToolIdentifiers(context);
58
+ const unknown = references.filter((name) => !identifiers.includes(name));
59
+ if (unknown.length)
60
+ throw new WorkflowError("UNKNOWN_TOOL", `Workflow script calls tools this session cannot call: ${unknown.map((name) => `tools.${name}`).join(", ")}`);
61
+ const ambiguous = references.filter((name) => identifiers.indexOf(name) !== identifiers.lastIndexOf(name));
62
+ if (ambiguous.length)
63
+ throw new WorkflowError("UNKNOWN_TOOL", `Workflow script calls tools whose names collide after normalization: ${ambiguous.map((name) => `tools.${name}`).join(", ")}`);
64
+ }
65
+ function describeScriptOutput(schema) {
66
+ if (!object(schema) || schema.type !== "object" || !object(schema.properties))
67
+ return schema === undefined ? undefined : "a JSON value";
68
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
69
+ return `\`{ ${Object.keys(schema.properties).map((name) => required.has(name) ? name : `${name}?`).join(", ")} }\``;
70
+ }
71
+ /**
72
+ * Tells the model what `tools.<id>(args)` resolves to for declared tools with an output schema; the
73
+ * workflow description says that the others resolve to their text. With codemode active its own
74
+ * identical `Codemode:` lines say this, and pi keeps only one description per tool, so this defers.
75
+ */
76
+ export function prepareScriptToolLoadout(loadout) {
77
+ if (loadout.declared.some(({ name }) => name === "codemode"))
78
+ return undefined;
79
+ const callable = new Set(loadout.callable.map(({ name }) => name));
80
+ const descriptions = {};
81
+ for (const tool of loadout.declared) {
82
+ const output = callable.has(tool.name) && !scriptToolExcluded(tool.name) ? describeScriptOutput(tool.outputSchema) : undefined;
83
+ if (output)
84
+ descriptions[tool.name] = `${tool.description.trim()}\n\nWorkflow scripts: \`tools.${scriptToolIdentifier(tool.name)}(args)\` resolves to ${output}.`;
85
+ }
86
+ return { descriptions };
87
+ }
@@ -1,4 +1,4 @@
1
- export declare function processAlive(pid: number, startedAt?: number): Promise<boolean>;
1
+ export declare function processAlive(pid: number, startedAt?: number, procRoot?: string): Promise<boolean>;
2
2
  export declare function hasLiveSessionLease(cwd: string, sessionId: string, home?: string): Promise<boolean>;
3
3
  export declare class SessionLease {
4
4
  #private;
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { link, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
@@ -10,22 +11,55 @@ import { runsDirectory } from "./paths.js";
10
11
  const SESSION_OWNER_FILE = "owner.json";
11
12
  const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
12
13
  const RUN_CREATE_TEMP = /^\.([a-zA-Z0-9._-]+)\.(\d+)\.[0-9a-f-]+\.tmp$/;
13
- export async function processAlive(pid, startedAt) {
14
+ const START_MARGIN_MS = 60_000;
15
+ let clockTicks;
16
+ function clockTickRate() {
17
+ if (clockTicks !== undefined)
18
+ return clockTicks;
19
+ try {
20
+ const rate = Number(execFileSync("getconf", ["CLK_TCK"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
21
+ clockTicks = Number.isInteger(rate) && rate > 0 ? rate : 100;
22
+ }
23
+ catch {
24
+ clockTicks = 100;
25
+ }
26
+ return clockTicks;
27
+ }
28
+ // Process start in epoch ms from /proc/<pid>/stat field 22; undefined when unreadable or unparsable.
29
+ async function linuxProcessStart(pid, procRoot) {
30
+ try {
31
+ const text = await readFile(join(procRoot, String(pid), "stat"), "utf8");
32
+ // Field 2 (command name) may contain spaces and parentheses, so count fields after the last ")".
33
+ const close = text.lastIndexOf(")");
34
+ const ticks = close < 0 ? Number.NaN : Number(text.slice(close + 2).split(" ")[19]);
35
+ const btime = /^btime (\d+)$/m.exec(await readFile(join(procRoot, "stat"), "utf8"))?.[1];
36
+ if (btime === undefined || !Number.isInteger(ticks) || ticks < 0)
37
+ return undefined;
38
+ return Number(btime) * 1000 + (ticks / clockTickRate()) * 1000;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ // The /proc/<pid> ctime is not a start time: procfs rebuilds the inode after cache reclaim.
45
+ // A live owner never starts after its stamp; a reused pid starts later.
46
+ export async function processAlive(pid, startedAt, procRoot = "/proc") {
14
47
  try {
15
48
  process.kill(pid, 0);
16
49
  }
17
50
  catch (error) {
18
51
  return !isNodeError(error, "ESRCH");
19
52
  }
20
- if (startedAt !== undefined && process.platform === "linux") {
21
- try {
22
- if ((await stat(`/proc/${String(pid)}`)).ctimeMs > startedAt)
23
- return false;
24
- }
25
- catch (error) {
26
- if (isNodeError(error, "ENOENT"))
27
- return false;
28
- }
53
+ if (startedAt === undefined || process.platform !== "linux")
54
+ return true;
55
+ const start = await linuxProcessStart(pid, procRoot);
56
+ if (start !== undefined)
57
+ return start <= startedAt + START_MARGIN_MS;
58
+ try {
59
+ process.kill(pid, 0);
60
+ }
61
+ catch (error) {
62
+ return !isNodeError(error, "ESRCH");
29
63
  }
30
64
  return true;
31
65
  }
@@ -98,7 +132,7 @@ export async function acquireSessionLease(cwd, sessionId, home = homedir()) {
98
132
  const path = join(directory, SESSION_OWNER_FILE);
99
133
  for (;;) {
100
134
  const token = randomUUID();
101
- const owner = { pid: process.pid, token, startedAt: process.platform === "linux" ? (await stat(`/proc/${String(process.pid)}`)).ctimeMs : Date.now() };
135
+ const owner = { pid: process.pid, token, startedAt: Date.now() };
102
136
  try {
103
137
  const handle = await open(path, "wx", 0o600);
104
138
  try {
@@ -1,13 +1,29 @@
1
- import type { AgentResourcePolicy, ContextFileScope, WorkflowExtensionSettings, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution } from "./types.js";
1
+ import { type ToolAnnotations } from "@earendil-works/pi-coding-agent";
2
+ import type { AgentResourcePolicy, AgentResourceSelectorSources, ContextFileScope, WorkflowExtensionSettings, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution } from "./types.js";
2
3
  export { workflowProjectSettingsPath } from "./paths.js";
3
4
  export declare const DEFAULT_SETTINGS: Readonly<WorkflowSettings>;
4
5
  export declare function workflowSettingsPath(agentDir?: string): string;
5
6
  export declare function validateSelectorList(value: unknown, path: string, kind: "skills" | "extensions" | "tools", errorCode?: "INVALID_SETTINGS" | "INVALID_METADATA", normalizeExtensions?: boolean): readonly string[] | undefined;
6
- export declare function validateContextFileScopes(value: unknown, rolePath: string): readonly ContextFileScope[] | undefined;
7
+ export declare function validateContextFileScopes(value: unknown, sourcePath: string): readonly ContextFileScope[] | undefined;
7
8
  export declare function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode?: "INVALID_SETTINGS" | "INVALID_METADATA"): WorkflowExtensionSettings | undefined;
8
9
  export declare function loadSettings(path?: string): Readonly<WorkflowSettings>;
10
+ /**
11
+ * Exposure and annotations of a workflow or subagent tool. Under the global `codemodeTools` setting,
12
+ * `model-only` keeps the model's direct access and stops codemode scripts from calling the tool.
13
+ * Tools are registered once at load, so the setting is global and applies after a reload.
14
+ * Read-only tools carry `readOnlyHint` so permission extensions need not confirm them.
15
+ */
16
+ export declare function workflowToolExposure(name: string, codemodeTools: WorkflowSettings["codemodeTools"]): {
17
+ exposure?: "model-only";
18
+ annotations?: ToolAnnotations;
19
+ };
20
+ /** The `codemodeTools` setting at load time. Invalid settings keep the default; launches report them. */
21
+ export declare function loadCodemodeToolsSetting(agentDir?: string): WorkflowSettings["codemodeTools"];
9
22
  export declare function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides>;
10
- export declare function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath?: string): WorkflowSettingsResolution;
23
+ export declare function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath?: string): WorkflowSettingsResolution & {
24
+ selectorSources: AgentResourceSelectorSources;
25
+ };
11
26
  export declare function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void;
12
27
  export declare function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath?: string): AgentResourcePolicy;
13
28
  export declare function saveModelAliases(path?: string, aliases?: Readonly<Record<string, string>>): void;
29
+ export declare function canonicalExtensionSelector(selector: string, cwd: string): string;