supipowers 2.1.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.
Files changed (44) hide show
  1. package/README.md +71 -12
  2. package/package.json +4 -8
  3. package/skills/ui-design/SKILL.md +2 -2
  4. package/src/ai/final-message.ts +15 -1
  5. package/src/ai/schema-text.ts +60 -40
  6. package/src/ai/schema-validation.ts +88 -0
  7. package/src/ai/structured-output.ts +19 -19
  8. package/src/bootstrap.ts +3 -0
  9. package/src/commands/fix-pr.ts +166 -26
  10. package/src/commands/optimize-context.ts +153 -16
  11. package/src/commands/runbook.ts +511 -0
  12. package/src/config/schema.ts +102 -139
  13. package/src/context/rule-renderer.ts +274 -2
  14. package/src/context/runbook-extension-template.ts +193 -0
  15. package/src/context/startup-check.ts +197 -2
  16. package/src/context/startup-optimizer.ts +133 -10
  17. package/src/docs/contracts.ts +13 -23
  18. package/src/fix-pr/assessment.ts +63 -24
  19. package/src/fix-pr/contracts.ts +15 -23
  20. package/src/fix-pr/fetch-comments.ts +119 -0
  21. package/src/fix-pr/prompt-builder.ts +19 -8
  22. package/src/git/commit-contract.ts +13 -19
  23. package/src/git/commit.ts +168 -6
  24. package/src/harness/command.ts +98 -6
  25. package/src/harness/git-verification.ts +515 -0
  26. package/src/harness/git-verify-qa.ts +406 -0
  27. package/src/harness/pipeline.ts +17 -8
  28. package/src/harness/stages/implement-apply.ts +61 -4
  29. package/src/harness/stages/validate.ts +108 -0
  30. package/src/lsp/capabilities.ts +9 -12
  31. package/src/lsp/contracts.ts +15 -23
  32. package/src/planning/planning-ask-tool.ts +13 -2
  33. package/src/planning/spec.ts +21 -27
  34. package/src/planning/system-prompt.ts +1 -1
  35. package/src/planning/validate.ts +4 -7
  36. package/src/platform/progress.ts +11 -0
  37. package/src/quality/contracts.ts +15 -23
  38. package/src/quality/schemas.ts +40 -67
  39. package/src/release/contracts.ts +19 -28
  40. package/src/review/types.ts +142 -186
  41. package/src/types.ts +45 -2
  42. package/src/ui-design/session.ts +13 -2
  43. package/src/ui-design/system-prompt.ts +2 -2
  44. package/src/ultraplan/contracts.ts +458 -524
