@gajae-code/coding-agent 0.11.8 → 0.11.10

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/types/cli.d.ts +1 -0
  3. package/dist/types/config/model-profiles.d.ts +2 -3
  4. package/dist/types/config/model-registry.d.ts +2 -0
  5. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  6. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  7. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  8. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  9. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  10. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  11. package/dist/types/gjc-runtime/state-writer.d.ts +45 -0
  12. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  13. package/dist/types/modes/components/welcome.d.ts +2 -0
  14. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  15. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  16. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
  17. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  18. package/dist/types/sdk/bus/index.d.ts +8 -1
  19. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  20. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  21. package/dist/types/sdk/bus/telegram-daemon.d.ts +74 -5
  22. package/dist/types/sdk/session.d.ts +7 -0
  23. package/dist/types/session/agent-session.d.ts +2 -0
  24. package/dist/types/session/internal/managed-session-scope.d.ts +13 -3
  25. package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
  26. package/dist/types/session/session-manager.d.ts +5 -1
  27. package/dist/types/session/session-storage.d.ts +13 -0
  28. package/dist/types/skill-state/active-state.d.ts +4 -0
  29. package/dist/types/task/provider-retry-status.d.ts +13 -0
  30. package/dist/types/task/types.d.ts +3 -0
  31. package/dist/types/tools/index.d.ts +3 -1
  32. package/package.json +8 -8
  33. package/scripts/generate-sdk-operation-inventory.ts +1 -0
  34. package/src/cli/mcp-cli.ts +4 -21
  35. package/src/cli/notify-cli.ts +1 -0
  36. package/src/cli.ts +32 -0
  37. package/src/commands/deep-interview.ts +9 -4
  38. package/src/commit/agentic/index.ts +3 -3
  39. package/src/commit/map-reduce/index.ts +2 -2
  40. package/src/config/model-profiles.ts +9 -10
  41. package/src/config/model-registry.ts +4 -2
  42. package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
  43. package/src/eval/js/tool-bridge.ts +1 -1
  44. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  45. package/src/gjc-runtime/deep-interview-draft.ts +843 -0
  46. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  47. package/src/gjc-runtime/deep-interview-recorder.ts +310 -227
  48. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  49. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  50. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  51. package/src/gjc-runtime/ralplan-runtime.ts +3 -1
  52. package/src/gjc-runtime/state-writer.ts +166 -2
  53. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  54. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  55. package/src/hooks/skill-state.ts +23 -1
  56. package/src/internal-urls/docs-index.generated.ts +6 -5
  57. package/src/modes/components/welcome.ts +10 -2
  58. package/src/modes/controllers/selector-controller.ts +38 -8
  59. package/src/modes/interactive-mode.ts +29 -9
  60. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  61. package/src/prompts/agents/architect.md +8 -1
  62. package/src/prompts/agents/critic.md +8 -1
  63. package/src/prompts/agents/planner.md +8 -1
  64. package/src/prompts/system/system-prompt.md +1 -0
  65. package/src/runtime-mcp/json-rpc.ts +7 -4
  66. package/src/runtime-mcp/redaction.ts +30 -0
  67. package/src/sdk/broker/identity.ts +22 -12
  68. package/src/sdk/broker/session-index.ts +11 -1
  69. package/src/sdk/bus/chat-daemon-control.ts +5 -3
  70. package/src/sdk/bus/daemon-paths.ts +2 -0
  71. package/src/sdk/bus/index.ts +60 -5
  72. package/src/sdk/bus/notification-service.ts +19 -5
  73. package/src/sdk/bus/operator-runtime.ts +31 -5
  74. package/src/sdk/bus/telegram-cli.ts +6 -13
  75. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  76. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  77. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  78. package/src/sdk/bus/telegram-daemon.ts +685 -120
  79. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  80. package/src/sdk/session.ts +12 -0
  81. package/src/session/agent-session.ts +140 -10
  82. package/src/session/internal/managed-session-scope.ts +339 -228
  83. package/src/session/internal/managed-session-storage.ts +17 -4
  84. package/src/session/session-manager.ts +96 -33
  85. package/src/session/session-storage.ts +23 -0
  86. package/src/skill-state/active-state.ts +10 -0
  87. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  88. package/src/task/executor.ts +154 -44
  89. package/src/task/provider-retry-status.ts +88 -0
  90. package/src/task/render.ts +45 -26
  91. package/src/task/types.ts +3 -0
  92. package/src/tools/ask.ts +1 -0
  93. package/src/tools/bash-allowed-prefixes.ts +45 -1
  94. package/src/tools/index.ts +3 -1
  95. package/src/tools/subagent-render.ts +74 -16
  96. package/src/tools/subagent.ts +2 -0
  97. package/vendor/insane-search/MANIFEST.json +4 -1
  98. package/vendor/insane-search/engine/bias_check.py +5 -0
  99. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  100. package/vendor/insane-search/engine/transport.py +13 -2
  101. package/vendor/insane-search/engine/validators.py +4 -1
