pi-forge 1.2.3 → 1.2.5

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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/client/assets/{CodeMirrorEditor-1gu-DS9k.js → CodeMirrorEditor-DXmxwE2Z.js} +2 -2
  3. package/dist/client/assets/{CodeMirrorEditor-1gu-DS9k.js.map → CodeMirrorEditor-DXmxwE2Z.js.map} +1 -1
  4. package/dist/client/assets/index-CMSjnWtF.js +365 -0
  5. package/dist/client/assets/index-CMSjnWtF.js.map +1 -0
  6. package/dist/client/assets/index-Cp8qEy7Q.css +1 -0
  7. package/dist/client/index.html +2 -2
  8. package/dist/client/sw.js +1 -1
  9. package/dist/client/sw.js.map +1 -1
  10. package/dist/server/agent-extensions/compaction-continuation.js +65 -0
  11. package/dist/server/agent-extensions/compaction-continuation.js.map +1 -0
  12. package/dist/server/agent-resource-loader.js +10 -0
  13. package/dist/server/agent-resource-loader.js.map +1 -1
  14. package/dist/server/ask-user-question/envelope.js +56 -0
  15. package/dist/server/ask-user-question/envelope.js.map +1 -0
  16. package/dist/server/ask-user-question/prompt-strings.js +44 -0
  17. package/dist/server/ask-user-question/prompt-strings.js.map +1 -0
  18. package/dist/server/ask-user-question/registry.js +157 -0
  19. package/dist/server/ask-user-question/registry.js.map +1 -0
  20. package/dist/server/ask-user-question/tool.js +115 -0
  21. package/dist/server/ask-user-question/tool.js.map +1 -0
  22. package/dist/server/ask-user-question/types.js +27 -0
  23. package/dist/server/ask-user-question/types.js.map +1 -0
  24. package/dist/server/ask-user-question/validate.js +135 -0
  25. package/dist/server/ask-user-question/validate.js.map +1 -0
  26. package/dist/server/config.js +10 -0
  27. package/dist/server/config.js.map +1 -1
  28. package/dist/server/index.js +16 -0
  29. package/dist/server/index.js.map +1 -1
  30. package/dist/server/mcp/tool-bridge.js +14 -8
  31. package/dist/server/mcp/tool-bridge.js.map +1 -1
  32. package/dist/server/processes/envelope.js +60 -0
  33. package/dist/server/processes/envelope.js.map +1 -0
  34. package/dist/server/processes/log-store.js +132 -0
  35. package/dist/server/processes/log-store.js.map +1 -0
  36. package/dist/server/processes/manager.js +348 -0
  37. package/dist/server/processes/manager.js.map +1 -0
  38. package/dist/server/processes/prompt-strings.js +43 -0
  39. package/dist/server/processes/prompt-strings.js.map +1 -0
  40. package/dist/server/processes/tool.js +273 -0
  41. package/dist/server/processes/tool.js.map +1 -0
  42. package/dist/server/processes/types.js +21 -0
  43. package/dist/server/processes/types.js.map +1 -0
  44. package/dist/server/processes/watches.js +59 -0
  45. package/dist/server/processes/watches.js.map +1 -0
  46. package/dist/server/quick-actions.js +141 -0
  47. package/dist/server/quick-actions.js.map +1 -0
  48. package/dist/server/routes/ask-user-question.js +129 -0
  49. package/dist/server/routes/ask-user-question.js.map +1 -0
  50. package/dist/server/routes/config.js +12 -0
  51. package/dist/server/routes/config.js.map +1 -1
  52. package/dist/server/routes/processes.js +228 -0
  53. package/dist/server/routes/processes.js.map +1 -0
  54. package/dist/server/routes/quick-actions.js +384 -0
  55. package/dist/server/routes/quick-actions.js.map +1 -0
  56. package/dist/server/routes/todos.js +67 -0
  57. package/dist/server/routes/todos.js.map +1 -0
  58. package/dist/server/session-registry.js +72 -4
  59. package/dist/server/session-registry.js.map +1 -1
  60. package/dist/server/sse-bridge.js +225 -4
  61. package/dist/server/sse-bridge.js.map +1 -1
  62. package/dist/server/todo/envelope.js +87 -0
  63. package/dist/server/todo/envelope.js.map +1 -0
  64. package/dist/server/todo/invariants.js +21 -0
  65. package/dist/server/todo/invariants.js.map +1 -0
  66. package/dist/server/todo/prompt-strings.js +29 -0
  67. package/dist/server/todo/prompt-strings.js.map +1 -0
  68. package/dist/server/todo/reducer.js +189 -0
  69. package/dist/server/todo/reducer.js.map +1 -0
  70. package/dist/server/todo/replay.js +45 -0
  71. package/dist/server/todo/replay.js.map +1 -0
  72. package/dist/server/todo/store.js +92 -0
  73. package/dist/server/todo/store.js.map +1 -0
  74. package/dist/server/todo/task-graph.js +60 -0
  75. package/dist/server/todo/task-graph.js.map +1 -0
  76. package/dist/server/todo/tool.js +95 -0
  77. package/dist/server/todo/tool.js.map +1 -0
  78. package/dist/server/todo/types.js +23 -0
  79. package/dist/server/todo/types.js.map +1 -0
  80. package/package.json +1 -1
  81. package/dist/client/assets/index-BxZV6ddv.js +0 -359
  82. package/dist/client/assets/index-BxZV6ddv.js.map +0 -1
  83. package/dist/client/assets/index-KUhxvBxw.css +0 -1
