supipowers 2.2.0 → 2.2.1

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.
@@ -3,7 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import type { Platform } from "../platform/types.js";
5
5
  import type { WorkspaceTarget } from "../types.js";
6
- import { buildWorkspaceTargetOptionLabel, parseTargetArg, selectWorkspaceTarget, stripCliArg, tokenizeCliArgs } from "../workspace/selector.js";
6
+ import { buildWorkspaceTargetOptionLabel, parseTargetArg, stripCliArg, tokenizeCliArgs } from "../workspace/selector.js";
7
7
  import { resolvePackageManager } from "../workspace/package-manager.js";
8
8
  import { resolveRepoRoot } from "../workspace/repo-root.js";
9
9
  import { discoverWorkspaceTargets } from "../workspace/targets.js";
@@ -31,6 +31,45 @@ import { loadModelConfig } from "../config/model-config.js";
31
31
  import { detectBotReviewers } from "../fix-pr/bot-detector.js";
32
32
  import { runFixPrAssessment, groupAssessmentsIntoBatches } from "../fix-pr/assessment.js";
33
33
  import { updateFixPrSession } from "../storage/fix-pr-sessions.js";
34
+ import { createWorkflowProgress } from "../platform/progress.js";
35
+
36
+
37
+ const FIX_PR_ASSESSMENT_TIMEOUT_MS = 5 * 60 * 1_000;
38
+ const FIX_PR_ASSESSMENT_COMMENTS_PER_BATCH = 12;
39
+
40
+ const FIX_PR_STEPS = [
41
+ { key: "fetch-comments", label: "Fetch PR comments" },
42
+ { key: "select-target", label: "Select target" },
43
+ { key: "prepare-session", label: "Prepare session" },
44
+ { key: "llm-assessment", label: "LLM assessment" },
45
+ { key: "send-prompt", label: "Send fix prompt" },
46
+ ] as const;
47
+
48
+ function createFixPrProgress(ctx: any) {
49
+ const progress = createWorkflowProgress(ctx.ui, {
50
+ title: "supi:fix-pr",
51
+ statusKey: "supi-fix-pr",
52
+ statusLabel: "Fixing PR...",
53
+ widgetKey: "supi-fix-pr",
54
+ clearStatusKeys: ["supi-model"],
55
+ steps: [...FIX_PR_STEPS],
56
+ });
57
+
58
+ return {
59
+ activate(stepIndex: number, detail?: string): void {
60
+ progress.activate(FIX_PR_STEPS[stepIndex]!.key, detail);
61
+ },
62
+ complete(stepIndex: number, detail?: string): void {
63
+ progress.complete(FIX_PR_STEPS[stepIndex]!.key, detail);
64
+ },
65
+ fail(stepIndex: number, detail?: string): void {
66
+ progress.fail(FIX_PR_STEPS[stepIndex]!.key, detail);
67
+ },
68
+ dispose(): void {
69
+ progress.dispose();
70
+ },
71
+ };
72
+ }
34
73
 
