@davidorex/pi-workflows 0.2.0 → 0.3.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.
Files changed (215) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/README.md +12 -2
  3. package/agents/architecture-designer.agent.yaml +58 -0
  4. package/agents/architecture-inferrer.agent.yaml +60 -0
  5. package/agents/gap-identifier.agent.yaml +66 -0
  6. package/agents/handoff-writer.agent.yaml +64 -0
  7. package/agents/plan-creator.agent.yaml +63 -0
  8. package/agents/project-definer.agent.yaml +53 -0
  9. package/agents/project-inferrer.agent.yaml +63 -0
  10. package/agents/requirements-gatherer.agent.yaml +58 -0
  11. package/dist/agent-spec.d.ts +27 -0
  12. package/dist/agent-spec.d.ts.map +1 -0
  13. package/dist/agent-spec.js +112 -0
  14. package/dist/agent-spec.js.map +1 -0
  15. package/dist/checkpoint.d.ts +50 -0
  16. package/dist/checkpoint.d.ts.map +1 -0
  17. package/dist/checkpoint.js +109 -0
  18. package/dist/checkpoint.js.map +1 -0
  19. package/dist/completion.d.ts +18 -0
  20. package/dist/completion.d.ts.map +1 -0
  21. package/dist/completion.js +58 -0
  22. package/dist/completion.js.map +1 -0
  23. package/dist/dag.d.ts +56 -0
  24. package/dist/dag.d.ts.map +1 -0
  25. package/dist/dag.js +172 -0
  26. package/dist/dag.js.map +1 -0
  27. package/dist/dispatch.d.ts +35 -0
  28. package/dist/dispatch.d.ts.map +1 -0
  29. package/dist/dispatch.js +311 -0
  30. package/dist/dispatch.js.map +1 -0
  31. package/dist/expression.d.ts +64 -0
  32. package/dist/expression.d.ts.map +1 -0
  33. package/dist/expression.js +294 -0
  34. package/dist/expression.js.map +1 -0
  35. package/dist/format.d.ts +19 -0
  36. package/dist/format.d.ts.map +1 -0
  37. package/dist/format.js +42 -0
  38. package/dist/format.js.map +1 -0
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +675 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/output.d.ts +12 -0
  44. package/dist/output.d.ts.map +1 -0
  45. package/dist/output.js +59 -0
  46. package/dist/output.js.map +1 -0
  47. package/dist/state.d.ts +66 -0
  48. package/dist/state.d.ts.map +1 -0
  49. package/dist/state.js +227 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/step-agent.d.ts +35 -0
  52. package/dist/step-agent.d.ts.map +1 -0
  53. package/dist/step-agent.js +156 -0
  54. package/dist/step-agent.js.map +1 -0
  55. package/dist/step-command.d.ts +21 -0
  56. package/dist/step-command.d.ts.map +1 -0
  57. package/dist/step-command.js +150 -0
  58. package/dist/step-command.js.map +1 -0
  59. package/dist/step-foreach.d.ts +33 -0
  60. package/dist/step-foreach.d.ts.map +1 -0
  61. package/dist/step-foreach.js +112 -0
  62. package/dist/step-foreach.js.map +1 -0
  63. package/dist/step-gate.d.ts +18 -0
  64. package/dist/step-gate.d.ts.map +1 -0
  65. package/dist/step-gate.js +137 -0
  66. package/dist/step-gate.js.map +1 -0
  67. package/dist/step-loop.d.ts +32 -0
  68. package/dist/step-loop.d.ts.map +1 -0
  69. package/dist/step-loop.js +278 -0
  70. package/dist/step-loop.js.map +1 -0
  71. package/dist/step-monitor.d.ts +56 -0
  72. package/dist/step-monitor.d.ts.map +1 -0
  73. package/dist/step-monitor.js +317 -0
  74. package/dist/step-monitor.js.map +1 -0
  75. package/dist/step-parallel.d.ts +43 -0
  76. package/dist/step-parallel.d.ts.map +1 -0
  77. package/dist/step-parallel.js +122 -0
  78. package/dist/step-parallel.js.map +1 -0
  79. package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
  80. package/dist/step-pause.d.ts.map +1 -0
  81. package/dist/step-pause.js +22 -0
  82. package/dist/step-pause.js.map +1 -0
  83. package/dist/step-shared.d.ts +57 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +125 -0
  86. package/dist/step-shared.js.map +1 -0
  87. package/dist/step-transform.d.ts +11 -0
  88. package/dist/step-transform.d.ts.map +1 -0
  89. package/dist/step-transform.js +41 -0
  90. package/dist/step-transform.js.map +1 -0
  91. package/dist/template.d.ts +51 -0
  92. package/dist/template.d.ts.map +1 -0
  93. package/{src/template.ts → dist/template.js} +32 -38
  94. package/dist/template.js.map +1 -0
  95. package/dist/test-helpers.d.ts +17 -0
  96. package/dist/test-helpers.d.ts.map +1 -0
  97. package/dist/test-helpers.js +46 -0
  98. package/dist/test-helpers.js.map +1 -0
  99. package/dist/tui.d.ts +47 -0
  100. package/dist/tui.d.ts.map +1 -0
  101. package/dist/tui.js +143 -0
  102. package/dist/tui.js.map +1 -0
  103. package/dist/types.d.ts +164 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +3 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/workflow-discovery.d.ts +23 -0
  108. package/dist/workflow-discovery.d.ts.map +1 -0
  109. package/dist/workflow-discovery.js +103 -0
  110. package/dist/workflow-discovery.js.map +1 -0
  111. package/dist/workflow-executor.d.ts +50 -0
  112. package/dist/workflow-executor.d.ts.map +1 -0
  113. package/dist/workflow-executor.js +726 -0
  114. package/dist/workflow-executor.js.map +1 -0
  115. package/dist/workflow-sdk.d.ts +44 -0
  116. package/dist/workflow-sdk.d.ts.map +1 -0
  117. package/dist/workflow-sdk.js +438 -0
  118. package/dist/workflow-sdk.js.map +1 -0
  119. package/dist/workflow-spec.d.ts +33 -0
  120. package/dist/workflow-spec.d.ts.map +1 -0
  121. package/dist/workflow-spec.js +464 -0
  122. package/dist/workflow-spec.js.map +1 -0
  123. package/dist/workflows-dir.d.ts +3 -0
  124. package/dist/workflows-dir.d.ts.map +1 -0
  125. package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
  126. package/dist/workflows-dir.js.map +1 -0
  127. package/package.json +53 -38
  128. package/schemas/decomposition-specs.schema.json +48 -48
  129. package/schemas/execution-results.schema.json +48 -48
  130. package/schemas/investigation-findings.schema.json +43 -43
  131. package/schemas/pattern-analysis.schema.json +41 -41
  132. package/schemas/phase.schema.json +48 -0
  133. package/schemas/plan-breakdown.schema.json +20 -20
  134. package/schemas/quality-analysis.schema.json +33 -33
  135. package/schemas/research-findings.schema.json +42 -42
  136. package/schemas/structure-analysis.schema.json +40 -40
  137. package/schemas/synthesis.schema.json +31 -31
  138. package/schemas/verifier-output.schema.json +94 -94
  139. package/skill-narrative.md +84 -0
  140. package/skills/pi-workflows/SKILL.md +247 -0
  141. package/templates/architecture-designer/task.md +117 -0
  142. package/templates/architecture-inferrer/task.md +61 -0
  143. package/templates/gap-identifier/task.md +103 -0
  144. package/templates/handoff-writer/task.md +91 -0
  145. package/templates/plan-creator/task.md +143 -0
  146. package/templates/project-definer/task.md +67 -0
  147. package/templates/project-inferrer/task.md +56 -0
  148. package/templates/requirements-gatherer/task.md +90 -0
  149. package/workflows/analyze-existing-project.workflow.yaml +74 -0
  150. package/workflows/create-handoff.workflow.yaml +58 -0
  151. package/workflows/create-phase.workflow.yaml +1 -1
  152. package/workflows/do-gap.workflow.yaml +4 -4
  153. package/workflows/fix-audit.workflow.yaml +1 -1
  154. package/workflows/gap-to-phase.workflow.yaml +1 -1
  155. package/workflows/init-new-project.workflow.yaml +55 -0
  156. package/workflows/plan-from-requirements.workflow.yaml +51 -0
  157. package/workflows/self-implement.workflow.yaml +3 -3
  158. package/workflows/typed-analysis.workflow.yaml +4 -4
  159. package/src/agent-spec.test.ts +0 -289
  160. package/src/agent-spec.ts +0 -122
  161. package/src/block-validation.test.ts +0 -350
  162. package/src/checkpoint.test.ts +0 -237
  163. package/src/checkpoint.ts +0 -140
  164. package/src/completion.test.ts +0 -143
  165. package/src/completion.ts +0 -68
  166. package/src/dag.test.ts +0 -350
  167. package/src/dag.ts +0 -219
  168. package/src/dispatch.test.ts +0 -475
  169. package/src/dispatch.ts +0 -353
  170. package/src/expression.test.ts +0 -353
  171. package/src/expression.ts +0 -332
  172. package/src/format.test.ts +0 -80
  173. package/src/format.ts +0 -41
  174. package/src/graduated-failure.test.ts +0 -556
  175. package/src/index.test.ts +0 -114
  176. package/src/index.ts +0 -516
  177. package/src/loop.test.ts +0 -551
  178. package/src/output.test.ts +0 -213
  179. package/src/output.ts +0 -70
  180. package/src/parallel-integration.test.ts +0 -175
  181. package/src/resume.test.ts +0 -192
  182. package/src/state.test.ts +0 -192
  183. package/src/state.ts +0 -254
  184. package/src/step-agent.test.ts +0 -327
  185. package/src/step-agent.ts +0 -178
  186. package/src/step-command.test.ts +0 -330
  187. package/src/step-command.ts +0 -132
  188. package/src/step-foreach.test.ts +0 -647
  189. package/src/step-foreach.ts +0 -148
  190. package/src/step-gate.test.ts +0 -185
  191. package/src/step-gate.ts +0 -116
  192. package/src/step-loop.test.ts +0 -626
  193. package/src/step-loop.ts +0 -323
  194. package/src/step-parallel.test.ts +0 -475
  195. package/src/step-parallel.ts +0 -168
  196. package/src/step-pause.test.ts +0 -30
  197. package/src/step-shared.test.ts +0 -355
  198. package/src/step-shared.ts +0 -157
  199. package/src/step-transform.test.ts +0 -155
  200. package/src/step-transform.ts +0 -47
  201. package/src/template-integration.test.ts +0 -72
  202. package/src/template.test.ts +0 -162
  203. package/src/test-helpers.ts +0 -51
  204. package/src/tui.test.ts +0 -355
  205. package/src/tui.ts +0 -182
  206. package/src/types.ts +0 -195
  207. package/src/verifier-schema.test.ts +0 -226
  208. package/src/workflow-discovery.test.ts +0 -105
  209. package/src/workflow-discovery.ts +0 -114
  210. package/src/workflow-executor.test.ts +0 -1532
  211. package/src/workflow-executor.ts +0 -810
  212. package/src/workflow-sdk.test.ts +0 -238
  213. package/src/workflow-sdk.ts +0 -262
  214. package/src/workflow-spec.test.ts +0 -576
  215. package/src/workflow-spec.ts +0 -471
