@hobin/developer 0.1.6 → 0.1.7

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.
@@ -1,511 +1,555 @@
1
- import { applyDeveloperEvent, developerSnapshot, initialState } from "./machine.ts";
1
+ import {
2
+ applyDeveloperEvent,
3
+ developerSnapshot,
4
+ initialState,
5
+ } from "./machine.ts";
2
6
 
3
7
  export {
4
- applyDeveloperEvent,
5
- canApplyDeveloperEvent,
6
- developerMachine,
7
- developerSnapshot,
8
- initialState,
8
+ applyDeveloperEvent,
9
+ canApplyDeveloperEvent,
10
+ developerMachine,
11
+ developerSnapshot,
12
+ initialState,
9
13
  } from "./machine.ts";
10
14
 
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;
14
- export const LEGACY_PROTOCOL = "developer/v1" as const;
15
- export const MODE_ENTRY = "developer.mode" as const;
15
+ export const PROTOCOL = "developer/v5" as const;
16
+ export const ACTIVATION_ENTRY = "developer.activation" as const;
16
17
  export const FOCUS_ENTRY = "developer.question-focus" as const;
17
18
  export const ROUTE_TOOL = "developer_route_question" as const;
18
19
  export const JUDGMENT_TOOL = "developer_record_judgment" as const;
19
- export const LEGACY_ROUTE_TOOL = "route_question" as const;
20
- export const LEGACY_JUDGMENT_TOOL = "record_judgment" as const;
21
20
  export const MAX_RESPONSE_FIELDS = 20;
22
21
  export const MAX_RESPONSE_OPTIONS = 20;
23
22
  export const MAX_RESPONSE_IDENTIFIER_CHARS = 64;
24
23
  export const MAX_RESPONSE_TEXT_CHARS = 2_000;
25
24
 
26
- export type DeveloperMode = "off" | "on" | "strict";
27
- export type JudgmentStatus = "resolved" | "needs-evidence" | "not-applicable" | "blocked";
25
+ export type JudgmentStatus =
26
+ | "resolved"
27
+ | "needs-evidence"
28
+ | "not-applicable"
29
+ | "blocked";
28
30
  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";