@@ -0,0 +1,1674 @@
1
+ import { createHash } from "node:crypto";
2
+ import { computeAmbiguityFloor, scoreToUnits } from "./deep-interview-ambiguity";
3
+ import type { DeepInterviewDraftKind } from "./deep-interview-payload";
4
+ import { runDeepInterviewPostCommitEffects } from "./deep-interview-recorder";
5
+ import {
6
+ answerHash,
7
+ applyDeepInterviewRoundResultV1,
8
+ canonicalDeepInterviewJson,
9
+ type DeepInterviewFactOperation,
10
+ type DeepInterviewResolution,
11
+ type DeepInterviewRoundResultV1,
12
+ deepInterviewAnswerIdentityEqual,
13
+ deriveRoundKey,
14
+ questionHash,
15
+ validateDeepInterviewV1Envelope,
16
+ } from "./deep-interview-state";
17
+ import { modeStatePath } from "./session-layout";
18
+ import {
19
+ GuardedWorkflowEnvelopeError,
20
+ readExistingStateForMutation,
21
+ transformGuardedWorkflowEnvelopeAtomic,
22
+ verifyWorkflowEnvelopeReceiptValue,
23
+ workflowEnvelopeContentSha256,
24
+ } from "./state-writer";
25
+
26
+ export interface DeepInterviewRepairResult {
27
+ status: number;
28
+ stdout?: string;
29
+ stderr?: string;
30
+ }
31
+
32
+ export const DEEP_INTERVIEW_REPAIR_VERBS = [
33
+ "initialize-context",
34
+ "confirm-topology",
35
+ "record-answer",
36
+ "apply-round-result",
37
+ "inspect",
38
+ "sanity-check",
39
+ ] as const;
40
+
41
+ export type DeepInterviewRepairVerb = (typeof DEEP_INTERVIEW_REPAIR_VERBS)[number];
42
+
43
+ const VERBS = new Set<string>(DEEP_INTERVIEW_REPAIR_VERBS);
44
+
45
+ export function isDeepInterviewRepairVerb(value: string): value is DeepInterviewRepairVerb {
46
+ return VERBS.has(value);
47
+ }
48
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
49
+ const MAX_BYTES = 64 * 1024;
50
+
51
+ class RepairError extends Error {
52
+ constructor(
53
+ readonly code: string,
54
+ readonly status = 2,
55
+ ) {
56
+ super(code);
57
+ }
58
+ }
59
+ function issue(code: string) {
60
+ return { code, message: code };
61
+ }
62
+ function errorResult(error: unknown): DeepInterviewRepairResult {
63
+ const typed =
64
+ error instanceof RepairError
65
+ ? error
66
+ : error instanceof GuardedWorkflowEnvelopeError
67
+ ? new RepairError(
68
+ error.code,
69
+ error.code === "DI_ROUND_RESULT_CONFLICT" ||
70
+ error.code === "DI_TOPOLOGY_CONFLICT" ||
71
+ error.code === "DI_ANSWER_CONFLICT" ||
72
+ error.code === "DI_SHELL_CONFLICT"
73
+ ? 4
74
+ : 3,
75
+ )
76
+ : new RepairError("DI_INTERNAL_ERROR", 3);
77
+ return { status: typed.status, stderr: `${JSON.stringify({ ok: false, issue: issue(typed.code) })}\n` };
78
+ }
79
+ function strictJson(value: string): unknown {
80
+ let index = 0;
81
+ const whitespace = () => {
82
+ while (/\s/.test(value[index] ?? "")) index++;
83
+ };
84
+ const string = (): string => {
85
+ const start = index++;
86
+ while (index < value.length) {
87
+ if (value[index] === "\\") {
88
+ index += 2;
89
+ continue;
90
+ }
91
+ if (value[index++] === '"') return JSON.parse(value.slice(start, index)) as string;
92
+ }
93
+ throw new Error("invalid JSON");
94
+ };
95
+ const node = (): void => {
96
+ whitespace();
97
+ if (value[index] === "{") {
98
+ index++;
99
+ const keys = new Set<string>();
100
+ whitespace();
101
+ if (value[index] === "}") {
102
+ index++;
103
+ return;
104
+ }
105
+ for (;;) {
106
+ whitespace();
107
+ if (value[index] !== '"') throw new Error("invalid JSON");
108
+ const key = string();
109
+ if (keys.has(key)) throw new Error("duplicate JSON key");
110
+ keys.add(key);
111
+ whitespace();
112
+ if (value[index++] !== ":") throw new Error("invalid JSON");
113
+ node();
114
+ whitespace();
115
+ if (value[index] === "}") {
116
+ index++;
117
+ return;
118
+ }
119
+ if (value[index++] !== ",") throw new Error("invalid JSON");
120
+ }
121
+ }
122
+ if (value[index] === "[") {
123
+ index++;
124
+ whitespace();
125
+ if (value[index] === "]") {
126
+ index++;
127
+ return;
128
+ }
129
+ for (;;) {
130
+ node();
131
+ whitespace();
132
+ if (value[index] === "]") {
133
+ index++;
134
+ return;
135
+ }
136
+ if (value[index++] !== ",") throw new Error("invalid JSON");
137
+ }
138
+ }
139
+ if (value[index] === '"') {
140
+ string();
141
+ return;
142
+ }
143
+ const primitive = /^(?:true|false|null|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)/.exec(
144
+ value.slice(index),
145
+ );
146
+ if (!primitive) throw new Error("invalid JSON");
147
+ index += primitive[0].length;
148
+ };
149
+ node();
150
+ whitespace();
151
+ if (index !== value.length) throw new Error("invalid JSON");
152
+ return JSON.parse(value) as unknown;
153
+ }
154
+
155
+ function structuredObject(value: unknown, code: string, max: number): Record<string, unknown> {
156
+ if (typeof value === "string") return objectJson(value, code, max);
157
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
158
+ throw new RepairError(code);
159
+ const encoded = JSON.stringify(value);
160
+ if (typeof encoded !== "string" || Buffer.byteLength(encoded) > max) throw new RepairError(code);
161
+ return value as Record<string, unknown>;
162
+ }
163
+
164
+ function objectJson(value: string | undefined, code: string, max: number): Record<string, unknown> {
165
+ if (value === undefined || Buffer.byteLength(value) > MAX_BYTES || Buffer.byteLength(value) > max)
166
+ throw new RepairError(code);
167
+ try {
168
+ const parsed: unknown = strictJson(value);
169
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
170
+ return parsed as Record<string, unknown>;
171
+ } catch {
172
+ throw new RepairError(code);
173
+ }
174
+ }
175
+ function stringJson(value: string | undefined): string {
176
+ if (value === undefined || Buffer.byteLength(value) > 2048) throw new RepairError("DI_INVALID_QUESTION_JSON");
177
+ try {
178
+ const parsed: unknown = strictJson(value);
179
+ if (typeof parsed !== "string" || !parsed || Buffer.byteLength(parsed) > 2048) throw new Error();
180
+ return parsed;
181
+ } catch {
182
+ throw new RepairError("DI_INVALID_QUESTION_JSON");
183
+ }
184
+ }
185
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[], code: string): void {
186
+ if (Object.keys(value).some(key => !keys.includes(key))) throw new RepairError(code);
187
+ }
188
+
189
+ const DIMENSIONS = ["goal", "constraints", "criteria", "context"] as const;
190
+ function resolutionJson(value: unknown): DeepInterviewResolution {
191
+ switch (value) {
192
+ case "auto_research_accepted":
193
+ case "auto_answer":
194
+ case "direct":
195
+ case "refined":
196
+ case "cited_confirmation":
197
+ return value;
198
+ default:
199
+ throw new RepairError("DI_INVALID_RESULT_JSON");
200
+ }
201
+ }
202
+
203
+ function text(value: unknown, max: number): string {
204
+ if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value) > max)
205
+ throw new RepairError("DI_INVALID_RESULT_JSON");
206
+ return value;
207
+ }
208
+ function id(value: unknown): string {
209
+ if (typeof value !== "string" || !ID.test(value)) throw new RepairError("DI_INVALID_RESULT_JSON");
210
+ return value;
211
+ }
212
+ function record(value: unknown, code = "DI_INVALID_RESULT_JSON"): Record<string, unknown> {
213
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new RepairError(code);
214
+ return value as Record<string, unknown>;
215
+ }
216
+ function list(value: unknown, max = 64, budget?: { remaining: number }): unknown[] {
217
+ if (!Array.isArray(value) || value.length > max) throw new RepairError("DI_INVALID_RESULT_JSON");
218
+ if (budget !== undefined) {
219
+ budget.remaining -= value.length;
220
+ if (budget.remaining < 0) throw new RepairError("DI_INVALID_RESULT_JSON");
221
+ }
222
+ return value;
223
+ }
224
+ function dimension(
225
+ value: unknown,
226
+ allowed: readonly ("goal" | "constraints" | "criteria" | "context")[] = DIMENSIONS,
227
+ ): "goal" | "constraints" | "criteria" | "context" {
228
+ if (typeof value !== "string" || !allowed.includes(value as (typeof DIMENSIONS)[number]))
229
+ throw new RepairError("DI_INVALID_RESULT_JSON");
230
+ return value as "goal" | "constraints" | "criteria" | "context";
231
+ }
232
+ function score(value: unknown): number {
233
+ if (typeof value !== "number") throw new RepairError("DI_INVALID_RESULT_JSON");
234
+ try {
235
+ scoreToUnits(value);
236
+ return value;
237
+ } catch {
238
+ throw new RepairError("DI_INVALID_RESULT_JSON");
239
+ }
240
+ }
241
+ function scores(
242
+ value: unknown,
243
+ dimensions: readonly ("goal" | "constraints" | "criteria" | "context")[],
244
+ ): Record<"goal" | "constraints" | "criteria" | "context", number> {
245
+ const input = record(value);
246
+ exactKeys(input, dimensions, "DI_INVALID_RESULT_JSON");
247
+ if (Object.keys(input).length !== dimensions.length) throw new RepairError("DI_INVALID_RESULT_JSON");
248
+ return Object.fromEntries(dimensions.map(dimension => [dimension, score(input[dimension])])) as Record<
249
+ "goal" | "constraints" | "criteria" | "context",
250
+ number
251
+ >;
252
+ }
253
+ function optionalText(value: unknown, max: number): string | undefined {
254
+ return value === undefined ? undefined : text(value, max);
255
+ }
256
+ export function decodeDeepInterviewAnswerJson(value: unknown): {
257
+ selected_options: string[];
258
+ custom_input: string | null;
259
+ } {
260
+ const answer = structuredObject(value, "DI_INVALID_ANSWER_JSON", 8 * 1024);
261
+ exactKeys(answer, ["selected_options", "custom_input"], "DI_INVALID_ANSWER_JSON");
262
+ if (!Array.isArray(answer.selected_options) || answer.selected_options.length > 64)
263
+ throw new RepairError("DI_INVALID_ANSWER_JSON");
264
+ const selected_options = answer.selected_options.map(option => {
265
+ if (typeof option !== "string" || !option || Buffer.byteLength(option) > 2048)
266
+ throw new RepairError("DI_INVALID_ANSWER_JSON");
267
+ return option;
268
+ });
269
+ if (
270
+ answer.custom_input !== null &&
271
+ (typeof answer.custom_input !== "string" || Buffer.byteLength(answer.custom_input) > 4096)
272
+ )
273
+ throw new RepairError("DI_INVALID_ANSWER_JSON");
274
+ return { selected_options, custom_input: answer.custom_input };
275
+ }
276
+ export function decodeDeepInterviewRoundResultJson(
277
+ value: unknown,
278
+ state: Record<string, unknown>,
279
+ ): DeepInterviewRoundResultV1 {
280
+ const result = structuredObject(value, "DI_INVALID_RESULT_JSON", 48 * 1024);
281
+ const dimensions =
282
+ state.type === "brownfield"
283
+ ? DIMENSIONS
284
+ : state.type === "greenfield"
285
+ ? DIMENSIONS.slice(0, -1)
286
+ : (() => {
287
+ throw new RepairError("DI_STATE_SCHEMA_INVALID", 3);
288
+ })();
289
+ const budget = { remaining: 64 };
290
+ exactKeys(
291
+ result,
292
+ ["global_scores", "component_updates", "targeting", "triggers", "fact_ops", "ontology", "bookkeeping"],
293
+ "DI_INVALID_RESULT_JSON",
294
+ );
295
+ const component_updates =
296
+ result.component_updates === undefined
297
+ ? undefined
298
+ : list(result.component_updates, 12, budget).map(value => {
299
+ const update = record(value);
300
+ exactKeys(update, ["component_id", "scores"], "DI_INVALID_RESULT_JSON");
301
+ return { component_id: id(update.component_id), scores: scores(update.scores, dimensions) };
302
+ });
303
+ if (
304
+ component_updates &&
305
+ new Set(component_updates.map(update => update.component_id)).size !== component_updates.length
306
+ )
307
+ throw new RepairError("DI_INVALID_RESULT_JSON");
308
+ const targeting =
309
+ result.targeting === undefined
310
+ ? undefined
311
+ : (() => {
312
+ const input = record(result.targeting);
313
+ exactKeys(
314
+ input,
315
+ [
316
+ "target_component_id",
317
+ "target_dimension",
318
+ "weakest_component_id",
319
+ "weakest_dimension",
320
+ "last_targeted_component_id",
321
+ ],
322
+ "DI_INVALID_RESULT_JSON",
323
+ );
324
+ if (input.last_targeted_component_id !== null) id(input.last_targeted_component_id);
325
+ return {
326
+ target_component_id: id(input.target_component_id),
327
+ target_dimension: dimension(input.target_dimension, dimensions),
328
+ weakest_component_id: id(input.weakest_component_id),
329
+ weakest_dimension: dimension(input.weakest_dimension, dimensions),
330
+ last_targeted_component_id: input.last_targeted_component_id as string | null,
331
+ };
332
+ })();
333
+ const fact_ops =
334
+ result.fact_ops === undefined
335
+ ? undefined
336
+ : list(result.fact_ops, 32, budget).map(value => {
337
+ const input = record(value);
338
+ const op = input.op;
339
+ if (op === "add") {
340
+ exactKeys(
341
+ input,
342
+ ["op", "id", "statement", "component", "dimension", "evidence"],
343
+ "DI_INVALID_RESULT_JSON",
344
+ );
345
+ const operation: DeepInterviewFactOperation = {
346
+ op: "add",
347
+ id: id(input.id),
348
+ statement: text(input.statement, 4096),
349
+ component: optionalText(input.component, 128),
350
+ dimension: input.dimension === undefined ? undefined : dimension(input.dimension, dimensions),
351
+ evidence: optionalText(input.evidence, 4096),
352
+ };
353
+ return operation;
354
+ }
355
+ if (op === "dispute") {
356
+ exactKeys(input, ["op", "id"], "DI_INVALID_RESULT_JSON");
357
+ const operation: DeepInterviewFactOperation = { op: "dispute", id: id(input.id) };
358
+ return operation;
359
+ }
360
+ if (op === "supersede") {
361
+ exactKeys(input, ["op", "id", "target_id"], "DI_INVALID_RESULT_JSON");
362
+ const operation: DeepInterviewFactOperation = {
363
+ op: "supersede",
364
+ id: id(input.id),
365
+ target_id: id(input.target_id),
366
+ };
367
+ return operation;
368
+ }
369
+ throw new RepairError("DI_INVALID_RESULT_JSON");
370
+ });
371
+ const factIds = new Set([
372
+ ...(Array.isArray(state.established_facts)
373
+ ? state.established_facts.flatMap(fact => {
374
+ const candidate =
375
+ fact && typeof fact === "object" && !Array.isArray(fact)
376
+ ? (fact as Record<string, unknown>).id
377
+ : undefined;
378
+ return typeof candidate === "string" && ID.test(candidate) ? [candidate] : [];
379
+ })
380
+ : []),
381
+ ]);
382
+ for (const operation of fact_ops ?? []) {
383
+ if (operation.op === "add") {
384
+ if (factIds.has(operation.id)) throw new RepairError("DI_INVALID_RESULT_JSON");
385
+ factIds.add(operation.id);
386
+ } else if (
387
+ !factIds.has(operation.id) ||
388
+ (operation.op === "supersede" && (operation.target_id === undefined || !factIds.has(operation.target_id)))
389
+ ) {
390
+ throw new RepairError("DI_INVALID_RESULT_JSON");
391
+ }
392
+ }
393
+ const triggers =
394
+ result.triggers === undefined
395
+ ? undefined
396
+ : list(result.triggers, 16, budget).map(value => {
397
+ const input = record(value);
398
+ const status = input.status;
399
+ exactKeys(
400
+ input,
401
+ ["kind", "name", "status", "component", "dimension", "evidence", "contradictedFactId", "rationale"],
402
+ "DI_INVALID_RESULT_JSON",
403
+ );
404
+ if (
405
+ !["A", "B", "C", "D"].includes(String(input.kind)) ||
406
+ !["active", "disputed", "unresolved"].includes(String(status))
407
+ )
408
+ throw new RepairError("DI_INVALID_RESULT_JSON");
409
+ if (input.contradictedFactId !== undefined && !factIds.has(id(input.contradictedFactId)))
410
+ throw new RepairError("DI_INVALID_RESULT_JSON");
411
+ if ((status === "disputed" || status === "unresolved") && input.rationale === undefined)
412
+ throw new RepairError("DI_INVALID_RESULT_JSON");
413
+ const trigger = {
414
+ kind: input.kind as "A" | "B" | "C" | "D",
415
+ name: text(input.name, 1024),
416
+ status: status as "active" | "disputed" | "unresolved",
417
+ component: id(input.component),
418
+ dimension: dimension(input.dimension, dimensions),
419
+ ...(input.evidence === undefined ? {} : { evidence: optionalText(input.evidence, 4096) }),
420
+ ...(input.contradictedFactId === undefined
421
+ ? {}
422
+ : { contradictedFactId: id(input.contradictedFactId) }),
423
+ ...(input.rationale === undefined ? {} : { rationale: optionalText(input.rationale, 4096) }),
424
+ };
425
+ return trigger;
426
+ });
427
+ const ontology =
428
+ result.ontology === undefined
429
+ ? undefined
430
+ : (() => {
431
+ const input = record(result.ontology);
432
+ exactKeys(input, ["entities", "relationships", "reasoning"], "DI_INVALID_RESULT_JSON");
433
+ const entities = list(input.entities, 32, budget).map(value => {
434
+ const entity = record(value);
435
+ exactKeys(entity, ["id", "name", "type", "fields"], "DI_INVALID_RESULT_JSON");
436
+ return {
437
+ id: id(entity.id),
438
+ name: text(entity.name, 1024),
439
+ type: text(entity.type, 1024),
440
+ fields: list(entity.fields, 16, budget).map(field => text(field, 1024)),
441
+ };
442
+ });
443
+ const entityIds = new Set(entities.map(entity => entity.id));
444
+ if (entityIds.size !== entities.length) throw new RepairError("DI_INVALID_RESULT_JSON");
445
+ return {
446
+ entities,
447
+ relationships: list(input.relationships, 16, budget).map(value => {
448
+ const relation = record(value);
449
+ exactKeys(relation, ["id", "from_entity_id", "to_entity_id", "type"], "DI_INVALID_RESULT_JSON");
450
+ const from_entity_id = id(relation.from_entity_id),
451
+ to_entity_id = id(relation.to_entity_id);
452
+ if (!entityIds.has(from_entity_id) || !entityIds.has(to_entity_id))
453
+ throw new RepairError("DI_INVALID_RESULT_JSON");
454
+ return { id: id(relation.id), from_entity_id, to_entity_id, type: text(relation.type, 1024) };
455
+ }),
456
+ reasoning: list(input.reasoning, 32, budget).map(value => {
457
+ const item = record(value);
458
+ exactKeys(item, ["statement", "evidence"], "DI_INVALID_RESULT_JSON");
459
+ return { statement: text(item.statement, 4096), evidence: optionalText(item.evidence, 4096) };
460
+ }),
461
+ };
462
+ })();
463
+ const bookkeeping =
464
+ result.bookkeeping === undefined
465
+ ? undefined
466
+ : (() => {
467
+ const input = record(result.bookkeeping);
468
+ exactKeys(input, ["resolution", "round_ids", "counter_deltas"], "DI_INVALID_RESULT_JSON");
469
+ const resolution = resolutionJson(input.resolution);
470
+ const counter_deltas: Record<string, number> | undefined =
471
+ input.counter_deltas === undefined
472
+ ? undefined
473
+ : (() => {
474
+ const deltas: Record<string, number> = {};
475
+ for (const [key, value] of Object.entries(record(input.counter_deltas))) {
476
+ if (typeof value !== "number") throw new RepairError("DI_INVALID_RESULT_JSON");
477
+ const counterDelta: number = value;
478
+ if (!Number.isSafeInteger(counterDelta) || Math.abs(counterDelta) > 10_000)
479
+ throw new RepairError("DI_INVALID_RESULT_JSON");
480
+ deltas[id(key)] = counterDelta;
481
+ }
482
+ return deltas;
483
+ })();
484
+ return {
485
+ resolution,
486
+ round_ids: input.round_ids === undefined ? undefined : list(input.round_ids, 32, budget).map(id),
487
+ counter_deltas,
488
+ };
489
+ })();
490
+ return {
491
+ global_scores: scores(result.global_scores, dimensions),
492
+ component_updates,
493
+ targeting,
494
+ triggers,
495
+ fact_ops,
496
+ ontology,
497
+ bookkeeping,
498
+ };
499
+ }
500
+ function inputText(value: unknown, max: number): string {
501
+ if (typeof value !== "string" || Buffer.byteLength(value) > max) throw new RepairError("DI_INVALID_INPUT_JSON");
502
+ return value;
503
+ }
504
+ export function decodeDeepInterviewSetupJson(value: unknown): Record<string, unknown> {
505
+ const input = structuredObject(value, "DI_INVALID_INPUT_JSON", 24 * 1024);
506
+ exactKeys(
507
+ input,
508
+ [
509
+ "type",
510
+ "interview_id",
511
+ "initial_idea",
512
+ "initial_context_summary",
513
+ "codebase_context",
514
+ "challenge_modes_used",
515
+ "threshold",
516
+ "threshold_source",
517
+ "language",
518
+ "trace",
519
+ "trace_summary",
520
+ ],
521
+ "DI_INVALID_INPUT_JSON",
522
+ );
523
+ if (
524
+ (input.type !== "greenfield" && input.type !== "brownfield") ||
525
+ typeof input.threshold !== "number" ||
526
+ input.threshold <= 0
527
+ )
528
+ throw new RepairError("DI_INVALID_INPUT_JSON");
529
+ try {
530
+ scoreToUnits(input.threshold);
531
+ } catch {
532
+ throw new RepairError("DI_INVALID_INPUT_JSON");
533
+ }
534
+ for (const key of [
535
+ "interview_id",
536
+ "initial_idea",
537
+ "initial_context_summary",
538
+ "codebase_context",
539
+ "threshold_source",
540
+ "language",
541
+ "trace_summary",
542
+ ] as const)
543
+ if (input[key] !== undefined) inputText(input[key], 4096);
544
+ if (input.interview_id !== undefined && !ID.test(input.interview_id as string))
545
+ throw new RepairError("DI_INVALID_INPUT_JSON");
546
+ if (
547
+ input.challenge_modes_used !== undefined &&
548
+ (!Array.isArray(input.challenge_modes_used) ||
549
+ input.challenge_modes_used.length > 64 ||
550
+ input.challenge_modes_used.some(item => typeof item !== "string" || Buffer.byteLength(item) > 256))
551
+ )
552
+ throw new RepairError("DI_INVALID_INPUT_JSON");
553
+ if (
554
+ input.trace !== undefined &&
555
+ (!Array.isArray(input.trace) ||
556
+ input.trace.length > 64 ||
557
+ input.trace.some(item => typeof item !== "string" || Buffer.byteLength(item) > 2048))
558
+ )
559
+ throw new RepairError("DI_INVALID_INPUT_JSON");
560
+ return input;
561
+ }
562
+ export function decodeDeepInterviewTopologyJson(value: unknown): Record<string, unknown> {
563
+ const input = structuredObject(value, "DI_INVALID_INPUT_JSON", 24 * 1024);
564
+ exactKeys(input, ["components", "deferred_components"], "DI_INVALID_INPUT_JSON");
565
+ if (
566
+ !Array.isArray(input.components) ||
567
+ input.components.length > 64 ||
568
+ !Array.isArray(input.deferred_components) ||
569
+ input.deferred_components.length > 64
570
+ )
571
+ throw new RepairError("DI_INVALID_INPUT_JSON");
572
+ const components = input.components.map(value => {
573
+ const component = record(value, "DI_INVALID_INPUT_JSON");
574
+ exactKeys(component, ["id", "name", "status", "active"], "DI_INVALID_INPUT_JSON");
575
+ if (
576
+ typeof component.id !== "string" ||
577
+ !ID.test(component.id) ||
578
+ (component.name !== undefined &&
579
+ (typeof component.name !== "string" || !component.name || Buffer.byteLength(component.name) > 1024)) ||
580
+ (component.status !== undefined && component.status !== "active" && component.status !== "deferred") ||
581
+ (component.active !== undefined && typeof component.active !== "boolean")
582
+ )
583
+ throw new RepairError("DI_INVALID_INPUT_JSON");
584
+ return component;
585
+ });
586
+ const ids = new Set(components.map(component => component.id as string));
587
+ if (
588
+ ids.size !== components.length ||
589
+ input.deferred_components.some(value => typeof value !== "string" || !ids.has(value))
590
+ )
591
+ throw new RepairError("DI_INVALID_INPUT_JSON");
592
+ return input;
593
+ }
594
+ function assertTopologyInspectable(input: Record<string, unknown>, confirmedAt: string): void {
595
+ const components = input.components as Record<string, unknown>[];
596
+ const deferredComponents = input.deferred_components as string[];
597
+ const deferredComponentIds = new Set(deferredComponents);
598
+ const projection = {
599
+ status: "confirmed",
600
+ confirmed_at: confirmedAt,
601
+ components: components
602
+ .filter(component => component.status !== "deferred" && !deferredComponentIds.has(component.id as string))
603
+ .map(component => ({
604
+ id: component.id,
605
+ name: textView(component.name),
606
+ description: null,
607
+ active: component.active !== false,
608
+ deferred: false,
609
+ scores: { goal: null, constraints: null, criteria: null, context: null },
610
+ weakest_dimension: null,
611
+ })),
612
+ deferrals: deferredComponents.map(component_id => ({
613
+ component_id,
614
+ reason: textView(""),
615
+ created_at: confirmedAt,
616
+ until_round: null,
617
+ })),
618
+ last_targeted_component_id: null,
619
+ };
620
+ if (Buffer.byteLength(JSON.stringify(projection)) > DATA_LIMIT) throw new RepairError("DI_OUTPUT_LIMIT_EXCEEDED");
621
+ }
622
+ interface ParsedRepairCommand {
623
+ verb: DeepInterviewRepairVerb;
624
+ flags: Map<string, string>;
625
+ }
626
+
627
+ function parse(args: readonly string[]): ParsedRepairCommand {
628
+ const verb = args[0];
629
+ if (verb === undefined || !isDeepInterviewRepairVerb(verb)) throw new RepairError("DI_UNKNOWN_COMMAND");
630
+ const flags = new Map<string, string>();
631
+ let hasJson = false;
632
+ for (let i = 1; i < args.length; i++) {
633
+ const token = args[i];
634
+ if (!token.startsWith("--") || token.includes("=")) throw new RepairError("DI_INVALID_ARGUMENT");
635
+ if (token === "--json") {
636
+ if (hasJson) throw new RepairError("DI_INVALID_ARGUMENT");
637
+ hasJson = true;
638
+ continue;
639
+ }
640
+ const value = args[++i];
641
+ if (value === undefined || value.startsWith("--") || flags.has(token))
642
+ throw new RepairError("DI_INVALID_ARGUMENT");
643
+ flags.set(token, value);
644
+ }
645
+ if (!hasJson) throw new RepairError("DI_JSON_REQUIRED");
646
+ const session = flags.get("--session-id");
647
+ if (!session || !ID.test(session)) throw new RepairError("DI_INVALID_SESSION_ID");
648
+ return { verb, flags };
649
+ }
650
+ function safeInt(value: string | undefined, code: string, positive = false): number {
651
+ if (!value || !/^(0|[1-9][0-9]*)$/.test(value)) throw new RepairError(code);
652
+ const number = Number(value);
653
+ if (!Number.isSafeInteger(number) || (positive && number < 1)) throw new RepairError(code);
654
+ return number;
655
+ }
656
+ function statePath(cwd: string, session: string) {
657
+ return modeStatePath(cwd, session, "deep-interview");
658
+ }
659
+ function validateMutation(
660
+ parsed: ParsedRepairCommand,
661
+ allowed: readonly string[],
662
+ needsRevision = true,
663
+ ): { session: string; revision?: number } {
664
+ for (const key of parsed.flags.keys()) if (!allowed.includes(key)) throw new RepairError("DI_INVALID_ARGUMENT");
665
+ const session = parsed.flags.get("--session-id")!;
666
+ const revision = needsRevision
667
+ ? safeInt(parsed.flags.get("--expected-revision"), "DI_INVALID_EXPECTED_REVISION")
668
+ : undefined;
669
+ if (needsRevision && parsed.flags.get("--schema-version") !== "1")
670
+ throw new RepairError("DI_INVALID_SCHEMA_VERSION");
671
+ return { session, revision };
672
+ }
673
+ function transformResult(
674
+ command: string,
675
+ path: string,
676
+ result: {
677
+ revision: number;
678
+ written: boolean;
679
+ stamped?: Record<string, unknown>;
680
+ warnings?: readonly { code: string; message: string }[];
681
+ },
682
+ ) {
683
+ const state = result.stamped?.state;
684
+ const authoritative =
685
+ state && typeof state === "object" && !Array.isArray(state)
686
+ ? {
687
+ current_ambiguity: (state as Record<string, unknown>).current_ambiguity ?? null,
688
+ effective_ambiguity: (state as Record<string, unknown>).effective_ambiguity ?? null,
689
+ floor: (state as Record<string, unknown>).floor ?? null,
690
+ ambiguity_milestone: (state as Record<string, unknown>).ambiguity_milestone ?? null,
691
+ }
692
+ : undefined;
693
+ return {
694
+ ok: true,
695
+ command,
696
+ state_path: path,
697
+ state_revision: result.revision,
698
+ written: result.written,
699
+ content_sha256: result.stamped ? workflowEnvelopeContentSha256(result.stamped) : undefined,
700
+ transition: authoritative,
701
+ warnings: result.warnings ?? [],
702
+ };
703
+ }
704
+
705
+ async function consumeDraftMutation(
706
+ args: readonly string[],
707
+ cwd: string,
708
+ ): Promise<DeepInterviewRepairResult | undefined> {
709
+ const draft = args.indexOf("--draft-id");
710
+ if (draft === -1) return undefined;
711
+ const verb = args[0];
712
+ if (!verb || !["initialize-context", "confirm-topology", "record-answer", "apply-round-result"].includes(verb))
713
+ throw new RepairError("DI_UNKNOWN_COMMAND");
714
+ const flags = new Map<string, string>();
715
+ let hasJson = false;
716
+ for (let index = 1; index < args.length; ) {
717
+ const flag = args[index++];
718
+ if (flag === "--json") {
719
+ if (hasJson) throw new RepairError("DI_INPUT_MODE_CONFLICT");
720
+ hasJson = true;
721
+ if (args[index] === "true") index++;
722
+ continue;
723
+ }
724
+ const value = args[index++];
725
+ if (
726
+ !flag?.startsWith("--") ||
727
+ !["--draft-id", "--expected-draft-revision"].includes(flag) ||
728
+ value === undefined ||
729
+ value.startsWith("--") ||
730
+ flags.has(flag)
731
+ )
732
+ throw new RepairError("DI_INPUT_MODE_CONFLICT");
733
+ flags.set(flag, value);
734
+ }
735
+ if (!hasJson) throw new RepairError("DI_JSON_REQUIRED");
736
+ const id = flags.get("--draft-id");
737
+ const revision = flags.get("--expected-draft-revision");
738
+ if (!id || !revision) throw new RepairError("DI_INVALID_ARGUMENT");
739
+ const { runDeepInterviewDraftInternalConsumeCommand } = await import("./deep-interview-draft");
740
+ const result = await runDeepInterviewDraftInternalConsumeCommand(
741
+ id,
742
+ Number(revision),
743
+ verb as DeepInterviewDraftKind,
744
+ cwd,
745
+ );
746
+ return { status: result.status, stdout: result.stdout, stderr: result.stderr };
747
+ }
748
+
749
+ export async function runDeepInterviewRepairCommand(
750
+ args: readonly string[],
751
+ cwd: string,
752
+ ): Promise<DeepInterviewRepairResult> {
753
+ try {
754
+ const consumed = await consumeDraftMutation(args, cwd);
755
+ if (consumed) return consumed;
756
+ const parsed = parse(args);
757
+ if (parsed.verb === "sanity-check") return await sanity(parsed, cwd);
758
+ if (parsed.verb === "inspect") return await inspect(parsed, cwd);
759
+ const result = await mutate(parsed, cwd);
760
+ return { status: 0, stdout: `${JSON.stringify(result)}\n` };
761
+ } catch (error) {
762
+ return errorResult(error);
763
+ }
764
+ }
765
+ async function mutate(parsed: ParsedRepairCommand, cwd: string): Promise<Record<string, unknown>> {
766
+ const configs: Record<Exclude<DeepInterviewRepairVerb, "inspect" | "sanity-check">, readonly string[]> = {
767
+ "initialize-context": ["--session-id", "--schema-version", "--expected-revision", "--input-json"],
768
+ "confirm-topology": ["--session-id", "--schema-version", "--expected-revision", "--input-json"],
769
+ "record-answer": [
770
+ "--session-id",
771
+ "--schema-version",
772
+ "--expected-revision",
773
+ "--round",
774
+ "--question-id",
775
+ "--question-json",
776
+ "--answer-json",
777
+ "--round-id",
778
+ "--component-id",
779
+ "--dimension",
780
+ ],
781
+ "apply-round-result": [
782
+ "--session-id",
783
+ "--schema-version",
784
+ "--expected-revision",
785
+ "--round",
786
+ "--question-id",
787
+ "--result-json",
788
+ "--round-id",
789
+ ],
790
+ };
791
+ const { session, revision } = validateMutation(parsed, configs[parsed.verb as keyof typeof configs]);
792
+ const setupInput =
793
+ parsed.verb === "initialize-context" ? decodeDeepInterviewSetupJson(parsed.flags.get("--input-json")) : undefined;
794
+ const topologyInput =
795
+ parsed.verb === "confirm-topology"
796
+ ? decodeDeepInterviewTopologyJson(parsed.flags.get("--input-json"))
797
+ : undefined;
798
+ const path = statePath(cwd, session);
799
+ const now = new Date().toISOString();
800
+ let nativeProjection: object | undefined;
801
+ const output = await transformGuardedWorkflowEnvelopeAtomic(path, {
802
+ cwd,
803
+ expectedRevision: revision!,
804
+ receipt: {
805
+ cwd,
806
+ skill: "deep-interview",
807
+ owner: "gjc-runtime",
808
+ command: `gjc deep-interview ${parsed.verb}`,
809
+ sessionId: session,
810
+ nowIso: now,
811
+ },
812
+ validate(value, legacy) {
813
+ if (legacy) validateLegacyDeepInterviewEnvelope(value);
814
+ else validateDeepInterviewV1Envelope(value);
815
+ },
816
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview", sessionId: session },
817
+ transform(current) {
818
+ const state = (
819
+ current.state && typeof current.state === "object" && !Array.isArray(current.state) ? current.state : {}
820
+ ) as Record<string, unknown>;
821
+ if (parsed.verb === "initialize-context") {
822
+ const input = setupInput!;
823
+ const unresolvedSetup =
824
+ state.setup !== null &&
825
+ typeof state.setup === "object" &&
826
+ !Array.isArray(state.setup) &&
827
+ (state.setup as Record<string, unknown>).status === "unresolved";
828
+ const missing: Record<string, unknown> = {};
829
+ for (const [key, value] of Object.entries(input)) {
830
+ if (key === "type" && unresolvedSetup) {
831
+ if (
832
+ current.trace !== undefined &&
833
+ canonicalDeepInterviewJson(state.type) !== canonicalDeepInterviewJson(value)
834
+ )
835
+ throw new RepairError("DI_SETUP_CONFLICT", 4);
836
+ if (canonicalDeepInterviewJson(state.type) !== canonicalDeepInterviewJson(value))
837
+ missing[key] = value;
838
+ } else if (Object.hasOwn(state, key)) {
839
+ if (canonicalDeepInterviewJson(state[key]) !== canonicalDeepInterviewJson(value))
840
+ throw new RepairError("DI_SETUP_CONFLICT", 4);
841
+ } else {
842
+ missing[key] = value;
843
+ }
844
+ }
845
+ if (Object.keys(missing).length === 0 && !unresolvedSetup) return { kind: "noop" as const };
846
+ const { setup: _setup, ...initializedState } = state;
847
+ return {
848
+ kind: "write" as const,
849
+ value: { ...current, schema_version: 1, state: { ...initializedState, ...missing } },
850
+ };
851
+ }
852
+ if (parsed.verb === "confirm-topology") {
853
+ const input = topologyInput!;
854
+ assertTopologyInspectable(input, now);
855
+ const existing = state.topology;
856
+ const normalized = canonicalDeepInterviewJson(input);
857
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
858
+ const existingTopology = existing as Record<string, unknown>;
859
+ const isEmptyPendingTopology =
860
+ existingTopology.status === "pending" &&
861
+ Array.isArray(existingTopology.components) &&
862
+ existingTopology.components.length === 0 &&
863
+ Array.isArray(existingTopology.deferred_components) &&
864
+ existingTopology.deferred_components.length === 0;
865
+ if (!isEmptyPendingTopology) {
866
+ const { status: _status, confirmed_at: _confirmedAt, ...prior } = existingTopology;
867
+ if (canonicalDeepInterviewJson(prior) === normalized) return { kind: "noop" as const };
868
+ throw new RepairError("DI_TOPOLOGY_CONFLICT", 4);
869
+ }
870
+ }
871
+ const { setup: _setup, ...confirmedState } = state;
872
+ return {
873
+ kind: "write" as const,
874
+ value: {
875
+ ...current,
876
+ schema_version: 1,
877
+ state: { ...confirmedState, topology: { ...input, status: "confirmed", confirmed_at: now } },
878
+ },
879
+ };
880
+ }
881
+ const round = safeInt(parsed.flags.get("--round"), "DI_INVALID_ROUND", true);
882
+ const questionId = parsed.flags.get("--question-id");
883
+ if (!questionId || !ID.test(questionId)) throw new RepairError("DI_INVALID_QUESTION_ID");
884
+ const roundId = parsed.flags.get("--round-id");
885
+ if (roundId !== undefined && !ID.test(roundId)) throw new RepairError("DI_INVALID_ROUND_ID");
886
+ const key = deriveRoundKey(typeof state.interview_id === "string" ? state.interview_id : undefined, {
887
+ round,
888
+ round_id: roundId,
889
+ questionId,
890
+ });
891
+ if (parsed.verb === "record-answer") {
892
+ const component = parsed.flags.get("--component-id");
893
+ const requestedDimension = parsed.flags.get("--dimension");
894
+ if (component !== undefined && !ID.test(component)) throw new RepairError("DI_INVALID_COMPONENT_ID");
895
+ if (
896
+ requestedDimension !== undefined &&
897
+ !DIMENSIONS.includes(requestedDimension as (typeof DIMENSIONS)[number])
898
+ )
899
+ throw new RepairError("DI_INVALID_DIMENSION");
900
+ const question = stringJson(parsed.flags.get("--question-json"));
901
+ const answer = decodeDeepInterviewAnswerJson(parsed.flags.get("--answer-json"));
902
+ const rounds = Array.isArray(state.rounds) ? [...state.rounds] : [];
903
+ const candidate = {
904
+ round,
905
+ round_key: key,
906
+ round_id: roundId ?? undefined,
907
+ question_id: questionId,
908
+ component,
909
+ dimension: requestedDimension,
910
+ question_text: question,
911
+ question_hash: questionHash(question),
912
+ answer_hash: answerHash(answer.selected_options, answer.custom_input ?? undefined),
913
+ selected_options: answer.selected_options,
914
+ custom_input: answer.custom_input ?? undefined,
915
+ };
916
+ const existing = rounds.find(
917
+ value => value && typeof value === "object" && (value as Record<string, unknown>).round_key === key,
918
+ );
919
+ if (existing) {
920
+ if (
921
+ deepInterviewAnswerIdentityEqual(
922
+ existing as Parameters<typeof deepInterviewAnswerIdentityEqual>[0],
923
+ candidate,
924
+ )
925
+ )
926
+ return { kind: "noop" as const };
927
+ throw new RepairError(
928
+ (existing as Record<string, unknown>).lifecycle === "scored"
929
+ ? "DI_SHELL_CONFLICT"
930
+ : "DI_ANSWER_CONFLICT",
931
+ 4,
932
+ );
933
+ }
934
+ rounds.push({
935
+ ...candidate,
936
+ lifecycle: "answered",
937
+ answered_at: now,
938
+ });
939
+ return { kind: "write" as const, value: { ...current, schema_version: 1, state: { ...state, rounds } } };
940
+ }
941
+ const result = decodeDeepInterviewRoundResultJson(parsed.flags.get("--result-json"), state);
942
+ try {
943
+ const outcome = applyDeepInterviewRoundResultV1(current, key, result, now);
944
+ nativeProjection = outcome.projection;
945
+ if (outcome.kind === "noop") return { kind: "noop" as const };
946
+ return {
947
+ kind: "write" as const,
948
+ value: {
949
+ ...persistNativeTriggerMetrics(state, outcome.envelope, key, result),
950
+ schema_version: 1,
951
+ },
952
+ };
953
+ } catch (error) {
954
+ if (error instanceof Error && error.message.startsWith("DI_"))
955
+ throw new RepairError(error.message, error.message.includes("CONFLICT") ? 4 : 3);
956
+ throw error;
957
+ }
958
+ },
959
+ });
960
+ const warnings =
961
+ output.written && output.stamped
962
+ ? await runDeepInterviewPostCommitEffects({
963
+ cwd,
964
+ statePath: path,
965
+ sessionId: session,
966
+ envelope: output.stamped,
967
+ revision: output.revision,
968
+ writer: "deep-interview-repair",
969
+ auditWarnings: output.warnings,
970
+ })
971
+ : [];
972
+ return {
973
+ ...transformResult(parsed.verb, path, { ...output, warnings }),
974
+ native_projection: nativeProjection,
975
+ };
976
+ }
977
+ function truncateUtf8(value: string, maxBytes: number): string {
978
+ if (Buffer.byteLength(value) <= maxBytes) return value;
979
+ let result = "";
980
+ for (const character of value) {
981
+ if (Buffer.byteLength(result) + Buffer.byteLength(character) > maxBytes) break;
982
+ result += character;
983
+ }
984
+ return result;
985
+ }
986
+ type InspectSelector = "summary" | "recent-scored" | "pending" | "round" | "topology" | "facts" | "triggers" | "floor";
987
+ const INSPECT_SELECTORS = new Set<InspectSelector>([
988
+ "summary",
989
+ "recent-scored",
990
+ "pending",
991
+ "round",
992
+ "topology",
993
+ "facts",
994
+ "triggers",
995
+ "floor",
996
+ ]);
997
+ const PAGED_SELECTORS = new Set<InspectSelector>(["recent-scored", "pending", "facts", "triggers"]);
998
+ const DATA_LIMIT = 16 * 1024;
999
+ const RESPONSE_LIMIT = 48 * 1024;
1000
+ const asRecord = (value: unknown): Record<string, unknown> =>
1001
+ value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
1002
+ const textView = (value: unknown, limit = 1024) => {
1003
+ const text = typeof value === "string" ? value : "";
1004
+ const original_bytes = Buffer.byteLength(text);
1005
+ return { value: truncateUtf8(text, limit), truncated: original_bytes > limit, original_bytes };
1006
+ };
1007
+ const nullableTextView = (value: unknown, limit = 1024) => (typeof value === "string" ? textView(value, limit) : null);
1008
+ const nullableId = (value: unknown) => (typeof value === "string" && ID.test(value) ? value : null);
1009
+ const nullableScore = (value: unknown) =>
1010
+ typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : null;
1011
+ function persistNativeTriggerMetrics(
1012
+ priorState: Record<string, unknown>,
1013
+ envelope: Record<string, unknown>,
1014
+ roundKey: string,
1015
+ result: DeepInterviewRoundResultV1,
1016
+ ): Record<string, unknown> {
1017
+ const nextState = asRecord(envelope.state);
1018
+ const rounds = Array.isArray(nextState.rounds) ? nextState.rounds.map(asRecord) : [];
1019
+ const roundIndex = rounds.findIndex(round => round.round_key === roundKey);
1020
+ if (roundIndex < 0) throw new Error("DI_STATE_SCHEMA_INVALID");
1021
+ const round = rounds[roundIndex];
1022
+ const prior = rounds
1023
+ .filter(
1024
+ candidate =>
1025
+ candidate.lifecycle === "scored" &&
1026
+ (Number(candidate.round) < Number(round.round) ||
1027
+ (Number(candidate.round) === Number(round.round) &&
1028
+ String(candidate.round_key) < String(round.round_key))),
1029
+ )
1030
+ .sort(
1031
+ (left, right) =>
1032
+ Number(right.round) - Number(left.round) || String(right.round_key).localeCompare(String(left.round_key)),
1033
+ )[0];
1034
+ const priorTopology = asRecord(priorState.topology);
1035
+ const priorComponents = Array.isArray(priorTopology.components) ? priorTopology.components.map(asRecord) : [];
1036
+ const nextTopology = asRecord(nextState.topology);
1037
+ const nextComponents = Array.isArray(nextTopology.components) ? nextTopology.components.map(asRecord) : [];
1038
+ const hasTopologyComponents = priorComponents.length > 0;
1039
+ const priorAmbiguity = nullableScore(prior?.ambiguity);
1040
+ const newAmbiguity = nullableScore(round.ambiguity);
1041
+ const triggers = (Array.isArray(round.triggers) ? round.triggers : []).map(value => {
1042
+ const trigger = asRecord(value);
1043
+ const dimension = String(trigger.dimension);
1044
+ const component = String(trigger.component);
1045
+ const priorComponent = priorComponents.find(candidate => candidate.id === component);
1046
+ const nextComponent = nextComponents.find(candidate => candidate.id === component);
1047
+ const metrics =
1048
+ trigger.status === "active"
1049
+ ? {
1050
+ prior_dimension_score: hasTopologyComponents
1051
+ ? nullableScore(asRecord(priorComponent?.clarity_scores)[dimension])
1052
+ : nullableScore(asRecord(prior?.scores)[dimension]),
1053
+ new_dimension_score: hasTopologyComponents
1054
+ ? nullableScore(asRecord(nextComponent?.clarity_scores)[dimension])
1055
+ : nullableScore(asRecord(result.global_scores)[dimension]),
1056
+ prior_effective_ambiguity: priorAmbiguity,
1057
+ new_effective_ambiguity: newAmbiguity,
1058
+ }
1059
+ : {
1060
+ prior_dimension_score: null,
1061
+ new_dimension_score: null,
1062
+ prior_effective_ambiguity: null,
1063
+ new_effective_ambiguity: null,
1064
+ };
1065
+ return {
1066
+ ...trigger,
1067
+ ...metrics,
1068
+ };
1069
+ });
1070
+ rounds[roundIndex] = { ...round, triggers };
1071
+ return { ...envelope, state: { ...nextState, rounds } };
1072
+ }
1073
+ function roundProjection(value: unknown, shell = false): Record<string, unknown> {
1074
+ const round = asRecord(value);
1075
+ const answer = {
1076
+ selected_options: Array.isArray(round.selected_options)
1077
+ ? round.selected_options.map(option => textView(option))
1078
+ : [],
1079
+ custom_input: nullableTextView(round.custom_input),
1080
+ };
1081
+ const base = {
1082
+ round_key: String(round.round_key),
1083
+ round: Number(round.round),
1084
+ round_id: nullableId(round.round_id),
1085
+ question_id: nullableId(round.question_id),
1086
+ component_id: nullableId(round.component),
1087
+ dimension: ["goal", "constraints", "criteria", "context"].includes(String(round.dimension))
1088
+ ? round.dimension
1089
+ : null,
1090
+ question: nullableTextView(round.question_text, 2048),
1091
+ answer,
1092
+ lifecycle:
1093
+ round.lifecycle === "scored"
1094
+ ? "scored"
1095
+ : round.lifecycle === "pending_scoring"
1096
+ ? "pending_scoring"
1097
+ : "answered",
1098
+ answered_at: String(round.answered_at),
1099
+ };
1100
+ if (shell)
1101
+ return {
1102
+ ...base,
1103
+ question_id: String(round.question_id),
1104
+ question: textView(round.question_text, 2048),
1105
+ lifecycle: base.lifecycle === "scored" ? "answered" : base.lifecycle,
1106
+ };
1107
+ return {
1108
+ ...base,
1109
+ scored_at: typeof round.scored_at === "string" ? round.scored_at : null,
1110
+ weighted_ambiguity: nullableScore(round.reported_ambiguity),
1111
+ effective_ambiguity: nullableScore(round.ambiguity),
1112
+ floor: nullableScore(round.ambiguity_floor),
1113
+ round_result_digest:
1114
+ typeof round.round_result_digest === "string" && /^[0-9a-f]{64}$/.test(round.round_result_digest)
1115
+ ? { v: 1, algorithm: "sha256", value: round.round_result_digest }
1116
+ : null,
1117
+ };
1118
+ }
1119
+ function sortKey(selector: InspectSelector, item: Record<string, unknown>, insertion = 0): unknown[] {
1120
+ if (selector === "recent-scored" || selector === "pending") return [item.round, item.round_key];
1121
+ if (selector === "facts") return [item.id, insertion];
1122
+ return [item.source_round, item.source_round_key, insertion];
1123
+ }
1124
+ function compareKeys(selector: InspectSelector, left: unknown[], right: unknown[]): number {
1125
+ for (let index = 0; index < left.length; index++) {
1126
+ const order =
1127
+ typeof left[index] === "number" && typeof right[index] === "number"
1128
+ ? Number(left[index]) - Number(right[index])
1129
+ : String(left[index]).localeCompare(String(right[index]));
1130
+ if (order !== 0) return selector === "recent-scored" ? -order : order;
1131
+ }
1132
+ return 0;
1133
+ }
1134
+ function cursorFor(selector: InspectSelector, revision: number, view_sha256: string, last_sort_key: unknown[]) {
1135
+ return Buffer.from(JSON.stringify({ v: 1, selector, revision, view_sha256, last_sort_key })).toString("base64url");
1136
+ }
1137
+ function cursorOffset(
1138
+ token: string | undefined,
1139
+ selector: InspectSelector,
1140
+ revision: number,
1141
+ view: string,
1142
+ items: Record<string, unknown>[],
1143
+ ): number {
1144
+ if (!token) return 0;
1145
+ let cursor: Record<string, unknown>;
1146
+ try {
1147
+ const decoded = Buffer.from(token, "base64url").toString("utf8");
1148
+ if (!/^[A-Za-z0-9_-]+$/.test(token) || Buffer.from(decoded).toString("base64url") !== token) throw new Error();
1149
+ cursor = strictJson(decoded) as Record<string, unknown>;
1150
+ } catch {
1151
+ throw new RepairError("DI_CURSOR_INVALID");
1152
+ }
1153
+ const arity = selector === "triggers" ? 3 : 2;
1154
+ if (
1155
+ !cursor ||
1156
+ Object.keys(cursor).length !== 5 ||
1157
+ ["v", "selector", "revision", "view_sha256", "last_sort_key"].some(key => !Object.hasOwn(cursor, key)) ||
1158
+ cursor.v !== 1 ||
1159
+ cursor.selector !== selector ||
1160
+ !Number.isSafeInteger(cursor.revision) ||
1161
+ typeof cursor.view_sha256 !== "string" ||
1162
+ !Array.isArray(cursor.last_sort_key) ||
1163
+ cursor.last_sort_key.length !== arity ||
1164
+ cursor.last_sort_key.some((value, index) => {
1165
+ const numberPart =
1166
+ selector === "recent-scored" || selector === "pending"
1167
+ ? index === 0
1168
+ : selector === "facts"
1169
+ ? index === 1
1170
+ : index === 0 || index === 2;
1171
+ return numberPart ? typeof value !== "number" || !Number.isSafeInteger(value) : typeof value !== "string";
1172
+ })
1173
+ )
1174
+ throw new RepairError("DI_CURSOR_INVALID");
1175
+ if (cursor.revision !== revision || cursor.view_sha256 !== view) throw new RepairError("DI_CURSOR_STALE", 3);
1176
+ const found = items.findIndex(
1177
+ item =>
1178
+ canonicalDeepInterviewJson(sortKey(selector, item, Number(item.insertion_index ?? 0))) ===
1179
+ canonicalDeepInterviewJson(cursor.last_sort_key),
1180
+ );
1181
+ if (found < 0) throw new RepairError("DI_CURSOR_INVALID");
1182
+ return found + 1;
1183
+ }
1184
+ const LEGACY_COLLECTION_LIMIT = 64;
1185
+ function validateLegacyDeepInterviewEnvelope(value: Record<string, unknown>): void {
1186
+ const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
1187
+ Boolean(candidate) && typeof candidate === "object" && !Array.isArray(candidate);
1188
+ const isId = (candidate: unknown) => typeof candidate === "string" && ID.test(candidate);
1189
+ const isScore = (candidate: unknown) =>
1190
+ typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0 && candidate <= 1;
1191
+ const isBoundedText = (candidate: unknown, max = 4096): candidate is string =>
1192
+ typeof candidate === "string" && Buffer.byteLength(candidate) <= max;
1193
+ const boundedRecords = (candidate: unknown): Record<string, unknown>[] => {
1194
+ if (
1195
+ !Array.isArray(candidate) ||
1196
+ candidate.length > LEGACY_COLLECTION_LIMIT ||
1197
+ candidate.some(entry => !isRecord(entry))
1198
+ )
1199
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1200
+ return candidate;
1201
+ };
1202
+ const optional = (candidate: unknown, predicate: (item: unknown) => boolean) =>
1203
+ candidate === undefined || candidate === null || predicate(candidate);
1204
+
1205
+ const state = value.state;
1206
+ if (
1207
+ !isRecord(state) ||
1208
+ (value.state_revision !== undefined &&
1209
+ (!Number.isSafeInteger(value.state_revision) || (value.state_revision as number) < 0)) ||
1210
+ (value.active !== undefined && typeof value.active !== "boolean") ||
1211
+ (value.current_phase !== undefined && typeof value.current_phase !== "string")
1212
+ )
1213
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1214
+
1215
+ const rounds = state.rounds === undefined ? [] : boundedRecords(state.rounds);
1216
+ const facts = state.established_facts === undefined ? [] : boundedRecords(state.established_facts);
1217
+ for (const round of rounds) {
1218
+ if (
1219
+ !optional(round.round_key, isId) ||
1220
+ !optional(round.round, value => Number.isSafeInteger(value) && (value as number) > 0) ||
1221
+ !optional(round.lifecycle, value => ["answered", "pending_scoring", "scored"].includes(String(value))) ||
1222
+ !optional(round.question_id, isId) ||
1223
+ !optional(round.question_text, isBoundedText) ||
1224
+ !optional(round.question_hash, value => isBoundedText(value, 128) && value.length > 0) ||
1225
+ !optional(round.answer_hash, value => isBoundedText(value, 128) && value.length > 0) ||
1226
+ !optional(
1227
+ round.selected_options,
1228
+ value =>
1229
+ Array.isArray(value) &&
1230
+ value.length <= LEGACY_COLLECTION_LIMIT &&
1231
+ value.every(
1232
+ option => typeof option === "string" && option.length > 0 && Buffer.byteLength(option) <= 2048,
1233
+ ),
1234
+ ) ||
1235
+ !optional(round.custom_input, value => typeof value === "string" && Buffer.byteLength(value) <= 4096) ||
1236
+ !optional(round.scores, value => {
1237
+ if (
1238
+ !isRecord(value) ||
1239
+ Object.keys(value).some(key => !DIMENSIONS.includes(key as (typeof DIMENSIONS)[number]))
1240
+ )
1241
+ return false;
1242
+ return Object.values(value).every(isScore);
1243
+ }) ||
1244
+ !["ambiguity", "reported_ambiguity", "ambiguity_floor", "weighted_ambiguity", "effective_ambiguity"].every(
1245
+ key => optional(round[key], isScore),
1246
+ ) ||
1247
+ !optional(round.auto_answered, value => typeof value === "boolean") ||
1248
+ !optional(round.triggers, value => {
1249
+ const triggers = boundedRecords(value);
1250
+ return triggers.every(
1251
+ trigger =>
1252
+ optional(trigger.kind, value => ["A", "B", "C", "D"].includes(String(value))) &&
1253
+ optional(trigger.name, isBoundedText) &&
1254
+ optional(trigger.status, value => ["active", "disputed", "unresolved"].includes(String(value))) &&
1255
+ optional(trigger.component, isId) &&
1256
+ optional(trigger.dimension, value =>
1257
+ ["goal", "constraints", "criteria", "context"].includes(String(value)),
1258
+ ) &&
1259
+ optional(trigger.evidence, isBoundedText) &&
1260
+ optional(trigger.rationale, isBoundedText) &&
1261
+ optional(trigger.contradictedFactId, isId),
1262
+ );
1263
+ })
1264
+ )
1265
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1266
+ }
1267
+ for (const fact of facts) {
1268
+ if (
1269
+ !optional(fact.id, isId) ||
1270
+ !optional(fact.round, value => Number.isSafeInteger(value) && (value as number) > 0) ||
1271
+ !optional(fact.statement, isBoundedText) ||
1272
+ !optional(fact.disputed, value => typeof value === "boolean") ||
1273
+ !optional(fact.superseded_by, isId) ||
1274
+ !optional(fact.component, isId) ||
1275
+ !optional(fact.dimension, value => ["goal", "constraints", "criteria", "context"].includes(String(value))) ||
1276
+ !optional(fact.evidence, isBoundedText) ||
1277
+ !optional(fact.resolution_reason, isBoundedText)
1278
+ )
1279
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1280
+ }
1281
+ if (state.topology !== undefined) {
1282
+ if (!isRecord(state.topology)) throw new Error("DI_STATE_SCHEMA_INVALID");
1283
+ const topology = state.topology;
1284
+ if (
1285
+ !optional(topology.components, value =>
1286
+ boundedRecords(value).every(
1287
+ component =>
1288
+ isId(component.id) &&
1289
+ optional(component.name, isBoundedText) &&
1290
+ optional(component.description, isBoundedText) &&
1291
+ optional(component.status, value => typeof value === "string") &&
1292
+ optional(component.active, value => typeof value === "boolean") &&
1293
+ optional(component.weakest_dimension, value =>
1294
+ ["goal", "constraints", "criteria", "context"].includes(String(value)),
1295
+ ) &&
1296
+ optional(component.clarity_scores, value => {
1297
+ if (!isRecord(value)) return false;
1298
+ return ["goal", "constraints", "criteria", "context"].every(
1299
+ dimension =>
1300
+ value[dimension] === undefined || value[dimension] === null || isScore(value[dimension]),
1301
+ );
1302
+ }),
1303
+ ),
1304
+ ) ||
1305
+ !optional(
1306
+ topology.deferred_components,
1307
+ value => Array.isArray(value) && value.length <= LEGACY_COLLECTION_LIMIT && value.every(isId),
1308
+ )
1309
+ )
1310
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1311
+ }
1312
+ if (state.ontology_snapshots !== undefined) {
1313
+ const snapshots = boundedRecords(state.ontology_snapshots);
1314
+ let priorRound = 0;
1315
+ for (const snapshot of snapshots) {
1316
+ if (
1317
+ !Number.isSafeInteger(snapshot.round) ||
1318
+ (snapshot.round as number) <= priorRound ||
1319
+ typeof snapshot.captured_at !== "string" ||
1320
+ !Number.isFinite(Date.parse(snapshot.captured_at)) ||
1321
+ !Array.isArray(snapshot.entities) ||
1322
+ snapshot.entities.length > LEGACY_COLLECTION_LIMIT ||
1323
+ !["no_entities", "first_round", "compared"].includes(String(snapshot.basis)) ||
1324
+ !Number.isSafeInteger(snapshot.stable_entities) ||
1325
+ !Number.isSafeInteger(snapshot.new_entities) ||
1326
+ !Number.isSafeInteger(snapshot.changed_entities) ||
1327
+ (snapshot.stability_ratio !== null && !isScore(snapshot.stability_ratio))
1328
+ )
1329
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1330
+ const entities = boundedRecords(snapshot.entities);
1331
+ if (
1332
+ (snapshot.stable_entities as number) < 0 ||
1333
+ (snapshot.new_entities as number) < 0 ||
1334
+ (snapshot.changed_entities as number) < 0 ||
1335
+ (snapshot.stable_entities as number) +
1336
+ (snapshot.new_entities as number) +
1337
+ (snapshot.changed_entities as number) !==
1338
+ entities.length ||
1339
+ ((snapshot.basis === "no_entities" || snapshot.basis === "first_round") &&
1340
+ snapshot.stability_ratio !== null) ||
1341
+ (snapshot.basis === "compared" && snapshot.stability_ratio === null) ||
1342
+ entities.some(
1343
+ entity =>
1344
+ !isId(entity.id) ||
1345
+ !isBoundedText(entity.name, 1024) ||
1346
+ entity.name === "" ||
1347
+ !isBoundedText(entity.type, 1024) ||
1348
+ !Array.isArray(entity.fields) ||
1349
+ entity.fields.length > 16 ||
1350
+ !entity.fields.every(field => isBoundedText(field, 1024)) ||
1351
+ !Array.isArray(entity.relationships) ||
1352
+ entity.relationships.length > 16 ||
1353
+ !entity.relationships.every(relationship => isBoundedText(relationship, 1024)),
1354
+ )
1355
+ )
1356
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1357
+ priorRound = snapshot.round as number;
1358
+ }
1359
+ }
1360
+ if (state.counters !== undefined) {
1361
+ if (!isRecord(state.counters) || Object.keys(state.counters).length > LEGACY_COLLECTION_LIMIT)
1362
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1363
+ for (const [key, count] of Object.entries(state.counters))
1364
+ if (!isId(key) || typeof count !== "number" || !Number.isSafeInteger(count) || Math.abs(count) > 1_000_000_000)
1365
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1366
+ }
1367
+ if (
1368
+ state.auto_answer_streak !== undefined &&
1369
+ (!Number.isSafeInteger(state.auto_answer_streak) || (state.auto_answer_streak as number) < 0)
1370
+ )
1371
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1372
+ for (const key of ["auto_answered_rounds", "auto_researched_rounds"] as const) {
1373
+ if (
1374
+ state[key] !== undefined &&
1375
+ (!Array.isArray(state[key]) ||
1376
+ state[key].length > LEGACY_COLLECTION_LIMIT ||
1377
+ !state[key].every(value => isId(value) || (Number.isSafeInteger(value) && value > 0)))
1378
+ )
1379
+ throw new Error("DI_STATE_SCHEMA_INVALID");
1380
+ }
1381
+ }
1382
+
1383
+ function inspectState(read: Record<string, unknown>, legacy: boolean): Record<string, unknown> {
1384
+ try {
1385
+ if (legacy) validateLegacyDeepInterviewEnvelope(read);
1386
+ else validateDeepInterviewV1Envelope(read);
1387
+ } catch {
1388
+ throw new RepairError("DI_STATE_SCHEMA_INVALID", 3);
1389
+ }
1390
+ return read.state as Record<string, unknown>;
1391
+ }
1392
+
1393
+ type DeepInterviewDiagnostic = {
1394
+ code?: string;
1395
+ legacy: boolean;
1396
+ state?: Record<string, unknown>;
1397
+ value?: Record<string, unknown>;
1398
+ };
1399
+
1400
+ function receiptIssue(receipt: ReturnType<typeof verifyWorkflowEnvelopeReceiptValue>): string | undefined {
1401
+ if (receipt === "native-valid" || receipt === "legacy") return undefined;
1402
+ return `DI_RECEIPT_${
1403
+ receipt === "checksum-mismatch" ? "CHECKSUM_MISMATCH" : receipt === "receipt-missing" ? "MISSING" : "MALFORMED"
1404
+ }`;
1405
+ }
1406
+
1407
+ function diagnoseDeepInterviewState(
1408
+ read: Awaited<ReturnType<typeof readExistingStateForMutation>>,
1409
+ path: string,
1410
+ ): DeepInterviewDiagnostic {
1411
+ if (read.kind !== "valid")
1412
+ return { code: read.kind === "absent" ? "DI_STATE_ABSENT" : "DI_STATE_CORRUPT", legacy: false };
1413
+
1414
+ const receipt = verifyWorkflowEnvelopeReceiptValue(read.value, path);
1415
+ const receiptCode = receiptIssue(receipt);
1416
+ if (receiptCode) return { code: receiptCode, legacy: false };
1417
+
1418
+ const legacy = receipt === "legacy";
1419
+ let state: Record<string, unknown>;
1420
+ try {
1421
+ state = inspectState(read.value, legacy);
1422
+ } catch (error) {
1423
+ return { code: error instanceof RepairError ? error.code : "DI_STATE_SCHEMA_INVALID", legacy };
1424
+ }
1425
+
1426
+ if (read.value.current_phase === "complete" || read.value.current_phase === "handoff" || read.value.active === false)
1427
+ return { code: "DI_PHASE_NOT_REPAIRABLE", legacy };
1428
+
1429
+ return { legacy, state, value: read.value };
1430
+ }
1431
+ async function inspect(parsed: ParsedRepairCommand, cwd: string): Promise<DeepInterviewRepairResult> {
1432
+ const { session } = validateMutation(
1433
+ parsed,
1434
+ ["--session-id", "--selector", "--round-key", "--limit", "--cursor"],
1435
+ false,
1436
+ );
1437
+ const selector = parsed.flags.get("--selector") as InspectSelector | undefined;
1438
+ if (!selector || !INSPECT_SELECTORS.has(selector)) throw new RepairError("DI_INVALID_SELECTOR");
1439
+ const paged = PAGED_SELECTORS.has(selector);
1440
+ for (const flag of parsed.flags.keys())
1441
+ if (
1442
+ ![
1443
+ "--session-id",
1444
+ "--selector",
1445
+ ...(selector === "round" ? ["--round-key"] : []),
1446
+ ...(paged ? ["--limit", "--cursor"] : []),
1447
+ ].includes(flag)
1448
+ )
1449
+ throw new RepairError("DI_SELECTOR_ARGUMENT_INVALID");
1450
+ if (selector === "round" && !parsed.flags.get("--round-key")) throw new RepairError("DI_SELECTOR_ARGUMENT_INVALID");
1451
+ const path = statePath(cwd, session);
1452
+ const diagnostic = diagnoseDeepInterviewState(await readExistingStateForMutation(path), path);
1453
+ if (diagnostic.code || !diagnostic.state || !diagnostic.value)
1454
+ throw new RepairError(diagnostic.code ?? "DI_INTERNAL_ERROR", 3);
1455
+ const { legacy, state, value } = diagnostic;
1456
+ const revision =
1457
+ typeof value.state_revision === "number" && Number.isSafeInteger(value.state_revision) ? value.state_revision : 0;
1458
+ const rounds = Array.isArray(state.rounds) ? state.rounds.map(asRecord) : [];
1459
+ let data: Record<string, unknown>;
1460
+ let collection: Record<string, unknown>[] = [];
1461
+ if (selector === "summary")
1462
+ data = {
1463
+ interview_id: nullableId(state.interview_id),
1464
+ type: state.type === "greenfield" || state.type === "brownfield" ? state.type : null,
1465
+ initial_idea: nullableTextView(state.initial_idea),
1466
+ resolution: ["quick", "standard", "deep"].includes(String(value.resolution))
1467
+ ? value.resolution
1468
+ : ["quick", "standard", "deep"].includes(String(state.resolution))
1469
+ ? state.resolution
1470
+ : null,
1471
+ threshold: nullableScore(state.threshold),
1472
+ current_ambiguity: nullableScore(state.current_ambiguity),
1473
+ ambiguity_milestone: ["initial", "progress", "refined", "ready"].includes(String(state.ambiguity_milestone))
1474
+ ? state.ambiguity_milestone
1475
+ : null,
1476
+ topology_status: asRecord(state.topology).status === "confirmed" ? "confirmed" : "pending",
1477
+ state_revision: revision,
1478
+ };
1479
+ else if (selector === "round") {
1480
+ const round = rounds.find(value => value.round_key === parsed.flags.get("--round-key"));
1481
+ if (!round) throw new RepairError("DI_ROUND_NOT_FOUND", 3);
1482
+ data = { item: roundProjection(round) };
1483
+ } else if (selector === "topology") {
1484
+ const topology = asRecord(state.topology);
1485
+ const deferredComponents = (
1486
+ Array.isArray(topology.deferred_components) ? topology.deferred_components : []
1487
+ ).filter((value): value is string => typeof value === "string" && ID.test(value));
1488
+ const deferredComponentIds = new Set(deferredComponents);
1489
+ data = {
1490
+ status: topology.status === "confirmed" ? "confirmed" : "pending",
1491
+ confirmed_at: typeof topology.confirmed_at === "string" ? topology.confirmed_at : null,
1492
+ components: (Array.isArray(topology.components) ? topology.components : [])
1493
+ .filter(component => {
1494
+ const item = asRecord(component);
1495
+ return item.status !== "deferred" && !deferredComponentIds.has(String(item.id));
1496
+ })
1497
+ .map(component => {
1498
+ const item = asRecord(component);
1499
+ const scores = asRecord(item.clarity_scores);
1500
+ return {
1501
+ id: String(item.id),
1502
+ name: textView(item.name),
1503
+ description: nullableTextView(item.description),
1504
+ active: item.active !== false,
1505
+ deferred: false,
1506
+ scores: {
1507
+ goal: nullableScore(scores.goal),
1508
+ constraints: nullableScore(scores.constraints),
1509
+ criteria: nullableScore(scores.criteria),
1510
+ context: state.type === "greenfield" ? null : nullableScore(scores.context),
1511
+ },
1512
+ weakest_dimension: ["goal", "constraints", "criteria", "context"].includes(
1513
+ String(item.weakest_dimension),
1514
+ )
1515
+ ? item.weakest_dimension
1516
+ : null,
1517
+ };
1518
+ }),
1519
+ deferrals: deferredComponents.map(component_id => ({
1520
+ component_id,
1521
+ reason: textView(""),
1522
+ created_at: typeof topology.confirmed_at === "string" ? topology.confirmed_at : "",
1523
+ until_round: null,
1524
+ })),
1525
+ last_targeted_component_id: nullableId(topology.last_targeted_component_id),
1526
+ };
1527
+ } else if (selector === "floor") {
1528
+ const floor = computeAmbiguityFloor(state);
1529
+ data = {
1530
+ floor: floor.floor,
1531
+ disputed_fact_count: floor.disputed_fact_count,
1532
+ unscored_active_component_count: floor.unscored_active_component_count,
1533
+ auto_answer_ratio: floor.auto_answer_ratio,
1534
+ weighted_ambiguity: nullableScore(state.weighted_ambiguity),
1535
+ effective_ambiguity: nullableScore(state.effective_ambiguity),
1536
+ };
1537
+ } else {
1538
+ if (selector === "recent-scored")
1539
+ collection = rounds.filter(round => round.lifecycle === "scored").map(round => roundProjection(round));
1540
+ else if (selector === "pending")
1541
+ collection = rounds
1542
+ .filter(round => round.lifecycle === "answered" || round.lifecycle === "pending_scoring")
1543
+ .map(round => roundProjection(round, true));
1544
+ else if (selector === "facts")
1545
+ collection = (Array.isArray(state.established_facts) ? state.established_facts : []).map(
1546
+ (value, insertion_index) => {
1547
+ const fact = asRecord(value);
1548
+ return {
1549
+ id: String(fact.id),
1550
+ status: fact.disputed === true ? (fact.superseded_by ? "resolved" : "disputed") : "established",
1551
+ source_round: Number(fact.round),
1552
+ component_id: nullableId(fact.component),
1553
+ dimension: ["goal", "constraints", "criteria", "context"].includes(String(fact.dimension))
1554
+ ? fact.dimension
1555
+ : null,
1556
+ statement: textView(fact.statement),
1557
+ evidence: nullableTextView(fact.evidence),
1558
+ resolution_reason: nullableTextView(fact.resolution_reason),
1559
+ superseded_by: nullableId(fact.superseded_by),
1560
+ insertion_index,
1561
+ };
1562
+ },
1563
+ );
1564
+ else
1565
+ collection = rounds.flatMap(round =>
1566
+ (Array.isArray(round.triggers) ? round.triggers : []).map((value, insertion_index) => {
1567
+ const trigger = asRecord(value);
1568
+ return {
1569
+ kind: trigger.kind,
1570
+ name: textView(trigger.name),
1571
+ status: trigger.status,
1572
+ source_round: Number(round.round),
1573
+ source_round_key: String(round.round_key),
1574
+ component_id: String(trigger.component),
1575
+ dimension: trigger.dimension,
1576
+ prior_dimension_score:
1577
+ trigger.status === "active" ? nullableScore(trigger.prior_dimension_score) : null,
1578
+ new_dimension_score: trigger.status === "active" ? nullableScore(trigger.new_dimension_score) : null,
1579
+ prior_effective_ambiguity:
1580
+ trigger.status === "active" ? nullableScore(trigger.prior_effective_ambiguity) : null,
1581
+ new_effective_ambiguity:
1582
+ trigger.status === "active" ? nullableScore(trigger.new_effective_ambiguity) : null,
1583
+ evidence: nullableTextView(trigger.evidence),
1584
+ rationale: nullableTextView(trigger.rationale),
1585
+ contradicted_fact_id: nullableId(trigger.contradictedFactId),
1586
+ insertion_index,
1587
+ };
1588
+ }),
1589
+ );
1590
+ collection.sort((a, b) =>
1591
+ compareKeys(
1592
+ selector,
1593
+ sortKey(selector, a, Number(a.insertion_index ?? 0)),
1594
+ sortKey(selector, b, Number(b.insertion_index ?? 0)),
1595
+ ),
1596
+ );
1597
+ const view_sha256 = createHash("sha256").update(canonicalDeepInterviewJson(collection)).digest("hex");
1598
+ const offset = cursorOffset(parsed.flags.get("--cursor"), selector, revision, view_sha256, collection);
1599
+ const limit = parsed.flags.has("--limit") ? safeInt(parsed.flags.get("--limit"), "DI_INVALID_LIMIT", true) : 10;
1600
+ if (limit > 25) throw new RepairError("DI_INVALID_LIMIT");
1601
+ const items: Record<string, unknown>[] = [];
1602
+ for (const item of collection.slice(offset, offset + limit)) {
1603
+ if (Buffer.byteLength(JSON.stringify({ items: [...items, item] })) > DATA_LIMIT) break;
1604
+ items.push(item);
1605
+ }
1606
+ if (!items.length && offset < collection.length) throw new RepairError("DI_OUTPUT_LIMIT_EXCEEDED", 3);
1607
+ data = { items };
1608
+ const omitted = offset + items.length < collection.length;
1609
+ const next_cursor = omitted
1610
+ ? cursorFor(
1611
+ selector,
1612
+ revision,
1613
+ view_sha256,
1614
+ sortKey(selector, items.at(-1)!, Number(items.at(-1)!.insertion_index ?? 0)),
1615
+ )
1616
+ : null;
1617
+ const response = {
1618
+ ok: true,
1619
+ command: "inspect",
1620
+ schema_version: 1,
1621
+ state_path: path,
1622
+ state_revision: revision,
1623
+ content_sha256: legacy ? null : workflowEnvelopeContentSha256(value),
1624
+ view_sha256,
1625
+ limits_version: 1,
1626
+ data,
1627
+ returned_count: items.length,
1628
+ total_count: collection.length,
1629
+ bytes_returned: Buffer.byteLength(JSON.stringify(data)),
1630
+ truncated: omitted,
1631
+ next_cursor,
1632
+ };
1633
+ if (Buffer.byteLength(JSON.stringify(response)) > RESPONSE_LIMIT)
1634
+ throw new RepairError("DI_OUTPUT_LIMIT_EXCEEDED", 3);
1635
+ return { status: 0, stdout: `${JSON.stringify(response)}\n` };
1636
+ }
1637
+ const bytes_returned = Buffer.byteLength(JSON.stringify(data));
1638
+ if (bytes_returned > DATA_LIMIT) throw new RepairError("DI_OUTPUT_LIMIT_EXCEEDED", 3);
1639
+ const response = {
1640
+ ok: true,
1641
+ command: "inspect",
1642
+ schema_version: 1,
1643
+ state_path: path,
1644
+ state_revision: revision,
1645
+ content_sha256: legacy ? null : workflowEnvelopeContentSha256(value),
1646
+ view_sha256: createHash("sha256").update(canonicalDeepInterviewJson(data)).digest("hex"),
1647
+ limits_version: 1,
1648
+ data,
1649
+ returned_count: 1,
1650
+ total_count: 1,
1651
+ bytes_returned,
1652
+ truncated: false,
1653
+ next_cursor: null,
1654
+ };
1655
+ if (Buffer.byteLength(JSON.stringify(response)) > RESPONSE_LIMIT)
1656
+ throw new RepairError("DI_OUTPUT_LIMIT_EXCEEDED", 3);
1657
+ return { status: 0, stdout: `${JSON.stringify(response)}\n` };
1658
+ }
1659
+ async function sanity(parsed: ParsedRepairCommand, cwd: string): Promise<DeepInterviewRepairResult> {
1660
+ const { session } = validateMutation(parsed, ["--session-id"], false);
1661
+ const path = statePath(cwd, session);
1662
+ const diagnostic = diagnoseDeepInterviewState(await readExistingStateForMutation(path), path);
1663
+ const issues = diagnostic.code ? [issue(diagnostic.code)] : [];
1664
+ return {
1665
+ status: 0,
1666
+ stdout: `${JSON.stringify({
1667
+ ok: true,
1668
+ command: "sanity-check",
1669
+ healthy: issues.length === 0,
1670
+ issues,
1671
+ limits_version: 1,
1672
+ })}\n`,
1673
+ };
1674
+ }