@@ -1,238 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import os from "node:os";
6
- import {
7
- filterNames, stepTypes, expressionRoots,
8
- availableAgents, availableTemplates, availableSchemas,
9
- extractExpressions, declaredSteps, declaredAgentRefs, declaredSchemaRefs,
10
- FILTER_NAMES, STEP_TYPES,
11
- } from "./workflow-sdk.ts";
12
- import type { WorkflowSpec, StepSpec } from "./types.ts";
13
-
14
- function makeTmpDir(prefix: string): string {
15
- return fs.mkdtempSync(path.join(os.tmpdir(), `sdk-${prefix}-`));
16
- }
17
-
18
- // ── Vocabulary ───────────────────────────────────────────────────────────────
19
-
20
- describe("vocabulary", () => {
21
- it("filterNames returns current filter set", () => {
22
- const names = filterNames();
23
- assert.ok(names.includes("json"));
24
- assert.ok(names.includes("duration"));
25
- assert.ok(names.includes("currency"));
26
- assert.ok(names.includes("length"));
27
- assert.ok(names.includes("keys"));
28
- assert.ok(names.includes("filter"));
29
- });
30
-
31
- it("filterNames length matches FILTER_NAMES export", () => {
32
- assert.strictEqual(filterNames().length, FILTER_NAMES.length);
33
- });
34
-
35
- it("stepTypes returns descriptors with required fields", () => {
36
- const types = stepTypes();
37
- assert.ok(types.length >= 7);
38
- for (const t of types) {
39
- assert.ok(typeof t.name === "string");
40
- assert.ok(typeof t.field === "string");
41
- assert.ok(typeof t.retryable === "boolean");
42
- assert.ok(typeof t.supportsInput === "boolean");
43
- assert.ok(typeof t.supportsOutput === "boolean");
44
- }
45
- });
46
-
47
- it("stepTypes field names match STEP_TYPES export", () => {
48
- const fromFn = stepTypes().map(t => t.field).sort();
49
- const fromExport = STEP_TYPES.map(t => t.field).sort();
50
- assert.deepStrictEqual(fromFn, fromExport);
51
- });
52
-
53
- it("expressionRoots includes input and steps", () => {
54
- const roots = expressionRoots();
55
- assert.ok(roots.includes("input"));
56
- assert.ok(roots.includes("steps"));
57
- });
58
- });
59
-
60
- // ── Discovery ────────────────────────────────────────────────────────────────
61
-
62
- describe("discovery", () => {
63
- it("availableAgents finds agents in .pi/agents/", (t) => {
64
- const tmpDir = makeTmpDir("agents");
65
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
66
-
67
- const agentDir = path.join(tmpDir, ".pi", "agents");
68
- fs.mkdirSync(agentDir, { recursive: true });
69
- fs.writeFileSync(path.join(agentDir, "test-agent.agent.yaml"), "name: test-agent\ntools: [read]\n");
70
-
71
- const agents = availableAgents(tmpDir, "/nonexistent"); // no builtins
72
- assert.ok(agents.some(a => a.name === "test-agent"));
73
- });
74
-
75
- it("availableAgents skips malformed YAML", (t) => {
76
- const tmpDir = makeTmpDir("agents-bad");
77
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
78
-
79
- const agentDir = path.join(tmpDir, ".pi", "agents");
80
- fs.mkdirSync(agentDir, { recursive: true });
81
- fs.writeFileSync(path.join(agentDir, "good.agent.yaml"), "name: good\ntools: [read]\n");
82
- fs.writeFileSync(path.join(agentDir, "bad.agent.yaml"), ":::invalid yaml{{{\n");
83
-
84
- const agents = availableAgents(tmpDir, "/nonexistent");
85
- assert.ok(agents.some(a => a.name === "good"));
86
- assert.ok(!agents.some(a => a.name === "bad"));
87
- });
88
-
89
- it("availableTemplates finds .md files", (t) => {
90
- const tmpDir = makeTmpDir("templates");
91
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
92
-
93
- const tplDir = path.join(tmpDir, ".pi", "templates", "my-agent");
94
- fs.mkdirSync(tplDir, { recursive: true });
95
- fs.writeFileSync(path.join(tplDir, "task.md"), "# Task\n");
96
-
97
- const templates = availableTemplates(tmpDir, "/nonexistent");
98
- assert.ok(templates.includes(path.join("my-agent", "task.md")));
99
- });
100
-
101
- it("availableSchemas finds builtin schemas", (t) => {
102
- const tmpDir = makeTmpDir("schemas");
103
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
104
-
105
- const schemasDir = path.join(tmpDir, "test-schemas");
106
- fs.mkdirSync(schemasDir, { recursive: true });
107
- fs.writeFileSync(path.join(schemasDir, "result.schema.json"), "{}");
108
-
109
- const schemas = availableSchemas(tmpDir, schemasDir);
110
- assert.ok(schemas.some(s => s.includes("result.schema.json")));
111
- });
112
- });
113
-
114
- // ── Introspection ────────────────────────────────────────────────────────────
115
-
116
- function makeSpec(steps: Record<string, StepSpec>, extras?: Partial<WorkflowSpec>): WorkflowSpec {
117
- return {
118
- name: "test",
119
- description: "test",
120
- steps,
121
- source: "project",
122
- filePath: "/tmp/test.workflow.yaml",
123
- ...extras,
124
- };
125
- }
126
-
127
- describe("introspection", () => {
128
- it("extractExpressions finds step references", () => {
129
- const spec = makeSpec({
130
- load: { command: "echo hello" },
131
- process: { agent: "coder", when: "${{ steps.load.output.ready }}" },
132
- });
133
- const exprs = extractExpressions(spec);
134
- assert.ok(exprs.some(e => e.stepRefs.includes("load")));
135
- });
136
-
137
- it("extractExpressions finds filter names", () => {
138
- const spec = makeSpec({
139
- s1: { agent: "coder", input: "Data: ${{ steps.load.output | json }}" },
140
- });
141
- const exprs = extractExpressions(spec);
142
- const jsonExpr = exprs.find(e => e.filterName === "json");
143
- assert.ok(jsonExpr);
144
- });
145
-
146
- it("extractExpressions scans when, forEach, command, gate.check, completion", () => {
147
- const spec = makeSpec({
148
- load: { command: "echo '${{ input.x }}'" },
149
- check: { gate: { check: "test '${{ steps.load.output.ok }}' = 'true'" } },
150
- iter: { agent: "a", forEach: "${{ steps.load.output.items }}", as: "item" },
151
- cond: { agent: "b", when: "${{ steps.check.output.passed }}" },
152
- }, {
153
- completion: {
154
- message: "Done: ${{ steps.iter.output | json }}",
155
- include: ["steps.load.output"],
156
- },
157
- });
158
- const exprs = extractExpressions(spec);
159
- const fields = exprs.map(e => e.field);
160
- assert.ok(fields.some(f => f.includes("command")));
161
- assert.ok(fields.some(f => f.includes("gate.check")));
162
- assert.ok(fields.some(f => f.includes("forEach")));
163
- assert.ok(fields.some(f => f.includes("when")));
164
- assert.ok(fields.some(f => f.includes("completion.message")));
165
- });
166
-
167
- it("extractExpressions handles nested loop steps", () => {
168
- const spec = makeSpec({
169
- outer: {
170
- loop: {
171
- maxAttempts: 3,
172
- steps: {
173
- inner: { agent: "coder", when: "${{ steps.outer.output.retry }}" },
174
- },
175
- },
176
- },
177
- });
178
- const exprs = extractExpressions(spec);
179
- assert.ok(exprs.some(e => e.field.includes("loop.steps.inner")));
180
- });
181
-
182
- it("declaredSteps returns step names", () => {
183
- const spec = makeSpec({ a: { command: "echo" }, b: { agent: "x" }, c: { pause: true } });
184
- assert.deepStrictEqual(declaredSteps(spec), ["a", "b", "c"]);
185
- });
186
-
187
- it("declaredAgentRefs extracts agent names", () => {
188
- const spec = makeSpec({
189
- s1: { agent: "investigator" },
190
- s2: { command: "echo" },
191
- s3: { agent: "decomposer" },
192
- });
193
- const refs = declaredAgentRefs(spec);
194
- assert.ok(refs.includes("investigator"));
195
- assert.ok(refs.includes("decomposer"));
196
- assert.strictEqual(refs.length, 2);
197
- });
198
-
199
- it("declaredAgentRefs finds agents in nested forEach/loop/parallel", () => {
200
- const spec = makeSpec({
201
- outer: {
202
- agent: "outer-agent",
203
- loop: {
204
- maxAttempts: 2,
205
- steps: {
206
- inner: { agent: "inner-agent" },
207
- },
208
- },
209
- },
210
- par: {
211
- parallel: {
212
- a: { agent: "par-agent" },
213
- b: { command: "echo" },
214
- },
215
- },
216
- });
217
- const refs = declaredAgentRefs(spec);
218
- assert.ok(refs.includes("outer-agent"));
219
- assert.ok(refs.includes("inner-agent"));
220
- assert.ok(refs.includes("par-agent"));
221
- });
222
-
223
- it("declaredSchemaRefs extracts output.schema and artifact.schema paths", () => {
224
- const spec = makeSpec({
225
- s1: { agent: "a", output: { schema: "schemas/findings.schema.json" } },
226
- s2: { command: "echo", output: { schema: "schemas/results.schema.json" } },
227
- }, {
228
- artifacts: {
229
- report: { path: "./report.json", from: "steps.s1.output", schema: "schemas/report.schema.json" },
230
- },
231
- });
232
- const refs = declaredSchemaRefs(spec);
233
- assert.ok(refs.includes("schemas/findings.schema.json"));
234
- assert.ok(refs.includes("schemas/results.schema.json"));
235
- assert.ok(refs.includes("schemas/report.schema.json"));
236
- assert.strictEqual(refs.length, 3);
237
- });
238
- });
@@ -1,262 +0,0 @@
1
- /**
2
- * Workflow SDK — single queryable surface for the workflow extension's
3
- * vocabulary, discovery, and spec introspection. Derives dynamically
4
- * from source registries and filesystem — add a filter, agent, template,
5
- * or schema, and it appears here automatically.
6
- */
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import os from "node:os";
10
- import { FILTER_NAMES, EXPRESSION_ROOTS } from "./expression.ts";
11
- import { STEP_TYPES } from "./workflow-spec.ts";
12
- import type { StepTypeDescriptor } from "./workflow-spec.ts";
13
- import { discoverWorkflows } from "./workflow-discovery.ts";
14
- import { parseAgentYaml } from "./agent-spec.ts";
15
- import type { AgentSpec, WorkflowSpec, StepSpec } from "./types.ts";
16
-
17
- // Re-export for single-import convenience
18
- export { FILTER_NAMES, STEP_TYPES, EXPRESSION_ROOTS };
19
- export type { StepTypeDescriptor };
20
-
21
- const EXPR_PATTERN = /\$\{\{\s*(.*?)\s*\}\}/g;
22
-
23
- // ── Vocabulary (derived from code registries) ────────────────────────────────
24
-
25
- export function filterNames(): string[] { return FILTER_NAMES; }
26
- export function stepTypes(): StepTypeDescriptor[] { return STEP_TYPES; }
27
- export function expressionRoots(): readonly string[] { return EXPRESSION_ROOTS; }
28
-
29
- // ── Discovery (derived from filesystem) ──────────────────────────────────────
30
-
31
- export function availableAgents(cwd: string, builtinDir?: string): AgentSpec[] {
32
- const defaultBuiltinDir = builtinDir ?? path.resolve(import.meta.dirname, "..", "agents");
33
- const searchDirs = [
34
- path.join(cwd, ".pi", "agents"),
35
- path.join(os.homedir(), ".pi", "agent", "agents"),
36
- defaultBuiltinDir,
37
- ];
38
- const seen = new Set<string>();
39
- const agents: AgentSpec[] = [];
40
-
41
- for (const dir of searchDirs) {
42
- if (!fs.existsSync(dir)) continue;
43
- for (const file of fs.readdirSync(dir)) {
44
- if (!file.endsWith(".agent.yaml")) continue;
45
- const name = file.replace(".agent.yaml", "");
46
- if (seen.has(name)) continue; // higher-priority dir already found this agent
47
- seen.add(name);
48
- try {
49
- agents.push(parseAgentYaml(path.join(dir, file)));
50
- } catch {
51
- // skip malformed specs
52
- }
53
- }
54
- }
55
- return agents.sort((a, b) => a.name.localeCompare(b.name));
56
- }
57
-
58
- export function availableTemplates(cwd: string, builtinDir?: string): string[] {
59
- const defaultBuiltinDir = builtinDir ?? path.resolve(import.meta.dirname, "..", "templates");
60
- const searchDirs = [
61
- path.join(cwd, ".pi", "templates"),
62
- path.join(os.homedir(), ".pi", "agent", "templates"),
63
- defaultBuiltinDir,
64
- ];
65
- const seen = new Set<string>();
66
-
67
- for (const dir of searchDirs) {
68
- if (!fs.existsSync(dir)) continue;
69
- walkFiles(dir, (filePath) => {
70
- if (filePath.endsWith(".md") || filePath.endsWith(".txt")) {
71
- const rel = path.relative(dir, filePath);
72
- if (!seen.has(rel)) seen.add(rel);
73
- }
74
- });
75
- }
76
- return Array.from(seen).sort();
77
- }
78
-
79
- export function availableSchemas(cwd: string, builtinDir?: string): string[] {
80
- const defaultBuiltinSchemas = builtinDir ?? path.resolve(import.meta.dirname, "..", "schemas");
81
- const dirs = [
82
- defaultBuiltinSchemas,
83
- ];
84
- const schemas: string[] = [];
85
-
86
- for (const dir of dirs) {
87
- if (!fs.existsSync(dir)) continue;
88
- for (const file of fs.readdirSync(dir)) {
89
- if (file.endsWith(".schema.json")) {
90
- schemas.push(path.join(dir, file));
91
- }
92
- }
93
- }
94
- return schemas.sort();
95
- }
96
-
97
- export function availableWorkflows(cwd: string): WorkflowSpec[] {
98
- return discoverWorkflows(cwd);
99
- }
100
-
101
- // ── Introspection (derived from parsed spec) ─────────────────────────────────
102
-
103
- export interface ExpressionRef {
104
- expression: string;
105
- field: string;
106
- stepRefs: string[];
107
- filterName?: string;
108
- }
109
-
110
- /**
111
- * Extract all ${{ }} expressions from a workflow spec with their source locations.
112
- * Walks the entire spec tree including nested steps in loops and parallel blocks.
113
- */
114
- export function extractExpressions(spec: WorkflowSpec): ExpressionRef[] {
115
- const refs: ExpressionRef[] = [];
116
-
117
- // Walk top-level steps
118
- for (const [stepName, step] of Object.entries(spec.steps)) {
119
- extractFromStep(step, `steps.${stepName}`, refs);
120
- }
121
-
122
- // Walk completion
123
- if (spec.completion) {
124
- if (spec.completion.template) {
125
- extractFromString(spec.completion.template, "completion.template", refs);
126
- }
127
- if (spec.completion.message) {
128
- extractFromString(spec.completion.message, "completion.message", refs);
129
- }
130
- if (spec.completion.include) {
131
- for (const inc of spec.completion.include) {
132
- extractFromString(inc, "completion.include", refs);
133
- }
134
- }
135
- }
136
-
137
- // Walk artifacts
138
- if (spec.artifacts) {
139
- for (const [name, art] of Object.entries(spec.artifacts)) {
140
- extractFromString(art.path, `artifacts.${name}.path`, refs);
141
- extractFromString(art.from, `artifacts.${name}.from`, refs);
142
- }
143
- }
144
-
145
- return refs;
146
- }
147
-
148
- function extractFromStep(step: StepSpec, prefix: string, refs: ExpressionRef[]): void {
149
- if (step.when) extractFromString(step.when, `${prefix}.when`, refs);
150
- if (step.forEach) extractFromString(step.forEach, `${prefix}.forEach`, refs);
151
- if (step.input) extractFromString(step.input as string, `${prefix}.input`, refs);
152
- if (step.command) extractFromString(step.command, `${prefix}.command`, refs);
153
-
154
- // Gate check
155
- if (step.gate?.check) extractFromString(step.gate.check, `${prefix}.gate.check`, refs);
156
-
157
- // Transform mapping
158
- if (step.transform?.mapping) {
159
- extractFromValue(step.transform.mapping, `${prefix}.transform.mapping`, refs);
160
- }
161
-
162
- // Nested loop steps
163
- if (step.loop?.steps) {
164
- for (const [subName, subStep] of Object.entries(step.loop.steps)) {
165
- extractFromStep(subStep, `${prefix}.loop.steps.${subName}`, refs);
166
- }
167
- }
168
-
169
- // Nested parallel steps
170
- if (step.parallel) {
171
- for (const [subName, subStep] of Object.entries(step.parallel)) {
172
- extractFromStep(subStep, `${prefix}.parallel.${subName}`, refs);
173
- }
174
- }
175
- }
176
-
177
- function extractFromValue(value: unknown, field: string, refs: ExpressionRef[]): void {
178
- if (typeof value === "string") {
179
- extractFromString(value, field, refs);
180
- } else if (Array.isArray(value)) {
181
- for (let i = 0; i < value.length; i++) {
182
- extractFromValue(value[i], `${field}[${i}]`, refs);
183
- }
184
- } else if (value && typeof value === "object") {
185
- for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
186
- extractFromValue(val, `${field}.${key}`, refs);
187
- }
188
- }
189
- }
190
-
191
- function extractFromString(str: string, field: string, refs: ExpressionRef[]): void {
192
- EXPR_PATTERN.lastIndex = 0;
193
- let match: RegExpExecArray | null;
194
- while ((match = EXPR_PATTERN.exec(str)) !== null) {
195
- const raw = match[1].trim();
196
- const pipeIdx = raw.indexOf("|");
197
- const exprPart = pipeIdx >= 0 ? raw.slice(0, pipeIdx).trim() : raw;
198
- const filterName = pipeIdx >= 0 ? raw.slice(pipeIdx + 1).trim() : undefined;
199
-
200
- // Extract step references: steps.X.anything → step name is X
201
- const stepRefs: string[] = [];
202
- const stepRefPattern = /\bsteps\.(\w+)/g;
203
- let stepMatch: RegExpExecArray | null;
204
- while ((stepMatch = stepRefPattern.exec(exprPart)) !== null) {
205
- if (!stepRefs.includes(stepMatch[1])) {
206
- stepRefs.push(stepMatch[1]);
207
- }
208
- }
209
-
210
- refs.push({ expression: raw, field, stepRefs, filterName });
211
- }
212
- }
213
-
214
- export function declaredSteps(spec: WorkflowSpec): string[] {
215
- return Object.keys(spec.steps);
216
- }
217
-
218
- export function declaredAgentRefs(spec: WorkflowSpec): string[] {
219
- const agents: string[] = [];
220
- collectAgentRefs(spec.steps, agents);
221
- return [...new Set(agents)];
222
- }
223
-
224
- function collectAgentRefs(steps: Record<string, StepSpec>, agents: string[]): void {
225
- for (const step of Object.values(steps)) {
226
- if (step.agent) agents.push(step.agent);
227
- if (step.loop?.steps) collectAgentRefs(step.loop.steps, agents);
228
- if (step.parallel) collectAgentRefs(step.parallel, agents);
229
- }
230
- }
231
-
232
- export function declaredSchemaRefs(spec: WorkflowSpec): string[] {
233
- const schemas: string[] = [];
234
- collectSchemaRefs(spec.steps, schemas);
235
- if (spec.artifacts) {
236
- for (const art of Object.values(spec.artifacts)) {
237
- if (art.schema) schemas.push(art.schema);
238
- }
239
- }
240
- return [...new Set(schemas)];
241
- }
242
-
243
- function collectSchemaRefs(steps: Record<string, StepSpec>, schemas: string[]): void {
244
- for (const step of Object.values(steps)) {
245
- if (step.output?.schema) schemas.push(step.output.schema);
246
- if (step.loop?.steps) collectSchemaRefs(step.loop.steps, schemas);
247
- if (step.parallel) collectSchemaRefs(step.parallel, schemas);
248
- }
249
- }
250
-
251
- // ── Utility ──────────────────────────────────────────────────────────────────
252
-
253
- function walkFiles(dir: string, callback: (filePath: string) => void): void {
254
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
255
- const full = path.join(dir, entry.name);
256
- if (entry.isDirectory()) {
257
- walkFiles(full, callback);
258
- } else if (entry.isFile()) {
259
- callback(full);
260
- }
261
- }
262
- }