32
- export type DirectExecutionProfile = "ordinary" | "behavior-preserving-structure";
33
-
34
- export interface ModeEvent {
35
- protocol: typeof PROTOCOL;
36
- kind: "mode";
37
- mode: DeveloperMode;
31
+ export type QuestionResolutionOwner =
32
+ | "agent"
33
+ | "user"
34
+ | "environment"
35
+ | "unknown";
36
+ export type QuestionGate =
37
+ | "none"
38
+ | "before-implementation"
39
+ | "before-completion";
40
+ export type QuestionUpdateStatus =
41
+ | "resolved"
42
+ | "not-applicable"
43
+ | "open"
44
+ | "blocked";
45
+ export type ImplementationProfile =
46
+ | "ordinary"
47
+ | "behavior-preserving-structure";
48
+
49
+ export interface ActivationEvent {
50
+ protocol: typeof PROTOCOL;
51
+ kind: "activation";
52
+ enabled: boolean;
38
53
  }
39
54
 
40
55
  export interface FocusEvent {
41
- protocol: typeof PROTOCOL;
42
- kind: "focus";
43
- questionId: string;
56
+ protocol: typeof PROTOCOL;
57
+ kind: "focus";
58
+ questionId: string;
44
59
  }
45
60
 
46
- export interface DirectStepContract {
47
- movement: string;
48
- stopCondition: string;
49
- verification: string;
61
+ export interface ImplementationStepContract {
62
+ movement: string;
63
+ stopCondition: string;
64
+ verification: string;
50
65
  }
51
66
 
52
67
  export interface RouteAlternative {
53
- owner: string;
54
- reason: string;
68
+ target: string;
69
+ reason: string;
55
70
  }
56
71
 
57
72
  export interface RouteEvent {
58
- protocol: typeof PROTOCOL;
59
- kind: "route";
60
- routeId: string;
61
- question: string;
62
- owner: string;
63
- reason: string;
64
- knownEvidence: string[];
65
- consideredAlternatives: RouteAlternative[];
66
- targetQuestionId?: string;
67
- methodLocation?: string;
68
- executionProfile?: DirectExecutionProfile;
69
- directStep?: DirectStepContract;
73
+ protocol: typeof PROTOCOL;
74
+ kind: "route";
75
+ routeId: string;
76
+ question: string;
77
+ target: string;
78
+ reason: string;
79
+ knownEvidence: string[];
80
+ consideredAlternatives: RouteAlternative[];
81
+ targetQuestionId?: string;
82
+ methodLocation?: string;
83
+ executionProfile?: ImplementationProfile;
84
+ implementationStep?: ImplementationStepContract;
70
85
  }
71
86
 
72
87
  export interface ChoiceResponseOption {
73
- value: string;
74
- label: string;
75
- description?: string;
76
- detailPrompt?: string;
88
+ value: string;
89
+ label: string;
90
+ description?: string;
91
+ detailPrompt?: string;
77
92
  }
78
93
 
79
94
  export interface ChoiceResponseField {
80
- id: string;
81
- prompt: string;
82
- description?: string;
83
- options: ChoiceResponseOption[];
95
+ id: string;
96
+ prompt: string;
97
+ description?: string;
98
+ options: ChoiceResponseOption[];
84
99
  }
85
100
 
86
101
  export interface ChoiceResponseSpec {
87
- kind: "choice-form";
88
- fields: ChoiceResponseField[];
102
+ kind: "choice-form";
103
+ fields: ChoiceResponseField[];
89
104
  }
90
105
 
91
106
  export interface PendingQuestion {
92
- id: string;
93
- question: string;
94
- context?: string;
95
- responseSpec?: ChoiceResponseSpec;
96
- status: PendingQuestionStatus;
97
- resolutionOwner: QuestionResolutionOwner;
98
- gate: QuestionGate;
99
- resolutionCriteria: string;
100
- sourceRouteId: string;
107
+ id: string;
108
+ question: string;
109
+ context?: string;
110
+ responseSpec?: ChoiceResponseSpec;
111
+ status: PendingQuestionStatus;
112
+ resolutionOwner: QuestionResolutionOwner;
113
+ gate: QuestionGate;
114
+ resolutionCriteria: string;
115
+ sourceRouteId: string;
101
116
  }
102
117
 
103
118
  export interface QuestionUpdate {
104
- questionId: string;
105
- status: QuestionUpdateStatus;
106
- result: string;
107
- basis: string[];
119
+ questionId: string;
120
+ status: QuestionUpdateStatus;
121
+ result: string;
122
+ basis: string[];
108
123
  }
109
124
 
110
125
  export interface JudgmentEvent {
111
- protocol: typeof PROTOCOL;
112
- kind: "judgment";
113
- routeId: string;
114
- question: string;
115
- owner: string;
116
- status: JudgmentStatus;
117
- result: string;
118
- basis: string[];
119
- openedQuestions: PendingQuestion[];
120
- questionUpdates: QuestionUpdate[];
121
- artifacts: string[];
122
- changedArtifacts: boolean;
123
- }
124
-
125
- export type DeveloperEvent = ModeEvent | FocusEvent | RouteEvent | JudgmentEvent;
126
+ protocol: typeof PROTOCOL;
127
+ kind: "judgment";
128
+ routeId: string;
129
+ question: string;
130
+ target: string;
131
+ status: JudgmentStatus;
132
+ result: string;
133
+ basis: string[];
134
+ openedQuestions: PendingQuestion[];
135
+ questionUpdates: QuestionUpdate[];
136
+ artifacts: string[];
137
+ changedArtifacts: boolean;
138
+ }
139
+
140
+ export type DeveloperEvent =
141
+ | ActivationEvent
142
+ | FocusEvent
143
+ | RouteEvent
144
+ | JudgmentEvent;
126
145
 
127
146
  export interface DeveloperState {
128
- mode: DeveloperMode;
129
- activeRoute?: RouteEvent;
130
- lastRoute?: RouteEvent;
131
- lastJudgment?: JudgmentEvent;
132
- routeHistory: RouteEvent[];
133
- judgmentHistory: JudgmentEvent[];
134
- pendingQuestions: PendingQuestion[];
135
- focusedQuestionId?: string;
136
- rerouteRequired: boolean;
137
- implementationFramingRequired: boolean;
138
- verificationRequired: boolean;
147
+ enabled: boolean;
148
+ activeRoute?: RouteEvent;
149
+ lastRoute?: RouteEvent;
150
+ lastJudgment?: JudgmentEvent;
151
+ routeHistory: RouteEvent[];
152
+ judgmentHistory: JudgmentEvent[];
153
+ pendingQuestions: PendingQuestion[];
154
+ focusedQuestionId?: string;
155
+ rerouteRequired: boolean;
156
+ implementationFramingRequired: boolean;
157
+ verificationRequired: boolean;
139
158
  }
140
159
 
141
160
  export type ProtocolState =
142
- | "idle"
143
- | "needs-judgment"
144
- | "needs-evidence"
145
- | "needs-answer"
146
- | "needs-routing"
147
- | "needs-verification"
148
- | "blocked";
161
+ | "idle"
162
+ | "needs-judgment"
163
+ | "needs-evidence"
164
+ | "needs-answer"
165
+ | "needs-routing"
166
+ | "needs-verification"
167
+ | "blocked";
149
168
 
150
169
  export function protocolState(state: DeveloperState): ProtocolState {
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";
163
- return "idle";
170
+ const snapshot = developerSnapshot(state);
171
+ if (!snapshot.matches({ route: "idle" })) return "needs-judgment";
172
+ if (
173
+ snapshot.hasTag("blocks-implementation") ||
174
+ state.pendingQuestions.some((question) => question.status === "blocked")
175
+ )
176
+ return "blocked";
177
+ if (
178
+ state.pendingQuestions.some(
179
+ (question) => question.resolutionOwner === "user",
180
+ )
181
+ )
182
+ return "needs-answer";
183
+ if (snapshot.matches({ questions: "open" })) return "needs-evidence";
184
+ if (snapshot.hasTag("reroute-required")) return "needs-routing";
185
+ if (snapshot.hasTag("verification-required")) return "needs-verification";
186
+ return "idle";
164
187
  }
165
188
 
166
189
  function isObject(value: unknown): value is Record<string, unknown> {
167
- return Boolean(value) && typeof value === "object";
190
+ return Boolean(value) && typeof value === "object";
168
191
  }
169
192
 
170
193
  function isStringArray(value: unknown): value is string[] {
171
- return Array.isArray(value) && value.every((item) => typeof item === "string");
172
- }
173
-
174
- function isMode(value: unknown): value is DeveloperMode {
175
- return value === "off" || value === "on" || value === "strict";
194
+ return (
195
+ Array.isArray(value) && value.every((item) => typeof item === "string")
196
+ );
176
197
  }
177
198
 
178
199
  function isJudgmentStatus(value: unknown): value is JudgmentStatus {
179
- return (
180
- value === "resolved" ||
181
- value === "needs-evidence" ||
182
- value === "not-applicable" ||
183
- value === "blocked"
184
- );
185
- }
186
-
187
- function isDirectExecutionProfile(value: unknown): value is DirectExecutionProfile {
188
- return value === "ordinary" || value === "behavior-preserving-structure";
189
- }
190
-
191
- function parseDirectStep(value: unknown): DirectStepContract | undefined {
192
- if (!isObject(value)) return undefined;
193
- if (
194
- typeof value.movement !== "string" ||
195
- typeof value.stopCondition !== "string" ||
196
- typeof value.verification !== "string"
197
- ) {
198
- return undefined;
199
- }
200
- return {
201
- movement: value.movement,
202
- stopCondition: value.stopCondition,
203
- verification: value.verification,
204
- };
200
+ return (
201
+ value === "resolved" ||
202
+ value === "needs-evidence" ||
203
+ value === "not-applicable" ||
204
+ value === "blocked"
205
+ );
206
+ }
207
+
208
+ function isImplementationProfile(
209
+ value: unknown,
210
+ ): value is ImplementationProfile {
211
+ return value === "ordinary" || value === "behavior-preserving-structure";
212
+ }
213
+
214
+ function parseImplementationStep(
215
+ value: unknown,
216
+ ): ImplementationStepContract | undefined {
217
+ if (!isObject(value)) return undefined;
218
+ if (
219
+ typeof value.movement !== "string" ||
220
+ typeof value.stopCondition !== "string" ||
221
+ typeof value.verification !== "string"
222
+ ) {
223
+ return undefined;
224
+ }
225
+ return {
226
+ movement: value.movement,
227
+ stopCondition: value.stopCondition,
228
+ verification: value.verification,
229
+ };
205
230
  }
206
231
 
207
232
  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";
233
+ if (
234
+ !isObject(value) ||
235
+ typeof value.target !== "string" ||
236
+ typeof value.reason !== "string"
237
+ ) {
238
+ return undefined;
239
+ }
240
+ return { target: value.target, reason: value.reason };
241
+ }
242
+
243
+ function isQuestionResolutionOwner(
244
+ value: unknown,
245
+ ): value is QuestionResolutionOwner {
246
+ return (
247
+ value === "agent" ||
248
+ value === "user" ||
249
+ value === "environment" ||
250
+ value === "unknown"
251
+ );
216
252
  }