35
74
  modelRegistry.register({
36
75
  id: "fix-pr",
@@ -47,6 +86,7 @@ modelRegistry.register({
47
86
  harnessRoleHint: "default",
48
87
  });
49
88
 
89
+
50
90
  function getScriptsDir(): string {
51
91
  return path.join(moduleDir(import.meta.url), "..", "fix-pr", "scripts");
52
92
  }
@@ -91,6 +131,12 @@ function formatUnscopedCommentCount(count: number): string {
91
131
  return `${formatCommentCount(count)} without file path`;
92
132
  }
93
133
 
134
+ type FixPrSelectedTarget =
135
+ | { kind: "workspace"; target: WorkspaceTarget }
136
+ | { kind: "all"; target: WorkspaceTarget };
137
+
138
+ const ALL_FIX_PR_TARGET_ID = "all";
139
+
94
140
  function buildCommentTargetOptions(
95
141
  targets: readonly WorkspaceTarget[],
96
142
  commentsByTargetId: ReadonlyMap<string, readonly PrComment[]>,
@@ -109,6 +155,43 @@ function buildCommentTargetOptions(
109
155
  });
110
156
  }
111
157
 
158
+ function createAllCommentsTarget(repoRoot: string, packageManager: WorkspaceTarget["packageManager"]): WorkspaceTarget {
159
+ return {
160
+ id: ALL_FIX_PR_TARGET_ID,
161
+ name: "all",
162
+ kind: "workspace",
163
+ repoRoot,
164
+ packageDir: repoRoot,
165
+ manifestPath: path.join(repoRoot, "package.json"),
166
+ relativeDir: ALL_FIX_PR_TARGET_ID,
167
+ version: "0.0.0",
168
+ private: true,
169
+ packageManager,
170
+ };
171
+ }
172
+
173
+ function describeFixPrSelectedTarget(selected: FixPrSelectedTarget): string {
174
+ return selected.kind === "all" ? "all targets" : describeTarget(selected.target);
175
+ }
176
+
177
+ function getSelectedTargetComments(
178
+ selected: FixPrSelectedTarget,
179
+ commentsByTargetId: ReadonlyMap<string, readonly PrComment[]>,
180
+ unscopedComments: readonly PrComment[],
181
+ ): readonly PrComment[] {
182
+ if (selected.kind === "workspace") {
183
+ return commentsByTargetId.get(selected.target.id) ?? [];
184
+ }
185
+
186
+ const comments: PrComment[] = [];
187
+ for (const targetComments of commentsByTargetId.values()) {
188
+ comments.push(...targetComments);
189
+ }
190
+ comments.push(...unscopedComments);
191
+ return comments;
192
+ }
193
+
194
+
112
195
  function countUnresolvedAssessments(assessment: FixPrAssessmentBatch): number {
113
196
  return assessment.assessments.filter((item: FixPrAssessmentBatch["assessments"][number]) => item.verdict !== "apply").length;
114
197
  }
@@ -211,14 +294,18 @@ export function registerFixPrCommand(platform: Platform): void {
211
294
  return;
212
295
  }
213
296
 
297
+ const progress = createFixPrProgress(ctx);
214
298
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "supi-fix-pr-"));
215
299
  try {
300
+ progress.activate(0, `PR #${prNumber}`);
216
301
  const fetchedCommentsPath = path.join(tempDir, "comments.jsonl");
217
302
  const fetchError = await fetchPrComments(platform, repo, prNumber, fetchedCommentsPath, repoRoot);
218
303
  if (fetchError) {
304
+ progress.fail(0, "failed");
219
305
  notifyError(ctx, "Failed to fetch PR comments", fetchError);
220
306
  return;
221
307
  }
308
+ progress.complete(0, `PR #${prNumber}`);
222
309
 
223
310
  const fetchedComments = fs.readFileSync(fetchedCommentsPath, "utf-8").trim();
224
311
  if (!fetchedComments) {
@@ -238,36 +325,74 @@ export function registerFixPrCommand(platform: Platform): void {
238
325
  clusteredComments.commentsByTargetId,
239
326
  );
240
327
 
241
- if (targetOptions.length === 0) {
242
- const detail = clusteredComments.unscopedComments.length > 0
243
- ? `PR comments were fetched but only ${formatUnscopedCommentCount(clusteredComments.unscopedComments.length)} could not be assigned to a workspace target`
244
- : "PR comments were fetched but could not be assigned to a package or root target";
245
- notifyWarning(ctx, "No actionable comments found", detail);
328
+ const allTarget = createAllCommentsTarget(repoRoot, packageManager.id);
329
+ const allOption = {
330
+ target: allTarget,
331
+ changed: true,
332
+ label: buildWorkspaceTargetOptionLabel(
333
+ { target: allTarget, changed: true },
334
+ [formatCommentCount(parsedComments.length)],
335
+ ),
336
+ };
337
+
338
+ if (targetOptions.length === 0 && clusteredComments.unscopedComments.length === 0) {
339
+ notifyWarning(ctx, "No actionable comments found", "PR comments were fetched but could not be assigned to a package or root target");
246
340
  return;
247
341
  }
248
342
 
249
- if (!requestedTarget && !ctx.hasUI && targetOptions.length > 1) {
250
- notifyError(ctx, "Multiple comment targets found", buildAvailableTargetDetail(targetOptions, clusteredComments.unscopedComments.length));
251
- return;
343
+ progress.activate(1, `${formatCommentCount(parsedComments.length)}`);
344
+ let selected: FixPrSelectedTarget | null = null;
345
+ if (requestedTarget === ALL_FIX_PR_TARGET_ID) {
346
+ selected = { kind: "all", target: allTarget };
347
+ } else if (requestedTarget) {
348
+ const target = targetOptions
349
+ .map((option) => option.target)
350
+ .find((optionTarget) => optionTarget.id === requestedTarget || optionTarget.name === requestedTarget);
351
+ selected = target ? { kind: "workspace", target } : null;
352
+ } else if (!ctx.hasUI) {
353
+ selected = { kind: "all", target: allTarget };
354
+ } else {
355
+ const labels = [
356
+ allOption.label,
357
+ ...targetOptions.map((option) => option.label ?? buildWorkspaceTargetOptionLabel(option)),
358
+ ];
359
+ const choice = await ctx.ui.select("Fix-PR target", labels, {
360
+ helpText: "Select all comments or one target to process for this run",
361
+ });
362
+ if (!choice) {
363
+ progress.fail(1, "cancelled");
364
+ return;
365
+ }
366
+ const selectedIndex = labels.indexOf(choice);
367
+ selected = selectedIndex === 0
368
+ ? { kind: "all", target: allTarget }
369
+ : targetOptions[selectedIndex - 1]
370
+ ? { kind: "workspace", target: targetOptions[selectedIndex - 1].target }
371
+ : null;
252
372
  }
253
373
 
254
- const selectedTarget = await selectWorkspaceTarget(ctx, targetOptions, requestedTarget, {
255
- title: "Fix-PR target",
256
- helpText: "Select one target to process for this run",
257
- });
258
- if (!selectedTarget) {
374
+ if (!selected) {
259
375
  if (requestedTarget) {
260
376
  notifyError(ctx, "Target has no review comments", buildAvailableTargetDetail(targetOptions, clusteredComments.unscopedComments.length));
261
377
  }
378
+ progress.fail(1, "empty");
262
379
  return;
263
380
  }
381
+ progress.complete(1, describeFixPrSelectedTarget(selected));
264
382
 
265
- const selectedComments = clusteredComments.commentsByTargetId.get(selectedTarget.id) ?? [];
383
+ const selectedTarget = selected.target;
384
+ const selectedComments = getSelectedTargetComments(
385
+ selected,
386
+ clusteredComments.commentsByTargetId,
387
+ clusteredComments.unscopedComments,
388
+ );
266
389
  if (selectedComments.length === 0) {
267
390
  notifyInfo(ctx, "No comments for selected target", `${selectedTarget.id} has no actionable comments in this PR snapshot`);
268
391
  return;
269
392
  }
270
393
 
394
+ progress.activate(2, `${formatCommentCount(selectedComments.length)}`);
395
+
271
396
  let activeSession = findActiveFixPrSession(platform.paths, selectedTarget, repo, prNumber);
272
397
  if (activeSession && ctx.hasUI) {
273
398
  const choice = await ctx.ui.select(
@@ -278,7 +403,10 @@ export function registerFixPrCommand(platform: Platform): void {
278
403
  ],
279
404
  { helpText: "Select session · Esc to cancel" },
280
405
  );
281
- if (!choice) return;
406
+ if (!choice) {
407
+ progress.fail(2, "cancelled");
408
+ return;
409
+ }
282
410
  if (choice.startsWith("Start new")) activeSession = null;
283
411
  }
284
412
 
@@ -328,12 +456,17 @@ export function registerFixPrCommand(platform: Platform): void {
328
456
 
329
457
  const taskResolved = resolveModelForAction("task", modelRegistry, modelConfig, bridge);
330
458
  const taskModel = taskResolved.model ?? resolved.model ?? "claude-sonnet-4-6";
331
- const deferredCommentsSummary = buildDeferredCommentsSummary(
332
- targetOptions,
333
- clusteredComments.commentsByTargetId,
334
- selectedTarget,
335
- clusteredComments.unscopedComments.length,
336
- );
459
+ const deferredCommentsSummary = selected.kind === "all"
460
+ ? null
461
+ : buildDeferredCommentsSummary(
462
+ targetOptions,
463
+ clusteredComments.commentsByTargetId,
464
+ selectedTarget,
465
+ clusteredComments.unscopedComments.length,
466
+ );
467
+ progress.complete(2, activeSession ? `resume ${ledger.id}` : `new ${ledger.id}`);
468
+
469
+ progress.activate(3, `${formatCommentCount(selectedComments.length)}`);
337
470
 
338
471
  const assessmentResult = await runFixPrAssessment({
339
472
  createAgentSession: platform.createAgentSession,
@@ -342,17 +475,22 @@ export function registerFixPrCommand(platform: Platform): void {
342
475
  comments: selectedComments,
343
476
  repo,
344
477
  prNumber,
345
- selectedTargetLabel: describeTarget(selectedTarget),
478
+ selectedTargetLabel: describeFixPrSelectedTarget(selected),
346
479
  model: resolved.model,
347
480
  thinkingLevel: resolved.thinkingLevel,
481
+ timeoutMs: FIX_PR_ASSESSMENT_TIMEOUT_MS,
482
+ maxCommentsPerBatch: FIX_PR_ASSESSMENT_COMMENTS_PER_BATCH,
348
483
  });
349
484
  if (assessmentResult.status === "blocked") {
485
+ progress.fail(3, "blocked");
350
486
  notifyError(ctx, "Fix-PR assessment failed", assessmentResult.error);
351
487
  return;
352
488
  }
489
+ progress.complete(3, `${formatCommentCount(assessmentResult.output.assessments.length)} assessed`);
353
490
  const assessment = assessmentResult.output;
354
491
  const unresolvedAssessmentCount = countUnresolvedAssessments(assessment);
355
492
  const workBatches = groupAssessmentsIntoBatches(assessment);
493
+ progress.activate(4, `${workBatches.length} batch${workBatches.length === 1 ? "" : "es"}`);
356
494
  ledger.assessment = assessment;
357
495
  updateFixPrSession(platform.paths, selectedTarget, ledger);
358
496
 
@@ -360,7 +498,7 @@ export function registerFixPrCommand(platform: Platform): void {
360
498
  notifyWarning(
361
499
  ctx,
362
500
  "Unresolved comments remain",
363
- `${formatCommentCount(unresolvedAssessmentCount)} for ${describeTarget(selectedTarget)} still need rejection or investigation handling before this run can be considered complete.`,
501
+ `${formatCommentCount(unresolvedAssessmentCount)} for ${describeFixPrSelectedTarget(selected)} still need rejection or investigation handling before this run can be considered complete.`,
364
502
  );
365
503
  }
366
504
 
@@ -374,7 +512,7 @@ export function registerFixPrCommand(platform: Platform): void {
374
512
  iteration: ledger.iteration,
375
513
  skillContent,
376
514
  taskModel,
377
- selectedTargetLabel: describeTarget(selectedTarget),
515
+ selectedTargetLabel: describeFixPrSelectedTarget(selected),
378
516
  deferredCommentsSummary,
379
517
  assessment,
380
518
  workBatches,
@@ -388,14 +526,16 @@ export function registerFixPrCommand(platform: Platform): void {
388
526
  },
389
527
  { deliverAs: "steer", triggerTurn: true },
390
528
  );
529
+ progress.complete(4, "sent");
391
530
 
392
- const detailParts = [`${formatCommentCount(selectedComments.length)} for ${describeTarget(selectedTarget)}`];
531
+ const detailParts = [`${formatCommentCount(selectedComments.length)} for ${describeFixPrSelectedTarget(selected)}`];
393
532
  if (deferredCommentsSummary) {
394
533
  detailParts.push(`deferred: ${deferredCommentsSummary}`);
395
534
  }
396
535
  notifyInfo(ctx, `Fix-PR started: PR #${prNumber}`, `${detailParts.join(" | ")} | session ${ledger.id}`);
397
536
  } finally {
398
537
  fs.rmSync(tempDir, { recursive: true, force: true });
538
+ progress.dispose();
399
539
  }
400
540
  },
401
541
  });
@@ -1,176 +1,142 @@
1
- import type { TSchema } from "@sinclair/typebox";
2
- import { Type } from "@sinclair/typebox";
3
- import { Value } from "@sinclair/typebox/value";
1
+ import { z } from "zod/v4";
2
+ import type { ZodType } from "zod/v4";
4
3
  import type { SupipowersConfig } from "../types.js";
5
4
  import { QualityGatesSchema } from "../quality/schemas.js";
6
5
  import { UltraPlanConfigSchema } from "../ultraplan/contracts.js";
6
+ import { collectSchemaValidationErrors } from "../ai/schema-validation.js";
7
7
 
8
8
  const TAG_FORMAT_PATTERN = "^(?:(?!\\$\\{version\\}).)*\\$\\{version\\}(?:(?!\\$\\{version\\}).)*$";
9
9
 
10
10
 
11
- export const ConfigSchema = Type.Object(
11
+ export const ConfigSchema = z.object(
12
12
  {
13
- version: Type.String(),
14
- quality: Type.Object(
13
+ version: z.string(),
14
+ quality: z.object(
15
15
  {
16
16
  gates: QualityGatesSchema,
17
17
  },
18
- { additionalProperties: false },
19
- ),
20
- lsp: Type.Object(
18
+ ).strict(),
19
+ lsp: z.object(
21
20
  {
22
- setupGuide: Type.Boolean(),
21
+ setupGuide: z.boolean(),
23
22
  },
24
- { additionalProperties: false },
25
- ),
26
- qa: Type.Object(
23
+ ).strict(),
24
+ qa: z.object(
27
25
  {
28
- framework: Type.Union([Type.String(), Type.Null()]),
29
- e2e: Type.Boolean(),
26
+ framework: z.string().nullable(),
27
+ e2e: z.boolean(),
30
28
  },
31
- { additionalProperties: false },
32
- ),
33
- release: Type.Object(
29
+ ).strict(),
30
+ release: z.object(
34
31
  {
35
- channels: Type.Array(Type.String()),
36
- tagFormat: Type.String({ pattern: TAG_FORMAT_PATTERN }),
37
- customChannels: Type.Optional(
38
- Type.Record(
39
- Type.String(),
40
- Type.Object({
41
- label: Type.String(),
42
- publishCommand: Type.String(),
43
- detectCommand: Type.Optional(Type.String()),
44
- }),
45
- ),
46
- ),
32
+ channels: z.array(z.string()),
33
+ tagFormat: z.string().regex(new RegExp(TAG_FORMAT_PATTERN)),
34
+ customChannels: z.record(
35
+ z.string(),
36
+ z.object({
37
+ label: z.string(),
38
+ publishCommand: z.string(),
39
+ detectCommand: z.string().optional(),
40
+ }),
41
+ ).optional(),
47
42
  },
48
- { additionalProperties: false },
49
- ),
43
+ ).strict(),
50
44
  ultraplan: UltraPlanConfigSchema,
51
- contextMode: Type.Object(
45
+ contextMode: z.object(
52
46
  {
53
- enabled: Type.Boolean(),
54
- compressionThreshold: Type.Number({ minimum: 1024 }),
55
- blockHttpCommands: Type.Boolean(),
56
- routingInstructions: Type.Boolean(),
57
- eventTracking: Type.Boolean(),
58
- compaction: Type.Boolean(),
59
- llmSummarization: Type.Boolean(),
60
- llmThreshold: Type.Number({ minimum: 4096 }),
61
- enforceRouting: Type.Boolean(),
62
- lazyTools: Type.Object(
47
+ enabled: z.boolean(),
48
+ compressionThreshold: z.number().min(1024),
49
+ blockHttpCommands: z.boolean(),
50
+ routingInstructions: z.boolean(),
51
+ eventTracking: z.boolean(),
52
+ compaction: z.boolean(),
53
+ llmSummarization: z.boolean(),
54
+ llmThreshold: z.number().min(4096),
55
+ enforceRouting: z.boolean(),
56
+ lazyTools: z.object(
63
57
  {
64
- enabled: Type.Boolean(),
65
- mode: Type.Union([
66
- Type.Literal("conservative"),
67
- Type.Literal("balanced"),
68
- Type.Literal("aggressive"),
69
- ]),
70
- alwaysKeep: Type.Array(Type.String()),
71
- commandAllowlist: Type.Record(Type.String(), Type.Array(Type.String())),
72
- keywordTools: Type.Record(Type.String(), Type.Array(Type.String())),
58
+ enabled: z.boolean(),
59
+ mode: z.enum(["conservative", "balanced", "aggressive"]),
60
+ alwaysKeep: z.array(z.string()),
61
+ commandAllowlist: z.record(z.string(), z.array(z.string())),
62
+ keywordTools: z.record(z.string(), z.array(z.string())),
73
63
  },
74
- { additionalProperties: false },
75
- ),
76
- processors: Type.Object(
64
+ ).strict(),
65
+ processors: z.object(
77
66
  {
78
- enabled: Type.Boolean(),
79
- disable: Type.Array(
80
- Type.Union([
81
- Type.Literal("git"),
82
- Type.Literal("test"),
83
- Type.Literal("lint"),
84
- Type.Literal("build"),
85
- Type.Literal("k8s"),
86
- Type.Literal("docker"),
87
- Type.Literal("log"),
88
- Type.Literal("json"),
89
- ]),
67
+ enabled: z.boolean(),
68
+ disable: z.array(
69
+ z.enum(["git", "test", "lint", "build", "k8s", "docker", "log", "json"]),
90
70
  ),
91
71
  },
92
- { additionalProperties: false },
93
- ),
94
- cacheHandles: Type.Object(
72
+ ).strict(),
73
+ cacheHandles: z.object(
95
74
  {
96
- enabled: Type.Boolean(),
97
- spillThresholdBytes: Type.Number({ minimum: 1024 }),
98
- previewBytes: Type.Number({ minimum: 256 }),
75
+ enabled: z.boolean(),
76
+ spillThresholdBytes: z.number().min(1024),
77
+ previewBytes: z.number().min(256),
99
78
  },
100
- { additionalProperties: false },
101
- ),
102
- repomap: Type.Object(
79
+ ).strict(),
80
+ repomap: z.object(
103
81
  {
104
- enabled: Type.Boolean(),
105
- tokenBudget: Type.Number({ minimum: 100 }),
106
- maxFiles: Type.Number({ minimum: 1 }),
82
+ enabled: z.boolean(),
83
+ tokenBudget: z.number().min(100),
84
+ maxFiles: z.number().min(1),
107
85
  },
108
- { additionalProperties: false },
109
- ),
110
- memory: Type.Object(
86
+ ).strict(),
87
+ memory: z.object(
111
88
  {
112
- enabled: Type.Boolean(),
113
- byteBudget: Type.Number({ minimum: 256 }),
114
- maxRows: Type.Number({ minimum: 1 }),
115
- retentionDays: Type.Number({ minimum: 1 }),
116
- focusChainCadence: Type.Integer({ minimum: 1 }),
89
+ enabled: z.boolean(),
90
+ byteBudget: z.number().min(256),
91
+ maxRows: z.number().min(1),
92
+ retentionDays: z.number().min(1),
93
+ focusChainCadence: z.number().int().min(1),
117
94
  },
118
- { additionalProperties: false },
119
- ),
95
+ ).strict(),
120
96
  },
121
- { additionalProperties: false },
122
- ),
123
- mempalace: Type.Object(
97
+ ).strict(),
98
+ mempalace: z.object(
124
99
  {
125
- enabled: Type.Boolean(),
126
- packageVersion: Type.String({ minLength: 1 }),
127
- managedVenvPath: Type.String({ minLength: 1 }),
128
- palacePath: Type.String({ minLength: 1 }),
129
- defaultWingStrategy: Type.Union([
130
- Type.Literal("repo-name"),
131
- Type.Literal("project-slug"),
132
- Type.Literal("explicit"),
133
- ]),
134
- explicitWing: Type.Union([Type.String(), Type.Null()]),
135
- defaultAgentName: Type.String({ minLength: 1 }),
136
- autoSetup: Type.Boolean(),
137
- hooks: Type.Object(
100
+ enabled: z.boolean(),
101
+ packageVersion: z.string().min(1),
102
+ managedVenvPath: z.string().min(1),
103
+ palacePath: z.string().min(1),
104
+ defaultWingStrategy: z.enum(["repo-name", "project-slug", "explicit"]),
105
+ explicitWing: z.string().nullable(),
106
+ defaultAgentName: z.string().min(1),
107
+ autoSetup: z.boolean(),
108
+ hooks: z.object(
138
109
  {
139
- wakeUp: Type.Boolean(),
140
- searchGuidance: Type.Boolean(),
141
- autoSearchOnPrompt: Type.Boolean(),
142
- compactionCheckpoint: Type.Boolean(),
143
- shutdownDiary: Type.Boolean(),
110
+ wakeUp: z.boolean(),
111
+ searchGuidance: z.boolean(),
112
+ autoSearchOnPrompt: z.boolean(),
113
+ compactionCheckpoint: z.boolean(),
114
+ shutdownDiary: z.boolean(),
144
115
  },
145
- { additionalProperties: false },
146
- ),
147
- budgets: Type.Object(
116
+ ).strict(),
117
+ budgets: z.object(
148
118
  {
149
- wakeUpTokens: Type.Integer({ minimum: 1 }),
150
- searchResultChars: Type.Integer({ minimum: 1 }),
151
- listResultChars: Type.Integer({ minimum: 1 }),
152
- diaryChars: Type.Integer({ minimum: 1 }),
153
- autoSearchTokens: Type.Integer({ minimum: 1 }),
154
- wakeUpInjectionEvery: Type.Integer({ minimum: 1 }),
155
- autoSearchSimilarityFloor: Type.Number({ minimum: 0, maximum: 1 }),
156
- autoSearchBm25Floor: Type.Number({ minimum: 0 }),
119
+ wakeUpTokens: z.number().int().min(1),
120
+ searchResultChars: z.number().int().min(1),
121
+ listResultChars: z.number().int().min(1),
122
+ diaryChars: z.number().int().min(1),
123
+ autoSearchTokens: z.number().int().min(1),
124
+ wakeUpInjectionEvery: z.number().int().min(1),
125
+ autoSearchSimilarityFloor: z.number().min(0).max(1),
126
+ autoSearchBm25Floor: z.number().min(0),
157
127
  },
158
- { additionalProperties: false },
159
- ),
160
- timeouts: Type.Object(
128
+ ).strict(),
129
+ timeouts: z.object(
161
130
  {
162
- setupMs: Type.Integer({ minimum: 1 }),
163
- bridgeMs: Type.Integer({ minimum: 1 }),
164
- hookMs: Type.Integer({ minimum: 1 }),
131
+ setupMs: z.number().int().min(1),
132
+ bridgeMs: z.number().int().min(1),
133
+ hookMs: z.number().int().min(1),
165
134
  },
166
- { additionalProperties: false },
167
- ),
135
+ ).strict(),
168
136
  },
169
- { additionalProperties: false },
170
- ),
137
+ ).strict(),
171
138
  },
172
- { additionalProperties: false },
173
- );
139
+ ).strict();
174
140
 
175
141
  export interface ConfigParseError {
176
142
  source: "global" | "root";
@@ -190,13 +156,10 @@ export interface InspectionLoadResult {
190
156
  validationErrors: ConfigValidationError[];
191
157
  }
192
158
 
193
- function normalizeErrorPath(path: string): string {
194
- return path.replace(/^\//, "").replace(/\//g, ".") || "(root)";
195
- }
196
159
 
197
- function collectValidationErrors(schema: TSchema, data: unknown): ConfigValidationError[] {
198
- return [...Value.Errors(schema, data)].map((error) => ({
199
- path: normalizeErrorPath(error.path),
160
+ function collectValidationErrors(schema: ZodType, data: unknown): ConfigValidationError[] {
161
+ return collectSchemaValidationErrors(schema, data).map((error) => ({
162
+ path: error.path,
200
163
  message: error.message,
201
164
  }));
202
165
  }
@@ -5,7 +5,7 @@
5
5
  // retry loop (runWithOutputValidation) will hand validation errors back to
6
6
  // the model rather than letting a silent regex heuristic invent findings.
7
7
 
8
- import { Type, type Static } from "@sinclair/typebox";
8
+ import { z } from "zod/v4"
9
9
 
10
10
  export const DOC_DRIFT_SEVERITIES = ["info", "warning", "error"] as const;
11
11
  export const DOC_DRIFT_STATUSES = ["ok", "drifted"] as const;
@@ -13,27 +13,17 @@ export const DOC_DRIFT_STATUSES = ["ok", "drifted"] as const;
13
13
  export type DocDriftSeverity = (typeof DOC_DRIFT_SEVERITIES)[number];
14
14
  export type DocDriftStatus = (typeof DOC_DRIFT_STATUSES)[number];
15
15
 
16
- export const DocDriftFindingSchema = Type.Object(
17
- {
18
- file: Type.String({ minLength: 1 }),
19
- description: Type.String({ minLength: 1 }),
20
- severity: Type.Union(
21
- DOC_DRIFT_SEVERITIES.map((value) => Type.Literal(value)),
22
- ),
23
- relatedFiles: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
24
- },
25
- { additionalProperties: false },
26
- );
16
+ export const DocDriftFindingSchema = z.object({
17
+ file: z.string().min(1),
18
+ description: z.string().min(1),
19
+ severity: z.enum(DOC_DRIFT_SEVERITIES),
20
+ relatedFiles: z.array(z.string().min(1)).optional(),
21
+ }).strict();
27
22
 
28
- export const DocDriftOutputSchema = Type.Object(
29
- {
30
- findings: Type.Array(DocDriftFindingSchema),
31
- status: Type.Union(
32
- DOC_DRIFT_STATUSES.map((value) => Type.Literal(value)),
33
- ),
34
- },
35
- { additionalProperties: false },
36
- );
23
+ export const DocDriftOutputSchema = z.object({
24
+ findings: z.array(DocDriftFindingSchema),
25
+ status: z.enum(DOC_DRIFT_STATUSES),
26
+ }).strict();
37
27
 
38
- export type DocDriftFinding = Static<typeof DocDriftFindingSchema>;
39
- export type DocDriftOutput = Static<typeof DocDriftOutputSchema>;
28
+ export type DocDriftFinding = z.infer<typeof DocDriftFindingSchema>;
29
+ export type DocDriftOutput = z.infer<typeof DocDriftOutputSchema>;