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.
- package/README.md +71 -12
- package/package.json +4 -8
- package/skills/ui-design/SKILL.md +2 -2
- package/src/ai/final-message.ts +15 -1
- package/src/ai/schema-text.ts +60 -40
- package/src/ai/schema-validation.ts +88 -0
- package/src/ai/structured-output.ts +19 -19
- package/src/bootstrap.ts +3 -0
- package/src/commands/fix-pr.ts +166 -26
- package/src/commands/optimize-context.ts +153 -16
- package/src/commands/runbook.ts +511 -0
- package/src/config/schema.ts +102 -139
- package/src/context/rule-renderer.ts +274 -2
- package/src/context/runbook-extension-template.ts +193 -0
- package/src/context/startup-check.ts +197 -2
- package/src/context/startup-optimizer.ts +133 -10
- package/src/docs/contracts.ts +13 -23
- package/src/fix-pr/assessment.ts +63 -24
- package/src/fix-pr/contracts.ts +15 -23
- package/src/fix-pr/fetch-comments.ts +119 -0
- package/src/fix-pr/prompt-builder.ts +19 -8
- package/src/git/commit-contract.ts +13 -19
- package/src/git/commit.ts +168 -6
- package/src/harness/command.ts +98 -6
- package/src/harness/git-verification.ts +515 -0
- package/src/harness/git-verify-qa.ts +406 -0
- package/src/harness/pipeline.ts +17 -8
- package/src/harness/stages/implement-apply.ts +61 -4
- package/src/harness/stages/validate.ts +108 -0
- package/src/lsp/capabilities.ts +9 -12
- package/src/lsp/contracts.ts +15 -23
- package/src/planning/planning-ask-tool.ts +13 -2
- package/src/planning/spec.ts +21 -27
- package/src/planning/system-prompt.ts +1 -1
- package/src/planning/validate.ts +4 -7
- package/src/platform/progress.ts +11 -0
- package/src/quality/contracts.ts +15 -23
- package/src/quality/schemas.ts +40 -67
- package/src/release/contracts.ts +19 -28
- package/src/review/types.ts +142 -186
- package/src/types.ts +45 -2
- package/src/ui-design/session.ts +13 -2
- package/src/ui-design/system-prompt.ts +2 -2
- package/src/ultraplan/contracts.ts +458 -524
package/README.md
CHANGED
|
@@ -72,15 +72,16 @@ The installer scans for these and offers to install missing tooling where it can
|
|
|
72
72
|
| `/supi:config` | Interactive settings TUI |
|
|
73
73
|
| `/supi:status` | Show project plans and configuration summary |
|
|
74
74
|
| `/supi:doctor` | Diagnose extension health and missing dependencies |
|
|
75
|
-
| `/supi:generate` | Documentation drift
|
|
75
|
+
| `/supi:generate` | Documentation drift checks via `docs` (default); use `--target <package>` to scope |
|
|
76
76
|
| `/supi:update` | Update supipowers to the latest version |
|
|
77
77
|
| `/supi:agents` | Manage review agents |
|
|
78
78
|
| `/supi:ultraplan` | Multi-stage authoring pipeline (intake → scout → discover → research → synthesize → review → approve) |
|
|
79
79
|
| `/supi:harness` | Harness engineering pipeline and anti-slop guardrails |
|
|
80
80
|
| `/supi:memory` | Manage native MemPalace memory integration (`status`, `setup`) |
|
|
81
|
+
| `/runbook` | Show registered OMP rules, TTSR conditions, and slash commands without an LLM turn |
|
|
81
82
|
| `/supi:clear` | Clear metrics, cache, session knowledge, and memory |
|
|
82
83
|
|
|
83
|
-
Most commands steer the AI session. These are TUI-only — they open native dialogs without triggering the AI: `/supi`, `/supi:config`, `/supi:status`, `/supi:review`, `/supi:update`, `/supi:doctor`, `/supi:model`, `/supi:context`, `/supi:optimize-context`, `/supi:commit`, `/supi:release`, `/supi:checks`, `/supi:agents`, `/supi:ultraplan`, `/supi:harness`, `/supi:memory`, `/supi:clear`.
|
|
84
|
+
Most commands steer the AI session. These are TUI-only — they open native dialogs without triggering the AI: `/supi`, `/supi:config`, `/supi:status`, `/supi:review`, `/supi:update`, `/supi:doctor`, `/supi:model`, `/supi:context`, `/supi:optimize-context`, `/supi:commit`, `/supi:release`, `/supi:checks`, `/supi:agents`, `/supi:ultraplan`, `/supi:harness`, `/supi:memory`, `/supi:clear`, `/runbook`.
|
|
84
85
|
|
|
85
86
|
## How it works
|
|
86
87
|
|
|
@@ -88,6 +89,8 @@ Most commands steer the AI session. These are TUI-only — they open native dial
|
|
|
88
89
|
|
|
89
90
|
**Quality gates.** `/supi:checks` runs deterministic quality gates. Six gates are available: `lsp-diagnostics`, `lint`, `typecheck`, `format`, `test-suite`, and `build`. Each gate can be enabled independently via `/supi:config` or the shared repository config at `.omp/supipowers/config.json`. In monorepos, `/supi:checks` defaults to `All`, which runs the root target plus every workspace target sequentially; use `--target <package>` to narrow the run or `--target all` to request the batch mode explicitly. Gates report issues with severity levels.
|
|
90
91
|
|
|
92
|
+
**Documentation drift.** `/supi:generate docs` checks tracked documentation for drift from the current codebase. `docs` is the default subcommand, and `--target <package>` scopes discovery and checking to a workspace/package target; the root target covers repository-level docs.
|
|
93
|
+
|
|
91
94
|
**AI code review.** `/supi:review` runs a programmatic AI review pipeline with configurable depth (quick, deep, or multi-agent). It uses headless agent sessions with structured JSON validation, always validates findings before user action, writes the current validated findings to a session `findings.md` document, and then presents three next-step choices: `Fix now`, `Document only`, or `Discuss before fixing`.
|
|
92
95
|
|
|
93
96
|
**Review agents.** Multi-agent review loads agents from two scopes: global and project.
|
|
@@ -101,7 +104,7 @@ Most commands steer the AI session. These are TUI-only — they open native dial
|
|
|
101
104
|
|
|
102
105
|
Use `/supi:agents` to inspect the merged set that will actually run.
|
|
103
106
|
|
|
104
|
-
**PR fixing.** `/supi:fix-pr` fetches PR review comments, critically assesses each one, checks for ripple effects, then fixes or rejects with evidence.
|
|
107
|
+
**PR fixing.** `/supi:fix-pr` fetches PR review comments, critically assesses each one, checks for ripple effects, then fixes or rejects with evidence. Known bot reviewers in the selected comment snapshot are auto-detected to configure re-review triggering; bot-authored comments are not filtered out solely because they are bots.
|
|
105
108
|
|
|
106
109
|
**Context protection.** Supipowers always enables built-in context protection through native `ctx_*` tools and routing hooks. Search/find and web-fetch style operations are redirected to sandboxed execution or indexed storage, and oversized tool results are compressed before they reach the conversation.
|
|
107
110
|
|
|
@@ -131,17 +134,50 @@ Use `/supi:agents` to inspect the merged set that will actually run.
|
|
|
131
134
|
|
|
132
135
|
`/supi:checks` runs deterministic quality gates. Each gate is independently configurable in `quality.gates` via `/supi:config` or the shared config JSON files:
|
|
133
136
|
|
|
134
|
-
| Gate | What it checks | Config type
|
|
135
|
-
| ------------------ | ------------------------------- |
|
|
136
|
-
| `lsp-diagnostics` | Language server diagnostics | enabled
|
|
137
|
-
| `lint` | Linter (e.g. `eslint`, `biome`) | enabled +
|
|
138
|
-
| `typecheck` | Type checker (e.g. `tsc`) | enabled +
|
|
139
|
-
| `format` | Formatter check | enabled +
|
|
140
|
-
| `test-suite` | Test runner | enabled +
|
|
141
|
-
| `build` | Build verification | enabled +
|
|
137
|
+
| Gate | What it checks | Config type |
|
|
138
|
+
| ------------------ | ------------------------------- | ------------------------------ |
|
|
139
|
+
| `lsp-diagnostics` | Language server diagnostics | `enabled` |
|
|
140
|
+
| `lint` | Linter (e.g. `eslint`, `biome`) | `enabled: true` + `runs[]` |
|
|
141
|
+
| `typecheck` | Type checker (e.g. `tsc`) | `enabled: true` + `runs[]` |
|
|
142
|
+
| `format` | Formatter check | `enabled: true` + `runs[]` |
|
|
143
|
+
| `test-suite` | Test runner | `enabled: true` + `runs[]` |
|
|
144
|
+
| `build` | Build verification | `enabled: true` + `runs[]` |
|
|
142
145
|
|
|
143
146
|
Gates default to disabled. Enable them globally in `~/.omp/supipowers/config.json` or per-repository in `.omp/supipowers/config.json`. In monorepos, the repository config is shared by the root target and every workspace, and `/supi:checks` defaults to `All` (root target + every workspace target).
|
|
144
147
|
|
|
148
|
+
Enabled command gates require `runs: [{ command, target }]`. `target.scope` must be one of `all-targets`, `root`, `all-workspaces`, or `workspace`; `workspace` selectors also require `relativeDir`.
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"quality": {
|
|
153
|
+
"gates": {
|
|
154
|
+
"typecheck": {
|
|
155
|
+
"enabled": true,
|
|
156
|
+
"runs": [
|
|
157
|
+
{
|
|
158
|
+
"command": "bun run typecheck",
|
|
159
|
+
"target": { "scope": "all-targets" }
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
"test-suite": {
|
|
164
|
+
"enabled": true,
|
|
165
|
+
"runs": [
|
|
166
|
+
{
|
|
167
|
+
"command": "bun test",
|
|
168
|
+
"target": { "scope": "root" }
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"command": "bun --cwd packages/api test",
|
|
172
|
+
"target": { "scope": "workspace", "relativeDir": "packages/api" }
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
145
181
|
## Configuration
|
|
146
182
|
|
|
147
183
|
```
|
|
@@ -215,9 +251,32 @@ Supipowers ships runtime-loaded prompt skills that are also available to the age
|
|
|
215
251
|
| `receiving-code-review` | Agent sessions |
|
|
216
252
|
| `release` | `/supi:release` |
|
|
217
253
|
| `context-mode` | Context window guidance |
|
|
254
|
+
| `ultraplan-intake` | `/supi:ultraplan plan` intake stage |
|
|
255
|
+
| `ultraplan-scout` | `/supi:ultraplan plan` scout stage |
|
|
256
|
+
| `ultraplan-discover` | `/supi:ultraplan discover` |
|
|
257
|
+
| `ultraplan-research` | `/supi:ultraplan research` |
|
|
258
|
+
| `ultraplan-synthesize` | `/supi:ultraplan synthesize` |
|
|
259
|
+
| `ultraplan-review` | `/supi:ultraplan review` orchestration |
|
|
260
|
+
| `ultraplan-review-structure` | `/supi:ultraplan review` structure checker |
|
|
261
|
+
| `ultraplan-review-scope` | `/supi:ultraplan review` scope checker |
|
|
262
|
+
| `ultraplan-review-tdd` | `/supi:ultraplan review` TDD checker |
|
|
218
263
|
| `creating-supi-agents` | Agent creation guidance |
|
|
219
264
|
| `harness` | `/supi:harness` |
|
|
220
265
|
|
|
266
|
+
## Containerized deployments
|
|
267
|
+
|
|
268
|
+
Supipowers runs unchanged inside containerized OMP installs (robomp slots, the swarm extension, CI runners). When the slot must stay credential-free, run a sidecar `omp auth-gateway` outside the container and pin the per-provider transport in `~/.omp/agent/models.yml`:
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
providers:
|
|
272
|
+
anthropic:
|
|
273
|
+
transport: pi-native
|
|
274
|
+
baseUrl: http://llm-gateway.internal:4000
|
|
275
|
+
apiKey: <gateway-bearer>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The slot keeps resolving pricing, capabilities, and thinking config locally from its bundled `models.json`; only the streaming dispatch is redirected through the gateway, which holds the real provider tokens. Multi-host credential sync uses the matching `omp auth-broker` subcommand. Requires OMP ≥ 15.1.3.
|
|
279
|
+
|
|
221
280
|
## Development
|
|
222
281
|
|
|
223
282
|
```bash
|
|
@@ -229,4 +288,4 @@ bun run build # emit to dist/
|
|
|
229
288
|
|
|
230
289
|
Tests live in `tests/`, mirroring `src/` one-to-one. The test runner is Bun's built-in `bun:test`.
|
|
231
290
|
|
|
232
|
-
Peer dependencies (`@oh-my-pi/pi-coding-agent`, `@oh-my-pi/pi-ai`, `@oh-my-pi/pi-tui
|
|
291
|
+
Peer dependencies (`@oh-my-pi/pi-coding-agent`, `@oh-my-pi/pi-ai`, `@oh-my-pi/pi-tui`) are provided by the OMP host at runtime; matching devDependencies are installed for type-checking during development.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supipowers",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Workflow extension for OMP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -67,13 +67,13 @@
|
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@clack/prompts": "^0.10.0",
|
|
69
69
|
"handlebars": "^4.7.8",
|
|
70
|
-
"yaml": "^2.8.3"
|
|
70
|
+
"yaml": "^2.8.3",
|
|
71
|
+
"zod": "^4.3.0"
|
|
71
72
|
},
|
|
72
73
|
"peerDependencies": {
|
|
73
74
|
"@oh-my-pi/pi-coding-agent": "*",
|
|
74
75
|
"@oh-my-pi/pi-ai": "*",
|
|
75
|
-
"@oh-my-pi/pi-tui": "*"
|
|
76
|
-
"@sinclair/typebox": "*"
|
|
76
|
+
"@oh-my-pi/pi-tui": "*"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
79
79
|
"@oh-my-pi/pi-coding-agent": {
|
|
@@ -84,16 +84,12 @@
|
|
|
84
84
|
},
|
|
85
85
|
"@oh-my-pi/pi-tui": {
|
|
86
86
|
"optional": true
|
|
87
|
-
},
|
|
88
|
-
"@sinclair/typebox": {
|
|
89
|
-
"optional": true
|
|
90
87
|
}
|
|
91
88
|
},
|
|
92
89
|
"devDependencies": {
|
|
93
90
|
"@oh-my-pi/pi-ai": "latest",
|
|
94
91
|
"@oh-my-pi/pi-coding-agent": "latest",
|
|
95
92
|
"@oh-my-pi/pi-tui": "latest",
|
|
96
|
-
"@sinclair/typebox": "^0.34.48",
|
|
97
93
|
"@types/node": "^22.0.0",
|
|
98
94
|
"bun-types": "^1.3.11",
|
|
99
95
|
"typescript": "^5.9.3"
|
|
@@ -7,7 +7,7 @@ description: Design Director state machine for `/supi:ui-design`. Drives 9 model
|
|
|
7
7
|
|
|
8
8
|
Guide the Design Director through 9 model-owned phases to produce a validated design artifact under `<sessionDir>`. Loaded by `/supi:ui-design` via system-prompt override.
|
|
9
9
|
|
|
10
|
-
You **MUST NOT** generate production code, write outside the session directory, or skip phases. You **MUST NOT** call `
|
|
10
|
+
You **MUST NOT** generate production code, write outside the session directory, or skip phases. You **MUST NOT** call `resolve` with `extra.title`. Use `planning_ask` for every user question — never the raw `ask` tool.
|
|
11
11
|
|
|
12
12
|
## Director state machine
|
|
13
13
|
|
|
@@ -56,7 +56,7 @@ Every sub-agent MUST be passed the full `context.md` so component authors share
|
|
|
56
56
|
You MUST NOT:
|
|
57
57
|
- Write outside `<sessionDir>`.
|
|
58
58
|
- Generate production code (`.ts`, `.tsx`, `.vue`, `.svelte`, `.py`, etc.) intended for the user's codebase.
|
|
59
|
-
- Call `
|
|
59
|
+
- Call `resolve` with `extra.title` — the `/supi:ui-design` completion flow runs through the `agent_end` approval hook.
|
|
60
60
|
- Use the `ask` tool — use `planning_ask` for every user prompt.
|
|
61
61
|
- Skip a phase or declare "done" without updating `manifest.json`.
|
|
62
62
|
- Invoke `task` without a completed filename-collision check (Phase 3).
|
package/src/ai/final-message.ts
CHANGED
|
@@ -56,6 +56,13 @@ function extractTextFromContent(content: unknown): string {
|
|
|
56
56
|
return "";
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function createTimeoutPromise(timeoutMs: number): Promise<never> {
|
|
60
|
+
return new Promise((_, reject) => {
|
|
61
|
+
setTimeout(() => reject(new Error(`Agent session timed out after ${timeoutMs}ms.`)), timeoutMs);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
59
66
|
/**
|
|
60
67
|
* Walk the message list backwards and return the last assistant message text.
|
|
61
68
|
* Returns null when no assistant message contains usable text.
|
|
@@ -99,7 +106,14 @@ export async function runStructuredAgentSession(
|
|
|
99
106
|
});
|
|
100
107
|
|
|
101
108
|
try {
|
|
102
|
-
|
|
109
|
+
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
110
|
+
await Promise.race([
|
|
111
|
+
session.prompt(options.prompt, { expandPromptTemplates: false }),
|
|
112
|
+
createTimeoutPromise(options.timeoutMs),
|
|
113
|
+
]);
|
|
114
|
+
} else {
|
|
115
|
+
await session.prompt(options.prompt, { expandPromptTemplates: false });
|
|
116
|
+
}
|
|
103
117
|
const finalText = extractFinalAssistantText(session.state.messages);
|
|
104
118
|
|
|
105
119
|
if (!finalText) {
|
package/src/ai/schema-text.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/ai/schema-text.ts
|
|
2
2
|
//
|
|
3
|
-
// Render a
|
|
4
|
-
// prompts. One canonical rendering means that adding a field to a
|
|
3
|
+
// Render a Zod schema as compact TS-like text suitable for embedding in
|
|
4
|
+
// prompts. One canonical rendering means that adding a field to a Zod
|
|
5
5
|
// contract automatically updates every prompt that references it through
|
|
6
6
|
// this module — no hand-maintained schema prose to drift.
|
|
7
7
|
//
|
|
@@ -10,17 +10,24 @@
|
|
|
10
10
|
// ReviewOutputSchema / ReviewFixOutputSchema for both the main prompt
|
|
11
11
|
// and the retry prompt produced by runWithOutputValidation.
|
|
12
12
|
//
|
|
13
|
+
// Implementation note:
|
|
14
|
+
// We don't walk Zod's internal `_zod.def` tree directly. Zod's JSON Schema
|
|
15
|
+
// accessor (`z.toJSONSchema`) already emits draft-2020-12 output for every
|
|
16
|
+
// shape we use; we walk that intermediate JSON Schema instead, which keeps
|
|
17
|
+
// this module independent of Zod's internal AST changes.
|
|
18
|
+
//
|
|
13
19
|
// Non-goals:
|
|
14
|
-
// - Produce standards-compliant JSON Schema output.
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// contract surface; extend when a real consumer needs more.
|
|
20
|
+
// - Produce standards-compliant JSON Schema output. Call `z.toJSONSchema`
|
|
21
|
+
// directly for that. This renderer optimises for model readability.
|
|
22
|
+
// - Capture every modifier. Supported shapes cover the current contract
|
|
23
|
+
// surface; extend when a real consumer needs more.
|
|
19
24
|
|
|
20
|
-
import type
|
|
25
|
+
import { z, type ZodType } from "zod/v4";
|
|
21
26
|
|
|
22
27
|
const INDENT = " ";
|
|
23
28
|
|
|
29
|
+
type JsonSchemaNode = Record<string, unknown>;
|
|
30
|
+
|
|
24
31
|
export interface RenderSchemaOptions {
|
|
25
32
|
/** Start indent (internal recursion use). */
|
|
26
33
|
depth?: number;
|
|
@@ -36,33 +43,37 @@ function renderLiteral(value: unknown): string {
|
|
|
36
43
|
return String(value);
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
function renderUnion(parts: readonly
|
|
46
|
+
function renderUnion(parts: readonly JsonSchemaNode[], depth: number): string {
|
|
40
47
|
if (parts.length === 0) return "never";
|
|
41
|
-
return parts.map((p) =>
|
|
48
|
+
return parts.map((p) => renderJsonSchema(p, depth)).join(" | ");
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
function renderObject(schema:
|
|
45
|
-
const props = schema.properties as Record<string,
|
|
51
|
+
function renderObject(schema: JsonSchemaNode, depth: number): string {
|
|
52
|
+
const props = schema.properties as Record<string, JsonSchemaNode> | undefined;
|
|
46
53
|
if (!props || Object.keys(props).length === 0) {
|
|
47
54
|
return "{}";
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
const required: string[] = Array.isArray(schema.required)
|
|
57
|
+
const required: string[] = Array.isArray(schema.required)
|
|
58
|
+
? (schema.required as string[])
|
|
59
|
+
: [];
|
|
51
60
|
const lines: string[] = ["{"];
|
|
52
61
|
const childDepth = depth + 1;
|
|
53
62
|
|
|
54
63
|
for (const [key, child] of Object.entries(props)) {
|
|
55
64
|
const isRequired = required.includes(key);
|
|
56
65
|
const separator = isRequired ? ":" : "?:";
|
|
57
|
-
lines.push(`${indent(childDepth)}${key}${separator} ${
|
|
66
|
+
lines.push(`${indent(childDepth)}${key}${separator} ${renderJsonSchema(child, childDepth)};`);
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
lines.push(`${indent(depth)}}`);
|
|
61
70
|
return lines.join("\n");
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
function renderArray(schema:
|
|
65
|
-
const
|
|
73
|
+
function renderArray(schema: JsonSchemaNode, depth: number): string {
|
|
74
|
+
const items = schema.items as JsonSchemaNode | undefined;
|
|
75
|
+
if (!items) return "unknown[]";
|
|
76
|
+
const inner = renderJsonSchema(items, depth);
|
|
66
77
|
// Wrap multiline object types as Array<...> for readability.
|
|
67
78
|
if (inner.includes("\n")) {
|
|
68
79
|
return `Array<${inner}>`;
|
|
@@ -70,44 +81,36 @@ function renderArray(schema: any, depth: number): string {
|
|
|
70
81
|
return `${inner}[]`;
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
function
|
|
74
|
-
return
|
|
84
|
+
function isZodSchema(value: unknown): value is ZodType {
|
|
85
|
+
return value !== null && typeof value === "object" && "_zod" in (value as Record<string, unknown>);
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
* Render a TypeBox schema as a compact TS-like type string. Safe to pass as
|
|
79
|
-
* the `schema:` param to `runWithOutputValidation` and the `{{outputSchema}}`
|
|
80
|
-
* placeholder inside review prompts.
|
|
81
|
-
*/
|
|
82
|
-
export function renderSchemaText(schema: TSchema, options: RenderSchemaOptions = {}): string {
|
|
83
|
-
const depth = options.depth ?? 0;
|
|
84
|
-
const any = schema as any;
|
|
85
|
-
|
|
88
|
+
function renderJsonSchema(schema: JsonSchemaNode, depth: number): string {
|
|
86
89
|
// Literal / const
|
|
87
|
-
if (
|
|
88
|
-
return renderLiteral(
|
|
90
|
+
if ("const" in schema) {
|
|
91
|
+
return renderLiteral(schema.const);
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
// Explicit enum
|
|
92
|
-
if (Array.isArray(
|
|
93
|
-
return
|
|
95
|
+
if (Array.isArray(schema.enum)) {
|
|
96
|
+
return schema.enum.map(renderLiteral).join(" | ");
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
// Union (anyOf / oneOf)
|
|
97
|
-
if (Array.isArray(
|
|
98
|
-
return renderUnion(
|
|
100
|
+
if (Array.isArray(schema.anyOf)) {
|
|
101
|
+
return renderUnion(schema.anyOf as JsonSchemaNode[], depth);
|
|
99
102
|
}
|
|
100
|
-
if (Array.isArray(
|
|
101
|
-
return renderUnion(
|
|
103
|
+
if (Array.isArray(schema.oneOf)) {
|
|
104
|
+
return renderUnion(schema.oneOf as JsonSchemaNode[], depth);
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
// Primitive / structural by `type`
|
|
105
|
-
const type =
|
|
108
|
+
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
106
109
|
switch (type) {
|
|
107
110
|
case "object":
|
|
108
|
-
return renderObject(
|
|
111
|
+
return renderObject(schema, depth);
|
|
109
112
|
case "array":
|
|
110
|
-
return renderArray(
|
|
113
|
+
return renderArray(schema, depth);
|
|
111
114
|
case "string":
|
|
112
115
|
return "string";
|
|
113
116
|
case "integer":
|
|
@@ -119,11 +122,28 @@ export function renderSchemaText(schema: TSchema, options: RenderSchemaOptions =
|
|
|
119
122
|
case "null":
|
|
120
123
|
return "null";
|
|
121
124
|
default:
|
|
122
|
-
// Fall through — unknown shape
|
|
123
125
|
break;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// Nothing matched — render as `unknown` rather than throwing so prompts
|
|
127
|
-
// still get something readable if someone adds an exotic
|
|
129
|
+
// still get something readable if someone adds an exotic shape.
|
|
128
130
|
return "unknown";
|
|
129
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Render a Zod schema as a compact TS-like type string. Safe to pass as
|
|
135
|
+
* the `schema:` param to `runWithOutputValidation` and the `{{outputSchema}}`
|
|
136
|
+
* placeholder inside review prompts.
|
|
137
|
+
*
|
|
138
|
+
* The OMP runtime injects a Zod-backed shim for any extension that still
|
|
139
|
+
* imports `@sinclair/typebox`, so even legacy callers reach this function
|
|
140
|
+
* with a Zod schema. Non-Zod inputs (legitimate JSON Schema literals) are
|
|
141
|
+
* walked as-is.
|
|
142
|
+
*/
|
|
143
|
+
export function renderSchemaText(schema: ZodType | JsonSchemaNode, options: RenderSchemaOptions = {}): string {
|
|
144
|
+
const depth = options.depth ?? 0;
|
|
145
|
+
const jsonSchema = isZodSchema(schema)
|
|
146
|
+
? (z.toJSONSchema(schema, { target: "draft-2020-12" }) as JsonSchemaNode)
|
|
147
|
+
: schema;
|
|
148
|
+
return renderJsonSchema(jsonSchema, depth);
|
|
149
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/ai/schema-validation.ts
|
|
2
|
+
//
|
|
3
|
+
// Thin façade over Zod's `safeParse` that produces a flat `ValidationError[]`
|
|
4
|
+
// shape compatible with the rest of supipowers (`parseStructuredOutput`,
|
|
5
|
+
// `getUltraPlanSchemaErrors`, every gate prompt that formats validation
|
|
6
|
+
// failures for retry).
|
|
7
|
+
//
|
|
8
|
+
// All contracts in supipowers are authored as Zod schemas (`zod/v4`). The
|
|
9
|
+
// helpers here intentionally accept the structural interface — anything with
|
|
10
|
+
// a working `safeParse` — so they keep working under the OMP TypeBox-shim
|
|
11
|
+
// (extension load time) without needing a separate code path.
|
|
12
|
+
|
|
13
|
+
import type { ZodType } from "zod/v4";
|
|
14
|
+
import type { ValidationError } from "../types.js";
|
|
15
|
+
|
|
16
|
+
interface ZodIssueLike {
|
|
17
|
+
path: ReadonlyArray<string | number | symbol>;
|
|
18
|
+
message: string;
|
|
19
|
+
code?: string;
|
|
20
|
+
expected?: unknown;
|
|
21
|
+
received?: unknown;
|
|
22
|
+
/** Present on Zod 4 `unrecognized_keys` issues. */
|
|
23
|
+
keys?: ReadonlyArray<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pathToString(path: ReadonlyArray<string | number | symbol>): string {
|
|
27
|
+
// Zod 4 path segments are `(string | number | symbol)[]`; symbols only
|
|
28
|
+
// appear for schemas with symbol keys (not used in supipowers). Drop them
|
|
29
|
+
// so the printed path stays readable.
|
|
30
|
+
const stringy = path.filter((segment): segment is string | number => typeof segment !== "symbol");
|
|
31
|
+
return stringy.length === 0 ? "(root)" : stringy.map(String).join(".");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function expandIssue(issue: ZodIssueLike): ValidationError[] {
|
|
35
|
+
// Zod 4 reports unrecognized strict-object keys with the offending keys in
|
|
36
|
+
// `issue.keys` and the path stopped at the parent object. Expand each key
|
|
37
|
+
// into its own ValidationError with the key appended to the path so
|
|
38
|
+
// formatted error strings (`<path>: <message>`) still identify the exact
|
|
39
|
+
// field the model produced wrongly. This matches the prompt-driven
|
|
40
|
+
// self-correction loop in `parseStructuredOutput`, which needs to tell
|
|
41
|
+
// the model which key to drop.
|
|
42
|
+
if (issue.code === "unrecognized_keys" && Array.isArray(issue.keys) && issue.keys.length > 0) {
|
|
43
|
+
return issue.keys.map((key) => ({
|
|
44
|
+
path: pathToString([...issue.path, key]),
|
|
45
|
+
message: issue.message,
|
|
46
|
+
...(issue.code ? { code: issue.code } : {}),
|
|
47
|
+
...(issue.expected !== undefined ? { expected: issue.expected } : {}),
|
|
48
|
+
...(issue.received !== undefined ? { received: issue.received } : {}),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return [{
|
|
53
|
+
path: pathToString(issue.path),
|
|
54
|
+
message: issue.message,
|
|
55
|
+
...(issue.code ? { code: issue.code } : {}),
|
|
56
|
+
...(issue.expected !== undefined ? { expected: issue.expected } : {}),
|
|
57
|
+
...(issue.received !== undefined ? { received: issue.received } : {}),
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate `data` against `schema`. Returns an empty array on success and
|
|
63
|
+
* a stable `{path, message, ...}` shape on failure. Callers format the
|
|
64
|
+
* result for prompts/CLI/UI without further normalisation.
|
|
65
|
+
*/
|
|
66
|
+
export function collectSchemaValidationErrors(schema: ZodType, data: unknown): ValidationError[] {
|
|
67
|
+
const result = schema.safeParse(data);
|
|
68
|
+
if (result.success) return [];
|
|
69
|
+
return result.error.issues.flatMap((issue) => expandIssue(issue as ZodIssueLike));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Convenience wrapper. Equivalent to `collectSchemaValidationErrors(...).length === 0`. */
|
|
73
|
+
export function checkSchema(schema: ZodType, data: unknown): boolean {
|
|
74
|
+
return schema.safeParse(data).success;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse `data` against `schema`. On success returns the schema-validated
|
|
79
|
+
* (and Zod-transformed) value; on failure returns the flattened error list.
|
|
80
|
+
*/
|
|
81
|
+
export function parseSchema<T>(schema: ZodType<T>, data: unknown): { success: true; data: T } | { success: false; errors: ValidationError[] } {
|
|
82
|
+
const result = schema.safeParse(data);
|
|
83
|
+
if (result.success) return { success: true, data: result.data };
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
errors: result.error.issues.flatMap((issue) => expandIssue(issue as ZodIssueLike)),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
// One canonical renderer lives in `./template.ts`. Neither has a review-
|
|
13
13
|
// specific name any more.
|
|
14
14
|
|
|
15
|
-
import type {
|
|
16
|
-
import { Value } from "@sinclair/typebox/value";
|
|
15
|
+
import type { ZodType } from "zod/v4";
|
|
17
16
|
import invalidOutputRetryPrompt from "./prompts/invalid-output-retry.md" with { type: "text" };
|
|
18
17
|
import { runStructuredAgentSession } from "./final-message.js";
|
|
19
18
|
import { renderTemplate } from "./template.js";
|
|
@@ -21,6 +20,7 @@ import { stripMarkdownCodeFence } from "../text.js";
|
|
|
21
20
|
import type { GateExecutionContext, ReliabilityOutcome, ValidationError } from "../types.js";
|
|
22
21
|
import type { PlatformPaths } from "../platform/types.js";
|
|
23
22
|
import { appendReliabilityRecord } from "../storage/reliability-metrics.js";
|
|
23
|
+
import { collectSchemaValidationErrors, parseSchema } from "./schema-validation.js";
|
|
24
24
|
|
|
25
25
|
export interface StructuredParseResult<T> {
|
|
26
26
|
output: T | null;
|
|
@@ -94,35 +94,34 @@ function truncateForPrompt(text: string, maxLength = 1200): string {
|
|
|
94
94
|
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
return path.replace(/^\//, "").replace(/\//g, ".") || "(root)";
|
|
99
|
-
}
|
|
97
|
+
|
|
100
98
|
|
|
101
99
|
/**
|
|
102
|
-
* Collect schema validation errors
|
|
103
|
-
*
|
|
104
|
-
*
|
|
100
|
+
* Collect schema validation errors in a stable {path, message} shape. Used by
|
|
101
|
+
* parseStructuredOutput and by any code that needs to format schema-check
|
|
102
|
+
* failures for humans or prompts.
|
|
105
103
|
*/
|
|
106
|
-
export function collectValidationErrors(schema:
|
|
107
|
-
return
|
|
108
|
-
path: normalizeErrorPath(error.path),
|
|
109
|
-
message: error.message,
|
|
110
|
-
}));
|
|
104
|
+
export function collectValidationErrors(schema: ZodType, data: unknown): ValidationError[] {
|
|
105
|
+
return collectSchemaValidationErrors(schema, data);
|
|
111
106
|
}
|
|
112
107
|
|
|
113
108
|
/**
|
|
114
109
|
* Render validation errors as `path: message` lines.
|
|
115
110
|
*/
|
|
116
111
|
export function formatValidationErrors(errors: ValidationError[]): string[] {
|
|
117
|
-
return errors.map((error) =>
|
|
112
|
+
return errors.map((error) => {
|
|
113
|
+
const code = error.code ? ` [${error.code}]` : "";
|
|
114
|
+
const expected = error.expected !== undefined ? ` Expected: ${JSON.stringify(error.expected)}.` : "";
|
|
115
|
+
return `${error.path}${code}: ${error.message}${expected}`;
|
|
116
|
+
});
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
/**
|
|
121
|
-
* Strip markdown fences, JSON-parse, and schema-check
|
|
120
|
+
* Strip markdown fences, JSON-parse, and schema-check.
|
|
122
121
|
* Returns {output: T, error: null} on success; {output: null, error: string}
|
|
123
122
|
* on failure with a human-readable error suitable for retry prompts.
|
|
124
123
|
*/
|
|
125
|
-
export function parseStructuredOutput<T>(raw: string, schema:
|
|
124
|
+
export function parseStructuredOutput<T>(raw: string, schema: ZodType<T>): StructuredParseResult<T> {
|
|
126
125
|
let parsed: unknown;
|
|
127
126
|
|
|
128
127
|
try {
|
|
@@ -134,8 +133,9 @@ export function parseStructuredOutput<T>(raw: string, schema: TSchema): Structur
|
|
|
134
133
|
};
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
const result = parseSchema<T>(schema, parsed);
|
|
137
|
+
if (!result.success) {
|
|
138
|
+
const errors = formatValidationErrors(result.errors);
|
|
139
139
|
return {
|
|
140
140
|
output: null,
|
|
141
141
|
error: errors.length > 0 ? errors.join("; ") : "Output does not match the required schema.",
|
|
@@ -143,7 +143,7 @@ export function parseStructuredOutput<T>(raw: string, schema: TSchema): Structur
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
return {
|
|
146
|
-
output:
|
|
146
|
+
output: result.data,
|
|
147
147
|
error: null,
|
|
148
148
|
};
|
|
149
149
|
}
|
package/src/bootstrap.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { registerUltraPlanAuthoringTool } from "./ultraplan/authoring-tool.js";
|
|
|
43
43
|
import { registerUltraPlanAuthoringPipelineTools } from "./ultraplan/authoring/authoring-tools.js";
|
|
44
44
|
import { registerActiveToolController } from "./tool-catalog/active-tool-controller.js";
|
|
45
45
|
import { registerMempalaceHooks } from "./mempalace/hooks.js";
|
|
46
|
+
import { registerRunbookCommand, handleRunbook } from "./commands/runbook.js";
|
|
46
47
|
import { registerMempalaceTool } from "./mempalace/tool.js";
|
|
47
48
|
|
|
48
49
|
// TUI-only commands — intercepted at the input level to prevent
|
|
@@ -65,6 +66,7 @@ const TUI_COMMANDS: Record<string, (platform: Platform, ctx: any, args?: string)
|
|
|
65
66
|
"supi:ultraplan": (platform, ctx, args) => handleUltraplan(platform, ctx, args),
|
|
66
67
|
"supi:harness": (platform, ctx, args) => { void handleHarness(platform, ctx, args); },
|
|
67
68
|
"supi:memory": (platform, ctx, args) => handleMemory(platform, ctx, args),
|
|
69
|
+
"runbook": (platform, ctx, args) => handleRunbook(platform, ctx, args),
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
function getInstalledVersion(platform: Platform): string | null {
|
|
@@ -101,6 +103,7 @@ export function bootstrap(platform: Platform): void {
|
|
|
101
103
|
registerUltraplanCommand(platform);
|
|
102
104
|
registerHarnessCommand(platform);
|
|
103
105
|
registerMemoryCommand(platform);
|
|
106
|
+
registerRunbookCommand(platform);
|
|
104
107
|
|
|
105
108
|
|
|
106
109
|
registerUltraPlanRuntimeTools(platform);
|