217
253
 
218
254
  function isQuestionGate(value: unknown): value is QuestionGate {
219
- return value === "none" || value === "before-direct" || value === "before-completion";
255
+ return (
256
+ value === "none" ||
257
+ value === "before-implementation" ||
258
+ value === "before-completion"
259
+ );
220
260
  }
221
261
 
222
262
  function isQuestionUpdateStatus(value: unknown): value is QuestionUpdateStatus {
223
- return (
224
- value === "resolved" || value === "not-applicable" || value === "open" || value === "blocked"
225
- );
263
+ return (
264
+ value === "resolved" ||
265
+ value === "not-applicable" ||
266
+ value === "open" ||
267
+ value === "blocked"
268
+ );
226
269
  }
227
270
 
228
271
  const RESPONSE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
229
272
 
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;
273
+ function requiredResponseText(
274
+ value: unknown,
275
+ maxChars: number,
276
+ ): string | undefined {
277
+ if (typeof value !== "string") return undefined;
278
+ const text = value.trim();
279
+ return text && text.length <= maxChars ? text : undefined;
234
280
  }
235
281
 
236
282
  function optionalResponseText(value: unknown): string | undefined | null {
237
- if (value === undefined) return undefined;
238
- return requiredResponseText(value, MAX_RESPONSE_TEXT_CHARS) ?? null;
283
+ if (value === undefined) return undefined;
284
+ return requiredResponseText(value, MAX_RESPONSE_TEXT_CHARS) ?? null;
239
285
  }
