@hobin/developer 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -39
- package/extensions/developer.ts +827 -123
- package/extensions/machine.ts +344 -0
- package/extensions/state.ts +242 -100
- package/extensions/tool-policy.ts +54 -25
- package/extensions/tui.ts +598 -155
- package/package.json +7 -2
- package/skills/abstraction-review/SKILL.md +6 -5
- package/skills/adversarial-eval/SKILL.md +7 -3
- package/skills/model/SKILL.md +13 -4
- package/skills/naming-judgment/SKILL.md +9 -3
- package/skills/schedule/SKILL.md +6 -3
- package/skills/signal/SKILL.md +6 -3
- package/skills/sketch/SKILL.md +29 -10
- package/skills/specify/SKILL.md +11 -2
- package/skills/verify/SKILL.md +10 -4
- package/skills/visualize/SKILL.md +5 -3
package/extensions/state.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { applyDeveloperEvent, developerSnapshot, initialState } from "./machine.ts";
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
applyDeveloperEvent,
|
|
5
|
+
canApplyDeveloperEvent,
|
|
6
|
+
developerMachine,
|
|
7
|
+
developerSnapshot,
|
|
8
|
+
initialState,
|
|
9
|
+
} from "./machine.ts";
|
|
10
|
+
|
|
11
|
+
export const PROTOCOL = "developer/v4" as const;
|
|
12
|
+
export const PREVIOUS_PROTOCOL = "developer/v3" as const;
|
|
13
|
+
export const OLDER_PROTOCOL = "developer/v2" as const;
|
|
3
14
|
export const LEGACY_PROTOCOL = "developer/v1" as const;
|
|
4
15
|
export const MODE_ENTRY = "developer.mode" as const;
|
|
5
16
|
export const FOCUS_ENTRY = "developer.question-focus" as const;
|
|
@@ -7,10 +18,17 @@ export const ROUTE_TOOL = "developer_route_question" as const;
|
|
|
7
18
|
export const JUDGMENT_TOOL = "developer_record_judgment" as const;
|
|
8
19
|
export const LEGACY_ROUTE_TOOL = "route_question" as const;
|
|
9
20
|
export const LEGACY_JUDGMENT_TOOL = "record_judgment" as const;
|
|
21
|
+
export const MAX_RESPONSE_FIELDS = 20;
|
|
22
|
+
export const MAX_RESPONSE_OPTIONS = 20;
|
|
23
|
+
export const MAX_RESPONSE_IDENTIFIER_CHARS = 64;
|
|
24
|
+
export const MAX_RESPONSE_TEXT_CHARS = 2_000;
|
|
10
25
|
|
|
11
26
|
export type DeveloperMode = "off" | "on" | "strict";
|
|
12
27
|
export type JudgmentStatus = "resolved" | "needs-evidence" | "not-applicable" | "blocked";
|
|
13
|
-
export type PendingQuestionStatus = "
|
|
28
|
+
export type PendingQuestionStatus = "open" | "blocked";
|
|
29
|
+
export type QuestionResolutionOwner = "agent" | "user" | "environment" | "unknown";
|
|
30
|
+
export type QuestionGate = "none" | "before-direct" | "before-completion";
|
|
31
|
+
export type QuestionUpdateStatus = "resolved" | "not-applicable" | "open" | "blocked";
|
|
14
32
|
export type DirectExecutionProfile = "ordinary" | "behavior-preserving-structure";
|
|
15
33
|
|
|
16
34
|
export interface ModeEvent {
|
|
@@ -31,6 +49,11 @@ export interface DirectStepContract {
|
|
|
31
49
|
verification: string;
|
|
32
50
|
}
|
|
33
51
|
|
|
52
|
+
export interface RouteAlternative {
|
|
53
|
+
owner: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
export interface RouteEvent {
|
|
35
58
|
protocol: typeof PROTOCOL;
|
|
36
59
|
kind: "route";
|
|
@@ -39,19 +62,51 @@ export interface RouteEvent {
|
|
|
39
62
|
owner: string;
|
|
40
63
|
reason: string;
|
|
41
64
|
knownEvidence: string[];
|
|
65
|
+
consideredAlternatives: RouteAlternative[];
|
|
42
66
|
targetQuestionId?: string;
|
|
43
67
|
methodLocation?: string;
|
|
44
68
|
executionProfile?: DirectExecutionProfile;
|
|
45
69
|
directStep?: DirectStepContract;
|
|
46
70
|
}
|
|
47
71
|
|
|
72
|
+
export interface ChoiceResponseOption {
|
|
73
|
+
value: string;
|
|
74
|
+
label: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
detailPrompt?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ChoiceResponseField {
|
|
80
|
+
id: string;
|
|
81
|
+
prompt: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
options: ChoiceResponseOption[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ChoiceResponseSpec {
|
|
87
|
+
kind: "choice-form";
|
|
88
|
+
fields: ChoiceResponseField[];
|
|
89
|
+
}
|
|
90
|
+
|
|
48
91
|
export interface PendingQuestion {
|
|
49
92
|
id: string;
|
|
50
93
|
question: string;
|
|
94
|
+
context?: string;
|
|
95
|
+
responseSpec?: ChoiceResponseSpec;
|
|
51
96
|
status: PendingQuestionStatus;
|
|
97
|
+
resolutionOwner: QuestionResolutionOwner;
|
|
98
|
+
gate: QuestionGate;
|
|
99
|
+
resolutionCriteria: string;
|
|
52
100
|
sourceRouteId: string;
|
|
53
101
|
}
|
|
54
102
|
|
|
103
|
+
export interface QuestionUpdate {
|
|
104
|
+
questionId: string;
|
|
105
|
+
status: QuestionUpdateStatus;
|
|
106
|
+
result: string;
|
|
107
|
+
basis: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
55
110
|
export interface JudgmentEvent {
|
|
56
111
|
protocol: typeof PROTOCOL;
|
|
57
112
|
kind: "judgment";
|
|
@@ -62,6 +117,7 @@ export interface JudgmentEvent {
|
|
|
62
117
|
result: string;
|
|
63
118
|
basis: string[];
|
|
64
119
|
openedQuestions: PendingQuestion[];
|
|
120
|
+
questionUpdates: QuestionUpdate[];
|
|
65
121
|
artifacts: string[];
|
|
66
122
|
changedArtifacts: boolean;
|
|
67
123
|
}
|
|
@@ -73,108 +129,40 @@ export interface DeveloperState {
|
|
|
73
129
|
activeRoute?: RouteEvent;
|
|
74
130
|
lastRoute?: RouteEvent;
|
|
75
131
|
lastJudgment?: JudgmentEvent;
|
|
132
|
+
routeHistory: RouteEvent[];
|
|
133
|
+
judgmentHistory: JudgmentEvent[];
|
|
76
134
|
pendingQuestions: PendingQuestion[];
|
|
77
135
|
focusedQuestionId?: string;
|
|
136
|
+
rerouteRequired: boolean;
|
|
78
137
|
implementationFramingRequired: boolean;
|
|
79
138
|
verificationRequired: boolean;
|
|
80
139
|
}
|
|
81
140
|
|
|
82
|
-
export
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
141
|
+
export type ProtocolState =
|
|
142
|
+
| "idle"
|
|
143
|
+
| "needs-judgment"
|
|
144
|
+
| "needs-evidence"
|
|
145
|
+
| "needs-answer"
|
|
146
|
+
| "needs-routing"
|
|
147
|
+
| "needs-verification"
|
|
148
|
+
| "blocked";
|
|
90
149
|
|
|
91
150
|
export function protocolState(state: DeveloperState): ProtocolState {
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
if (
|
|
95
|
-
|
|
151
|
+
const snapshot = developerSnapshot(state);
|
|
152
|
+
if (!snapshot.matches({ route: "idle" })) return "needs-judgment";
|
|
153
|
+
if (
|
|
154
|
+
snapshot.hasTag("blocks-direct") ||
|
|
155
|
+
state.pendingQuestions.some((question) => question.status === "blocked")
|
|
156
|
+
)
|
|
157
|
+
return "blocked";
|
|
158
|
+
if (state.pendingQuestions.some((question) => question.resolutionOwner === "user"))
|
|
159
|
+
return "needs-answer";
|
|
160
|
+
if (snapshot.matches({ questions: "open" })) return "needs-evidence";
|
|
161
|
+
if (snapshot.hasTag("reroute-required")) return "needs-routing";
|
|
162
|
+
if (snapshot.hasTag("verification-required")) return "needs-verification";
|
|
96
163
|
return "idle";
|
|
97
164
|
}
|
|
98
165
|
|
|
99
|
-
function upsertQuestion(questions: PendingQuestion[], next: PendingQuestion): PendingQuestion[] {
|
|
100
|
-
return [...questions.filter((question) => question.id !== next.id), next];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function applyDeveloperEvent(state: DeveloperState, event: DeveloperEvent): DeveloperState {
|
|
104
|
-
if (event.kind === "mode") {
|
|
105
|
-
if (event.mode === "off") return initialState();
|
|
106
|
-
return { ...state, mode: event.mode };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (event.kind === "focus") {
|
|
110
|
-
if (!state.pendingQuestions.some((question) => question.id === event.questionId)) return state;
|
|
111
|
-
return { ...state, focusedQuestionId: event.questionId };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (event.kind === "route") {
|
|
115
|
-
if (state.activeRoute) return state;
|
|
116
|
-
return {
|
|
117
|
-
...state,
|
|
118
|
-
activeRoute: event,
|
|
119
|
-
lastRoute: event,
|
|
120
|
-
focusedQuestionId:
|
|
121
|
-
event.targetQuestionId === state.focusedQuestionId ? undefined : state.focusedQuestionId,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!state.activeRoute || state.activeRoute.routeId !== event.routeId) return state;
|
|
126
|
-
|
|
127
|
-
const route = state.activeRoute;
|
|
128
|
-
const judgment = { ...event, question: route.question, owner: route.owner };
|
|
129
|
-
let pending = [...state.pendingQuestions];
|
|
130
|
-
const remainsOpen = event.status === "needs-evidence" || event.status === "blocked";
|
|
131
|
-
|
|
132
|
-
if (route.targetQuestionId) {
|
|
133
|
-
pending = pending.filter((question) => question.id !== route.targetQuestionId);
|
|
134
|
-
if (remainsOpen) {
|
|
135
|
-
pending = upsertQuestion(pending, {
|
|
136
|
-
id: route.targetQuestionId,
|
|
137
|
-
question: route.question,
|
|
138
|
-
status: event.status === "blocked" ? "blocked" : "needs-evidence",
|
|
139
|
-
sourceRouteId: route.routeId,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
} else if (remainsOpen) {
|
|
143
|
-
pending = upsertQuestion(pending, {
|
|
144
|
-
id: `question:${route.routeId}`,
|
|
145
|
-
question: route.question,
|
|
146
|
-
status: event.status === "blocked" ? "blocked" : "needs-evidence",
|
|
147
|
-
sourceRouteId: route.routeId,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
for (const question of event.openedQuestions) pending = upsertQuestion(pending, question);
|
|
152
|
-
|
|
153
|
-
const closesFramingGate =
|
|
154
|
-
(route.owner === "sketch" || route.owner === "signal") &&
|
|
155
|
-
(event.status === "resolved" || event.status === "not-applicable");
|
|
156
|
-
const opensFramingGate = route.owner === "model" && event.status === "resolved";
|
|
157
|
-
let implementationFramingRequired = state.implementationFramingRequired;
|
|
158
|
-
if (opensFramingGate) implementationFramingRequired = true;
|
|
159
|
-
if (closesFramingGate) implementationFramingRequired = false;
|
|
160
|
-
|
|
161
|
-
let verificationRequired = state.verificationRequired;
|
|
162
|
-
if (route.owner === "direct" && event.changedArtifacts) verificationRequired = true;
|
|
163
|
-
if (route.owner === "verify" && event.status === "resolved") verificationRequired = false;
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
...state,
|
|
167
|
-
activeRoute: undefined,
|
|
168
|
-
lastJudgment: judgment,
|
|
169
|
-
pendingQuestions: pending,
|
|
170
|
-
focusedQuestionId: pending.some((question) => question.id === state.focusedQuestionId)
|
|
171
|
-
? state.focusedQuestionId
|
|
172
|
-
: undefined,
|
|
173
|
-
implementationFramingRequired,
|
|
174
|
-
verificationRequired,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
166
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
179
167
|
return Boolean(value) && typeof value === "object";
|
|
180
168
|
}
|
|
@@ -188,7 +176,12 @@ function isMode(value: unknown): value is DeveloperMode {
|
|
|
188
176
|
}
|
|
189
177
|
|
|
190
178
|
function isJudgmentStatus(value: unknown): value is JudgmentStatus {
|
|
191
|
-
return
|
|
179
|
+
return (
|
|
180
|
+
value === "resolved" ||
|
|
181
|
+
value === "needs-evidence" ||
|
|
182
|
+
value === "not-applicable" ||
|
|
183
|
+
value === "blocked"
|
|
184
|
+
);
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
function isDirectExecutionProfile(value: unknown): value is DirectExecutionProfile {
|
|
@@ -211,31 +204,158 @@ function parseDirectStep(value: unknown): DirectStepContract | undefined {
|
|
|
211
204
|
};
|
|
212
205
|
}
|
|
213
206
|
|
|
207
|
+
function parseRouteAlternative(value: unknown): RouteAlternative | undefined {
|
|
208
|
+
if (!isObject(value) || typeof value.owner !== "string" || typeof value.reason !== "string") {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return { owner: value.owner, reason: value.reason };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isQuestionResolutionOwner(value: unknown): value is QuestionResolutionOwner {
|
|
215
|
+
return value === "agent" || value === "user" || value === "environment" || value === "unknown";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isQuestionGate(value: unknown): value is QuestionGate {
|
|
219
|
+
return value === "none" || value === "before-direct" || value === "before-completion";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isQuestionUpdateStatus(value: unknown): value is QuestionUpdateStatus {
|
|
223
|
+
return (
|
|
224
|
+
value === "resolved" || value === "not-applicable" || value === "open" || value === "blocked"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const RESPONSE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
229
|
+
|
|
230
|
+
function requiredResponseText(value: unknown, maxChars: number): string | undefined {
|
|
231
|
+
if (typeof value !== "string") return undefined;
|
|
232
|
+
const text = value.trim();
|
|
233
|
+
return text && text.length <= maxChars ? text : undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function optionalResponseText(value: unknown): string | undefined | null {
|
|
237
|
+
if (value === undefined) return undefined;
|
|
238
|
+
return requiredResponseText(value, MAX_RESPONSE_TEXT_CHARS) ?? null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseChoiceResponseOption(
|
|
242
|
+
value: unknown,
|
|
243
|
+
seenValues: Set<string>,
|
|
244
|
+
): ChoiceResponseOption | undefined {
|
|
245
|
+
if (!isObject(value)) return undefined;
|
|
246
|
+
const optionValue = requiredResponseText(value.value, MAX_RESPONSE_IDENTIFIER_CHARS);
|
|
247
|
+
const label = requiredResponseText(value.label, MAX_RESPONSE_TEXT_CHARS);
|
|
248
|
+
const description = optionalResponseText(value.description);
|
|
249
|
+
const detailPrompt = optionalResponseText(value.detailPrompt);
|
|
250
|
+
if (!optionValue || !RESPONSE_IDENTIFIER.test(optionValue)) return undefined;
|
|
251
|
+
if (seenValues.has(optionValue) || !label) return undefined;
|
|
252
|
+
if (description === null || detailPrompt === null) return undefined;
|
|
253
|
+
seenValues.add(optionValue);
|
|
254
|
+
return { value: optionValue, label, description, detailPrompt };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function parseChoiceResponseField(
|
|
258
|
+
value: unknown,
|
|
259
|
+
seenIds: Set<string>,
|
|
260
|
+
): ChoiceResponseField | undefined {
|
|
261
|
+
if (!isObject(value) || !Array.isArray(value.options)) return undefined;
|
|
262
|
+
const id = requiredResponseText(value.id, MAX_RESPONSE_IDENTIFIER_CHARS);
|
|
263
|
+
const prompt = requiredResponseText(value.prompt, MAX_RESPONSE_TEXT_CHARS);
|
|
264
|
+
const description = optionalResponseText(value.description);
|
|
265
|
+
if (!id || !RESPONSE_IDENTIFIER.test(id)) return undefined;
|
|
266
|
+
if (seenIds.has(id) || !prompt || description === null) return undefined;
|
|
267
|
+
if (value.options.length < 2 || value.options.length > MAX_RESPONSE_OPTIONS) return undefined;
|
|
268
|
+
|
|
269
|
+
seenIds.add(id);
|
|
270
|
+
const seenValues = new Set<string>();
|
|
271
|
+
const options: ChoiceResponseOption[] = [];
|
|
272
|
+
for (const rawOption of value.options) {
|
|
273
|
+
const option = parseChoiceResponseOption(rawOption, seenValues);
|
|
274
|
+
if (!option) return undefined;
|
|
275
|
+
options.push(option);
|
|
276
|
+
}
|
|
277
|
+
return { id, prompt, description, options };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function parseChoiceResponseSpec(value: unknown): ChoiceResponseSpec | undefined {
|
|
281
|
+
if (!isObject(value) || value.kind !== "choice-form" || !Array.isArray(value.fields)) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
if (value.fields.length === 0 || value.fields.length > MAX_RESPONSE_FIELDS) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const seenIds = new Set<string>();
|
|
288
|
+
const fields: ChoiceResponseField[] = [];
|
|
289
|
+
for (const rawField of value.fields) {
|
|
290
|
+
const field = parseChoiceResponseField(rawField, seenIds);
|
|
291
|
+
if (!field) return undefined;
|
|
292
|
+
fields.push(field);
|
|
293
|
+
}
|
|
294
|
+
return { kind: "choice-form", fields };
|
|
295
|
+
}
|
|
296
|
+
|
|
214
297
|
function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
|
|
215
298
|
if (!isObject(value)) return undefined;
|
|
216
299
|
if (
|
|
217
300
|
typeof value.id !== "string" ||
|
|
218
301
|
typeof value.question !== "string" ||
|
|
219
|
-
(value.status !== "
|
|
302
|
+
(value.status !== "open" && value.status !== "blocked" && value.status !== "needs-evidence") ||
|
|
220
303
|
typeof value.sourceRouteId !== "string"
|
|
221
304
|
) {
|
|
222
305
|
return undefined;
|
|
223
306
|
}
|
|
307
|
+
const legacyBlocked = value.status === "blocked";
|
|
308
|
+
let resolutionOwner: QuestionResolutionOwner = legacyBlocked ? "unknown" : "agent";
|
|
309
|
+
if (isQuestionResolutionOwner(value.resolutionOwner)) resolutionOwner = value.resolutionOwner;
|
|
310
|
+
let gate: QuestionGate = legacyBlocked ? "before-completion" : "none";
|
|
311
|
+
if (isQuestionGate(value.gate)) gate = value.gate;
|
|
312
|
+
const resolutionCriteria =
|
|
313
|
+
typeof value.resolutionCriteria === "string"
|
|
314
|
+
? value.resolutionCriteria
|
|
315
|
+
: `Obtain evidence that settles: ${value.question}`;
|
|
316
|
+
const context = typeof value.context === "string" ? value.context.trim() || undefined : undefined;
|
|
317
|
+
const responseSpec =
|
|
318
|
+
resolutionOwner === "user" ? parseChoiceResponseSpec(value.responseSpec) : undefined;
|
|
224
319
|
return {
|
|
225
320
|
id: value.id,
|
|
226
321
|
question: value.question,
|
|
227
|
-
|
|
322
|
+
context,
|
|
323
|
+
responseSpec,
|
|
324
|
+
status: legacyBlocked ? "blocked" : "open",
|
|
325
|
+
resolutionOwner,
|
|
326
|
+
gate,
|
|
327
|
+
resolutionCriteria,
|
|
228
328
|
sourceRouteId: value.sourceRouteId,
|
|
229
329
|
};
|
|
230
330
|
}
|
|
231
331
|
|
|
332
|
+
function parseQuestionUpdate(value: unknown): QuestionUpdate | undefined {
|
|
333
|
+
if (
|
|
334
|
+
!isObject(value) ||
|
|
335
|
+
typeof value.questionId !== "string" ||
|
|
336
|
+
!isQuestionUpdateStatus(value.status) ||
|
|
337
|
+
typeof value.result !== "string" ||
|
|
338
|
+
!isStringArray(value.basis)
|
|
339
|
+
) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
questionId: value.questionId,
|
|
344
|
+
status: value.status,
|
|
345
|
+
result: value.result,
|
|
346
|
+
basis: value.basis,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
232
350
|
export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefined {
|
|
233
351
|
if (!isObject(value)) return undefined;
|
|
234
352
|
if (
|
|
235
353
|
value.protocol !== PROTOCOL &&
|
|
236
354
|
value.protocol !== PREVIOUS_PROTOCOL &&
|
|
355
|
+
value.protocol !== OLDER_PROTOCOL &&
|
|
237
356
|
value.protocol !== LEGACY_PROTOCOL
|
|
238
|
-
)
|
|
357
|
+
)
|
|
358
|
+
return undefined;
|
|
239
359
|
|
|
240
360
|
if (value.kind === "mode") {
|
|
241
361
|
if (!isMode(value.mode)) return undefined;
|
|
@@ -243,7 +363,11 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
243
363
|
}
|
|
244
364
|
|
|
245
365
|
if (value.kind === "focus") {
|
|
246
|
-
if (
|
|
366
|
+
if (
|
|
367
|
+
(value.protocol !== PROTOCOL && value.protocol !== PREVIOUS_PROTOCOL) ||
|
|
368
|
+
typeof value.questionId !== "string"
|
|
369
|
+
)
|
|
370
|
+
return undefined;
|
|
247
371
|
return { protocol: PROTOCOL, kind: "focus", questionId: value.questionId };
|
|
248
372
|
}
|
|
249
373
|
|
|
@@ -254,6 +378,8 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
254
378
|
typeof value.owner !== "string" ||
|
|
255
379
|
typeof value.reason !== "string" ||
|
|
256
380
|
!isStringArray(value.knownEvidence) ||
|
|
381
|
+
(value.consideredAlternatives !== undefined &&
|
|
382
|
+
!Array.isArray(value.consideredAlternatives)) ||
|
|
257
383
|
(value.targetQuestionId !== undefined && typeof value.targetQuestionId !== "string") ||
|
|
258
384
|
(value.methodLocation !== undefined && typeof value.methodLocation !== "string") ||
|
|
259
385
|
(value.executionProfile !== undefined && !isDirectExecutionProfile(value.executionProfile)) ||
|
|
@@ -261,6 +387,10 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
261
387
|
) {
|
|
262
388
|
return undefined;
|
|
263
389
|
}
|
|
390
|
+
const consideredAlternatives = Array.isArray(value.consideredAlternatives)
|
|
391
|
+
? value.consideredAlternatives.map(parseRouteAlternative)
|
|
392
|
+
: [];
|
|
393
|
+
if (consideredAlternatives.some((alternative) => !alternative)) return undefined;
|
|
264
394
|
return {
|
|
265
395
|
protocol: PROTOCOL,
|
|
266
396
|
kind: "route",
|
|
@@ -269,6 +399,7 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
269
399
|
owner: value.owner,
|
|
270
400
|
reason: value.reason,
|
|
271
401
|
knownEvidence: value.knownEvidence,
|
|
402
|
+
consideredAlternatives: consideredAlternatives as RouteAlternative[],
|
|
272
403
|
targetQuestionId: value.targetQuestionId,
|
|
273
404
|
methodLocation: value.methodLocation,
|
|
274
405
|
executionProfile: value.executionProfile,
|
|
@@ -303,9 +434,13 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
303
434
|
openedQuestions: value.openQuestions.map((question, index) => ({
|
|
304
435
|
id: `question:${value.routeId}:legacy:${index + 1}`,
|
|
305
436
|
question,
|
|
306
|
-
status: "
|
|
437
|
+
status: "open",
|
|
438
|
+
resolutionOwner: "agent",
|
|
439
|
+
gate: "none",
|
|
440
|
+
resolutionCriteria: `Obtain evidence that settles: ${question}`,
|
|
307
441
|
sourceRouteId: String(value.routeId),
|
|
308
442
|
})),
|
|
443
|
+
questionUpdates: [],
|
|
309
444
|
artifacts: value.artifacts,
|
|
310
445
|
changedArtifacts: false,
|
|
311
446
|
};
|
|
@@ -314,6 +449,12 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
314
449
|
if (!Array.isArray(value.openedQuestions)) return undefined;
|
|
315
450
|
const openedQuestions = value.openedQuestions.map(parsePendingQuestion);
|
|
316
451
|
if (openedQuestions.some((question) => !question)) return undefined;
|
|
452
|
+
if (value.questionUpdates !== undefined && !Array.isArray(value.questionUpdates))
|
|
453
|
+
return undefined;
|
|
454
|
+
const questionUpdates = Array.isArray(value.questionUpdates)
|
|
455
|
+
? value.questionUpdates.map(parseQuestionUpdate)
|
|
456
|
+
: [];
|
|
457
|
+
if (questionUpdates.some((update) => !update)) return undefined;
|
|
317
458
|
return {
|
|
318
459
|
protocol: PROTOCOL,
|
|
319
460
|
kind: "judgment",
|
|
@@ -324,6 +465,7 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
324
465
|
result: value.result,
|
|
325
466
|
basis: value.basis,
|
|
326
467
|
openedQuestions: openedQuestions as PendingQuestion[],
|
|
468
|
+
questionUpdates: questionUpdates as QuestionUpdate[],
|
|
327
469
|
artifacts: value.artifacts,
|
|
328
470
|
changedArtifacts: typeof value.changedArtifacts === "boolean" ? value.changedArtifacts : false,
|
|
329
471
|
};
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { DeveloperMode } from "./state.ts";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export type ControlledToolCapability = "execute" | "mutate";
|
|
4
|
+
|
|
5
|
+
const CONTROLLED_BUILTIN_CAPABILITIES = new Map<string, ControlledToolCapability>([
|
|
6
|
+
["bash", "execute"],
|
|
7
|
+
["edit", "mutate"],
|
|
8
|
+
["write", "mutate"],
|
|
9
|
+
]);
|
|
4
10
|
|
|
5
11
|
export interface ToolMetadataLike {
|
|
6
12
|
name: string;
|
|
@@ -16,46 +22,69 @@ export interface ToolPolicyResult {
|
|
|
16
22
|
memory: ToolPolicyMemory;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
export interface ProtocolToolAccess {
|
|
26
|
+
canExecute: boolean;
|
|
27
|
+
canMutate: boolean;
|
|
28
|
+
hasBeforeDirectGate: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function builtinControlledToolCapabilities(
|
|
32
|
+
tools: ToolMetadataLike[],
|
|
33
|
+
): Map<string, ControlledToolCapability> {
|
|
34
|
+
const result = new Map<string, ControlledToolCapability>();
|
|
35
|
+
for (const tool of tools) {
|
|
36
|
+
if (tool.sourceInfo.source !== "builtin") continue;
|
|
37
|
+
const capability = CONTROLLED_BUILTIN_CAPABILITIES.get(tool.name);
|
|
38
|
+
if (capability) result.set(tool.name, capability);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isControlledToolAllowed(input: {
|
|
44
|
+
mode: DeveloperMode;
|
|
45
|
+
capability: ControlledToolCapability;
|
|
46
|
+
access: ProtocolToolAccess;
|
|
47
|
+
}): boolean {
|
|
48
|
+
if (input.mode === "off") return true;
|
|
49
|
+
|
|
50
|
+
if (input.access.hasBeforeDirectGate) {
|
|
51
|
+
if (input.capability === "mutate") return false;
|
|
52
|
+
return input.access.canExecute && !input.access.canMutate;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (input.mode === "on") return true;
|
|
56
|
+
if (input.capability === "execute") return input.access.canExecute;
|
|
57
|
+
return input.access.canMutate;
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
export function reconcileProtocolTools(input: {
|
|
28
61
|
activeTools: string[];
|
|
29
62
|
allTools: ToolMetadataLike[];
|
|
30
63
|
mode: DeveloperMode;
|
|
31
|
-
|
|
64
|
+
access: ProtocolToolAccess;
|
|
32
65
|
protocolTools: readonly string[];
|
|
33
66
|
memory: ToolPolicyMemory;
|
|
34
67
|
}): ToolPolicyResult {
|
|
35
68
|
const active = new Set(input.activeTools);
|
|
36
|
-
const
|
|
69
|
+
const controlledBuiltins = builtinControlledToolCapabilities(input.allTools);
|
|
37
70
|
const withheld = new Set(
|
|
38
|
-
[...input.memory.withheldBuiltins].filter((name) =>
|
|
71
|
+
[...input.memory.withheldBuiltins].filter((name) => controlledBuiltins.has(name)),
|
|
39
72
|
);
|
|
40
73
|
|
|
41
|
-
if (input.mode === "
|
|
74
|
+
if (input.mode === "off") {
|
|
75
|
+
for (const name of withheld) active.add(name);
|
|
76
|
+
withheld.clear();
|
|
77
|
+
for (const tool of input.protocolTools) active.delete(tool);
|
|
78
|
+
} else {
|
|
42
79
|
for (const tool of input.protocolTools) active.add(tool);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
for (const [name, capability] of controlledBuiltins) {
|
|
81
|
+
const allowed = isControlledToolAllowed({ mode: input.mode, capability, access: input.access });
|
|
82
|
+
if (allowed) {
|
|
83
|
+
if (withheld.delete(name)) active.add(name);
|
|
84
|
+
} else if (active.delete(name)) {
|
|
85
|
+
withheld.add(name);
|
|
49
86
|
}
|
|
50
87
|
}
|
|
51
|
-
} else {
|
|
52
|
-
for (const tool of withheld) active.add(tool);
|
|
53
|
-
withheld.clear();
|
|
54
|
-
|
|
55
|
-
for (const tool of input.protocolTools) {
|
|
56
|
-
if (input.mode === "on") active.add(tool);
|
|
57
|
-
else active.delete(tool);
|
|
58
|
-
}
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
return {
|