@@ -1,5 +1,5 @@
1
- import { Type } from "@sinclair/typebox";
2
- import { Value } from "@sinclair/typebox/value";
1
+ import { z } from "zod/v4";
2
+ import type { ZodType } from "zod/v4";
3
3
  import type {
4
4
  ConfiguredReviewAgent,
5
5
  ReviewAgentConfig,
@@ -18,6 +18,7 @@ import type {
18
18
  ReviewSessionArtifacts,
19
19
  ThinkingLevel,
20
20
  } from "../types.js";
21
+ import { checkSchema } from "../ai/schema-validation.js";
21
22
  export type {
22
23
  ConfiguredReviewAgent,
23
24
  ReviewAgentConfig,
@@ -45,226 +46,181 @@ export const REVIEW_SESSION_STATUSES = ["running", "completed", "blocked", "canc
45
46
  export const REVIEW_POST_CONSOLIDATION_ACTIONS = ["fix-now", "document-only", "discuss-before-fixing"] as const;
46
47
  export const REVIEW_FIX_STATUSES = ["applied", "skipped", "failed"] as const;
47
48
 
48
- export const ReviewScopeFileSchema = Type.Object(
49
- {
50
- path: Type.String({ minLength: 1 }),
51
- additions: Type.Number({ minimum: 0 }),
52
- deletions: Type.Number({ minimum: 0 }),
53
- diff: Type.String(),
54
- },
55
- { additionalProperties: false },
56
- );
57
-
58
- export const ReviewScopeStatsSchema = Type.Object(
59
- {
60
- filesChanged: Type.Number({ minimum: 0 }),
61
- excludedFiles: Type.Number({ minimum: 0 }),
62
- additions: Type.Number({ minimum: 0 }),
63
- deletions: Type.Number({ minimum: 0 }),
64
- },
65
- { additionalProperties: false },
66
- );
67
-
68
- export const ReviewScopeSchema = Type.Object(
69
- {
70
- mode: Type.Union(REVIEW_SCOPE_MODES.map((mode) => Type.Literal(mode))),
71
- description: Type.String({ minLength: 1 }),
72
- diff: Type.String(),
73
- files: Type.Array(ReviewScopeFileSchema),
74
- stats: ReviewScopeStatsSchema,
75
- baseBranch: Type.Optional(Type.String({ minLength: 1 })),
76
- commit: Type.Optional(Type.String({ minLength: 1 })),
77
- customInstructions: Type.Optional(Type.String({ minLength: 1 })),
78
- },
79
- { additionalProperties: false },
80
- );
81
-
82
- export const ReviewFindingValidationSchema = Type.Object(
83
- {
84
- verdict: Type.Union(REVIEW_VALIDATION_VERDICTS.map((value) => Type.Literal(value))),
85
- reasoning: Type.String({ minLength: 1 }),
86
- validatedBy: Type.String({ minLength: 1 }),
87
- validatedAt: Type.String({ minLength: 1 }),
88
- },
89
- { additionalProperties: false },
90
- );
91
-
92
- export const ReviewFindingSchema = Type.Object(
93
- {
94
- id: Type.String({ minLength: 1 }),
95
- title: Type.String({ minLength: 1 }),
96
- severity: Type.Union(REVIEW_FINDING_SEVERITIES.map((value) => Type.Literal(value))),
97
- priority: Type.Union(REVIEW_FINDING_PRIORITIES.map((value) => Type.Literal(value))),
98
- confidence: Type.Number({ minimum: 0, maximum: 1 }),
99
- file: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
100
- lineStart: Type.Union([Type.Number({ minimum: 1 }), Type.Null()]),
101
- lineEnd: Type.Union([Type.Number({ minimum: 1 }), Type.Null()]),
102
- body: Type.String({ minLength: 1 }),
103
- suggestion: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
104
- agent: Type.Optional(Type.String({ minLength: 1 })),
105
- validation: Type.Optional(ReviewFindingValidationSchema),
106
- },
107
- { additionalProperties: false },
108
- );
109
-
110
- export const ReviewOutputSchema = Type.Object(
111
- {
112
- findings: Type.Array(ReviewFindingSchema),
113
- summary: Type.String({ minLength: 1 }),
114
- status: Type.Union(REVIEW_OUTPUT_STATUSES.map((value) => Type.Literal(value))),
115
- },
116
- { additionalProperties: false },
117
- );
118
-
119
- export const ReviewAgentConfigSchema = Type.Object(
120
- {
121
- name: Type.String({ minLength: 1 }),
122
- enabled: Type.Boolean(),
123
- data: Type.String({ minLength: 1 }),
124
- model: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
125
- thinkingLevel: Type.Optional(Type.Union([
126
- Type.Literal("off"),
127
- Type.Literal("minimal"),
128
- Type.Literal("low"),
129
- Type.Literal("medium"),
130
- Type.Literal("high"),
131
- Type.Literal("xhigh"),
132
- Type.Null(),
133
- ])),
134
- peerCoordination: Type.Optional(Type.Boolean()),
135
- },
136
- { additionalProperties: false },
137
- );
138
-
139
- export const ReviewAgentsConfigSchema = Type.Object(
140
- {
141
- agents: Type.Array(ReviewAgentConfigSchema),
142
- },
143
- { additionalProperties: false },
144
- );
145
-
146
- export const ReviewAgentFrontmatterSchema = Type.Object(
147
- {
148
- name: Type.String({ minLength: 1 }),
149
- description: Type.String({ minLength: 1 }),
150
- focus: Type.Optional(Type.String({ minLength: 1 })),
151
- },
152
- { additionalProperties: false },
153
- );
154
-
155
- export const ReviewIterationSummarySchema = Type.Object(
156
- {
157
- iteration: Type.Number({ minimum: 1 }),
158
- findings: Type.Number({ minimum: 0 }),
159
- status: Type.Union(REVIEW_OUTPUT_STATUSES.map((value) => Type.Literal(value))),
160
- file: Type.String({ minLength: 1 }),
161
- createdAt: Type.String({ minLength: 1 }),
162
- },
163
- { additionalProperties: false },
164
- );
165
-
166
- export const ReviewFixRecordSchema = Type.Object(
167
- {
168
- findingIds: Type.Array(Type.String({ minLength: 1 })),
169
- file: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
170
- status: Type.Union(REVIEW_FIX_STATUSES.map((value) => Type.Literal(value))),
171
- summary: Type.String({ minLength: 1 }),
172
- },
173
- { additionalProperties: false },
174
- );
49
+ export const ReviewScopeFileSchema = z.object({
50
+ path: z.string().min(1),
51
+ additions: z.number().min(0),
52
+ deletions: z.number().min(0),
53
+ diff: z.string(),
54
+ }).strict();
55
+
56
+ export const ReviewScopeStatsSchema = z.object({
57
+ filesChanged: z.number().min(0),
58
+ excludedFiles: z.number().min(0),
59
+ additions: z.number().min(0),
60
+ deletions: z.number().min(0),
61
+ }).strict();
62
+
63
+ export const ReviewScopeSchema = z.object({
64
+ mode: z.enum(REVIEW_SCOPE_MODES),
65
+ description: z.string().min(1),
66
+ diff: z.string(),
67
+ files: z.array(ReviewScopeFileSchema),
68
+ stats: ReviewScopeStatsSchema,
69
+ baseBranch: z.string().min(1).optional(),
70
+ commit: z.string().min(1).optional(),
71
+ customInstructions: z.string().min(1).optional(),
72
+ }).strict();
73
+
74
+ export const ReviewFindingValidationSchema = z.object({
75
+ verdict: z.enum(REVIEW_VALIDATION_VERDICTS),
76
+ reasoning: z.string().min(1),
77
+ validatedBy: z.string().min(1),
78
+ validatedAt: z.string().min(1),
79
+ }).strict();
80
+
81
+ export const ReviewFindingSchema = z.object({
82
+ id: z.string().min(1),
83
+ title: z.string().min(1),
84
+ severity: z.enum(REVIEW_FINDING_SEVERITIES),
85
+ priority: z.enum(REVIEW_FINDING_PRIORITIES),
86
+ confidence: z.number().min(0).max(1),
87
+ file: z.string().min(1).nullable(),
88
+ lineStart: z.number().min(1).nullable(),
89
+ lineEnd: z.number().min(1).nullable(),
90
+ body: z.string().min(1),
91
+ suggestion: z.string().min(1).nullable(),
92
+ agent: z.string().min(1).optional(),
93
+ validation: ReviewFindingValidationSchema.optional(),
94
+ }).strict();
95
+
96
+ export const ReviewOutputSchema = z.object({
97
+ findings: z.array(ReviewFindingSchema),
98
+ summary: z.string().min(1),
99
+ status: z.enum(REVIEW_OUTPUT_STATUSES),
100
+ }).strict();
101
+
102
+ export const ReviewAgentConfigSchema = z.object({
103
+ name: z.string().min(1),
104
+ enabled: z.boolean(),
105
+ data: z.string().min(1),
106
+ model: z.string().min(1).nullable(),
107
+ thinkingLevel: z.union([
108
+ z.literal("off"),
109
+ z.literal("minimal"),
110
+ z.literal("low"),
111
+ z.literal("medium"),
112
+ z.literal("high"),
113
+ z.literal("xhigh"),
114
+ z.null(),
115
+ ]).optional(),
116
+ peerCoordination: z.boolean().optional(),
117
+ }).strict();
118
+
119
+ export const ReviewAgentsConfigSchema = z.object({
120
+ agents: z.array(ReviewAgentConfigSchema),
121
+ }).strict();
122
+
123
+ export const ReviewAgentFrontmatterSchema = z.object({
124
+ name: z.string().min(1),
125
+ description: z.string().min(1),
126
+ focus: z.string().min(1).optional(),
127
+ }).strict();
128
+
129
+ export const ReviewIterationSummarySchema = z.object({
130
+ iteration: z.number().min(1),
131
+ findings: z.number().min(0),
132
+ status: z.enum(REVIEW_OUTPUT_STATUSES),
133
+ file: z.string().min(1),
134
+ createdAt: z.string().min(1),
135
+ }).strict();
136
+
137
+ export const ReviewFixRecordSchema = z.object({
138
+ findingIds: z.array(z.string().min(1)),
139
+ file: z.string().min(1).nullable(),
140
+ status: z.enum(REVIEW_FIX_STATUSES),
141
+ summary: z.string().min(1),
142
+ }).strict();
175
143
 
176
144
  export const REVIEW_FIX_OUTPUT_STATUSES = ["applied", "partial", "skipped", "blocked"] as const;
177
145
 
178
- export const ReviewFixOutputSchema = Type.Object(
179
- {
180
- fixes: Type.Array(ReviewFixRecordSchema),
181
- summary: Type.String({ minLength: 1 }),
182
- status: Type.Union(REVIEW_FIX_OUTPUT_STATUSES.map((value) => Type.Literal(value))),
183
- },
184
- { additionalProperties: false },
185
- );
186
-
187
-
188
- export const ReviewSessionArtifactsSchema = Type.Object(
189
- {
190
- scope: Type.String({ minLength: 1 }),
191
- iterationsDir: Type.String({ minLength: 1 }),
192
- agentsDir: Type.String({ minLength: 1 }),
193
- rawFindings: Type.Optional(Type.String({ minLength: 1 })),
194
- validatedFindings: Type.Optional(Type.String({ minLength: 1 })),
195
- consolidatedFindings: Type.Optional(Type.String({ minLength: 1 })),
196
- findingsReport: Type.Optional(Type.String({ minLength: 1 })),
197
- },
198
- { additionalProperties: false },
199
- );
200
-
201
- export const ReviewSessionSchema = Type.Object(
202
- {
203
- id: Type.String({ minLength: 1 }),
204
- createdAt: Type.String({ minLength: 1 }),
205
- updatedAt: Type.String({ minLength: 1 }),
206
- level: Type.Union(REVIEW_LEVELS.map((value) => Type.Literal(value))),
207
- status: Type.Union(REVIEW_SESSION_STATUSES.map((value) => Type.Literal(value))),
208
- scope: ReviewScopeSchema,
209
- validateFindings: Type.Boolean(),
210
- consolidate: Type.Boolean(),
211
- postConsolidationAction: Type.Union([
212
- Type.Union(REVIEW_POST_CONSOLIDATION_ACTIONS.map((value) => Type.Literal(value))),
213
- Type.Null(),
214
- ]),
215
- maxIterations: Type.Number({ minimum: 0 }),
216
- currentIteration: Type.Number({ minimum: 0 }),
217
- iterations: Type.Array(ReviewIterationSummarySchema),
218
- fixes: Type.Array(ReviewFixRecordSchema),
219
- artifacts: ReviewSessionArtifactsSchema,
220
- agents: Type.Array(Type.String({ minLength: 1 })),
221
- },
222
- { additionalProperties: false },
223
- );
146
+ export const ReviewFixOutputSchema = z.object({
147
+ fixes: z.array(ReviewFixRecordSchema),
148
+ summary: z.string().min(1),
149
+ status: z.enum(REVIEW_FIX_OUTPUT_STATUSES),
150
+ }).strict();
151
+
152
+
153
+ export const ReviewSessionArtifactsSchema = z.object({
154
+ scope: z.string().min(1),
155
+ iterationsDir: z.string().min(1),
156
+ agentsDir: z.string().min(1),
157
+ rawFindings: z.string().min(1).optional(),
158
+ validatedFindings: z.string().min(1).optional(),
159
+ consolidatedFindings: z.string().min(1).optional(),
160
+ findingsReport: z.string().min(1).optional(),
161
+ }).strict();
162
+
163
+ export const ReviewSessionSchema = z.object({
164
+ id: z.string().min(1),
165
+ createdAt: z.string().min(1),
166
+ updatedAt: z.string().min(1),
167
+ level: z.enum(REVIEW_LEVELS),
168
+ status: z.enum(REVIEW_SESSION_STATUSES),
169
+ scope: ReviewScopeSchema,
170
+ validateFindings: z.boolean(),
171
+ consolidate: z.boolean(),
172
+ postConsolidationAction: z.enum(REVIEW_POST_CONSOLIDATION_ACTIONS).nullable(),
173
+ maxIterations: z.number().min(0),
174
+ currentIteration: z.number().min(0),
175
+ iterations: z.array(ReviewIterationSummarySchema),
176
+ fixes: z.array(ReviewFixRecordSchema),
177
+ artifacts: ReviewSessionArtifactsSchema,
178
+ agents: z.array(z.string().min(1)),
179
+ }).strict();
224
180
 
225
181
 
226
182
  export function isReviewScopeFile(value: unknown): value is ReviewScopeFile {
227
- return Value.Check(ReviewScopeFileSchema, value);
183
+ return checkSchema(ReviewScopeFileSchema, value);
228
184
  }
229
185
 
230
186
  export function isReviewScopeStats(value: unknown): value is ReviewScopeStats {
231
- return Value.Check(ReviewScopeStatsSchema, value);
187
+ return checkSchema(ReviewScopeStatsSchema, value);
232
188
  }
233
189
 
234
190
  export function isReviewScope(value: unknown): value is ReviewScope {
235
- return Value.Check(ReviewScopeSchema, value);
191
+ return checkSchema(ReviewScopeSchema, value);
236
192
  }
237
193
 
238
194
  export function isReviewFinding(value: unknown): value is ReviewFinding {
239
- return Value.Check(ReviewFindingSchema, value);
195
+ return checkSchema(ReviewFindingSchema, value);
240
196
  }
241
197
 
242
198
  export function isReviewOutput(value: unknown): value is ReviewOutput {
243
- return Value.Check(ReviewOutputSchema, value);
199
+ return checkSchema(ReviewOutputSchema, value);
244
200
  }
245
201
 
246
202
  export function isReviewAgentConfig(value: unknown): value is ReviewAgentConfig {
247
- return Value.Check(ReviewAgentConfigSchema, value);
203
+ return checkSchema(ReviewAgentConfigSchema, value);
248
204
  }
249
205
 
250
206
  export function isReviewAgentsConfig(value: unknown): value is ReviewAgentsConfig {
251
- return Value.Check(ReviewAgentsConfigSchema, value);
207
+ return checkSchema(ReviewAgentsConfigSchema, value);
252
208
  }
253
209
 
254
210
  export function isReviewSessionArtifacts(value: unknown): value is ReviewSessionArtifacts {
255
- return Value.Check(ReviewSessionArtifactsSchema, value);
211
+ return checkSchema(ReviewSessionArtifactsSchema, value);
256
212
  }
257
213
 
258
214
  export function isReviewIterationSummary(value: unknown): value is ReviewIterationSummary {
259
- return Value.Check(ReviewIterationSummarySchema, value);
215
+ return checkSchema(ReviewIterationSummarySchema, value);
260
216
  }
261
217
 
262
218
  export function isReviewFixRecord(value: unknown): value is ReviewFixRecord {
263
- return Value.Check(ReviewFixRecordSchema, value);
219
+ return checkSchema(ReviewFixRecordSchema, value);
264
220
  }
265
221
 
266
222
  export function isReviewSession(value: unknown): value is ReviewSession {
267
- return Value.Check(ReviewSessionSchema, value);
223
+ return checkSchema(ReviewSessionSchema, value);
268
224
  }
269
225
 
270
226
  export function isReviewAgentDefinition(value: unknown): value is ReviewAgentDefinition {
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TSchema } from "@sinclair/typebox";
1
+ import type { ZodType } from "zod/v4";
2
2
  import type { AgentSession, AgentSessionOptions, ExecOptions, ExecResult } from "./platform/types.js";
3
3
 
4
4
 
@@ -6,6 +6,9 @@ import type { AgentSession, AgentSessionOptions, ExecOptions, ExecResult } from
6
6
  export interface ValidationError {
7
7
  path: string;
8
8
  message: string;
9
+ code?: string;
10
+ expected?: unknown;
11
+ received?: unknown;
9
12
  }
10
13
  // src/types.ts — Shared type definitions for supipowers
11
14
 
@@ -365,7 +368,7 @@ export interface GateExecutionContext {
365
368
  export interface GateDefinition<TConfig> {
366
369
  id: GateId;
367
370
  description: string;
368
- configSchema: TSchema;
371
+ configSchema: ZodType;
369
372
  detect(projectFacts: ProjectFacts): GateDetectionResult<TConfig> | null;
370
373
  run(context: GateExecutionContext, config: TConfig): Promise<GateResult>;
371
374
  }
@@ -1826,6 +1829,46 @@ export interface HarnessCiConfig {
1826
1829
  * an explicit truthy `enabled`, so legacy specs do not trip it.
1827
1830
  */
1828
1831
  prComment?: HarnessPrCommentConfig;
1832
+ /**
1833
+ * Optional Git topology + branch-protection wiring captured by the interactive
1834
+ * `git-verify` sub-step run between Design and Plan. Absent on legacy specs.
1835
+ *
1836
+ * - `mainBranch` is the canonical protected branch (typically `main` or `master`).
1837
+ * - `devBranch` is the development branch dev work flows through; `null` when the user
1838
+ * opts out of the convention.
1839
+ * - `enforceMainFromDevOnly` controls both the CI-side guardrail (a `verify-pr-source`
1840
+ * job appended to the rendered workflow) and the opportunistic server-side ruleset
1841
+ * applied via `gh api`. The CI guardrail is deterministic; the ruleset is best-effort.
1842
+ * - `verification` records what the interactive helper actually did. `appliedProtections`
1843
+ * is the set of enforcement layers that landed (e.g. `"ci-guardrail"`, `"ruleset"`).
1844
+ * `findings` carries non-fatal issues surfaced during the run; the validate stage
1845
+ * folds them into its report. `manualInstructionsPath` points at the rendered
1846
+ * fallback doc when `gh` is unavailable or lacks scope.
1847
+ */
1848
+ git?: HarnessCiGitConfig;
1849
+ }
1850
+
1851
+ /** Git/branch-protection block recorded by the interactive verification helper. */
1852
+ export interface HarnessCiGitConfig {
1853
+ mainBranch: string;
1854
+ devBranch: string | null;
1855
+ enforceMainFromDevOnly: boolean;
1856
+ verification: HarnessCiGitVerification | null;
1857
+ }
1858
+
1859
+ /** Result block recorded by `runGitVerificationQa` for downstream stages to consume. */
1860
+ export interface HarnessCiGitVerification {
1861
+ checkedAt: string;
1862
+ appliedProtections: string[];
1863
+ findings: HarnessCiGitFinding[];
1864
+ /** Relative path (under the session dir) to the rendered manual-instructions doc, or null. */
1865
+ manualInstructionsPath: string | null;
1866
+ }
1867
+
1868
+ export interface HarnessCiGitFinding {
1869
+ severity: "info" | "warning" | "error";
1870
+ message: string;
1871
+ remediation?: string;
1829
1872
  }
1830
1873
 
1831
1874
 
@@ -898,10 +898,10 @@ export function registerUiDesignToolGuard(platform: Platform): void {
898
898
  const session = activeSession;
899
899
  if (!session) return;
900
900
 
901
- if (event.toolName === "exit_plan_mode") {
901
+ if (event.toolName === "resolve" && isUiDesignPlanApprovalResolveInput(event.input)) {
902
902
  return {
903
903
  block: true,
904
- reason: "UI-design mode: completion is driven by the agent_end approval hook; do not call exit_plan_mode.",
904
+ reason: "UI-design mode: completion is driven by the agent_end approval hook; do not call `resolve` with `extra.title`.",
905
905
  };
906
906
  }
907
907
 
@@ -943,6 +943,17 @@ export function registerUiDesignToolGuard(platform: Platform): void {
943
943
  });
944
944
  }
945
945
 
946
+ function isUiDesignPlanApprovalResolveInput(input: unknown): boolean {
947
+ if (input === null || typeof input !== "object" || Array.isArray(input)) return false;
948
+ const candidate = input as { action?: unknown; extra?: unknown };
949
+ if (candidate.action !== "apply") return false;
950
+ const extra = candidate.extra;
951
+ return extra !== null
952
+ && typeof extra === "object"
953
+ && !Array.isArray(extra)
954
+ && typeof (extra as { title?: unknown }).title === "string";
955
+ }
956
+
946
957
  /**
947
958
  * Register the `agent_end` hook that drives the ui-design approval UI.
948
959
  *
@@ -129,7 +129,7 @@ function buildHardGate(options: UiDesignSystemPromptOptions): string[] {
129
129
  "",
130
130
  `- All file writes MUST happen inside \`${options.sessionDir}\`. Writing anywhere else is forbidden.`,
131
131
  "- You **MUST NOT** generate production code (`.ts`, `.tsx`, `.vue`, `.svelte`, `.py`) into the user's codebase.",
132
- "- You **MUST NOT** call `exit_plan_mode` or `ExitPlanMode` — completion is driven by the agent_end approval hook.",
132
+ "- You **MUST NOT** call `resolve` with `extra.title` — completion is driven by the agent_end approval hook.",
133
133
  "- You **MUST NOT** use the `ask` tool. Use `planning_ask` for every user question.",
134
134
  "- You **MUST NOT** skip a phase. Each phase's precondition file MUST exist on disk before you advance.",
135
135
  "- You **MUST NOT** declare completion without updating `manifest.json`.",
@@ -256,7 +256,7 @@ function buildCriticalBlock(options: UiDesignSystemPromptOptions): string {
256
256
  "## Completion",
257
257
  "",
258
258
  "Completion is driven by `manifest.json`. Set `status: \"complete\"` + `approvedAt` only after the user approves via Phase 9's review gate.",
259
- "You **MUST NOT** call `exit_plan_mode`, `ExitPlanMode`, or write to `local://PLAN.md`.",
259
+ "You **MUST NOT** call `resolve` with `extra.title`, or write to `local://PLAN.md`.",
260
260
  "After updating the manifest to a terminal state, stop and yield your turn — the approval UI handles teardown.",
261
261
  "</critical>",
262
262
  ].join("\n");