240
286
 
241
287
  function parseChoiceResponseOption(
242
- value: unknown,
243
- seenValues: Set<string>,
288
+ value: unknown,
289
+ seenValues: Set<string>,
244
290
  ): 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 };
291
+ if (!isObject(value)) return undefined;
292
+ const optionValue = requiredResponseText(
293
+ value.value,
294
+ MAX_RESPONSE_IDENTIFIER_CHARS,
295
+ );
296
+ const label = requiredResponseText(value.label, MAX_RESPONSE_TEXT_CHARS);
297
+ const description = optionalResponseText(value.description);
298
+ const detailPrompt = optionalResponseText(value.detailPrompt);
299
+ if (!optionValue || !RESPONSE_IDENTIFIER.test(optionValue)) return undefined;
300
+ if (seenValues.has(optionValue) || !label) return undefined;
301
+ if (description === null || detailPrompt === null) return undefined;
302
+ seenValues.add(optionValue);
303
+ return { value: optionValue, label, description, detailPrompt };
255
304
  }
256
305
 
257
306
  function parseChoiceResponseField(
258
- value: unknown,
259
- seenIds: Set<string>,
307
+ value: unknown,
308
+ seenIds: Set<string>,
260
309
  ): 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 };
310
+ if (!isObject(value) || !Array.isArray(value.options)) return undefined;
311
+ const id = requiredResponseText(value.id, MAX_RESPONSE_IDENTIFIER_CHARS);
312
+ const prompt = requiredResponseText(value.prompt, MAX_RESPONSE_TEXT_CHARS);
313
+ const description = optionalResponseText(value.description);
314
+ if (!id || !RESPONSE_IDENTIFIER.test(id)) return undefined;
315
+ if (seenIds.has(id) || !prompt || description === null) return undefined;
316
+ if (value.options.length < 2 || value.options.length > MAX_RESPONSE_OPTIONS)
317
+ return undefined;
318
+
319
+ seenIds.add(id);
320
+ const seenValues = new Set<string>();
321
+ const options: ChoiceResponseOption[] = [];
322
+ for (const rawOption of value.options) {
323
+ const option = parseChoiceResponseOption(rawOption, seenValues);
324
+ if (!option) return undefined;
325
+ options.push(option);
326
+ }
327
+ return { id, prompt, description, options };
328
+ }
329
+
330
+ export function parseChoiceResponseSpec(
331
+ value: unknown,
332
+ ): ChoiceResponseSpec | undefined {
333
+ if (
334
+ !isObject(value) ||
335
+ value.kind !== "choice-form" ||
336
+ !Array.isArray(value.fields)
337
+ ) {
338
+ return undefined;
339
+ }
340
+ if (value.fields.length === 0 || value.fields.length > MAX_RESPONSE_FIELDS) {
341
+ return undefined;
342
+ }
343
+ const seenIds = new Set<string>();
344
+ const fields: ChoiceResponseField[] = [];
345
+ for (const rawField of value.fields) {
346
+ const field = parseChoiceResponseField(rawField, seenIds);
347
+ if (!field) return undefined;
348
+ fields.push(field);
349
+ }
350
+ return { kind: "choice-form", fields };
295
351
  }