@@ -0,0 +1,384 @@
1
+ import { spawn } from "node:child_process";
2
+ import { config } from "../config.js";
3
+ import { getProject } from "../project-manager.js";
4
+ import { scrubbedEnv } from "../pty-manager.js";
5
+ import { createQuickAction, DEFAULT_TIMEOUT_MS, deleteQuickAction, getQuickAction, isCommandAction, isPromptAction, MAX_COMMAND_BYTES, MAX_PROMPT_BYTES, MAX_TIMEOUT_MS, QuickActionNotFoundError, readQuickActions, updateQuickAction, } from "../quick-actions.js";
6
+ import { errorSchema } from "./_schemas.js";
7
+ /**
8
+ * Wire shape — same on the way in (POST/PUT body) and out
9
+ * (GET response). Discriminator is presence of `command` vs `text`;
10
+ * the validator below rejects both/neither with 400.
11
+ */
12
+ const quickActionSchema = {
13
+ type: "object",
14
+ required: ["id", "name"],
15
+ properties: {
16
+ id: { type: "string" },
17
+ name: { type: "string" },
18
+ enabled: { type: "boolean" },
19
+ command: { type: "string" },
20
+ timeoutMs: { type: "integer" },
21
+ text: { type: "string" },
22
+ mode: { type: "string", enum: ["send", "insert"] },
23
+ },
24
+ };
25
+ const runResultSchema = {
26
+ type: "object",
27
+ required: ["success", "exitCode", "stdout", "stderr", "durationMs", "timedOut", "truncated"],
28
+ properties: {
29
+ success: { type: "boolean" },
30
+ exitCode: { type: ["integer", "null"] },
31
+ stdout: { type: "string" },
32
+ stderr: { type: "string" },
33
+ durationMs: { type: "integer" },
34
+ timedOut: { type: "boolean" },
35
+ truncated: { type: "boolean" },
36
+ },
37
+ };
38
+ /** Per-stream output cap. A chip that prints 100 MB of node_modules
39
+ * paths shouldn't drag the request handler down — truncate and flag
40
+ * it; the user can re-run in the integrated terminal if they need the
41
+ * full output. */
42
+ const MAX_OUTPUT_BYTES = 1_000_000;
43
+ /**
44
+ * Normalise + validate one wire body into a QuickAction-shaped patch.
45
+ * Centralises the one-of discriminator + byte caps so POST and PUT
46
+ * both produce identical 400s on the same bad input.
47
+ */
48
+ function validateBody(body, reply) {
49
+ const trimmedName = body.name.trim();
50
+ if (trimmedName.length === 0) {
51
+ reply.code(400).send({ error: "invalid_name" });
52
+ return undefined;
53
+ }
54
+ const hasCmd = typeof body.command === "string" && body.command.length > 0;
55
+ const hasText = typeof body.text === "string" && body.text.length > 0;
56
+ if (hasCmd === hasText) {
57
+ reply.code(400).send({
58
+ error: "invalid_action",
59
+ message: "exactly one of `command` or `text` is required",
60
+ });
61
+ return undefined;
62
+ }
63
+ const out = { name: trimmedName };
64
+ if (body.enabled !== undefined)
65
+ out.enabled = body.enabled;
66
+ if (hasCmd) {
67
+ const cmd = body.command;
68
+ if (Buffer.byteLength(cmd, "utf8") > MAX_COMMAND_BYTES) {
69
+ reply.code(400).send({ error: "command_too_large" });
70
+ return undefined;
71
+ }
72
+ out.command = cmd;
73
+ if (body.timeoutMs !== undefined) {
74
+ const t = body.timeoutMs;
75
+ if (!Number.isInteger(t) || t <= 0) {
76
+ reply.code(400).send({ error: "invalid_timeout" });
77
+ return undefined;
78
+ }
79
+ out.timeoutMs = Math.min(t, MAX_TIMEOUT_MS);
80
+ }
81
+ }
82
+ else {
83
+ const text = body.text;
84
+ if (Buffer.byteLength(text, "utf8") > MAX_PROMPT_BYTES) {
85
+ reply.code(400).send({ error: "prompt_too_large" });
86
+ return undefined;
87
+ }
88
+ out.text = text;
89
+ out.mode = body.mode ?? "send";
90
+ }
91
+ return out;
92
+ }
93
+ function handleError(reply, err) {
94
+ if (err instanceof QuickActionNotFoundError) {
95
+ return reply.code(404).send({ error: "action_not_found" });
96
+ }
97
+ reply.log.error({ err }, "quick-actions route error");
98
+ return reply.code(500).send({ error: "internal_error" });
99
+ }
100
+ /**
101
+ * Spawn the command under `/bin/sh -c` with a scrubbed env (same
102
+ * posture as the integrated terminal — no pi-forge / provider
103
+ * secrets leak into chip output). stdout and stderr are captured
104
+ * separately so the chat card can render them in distinct blocks.
105
+ */
106
+ async function runCommand(command, cwd, timeoutMs) {
107
+ return new Promise((resolve) => {
108
+ const proc = spawn("/bin/sh", ["-c", command], {
109
+ cwd,
110
+ env: scrubbedEnv(),
111
+ stdio: ["ignore", "pipe", "pipe"],
112
+ });
113
+ const stdoutChunks = [];
114
+ const stderrChunks = [];
115
+ let stdoutLen = 0;
116
+ let stderrLen = 0;
117
+ let truncated = false;
118
+ let timedOut = false;
119
+ proc.stdout?.on("data", (chunk) => {
120
+ const remaining = MAX_OUTPUT_BYTES - stdoutLen;
121
+ if (remaining <= 0) {
122
+ truncated = true;
123
+ return;
124
+ }
125
+ if (chunk.length > remaining) {
126
+ stdoutChunks.push(chunk.subarray(0, remaining));
127
+ stdoutLen += remaining;
128
+ truncated = true;
129
+ }
130
+ else {
131
+ stdoutChunks.push(chunk);
132
+ stdoutLen += chunk.length;
133
+ }
134
+ });
135
+ proc.stderr?.on("data", (chunk) => {
136
+ const remaining = MAX_OUTPUT_BYTES - stderrLen;
137
+ if (remaining <= 0) {
138
+ truncated = true;
139
+ return;
140
+ }
141
+ if (chunk.length > remaining) {
142
+ stderrChunks.push(chunk.subarray(0, remaining));
143
+ stderrLen += remaining;
144
+ truncated = true;
145
+ }
146
+ else {
147
+ stderrChunks.push(chunk);
148
+ stderrLen += chunk.length;
149
+ }
150
+ });
151
+ const timer = setTimeout(() => {
152
+ timedOut = true;
153
+ try {
154
+ proc.kill("SIGTERM");
155
+ }
156
+ catch {
157
+ // best-effort
158
+ }
159
+ // SIGKILL grace — match the exec-route convention.
160
+ setTimeout(() => {
161
+ try {
162
+ proc.kill("SIGKILL");
163
+ }
164
+ catch {
165
+ // best-effort
166
+ }
167
+ }, 2000);
168
+ }, timeoutMs);
169
+ proc.on("error", (err) => {
170
+ clearTimeout(timer);
171
+ // Spawn error (ENOENT on `/bin/sh`, etc.) — surface as a failed
172
+ // run with the error on stderr so the chat card shows it.
173
+ resolve({
174
+ exitCode: null,
175
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
176
+ stderr: err.message,
177
+ timedOut,
178
+ truncated,
179
+ });
180
+ });
181
+ proc.on("close", (code) => {
182
+ clearTimeout(timer);
183
+ resolve({
184
+ exitCode: code,
185
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
186
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
187
+ timedOut,
188
+ truncated,
189
+ });
190
+ });
191
+ });
192
+ }
193
+ export const quickActionRoutes = async (fastify) => {
194
+ fastify.get("/quick-actions", {
195
+ schema: {
196
+ description: "List all quick-action chips. Global (not per-project) — chips " +
197
+ "are operator-personal, same install-private rationale as the " +
198
+ "other forge-owned config files.",
199
+ tags: ["config"],
200
+ response: {
201
+ 200: {
202
+ type: "object",
203
+ required: ["actions"],
204
+ properties: {
205
+ actions: { type: "array", items: quickActionSchema },
206
+ },
207
+ },
208
+ },
209
+ },
210
+ }, async () => ({ actions: await readQuickActions() }));
211
+ fastify.post("/quick-actions", {
212
+ schema: {
213
+ description: "Create a quick-action chip. Exactly one of `command` or `text` " +
214
+ "must be set — presence is the kind discriminator (no `kind` " +
215
+ "field on the wire, matches the MCP server-config convention). " +
216
+ "400 on both/neither, 400 on byte-cap overflow.",
217
+ tags: ["config"],
218
+ body: {
219
+ type: "object",
220
+ required: ["name"],
221
+ additionalProperties: false,
222
+ properties: {
223
+ name: { type: "string", minLength: 1, maxLength: 200 },
224
+ enabled: { type: "boolean" },
225
+ command: { type: "string" },
226
+ timeoutMs: { type: "integer", minimum: 1 },
227
+ text: { type: "string" },
228
+ mode: { type: "string", enum: ["send", "insert"] },
229
+ },
230
+ },
231
+ response: {
232
+ 201: quickActionSchema,
233
+ 400: errorSchema,
234
+ },
235
+ },
236
+ }, async (req, reply) => {
237
+ const validated = validateBody(req.body, reply);
238
+ if (validated === undefined)
239
+ return reply;
240
+ try {
241
+ const created = await createQuickAction(validated);
242
+ return reply.code(201).send(created);
243
+ }
244
+ catch (err) {
245
+ return handleError(reply, err);
246
+ }
247
+ });
248
+ fastify.put("/quick-actions/:id", {
249
+ schema: {
250
+ description: "Replace a quick-action chip. Full record on the body — switching " +
251
+ "kind (command ↔ prompt) drops the now-unused fields. Same " +
252
+ "validation rules as POST.",
253
+ tags: ["config"],
254
+ params: {
255
+ type: "object",
256
+ required: ["id"],
257
+ properties: { id: { type: "string" } },
258
+ },
259
+ body: {
260
+ type: "object",
261
+ required: ["name"],
262
+ additionalProperties: false,
263
+ properties: {
264
+ name: { type: "string", minLength: 1, maxLength: 200 },
265
+ enabled: { type: "boolean" },
266
+ command: { type: "string" },
267
+ timeoutMs: { type: "integer", minimum: 1 },
268
+ text: { type: "string" },
269
+ mode: { type: "string", enum: ["send", "insert"] },
270
+ },
271
+ },
272
+ response: {
273
+ 200: quickActionSchema,
274
+ 400: errorSchema,
275
+ 404: errorSchema,
276
+ },
277
+ },
278
+ }, async (req, reply) => {
279
+ const validated = validateBody(req.body, reply);
280
+ if (validated === undefined)
281
+ return reply;
282
+ try {
283
+ const updated = await updateQuickAction(req.params.id, validated);
284
+ return updated;
285
+ }
286
+ catch (err) {
287
+ return handleError(reply, err);
288
+ }
289
+ });
290
+ fastify.delete("/quick-actions/:id", {
291
+ schema: {
292
+ description: "Delete a quick-action chip.",
293
+ tags: ["config"],
294
+ params: {
295
+ type: "object",
296
+ required: ["id"],
297
+ properties: { id: { type: "string" } },
298
+ },
299
+ response: {
300
+ 204: { type: "null" },
301
+ 404: errorSchema,
302
+ },
303
+ },
304
+ }, async (req, reply) => {
305
+ try {
306
+ await deleteQuickAction(req.params.id);
307
+ return reply.code(204).send();
308
+ }
309
+ catch (err) {
310
+ return handleError(reply, err);
311
+ }
312
+ });
313
+ fastify.post("/quick-actions/:id/run", {
314
+ schema: {
315
+ description: "Execute a command-kind quick action in the named project's " +
316
+ "cwd. Returns captured stdout/stderr and the exit code. The " +
317
+ "spawned shell inherits a SCRUBBED env (same as the " +
318
+ "integrated terminal — no pi-forge or provider secrets). " +
319
+ "Hard-gated under MINIMAL_UI: command runs return 403 " +
320
+ "`command_actions_disabled_in_minimal` regardless of who " +
321
+ "is calling. Prompt-kind actions are not executable here " +
322
+ "(420-style validation error) — those are a pure client " +
323
+ "concern that route through the composer or sendPrompt.",
324
+ tags: ["config"],
325
+ params: {
326
+ type: "object",
327
+ required: ["id"],
328
+ properties: { id: { type: "string" } },
329
+ },
330
+ body: {
331
+ type: "object",
332
+ required: ["projectId"],
333
+ additionalProperties: false,
334
+ properties: {
335
+ projectId: { type: "string", minLength: 1 },
336
+ },
337
+ },
338
+ response: {
339
+ 200: runResultSchema,
340
+ 400: errorSchema,
341
+ 403: errorSchema,
342
+ 404: errorSchema,
343
+ },
344
+ },
345
+ }, async (req, reply) => {
346
+ // Defense in depth — the client also hides command chips
347
+ // under MINIMAL_UI, but a stale tab or scripted caller could
348
+ // still POST here. Refuse at the route.
349
+ if (config.minimalUi) {
350
+ return reply.code(403).send({ error: "command_actions_disabled_in_minimal" });
351
+ }
352
+ const action = await getQuickAction(req.params.id);
353
+ if (action === undefined) {
354
+ return reply.code(404).send({ error: "action_not_found" });
355
+ }
356
+ if (isPromptAction(action)) {
357
+ return reply.code(400).send({
358
+ error: "not_a_command_action",
359
+ message: "prompt actions are dispatched client-side, not via this route",
360
+ });
361
+ }
362
+ if (!isCommandAction(action)) {
363
+ return reply.code(400).send({ error: "invalid_action" });
364
+ }
365
+ const project = await getProject(req.body.projectId);
366
+ if (project === undefined) {
367
+ return reply.code(404).send({ error: "project_not_found" });
368
+ }
369
+ const timeoutMs = Math.min(action.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
370
+ const started = Date.now();
371
+ const result = await runCommand(action.command, project.path, timeoutMs);
372
+ const durationMs = Date.now() - started;
373
+ return {
374
+ success: result.exitCode === 0 && !result.timedOut,
375
+ exitCode: result.exitCode,
376
+ stdout: result.stdout,
377
+ stderr: result.stderr,
378
+ durationMs,
379
+ timedOut: result.timedOut,
380
+ truncated: result.truncated,
381
+ };
382
+ });
383
+ };
384
+ //# sourceMappingURL=quick-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quick-actions.js","sourceRoot":"","sources":["../../src/routes/quick-actions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import { getSession } from "../session-registry.js";
2
+ import { getState } from "../todo/store.js";
3
+ import { errorSchema } from "./_schemas.js";
4
+ /**
5
+ * GET /sessions/:id/todos
6
+ *
7
+ * Returns the current todo state for a session. Used by the UI
8
+ * panel as the initial fetch when an SSE snapshot hasn't landed
9
+ * yet (cold load, cross-tab join). The store's `getState` is
10
+ * cache-first with replay-on-miss, so this is honest after a
11
+ * server restart even if the cache is empty.
12
+ *
13
+ * The wire shape mirrors the SSE `todo_update` event so the
14
+ * client can use one normaliser for both paths.
15
+ */
16
+ const taskSchema = {
17
+ type: "object",
18
+ required: ["id", "subject", "status"],
19
+ additionalProperties: true,
20
+ properties: {
21
+ id: { type: "integer" },
22
+ subject: { type: "string" },
23
+ description: { type: "string" },
24
+ activeForm: { type: "string" },
25
+ status: { type: "string", enum: ["pending", "in_progress", "completed", "deleted"] },
26
+ blockedBy: { type: "array", items: { type: "integer" } },
27
+ owner: { type: "string" },
28
+ metadata: { type: "object", additionalProperties: true },
29
+ },
30
+ };
31
+ export const todoRoutes = async (fastify) => {
32
+ fastify.get("/sessions/:id/todos", {
33
+ schema: {
34
+ description: "Return the current todo list for this session. Cache-first; " +
35
+ "rebuilds from the session branch on cache miss so a server " +
36
+ "restart doesn't lie about state.",
37
+ tags: ["sessions"],
38
+ params: {
39
+ type: "object",
40
+ required: ["id"],
41
+ properties: { id: { type: "string" } },
42
+ },
43
+ response: {
44
+ 200: {
45
+ type: "object",
46
+ required: ["tasks", "nextId"],
47
+ properties: {
48
+ tasks: { type: "array", items: taskSchema },
49
+ nextId: { type: "integer" },
50
+ },
51
+ },
52
+ 404: errorSchema,
53
+ },
54
+ },
55
+ }, async (req, reply) => {
56
+ const live = getSession(req.params.id);
57
+ if (live === undefined) {
58
+ return reply.code(404).send({ error: "session_not_found" });
59
+ }
60
+ // AgentSession.sessionManager is the public accessor (see
61
+ // agent-session.d.ts). getState is cache-first with replay-
62
+ // on-miss, so this stays honest after a server restart.
63
+ const state = getState(req.params.id, live.session.sessionManager);
64
+ return { tasks: state.tasks, nextId: state.nextId };
65
+ });
66
+ };
67
+ //# sourceMappingURL=todos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todos.js","sourceRoot":"","sources":["../../src/routes/todos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,GAAG;IACjB,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC;IACrC,oBAAoB,EAAE,IAAI;IAC1B,UAAU,EAAE;QACV,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;QACvB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC3B,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC/B,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC9B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE;QACpF,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE;QACxD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACzB,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,IAAI,EAAE;KACzD;CACO,CAAC;AAEX,MAAM,CAAC,MAAM,UAAU,GAAuB,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9D,OAAO,CAAC,GAAG,CACT,qBAAqB,EACrB;QACE,MAAM,EAAE;YACN,WAAW,EACT,8DAA8D;gBAC9D,6DAA6D;gBAC7D,kCAAkC;YACpC,IAAI,EAAE,CAAC,UAAU,CAAC;YAClB,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,QAAQ,EAAE,CAAC,IAAI,CAAC;gBAChB,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;aACvC;YACD,QAAQ,EAAE;gBACR,GAAG,EAAE;oBACH,IAAI,EAAE,QAAQ;oBACd,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC;oBAC7B,UAAU,EAAE;wBACV,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE;wBAC3C,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;qBAC5B;iBACF;gBACD,GAAG,EAAE,WAAW;aACjB;SACF;KACF,EACD,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QACnB,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;QAC9D,CAAC;QACD,0DAA0D;QAC1D,4DAA4D;QAC5D,wDAAwD;QACxD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACnE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IACtD,CAAC,CACF,CAAC;AACJ,CAAC,CAAC"}
@@ -9,6 +9,11 @@ import { readProjects } from "./project-manager.js";
9
9
  import { filterEnabledTools, readToolOverrides } from "./tool-overrides.js";
10
10
  import { discoverExtensionResources } from "./extensions-discovery.js";
11
11
  import { customToolsForProject as mcpCustomToolsForProject, ensureProjectLoaded as mcpEnsureProjectLoaded, isGloballyEnabled as mcpIsGloballyEnabled, } from "./mcp/manager.js";
12
+ import { createAskUserQuestionTool } from "./ask-user-question/tool.js";
13
+ import { createTodoTool } from "./todo/tool.js";
14
+ import { clearForSession as clearTodoForSession, refreshFromBranch as refreshTodoFromBranch, } from "./todo/store.js";
15
+ import { createProcessTool } from "./processes/tool.js";
16
+ import { processManager } from "./processes/manager.js";
12
17
  export class SessionNotFoundError extends Error {
13
18
  constructor(id) {
14
19
  super(`session not found: ${id}`);
@@ -52,6 +57,21 @@ export const BUILTIN_TOOL_NAMES = [
52
57
  "grep",
53
58
  "find",
54
59
  "ls",
60
+ // ask_user_question is implemented in pi-forge (see ask-user-question/),
61
+ // not in the pi SDK. Listed here so the Tools settings tab surfaces it
62
+ // under "Built-in tools" — disabling it filters it out of the allowlist
63
+ // passed to createAgentSession, so the agent never sees the tool.
64
+ "ask_user_question",
65
+ // todo is implemented in pi-forge (see todo/), contract-compatible with
66
+ // @juicesharp/rpiv-todo. Same disable-via-settings semantics as
67
+ // ask_user_question.
68
+ "todo",
69
+ // process is implemented in pi-forge (see processes/), contract-
70
+ // compatible with @aliou/pi-processes. Manages background processes
71
+ // the agent spawns (dev servers, watchers, etc.) — separate spawn
72
+ // surface from bash, with lifecycle management + log capture +
73
+ // regex watches. Disable here to filter out of the allowlist.
74
+ "process",
55
75
  ];
56
76
  /**
57
77
  * Build the `tools` allowlist passed to `createAgentSession` for this
@@ -247,7 +267,15 @@ export async function createSession(projectId, workspacePath) {
247
267
  // agentDir IS passed: without it, the SDK falls back to ~/.pi/agent and
248
268
  // ignores PI_CONFIG_DIR entirely, breaking auth.json/models.json wiring
249
269
  // for Phase 6's prompt route.
250
- const customTools = await resolveMcpCustomTools(projectId, workspacePath);
270
+ const mcpTools = await resolveMcpCustomTools(projectId, workspacePath);
271
+ // SessionManager.getSessionId() is synchronous and stable from
272
+ // create() onward — read it BEFORE createAgentSession so the
273
+ // forge-native ask_user_question tool can bind to the right
274
+ // session in its execute() closure.
275
+ const askTool = createAskUserQuestionTool(sessionManager.getSessionId());
276
+ const todoTool = createTodoTool(sessionManager.getSessionId(), sessionManager);
277
+ const processTool = createProcessTool(sessionManager.getSessionId(), workspacePath);
278
+ const customTools = [...mcpTools, askTool, todoTool, processTool];
251
279
  const settingsManager = await buildSessionSettingsManager(workspacePath, projectId);
252
280
  const resourceLoader = await buildForgeResourceLoader(workspacePath, config.piConfigDir, settingsManager, projectId);
253
281
  const { session } = await createAgentSession({
@@ -426,7 +454,15 @@ export async function resumeSession(sessionId, projectId, workspacePath) {
426
454
  // collapses to the project session dir.
427
455
  const childSessionDir = match.parentSessionId !== undefined ? join(match.path, "..") : dir;
428
456
  const sessionManager = SessionManager.open(match.path, childSessionDir, workspacePath);
429
- const customTools = await resolveMcpCustomTools(projectId, workspacePath);
457
+ const mcpTools = await resolveMcpCustomTools(projectId, workspacePath);
458
+ const askTool = createAskUserQuestionTool(sessionManager.getSessionId());
459
+ const todoTool = createTodoTool(sessionManager.getSessionId(), sessionManager);
460
+ const processTool = createProcessTool(sessionManager.getSessionId(), workspacePath);
461
+ const customTools = [...mcpTools, askTool, todoTool, processTool];
462
+ // Resumed session — refresh the todo cache from the branch so
463
+ // the UI panel sees the persisted state on first SSE connect,
464
+ // not an empty list.
465
+ refreshTodoFromBranch(sessionManager.getSessionId(), sessionManager);
430
466
  const settingsManager = await buildSessionSettingsManager(workspacePath, projectId);
431
467
  const resourceLoader = await buildForgeResourceLoader(workspacePath, config.piConfigDir, settingsManager, projectId);
432
468
  const { session } = await createAgentSession({
@@ -604,6 +640,17 @@ export async function disposeSession(sessionId) {
604
640
  catch {
605
641
  // ignore — SDK doesn't currently throw, but H2-defensive
606
642
  }
643
+ // Drop the todo cache for this session. The cache is a fast
644
+ // path; even without this, a future session with the same id
645
+ // would recover via replay-on-miss. Cleaner to be explicit.
646
+ clearTodoForSession(sessionId);
647
+ // Terminate every live process owned by this session (SIGTERM,
648
+ // brief grace, SIGKILL) and remove the per-session log dir.
649
+ // Best-effort + bounded — disposeSession itself can take up to
650
+ // GRACE_MS but no longer; the outer disposeAllSessions caller
651
+ // doesn't block on this beyond its own ABORT_TIMEOUT_MS race
652
+ // because we await it last.
653
+ await processManager.disposeSession(sessionId).catch(() => undefined);
607
654
  }
608
655
  finally {
609
656
  registry.delete(sessionId);
@@ -1015,7 +1062,15 @@ async function forkSessionLocked(sessionId, entryId) {
1015
1062
  throw new Error("fork_failed");
1016
1063
  const dir = sessionDirFor(source.projectId);
1017
1064
  const sessionManager = SessionManager.open(newPath, dir, source.workspacePath);
1018
- const customTools = await resolveMcpCustomTools(source.projectId, source.workspacePath);
1065
+ const mcpTools = await resolveMcpCustomTools(source.projectId, source.workspacePath);
1066
+ const askTool = createAskUserQuestionTool(sessionManager.getSessionId());
1067
+ const todoTool = createTodoTool(sessionManager.getSessionId(), sessionManager);
1068
+ const processTool = createProcessTool(sessionManager.getSessionId(), source.workspacePath);
1069
+ const customTools = [...mcpTools, askTool, todoTool, processTool];
1070
+ // Forked session — replay the branch (which now belongs to the
1071
+ // fork) so the new session's todo cache reflects the inherited
1072
+ // state, not the parent's stale entry.
1073
+ refreshTodoFromBranch(sessionManager.getSessionId(), sessionManager);
1019
1074
  const settingsManager = await buildSessionSettingsManager(source.workspacePath, source.projectId);
1020
1075
  const resourceLoader = await buildForgeResourceLoader(source.workspacePath, config.piConfigDir, settingsManager, source.projectId);
1021
1076
  const { session } = await createAgentSession({
@@ -1079,7 +1134,20 @@ async function forkSessionLocked(sessionId, entryId) {
1079
1134
  try {
1080
1135
  source.unsubscribe();
1081
1136
  const restoredManager = SessionManager.open(originalSourceFile, dir, source.workspacePath);
1082
- const restoredCustomTools = await resolveMcpCustomTools(source.projectId, source.workspacePath);
1137
+ const restoredMcpTools = await resolveMcpCustomTools(source.projectId, source.workspacePath);
1138
+ const restoredAskTool = createAskUserQuestionTool(restoredManager.getSessionId());
1139
+ const restoredTodoTool = createTodoTool(restoredManager.getSessionId(), restoredManager);
1140
+ const restoredProcessTool = createProcessTool(restoredManager.getSessionId(), source.workspacePath);
1141
+ const restoredCustomTools = [
1142
+ ...restoredMcpTools,
1143
+ restoredAskTool,
1144
+ restoredTodoTool,
1145
+ restoredProcessTool,
1146
+ ];
1147
+ // Re-derive the original source's todo cache from the (now
1148
+ // un-mutated) source JSONL — the SDK's fork machinery left
1149
+ // the cache pointing at fork state.
1150
+ refreshTodoFromBranch(restoredManager.getSessionId(), restoredManager);
1083
1151
  const restoredSettingsManager = await buildSessionSettingsManager(source.workspacePath, source.projectId);
1084
1152
  const restoredResourceLoader = await buildForgeResourceLoader(source.workspacePath, config.piConfigDir, restoredSettingsManager, source.projectId);
1085
1153
  const { session: restoredSession } = await createAgentSession({