@hyperdreamer/pi-webui 1.11.0-beta.1 → 1.11.0-beta.10
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/README.md +17 -2
- package/dist/cli.js +265 -32
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-DLZCHlMU.js → CodeViewer-CAf_j27u.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-XCb8NlrO.js → UnifiedDiffViewer-wBjEJWa1.js} +1 -1
- package/dist/client/assets/{index-BaDGxg8F.js → index-5EtDb9nj.js} +782 -457
- package/dist/client/index.html +1 -1
- package/dist/config.js +48 -11
- package/dist/config.js.map +1 -1
- package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +1 -0
- package/dist/plugin-api.d.ts +2 -0
- package/dist/server/app.js +6 -1
- package/dist/server/app.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +40 -5
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/realtime/sessionStatusCoalescer.js +137 -0
- package/dist/server/realtime/sessionStatusCoalescer.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +39 -5
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/modelPolicyCapability.js +88 -0
- package/dist/server/sessions/modelPolicyCapability.js.map +1 -0
- package/dist/server/sessions/modelPolicyTool.js +31 -0
- package/dist/server/sessions/modelPolicyTool.js.map +1 -0
- package/dist/server/sessions/modelTierRegistry.js +14 -0
- package/dist/server/sessions/modelTierRegistry.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +1643 -433
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionDefaultsRoutes.js +39 -0
- package/dist/server/sessions/sessionDefaultsRoutes.js.map +1 -1
- package/dist/server/sessions/sessionDefaultsService.js +36 -6
- package/dist/server/sessions/sessionDefaultsService.js.map +1 -1
- package/dist/server/sessions/sessionMetadataStore.js +107 -13
- package/dist/server/sessions/sessionMetadataStore.js.map +1 -1
- package/dist/server/sessions/sessionModelPolicy.js +118 -0
- package/dist/server/sessions/sessionModelPolicy.js.map +1 -0
- package/dist/server/sessions/sessionReorder.js +117 -0
- package/dist/server/sessions/sessionReorder.js.map +1 -0
- package/dist/server/sessions/sessionRouteFastifyOptions.js +8 -0
- package/dist/server/sessions/sessionRouteFastifyOptions.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +208 -3
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js +164 -0
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js.map +1 -0
- package/dist/server/sessions/utilityModelExtension.js +199 -0
- package/dist/server/sessions/utilityModelExtension.js.map +1 -0
- package/dist/server/sessions/utilityModelResolver.js +77 -0
- package/dist/server/sessions/utilityModelResolver.js.map +1 -0
- package/dist/server/sessions/utilityModelSettingsRoutes.js +62 -0
- package/dist/server/sessions/utilityModelSettingsRoutes.js.map +1 -0
- package/dist/server/sessions/utilityModelSettingsService.js +101 -0
- package/dist/server/sessions/utilityModelSettingsService.js.map +1 -0
- package/dist/server/skills/optionalSkillInstall.js +69 -0
- package/dist/server/skills/optionalSkillInstall.js.map +1 -0
- package/dist/server/skills/optionalSkillInstaller.js +148 -0
- package/dist/server/skills/optionalSkillInstaller.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +111 -3
- package/dist/shared/apiTypes.js +9 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +12 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +5 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/assets/pi-webui-desktop.png +0 -0
- package/docs/config.md +38 -3
- package/optional-skills/deterministic-subagent-driven-development/SKILL.md +224 -0
- package/optional-skills/deterministic-subagent-driven-development/pi-webui-skill.json +28 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/final-reviewer.md +132 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/implementer.md +101 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/re-reviewer.md +60 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/task-reviewer.md +80 -0
- package/optional-skills/deterministic-subagent-driven-development/references/capability-contract.md +174 -0
- package/optional-skills/deterministic-subagent-driven-development/references/plan-contract.md +268 -0
- package/optional-skills/deterministic-subagent-driven-development/references/state-machine.md +177 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/manifest.mjs +258 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/plan-policy.mjs +350 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/prompt-renderer.mjs +290 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-machine.mjs +1264 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-store.mjs +532 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state +3 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state.mjs +349 -0
- package/optional-skills/deterministic-writing-plans/SKILL.md +232 -0
- package/optional-skills/deterministic-writing-plans/references/grammar.md +84 -0
- package/optional-skills/deterministic-writing-plans/templates/plan-skeleton.md +143 -0
- package/package.json +12 -9
package/optional-skills/deterministic-subagent-driven-development/scripts/lib/prompt-renderer.mjs
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic prompt rendering.
|
|
3
|
+
*
|
|
4
|
+
* A rendered prompt is dispatch input whose exact bytes are stored in state and
|
|
5
|
+
* reissued verbatim on recovery. Rendering is therefore a pure function of
|
|
6
|
+
* (tier, role, context): no clock, no working directory, no environment, no
|
|
7
|
+
* template evaluation.
|
|
8
|
+
*
|
|
9
|
+
* There is deliberately no general-purpose templating here. A template engine
|
|
10
|
+
* would let a context value introduce a tier label, a heading, or another
|
|
11
|
+
* placeholder, and the prompt is the one artifact a child treats as instructions.
|
|
12
|
+
* Role contracts are static files copied verbatim; context is emitted as a
|
|
13
|
+
* validated, escaped key/value list.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
17
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { TIERS } from "./plan-policy.mjs";
|
|
20
|
+
|
|
21
|
+
/** Maximum bytes for a rendered prompt, matching the dispatch-intent bound. */
|
|
22
|
+
const MAX_PROMPT_BYTES = 384 * 1024;
|
|
23
|
+
|
|
24
|
+
/** Maximum UTF-8 bytes for any single path. */
|
|
25
|
+
const MAX_PATH_BYTES = 4096;
|
|
26
|
+
|
|
27
|
+
/** Maximum finding records the contract allows in one ledger. */
|
|
28
|
+
const MAX_FINDING_IDS = 256;
|
|
29
|
+
|
|
30
|
+
const TIER_SET = new Set(TIERS);
|
|
31
|
+
|
|
32
|
+
/** A rejected render. */
|
|
33
|
+
export class RenderError extends Error {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "RenderError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fail = (message) => {
|
|
41
|
+
throw new RenderError(message);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Role definitions.
|
|
46
|
+
*
|
|
47
|
+
* `paths` and `scalars` are the complete accepted key set for each role. Anything
|
|
48
|
+
* else is rejected rather than ignored, so a typo in a context file fails loudly
|
|
49
|
+
* instead of silently omitting information a child needed.
|
|
50
|
+
*/
|
|
51
|
+
const ROLES = Object.freeze({
|
|
52
|
+
implementer: {
|
|
53
|
+
template: "implementer.md",
|
|
54
|
+
paths: ["briefPath", "reportPath"],
|
|
55
|
+
scalars: ["task"],
|
|
56
|
+
optional: ["contextPath", "findingPackagePath", "baseSha", "headSha", "round"],
|
|
57
|
+
},
|
|
58
|
+
"task-reviewer": {
|
|
59
|
+
template: "task-reviewer.md",
|
|
60
|
+
paths: ["briefPath", "reportPath"],
|
|
61
|
+
scalars: ["task"],
|
|
62
|
+
optional: ["baseSha", "headSha", "reviewPackagePath"],
|
|
63
|
+
},
|
|
64
|
+
"re-reviewer": {
|
|
65
|
+
template: "re-reviewer.md",
|
|
66
|
+
paths: ["briefPath", "reportPath"],
|
|
67
|
+
scalars: ["task"],
|
|
68
|
+
optional: ["baseSha", "headSha", "reviewPackagePath", "findingIds", "round"],
|
|
69
|
+
},
|
|
70
|
+
"final-reviewer": {
|
|
71
|
+
template: "final-reviewer.md",
|
|
72
|
+
paths: ["briefPath", "reportPath"],
|
|
73
|
+
scalars: ["task"],
|
|
74
|
+
optional: ["baseSha", "headSha", "reviewPackagePath", "findingIds", "ledgerPath"],
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Keys every role pins, naming the roots that confine every other path. */
|
|
79
|
+
const ROOT_KEYS = Object.freeze(["worktree", "runRoot"]);
|
|
80
|
+
|
|
81
|
+
/** Emission order. Fixed, because byte-exact output cannot depend on key order. */
|
|
82
|
+
const FIELD_ORDER = Object.freeze([
|
|
83
|
+
"worktree",
|
|
84
|
+
"runRoot",
|
|
85
|
+
"task",
|
|
86
|
+
"round",
|
|
87
|
+
"baseSha",
|
|
88
|
+
"headSha",
|
|
89
|
+
"briefPath",
|
|
90
|
+
"contextPath",
|
|
91
|
+
"reportPath",
|
|
92
|
+
"reviewPackagePath",
|
|
93
|
+
"findingPackagePath",
|
|
94
|
+
"ledgerPath",
|
|
95
|
+
"findingIds",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
99
|
+
const FINDING_ID = /^[A-Za-z0-9._:-]{1,64}$/u;
|
|
100
|
+
|
|
101
|
+
/** Reject control characters anywhere in a rendered value. */
|
|
102
|
+
const requireClean = (value, field) => {
|
|
103
|
+
// eslint-disable-next-line no-control-regex -- rejecting these is the point.
|
|
104
|
+
if (/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
105
|
+
fail(`${field} must not contain a control character`);
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validate a path: absolute, normalized, bounded, and confined to a pinned root.
|
|
112
|
+
*
|
|
113
|
+
* When the path already exists its real path is checked too, so a symlink cannot
|
|
114
|
+
* point a "confined" path at content outside the run. For an output path the real
|
|
115
|
+
* parent is checked, since the file itself will not exist yet.
|
|
116
|
+
*/
|
|
117
|
+
const validatePath = (value, field, roots, { mustExist = false } = {}) => {
|
|
118
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
119
|
+
fail(`${field} must be a non-empty string`);
|
|
120
|
+
}
|
|
121
|
+
requireClean(value, field);
|
|
122
|
+
if (Buffer.byteLength(value, "utf8") > MAX_PATH_BYTES) {
|
|
123
|
+
fail(`${field} exceeds ${String(MAX_PATH_BYTES)} bytes`);
|
|
124
|
+
}
|
|
125
|
+
if (!isAbsolute(value)) fail(`${field} must be an absolute path: ${value}`);
|
|
126
|
+
if (value.split("/").includes("..")) {
|
|
127
|
+
fail(`${field} must be normalized and contain no ".." segment`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const within = (candidate) =>
|
|
131
|
+
roots.some((root) => candidate === root || candidate.startsWith(`${root}/`));
|
|
132
|
+
|
|
133
|
+
if (!within(resolve(value))) {
|
|
134
|
+
fail(`${field} must live beneath a pinned root: ${value}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let real;
|
|
138
|
+
try {
|
|
139
|
+
real = realpathSync(value);
|
|
140
|
+
} catch {
|
|
141
|
+
real = null;
|
|
142
|
+
}
|
|
143
|
+
if (real === null && mustExist) fail(`${field} does not exist: ${value}`);
|
|
144
|
+
if (real !== null && !within(real)) {
|
|
145
|
+
fail(`${field} resolves outside the pinned roots via a symlink: ${value} -> ${real}`);
|
|
146
|
+
}
|
|
147
|
+
if (real === null) {
|
|
148
|
+
// The file may legitimately not exist yet; its parent still must not escape.
|
|
149
|
+
let parentReal;
|
|
150
|
+
try {
|
|
151
|
+
parentReal = realpathSync(dirname(value));
|
|
152
|
+
} catch {
|
|
153
|
+
fail(`${field} names a directory that does not exist: ${dirname(value)}`);
|
|
154
|
+
}
|
|
155
|
+
if (!within(parentReal)) {
|
|
156
|
+
fail(`${field} has a real parent outside the pinned roots: ${parentReal}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Render one context value as a single line. */
|
|
163
|
+
const renderValue = (key, value) => {
|
|
164
|
+
if (Array.isArray(value)) return `- ${key}: ${value.join(", ")}`;
|
|
165
|
+
if (typeof value === "number") return `- ${key}: ${String(value)}`;
|
|
166
|
+
return `- ${key}: ${value}`;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate a context object against its role.
|
|
171
|
+
*
|
|
172
|
+
* Returns the accepted subset in a fixed emission order.
|
|
173
|
+
*/
|
|
174
|
+
const validateContext = (role, context) => {
|
|
175
|
+
const definition = ROLES[role];
|
|
176
|
+
const accepted = new Set([
|
|
177
|
+
...ROOT_KEYS,
|
|
178
|
+
...definition.paths,
|
|
179
|
+
...definition.scalars,
|
|
180
|
+
...definition.optional,
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const unexpected = Object.keys(context).filter((key) => !accepted.has(key));
|
|
184
|
+
if (unexpected.length > 0) {
|
|
185
|
+
fail(`unexpected context key(s) for role ${role}: ${unexpected.join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const key of ROOT_KEYS) {
|
|
189
|
+
const value = context[key];
|
|
190
|
+
if (typeof value !== "string" || !isAbsolute(value)) {
|
|
191
|
+
fail(`${key} must be a pinned absolute path`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const roots = ROOT_KEYS.map((key) => resolve(context[key]));
|
|
195
|
+
|
|
196
|
+
for (const key of definition.paths) {
|
|
197
|
+
if (context[key] === undefined) fail(`role ${role} requires ${key}`);
|
|
198
|
+
validatePath(context[key], key, roots);
|
|
199
|
+
}
|
|
200
|
+
for (const key of definition.scalars) {
|
|
201
|
+
const value = context[key];
|
|
202
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
203
|
+
fail(`${key} must be a positive integer for role ${role}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const key of definition.optional) {
|
|
208
|
+
const value = context[key];
|
|
209
|
+
if (value === undefined) continue;
|
|
210
|
+
if (key.endsWith("Path")) {
|
|
211
|
+
validatePath(value, key, roots);
|
|
212
|
+
} else if (key === "baseSha" || key === "headSha") {
|
|
213
|
+
if (typeof value !== "string" || !SHA.test(value)) {
|
|
214
|
+
fail(`${key} must be a 40-character lowercase hex object name`);
|
|
215
|
+
}
|
|
216
|
+
} else if (key === "round") {
|
|
217
|
+
if (!Number.isInteger(value) || value < 1 || value > 5) {
|
|
218
|
+
fail("round must be an integer from 1 to 5");
|
|
219
|
+
}
|
|
220
|
+
} else if (key === "findingIds") {
|
|
221
|
+
if (!Array.isArray(value)) fail("findingIds must be an array");
|
|
222
|
+
if (value.length > MAX_FINDING_IDS) {
|
|
223
|
+
fail(
|
|
224
|
+
`findingIds holds ${String(value.length)} entries, exceeding the ${String(MAX_FINDING_IDS)}-finding bound`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
for (const [index, id] of value.entries()) {
|
|
228
|
+
if (typeof id !== "string" || !FINDING_ID.test(id)) {
|
|
229
|
+
fail(`findingIds[${String(index)}] must match ^[A-Za-z0-9._:-]{1,64}$`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
fail(`no validation rule for optional key ${key}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return FIELD_ORDER.filter((key) => context[key] !== undefined).map((key) => [key, context[key]]);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Render a prompt.
|
|
242
|
+
*
|
|
243
|
+
* Byte layout, fixed by contract because dispatch stores and reissues these bytes:
|
|
244
|
+
*
|
|
245
|
+
* Model tier: <tier>\n
|
|
246
|
+
* \n
|
|
247
|
+
* <role contract, trailing whitespace trimmed>\n
|
|
248
|
+
* \n
|
|
249
|
+
* ## Dispatch Context\n\n<one "- key: value" line each>\n
|
|
250
|
+
* \n
|
|
251
|
+
* ## Return Channel\n\n<two lines>\n
|
|
252
|
+
*/
|
|
253
|
+
export function renderPrompt({ tier, role, context, skillRoot }) {
|
|
254
|
+
if (!TIER_SET.has(tier)) {
|
|
255
|
+
fail(`unknown tier: ${String(tier)} (expected one of ${TIERS.join(", ")})`);
|
|
256
|
+
}
|
|
257
|
+
const definition = ROLES[role];
|
|
258
|
+
if (definition === undefined) {
|
|
259
|
+
fail(`unknown role: ${String(role)} (expected one of ${Object.keys(ROLES).join(", ")})`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const fields = validateContext(role, context);
|
|
263
|
+
const template = readFileSync(join(skillRoot, "prompts", definition.template), "utf8");
|
|
264
|
+
|
|
265
|
+
const rendered = [
|
|
266
|
+
`Model tier: ${tier}`,
|
|
267
|
+
"",
|
|
268
|
+
template.trimEnd(),
|
|
269
|
+
"",
|
|
270
|
+
"## Dispatch Context",
|
|
271
|
+
"",
|
|
272
|
+
...fields.map(([key, value]) => renderValue(key, value)),
|
|
273
|
+
"",
|
|
274
|
+
"## Return Channel",
|
|
275
|
+
"",
|
|
276
|
+
`Write exactly one report at ${context.reportPath}.`,
|
|
277
|
+
"Return exactly one status token defined by your role contract above.",
|
|
278
|
+
"",
|
|
279
|
+
].join("\n");
|
|
280
|
+
|
|
281
|
+
if (Buffer.byteLength(rendered, "utf8") > MAX_PROMPT_BYTES) {
|
|
282
|
+
fail(
|
|
283
|
+
`rendered prompt is ${String(Buffer.byteLength(rendered, "utf8"))} bytes, exceeding ${String(MAX_PROMPT_BYTES)} (384 KiB)`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (rendered.includes("{{")) {
|
|
287
|
+
fail("rendered prompt contains an unresolved template marker");
|
|
288
|
+
}
|
|
289
|
+
return rendered;
|
|
290
|
+
}
|