@agimon-ai/po-plan 0.0.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/apiContracts.cjs +57 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +52 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/config-BC-Tou05.mjs +25 -0
- package/dist/config-BC-Tou05.mjs.map +1 -0
- package/dist/config-D0iTexGm.cjs +42 -0
- package/dist/config-D0iTexGm.cjs.map +1 -0
- package/dist/config-DWo57HKB.d.mts +13 -0
- package/dist/config-DWo57HKB.d.mts.map +1 -0
- package/dist/config.cjs +12 -0
- package/dist/config.d.mts +2 -0
- package/dist/config.mjs +2 -0
- package/dist/extensions/mcp-ui.mjs +46 -0
- package/dist/extensions/mcp-ui.mjs.map +1 -0
- package/dist/extensions/mcp.cjs +93 -0
- package/dist/extensions/mcp.cjs.map +1 -0
- package/dist/extensions/mcp.d.mts +5 -0
- package/dist/extensions/mcp.d.mts.map +1 -0
- package/dist/extensions/mcp.mjs +88 -0
- package/dist/extensions/mcp.mjs.map +1 -0
- package/dist/extensions/server.cjs +1025 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +993 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +861 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/fableFlow-BDc8a__Y.mjs +214 -0
- package/dist/fableFlow-BDc8a__Y.mjs.map +1 -0
- package/dist/fableFlow-CzfrLPzJ.cjs +279 -0
- package/dist/fableFlow-CzfrLPzJ.cjs.map +1 -0
- package/dist/fableFlow.cjs +13 -0
- package/dist/fableFlow.d.mts +2 -0
- package/dist/fableFlow.mjs +2 -0
- package/dist/index-C8e7IK60.d.mts +68 -0
- package/dist/index-C8e7IK60.d.mts.map +1 -0
- package/dist/index-C9QBG9cw.d.mts +51 -0
- package/dist/index-C9QBG9cw.d.mts.map +1 -0
- package/dist/index.cjs +31 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +4 -0
- package/dist/logSinkTelemetry.cjs +42 -0
- package/dist/logSinkTelemetry.cjs.map +1 -0
- package/dist/logSinkTelemetry.d.mts +33 -0
- package/dist/logSinkTelemetry.d.mts.map +1 -0
- package/dist/logSinkTelemetry.mjs +40 -0
- package/dist/logSinkTelemetry.mjs.map +1 -0
- package/dist/planConfig.cjs +190 -0
- package/dist/planConfig.cjs.map +1 -0
- package/dist/planConfig.d.mts +47 -0
- package/dist/planConfig.d.mts.map +1 -0
- package/dist/planConfig.mjs +186 -0
- package/dist/planConfig.mjs.map +1 -0
- package/dist/prompts-DUVUPNZd.cjs +103 -0
- package/dist/prompts-DUVUPNZd.cjs.map +1 -0
- package/dist/prompts-DnVJGtEC.mjs +68 -0
- package/dist/prompts-DnVJGtEC.mjs.map +1 -0
- package/dist/prompts.cjs +8 -0
- package/dist/prompts.d.mts +2 -0
- package/dist/prompts.mjs +2 -0
- package/llms.txt +9 -0
- package/package.json +164 -0
- package/src/prompts/po-use-plan/SKILL.md +44 -0
- package/src/types/planApi.ts +125 -0
- package/src/types/planSettings.ts +78 -0
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
import { l as resolvePlanningPlansDirectory, r as loadDoomConfig } from "../config-BC-Tou05.mjs";
|
|
2
|
+
import { i as buildFlavorPlanningPrompt, o as buildPlanModeBasePrompt } from "../prompts-DnVJGtEC.mjs";
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import { composeServerProviderOverrides, defineServerPlugin, readPackageResource } from "@agimon-ai/po-core/serverFacet";
|
|
5
|
+
import { composeRootHooks, defineRoot, defineRoute, defineRoutedContribution, defineToolRestriction } from "@agimon-ai/po-core/extensionFile";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import fs, { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
import fs$1 from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { piAgentDirectory } from "@agimon-ai/po-core/piSettings";
|
|
14
|
+
import { apiResponse, defineApiRoutes } from "@agimon-ai/po-core/web";
|
|
15
|
+
import { DOOM_VOICE_AUTO_MODE_ID } from "@agimon-ai/po-core/voiceTools";
|
|
16
|
+
import { defineMinorMode, serverMinorModes } from "@agimon-ai/po-minor-mode";
|
|
17
|
+
//#region src/schemas/mcpTools.ts
|
|
18
|
+
const markdown = Type.String({ minLength: 1 });
|
|
19
|
+
Type.Object({ markdown }, { additionalProperties: false });
|
|
20
|
+
const writePlanParameters = Type.Object({ markdown: Type.Optional(markdown) }, { additionalProperties: false });
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/services/mcpTools/index.ts
|
|
23
|
+
const PLAN_MCP_TOOLS_SERVICE = "doom/plan-mcp-tools";
|
|
24
|
+
function mountMcpTools(tools) {
|
|
25
|
+
return (context) => {
|
|
26
|
+
context.plugin((providerContext) => {
|
|
27
|
+
providerContext.provide(PLAN_MCP_TOOLS_SERVICE, {
|
|
28
|
+
get: (name) => tools.find((tool) => tool.name === name),
|
|
29
|
+
names: () => tools.map((tool) => tool.name)
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/services/planPointer/index.ts
|
|
36
|
+
/**
|
|
37
|
+
* Record and read where write_plan left this session's plan file.
|
|
38
|
+
*
|
|
39
|
+
* Pi already gives every session an agent directory, so the records live under
|
|
40
|
+
* it rather than in a second place a reader would have to know about. The
|
|
41
|
+
* posture matches the plan file itself, 0700 on the directory and 0600 on the
|
|
42
|
+
* record: a plan is a private working document, not something to leave
|
|
43
|
+
* world-readable in a shared home.
|
|
44
|
+
*/
|
|
45
|
+
const POINTER_DIRECTORY = "doom-plan";
|
|
46
|
+
const PRIVATE_DIRECTORY_MODE = 448;
|
|
47
|
+
const PRIVATE_FILE_MODE = 384;
|
|
48
|
+
function isRecord$1(value) {
|
|
49
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
var PlanPointerService = class {
|
|
52
|
+
env;
|
|
53
|
+
homeDirectory;
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.env = options.env ?? {};
|
|
56
|
+
this.homeDirectory = options.homeDirectory ?? os.homedir();
|
|
57
|
+
}
|
|
58
|
+
/** Where the records live; exposed so a test can look without guessing. */
|
|
59
|
+
directory() {
|
|
60
|
+
return path.join(piAgentDirectory(this.env, this.homeDirectory), POINTER_DIRECTORY);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The record for one session. The id is hashed rather than used as written:
|
|
64
|
+
* it reaches here from a host that may word it however it likes, and a
|
|
65
|
+
* filename is not the place to find that out.
|
|
66
|
+
*/
|
|
67
|
+
pathFor(sessionId) {
|
|
68
|
+
return path.join(this.directory(), `${this.hash(sessionId)}.json`);
|
|
69
|
+
}
|
|
70
|
+
read(sessionId) {
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = fs$1.readFileSync(this.pathFor(sessionId), "utf8");
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!isRecord$1(parsed)) return void 0;
|
|
84
|
+
const { path: planPath, title, writtenAt, planId } = parsed;
|
|
85
|
+
if (typeof planPath !== "string" || !path.isAbsolute(planPath)) return void 0;
|
|
86
|
+
if (typeof title !== "string" || typeof writtenAt !== "string") return void 0;
|
|
87
|
+
return {
|
|
88
|
+
path: planPath,
|
|
89
|
+
title,
|
|
90
|
+
writtenAt,
|
|
91
|
+
...typeof planId === "string" ? { planId } : {}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
write(sessionId, record) {
|
|
95
|
+
fs$1.mkdirSync(this.directory(), {
|
|
96
|
+
recursive: true,
|
|
97
|
+
mode: PRIVATE_DIRECTORY_MODE
|
|
98
|
+
});
|
|
99
|
+
fs$1.writeFileSync(this.pathFor(sessionId), JSON.stringify(record), { mode: PRIVATE_FILE_MODE });
|
|
100
|
+
}
|
|
101
|
+
clear(sessionId) {
|
|
102
|
+
fs$1.rmSync(this.pathFor(sessionId), { force: true });
|
|
103
|
+
}
|
|
104
|
+
hash(value) {
|
|
105
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/types/apiRoutes.ts
|
|
110
|
+
/**
|
|
111
|
+
* This package's routes, as data.
|
|
112
|
+
*
|
|
113
|
+
* No scope and no base path. The build reads both off
|
|
114
|
+
* `src/extensions/workspaces/sessions/(backend)/api/plans/`, so the mount is
|
|
115
|
+
* stated once, by the folder that creates it. The paths used to be written
|
|
116
|
+
* here as helpers, again in the contract, and again inside every URL the page
|
|
117
|
+
* built, and the copies were free to disagree.
|
|
118
|
+
*
|
|
119
|
+
* This is the one file the routes, the Hono app and the browser all read, so a
|
|
120
|
+
* path cannot move on one side alone.
|
|
121
|
+
*
|
|
122
|
+
* Neither route takes a query or a path parameter: a session has one current
|
|
123
|
+
* plan, so `current` answers it and `save` takes the manual write back.
|
|
124
|
+
*/
|
|
125
|
+
var apiRoutes_default = defineApiRoutes({
|
|
126
|
+
current: {
|
|
127
|
+
method: "GET",
|
|
128
|
+
path: "/current",
|
|
129
|
+
response: apiResponse()
|
|
130
|
+
},
|
|
131
|
+
save: {
|
|
132
|
+
method: "PUT",
|
|
133
|
+
path: "/content",
|
|
134
|
+
response: apiResponse()
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/types/planApi.ts
|
|
139
|
+
/**
|
|
140
|
+
* The wire vocabulary this package's API shares with whatever calls it.
|
|
141
|
+
*
|
|
142
|
+
* It lives under src/types because that is the one server root a browser
|
|
143
|
+
* bundle may read: the routes answer with these shapes and the cockpit plugin
|
|
144
|
+
* reads them, so neither half can drift from the other. The paths themselves
|
|
145
|
+
* are not here; they are declared once in `./apiRoutes`, which the Hono app
|
|
146
|
+
* and the generated client both read.
|
|
147
|
+
*
|
|
148
|
+
* Two routes, and neither takes a path. A session has one current plan, so
|
|
149
|
+
* `current` answers it and `content` takes the manual save back; the file they
|
|
150
|
+
* act on is the one write_plan recorded, never one the page named. That is
|
|
151
|
+
* also why there is nothing to escape here: a reader cannot address a second
|
|
152
|
+
* file through these URLs.
|
|
153
|
+
*/
|
|
154
|
+
/** Where a host mounts this package's API; the segment after /api/plugins/. */
|
|
155
|
+
const API_BASE_PATH = "plans";
|
|
156
|
+
/**
|
|
157
|
+
* Footer status key the session publishes once a plan exists, which is what
|
|
158
|
+
* makes the cockpit's activity group appear at all. It outlives plan mode: the
|
|
159
|
+
* plan is most worth reading while the agent implements it.
|
|
160
|
+
*/
|
|
161
|
+
const PLAN_STATUS_KEY = "doom-plan-document";
|
|
162
|
+
/** The selector contract shared by complete_plan and its cockpit composer prompt. */
|
|
163
|
+
const PLAN_REVIEW_TITLE = "Plan complete. What would you like to do?";
|
|
164
|
+
const EXIT_PLAN_MODE_CHOICE = "Exit plan mode and start implementation";
|
|
165
|
+
const CONTINUE_PLANNING_CHOICE = "Continue planning";
|
|
166
|
+
const EXIT_PLAN_DECISION = "exit";
|
|
167
|
+
const CONTINUE_PLAN_DECISION = "continue";
|
|
168
|
+
/**
|
|
169
|
+
* The label the reader sees paired with the decision the tool acts on. A headless
|
|
170
|
+
* client answers with `value`, so a facet never has to recognise the wording; Pi's
|
|
171
|
+
* own `ui.select` carries labels only, so its half maps the label back itself.
|
|
172
|
+
*/
|
|
173
|
+
const PLAN_REVIEW_CHOICES = [{
|
|
174
|
+
label: EXIT_PLAN_MODE_CHOICE,
|
|
175
|
+
value: EXIT_PLAN_DECISION
|
|
176
|
+
}, {
|
|
177
|
+
label: CONTINUE_PLANNING_CHOICE,
|
|
178
|
+
value: CONTINUE_PLAN_DECISION
|
|
179
|
+
}];
|
|
180
|
+
PLAN_REVIEW_CHOICES.map((choice) => choice.label);
|
|
181
|
+
/** Between the plan's title and the stamp that marks a rewrite. */
|
|
182
|
+
const STATUS_SEPARATOR = " · ";
|
|
183
|
+
/** The status line the session publishes; the stamp changes on every write. */
|
|
184
|
+
function formatPlanStatus(title, stamp) {
|
|
185
|
+
return `${title}${STATUS_SEPARATOR}${stamp}`;
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/services/planApi/index.ts
|
|
189
|
+
/**
|
|
190
|
+
* This package's HTTP surface: the session's current plan, and the manual save.
|
|
191
|
+
*
|
|
192
|
+
* Each path comes from the route table the browser reads, so a route the page
|
|
193
|
+
* calls and the route this serves cannot be spelled apart. Both are relative
|
|
194
|
+
* to the mount its host gives it, so nothing here repeats where it was mounted. The host is one session's own server, which is what
|
|
195
|
+
* makes the session id available without the page supplying it.
|
|
196
|
+
*
|
|
197
|
+
* Neither route takes a path. A session has exactly one current plan, and the
|
|
198
|
+
* file both routes act on is the one the pointer names, so a page can address
|
|
199
|
+
* the plan and nothing else; there is no traversal question to answer because
|
|
200
|
+
* there is no path to traverse.
|
|
201
|
+
*
|
|
202
|
+
* The save refuses unless the reader proves they were looking at the content
|
|
203
|
+
* that is still on disk. The agent may rewrite the plan while a tab is open,
|
|
204
|
+
* and silently discarding whichever side finished second is the one outcome
|
|
205
|
+
* worth ruling out.
|
|
206
|
+
*/
|
|
207
|
+
/** Past this, the plan is reported rather than carried. */
|
|
208
|
+
const MAX_PLAN_BYTES = 1048576;
|
|
209
|
+
const NO_PLAN = "This session has not written a plan yet.";
|
|
210
|
+
const STALE_SAVE = "The plan changed since it was opened.";
|
|
211
|
+
const NOT_A_FILE = "The plan file is no longer a regular file, so it was not written.";
|
|
212
|
+
function hashOf(content) {
|
|
213
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
214
|
+
}
|
|
215
|
+
/** Reads the plan as the source view will show it, or says why it cannot. */
|
|
216
|
+
async function readPlan(planPath) {
|
|
217
|
+
let raw;
|
|
218
|
+
try {
|
|
219
|
+
raw = await fs.readFile(planPath);
|
|
220
|
+
} catch {
|
|
221
|
+
return {
|
|
222
|
+
content: "",
|
|
223
|
+
hash: "",
|
|
224
|
+
unavailable: true,
|
|
225
|
+
reason: "the plan file no longer exists"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (raw.byteLength > 1048576) return {
|
|
229
|
+
content: "",
|
|
230
|
+
hash: "",
|
|
231
|
+
unavailable: true,
|
|
232
|
+
reason: `the plan is larger than ${MAX_PLAN_BYTES} bytes`
|
|
233
|
+
};
|
|
234
|
+
const content = raw.toString("utf8");
|
|
235
|
+
return {
|
|
236
|
+
content,
|
|
237
|
+
hash: hashOf(content),
|
|
238
|
+
unavailable: false
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function createPlanApi(options = {}) {
|
|
242
|
+
const app = new Hono();
|
|
243
|
+
const pointers = options.pointers ?? new PlanPointerService({ env: process.env });
|
|
244
|
+
const pointerFor = () => options.sessionId === void 0 ? void 0 : pointers.read(options.sessionId);
|
|
245
|
+
app.get(apiRoutes_default.current.path, async (context) => {
|
|
246
|
+
const pointer = pointerFor();
|
|
247
|
+
if (pointer === void 0) return context.json({ error: NO_PLAN }, 404);
|
|
248
|
+
const view = {
|
|
249
|
+
path: pointer.path,
|
|
250
|
+
title: pointer.title,
|
|
251
|
+
writtenAt: pointer.writtenAt,
|
|
252
|
+
...pointer.planId === void 0 ? {} : { planId: pointer.planId },
|
|
253
|
+
...await readPlan(pointer.path)
|
|
254
|
+
};
|
|
255
|
+
return context.json(view);
|
|
256
|
+
});
|
|
257
|
+
app.put(apiRoutes_default.save.path, async (context) => {
|
|
258
|
+
let request;
|
|
259
|
+
try {
|
|
260
|
+
request = await context.req.json();
|
|
261
|
+
} catch {
|
|
262
|
+
return context.json({ error: "The save body is not JSON." }, 400);
|
|
263
|
+
}
|
|
264
|
+
if (typeof request.content !== "string" || typeof request.expectedHash !== "string") return context.json({ error: "A save carries the content to write and the hash it was read at." }, 400);
|
|
265
|
+
const pointer = pointerFor();
|
|
266
|
+
if (pointer === void 0) return context.json({ error: NO_PLAN }, 404);
|
|
267
|
+
const current = await readPlan(pointer.path);
|
|
268
|
+
if (current.unavailable) return context.json({ error: current.reason ?? NO_PLAN }, 409);
|
|
269
|
+
if (current.hash !== request.expectedHash) return context.json({
|
|
270
|
+
error: STALE_SAVE,
|
|
271
|
+
hash: current.hash
|
|
272
|
+
}, 409);
|
|
273
|
+
const stats = await fs.lstat(pointer.path);
|
|
274
|
+
if (!stats.isFile() || stats.nlink !== 1) return context.json({ error: NOT_A_FILE }, 409);
|
|
275
|
+
await fs.writeFile(pointer.path, request.content, {
|
|
276
|
+
encoding: "utf8",
|
|
277
|
+
mode: 384
|
|
278
|
+
});
|
|
279
|
+
return context.json({ hash: hashOf(request.content) });
|
|
280
|
+
});
|
|
281
|
+
return app;
|
|
282
|
+
}
|
|
283
|
+
/** The named export a host imports from this package's built entry. */
|
|
284
|
+
const api = {
|
|
285
|
+
basePath: API_BASE_PATH,
|
|
286
|
+
start(context) {
|
|
287
|
+
const app = createPlanApi(context.sessionId === void 0 ? {} : { sessionId: context.sessionId });
|
|
288
|
+
return {
|
|
289
|
+
fetch: (request) => app.fetch(request),
|
|
290
|
+
close: () => void 0
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/services/optionalTeamServices/index.ts
|
|
296
|
+
const DOOM_SUBAGENT_POLICY_SERVICE = "doom/subagent-policy";
|
|
297
|
+
function readDoomSubagentPolicyService(ctx) {
|
|
298
|
+
return ctx.get(DOOM_SUBAGENT_POLICY_SERVICE);
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/services/planServerSession/index.ts
|
|
302
|
+
const RECORD_DEBUG_EVIDENCE_TOOL = "record_debug_evidence";
|
|
303
|
+
const WRITE_PLAN_TOOL = "write_plan";
|
|
304
|
+
const COMPLETE_PLAN_TOOL = "complete_plan";
|
|
305
|
+
const PLAN_MODE_ID = "plan";
|
|
306
|
+
const PLAN_MODEL_SNAPSHOT = "plan-server-model-snapshot";
|
|
307
|
+
const PLAN_FLAVOR_SNAPSHOT = "plan-server-flavor";
|
|
308
|
+
const PLAN_DEBUG_EVIDENCE = "plan-debug-evidence";
|
|
309
|
+
const PLAN_DOCUMENT = "plan-document";
|
|
310
|
+
/** This host supports UI review without narrated decisions. */
|
|
311
|
+
const PLAN_SERVER_CAPABILITIES = {
|
|
312
|
+
completePlanTakesDecision: false,
|
|
313
|
+
narratesReview: false,
|
|
314
|
+
fableAvailable: false
|
|
315
|
+
};
|
|
316
|
+
const PLAN_SUBAGENT_POLICY = {
|
|
317
|
+
owner: "@agimon-ai/po-plan",
|
|
318
|
+
allowedTools: [
|
|
319
|
+
"read",
|
|
320
|
+
"bash",
|
|
321
|
+
"grep",
|
|
322
|
+
"find",
|
|
323
|
+
"ls",
|
|
324
|
+
"mcp"
|
|
325
|
+
],
|
|
326
|
+
requiredTools: ["bash"],
|
|
327
|
+
allowMcpTools: true,
|
|
328
|
+
allowedExternalProfiles: [],
|
|
329
|
+
denyExtensions: false
|
|
330
|
+
};
|
|
331
|
+
function serverPlanSubagentPolicy(host) {
|
|
332
|
+
return (context) => {
|
|
333
|
+
context.inject([DOOM_SUBAGENT_POLICY_SERVICE], (child) => {
|
|
334
|
+
const service = readDoomSubagentPolicyService(child);
|
|
335
|
+
if (!service) return void 0;
|
|
336
|
+
let handle;
|
|
337
|
+
const sync = () => {
|
|
338
|
+
if (modeSelected(host)) {
|
|
339
|
+
if (handle) handle.update(PLAN_SUBAGENT_POLICY);
|
|
340
|
+
else handle = service.register(PLAN_SUBAGENT_POLICY);
|
|
341
|
+
} else {
|
|
342
|
+
handle?.dispose();
|
|
343
|
+
handle = void 0;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
sync();
|
|
347
|
+
child.effect(() => host.subscribeSelection(sync));
|
|
348
|
+
return () => handle?.dispose();
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function modeSelected(host) {
|
|
353
|
+
return (host.context.selection.state?.["minor-mode"] ?? []).includes(PLAN_MODE_ID);
|
|
354
|
+
}
|
|
355
|
+
function output(value, isError = false) {
|
|
356
|
+
return {
|
|
357
|
+
content: [{
|
|
358
|
+
type: "text",
|
|
359
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
360
|
+
}],
|
|
361
|
+
details: value,
|
|
362
|
+
...isError ? { isError: true } : {}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function createPlanServerSession(host) {
|
|
366
|
+
let flavor = "normal";
|
|
367
|
+
let debugEvidence;
|
|
368
|
+
let modeOwner;
|
|
369
|
+
const selected = () => modeSelected(host);
|
|
370
|
+
const homeDirectory = () => host.context.environment.HOME ?? os.homedir();
|
|
371
|
+
const plansDirectory = () => resolvePlanningPlansDirectory(loadDoomConfig(host.context.repoRoot, homeDirectory()).modes?.planning?.plansDirectory, host.context.repoRoot, homeDirectory());
|
|
372
|
+
const pointers = new PlanPointerService({ env: host.context.environment });
|
|
373
|
+
/**
|
|
374
|
+
* The activity group's line, and the pointer its panel reads.
|
|
375
|
+
*
|
|
376
|
+
* A cockpit drives this facet and never the Pi runtime, so both were written
|
|
377
|
+
* only on the Pi side: the group never appeared, and when it did its panel
|
|
378
|
+
* had no pointer to answer from.
|
|
379
|
+
*/
|
|
380
|
+
const announcePlanDocument = (filePath, title, writtenAt) => {
|
|
381
|
+
try {
|
|
382
|
+
pointers.write(host.context.sessionId, {
|
|
383
|
+
path: filePath,
|
|
384
|
+
title,
|
|
385
|
+
writtenAt
|
|
386
|
+
});
|
|
387
|
+
} catch {}
|
|
388
|
+
host.context.client.setStatus(PLAN_STATUS_KEY, formatPlanStatus(title, planStampOf(writtenAt)));
|
|
389
|
+
};
|
|
390
|
+
const modeState = () => {
|
|
391
|
+
const active = selected();
|
|
392
|
+
return {
|
|
393
|
+
activation: active ? "active" : "inactive",
|
|
394
|
+
condition: "ready",
|
|
395
|
+
...active ? {
|
|
396
|
+
detail: `${flavor} - read only`,
|
|
397
|
+
modelContextVariant: flavor
|
|
398
|
+
} : {},
|
|
399
|
+
actions: [{
|
|
400
|
+
id: "activate",
|
|
401
|
+
enabled: true
|
|
402
|
+
}, {
|
|
403
|
+
id: "deactivate",
|
|
404
|
+
enabled: active,
|
|
405
|
+
...active ? {} : { disabledReason: "Plan mode is inactive." }
|
|
406
|
+
}]
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
const publishMode = () => modeOwner?.publish();
|
|
410
|
+
const restoreModel = async () => {
|
|
411
|
+
const session = host.context.session;
|
|
412
|
+
const data = (await session.entries({
|
|
413
|
+
type: "custom",
|
|
414
|
+
customType: PLAN_MODEL_SNAPSHOT,
|
|
415
|
+
limit: 1
|
|
416
|
+
}))[0]?.data;
|
|
417
|
+
if (data === void 0 || data === null) return;
|
|
418
|
+
if (typeof data !== "object" || !("thinkingLevel" in data) || ![
|
|
419
|
+
"off",
|
|
420
|
+
"minimal",
|
|
421
|
+
"low",
|
|
422
|
+
"medium",
|
|
423
|
+
"high",
|
|
424
|
+
"xhigh",
|
|
425
|
+
"max"
|
|
426
|
+
].includes(String(data.thinkingLevel))) throw new Error("The saved planning model settings are invalid.");
|
|
427
|
+
if (!session.setModelSettings) throw new Error("This session cannot restore its model settings.");
|
|
428
|
+
await session.setModelSettings(data);
|
|
429
|
+
await session.appendCustomEntry(PLAN_MODEL_SNAPSHOT, null);
|
|
430
|
+
};
|
|
431
|
+
const selectPlan = async (enabled, nextFlavor = flavor) => {
|
|
432
|
+
const wasEnabled = selected();
|
|
433
|
+
const session = host.context.session;
|
|
434
|
+
if (enabled && !wasEnabled) {
|
|
435
|
+
const config = loadDoomConfig(host.context.repoRoot, homeDirectory()).modes?.planning?.main;
|
|
436
|
+
if (config?.model || config?.thinking) {
|
|
437
|
+
if (!session.readModelSettings || !session.setModelSettings) throw new Error("This session cannot apply planning model settings.");
|
|
438
|
+
const previous = await session.readModelSettings();
|
|
439
|
+
const settings = {};
|
|
440
|
+
if (config.model) {
|
|
441
|
+
const separator = config.model.indexOf("/");
|
|
442
|
+
const provider = separator > 0 ? config.model.slice(0, separator) : previous.model?.provider;
|
|
443
|
+
if (!provider) throw new Error("The planning model needs a provider.");
|
|
444
|
+
settings.model = {
|
|
445
|
+
provider,
|
|
446
|
+
id: separator > 0 ? config.model.slice(separator + 1) : config.model
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (config.thinking) settings.thinkingLevel = config.thinking;
|
|
450
|
+
await session.appendCustomEntry(PLAN_MODEL_SNAPSHOT, previous);
|
|
451
|
+
try {
|
|
452
|
+
await session.setModelSettings(settings);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
await session.setModelSettings(previous);
|
|
455
|
+
await session.appendCustomEntry(PLAN_MODEL_SNAPSHOT, null);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} else if (!enabled && wasEnabled) await restoreModel();
|
|
460
|
+
const modes = (host.context.selection.state?.["minor-mode"] ?? []).filter((mode) => mode !== PLAN_MODE_ID);
|
|
461
|
+
await host.changeSelection({
|
|
462
|
+
axis: "state",
|
|
463
|
+
key: "minor-mode",
|
|
464
|
+
values: enabled ? [...modes, PLAN_MODE_ID] : modes
|
|
465
|
+
});
|
|
466
|
+
flavor = nextFlavor;
|
|
467
|
+
await session.appendCustomEntry(PLAN_FLAVOR_SNAPSHOT, enabled ? { flavor } : null);
|
|
468
|
+
publishMode();
|
|
469
|
+
};
|
|
470
|
+
/** Newest value for a custom entry type, or undefined when none was recorded. */
|
|
471
|
+
const latestEntry = async (customType) => {
|
|
472
|
+
return (await host.context.session.entries({
|
|
473
|
+
type: "custom",
|
|
474
|
+
customType,
|
|
475
|
+
limit: 1
|
|
476
|
+
}))[0]?.data ?? void 0;
|
|
477
|
+
};
|
|
478
|
+
const restorePlanState = async () => {
|
|
479
|
+
const saved = await latestEntry(PLAN_FLAVOR_SNAPSHOT);
|
|
480
|
+
if (saved !== void 0 && saved !== null && typeof saved === "object" && "flavor" in saved) {
|
|
481
|
+
const candidate = String(saved.flavor);
|
|
482
|
+
if (candidate === "fable") flavor = "normal";
|
|
483
|
+
else if (candidate === "normal" || candidate === "debug" || candidate === "code-review") flavor = candidate;
|
|
484
|
+
}
|
|
485
|
+
const evidence = await latestEntry(PLAN_DEBUG_EVIDENCE);
|
|
486
|
+
if (evidence === void 0 || evidence === null) return;
|
|
487
|
+
try {
|
|
488
|
+
debugEvidence = parseDebugEvidencePacket(evidence);
|
|
489
|
+
} catch {
|
|
490
|
+
debugEvidence = void 0;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
modeOwner = defineMinorMode({
|
|
494
|
+
descriptor: {
|
|
495
|
+
source: "@agimon-ai/po-plan",
|
|
496
|
+
id: PLAN_MODE_ID,
|
|
497
|
+
label: "Plan",
|
|
498
|
+
description: "Read-only planning with normal, debug, and code review flavors.",
|
|
499
|
+
order: 40,
|
|
500
|
+
actions: [{
|
|
501
|
+
id: "activate",
|
|
502
|
+
label: "Activate",
|
|
503
|
+
description: "Activate plan mode or switch its planning flavor.",
|
|
504
|
+
contexts: ["headless"],
|
|
505
|
+
parameters: [{
|
|
506
|
+
name: "flavor",
|
|
507
|
+
label: "Flavor",
|
|
508
|
+
kind: "enum",
|
|
509
|
+
required: true,
|
|
510
|
+
choices: [
|
|
511
|
+
{
|
|
512
|
+
value: "normal",
|
|
513
|
+
label: "Normal"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
value: "debug",
|
|
517
|
+
label: "Debug"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
value: "code-review",
|
|
521
|
+
label: "Code Review"
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
}]
|
|
525
|
+
}, {
|
|
526
|
+
id: "deactivate",
|
|
527
|
+
label: "Deactivate",
|
|
528
|
+
description: "Exit plan mode and restore the previous agent configuration.",
|
|
529
|
+
contexts: ["headless"],
|
|
530
|
+
parameters: []
|
|
531
|
+
}]
|
|
532
|
+
},
|
|
533
|
+
state: () => modeState(),
|
|
534
|
+
async handleAction(_runtime, actionId, argumentsValue, { signal }) {
|
|
535
|
+
signal.throwIfAborted();
|
|
536
|
+
if (actionId === "activate") {
|
|
537
|
+
const nextFlavor = argumentsValue.flavor;
|
|
538
|
+
if (nextFlavor !== "normal" && nextFlavor !== "debug" && nextFlavor !== "code-review") throw new Error("A valid plan flavor is required.");
|
|
539
|
+
await selectPlan(true, nextFlavor);
|
|
540
|
+
return { message: `Plan mode is active with the ${nextFlavor} flavor.` };
|
|
541
|
+
}
|
|
542
|
+
if (actionId === "deactivate") {
|
|
543
|
+
await selectPlan(false);
|
|
544
|
+
return { message: "Plan mode deactivated." };
|
|
545
|
+
}
|
|
546
|
+
throw new Error(`Unknown plan mode action: ${actionId}`);
|
|
547
|
+
}
|
|
548
|
+
}).createOwner(void 0);
|
|
549
|
+
return {
|
|
550
|
+
services: [serverMinorModes([modeOwner]), serverPlanSubagentPolicy(host)],
|
|
551
|
+
resources: [{
|
|
552
|
+
when: {
|
|
553
|
+
state: { "minor-mode": PLAN_MODE_ID },
|
|
554
|
+
attribution: {
|
|
555
|
+
kind: "minor",
|
|
556
|
+
mode: PLAN_MODE_ID
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
name: "po-use-plan",
|
|
560
|
+
kind: "skill",
|
|
561
|
+
read: () => readPackageResource(import.meta.url, "src/prompts/po-use-plan/SKILL.md")
|
|
562
|
+
}],
|
|
563
|
+
tools: [
|
|
564
|
+
{
|
|
565
|
+
when: {
|
|
566
|
+
state: { "minor-mode": PLAN_MODE_ID },
|
|
567
|
+
attribution: {
|
|
568
|
+
kind: "minor",
|
|
569
|
+
mode: PLAN_MODE_ID
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
name: RECORD_DEBUG_EVIDENCE_TOOL,
|
|
573
|
+
label: "Record Debug Evidence",
|
|
574
|
+
description: "Record bounded debug evidence as optional planning context.",
|
|
575
|
+
parameters: {
|
|
576
|
+
type: "object",
|
|
577
|
+
properties: {
|
|
578
|
+
issue: { type: "string" },
|
|
579
|
+
expectedBehavior: { type: "string" },
|
|
580
|
+
reproductionAttempt: { type: "string" },
|
|
581
|
+
actualBehavior: { type: "string" },
|
|
582
|
+
logs: {
|
|
583
|
+
type: "array",
|
|
584
|
+
items: { type: "string" }
|
|
585
|
+
},
|
|
586
|
+
correlatedTraceEvidence: {
|
|
587
|
+
type: "array",
|
|
588
|
+
items: { type: "string" }
|
|
589
|
+
},
|
|
590
|
+
processOutput: {
|
|
591
|
+
type: "array",
|
|
592
|
+
items: { type: "string" }
|
|
593
|
+
},
|
|
594
|
+
browserConsoleEvidence: {
|
|
595
|
+
type: "array",
|
|
596
|
+
items: { type: "string" }
|
|
597
|
+
},
|
|
598
|
+
correlationIds: {
|
|
599
|
+
type: "array",
|
|
600
|
+
items: { type: "string" }
|
|
601
|
+
},
|
|
602
|
+
timestamps: {
|
|
603
|
+
type: "array",
|
|
604
|
+
items: { type: "string" }
|
|
605
|
+
},
|
|
606
|
+
verifiedFacts: {
|
|
607
|
+
type: "array",
|
|
608
|
+
items: { type: "string" }
|
|
609
|
+
},
|
|
610
|
+
hypotheses: {
|
|
611
|
+
type: "array",
|
|
612
|
+
items: { type: "string" }
|
|
613
|
+
},
|
|
614
|
+
unavailableEvidence: {
|
|
615
|
+
type: "array",
|
|
616
|
+
items: { type: "string" }
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
required: ["issue"],
|
|
620
|
+
additionalProperties: false
|
|
621
|
+
},
|
|
622
|
+
executionMode: "serial",
|
|
623
|
+
async execute(_toolCallId, parameters) {
|
|
624
|
+
try {
|
|
625
|
+
const parsedEvidence = parseDebugEvidencePacket(parameters);
|
|
626
|
+
await host.context.session.appendCustomEntry(PLAN_DEBUG_EVIDENCE, parsedEvidence);
|
|
627
|
+
debugEvidence = parsedEvidence;
|
|
628
|
+
return output({ recorded: true });
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return output(error instanceof Error ? error.message : String(error), true);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
when: {
|
|
636
|
+
state: { "minor-mode": PLAN_MODE_ID },
|
|
637
|
+
attribution: {
|
|
638
|
+
kind: "minor",
|
|
639
|
+
mode: PLAN_MODE_ID
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
name: WRITE_PLAN_TOOL,
|
|
643
|
+
label: "Write Plan",
|
|
644
|
+
description: "Save the supplied Markdown plan, or the plan already presented in the local session.",
|
|
645
|
+
parameters: writePlanParameters,
|
|
646
|
+
executionMode: "serial",
|
|
647
|
+
async execute(toolCallId, parameters, signal) {
|
|
648
|
+
try {
|
|
649
|
+
signal?.throwIfAborted();
|
|
650
|
+
let content = typeof parameters === "object" && parameters !== null && "markdown" in parameters ? parameters.markdown : void 0;
|
|
651
|
+
if (content !== void 0 && typeof content !== "string") throw new Error("Plan Markdown must be a string.");
|
|
652
|
+
if (content === void 0) content = visiblePlanForToolCall(await host.context.session.entries(), toolCallId);
|
|
653
|
+
if (content === void 0) {
|
|
654
|
+
if (host.context.selection.state?.["minor-mode"]?.includes(DOOM_VOICE_AUTO_MODE_ID)) throw new Error("Supply Markdown to write_plan during autonomous Voice; UI input is unavailable.");
|
|
655
|
+
const requested = await host.context.client.request({
|
|
656
|
+
kind: "input",
|
|
657
|
+
title: "Implementation plan",
|
|
658
|
+
multiline: true
|
|
659
|
+
}, signal);
|
|
660
|
+
content = typeof requested === "string" ? requested : "";
|
|
661
|
+
}
|
|
662
|
+
if (typeof content !== "string" || !content.trim()) throw new Error("No implementation plan content was supplied.");
|
|
663
|
+
const directory = plansDirectory();
|
|
664
|
+
await mkdir(directory, {
|
|
665
|
+
recursive: true,
|
|
666
|
+
mode: 448
|
|
667
|
+
});
|
|
668
|
+
const title = planTitleOf(content);
|
|
669
|
+
const writtenAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
670
|
+
const filename = `${planTitleSlug(content)}-${Date.now()}.md`;
|
|
671
|
+
const filePath = path.join(directory, filename);
|
|
672
|
+
await writeFile(filePath, `${content.trim()}\n`, {
|
|
673
|
+
encoding: "utf8",
|
|
674
|
+
mode: 384,
|
|
675
|
+
flag: "wx"
|
|
676
|
+
});
|
|
677
|
+
await host.context.session.appendCustomEntry(PLAN_DOCUMENT, {
|
|
678
|
+
path: filePath,
|
|
679
|
+
content: content.trim(),
|
|
680
|
+
title,
|
|
681
|
+
writtenAt
|
|
682
|
+
});
|
|
683
|
+
announcePlanDocument(filePath, title, writtenAt);
|
|
684
|
+
await selectPlan(true);
|
|
685
|
+
const saved = {
|
|
686
|
+
path: filePath,
|
|
687
|
+
written: true
|
|
688
|
+
};
|
|
689
|
+
return {
|
|
690
|
+
...output(saved),
|
|
691
|
+
structuredContent: saved
|
|
692
|
+
};
|
|
693
|
+
} catch (error) {
|
|
694
|
+
return output(error instanceof Error ? error.message : String(error), true);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
when: {
|
|
700
|
+
state: { "minor-mode": PLAN_MODE_ID },
|
|
701
|
+
attribution: {
|
|
702
|
+
kind: "minor",
|
|
703
|
+
mode: PLAN_MODE_ID
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
name: COMPLETE_PLAN_TOOL,
|
|
707
|
+
label: "Complete Plan",
|
|
708
|
+
description: "Request exit-or-continue approval in the local Po UI. Requires the user to interact there; this is not a ChatGPT approval dialog.",
|
|
709
|
+
parameters: {
|
|
710
|
+
type: "object",
|
|
711
|
+
properties: {},
|
|
712
|
+
additionalProperties: false
|
|
713
|
+
},
|
|
714
|
+
executionMode: "serial",
|
|
715
|
+
async execute(_toolCallId, _parameters, signal) {
|
|
716
|
+
try {
|
|
717
|
+
if (host.context.selection.state?.["minor-mode"]?.includes(DOOM_VOICE_AUTO_MODE_ID)) throw new Error("UI plan review is unavailable during autonomous Voice. Wait for explicit user direction.");
|
|
718
|
+
const exited = await host.context.client.request({
|
|
719
|
+
kind: "select",
|
|
720
|
+
title: PLAN_REVIEW_TITLE,
|
|
721
|
+
options: PLAN_REVIEW_CHOICES.map((choice) => ({ ...choice }))
|
|
722
|
+
}, signal) === EXIT_PLAN_DECISION;
|
|
723
|
+
await host.context.session.appendCustomEntry("plan-review", { decision: exited ? EXIT_PLAN_DECISION : CONTINUE_PLAN_DECISION });
|
|
724
|
+
await selectPlan(!exited);
|
|
725
|
+
return {
|
|
726
|
+
content: [{
|
|
727
|
+
type: "text",
|
|
728
|
+
text: exited ? PLAN_EXIT_APPROVED_TEXT : PLAN_CONTINUE_TEXT
|
|
729
|
+
}],
|
|
730
|
+
details: { exited }
|
|
731
|
+
};
|
|
732
|
+
} catch (error) {
|
|
733
|
+
return output(error instanceof Error ? error.message : String(error), true);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
],
|
|
738
|
+
hooks: [
|
|
739
|
+
{
|
|
740
|
+
when: {
|
|
741
|
+
state: { "minor-mode": PLAN_MODE_ID },
|
|
742
|
+
attribution: {
|
|
743
|
+
kind: "minor",
|
|
744
|
+
mode: PLAN_MODE_ID
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
event: "before_agent_start",
|
|
748
|
+
async handle(event) {
|
|
749
|
+
const prompt = typeof event.systemPrompt === "string" ? event.systemPrompt : "";
|
|
750
|
+
const directory = plansDirectory();
|
|
751
|
+
const sections = [buildPlanModeBasePrompt(directory, PLAN_SERVER_CAPABILITIES), buildFlavorPlanningPrompt(flavor, directory, debugEvidence)];
|
|
752
|
+
const saved = await latestEntry(PLAN_DOCUMENT);
|
|
753
|
+
if (saved !== void 0 && saved !== null && typeof saved === "object" && "path" in saved) {
|
|
754
|
+
const document = saved;
|
|
755
|
+
let content = document.content ?? "";
|
|
756
|
+
try {
|
|
757
|
+
content = await readFile(document.path, "utf8");
|
|
758
|
+
} catch {
|
|
759
|
+
content = document.content ?? "";
|
|
760
|
+
}
|
|
761
|
+
if (content.trim()) sections.push(`[CURRENT PLAN]\nSource: ${document.path}\n\n${content}`);
|
|
762
|
+
}
|
|
763
|
+
return { systemPrompt: `${prompt}\n\n${sections.join("\n\n")}`.trim() };
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
event: "session_start",
|
|
768
|
+
async handle() {
|
|
769
|
+
await restorePlanState();
|
|
770
|
+
const pointer = pointers.read(host.context.sessionId);
|
|
771
|
+
if (pointer) host.context.client.setStatus(PLAN_STATUS_KEY, formatPlanStatus(pointer.title, planStampOf(pointer.writtenAt)));
|
|
772
|
+
if (!selected()) await restoreModel();
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
event: "context",
|
|
777
|
+
async handle(event) {
|
|
778
|
+
if (!Array.isArray(event.messages)) return;
|
|
779
|
+
return { messages: event.messages.filter((message) => message.customType !== PLAN_MODE_CONTEXT) };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
]
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const DEFAULT_PLAN_TITLE = "implementation-plan";
|
|
786
|
+
const PLAN_TITLE_MAX_LENGTH = 64;
|
|
787
|
+
const DEBUG_EVIDENCE_MAX_BYTES = 32768;
|
|
788
|
+
const DEBUG_EVIDENCE_MAX_TEXT_BYTES = 4096;
|
|
789
|
+
const DEBUG_EVIDENCE_MAX_ITEMS = 32;
|
|
790
|
+
const PLAN_MODE_CONTEXT = "agent-harness-plan-mode-context";
|
|
791
|
+
const PLAN_EXIT_APPROVED_TEXT = "The user approved exiting plan mode. Full tool access is restored. Begin implementing the approved plan.";
|
|
792
|
+
const PLAN_CONTINUE_TEXT = "The user chose to continue planning. Remain read-only and refine the plan.";
|
|
793
|
+
/** The plan's own heading, as the status line and the pointer record word it. */
|
|
794
|
+
function planTitleOf(markdown) {
|
|
795
|
+
return markdown.match(/^#\s+(.+?)\s*$/m)?.[1] ?? DEFAULT_PLAN_TITLE;
|
|
796
|
+
}
|
|
797
|
+
function planTitleSlug(markdown) {
|
|
798
|
+
return planTitleOf(markdown).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, PLAN_TITLE_MAX_LENGTH) || DEFAULT_PLAN_TITLE;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* How the status line words when a plan was written. A malformed timestamp is
|
|
802
|
+
* shown as it came rather than as an invalid date: the stamp's other job is to
|
|
803
|
+
* change on a rewrite, and it can still do that.
|
|
804
|
+
*/
|
|
805
|
+
function planStampOf(writtenAt) {
|
|
806
|
+
const at = new Date(writtenAt);
|
|
807
|
+
return Number.isNaN(at.getTime()) ? writtenAt : at.toLocaleTimeString();
|
|
808
|
+
}
|
|
809
|
+
function visiblePlanForToolCall(entries, toolCallId) {
|
|
810
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
811
|
+
const entry = entries[index];
|
|
812
|
+
if (!entry || typeof entry !== "object") continue;
|
|
813
|
+
const candidate = entry;
|
|
814
|
+
if (candidate.type !== "message" || candidate.message?.role !== "assistant") continue;
|
|
815
|
+
if (!Array.isArray(candidate.message.content)) continue;
|
|
816
|
+
const toolIndex = candidate.message.content.findIndex((block) => {
|
|
817
|
+
if (!block || typeof block !== "object") return false;
|
|
818
|
+
const content = block;
|
|
819
|
+
return content.type === "toolCall" && content.id === toolCallId && content.name === WRITE_PLAN_TOOL;
|
|
820
|
+
});
|
|
821
|
+
if (toolIndex < 0) continue;
|
|
822
|
+
return candidate.message.content.slice(0, toolIndex).flatMap((block) => {
|
|
823
|
+
if (!block || typeof block !== "object") return [];
|
|
824
|
+
const content = block;
|
|
825
|
+
return content.type === "text" && typeof content.text === "string" ? [content.text] : [];
|
|
826
|
+
}).join("\n\n").trim() || void 0;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function isRecord(value) {
|
|
830
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
831
|
+
}
|
|
832
|
+
function isString(value) {
|
|
833
|
+
return typeof value === "string";
|
|
834
|
+
}
|
|
835
|
+
function boundedDebugText(value, field, required) {
|
|
836
|
+
if (!isString(value)) throw new Error(`${field} must be a string.`);
|
|
837
|
+
const text = value.trim();
|
|
838
|
+
if (required && !text) throw new Error(`${field} must not be empty.`);
|
|
839
|
+
if (Buffer.byteLength(text, "utf8") > DEBUG_EVIDENCE_MAX_TEXT_BYTES) throw new Error(`${field} exceeds the bounded debug evidence limit.`);
|
|
840
|
+
return text;
|
|
841
|
+
}
|
|
842
|
+
function boundedDebugList(value, field) {
|
|
843
|
+
if (!Array.isArray(value) || value.length > DEBUG_EVIDENCE_MAX_ITEMS) throw new Error(`${field} must be a bounded string array.`);
|
|
844
|
+
return value.map((item, index) => boundedDebugText(item, `${field}[${index}]`, false));
|
|
845
|
+
}
|
|
846
|
+
function optionalDebugText(value, field) {
|
|
847
|
+
return value === void 0 ? "" : boundedDebugText(value, field, false);
|
|
848
|
+
}
|
|
849
|
+
function optionalDebugList(value, field) {
|
|
850
|
+
return value === void 0 ? [] : boundedDebugList(value, field);
|
|
851
|
+
}
|
|
852
|
+
function assertExactObjectKeys(value, keys, field) {
|
|
853
|
+
const allowed = new Set(keys);
|
|
854
|
+
const unexpected = Object.keys(value).find((key) => !allowed.has(key));
|
|
855
|
+
if (unexpected) throw new Error(`${field} contains unsupported field '${unexpected}'.`);
|
|
856
|
+
}
|
|
857
|
+
function parseDebugEvidencePacket(value) {
|
|
858
|
+
if (!isRecord(value)) throw new Error("Debug evidence must be an object.");
|
|
859
|
+
assertExactObjectKeys(value, [
|
|
860
|
+
"issue",
|
|
861
|
+
"expectedBehavior",
|
|
862
|
+
"reproductionAttempt",
|
|
863
|
+
"actualBehavior",
|
|
864
|
+
"logs",
|
|
865
|
+
"correlatedTraceEvidence",
|
|
866
|
+
"processOutput",
|
|
867
|
+
"browserConsoleEvidence",
|
|
868
|
+
"correlationIds",
|
|
869
|
+
"timestamps",
|
|
870
|
+
"verifiedFacts",
|
|
871
|
+
"hypotheses",
|
|
872
|
+
"unavailableEvidence"
|
|
873
|
+
], "Debug evidence");
|
|
874
|
+
const packet = {
|
|
875
|
+
issue: boundedDebugText(value.issue, "issue", true),
|
|
876
|
+
expectedBehavior: optionalDebugText(value.expectedBehavior, "expectedBehavior"),
|
|
877
|
+
reproductionAttempt: optionalDebugText(value.reproductionAttempt, "reproductionAttempt"),
|
|
878
|
+
actualBehavior: optionalDebugText(value.actualBehavior, "actualBehavior"),
|
|
879
|
+
logs: optionalDebugList(value.logs, "logs"),
|
|
880
|
+
correlatedTraceEvidence: optionalDebugList(value.correlatedTraceEvidence, "correlatedTraceEvidence"),
|
|
881
|
+
processOutput: optionalDebugList(value.processOutput, "processOutput"),
|
|
882
|
+
browserConsoleEvidence: optionalDebugList(value.browserConsoleEvidence, "browserConsoleEvidence"),
|
|
883
|
+
correlationIds: optionalDebugList(value.correlationIds, "correlationIds"),
|
|
884
|
+
timestamps: optionalDebugList(value.timestamps, "timestamps"),
|
|
885
|
+
verifiedFacts: optionalDebugList(value.verifiedFacts, "verifiedFacts"),
|
|
886
|
+
hypotheses: optionalDebugList(value.hypotheses, "hypotheses"),
|
|
887
|
+
unavailableEvidence: optionalDebugList(value.unavailableEvidence, "unavailableEvidence")
|
|
888
|
+
};
|
|
889
|
+
if (Buffer.byteLength(JSON.stringify(packet), "utf8") > DEBUG_EVIDENCE_MAX_BYTES) throw new Error("Debug evidence exceeds the bounded packet size.");
|
|
890
|
+
return packet;
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region src/extensions/workspaces/sessions/(backend)/root.server.ts
|
|
894
|
+
var root_server_default = defineRoot(({ agent }) => {
|
|
895
|
+
const value = {
|
|
896
|
+
api: [api],
|
|
897
|
+
...agent ? createPlanServerSession(agent) : {}
|
|
898
|
+
};
|
|
899
|
+
return {
|
|
900
|
+
value,
|
|
901
|
+
services: [...value.services ?? [], ...value.tools?.length ? [mountMcpTools(value.tools)] : []]
|
|
902
|
+
};
|
|
903
|
+
});
|
|
904
|
+
//#endregion
|
|
905
|
+
//#region src/extensions/workspaces/sessions/(backend)/api/plans/route.server.ts
|
|
906
|
+
var route_server_default = defineRoute((context) => context.root.api[0]);
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/beforeAgentStart.server.ts
|
|
909
|
+
var beforeAgentStart_server_default = defineRoutedContribution((context) => context.root.hooks?.[0], { cardinality: "optional" });
|
|
910
|
+
//#endregion
|
|
911
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/context.server.ts
|
|
912
|
+
var context_server_default = defineRoutedContribution((context) => context.root.hooks?.[2], { cardinality: "optional" });
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/sessionStart.server.ts
|
|
915
|
+
var sessionStart_server_default = defineRoutedContribution((context) => context.root.hooks?.[1], { cardinality: "optional" });
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/extensions/workspaces/sessions/(backend)/resource/poUsePlan.server.ts
|
|
918
|
+
var poUsePlan_server_default = defineRoutedContribution((context) => context.root.resources?.[0], { cardinality: "optional" });
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/extensions/workspaces/sessions/(backend)/tool-restriction/plan.server.ts
|
|
921
|
+
var plan_server_default = defineToolRestriction({
|
|
922
|
+
when: { state: { "minor-mode": DOOM_VOICE_AUTO_MODE_ID } },
|
|
923
|
+
excludedTools: ["complete_plan"]
|
|
924
|
+
});
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/extensions/workspaces/sessions/(backend)/tool-restriction/planReadOnly.server.ts
|
|
927
|
+
var planReadOnly_server_default = defineToolRestriction({
|
|
928
|
+
when: { state: { "minor-mode": "plan" } },
|
|
929
|
+
excludedTools: ["edit", "write"]
|
|
930
|
+
});
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/extensions/workspaces/sessions/(backend)/tool/completePlan.server.ts
|
|
933
|
+
var completePlan_server_default = defineRoutedContribution((context) => context.root.tools?.[2], { cardinality: "optional" });
|
|
934
|
+
//#endregion
|
|
935
|
+
//#region src/extensions/workspaces/sessions/(backend)/tool/recordDebugEvidence.server.ts
|
|
936
|
+
var recordDebugEvidence_server_default = defineRoutedContribution((context) => context.root.tools?.[0], { cardinality: "optional" });
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/extensions/workspaces/sessions/(backend)/tool/writePlan.server.ts
|
|
939
|
+
var writePlan_server_default = defineRoutedContribution((context) => context.root.tools?.[1], { cardinality: "optional" });
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region generated/server.ts
|
|
942
|
+
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
943
|
+
/* v8 ignore next -- generated normalization branch */
|
|
944
|
+
const defined = (values) => values.filter((value) => value !== void 0);
|
|
945
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
946
|
+
...identity,
|
|
947
|
+
...value
|
|
948
|
+
};
|
|
949
|
+
const facet = defineServerPlugin({
|
|
950
|
+
name: "@agimon-ai/po-plan",
|
|
951
|
+
providerOverrides: composeServerProviderOverrides(root_server_default),
|
|
952
|
+
session: async (context) => {
|
|
953
|
+
const scopeSession = await root_server_default(context);
|
|
954
|
+
const contextSession = {
|
|
955
|
+
...context,
|
|
956
|
+
root: scopeSession.value
|
|
957
|
+
};
|
|
958
|
+
return {
|
|
959
|
+
...composeRootHooks(scopeSession),
|
|
960
|
+
get api() {
|
|
961
|
+
return [at(route_server_default, contextSession)];
|
|
962
|
+
},
|
|
963
|
+
get hooks() {
|
|
964
|
+
return defined([
|
|
965
|
+
via({ event: "before_agent_start" }, at(beforeAgentStart_server_default, contextSession)),
|
|
966
|
+
via({ event: "context" }, at(context_server_default, contextSession)),
|
|
967
|
+
via({ event: "session_start" }, at(sessionStart_server_default, contextSession))
|
|
968
|
+
]);
|
|
969
|
+
},
|
|
970
|
+
get resources() {
|
|
971
|
+
return defined([at(poUsePlan_server_default, contextSession)]);
|
|
972
|
+
},
|
|
973
|
+
get toolRestrictions() {
|
|
974
|
+
return [at(plan_server_default, contextSession), at(planReadOnly_server_default, contextSession)];
|
|
975
|
+
},
|
|
976
|
+
get tools() {
|
|
977
|
+
return defined([
|
|
978
|
+
via({ name: "complete_plan" }, at(completePlan_server_default, contextSession)),
|
|
979
|
+
via({ name: "record_debug_evidence" }, at(recordDebugEvidence_server_default, contextSession)),
|
|
980
|
+
via({ name: "write_plan" }, at(writePlan_server_default, contextSession))
|
|
981
|
+
]);
|
|
982
|
+
},
|
|
983
|
+
services: [...scopeSession.services ?? []],
|
|
984
|
+
get activities() {
|
|
985
|
+
return [...scopeSession.activities ?? []];
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
//#endregion
|
|
991
|
+
export { facet as default, facet };
|
|
992
|
+
|
|
993
|
+
//# sourceMappingURL=server.mjs.map
|