296
352
 
297
353
  function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
298
- if (!isObject(value)) return undefined;
299
- if (
300
- typeof value.id !== "string" ||
301
- typeof value.question !== "string" ||
302
- (value.status !== "open" && value.status !== "blocked" && value.status !== "needs-evidence") ||
303
- typeof value.sourceRouteId !== "string"
304
- ) {
305
- return undefined;
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;
319
- return {
320
- id: value.id,
321
- question: value.question,
322
- context,
323
- responseSpec,
324
- status: legacyBlocked ? "blocked" : "open",
325
- resolutionOwner,
326
- gate,
327
- resolutionCriteria,
328
- sourceRouteId: value.sourceRouteId,
329
- };
354
+ if (!isObject(value)) return undefined;
355
+ if (
356
+ typeof value.id !== "string" ||
357
+ typeof value.question !== "string" ||
358
+ (value.status !== "open" &&
359
+ value.status !== "blocked" &&
360
+ value.status !== "needs-evidence") ||
361
+ typeof value.sourceRouteId !== "string"
362
+ ) {
363
+ return undefined;
364
+ }
365
+ const legacyBlocked = value.status === "blocked";
366
+ let resolutionOwner: QuestionResolutionOwner = legacyBlocked
367
+ ? "unknown"
368
+ : "agent";
369
+ if (isQuestionResolutionOwner(value.resolutionOwner))
370
+ resolutionOwner = value.resolutionOwner;
371
+ let gate: QuestionGate = legacyBlocked ? "before-completion" : "none";
372
+ if (isQuestionGate(value.gate)) gate = value.gate;
373
+ const resolutionCriteria =
374
+ typeof value.resolutionCriteria === "string"
375
+ ? value.resolutionCriteria
376
+ : `Obtain evidence that settles: ${value.question}`;
377
+ const context =
378
+ typeof value.context === "string"
379
+ ? value.context.trim() || undefined
380
+ : undefined;
381
+ const responseSpec =
382
+ resolutionOwner === "user"
383
+ ? parseChoiceResponseSpec(value.responseSpec)
384
+ : undefined;
385
+ return {
386
+ id: value.id,
387
+ question: value.question,
388
+ context,
389
+ responseSpec,
390
+ status: legacyBlocked ? "blocked" : "open",
391
+ resolutionOwner,
392
+ gate,
393
+ resolutionCriteria,
394
+ sourceRouteId: value.sourceRouteId,
395
+ };
330
396
  }
331
397
 
332
398
  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
