@miphamai/cli 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { SubAgent } from '../../agent/sub-agent'
2
2
  import type { ProviderRegistry } from '../../providers/registry'
3
3
  import type { ToolDefinition } from '../../shared/index.ts'
4
+ import type { PermissionSystem } from '../../core/permission'
4
5
  import { validateJSONSchema, formatValidationErrors } from '../schema-validator'
5
6
 
6
7
  export interface WorkflowAgentOpts {
@@ -12,17 +13,28 @@ export interface WorkflowAgentOpts {
12
13
  effort?: 'low' | 'medium' | 'high' | 'max'
13
14
  /** Maximum retries on schema validation failure (default: 2). */
14
15
  maxRetries?: number
16
+ /** Permission system for sub-agent tool execution (optional). */
17
+ permissionSystem?: PermissionSystem
18
+ /** Run the agent in an isolated git worktree (optional). */
19
+ isolation?: 'worktree'
15
20
  }
16
21
 
17
22
  /**
18
23
  * Workflow agent() primitive — creates a SubAgent with optional
19
- * provider/model override and structured output schema validation.
24
+ * provider/model override, structured output schema validation,
25
+ * and git worktree isolation.
20
26
  *
21
27
  * When `schema` is provided:
22
28
  * 1. The sub-agent is prompted to return valid JSON matching the schema.
23
29
  * 2. The result is JSON.parsed and validated against the schema.
24
30
  * 3. On validation failure, the sub-agent is retried with error feedback.
25
31
  * 4. Returns the validated object, or { raw, validationErrors } on final failure.
32
+ *
33
+ * When `isolation: 'worktree'` is set:
34
+ * 1. A git worktree is created at .claude/worktrees/wf-<slug>
35
+ * 2. The sub-agent runs with its cwd set to the worktree path
36
+ * 3. Changes are auto-committed (best-effort)
37
+ * 4. The worktree is cleaned up after execution
26
38
  */
27
39
  export async function workflowAgent(
28
40
  prompt: string,
@@ -39,6 +51,26 @@ export async function workflowAgent(
39
51
 
40
52
  const maxRetries = opts.maxRetries ?? 2
41
53
 
54
+ // ── Worktree isolation setup ──
55
+ let worktreePath: string | undefined
56
+ let worktreeBranch: string | undefined
57
+
58
+ if (opts.isolation === 'worktree') {
59
+ const slug = `wf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
60
+ worktreeBranch = `worktree/${slug}`
61
+ worktreePath = `.claude/worktrees/${slug}`
62
+
63
+ const proc = Bun.spawn(['git', 'worktree', 'add', '-b', worktreeBranch, worktreePath, 'HEAD'], {
64
+ stdout: 'pipe',
65
+ stderr: 'pipe',
66
+ })
67
+ const exitCode = await proc.exited
68
+ if (exitCode !== 0) {
69
+ const stderr = await new Response(proc.stderr).text()
70
+ throw new Error(`Worktree creation failed: ${stderr.slice(0, 500)}`)
71
+ }
72
+ }
73
+
42
74
  // Build the prompt — if schema is requested, instruct the model to return JSON
43
75
  let effectivePrompt = prompt
44
76
  if (opts.schema) {
@@ -50,64 +82,118 @@ export async function workflowAgent(
50
82
  `Return ONLY the JSON object, no other text. Do not wrap in markdown code fences.`
51
83
  }
52
84
 
53
- const sub = new SubAgent(registry, toolRegistry)
85
+ const sub = new SubAgent(registry, toolRegistry, opts.permissionSystem)
54
86
 
55
- // ── Attempt execution with optional schema validation + retry ──
87
+ // ── Execute with optional schema validation + retry ──
88
+ let result: unknown = ''
56
89
  let lastResult = ''
57
90
  let lastErrors: string[] = []
58
91
 
59
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
60
- const retryPrompt =
61
- attempt === 0
62
- ? effectivePrompt
63
- : `${effectivePrompt}\n\n` +
64
- `[RETRY #${attempt}] Your previous response did NOT match the required schema.\n` +
65
- `Validation errors:\n${lastErrors.map((e) => ` • ${e}`).join('\n')}\n\n` +
66
- `Please fix the errors and return a valid JSON object matching the schema exactly.`
67
-
68
- const result = await sub.execute(retryPrompt, opts.label || 'workflow-agent', {
69
- type: 'general',
70
- modelOverride: opts.model,
71
- allowedTools: undefined, // use all tools by default
72
- })
92
+ try {
93
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
94
+ const retryPrompt =
95
+ attempt === 0
96
+ ? effectivePrompt
97
+ : `${effectivePrompt}\n\n` +
98
+ `[RETRY #${attempt}] Your previous response did NOT match the required schema.\n` +
99
+ `Validation errors:\n${lastErrors.map((e) => ` • ${e}`).join('\n')}\n\n` +
100
+ `Please fix the errors and return a valid JSON object matching the schema exactly.`
73
101
 
74
- lastResult = result
102
+ const textResult = await sub.execute(retryPrompt, opts.label || 'workflow-agent', {
103
+ type: 'general',
104
+ modelOverride: opts.model,
105
+ allowedTools: undefined, // use all tools by default
106
+ worktreePath,
107
+ })
75
108
 
76
- // No schema — return raw result
77
- if (!opts.schema) {
78
- return result
79
- }
109
+ lastResult = textResult
80
110
 
81
- // Try to parse and validate
82
- try {
83
- // Extract JSON from potential markdown fences
84
- let jsonStr = result.trim()
85
- const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/)
86
- if (fenceMatch) {
87
- jsonStr = fenceMatch[1]!.trim()
111
+ // No schema — return raw result
112
+ if (!opts.schema) {
113
+ result = textResult
114
+ return result
88
115
  }
89
116
 
90
- const parsed = JSON.parse(jsonStr)
91
- const errors = validateJSONSchema(parsed, opts.schema as Record<string, unknown>)
117
+ // Try to parse and validate
118
+ try {
119
+ // Extract JSON from potential markdown fences
120
+ let jsonStr = textResult.trim()
121
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/)
122
+ if (fenceMatch) {
123
+ jsonStr = fenceMatch[1]!.trim()
124
+ }
92
125
 
93
- if (errors.length === 0) {
94
- // Valid! Return the parsed object
95
- return parsed
126
+ const parsed = JSON.parse(jsonStr)
127
+ const errors = validateJSONSchema(parsed, opts.schema as Record<string, unknown>)
128
+
129
+ if (errors.length === 0) {
130
+ // Valid! Return the parsed object
131
+ result = parsed
132
+ return result
133
+ }
134
+
135
+ // Not valid — collect errors for retry
136
+ lastErrors = [formatValidationErrors(errors)]
137
+ } catch (err) {
138
+ // JSON parse error
139
+ lastErrors = [`JSON parse error: ${String(err)}. Ensure your response is valid JSON only.`]
96
140
  }
141
+ }
97
142
 
98
- // Not valid — collect errors for retry
99
- lastErrors = [formatValidationErrors(errors)]
100
- } catch (err) {
101
- // JSON parse error
102
- lastErrors = [`JSON parse error: ${String(err)}. Ensure your response is valid JSON only.`]
143
+ // All retries exhausted — return raw result with validation errors
144
+ try {
145
+ const parsed = JSON.parse(lastResult)
146
+ result = { raw: lastResult, parsed, validationErrors: lastErrors }
147
+ } catch {
148
+ result = { raw: lastResult, validationErrors: lastErrors }
103
149
  }
104
- }
105
150
 
106
- // All retries exhausted — return raw result with validation errors
107
- try {
108
- const parsed = JSON.parse(lastResult)
109
- return { raw: lastResult, parsed, validationErrors: lastErrors }
110
- } catch {
111
- return { raw: lastResult, validationErrors: lastErrors }
151
+ return result
152
+ } finally {
153
+ // ── Cleanup worktree ──
154
+ if (worktreePath) {
155
+ // Best-effort: auto-commit any changes
156
+ try {
157
+ const addProc = Bun.spawn(['git', '-C', worktreePath, 'add', '-A'], {
158
+ stdout: 'pipe',
159
+ stderr: 'pipe',
160
+ })
161
+ await addProc.exited
162
+
163
+ const statusProc = Bun.spawn(['git', '-C', worktreePath, 'status', '--porcelain'], {
164
+ stdout: 'pipe',
165
+ stderr: 'pipe',
166
+ })
167
+ const status = await new Response(statusProc.stdout).text()
168
+ if (status.trim()) {
169
+ const commitMsg = opts.label ? `workflow: ${opts.label}` : 'workflow: agent changes'
170
+ const commitProc = Bun.spawn(['git', '-C', worktreePath, 'commit', '-m', commitMsg], {
171
+ stdout: 'pipe',
172
+ stderr: 'pipe',
173
+ })
174
+ await commitProc.exited
175
+ }
176
+ } catch {
177
+ /* best-effort */
178
+ }
179
+
180
+ // Remove worktree and branch
181
+ try {
182
+ const rmProc = Bun.spawn(['git', 'worktree', 'remove', '--force', worktreePath], {
183
+ stdout: 'pipe',
184
+ stderr: 'pipe',
185
+ })
186
+ await rmProc.exited
187
+ if (worktreeBranch) {
188
+ const brProc = Bun.spawn(['git', 'branch', '-D', worktreeBranch], {
189
+ stdout: 'pipe',
190
+ stderr: 'pipe',
191
+ })
192
+ await brProc.exited
193
+ }
194
+ } catch {
195
+ /* best-effort */
196
+ }
197
+ }
112
198
  }
113
199
  }
@@ -0,0 +1,103 @@
1
+ import { parallel } from './parallel'
2
+
3
+ /**
4
+ * loopUntilConvergence() — workflow primitive for iterative discovery with
5
+ * deduplication, adversarial verification, and convergence detection.
6
+ *
7
+ * Orchestrates multiple "finders" over successive rounds until no new items
8
+ * are discovered for `dryRounds` consecutive rounds (convergence), or
9
+ * `maxRounds` is reached (cutoff).
10
+ *
11
+ * Each item is keyed by `keyFn` for deduplication across rounds. If a
12
+ * `verify` function is provided, only items that survive verification
13
+ * enter the confirmed set — but all seen items (even those that fail
14
+ * verification) are tracked in the seen-set to prevent re-discovery.
15
+ */
16
+
17
+ export interface VerifyVote {
18
+ real: boolean
19
+ reason: string
20
+ }
21
+
22
+ export interface VerifyResult<T = unknown> {
23
+ finding: T
24
+ survives: boolean
25
+ votes: VerifyVote[]
26
+ score: number
27
+ }
28
+
29
+ export interface LoopUntilConvergenceResult<T> {
30
+ confirmed: T[]
31
+ totalSeen: number
32
+ rounds: number
33
+ converged: boolean
34
+ }
35
+
36
+ export interface LoopUntilConvergenceOpts<T> {
37
+ finders: Array<() => Promise<{ items: T[] } | null>>
38
+ keyFn: (item: T) => string
39
+ verify?: (item: T) => Promise<VerifyResult<T>>
40
+ dryRounds?: number
41
+ maxRounds?: number
42
+ }
43
+
44
+ export async function loopUntilConvergence<T>(
45
+ opts: LoopUntilConvergenceOpts<T>,
46
+ ): Promise<LoopUntilConvergenceResult<T>> {
47
+ const dryRounds = opts.dryRounds ?? 2
48
+ const maxRounds = opts.maxRounds ?? 20
49
+
50
+ const seen = new Set<string>()
51
+ const confirmed: T[] = []
52
+ let dry = 0
53
+ let rounds = 0
54
+
55
+ while (dry < dryRounds && rounds < maxRounds) {
56
+ rounds++
57
+
58
+ // FAN OUT: all finders run in parallel
59
+ const raw = await parallel(opts.finders.map((f) => () => f()))
60
+
61
+ // EDGE LOGIC: flatMap + dedup (pure JS, zero tokens)
62
+ const items: T[] = []
63
+ for (const result of raw) {
64
+ if (result && result.items) {
65
+ items.push(...result.items)
66
+ }
67
+ }
68
+
69
+ // Dedup against SEEN set, not confirmed
70
+ const fresh = items.filter((item) => {
71
+ const key = opts.keyFn(item)
72
+ if (seen.has(key)) return false
73
+ seen.add(key)
74
+ return true
75
+ })
76
+
77
+ if (fresh.length === 0) {
78
+ dry++ // no new unique items → trending toward convergence
79
+ continue
80
+ }
81
+
82
+ dry = 0 // new items found → reset dry counter
83
+
84
+ // VERIFY: optional quality gate
85
+ if (opts.verify) {
86
+ const judged = await parallel(fresh.map((item) => () => opts.verify!(item)))
87
+ for (const j of judged) {
88
+ if (j && j.survives) {
89
+ confirmed.push(j.finding as T)
90
+ }
91
+ }
92
+ } else {
93
+ confirmed.push(...fresh)
94
+ }
95
+ }
96
+
97
+ return {
98
+ confirmed,
99
+ totalSeen: seen.size,
100
+ rounds,
101
+ converged: dry >= dryRounds,
102
+ }
103
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * verify() and judge() — workflow primitives for adversarial verification,
3
+ * multi-perspective review, and multi-judge evaluation.
4
+ *
5
+ * verify() supports 3 modes:
6
+ * - adversarial: N skeptics try to refute the finding (majority wins)
7
+ * - perspective: N lenses each evaluate from a specific angle (at least 1 confirms)
8
+ * - consensus: N voters must unanimously agree
9
+ *
10
+ * judge() evaluates N attempts by M judges, computes average scores,
11
+ * picks the winner, and optionally synthesizes a final result.
12
+ */
13
+
14
+ import { workflowAgent } from './agent'
15
+ import { parallel } from './parallel'
16
+ import type { WorkflowAgentOpts } from './agent'
17
+
18
+ // Agent function signature matching the sandbox-injected pattern:
19
+ // (prompt, opts?) → result. In production, the workflow runtime binds
20
+ // ProviderRegistry + ToolRegistry into a function of this shape and
21
+ // injects it via _mockAgent.
22
+ type AgentFn = (prompt: string, opts?: WorkflowAgentOpts) => Promise<unknown>
23
+
24
+ // ── Types ──
25
+
26
+ export interface VerifyResult {
27
+ finding: unknown
28
+ survives: boolean
29
+ votes: Array<{ real: boolean; reason: string; lens?: string }>
30
+ score: number
31
+ }
32
+
33
+ export type VerifyMode = 'adversarial' | 'perspective' | 'consensus'
34
+
35
+ export interface VerifyOpts {
36
+ mode: VerifyMode
37
+ skeptics?: number // adversarial: default 3
38
+ lenses?: string[] // perspective: e.g. ['correctness', 'security']
39
+ voters?: number // consensus: default 3
40
+ threshold?: number
41
+ schema: Record<string, unknown>
42
+ /** Test-only: inject a mock agent function. When not provided, falls back to workflowAgent. */
43
+ _mockAgent?: (prompt: string, opts?: WorkflowAgentOpts) => Promise<unknown>
44
+ }
45
+
46
+ interface VerdictVote {
47
+ real: boolean
48
+ reason: string
49
+ lens?: string
50
+ }
51
+
52
+ export interface JudgeResult {
53
+ winner: unknown
54
+ winnerIndex: number
55
+ scores: Array<{
56
+ attemptIndex: number
57
+ judgeIndex: number
58
+ criteria: Record<string, number>
59
+ total: number
60
+ notes: string
61
+ }>
62
+ synthesis?: string
63
+ }
64
+
65
+ export interface JudgeOpts {
66
+ criteria: string[]
67
+ judges?: number // default: 3
68
+ synthesize?: boolean // default: true
69
+ schema: Record<string, unknown>
70
+ /** Test-only: inject a mock agent function. */
71
+ _mockAgent?: (prompt: string, opts?: WorkflowAgentOpts) => Promise<unknown>
72
+ }
73
+
74
+ // ── Helpers ──
75
+
76
+ function defaultThreshold(mode: VerifyMode, total: number): number {
77
+ if (mode === 'consensus') return total // all must agree
78
+ if (mode === 'perspective') return 1 // at least one lens confirms
79
+ return Math.ceil(total / 2) // adversarial: majority
80
+ }
81
+
82
+ // ── verify() ──
83
+
84
+ export async function verify(finding: unknown, opts: VerifyOpts): Promise<VerifyResult> {
85
+ // Prefer the test hook (_mockAgent), fall back to workflowAgent.
86
+ // In production sandbox usage, _mockAgent is set to the pre-bound agent
87
+ // function injected by the workflow runtime.
88
+ const agentFn: AgentFn = opts._mockAgent ?? (workflowAgent as unknown as AgentFn)
89
+ const mode = opts.mode
90
+
91
+ const findingStr = JSON.stringify(finding, null, 2)
92
+ const schemaDesc = JSON.stringify(opts.schema)
93
+
94
+ let prompts: Array<{ prompt: string; lens?: string }>
95
+
96
+ switch (mode) {
97
+ case 'adversarial': {
98
+ const count = opts.skeptics ?? 3
99
+ prompts = Array.from({ length: count }, (_, i) => ({
100
+ prompt:
101
+ `You are a skeptical reviewer (skeptic #${i + 1}). Try to REFUTE this finding. Default to real=false if uncertain.\n\n` +
102
+ `Finding:\n${findingStr}\n\nReturn JSON matching this schema:\n${schemaDesc}`,
103
+ }))
104
+ break
105
+ }
106
+ case 'perspective': {
107
+ const lenses = opts.lenses ?? ['correctness']
108
+ prompts = lenses.map((lens) => ({
109
+ prompt:
110
+ `Judge this finding through the "${lens}" lens. Is it valid from this perspective?\n\n` +
111
+ `Finding:\n${findingStr}\n\nReturn JSON matching this schema:\n${schemaDesc}`,
112
+ lens,
113
+ }))
114
+ break
115
+ }
116
+ case 'consensus': {
117
+ const count = opts.voters ?? 3
118
+ prompts = Array.from({ length: count }, () => ({
119
+ prompt:
120
+ `Is this finding correct? Be honest and critical. Vote real=true only if you are fully convinced.\n\n` +
121
+ `Finding:\n${findingStr}\n\nReturn JSON matching this schema:\n${schemaDesc}`,
122
+ }))
123
+ break
124
+ }
125
+ }
126
+
127
+ // Fan out all agent calls concurrently
128
+ const rawVotes = await parallel(
129
+ prompts.map(
130
+ (p) => () => agentFn(p.prompt, { schema: opts.schema as WorkflowAgentOpts['schema'] }),
131
+ ),
132
+ )
133
+
134
+ // Filter out failed agents (null) and build vote objects
135
+ const votes: VerdictVote[] = []
136
+ for (let i = 0; i < rawVotes.length; i++) {
137
+ const v = rawVotes[i]
138
+ if (v && typeof v === 'object') {
139
+ const vote = v as Record<string, unknown>
140
+ votes.push({
141
+ real: Boolean(vote.real),
142
+ reason: String(vote.reason ?? ''),
143
+ lens: prompts[i]?.lens,
144
+ })
145
+ }
146
+ }
147
+
148
+ const threshold = opts.threshold ?? defaultThreshold(mode, votes.length)
149
+ const realCount = votes.filter((v) => v.real).length
150
+ const survives = realCount >= threshold
151
+
152
+ return {
153
+ finding,
154
+ survives,
155
+ votes,
156
+ score: votes.length > 0 ? realCount / votes.length : 0,
157
+ }
158
+ }
159
+
160
+ // ── judge() ──
161
+
162
+ export async function judge(attempts: unknown[], opts: JudgeOpts): Promise<JudgeResult> {
163
+ // Prefer the test hook, fall back to workflowAgent.
164
+ const agentFn: AgentFn = opts._mockAgent ?? (workflowAgent as unknown as AgentFn)
165
+ const judgeCount = opts.judges ?? 3
166
+ const schemaDesc = JSON.stringify(opts.schema)
167
+
168
+ // Phase 1: each judge scores each attempt
169
+ interface ScoreEntry {
170
+ attemptIndex: number
171
+ judgeIndex: number
172
+ criteria: Record<string, number>
173
+ total: number
174
+ notes: string
175
+ }
176
+
177
+ // Build the cross-product of judges × attempts
178
+ const scorePrompts: Array<{
179
+ attempt: unknown
180
+ attemptIndex: number
181
+ judgeIndex: number
182
+ }> = []
183
+ for (let ji = 0; ji < judgeCount; ji++) {
184
+ for (let ai = 0; ai < attempts.length; ai++) {
185
+ scorePrompts.push({
186
+ attempt: attempts[ai],
187
+ attemptIndex: ai,
188
+ judgeIndex: ji,
189
+ })
190
+ }
191
+ }
192
+
193
+ // Fan out all scoring calls concurrently
194
+ const rawScores = await parallel(
195
+ scorePrompts.map(
196
+ (sp) => () =>
197
+ agentFn(
198
+ `You are judge #${sp.judgeIndex + 1}. Score this attempt against the criteria: ${opts.criteria.join(', ')}.\n\n` +
199
+ `Attempt:\n${JSON.stringify(sp.attempt, null, 2)}\n\n` +
200
+ `Return JSON matching this schema:\n${schemaDesc}`,
201
+ { schema: opts.schema as WorkflowAgentOpts['schema'] },
202
+ ),
203
+ ),
204
+ )
205
+
206
+ // Parse and validate each score result
207
+ const scores: ScoreEntry[] = []
208
+ for (let i = 0; i < rawScores.length; i++) {
209
+ const raw = rawScores[i]
210
+ const sp = scorePrompts[i]!
211
+ if (raw && typeof raw === 'object') {
212
+ const obj = raw as Record<string, unknown>
213
+ const criteriaObj = (obj.scores as Record<string, number>) ?? {}
214
+ const total = Object.values(criteriaObj).reduce(
215
+ (sum, v) => sum + (typeof v === 'number' ? v : 0),
216
+ 0,
217
+ )
218
+ scores.push({
219
+ attemptIndex: sp.attemptIndex,
220
+ judgeIndex: sp.judgeIndex,
221
+ criteria: criteriaObj,
222
+ total,
223
+ notes: String(obj.notes ?? ''),
224
+ })
225
+ }
226
+ }
227
+
228
+ // Compute winner: highest average total across judges
229
+ const attemptTotals = new Map<number, number>()
230
+ const attemptCounts = new Map<number, number>()
231
+ for (const s of scores) {
232
+ attemptTotals.set(s.attemptIndex, (attemptTotals.get(s.attemptIndex) ?? 0) + s.total)
233
+ attemptCounts.set(s.attemptIndex, (attemptCounts.get(s.attemptIndex) ?? 0) + 1)
234
+ }
235
+
236
+ let winnerIndex = 0
237
+ let bestAvg = -Infinity
238
+ for (const [idx, total] of attemptTotals) {
239
+ const count = attemptCounts.get(idx) ?? 1
240
+ const avg = total / count
241
+ if (avg > bestAvg) {
242
+ bestAvg = avg
243
+ winnerIndex = idx
244
+ }
245
+ }
246
+
247
+ // Phase 2: optional synthesis
248
+ let synthesis: string | undefined
249
+ if (opts.synthesize !== false) {
250
+ const winner = attempts[winnerIndex]
251
+ const runnerUps = attempts
252
+ .map((a, i) => ({ attempt: a, index: i }))
253
+ .filter((e) => e.index !== winnerIndex)
254
+
255
+ const synthPrompt =
256
+ `Synthesize the final result from the WINNING approach, grafting the best ideas from runner-ups.\n\n` +
257
+ `WINNER:\n${JSON.stringify(winner, null, 2)}\n\n` +
258
+ `RUNNER-UPS:\n${JSON.stringify(runnerUps, null, 2)}\n\n` +
259
+ `Provide a comprehensive synthesis combining the winner's structure with the best elements from other approaches.`
260
+
261
+ const synthResult = await agentFn(synthPrompt)
262
+ if (typeof synthResult === 'string') {
263
+ synthesis = synthResult
264
+ } else if (synthResult && typeof synthResult === 'object' && 'synthesis' in synthResult) {
265
+ synthesis = String((synthResult as Record<string, unknown>).synthesis)
266
+ }
267
+ }
268
+
269
+ return {
270
+ winner: attempts[winnerIndex],
271
+ winnerIndex,
272
+ scores,
273
+ synthesis,
274
+ }
275
+ }
@@ -5,6 +5,8 @@ import { workflowAgent } from './primitives/agent'
5
5
  import { parallel } from './primitives/parallel'
6
6
  import { pipeline } from './primitives/pipeline'
7
7
  import { phase as phasePrimitive } from './primitives/phase'
8
+ import { verify, judge } from './primitives/verify'
9
+ import { loopUntilConvergence } from './primitives/loop'
8
10
  import type { ProviderRegistry } from '../providers/registry'
9
11
  import type { QueryEngine } from '../core/engine'
10
12
 
@@ -94,6 +96,7 @@ export async function runWorkflow(
94
96
 
95
97
  const registry: ProviderRegistry = engine.getRegistry()
96
98
  const toolRegistry = engine.getTools()
99
+ const permission = engine.getPermission()
97
100
 
98
101
  const budget = createBudget(budgetTotal)
99
102
 
@@ -109,12 +112,10 @@ export async function runWorkflow(
109
112
  }
110
113
  cacheMisses++
111
114
 
112
- const result = await workflowAgent(
113
- prompt,
114
- registry,
115
- toolRegistry,
116
- opts as Record<string, unknown>,
117
- )
115
+ const result = await workflowAgent(prompt, registry, toolRegistry, {
116
+ ...(opts || {}),
117
+ permissionSystem: permission,
118
+ } as Record<string, unknown>)
118
119
  appendJournal(runId, {
119
120
  type: 'agent',
120
121
  prompt,
@@ -147,6 +148,9 @@ export async function runWorkflow(
147
148
  'agent',
148
149
  'parallel',
149
150
  'pipeline',
151
+ 'verify',
152
+ 'judge',
153
+ 'loopUntilConvergence',
150
154
  'phase',
151
155
  'log',
152
156
  'args',
@@ -154,7 +158,18 @@ export async function runWorkflow(
154
158
  wrappedScript,
155
159
  )
156
160
 
157
- const result = await scriptFn(agent, parallel, pipeline, wrappedPhase, log, args, budget)
161
+ const result = await scriptFn(
162
+ agent,
163
+ parallel,
164
+ pipeline,
165
+ verify,
166
+ judge,
167
+ loopUntilConvergence,
168
+ wrappedPhase,
169
+ log,
170
+ args,
171
+ budget,
172
+ )
158
173
 
159
174
  // Count journal entries from state
160
175
  const priorEntries = loadJournal(runId)