@hobin/developer 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -11
- package/extensions/developer.ts +381 -92
- package/extensions/state.ts +120 -8
- package/extensions/tui.ts +341 -37
- package/package.json +1 -1
package/extensions/developer.ts
CHANGED
|
@@ -22,6 +22,10 @@ import { availablePackageSkills, renderSkillMethod } from "./skills.ts";
|
|
|
22
22
|
import {
|
|
23
23
|
FOCUS_ENTRY,
|
|
24
24
|
JUDGMENT_TOOL,
|
|
25
|
+
MAX_RESPONSE_FIELDS,
|
|
26
|
+
MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
27
|
+
MAX_RESPONSE_OPTIONS,
|
|
28
|
+
MAX_RESPONSE_TEXT_CHARS,
|
|
25
29
|
MODE_ENTRY,
|
|
26
30
|
PROTOCOL,
|
|
27
31
|
ROUTE_TOOL,
|
|
@@ -30,8 +34,10 @@ import {
|
|
|
30
34
|
developerSnapshot,
|
|
31
35
|
initialState,
|
|
32
36
|
normalizeDeveloperEvent,
|
|
37
|
+
parseChoiceResponseSpec,
|
|
33
38
|
protocolState,
|
|
34
39
|
reconstructState,
|
|
40
|
+
type ChoiceResponseSpec,
|
|
35
41
|
type DeveloperMode,
|
|
36
42
|
type DeveloperState,
|
|
37
43
|
type DirectExecutionProfile,
|
|
@@ -57,10 +63,12 @@ import {
|
|
|
57
63
|
import {
|
|
58
64
|
DeveloperWidget,
|
|
59
65
|
editQuestionResolutionRequest,
|
|
66
|
+
promptImmediateUserQuestion,
|
|
60
67
|
renderDeveloperFooter,
|
|
61
68
|
showDeveloperActionSelector,
|
|
62
69
|
showDeveloperStatus,
|
|
63
70
|
showPendingQuestionSelector,
|
|
71
|
+
type ImmediateQuestionDisposition,
|
|
64
72
|
} from "./tui.ts";
|
|
65
73
|
|
|
66
74
|
const PROTOCOL_TOOLS = [ROUTE_TOOL, JUDGMENT_TOOL] as const;
|
|
@@ -73,6 +81,7 @@ const structuralChangeMethodPath = resolve(
|
|
|
73
81
|
);
|
|
74
82
|
const MAX_PENDING_QUESTIONS = 20;
|
|
75
83
|
const MAX_QUESTION_CHARS = 2_000;
|
|
84
|
+
const MAX_QUESTION_CONTEXT_CHARS = 8_000;
|
|
76
85
|
const MAX_EVIDENCE_CHARS = 2_000;
|
|
77
86
|
const MAX_RESULT_CHARS = 12_000;
|
|
78
87
|
const MAX_ARTIFACT_CHARS = 4_096;
|
|
@@ -124,11 +133,35 @@ function compactLine(value: string, maxChars = 160): string {
|
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
function normalizedQuestion(value: string): string {
|
|
127
|
-
return value
|
|
136
|
+
return value
|
|
137
|
+
.toLocaleLowerCase()
|
|
138
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
139
|
+
.trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface ChoiceResponseOptionInput {
|
|
143
|
+
value: string;
|
|
144
|
+
label: string;
|
|
145
|
+
description?: string;
|
|
146
|
+
detail_prompt?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface ChoiceResponseFieldInput {
|
|
150
|
+
id: string;
|
|
151
|
+
prompt: string;
|
|
152
|
+
description?: string;
|
|
153
|
+
options: ChoiceResponseOptionInput[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface ChoiceResponseSpecInput {
|
|
157
|
+
kind: "choice-form";
|
|
158
|
+
fields: ChoiceResponseFieldInput[];
|
|
128
159
|
}
|
|
129
160
|
|
|
130
161
|
interface OpenQuestionInput {
|
|
131
162
|
question: string;
|
|
163
|
+
context?: string;
|
|
164
|
+
response_spec?: ChoiceResponseSpecInput;
|
|
132
165
|
status: PendingQuestionStatus;
|
|
133
166
|
resolution_owner: Exclude<QuestionResolutionOwner, "unknown">;
|
|
134
167
|
gate: QuestionGate;
|
|
@@ -161,6 +194,36 @@ function legacyOpenQuestion(question: string, judgmentStatus?: string): OpenQues
|
|
|
161
194
|
};
|
|
162
195
|
}
|
|
163
196
|
|
|
197
|
+
function buildChoiceResponseSpec(
|
|
198
|
+
input: ChoiceResponseSpecInput | undefined,
|
|
199
|
+
owner: OpenQuestionInput["resolution_owner"],
|
|
200
|
+
): ChoiceResponseSpec | undefined {
|
|
201
|
+
if (!input) return undefined;
|
|
202
|
+
if (owner !== "user") {
|
|
203
|
+
fail("response_spec is valid only for user-owned questions.");
|
|
204
|
+
}
|
|
205
|
+
const responseSpec = parseChoiceResponseSpec({
|
|
206
|
+
kind: input.kind,
|
|
207
|
+
fields: input.fields.map((field) => ({
|
|
208
|
+
id: field.id,
|
|
209
|
+
prompt: field.prompt,
|
|
210
|
+
description: field.description,
|
|
211
|
+
options: field.options.map((option) => ({
|
|
212
|
+
value: option.value,
|
|
213
|
+
label: option.label,
|
|
214
|
+
description: option.description,
|
|
215
|
+
detailPrompt: option.detail_prompt,
|
|
216
|
+
})),
|
|
217
|
+
})),
|
|
218
|
+
});
|
|
219
|
+
if (!responseSpec) {
|
|
220
|
+
fail(
|
|
221
|
+
"response_spec must use unique field IDs and unique option values with non-whitespace text.",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return responseSpec;
|
|
225
|
+
}
|
|
226
|
+
|
|
164
227
|
function buildOpenedQuestions(
|
|
165
228
|
input: OpenQuestionInput[],
|
|
166
229
|
state: DeveloperState,
|
|
@@ -172,6 +235,8 @@ function buildOpenedQuestions(
|
|
|
172
235
|
const openedQuestions: PendingQuestion[] = [];
|
|
173
236
|
for (const [index, item] of input.entries()) {
|
|
174
237
|
const question = item.question.trim();
|
|
238
|
+
const context = item.context?.trim() || undefined;
|
|
239
|
+
const responseSpec = buildChoiceResponseSpec(item.response_spec, item.resolution_owner);
|
|
175
240
|
const resolutionCriteria = item.resolution_criteria.trim();
|
|
176
241
|
if (!question || !resolutionCriteria) {
|
|
177
242
|
fail("Each open question requires non-whitespace question and resolution_criteria text.");
|
|
@@ -182,6 +247,8 @@ function buildOpenedQuestions(
|
|
|
182
247
|
const pendingQuestion: PendingQuestion = {
|
|
183
248
|
id: questionId,
|
|
184
249
|
question,
|
|
250
|
+
context,
|
|
251
|
+
responseSpec,
|
|
185
252
|
status: item.status,
|
|
186
253
|
resolutionOwner: item.resolution_owner,
|
|
187
254
|
gate: item.gate,
|
|
@@ -195,7 +262,41 @@ function buildOpenedQuestions(
|
|
|
195
262
|
return openedQuestions;
|
|
196
263
|
}
|
|
197
264
|
|
|
198
|
-
function
|
|
265
|
+
function firstImmediateUserQuestion(
|
|
266
|
+
previous: readonly PendingQuestion[],
|
|
267
|
+
opened: readonly PendingQuestion[],
|
|
268
|
+
): PendingQuestion | undefined {
|
|
269
|
+
const previousIds = new Set(previous.map((question) => question.id));
|
|
270
|
+
return opened.find(
|
|
271
|
+
(question) =>
|
|
272
|
+
!previousIds.has(question.id) &&
|
|
273
|
+
question.status === "open" &&
|
|
274
|
+
question.resolutionOwner === "user" &&
|
|
275
|
+
question.gate === "before-direct",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function immediateQuestionMessage(
|
|
280
|
+
question: PendingQuestion | undefined,
|
|
281
|
+
disposition: ImmediateQuestionDisposition | undefined,
|
|
282
|
+
): string {
|
|
283
|
+
if (!question) return "";
|
|
284
|
+
if (disposition?.kind === "answer") {
|
|
285
|
+
return [
|
|
286
|
+
"",
|
|
287
|
+
`Immediate user answer for ${question.id}:`,
|
|
288
|
+
disposition.request,
|
|
289
|
+
"",
|
|
290
|
+
`Route this exact question with open_question_id=${question.id}, use the answer as new evidence, and close it only through question_updates.`,
|
|
291
|
+
].join("\n");
|
|
292
|
+
}
|
|
293
|
+
return `\nThe user left ${question.id} open for later.`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildQuestionUpdates(
|
|
297
|
+
input: QuestionUpdateInput[],
|
|
298
|
+
state: DeveloperState,
|
|
299
|
+
): QuestionUpdate[] {
|
|
199
300
|
const questionUpdates = input.map((update) => ({
|
|
200
301
|
questionId: update.question_id.trim(),
|
|
201
302
|
status: update.status,
|
|
@@ -205,15 +306,21 @@ function buildQuestionUpdates(input: QuestionUpdateInput[], state: DeveloperStat
|
|
|
205
306
|
const knownQuestionIds = new Set(state.pendingQuestions.map((question) => question.id));
|
|
206
307
|
const updatedIds = new Set<string>();
|
|
207
308
|
for (const update of questionUpdates) {
|
|
208
|
-
if (!knownQuestionIds.has(update.questionId))
|
|
209
|
-
|
|
309
|
+
if (!knownQuestionIds.has(update.questionId))
|
|
310
|
+
fail(`Unknown pending question ID: ${update.questionId}`);
|
|
311
|
+
if (updatedIds.has(update.questionId))
|
|
312
|
+
fail(`Question ${update.questionId} was updated more than once.`);
|
|
210
313
|
updatedIds.add(update.questionId);
|
|
211
314
|
if (!update.result) fail(`Question update ${update.questionId} requires a non-empty result.`);
|
|
212
315
|
if (
|
|
213
|
-
(update.status === "resolved" ||
|
|
316
|
+
(update.status === "resolved" ||
|
|
317
|
+
update.status === "not-applicable" ||
|
|
318
|
+
update.status === "blocked") &&
|
|
214
319
|
update.basis.length === 0
|
|
215
320
|
) {
|
|
216
|
-
fail(
|
|
321
|
+
fail(
|
|
322
|
+
`Question update ${update.questionId} with status ${update.status} requires concrete basis.`,
|
|
323
|
+
);
|
|
217
324
|
}
|
|
218
325
|
}
|
|
219
326
|
return questionUpdates;
|
|
@@ -280,7 +387,7 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
280
387
|
`- On every ${JUDGMENT_TOOL} call, re-check all pending questions against the new evidence. Use question_updates to resolve questions naturally even when the current route did not focus them.`,
|
|
281
388
|
"- question_updates accepts only question IDs that were already listed as pending before the current route. Never use route_id or an ID for a new open_questions entry; use an empty array or omit question_updates when no pending question exists.",
|
|
282
389
|
"- A focused question always requires an explicit question_updates entry. Resolve or dismiss it with concrete basis, retain it as open/blocked, or explicitly replace the broad question while opening narrower children.",
|
|
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.",
|
|
390
|
+
"- Pending questions declare who can resolve them, what they block, and observable resolution criteria. Include context when detailed choices, examples, or constraints are needed to answer. For finite required user choices, also provide a choice-form response_spec with one field per decision; do not rely on parsing Markdown into controls. Never guess a user-owned answer or bypass a before-direct gate.",
|
|
284
391
|
"- Use before-direct only when the missing answer can change whether or what artifact mutation is valid. An agent-owned before-direct question must be resolvable through a non-direct skill route using observation or evidence execution; it must never require the mutation it blocks.",
|
|
285
392
|
"- Product files are changed with Pi implementation tools. Developer protocol tools only route and record judgments.",
|
|
286
393
|
];
|
|
@@ -294,7 +401,7 @@ function protocolPrompt(state: DeveloperState, availableSkillNames: string[]): s
|
|
|
294
401
|
|
|
295
402
|
if (state.implementationFramingRequired) {
|
|
296
403
|
lines.push(
|
|
297
|
-
"
|
|
404
|
+
"Direct gate: the latest resolved model exposed implementation work. Before direct mutation, route sketch for new feature shape or signal for existing-code structural movement; other judgments may still be routed first.",
|
|
298
405
|
);
|
|
299
406
|
}
|
|
300
407
|
if (state.verificationRequired) {
|
|
@@ -396,7 +503,12 @@ function expandedJudgment(event: JudgmentEvent, statusText: string, theme: Theme
|
|
|
396
503
|
),
|
|
397
504
|
),
|
|
398
505
|
...(event.questionUpdates ?? []).map((update) =>
|
|
399
|
-
detailLine(
|
|
506
|
+
detailLine(
|
|
507
|
+
theme,
|
|
508
|
+
`question ${update.status}`,
|
|
509
|
+
`${update.questionId} — ${update.result}`,
|
|
510
|
+
"accent",
|
|
511
|
+
),
|
|
400
512
|
),
|
|
401
513
|
];
|
|
402
514
|
if (evidence.length > 0) container.addChild(new Text(evidence.join("\n"), 0, 0));
|
|
@@ -465,8 +577,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
465
577
|
...state.pendingQuestions
|
|
466
578
|
.slice(0, 3)
|
|
467
579
|
.map((question) => `open · ${question.id} · ${compactLine(question.question)}`),
|
|
468
|
-
...(state.pendingQuestions.length > 3
|
|
469
|
-
|
|
580
|
+
...(state.pendingQuestions.length > 3
|
|
581
|
+
? [`open · +${state.pendingQuestions.length - 3} more`]
|
|
582
|
+
: []),
|
|
583
|
+
...(state.implementationFramingRequired
|
|
584
|
+
? ["gate · frame implementation before direct (sketch or signal)"]
|
|
585
|
+
: []),
|
|
470
586
|
...(state.verificationRequired ? ["next · verify changed artifacts before completion"] : []),
|
|
471
587
|
];
|
|
472
588
|
ctx.ui.setWidget("developer", lines, { placement: "belowEditor" });
|
|
@@ -504,7 +620,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
504
620
|
}),
|
|
505
621
|
),
|
|
506
622
|
open_question_id: Type.Optional(
|
|
507
|
-
Type.String({
|
|
623
|
+
Type.String({
|
|
624
|
+
maxLength: 512,
|
|
625
|
+
description: "Exact pending question ID when this route revisits one",
|
|
626
|
+
}),
|
|
508
627
|
),
|
|
509
628
|
};
|
|
510
629
|
const RouteAlternativeParam = Type.Object(
|
|
@@ -517,7 +636,8 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
517
636
|
reason: Type.String({
|
|
518
637
|
minLength: 1,
|
|
519
638
|
maxLength: MAX_EVIDENCE_CHARS,
|
|
520
|
-
description:
|
|
639
|
+
description:
|
|
640
|
+
"Why this skill would add no useful judgment before the proposed direct movement",
|
|
521
641
|
}),
|
|
522
642
|
},
|
|
523
643
|
{ additionalProperties: false },
|
|
@@ -533,16 +653,22 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
533
653
|
description: "Exact skill name from the current Available Developer skills list",
|
|
534
654
|
}),
|
|
535
655
|
},
|
|
536
|
-
{
|
|
656
|
+
{
|
|
657
|
+
additionalProperties: false,
|
|
658
|
+
description: "Route one question to a Developer skill",
|
|
659
|
+
},
|
|
537
660
|
),
|
|
538
661
|
Type.Object(
|
|
539
662
|
{
|
|
540
663
|
...SharedRouteParams,
|
|
541
|
-
owner: Type.Literal("direct", {
|
|
664
|
+
owner: Type.Literal("direct", {
|
|
665
|
+
description: "Use Pi implementation tools for an already-justified action",
|
|
666
|
+
}),
|
|
542
667
|
movement: Type.String({
|
|
543
668
|
minLength: 1,
|
|
544
669
|
maxLength: MAX_QUESTION_CHARS,
|
|
545
|
-
description:
|
|
670
|
+
description:
|
|
671
|
+
"One locally explainable behavioral or structural movement; not a multi-step implementation queue",
|
|
546
672
|
}),
|
|
547
673
|
stop_condition: Type.String({
|
|
548
674
|
minLength: 1,
|
|
@@ -563,11 +689,15 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
563
689
|
),
|
|
564
690
|
execution_profile: Type.Optional(
|
|
565
691
|
Type.Literal("behavior-preserving-structure", {
|
|
566
|
-
description:
|
|
692
|
+
description:
|
|
693
|
+
"Load the focused structural-mutation protocol; omit for ordinary direct action",
|
|
567
694
|
}),
|
|
568
695
|
),
|
|
569
696
|
},
|
|
570
|
-
{
|
|
697
|
+
{
|
|
698
|
+
additionalProperties: false,
|
|
699
|
+
description: "Route one already-justified direct action",
|
|
700
|
+
},
|
|
571
701
|
),
|
|
572
702
|
]);
|
|
573
703
|
|
|
@@ -587,10 +717,14 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
587
717
|
executionMode: "sequential",
|
|
588
718
|
renderShell: "self",
|
|
589
719
|
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
590
|
-
if (state.mode === "off")
|
|
720
|
+
if (state.mode === "off")
|
|
721
|
+
fail("Developer protocol is off. Run /develop on or /develop strict first.");
|
|
591
722
|
if (state.activeRoute || routeOpening) {
|
|
592
|
-
if (!state.activeRoute)
|
|
593
|
-
|
|
723
|
+
if (!state.activeRoute)
|
|
724
|
+
fail("Another Developer route is currently opening. Wait for it to finish.");
|
|
725
|
+
fail(
|
|
726
|
+
`Route ${state.activeRoute.routeId} is still active. Record its judgment before routing another question.`,
|
|
727
|
+
);
|
|
594
728
|
}
|
|
595
729
|
routeOpening = true;
|
|
596
730
|
|
|
@@ -601,7 +735,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
601
735
|
|
|
602
736
|
const explicitQuestionId = params.open_question_id?.trim() || undefined;
|
|
603
737
|
const targetQuestionId = inferredQuestionId(state, question, explicitQuestionId);
|
|
604
|
-
if (
|
|
738
|
+
if (
|
|
739
|
+
targetQuestionId &&
|
|
740
|
+
!state.pendingQuestions.some((item) => item.id === targetQuestionId)
|
|
741
|
+
) {
|
|
605
742
|
fail(`Unknown pending question ID: ${targetQuestionId}`);
|
|
606
743
|
}
|
|
607
744
|
|
|
@@ -616,7 +753,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
616
753
|
.join("; ")}. Resolve those questions first.`,
|
|
617
754
|
);
|
|
618
755
|
}
|
|
619
|
-
const knownEvidence = (params.known_evidence ?? [])
|
|
756
|
+
const knownEvidence = (params.known_evidence ?? [])
|
|
757
|
+
.map((item) => item.trim())
|
|
758
|
+
.filter(Boolean);
|
|
620
759
|
const consideredAlternatives: RouteAlternative[] =
|
|
621
760
|
owner === "direct" && "alternatives_considered" in params
|
|
622
761
|
? (params.alternatives_considered ?? []).map((alternative) => ({
|
|
@@ -629,12 +768,17 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
629
768
|
fail(`Considered alternative ${alternative.owner} is unavailable or disabled.`);
|
|
630
769
|
}
|
|
631
770
|
}
|
|
632
|
-
if (
|
|
771
|
+
if (
|
|
772
|
+
new Set(consideredAlternatives.map((alternative) => alternative.owner)).size !==
|
|
773
|
+
consideredAlternatives.length
|
|
774
|
+
) {
|
|
633
775
|
fail("Each considered alternative must name a different available Developer skill.");
|
|
634
776
|
}
|
|
635
777
|
if (owner === "direct" && state.lastJudgment?.owner === "direct") {
|
|
636
778
|
if (knownEvidence.length === 0) {
|
|
637
|
-
fail(
|
|
779
|
+
fail(
|
|
780
|
+
"A consecutive direct route must cite evidence from the previous direct landing in known_evidence.",
|
|
781
|
+
);
|
|
638
782
|
}
|
|
639
783
|
if (availableSkills.size > 0 && consideredAlternatives.length === 0) {
|
|
640
784
|
fail(
|
|
@@ -653,7 +797,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
653
797
|
}
|
|
654
798
|
const skill = owner === "direct" ? undefined : availableSkills.get(owner);
|
|
655
799
|
if (owner !== "direct" && !skill) {
|
|
656
|
-
fail(
|
|
800
|
+
fail(
|
|
801
|
+
`Developer skill ${owner} is unavailable or disabled in the current Pi resource configuration.`,
|
|
802
|
+
);
|
|
657
803
|
}
|
|
658
804
|
const requestedExecutionProfile =
|
|
659
805
|
"execution_profile" in params ? params.execution_profile : undefined;
|
|
@@ -682,15 +828,13 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
682
828
|
owner === "direct"
|
|
683
829
|
? {
|
|
684
830
|
movement: ("movement" in params ? params.movement : question).trim(),
|
|
685
|
-
stopCondition: (
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
: "Reach a green, pausable, reviewable stable landing."
|
|
831
|
+
stopCondition: ("stop_condition" in params
|
|
832
|
+
? params.stop_condition
|
|
833
|
+
: "Reach a green, pausable, reviewable stable landing."
|
|
689
834
|
).trim(),
|
|
690
|
-
verification: (
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
: "Run the narrowest relevant check and inspect the resulting diff or output."
|
|
835
|
+
verification: ("verification" in params
|
|
836
|
+
? params.verification
|
|
837
|
+
: "Run the narrowest relevant check and inspect the resulting diff or output."
|
|
694
838
|
).trim(),
|
|
695
839
|
}
|
|
696
840
|
: undefined;
|
|
@@ -713,8 +857,12 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
713
857
|
`Route ID: ${event.routeId}`,
|
|
714
858
|
`Question: ${event.question}`,
|
|
715
859
|
`Target: ${event.owner}`,
|
|
716
|
-
skill
|
|
717
|
-
|
|
860
|
+
skill
|
|
861
|
+
? `Skill location: ${skill.filePath}`
|
|
862
|
+
: "Skill location: direct action; no skill file",
|
|
863
|
+
executionProfile
|
|
864
|
+
? `Execution profile: ${executionProfile}`
|
|
865
|
+
: "Execution profile: skill judgment",
|
|
718
866
|
...(directStep
|
|
719
867
|
? [
|
|
720
868
|
`Movement: ${directStep.movement}`,
|
|
@@ -732,7 +880,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
732
880
|
.join(" | ")
|
|
733
881
|
: "none"
|
|
734
882
|
}`,
|
|
735
|
-
targetQuestionId
|
|
883
|
+
targetQuestionId
|
|
884
|
+
? `Revisits pending question: ${targetQuestionId}`
|
|
885
|
+
: "Revisits pending question: none",
|
|
736
886
|
`When this route has done its job, call ${JUDGMENT_TOOL} with this exact route ID.`,
|
|
737
887
|
"",
|
|
738
888
|
"---",
|
|
@@ -741,7 +891,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
741
891
|
].join("\n");
|
|
742
892
|
ensureSafeToolText(response, "Developer route result");
|
|
743
893
|
if (!canApplyDeveloperEvent(state, event)) {
|
|
744
|
-
fail(
|
|
894
|
+
fail(
|
|
895
|
+
"Developer machine guard rejected the route transition from the current branch state.",
|
|
896
|
+
);
|
|
745
897
|
}
|
|
746
898
|
|
|
747
899
|
state = applyDeveloperEvent(state, event);
|
|
@@ -755,7 +907,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
755
907
|
renderCall(args, theme, context) {
|
|
756
908
|
const owner = typeof args.owner === "string" && args.owner.length > 0 ? args.owner : "…";
|
|
757
909
|
const question =
|
|
758
|
-
typeof args.question === "string" && args.question.length > 0
|
|
910
|
+
typeof args.question === "string" && args.question.length > 0
|
|
911
|
+
? compactLine(args.question)
|
|
912
|
+
: "…";
|
|
759
913
|
return reusableText(
|
|
760
914
|
`${theme.fg("toolTitle", theme.bold(ROUTE_TOOL))} ${theme.fg("accent", owner)} ${theme.fg("muted", question)}`,
|
|
761
915
|
context.lastComponent,
|
|
@@ -763,7 +917,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
763
917
|
},
|
|
764
918
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
765
919
|
if (isPartial) {
|
|
766
|
-
return reusableText(
|
|
920
|
+
return reusableText(
|
|
921
|
+
theme.fg("warning", "routing development question…"),
|
|
922
|
+
context.lastComponent,
|
|
923
|
+
);
|
|
767
924
|
}
|
|
768
925
|
if (context.isError) {
|
|
769
926
|
return reusableText(
|
|
@@ -803,9 +960,66 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
803
960
|
},
|
|
804
961
|
});
|
|
805
962
|
|
|
963
|
+
const ChoiceResponseOptionParam = Type.Object(
|
|
964
|
+
{
|
|
965
|
+
value: Type.String({
|
|
966
|
+
minLength: 1,
|
|
967
|
+
maxLength: MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
968
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
969
|
+
}),
|
|
970
|
+
label: Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS }),
|
|
971
|
+
description: Type.Optional(Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS })),
|
|
972
|
+
detail_prompt: Type.Optional(
|
|
973
|
+
Type.String({
|
|
974
|
+
minLength: 1,
|
|
975
|
+
maxLength: MAX_RESPONSE_TEXT_CHARS,
|
|
976
|
+
description: "Required non-empty detail when this option is selected",
|
|
977
|
+
}),
|
|
978
|
+
),
|
|
979
|
+
},
|
|
980
|
+
{ additionalProperties: false },
|
|
981
|
+
);
|
|
982
|
+
const ChoiceResponseFieldParam = Type.Object(
|
|
983
|
+
{
|
|
984
|
+
id: Type.String({
|
|
985
|
+
minLength: 1,
|
|
986
|
+
maxLength: MAX_RESPONSE_IDENTIFIER_CHARS,
|
|
987
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
988
|
+
}),
|
|
989
|
+
prompt: Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS }),
|
|
990
|
+
description: Type.Optional(Type.String({ minLength: 1, maxLength: MAX_RESPONSE_TEXT_CHARS })),
|
|
991
|
+
options: Type.Array(ChoiceResponseOptionParam, {
|
|
992
|
+
minItems: 2,
|
|
993
|
+
maxItems: MAX_RESPONSE_OPTIONS,
|
|
994
|
+
}),
|
|
995
|
+
},
|
|
996
|
+
{ additionalProperties: false },
|
|
997
|
+
);
|
|
998
|
+
const ChoiceResponseSpecParam = Type.Object(
|
|
999
|
+
{
|
|
1000
|
+
kind: Type.Literal("choice-form"),
|
|
1001
|
+
fields: Type.Array(ChoiceResponseFieldParam, {
|
|
1002
|
+
minItems: 1,
|
|
1003
|
+
maxItems: MAX_RESPONSE_FIELDS,
|
|
1004
|
+
}),
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
additionalProperties: false,
|
|
1008
|
+
description:
|
|
1009
|
+
"Explicit controls for finite user-owned decisions; use one required single-choice field per decision instead of expecting the TUI to parse Markdown context",
|
|
1010
|
+
},
|
|
1011
|
+
);
|
|
806
1012
|
const OpenQuestionParam = Type.Object(
|
|
807
1013
|
{
|
|
808
1014
|
question: Type.String({ minLength: 1, maxLength: MAX_QUESTION_CHARS }),
|
|
1015
|
+
context: Type.Optional(
|
|
1016
|
+
Type.String({
|
|
1017
|
+
maxLength: MAX_QUESTION_CONTEXT_CHARS,
|
|
1018
|
+
description:
|
|
1019
|
+
"Detailed Markdown choices, examples, or constraints needed to resolve the question",
|
|
1020
|
+
}),
|
|
1021
|
+
),
|
|
1022
|
+
response_spec: Type.Optional(ChoiceResponseSpecParam),
|
|
809
1023
|
status: StringEnum(["open", "blocked"] as const, {
|
|
810
1024
|
description: "Whether the required evidence or answer is currently obtainable",
|
|
811
1025
|
}),
|
|
@@ -834,12 +1048,18 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
834
1048
|
}),
|
|
835
1049
|
status: StringEnum(["resolved", "not-applicable", "open", "blocked"] as const),
|
|
836
1050
|
result: Type.String({ minLength: 1, maxLength: MAX_RESULT_CHARS }),
|
|
837
|
-
basis: Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), {
|
|
1051
|
+
basis: Type.Array(Type.String({ maxLength: MAX_EVIDENCE_CHARS }), {
|
|
1052
|
+
maxItems: 20,
|
|
1053
|
+
}),
|
|
838
1054
|
},
|
|
839
1055
|
{ additionalProperties: false },
|
|
840
1056
|
);
|
|
841
1057
|
const JudgmentParams = Type.Object({
|
|
842
|
-
route_id: Type.String({
|
|
1058
|
+
route_id: Type.String({
|
|
1059
|
+
minLength: 1,
|
|
1060
|
+
maxLength: 512,
|
|
1061
|
+
description: `Exact route ID returned by ${ROUTE_TOOL}`,
|
|
1062
|
+
}),
|
|
843
1063
|
status: StringEnum(["resolved", "needs-evidence", "not-applicable", "blocked"] as const),
|
|
844
1064
|
result: Type.String({
|
|
845
1065
|
minLength: 1,
|
|
@@ -882,7 +1102,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
882
1102
|
promptGuidelines: [
|
|
883
1103
|
`Use ${JUDGMENT_TOOL} with the exact active Developer route ID.`,
|
|
884
1104
|
`Use ${JUDGMENT_TOOL} result as Markdown and preserve the routed skill's inspectable tables, diagrams, matrices, timelines, and code blocks instead of reducing them to prose.`,
|
|
885
|
-
`Use ${JUDGMENT_TOOL} open_questions with resolution_owner, gate, and resolution_criteria;
|
|
1105
|
+
`Use ${JUDGMENT_TOOL} open_questions with resolution_owner, gate, and resolution_criteria; for finite required user decisions, add a choice-form response_spec with one field per decision. Use question_updates whenever current evidence settles or changes any existing pending question.`,
|
|
886
1106
|
`Do not use ${JUDGMENT_TOOL} with resolved, not-applicable, or blocked status without at least one concrete basis.`,
|
|
887
1107
|
],
|
|
888
1108
|
parameters: JudgmentParams,
|
|
@@ -905,9 +1125,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
905
1125
|
},
|
|
906
1126
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
907
1127
|
if (state.mode === "off") fail("Developer protocol is off.");
|
|
908
|
-
|
|
909
|
-
if (
|
|
910
|
-
|
|
1128
|
+
const activeRoute = state.activeRoute;
|
|
1129
|
+
if (!activeRoute) fail("There is no active Developer route to close.");
|
|
1130
|
+
if (params.route_id !== activeRoute.routeId) {
|
|
1131
|
+
fail(`Route ID mismatch. Active route is ${activeRoute.routeId}.`);
|
|
911
1132
|
}
|
|
912
1133
|
|
|
913
1134
|
const basis = params.basis.map((item) => item.trim()).filter(Boolean);
|
|
@@ -917,17 +1138,23 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
917
1138
|
fail(`${params.status} judgments require at least one concrete basis.`);
|
|
918
1139
|
}
|
|
919
1140
|
|
|
920
|
-
const openedQuestions = buildOpenedQuestions(
|
|
1141
|
+
const openedQuestions = buildOpenedQuestions(
|
|
1142
|
+
params.open_questions ?? [],
|
|
1143
|
+
state,
|
|
1144
|
+
params.route_id,
|
|
1145
|
+
);
|
|
921
1146
|
if (
|
|
922
1147
|
availableSkills.size === 0 &&
|
|
923
1148
|
openedQuestions.some(
|
|
924
1149
|
(question) => question.resolutionOwner === "agent" && question.gate === "before-direct",
|
|
925
1150
|
)
|
|
926
1151
|
) {
|
|
927
|
-
fail(
|
|
1152
|
+
fail(
|
|
1153
|
+
"An agent-owned before-direct question requires an available non-direct skill resolution path.",
|
|
1154
|
+
);
|
|
928
1155
|
}
|
|
929
1156
|
const remainsOpen = params.status === "needs-evidence" || params.status === "blocked";
|
|
930
|
-
if (remainsOpen && !
|
|
1157
|
+
if (remainsOpen && !activeRoute.targetQuestionId && openedQuestions.length === 0) {
|
|
931
1158
|
fail(
|
|
932
1159
|
"A needs-evidence or blocked judgment for a new question must include at least one structured open_questions entry.",
|
|
933
1160
|
);
|
|
@@ -943,10 +1170,14 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
943
1170
|
if (openedQuestions.some((question) => updatedQuestionIds.has(question.id))) {
|
|
944
1171
|
fail("A question cannot be opened and updated in the same judgment.");
|
|
945
1172
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1173
|
+
const reopensCurrentQuestion = openedQuestions.some(
|
|
1174
|
+
(question) =>
|
|
1175
|
+
normalizedQuestion(question.question) === normalizedQuestion(activeRoute.question),
|
|
1176
|
+
);
|
|
1177
|
+
if (!remainsOpen && reopensCurrentQuestion) {
|
|
1178
|
+
fail("A resolved or not-applicable judgment cannot reopen its own question.");
|
|
948
1179
|
}
|
|
949
|
-
const targetQuestionId =
|
|
1180
|
+
const targetQuestionId = activeRoute.targetQuestionId;
|
|
950
1181
|
const targetUpdate = targetQuestionId
|
|
951
1182
|
? questionUpdates.find((update) => update.questionId === targetQuestionId)
|
|
952
1183
|
: undefined;
|
|
@@ -954,15 +1185,20 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
954
1185
|
fail(`Focused question ${targetQuestionId} requires an explicit question_updates entry.`);
|
|
955
1186
|
}
|
|
956
1187
|
if (targetUpdate) {
|
|
957
|
-
const closesTarget =
|
|
1188
|
+
const closesTarget =
|
|
1189
|
+
targetUpdate.status === "resolved" || targetUpdate.status === "not-applicable";
|
|
958
1190
|
if (!remainsOpen && !closesTarget) {
|
|
959
1191
|
fail("A resolved focused judgment must explicitly resolve or dismiss its question.");
|
|
960
1192
|
}
|
|
961
1193
|
if (remainsOpen && openedQuestions.length === 0 && closesTarget) {
|
|
962
|
-
fail(
|
|
1194
|
+
fail(
|
|
1195
|
+
"A focused question cannot close while its judgment reports no replacement evidence question.",
|
|
1196
|
+
);
|
|
963
1197
|
}
|
|
964
1198
|
if (remainsOpen && openedQuestions.length > 0 && !closesTarget) {
|
|
965
|
-
fail(
|
|
1199
|
+
fail(
|
|
1200
|
+
"Replacement questions require explicitly resolving or dismissing the broader focused question.",
|
|
1201
|
+
);
|
|
966
1202
|
}
|
|
967
1203
|
}
|
|
968
1204
|
|
|
@@ -970,8 +1206,8 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
970
1206
|
protocol: PROTOCOL,
|
|
971
1207
|
kind: "judgment",
|
|
972
1208
|
routeId: params.route_id,
|
|
973
|
-
question:
|
|
974
|
-
owner:
|
|
1209
|
+
question: activeRoute.question,
|
|
1210
|
+
owner: activeRoute.owner,
|
|
975
1211
|
status: params.status,
|
|
976
1212
|
result,
|
|
977
1213
|
basis,
|
|
@@ -981,7 +1217,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
981
1217
|
changedArtifacts: routesWithMutation.has(params.route_id),
|
|
982
1218
|
};
|
|
983
1219
|
if (!canApplyDeveloperEvent(state, event)) {
|
|
984
|
-
fail(
|
|
1220
|
+
fail(
|
|
1221
|
+
"Developer machine guard rejected the judgment transition from the current active route.",
|
|
1222
|
+
);
|
|
985
1223
|
}
|
|
986
1224
|
const nextState = applyDeveloperEvent(state, event);
|
|
987
1225
|
if (nextState.pendingQuestions.length > MAX_PENDING_QUESTIONS) {
|
|
@@ -990,11 +1228,31 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
990
1228
|
);
|
|
991
1229
|
}
|
|
992
1230
|
|
|
1231
|
+
const immediateQuestion =
|
|
1232
|
+
ctx.mode === "tui"
|
|
1233
|
+
? firstImmediateUserQuestion(state.pendingQuestions, event.openedQuestions)
|
|
1234
|
+
: undefined;
|
|
1235
|
+
let immediateDisposition: ImmediateQuestionDisposition | undefined;
|
|
1236
|
+
if (immediateQuestion) {
|
|
1237
|
+
try {
|
|
1238
|
+
immediateDisposition = await promptImmediateUserQuestion(ctx, immediateQuestion);
|
|
1239
|
+
} catch {
|
|
1240
|
+
ctx.ui.notify(
|
|
1241
|
+
"Could not open the Developer decision prompt; the question remains open.",
|
|
1242
|
+
"warning",
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
993
1247
|
const nextMessage = judgmentNextMessage(event, nextState);
|
|
994
1248
|
const resolvedQuestionCount = event.questionUpdates.filter(
|
|
995
1249
|
(update) => update.status === "resolved" || update.status === "not-applicable",
|
|
996
1250
|
).length;
|
|
997
|
-
const response =
|
|
1251
|
+
const response =
|
|
1252
|
+
`Recorded ${event.status} judgment for ${event.routeId}: ${event.result}\n` +
|
|
1253
|
+
`Question updates: ${resolvedQuestionCount} resolved, ${event.questionUpdates.length - resolvedQuestionCount} retained or blocked.\n` +
|
|
1254
|
+
nextMessage +
|
|
1255
|
+
immediateQuestionMessage(immediateQuestion, immediateDisposition);
|
|
998
1256
|
ensureSafeToolText(response, "Developer judgment result");
|
|
999
1257
|
|
|
1000
1258
|
routesWithMutation.delete(params.route_id);
|
|
@@ -1006,7 +1264,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1006
1264
|
renderCall(args, theme, context) {
|
|
1007
1265
|
const status = typeof args.status === "string" && args.status.length > 0 ? args.status : "…";
|
|
1008
1266
|
const result =
|
|
1009
|
-
typeof args.result === "string" && args.result.length > 0
|
|
1267
|
+
typeof args.result === "string" && args.result.length > 0
|
|
1268
|
+
? compactJudgmentResult(args.result)
|
|
1269
|
+
: "…";
|
|
1010
1270
|
const statusText =
|
|
1011
1271
|
status === "resolved"
|
|
1012
1272
|
? theme.fg("success", status)
|
|
@@ -1022,7 +1282,10 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1022
1282
|
},
|
|
1023
1283
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1024
1284
|
if (isPartial) {
|
|
1025
|
-
return reusableText(
|
|
1285
|
+
return reusableText(
|
|
1286
|
+
theme.fg("warning", "recording development judgment…"),
|
|
1287
|
+
context.lastComponent,
|
|
1288
|
+
);
|
|
1026
1289
|
}
|
|
1027
1290
|
if (context.isError) {
|
|
1028
1291
|
return reusableText(
|
|
@@ -1048,10 +1311,7 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1048
1311
|
|
|
1049
1312
|
const refreshAvailableSkills = (ctx: ExtensionCommandContext) => {
|
|
1050
1313
|
if (typeof ctx.getSystemPromptOptions !== "function") return;
|
|
1051
|
-
availableSkills = availablePackageSkills(
|
|
1052
|
-
ctx.getSystemPromptOptions().skills ?? [],
|
|
1053
|
-
skillsRoot,
|
|
1054
|
-
);
|
|
1314
|
+
availableSkills = availablePackageSkills(ctx.getSystemPromptOptions().skills ?? [], skillsRoot);
|
|
1055
1315
|
};
|
|
1056
1316
|
|
|
1057
1317
|
const statusMessage = () => {
|
|
@@ -1083,7 +1343,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1083
1343
|
? state.judgmentHistory
|
|
1084
1344
|
.slice(-10)
|
|
1085
1345
|
.map((judgment) => {
|
|
1086
|
-
const route = state.routeHistory.find(
|
|
1346
|
+
const route = state.routeHistory.find(
|
|
1347
|
+
(candidate) => candidate.routeId === judgment.routeId,
|
|
1348
|
+
);
|
|
1087
1349
|
const alternatives = (route?.consideredAlternatives ?? [])
|
|
1088
1350
|
.map((alternative) => `${alternative.owner}: ${alternative.reason}`)
|
|
1089
1351
|
.join("; ");
|
|
@@ -1120,11 +1382,44 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1120
1382
|
: null;
|
|
1121
1383
|
},
|
|
1122
1384
|
handler: async (args, ctx) => {
|
|
1385
|
+
const submitQuestionResolution = async (question: PendingQuestion): Promise<boolean> => {
|
|
1386
|
+
const request = await editQuestionResolutionRequest(ctx, question);
|
|
1387
|
+
if (request === undefined) return false;
|
|
1388
|
+
const focusEvent: FocusEvent = {
|
|
1389
|
+
protocol: PROTOCOL,
|
|
1390
|
+
kind: "focus",
|
|
1391
|
+
questionId: question.id,
|
|
1392
|
+
};
|
|
1393
|
+
pi.appendEntry(FOCUS_ENTRY, focusEvent);
|
|
1394
|
+
state = applyDeveloperEvent(state, focusEvent);
|
|
1395
|
+
refreshUI(ctx);
|
|
1396
|
+
ctx.ui.setEditorText("");
|
|
1397
|
+
ctx.ui.notify(
|
|
1398
|
+
"Question response submitted. It remains open until Developer records a resolved or not-applicable judgment.",
|
|
1399
|
+
"info",
|
|
1400
|
+
);
|
|
1401
|
+
if (ctx.isIdle()) pi.sendUserMessage(request);
|
|
1402
|
+
else pi.sendUserMessage(request, { deliverAs: "followUp" });
|
|
1403
|
+
return true;
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1123
1406
|
let action = args.trim();
|
|
1124
1407
|
if (!action && ctx.mode === "tui") {
|
|
1125
1408
|
refreshAvailableSkills(ctx);
|
|
1126
|
-
|
|
1127
|
-
|
|
1409
|
+
while (true) {
|
|
1410
|
+
const selection = await showDeveloperActionSelector(ctx, state);
|
|
1411
|
+
if (!selection) return;
|
|
1412
|
+
if (selection.kind === "question") {
|
|
1413
|
+
const question = state.pendingQuestions.find(
|
|
1414
|
+
(item) => item.id === selection.questionId,
|
|
1415
|
+
);
|
|
1416
|
+
if (!question) return;
|
|
1417
|
+
if (await submitQuestionResolution(question)) return;
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
action = selection.value;
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1128
1423
|
}
|
|
1129
1424
|
if (!action) action = "status";
|
|
1130
1425
|
if (action === "on" || action === "strict" || action === "off") {
|
|
@@ -1155,28 +1450,19 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1155
1450
|
return;
|
|
1156
1451
|
}
|
|
1157
1452
|
if (ctx.mode === "tui") {
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
questionId
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
refreshUI(ctx);
|
|
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" });
|
|
1179
|
-
return;
|
|
1453
|
+
const soleQuestion =
|
|
1454
|
+
state.pendingQuestions.length === 1 ? state.pendingQuestions[0] : undefined;
|
|
1455
|
+
if (soleQuestion) {
|
|
1456
|
+
await submitQuestionResolution(soleQuestion);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
while (true) {
|
|
1460
|
+
const questionId = await showPendingQuestionSelector(ctx, state.pendingQuestions);
|
|
1461
|
+
if (!questionId) return;
|
|
1462
|
+
const question = state.pendingQuestions.find((item) => item.id === questionId);
|
|
1463
|
+
if (!question) return;
|
|
1464
|
+
if (await submitQuestionResolution(question)) return;
|
|
1465
|
+
}
|
|
1180
1466
|
}
|
|
1181
1467
|
ctx.ui.notify(
|
|
1182
1468
|
state.pendingQuestions
|
|
@@ -1222,7 +1508,9 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1222
1508
|
pi.on("before_agent_start", (event) => {
|
|
1223
1509
|
availableSkills = availablePackageSkills(event.systemPromptOptions.skills ?? [], skillsRoot);
|
|
1224
1510
|
if (state.mode === "off") return;
|
|
1225
|
-
return {
|
|
1511
|
+
return {
|
|
1512
|
+
systemPrompt: event.systemPrompt + protocolPrompt(state, [...availableSkills.keys()]),
|
|
1513
|
+
};
|
|
1226
1514
|
});
|
|
1227
1515
|
|
|
1228
1516
|
pi.on("tool_call", (event) => {
|
|
@@ -1239,7 +1527,8 @@ export default async function developer(pi: ExtensionAPI) {
|
|
|
1239
1527
|
(question) => question.gate === "before-direct",
|
|
1240
1528
|
);
|
|
1241
1529
|
if (directBlockers.length > 0) {
|
|
1242
|
-
const blockedAction =
|
|
1530
|
+
const blockedAction =
|
|
1531
|
+
capability === "execute" ? "unrouted shell execution" : "artifact mutation";
|
|
1243
1532
|
return {
|
|
1244
1533
|
block: true,
|
|
1245
1534
|
reason: `Developer question gate blocks ${blockedAction} until resolved: ${directBlockers
|