@hobin/developer 0.1.4 → 0.1.5
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 +149 -38
- package/extensions/developer.ts +483 -68
- package/extensions/machine.ts +344 -0
- package/extensions/state.ts +128 -98
- package/extensions/tool-policy.ts +54 -25
- package/extensions/tui.ts +263 -124
- package/package.json +7 -2
- package/skills/abstraction-review/SKILL.md +6 -5
- package/skills/adversarial-eval/SKILL.md +7 -3
- package/skills/model/SKILL.md +13 -4
- package/skills/naming-judgment/SKILL.md +9 -3
- package/skills/schedule/SKILL.md +6 -3
- package/skills/signal/SKILL.md +6 -3
- package/skills/sketch/SKILL.md +29 -10
- package/skills/specify/SKILL.md +11 -2
- package/skills/verify/SKILL.md +10 -4
- package/skills/visualize/SKILL.md +5 -3
package/extensions/developer.ts
CHANGED
|
@@ -6,14 +6,17 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_MAX_BYTES,
|
|
8
8
|
DEFAULT_MAX_LINES,
|
|
9
|
+
getMarkdownTheme,
|
|
9
10
|
keyHint,
|
|
10
11
|
type ExtensionAPI,
|
|
11
12
|
type ExtensionCommandContext,
|
|
12
13
|
type ExtensionContext,
|
|
13
14
|
type Skill,
|
|
15
|
+
type Theme,
|
|
16
|
+
type ThemeColor,
|
|
14
17
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
16
|
-
import { Type } from "typebox";
|
|
18
|
+
import { Container, Markdown, Text } from "@earendil-works/pi-tui";
|
|
19
|
+
import { Type, type Static } from "typebox";
|
|
17
20
|
|
|
18
21
|
import { availablePackageSkills, renderSkillMethod } from "./skills.ts";
|
|
19
22
|
import {
|
|
@@ -23,6 +26,8 @@ import {
|
|
|
23
26
|
PROTOCOL,
|
|
24
27
|
ROUTE_TOOL,
|
|
25
28
|
applyDeveloperEvent,
|
|
29
|
+
canApplyDeveloperEvent,
|
|
30
|
+
developerSnapshot,
|
|
26
31
|
initialState,
|
|
27
32
|
normalizeDeveloperEvent,
|
|
28
33
|
protocolState,
|
|
@@ -33,16 +38,25 @@ import {
|
|
|
33
38
|
type FocusEvent,
|
|
34
39
|
type JudgmentEvent,
|
|
35
40
|
type ModeEvent,
|
|
41
|
+
type PendingQuestion,
|
|
42
|
+
type PendingQuestionStatus,
|
|
43
|
+
type QuestionGate,
|
|
44
|
+
type QuestionResolutionOwner,
|
|
45
|
+
type QuestionUpdate,
|
|
46
|
+
type QuestionUpdateStatus,
|
|
47
|
+
type RouteAlternative,
|
|
36
48
|
type RouteEvent,
|
|
37
49
|
} from "./state.ts";
|
|
38
50
|
import {
|
|
39
|
-
|
|
51
|
+
builtinControlledToolCapabilities,
|
|
52
|
+
isControlledToolAllowed,
|
|
40
53
|
reconcileProtocolTools,
|
|
54
|
+
type ProtocolToolAccess,
|
|
41
55
|
type ToolPolicyMemory,
|
|
42
56
|
} from "./tool-policy.ts";
|
|
43
57
|
import {
|
|
44
58
|
DeveloperWidget,
|
|
45
|
-
|
|
59
|
+
editQuestionResolutionRequest,
|
|
46
60
|
renderDeveloperFooter,
|
|
47
61
|
showDeveloperActionSelector,
|
|
48
62
|
showDeveloperStatus,
|
|
@@ -60,7 +74,7 @@ const structuralChangeMethodPath = resolve(
|
|
|
60
74
|
const MAX_PENDING_QUESTIONS = 20;
|
|
61
75
|
const MAX_QUESTION_CHARS = 2_000;
|
|
62
76
|
const MAX_EVIDENCE_CHARS = 2_000;
|
|
63
|
-
const MAX_RESULT_CHARS =
|
|
77
|
+
const MAX_RESULT_CHARS = 12_000;
|
|
64
78
|
const MAX_ARTIFACT_CHARS = 4_096;
|
|
65
79
|
const DEVELOPER_COMMAND_ACTIONS = ["on", "strict", "status", "questions", "off"] as const;
|
|
66
80
|
|
|
@@ -113,6 +127,112 @@ function normalizedQuestion(value: string): string {
|
|
|
113
127
|
return value.toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
114
128
|
}
|
|
115
129
|
|
|
130
|
+
interface OpenQuestionInput {
|
|
131
|
+
question: string;
|
|
132
|
+
status: PendingQuestionStatus;
|
|
133
|
+
resolution_owner: Exclude<QuestionResolutionOwner, "unknown">;
|
|
134
|
+
gate: QuestionGate;
|
|
135
|
+
resolution_criteria: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface QuestionUpdateInput {
|
|
139
|
+
question_id: string;
|
|
140
|
+
status: QuestionUpdateStatus;
|
|
141
|
+
result: string;
|
|
142
|
+
basis: string[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function legacyOpenQuestion(question: string, judgmentStatus?: string): OpenQuestionInput {
|
|
146
|
+
if (judgmentStatus === "blocked") {
|
|
147
|
+
return {
|
|
148
|
+
question,
|
|
149
|
+
status: "blocked",
|
|
150
|
+
resolution_owner: "environment",
|
|
151
|
+
gate: "before-completion",
|
|
152
|
+
resolution_criteria: `Obtain evidence that settles: ${question}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
question,
|
|
157
|
+
status: "open",
|
|
158
|
+
resolution_owner: "agent",
|
|
159
|
+
gate: "none",
|
|
160
|
+
resolution_criteria: `Obtain evidence that settles: ${question}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildOpenedQuestions(
|
|
165
|
+
input: OpenQuestionInput[],
|
|
166
|
+
state: DeveloperState,
|
|
167
|
+
routeId: string,
|
|
168
|
+
): PendingQuestion[] {
|
|
169
|
+
const questionIds = new Map(
|
|
170
|
+
state.pendingQuestions.map((question) => [normalizedQuestion(question.question), question.id]),
|
|
171
|
+
);
|
|
172
|
+
const openedQuestions: PendingQuestion[] = [];
|
|
173
|
+
for (const [index, item] of input.entries()) {
|
|
174
|
+
const question = item.question.trim();
|
|
175
|
+
const resolutionCriteria = item.resolution_criteria.trim();
|
|
176
|
+
if (!question || !resolutionCriteria) {
|
|
177
|
+
fail("Each open question requires non-whitespace question and resolution_criteria text.");
|
|
178
|
+
}
|
|
179
|
+
const questionKey = normalizedQuestion(question);
|
|
180
|
+
const questionId = questionIds.get(questionKey) ?? `question:${routeId}:open:${index + 1}`;
|
|
181
|
+
questionIds.set(questionKey, questionId);
|
|
182
|
+
const pendingQuestion: PendingQuestion = {
|
|
183
|
+
id: questionId,
|
|
184
|
+
question,
|
|
185
|
+
status: item.status,
|
|
186
|
+
resolutionOwner: item.resolution_owner,
|
|
187
|
+
gate: item.gate,
|
|
188
|
+
resolutionCriteria,
|
|
189
|
+
sourceRouteId: routeId,
|
|
190
|
+
};
|
|
191
|
+
const duplicateIndex = openedQuestions.findIndex((candidate) => candidate.id === questionId);
|
|
192
|
+
if (duplicateIndex === -1) openedQuestions.push(pendingQuestion);
|
|
193
|
+
else openedQuestions[duplicateIndex] = pendingQuestion;
|
|
194
|
+
}
|
|
195
|
+
return openedQuestions;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildQuestionUpdates(input: QuestionUpdateInput[], state: DeveloperState): QuestionUpdate[] {
|
|
199
|
+
const questionUpdates = input.map((update) => ({
|
|
200
|
+
questionId: update.question_id.trim(),
|
|
201
|
+
status: update.status,
|
|
202
|
+
result: update.result.trim(),
|
|
203
|
+
basis: update.basis.map((item) => item.trim()).filter(Boolean),
|
|
204
|
+
}));
|
|
205
|
+
const knownQuestionIds = new Set(state.pendingQuestions.map((question) => question.id));
|
|
206
|
+
const updatedIds = new Set<string>();
|
|
207
|
+
for (const update of questionUpdates) {
|
|
208
|
+
if (!knownQuestionIds.has(update.questionId)) fail(`Unknown pending question ID: ${update.questionId}`);
|
|
209
|
+
if (updatedIds.has(update.questionId)) fail(`Question ${update.questionId} was updated more than once.`);
|
|
210
|
+
updatedIds.add(update.questionId);
|
|
211
|
+
if (!update.result) fail(`Question update ${update.questionId} requires a non-empty result.`);
|
|
212
|
+
if (
|
|
213
|
+
(update.status === "resolved" || update.status === "not-applicable" || update.status === "blocked") &&
|
|
214
|
+
update.basis.length === 0
|
|
215
|
+
) {
|
|
216
|
+
fail(`Question update ${update.questionId} with status ${update.status} requires concrete basis.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return questionUpdates;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function judgmentNextMessage(event: JudgmentEvent, nextState: DeveloperState): string {
|
|
223
|
+
if (event.owner === "direct") {
|
|
224
|
+
if (nextState.verificationRequired) {
|
|
225
|
+
return "Stable landing recorded. Route again from the new evidence; verify is required before claiming completion.";
|
|
226
|
+
}
|
|
227
|
+
return "Stable landing recorded. Route again from the new evidence before selecting another movement.";
|
|
228
|
+
}
|
|
229
|
+
const next = protocolState(nextState);
|
|
230
|
+
if (next === "idle") {
|
|
231
|
+
return "Developer protocol is idle. This is routing state only and does not prove task completion.";
|
|
232
|
+
}
|
|
233
|
+
return `Developer protocol is ${next}. Address the current routing obligation before handoff.`;
|
|
234
|
+
}
|
|
235
|
+
|
|
116
236
|
function inferredQuestionId(
|
|
117
237
|
state: DeveloperState,
|
|
118
238
|
question: string,
|
|
@@ -124,8 +244,16 @@ function inferredQuestionId(
|
|
|
124
244
|
const exact = state.pendingQuestions.filter(
|
|
125
245
|
(pending) => normalizedQuestion(pending.question) === normalized,
|
|
126
246
|
);
|
|
127
|
-
|
|
128
|
-
|
|
247
|
+
return exact.length === 1 ? exact[0]?.id : undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function protocolToolAccess(state: DeveloperState): ProtocolToolAccess {
|
|
251
|
+
const snapshot = developerSnapshot(state);
|
|
252
|
+
return {
|
|
253
|
+
canExecute: snapshot.hasTag("execute"),
|
|
254
|
+
canMutate: snapshot.hasTag("mutate"),
|
|
255
|
+
hasBeforeDirectGate: snapshot.hasTag("blocks-direct"),
|
|
256
|
+
};
|
|
129
257
|
}
|
|
130
258
|
|
|
131
259
|
function summarizeState(state: DeveloperState): string {
|
|
@@ -145,14 +273,22 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
145
273
|
"- A direct step has one observable difference and one structural or behavioral purpose. Stop when its failure is locally explainable and the repository is green, pausable, and reviewable.",
|
|
146
274
|
"- For direct structural work intended to preserve behavior, set execution_profile=behavior-preserving-structure; omit the field for other direct actions and every skill route.",
|
|
147
275
|
`- Follow the routed method, then close the route with ${JUDGMENT_TOOL}. After every direct stable landing, route again from the new evidence before selecting another movement.`,
|
|
276
|
+
"- Consecutive direct routes are valid when the new evidence still justifies direct action. On a reroute after direct, explicitly reconsider the most plausible skill routes and record why they are not needed; do not choose direct by momentum.",
|
|
148
277
|
"- Do not carry a predetermined implementation queue through multiple direct steps. Re-observe after each stable landing and reroute to a skill whenever meaning, cases, design, structural direction, timing, naming, or evidence becomes uncertain.",
|
|
149
278
|
"- Protocol state is routing bookkeeping. Idle never proves product completion, user acceptance, or current verification.",
|
|
279
|
+
"- 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.",
|
|
280
|
+
`- 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.`,
|
|
281
|
+
"- 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.",
|
|
282
|
+
"- 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.",
|
|
283
|
+
"- Pending questions declare who can resolve them, what they block, and observable resolution criteria. Never guess a user-owned answer or bypass a before-direct gate.",
|
|
284
|
+
"- Use before-direct only when the missing answer can change whether or what artifact mutation is valid. An agent-owned before-direct question must be resolvable through a non-direct skill route using observation or evidence execution; it must never require the mutation it blocks.",
|
|
150
285
|
"- Product files are changed with Pi implementation tools. Developer protocol tools only route and record judgments.",
|
|
151
286
|
];
|
|
152
287
|
|
|
153
288
|
if (state.mode === "strict") {
|
|
154
289
|
lines.push(
|
|
155
|
-
"- Strict mode withholds
|
|
290
|
+
"- Strict mode withholds Pi built-in bash while idle, restores bash for a routed skill judgment so it can inspect or run evidence checks, and restores edit/write only for a direct route. It does not classify or sandbox other extensions' tools.",
|
|
291
|
+
"- Use a skill judgment route, not a direct route, when repository discovery or verifier execution is the unresolved work. Direct remains the artifact-mutation lane.",
|
|
156
292
|
);
|
|
157
293
|
}
|
|
158
294
|
|
|
@@ -166,6 +302,19 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
166
302
|
"Verification debt: a direct route changed artifacts after the last resolved verify judgment. Route verify before claiming completion or handing off as done.",
|
|
167
303
|
);
|
|
168
304
|
}
|
|
305
|
+
const completionGates = state.pendingQuestions.filter(
|
|
306
|
+
(question) => question.gate === "before-completion" || question.gate === "before-direct",
|
|
307
|
+
);
|
|
308
|
+
if (completionGates.length > 0) {
|
|
309
|
+
lines.push(
|
|
310
|
+
`Completion gate: ${completionGates.map((question) => question.id).join(", ")} must be resolved before a completion claim.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (!state.activeRoute && state.rerouteRequired) {
|
|
314
|
+
lines.push(
|
|
315
|
+
"Rerouting checkpoint: use the previous direct landing as known_evidence. If direct is still right, include alternatives_considered for the plausible available skills and explain why each adds no useful judgment now.",
|
|
316
|
+
);
|
|
317
|
+
}
|
|
169
318
|
|
|
170
319
|
if (state.activeRoute) {
|
|
171
320
|
lines.push(
|
|
@@ -180,10 +329,13 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
180
329
|
if (state.pendingQuestions.length > 0) {
|
|
181
330
|
lines.push("Pending Developer questions:");
|
|
182
331
|
for (const question of state.pendingQuestions) {
|
|
183
|
-
lines.push(
|
|
332
|
+
lines.push(
|
|
333
|
+
`- ${question.id} · ${question.status} · owner=${question.resolutionOwner} · gate=${question.gate} · ${question.question} · resolves when: ${question.resolutionCriteria}`,
|
|
334
|
+
);
|
|
184
335
|
}
|
|
185
336
|
lines.push(
|
|
186
|
-
"- Revisit questions naturally. Developer
|
|
337
|
+
"- 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.",
|
|
338
|
+
"- question_updates may resolve any pending ID from implementation, test, inspection, user, or environment evidence; each resolved update requires concrete basis.",
|
|
187
339
|
);
|
|
188
340
|
}
|
|
189
341
|
lines.push(
|
|
@@ -199,9 +351,56 @@ function routeRenderText(event: RouteEvent | undefined): string {
|
|
|
199
351
|
return `${event.owner}${profile} · ${compactLine(event.question)}${target}`;
|
|
200
352
|
}
|
|
201
353
|
|
|
354
|
+
function compactJudgmentResult(result: string, maxChars = 160): string {
|
|
355
|
+
const firstContentLine = result
|
|
356
|
+
.split(/\r?\n/)
|
|
357
|
+
.map((line) => line.trim())
|
|
358
|
+
.find((line) => line && !line.startsWith("```"));
|
|
359
|
+
return compactLine(firstContentLine ?? result, maxChars);
|
|
360
|
+
}
|
|
361
|
+
|
|
202
362
|
function judgmentRenderText(event: JudgmentEvent | undefined): string {
|
|
203
363
|
if (!event) return "Judgment unavailable";
|
|
204
|
-
return `${event.status} · ${
|
|
364
|
+
return `${event.status} · ${compactJudgmentResult(event.result)}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function detailLine(
|
|
368
|
+
theme: Theme,
|
|
369
|
+
label: string,
|
|
370
|
+
value: string,
|
|
371
|
+
valueColor: ThemeColor = "muted",
|
|
372
|
+
): string {
|
|
373
|
+
return `${theme.fg("dim", `${label} · `)}${theme.fg(valueColor, value)}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function expandedJudgment(event: JudgmentEvent, statusText: string, theme: Theme): Container {
|
|
377
|
+
const container = new Container();
|
|
378
|
+
const header = [
|
|
379
|
+
statusText,
|
|
380
|
+
detailLine(theme, "route", event.routeId, "text"),
|
|
381
|
+
detailLine(theme, "target", event.owner, "accent"),
|
|
382
|
+
detailLine(theme, "question", event.question, "text"),
|
|
383
|
+
];
|
|
384
|
+
container.addChild(new Text(header.join("\n"), 0, 0));
|
|
385
|
+
container.addChild(new Markdown(event.result, 0, 0, getMarkdownTheme()));
|
|
386
|
+
|
|
387
|
+
const evidence = [
|
|
388
|
+
...event.basis.map((basis) => detailLine(theme, "basis", basis)),
|
|
389
|
+
...event.artifacts.map((artifact) => detailLine(theme, "artifact", artifact)),
|
|
390
|
+
...event.openedQuestions.map((question) =>
|
|
391
|
+
detailLine(
|
|
392
|
+
theme,
|
|
393
|
+
`opened ${question.resolutionOwner}/${question.gate}`,
|
|
394
|
+
`${question.question} — resolves when: ${question.resolutionCriteria}`,
|
|
395
|
+
"warning",
|
|
396
|
+
),
|
|
397
|
+
),
|
|
398
|
+
...(event.questionUpdates ?? []).map((update) =>
|
|
399
|
+
detailLine(theme, `question ${update.status}`, `${update.questionId} — ${update.result}`, "accent"),
|
|
400
|
+
),
|
|
401
|
+
];
|
|
402
|
+
if (evidence.length > 0) container.addChild(new Text(evidence.join("\n"), 0, 0));
|
|
403
|
+
return container;
|
|
205
404
|
}
|
|
206
405
|
|
|
207
406
|
export default async function developer(pi: ExtensionAPI) {
|
|
@@ -222,7 +421,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
222
421
|
activeTools: current,
|
|
223
422
|
allTools: pi.getAllTools(),
|
|
224
423
|
mode: state.mode,
|
|
225
|
-
|
|
424
|
+
access: protocolToolAccess(state),
|
|
226
425
|
protocolTools: PROTOCOL_TOOLS,
|
|
227
426
|
memory: toolPolicyMemory,
|
|
228
427
|
});
|
|
@@ -308,6 +507,21 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
308
507
|
Type.String({ maxLength: 512, description: "Exact pending question ID when this route revisits one" }),
|
|
309
508
|
),
|
|
310
509
|
};
|
|
510
|
+
const RouteAlternativeParam = Type.Object(
|
|
511
|
+
{
|
|
512
|
+
owner: Type.String({
|
|
513
|
+
minLength: 1,
|
|
514
|
+
maxLength: 64,
|
|
515
|
+
description: "Exact available Developer skill name that was reconsidered",
|
|
516
|
+
}),
|
|
517
|
+
reason: Type.String({
|
|
518
|
+
minLength: 1,
|
|
519
|
+
maxLength: MAX_EVIDENCE_CHARS,
|
|
520
|
+
description: "Why this skill would add no useful judgment before the proposed direct movement",
|
|
521
|
+
}),
|
|
522
|
+
},
|
|
523
|
+
{ additionalProperties: false },
|
|
524
|
+
);
|
|
311
525
|
const RouteParams = Type.Union([
|
|
312
526
|
Type.Object(
|
|
313
527
|
{
|
|
@@ -340,6 +554,13 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
340
554
|
maxLength: MAX_EVIDENCE_CHARS,
|
|
341
555
|
description: "The narrowest check that can catch the likely break in this movement",
|
|
342
556
|
}),
|
|
557
|
+
alternatives_considered: Type.Optional(
|
|
558
|
+
Type.Array(RouteAlternativeParam, {
|
|
559
|
+
maxItems: 6,
|
|
560
|
+
description:
|
|
561
|
+
"On a reroute after direct, the plausible skill routes reconsidered and why each is unnecessary now",
|
|
562
|
+
}),
|
|
563
|
+
),
|
|
343
564
|
execution_profile: Type.Optional(
|
|
344
565
|
Type.Literal("behavior-preserving-structure", {
|
|
345
566
|
description: "Load the focused structural-mutation protocol; omit for ordinary direct action",
|
|
@@ -359,10 +580,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
359
580
|
promptGuidelines: [
|
|
360
581
|
`Call ${ROUTE_TOOL} only when there is no active Developer route.`,
|
|
361
582
|
`Use ${ROUTE_TOOL} with the most focused skill supported by current evidence; owner=direct requires one movement, one stable landing, and one narrow verification.`,
|
|
583
|
+
`When ${ROUTE_TOOL} follows a direct judgment with another direct route, cite the previous landing in known_evidence and record plausible skill routes in alternatives_considered instead of selecting direct by momentum.`,
|
|
362
584
|
`After a resolved model, use sketch for first feature implementation framing or signal for existing-code structural movement before direct mutation.`,
|
|
363
585
|
],
|
|
364
586
|
parameters: RouteParams,
|
|
365
587
|
executionMode: "sequential",
|
|
588
|
+
renderShell: "self",
|
|
366
589
|
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
367
590
|
if (state.mode === "off") fail("Developer protocol is off. Run /develop on or /develop strict first.");
|
|
368
591
|
if (state.activeRoute || routeOpening) {
|
|
@@ -383,6 +606,42 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
383
606
|
}
|
|
384
607
|
|
|
385
608
|
const owner = params.owner;
|
|
609
|
+
const directBlockers = state.pendingQuestions.filter(
|
|
610
|
+
(pending) => pending.gate === "before-direct",
|
|
611
|
+
);
|
|
612
|
+
if (owner === "direct" && directBlockers.length > 0) {
|
|
613
|
+
fail(
|
|
614
|
+
`Direct work is blocked by ${directBlockers
|
|
615
|
+
.map((pending) => `${pending.id} (${pending.resolutionOwner}: ${pending.question})`)
|
|
616
|
+
.join("; ")}. Resolve those questions first.`,
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
const knownEvidence = (params.known_evidence ?? []).map((item) => item.trim()).filter(Boolean);
|
|
620
|
+
const consideredAlternatives: RouteAlternative[] =
|
|
621
|
+
owner === "direct" && "alternatives_considered" in params
|
|
622
|
+
? (params.alternatives_considered ?? []).map((alternative) => ({
|
|
623
|
+
owner: alternative.owner.trim(),
|
|
624
|
+
reason: alternative.reason.trim(),
|
|
625
|
+
}))
|
|
626
|
+
: [];
|
|
627
|
+
for (const alternative of consideredAlternatives) {
|
|
628
|
+
if (!availableSkills.has(alternative.owner)) {
|
|
629
|
+
fail(`Considered alternative ${alternative.owner} is unavailable or disabled.`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (new Set(consideredAlternatives.map((alternative) => alternative.owner)).size !== consideredAlternatives.length) {
|
|
633
|
+
fail("Each considered alternative must name a different available Developer skill.");
|
|
634
|
+
}
|
|
635
|
+
if (owner === "direct" && state.lastJudgment?.owner === "direct") {
|
|
636
|
+
if (knownEvidence.length === 0) {
|
|
637
|
+
fail("A consecutive direct route must cite evidence from the previous direct landing in known_evidence.");
|
|
638
|
+
}
|
|
639
|
+
if (availableSkills.size > 0 && consideredAlternatives.length === 0) {
|
|
640
|
+
fail(
|
|
641
|
+
"A consecutive direct route must record the plausible available skill routes in alternatives_considered and explain why they are not needed now.",
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
386
645
|
if (
|
|
387
646
|
owner === "direct" &&
|
|
388
647
|
state.implementationFramingRequired &&
|
|
@@ -443,7 +702,8 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
443
702
|
question,
|
|
444
703
|
owner,
|
|
445
704
|
reason,
|
|
446
|
-
knownEvidence
|
|
705
|
+
knownEvidence,
|
|
706
|
+
consideredAlternatives,
|
|
447
707
|
targetQuestionId,
|
|
448
708
|
methodLocation: skill?.filePath,
|
|
449
709
|
executionProfile,
|
|
@@ -465,6 +725,13 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
465
725
|
: []),
|
|
466
726
|
`Reason: ${event.reason}`,
|
|
467
727
|
`Known evidence: ${event.knownEvidence.length > 0 ? event.knownEvidence.join(" | ") : "none"}`,
|
|
728
|
+
`Alternatives reconsidered: ${
|
|
729
|
+
event.consideredAlternatives.length > 0
|
|
730
|
+
? event.consideredAlternatives
|
|
731
|
+
.map((alternative) => `${alternative.owner} — ${alternative.reason}`)
|
|
732
|
+
.join(" | ")
|
|
733
|
+
: "none"
|
|
734
|
+
}`,
|
|
468
735
|
targetQuestionId ? `Revisits pending question: ${targetQuestionId}` : "Revisits pending question: none",
|
|
469
736
|
`When this route has done its job, call ${JUDGMENT_TOOL} with this exact route ID.`,
|
|
470
737
|
"",
|
|
@@ -473,6 +740,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
473
740
|
method,
|
|
474
741
|
].join("\n");
|
|
475
742
|
ensureSafeToolText(response, "Developer route result");
|
|
743
|
+
if (!canApplyDeveloperEvent(state, event)) {
|
|
744
|
+
fail("Developer machine guard rejected the route transition from the current branch state.");
|
|
745
|
+
}
|
|
476
746
|
|
|
477
747
|
state = applyDeveloperEvent(state, event);
|
|
478
748
|
syncProtocolTools();
|
|
@@ -504,25 +774,28 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
504
774
|
const event = result.details as RouteEvent | undefined;
|
|
505
775
|
let text = theme.fg("success", `routed ${routeRenderText(event)}`);
|
|
506
776
|
if (expanded && event) {
|
|
507
|
-
text += `\n${theme
|
|
508
|
-
text += `\n${theme
|
|
509
|
-
text += `\n${theme
|
|
777
|
+
text += `\n${detailLine(theme, "route", event.routeId, "text")}`;
|
|
778
|
+
text += `\n${detailLine(theme, "question", event.question, "text")}`;
|
|
779
|
+
text += `\n${detailLine(theme, "reason", event.reason)}`;
|
|
510
780
|
if (event.knownEvidence.length > 0) {
|
|
511
781
|
for (const evidence of event.knownEvidence) {
|
|
512
|
-
text += `\n${theme
|
|
782
|
+
text += `\n${detailLine(theme, "evidence", evidence)}`;
|
|
513
783
|
}
|
|
514
784
|
} else {
|
|
515
|
-
text += `\n${theme
|
|
785
|
+
text += `\n${detailLine(theme, "evidence", "none recorded before routing", "warning")}`;
|
|
786
|
+
}
|
|
787
|
+
for (const alternative of event.consideredAlternatives ?? []) {
|
|
788
|
+
text += `\n${detailLine(theme, `considered ${alternative.owner}`, alternative.reason)}`;
|
|
516
789
|
}
|
|
517
|
-
text += `\n${theme
|
|
518
|
-
text += `\n${theme
|
|
790
|
+
text += `\n${detailLine(theme, "revisits", event.targetQuestionId ?? "none")}`;
|
|
791
|
+
text += `\n${detailLine(theme, "skill", event.methodLocation ?? "direct action")}`;
|
|
519
792
|
if (event.executionProfile) {
|
|
520
|
-
text += `\n${theme
|
|
793
|
+
text += `\n${detailLine(theme, "profile", event.executionProfile)}`;
|
|
521
794
|
}
|
|
522
795
|
if (event.directStep) {
|
|
523
|
-
text += `\n${theme
|
|
524
|
-
text += `\n${theme
|
|
525
|
-
text += `\n${theme
|
|
796
|
+
text += `\n${detailLine(theme, "movement", event.directStep.movement, "text")}`;
|
|
797
|
+
text += `\n${detailLine(theme, "landing", event.directStep.stopCondition)}`;
|
|
798
|
+
text += `\n${detailLine(theme, "verify", event.directStep.verification)}`;
|
|
526
799
|
}
|
|
527
800
|
}
|
|
528
801
|
if (!expanded && event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
@@ -530,22 +803,66 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
530
803
|
},
|
|
531
804
|
});
|
|
532
805
|
|
|
806
|
+
const OpenQuestionParam = Type.Object(
|
|
807
|
+
{
|
|
808
|
+
question: Type.String({ minLength: 1, maxLength: MAX_QUESTION_CHARS }),
|
|
809
|
+
status: StringEnum(["open", "blocked"] as const, {
|
|
810
|
+
description: "Whether the required evidence or answer is currently obtainable",
|
|
811
|
+
}),
|
|
812
|
+
resolution_owner: StringEnum(["agent", "user", "environment"] as const, {
|
|
813
|
+
description: "Who can provide the evidence or decision that resolves this question",
|
|
814
|
+
}),
|
|
815
|
+
gate: StringEnum(["none", "before-direct", "before-completion"] as const, {
|
|
816
|
+
description:
|
|
817
|
+
"What work this question blocks; an agent before-direct question must be resolvable through a non-direct evidence route",
|
|
818
|
+
}),
|
|
819
|
+
resolution_criteria: Type.String({
|
|
820
|
+
minLength: 1,
|
|
821
|
+
maxLength: MAX_EVIDENCE_CHARS,
|
|
822
|
+
description: "Observable evidence or answer that will settle the question",
|
|
823
|
+
}),
|
|
824
|
+
},
|
|
825
|
+
{ additionalProperties: false },
|
|
826
|
+
);
|
|
827
|
+
const QuestionUpdateParam = Type.Object(
|
|
828
|
+
{
|
|
829
|
+
question_id: Type.String({
|
|
830
|
+
minLength: 1,
|
|
831
|
+
maxLength: 512,
|
|
832
|
+
description:
|
|
833
|
+
"Exact ID of a question that was pending before this route; never route_id or a newly opened question ID",
|
|
834
|
+
}),
|
|
835
|
+
status: StringEnum(["resolved", "not-applicable", "open", "blocked"] as const),
|
|
836
|
+
result: Type.String({ minLength: 1, maxLength: MAX_RESULT_CHARS }),
|
|
837
|
+
basis: Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), { maxItems: 20 }),
|
|
838
|
+
},
|
|
839
|
+
{ additionalProperties: false },
|
|
840
|
+
);
|
|
533
841
|
const JudgmentParams = Type.Object({
|
|
534
842
|
route_id: Type.String({ minLength: 1, maxLength: 512, description: `Exact route ID returned by ${ROUTE_TOOL}` }),
|
|
535
843
|
status: StringEnum(["resolved", "needs-evidence", "not-applicable", "blocked"] as const),
|
|
536
844
|
result: Type.String({
|
|
537
845
|
minLength: 1,
|
|
538
846
|
maxLength: MAX_RESULT_CHARS,
|
|
539
|
-
description:
|
|
847
|
+
description:
|
|
848
|
+
"The resulting judgment in Markdown; preserve the routed skill's inspectable tables, diagrams, matrices, timelines, or code blocks",
|
|
540
849
|
}),
|
|
541
850
|
basis: Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), {
|
|
542
851
|
maxItems: 20,
|
|
543
852
|
description: "Evidence supporting the judgment or blocker",
|
|
544
853
|
}),
|
|
545
854
|
open_questions: Type.Optional(
|
|
546
|
-
Type.Array(
|
|
855
|
+
Type.Array(OpenQuestionParam, {
|
|
547
856
|
maxItems: 10,
|
|
548
|
-
description:
|
|
857
|
+
description:
|
|
858
|
+
"New unresolved questions with explicit resolution owner, gate, and observable resolution criteria",
|
|
859
|
+
}),
|
|
860
|
+
),
|
|
861
|
+
question_updates: Type.Optional(
|
|
862
|
+
Type.Array(QuestionUpdateParam, {
|
|
863
|
+
maxItems: 20,
|
|
864
|
+
description:
|
|
865
|
+
"Updates to questions already pending before this route; use [] when none exist, and never put route_id or newly opened questions here",
|
|
549
866
|
}),
|
|
550
867
|
),
|
|
551
868
|
artifacts: Type.Optional(
|
|
@@ -564,10 +881,28 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
564
881
|
promptSnippet: "Record evidence and close the active development route",
|
|
565
882
|
promptGuidelines: [
|
|
566
883
|
`Use ${JUDGMENT_TOOL} with the exact active Developer route ID.`,
|
|
884
|
+
`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.`,
|
|
885
|
+
`Use ${JUDGMENT_TOOL} open_questions with resolution_owner, gate, and resolution_criteria; use question_updates whenever current evidence settles or changes any existing pending question.`,
|
|
567
886
|
`Do not use ${JUDGMENT_TOOL} with resolved, not-applicable, or blocked status without at least one concrete basis.`,
|
|
568
887
|
],
|
|
569
888
|
parameters: JudgmentParams,
|
|
570
889
|
executionMode: "sequential",
|
|
890
|
+
renderShell: "self",
|
|
891
|
+
prepareArguments(args): Static<typeof JudgmentParams> {
|
|
892
|
+
const input = args as Static<typeof JudgmentParams> & {
|
|
893
|
+
status?: string;
|
|
894
|
+
open_questions?: unknown[];
|
|
895
|
+
};
|
|
896
|
+
if (!input || typeof input !== "object" || !Array.isArray(input.open_questions)) {
|
|
897
|
+
return args as Static<typeof JudgmentParams>;
|
|
898
|
+
}
|
|
899
|
+
return {
|
|
900
|
+
...input,
|
|
901
|
+
open_questions: input.open_questions.map((question) =>
|
|
902
|
+
typeof question === "string" ? legacyOpenQuestion(question, input.status) : question,
|
|
903
|
+
) as Static<typeof JudgmentParams>["open_questions"],
|
|
904
|
+
};
|
|
905
|
+
},
|
|
571
906
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
572
907
|
if (state.mode === "off") fail("Developer protocol is off.");
|
|
573
908
|
if (!state.activeRoute) fail("There is no active Developer route to close.");
|
|
@@ -582,15 +917,55 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
582
917
|
fail(`${params.status} judgments require at least one concrete basis.`);
|
|
583
918
|
}
|
|
584
919
|
|
|
585
|
-
const openedQuestions = (params.open_questions ?? [])
|
|
586
|
-
|
|
587
|
-
.
|
|
588
|
-
.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
920
|
+
const openedQuestions = buildOpenedQuestions(params.open_questions ?? [], state, params.route_id);
|
|
921
|
+
if (
|
|
922
|
+
availableSkills.size === 0 &&
|
|
923
|
+
openedQuestions.some(
|
|
924
|
+
(question) => question.resolutionOwner === "agent" && question.gate === "before-direct",
|
|
925
|
+
)
|
|
926
|
+
) {
|
|
927
|
+
fail("An agent-owned before-direct question requires an available non-direct skill resolution path.");
|
|
928
|
+
}
|
|
929
|
+
const remainsOpen = params.status === "needs-evidence" || params.status === "blocked";
|
|
930
|
+
if (remainsOpen && !state.activeRoute.targetQuestionId && openedQuestions.length === 0) {
|
|
931
|
+
fail(
|
|
932
|
+
"A needs-evidence or blocked judgment for a new question must include at least one structured open_questions entry.",
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (state.pendingQuestions.length > 0 && params.question_updates === undefined) {
|
|
937
|
+
fail(
|
|
938
|
+
"Pending questions exist. Include question_updates (an empty array is valid) after rechecking them against this route's evidence.",
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const questionUpdates = buildQuestionUpdates(params.question_updates ?? [], state);
|
|
942
|
+
const updatedQuestionIds = new Set(questionUpdates.map((update) => update.questionId));
|
|
943
|
+
if (openedQuestions.some((question) => updatedQuestionIds.has(question.id))) {
|
|
944
|
+
fail("A question cannot be opened and updated in the same judgment.");
|
|
945
|
+
}
|
|
946
|
+
if (!remainsOpen && openedQuestions.length > 0) {
|
|
947
|
+
fail("A resolved or not-applicable judgment cannot open new questions.");
|
|
948
|
+
}
|
|
949
|
+
const targetQuestionId = state.activeRoute.targetQuestionId;
|
|
950
|
+
const targetUpdate = targetQuestionId
|
|
951
|
+
? questionUpdates.find((update) => update.questionId === targetQuestionId)
|
|
952
|
+
: undefined;
|
|
953
|
+
if (targetQuestionId && !targetUpdate) {
|
|
954
|
+
fail(`Focused question ${targetQuestionId} requires an explicit question_updates entry.`);
|
|
955
|
+
}
|
|
956
|
+
if (targetUpdate) {
|
|
957
|
+
const closesTarget = targetUpdate.status === "resolved" || targetUpdate.status === "not-applicable";
|
|
958
|
+
if (!remainsOpen && !closesTarget) {
|
|
959
|
+
fail("A resolved focused judgment must explicitly resolve or dismiss its question.");
|
|
960
|
+
}
|
|
961
|
+
if (remainsOpen && openedQuestions.length === 0 && closesTarget) {
|
|
962
|
+
fail("A focused question cannot close while its judgment reports no replacement evidence question.");
|
|
963
|
+
}
|
|
964
|
+
if (remainsOpen && openedQuestions.length > 0 && !closesTarget) {
|
|
965
|
+
fail("Replacement questions require explicitly resolving or dismissing the broader focused question.");
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
594
969
|
const event: JudgmentEvent = {
|
|
595
970
|
protocol: PROTOCOL,
|
|
596
971
|
kind: "judgment",
|
|
@@ -601,9 +976,13 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
601
976
|
result,
|
|
602
977
|
basis,
|
|
603
978
|
openedQuestions,
|
|
979
|
+
questionUpdates,
|
|
604
980
|
artifacts: (params.artifacts ?? []).map((item) => item.trim()).filter(Boolean),
|
|
605
981
|
changedArtifacts: routesWithMutation.has(params.route_id),
|
|
606
982
|
};
|
|
983
|
+
if (!canApplyDeveloperEvent(state, event)) {
|
|
984
|
+
fail("Developer machine guard rejected the judgment transition from the current active route.");
|
|
985
|
+
}
|
|
607
986
|
const nextState = applyDeveloperEvent(state, event);
|
|
608
987
|
if (nextState.pendingQuestions.length > MAX_PENDING_QUESTIONS) {
|
|
609
988
|
fail(
|
|
@@ -611,16 +990,11 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
611
990
|
);
|
|
612
991
|
}
|
|
613
992
|
|
|
614
|
-
const
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
: "Stable landing recorded. Route again from the new evidence before selecting another movement."
|
|
620
|
-
: next === "idle"
|
|
621
|
-
? "Developer protocol is idle. This is routing state only and does not prove task completion."
|
|
622
|
-
: `Developer protocol is ${next}. Address the current routing obligation before handoff.`;
|
|
623
|
-
const response = `Recorded ${event.status} judgment for ${event.routeId}: ${event.result}\n${nextMessage}`;
|
|
993
|
+
const nextMessage = judgmentNextMessage(event, nextState);
|
|
994
|
+
const resolvedQuestionCount = event.questionUpdates.filter(
|
|
995
|
+
(update) => update.status === "resolved" || update.status === "not-applicable",
|
|
996
|
+
).length;
|
|
997
|
+
const response = `Recorded ${event.status} judgment for ${event.routeId}: ${event.result}\nQuestion updates: ${resolvedQuestionCount} resolved, ${event.questionUpdates.length - resolvedQuestionCount} retained or blocked.\n${nextMessage}`;
|
|
624
998
|
ensureSafeToolText(response, "Developer judgment result");
|
|
625
999
|
|
|
626
1000
|
routesWithMutation.delete(params.route_id);
|
|
@@ -632,7 +1006,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
632
1006
|
renderCall(args, theme, context) {
|
|
633
1007
|
const status = typeof args.status === "string" && args.status.length > 0 ? args.status : "…";
|
|
634
1008
|
const result =
|
|
635
|
-
typeof args.result === "string" && args.result.length > 0 ?
|
|
1009
|
+
typeof args.result === "string" && args.result.length > 0 ? compactJudgmentResult(args.result) : "…";
|
|
636
1010
|
const statusText =
|
|
637
1011
|
status === "resolved"
|
|
638
1012
|
? theme.fg("success", status)
|
|
@@ -666,18 +1040,8 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
666
1040
|
: event?.status === "blocked"
|
|
667
1041
|
? theme.fg("error", summary)
|
|
668
1042
|
: theme.fg("muted", summary);
|
|
669
|
-
if (expanded && event)
|
|
670
|
-
|
|
671
|
-
text += `\n${theme.fg("dim", `target · ${event.owner}`)}`;
|
|
672
|
-
text += `\n${theme.fg("dim", `question · ${event.question}`)}`;
|
|
673
|
-
text += `\n${theme.fg("dim", `result · ${event.result}`)}`;
|
|
674
|
-
for (const basis of event.basis) text += `\n${theme.fg("dim", `basis · ${basis}`)}`;
|
|
675
|
-
for (const artifact of event.artifacts) text += `\n${theme.fg("dim", `artifact · ${artifact}`)}`;
|
|
676
|
-
for (const question of event.openedQuestions) {
|
|
677
|
-
text += `\n${theme.fg("warning", `opened · ${question.id} · ${question.question}`)}`;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (!expanded && event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
1043
|
+
if (expanded && event) return expandedJudgment(event, text, theme);
|
|
1044
|
+
if (event) text += ` · ${keyHint("app.tools.expand", "details")}`;
|
|
681
1045
|
return reusableText(text, context.lastComponent);
|
|
682
1046
|
},
|
|
683
1047
|
});
|
|
@@ -697,7 +1061,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
697
1061
|
const pending =
|
|
698
1062
|
state.pendingQuestions.length > 0
|
|
699
1063
|
? state.pendingQuestions
|
|
700
|
-
.map(
|
|
1064
|
+
.map(
|
|
1065
|
+
(question) =>
|
|
1066
|
+
`${question.id} · ${question.status} · ${question.resolutionOwner}/${question.gate} · ${question.question} · resolves when: ${question.resolutionCriteria}`,
|
|
1067
|
+
)
|
|
701
1068
|
.join(" | ")
|
|
702
1069
|
: "none";
|
|
703
1070
|
const last = state.lastJudgment
|
|
@@ -711,12 +1078,31 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
711
1078
|
state.lastJudgment && state.lastJudgment.artifacts.length > 0
|
|
712
1079
|
? state.lastJudgment.artifacts.join(" | ")
|
|
713
1080
|
: "none";
|
|
1081
|
+
const history =
|
|
1082
|
+
state.judgmentHistory.length > 0
|
|
1083
|
+
? state.judgmentHistory
|
|
1084
|
+
.slice(-10)
|
|
1085
|
+
.map((judgment) => {
|
|
1086
|
+
const route = state.routeHistory.find((candidate) => candidate.routeId === judgment.routeId);
|
|
1087
|
+
const alternatives = (route?.consideredAlternatives ?? [])
|
|
1088
|
+
.map((alternative) => `${alternative.owner}: ${alternative.reason}`)
|
|
1089
|
+
.join("; ");
|
|
1090
|
+
return `${judgment.owner} · ${judgment.status} · ${judgment.result}${
|
|
1091
|
+
alternatives ? ` · considered ${alternatives}` : ""
|
|
1092
|
+
}`;
|
|
1093
|
+
})
|
|
1094
|
+
.join("\n")
|
|
1095
|
+
: "none";
|
|
714
1096
|
return (
|
|
715
1097
|
`${summarizeState(state)}` +
|
|
716
1098
|
`\nactive: ${active}` +
|
|
717
1099
|
`\nlast: ${last}` +
|
|
718
1100
|
`\nbasis: ${basis}` +
|
|
719
1101
|
`\nartifacts: ${artifacts}` +
|
|
1102
|
+
`\nimplementation framing: ${state.implementationFramingRequired ? "required" : "clear"}` +
|
|
1103
|
+
`\ncheckpoint: ${state.rerouteRequired ? "reroute required" : "ready"}` +
|
|
1104
|
+
`\nverification: ${state.verificationRequired ? "required" : "current"}` +
|
|
1105
|
+
`\nhistory:\n${history}` +
|
|
720
1106
|
`\nactive tools: ${pi.getActiveTools().join(", ")}` +
|
|
721
1107
|
`\navailable skills: ${[...availableSkills.keys()].join(", ") || "none"}` +
|
|
722
1108
|
`\npending: ${pending}` +
|
|
@@ -778,16 +1164,26 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
778
1164
|
kind: "focus",
|
|
779
1165
|
questionId: question.id,
|
|
780
1166
|
};
|
|
1167
|
+
const request = await editQuestionResolutionRequest(ctx, question);
|
|
1168
|
+
if (request === undefined) return;
|
|
781
1169
|
pi.appendEntry(FOCUS_ENTRY, focusEvent);
|
|
782
1170
|
state = applyDeveloperEvent(state, focusEvent);
|
|
783
1171
|
refreshUI(ctx);
|
|
784
|
-
|
|
785
|
-
ctx.ui.notify(
|
|
1172
|
+
ctx.ui.setEditorText("");
|
|
1173
|
+
ctx.ui.notify(
|
|
1174
|
+
"Question response submitted. It remains open until Developer records a resolved or not-applicable judgment.",
|
|
1175
|
+
"info",
|
|
1176
|
+
);
|
|
1177
|
+
if (ctx.isIdle()) pi.sendUserMessage(request);
|
|
1178
|
+
else pi.sendUserMessage(request, { deliverAs: "followUp" });
|
|
786
1179
|
return;
|
|
787
1180
|
}
|
|
788
1181
|
ctx.ui.notify(
|
|
789
1182
|
state.pendingQuestions
|
|
790
|
-
.map(
|
|
1183
|
+
.map(
|
|
1184
|
+
(question) =>
|
|
1185
|
+
`${question.id} · ${question.status} · ${question.resolutionOwner}/${question.gate} · ${question.question}\n resolves when: ${question.resolutionCriteria}`,
|
|
1186
|
+
)
|
|
791
1187
|
.join("\n"),
|
|
792
1188
|
"info",
|
|
793
1189
|
);
|
|
@@ -830,15 +1226,34 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
830
1226
|
});
|
|
831
1227
|
|
|
832
1228
|
pi.on("tool_call", (event) => {
|
|
833
|
-
const
|
|
834
|
-
if (
|
|
835
|
-
|
|
1229
|
+
const capability = builtinControlledToolCapabilities(pi.getAllTools()).get(event.toolName);
|
|
1230
|
+
if (!capability) return;
|
|
1231
|
+
|
|
1232
|
+
const access = protocolToolAccess(state);
|
|
1233
|
+
if (isControlledToolAllowed({ mode: state.mode, capability, access })) {
|
|
1234
|
+
if (access.canMutate && state.activeRoute) routesWithMutation.add(state.activeRoute.routeId);
|
|
836
1235
|
return;
|
|
837
1236
|
}
|
|
838
|
-
|
|
1237
|
+
|
|
1238
|
+
const directBlockers = state.pendingQuestions.filter(
|
|
1239
|
+
(question) => question.gate === "before-direct",
|
|
1240
|
+
);
|
|
1241
|
+
if (directBlockers.length > 0) {
|
|
1242
|
+
const blockedAction = capability === "execute" ? "unrouted shell execution" : "artifact mutation";
|
|
1243
|
+
return {
|
|
1244
|
+
block: true,
|
|
1245
|
+
reason: `Developer question gate blocks ${blockedAction} until resolved: ${directBlockers
|
|
1246
|
+
.map((question) => `${question.id} (${question.resolutionOwner}: ${question.question})`)
|
|
1247
|
+
.join("; ")}. A non-direct judgment route may still use bash to obtain evidence.`,
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
839
1251
|
return {
|
|
840
1252
|
block: true,
|
|
841
|
-
reason:
|
|
1253
|
+
reason:
|
|
1254
|
+
capability === "execute"
|
|
1255
|
+
? `Developer strict mode requires an active judgment or direct route before Pi built-in bash can execute evidence checks.`
|
|
1256
|
+
: `Developer strict mode requires an active ${ROUTE_TOOL} targeting direct action (owner=direct) before Pi built-in edit or write.`,
|
|
842
1257
|
};
|
|
843
1258
|
});
|
|
844
1259
|
|