@goah/cli 0.11.5 → 0.13.0

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 (160) hide show
  1. package/dist/cli.js +165 -66
  2. package/dist/cli.js.map +1 -1
  3. package/dist/console/assets/index-3sYrDMmt.css +1 -0
  4. package/dist/console/assets/index-Rtc_B7cs.js +52 -0
  5. package/dist/console/assets/index-Rtc_B7cs.js.map +1 -0
  6. package/dist/console/index.html +2 -2
  7. package/dist/control.d.ts +13 -13
  8. package/dist/control.d.ts.map +1 -1
  9. package/dist/control.js +77 -50
  10. package/dist/control.js.map +1 -1
  11. package/dist/index.d.ts +6 -23
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +156 -28
  14. package/dist/index.js.map +1 -1
  15. package/dist/live-config.js +2 -2
  16. package/dist/live-config.js.map +1 -1
  17. package/dist/prompt-queue.d.ts.map +1 -1
  18. package/dist/prompt-queue.js +5 -0
  19. package/dist/prompt-queue.js.map +1 -1
  20. package/dist/setup-wizard.d.ts +13 -2
  21. package/dist/setup-wizard.d.ts.map +1 -1
  22. package/dist/setup-wizard.js +87 -63
  23. package/dist/setup-wizard.js.map +1 -1
  24. package/dist/thread-inspect.d.ts +47 -0
  25. package/dist/thread-inspect.d.ts.map +1 -0
  26. package/dist/thread-inspect.js +78 -0
  27. package/dist/thread-inspect.js.map +1 -0
  28. package/dist/transcript.d.ts +2 -0
  29. package/dist/transcript.d.ts.map +1 -0
  30. package/dist/transcript.js +2 -0
  31. package/dist/transcript.js.map +1 -0
  32. package/dist/tui-theme.d.ts +1 -0
  33. package/dist/tui-theme.d.ts.map +1 -1
  34. package/dist/tui-theme.js +1 -0
  35. package/dist/tui-theme.js.map +1 -1
  36. package/dist/tui.d.ts +8 -2
  37. package/dist/tui.d.ts.map +1 -1
  38. package/dist/tui.js +291 -90
  39. package/dist/tui.js.map +1 -1
  40. package/dist/update.d.ts +1 -0
  41. package/dist/update.d.ts.map +1 -1
  42. package/dist/update.js +15 -7
  43. package/dist/update.js.map +1 -1
  44. package/dist/web-console.d.ts +1 -1
  45. package/dist/web-console.d.ts.map +1 -1
  46. package/dist/web-console.js +27 -37
  47. package/dist/web-console.js.map +1 -1
  48. package/dist/welcome.d.ts +2 -2
  49. package/dist/welcome.d.ts.map +1 -1
  50. package/dist/welcome.js +24 -36
  51. package/dist/welcome.js.map +1 -1
  52. package/node_modules/goah-ledger-contract/dist/execution.d.ts +272 -130
  53. package/node_modules/goah-ledger-contract/dist/execution.d.ts.map +1 -1
  54. package/node_modules/goah-ledger-contract/dist/execution.js +31 -19
  55. package/node_modules/goah-ledger-contract/dist/execution.js.map +1 -1
  56. package/node_modules/goah-ledger-contract/dist/index.d.ts +1 -2
  57. package/node_modules/goah-ledger-contract/dist/index.d.ts.map +1 -1
  58. package/node_modules/goah-ledger-contract/dist/index.js +1 -2
  59. package/node_modules/goah-ledger-contract/dist/index.js.map +1 -1
  60. package/node_modules/goah-ledger-contract/dist/kernel.d.ts +1 -1
  61. package/node_modules/goah-ledger-contract/dist/kernel.d.ts.map +1 -1
  62. package/node_modules/goah-ledger-contract/dist/kernel.js +1 -1
  63. package/node_modules/goah-ledger-contract/dist/kernel.js.map +1 -1
  64. package/node_modules/goah-ledger-contract/dist/transcript.d.ts +63 -0
  65. package/node_modules/goah-ledger-contract/dist/transcript.d.ts.map +1 -0
  66. package/node_modules/goah-ledger-contract/dist/transcript.js +115 -0
  67. package/node_modules/goah-ledger-contract/dist/transcript.js.map +1 -0
  68. package/node_modules/goah-ledger-contract/package.json +3 -4
  69. package/node_modules/goah-ledger-sqlite/dist/index.d.ts +32 -23
  70. package/node_modules/goah-ledger-sqlite/dist/index.d.ts.map +1 -1
  71. package/node_modules/goah-ledger-sqlite/dist/index.js +764 -394
  72. package/node_modules/goah-ledger-sqlite/dist/index.js.map +1 -1
  73. package/node_modules/goah-ledger-sqlite/package.json +2 -2
  74. package/node_modules/goah-runner-pi/dist/configurator.d.ts.map +1 -1
  75. package/node_modules/goah-runner-pi/dist/configurator.js +186 -30
  76. package/node_modules/goah-runner-pi/dist/configurator.js.map +1 -1
  77. package/node_modules/goah-runner-pi/dist/credential-store.d.ts +2 -0
  78. package/node_modules/goah-runner-pi/dist/credential-store.d.ts.map +1 -1
  79. package/node_modules/goah-runner-pi/dist/credential-store.js +117 -23
  80. package/node_modules/goah-runner-pi/dist/credential-store.js.map +1 -1
  81. package/node_modules/goah-runner-pi/dist/index.d.ts +18 -6
  82. package/node_modules/goah-runner-pi/dist/index.d.ts.map +1 -1
  83. package/node_modules/goah-runner-pi/dist/index.js +97 -12
  84. package/node_modules/goah-runner-pi/dist/index.js.map +1 -1
  85. package/node_modules/goah-runner-pi/dist/pi-worker.d.ts +4 -2
  86. package/node_modules/goah-runner-pi/dist/pi-worker.d.ts.map +1 -1
  87. package/node_modules/goah-runner-pi/dist/pi-worker.js +220 -69
  88. package/node_modules/goah-runner-pi/dist/pi-worker.js.map +1 -1
  89. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.d.ts +2 -0
  90. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.d.ts.map +1 -0
  91. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.js +3 -0
  92. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.js.map +1 -0
  93. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.d.ts +2 -0
  94. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.d.ts.map +1 -0
  95. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.js +5 -0
  96. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.js.map +1 -0
  97. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.d.ts +2 -0
  98. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.d.ts.map +1 -0
  99. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.js +5 -0
  100. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.js.map +1 -0
  101. package/node_modules/goah-runner-pi/dist/test-helpers.d.ts +1 -1
  102. package/node_modules/goah-runner-pi/dist/test-helpers.d.ts.map +1 -1
  103. package/node_modules/goah-runner-pi/dist/test-helpers.js +3 -1
  104. package/node_modules/goah-runner-pi/dist/test-helpers.js.map +1 -1
  105. package/node_modules/goah-runner-pi/dist/verification-worker.d.ts.map +1 -1
  106. package/node_modules/goah-runner-pi/dist/verification-worker.js +3 -5
  107. package/node_modules/goah-runner-pi/dist/verification-worker.js.map +1 -1
  108. package/node_modules/goah-runner-pi/package.json +2 -2
  109. package/node_modules/goah-supervisor/dist/context-view.d.ts +4 -5
  110. package/node_modules/goah-supervisor/dist/context-view.d.ts.map +1 -1
  111. package/node_modules/goah-supervisor/dist/context-view.js +7 -20
  112. package/node_modules/goah-supervisor/dist/context-view.js.map +1 -1
  113. package/node_modules/goah-supervisor/dist/index.d.ts +38 -36
  114. package/node_modules/goah-supervisor/dist/index.d.ts.map +1 -1
  115. package/node_modules/goah-supervisor/dist/index.js +762 -575
  116. package/node_modules/goah-supervisor/dist/index.js.map +1 -1
  117. package/node_modules/goah-supervisor/dist/roles.d.ts +2 -1
  118. package/node_modules/goah-supervisor/dist/roles.d.ts.map +1 -1
  119. package/node_modules/goah-supervisor/dist/roles.js +20 -5
  120. package/node_modules/goah-supervisor/dist/roles.js.map +1 -1
  121. package/node_modules/goah-supervisor/dist/verification.d.ts +8 -29
  122. package/node_modules/goah-supervisor/dist/verification.d.ts.map +1 -1
  123. package/node_modules/goah-supervisor/dist/verification.js +71 -46
  124. package/node_modules/goah-supervisor/dist/verification.js.map +1 -1
  125. package/node_modules/goah-supervisor/package.json +2 -2
  126. package/node_modules/goah-testkit/dist/faux-runner-worker.js +16 -9
  127. package/node_modules/goah-testkit/dist/faux-runner-worker.js.map +1 -1
  128. package/node_modules/goah-testkit/dist/index.d.ts +13 -16
  129. package/node_modules/goah-testkit/dist/index.d.ts.map +1 -1
  130. package/node_modules/goah-testkit/dist/index.js +27 -44
  131. package/node_modules/goah-testkit/dist/index.js.map +1 -1
  132. package/node_modules/goah-testkit/package.json +5 -5
  133. package/package.json +7 -8
  134. package/dist/console/assets/index-BJTGvlCH.js +0 -52
  135. package/dist/console/assets/index-BJTGvlCH.js.map +0 -1
  136. package/dist/console/assets/index-msBn41Cl.css +0 -1
  137. package/dist/inspect.d.ts +0 -61
  138. package/dist/inspect.d.ts.map +0 -1
  139. package/dist/inspect.js +0 -110
  140. package/dist/inspect.js.map +0 -1
  141. package/dist/metrics.d.ts +0 -2
  142. package/dist/metrics.d.ts.map +0 -1
  143. package/dist/metrics.js +0 -2
  144. package/dist/metrics.js.map +0 -1
  145. package/dist/session.d.ts +0 -2
  146. package/dist/session.d.ts.map +0 -1
  147. package/dist/session.js +0 -2
  148. package/dist/session.js.map +0 -1
  149. package/node_modules/goah-ledger-contract/dist/metrics.d.ts +0 -40
  150. package/node_modules/goah-ledger-contract/dist/metrics.d.ts.map +0 -1
  151. package/node_modules/goah-ledger-contract/dist/metrics.js +0 -25
  152. package/node_modules/goah-ledger-contract/dist/metrics.js.map +0 -1
  153. package/node_modules/goah-ledger-contract/dist/session.d.ts +0 -63
  154. package/node_modules/goah-ledger-contract/dist/session.d.ts.map +0 -1
  155. package/node_modules/goah-ledger-contract/dist/session.js +0 -115
  156. package/node_modules/goah-ledger-contract/dist/session.js.map +0 -1
  157. package/node_modules/goah-testkit/dist/mock-connector-worker.d.ts +0 -2
  158. package/node_modules/goah-testkit/dist/mock-connector-worker.d.ts.map +0 -1
  159. package/node_modules/goah-testkit/dist/mock-connector-worker.js +0 -29
  160. package/node_modules/goah-testkit/dist/mock-connector-worker.js.map +0 -1
