@hobin/developer 0.1.6 → 0.1.8
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 +127 -95
- package/SOURCES.md +8 -8
- package/extensions/developer.ts +1752 -1441
- package/extensions/machine.ts +418 -286
- package/extensions/references/behavior-preserving-structural-change.md +14 -13
- package/extensions/state.ts +463 -419
- package/extensions/tool-policy.ts +158 -65
- package/extensions/tui.ts +1666 -910
- package/package.json +1 -1
- package/skills/sketch/references/responsibility-and-variation.md +1 -1
package/extensions/developer.ts
CHANGED
|
@@ -4,80 +4,85 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
|
|
5
5
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
DEFAULT_MAX_BYTES,
|
|
8
|
+
DEFAULT_MAX_LINES,
|
|
9
|
+
getMarkdownTheme,
|
|
10
|
+
keyHint,
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
type ExtensionCommandContext,
|
|
13
|
+
type ExtensionContext,
|
|
14
|
+
type Skill,
|
|
15
|
+
type Theme,
|
|
16
|
+
type ThemeColor,
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { Container, Markdown, Text } from "@earendil-works/pi-tui";
|
|
19
19
|
import { Type, type Static } from "typebox";
|
|
20
20
|
|
|
21
21
|
import { availablePackageSkills, renderSkillMethod } from "./skills.ts";
|
|
22
22
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
type RouteEvent,
|
|
23
|
+
ACTIVATION_ENTRY,
|
|
24
|
+
FOCUS_ENTRY,
|
|
25
|
+
JUDGMENT_TOOL,
|
|
26
|
+
MAX_RESPONSE_FIELDS,
|
|
27
|
+
MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
28
|
+
MAX_RESPONSE_OPTIONS,
|
|
29
|
+
MAX_RESPONSE_TEXT_CHARS,
|
|
30
|
+
PROTOCOL,
|
|
31
|
+
ROUTE_TOOL,
|
|
32
|
+
applyDeveloperEvent,
|
|
33
|
+
canApplyDeveloperEvent,
|
|
34
|
+
developerSnapshot,
|
|
35
|
+
initialState,
|
|
36
|
+
normalizeDeveloperEvent,
|
|
37
|
+
parseChoiceResponseSpec,
|
|
38
|
+
protocolState,
|
|
39
|
+
reconstructState,
|
|
40
|
+
type ActivationEvent,
|
|
41
|
+
type ChoiceResponseSpec,
|
|
42
|
+
type DeveloperState,
|
|
43
|
+
type ImplementationProfile,
|
|
44
|
+
type FocusEvent,
|
|
45
|
+
type JudgmentEvent,
|
|
46
|
+
type PendingQuestion,
|
|
47
|
+
type PendingQuestionStatus,
|
|
48
|
+
type QuestionGate,
|
|
49
|
+
type QuestionResolutionOwner,
|
|
50
|
+
type QuestionUpdate,
|
|
51
|
+
type QuestionUpdateStatus,
|
|
52
|
+
type RouteAlternative,
|
|
53
|
+
type RouteEvent,
|
|
55
54
|
} from "./state.ts";
|
|
56
55
|
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
TOOL_POLICY_LIFECYCLE_ENTRY,
|
|
57
|
+
builtinControlledToolCapabilities,
|
|
58
|
+
isControlledToolAllowed,
|
|
59
|
+
reconcileProtocolTools,
|
|
60
|
+
reloadSafeToolPolicyMarker,
|
|
61
|
+
toolPolicyReloadRequiresRestart,
|
|
62
|
+
type ProtocolToolAccess,
|
|
63
|
+
type ToolPolicyMemory,
|
|
62
64
|
} from "./tool-policy.ts";
|
|
63
65
|
import {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
DeveloperWidget,
|
|
67
|
+
developerHistoryEntries,
|
|
68
|
+
editQuestionResolutionRequest,
|
|
69
|
+
promptImmediateUserQuestion,
|
|
70
|
+
renderDeveloperFooter,
|
|
71
|
+
showDeveloperHistoryDetail,
|
|
72
|
+
showDeveloperHistorySelector,
|
|
73
|
+
showDeveloperSettings,
|
|
74
|
+
showDeveloperStatus,
|
|
75
|
+
showPendingQuestionSelector,
|
|
76
|
+
type ImmediateQuestionDisposition,
|
|
72
77
|
} from "./tui.ts";
|
|
73
78
|
|
|
74
79
|
const PROTOCOL_TOOLS = [ROUTE_TOOL, JUDGMENT_TOOL] as const;
|
|
75
80
|
const extensionRoot = dirname(fileURLToPath(import.meta.url));
|
|
76
81
|
const skillsRoot = resolve(extensionRoot, "..", "skills");
|
|
77
82
|
const structuralChangeMethodPath = resolve(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
extensionRoot,
|
|
84
|
+
"references",
|
|
85
|
+
"behavior-preserving-structural-change.md",
|
|
81
86
|
);
|
|
82
87
|
const MAX_PENDING_QUESTIONS = 20;
|
|
83
88
|
const MAX_QUESTION_CHARS = 2_000;
|
|
@@ -85,1478 +90,1784 @@ const MAX_QUESTION_CONTEXT_CHARS = 8_000;
|
|
|
85
90
|
const MAX_EVIDENCE_CHARS = 2_000;
|
|
86
91
|
const MAX_RESULT_CHARS = 12_000;
|
|
87
92
|
const MAX_ARTIFACT_CHARS = 4_096;
|
|
88
|
-
const DEVELOPER_COMMAND_ACTIONS = ["on", "
|
|
93
|
+
const DEVELOPER_COMMAND_ACTIONS = ["on", "status", "questions", "off"] as const;
|
|
94
|
+
const TOOL_POLICY_RESTART_MESSAGE =
|
|
95
|
+
"Developer detected an in-process package reload from a version without reload-safe tool handoff. Restart the Pi process before enabling Developer; /reload and /develop off/on cannot safely reconstruct the prior built-in tool selection.";
|
|
89
96
|
|
|
90
97
|
function textResult<T>(text: string, details: T) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text" as const, text }],
|
|
100
|
+
details,
|
|
101
|
+
};
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
function resultText(result: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
function resultText(result: {
|
|
105
|
+
content: Array<{ type: string; text?: string }>;
|
|
106
|
+
}): string {
|
|
107
|
+
return result.content
|
|
108
|
+
.filter((item) => item.type === "text" && item.text)
|
|
109
|
+
.map((item) => item.text)
|
|
110
|
+
.join("\n");
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
function reusableText(content: string, lastComponent: unknown): Text {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
const component =
|
|
115
|
+
lastComponent instanceof Text ? lastComponent : new Text("", 0, 0);
|
|
116
|
+
component.setText(content);
|
|
117
|
+
return component;
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
function ensureSafeToolText(text: string, label: string): void {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
122
|
+
const lines = text.split(/\r?\n/).length;
|
|
123
|
+
if (bytes > DEFAULT_MAX_BYTES || lines > DEFAULT_MAX_LINES) {
|
|
124
|
+
fail(
|
|
125
|
+
`${label} exceeds Pi's tool-output limit (${bytes} bytes, ${lines} lines). Narrow the routed question or evidence.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
function fail(message: string): never {
|
|
121
|
-
|
|
131
|
+
throw new Error(message);
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
function sameToolSet(left: string[], right: string[]): boolean {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
if (left.length !== right.length) return false;
|
|
136
|
+
const rightSet = new Set(right);
|
|
137
|
+
return left.every((tool) => rightSet.has(tool));
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
function compactLine(value: string, maxChars = 160): string {
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
const line = value.replace(/\s+/g, " ").trim();
|
|
142
|
+
return line.length <= maxChars ? line : `${line.slice(0, maxChars - 1)}…`;
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
function normalizedQuestion(value: string): string {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
return value
|
|
147
|
+
.toLocaleLowerCase()
|
|
148
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
149
|
+
.trim();
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
interface ChoiceResponseOptionInput {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
value: string;
|
|
154
|
+
label: string;
|
|
155
|
+
description?: string;
|
|
156
|
+
detail_prompt?: string;
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
interface ChoiceResponseFieldInput {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
id: string;
|
|
161
|
+
prompt: string;
|
|
162
|
+
description?: string;
|
|
163
|
+
options: ChoiceResponseOptionInput[];
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
interface ChoiceResponseSpecInput {
|
|
157
|
-
|
|
158
|
-
|
|
167
|
+
kind: "choice-form";
|
|
168
|
+
fields: ChoiceResponseFieldInput[];
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
interface OpenQuestionInput {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
question: string;
|
|
173
|
+
context?: string;
|
|
174
|
+
response_spec?: ChoiceResponseSpecInput;
|
|
175
|
+
status: PendingQuestionStatus;
|
|
176
|
+
resolution_owner: Exclude<QuestionResolutionOwner, "unknown">;
|
|
177
|
+
gate: QuestionGate;
|
|
178
|
+
resolution_criteria: string;
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
interface QuestionUpdateInput {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
question_id: string;
|
|
183
|
+
status: QuestionUpdateStatus;
|
|
184
|
+
result: string;
|
|
185
|
+
basis: string[];
|
|
176
186
|
}
|
|
177
187
|
|
|
178
|
-
function legacyOpenQuestion(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
function legacyOpenQuestion(
|
|
189
|
+
question: string,
|
|
190
|
+
judgmentStatus?: string,
|
|
191
|
+
): OpenQuestionInput {
|
|
192
|
+
if (judgmentStatus === "blocked") {
|
|
193
|
+
return {
|
|
194
|
+
question,
|
|
195
|
+
status: "blocked",
|
|
196
|
+
resolution_owner: "environment",
|
|
197
|
+
gate: "before-completion",
|
|
198
|
+
resolution_criteria: `Obtain evidence that settles: ${question}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
question,
|
|
203
|
+
status: "open",
|
|
204
|
+
resolution_owner: "agent",
|
|
205
|
+
gate: "none",
|
|
206
|
+
resolution_criteria: `Obtain evidence that settles: ${question}`,
|
|
207
|
+
};
|
|
195
208
|
}
|
|
196
209
|
|
|
197
210
|
function buildChoiceResponseSpec(
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
input: ChoiceResponseSpecInput | undefined,
|
|
212
|
+
owner: OpenQuestionInput["resolution_owner"],
|
|
200
213
|
): ChoiceResponseSpec | undefined {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
214
|
+
if (!input) return undefined;
|
|
215
|
+
if (owner !== "user") {
|
|
216
|
+
fail("response_spec is valid only for user-owned questions.");
|
|
217
|
+
}
|
|
218
|
+
const responseSpec = parseChoiceResponseSpec({
|
|
219
|
+
kind: input.kind,
|
|
220
|
+
fields: input.fields.map((field) => ({
|
|
221
|
+
id: field.id,
|
|
222
|
+
prompt: field.prompt,
|
|
223
|
+
description: field.description,
|
|
224
|
+
options: field.options.map((option) => ({
|
|
225
|
+
value: option.value,
|
|
226
|
+
label: option.label,
|
|
227
|
+
description: option.description,
|
|
228
|
+
detailPrompt: option.detail_prompt,
|
|
229
|
+
})),
|
|
230
|
+
})),
|
|
231
|
+
});
|
|
232
|
+
if (!responseSpec) {
|
|
233
|
+
fail(
|
|
234
|
+
"response_spec must use unique field IDs and unique option values with non-whitespace text.",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return responseSpec;
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
function buildOpenedQuestions(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
input: OpenQuestionInput[],
|
|
242
|
+
state: DeveloperState,
|
|
243
|
+
routeId: string,
|
|
231
244
|
): PendingQuestion[] {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
245
|
+
const questionIds = new Map(
|
|
246
|
+
state.pendingQuestions.map((question) => [
|
|
247
|
+
normalizedQuestion(question.question),
|
|
248
|
+
question.id,
|
|
249
|
+
]),
|
|
250
|
+
);
|
|
251
|
+
const openedQuestions: PendingQuestion[] = [];
|
|
252
|
+
for (const [index, item] of input.entries()) {
|
|
253
|
+
const question = item.question.trim();
|
|
254
|
+
const context = item.context?.trim() || undefined;
|
|
255
|
+
const responseSpec = buildChoiceResponseSpec(
|
|
256
|
+
item.response_spec,
|
|
257
|
+
item.resolution_owner,
|
|
258
|
+
);
|
|
259
|
+
const resolutionCriteria = item.resolution_criteria.trim();
|
|
260
|
+
if (!question || !resolutionCriteria) {
|
|
261
|
+
fail(
|
|
262
|
+
"Each open question requires non-whitespace question and resolution_criteria text.",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (item.resolution_owner === "user" && item.gate !== "none" && !context) {
|
|
266
|
+
fail(
|
|
267
|
+
"User-owned questions with a before-implementation or before-completion gate require non-whitespace context that explains the decision before asking for an answer.",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const questionKey = normalizedQuestion(question);
|
|
271
|
+
const questionId =
|
|
272
|
+
questionIds.get(questionKey) ?? `question:${routeId}:open:${index + 1}`;
|
|
273
|
+
questionIds.set(questionKey, questionId);
|
|
274
|
+
const pendingQuestion: PendingQuestion = {
|
|
275
|
+
id: questionId,
|
|
276
|
+
question,
|
|
277
|
+
context,
|
|
278
|
+
responseSpec,
|
|
279
|
+
status: item.status,
|
|
280
|
+
resolutionOwner: item.resolution_owner,
|
|
281
|
+
gate: item.gate,
|
|
282
|
+
resolutionCriteria,
|
|
283
|
+
sourceRouteId: routeId,
|
|
284
|
+
};
|
|
285
|
+
const duplicateIndex = openedQuestions.findIndex(
|
|
286
|
+
(candidate) => candidate.id === questionId,
|
|
287
|
+
);
|
|
288
|
+
if (duplicateIndex === -1) openedQuestions.push(pendingQuestion);
|
|
289
|
+
else openedQuestions[duplicateIndex] = pendingQuestion;
|
|
290
|
+
}
|
|
291
|
+
return openedQuestions;
|
|
263
292
|
}
|
|
264
293
|
|
|
265
294
|
function firstImmediateUserQuestion(
|
|
266
|
-
|
|
267
|
-
|
|
295
|
+
previous: readonly PendingQuestion[],
|
|
296
|
+
opened: readonly PendingQuestion[],
|
|
268
297
|
): PendingQuestion | undefined {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
298
|
+
const previousIds = new Set(previous.map((question) => question.id));
|
|
299
|
+
return opened.find(
|
|
300
|
+
(question) =>
|
|
301
|
+
!previousIds.has(question.id) &&
|
|
302
|
+
question.status === "open" &&
|
|
303
|
+
question.resolutionOwner === "user" &&
|
|
304
|
+
question.gate === "before-implementation",
|
|
305
|
+
);
|
|
277
306
|
}
|
|
278
307
|
|
|
279
308
|
function immediateQuestionMessage(
|
|
280
|
-
|
|
281
|
-
|
|
309
|
+
question: PendingQuestion | undefined,
|
|
310
|
+
disposition: ImmediateQuestionDisposition | undefined,
|
|
282
311
|
): string {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
312
|
+
if (!question) return "";
|
|
313
|
+
if (disposition?.kind === "answer") {
|
|
314
|
+
return [
|
|
315
|
+
"",
|
|
316
|
+
`Immediate user answer for ${question.id}:`,
|
|
317
|
+
disposition.request,
|
|
318
|
+
"",
|
|
319
|
+
`Route this exact question with open_question_id=${question.id}, use the answer as new evidence, and close it only through question_updates.`,
|
|
320
|
+
].join("\n");
|
|
321
|
+
}
|
|
322
|
+
return `\nThe user left ${question.id} open for later.`;
|
|
294
323
|
}
|
|
295
324
|
|
|
296
325
|
function buildQuestionUpdates(
|
|
297
|
-
|
|
298
|
-
|
|
326
|
+
input: QuestionUpdateInput[],
|
|
327
|
+
state: DeveloperState,
|
|
299
328
|
): QuestionUpdate[] {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
329
|
+
const questionUpdates = input.map((update) => ({
|
|
330
|
+
questionId: update.question_id.trim(),
|
|
331
|
+
status: update.status,
|
|
332
|
+
result: update.result.trim(),
|
|
333
|
+
basis: update.basis.map((item) => item.trim()).filter(Boolean),
|
|
334
|
+
}));
|
|
335
|
+
const knownQuestionIds = new Set(
|
|
336
|
+
state.pendingQuestions.map((question) => question.id),
|
|
337
|
+
);
|
|
338
|
+
const updatedIds = new Set<string>();
|
|
339
|
+
for (const update of questionUpdates) {
|
|
340
|
+
if (!knownQuestionIds.has(update.questionId))
|
|
341
|
+
fail(`Unknown pending question ID: ${update.questionId}`);
|
|
342
|
+
if (updatedIds.has(update.questionId))
|
|
343
|
+
fail(`Question ${update.questionId} was updated more than once.`);
|
|
344
|
+
updatedIds.add(update.questionId);
|
|
345
|
+
if (!update.result)
|
|
346
|
+
fail(`Question update ${update.questionId} requires a non-empty result.`);
|
|
347
|
+
if (
|
|
348
|
+
(update.status === "resolved" ||
|
|
349
|
+
update.status === "not-applicable" ||
|
|
350
|
+
update.status === "blocked") &&
|
|
351
|
+
update.basis.length === 0
|
|
352
|
+
) {
|
|
353
|
+
fail(
|
|
354
|
+
`Question update ${update.questionId} with status ${update.status} requires concrete basis.`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return questionUpdates;
|
|
327
359
|
}
|
|
328
360
|
|
|
329
|
-
function judgmentNextMessage(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
361
|
+
function judgmentNextMessage(
|
|
362
|
+
event: JudgmentEvent,
|
|
363
|
+
nextState: DeveloperState,
|
|
364
|
+
): string {
|
|
365
|
+
if (event.target === "implementation") {
|
|
366
|
+
if (nextState.verificationRequired) {
|
|
367
|
+
return "Stable landing recorded. Route again from the new evidence; verify is required before claiming completion.";
|
|
368
|
+
}
|
|
369
|
+
return "Stable landing recorded. Route again from the new evidence before selecting another movement.";
|
|
370
|
+
}
|
|
371
|
+
const next = protocolState(nextState);
|
|
372
|
+
if (next === "idle") {
|
|
373
|
+
return "Developer protocol is idle. This is routing state only and does not prove task completion.";
|
|
374
|
+
}
|
|
375
|
+
return `Developer protocol is ${next}. Address the current routing obligation before handoff.`;
|
|
341
376
|
}
|
|
342
377
|
|
|
343
378
|
function inferredQuestionId(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
379
|
+
state: DeveloperState,
|
|
380
|
+
question: string,
|
|
381
|
+
explicitQuestionId?: string,
|
|
347
382
|
): string | undefined {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
383
|
+
if (explicitQuestionId) return explicitQuestionId;
|
|
384
|
+
if (state.focusedQuestionId) return state.focusedQuestionId;
|
|
385
|
+
const normalized = normalizedQuestion(question);
|
|
386
|
+
const exact = state.pendingQuestions.filter(
|
|
387
|
+
(pending) => normalizedQuestion(pending.question) === normalized,
|
|
388
|
+
);
|
|
389
|
+
return exact.length === 1 ? exact[0]?.id : undefined;
|
|
355
390
|
}
|
|
356
391
|
|
|
357
392
|
function protocolToolAccess(state: DeveloperState): ProtocolToolAccess {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
393
|
+
const snapshot = developerSnapshot(state);
|
|
394
|
+
return {
|
|
395
|
+
allowsShell: snapshot.hasTag("execute"),
|
|
396
|
+
allowsArtifactTools: snapshot.hasTag("mutate"),
|
|
397
|
+
hasBeforeImplementationGate: snapshot.hasTag("blocks-implementation"),
|
|
398
|
+
};
|
|
364
399
|
}
|
|
365
400
|
|
|
366
401
|
function summarizeState(state: DeveloperState): string {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
402
|
+
if (!state.enabled) return "developer: off";
|
|
403
|
+
const target = state.activeRoute ? state.activeRoute.target : "none";
|
|
404
|
+
return `developer: on · target: ${target} · ${protocolState(state)}`;
|
|
370
405
|
}
|
|
371
406
|
|
|
372
|
-
function protocolPrompt(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
407
|
+
function protocolPrompt(
|
|
408
|
+
state: DeveloperState,
|
|
409
|
+
availableSkillNames: string[],
|
|
410
|
+
): string {
|
|
411
|
+
const lines = [
|
|
412
|
+
"",
|
|
413
|
+
"Developer protocol is active.",
|
|
414
|
+
"- Use the default topology as a conditional backbone, not a rigid lifecycle: clarify meaning when needed -> model consequential cases -> sketch the first implementation surface for new behavior or signal the smallest structural movement in existing code -> execute one implementation step -> verify current claims.",
|
|
415
|
+
"- Adapt away from that topology when evidence makes a stage not applicable, but never jump directly from a resolved model to mutation: use sketch for feature implementation or signal for existing-code structural movement first.",
|
|
416
|
+
`- Call ${ROUTE_TOOL} for exactly one concrete judgment or one green-to-green implementation movement.`,
|
|
417
|
+
"- Use target=implementation only when the next local movement, stable landing, and narrow verification are already justified; otherwise choose the focused skill whose scope fits the current question.",
|
|
418
|
+
"- An implementation 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.",
|
|
419
|
+
"- For implementation structural work intended to preserve behavior, set execution_profile=behavior-preserving-structure; omit the field for other implementation actions and every skill route.",
|
|
420
|
+
`- Follow the routed method, then close the route with ${JUDGMENT_TOOL}. After every implementation stable landing, route again from the new evidence before selecting another movement.`,
|
|
421
|
+
"- Consecutive implementation routes are valid when the new evidence still justifies implementation action. On a reroute after implementation, explicitly reconsider the most plausible skill routes and record why they are not needed; do not choose implementation by momentum.",
|
|
422
|
+
"- Do not carry a predetermined implementation queue through multiple implementation steps. Re-observe after each stable landing and reroute to a skill whenever meaning, cases, design, structural direction, timing, naming, or evidence becomes uncertain.",
|
|
423
|
+
"- Protocol state is routing bookkeeping. Idle never proves product completion, user acceptance, or current verification.",
|
|
424
|
+
"- 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.",
|
|
425
|
+
`- 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.`,
|
|
426
|
+
"- 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.",
|
|
427
|
+
"- 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.",
|
|
428
|
+
"- Pending questions declare who can resolve them, what they block, and observable resolution criteria. User-owned before-implementation or before-completion questions require context that explains the decision before asking for an 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-implementation gate.",
|
|
429
|
+
"- Use before-implementation only when the missing answer can change whether or what artifact mutation is valid. An agent-owned before-implementation question must be resolvable through a non-implementation skill route using observation or evidence execution; it must never require the mutation it blocks.",
|
|
430
|
+
"- Product files are changed with Pi implementation tools. Developer protocol tools only route and record judgments.",
|
|
431
|
+
"- Developer 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 an implementation route. It does not classify or sandbox other extensions' tools.",
|
|
432
|
+
"- Use a skill judgment route, not an implementation route, when repository discovery or verifier execution is the unresolved work. Implementation remains the artifact-mutation lane.",
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
if (state.implementationFramingRequired) {
|
|
436
|
+
lines.push(
|
|
437
|
+
"Implementation gate: the latest resolved model exposed implementation work. Before implementation mutation, route sketch for new feature shape or signal for existing-code structural movement; other judgments may still be routed first.",
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (state.verificationRequired) {
|
|
441
|
+
lines.push(
|
|
442
|
+
"Verification debt: an implementation route changed artifacts after the last resolved verify judgment. Route verify before claiming completion or handing off as done.",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const completionGates = state.pendingQuestions.filter(
|
|
446
|
+
(question) =>
|
|
447
|
+
question.gate === "before-completion" ||
|
|
448
|
+
question.gate === "before-implementation",
|
|
449
|
+
);
|
|
450
|
+
if (completionGates.length > 0) {
|
|
451
|
+
lines.push(
|
|
452
|
+
`Completion gate: ${completionGates.map((question) => question.id).join(", ")} must be resolved before a completion claim.`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (!state.activeRoute && state.rerouteRequired) {
|
|
456
|
+
lines.push(
|
|
457
|
+
"Rerouting checkpoint: use the previous implementation landing as known_evidence. If implementation is still right, include alternatives_considered for the plausible available skills and explain why each adds no useful judgment now.",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (state.activeRoute) {
|
|
462
|
+
lines.push(
|
|
463
|
+
`Active route: ${state.activeRoute.routeId} · ${state.activeRoute.target} · ${state.activeRoute.question}`,
|
|
464
|
+
);
|
|
465
|
+
if (state.activeRoute.methodLocation) {
|
|
466
|
+
lines.push(
|
|
467
|
+
`Active skill location: ${state.activeRoute.methodLocation}. Read it again if compaction or a later turn no longer contains the full instructions.`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (state.pendingQuestions.length > 0) {
|
|
472
|
+
lines.push("Pending Developer questions:");
|
|
473
|
+
for (const question of state.pendingQuestions) {
|
|
474
|
+
lines.push(
|
|
475
|
+
`- ${question.id} · ${question.status} · owner=${question.resolutionOwner} · gate=${question.gate} · ${question.question} · resolves when: ${question.resolutionCriteria}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
lines.push(
|
|
479
|
+
"- 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.",
|
|
480
|
+
"- question_updates may resolve any pending ID from implementation, test, inspection, user, or environment evidence; each resolved update requires concrete basis.",
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
lines.push(
|
|
484
|
+
`Available Developer skills: ${availableSkillNames.length > 0 ? availableSkillNames.join(", ") : "none; use implementation only"}.`,
|
|
485
|
+
);
|
|
486
|
+
return lines.join("\n");
|
|
452
487
|
}
|
|
453
488
|
|
|
454
489
|
function routeRenderText(event: RouteEvent | undefined): string {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
490
|
+
if (!event) return "Route unavailable";
|
|
491
|
+
const target = event.targetQuestionId
|
|
492
|
+
? ` · revisits ${event.targetQuestionId}`
|
|
493
|
+
: "";
|
|
494
|
+
const profile = event.executionProfile ? `/${event.executionProfile}` : "";
|
|
495
|
+
return `${event.target}${profile} · ${compactLine(event.question)}${target}`;
|
|
459
496
|
}
|
|
460
497
|
|
|
461
498
|
function compactJudgmentResult(result: string, maxChars = 160): string {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
499
|
+
const firstContentLine = result
|
|
500
|
+
.split(/\r?\n/)
|
|
501
|
+
.map((line) => line.trim())
|
|
502
|
+
.find((line) => line && !line.startsWith("```"));
|
|
503
|
+
return compactLine(firstContentLine ?? result, maxChars);
|
|
467
504
|
}
|
|
468
505
|
|
|
469
506
|
function judgmentRenderText(event: JudgmentEvent | undefined): string {
|
|
470
|
-
|
|
471
|
-
|
|
507
|
+
if (!event) return "Judgment unavailable";
|
|
508
|
+
return `${event.status} · ${compactJudgmentResult(event.result)}`;
|
|
472
509
|
}
|
|
473
510
|
|
|
474
511
|
function detailLine(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
512
|
+
theme: Theme,
|
|
513
|
+
label: string,
|
|
514
|
+
value: string,
|
|
515
|
+
valueColor: ThemeColor = "muted",
|
|
479
516
|
): string {
|
|
480
|
-
|
|
517
|
+
return `${theme.fg("dim", `${label} · `)}${theme.fg(valueColor, value)}`;
|
|
481
518
|
}
|
|
482
519
|
|
|
483
|
-
function expandedJudgment(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
520
|
+
function expandedJudgment(
|
|
521
|
+
event: JudgmentEvent,
|
|
522
|
+
statusText: string,
|
|
523
|
+
theme: Theme,
|
|
524
|
+
): Container {
|
|
525
|
+
const container = new Container();
|
|
526
|
+
const header = [
|
|
527
|
+
statusText,
|
|
528
|
+
detailLine(theme, "route", event.routeId, "text"),
|
|
529
|
+
detailLine(theme, "target", event.target, "accent"),
|
|
530
|
+
detailLine(theme, "question", event.question, "text"),
|
|
531
|
+
];
|
|
532
|
+
container.addChild(new Text(header.join("\n"), 0, 0));
|
|
533
|
+
container.addChild(new Markdown(event.result, 0, 0, getMarkdownTheme()));
|
|
534
|
+
|
|
535
|
+
const evidence = [
|
|
536
|
+
...event.basis.map((basis) => detailLine(theme, "basis", basis)),
|
|
537
|
+
...event.artifacts.map((artifact) =>
|
|
538
|
+
detailLine(theme, "artifact", artifact),
|
|
539
|
+
),
|
|
540
|
+
...event.openedQuestions.map((question) =>
|
|
541
|
+
detailLine(
|
|
542
|
+
theme,
|
|
543
|
+
`opened ${question.resolutionOwner}/${question.gate}`,
|
|
544
|
+
`${question.question} — resolves when: ${question.resolutionCriteria}`,
|
|
545
|
+
"warning",
|
|
546
|
+
),
|
|
547
|
+
),
|
|
548
|
+
...(event.questionUpdates ?? []).map((update) =>
|
|
549
|
+
detailLine(
|
|
550
|
+
theme,
|
|
551
|
+
`question ${update.status}`,
|
|
552
|
+
`${update.questionId} — ${update.result}`,
|
|
553
|
+
"accent",
|
|
554
|
+
),
|
|
555
|
+
),
|
|
556
|
+
];
|
|
557
|
+
if (evidence.length > 0)
|
|
558
|
+
container.addChild(new Text(evidence.join("\n"), 0, 0));
|
|
559
|
+
return container;
|
|
516
560
|
}
|
|
517
561
|
|
|
518
562
|
export default async function developer(pi: ExtensionAPI) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
563
|
+
let availableSkills = new Map<string, Skill>();
|
|
564
|
+
let state = initialState();
|
|
565
|
+
let routeOpening = false;
|
|
566
|
+
const routesWithMutation = new Set<string>();
|
|
567
|
+
let toolPolicyMemory: ToolPolicyMemory = { withheldBuiltins: new Set() };
|
|
568
|
+
let toolPolicyRestartRequired = false;
|
|
569
|
+
|
|
570
|
+
pi.registerFlag("develop", {
|
|
571
|
+
description: "Start with the Developer protocol enabled",
|
|
572
|
+
type: "boolean",
|
|
573
|
+
default: false,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const syncProtocolTools = () => {
|
|
577
|
+
const current = pi.getActiveTools();
|
|
578
|
+
const next = reconcileProtocolTools({
|
|
579
|
+
activeTools: current,
|
|
580
|
+
allTools: pi.getAllTools(),
|
|
581
|
+
enabled: state.enabled,
|
|
582
|
+
access: protocolToolAccess(state),
|
|
583
|
+
protocolTools: PROTOCOL_TOOLS,
|
|
584
|
+
memory: toolPolicyMemory,
|
|
585
|
+
});
|
|
586
|
+
toolPolicyMemory = next.memory;
|
|
587
|
+
if (!sameToolSet(current, next.activeTools))
|
|
588
|
+
pi.setActiveTools(next.activeTools);
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const releaseProtocolTools = () => {
|
|
592
|
+
const current = pi.getActiveTools();
|
|
593
|
+
const next = reconcileProtocolTools({
|
|
594
|
+
activeTools: current,
|
|
595
|
+
allTools: pi.getAllTools(),
|
|
596
|
+
enabled: false,
|
|
597
|
+
access: protocolToolAccess(state),
|
|
598
|
+
protocolTools: PROTOCOL_TOOLS,
|
|
599
|
+
memory: toolPolicyMemory,
|
|
600
|
+
});
|
|
601
|
+
toolPolicyMemory = next.memory;
|
|
602
|
+
if (!sameToolSet(current, next.activeTools))
|
|
603
|
+
pi.setActiveTools(next.activeTools);
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const refreshUI = (ctx: ExtensionContext) => {
|
|
607
|
+
if (toolPolicyRestartRequired) {
|
|
608
|
+
ctx.ui.setStatus("developer", "developer · restart required");
|
|
609
|
+
ctx.ui.setWidget(
|
|
610
|
+
"developer",
|
|
611
|
+
["blocked · restart Pi to reset Developer tool access"],
|
|
612
|
+
{ placement: "belowEditor" },
|
|
613
|
+
);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (!state.enabled) {
|
|
617
|
+
ctx.ui.setStatus("developer", undefined);
|
|
618
|
+
ctx.ui.setWidget("developer", undefined);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
ctx.ui.setStatus(
|
|
623
|
+
"developer",
|
|
624
|
+
ctx.mode === "tui"
|
|
625
|
+
? renderDeveloperFooter(state, ctx.ui.theme)
|
|
626
|
+
: summarizeState(state),
|
|
627
|
+
);
|
|
628
|
+
if (
|
|
629
|
+
!state.activeRoute &&
|
|
630
|
+
state.pendingQuestions.length === 0 &&
|
|
631
|
+
!state.implementationFramingRequired &&
|
|
632
|
+
!state.verificationRequired
|
|
633
|
+
) {
|
|
634
|
+
ctx.ui.setWidget("developer", undefined);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (ctx.mode === "tui") {
|
|
639
|
+
const viewState = state;
|
|
640
|
+
ctx.ui.setWidget(
|
|
641
|
+
"developer",
|
|
642
|
+
(_tui, theme) => new DeveloperWidget(viewState, theme),
|
|
643
|
+
{
|
|
644
|
+
placement: "belowEditor",
|
|
645
|
+
},
|
|
646
|
+
);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const lines = [
|
|
651
|
+
...(state.activeRoute
|
|
652
|
+
? [
|
|
653
|
+
`route · ${state.activeRoute.target} · ${compactLine(state.activeRoute.question)}`,
|
|
654
|
+
]
|
|
655
|
+
: []),
|
|
656
|
+
...state.pendingQuestions
|
|
657
|
+
.slice(0, 3)
|
|
658
|
+
.map(
|
|
659
|
+
(question) =>
|
|
660
|
+
`open · ${question.id} · ${compactLine(question.question)}`,
|
|
661
|
+
),
|
|
662
|
+
...(state.pendingQuestions.length > 3
|
|
663
|
+
? [`open · +${state.pendingQuestions.length - 3} more`]
|
|
664
|
+
: []),
|
|
665
|
+
...(state.implementationFramingRequired
|
|
666
|
+
? ["gate · frame implementation before mutation (sketch or signal)"]
|
|
667
|
+
: []),
|
|
668
|
+
...(state.verificationRequired
|
|
669
|
+
? ["next · verify changed artifacts before completion"]
|
|
670
|
+
: []),
|
|
671
|
+
];
|
|
672
|
+
ctx.ui.setWidget("developer", lines, { placement: "belowEditor" });
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const reconstruct = (ctx: ExtensionContext) => {
|
|
676
|
+
state = toolPolicyRestartRequired
|
|
677
|
+
? initialState()
|
|
678
|
+
: reconstructState(ctx.sessionManager.getBranch());
|
|
679
|
+
syncProtocolTools();
|
|
680
|
+
refreshUI(ctx);
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const setEnabled = (enabled: boolean, ctx: ExtensionContext): boolean => {
|
|
684
|
+
if (enabled && toolPolicyRestartRequired) {
|
|
685
|
+
ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
|
|
686
|
+
refreshUI(ctx);
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const event: ActivationEvent = {
|
|
690
|
+
protocol: PROTOCOL,
|
|
691
|
+
kind: "activation",
|
|
692
|
+
enabled,
|
|
693
|
+
};
|
|
694
|
+
pi.appendEntry(ACTIVATION_ENTRY, event);
|
|
695
|
+
state = applyDeveloperEvent(state, event);
|
|
696
|
+
syncProtocolTools();
|
|
697
|
+
refreshUI(ctx);
|
|
698
|
+
return true;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const SharedRouteParams = {
|
|
702
|
+
question: Type.String({
|
|
703
|
+
minLength: 1,
|
|
704
|
+
maxLength: MAX_QUESTION_CHARS,
|
|
705
|
+
description: "The single concrete judgment or action question to route",
|
|
706
|
+
}),
|
|
707
|
+
reason: Type.String({
|
|
708
|
+
minLength: 1,
|
|
709
|
+
maxLength: MAX_QUESTION_CHARS,
|
|
710
|
+
description: "Why this route target fits the current evidence",
|
|
711
|
+
}),
|
|
712
|
+
known_evidence: Type.Optional(
|
|
713
|
+
Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), {
|
|
714
|
+
maxItems: 12,
|
|
715
|
+
description: "Evidence already known before routing",
|
|
716
|
+
}),
|
|
717
|
+
),
|
|
718
|
+
open_question_id: Type.Optional(
|
|
719
|
+
Type.String({
|
|
720
|
+
maxLength: 512,
|
|
721
|
+
description: "Exact pending question ID when this route revisits one",
|
|
722
|
+
}),
|
|
723
|
+
),
|
|
724
|
+
};
|
|
725
|
+
const RouteAlternativeParam = Type.Object(
|
|
726
|
+
{
|
|
727
|
+
target: Type.String({
|
|
728
|
+
minLength: 1,
|
|
729
|
+
maxLength: 64,
|
|
730
|
+
description:
|
|
731
|
+
"Exact available Developer skill name that was reconsidered",
|
|
732
|
+
}),
|
|
733
|
+
reason: Type.String({
|
|
734
|
+
minLength: 1,
|
|
735
|
+
maxLength: MAX_EVIDENCE_CHARS,
|
|
736
|
+
description:
|
|
737
|
+
"Why this skill would add no useful judgment before the proposed implementation movement",
|
|
738
|
+
}),
|
|
739
|
+
},
|
|
740
|
+
{ additionalProperties: false },
|
|
741
|
+
);
|
|
742
|
+
const RouteParams = Type.Union([
|
|
743
|
+
Type.Object(
|
|
744
|
+
{
|
|
745
|
+
...SharedRouteParams,
|
|
746
|
+
target: Type.String({
|
|
747
|
+
minLength: 1,
|
|
748
|
+
maxLength: 64,
|
|
749
|
+
pattern: "^(?!implementation$)[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
750
|
+
description:
|
|
751
|
+
"Exact skill name from the current Available Developer skills list",
|
|
752
|
+
}),
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
additionalProperties: false,
|
|
756
|
+
description: "Route one question to a Developer skill",
|
|
757
|
+
},
|
|
758
|
+
),
|
|
759
|
+
Type.Object(
|
|
760
|
+
{
|
|
761
|
+
...SharedRouteParams,
|
|
762
|
+
target: Type.Literal("implementation", {
|
|
763
|
+
description:
|
|
764
|
+
"Use Pi implementation tools for an already-justified action",
|
|
765
|
+
}),
|
|
766
|
+
movement: Type.String({
|
|
767
|
+
minLength: 1,
|
|
768
|
+
maxLength: MAX_QUESTION_CHARS,
|
|
769
|
+
description:
|
|
770
|
+
"One locally explainable behavioral or structural movement; not a multi-step implementation queue",
|
|
771
|
+
}),
|
|
772
|
+
stop_condition: Type.String({
|
|
773
|
+
minLength: 1,
|
|
774
|
+
maxLength: MAX_QUESTION_CHARS,
|
|
775
|
+
description:
|
|
776
|
+
"The green, pausable, reviewable stable landing that ends this implementation route",
|
|
777
|
+
}),
|
|
778
|
+
verification: Type.String({
|
|
779
|
+
minLength: 1,
|
|
780
|
+
maxLength: MAX_EVIDENCE_CHARS,
|
|
781
|
+
description:
|
|
782
|
+
"The narrowest check that can catch the likely break in this movement",
|
|
783
|
+
}),
|
|
784
|
+
alternatives_considered: Type.Optional(
|
|
785
|
+
Type.Array(RouteAlternativeParam, {
|
|
786
|
+
maxItems: 6,
|
|
787
|
+
description:
|
|
788
|
+
"On a reroute after implementation, the plausible skill routes reconsidered and why each is unnecessary now",
|
|
789
|
+
}),
|
|
790
|
+
),
|
|
791
|
+
execution_profile: Type.Optional(
|
|
792
|
+
Type.Literal("behavior-preserving-structure", {
|
|
793
|
+
description:
|
|
794
|
+
"Load the focused structural-mutation protocol; omit for ordinary implementation action",
|
|
795
|
+
}),
|
|
796
|
+
),
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
additionalProperties: false,
|
|
800
|
+
description: "Route one already-justified implementation action",
|
|
801
|
+
},
|
|
802
|
+
),
|
|
803
|
+
]);
|
|
804
|
+
|
|
805
|
+
pi.registerTool({
|
|
806
|
+
name: ROUTE_TOOL,
|
|
807
|
+
label: "Developer Route Question",
|
|
808
|
+
description:
|
|
809
|
+
"Route one concrete judgment or one green-to-green implementation movement. Uses an adaptive default topology: model, then sketch for feature shape or signal for structural movement, implementation stable landings, and verify before completion.",
|
|
810
|
+
promptSnippet: "Choose how to handle one development question",
|
|
811
|
+
promptGuidelines: [
|
|
812
|
+
`Call ${ROUTE_TOOL} only when there is no active Developer route.`,
|
|
813
|
+
`Use ${ROUTE_TOOL} with the most focused skill supported by current evidence; target=implementation requires one movement, one stable landing, and one narrow verification.`,
|
|
814
|
+
`When ${ROUTE_TOOL} follows an implementation judgment with another implementation route, cite the previous landing in known_evidence and record plausible skill routes in alternatives_considered instead of selecting implementation by momentum.`,
|
|
815
|
+
`After a resolved model, use sketch for first feature implementation framing or signal for existing-code structural movement before implementation mutation.`,
|
|
816
|
+
],
|
|
817
|
+
parameters: RouteParams,
|
|
818
|
+
executionMode: "sequential",
|
|
819
|
+
renderShell: "self",
|
|
820
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
821
|
+
if (!state.enabled)
|
|
822
|
+
fail("Developer protocol is off. Run /develop on first.");
|
|
823
|
+
if (state.activeRoute || routeOpening) {
|
|
824
|
+
if (!state.activeRoute)
|
|
825
|
+
fail(
|
|
826
|
+
"Another Developer route is currently opening. Wait for it to finish.",
|
|
827
|
+
);
|
|
828
|
+
fail(
|
|
829
|
+
`Route ${state.activeRoute.routeId} is still active. Record its judgment before routing another question.`,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
routeOpening = true;
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
const question = params.question.trim();
|
|
836
|
+
const reason = params.reason.trim();
|
|
837
|
+
if (!question || !reason)
|
|
838
|
+
fail("Question and reason must contain non-whitespace text.");
|
|
839
|
+
|
|
840
|
+
const explicitQuestionId = params.open_question_id?.trim() || undefined;
|
|
841
|
+
const targetQuestionId = inferredQuestionId(
|
|
842
|
+
state,
|
|
843
|
+
question,
|
|
844
|
+
explicitQuestionId,
|
|
845
|
+
);
|
|
846
|
+
if (
|
|
847
|
+
targetQuestionId &&
|
|
848
|
+
!state.pendingQuestions.some((item) => item.id === targetQuestionId)
|
|
849
|
+
) {
|
|
850
|
+
fail(`Unknown pending question ID: ${targetQuestionId}`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const target = params.target;
|
|
854
|
+
const implementationBlockers = state.pendingQuestions.filter(
|
|
855
|
+
(pending) => pending.gate === "before-implementation",
|
|
856
|
+
);
|
|
857
|
+
if (target === "implementation" && implementationBlockers.length > 0) {
|
|
858
|
+
fail(
|
|
859
|
+
`Implementation work is blocked by ${implementationBlockers
|
|
860
|
+
.map(
|
|
861
|
+
(pending) =>
|
|
862
|
+
`${pending.id} (${pending.resolutionOwner}: ${pending.question})`,
|
|
863
|
+
)
|
|
864
|
+
.join("; ")}. Resolve those questions first.`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
const knownEvidence = (params.known_evidence ?? [])
|
|
868
|
+
.map((item) => item.trim())
|
|
869
|
+
.filter(Boolean);
|
|
870
|
+
const consideredAlternatives: RouteAlternative[] =
|
|
871
|
+
target === "implementation" && "alternatives_considered" in params
|
|
872
|
+
? (params.alternatives_considered ?? []).map((alternative) => ({
|
|
873
|
+
target: alternative.target.trim(),
|
|
874
|
+
reason: alternative.reason.trim(),
|
|
875
|
+
}))
|
|
876
|
+
: [];
|
|
877
|
+
for (const alternative of consideredAlternatives) {
|
|
878
|
+
if (!availableSkills.has(alternative.target)) {
|
|
879
|
+
fail(
|
|
880
|
+
`Considered alternative ${alternative.target} is unavailable or disabled.`,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (
|
|
885
|
+
new Set(
|
|
886
|
+
consideredAlternatives.map((alternative) => alternative.target),
|
|
887
|
+
).size !== consideredAlternatives.length
|
|
888
|
+
) {
|
|
889
|
+
fail(
|
|
890
|
+
"Each considered alternative must name a different available Developer skill.",
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
if (
|
|
894
|
+
target === "implementation" &&
|
|
895
|
+
state.lastJudgment?.target === "implementation"
|
|
896
|
+
) {
|
|
897
|
+
if (knownEvidence.length === 0) {
|
|
898
|
+
fail(
|
|
899
|
+
"A consecutive implementation route must cite evidence from the previous implementation landing in known_evidence.",
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
if (availableSkills.size > 0 && consideredAlternatives.length === 0) {
|
|
903
|
+
fail(
|
|
904
|
+
"A consecutive implementation route must record the plausible available skill routes in alternatives_considered and explain why they are not needed now.",
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (
|
|
909
|
+
target === "implementation" &&
|
|
910
|
+
state.implementationFramingRequired &&
|
|
911
|
+
(availableSkills.has("sketch") || availableSkills.has("signal"))
|
|
912
|
+
) {
|
|
913
|
+
fail(
|
|
914
|
+
"The latest resolved model requires implementation framing before implementation work. Route sketch for new feature shape or signal for existing-code structural movement.",
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
const skill =
|
|
918
|
+
target === "implementation" ? undefined : availableSkills.get(target);
|
|
919
|
+
if (target !== "implementation" && !skill) {
|
|
920
|
+
fail(
|
|
921
|
+
`Developer skill ${target} is unavailable or disabled in the current Pi resource configuration.`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const requestedExecutionProfile =
|
|
925
|
+
"execution_profile" in params ? params.execution_profile : undefined;
|
|
926
|
+
if (
|
|
927
|
+
target !== "implementation" &&
|
|
928
|
+
requestedExecutionProfile !== undefined
|
|
929
|
+
) {
|
|
930
|
+
fail("execution_profile is valid only when target=implementation.");
|
|
931
|
+
}
|
|
932
|
+
const executionProfile: ImplementationProfile | undefined =
|
|
933
|
+
target === "implementation"
|
|
934
|
+
? (requestedExecutionProfile ?? "ordinary")
|
|
935
|
+
: undefined;
|
|
936
|
+
|
|
937
|
+
const method =
|
|
938
|
+
target !== "implementation"
|
|
939
|
+
? await renderSkillMethod(skill!)
|
|
940
|
+
: executionProfile === "behavior-preserving-structure"
|
|
941
|
+
? [
|
|
942
|
+
'<developer-implementation-profile name="behavior-preserving-structure">',
|
|
943
|
+
(await readFile(structuralChangeMethodPath, "utf8")).trim(),
|
|
944
|
+
"</developer-implementation-profile>",
|
|
945
|
+
].join("\n")
|
|
946
|
+
: [
|
|
947
|
+
"# Implementation action",
|
|
948
|
+
"",
|
|
949
|
+
"The next local action is already justified. Keep this route open while using Pi implementation tools and collecting evidence.",
|
|
950
|
+
].join("\n");
|
|
951
|
+
|
|
952
|
+
const implementationStep =
|
|
953
|
+
target === "implementation"
|
|
954
|
+
? {
|
|
955
|
+
movement: ("movement" in params
|
|
956
|
+
? params.movement
|
|
957
|
+
: question
|
|
958
|
+
).trim(),
|
|
959
|
+
stopCondition: ("stop_condition" in params
|
|
960
|
+
? params.stop_condition
|
|
961
|
+
: "Reach a green, pausable, reviewable stable landing."
|
|
962
|
+
).trim(),
|
|
963
|
+
verification: ("verification" in params
|
|
964
|
+
? params.verification
|
|
965
|
+
: "Run the narrowest relevant check and inspect the resulting diff or output."
|
|
966
|
+
).trim(),
|
|
967
|
+
}
|
|
968
|
+
: undefined;
|
|
969
|
+
|
|
970
|
+
const event: RouteEvent = {
|
|
971
|
+
protocol: PROTOCOL,
|
|
972
|
+
kind: "route",
|
|
973
|
+
routeId: `route:${toolCallId}`,
|
|
974
|
+
question,
|
|
975
|
+
target,
|
|
976
|
+
reason,
|
|
977
|
+
knownEvidence,
|
|
978
|
+
consideredAlternatives,
|
|
979
|
+
targetQuestionId,
|
|
980
|
+
methodLocation: skill?.filePath,
|
|
981
|
+
executionProfile,
|
|
982
|
+
implementationStep,
|
|
983
|
+
};
|
|
984
|
+
const response = [
|
|
985
|
+
`Route ID: ${event.routeId}`,
|
|
986
|
+
`Question: ${event.question}`,
|
|
987
|
+
`Target: ${event.target}`,
|
|
988
|
+
skill
|
|
989
|
+
? `Skill location: ${skill.filePath}`
|
|
990
|
+
: "Skill location: implementation action; no skill file",
|
|
991
|
+
executionProfile
|
|
992
|
+
? `Execution profile: ${executionProfile}`
|
|
993
|
+
: "Execution profile: skill judgment",
|
|
994
|
+
...(implementationStep
|
|
995
|
+
? [
|
|
996
|
+
`Movement: ${implementationStep.movement}`,
|
|
997
|
+
`Stable landing: ${implementationStep.stopCondition}`,
|
|
998
|
+
`Narrow verification: ${implementationStep.verification}`,
|
|
999
|
+
"Stop this implementation route at that landing, record the evidence, and route again before another movement.",
|
|
1000
|
+
]
|
|
1001
|
+
: []),
|
|
1002
|
+
`Reason: ${event.reason}`,
|
|
1003
|
+
`Known evidence: ${event.knownEvidence.length > 0 ? event.knownEvidence.join(" | ") : "none"}`,
|
|
1004
|
+
`Alternatives reconsidered: ${
|
|
1005
|
+
event.consideredAlternatives.length > 0
|
|
1006
|
+
? event.consideredAlternatives
|
|
1007
|
+
.map(
|
|
1008
|
+
(alternative) =>
|
|
1009
|
+
`${alternative.target} — ${alternative.reason}`,
|
|
1010
|
+
)
|
|
1011
|
+
.join(" | ")
|
|
1012
|
+
: "none"
|
|
1013
|
+
}`,
|
|
1014
|
+
targetQuestionId
|
|
1015
|
+
? `Revisits pending question: ${targetQuestionId}`
|
|
1016
|
+
: "Revisits pending question: none",
|
|
1017
|
+
`When this route has done its job, call ${JUDGMENT_TOOL} with this exact route ID.`,
|
|
1018
|
+
"",
|
|
1019
|
+
"---",
|
|
1020
|
+
"",
|
|
1021
|
+
method,
|
|
1022
|
+
].join("\n");
|
|
1023
|
+
ensureSafeToolText(response, "Developer route result");
|
|
1024
|
+
if (!canApplyDeveloperEvent(state, event)) {
|
|
1025
|
+
fail(
|
|
1026
|
+
"Developer machine guard rejected the route transition from the current branch state.",
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
state = applyDeveloperEvent(state, event);
|
|
1031
|
+
syncProtocolTools();
|
|
1032
|
+
refreshUI(ctx);
|
|
1033
|
+
return textResult(response, event);
|
|
1034
|
+
} finally {
|
|
1035
|
+
routeOpening = false;
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
renderCall(args, theme, context) {
|
|
1039
|
+
const target =
|
|
1040
|
+
typeof args.target === "string" && args.target.length > 0
|
|
1041
|
+
? args.target
|
|
1042
|
+
: "…";
|
|
1043
|
+
const question =
|
|
1044
|
+
typeof args.question === "string" && args.question.length > 0
|
|
1045
|
+
? compactLine(args.question)
|
|
1046
|
+
: "…";
|
|
1047
|
+
return reusableText(
|
|
1048
|
+
`${theme.fg("toolTitle", theme.bold(ROUTE_TOOL))} ${theme.fg("accent", target)} ${theme.fg("muted", question)}`,
|
|
1049
|
+
context.lastComponent,
|
|
1050
|
+
);
|
|
1051
|
+
},
|
|
1052
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1053
|
+
if (isPartial) {
|
|
1054
|
+
return reusableText(
|
|
1055
|
+
theme.fg("warning", "routing development question…"),
|
|
1056
|
+
context.lastComponent,
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (context.isError) {
|
|
1060
|
+
return reusableText(
|
|
1061
|
+
theme.fg("error", resultText(result) || "Developer route failed"),
|
|
1062
|
+
context.lastComponent,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
const event = result.details as RouteEvent | undefined;
|
|
1066
|
+
let text = theme.fg("success", `routed ${routeRenderText(event)}`);
|
|
1067
|
+
if (expanded && event) {
|
|
1068
|
+
text += `\n${detailLine(theme, "route", event.routeId, "text")}`;
|
|
1069
|
+
text += `\n${detailLine(theme, "question", event.question, "text")}`;
|
|
1070
|
+
text += `\n${detailLine(theme, "reason", event.reason)}`;
|
|
1071
|
+
if (event.knownEvidence.length > 0) {
|
|
1072
|
+
for (const evidence of event.knownEvidence) {
|
|
1073
|
+
text += `\n${detailLine(theme, "evidence", evidence)}`;
|
|
1074
|
+
}
|
|
1075
|
+
} else {
|
|
1076
|
+
text += `\n${detailLine(theme, "evidence", "none recorded before routing", "warning")}`;
|
|
1077
|
+
}
|
|
1078
|
+
for (const alternative of event.consideredAlternatives ?? []) {
|
|
1079
|
+
text += `\n${detailLine(theme, `considered ${alternative.target}`, alternative.reason)}`;
|
|
1080
|
+
}
|
|
1081
|
+
text += `\n${detailLine(theme, "revisits", event.targetQuestionId ?? "none")}`;
|
|
1082
|
+
text += `\n${detailLine(theme, "skill", event.methodLocation ?? "implementation action")}`;
|
|
1083
|
+
if (event.executionProfile) {
|
|
1084
|
+
text += `\n${detailLine(theme, "profile", event.executionProfile)}`;
|
|
1085
|
+
}
|
|
1086
|
+
if (event.implementationStep) {
|
|
1087
|
+
text += `\n${detailLine(theme, "movement", event.implementationStep.movement, "text")}`;
|
|
1088
|
+
text += `\n${detailLine(theme, "landing", event.implementationStep.stopCondition)}`;
|
|
1089
|
+
text += `\n${detailLine(theme, "verify", event.implementationStep.verification)}`;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (!expanded && event)
|
|
1093
|
+
text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
1094
|
+
return reusableText(text, context.lastComponent);
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
const ChoiceResponseOptionParam = Type.Object(
|
|
1099
|
+
{
|
|
1100
|
+
value: Type.String({
|
|
1101
|
+
minLength: 1,
|
|
1102
|
+
maxLength: MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
1103
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
1104
|
+
}),
|
|
1105
|
+
label: Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS }),
|
|
1106
|
+
description: Type.Optional(
|
|
1107
|
+
Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS }),
|
|
1108
|
+
),
|
|
1109
|
+
detail_prompt: Type.Optional(
|
|
1110
|
+
Type.String({
|
|
1111
|
+
minLength: 1,
|
|
1112
|
+
maxLength: MAX_RESPONSE_TEXT_CHARS,
|
|
1113
|
+
description: "Required non-empty detail when this option is selected",
|
|
1114
|
+
}),
|
|
1115
|
+
),
|
|
1116
|
+
},
|
|
1117
|
+
{ additionalProperties: false },
|
|
1118
|
+
);
|
|
1119
|
+
const ChoiceResponseFieldParam = Type.Object(
|
|
1120
|
+
{
|
|
1121
|
+
id: Type.String({
|
|
1122
|
+
minLength: 1,
|
|
1123
|
+
maxLength: MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
1124
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
1125
|
+
}),
|
|
1126
|
+
prompt: Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS }),
|
|
1127
|
+
description: Type.Optional(
|
|
1128
|
+
Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS }),
|
|
1129
|
+
),
|
|
1130
|
+
options: Type.Array(ChoiceResponseOptionParam, {
|
|
1131
|
+
minItems: 2,
|
|
1132
|
+
maxItems: MAX_RESPONSE_OPTIONS,
|
|
1133
|
+
}),
|
|
1134
|
+
},
|
|
1135
|
+
{ additionalProperties: false },
|
|
1136
|
+
);
|
|
1137
|
+
const ChoiceResponseSpecParam = Type.Object(
|
|
1138
|
+
{
|
|
1139
|
+
kind: Type.Literal("choice-form"),
|
|
1140
|
+
fields: Type.Array(ChoiceResponseFieldParam, {
|
|
1141
|
+
minItems: 1,
|
|
1142
|
+
maxItems: MAX_RESPONSE_FIELDS,
|
|
1143
|
+
}),
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
additionalProperties: false,
|
|
1147
|
+
description:
|
|
1148
|
+
"Explicit controls for finite user-owned decisions; use one required single-choice field per decision instead of expecting the TUI to parse Markdown context",
|
|
1149
|
+
},
|
|
1150
|
+
);
|
|
1151
|
+
const OpenQuestionParam = Type.Object(
|
|
1152
|
+
{
|
|
1153
|
+
question: Type.String({ minLength: 1, maxLength: MAX_QUESTION_CHARS }),
|
|
1154
|
+
context: Type.Optional(
|
|
1155
|
+
Type.String({
|
|
1156
|
+
maxLength: MAX_QUESTION_CONTEXT_CHARS,
|
|
1157
|
+
description:
|
|
1158
|
+
"Explanation shown before answer controls; required for user-owned before-implementation or before-completion questions",
|
|
1159
|
+
}),
|
|
1160
|
+
),
|
|
1161
|
+
response_spec: Type.Optional(ChoiceResponseSpecParam),
|
|
1162
|
+
status: StringEnum(["open", "blocked"] as const, {
|
|
1163
|
+
description:
|
|
1164
|
+
"Whether the required evidence or answer is currently obtainable",
|
|
1165
|
+
}),
|
|
1166
|
+
resolution_owner: StringEnum(["agent", "user", "environment"] as const, {
|
|
1167
|
+
description:
|
|
1168
|
+
"Who can provide the evidence or decision that resolves this question",
|
|
1169
|
+
}),
|
|
1170
|
+
gate: StringEnum(
|
|
1171
|
+
["none", "before-implementation", "before-completion"] as const,
|
|
1172
|
+
{
|
|
1173
|
+
description:
|
|
1174
|
+
"What work this question blocks; an agent before-implementation question must be resolvable through a non-implementation evidence route",
|
|
1175
|
+
},
|
|
1176
|
+
),
|
|
1177
|
+
resolution_criteria: Type.String({
|
|
1178
|
+
minLength: 1,
|
|
1179
|
+
maxLength: MAX_EVIDENCE_CHARS,
|
|
1180
|
+
description:
|
|
1181
|
+
"Observable evidence or answer that will settle the question",
|
|
1182
|
+
}),
|
|
1183
|
+
},
|
|
1184
|
+
{ additionalProperties: false },
|
|
1185
|
+
);
|
|
1186
|
+
const QuestionUpdateParam = Type.Object(
|
|
1187
|
+
{
|
|
1188
|
+
question_id: Type.String({
|
|
1189
|
+
minLength: 1,
|
|
1190
|
+
maxLength: 512,
|
|
1191
|
+
description:
|
|
1192
|
+
"Exact ID of a question that was pending before this route; never route_id or a newly opened question ID",
|
|
1193
|
+
}),
|
|
1194
|
+
status: StringEnum([
|
|
1195
|
+
"resolved",
|
|
1196
|
+
"not-applicable",
|
|
1197
|
+
"open",
|
|
1198
|
+
"blocked",
|
|
1199
|
+
] as const),
|
|
1200
|
+
result: Type.String({ minLength: 1, maxLength: MAX_RESULT_CHARS }),
|
|
1201
|
+
basis: Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), {
|
|
1202
|
+
maxItems: 20,
|
|
1203
|
+
}),
|
|
1204
|
+
},
|
|
1205
|
+
{ additionalProperties: false },
|
|
1206
|
+
);
|
|
1207
|
+
const JudgmentParams = Type.Object({
|
|
1208
|
+
route_id: Type.String({
|
|
1209
|
+
minLength: 1,
|
|
1210
|
+
maxLength: 512,
|
|
1211
|
+
description: `Exact route ID returned by ${ROUTE_TOOL}`,
|
|
1212
|
+
}),
|
|
1213
|
+
status: StringEnum([
|
|
1214
|
+
"resolved",
|
|
1215
|
+
"needs-evidence",
|
|
1216
|
+
"not-applicable",
|
|
1217
|
+
"blocked",
|
|
1218
|
+
] as const),
|
|
1219
|
+
result: Type.String({
|
|
1220
|
+
minLength: 1,
|
|
1221
|
+
maxLength: MAX_RESULT_CHARS,
|
|
1222
|
+
description:
|
|
1223
|
+
"The resulting judgment in Markdown; preserve the routed skill's inspectable tables, diagrams, matrices, timelines, or code blocks",
|
|
1224
|
+
}),
|
|
1225
|
+
basis: Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), {
|
|
1226
|
+
maxItems: 20,
|
|
1227
|
+
description: "Evidence supporting the judgment or blocker",
|
|
1228
|
+
}),
|
|
1229
|
+
open_questions: Type.Optional(
|
|
1230
|
+
Type.Array(OpenQuestionParam, {
|
|
1231
|
+
maxItems: 10,
|
|
1232
|
+
description:
|
|
1233
|
+
"New unresolved questions with explicit resolution target, gate, and observable resolution criteria",
|
|
1234
|
+
}),
|
|
1235
|
+
),
|
|
1236
|
+
question_updates: Type.Optional(
|
|
1237
|
+
Type.Array(QuestionUpdateParam, {
|
|
1238
|
+
maxItems: 20,
|
|
1239
|
+
description:
|
|
1240
|
+
"Updates to questions already pending before this route; use [] when none exist, and never put route_id or newly opened questions here",
|
|
1241
|
+
}),
|
|
1242
|
+
),
|
|
1243
|
+
artifacts: Type.Optional(
|
|
1244
|
+
Type.Array(Type.String({ maxLength: MAX_ARTIFACT_CHARS }), {
|
|
1245
|
+
maxItems: 20,
|
|
1246
|
+
description: "Relevant paths, commands, tests, or outputs",
|
|
1247
|
+
}),
|
|
1248
|
+
),
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
pi.registerTool({
|
|
1252
|
+
name: JUDGMENT_TOOL,
|
|
1253
|
+
label: "Developer Record Judgment",
|
|
1254
|
+
description:
|
|
1255
|
+
"Close the active Developer route with its result, evidence, newly opened questions, and relevant artifacts. This records a local judgment, not task completion.",
|
|
1256
|
+
promptSnippet: "Record evidence and close the active development route",
|
|
1257
|
+
promptGuidelines: [
|
|
1258
|
+
`Use ${JUDGMENT_TOOL} with the exact active Developer route ID.`,
|
|
1259
|
+
`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.`,
|
|
1260
|
+
`Use ${JUDGMENT_TOOL} open_questions with resolution_owner, gate, and resolution_criteria. User-owned gated questions require explanatory context before controls; for finite required 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.`,
|
|
1261
|
+
`Do not use ${JUDGMENT_TOOL} with resolved, not-applicable, or blocked status without at least one concrete basis.`,
|
|
1262
|
+
],
|
|
1263
|
+
parameters: JudgmentParams,
|
|
1264
|
+
executionMode: "sequential",
|
|
1265
|
+
renderShell: "self",
|
|
1266
|
+
prepareArguments(args): Static<typeof JudgmentParams> {
|
|
1267
|
+
const input = args as Static<typeof JudgmentParams> & {
|
|
1268
|
+
status?: string;
|
|
1269
|
+
open_questions?: unknown[];
|
|
1270
|
+
};
|
|
1271
|
+
if (
|
|
1272
|
+
!input ||
|
|
1273
|
+
typeof input !== "object" ||
|
|
1274
|
+
!Array.isArray(input.open_questions)
|
|
1275
|
+
) {
|
|
1276
|
+
return args as Static<typeof JudgmentParams>;
|
|
1277
|
+
}
|
|
1278
|
+
return {
|
|
1279
|
+
...input,
|
|
1280
|
+
open_questions: input.open_questions.map((question) =>
|
|
1281
|
+
typeof question === "string"
|
|
1282
|
+
? legacyOpenQuestion(question, input.status)
|
|
1283
|
+
: question,
|
|
1284
|
+
) as Static<typeof JudgmentParams>["open_questions"],
|
|
1285
|
+
};
|
|
1286
|
+
},
|
|
1287
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1288
|
+
if (!state.enabled) fail("Developer protocol is off.");
|
|
1289
|
+
const activeRoute = state.activeRoute;
|
|
1290
|
+
if (!activeRoute) fail("There is no active Developer route to close.");
|
|
1291
|
+
if (params.route_id !== activeRoute.routeId) {
|
|
1292
|
+
fail(`Route ID mismatch. Active route is ${activeRoute.routeId}.`);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const basis = params.basis.map((item) => item.trim()).filter(Boolean);
|
|
1296
|
+
const result = params.result.trim();
|
|
1297
|
+
if (!result) fail("A judgment result must contain non-whitespace text.");
|
|
1298
|
+
if (params.status !== "needs-evidence" && basis.length === 0) {
|
|
1299
|
+
fail(`${params.status} judgments require at least one concrete basis.`);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const openedQuestions = buildOpenedQuestions(
|
|
1303
|
+
params.open_questions ?? [],
|
|
1304
|
+
state,
|
|
1305
|
+
params.route_id,
|
|
1306
|
+
);
|
|
1307
|
+
if (
|
|
1308
|
+
availableSkills.size === 0 &&
|
|
1309
|
+
openedQuestions.some(
|
|
1310
|
+
(question) =>
|
|
1311
|
+
question.resolutionOwner === "agent" &&
|
|
1312
|
+
question.gate === "before-implementation",
|
|
1313
|
+
)
|
|
1314
|
+
) {
|
|
1315
|
+
fail(
|
|
1316
|
+
"An agent-owned before-implementation question requires an available non-implementation skill resolution path.",
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
const remainsOpen =
|
|
1320
|
+
params.status === "needs-evidence" || params.status === "blocked";
|
|
1321
|
+
if (
|
|
1322
|
+
remainsOpen &&
|
|
1323
|
+
!activeRoute.targetQuestionId &&
|
|
1324
|
+
openedQuestions.length === 0
|
|
1325
|
+
) {
|
|
1326
|
+
fail(
|
|
1327
|
+
"A needs-evidence or blocked judgment for a new question must include at least one structured open_questions entry.",
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (
|
|
1332
|
+
state.pendingQuestions.length > 0 &&
|
|
1333
|
+
params.question_updates === undefined
|
|
1334
|
+
) {
|
|
1335
|
+
fail(
|
|
1336
|
+
"Pending questions exist. Include question_updates (an empty array is valid) after rechecking them against this route's evidence.",
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
const questionUpdates = buildQuestionUpdates(
|
|
1340
|
+
params.question_updates ?? [],
|
|
1341
|
+
state,
|
|
1342
|
+
);
|
|
1343
|
+
const updatedQuestionIds = new Set(
|
|
1344
|
+
questionUpdates.map((update) => update.questionId),
|
|
1345
|
+
);
|
|
1346
|
+
if (
|
|
1347
|
+
openedQuestions.some((question) => updatedQuestionIds.has(question.id))
|
|
1348
|
+
) {
|
|
1349
|
+
fail("A question cannot be opened and updated in the same judgment.");
|
|
1350
|
+
}
|
|
1351
|
+
const reopensCurrentQuestion = openedQuestions.some(
|
|
1352
|
+
(question) =>
|
|
1353
|
+
normalizedQuestion(question.question) ===
|
|
1354
|
+
normalizedQuestion(activeRoute.question),
|
|
1355
|
+
);
|
|
1356
|
+
if (!remainsOpen && reopensCurrentQuestion) {
|
|
1357
|
+
fail(
|
|
1358
|
+
"A resolved or not-applicable judgment cannot reopen its own question.",
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
const targetQuestionId = activeRoute.targetQuestionId;
|
|
1362
|
+
const targetUpdate = targetQuestionId
|
|
1363
|
+
? questionUpdates.find(
|
|
1364
|
+
(update) => update.questionId === targetQuestionId,
|
|
1365
|
+
)
|
|
1366
|
+
: undefined;
|
|
1367
|
+
if (targetQuestionId && !targetUpdate) {
|
|
1368
|
+
fail(
|
|
1369
|
+
`Focused question ${targetQuestionId} requires an explicit question_updates entry.`,
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
if (targetUpdate) {
|
|
1373
|
+
const closesTarget =
|
|
1374
|
+
targetUpdate.status === "resolved" ||
|
|
1375
|
+
targetUpdate.status === "not-applicable";
|
|
1376
|
+
if (!remainsOpen && !closesTarget) {
|
|
1377
|
+
fail(
|
|
1378
|
+
"A resolved focused judgment must explicitly resolve or dismiss its question.",
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
if (remainsOpen && openedQuestions.length === 0 && closesTarget) {
|
|
1382
|
+
fail(
|
|
1383
|
+
"A focused question cannot close while its judgment reports no replacement evidence question.",
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (remainsOpen && openedQuestions.length > 0 && !closesTarget) {
|
|
1387
|
+
fail(
|
|
1388
|
+
"Replacement questions require explicitly resolving or dismissing the broader focused question.",
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const event: JudgmentEvent = {
|
|
1394
|
+
protocol: PROTOCOL,
|
|
1395
|
+
kind: "judgment",
|
|
1396
|
+
routeId: params.route_id,
|
|
1397
|
+
question: activeRoute.question,
|
|
1398
|
+
target: activeRoute.target,
|
|
1399
|
+
status: params.status,
|
|
1400
|
+
result,
|
|
1401
|
+
basis,
|
|
1402
|
+
openedQuestions,
|
|
1403
|
+
questionUpdates,
|
|
1404
|
+
artifacts: (params.artifacts ?? [])
|
|
1405
|
+
.map((item) => item.trim())
|
|
1406
|
+
.filter(Boolean),
|
|
1407
|
+
changedArtifacts: routesWithMutation.has(params.route_id),
|
|
1408
|
+
};
|
|
1409
|
+
if (!canApplyDeveloperEvent(state, event)) {
|
|
1410
|
+
fail(
|
|
1411
|
+
"Developer machine guard rejected the judgment transition from the current active route.",
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
const nextState = applyDeveloperEvent(state, event);
|
|
1415
|
+
if (nextState.pendingQuestions.length > MAX_PENDING_QUESTIONS) {
|
|
1416
|
+
fail(
|
|
1417
|
+
`Developer protocol would retain ${nextState.pendingQuestions.length} pending questions; resolve or consolidate them before opening more.`,
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const immediateQuestion =
|
|
1422
|
+
ctx.mode === "tui"
|
|
1423
|
+
? firstImmediateUserQuestion(
|
|
1424
|
+
state.pendingQuestions,
|
|
1425
|
+
event.openedQuestions,
|
|
1426
|
+
)
|
|
1427
|
+
: undefined;
|
|
1428
|
+
let immediateDisposition: ImmediateQuestionDisposition | undefined;
|
|
1429
|
+
if (immediateQuestion) {
|
|
1430
|
+
try {
|
|
1431
|
+
immediateDisposition = await promptImmediateUserQuestion(
|
|
1432
|
+
ctx,
|
|
1433
|
+
immediateQuestion,
|
|
1434
|
+
);
|
|
1435
|
+
} catch {
|
|
1436
|
+
ctx.ui.notify(
|
|
1437
|
+
"Could not open the Developer decision prompt; the question remains open.",
|
|
1438
|
+
"warning",
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const nextMessage = judgmentNextMessage(event, nextState);
|
|
1444
|
+
const resolvedQuestionCount = event.questionUpdates.filter(
|
|
1445
|
+
(update) =>
|
|
1446
|
+
update.status === "resolved" || update.status === "not-applicable",
|
|
1447
|
+
).length;
|
|
1448
|
+
const response =
|
|
1449
|
+
`Recorded ${event.status} judgment for ${event.routeId}: ${event.result}\n` +
|
|
1450
|
+
`Question updates: ${resolvedQuestionCount} resolved, ${event.questionUpdates.length - resolvedQuestionCount} retained or blocked.\n` +
|
|
1451
|
+
nextMessage +
|
|
1452
|
+
immediateQuestionMessage(immediateQuestion, immediateDisposition);
|
|
1453
|
+
ensureSafeToolText(response, "Developer judgment result");
|
|
1454
|
+
|
|
1455
|
+
routesWithMutation.delete(params.route_id);
|
|
1456
|
+
state = nextState;
|
|
1457
|
+
syncProtocolTools();
|
|
1458
|
+
refreshUI(ctx);
|
|
1459
|
+
return textResult(response, event);
|
|
1460
|
+
},
|
|
1461
|
+
renderCall(args, theme, context) {
|
|
1462
|
+
const status =
|
|
1463
|
+
typeof args.status === "string" && args.status.length > 0
|
|
1464
|
+
? args.status
|
|
1465
|
+
: "…";
|
|
1466
|
+
const result =
|
|
1467
|
+
typeof args.result === "string" && args.result.length > 0
|
|
1468
|
+
? compactJudgmentResult(args.result)
|
|
1469
|
+
: "…";
|
|
1470
|
+
const statusText =
|
|
1471
|
+
status === "resolved"
|
|
1472
|
+
? theme.fg("success", status)
|
|
1473
|
+
: status === "needs-evidence"
|
|
1474
|
+
? theme.fg("warning", status)
|
|
1475
|
+
: status === "blocked"
|
|
1476
|
+
? theme.fg("error", status)
|
|
1477
|
+
: theme.fg("muted", status);
|
|
1478
|
+
return reusableText(
|
|
1479
|
+
`${theme.fg("toolTitle", theme.bold(JUDGMENT_TOOL))} ${statusText} ${theme.fg("muted", result)}`,
|
|
1480
|
+
context.lastComponent,
|
|
1481
|
+
);
|
|
1482
|
+
},
|
|
1483
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1484
|
+
if (isPartial) {
|
|
1485
|
+
return reusableText(
|
|
1486
|
+
theme.fg("warning", "recording development judgment…"),
|
|
1487
|
+
context.lastComponent,
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
if (context.isError) {
|
|
1491
|
+
return reusableText(
|
|
1492
|
+
theme.fg("error", resultText(result) || "Developer judgment failed"),
|
|
1493
|
+
context.lastComponent,
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
const event = result.details as JudgmentEvent | undefined;
|
|
1497
|
+
const summary = judgmentRenderText(event);
|
|
1498
|
+
let text =
|
|
1499
|
+
event?.status === "resolved"
|
|
1500
|
+
? theme.fg("success", summary)
|
|
1501
|
+
: event?.status === "needs-evidence"
|
|
1502
|
+
? theme.fg("warning", summary)
|
|
1503
|
+
: event?.status === "blocked"
|
|
1504
|
+
? theme.fg("error", summary)
|
|
1505
|
+
: theme.fg("muted", summary);
|
|
1506
|
+
if (expanded && event) return expandedJudgment(event, text, theme);
|
|
1507
|
+
if (event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
1508
|
+
return reusableText(text, context.lastComponent);
|
|
1509
|
+
},
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
const refreshAvailableSkills = (ctx: ExtensionCommandContext) => {
|
|
1513
|
+
if (typeof ctx.getSystemPromptOptions !== "function") return;
|
|
1514
|
+
availableSkills = availablePackageSkills(
|
|
1515
|
+
ctx.getSystemPromptOptions().skills ?? [],
|
|
1516
|
+
skillsRoot,
|
|
1517
|
+
);
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
const statusMessage = () => {
|
|
1521
|
+
const active = state.activeRoute
|
|
1522
|
+
? `${state.activeRoute.routeId} · ${state.activeRoute.target} · ${state.activeRoute.question}`
|
|
1523
|
+
: "none";
|
|
1524
|
+
const pending =
|
|
1525
|
+
state.pendingQuestions.length > 0
|
|
1526
|
+
? state.pendingQuestions
|
|
1527
|
+
.map(
|
|
1528
|
+
(question) =>
|
|
1529
|
+
`${question.id} · ${question.status} · ${question.resolutionOwner}/${question.gate} · ${question.question} · resolves when: ${question.resolutionCriteria}`,
|
|
1530
|
+
)
|
|
1531
|
+
.join(" | ")
|
|
1532
|
+
: "none";
|
|
1533
|
+
const last = state.lastJudgment
|
|
1534
|
+
? `${state.lastJudgment.status} · ${state.lastJudgment.result}`
|
|
1535
|
+
: "none";
|
|
1536
|
+
const basis =
|
|
1537
|
+
state.lastJudgment && state.lastJudgment.basis.length > 0
|
|
1538
|
+
? state.lastJudgment.basis.join(" | ")
|
|
1539
|
+
: "none";
|
|
1540
|
+
const artifacts =
|
|
1541
|
+
state.lastJudgment && state.lastJudgment.artifacts.length > 0
|
|
1542
|
+
? state.lastJudgment.artifacts.join(" | ")
|
|
1543
|
+
: "none";
|
|
1544
|
+
const history =
|
|
1545
|
+
state.judgmentHistory.length > 0
|
|
1546
|
+
? state.judgmentHistory
|
|
1547
|
+
.slice(-10)
|
|
1548
|
+
.map((judgment) => {
|
|
1549
|
+
const route = state.routeHistory.find(
|
|
1550
|
+
(candidate) => candidate.routeId === judgment.routeId,
|
|
1551
|
+
);
|
|
1552
|
+
const alternatives = (route?.consideredAlternatives ?? [])
|
|
1553
|
+
.map(
|
|
1554
|
+
(alternative) =>
|
|
1555
|
+
`${alternative.target}: ${alternative.reason}`,
|
|
1556
|
+
)
|
|
1557
|
+
.join("; ");
|
|
1558
|
+
return `${judgment.target} · ${judgment.status} · ${judgment.result}${
|
|
1559
|
+
alternatives ? ` · considered ${alternatives}` : ""
|
|
1560
|
+
}`;
|
|
1561
|
+
})
|
|
1562
|
+
.join("\n")
|
|
1563
|
+
: "none";
|
|
1564
|
+
return (
|
|
1565
|
+
`${summarizeState(state)}` +
|
|
1566
|
+
`\nactive: ${active}` +
|
|
1567
|
+
`\nlast: ${last}` +
|
|
1568
|
+
`\nbasis: ${basis}` +
|
|
1569
|
+
`\nartifacts: ${artifacts}` +
|
|
1570
|
+
`\nimplementation framing: ${state.implementationFramingRequired ? "required" : "clear"}` +
|
|
1571
|
+
`\ncheckpoint: ${state.rerouteRequired ? "reroute required" : "ready"}` +
|
|
1572
|
+
`\nverification: ${state.verificationRequired ? "required" : "current"}` +
|
|
1573
|
+
`\nhistory:\n${history}` +
|
|
1574
|
+
`\nactive tools: ${pi.getActiveTools().join(", ")}` +
|
|
1575
|
+
`\navailable skills: ${[...availableSkills.keys()].join(", ") || "none"}` +
|
|
1576
|
+
`\npending: ${pending}` +
|
|
1577
|
+
"\nprotocol state is not a product-completion claim"
|
|
1578
|
+
);
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
pi.registerCommand("develop", {
|
|
1582
|
+
description:
|
|
1583
|
+
"Control or inspect Developer: /develop on | status | questions | off",
|
|
1584
|
+
getArgumentCompletions(prefix) {
|
|
1585
|
+
const normalized = prefix.trim();
|
|
1586
|
+
const matches = DEVELOPER_COMMAND_ACTIONS.filter((action) =>
|
|
1587
|
+
action.startsWith(normalized),
|
|
1588
|
+
);
|
|
1589
|
+
return matches.length > 0
|
|
1590
|
+
? matches.map((action) => ({ value: action, label: action }))
|
|
1591
|
+
: null;
|
|
1592
|
+
},
|
|
1593
|
+
handler: async (args, ctx) => {
|
|
1594
|
+
const submitQuestionResolution = async (
|
|
1595
|
+
question: PendingQuestion,
|
|
1596
|
+
): Promise<boolean> => {
|
|
1597
|
+
const request = await editQuestionResolutionRequest(ctx, question);
|
|
1598
|
+
if (request === undefined) return false;
|
|
1599
|
+
const focusEvent: FocusEvent = {
|
|
1600
|
+
protocol: PROTOCOL,
|
|
1601
|
+
kind: "focus",
|
|
1602
|
+
questionId: question.id,
|
|
1603
|
+
};
|
|
1604
|
+
pi.appendEntry(FOCUS_ENTRY, focusEvent);
|
|
1605
|
+
state = applyDeveloperEvent(state, focusEvent);
|
|
1606
|
+
refreshUI(ctx);
|
|
1607
|
+
ctx.ui.setEditorText("");
|
|
1608
|
+
ctx.ui.notify(
|
|
1609
|
+
"Question response submitted. It remains open until Developer records a resolved or not-applicable judgment.",
|
|
1610
|
+
"info",
|
|
1611
|
+
);
|
|
1612
|
+
if (ctx.isIdle()) pi.sendUserMessage(request);
|
|
1613
|
+
else pi.sendUserMessage(request, { deliverAs: "followUp" });
|
|
1614
|
+
return true;
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
const setAndNotifyEnabled = (enabled: boolean): boolean => {
|
|
1618
|
+
if (!setEnabled(enabled, ctx)) return false;
|
|
1619
|
+
ctx.ui.notify(`Developer: ${enabled ? "on" : "off"}`, "info");
|
|
1620
|
+
return true;
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
const turnOff = async (): Promise<boolean> => {
|
|
1624
|
+
if (
|
|
1625
|
+
ctx.mode === "tui" &&
|
|
1626
|
+
(state.activeRoute || state.pendingQuestions.length > 0)
|
|
1627
|
+
) {
|
|
1628
|
+
const work = [
|
|
1629
|
+
...(state.activeRoute ? ["the active route"] : []),
|
|
1630
|
+
...(state.pendingQuestions.length > 0
|
|
1631
|
+
? [`${state.pendingQuestions.length} open question(s)`]
|
|
1632
|
+
: []),
|
|
1633
|
+
].join(" and ");
|
|
1634
|
+
const confirmed = await ctx.ui.confirm(
|
|
1635
|
+
"Turn off Developer?",
|
|
1636
|
+
`This clears ${work} from the current protocol state. Existing session history remains.`,
|
|
1637
|
+
);
|
|
1638
|
+
if (!confirmed) return false;
|
|
1639
|
+
}
|
|
1640
|
+
setAndNotifyEnabled(false);
|
|
1641
|
+
return true;
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
const inspectStatus = async () => {
|
|
1645
|
+
if (toolPolicyRestartRequired) {
|
|
1646
|
+
ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
|
|
1647
|
+
refreshUI(ctx);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
refreshAvailableSkills(ctx);
|
|
1651
|
+
if (ctx.mode === "tui") {
|
|
1652
|
+
await showDeveloperStatus(ctx, {
|
|
1653
|
+
state,
|
|
1654
|
+
activeTools: pi.getActiveTools(),
|
|
1655
|
+
availableSkills: [...availableSkills.keys()],
|
|
1656
|
+
});
|
|
1657
|
+
} else {
|
|
1658
|
+
ctx.ui.notify(statusMessage(), "info");
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
const inspectHistory = async (): Promise<void> => {
|
|
1663
|
+
if (state.judgmentHistory.length === 0) {
|
|
1664
|
+
ctx.ui.notify(
|
|
1665
|
+
"Developer has no judgment history on this branch.",
|
|
1666
|
+
"info",
|
|
1667
|
+
);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
let selectedRouteId: string | undefined;
|
|
1671
|
+
while (true) {
|
|
1672
|
+
const routeId = await showDeveloperHistorySelector(
|
|
1673
|
+
ctx,
|
|
1674
|
+
state,
|
|
1675
|
+
selectedRouteId,
|
|
1676
|
+
);
|
|
1677
|
+
if (!routeId) return;
|
|
1678
|
+
selectedRouteId = routeId;
|
|
1679
|
+
const entry = developerHistoryEntries(state).find(
|
|
1680
|
+
(candidate) => candidate.id === routeId,
|
|
1681
|
+
);
|
|
1682
|
+
if (!entry) continue;
|
|
1683
|
+
await showDeveloperHistoryDetail(ctx, entry);
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
const answerPendingQuestion = async (): Promise<boolean> => {
|
|
1688
|
+
if (state.pendingQuestions.length === 0) {
|
|
1689
|
+
ctx.ui.notify(
|
|
1690
|
+
"Developer has no open questions on the current branch.",
|
|
1691
|
+
"info",
|
|
1692
|
+
);
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
if (ctx.mode === "tui") {
|
|
1696
|
+
const soleQuestion =
|
|
1697
|
+
state.pendingQuestions.length === 1
|
|
1698
|
+
? state.pendingQuestions[0]
|
|
1699
|
+
: undefined;
|
|
1700
|
+
if (soleQuestion) return submitQuestionResolution(soleQuestion);
|
|
1701
|
+
while (true) {
|
|
1702
|
+
const questionId = await showPendingQuestionSelector(
|
|
1703
|
+
ctx,
|
|
1704
|
+
state.pendingQuestions,
|
|
1705
|
+
);
|
|
1706
|
+
if (!questionId) return false;
|
|
1707
|
+
const question = state.pendingQuestions.find(
|
|
1708
|
+
(item) => item.id === questionId,
|
|
1709
|
+
);
|
|
1710
|
+
if (!question) return false;
|
|
1711
|
+
if (await submitQuestionResolution(question)) return true;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
ctx.ui.notify(
|
|
1715
|
+
state.pendingQuestions
|
|
1716
|
+
.map(
|
|
1717
|
+
(question) =>
|
|
1718
|
+
`${question.id} · ${question.status} · ${question.resolutionOwner}/${question.gate} · ${question.question}\n resolves when: ${question.resolutionCriteria}`,
|
|
1719
|
+
)
|
|
1720
|
+
.join("\n"),
|
|
1721
|
+
"info",
|
|
1722
|
+
);
|
|
1723
|
+
return false;
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
const action = args.trim();
|
|
1727
|
+
if (!action && ctx.mode === "tui") {
|
|
1728
|
+
refreshAvailableSkills(ctx);
|
|
1729
|
+
while (true) {
|
|
1730
|
+
const selection = await showDeveloperSettings(ctx, {
|
|
1731
|
+
read: () => state,
|
|
1732
|
+
commitActivation(enabled) {
|
|
1733
|
+
setAndNotifyEnabled(enabled);
|
|
1734
|
+
return state;
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
if (!selection) return;
|
|
1738
|
+
if (selection.kind === "questions") {
|
|
1739
|
+
if (await answerPendingQuestion()) return;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (selection.kind === "history") {
|
|
1743
|
+
await inspectHistory();
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
await inspectStatus();
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
if (!action || action === "status") {
|
|
1751
|
+
await inspectStatus();
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
if (action === "on") {
|
|
1755
|
+
setAndNotifyEnabled(true);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (action === "off") {
|
|
1759
|
+
await turnOff();
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
if (action === "questions") {
|
|
1763
|
+
await answerPendingQuestion();
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
ctx.ui.notify("Usage: /develop on | status | questions | off", "warning");
|
|
1767
|
+
},
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
const entryRendererAPI = pi as ExtensionAPI & {
|
|
1771
|
+
registerEntryRenderer?: ExtensionAPI["registerEntryRenderer"];
|
|
1772
|
+
};
|
|
1773
|
+
entryRendererAPI.registerEntryRenderer?.(
|
|
1774
|
+
ACTIVATION_ENTRY,
|
|
1775
|
+
(entry, _options, theme) => {
|
|
1776
|
+
const event = normalizeDeveloperEvent(entry.data);
|
|
1777
|
+
if (!event || event.kind !== "activation") return undefined;
|
|
1778
|
+
return new Text(
|
|
1779
|
+
`${theme.fg("toolTitle", theme.bold("Developer"))} ${theme.fg("accent", event.enabled ? "on" : "off")}`,
|
|
1780
|
+
0,
|
|
1781
|
+
0,
|
|
1782
|
+
);
|
|
1783
|
+
},
|
|
1784
|
+
);
|
|
1785
|
+
|
|
1786
|
+
pi.on("before_agent_start", (event) => {
|
|
1787
|
+
availableSkills = availablePackageSkills(
|
|
1788
|
+
event.systemPromptOptions.skills ?? [],
|
|
1789
|
+
skillsRoot,
|
|
1790
|
+
);
|
|
1791
|
+
if (!state.enabled) return;
|
|
1792
|
+
return {
|
|
1793
|
+
systemPrompt:
|
|
1794
|
+
event.systemPrompt + protocolPrompt(state, [...availableSkills.keys()]),
|
|
1795
|
+
};
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
pi.on("tool_call", (event) => {
|
|
1799
|
+
const capability = builtinControlledToolCapabilities(pi.getAllTools()).get(
|
|
1800
|
+
event.toolName,
|
|
1801
|
+
);
|
|
1802
|
+
if (!capability) return;
|
|
1803
|
+
|
|
1804
|
+
const access = protocolToolAccess(state);
|
|
1805
|
+
if (
|
|
1806
|
+
isControlledToolAllowed({ enabled: state.enabled, capability, access })
|
|
1807
|
+
) {
|
|
1808
|
+
if (access.allowsArtifactTools && state.activeRoute)
|
|
1809
|
+
routesWithMutation.add(state.activeRoute.routeId);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
const implementationBlockers = state.pendingQuestions.filter(
|
|
1814
|
+
(question) => question.gate === "before-implementation",
|
|
1815
|
+
);
|
|
1816
|
+
if (implementationBlockers.length > 0) {
|
|
1817
|
+
const blockedAction =
|
|
1818
|
+
capability === "shell"
|
|
1819
|
+
? "unrouted shell execution"
|
|
1820
|
+
: "artifact mutation";
|
|
1821
|
+
return {
|
|
1822
|
+
block: true,
|
|
1823
|
+
reason: `Developer question gate blocks ${blockedAction} until resolved: ${implementationBlockers
|
|
1824
|
+
.map(
|
|
1825
|
+
(question) =>
|
|
1826
|
+
`${question.id} (${question.resolutionOwner}: ${question.question})`,
|
|
1827
|
+
)
|
|
1828
|
+
.join(
|
|
1829
|
+
"; ",
|
|
1830
|
+
)}. A non-implementation judgment route may still use bash to obtain evidence.`,
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
return {
|
|
1835
|
+
block: true,
|
|
1836
|
+
reason:
|
|
1837
|
+
capability === "shell"
|
|
1838
|
+
? "Developer requires an active judgment or implementation route before Pi built-in bash can execute evidence checks."
|
|
1839
|
+
: `Developer requires an active ${ROUTE_TOOL} targeting implementation (target=implementation) before Pi built-in edit or write.`,
|
|
1840
|
+
};
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
pi.on("session_start", (event, ctx) => {
|
|
1844
|
+
const branch = ctx.sessionManager.getBranch();
|
|
1845
|
+
toolPolicyRestartRequired =
|
|
1846
|
+
event.reason === "reload" &&
|
|
1847
|
+
toolPolicyReloadRequiresRestart({
|
|
1848
|
+
entries: branch,
|
|
1849
|
+
protocol: PROTOCOL,
|
|
1850
|
+
protocolTools: PROTOCOL_TOOLS,
|
|
1851
|
+
});
|
|
1852
|
+
reconstruct(ctx);
|
|
1853
|
+
if (toolPolicyRestartRequired) {
|
|
1854
|
+
ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
const startEnabled = pi.getFlag("develop");
|
|
1858
|
+
if (startEnabled === true && !state.enabled) setEnabled(true, ctx);
|
|
1859
|
+
});
|
|
1860
|
+
pi.on("session_tree", (_event, ctx) => reconstruct(ctx));
|
|
1861
|
+
pi.on("agent_settled", (_event, ctx) => refreshUI(ctx));
|
|
1862
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
1863
|
+
releaseProtocolTools();
|
|
1864
|
+
if (!toolPolicyRestartRequired) {
|
|
1865
|
+
pi.appendEntry(
|
|
1866
|
+
TOOL_POLICY_LIFECYCLE_ENTRY,
|
|
1867
|
+
reloadSafeToolPolicyMarker(PROTOCOL),
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
ctx.ui.setStatus("developer", undefined);
|
|
1871
|
+
ctx.ui.setWidget("developer", undefined);
|
|
1872
|
+
});
|
|
1562
1873
|
}
|