taskplane 0.24.22 → 0.24.24
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.
- package/extensions/taskplane/agent-bridge-extension.ts +196 -3
- package/extensions/taskplane/engine-worker.ts +4 -0
- package/extensions/taskplane/engine.ts +884 -12
- package/extensions/taskplane/execution.ts +12 -1
- package/extensions/taskplane/extension.ts +4 -0
- package/extensions/taskplane/lane-runner.ts +5 -0
- package/extensions/taskplane/merge.ts +6 -9
- package/extensions/taskplane/persistence.ts +12 -0
- package/extensions/taskplane/resume.ts +132 -22
- package/extensions/taskplane/types.ts +81 -16
- package/package.json +1 -1
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
* Tools:
|
|
9
9
|
* - notify_supervisor: send a reply or acknowledgment to supervisor
|
|
10
10
|
* - escalate_to_supervisor: escalate a blocker or ambiguity
|
|
11
|
+
* - request_segment_expansion: request runtime segment expansion via file IPC
|
|
11
12
|
*
|
|
12
13
|
* This extension is intentionally minimal and protocol-focused.
|
|
13
14
|
* It does NOT own:
|
|
14
|
-
* - review_step (deferred to TP-105+ lane-runner bridge work)
|
|
15
15
|
* - wait_for_review (deferred to persistent reviewer work)
|
|
16
|
-
* - request_segment_expansion (deferred to TP-086)
|
|
17
16
|
*
|
|
18
17
|
* File I/O only — writes to the agent's outbox directory.
|
|
19
18
|
* The lane-runner or engine polls outbox and surfaces to supervisor.
|
|
@@ -28,6 +27,7 @@ import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, unlinkS
|
|
|
28
27
|
import { join, dirname } from "path";
|
|
29
28
|
import { spawn as nodeSpawn } from "child_process";
|
|
30
29
|
import { randomBytes } from "crypto";
|
|
30
|
+
import { buildExpansionRequestId, type SegmentExpansionRequest } from "./types.ts";
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Resolve the outbox directory from environment variables.
|
|
@@ -36,7 +36,15 @@ import { randomBytes } from "crypto";
|
|
|
36
36
|
* with the bridge extension. Falls back to .pi/bridge-outbox/ in cwd.
|
|
37
37
|
*/
|
|
38
38
|
function resolveOutboxDir(): string {
|
|
39
|
-
|
|
39
|
+
if (process.env.TASKPLANE_OUTBOX_DIR) return process.env.TASKPLANE_OUTBOX_DIR;
|
|
40
|
+
|
|
41
|
+
const batchId = process.env.ORCH_BATCH_ID;
|
|
42
|
+
const agentId = process.env.TASKPLANE_AGENT_ID;
|
|
43
|
+
if (batchId && agentId) {
|
|
44
|
+
return join(process.cwd(), ".pi", "mailbox", batchId, agentId, "outbox");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return join(process.cwd(), ".pi", "bridge-outbox");
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
@@ -75,6 +83,53 @@ function writeOutbox(type: "reply" | "escalate", content: string, replyTo?: stri
|
|
|
75
83
|
return { id };
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
const REPO_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
87
|
+
const AUTONOMY_PATTERN = /^(interactive|supervised|autonomous)$/;
|
|
88
|
+
|
|
89
|
+
function resolveActiveSegmentId(): string | null {
|
|
90
|
+
const raw = (process.env.TASKPLANE_ACTIVE_SEGMENT_ID || process.env.TASKPLANE_SEGMENT_ID || "").trim();
|
|
91
|
+
if (!raw || raw === "null" || raw === "(none / whole-task execution)") return null;
|
|
92
|
+
return raw;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveTaskId(fromSegmentId: string): string {
|
|
96
|
+
const envTaskId = process.env.TASKPLANE_TASK_ID?.trim();
|
|
97
|
+
if (envTaskId) return envTaskId;
|
|
98
|
+
const idx = fromSegmentId.indexOf("::");
|
|
99
|
+
if (idx > 0) return fromSegmentId.slice(0, idx);
|
|
100
|
+
const folder = process.env.TASKPLANE_TASK_FOLDER || "";
|
|
101
|
+
const name = folder.split(/[\\/]/).filter(Boolean).at(-1) || "";
|
|
102
|
+
const match = name.match(/^[A-Z]+-\d+/);
|
|
103
|
+
return match ? match[0] : "unknown";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveSupervisorAutonomy(): "interactive" | "supervised" | "autonomous" {
|
|
107
|
+
const value = (process.env.TASKPLANE_SUPERVISOR_AUTONOMY || "autonomous").trim().toLowerCase();
|
|
108
|
+
if (AUTONOMY_PATTERN.test(value)) {
|
|
109
|
+
return value as "interactive" | "supervised" | "autonomous";
|
|
110
|
+
}
|
|
111
|
+
return "autonomous";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeSegmentExpansionRequest(request: SegmentExpansionRequest): string {
|
|
115
|
+
const outboxDir = resolveOutboxDir();
|
|
116
|
+
mkdirSync(outboxDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
const filename = `segment-expansion-${request.requestId}.json`;
|
|
119
|
+
const finalPath = join(outboxDir, filename);
|
|
120
|
+
const tempPath = `${finalPath}.tmp`;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
writeFileSync(tempPath, JSON.stringify(request, null, 2) + "\n", "utf-8");
|
|
124
|
+
renameSync(tempPath, finalPath);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
try { if (existsSync(tempPath)) unlinkSync(tempPath); } catch { /* cleanup */ }
|
|
127
|
+
throw new Error(`Failed to write segment expansion request: ${err instanceof Error ? err.message : String(err)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return finalPath;
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
export default function (pi: ExtensionAPI) {
|
|
79
134
|
pi.registerTool({
|
|
80
135
|
name: "notify_supervisor",
|
|
@@ -158,6 +213,144 @@ export default function (pi: ExtensionAPI) {
|
|
|
158
213
|
},
|
|
159
214
|
});
|
|
160
215
|
|
|
216
|
+
const activeSegmentId = resolveActiveSegmentId();
|
|
217
|
+
if (activeSegmentId) {
|
|
218
|
+
/**
|
|
219
|
+
* Worker RPC for requesting runtime segment expansion.
|
|
220
|
+
*
|
|
221
|
+
* Contract summary:
|
|
222
|
+
* - accepts requested repo IDs + rationale (+ optional placement/edges)
|
|
223
|
+
* - validates request shape and repo ID rules
|
|
224
|
+
* - writes `.pi/mailbox/{batchId}/{agentId}/outbox/segment-expansion-{requestId}.json`
|
|
225
|
+
* - returns acknowledgment payload (`accepted`, `requestId`, `message`)
|
|
226
|
+
*/
|
|
227
|
+
pi.registerTool({
|
|
228
|
+
name: "request_segment_expansion",
|
|
229
|
+
label: "Request Segment Expansion",
|
|
230
|
+
description:
|
|
231
|
+
"Request additional repository segments for the current task at runtime. " +
|
|
232
|
+
"Writes a request file to the worker outbox for engine processing.",
|
|
233
|
+
promptSnippet:
|
|
234
|
+
"request_segment_expansion(requestedRepoIds, rationale, placement?, edges?)",
|
|
235
|
+
promptGuidelines: [
|
|
236
|
+
"Use this when runtime discovery reveals additional repos are needed.",
|
|
237
|
+
"Do not wait for approval; continue current segment work after requesting.",
|
|
238
|
+
"requestedRepoIds must be non-empty, unique, and match /^[a-z0-9][a-z0-9._-]*$/.",
|
|
239
|
+
"In supervised/interactive autonomy, this tool returns accepted: false (V1 guard).",
|
|
240
|
+
],
|
|
241
|
+
parameters: Type.Object({
|
|
242
|
+
requestedRepoIds: Type.Array(Type.String({ description: "Repo ID to add" }), {
|
|
243
|
+
description: "Repo IDs to add as new segments",
|
|
244
|
+
}),
|
|
245
|
+
rationale: Type.String({
|
|
246
|
+
description: "Why these repos are needed",
|
|
247
|
+
}),
|
|
248
|
+
placement: Type.Optional(Type.Union([
|
|
249
|
+
Type.Literal("after-current"),
|
|
250
|
+
Type.Literal("end"),
|
|
251
|
+
], {
|
|
252
|
+
description: "Where to place new segments: after-current (default) or end",
|
|
253
|
+
})),
|
|
254
|
+
edges: Type.Optional(Type.Array(Type.Object({
|
|
255
|
+
from: Type.String({ description: "Source repo ID" }),
|
|
256
|
+
to: Type.String({ description: "Destination repo ID" }),
|
|
257
|
+
}), {
|
|
258
|
+
description: "Optional ordering edges between requested repos",
|
|
259
|
+
})),
|
|
260
|
+
}),
|
|
261
|
+
async execute(_toolCallId, params) {
|
|
262
|
+
const autonomy = resolveSupervisorAutonomy();
|
|
263
|
+
if (autonomy !== "autonomous") {
|
|
264
|
+
const rejected = {
|
|
265
|
+
accepted: false,
|
|
266
|
+
requestId: null,
|
|
267
|
+
message: "Segment expansion requires autonomous supervisor mode",
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: "text" as const, text: JSON.stringify(rejected) }],
|
|
271
|
+
details: rejected,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const requestedRepoIds = Array.isArray(params.requestedRepoIds)
|
|
276
|
+
? params.requestedRepoIds.map((id) => String(id).trim()).filter(Boolean)
|
|
277
|
+
: [];
|
|
278
|
+
const rejections: Array<{ repoId: string; reason: string }> = [];
|
|
279
|
+
|
|
280
|
+
if (requestedRepoIds.length === 0) {
|
|
281
|
+
rejections.push({ repoId: "", reason: "requestedRepoIds must be a non-empty array" });
|
|
282
|
+
} else {
|
|
283
|
+
const seen = new Set<string>();
|
|
284
|
+
for (const repoId of requestedRepoIds) {
|
|
285
|
+
if (!REPO_ID_PATTERN.test(repoId)) {
|
|
286
|
+
rejections.push({ repoId, reason: "invalid repo ID format" });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (seen.has(repoId)) {
|
|
290
|
+
rejections.push({ repoId, reason: "duplicate repo ID in request" });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
seen.add(repoId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (rejections.length > 0) {
|
|
298
|
+
const rejected = {
|
|
299
|
+
accepted: false,
|
|
300
|
+
requestId: null,
|
|
301
|
+
message: "Segment expansion request rejected by tool validation",
|
|
302
|
+
rejections,
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: "text" as const, text: JSON.stringify(rejected) }],
|
|
306
|
+
details: rejected,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const requestId = buildExpansionRequestId();
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
const request: SegmentExpansionRequest = {
|
|
313
|
+
requestId,
|
|
314
|
+
taskId: resolveTaskId(activeSegmentId),
|
|
315
|
+
fromSegmentId: activeSegmentId as SegmentExpansionRequest["fromSegmentId"],
|
|
316
|
+
requestedRepoIds,
|
|
317
|
+
rationale: String(params.rationale ?? "").trim(),
|
|
318
|
+
placement: params.placement === "end" ? "end" : "after-current",
|
|
319
|
+
edges: Array.isArray(params.edges)
|
|
320
|
+
? params.edges
|
|
321
|
+
.filter((edge): edge is { from: string; to: string } => Boolean(edge && typeof edge.from === "string" && typeof edge.to === "string"))
|
|
322
|
+
.map((edge) => ({ from: edge.from.trim(), to: edge.to.trim() }))
|
|
323
|
+
.filter((edge) => edge.from.length > 0 && edge.to.length > 0)
|
|
324
|
+
: [],
|
|
325
|
+
timestamp: now,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
writeSegmentExpansionRequest(request);
|
|
330
|
+
const accepted = {
|
|
331
|
+
accepted: true,
|
|
332
|
+
requestId,
|
|
333
|
+
message: "Segment expansion request accepted",
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text" as const, text: JSON.stringify(accepted) }],
|
|
337
|
+
details: accepted,
|
|
338
|
+
};
|
|
339
|
+
} catch (err) {
|
|
340
|
+
const failed = {
|
|
341
|
+
accepted: false,
|
|
342
|
+
requestId: null,
|
|
343
|
+
message: err instanceof Error ? err.message : String(err),
|
|
344
|
+
};
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text" as const, text: JSON.stringify(failed) }],
|
|
347
|
+
details: failed,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
161
354
|
// ── review_step Tool (TP-117) ─────────────────────────────────────
|
|
162
355
|
// Spawns a reviewer subprocess to evaluate work at step boundaries.
|
|
163
356
|
// The reviewer runs as a separate Pi process, writes feedback to
|
|
@@ -107,6 +107,8 @@ export interface EngineWorkerData {
|
|
|
107
107
|
agentRoot?: string;
|
|
108
108
|
/** Force flag for resume */
|
|
109
109
|
force?: boolean;
|
|
110
|
+
/** Supervisor autonomy mode propagated to worker bridge tools. */
|
|
111
|
+
supervisorAutonomy?: "interactive" | "supervised" | "autonomous";
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
// ── Serialization helpers (used by both main thread and worker) ──────
|
|
@@ -332,6 +334,7 @@ if (process.env.TASKPLANE_ENGINE_FORK === "1" && typeof process.send === "functi
|
|
|
332
334
|
data.agentRoot,
|
|
333
335
|
data.force ?? false,
|
|
334
336
|
onSupervisorAlert,
|
|
337
|
+
data.supervisorAutonomy ?? "autonomous",
|
|
335
338
|
)
|
|
336
339
|
: executeOrchBatch(
|
|
337
340
|
data.args ?? "",
|
|
@@ -346,6 +349,7 @@ if (process.env.TASKPLANE_ENGINE_FORK === "1" && typeof process.send === "functi
|
|
|
346
349
|
data.agentRoot,
|
|
347
350
|
onEngineEvent,
|
|
348
351
|
onSupervisorAlert,
|
|
352
|
+
data.supervisorAutonomy ?? "autonomous",
|
|
349
353
|
);
|
|
350
354
|
|
|
351
355
|
enginePromise
|