@signalridge/pi-workflows 1.2.1 → 1.3.2

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 (49) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +142 -17
  3. package/package.json +14 -8
  4. package/skills/workflow-authoring/SKILL.md +30 -0
  5. package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
  6. package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
  7. package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
  8. package/skills/workflow-authoring/examples/defensive-json-parsing.js +63 -0
  9. package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
  10. package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
  11. package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
  12. package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
  13. package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
  14. package/skills/workflow-authoring/examples/structured-output.js +37 -0
  15. package/skills/workflow-authoring/examples/tournament.js +81 -0
  16. package/skills/workflow-authoring/examples/validated-gate.js +63 -0
  17. package/skills/workflow-authoring/references/capabilities.md +44 -0
  18. package/skills/workflow-authoring/references/capability-details.md +294 -0
  19. package/skills/workflow-authoring/references/common-helpers.md +4 -0
  20. package/skills/workflow-authoring/references/debugging.md +29 -0
  21. package/skills/workflow-authoring/references/focused-recipes.md +12 -0
  22. package/skills/workflow-authoring/references/helpers.md +9 -0
  23. package/skills/workflow-authoring/references/lifecycle.md +37 -0
  24. package/skills/workflow-authoring/references/pattern-selection.md +15 -0
  25. package/skills/workflow-authoring/references/quality-helpers.md +8 -0
  26. package/skills/workflow-authoring/references/registry-ownership.md +17 -0
  27. package/skills/workflow-authoring/references/retry-helper.md +5 -0
  28. package/skills/workflow-authoring/references/review.md +43 -0
  29. package/skills/workflow-authoring/references/runtime.md +29 -0
  30. package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
  31. package/skills/workflow-authoring/references/versions.md +13 -0
  32. package/skills/workflow-patterns/SKILL.md +51 -0
  33. package/src/arming.ts +69 -0
  34. package/src/builtins.ts +331 -0
  35. package/src/capabilities.ts +318 -0
  36. package/src/code-review-scope.ts +144 -0
  37. package/src/engine.ts +1158 -2561
  38. package/src/errors.ts +136 -0
  39. package/src/index.ts +750 -103
  40. package/src/journal.ts +594 -1001
  41. package/src/rpc-client.ts +95 -24
  42. package/src/runtime.ts +1708 -0
  43. package/src/saved-workflows.ts +121 -0
  44. package/src/shared-store.ts +132 -0
  45. package/src/ui.ts +79 -42
  46. package/src/workflow-settings.ts +100 -0
  47. package/src/schema.ts +0 -215
  48. package/src/state-machine.ts +0 -79
  49. package/src/synthesis.ts +0 -140
