@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
@@ -0,0 +1,318 @@
1
+ /**
2
+ * capabilities.ts — machine-readable workflow capability contract (A11).
3
+ *
4
+ * One authoritative declaration of the runtime surface: every global a script
5
+ * may use, every option a helper accepts, and every workflow-tool input. The
6
+ * runtime builds its bindings from this declaration, and `check:capabilities`
7
+ * asserts that the shipped docs (README) stay aligned — the tool schema,
8
+ * documentation, and the real runtime face cannot drift apart.
9
+ */
10
+
11
+ import type { WorkflowRuntimeBindings } from "./runtime.js";
12
+
13
+ /** Classification of a capability entry. */
14
+ export type CapabilityClass = "runtime-global" | "workflow-tool-input" | "script-contract";
15
+
16
+ export interface CapabilityOption {
17
+ name: string;
18
+ kind: string;
19
+ optional: boolean;
20
+ default: string;
21
+ }
22
+
23
+ export interface CapabilityEntry {
24
+ name: string;
25
+ classification: CapabilityClass;
26
+ signature: string;
27
+ options: CapabilityOption[];
28
+ constraints: string[];
29
+ }
30
+
31
+ /** The contract format version. Bump on any breaking surface change. */
32
+ export const WORKFLOW_CAPABILITY_CONTRACT_VERSION = "1.0.0";
33
+
34
+ /**
35
+ * The runtime globals a script can rely on. `options` documents each helper's
36
+ * accepted inputs with defaults; `constraints` states the behavioral contract.
37
+ */
38
+ export const WORKFLOW_CAPABILITIES: readonly CapabilityEntry[] = [
39
+ {
40
+ name: "agent",
41
+ classification: "runtime-global",
42
+ signature: "agent(prompt, options?) => Promise<string | structured value | null>",
43
+ options: [
44
+ { name: "label", kind: "string", optional: true, default: "derived from phase and call count" },
45
+ { name: "phase", kind: "string", optional: true, default: "current phase" },
46
+ { name: "schema", kind: "plain JSON Schema", optional: true, default: "none" },
47
+ { name: "model", kind: "string", optional: true, default: "pi-subagents model resolver" },
48
+ { name: "tier", kind: "small|medium|large", optional: true, default: "pi-subagents workflow tier" },
49
+ { name: "isolation", kind: '"worktree"', optional: true, default: "pi-subagents worktree policy" },
50
+ { name: "thread", kind: "string", optional: true, default: "fresh session per call" },
51
+ { name: "toolset", kind: "string", optional: true, default: "configured host/agent toolset hint" },
52
+ { name: "excludeTools", kind: "string[]", optional: true, default: "none" },
53
+ { name: "agentType", kind: "string", optional: true, default: "general-purpose" },
54
+ { name: "timeoutMs", kind: "number | null", optional: true, default: "300000 (5min); null disables" },
55
+ { name: "retries", kind: "number", optional: true, default: "run retry count" },
56
+ ],
57
+ constraints: [
58
+ "recoverable failures return null after retries; nonrecoverable failures throw",
59
+ "schema noncompliance after bounded repair attempts is nonrecoverable",
60
+ "resume replays only the longest unchanged prefix; the first miss and every later call execute live",
61
+ "spawnKeys rotate by generation on resume so pi-subagents never raises a fingerprint conflict",
62
+ "explicit model is resolved and scope-checked by pi-subagents; unavailable selectors fail closed",
63
+ "same-thread calls must be sequential and preserve a stable workflow thread name",
64
+ "threads cannot be combined with worktree isolation",
65
+ ],
66
+ },
67
+ {
68
+ name: "parallel",
69
+ classification: "runtime-global",
70
+ signature: "parallel(thunks) => Promise<Array<unknown | null>>",
71
+ options: [],
72
+ constraints: [
73
+ "requires functions rather than promises",
74
+ "result order matches input order",
75
+ "recoverable thunk failures become null; nonrecoverable failures throw",
76
+ "a breached agent cap cancels only its own batch",
77
+ ],
78
+ },
79
+ {
80
+ name: "pipeline",
81
+ classification: "runtime-global",
82
+ signature: "pipeline(items, ...stages) => Promise<Array<unknown | null>>",
83
+ options: [],
84
+ constraints: [
85
+ "items run concurrently while stages per item run sequentially",
86
+ "each stage receives previousValue, originalItem, and zero-based index",
87
+ "a null stage result is passed to the next stage",
88
+ ],
89
+ },
90
+ {
91
+ name: "workflow",
92
+ classification: "runtime-global",
93
+ signature: "workflow(savedName, childArgs?) => Promise<unknown>",
94
+ options: [],
95
+ constraints: [
96
+ "one nested level",
97
+ "shares limiter, counters, token accounting, and store",
98
+ "nested runs get unique runIds via nestedCallSeq",
99
+ ],
100
+ },
101
+ {
102
+ name: "verify",
103
+ classification: "runtime-global",
104
+ signature: "verify(item, options?) => Promise<{ real, realCount, total, votes }>",
105
+ options: [
106
+ { name: "reviewers", kind: "number", optional: true, default: "2" },
107
+ { name: "threshold", kind: "number", optional: true, default: "0.5" },
108
+ { name: "lens", kind: "string | string[]", optional: true, default: "none" },
109
+ ],
110
+ constraints: ["threshold comparison is inclusive; real is false when no reviewer succeeds"],
111
+ },
112
+ {
113
+ name: "judgePanel",
114
+ classification: "runtime-global",
115
+ signature: "judgePanel(attempts, options?) => Promise<{ index, attempt, score, judgments } | undefined>",
116
+ options: [
117
+ { name: "judges", kind: "number", optional: true, default: "3" },
118
+ { name: "rubric", kind: "string", optional: true, default: "overall quality and correctness" },
119
+ ],
120
+ constraints: ["highest mean score wins with stable input index as the tie-break; empty input returns undefined"],
121
+ },
122
+ {
123
+ name: "loopUntilDry",
124
+ classification: "runtime-global",
125
+ signature: "loopUntilDry({ round, key?, consecutiveEmpty?, maxRounds? }) => Promise<unknown[]>",
126
+ options: [
127
+ { name: "round", kind: "(roundIndex) => unknown[]", optional: false, default: "required" },
128
+ { name: "key", kind: "(item) => string", optional: true, default: "JSON.stringify" },
129
+ { name: "consecutiveEmpty", kind: "number", optional: true, default: "2" },
130
+ { name: "maxRounds", kind: "number", optional: true, default: "50" },
131
+ ],
132
+ constraints: ["budget/agent-limit exhaustion returns the partial array instead of throwing"],
133
+ },
134
+ {
135
+ name: "completenessCheck",
136
+ classification: "runtime-global",
137
+ signature: "completenessCheck(taskArgs, results) => Promise<{ complete, missing? } | null>",
138
+ options: [],
139
+ constraints: ["only the first 4,000 characters of serialized result evidence are sent to the critic"],
140
+ },
141
+ {
142
+ name: "retry",
143
+ classification: "runtime-global",
144
+ signature: "retry(thunk, options?) => Promise<unknown>",
145
+ options: [
146
+ { name: "attempts", kind: "number", optional: true, default: "3" },
147
+ { name: "until", kind: "(result) => boolean", optional: true, default: "accept first result" },
148
+ ],
149
+ constraints: ["until must be synchronous"],
150
+ },
151
+ {
152
+ name: "gate",
153
+ classification: "runtime-global",
154
+ signature: "gate(thunk, validator, options?) => Promise<{ ok, value, attempts }>",
155
+ options: [{ name: "attempts", kind: "number", optional: true, default: "3" }],
156
+ constraints: ["a value is accepted only when the validator returns an object with a truthy ok"],
157
+ },
158
+ {
159
+ name: "checkpoint",
160
+ classification: "runtime-global",
161
+ signature: "checkpoint(prompt, options?) => Promise<unknown>",
162
+ options: [
163
+ { name: "default", kind: "unknown", optional: true, default: "true when no UI and omitted" },
164
+ { name: "headless", kind: '"default" | "abort"', optional: true, default: '"default"' },
165
+ { name: "kind", kind: '"confirm" | "input" | "select"', optional: true, default: '"confirm"' },
166
+ { name: "choices", kind: "string[]", optional: true, default: "none" },
167
+ { name: "timeoutMs", kind: "number", optional: true, default: "none" },
168
+ ],
169
+ constraints: [
170
+ "consumes one agent slot and no tokens",
171
+ "journaled answers replay only within an unchanged resume prefix",
172
+ ],
173
+ },
174
+ { name: "log", classification: "runtime-global", signature: "log(message) => void", options: [], constraints: [] },
175
+ {
176
+ name: "console",
177
+ classification: "runtime-global",
178
+ signature: "console: { log, info, warn, error }",
179
+ options: [],
180
+ constraints: ["compatibility only — new workflows should use log()"],
181
+ },
182
+ {
183
+ name: "phase",
184
+ classification: "runtime-global",
185
+ signature: "phase(title, options?) => void",
186
+ options: [{ name: "budget", kind: "number", optional: true, default: "positive soft pre-call token gate" }],
187
+ constraints: ["phase budgets are soft pre-call gates"],
188
+ },
189
+ { name: "args", classification: "runtime-global", signature: "args: unknown", options: [], constraints: [] },
190
+ { name: "cwd", classification: "runtime-global", signature: "cwd: string", options: [], constraints: [] },
191
+ {
192
+ name: "process",
193
+ classification: "runtime-global",
194
+ signature: "process: { cwd(): string }",
195
+ options: [],
196
+ constraints: ["restricted: only cwd() is exposed"],
197
+ },
198
+ {
199
+ name: "budget",
200
+ classification: "runtime-global",
201
+ signature: "budget: { total, spent(), remaining() }",
202
+ options: [],
203
+ constraints: ["frozen view over shared soft token accounting; nested workflows share the same accounting"],
204
+ },
205
+ {
206
+ name: "script",
207
+ classification: "workflow-tool-input",
208
+ signature: "script?: string",
209
+ options: [],
210
+ constraints: ["required raw JavaScript workflow source unless `name` is given"],
211
+ },
212
+ {
213
+ name: "name",
214
+ classification: "workflow-tool-input",
215
+ signature: "name?: string",
216
+ options: [],
217
+ constraints: [
218
+ "resolves a saved workflow first, then one of the 5 built-in patterns",
219
+ "mutually exclusive with script and resumeFromRunId",
220
+ ],
221
+ },
222
+ { name: "args", classification: "workflow-tool-input", signature: "args?: unknown", options: [], constraints: [] },
223
+ {
224
+ name: "background",
225
+ classification: "workflow-tool-input",
226
+ signature: "background?: boolean = true",
227
+ options: [],
228
+ constraints: [
229
+ "background workflows are headless; use background false when checkpoint must show foreground confirmation",
230
+ ],
231
+ },
232
+ {
233
+ name: "maxAgents",
234
+ classification: "workflow-tool-input",
235
+ signature: "maxAgents?: number = 1000",
236
+ options: [],
237
+ constraints: [],
238
+ },
239
+ {
240
+ name: "concurrency",
241
+ classification: "workflow-tool-input",
242
+ signature: "concurrency?: number",
243
+ options: [],
244
+ constraints: ["runtime clamps to 1..16"],
245
+ },
246
+ {
247
+ name: "agentRetries",
248
+ classification: "workflow-tool-input",
249
+ signature: "agentRetries?: number = 0",
250
+ options: [],
251
+ constraints: ["floored and clamped to 0..3"],
252
+ },
253
+ {
254
+ name: "agentTimeoutMs",
255
+ classification: "workflow-tool-input",
256
+ signature: "agentTimeoutMs?: number = 300000",
257
+ options: [],
258
+ constraints: ["null disables the hard timeout; default 300000ms (5min)"],
259
+ },
260
+ {
261
+ name: "tokenBudget",
262
+ classification: "workflow-tool-input",
263
+ signature: "tokenBudget?: number",
264
+ options: [],
265
+ constraints: ["soft pre-call gate; in-flight work can overshoot"],
266
+ },
267
+ {
268
+ name: "resumeFromRunId",
269
+ classification: "workflow-tool-input",
270
+ signature: "resumeFromRunId?: string",
271
+ options: [],
272
+ constraints: [
273
+ "resumes a prior incomplete run with an edited script",
274
+ "unchanged positional calls replay from cache until the first changed call",
275
+ "always runs in the background",
276
+ ],
277
+ },
278
+ {
279
+ name: "export const meta",
280
+ classification: "script-contract",
281
+ signature: "export const meta = { name, description, phases? }",
282
+ options: [],
283
+ constraints: [
284
+ "must be the first statement",
285
+ "name and description must be nonblank literals",
286
+ "spread, computed keys, methods, template interpolation, and __proto__/constructor/prototype are rejected",
287
+ ],
288
+ },
289
+ {
290
+ name: "determinism",
291
+ classification: "script-contract",
292
+ signature: "Date.now(), Math.random(), no-argument new Date() unavailable",
293
+ options: [],
294
+ constraints: ["pass timestamps and randomness through args"],
295
+ },
296
+ ];
297
+
298
+ /**
299
+ * Every runtime global the script realm actually exposes. `check:capabilities`
300
+ * asserts this set matches the declaration above, so a binding added to the
301
+ * runtime but not documented fails the gate.
302
+ */
303
+ export function declaredRuntimeGlobals(): string[] {
304
+ return WORKFLOW_CAPABILITIES.filter((entry) => entry.classification === "runtime-global").map((entry) => entry.name);
305
+ }
306
+
307
+ /** Validate that the runtime's binding surface matches the contract. */
308
+ export function checkRuntimeBindings(bindings: WorkflowRuntimeBindings): string[] {
309
+ const problems: string[] = [];
310
+ const declared = new Set(declaredRuntimeGlobals());
311
+ for (const name of declared) {
312
+ if (!(name in bindings)) problems.push(`runtime is missing declared global "${name}"`);
313
+ }
314
+ for (const name of Object.keys(bindings) as Array<keyof WorkflowRuntimeBindings>) {
315
+ if (!declared.has(name)) problems.push(`runtime exposes undocumented global "${String(name)}"`);
316
+ }
317
+ return problems;
318
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Diff discovery for the `/code-review` command.
3
+ *
4
+ * The `code-review` script reviews a diff it is handed; a user types a *scope*
5
+ * (nothing, a PR number, a revision range, or a path). This module turns the
6
+ * one into the other, and degrades to a plain `git diff HEAD` whenever the
7
+ * narrower attempt fails or comes back empty — a review of nothing looks
8
+ * identical to a review that found nothing, so silence is not an option.
9
+ *
10
+ * Commands are run as argv arrays, never as shell strings: a branch or path
11
+ * argument is attacker-influenced often enough that interpolation is a real
12
+ * injection surface.
13
+ */
14
+
15
+ /** Result of one command: `ok` is false for a non-zero exit or a spawn failure. */
16
+ export interface CommandResult {
17
+ ok: boolean;
18
+ stdout: string;
19
+ }
20
+
21
+ /** Runs one command with an argv array. Injected so this module is testable without git. */
22
+ export type CommandRunner = (file: string, args: string[]) => CommandResult;
23
+
24
+ export interface CodeReviewScope {
25
+ diff: string;
26
+ /** Human-readable provenance, shown in the report and passed to the script. */
27
+ diffSource: string;
28
+ /** Degradation messages to surface with `ctx.ui.notify`; empty on the happy path. */
29
+ notices: string[];
30
+ }
31
+
32
+ /** Diffs above this are truncated — the review agents have a finite context. */
33
+ export const MAX_DIFF_CHARS = 120_000;
34
+
35
+ /**
36
+ * Pathspecs excluded from the no-argument auto scope.
37
+ *
38
+ * Deliberately limited to high-confidence generated or vendored output. A false
39
+ * exclusion silently hides real changes from review, which is worse than the
40
+ * noise it saves, so anything ambiguous is left in.
41
+ */
42
+ export const AUTO_SCOPE_EXCLUSIONS: readonly string[] = [
43
+ ":(exclude)**/node_modules/**",
44
+ ":(exclude)**/dist/**",
45
+ ":(exclude)**/build/**",
46
+ ":(exclude)**/coverage/**",
47
+ ":(exclude)**/.next/**",
48
+ ":(exclude)**/__snapshots__/**",
49
+ ":(exclude)**/*.min.js",
50
+ ":(exclude)**/*.map",
51
+ ":(exclude)**/package-lock.json",
52
+ ":(exclude)**/bun.lock",
53
+ ":(exclude)**/bun.lockb",
54
+ ":(exclude)**/pnpm-lock.yaml",
55
+ ":(exclude)**/yarn.lock",
56
+ ":(exclude)**/Cargo.lock",
57
+ ":(exclude)**/go.sum",
58
+ ":(exclude)**/uv.lock",
59
+ ":(exclude)**/poetry.lock",
60
+ ];
61
+
62
+ function truncate(diff: string): { diff: string; truncated: boolean } {
63
+ if (diff.length <= MAX_DIFF_CHARS) return { diff, truncated: false };
64
+ return {
65
+ diff: `${diff.slice(0, MAX_DIFF_CHARS)}\n\n[diff truncated at ${MAX_DIFF_CHARS} characters]`,
66
+ truncated: true,
67
+ };
68
+ }
69
+
70
+ function finish(diff: string, diffSource: string, notices: string[]): CodeReviewScope {
71
+ const capped = truncate(diff);
72
+ if (capped.truncated) {
73
+ notices.push(`Diff truncated at ${MAX_DIFF_CHARS} characters — review covers the first part only.`);
74
+ }
75
+ return { diff: capped.diff, diffSource, notices };
76
+ }
77
+
78
+ /**
79
+ * Resolve the `/code-review` argument into a diff.
80
+ *
81
+ * - `""` → working tree against HEAD, generated artifacts excluded
82
+ * - `"123"` → `gh pr diff 123`
83
+ * - `"main..HEAD"` → that revision range
84
+ * - anything else → treated as a path
85
+ */
86
+ export function resolveCodeReviewScope(argument: string, run: CommandRunner): CodeReviewScope {
87
+ const text = argument.trim();
88
+ const notices: string[] = [];
89
+
90
+ if (/^\d+$/u.test(text)) {
91
+ const pr = run("gh", ["pr", "diff", text]);
92
+ if (!pr.ok) {
93
+ notices.push(`Could not read PR #${text} (is the gh CLI authenticated?). Nothing to review.`);
94
+ return finish("", `PR #${text}`, notices);
95
+ }
96
+ return finish(pr.stdout, `PR #${text}`, notices);
97
+ }
98
+
99
+ if (text.includes("..")) {
100
+ const range = run("git", ["diff", text]);
101
+ if (!range.ok) {
102
+ notices.push(`Could not diff range "${text}". Nothing to review.`);
103
+ return finish("", `range ${text}`, notices);
104
+ }
105
+ return finish(range.stdout, `range ${text}`, notices);
106
+ }
107
+
108
+ if (text) {
109
+ const scoped = run("git", ["diff", "HEAD", "--", text]);
110
+ if (!scoped.ok) {
111
+ notices.push(`Could not diff path "${text}". Nothing to review.`);
112
+ return finish("", `path ${text}`, notices);
113
+ }
114
+ return finish(scoped.stdout, `path ${text}`, notices);
115
+ }
116
+
117
+ // No argument: auto scope, with the two documented degradations.
118
+ const bare = () => run("git", ["diff", "HEAD"]);
119
+
120
+ const auto = run("git", ["diff", "HEAD", "--", ".", ...AUTO_SCOPE_EXCLUSIONS]);
121
+ if (!auto.ok) {
122
+ const fallback = bare();
123
+ notices.push("Auto-scoped diff failed; reviewing the full `git diff HEAD` instead.");
124
+ if (!fallback.ok) {
125
+ notices.push("`git diff HEAD` also failed — is this a git repository? Nothing to review.");
126
+ return finish("", "working tree", notices);
127
+ }
128
+ return finish(fallback.stdout, "working tree (unscoped)", notices);
129
+ }
130
+
131
+ if (auto.stdout.trim()) return finish(auto.stdout, "working tree vs HEAD", notices);
132
+
133
+ // The exclusions emptied the diff. Only fall back when there was in fact
134
+ // something to review — an genuinely clean tree must not be reported as a
135
+ // degradation.
136
+ const fallback = bare();
137
+ if (fallback.ok && fallback.stdout.trim()) {
138
+ notices.push("Every change was an excluded generated artifact; reviewing the full `git diff HEAD` instead.");
139
+ return finish(fallback.stdout, "working tree (unscoped)", notices);
140
+ }
141
+
142
+ notices.push("No uncommitted changes against HEAD — nothing to review.");
143
+ return finish("", "working tree vs HEAD", notices);
144
+ }