-
350
- export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefined {
351
- if (!isObject(value)) return undefined;
352
- if (
353
- value.protocol !== PROTOCOL &&
354
- value.protocol !== PREVIOUS_PROTOCOL &&
355
- value.protocol !== OLDER_PROTOCOL &&
356
- value.protocol !== LEGACY_PROTOCOL
357
- )
358
- return undefined;
359
-
360
- if (value.kind === "mode") {
361
- if (!isMode(value.mode)) return undefined;
362
- return { protocol: PROTOCOL, kind: "mode", mode: value.mode };
363
- }
364
-
365
- if (value.kind === "focus") {
366
- if (
367
- (value.protocol !== PROTOCOL && value.protocol !== PREVIOUS_PROTOCOL) ||
368
- typeof value.questionId !== "string"
369
- )
370
- return undefined;
371
- return { protocol: PROTOCOL, kind: "focus", questionId: value.questionId };
372
- }
373
-
374
- if (value.kind === "route") {
375
- if (
376
- typeof value.routeId !== "string" ||
377
- typeof value.question !== "string" ||
378
- typeof value.owner !== "string" ||
379
- typeof value.reason !== "string" ||
380
- !isStringArray(value.knownEvidence) ||
381
- (value.consideredAlternatives !== undefined &&
382
- !Array.isArray(value.consideredAlternatives)) ||
383
- (value.targetQuestionId !== undefined && typeof value.targetQuestionId !== "string") ||
384
- (value.methodLocation !== undefined && typeof value.methodLocation !== "string") ||
385
- (value.executionProfile !== undefined && !isDirectExecutionProfile(value.executionProfile)) ||
386
- (value.directStep !== undefined && !parseDirectStep(value.directStep))
387
- ) {
388
- return undefined;
389
- }
390
- const consideredAlternatives = Array.isArray(value.consideredAlternatives)
391
- ? value.consideredAlternatives.map(parseRouteAlternative)
392
- : [];
393
- if (consideredAlternatives.some((alternative) => !alternative)) return undefined;
394
- return {
395
- protocol: PROTOCOL,
396
- kind: "route",
397
- routeId: value.routeId,
398
- question: value.question,
399
- owner: value.owner,
400
- reason: value.reason,
401
- knownEvidence: value.knownEvidence,
402
- consideredAlternatives: consideredAlternatives as RouteAlternative[],
403
- targetQuestionId: value.targetQuestionId,
404
- methodLocation: value.methodLocation,
405
- executionProfile: value.executionProfile,
406
- directStep: value.directStep === undefined ? undefined : parseDirectStep(value.directStep),
407
- };
408
- }
409
-
410
- if (value.kind !== "judgment") return undefined;
411
- if (
412
- typeof value.routeId !== "string" ||
413
- typeof value.question !== "string" ||
414
- typeof value.owner !== "string" ||
415
- !isJudgmentStatus(value.status) ||
416
- typeof value.result !== "string" ||
417
- !isStringArray(value.basis) ||
418
- !isStringArray(value.artifacts)
419
- ) {
420
- return undefined;
421
- }
422
-
423
- if (value.protocol === LEGACY_PROTOCOL) {
424
- if (!isStringArray(value.openQuestions)) return undefined;
425
- return {
426
- protocol: PROTOCOL,
427
- kind: "judgment",
428
- routeId: value.routeId,
429
- question: value.question,
430
- owner: value.owner,
431
- status: value.status,
432
- result: value.result,
433
- basis: value.basis,
434
- openedQuestions: value.openQuestions.map((question, index) => ({
435
- id: `question:${value.routeId}:legacy:${index + 1}`,
436
- question,
437
- status: "open",
438
- resolutionOwner: "agent",
439
- gate: "none",
440
- resolutionCriteria: `Obtain evidence that settles: ${question}`,
441
- sourceRouteId: String(value.routeId),
442
- })),
443
- questionUpdates: [],
444
- artifacts: value.artifacts,
445
- changedArtifacts: false,
446
- };
447
- }
448
-
449
- if (!Array.isArray(value.openedQuestions)) return undefined;
450
- const openedQuestions = value.openedQuestions.map(parsePendingQuestion);
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;
458
- return {
459
- protocol: PROTOCOL,
460
- kind: "judgment",
461
- routeId: value.routeId,
462
- question: value.question,
463
- owner: value.owner,
464
- status: value.status,
465
- result: value.result,
466
- basis: value.basis,
467
- openedQuestions: openedQuestions as PendingQuestion[],
468
- questionUpdates: questionUpdates as QuestionUpdate[],
469
- artifacts: value.artifacts,
470
- changedArtifacts: typeof value.changedArtifacts === "boolean" ? value.changedArtifacts : false,
471
- };
399
+ if (
400
+ !isObject(value) ||
401
+ typeof value.questionId !== "string" ||
402
+ !isQuestionUpdateStatus(value.status) ||
403
+ typeof value.result !== "string" ||
404
+ !isStringArray(value.basis)
405
+ ) {
406
+ return undefined;
407
+ }
408
+ return {
409
+ questionId: value.questionId,
410
+ status: value.status,
411
+ result: value.result,
412
+ basis: value.basis,
413
+ };
414
+ }
415
+
416
+ export function normalizeDeveloperEvent(
417
+ value: unknown,
418
+ ): DeveloperEvent | undefined {
419
+ if (!isObject(value) || value.protocol !== PROTOCOL) return undefined;
420
+
421
+ if (value.kind === "activation") {
422
+ if (typeof value.enabled !== "boolean") return undefined;
423
+ return { protocol: PROTOCOL, kind: "activation", enabled: value.enabled };
424
+ }
425
+
426
+ if (value.kind === "focus") {
427
+ if (typeof value.questionId !== "string") return undefined;
428
+ return { protocol: PROTOCOL, kind: "focus", questionId: value.questionId };
429
+ }
430
+
431
+ if (value.kind === "route") {
432
+ if (
433
+ typeof value.routeId !== "string" ||
434
+ typeof value.question !== "string" ||
435
+ typeof value.target !== "string" ||
436
+ typeof value.reason !== "string" ||
437
+ !isStringArray(value.knownEvidence) ||
438
+ (value.consideredAlternatives !== undefined &&
439
+ !Array.isArray(value.consideredAlternatives)) ||
440
+ (value.targetQuestionId !== undefined &&
441
+ typeof value.targetQuestionId !== "string") ||
442
+ (value.methodLocation !== undefined &&
443
+ typeof value.methodLocation !== "string") ||
444
+ (value.executionProfile !== undefined &&
445
+ !isImplementationProfile(value.executionProfile)) ||
446
+ (value.implementationStep !== undefined &&
447
+ !parseImplementationStep(value.implementationStep))
448
+ ) {
449
+ return undefined;
450
+ }
451
+ const consideredAlternatives = Array.isArray(value.consideredAlternatives)
452
+ ? value.consideredAlternatives.map(parseRouteAlternative)
453
+ : [];
454
+ if (consideredAlternatives.some((alternative) => !alternative))
455
+ return undefined;
456
+ return {
457
+ protocol: PROTOCOL,
458
+ kind: "route",
459
+ routeId: value.routeId,
460
+ question: value.question,
461
+ target: value.target,
462
+ reason: value.reason,
463
+ knownEvidence: value.knownEvidence,
464
+ consideredAlternatives: consideredAlternatives as RouteAlternative[],
465
+ targetQuestionId: value.targetQuestionId,
466
+ methodLocation: value.methodLocation,
467
+ executionProfile: value.executionProfile,
468
+ implementationStep:
469
+ value.implementationStep === undefined
470
+ ? undefined
471
+ : parseImplementationStep(value.implementationStep),
472
+ };
473
+ }
474
+
475
+ if (value.kind !== "judgment") return undefined;
476
+ if (
477
+ typeof value.routeId !== "string" ||
478
+ typeof value.question !== "string" ||
479
+ typeof value.target !== "string" ||
480
+ !isJudgmentStatus(value.status) ||
481
+ typeof value.result !== "string" ||
482
+ !isStringArray(value.basis) ||
483
+ !isStringArray(value.artifacts) ||
484
+ !Array.isArray(value.openedQuestions)
485
+ ) {
486
+ return undefined;
487
+ }
488
+
489
+ const openedQuestions = value.openedQuestions.map(parsePendingQuestion);
490
+ if (openedQuestions.some((question) => !question)) return undefined;
491
+ if (
492
+ value.questionUpdates !== undefined &&
493
+ !Array.isArray(value.questionUpdates)
494
+ )
495
+ return undefined;
496
+ const questionUpdates = Array.isArray(value.questionUpdates)
497
+ ? value.questionUpdates.map(parseQuestionUpdate)
498
+ : [];
499
+ if (questionUpdates.some((update) => !update)) return undefined;
500
+ return {
501
+ protocol: PROTOCOL,
502
+ kind: "judgment",
503
+ routeId: value.routeId,
504
+ question: value.question,
505
+ target: value.target,
506
+ status: value.status,
507
+ result: value.result,
508
+ basis: value.basis,
509
+ openedQuestions: openedQuestions as PendingQuestion[],
510
+ questionUpdates: questionUpdates as QuestionUpdate[],
511
+ artifacts: value.artifacts,
512
+ changedArtifacts:
513
+ typeof value.changedArtifacts === "boolean"
514
+ ? value.changedArtifacts
515
+ : false,
516
+ };
472
517
  }
