@hobin/developer 0.1.4 → 0.1.6
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 +174 -39
- package/extensions/developer.ts +827 -123
- package/extensions/machine.ts +344 -0
- package/extensions/state.ts +242 -100
- package/extensions/tool-policy.ts +54 -25
- package/extensions/tui.ts +598 -155
- package/package.json +7 -2
- package/skills/abstraction-review/SKILL.md +6 -5
- package/skills/adversarial-eval/SKILL.md +7 -3
- package/skills/model/SKILL.md +13 -4
- package/skills/naming-judgment/SKILL.md +9 -3
- package/skills/schedule/SKILL.md +6 -3
- package/skills/signal/SKILL.md +6 -3
- package/skills/sketch/SKILL.md +29 -10
- package/skills/specify/SKILL.md +11 -2
- package/skills/verify/SKILL.md +10 -4
- package/skills/visualize/SKILL.md +5 -3
package/extensions/developer.ts
CHANGED
|
@@ -6,47 +6,69 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_MAX_BYTES,
|
|
8
8
|
DEFAULT_MAX_LINES,
|
|
9
|
+
getMarkdownTheme,
|
|
9
10
|
keyHint,
|
|
10
11
|
type ExtensionAPI,
|
|
11
12
|
type ExtensionCommandContext,
|
|
12
13
|
type ExtensionContext,
|
|
13
14
|
type Skill,
|
|
15
|
+
type Theme,
|
|
16
|
+
type ThemeColor,
|
|
14
17
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
16
|
-
import { Type } from "typebox";
|
|
18
|
+
import { Container, Markdown, Text } from "@earendil-works/pi-tui";
|
|
19
|
+
import { Type, type Static } from "typebox";
|
|
17
20
|
|
|
18
21
|
import { availablePackageSkills, renderSkillMethod } from "./skills.ts";
|
|
19
22
|
import {
|
|
20
23
|
FOCUS_ENTRY,
|
|
21
24
|
JUDGMENT_TOOL,
|
|
25
|
+
MAX_RESPONSE_FIELDS,
|
|
26
|
+
MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
27
|
+
MAX_RESPONSE_OPTIONS,
|
|
28
|
+
MAX_RESPONSE_TEXT_CHARS,
|
|
22
29
|
MODE_ENTRY,
|
|
23
30
|
PROTOCOL,
|
|
24
31
|
ROUTE_TOOL,
|
|
25
32
|
applyDeveloperEvent,
|
|
33
|
+
canApplyDeveloperEvent,
|
|
34
|
+
developerSnapshot,
|
|
26
35
|
initialState,
|
|
27
36
|
normalizeDeveloperEvent,
|
|
37
|
+
parseChoiceResponseSpec,
|
|
28
38
|
protocolState,
|
|
29
39
|
reconstructState,
|
|
40
|
+
type ChoiceResponseSpec,
|
|
30
41
|
type DeveloperMode,
|
|
31
42
|
type DeveloperState,
|
|
32
43
|
type DirectExecutionProfile,
|
|
33
44
|
type FocusEvent,
|
|
34
45
|
type JudgmentEvent,
|
|
35
46
|
type ModeEvent,
|
|
47
|
+
type PendingQuestion,
|
|
48
|
+
type PendingQuestionStatus,
|
|
49
|
+
type QuestionGate,
|
|
50
|
+
type QuestionResolutionOwner,
|
|
51
|
+
type QuestionUpdate,
|
|
52
|
+
type QuestionUpdateStatus,
|
|
53
|
+
type RouteAlternative,
|
|
36
54
|
type RouteEvent,
|
|
37
55
|
} from "./state.ts";
|
|
38
56
|
import {
|
|
39
|
-
|
|
57
|
+
builtinControlledToolCapabilities,
|
|
58
|
+
isControlledToolAllowed,
|
|
40
59
|
reconcileProtocolTools,
|
|
60
|
+
type ProtocolToolAccess,
|
|
41
61
|
type ToolPolicyMemory,
|
|
42
62
|
} from "./tool-policy.ts";
|
|
43
63
|
import {
|
|
44
64
|
DeveloperWidget,
|
|
45
|
-
|
|
65
|
+
editQuestionResolutionRequest,
|
|
66
|
+
promptImmediateUserQuestion,
|
|
46
67
|
renderDeveloperFooter,
|
|
47
68
|
showDeveloperActionSelector,
|
|
48
69
|
showDeveloperStatus,
|
|
49
70
|
showPendingQuestionSelector,
|
|
71
|
+
type ImmediateQuestionDisposition,
|
|
50
72
|
} from "./tui.ts";
|
|
51
73
|
|
|
52
74
|
const PROTOCOL_TOOLS = [ROUTE_TOOL, JUDGMENT_TOOL] as const;
|
|
@@ -59,8 +81,9 @@ const structuralChangeMethodPath = resolve(
|
|
|
59
81
|
);
|
|
60
82
|
const MAX_PENDING_QUESTIONS = 20;
|
|
61
83
|
const MAX_QUESTION_CHARS = 2_000;
|
|
84
|
+
const MAX_QUESTION_CONTEXT_CHARS = 8_000;
|
|
62
85
|
const MAX_EVIDENCE_CHARS = 2_000;
|
|
63
|
-
const MAX_RESULT_CHARS =
|
|
86
|
+
const MAX_RESULT_CHARS = 12_000;
|
|
64
87
|
const MAX_ARTIFACT_CHARS = 4_096;
|
|
65
88
|
const DEVELOPER_COMMAND_ACTIONS = ["on", "strict", "status", "questions", "off"] as const;
|
|
66
89
|
|
|
@@ -110,7 +133,211 @@ function compactLine(value: string, maxChars = 160): string {
|
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
function normalizedQuestion(value: string): string {
|
|
113
|
-
return value
|
|
136
|
+
return value
|
|
137
|
+
.toLocaleLowerCase()
|
|
138
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
139
|
+
.trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface ChoiceResponseOptionInput {
|
|
143
|
+
value: string;
|
|
144
|
+
label: string;
|
|
145
|
+
description?: string;
|
|
146
|
+
detail_prompt?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface ChoiceResponseFieldInput {
|
|
150
|
+
id: string;
|
|
151
|
+
prompt: string;
|
|
152
|
+
description?: string;
|
|
153
|
+
options: ChoiceResponseOptionInput[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface ChoiceResponseSpecInput {
|
|
157
|
+
kind: "choice-form";
|
|
158
|
+
fields: ChoiceResponseFieldInput[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface OpenQuestionInput {
|
|
162
|
+
question: string;
|
|
163
|
+
context?: string;
|
|
164
|
+
response_spec?: ChoiceResponseSpecInput;
|
|
165
|
+
status: PendingQuestionStatus;
|
|
166
|
+
resolution_owner: Exclude<QuestionResolutionOwner, "unknown">;
|
|
167
|
+
gate: QuestionGate;
|
|
168
|
+
resolution_criteria: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface QuestionUpdateInput {
|
|
172
|
+
question_id: string;
|
|
173
|
+
status: QuestionUpdateStatus;
|
|
174
|
+
result: string;
|
|
175
|
+
basis: string[];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function legacyOpenQuestion(question: string, judgmentStatus?: string): OpenQuestionInput {
|
|
179
|
+
if (judgmentStatus === "blocked") {
|
|
180
|
+
return {
|
|
181
|
+
question,
|
|
182
|
+
status: "blocked",
|
|
183
|
+
resolution_owner: "environment",
|
|
184
|
+
gate: "before-completion",
|
|
185
|
+
resolution_criteria: `Obtain evidence that settles: ${question}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
question,
|
|
190
|
+
status: "open",
|
|
191
|
+
resolution_owner: "agent",
|
|
192
|
+
gate: "none",
|
|
193
|
+
resolution_criteria: `Obtain evidence that settles: ${question}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildChoiceResponseSpec(
|
|
198
|
+
input: ChoiceResponseSpecInput | undefined,
|
|
199
|
+
owner: OpenQuestionInput["resolution_owner"],
|
|
200
|
+
): ChoiceResponseSpec | undefined {
|
|
201
|
+
if (!input) return undefined;
|
|
202
|
+
if (owner !== "user") {
|
|
203
|
+
fail("response_spec is valid only for user-owned questions.");
|
|
204
|
+
}
|
|
205
|
+
const responseSpec = parseChoiceResponseSpec({
|
|
206
|
+
kind: input.kind,
|
|
207
|
+
fields: input.fields.map((field) => ({
|
|
208
|
+
id: field.id,
|
|
209
|
+
prompt: field.prompt,
|
|
210
|
+
description: field.description,
|
|
211
|
+
options: field.options.map((option) => ({
|
|
212
|
+
value: option.value,
|
|
213
|
+
label: option.label,
|
|
214
|
+
description: option.description,
|
|
215
|
+
detailPrompt: option.detail_prompt,
|
|
216
|
+
})),
|
|
217
|
+
})),
|
|
218
|
+
});
|
|
219
|
+
if (!responseSpec) {
|
|
220
|
+
fail(
|
|
221
|
+
"response_spec must use unique field IDs and unique option values with non-whitespace text.",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return responseSpec;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildOpenedQuestions(
|
|
228
|
+
input: OpenQuestionInput[],
|
|
229
|
+
state: DeveloperState,
|
|
230
|
+
routeId: string,
|
|
231
|
+
): PendingQuestion[] {
|
|
232
|
+
const questionIds = new Map(
|
|
233
|
+
state.pendingQuestions.map((question) => [normalizedQuestion(question.question), question.id]),
|
|
234
|
+
);
|
|
235
|
+
const openedQuestions: PendingQuestion[] = [];
|
|
236
|
+
for (const [index, item] of input.entries()) {
|
|
237
|
+
const question = item.question.trim();
|
|
238
|
+
const context = item.context?.trim() || undefined;
|
|
239
|
+
const responseSpec = buildChoiceResponseSpec(item.response_spec, item.resolution_owner);
|
|
240
|
+
const resolutionCriteria = item.resolution_criteria.trim();
|
|
241
|
+
if (!question || !resolutionCriteria) {
|
|
242
|
+
fail("Each open question requires non-whitespace question and resolution_criteria text.");
|
|
243
|
+
}
|
|
244
|
+
const questionKey = normalizedQuestion(question);
|
|
245
|
+
const questionId = questionIds.get(questionKey) ?? `question:${routeId}:open:${index + 1}`;
|
|
246
|
+
questionIds.set(questionKey, questionId);
|
|
247
|
+
const pendingQuestion: PendingQuestion = {
|
|
248
|
+
id: questionId,
|
|
249
|
+
question,
|
|
250
|
+
context,
|
|
251
|
+
responseSpec,
|
|
252
|
+
status: item.status,
|
|
253
|
+
resolutionOwner: item.resolution_owner,
|
|
254
|
+
gate: item.gate,
|
|
255
|
+
resolutionCriteria,
|
|
256
|
+
sourceRouteId: routeId,
|
|
257
|
+
};
|
|
258
|
+
const duplicateIndex = openedQuestions.findIndex((candidate) => candidate.id === questionId);
|
|
259
|
+
if (duplicateIndex === -1) openedQuestions.push(pendingQuestion);
|
|
260
|
+
else openedQuestions[duplicateIndex] = pendingQuestion;
|
|
261
|
+
}
|
|
262
|
+
return openedQuestions;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function firstImmediateUserQuestion(
|
|
266
|
+
previous: readonly PendingQuestion[],
|
|
267
|
+
opened: readonly PendingQuestion[],
|
|
268
|
+
): PendingQuestion | undefined {
|
|
269
|
+
const previousIds = new Set(previous.map((question) => question.id));
|
|
270
|
+
return opened.find(
|
|
271
|
+
(question) =>
|
|
272
|
+
!previousIds.has(question.id) &&
|
|
273
|
+
question.status === "open" &&
|
|
274
|
+
question.resolutionOwner === "user" &&
|
|
275
|
+
question.gate === "before-direct",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function immediateQuestionMessage(
|
|
280
|
+
question: PendingQuestion | undefined,
|
|
281
|
+
disposition: ImmediateQuestionDisposition | undefined,
|
|
282
|
+
): string {
|
|
283
|
+
if (!question) return "";
|
|
284
|
+
if (disposition?.kind === "answer") {
|
|
285
|
+
return [
|
|
286
|
+
"",
|
|
287
|
+
`Immediate user answer for ${question.id}:`,
|
|
288
|
+
disposition.request,
|
|
289
|
+
"",
|
|
290
|
+
`Route this exact question with open_question_id=${question.id}, use the answer as new evidence, and close it only through question_updates.`,
|
|
291
|
+
].join("\n");
|
|
292
|
+
}
|
|
293
|
+
return `\nThe user left ${question.id} open for later.`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildQuestionUpdates(
|
|
297
|
+
input: QuestionUpdateInput[],
|
|
298
|
+
state: DeveloperState,
|
|
299
|
+
): QuestionUpdate[] {
|
|
300
|
+
const questionUpdates = input.map((update) => ({
|
|
301
|
+
questionId: update.question_id.trim(),
|
|
302
|
+
status: update.status,
|
|
303
|
+
result: update.result.trim(),
|
|
304
|
+
basis: update.basis.map((item) => item.trim()).filter(Boolean),
|
|
305
|
+
}));
|
|
306
|
+
const knownQuestionIds = new Set(state.pendingQuestions.map((question) => question.id));
|
|
307
|
+
const updatedIds = new Set<string>();
|
|
308
|
+
for (const update of questionUpdates) {
|
|
309
|
+
if (!knownQuestionIds.has(update.questionId))
|
|
310
|
+
fail(`Unknown pending question ID: ${update.questionId}`);
|
|
311
|
+
if (updatedIds.has(update.questionId))
|
|
312
|
+
fail(`Question ${update.questionId} was updated more than once.`);
|
|
313
|
+
updatedIds.add(update.questionId);
|
|
314
|
+
if (!update.result) fail(`Question update ${update.questionId} requires a non-empty result.`);
|
|
315
|
+
if (
|
|
316
|
+
(update.status === "resolved" ||
|
|
317
|
+
update.status === "not-applicable" ||
|
|
318
|
+
update.status === "blocked") &&
|
|
319
|
+
update.basis.length === 0
|
|
320
|
+
) {
|
|
321
|
+
fail(
|
|
322
|
+
`Question update ${update.questionId} with status ${update.status} requires concrete basis.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return questionUpdates;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function judgmentNextMessage(event: JudgmentEvent, nextState: DeveloperState): string {
|
|
330
|
+
if (event.owner === "direct") {
|
|
331
|
+
if (nextState.verificationRequired) {
|
|
332
|
+
return "Stable landing recorded. Route again from the new evidence; verify is required before claiming completion.";
|
|
333
|
+
}
|
|
334
|
+
return "Stable landing recorded. Route again from the new evidence before selecting another movement.";
|
|
335
|
+
}
|
|
336
|
+
const next = protocolState(nextState);
|
|
337
|
+
if (next === "idle") {
|
|
338
|
+
return "Developer protocol is idle. This is routing state only and does not prove task completion.";
|
|
339
|
+
}
|
|
340
|
+
return `Developer protocol is ${next}. Address the current routing obligation before handoff.`;
|
|
114
341
|
}
|
|
115
342
|
|
|
116
343
|
function inferredQuestionId(
|
|
@@ -124,8 +351,16 @@ function inferredQuestionId(
|
|
|
124
351
|
const exact = state.pendingQuestions.filter(
|
|
125
352
|
(pending) => normalizedQuestion(pending.question) === normalized,
|
|
126
353
|
);
|
|
127
|
-
|
|
128
|
-
|
|
354
|
+
return exact.length === 1 ? exact[0]?.id : undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function protocolToolAccess(state: DeveloperState): ProtocolToolAccess {
|
|
358
|
+
const snapshot = developerSnapshot(state);
|
|
359
|
+
return {
|
|
360
|
+
canExecute: snapshot.hasTag("execute"),
|
|
361
|
+
canMutate: snapshot.hasTag("mutate"),
|
|
362
|
+
hasBeforeDirectGate: snapshot.hasTag("blocks-direct"),
|
|
363
|
+
};
|
|
129
364
|
}
|
|
130
365
|
|
|
131
366
|
function summarizeState(state: DeveloperState): string {
|
|
@@ -145,20 +380,28 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
145
380
|
"- A direct step has one observable difference and one structural or behavioral purpose. Stop when its failure is locally explainable and the repository is green, pausable, and reviewable.",
|
|
146
381
|
"- For direct structural work intended to preserve behavior, set execution_profile=behavior-preserving-structure; omit the field for other direct actions and every skill route.",
|
|
147
382
|
`- Follow the routed method, then close the route with ${JUDGMENT_TOOL}. After every direct stable landing, route again from the new evidence before selecting another movement.`,
|
|
383
|
+
"- Consecutive direct routes are valid when the new evidence still justifies direct action. On a reroute after direct, explicitly reconsider the most plausible skill routes and record why they are not needed; do not choose direct by momentum.",
|
|
148
384
|
"- Do not carry a predetermined implementation queue through multiple direct steps. Re-observe after each stable landing and reroute to a skill whenever meaning, cases, design, structural direction, timing, naming, or evidence becomes uncertain.",
|
|
149
385
|
"- Protocol state is routing bookkeeping. Idle never proves product completion, user acceptance, or current verification.",
|
|
386
|
+
"- Preserve each skill's inspectable output surface in judgment result Markdown: keep its tables, diagrams, matrices, timelines, or code blocks instead of flattening them into prose.",
|
|
387
|
+
`- On every ${JUDGMENT_TOOL} call, re-check all pending questions against the new evidence. Use question_updates to resolve questions naturally even when the current route did not focus them.`,
|
|
388
|
+
"- question_updates accepts only question IDs that were already listed as pending before the current route. Never use route_id or an ID for a new open_questions entry; use an empty array or omit question_updates when no pending question exists.",
|
|
389
|
+
"- A focused question always requires an explicit question_updates entry. Resolve or dismiss it with concrete basis, retain it as open/blocked, or explicitly replace the broad question while opening narrower children.",
|
|
390
|
+
"- Pending questions declare who can resolve them, what they block, and observable resolution criteria. Include context when detailed choices, examples, or constraints are needed to answer. For finite required user choices, also provide a choice-form response_spec with one field per decision; do not rely on parsing Markdown into controls. Never guess a user-owned answer or bypass a before-direct gate.",
|
|
391
|
+
"- Use before-direct only when the missing answer can change whether or what artifact mutation is valid. An agent-owned before-direct question must be resolvable through a non-direct skill route using observation or evidence execution; it must never require the mutation it blocks.",
|
|
150
392
|
"- Product files are changed with Pi implementation tools. Developer protocol tools only route and record judgments.",
|
|
151
393
|
];
|
|
152
394
|
|
|
153
395
|
if (state.mode === "strict") {
|
|
154
396
|
lines.push(
|
|
155
|
-
"- Strict mode withholds
|
|
397
|
+
"- Strict mode withholds Pi built-in bash while idle, restores bash for a routed skill judgment so it can inspect or run evidence checks, and restores edit/write only for a direct route. It does not classify or sandbox other extensions' tools.",
|
|
398
|
+
"- Use a skill judgment route, not a direct route, when repository discovery or verifier execution is the unresolved work. Direct remains the artifact-mutation lane.",
|
|
156
399
|
);
|
|
157
400
|
}
|
|
158
401
|
|
|
159
402
|
if (state.implementationFramingRequired) {
|
|
160
403
|
lines.push(
|
|
161
|
-
"
|
|
404
|
+
"Direct gate: the latest resolved model exposed implementation work. Before direct mutation, route sketch for new feature shape or signal for existing-code structural movement; other judgments may still be routed first.",
|
|
162
405
|
);
|
|
163
406
|
}
|
|
164
407
|
if (state.verificationRequired) {
|
|
@@ -166,6 +409,19 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
166
409
|
"Verification debt: a direct route changed artifacts after the last resolved verify judgment. Route verify before claiming completion or handing off as done.",
|
|
167
410
|
);
|
|
168
411
|
}
|
|
412
|
+
const completionGates = state.pendingQuestions.filter(
|
|
413
|
+
(question) => question.gate === "before-completion" || question.gate === "before-direct",
|
|
414
|
+
);
|
|
415
|
+
if (completionGates.length > 0) {
|
|
416
|
+
lines.push(
|
|
417
|
+
`Completion gate: ${completionGates.map((question) => question.id).join(", ")} must be resolved before a completion claim.`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (!state.activeRoute && state.rerouteRequired) {
|
|
421
|
+
lines.push(
|
|
422
|
+
"Rerouting checkpoint: use the previous direct landing as known_evidence. If direct is still right, include alternatives_considered for the plausible available skills and explain why each adds no useful judgment now.",
|
|
423
|
+
);
|
|
424
|
+
}
|
|
169
425
|
|
|
170
426
|
if (state.activeRoute) {
|
|
171
427
|
lines.push(
|
|
@@ -180,10 +436,13 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
180
436
|
if (state.pendingQuestions.length > 0) {
|
|
181
437
|
lines.push("Pending Developer questions:");
|
|
182
438
|
for (const question of state.pendingQuestions) {
|
|
183
|
-
lines.push(
|
|
439
|
+
lines.push(
|
|
440
|
+
`- ${question.id} · ${question.status} · owner=${question.resolutionOwner} · gate=${question.gate} · ${question.question} · resolves when: ${question.resolutionCriteria}`,
|
|
441
|
+
);
|
|
184
442
|
}
|
|
185
443
|
lines.push(
|
|
186
|
-
"- Revisit questions naturally. Developer
|
|
444
|
+
"- Revisit questions naturally. Developer associates an explicitly focused or exactly matching pending question; open_question_id is an internal disambiguator when wording is intentionally changed or several questions remain.",
|
|
445
|
+
"- question_updates may resolve any pending ID from implementation, test, inspection, user, or environment evidence; each resolved update requires concrete basis.",
|
|
187
446
|
);
|
|
188
447
|
}
|
|
189
448
|
lines.push(
|
|
@@ -199,9 +458,61 @@ function routeRenderText(event: RouteEvent | undefined): string {
|
|
|
199
458
|
return `${event.owner}${profile} · ${compactLine(event.question)}${target}`;
|
|
200
459
|
}
|
|
201
460
|
|
|
461
|
+
function compactJudgmentResult(result: string, maxChars = 160): string {
|
|
462
|
+
const firstContentLine = result
|
|
463
|
+
.split(/\r?\n/)
|
|
464
|
+
.map((line) => line.trim())
|
|
465
|
+
.find((line) => line && !line.startsWith("```"));
|
|
466
|
+
return compactLine(firstContentLine ?? result, maxChars);
|
|
467
|
+
}
|
|
468
|
+
|
|
202
469
|
function judgmentRenderText(event: JudgmentEvent | undefined): string {
|
|
203
470
|
if (!event) return "Judgment unavailable";
|
|
204
|
-
return `${event.status} · ${
|
|
471
|
+
return `${event.status} · ${compactJudgmentResult(event.result)}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function detailLine(
|
|
475
|
+
theme: Theme,
|
|
476
|
+
label: string,
|
|
477
|
+
value: string,
|
|
478
|
+
valueColor: ThemeColor = "muted",
|
|
479
|
+
): string {
|
|
480
|
+
return `${theme.fg("dim", `${label} · `)}${theme.fg(valueColor, value)}`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function expandedJudgment(event: JudgmentEvent, statusText: string, theme: Theme): Container {
|
|
484
|
+
const container = new Container();
|
|
485
|
+
const header = [
|
|
486
|
+
statusText,
|
|
487
|
+
detailLine(theme, "route", event.routeId, "text"),
|
|
488
|
+
detailLine(theme, "target", event.owner, "accent"),
|
|
489
|
+
detailLine(theme, "question", event.question, "text"),
|
|
490
|
+
];
|
|
491
|
+
container.addChild(new Text(header.join("\n"), 0, 0));
|
|
492
|
+
container.addChild(new Markdown(event.result, 0, 0, getMarkdownTheme()));
|
|
493
|
+
|
|
494
|
+
const evidence = [
|
|
495
|
+
...event.basis.map((basis) => detailLine(theme, "basis", basis)),
|
|
496
|
+
...event.artifacts.map((artifact) => detailLine(theme, "artifact", artifact)),
|
|
497
|
+
...event.openedQuestions.map((question) =>
|
|
498
|
+
detailLine(
|
|
499
|
+
theme,
|
|
500
|
+
`opened ${question.resolutionOwner}/${question.gate}`,
|
|
501
|
+
`${question.question} — resolves when: ${question.resolutionCriteria}`,
|
|
502
|
+
"warning",
|
|
503
|
+
),
|
|
504
|
+
),
|
|
505
|
+
...(event.questionUpdates ?? []).map((update) =>
|
|
506
|
+
detailLine(
|
|
507
|
+
theme,
|
|
508
|
+
`question ${update.status}`,
|
|
509
|
+
`${update.questionId} — ${update.result}`,
|
|
510
|
+
"accent",
|
|
511
|
+
),
|
|
512
|
+
),
|
|
513
|
+
];
|
|
514
|
+
if (evidence.length > 0) container.addChild(new Text(evidence.join("\n"), 0, 0));
|
|
515
|
+
return container;
|
|
205
516
|
}
|
|
206
517
|
|
|
207
518
|
export default async function developer(pi: ExtensionAPI) {
|
|
@@ -222,7 +533,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
222
533
|
activeTools: current,
|
|
223
534
|
allTools: pi.getAllTools(),
|
|
224
535
|
mode: state.mode,
|
|
225
|
-
|
|
536
|
+
access: protocolToolAccess(state),
|
|
226
537
|
protocolTools: PROTOCOL_TOOLS,
|
|
227
538
|
memory: toolPolicyMemory,
|
|
228
539
|
});
|
|
@@ -266,8 +577,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
266
577
|
...state.pendingQuestions
|
|
267
578
|
.slice(0, 3)
|
|
268
579
|
.map((question) => `open · ${question.id} · ${compactLine(question.question)}`),
|
|
269
|
-
...(state.pendingQuestions.length > 3
|
|
270
|
-
|
|
580
|
+
...(state.pendingQuestions.length > 3
|
|
581
|
+
? [`open · +${state.pendingQuestions.length - 3} more`]
|
|
582
|
+
: []),
|
|
583
|
+
...(state.implementationFramingRequired
|
|
584
|
+
? ["gate · frame implementation before direct (sketch or signal)"]
|
|
585
|
+
: []),
|
|
271
586
|
...(state.verificationRequired ? ["next · verify changed artifacts before completion"] : []),
|
|
272
587
|
];
|
|
273
588
|
ctx.ui.setWidget("developer", lines, { placement: "belowEditor" });
|
|
@@ -305,9 +620,28 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
305
620
|
}),
|
|
306
621
|
),
|
|
307
622
|
open_question_id: Type.Optional(
|
|
308
|
-
Type.String({
|
|
623
|
+
Type.String({
|
|
624
|
+
maxLength: 512,
|
|
625
|
+
description: "Exact pending question ID when this route revisits one",
|
|
626
|
+
}),
|
|
309
627
|
),
|
|
310
628
|
};
|
|
629
|
+
const RouteAlternativeParam = Type.Object(
|
|
630
|
+
{
|
|
631
|
+
owner: Type.String({
|
|
632
|
+
minLength: 1,
|
|
633
|
+
maxLength: 64,
|
|
634
|
+
description: "Exact available Developer skill name that was reconsidered",
|
|
635
|
+
}),
|
|
636
|
+
reason: Type.String({
|
|
637
|
+
minLength: 1,
|
|
638
|
+
maxLength: MAX_EVIDENCE_CHARS,
|
|
639
|
+
description:
|
|
640
|
+
"Why this skill would add no useful judgment before the proposed direct movement",
|
|
641
|
+
}),
|
|
642
|
+
},
|
|
643
|
+
{ additionalProperties: false },
|
|
644
|
+
);
|
|
311
645
|
const RouteParams = Type.Union([
|
|
312
646
|
Type.Object(
|
|
313
647
|
{
|
|
@@ -319,16 +653,22 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
319
653
|
description: "Exact skill name from the current Available Developer skills list",
|
|
320
654
|
}),
|
|
321
655
|
},
|
|
322
|
-
{
|
|
656
|
+
{
|
|
657
|
+
additionalProperties: false,
|
|
658
|
+
description: "Route one question to a Developer skill",
|
|
659
|
+
},
|
|
323
660
|
),
|
|
324
661
|
Type.Object(
|
|
325
662
|
{
|
|
326
663
|
...SharedRouteParams,
|
|
327
|
-
owner: Type.Literal("direct", {
|
|
664
|
+
owner: Type.Literal("direct", {
|
|
665
|
+
description: "Use Pi implementation tools for an already-justified action",
|
|
666
|
+
}),
|
|
328
667
|
movement: Type.String({
|
|
329
668
|
minLength: 1,
|
|
330
669
|
maxLength: MAX_QUESTION_CHARS,
|
|
331
|
-
description:
|
|
670
|
+
description:
|
|
671
|
+
"One locally explainable behavioral or structural movement; not a multi-step implementation queue",
|
|
332
672
|
}),
|
|
333
673
|
stop_condition: Type.String({
|
|
334
674
|
minLength: 1,
|
|
@@ -340,13 +680,24 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
340
680
|
maxLength: MAX_EVIDENCE_CHARS,
|
|
341
681
|
description: "The narrowest check that can catch the likely break in this movement",
|
|
342
682
|
}),
|
|
683
|
+
alternatives_considered: Type.Optional(
|
|
684
|
+
Type.Array(RouteAlternativeParam, {
|
|
685
|
+
maxItems: 6,
|
|
686
|
+
description:
|
|
687
|
+
"On a reroute after direct, the plausible skill routes reconsidered and why each is unnecessary now",
|
|
688
|
+
}),
|
|
689
|
+
),
|
|
343
690
|
execution_profile: Type.Optional(
|
|
344
691
|
Type.Literal("behavior-preserving-structure", {
|
|
345
|
-
description:
|
|
692
|
+
description:
|
|
693
|
+
"Load the focused structural-mutation protocol; omit for ordinary direct action",
|
|
346
694
|
}),
|
|
347
695
|
),
|
|
348
696
|
},
|
|
349
|
-
{
|
|
697
|
+
{
|
|
698
|
+
additionalProperties: false,
|
|
699
|
+
description: "Route one already-justified direct action",
|
|
700
|
+
},
|
|
350
701
|
),
|
|
351
702
|
]);
|
|
352
703
|
|
|
@@ -359,15 +710,21 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
359
710
|
promptGuidelines: [
|
|
360
711
|
`Call ${ROUTE_TOOL} only when there is no active Developer route.`,
|
|
361
712
|
`Use ${ROUTE_TOOL} with the most focused skill supported by current evidence; owner=direct requires one movement, one stable landing, and one narrow verification.`,
|
|
713
|
+
`When ${ROUTE_TOOL} follows a direct judgment with another direct route, cite the previous landing in known_evidence and record plausible skill routes in alternatives_considered instead of selecting direct by momentum.`,
|
|
362
714
|
`After a resolved model, use sketch for first feature implementation framing or signal for existing-code structural movement before direct mutation.`,
|
|
363
715
|
],
|
|
364
716
|
parameters: RouteParams,
|
|
365
717
|
executionMode: "sequential",
|
|
718
|
+
renderShell: "self",
|
|
366
719
|
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
367
|
-
if (state.mode === "off")
|
|
720
|
+
if (state.mode === "off")
|
|
721
|
+
fail("Developer protocol is off. Run /develop on or /develop strict first.");
|
|
368
722
|
if (state.activeRoute || routeOpening) {
|
|
369
|
-
if (!state.activeRoute)
|
|
370
|
-
|
|
723
|
+
if (!state.activeRoute)
|
|
724
|
+
fail("Another Developer route is currently opening. Wait for it to finish.");
|
|
725
|
+
fail(
|
|
726
|
+
`Route ${state.activeRoute.routeId} is still active. Record its judgment before routing another question.`,
|
|
727
|
+
);
|
|
371
728
|
}
|
|
372
729
|
routeOpening = true;
|
|
373
730
|
|
|
@@ -378,11 +735,57 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
378
735
|
|
|
379
736
|
const explicitQuestionId = params.open_question_id?.trim() || undefined;
|
|
380
737
|
const targetQuestionId = inferredQuestionId(state, question, explicitQuestionId);
|
|
381
|
-
if (
|
|
738
|
+
if (
|
|
739
|
+
targetQuestionId &&
|
|
740
|
+
!state.pendingQuestions.some((item) => item.id === targetQuestionId)
|
|
741
|
+
) {
|
|
382
742
|
fail(`Unknown pending question ID: ${targetQuestionId}`);
|
|
383
743
|
}
|
|
384
744
|
|
|
385
745
|
const owner = params.owner;
|
|
746
|
+
const directBlockers = state.pendingQuestions.filter(
|
|
747
|
+
(pending) => pending.gate === "before-direct",
|
|
748
|
+
);
|
|
749
|
+
if (owner === "direct" && directBlockers.length > 0) {
|
|
750
|
+
fail(
|
|
751
|
+
`Direct work is blocked by ${directBlockers
|
|
752
|
+
.map((pending) => `${pending.id} (${pending.resolutionOwner}: ${pending.question})`)
|
|
753
|
+
.join("; ")}. Resolve those questions first.`,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
const knownEvidence = (params.known_evidence ?? [])
|
|
757
|
+
.map((item) => item.trim())
|
|
758
|
+
.filter(Boolean);
|
|
759
|
+
const consideredAlternatives: RouteAlternative[] =
|
|
760
|
+
owner === "direct" && "alternatives_considered" in params
|
|
761
|
+
? (params.alternatives_considered ?? []).map((alternative) => ({
|
|
762
|
+
owner: alternative.owner.trim(),
|
|
763
|
+
reason: alternative.reason.trim(),
|
|
764
|
+
}))
|
|
765
|
+
: [];
|
|
766
|
+
for (const alternative of consideredAlternatives) {
|
|
767
|
+
if (!availableSkills.has(alternative.owner)) {
|
|
768
|
+
fail(`Considered alternative ${alternative.owner} is unavailable or disabled.`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (
|
|
772
|
+
new Set(consideredAlternatives.map((alternative) => alternative.owner)).size !==
|
|
773
|
+
consideredAlternatives.length
|
|
774
|
+
) {
|
|
775
|
+
fail("Each considered alternative must name a different available Developer skill.");
|
|
776
|
+
}
|
|
777
|
+
if (owner === "direct" && state.lastJudgment?.owner === "direct") {
|
|
778
|
+
if (knownEvidence.length === 0) {
|
|
779
|
+
fail(
|
|
780
|
+
"A consecutive direct route must cite evidence from the previous direct landing in known_evidence.",
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (availableSkills.size > 0 && consideredAlternatives.length === 0) {
|
|
784
|
+
fail(
|
|
785
|
+
"A consecutive direct route must record the plausible available skill routes in alternatives_considered and explain why they are not needed now.",
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
386
789
|
if (
|
|
387
790
|
owner === "direct" &&
|
|
388
791
|
state.implementationFramingRequired &&
|
|
@@ -394,7 +797,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
394
797
|
}
|
|
395
798
|
const skill = owner === "direct" ? undefined : availableSkills.get(owner);
|
|
396
799
|
if (owner !== "direct" && !skill) {
|
|
397
|
-
fail(
|
|
800
|
+
fail(
|
|
801
|
+
`Developer skill ${owner} is unavailable or disabled in the current Pi resource configuration.`,
|
|
802
|
+
);
|
|
398
803
|
}
|
|
399
804
|
const requestedExecutionProfile =
|
|
400
805
|
"execution_profile" in params ? params.execution_profile : undefined;
|
|
@@ -423,15 +828,13 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
423
828
|
owner === "direct"
|
|
424
829
|
? {
|
|
425
830
|
movement: ("movement" in params ? params.movement : question).trim(),
|
|
426
|
-
stopCondition: (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
: "Reach a green, pausable, reviewable stable landing."
|
|
831
|
+
stopCondition: ("stop_condition" in params
|
|
832
|
+
? params.stop_condition
|
|
833
|
+
: "Reach a green, pausable, reviewable stable landing."
|
|
430
834
|
).trim(),
|
|
431
|
-
verification: (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
: "Run the narrowest relevant check and inspect the resulting diff or output."
|
|
835
|
+
verification: ("verification" in params
|
|
836
|
+
? params.verification
|
|
837
|
+
: "Run the narrowest relevant check and inspect the resulting diff or output."
|
|
435
838
|
).trim(),
|
|
436
839
|
}
|
|
437
840
|
: undefined;
|
|
@@ -443,7 +846,8 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
443
846
|
question,
|
|
444
847
|
owner,
|
|
445
848
|
reason,
|
|
446
|
-
knownEvidence
|
|
849
|
+
knownEvidence,
|
|
850
|
+
consideredAlternatives,
|
|
447
851
|
targetQuestionId,
|
|
448
852
|
methodLocation: skill?.filePath,
|
|
449
853
|
executionProfile,
|
|
@@ -453,8 +857,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
453
857
|
`Route ID: ${event.routeId}`,
|
|
454
858
|
`Question: ${event.question}`,
|
|
455
859
|
`Target: ${event.owner}`,
|
|
456
|
-
skill
|
|
457
|
-
|
|
860
|
+
skill
|
|
861
|
+
? `Skill location: ${skill.filePath}`
|
|
862
|
+
: "Skill location: direct action; no skill file",
|
|
863
|
+
executionProfile
|
|
864
|
+
? `Execution profile: ${executionProfile}`
|
|
865
|
+
: "Execution profile: skill judgment",
|
|
458
866
|
...(directStep
|
|
459
867
|
? [
|
|
460
868
|
`Movement: ${directStep.movement}`,
|
|
@@ -465,7 +873,16 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
465
873
|
: []),
|
|
466
874
|
`Reason: ${event.reason}`,
|
|
467
875
|
`Known evidence: ${event.knownEvidence.length > 0 ? event.knownEvidence.join(" | ") : "none"}`,
|
|
468
|
-
|
|
876
|
+
`Alternatives reconsidered: ${
|
|
877
|
+
event.consideredAlternatives.length > 0
|
|
878
|
+
? event.consideredAlternatives
|
|
879
|
+
.map((alternative) => `${alternative.owner} — ${alternative.reason}`)
|
|
880
|
+
.join(" | ")
|
|
881
|
+
: "none"
|
|
882
|
+
}`,
|
|
883
|
+
targetQuestionId
|
|
884
|
+
? `Revisits pending question: ${targetQuestionId}`
|
|
885
|
+
: "Revisits pending question: none",
|
|
469
886
|
`When this route has done its job, call ${JUDGMENT_TOOL} with this exact route ID.`,
|
|
470
887
|
"",
|
|
471
888
|
"---",
|
|
@@ -473,6 +890,11 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
473
890
|
method,
|
|
474
891
|
].join("\n");
|
|
475
892
|
ensureSafeToolText(response, "Developer route result");
|
|
893
|
+
if (!canApplyDeveloperEvent(state, event)) {
|
|
894
|
+
fail(
|
|
895
|
+
"Developer machine guard rejected the route transition from the current branch state.",
|
|
896
|
+
);
|
|
897
|
+
}
|
|
476
898
|
|
|
477
899
|
state = applyDeveloperEvent(state, event);
|
|
478
900
|
syncProtocolTools();
|
|
@@ -485,7 +907,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
485
907
|
renderCall(args, theme, context) {
|
|
486
908
|
const owner = typeof args.owner === "string" && args.owner.length > 0 ? args.owner : "…";
|
|
487
909
|
const question =
|
|
488
|
-
typeof args.question === "string" && args.question.length > 0
|
|
910
|
+
typeof args.question === "string" && args.question.length > 0
|
|
911
|
+
? compactLine(args.question)
|
|
912
|
+
: "…";
|
|
489
913
|
return reusableText(
|
|
490
914
|
`${theme.fg("toolTitle", theme.bold(ROUTE_TOOL))} ${theme.fg("accent", owner)} ${theme.fg("muted", question)}`,
|
|
491
915
|
context.lastComponent,
|
|
@@ -493,7 +917,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
493
917
|
},
|
|
494
918
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
495
919
|
if (isPartial) {
|
|
496
|
-
return reusableText(
|
|
920
|
+
return reusableText(
|
|
921
|
+
theme.fg("warning", "routing development question…"),
|
|
922
|
+
context.lastComponent,
|
|
923
|
+
);
|
|
497
924
|
}
|
|
498
925
|
if (context.isError) {
|
|
499
926
|
return reusableText(
|
|
@@ -504,25 +931,28 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
504
931
|
const event = result.details as RouteEvent | undefined;
|
|
505
932
|
let text = theme.fg("success", `routed ${routeRenderText(event)}`);
|
|
506
933
|
if (expanded && event) {
|
|
507
|
-
text += `\n${theme
|
|
508
|
-
text += `\n${theme
|
|
509
|
-
text += `\n${theme
|
|
934
|
+
text += `\n${detailLine(theme, "route", event.routeId, "text")}`;
|
|
935
|
+
text += `\n${detailLine(theme, "question", event.question, "text")}`;
|
|
936
|
+
text += `\n${detailLine(theme, "reason", event.reason)}`;
|
|
510
937
|
if (event.knownEvidence.length > 0) {
|
|
511
938
|
for (const evidence of event.knownEvidence) {
|
|
512
|
-
text += `\n${theme
|
|
939
|
+
text += `\n${detailLine(theme, "evidence", evidence)}`;
|
|
513
940
|
}
|
|
514
941
|
} else {
|
|
515
|
-
text += `\n${theme
|
|
942
|
+
text += `\n${detailLine(theme, "evidence", "none recorded before routing", "warning")}`;
|
|
516
943
|
}
|
|
517
|
-
|
|
518
|
-
|
|
944
|
+
for (const alternative of event.consideredAlternatives ?? []) {
|
|
945
|
+
text += `\n${detailLine(theme, `considered ${alternative.owner}`, alternative.reason)}`;
|
|
946
|
+
}
|
|
947
|
+
text += `\n${detailLine(theme, "revisits", event.targetQuestionId ?? "none")}`;
|
|
948
|
+
text += `\n${detailLine(theme, "skill", event.methodLocation ?? "direct action")}`;
|
|
519
949
|
if (event.executionProfile) {
|
|
520
|
-
text += `\n${theme
|
|
950
|
+
text += `\n${detailLine(theme, "profile", event.executionProfile)}`;
|
|
521
951
|
}
|
|
522
952
|
if (event.directStep) {
|
|
523
|
-
text += `\n${theme
|
|
524
|
-
text += `\n${theme
|
|
525
|
-
text += `\n${theme
|
|
953
|
+
text += `\n${detailLine(theme, "movement", event.directStep.movement, "text")}`;
|
|
954
|
+
text += `\n${detailLine(theme, "landing", event.directStep.stopCondition)}`;
|
|
955
|
+
text += `\n${detailLine(theme, "verify", event.directStep.verification)}`;
|
|
526
956
|
}
|
|
527
957
|
}
|
|
528
958
|
if (!expanded && event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
@@ -530,22 +960,129 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
530
960
|
},
|
|
531
961
|
});
|
|
532
962
|
|
|
963
|
+
const ChoiceResponseOptionParam = Type.Object(
|
|
964
|
+
{
|
|
965
|
+
value: Type.String({
|
|
966
|
+
minLength: 1,
|
|
967
|
+
maxLength: MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
968
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
969
|
+
}),
|
|
970
|
+
label: Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS }),
|
|
971
|
+
description: Type.Optional(Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS })),
|
|
972
|
+
detail_prompt: Type.Optional(
|
|
973
|
+
Type.String({
|
|
974
|
+
minLength: 1,
|
|
975
|
+
maxLength: MAX_RESPONSE_TEXT_CHARS,
|
|
976
|
+
description: "Required non-empty detail when this option is selected",
|
|
977
|
+
}),
|
|
978
|
+
),
|
|
979
|
+
},
|
|
980
|
+
{ additionalProperties: false },
|
|
981
|
+
);
|
|
982
|
+
const ChoiceResponseFieldParam = Type.Object(
|
|
983
|
+
{
|
|
984
|
+
id: Type.String({
|
|
985
|
+
minLength: 1,
|
|
986
|
+
maxLength: MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
987
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
988
|
+
}),
|
|
989
|
+
prompt: Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS }),
|
|
990
|
+
description: Type.Optional(Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS })),
|
|
991
|
+
options: Type.Array(ChoiceResponseOptionParam, {
|
|
992
|
+
minItems: 2,
|
|
993
|
+
maxItems: MAX_RESPONSE_OPTIONS,
|
|
994
|
+
}),
|
|
995
|
+
},
|
|
996
|
+
{ additionalProperties: false },
|
|
997
|
+
);
|
|
998
|
+
const ChoiceResponseSpecParam = Type.Object(
|
|
999
|
+
{
|
|
1000
|
+
kind: Type.Literal("choice-form"),
|
|
1001
|
+
fields: Type.Array(ChoiceResponseFieldParam, {
|
|
1002
|
+
minItems: 1,
|
|
1003
|
+
maxItems: MAX_RESPONSE_FIELDS,
|
|
1004
|
+
}),
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
additionalProperties: false,
|
|
1008
|
+
description:
|
|
1009
|
+
"Explicit controls for finite user-owned decisions; use one required single-choice field per decision instead of expecting the TUI to parse Markdown context",
|
|
1010
|
+
},
|
|
1011
|
+
);
|
|
1012
|
+
const OpenQuestionParam = Type.Object(
|
|
1013
|
+
{
|
|
1014
|
+
question: Type.String({ minLength: 1, maxLength: MAX_QUESTION_CHARS }),
|
|
1015
|
+
context: Type.Optional(
|
|
1016
|
+
Type.String({
|
|
1017
|
+
maxLength: MAX_QUESTION_CONTEXT_CHARS,
|
|
1018
|
+
description:
|
|
1019
|
+
"Detailed Markdown choices, examples, or constraints needed to resolve the question",
|
|
1020
|
+
}),
|
|
1021
|
+
),
|
|
1022
|
+
response_spec: Type.Optional(ChoiceResponseSpecParam),
|
|
1023
|
+
status: StringEnum(["open", "blocked"] as const, {
|
|
1024
|
+
description: "Whether the required evidence or answer is currently obtainable",
|
|
1025
|
+
}),
|
|
1026
|
+
resolution_owner: StringEnum(["agent", "user", "environment"] as const, {
|
|
1027
|
+
description: "Who can provide the evidence or decision that resolves this question",
|
|
1028
|
+
}),
|
|
1029
|
+
gate: StringEnum(["none", "before-direct", "before-completion"] as const, {
|
|
1030
|
+
description:
|
|
1031
|
+
"What work this question blocks; an agent before-direct question must be resolvable through a non-direct evidence route",
|
|
1032
|
+
}),
|
|
1033
|
+
resolution_criteria: Type.String({
|
|
1034
|
+
minLength: 1,
|
|
1035
|
+
maxLength: MAX_EVIDENCE_CHARS,
|
|
1036
|
+
description: "Observable evidence or answer that will settle the question",
|
|
1037
|
+
}),
|
|
1038
|
+
},
|
|
1039
|
+
{ additionalProperties: false },
|
|
1040
|
+
);
|
|
1041
|
+
const QuestionUpdateParam = Type.Object(
|
|
1042
|
+
{
|
|
1043
|
+
question_id: Type.String({
|
|
1044
|
+
minLength: 1,
|
|
1045
|
+
maxLength: 512,
|
|
1046
|
+
description:
|
|
1047
|
+
"Exact ID of a question that was pending before this route; never route_id or a newly opened question ID",
|
|
1048
|
+
}),
|
|
1049
|
+
status: StringEnum(["resolved", "not-applicable", "open", "blocked"] as const),
|
|
1050
|
+
result: Type.String({ minLength: 1, maxLength: MAX_RESULT_CHARS }),
|
|
1051
|
+
basis: Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), {
|
|
1052
|
+
maxItems: 20,
|
|
1053
|
+
}),
|
|
1054
|
+
},
|
|
1055
|
+
{ additionalProperties: false },
|
|
1056
|
+
);
|
|
533
1057
|
const JudgmentParams = Type.Object({
|
|
534
|
-
route_id: Type.String({
|
|
1058
|
+
route_id: Type.String({
|
|
1059
|
+
minLength: 1,
|
|
1060
|
+
maxLength: 512,
|
|
1061
|
+
description: `Exact route ID returned by ${ROUTE_TOOL}`,
|
|
1062
|
+
}),
|
|
535
1063
|
status: StringEnum(["resolved", "needs-evidence", "not-applicable", "blocked"] as const),
|
|
536
1064
|
result: Type.String({
|
|
537
1065
|
minLength: 1,
|
|
538
1066
|
maxLength: MAX_RESULT_CHARS,
|
|
539
|
-
description:
|
|
1067
|
+
description:
|
|
1068
|
+
"The resulting judgment in Markdown; preserve the routed skill's inspectable tables, diagrams, matrices, timelines, or code blocks",
|
|
540
1069
|
}),
|
|
541
1070
|
basis: Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), {
|
|
542
1071
|
maxItems: 20,
|
|
543
1072
|
description: "Evidence supporting the judgment or blocker",
|
|
544
1073
|
}),
|
|
545
1074
|
open_questions: Type.Optional(
|
|
546
|
-
Type.Array(
|
|
1075
|
+
Type.Array(OpenQuestionParam, {
|
|
547
1076
|
maxItems: 10,
|
|
548
|
-
description:
|
|
1077
|
+
description:
|
|
1078
|
+
"New unresolved questions with explicit resolution owner, gate, and observable resolution criteria",
|
|
1079
|
+
}),
|
|
1080
|
+
),
|
|
1081
|
+
question_updates: Type.Optional(
|
|
1082
|
+
Type.Array(QuestionUpdateParam, {
|
|
1083
|
+
maxItems: 20,
|
|
1084
|
+
description:
|
|
1085
|
+
"Updates to questions already pending before this route; use [] when none exist, and never put route_id or newly opened questions here",
|
|
549
1086
|
}),
|
|
550
1087
|
),
|
|
551
1088
|
artifacts: Type.Optional(
|
|
@@ -564,15 +1101,34 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
564
1101
|
promptSnippet: "Record evidence and close the active development route",
|
|
565
1102
|
promptGuidelines: [
|
|
566
1103
|
`Use ${JUDGMENT_TOOL} with the exact active Developer route ID.`,
|
|
1104
|
+
`Use ${JUDGMENT_TOOL} result as Markdown and preserve the routed skill's inspectable tables, diagrams, matrices, timelines, and code blocks instead of reducing them to prose.`,
|
|
1105
|
+
`Use ${JUDGMENT_TOOL} open_questions with resolution_owner, gate, and resolution_criteria; for finite required user decisions, add a choice-form response_spec with one field per decision. Use question_updates whenever current evidence settles or changes any existing pending question.`,
|
|
567
1106
|
`Do not use ${JUDGMENT_TOOL} with resolved, not-applicable, or blocked status without at least one concrete basis.`,
|
|
568
1107
|
],
|
|
569
1108
|
parameters: JudgmentParams,
|
|
570
1109
|
executionMode: "sequential",
|
|
1110
|
+
renderShell: "self",
|
|
1111
|
+
prepareArguments(args): Static<typeof JudgmentParams> {
|
|
1112
|
+
const input = args as Static<typeof JudgmentParams> & {
|
|
1113
|
+
status?: string;
|
|
1114
|
+
open_questions?: unknown[];
|
|
1115
|
+
};
|
|
1116
|
+
if (!input || typeof input !== "object" || !Array.isArray(input.open_questions)) {
|
|
1117
|
+
return args as Static<typeof JudgmentParams>;
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
...input,
|
|
1121
|
+
open_questions: input.open_questions.map((question) =>
|
|
1122
|
+
typeof question === "string" ? legacyOpenQuestion(question, input.status) : question,
|
|
1123
|
+
) as Static<typeof JudgmentParams>["open_questions"],
|
|
1124
|
+
};
|
|
1125
|
+
},
|
|
571
1126
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
572
1127
|
if (state.mode === "off") fail("Developer protocol is off.");
|
|
573
|
-
|
|
574
|
-
if (
|
|
575
|
-
|
|
1128
|
+
const activeRoute = state.activeRoute;
|
|
1129
|
+
if (!activeRoute) fail("There is no active Developer route to close.");
|
|
1130
|
+
if (params.route_id !== activeRoute.routeId) {
|
|
1131
|
+
fail(`Route ID mismatch. Active route is ${activeRoute.routeId}.`);
|
|
576
1132
|
}
|
|
577
1133
|
|
|
578
1134
|
const basis = params.basis.map((item) => item.trim()).filter(Boolean);
|
|
@@ -582,28 +1138,89 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
582
1138
|
fail(`${params.status} judgments require at least one concrete basis.`);
|
|
583
1139
|
}
|
|
584
1140
|
|
|
585
|
-
const openedQuestions = (
|
|
586
|
-
.
|
|
587
|
-
|
|
588
|
-
.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1141
|
+
const openedQuestions = buildOpenedQuestions(
|
|
1142
|
+
params.open_questions ?? [],
|
|
1143
|
+
state,
|
|
1144
|
+
params.route_id,
|
|
1145
|
+
);
|
|
1146
|
+
if (
|
|
1147
|
+
availableSkills.size === 0 &&
|
|
1148
|
+
openedQuestions.some(
|
|
1149
|
+
(question) => question.resolutionOwner === "agent" && question.gate === "before-direct",
|
|
1150
|
+
)
|
|
1151
|
+
) {
|
|
1152
|
+
fail(
|
|
1153
|
+
"An agent-owned before-direct question requires an available non-direct skill resolution path.",
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
const remainsOpen = params.status === "needs-evidence" || params.status === "blocked";
|
|
1157
|
+
if (remainsOpen && !activeRoute.targetQuestionId && openedQuestions.length === 0) {
|
|
1158
|
+
fail(
|
|
1159
|
+
"A needs-evidence or blocked judgment for a new question must include at least one structured open_questions entry.",
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (state.pendingQuestions.length > 0 && params.question_updates === undefined) {
|
|
1164
|
+
fail(
|
|
1165
|
+
"Pending questions exist. Include question_updates (an empty array is valid) after rechecking them against this route's evidence.",
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
const questionUpdates = buildQuestionUpdates(params.question_updates ?? [], state);
|
|
1169
|
+
const updatedQuestionIds = new Set(questionUpdates.map((update) => update.questionId));
|
|
1170
|
+
if (openedQuestions.some((question) => updatedQuestionIds.has(question.id))) {
|
|
1171
|
+
fail("A question cannot be opened and updated in the same judgment.");
|
|
1172
|
+
}
|
|
1173
|
+
const reopensCurrentQuestion = openedQuestions.some(
|
|
1174
|
+
(question) =>
|
|
1175
|
+
normalizedQuestion(question.question) === normalizedQuestion(activeRoute.question),
|
|
1176
|
+
);
|
|
1177
|
+
if (!remainsOpen && reopensCurrentQuestion) {
|
|
1178
|
+
fail("A resolved or not-applicable judgment cannot reopen its own question.");
|
|
1179
|
+
}
|
|
1180
|
+
const targetQuestionId = activeRoute.targetQuestionId;
|
|
1181
|
+
const targetUpdate = targetQuestionId
|
|
1182
|
+
? questionUpdates.find((update) => update.questionId === targetQuestionId)
|
|
1183
|
+
: undefined;
|
|
1184
|
+
if (targetQuestionId && !targetUpdate) {
|
|
1185
|
+
fail(`Focused question ${targetQuestionId} requires an explicit question_updates entry.`);
|
|
1186
|
+
}
|
|
1187
|
+
if (targetUpdate) {
|
|
1188
|
+
const closesTarget =
|
|
1189
|
+
targetUpdate.status === "resolved" || targetUpdate.status === "not-applicable";
|
|
1190
|
+
if (!remainsOpen && !closesTarget) {
|
|
1191
|
+
fail("A resolved focused judgment must explicitly resolve or dismiss its question.");
|
|
1192
|
+
}
|
|
1193
|
+
if (remainsOpen && openedQuestions.length === 0 && closesTarget) {
|
|
1194
|
+
fail(
|
|
1195
|
+
"A focused question cannot close while its judgment reports no replacement evidence question.",
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
if (remainsOpen && openedQuestions.length > 0 && !closesTarget) {
|
|
1199
|
+
fail(
|
|
1200
|
+
"Replacement questions require explicitly resolving or dismissing the broader focused question.",
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
594
1205
|
const event: JudgmentEvent = {
|
|
595
1206
|
protocol: PROTOCOL,
|
|
596
1207
|
kind: "judgment",
|
|
597
1208
|
routeId: params.route_id,
|
|
598
|
-
question:
|
|
599
|
-
owner:
|
|
1209
|
+
question: activeRoute.question,
|
|
1210
|
+
owner: activeRoute.owner,
|
|
600
1211
|
status: params.status,
|
|
601
1212
|
result,
|
|
602
1213
|
basis,
|
|
603
1214
|
openedQuestions,
|
|
1215
|
+
questionUpdates,
|
|
604
1216
|
artifacts: (params.artifacts ?? []).map((item) => item.trim()).filter(Boolean),
|
|
605
1217
|
changedArtifacts: routesWithMutation.has(params.route_id),
|
|
606
1218
|
};
|
|
1219
|
+
if (!canApplyDeveloperEvent(state, event)) {
|
|
1220
|
+
fail(
|
|
1221
|
+
"Developer machine guard rejected the judgment transition from the current active route.",
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
607
1224
|
const nextState = applyDeveloperEvent(state, event);
|
|
608
1225
|
if (nextState.pendingQuestions.length > MAX_PENDING_QUESTIONS) {
|
|
609
1226
|
fail(
|
|
@@ -611,16 +1228,31 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
611
1228
|
);
|
|
612
1229
|
}
|
|
613
1230
|
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1231
|
+
const immediateQuestion =
|
|
1232
|
+
ctx.mode === "tui"
|
|
1233
|
+
? firstImmediateUserQuestion(state.pendingQuestions, event.openedQuestions)
|
|
1234
|
+
: undefined;
|
|
1235
|
+
let immediateDisposition: ImmediateQuestionDisposition | undefined;
|
|
1236
|
+
if (immediateQuestion) {
|
|
1237
|
+
try {
|
|
1238
|
+
immediateDisposition = await promptImmediateUserQuestion(ctx, immediateQuestion);
|
|
1239
|
+
} catch {
|
|
1240
|
+
ctx.ui.notify(
|
|
1241
|
+
"Could not open the Developer decision prompt; the question remains open.",
|
|
1242
|
+
"warning",
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const nextMessage = judgmentNextMessage(event, nextState);
|
|
1248
|
+
const resolvedQuestionCount = event.questionUpdates.filter(
|
|
1249
|
+
(update) => update.status === "resolved" || update.status === "not-applicable",
|
|
1250
|
+
).length;
|
|
1251
|
+
const response =
|
|
1252
|
+
`Recorded ${event.status} judgment for ${event.routeId}: ${event.result}\n` +
|
|
1253
|
+
`Question updates: ${resolvedQuestionCount} resolved, ${event.questionUpdates.length - resolvedQuestionCount} retained or blocked.\n` +
|
|
1254
|
+
nextMessage +
|
|
1255
|
+
immediateQuestionMessage(immediateQuestion, immediateDisposition);
|
|
624
1256
|
ensureSafeToolText(response, "Developer judgment result");
|
|
625
1257
|
|
|
626
1258
|
routesWithMutation.delete(params.route_id);
|
|
@@ -632,7 +1264,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
632
1264
|
renderCall(args, theme, context) {
|
|
633
1265
|
const status = typeof args.status === "string" && args.status.length > 0 ? args.status : "…";
|
|
634
1266
|
const result =
|
|
635
|
-
typeof args.result === "string" && args.result.length > 0
|
|
1267
|
+
typeof args.result === "string" && args.result.length > 0
|
|
1268
|
+
? compactJudgmentResult(args.result)
|
|
1269
|
+
: "…";
|
|
636
1270
|
const statusText =
|
|
637
1271
|
status === "resolved"
|
|
638
1272
|
? theme.fg("success", status)
|
|
@@ -648,7 +1282,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
648
1282
|
},
|
|
649
1283
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
650
1284
|
if (isPartial) {
|
|
651
|
-
return reusableText(
|
|
1285
|
+
return reusableText(
|
|
1286
|
+
theme.fg("warning", "recording development judgment…"),
|
|
1287
|
+
context.lastComponent,
|
|
1288
|
+
);
|
|
652
1289
|
}
|
|
653
1290
|
if (context.isError) {
|
|
654
1291
|
return reusableText(
|
|
@@ -666,28 +1303,15 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
666
1303
|
: event?.status === "blocked"
|
|
667
1304
|
? theme.fg("error", summary)
|
|
668
1305
|
: theme.fg("muted", summary);
|
|
669
|
-
if (expanded && event)
|
|
670
|
-
|
|
671
|
-
text += `\n${theme.fg("dim", `target · ${event.owner}`)}`;
|
|
672
|
-
text += `\n${theme.fg("dim", `question · ${event.question}`)}`;
|
|
673
|
-
text += `\n${theme.fg("dim", `result · ${event.result}`)}`;
|
|
674
|
-
for (const basis of event.basis) text += `\n${theme.fg("dim", `basis · ${basis}`)}`;
|
|
675
|
-
for (const artifact of event.artifacts) text += `\n${theme.fg("dim", `artifact · ${artifact}`)}`;
|
|
676
|
-
for (const question of event.openedQuestions) {
|
|
677
|
-
text += `\n${theme.fg("warning", `opened · ${question.id} · ${question.question}`)}`;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (!expanded && event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
1306
|
+
if (expanded && event) return expandedJudgment(event, text, theme);
|
|
1307
|
+
if (event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
681
1308
|
return reusableText(text, context.lastComponent);
|
|
682
1309
|
},
|
|
683
1310
|
});
|
|
684
1311
|
|
|
685
1312
|
const refreshAvailableSkills = (ctx: ExtensionCommandContext) => {
|
|
686
1313
|
if (typeof ctx.getSystemPromptOptions !== "function") return;
|
|
687
|
-
availableSkills = availablePackageSkills(
|
|
688
|
-
ctx.getSystemPromptOptions().skills ?? [],
|
|
689
|
-
skillsRoot,
|
|
690
|
-
);
|
|
1314
|
+
availableSkills = availablePackageSkills(ctx.getSystemPromptOptions().skills ?? [], skillsRoot);
|
|
691
1315
|
};
|
|
692
1316
|
|
|
693
1317
|
const statusMessage = () => {
|
|
@@ -697,7 +1321,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
697
1321
|
const pending =
|
|
698
1322
|
state.pendingQuestions.length > 0
|
|
699
1323
|
? state.pendingQuestions
|
|
700
|
-
.map(
|
|
1324
|
+
.map(
|
|
1325
|
+
(question) =>
|
|
1326
|
+
`${question.id} · ${question.status} · ${question.resolutionOwner}/${question.gate} · ${question.question} · resolves when: ${question.resolutionCriteria}`,
|
|
1327
|
+
)
|
|
701
1328
|
.join(" | ")
|
|
702
1329
|
: "none";
|
|
703
1330
|
const last = state.lastJudgment
|
|
@@ -711,12 +1338,33 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
711
1338
|
state.lastJudgment && state.lastJudgment.artifacts.length > 0
|
|
712
1339
|
? state.lastJudgment.artifacts.join(" | ")
|
|
713
1340
|
: "none";
|
|
1341
|
+
const history =
|
|
1342
|
+
state.judgmentHistory.length > 0
|
|
1343
|
+
? state.judgmentHistory
|
|
1344
|
+
.slice(-10)
|
|
1345
|
+
.map((judgment) => {
|
|
1346
|
+
const route = state.routeHistory.find(
|
|
1347
|
+
(candidate) => candidate.routeId === judgment.routeId,
|
|
1348
|
+
);
|
|
1349
|
+
const alternatives = (route?.consideredAlternatives ?? [])
|
|
1350
|
+
.map((alternative) => `${alternative.owner}: ${alternative.reason}`)
|
|
1351
|
+
.join("; ");
|
|
1352
|
+
return `${judgment.owner} · ${judgment.status} · ${judgment.result}${
|
|
1353
|
+
alternatives ? ` · considered ${alternatives}` : ""
|
|
1354
|
+
}`;
|
|
1355
|
+
})
|
|
1356
|
+
.join("\n")
|
|
1357
|
+
: "none";
|
|
714
1358
|
return (
|
|
715
1359
|
`${summarizeState(state)}` +
|
|
716
1360
|
`\nactive: ${active}` +
|
|
717
1361
|
`\nlast: ${last}` +
|
|
718
1362
|
`\nbasis: ${basis}` +
|
|
719
1363
|
`\nartifacts: ${artifacts}` +
|
|
1364
|
+
`\nimplementation framing: ${state.implementationFramingRequired ? "required" : "clear"}` +
|
|
1365
|
+
`\ncheckpoint: ${state.rerouteRequired ? "reroute required" : "ready"}` +
|
|
1366
|
+
`\nverification: ${state.verificationRequired ? "required" : "current"}` +
|
|
1367
|
+
`\nhistory:\n${history}` +
|
|
720
1368
|
`\nactive tools: ${pi.getActiveTools().join(", ")}` +
|
|
721
1369
|
`\navailable skills: ${[...availableSkills.keys()].join(", ") || "none"}` +
|
|
722
1370
|
`\npending: ${pending}` +
|
|
@@ -734,11 +1382,44 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
734
1382
|
: null;
|
|
735
1383
|
},
|
|
736
1384
|
handler: async (args, ctx) => {
|
|
1385
|
+
const submitQuestionResolution = async (question: PendingQuestion): Promise<boolean> => {
|
|
1386
|
+
const request = await editQuestionResolutionRequest(ctx, question);
|
|
1387
|
+
if (request === undefined) return false;
|
|
1388
|
+
const focusEvent: FocusEvent = {
|
|
1389
|
+
protocol: PROTOCOL,
|
|
1390
|
+
kind: "focus",
|
|
1391
|
+
questionId: question.id,
|
|
1392
|
+
};
|
|
1393
|
+
pi.appendEntry(FOCUS_ENTRY, focusEvent);
|
|
1394
|
+
state = applyDeveloperEvent(state, focusEvent);
|
|
1395
|
+
refreshUI(ctx);
|
|
1396
|
+
ctx.ui.setEditorText("");
|
|
1397
|
+
ctx.ui.notify(
|
|
1398
|
+
"Question response submitted. It remains open until Developer records a resolved or not-applicable judgment.",
|
|
1399
|
+
"info",
|
|
1400
|
+
);
|
|
1401
|
+
if (ctx.isIdle()) pi.sendUserMessage(request);
|
|
1402
|
+
else pi.sendUserMessage(request, { deliverAs: "followUp" });
|
|
1403
|
+
return true;
|
|
1404
|
+
};
|
|
1405
|
+
|
|
737
1406
|
let action = args.trim();
|
|
738
1407
|
if (!action && ctx.mode === "tui") {
|
|
739
1408
|
refreshAvailableSkills(ctx);
|
|
740
|
-
|
|
741
|
-
|
|
1409
|
+
while (true) {
|
|
1410
|
+
const selection = await showDeveloperActionSelector(ctx, state);
|
|
1411
|
+
if (!selection) return;
|
|
1412
|
+
if (selection.kind === "question") {
|
|
1413
|
+
const question = state.pendingQuestions.find(
|
|
1414
|
+
(item) => item.id === selection.questionId,
|
|
1415
|
+
);
|
|
1416
|
+
if (!question) return;
|
|
1417
|
+
if (await submitQuestionResolution(question)) return;
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
action = selection.value;
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
742
1423
|
}
|
|
743
1424
|
if (!action) action = "status";
|
|
744
1425
|
if (action === "on" || action === "strict" || action === "off") {
|
|
@@ -769,25 +1450,26 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
769
1450
|
return;
|
|
770
1451
|
}
|
|
771
1452
|
if (ctx.mode === "tui") {
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
questionId
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
ctx.ui.notify("Open question focused and loaded into the editor for review.", "info");
|
|
786
|
-
return;
|
|
1453
|
+
const soleQuestion =
|
|
1454
|
+
state.pendingQuestions.length === 1 ? state.pendingQuestions[0] : undefined;
|
|
1455
|
+
if (soleQuestion) {
|
|
1456
|
+
await submitQuestionResolution(soleQuestion);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
while (true) {
|
|
1460
|
+
const questionId = await showPendingQuestionSelector(ctx, state.pendingQuestions);
|
|
1461
|
+
if (!questionId) return;
|
|
1462
|
+
const question = state.pendingQuestions.find((item) => item.id === questionId);
|
|
1463
|
+
if (!question) return;
|
|
1464
|
+
if (await submitQuestionResolution(question)) return;
|
|
1465
|
+
}
|
|
787
1466
|
}
|
|
788
1467
|
ctx.ui.notify(
|
|
789
1468
|
state.pendingQuestions
|
|
790
|
-
.map(
|
|
1469
|
+
.map(
|
|
1470
|
+
(question) =>
|
|
1471
|
+
`${question.id} · ${question.status} · ${question.resolutionOwner}/${question.gate} · ${question.question}\n resolves when: ${question.resolutionCriteria}`,
|
|
1472
|
+
)
|
|
791
1473
|
.join("\n"),
|
|
792
1474
|
"info",
|
|
793
1475
|
);
|
|
@@ -826,19 +1508,41 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
826
1508
|
pi.on("before_agent_start", (event) => {
|
|
827
1509
|
availableSkills = availablePackageSkills(event.systemPromptOptions.skills ?? [], skillsRoot);
|
|
828
1510
|
if (state.mode === "off") return;
|
|
829
|
-
return {
|
|
1511
|
+
return {
|
|
1512
|
+
systemPrompt: event.systemPrompt + protocolPrompt(state, [...availableSkills.keys()]),
|
|
1513
|
+
};
|
|
830
1514
|
});
|
|
831
1515
|
|
|
832
1516
|
pi.on("tool_call", (event) => {
|
|
833
|
-
const
|
|
834
|
-
if (
|
|
835
|
-
|
|
1517
|
+
const capability = builtinControlledToolCapabilities(pi.getAllTools()).get(event.toolName);
|
|
1518
|
+
if (!capability) return;
|
|
1519
|
+
|
|
1520
|
+
const access = protocolToolAccess(state);
|
|
1521
|
+
if (isControlledToolAllowed({ mode: state.mode, capability, access })) {
|
|
1522
|
+
if (access.canMutate && state.activeRoute) routesWithMutation.add(state.activeRoute.routeId);
|
|
836
1523
|
return;
|
|
837
1524
|
}
|
|
838
|
-
|
|
1525
|
+
|
|
1526
|
+
const directBlockers = state.pendingQuestions.filter(
|
|
1527
|
+
(question) => question.gate === "before-direct",
|
|
1528
|
+
);
|
|
1529
|
+
if (directBlockers.length > 0) {
|
|
1530
|
+
const blockedAction =
|
|
1531
|
+
capability === "execute" ? "unrouted shell execution" : "artifact mutation";
|
|
1532
|
+
return {
|
|
1533
|
+
block: true,
|
|
1534
|
+
reason: `Developer question gate blocks ${blockedAction} until resolved: ${directBlockers
|
|
1535
|
+
.map((question) => `${question.id} (${question.resolutionOwner}: ${question.question})`)
|
|
1536
|
+
.join("; ")}. A non-direct judgment route may still use bash to obtain evidence.`,
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
|
|
839
1540
|
return {
|
|
840
1541
|
block: true,
|
|
841
|
-
reason:
|
|
1542
|
+
reason:
|
|
1543
|
+
capability === "execute"
|
|
1544
|
+
? `Developer strict mode requires an active judgment or direct route before Pi built-in bash can execute evidence checks.`
|
|
1545
|
+
: `Developer strict mode requires an active ${ROUTE_TOOL} targeting direct action (owner=direct) before Pi built-in edit or write.`,
|
|
842
1546
|
};
|
|
843
1547
|
});
|
|
844
1548
|
|