@@ -1,10 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
2
3
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { dirname, resolve, sep } from "node:path";
4
+ import { delimiter, dirname, join, resolve, sep } from "node:path";
5
+ import { homedir, tmpdir } from "node:os";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { Agent } from "@earendil-works/pi-agent-core";
6
8
  import { fauxAssistantMessage, fauxText, fauxToolCall, Type } from "@earendil-works/pi-ai";
7
- import { SESSION_FORMAT_VERSION } from "goah-ledger-contract";
9
+ import { TRANSCRIPT_FORMAT_VERSION } from "goah-ledger-contract";
8
10
  import { runProcessWorker } from "./index.js";
9
11
  import { createPiModel } from "./model-provider.js";
10
12
  export function compactMessages(messages, maxRecent = 8) {
@@ -19,7 +21,7 @@ export function compactMessages(messages, maxRecent = 8) {
19
21
  ];
20
22
  }
21
23
  export async function runPiWorker() {
22
- await runProcessWorker(async (request, emit, rpc) => {
24
+ await runProcessWorker(async (request, emit, rpc, controls) => {
23
25
  const contextRecord = typeof request.context === "object" && request.context !== null && !Array.isArray(request.context) ? request.context : {};
24
26
  const legacyRequest = request.turn === undefined;
25
27
  const binding = request.turn?.goalBinding;
@@ -33,6 +35,7 @@ export async function runPiWorker() {
33
35
  const configured = createPiModel(provider, modelId);
34
36
  const { models } = configured;
35
37
  const privateAuth = request.runtime && typeof request.runtime === "object" && !Array.isArray(request.runtime) ? request.runtime : {};
38
+ const protectedPaths = Array.isArray(privateAuth.protectedPaths) ? privateAuth.protectedPaths.filter((value) => typeof value === "string").map((path) => resolve(path)) : [];
36
39
  const model = typeof privateAuth.baseUrl === "string" ? { ...configured.model, baseUrl: privateAuth.baseUrl } : configured.model;
37
40
  if (provider === "faux") {
38
41
  const faux = configured.faux;
@@ -43,21 +46,26 @@ export async function runPiWorker() {
43
46
  const handoff = JSON.parse(process.env.GOAH_PI_FAUX_HANDOFF ?? "{}");
44
47
  const current = contextRecord.workRecord && typeof contextRecord.workRecord === "object" && !Array.isArray(contextRecord.workRecord) ? contextRecord.workRecord : {};
45
48
  const evidence = Array.isArray(contextRecord.sourceSeqs) ? contextRecord.sourceSeqs.filter((value) => typeof value === "number") : [];
46
- const record = `# Current State\n\nFaux Goal work completed.\n\n# Observations\n\n${JSON.stringify(handoff.observations ?? [])}\n\n# Work Completed\n\n${JSON.stringify(handoff.results ?? [])}\n\n# Decisions\n\nRecord the scripted result from ${request.wake.id}.\n\n# Blockers\n\n${String(handoff.blocker ?? "None.")}\n\n# Next Steps\n\n${JSON.stringify(handoff.nextSteps ?? [])}\n`;
47
- faux.setResponses([
48
- fauxAssistantMessage(fauxToolCall("work_record_update", { expectedRevision: Number(current.recordRevision ?? 0), content: record, reason: "record faux Goal progress", evidence: evidence.length ? [Math.max(...evidence)] : [] }), { stopReason: "toolUse" }),
49
- fauxAssistantMessage(fauxToolCall("handoff", handoff), { stopReason: "toolUse" }),
50
- ]);
49
+ const outcome = ["progress", "waiting", "blocked", "completion_proposed"].includes(String(handoff.outcome)) ? String(handoff.outcome) : "progress";
50
+ const handoffEvidence = Array.isArray(handoff.evidence) && handoff.evidence.every((value) => typeof value === "number") ? handoff.evidence : evidence.length ? [Math.max(...evidence)] : [];
51
+ const record = `# Current State\n\nFaux Goal outcome: ${outcome}.\n\n# Observations\n\nSee the cited Ledger evidence.\n\n# Work Completed\n\nExecuted the scripted faux Goal turn.\n\n# Decisions\n\nRecord the scripted result from ${request.execution.id}.\n\n# Blockers\n\n${outcome === "blocked" ? "Blocked." : "None."}\n\n# Next Steps\n\nFollow the current Goal and explicit tools.\n`;
52
+ const finalResponse = fauxAssistantMessage([fauxText(String(handoff.message ?? `Goal work recorded with outcome ${outcome}.`)), fauxToolCall("handoff", { outcome, evidence: handoffEvidence })], { stopReason: "toolUse" });
53
+ const recordResponse = fauxAssistantMessage(fauxToolCall("work_record_update", { expectedRevision: Number(current.recordRevision ?? 0), content: record, reason: "record faux Goal progress", evidence: evidence.length ? [Math.max(...evidence)] : [] }), { stopReason: "toolUse" });
54
+ const fatalBatch = fauxAssistantMessage([fauxText("Stale completion attempt."), fauxToolCall("handoff", { outcome, evidence: handoffEvidence }), fauxToolCall("write", { path: "fatal.txt", content: "must not be written" })], { stopReason: "toolUse" });
55
+ faux.setResponses(process.env.GOAH_PI_FAUX_FATAL_BATCH === "1" ? [fatalBatch] : process.env.GOAH_PI_FAUX_HANDOFF_REPAIR === "1" ? [fauxAssistantMessage([fauxText("Premature completion attempt."), fauxToolCall("handoff", { outcome, evidence: handoffEvidence })], { stopReason: "toolUse" }), recordResponse, finalResponse] : [recordResponse, finalResponse]);
51
56
  }
52
57
  else if (!goalState.bound) {
53
58
  faux.setResponses([fauxAssistantMessage([fauxText(process.env.GOAH_PI_FAUX_RESPONSE ?? "Hello from Goah.")])]);
54
59
  }
55
60
  else {
56
61
  const handoff = JSON.parse(process.env.GOAH_PI_FAUX_HANDOFF ?? "{}");
57
- faux.setResponses([fauxAssistantMessage(fauxToolCall("handoff", handoff), { stopReason: "toolUse" })]);
62
+ const { message, ...handoffData } = handoff;
63
+ faux.setResponses([fauxAssistantMessage([fauxText(String(message ?? "Goal work recorded.")), fauxToolCall("handoff", handoffData)], { stopReason: "toolUse" })]);
58
64
  }
59
65
  }
60
66
  let output = null;
67
+ let acceptedHandoff = null;
68
+ let revokedReason = "";
61
69
  let response = "";
62
70
  let responseFailure = "";
63
71
  let compactions = 0;
@@ -80,15 +88,19 @@ export async function runPiWorker() {
80
88
  : undefined;
81
89
  if (contextRecord.workRecord && typeof contextRecord.workRecord === "object" && !Array.isArray(contextRecord.workRecord) && typeof contextRecord.workRecord.recordRevision === "number")
82
90
  goalState.recordRevision = contextRecord.workRecord.recordRevision;
83
- const tools = createTools(root, (value) => { output = value; }, rpc, request.wake.startedAt, capabilities, goalState);
91
+ const tools = createTools(root, (value) => { if (!acceptedHandoff || !sameHandoff(acceptedHandoff.handoff, value))
92
+ throw new Error("Handoff tool executed without accepted validation"); output = { validationAttemptId: acceptedHandoff.attemptId, validationToken: acceptedHandoff.token, handoff: value }; }, rpc, capabilities, goalState, protectedPaths);
84
93
  const contextPolicy = resolveContextPolicy(model.contextWindow, process.env);
85
- emit({ type: "session.started", data: { formatVersion: SESSION_FORMAT_VERSION, provider, model: modelId, runner: "pi", contextWindowTokens: model.contextWindow, maxOutputTokensPerTurn: model.maxTokens } });
94
+ emit({ type: "transcript.started", data: { formatVersion: TRANSCRIPT_FORMAT_VERSION, provider, model: modelId, runner: "pi", contextWindowTokens: model.contextWindow, maxOutputTokensPerTurn: model.maxTokens } });
86
95
  const suppliedPrompt = typeof contextRecord.systemPrompt === "string" ? contextRecord.systemPrompt : undefined;
87
96
  const activeContext = typeof contextRecord.text === "string" ? contextRecord.text : JSON.stringify(request.context);
88
97
  const sourceSeqs = Array.isArray(contextRecord.sourceSeqs) ? contextRecord.sourceSeqs.filter((value) => Number.isInteger(value)) : [];
98
+ const basePrompt = process.env.GOAH_PI_SYSTEM_PROMPT ?? suppliedPrompt ?? (request.turn?.source.kind === "human" ? "You are Goah's primary Agent. Respond naturally to the Human." : `You are Goah Agent ${request.agent}. Inspect the supplied context and respond appropriately.`);
89
99
  const systemPrompt = goalState.bound
90
- ? `${process.env.GOAH_PI_SYSTEM_PROMPT ?? suppliedPrompt ?? "You are a goal-oriented worker."}\nYou must finish by calling handoff exactly once. Treat the supplied context as authoritative.`
91
- : `${process.env.GOAH_PI_SYSTEM_PROMPT ?? suppliedPrompt ?? "You are Goah's primary Agent."}\nRespond normally to the Human. Do not create a Goal for routine single-turn work. When the Human expresses a durable Goal, call create_goal; after create_goal, work_on_goal, or a Root resume succeeds with a Goal binding, update the Goal Work Record and finish by calling handoff. Treat the supplied context as authoritative.`;
100
+ ? `${basePrompt}\nThis Turn is Goal-bound. You must update its Work Record, write a concise human-readable assistant message describing what happened, and call handoff exactly once with only machine-readable outcome and evidence. The message and Handoff are separate outputs. Treat the supplied context as authoritative.`
101
+ : request.turn?.source.kind === "human"
102
+ ? `${basePrompt}\nThis Human Turn starts without a Goal binding. Finish with an ordinary response unless a Goal tool establishes a binding during this Turn; after a binding is established, update that Goal's Work Record, write a concise human-readable assistant message, and call handoff exactly once with separate machine-readable outcome and evidence. Treat the supplied context as authoritative.`
103
+ : `${basePrompt}\nThis Turn has no Goal binding. Finish with an ordinary response and do not call handoff. Treat the supplied context as authoritative.`;
92
104
  const agent = new Agent({
93
105
  initialState: {
94
106
  systemPrompt,
@@ -129,9 +141,33 @@ export async function runPiWorker() {
129
141
  }
130
142
  return view;
131
143
  },
132
- shouldStopAfterTurn: () => output !== null,
144
+ beforeToolCall: async ({ assistantMessage, toolCall, args }) => { if (revokedReason)
145
+ return { block: true, reason: `Turn authority was revoked: ${revokedReason}`, terminate: true }; if (toolCall.name !== "handoff")
146
+ return; acceptedHandoff = null; output = null; const handoff = args; try {
147
+ const result = await rpc("goal.handoff.validate", { handoff, candidateMessage: assistantResponseText(assistantMessage) });
148
+ if (result.accepted) {
149
+ acceptedHandoff = { attemptId: result.attemptId, token: result.token, handoff };
150
+ return;
151
+ }
152
+ const reason = result.issues.map((issue) => `- ${issue.message}`).join("\n");
153
+ if (result.fatal)
154
+ revokedReason = reason;
155
+ return { block: true, reason: `Handoff was not accepted:\n${reason}`, terminate: result.fatal };
156
+ }
157
+ catch (error) {
158
+ revokedReason = error instanceof Error ? error.message : String(error);
159
+ return { block: true, reason: `Handoff validation could not continue: ${revokedReason}`, terminate: true };
160
+ } },
161
+ shouldStopAfterTurn: () => output !== null || Boolean(revokedReason),
133
162
  toolExecution: "sequential",
134
163
  });
164
+ let acceptingSteering = true;
165
+ controls.onSteer((message) => {
166
+ if (!acceptingSteering)
167
+ return false;
168
+ agent.steer({ role: "user", content: message, timestamp: Date.now() });
169
+ return true;
170
+ });
135
171
  agent.subscribe((event) => {
136
172
  if (event.type === "turn_start")
137
173
  emit({ type: "turn.started", data: {} });
@@ -139,7 +175,7 @@ export async function runPiWorker() {
139
175
  const id = idFor(event.message);
140
176
  if (!emittedUsers.has(id)) {
141
177
  emittedUsers.add(id);
142
- emit({ type: "message.user", data: { message: sessionMessage(event.message, id) } });
178
+ emit({ type: "message.user", data: { message: transcriptMessage(event.message, id) } });
143
179
  }
144
180
  }
145
181
  else if (event.type === "message_update") {
@@ -149,7 +185,8 @@ export async function runPiWorker() {
149
185
  response = assistantResponseText(event.message);
150
186
  if ((event.message.stopReason === "error" || event.message.stopReason === "aborted") && event.message.errorMessage)
151
187
  responseFailure = event.message.errorMessage;
152
- emit({ type: "message.assistant.completed", data: { message: sessionMessage(event.message, idFor(event.message)) } });
188
+ const handoffIntent = hasAgentToolCall(event.message, "handoff");
189
+ emit({ type: "message.assistant.completed", data: { message: transcriptMessage(event.message, idFor(event.message)), ...(handoffIntent ? { completionIntent: "handoff", commitState: "provisional" } : { completionIntent: "ordinary", commitState: "committed" }) } });
153
190
  }
154
191
  else if (event.type === "tool_execution_start")
155
192
  emit({ type: "tool.called", data: { callId: event.toolCallId, name: event.toolName, arguments: JSON.parse(JSON.stringify(event.args)) } });
@@ -157,16 +194,26 @@ export async function runPiWorker() {
157
194
  emit({ type: "tool.completed", data: { callId: event.toolCallId, messageId: `tool:${event.toolCallId}`, name: event.toolName, result: JSON.parse(JSON.stringify(event.result)), isError: event.isError } });
158
195
  else if (event.type === "turn_end")
159
196
  emit({ type: "turn.completed", data: {} });
160
- else if (event.type === "agent_end")
161
- emit({ type: "session.completed", data: {} });
162
197
  });
163
- await agent.prompt(`Wake started at: ${request.wake.startedAt ?? "unknown"}\n\n${activeContext}\n\nRunner root: ${root}. Manage local files directly when the goal requires them.`);
198
+ const abortAgent = () => agent.abort();
199
+ process.once("SIGTERM", abortAgent);
200
+ process.once("SIGINT", abortAgent);
201
+ try {
202
+ await agent.prompt(`Turn started at: ${request.execution.startedAt}\n\n${activeContext}\n\nRunner root: ${root}. Manage local files directly when the goal requires them.`);
203
+ }
204
+ finally {
205
+ acceptingSteering = false;
206
+ process.off("SIGTERM", abortAgent);
207
+ process.off("SIGINT", abortAgent);
208
+ }
209
+ if (revokedReason)
210
+ return { outcome: "abnormal", reason: revokedReason };
164
211
  if (!goalState.bound)
165
212
  return response ? { outcome: "response", response: { content: response } } : { outcome: "abnormal", reason: responseFailure || "Pi worker exited without a response" };
166
213
  if (responseFailure)
167
214
  return { outcome: "abnormal", reason: responseFailure };
168
215
  if (!output)
169
- return { outcome: "abnormal", reason: "Pi worker exited without a valid handoff" };
216
+ return { outcome: "abnormal", reason: "Pi worker exited without an accepted Handoff" };
170
217
  return { outcome: "handoff", output };
171
218
  });
172
219
  }
@@ -185,32 +232,23 @@ function isJson(value) {
185
232
  return value.every(isJson);
186
233
  return Boolean(value && typeof value === "object" && Object.values(value).every(isJson));
187
234
  }
188
- function sessionMessage(message, id) {
235
+ function transcriptMessage(message, id) {
189
236
  const value = JSON.parse(JSON.stringify(message));
190
237
  const role = message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : "tool";
191
238
  return { id, role, content: (value.content ?? value), ...(value.usage !== undefined ? { usage: value.usage } : {}), ...(typeof value.stopReason === "string" ? { stopReason: value.stopReason } : {}), ...(typeof value.errorMessage === "string" ? { errorMessage: value.errorMessage } : {}) };
192
239
  }
193
- function createTools(root, handoff, rpc, wakeStartedAt, capabilities, goalState) {
240
+ function createTools(root, handoff, rpc, capabilities, goalState, protectedPaths = []) {
194
241
  const handoffTool = {
195
242
  name: "handoff",
196
243
  label: "Handoff",
197
- description: "Record a structured handoff and end the wake.",
244
+ description: "Record a structured handoff and end the Goal-bound Turn.",
198
245
  parameters: Type.Object({
199
- observations: Type.Optional(Type.Array(Type.String())),
200
- results: Type.Optional(Type.Array(Type.String())),
201
- nextSteps: Type.Optional(Type.Array(Type.String())),
202
- blocker: Type.Optional(Type.String()),
203
- material: Type.Optional(Type.Boolean()),
204
- outcome: Type.Optional(Type.Union([Type.Literal("progress"), Type.Literal("waiting"), Type.Literal("blocked"), Type.Literal("completion_proposed")])),
205
- evidence: Type.Optional(Type.Array(Type.Number())),
206
- nextWakeAt: Type.Optional(Type.String()),
246
+ outcome: Type.Union([Type.Literal("progress"), Type.Literal("waiting"), Type.Literal("blocked"), Type.Literal("completion_proposed")]),
247
+ evidence: Type.Array(Type.Number()),
207
248
  }),
208
249
  execute: async (_id, params) => {
209
250
  const input = params;
210
- const compact = goalState.binding && goalState.recordRevision !== undefined
211
- ? { goalId: goalState.binding.goalId, goalRevision: goalState.binding.goalRevision, recordRevision: goalState.recordRevision, outcome: input.outcome ?? (input.blocker ? "blocked" : input.material ? "completion_proposed" : "progress"), evidence: input.evidence ?? [] }
212
- : null;
213
- const value = { handoff: compact ?? { observations: input.observations ?? [], results: input.results ?? [], nextSteps: input.nextSteps ?? [], ...(input.blocker ? { blocker: input.blocker } : {}), ...(input.material === true ? { material: true } : {}) }, mail: [], nextWakeAt: validateNextWakeAt(input.nextWakeAt, wakeStartedAt) };
251
+ const value = { outcome: input.outcome, evidence: input.evidence };
214
252
  handoff(value);
215
253
  return { content: [{ type: "text", text: "handoff recorded" }], details: value, terminate: true };
216
254
  },
@@ -229,19 +267,19 @@ function createTools(root, handoff, rpc, wakeStartedAt, capabilities, goalState)
229
267
  const readTool = {
230
268
  name: "read", label: "Read", description: "Read a UTF-8 file inside the current working directory.",
231
269
  parameters: Type.Object({ path: Type.String() }),
232
- execute: async (_id, params) => { const input = params; return { content: [{ type: "text", text: await readFile(scoped(root, input.path), "utf8") }], details: {} }; },
270
+ execute: async (_id, params) => { const input = params; return { content: [{ type: "text", text: await readFile(scopedRunnerPath(root, input.path, protectedPaths), "utf8") }], details: {} }; },
233
271
  };
234
272
  const writeTool = {
235
273
  name: "write", label: "Write", description: "Create or replace a UTF-8 file inside the current working directory.",
236
274
  parameters: Type.Object({ path: Type.String(), content: Type.String() }),
237
- execute: async (_id, params) => { const input = params; const path = scoped(root, input.path); await mkdir(dirname(path), { recursive: true }); await writeFile(path, input.content); return { content: [{ type: "text", text: "written" }], details: {} }; },
275
+ execute: async (_id, params) => { const input = params; const path = scopedRunnerPath(root, input.path, protectedPaths); await mkdir(dirname(path), { recursive: true }); await writeFile(path, input.content); return { content: [{ type: "text", text: "written" }], details: {} }; },
238
276
  };
239
277
  const editTool = {
240
278
  name: "edit", label: "Edit", description: "Replace one exact text occurrence in a UTF-8 file inside the current working directory.",
241
279
  parameters: Type.Object({ path: Type.String(), oldText: Type.String(), newText: Type.String() }),
242
280
  execute: async (_id, params) => {
243
281
  const input = params;
244
- const path = scoped(root, input.path);
282
+ const path = scopedRunnerPath(root, input.path, protectedPaths);
245
283
  const source = await readFile(path, "utf8");
246
284
  const first = source.indexOf(input.oldText);
247
285
  if (first < 0)
@@ -254,9 +292,9 @@ function createTools(root, handoff, rpc, wakeStartedAt, capabilities, goalState)
254
292
  };
255
293
  const bashTool = {
256
294
  name: "bash", label: "Bash",
257
- description: "Run a shell command inside the local runner root. The command's process group is killed after the timeout; declare timeoutMs explicitly for builds, installs, or deployment waits that need longer.",
295
+ description: "Run a sandboxed shell command inside the local runner root. Goah credential/control state is masked. If protected state shares the root, use the write tool to create new top-level paths. The command's process group is killed after timeout.",
258
296
  parameters: Type.Object({ command: Type.String(), timeoutMs: Type.Optional(Type.Number()) }), executionMode: "sequential",
259
- execute: async (_id, params, signal) => runBashCommand(root, params, signal),
297
+ execute: async (_id, params, signal) => runBashCommand(root, params, signal, protectedPaths),
260
298
  };
261
299
  return [readTool, writeTool, editTool, bashTool, ...rpcTools, handoffTool];
262
300
  }
@@ -267,23 +305,42 @@ export function bashTimeoutMs(requested, env = process.env) {
267
305
  return Number.isFinite(value) && value > 0 ? Math.min(value, BASH_TIMEOUT_HARD_CAP_MS) : fallback;
268
306
  }
269
307
  /** Shell execution with a process-group timeout: a hung command becomes a model-visible tool error instead of a stalled wake. */
270
- export async function runBashCommand(root, input, signal) {
308
+ export async function runBashCommand(root, input, signal, protectedPaths = []) {
271
309
  const timeoutMs = bashTimeoutMs(input.timeoutMs);
272
- const child = spawn("/bin/sh", ["-lc", input.command], { cwd: root, env: toolEnvironment(), detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
310
+ const sandboxTemp = mkdtempSync(join(tmpdir(), "goah-bash-"));
311
+ const launch = sandboxedShell(input.command, root, protectedPaths, sandboxTemp);
312
+ if (!launch) {
313
+ if (sandboxTemp)
314
+ rmSync(sandboxTemp, { recursive: true, force: true });
315
+ return { content: [{ type: "text", text: "Bash is unavailable because this platform cannot isolate Goah credential and control state." }], details: { command: input.command }, isError: true };
316
+ }
317
+ const child = spawn(launch.command, launch.args, { cwd: root, env: toolEnvironment(sandboxTemp), detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
273
318
  let stdout = "";
274
319
  let stderr = "";
275
320
  let timedOut = false;
276
321
  let outputOverflow = false;
277
- child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); if (stdout.length > 1_000_000)
278
- outputOverflow = true; });
279
- child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); if (stderr.length > 1_000_000)
280
- outputOverflow = true; });
281
322
  const killGroup = () => { if (child.pid) {
282
323
  try {
283
324
  process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
284
325
  }
285
326
  catch { }
286
327
  } };
328
+ const appendOutput = (channel, chunk) => {
329
+ if (outputOverflow)
330
+ return;
331
+ const text = chunk.toString();
332
+ const remaining = Math.max(0, 1_000_000 - stdout.length - stderr.length);
333
+ if (channel === "stdout")
334
+ stdout += text.slice(0, remaining);
335
+ else
336
+ stderr += text.slice(0, remaining);
337
+ if (text.length > remaining) {
338
+ outputOverflow = true;
339
+ killGroup();
340
+ }
341
+ };
342
+ child.stdout?.on("data", (chunk) => appendOutput("stdout", chunk));
343
+ child.stderr?.on("data", (chunk) => appendOutput("stderr", chunk));
287
344
  const timer = setTimeout(() => { timedOut = true; killGroup(); }, timeoutMs);
288
345
  const onAbort = () => killGroup();
289
346
  signal?.addEventListener("abort", onAbort, { once: true });
@@ -292,29 +349,22 @@ export async function runBashCommand(root, input, signal) {
292
349
  child.once("close", (code, signalName) => resolve({ code, signal: signalName }));
293
350
  });
294
351
  try {
295
- if (outputOverflow)
296
- killGroup();
297
352
  const result = await close;
298
353
  if (timedOut)
299
354
  return { content: [{ type: "text", text: `Command timed out after ${timeoutMs}ms and its process group was killed. Declare a larger timeoutMs for long-running commands.` }], details: { command: input.command, timedOutAfterMs: timeoutMs }, isError: true };
300
355
  if (signal?.aborted)
301
356
  return { content: [{ type: "text", text: "Command aborted with the wake." }], details: { command: input.command }, isError: true };
302
- return { content: [{ type: "text", text: `${stdout}${stderr}`.slice(-50_000) }], details: { command: input.command, exitCode: result.code, signal: result.signal, ...(outputOverflow ? { outputOverflow: true } : {}) }, ...(outputOverflow ? { isError: true } : {}) };
357
+ killGroup();
358
+ const failed = outputOverflow || result.code !== 0 || result.signal !== null;
359
+ return { content: [{ type: "text", text: `${stdout}${stderr}`.slice(-50_000) }], details: { command: input.command, exitCode: result.code, signal: result.signal, ...(outputOverflow ? { outputOverflow: true } : {}) }, ...(failed ? { isError: true } : {}) };
303
360
  }
304
361
  finally {
305
362
  clearTimeout(timer);
306
363
  signal?.removeEventListener("abort", onAbort);
364
+ if (sandboxTemp)
365
+ rmSync(sandboxTemp, { recursive: true, force: true });
307
366
  }
308
367
  }
309
- export function validateNextWakeAt(value, wakeStartedAt) {
310
- if (value === undefined)
311
- return null;
312
- const next = Date.parse(value);
313
- const started = wakeStartedAt === null ? Number.NaN : Date.parse(wakeStartedAt);
314
- if (!Number.isFinite(next) || !Number.isFinite(started) || next <= started)
315
- throw new Error("nextWakeAt must be a valid time later than the current wake start");
316
- return new Date(next).toISOString();
317
- }
318
368
  export function resolveContextPolicy(contextWindowTokens, env) {
319
369
  if (!Number.isInteger(contextWindowTokens) || contextWindowTokens <= 0)
320
370
  throw new Error("model context window is missing");
@@ -356,17 +406,14 @@ function createRpcTools(rpc, allowed, onResult) {
356
406
  const definitions = [
357
407
  ["ledger.search", tool("ledger_search", "Search durable ledger facts.", "ledger.search", Type.Object({ query: Type.String(), limit: Type.Optional(Type.Number()) }))],
358
408
  ["memory.append", tool("memory_append", "Append a durable working-memory note that is injected into your future wakes. Record procedural knowledge, active hypotheses, and abandoned approaches with the reason; keep notes concise.", "memory.append", Type.Object({ note: Type.String() }))],
359
- ["mail.send", tool("send_mail", "Send a durable message to another agent or human.", "mail.send", Type.Object({ to: Type.String(), level: Type.Union([Type.Literal("fyi"), Type.Literal("decision"), Type.Literal("emergency")]), body: Type.Any() }))],
409
+ ["mail.send", tool("send_mail", "Send durable Mail to another Goal owner. goalId is required and routes the Mail into that owner's Goal Turn. Human communication must use request_human.", "mail.send", Type.Object({ to: Type.String(), goalId: Type.String(), level: Type.Union([Type.Literal("fyi"), Type.Literal("decision"), Type.Literal("emergency")]), body: Type.Any() }))],
360
410
  ["schedule.set", tool("schedule_wake", "Schedule this agent's next wake.", "schedule.set", Type.Object({ at: Type.String(), reason: Type.String() }))],
361
- ["action.submit", tool("submit_action", "Submit a gated external action with evidence.", "action.submit", Type.Object({ id: Type.String(), kind: Type.String(), connector: Type.String(), payload: Type.Any(), reason: Type.String(), evidence: Type.Array(Type.Number()) }))],
362
- ["audit.ack", tool("ack_audit_advice", "Acknowledge audit advice after incorporating it.", "audit.ack", Type.Object({ actionId: Type.String() }))],
363
- ["audit.write", tool("write_audit_advice", "Write audit advice for an action.", "audit.write", Type.Object({ actionId: Type.String(), body: Type.Any(), evidence: Type.Array(Type.Number()) }))],
364
411
  ["team.list", tool("team_list", "Read the ledger-derived team roster and liveness state.", "team.list", Type.Object({}))],
365
412
  ["goal.get", tool("get_goal", "Read the Goal bound to this Turn, or null when the Turn is unbound.", "goal.get", Type.Object({}))],
366
413
  ["goal.create", tool("create_goal", "Create a Root Goal from durable Human intent and bind this Turn. Do not use for greetings, questions, or routine single-turn work.", "goal.create", Type.Object({ objective: Type.String(), id: Type.Optional(Type.String()) }))],
367
414
  ["goal.work", tool("work_on_goal", "Bind this Human Turn to an existing active Goal owned by this Agent.", "goal.work", Type.Object({ goalId: Type.String() }))],
368
- ["goal.delegate", tool("delegate_goal", "Atomically create a Child Goal with observation and verification methods, its Work Record, decision brief, and initial Wake.", "goal.delegate", Type.Object({ id: Type.String(), parentGoalId: Type.String(), childGoal: Type.Object({ id: Type.String(), objective: Type.String(), observationMethod: Type.String(), verificationMethod: Type.String(), owner: Type.String() }), brief: Type.Any(), reason: Type.String(), evidence: Type.Array(Type.Number()) }))],
369
- ["goal.reassign", tool("reassign_goal", "Atomically transfer a child goal, notify both owners, and queue the new owner.", "goal.reassign", Type.Object({ id: Type.String(), goalId: Type.String(), newOwner: Type.String(), brief: Type.Any(), reason: Type.String(), evidence: Type.Array(Type.Number()) }))],
415
+ ["goal.delegate", tool("delegate_goal", "Atomically create a Child Goal with observation and verification methods, its Work Record, decision brief, and initial Wake.", "goal.delegate", Type.Object({ id: Type.String(), parentGoalId: Type.String(), expectedParentRevision: Type.Number(), childGoal: Type.Object({ id: Type.String(), objective: Type.String(), observationMethod: Type.String(), verificationMethod: Type.String(), owner: Type.String() }), brief: Type.Any(), reason: Type.String(), evidence: Type.Array(Type.Number()) }))],
416
+ ["goal.reassign", tool("reassign_goal", "Atomically transfer a child goal, notify both owners, and queue the new owner.", "goal.reassign", Type.Object({ id: Type.String(), goalId: Type.String(), expectedGoalRevision: Type.Number(), newOwner: Type.String(), brief: Type.Any(), reason: Type.String(), evidence: Type.Array(Type.Number()) }))],
370
417
  ["goal.revise", tool("revise_goal", "Revise a Child Goal objective, observation method, and verification method as one new revision.", "goal.revise", Type.Object({ goalId: Type.String(), objective: Type.String(), observationMethod: Type.String(), verificationMethod: Type.String(), reason: Type.String(), evidence: Type.Array(Type.Number()) }))],
371
418
  ["goal.pause", tool("pause_goal", "Pause a child goal and suppress queued motion.", "goal.pause", Type.Object({ goalId: Type.String() }))],
372
419
  ["goal.resume", tool("resume_goal", "Resume a Goal. A direct Human Root resume binds the current Turn; Child Goal resume requires an already bound parent Turn.", "goal.resume", Type.Object({ goalId: Type.String() }))],
@@ -378,23 +425,125 @@ function createRpcTools(rpc, allowed, onResult) {
378
425
  ["work_record.diff", tool("work_record_diff", "Compare two revisions of a Goal Work Record.", "work_record.diff", Type.Object({ goalId: Type.Optional(Type.String()), fromRevision: Type.Number(), toRevision: Type.Number() }))],
379
426
  ["work_record.search", tool("work_record_search", "Search every Goal Work Record.", "work_record.search", Type.Object({ query: Type.String(), limit: Type.Optional(Type.Number()) }))],
380
427
  ["work_record.update", tool("work_record_update", "Create the next version of the Work Record bound to this Turn with Ledger evidence.", "work_record.update", Type.Object({ expectedRevision: Type.Number(), content: Type.String(), reason: Type.String(), evidence: Type.Array(Type.Number()) }))],
381
- ["goal.put", tool("put_goal", "Create or update a goal using parent-layer authority.", "goal.put", Type.Object({ goal: Type.Any() }))],
428
+ ["goal.put", tool("put_goal", "Create or update a Goal through the authoritative goal.changed protocol using parent-layer authority.", "goal.put", Type.Object({ goal: Type.Any(), reason: Type.String(), evidence: Type.Array(Type.Number()) }))],
382
429
  ];
383
430
  return definitions.filter(([capability]) => !allowed || allowed.has(capability)).map(([, value]) => value);
384
431
  }
385
- function scoped(root, path) {
386
- const resolved = resolve(root, path);
387
- if (resolved !== root && !resolved.startsWith(`${root}${sep}`))
432
+ export function scopedRunnerPath(root, path, protectedPaths = []) {
433
+ const resolved = canonicalPath(resolve(root, path));
434
+ const canonicalRoot = canonicalPath(root);
435
+ if (resolved !== canonicalRoot && !resolved.startsWith(`${canonicalRoot}${sep}`))
388
436
  throw new Error("path escapes runner root");
437
+ if (protectedPaths.map(canonicalPath).some((privatePath) => resolved === privatePath || resolved.startsWith(`${privatePath}${sep}`)))
438
+ throw new Error("path is protected Goah state");
389
439
  return resolved;
390
440
  }
391
- function toolEnvironment() {
441
+ function sandboxedShell(command, root, protectedPaths, sandboxTemp) {
442
+ if (process.platform === "darwin" && existsSync("/usr/bin/sandbox-exec")) {
443
+ const workspacePaths = sandboxWorkspacePaths(canonicalPath(root), protectedPaths.map(canonicalPath));
444
+ const workspaceAncestors = pathAncestors(canonicalPath(root));
445
+ const toolchains = toolchainReadPaths(canonicalPath(root));
446
+ const readableAncestors = toolchains.flatMap(pathAncestors);
447
+ const readable = ["/System", "/usr", "/bin", "/sbin", "/Library", "/etc", "/dev", "/private/etc", "/private/var/select", "/Applications", ...toolchains, ...workspacePaths, ...(sandboxTemp ? [sandboxTemp] : [])].filter(existsSync).map(canonicalPath);
448
+ const writable = [...workspacePaths, ...(sandboxTemp ? [sandboxTemp] : []), "/dev"].map(canonicalPath);
449
+ const rules = ["(version 1)", "(deny default)", "(import \"system.sb\")", "(allow process*)", "(allow signal)", "(allow network*)", "(allow sysctl-read)", "(allow mach-lookup)", ...[...new Set([...workspaceAncestors, ...readableAncestors])].map((path) => `(allow file-read* (literal ${JSON.stringify(path)}))`), ...readable.map((path) => `(allow file-read* (subpath ${JSON.stringify(path)}))`), ...writable.map((path) => `(allow file-write* (subpath ${JSON.stringify(path)}))`), ...protectedPaths.map(canonicalPath).map((path) => `(deny file-read* file-write* (subpath ${JSON.stringify(path)}))`)];
450
+ return { command: "/usr/bin/sandbox-exec", args: ["-p", rules.join(" "), "/bin/sh", "-lc", command] };
451
+ }
452
+ if (process.platform === "linux") {
453
+ const bwrap = ["/usr/bin/bwrap", "/bin/bwrap"].find(existsSync);
454
+ if (bwrap) {
455
+ const canonicalRoot = canonicalPath(root);
456
+ if (["/usr", "/bin", "/sbin", "/lib", "/lib64", "/etc"].some((path) => canonicalRoot === path || canonicalRoot.startsWith(`${path}${sep}`)))
457
+ return null;
458
+ return { command: bwrap, args: linuxSandboxArgs(command, canonicalRoot, protectedPaths, sandboxTemp) };
459
+ }
460
+ }
461
+ return null;
462
+ }
463
+ export function linuxSandboxArgs(command, root, protectedPaths, sandboxTemp) {
464
+ const canonicalRoot = canonicalPath(root);
465
+ const workspacePaths = sandboxWorkspacePaths(canonicalRoot, protectedPaths.map(canonicalPath));
466
+ const systemPaths = ["/usr", "/bin", "/sbin", "/lib", "/lib64", "/etc"].filter(existsSync).flatMap((path) => ["--ro-bind", path, path]);
467
+ const mounted = ["/tmp", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/etc"];
468
+ const toolchains = toolchainReadPaths(canonicalRoot).filter((path) => !mounted.some((base) => path === base || path.startsWith(`${base}${sep}`)) && path !== canonicalRoot && !path.startsWith(`${canonicalRoot}${sep}`));
469
+ const destinations = [canonicalRoot, ...toolchains].flatMap((path) => pathAncestors(path).slice(1)).filter((path) => !mounted.some((base) => path === base || path.startsWith(`${base}${sep}`)));
470
+ const directories = [...new Set(destinations)].sort((left, right) => left.length - right.length).flatMap((path) => ["--dir", path]);
471
+ return ["--die-with-parent", "--unshare-all", "--share-net", ...systemPaths, "--dev", "/dev", "--proc", "/proc", "--tmpfs", "/tmp", ...(sandboxTemp ? ["--dir", canonicalPath(sandboxTemp)] : []), ...directories, ...toolchains.flatMap((path) => ["--ro-bind", path, path]), ...workspacePaths.flatMap((path) => ["--bind", path, path]), "--chdir", canonicalRoot, "/bin/sh", "-lc", command];
472
+ }
473
+ function toolchainReadPaths(root) {
474
+ const executable = canonicalPath(process.execPath);
475
+ const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean).map((path) => resolve(path));
476
+ const prefixes = ["/opt/homebrew", "/usr/local"].filter((prefix) => executable.startsWith(`${prefix}${sep}`) || pathEntries.some((path) => path.startsWith(`${prefix}${sep}`)));
477
+ const home = canonicalPath(homedir());
478
+ const managedHomeRuntime = [join(home, ".nvm", "versions", "node"), join(home, ".asdf", "installs", "node"), join(home, ".volta", "tools", "image", "node")].find((prefix) => executable.startsWith(`${prefix}${sep}`));
479
+ const runtimeRoot = prefixes.length ? [] : managedHomeRuntime ? [executable.slice(0, executable.indexOf(`${sep}bin${sep}`, managedHomeRuntime.length))] : [dirname(executable)];
480
+ return [...new Set([...prefixes, ...runtimeRoot])].filter((path) => path !== root && !path.startsWith(`${root}${sep}`));
481
+ }
482
+ export function sandboxWorkspacePaths(root, protectedPaths) {
483
+ if (!protectedPaths.some((path) => path === root || path.startsWith(`${root}${sep}`)))
484
+ return [root];
485
+ return [...new Set(readdirSync(root).flatMap((name) => { try {
486
+ return [canonicalPath(join(root, name))];
487
+ }
488
+ catch {
489
+ return [];
490
+ } }))]
491
+ .filter((candidate) => candidate !== root && candidate.startsWith(`${root}${sep}`))
492
+ .filter((candidate) => !protectedPaths.some((path) => path === candidate || path.startsWith(`${candidate}${sep}`) || candidate.startsWith(`${path}${sep}`)));
493
+ }
494
+ function canonicalPath(path) {
495
+ let current = resolve(path);
496
+ const suffix = [];
497
+ while (!existsSync(current)) {
498
+ const parent = dirname(current);
499
+ if (parent === current)
500
+ break;
501
+ suffix.unshift(current.slice(parent.length + 1));
502
+ current = parent;
503
+ }
504
+ return resolve(realpathSync(current), ...suffix);
505
+ }
506
+ function pathAncestors(path) {
507
+ const values = [];
508
+ let current = resolve(path);
509
+ while (true) {
510
+ values.unshift(current);
511
+ const parent = dirname(current);
512
+ if (parent === current)
513
+ return values;
514
+ current = parent;
515
+ }
516
+ }
517
+ function toolEnvironment(sandboxTemp = null) {
392
518
  const env = {};
393
519
  for (const name of ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "SHELL", "TERM", "USER"])
394
520
  if (process.env[name] !== undefined)
395
521
  env[name] = process.env[name];
522
+ if (sandboxTemp) {
523
+ env.TMPDIR = sandboxTemp;
524
+ env.TMP = sandboxTemp;
525
+ env.TEMP = sandboxTemp;
526
+ }
527
+ if (process.platform === "darwin") {
528
+ const developer = ["/Library/Developer/CommandLineTools", "/Applications/Xcode.app/Contents/Developer"].find(existsSync);
529
+ if (developer)
530
+ env.DEVELOPER_DIR = developer;
531
+ }
532
+ Object.assign(env, gitIdentityEnvironment());
396
533
  return env;
397
534
  }
535
+ function gitIdentityEnvironment() {
536
+ try {
537
+ const source = readFileSync(join(homedir(), ".gitconfig"), "utf8");
538
+ const user = source.match(/^\[user\]\s*\n((?:[ \t].*(?:\n|$))*)/m)?.[1] ?? "";
539
+ const name = user.match(/^\s*name\s*=\s*(.+)$/m)?.[1]?.trim();
540
+ const email = user.match(/^\s*email\s*=\s*(.+)$/m)?.[1]?.trim();
541
+ return { ...(name ? { GIT_AUTHOR_NAME: name, GIT_COMMITTER_NAME: name } : {}), ...(email ? { GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_EMAIL: email } : {}), GIT_CONFIG_GLOBAL: "/dev/null" };
542
+ }
543
+ catch {
544
+ return { GIT_CONFIG_GLOBAL: "/dev/null" };
545
+ }
546
+ }
398
547
  function estimateMessages(messages) { return Math.ceil(JSON.stringify(messages).length / 4); }
399
548
  function integerSetting(value, fallback) {
400
549
  if (value === undefined)
@@ -423,6 +572,8 @@ export function assistantResponseText(message) {
423
572
  .join("\n")
424
573
  .trim();
425
574
  }
575
+ function sameHandoff(left, right) { return left.outcome === right.outcome && left.evidence.length === right.evidence.length && left.evidence.every((value, index) => value === right.evidence[index]); }
576
+ function hasAgentToolCall(message, name) { return message.role === "assistant" && message.content.some((item) => item.type === "toolCall" && item.name === name); }
426
577
  if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)))
427
578
  await runPiWorker();
428
579
  //# sourceMappingURL=pi-worker.js.map