473
518
 
474
519
  interface BranchEntryLike {
475
- type: string;
476
- customType?: string;
477
- data?: unknown;
478
- message?: { role?: string; toolName?: string; details?: unknown };
479
- }
480
-
481
- const DEVELOPER_TOOL_NAMES = new Set<string>([
482
- ROUTE_TOOL,
483
- JUDGMENT_TOOL,
484
- LEGACY_ROUTE_TOOL,
485
- LEGACY_JUDGMENT_TOOL,
486
- ]);
487
-
488
- export function eventFromBranchEntry(entry: BranchEntryLike): DeveloperEvent | undefined {
489
- if (
490
- entry.type === "custom" &&
491
- (entry.customType === MODE_ENTRY || entry.customType === FOCUS_ENTRY)
492
- ) {
493
- return normalizeDeveloperEvent(entry.data);
494
- }
495
- if (
496
- entry.type === "message" &&
497
- entry.message?.role === "toolResult" &&
498
- entry.message.toolName &&
499
- DEVELOPER_TOOL_NAMES.has(entry.message.toolName)
500
- ) {
501
- return normalizeDeveloperEvent(entry.message.details);
502
- }
503
- return undefined;
504
- }
505
-
506
- export function reconstructState(entries: ReadonlyArray<BranchEntryLike>): DeveloperState {
507
- return entries.reduce((state, entry) => {
508
- const event = eventFromBranchEntry(entry);
509
- return event ? applyDeveloperEvent(state, event) : state;
510
- }, initialState());
520
+ type: string;
521
+ customType?: string;
522
+ data?: unknown;
523
+ message?: { role?: string; toolName?: string; details?: unknown };
524
+ }
525
+
526
+ const DEVELOPER_TOOL_NAMES = new Set<string>([ROUTE_TOOL, JUDGMENT_TOOL]);
527
+
528
+ export function eventFromBranchEntry(
529
+ entry: BranchEntryLike,
530
+ ): DeveloperEvent | undefined {
531
+ if (
532
+ entry.type === "custom" &&
533
+ (entry.customType === ACTIVATION_ENTRY || entry.customType === FOCUS_ENTRY)
534
+ ) {
535
+ return normalizeDeveloperEvent(entry.data);
536
+ }
537
+ if (
538
+ entry.type === "message" &&
539
+ entry.message?.role === "toolResult" &&
540
+ entry.message.toolName &&
541
+ DEVELOPER_TOOL_NAMES.has(entry.message.toolName)
542
+ ) {
543
+ return normalizeDeveloperEvent(entry.message.details);
544
+ }
545
+ return undefined;
546
+ }
547
+
548
+ export function reconstructState(
549
+ entries: ReadonlyArray<BranchEntryLike>,
550
+ ): DeveloperState {
551
+ return entries.reduce((state, entry) => {
552
+ const event = eventFromBranchEntry(entry);
553
+ return event ? applyDeveloperEvent(state, event) : state;
554
+ }, initialState());
511
555
  }