package/src/arming.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * arming.ts — the keyword that authorizes the workflow tool for a turn.
3
+ *
4
+ * Typing the bounded word `workflow` (or `workflows`, or a configured synonym)
5
+ * in an ordinary message is an explicit opt-in to multi-agent orchestration, so
6
+ * the message is annotated to tell the model the tool is available for this
7
+ * turn. Two properties matter, and both are easy to get wrong:
8
+ *
9
+ * 1. **Arming authorizes; it does not compel.** The directive says the model
10
+ * MAY run a workflow and may still decline, so "how do workflows work?"
11
+ * stays an ordinary question with an ordinary answer. A trigger that forced
12
+ * a run would make the word unusable in conversation.
13
+ *
14
+ * 2. **Only a real word counts.** A workflow is something people also write
15
+ * code about, so `myworkflow`, `workflow_name`, `src/workflow-editor.ts`,
16
+ * `workflow.ts`, and `--workflow-id` must not arm anything. A bare `\b`
17
+ * boundary is not enough: it treats `-`, `/`, and `.` as separators, so it
18
+ * matches inside every one of those but the identifier.
19
+ */
20
+
21
+ /** The word that arms the tool when no synonym is configured. */
22
+ export const DEFAULT_TRIGGER_WORD = "workflow";
23
+
24
+ /**
25
+ * Appended to an armed message. Phrased as permission with an explicit escape
26
+ * hatch: the model is told the user opted in, and told it may still answer
27
+ * directly when a workflow would be overkill.
28
+ */
29
+ export const WORKFLOW_ARMED_DIRECTIVE =
30
+ "<system-reminder>You typed the workflow trigger word, which counts as an explicit opt-in to multi-agent orchestration: the `workflow` tool is authorized for this turn. It runs in the background by default — the turn ends and the result is delivered back into the conversation when it finishes, which is expected rather than a stall, so you need not stay and block. Pass background:false only when the user is waiting for the result inline. This is permission, not an instruction: if the request is conversational, trivial, or better answered directly, answer it directly and do not run a workflow.</system-reminder>";
31
+
32
+ function escapeRegExp(text: string): string {
33
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34
+ }
35
+
36
+ /**
37
+ * Token boundaries that treat identifier, path, and flag punctuation as PART of
38
+ * the token rather than as a separator. `\p{ID_Continue}` covers letters,
39
+ * digits and `_`; `/`, `-` and `$` are added so paths, kebab-case identifiers,
40
+ * and flags are each one token.
41
+ *
42
+ * A trailing `.` is the one case that cannot be decided by the character alone:
43
+ * `workflow.ts` is a filename, but `a workflow.` is a sentence, and ending a
44
+ * sentence with the word is far too common to refuse. So a following dot blocks
45
+ * the match only when something continues the token after it — the second
46
+ * lookahead below. A LEADING dot stays disqualifying (`.workflow` reads as a
47
+ * dotfile or a property access, never as prose).
48
+ */
49
+ function triggerRegex(word: string): RegExp {
50
+ const escaped = escapeRegExp(word);
51
+ // The default word is the only one that also matches its plural: a configured
52
+ // synonym is matched exactly as the user spelled it.
53
+ const plural = word === DEFAULT_TRIGGER_WORD ? "s?" : "";
54
+ return new RegExp(
55
+ `(?<![/.$\\-\\p{ID_Continue}])${escaped}${plural}(?![/$\\-\\p{ID_Continue}])(?!\\.\\p{ID_Continue})`,
56
+ "iu",
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Whether `text` contains the trigger word as a standalone word.
62
+ *
63
+ * Arms: "run a workflow", "workflows, please", "use the WORKFLOW tool"
64
+ * Does not: "myworkflow", "workflow_name", "src/workflow-editor.ts",
65
+ * "workflow.ts", "--workflow-id", "/workflows" (a slash command)
66
+ */
67
+ export function hasTriggerWord(text: string, configuredWord?: string): boolean {
68
+ return triggerRegex(configuredWord ?? DEFAULT_TRIGGER_WORD).test(text);
69
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * builtins.ts — the five curated built-in workflow patterns.
3
+ *
4
+ * `deep-research` · `adversarial-review` · `code-review` · `multi-perspective`
5
+ * · `codebase-audit`. Each descriptor builds a static script that reads its
6
+ * inputs from `args`, so caller input is never interpolated into source (no
7
+ * escaping hazards). The scripts are written once here and reached through both
8
+ * the `workflow` tool's `name` input and the `/name` slash commands, so the two
9
+ * entry points cannot drift apart.
10
+ *
11
+ * Divergence from upstream: upstream injects host-side `web_search`/`web_fetch`
12
+ * tools for deep research; we do not — agents reach web access through their
13
+ * own configured tools (pi-web-access / MCP). Documented in the README.
14
+ */
15
+
16
+ export interface BuiltinWorkflowDescriptor {
17
+ /** Also the slash-command name (without the leading `/`). */
18
+ name: string;
19
+ description: string;
20
+ script: string;
21
+ /** Optional toolset hint persisted with the run (e.g. "web-research"). */
22
+ toolset?: string;
23
+ /**
24
+ * The `args` key that a bare slash-command argument fills.
25
+ *
26
+ * Each script reads its own named inputs (`question`, `task`, `topic`, …) and
27
+ * none of them reads a generic `prompt`, so the command handler must know
28
+ * which key the user's text belongs in. Getting this wrong is silent: the
29
+ * script simply runs with an empty input and still fans out agents.
30
+ *
31
+ * `code-review` is the exception — its `diff` is not something a user types,
32
+ * so its command resolves a diff first (see `code-review-scope.ts`) and
33
+ * passes `diff` plus `diffSource` itself.
34
+ */
35
+ primaryArg: string;
36
+ }
37
+
38
+ const DEEP_RESEARCH_SCRIPT = `export const meta = {
39
+ name: 'deep_research',
40
+ description: 'Deep research: parallel queries, cross-checked claims, cited report',
41
+ phases: [
42
+ { title: 'Queries' },
43
+ { title: 'Gather' },
44
+ { title: 'Verify' },
45
+ { title: 'Report' },
46
+ ],
47
+ }
48
+
49
+ const question = (args && args.question) || ''
50
+ const angles = (args && args.angles) || 4
51
+ const minSupport = (args && args.minSupport) || 2
52
+
53
+ phase('Queries')
54
+ const plan = await agent(
55
+ 'You are planning web research for this question:\\n' + question +
56
+ '\\n\\nProduce ' + angles + ' diverse, specific search queries that together cover the question from different angles. ' +
57
+ 'Use your web search tools when helpful to refine them.',
58
+ { label: 'plan queries', schema: { type: 'object', properties: { queries: { type: 'array', items: { type: 'string' } } }, required: ['queries'] } }
59
+ )
60
+ const planned = plan && Array.isArray(plan.queries) ? plan.queries.filter((q) => typeof q === 'string' && q.trim().length > 0) : []
61
+ const queries = (planned.length > 0 ? planned : [question]).slice(0, angles)
62
+
63
+ phase('Gather')
64
+ const gathered = await parallel(queries.map((q, i) => () =>
65
+ agent(
66
+ 'Research this query using your web search and fetch tools.\\nQuery: ' + q +
67
+ '\\n\\nSteps: (1) run a web search for the query; (2) fetch the most relevant result URLs; ' +
68
+ '(3) extract concrete, verifiable factual claims, each tagged with the exact source URL it came from. ' +
69
+ 'Do NOT invent sources or claims — report only what the fetched pages actually say.',
70
+ { label: 'research ' + (i + 1), schema: { type: 'object', properties: { sources: { type: 'array', items: { type: 'object', properties: { url: { type: 'string' }, claims: { type: 'array', items: { type: 'string' } } }, required: ['url', 'claims'] } } }, required: ['sources'] } }
71
+ )
72
+ ))
73
+ const allSources = gathered.filter(Boolean).flatMap((g) => (g && g.sources) || [])
74
+
75
+ phase('Verify')
76
+ const verdict = await agent(
77
+ 'Cross-check these research sources. Group claims that assert the same fact across different source URLs. ' +
78
+ 'Keep a claim only if it is supported by at least ' + minSupport + ' distinct source URLs OR by one clearly authoritative source. ' +
79
+ 'Discard claims found in a single weak source or that conflict with others.\\n\\nSOURCES JSON:\\n' + JSON.stringify(allSources),
80
+ { label: 'cross-check', schema: { type: 'object', properties: { supported: { type: 'array', items: { type: 'object', properties: { claim: { type: 'string' }, sources: { type: 'array', items: { type: 'string' } } }, required: ['claim', 'sources'] } }, discarded: { type: 'array', items: { type: 'string' } } }, required: ['supported'] } }
81
+ )
82
+
83
+ phase('Report')
84
+ const report = await agent(
85
+ 'Write a concise, well-structured research report that answers the question using ONLY the supported claims below. ' +
86
+ 'Cite source URLs inline next to each claim. If the evidence is thin, say so explicitly.\\n\\n' +
87
+ 'QUESTION: ' + question + '\\n\\nSUPPORTED CLAIMS JSON:\\n' + JSON.stringify((verdict && verdict.supported) || []),
88
+ { label: 'write report' }
89
+ )
90
+
91
+ return { question, queries, supported: (verdict && verdict.supported) || [], report }`;
92
+
93
+ const ADVERSARIAL_REVIEW_SCRIPT = `export const meta = {
94
+ name: 'adversarial_review',
95
+ description: 'Adversarial review: findings cross-checked by independent skeptics',
96
+ phases: [
97
+ { title: 'Investigate' },
98
+ { title: 'Refute' },
99
+ { title: 'Consensus' },
100
+ ],
101
+ }
102
+
103
+ const task = (args && args.task) || ''
104
+ const reviewers = (args && args.reviewers) || 2
105
+ const threshold = (args && args.threshold) || 0.5
106
+
107
+ phase('Investigate')
108
+ const investigation = await agent(
109
+ 'Investigate the following and list concrete, individually-checkable findings:\\n' + task,
110
+ { label: 'investigate', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }
111
+ )
112
+ const findings = (investigation && Array.isArray(investigation.findings) ? investigation.findings : [])
113
+
114
+ phase('Refute')
115
+ const judged = await parallel(findings.map((f, i) => () =>
116
+ parallel(Array.from({ length: reviewers }, (_, r) => () =>
117
+ agent(
118
+ 'You are a skeptical reviewer. Try to REFUTE this finding for the task below. ' +
119
+ 'Default to real=false when uncertain. Investigate with the available tools if needed.\\n\\n' +
120
+ 'TASK: ' + task + '\\nFINDING: ' + f,
121
+ { label: 'refute ' + (i + 1) + '.' + (r + 1), schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real'] } }
122
+ )
123
+ )).then((votes) => {
124
+ const valid = votes.filter(Boolean)
125
+ const realCount = valid.filter((v) => v && v.real).length
126
+ const ratio = valid.length ? realCount / valid.length : 0
127
+ return { finding: f, realVotes: realCount, totalVotes: valid.length, survives: ratio >= threshold }
128
+ })
129
+ ))
130
+
131
+ phase('Consensus')
132
+ const survivors = judged.filter(Boolean).filter((j) => j.survives)
133
+ const report = await agent(
134
+ 'Synthesize the surviving findings into a concise review. For each finding, note the skeptic agreement ' +
135
+ '(realVotes/totalVotes). Do not include findings that failed the refutation pass.\\n\\n' +
136
+ 'TASK: ' + task + '\\n\\nSURVIVING FINDINGS JSON:\\n' + JSON.stringify(survivors),
137
+ { label: 'consensus report' }
138
+ )
139
+
140
+ return { findings, judged: judged.filter(Boolean), survivors, report }`;
141
+
142
+ const CODE_REVIEW_SCRIPT = `export const meta = {
143
+ name: 'code_review',
144
+ description: 'Multi-angle parallel code review with a verify pass',
145
+ phases: [
146
+ { title: 'Scan' },
147
+ { title: 'Verify' },
148
+ { title: 'Report' },
149
+ ],
150
+ }
151
+
152
+ const diff = (args && args.diff) || ''
153
+ const diffSource = (args && args.diffSource) || 'provided diff'
154
+
155
+ phase('Scan')
156
+ const finders = await parallel([
157
+ () => agent('You are a correctness reviewer. Find bugs, race conditions, error-handling gaps, and edge-case failures in this diff.\\n\\n' + diff, { label: 'correctness', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
158
+ () => agent('You are a reuse reviewer. Find duplicated logic, reinvented wheels, and missed opportunities to reuse existing code or libraries.\\n\\n' + diff, { label: 'reuse', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
159
+ () => agent('You are a simplification reviewer. Find over-engineering, unnecessary complexity, and simpler alternatives.\\n\\n' + diff, { label: 'simplification', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
160
+ () => agent('You are an efficiency reviewer. Find performance problems, avoidable allocations, and scaling risks.\\n\\n' + diff, { label: 'efficiency', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
161
+ () => agent('You are an architecture reviewer. Assess the change at the right altitude: design fit, layering, boundaries, and future maintenance.\\n\\n' + diff, { label: 'altitude', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
162
+ () => agent('You are a security reviewer. Find trust-boundary, injection, secret-leak, permission, and unsafe-input risks in this diff.\\n\\n' + diff, { label: 'security', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
163
+ () => agent('You are a testability reviewer. Find missing regression tests, observability gaps, and rollout/recovery risks in this diff.\\n\\n' + diff, { label: 'testability', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
164
+ ])
165
+ const candidates = finders.filter(Boolean).flatMap((f) => (f && f.findings) || [])
166
+
167
+ phase('Verify')
168
+ const verified = await parallel(candidates.map((finding, i) => () =>
169
+ agent('You are a skeptical verifier. For this candidate finding, check whether it is a REAL issue in the diff. ' +
170
+ 'Default to real=false when uncertain.\\n\\nCANDIDATE: ' + finding + '\\n\\nDIFF:\\n' + diff,
171
+ { label: 'verify ' + (i + 1), schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real'] } })
172
+ ))
173
+
174
+ phase('Report')
175
+ const ranked = candidates
176
+ .map((finding, i) => ({ finding, verified: verified[i] }))
177
+ .filter((item) => item.verified && item.verified.real)
178
+ const report = await agent(
179
+ 'Write a ranked code review report for this diff (' + diffSource + '). ' +
180
+ 'Include only verified findings, ordered by severity, each with a concrete location and fix suggestion.\\n\\n' +
181
+ 'VERIFIED FINDINGS JSON:\\n' + JSON.stringify(ranked),
182
+ { label: 'final report' }
183
+ )
184
+
185
+ return { candidates, ranked, report }`;
186
+
187
+ const MULTI_PERSPECTIVE_SCRIPT = `export const meta = {
188
+ name: 'multi_perspective',
189
+ description: 'Evaluate a topic from multiple independent perspectives',
190
+ phases: [
191
+ { title: 'Perspectives' },
192
+ { title: 'Synthesis' },
193
+ ],
194
+ }
195
+
196
+ const topic = (args && args.topic) || ''
197
+ const perspectives = (args && Array.isArray(args.perspectives) && args.perspectives.length > 0 ? args.perspectives : ['technical', 'product', 'security', 'user experience', 'maintainability'])
198
+
199
+ phase('Perspectives')
200
+ const views = await parallel(perspectives.map((p, i) => () =>
201
+ agent('Evaluate this topic from the perspective of: ' + p + '. Be concrete and specific; note risks, trade-offs, and what matters most from this angle.\\n\\nTOPIC: ' + topic,
202
+ { label: 'perspective ' + (i + 1), schema: { type: 'object', properties: { angle: { type: 'string' }, points: { type: 'array', items: { type: 'string' } }, risks: { type: 'array', items: { type: 'string' } } }, required: ['angle', 'points', 'risks'] } })
203
+ ))
204
+
205
+ phase('Synthesis')
206
+ const synthesis = await agent(
207
+ 'Synthesize these perspectives into a balanced assessment. Identify where perspectives agree, where they conflict, and give a bottom-line recommendation with the key trade-offs.\\n\\nTOPIC: ' + topic + '\\n\\nPERSPECTIVES JSON:\\n' + JSON.stringify(views.filter(Boolean)),
208
+ { label: 'synthesis' }
209
+ )
210
+
211
+ return { views: views.filter(Boolean), synthesis }`;
212
+
213
+ const CODEBASE_AUDIT_SCRIPT = `export const meta = {
214
+ name: 'codebase_audit',
215
+ description: 'Audit a codebase for health, risk, and debt across dimensions',
216
+ phases: [
217
+ { title: 'Map' },
218
+ { title: 'Audit' },
219
+ { title: 'Report' },
220
+ ],
221
+ }
222
+
223
+ const root = (args && (args.scope || args.root)) || cwd
224
+ const checks = (args && Array.isArray(args.checks) ? args.checks : [])
225
+ const focus = (args && args.focus) || (checks.length > 0 ? checks.join(', ') : '')
226
+
227
+ phase('Map')
228
+ const map = await agent(
229
+ 'Explore the codebase at ' + root + ' and produce a structural map: top-level modules, entry points, ' +
230
+ 'data flow, and where complexity concentrates. Use the available file tools.\\n' +
231
+ (focus ? 'Focus/checks requested: ' + focus + '\\n' : ''),
232
+ { label: 'map', schema: { type: 'object', properties: { modules: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, purpose: { type: 'string' }, risk: { type: 'string' } }, required: ['name', 'purpose'] } }, entryPoints: { type: 'array', items: { type: 'string' } }, complexityHotspots: { type: 'array', items: { type: 'string' } } }, required: ['modules'] } }
233
+ )
234
+
235
+ phase('Audit')
236
+ const audits = await parallel([
237
+ () => agent('Audit this codebase for correctness risks: error handling, boundary conditions, concurrency, security. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'correctness', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
238
+ () => agent('Audit this codebase for maintainability: coupling, duplication, naming, test coverage, documentation. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'maintainability', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
239
+ () => agent('Audit this codebase for operational health: dependencies, build, deployment, observability, secrets handling. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'operations', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
240
+ ])
241
+
242
+ phase('Report')
243
+ const report = await agent(
244
+ 'Write a codebase health audit report: overall health score, ranked findings across dimensions with concrete locations, ' +
245
+ 'and a prioritized remediation roadmap.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map) + '\\n\\nAUDITS JSON:\\n' + JSON.stringify(audits.filter(Boolean)),
246
+ { label: 'audit report' }
247
+ )
248
+
249
+ return { map, audits: audits.filter(Boolean), report }`;
250
+
251
+ export const BUILTIN_WORKFLOWS: Readonly<Record<string, BuiltinWorkflowDescriptor>> = {
252
+ "deep-research": {
253
+ name: "deep-research",
254
+ description: "Deep research: parallel queries, cross-checked claims, cited report",
255
+ script: DEEP_RESEARCH_SCRIPT,
256
+ toolset: "web-research",
257
+ primaryArg: "question",
258
+ },
259
+ "adversarial-review": {
260
+ name: "adversarial-review",
261
+ description: "Adversarial review: findings cross-checked by independent skeptics",
262
+ script: ADVERSARIAL_REVIEW_SCRIPT,
263
+ primaryArg: "task",
264
+ },
265
+ "code-review": {
266
+ name: "code-review",
267
+ description: "Multi-angle parallel code review with a verify pass",
268
+ script: CODE_REVIEW_SCRIPT,
269
+ primaryArg: "diff",
270
+ },
271
+ "multi-perspective": {
272
+ name: "multi-perspective",
273
+ description: "Evaluate a topic from multiple independent perspectives",
274
+ script: MULTI_PERSPECTIVE_SCRIPT,
275
+ primaryArg: "topic",
276
+ },
277
+ "codebase-audit": {
278
+ name: "codebase-audit",
279
+ description: "Audit a codebase for health, risk, and debt across dimensions",
280
+ script: CODEBASE_AUDIT_SCRIPT,
281
+ // `root` already defaults to cwd, so free text is the more useful refinement:
282
+ // "/codebase-audit packages/pi-goal" reads as a focus the map agent honours.
283
+ primaryArg: "scope",
284
+ },
285
+ };
286
+
287
+ export function validateBuiltinArgs(name: string, args: unknown): void {
288
+ const value = args && typeof args === "object" && !Array.isArray(args) ? (args as Record<string, unknown>) : {};
289
+ const required =
290
+ name === "deep-research"
291
+ ? "question"
292
+ : name === "adversarial-review"
293
+ ? "task"
294
+ : name === "code-review"
295
+ ? "diff"
296
+ : name === "multi-perspective"
297
+ ? "topic"
298
+ : undefined;
299
+ if (required !== undefined && (typeof value[required] !== "string" || !value[required].trim())) {
300
+ throw new Error(`Built-in workflow "${name}" requires args.${required} to be a non-empty string.`);
301
+ }
302
+ if (
303
+ name === "codebase-audit" &&
304
+ value.scope !== undefined &&
305
+ (typeof value.scope !== "string" || !value.scope.trim())
306
+ ) {
307
+ throw new Error('Built-in workflow "codebase-audit" requires args.scope to be a non-empty string when provided.');
308
+ }
309
+ if (
310
+ name === "codebase-audit" &&
311
+ value.checks !== undefined &&
312
+ (!Array.isArray(value.checks) || !value.checks.every((check) => typeof check === "string" && check.trim()))
313
+ ) {
314
+ throw new Error('Built-in workflow "codebase-audit" requires args.checks to contain non-empty strings.');
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Every `args.<key>` a builtin script reads, keyed by workflow name.
320
+ *
321
+ * Kept next to the scripts so a test can assert that each `primaryArg` is a key
322
+ * its script actually consumes — the failure mode this guards against is silent
323
+ * (an unread key means an empty input, not an error).
324
+ */
325
+ export const BUILTIN_SCRIPT_ARG_KEYS: Readonly<Record<string, readonly string[]>> = {
326
+ "deep-research": ["angles", "minSupport", "question"],
327
+ "adversarial-review": ["reviewers", "task", "threshold"],
328
+ "code-review": ["diff", "diffSource"],
329
+ "multi-perspective": ["perspectives", "topic"],
330
+ "codebase-audit": ["checks", "focus", "root", "scope"],
331
+ };