@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,47 +0,0 @@
1
- /**
2
- * Transform step executor — produces output by resolving expressions in a mapping.
3
- * No LLM call, no subprocess, no shell command — pure expression resolution.
4
- */
5
- import type { TransformSpec, StepResult } from "./types.ts";
6
- import { resolveExpressions } from "./expression.ts";
7
- import { zeroUsage } from "./step-shared.ts";
8
- import { persistStepOutput } from "./output.ts";
9
-
10
- /**
11
- * Execute a transform step: produces output by resolving expressions in the mapping.
12
- * No LLM call, no subprocess, no shell command — pure expression resolution.
13
- */
14
- export function executeTransform(
15
- transform: TransformSpec,
16
- stepName: string,
17
- scope: Record<string, unknown>,
18
- runDir?: string,
19
- outputPath?: string,
20
- ): StepResult {
21
- const startTime = Date.now();
22
- try {
23
- const output = resolveExpressions(transform.mapping, scope);
24
- const result: StepResult = {
25
- step: stepName,
26
- agent: "transform",
27
- status: "completed",
28
- output,
29
- textOutput: JSON.stringify(output, null, 2),
30
- usage: zeroUsage(),
31
- durationMs: Date.now() - startTime,
32
- };
33
- if (runDir) {
34
- result.outputPath = persistStepOutput(runDir, stepName, output, undefined, outputPath);
35
- }
36
- return result;
37
- } catch (err) {
38
- return {
39
- step: stepName,
40
- agent: "transform",
41
- status: "failed",
42
- usage: zeroUsage(),
43
- durationMs: Date.now() - startTime,
44
- error: err instanceof Error ? err.message : String(err),
45
- };
46
- }
47
- }
@@ -1,72 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { createTemplateEnv, renderTemplateFile } from "./template.ts";
4
- import path from "node:path";
5
- import fs from "node:fs";
6
- import os from "node:os";
7
-
8
- describe("analyzer template inheritance", () => {
9
- const builtinDir = path.resolve(import.meta.dirname, "..", "templates");
10
-
11
- it("structure analyzer inherits base and overrides blocks", () => {
12
- const env = createTemplateEnv("/nonexistent", builtinDir);
13
- const result = renderTemplateFile(env, "analyzers/structure.md", {});
14
- assert.ok(result.includes("code structure analyst"));
15
- assert.ok(result.includes("Architecture"));
16
- assert.ok(result.includes("Module boundaries"));
17
- assert.ok(!result.includes("Test coverage"));
18
- });
19
-
20
- it("quality analyzer inherits base and overrides blocks", () => {
21
- const env = createTemplateEnv("/nonexistent", builtinDir);
22
- const result = renderTemplateFile(env, "analyzers/quality.md", {});
23
- assert.ok(result.includes("code quality analyst"));
24
- assert.ok(result.includes("Test coverage"));
25
- assert.ok(!result.includes("Architecture"));
26
- });
27
-
28
- it("pattern analyzer inherits base and overrides blocks", () => {
29
- const env = createTemplateEnv("/nonexistent", builtinDir);
30
- const result = renderTemplateFile(env, "analyzers/patterns.md", {});
31
- assert.ok(result.includes("design pattern analyst"));
32
- assert.ok(result.includes("Design patterns"));
33
- assert.ok(result.includes("Anti-patterns"));
34
- });
35
-
36
- it("all three produce different output from the same base", () => {
37
- const env = createTemplateEnv("/nonexistent", builtinDir);
38
- const s = renderTemplateFile(env, "analyzers/structure.md", {});
39
- const q = renderTemplateFile(env, "analyzers/quality.md", {});
40
- const p = renderTemplateFile(env, "analyzers/patterns.md", {});
41
- // All contain the shared preamble pattern
42
- assert.ok(s.includes("Given an exploration summary"));
43
- assert.ok(q.includes("Given an exploration summary"));
44
- assert.ok(p.includes("Given an exploration summary"));
45
- // But different identities
46
- assert.ok(s.includes("structure"));
47
- assert.ok(q.includes("quality"));
48
- assert.ok(p.includes("pattern"));
49
- });
50
-
51
- it("project template shadows builtin", () => {
52
- const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-proj-"));
53
- const projectTemplates = path.join(tmpProject, ".pi", "templates", "analyzers");
54
- fs.mkdirSync(projectTemplates, { recursive: true });
55
-
56
- fs.writeFileSync(path.join(projectTemplates, "structure.md"), [
57
- '{% extends "analyzers/base-analyzer.md" %}',
58
- "{% block identity %}You are a CUSTOM structure analyst.{% endblock %}",
59
- "{% block checklist %}",
60
- "1. **Custom**: Project-specific check",
61
- "{% endblock %}",
62
- ].join("\n"));
63
-
64
- const env = createTemplateEnv(tmpProject, builtinDir);
65
- const result = renderTemplateFile(env, "analyzers/structure.md", {});
66
- assert.ok(result.includes("CUSTOM structure analyst"));
67
- assert.ok(result.includes("Project-specific check"));
68
- assert.ok(!result.includes("Module boundaries"));
69
-
70
- fs.rmSync(tmpProject, { recursive: true });
71
- });
72
- });
@@ -1,162 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { createTemplateEnv, renderTemplate, renderTemplateFile } from "./template.ts";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import os from "node:os";
7
-
8
- describe("renderTemplate", () => {
9
- it("renders variable substitution", () => {
10
- const env = createTemplateEnv("/nonexistent");
11
- const result = renderTemplate(env, "Hello {{ name }}, you are {{ role }}", { name: "Alice", role: "analyst" });
12
- assert.strictEqual(result, "Hello Alice, you are analyst");
13
- });
14
-
15
- it("passes through plain text unchanged", () => {
16
- const env = createTemplateEnv("/nonexistent");
17
- const result = renderTemplate(env, "No templates here. Just markdown.", {});
18
- assert.strictEqual(result, "No templates here. Just markdown.");
19
- });
20
-
21
- it("renders if/else conditionals", () => {
22
- const env = createTemplateEnv("/nonexistent");
23
- const tmpl = "{% if verbose %}Detailed output{% else %}Brief output{% endif %}";
24
- assert.strictEqual(renderTemplate(env, tmpl, { verbose: true }), "Detailed output");
25
- assert.strictEqual(renderTemplate(env, tmpl, { verbose: false }), "Brief output");
26
- });
27
-
28
- it("renders for loops", () => {
29
- const env = createTemplateEnv("/nonexistent");
30
- const tmpl = "{% for item in items %}- {{ item }}\n{% endfor %}";
31
- const result = renderTemplate(env, tmpl, { items: ["a", "b", "c"] });
32
- assert.ok(result.includes("- a"));
33
- assert.ok(result.includes("- b"));
34
- assert.ok(result.includes("- c"));
35
- });
36
-
37
- it("handles undefined variables gracefully (empty string)", () => {
38
- const env = createTemplateEnv("/nonexistent");
39
- const result = renderTemplate(env, "Hello {{ name }}", {});
40
- assert.strictEqual(result, "Hello ");
41
- });
42
-
43
- it("preserves ${{ }} workflow expressions (not interpreted)", () => {
44
- const env = createTemplateEnv("/nonexistent");
45
- const result = renderTemplate(env, "Use ${{ steps.explore.output }}", {});
46
- assert.strictEqual(result, "Use ${{ steps.explore.output }}");
47
- });
48
-
49
- it("renders nested object access", () => {
50
- const env = createTemplateEnv("/nonexistent");
51
- const result = renderTemplate(env, "File: {{ context.file }}", { context: { file: "main.ts" } });
52
- assert.strictEqual(result, "File: main.ts");
53
- });
54
- });
55
-
56
- describe("renderTemplateFile", () => {
57
- it("renders a template file from the search path", () => {
58
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-test-"));
59
- fs.writeFileSync(path.join(tmpDir, "test.md"), "Hello {{ name }}");
60
-
61
- const env = createTemplateEnv("/nonexistent", tmpDir);
62
- const result = renderTemplateFile(env, "test.md", { name: "World" });
63
- assert.strictEqual(result, "Hello World");
64
-
65
- fs.rmSync(tmpDir, { recursive: true });
66
- });
67
-
68
- it("supports {% extends %} inheritance", () => {
69
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-test-"));
70
-
71
- fs.writeFileSync(path.join(tmpDir, "base.md"), [
72
- "# {{ title }}",
73
- "{% block intro %}Default intro{% endblock %}",
74
- "{% block body %}Default body{% endblock %}",
75
- ].join("\n"));
76
-
77
- fs.writeFileSync(path.join(tmpDir, "child.md"), [
78
- '{% extends "base.md" %}',
79
- "{% block body %}Custom body for {{ focus }}{% endblock %}",
80
- ].join("\n"));
81
-
82
- const env = createTemplateEnv("/nonexistent", tmpDir);
83
- const result = renderTemplateFile(env, "child.md", { title: "Report", focus: "security" });
84
- assert.ok(result.includes("# Report"), "should render parent title");
85
- assert.ok(result.includes("Default intro"), "should inherit intro block");
86
- assert.ok(result.includes("Custom body for security"), "should override body block");
87
- assert.ok(!result.includes("Default body"), "should NOT include default body");
88
-
89
- fs.rmSync(tmpDir, { recursive: true });
90
- });
91
-
92
- it("supports {% include %}", () => {
93
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-test-"));
94
-
95
- fs.writeFileSync(path.join(tmpDir, "fragment.md"), "This is a reusable fragment.");
96
- fs.writeFileSync(path.join(tmpDir, "main.md"), [
97
- "# Main",
98
- '{% include "fragment.md" %}',
99
- "End.",
100
- ].join("\n"));
101
-
102
- const env = createTemplateEnv("/nonexistent", tmpDir);
103
- const result = renderTemplateFile(env, "main.md", {});
104
- assert.ok(result.includes("# Main"));
105
- assert.ok(result.includes("This is a reusable fragment."));
106
- assert.ok(result.includes("End."));
107
-
108
- fs.rmSync(tmpDir, { recursive: true });
109
- });
110
-
111
- it("supports {% macro %}", () => {
112
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-test-"));
113
-
114
- fs.writeFileSync(path.join(tmpDir, "macros.md"), [
115
- "{% macro greeting(name) %}Hello, {{ name }}!{% endmacro %}",
116
- ].join("\n"));
117
- fs.writeFileSync(path.join(tmpDir, "main.md"), [
118
- '{% from "macros.md" import greeting %}',
119
- "{{ greeting('Alice') }}",
120
- "{{ greeting('Bob') }}",
121
- ].join("\n"));
122
-
123
- const env = createTemplateEnv("/nonexistent", tmpDir);
124
- const result = renderTemplateFile(env, "main.md", {});
125
- assert.ok(result.includes("Hello, Alice!"));
126
- assert.ok(result.includes("Hello, Bob!"));
127
-
128
- fs.rmSync(tmpDir, { recursive: true });
129
- });
130
-
131
- it("project templates shadow builtin templates", () => {
132
- const builtinDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-builtin-"));
133
- const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-project-"));
134
-
135
- const projectTemplates = path.join(projectDir, ".pi", "templates");
136
- fs.mkdirSync(projectTemplates, { recursive: true });
137
-
138
- fs.writeFileSync(path.join(builtinDir, "shared.md"), "BUILTIN version");
139
- fs.writeFileSync(path.join(projectTemplates, "shared.md"), "PROJECT version");
140
-
141
- const env = createTemplateEnv(projectDir, builtinDir);
142
- const result = renderTemplateFile(env, "shared.md", {});
143
- assert.strictEqual(result, "PROJECT version");
144
-
145
- fs.rmSync(builtinDir, { recursive: true });
146
- fs.rmSync(projectDir, { recursive: true });
147
- });
148
- });
149
-
150
- describe("createTemplateEnv", () => {
151
- it("creates an environment that renders plain text unchanged", () => {
152
- const env = createTemplateEnv("/nonexistent");
153
- const result = renderTemplate(env, "Just plain text", {});
154
- assert.strictEqual(result, "Just plain text");
155
- });
156
-
157
- it("works when no template directories exist", () => {
158
- const env = createTemplateEnv("/definitely/does/not/exist");
159
- const result = renderTemplate(env, "Still works: {{ x }}", { x: 42 });
160
- assert.strictEqual(result, "Still works: 42");
161
- });
162
- });
@@ -1,51 +0,0 @@
1
- /**
2
- * Shared test helpers — mock factories for ctx, pi, and workflow specs.
3
- */
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import os from "node:os";
7
- import type { WorkflowSpec, StepSpec } from "./types.ts";
8
-
9
- /**
10
- * Create a mock extension context for testing.
11
- */
12
- export function mockCtx(cwd: string) {
13
- return {
14
- cwd,
15
- hasUI: false,
16
- ui: {
17
- setWidget: () => {},
18
- notify: () => {},
19
- setStatus: () => {},
20
- },
21
- } as any;
22
- }
23
-
24
- /**
25
- * Create a mock pi API for testing.
26
- */
27
- export function mockPi() {
28
- const messages: any[] = [];
29
- return {
30
- sendMessage: (msg: any, opts: any) => messages.push({ msg, opts }),
31
- _messages: messages,
32
- } as any;
33
- }
34
-
35
- /**
36
- * Create a minimal WorkflowSpec for testing.
37
- * A fresh temp directory is created for filePath.
38
- */
39
- export function makeSpec(
40
- overrides: Partial<WorkflowSpec> & { steps: Record<string, StepSpec> },
41
- ): WorkflowSpec {
42
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-test-"));
43
- return {
44
- name: "test-workflow",
45
- description: "Test workflow",
46
- version: "1",
47
- source: "project" as const,
48
- filePath: path.join(tmpDir, "test.workflow.yaml"),
49
- ...overrides,
50
- };
51
- }
package/src/tui.test.ts DELETED
@@ -1,355 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import type { Theme } from "@mariozechner/pi-coding-agent";
4
- import type { TUI } from "@mariozechner/pi-tui";
5
- import { createProgressWidget, type ProgressWidgetState } from "./tui.ts";
6
- import type { WorkflowSpec, StepUsage } from "./types.ts";
7
-
8
- /**
9
- * Minimal Theme mock — passes through text unchanged so we can assert on content
10
- * without dealing with ANSI codes. Mocks exactly the methods tui.ts calls.
11
- */
12
- function mockTheme(): Theme {
13
- return {
14
- bold: (s: string) => s,
15
- fg: (_color: string, s: string) => s,
16
- dim: (s: string) => s,
17
- } as unknown as Theme;
18
- }
19
-
20
- /**
21
- * Minimal TUI mock — just needs requestRender().
22
- */
23
- function mockTUI(): TUI {
24
- return {
25
- requestRender: () => {},
26
- } as unknown as TUI;
27
- }
28
-
29
- function zeroUsage(): StepUsage {
30
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
31
- }
32
-
33
- function makeWidgetState(overrides: Partial<ProgressWidgetState> = {}): ProgressWidgetState {
34
- const spec: WorkflowSpec = {
35
- name: "test-workflow",
36
- description: "test",
37
- steps: {
38
- explore: { agent: "explorer" },
39
- analyze: { agent: "analyzer" },
40
- report: { agent: "reporter" },
41
- },
42
- source: "project",
43
- filePath: "/test.yaml",
44
- };
45
-
46
- return {
47
- spec,
48
- state: { input: {}, steps: {}, status: "running" },
49
- startTime: Date.now(),
50
- ...overrides,
51
- };
52
- }
53
-
54
- describe("createProgressWidget", () => {
55
- it("returns a factory function", () => {
56
- const factory = createProgressWidget(makeWidgetState());
57
- assert.strictEqual(typeof factory, "function");
58
- });
59
-
60
- it("factory creates a component with render and dispose", () => {
61
- const factory = createProgressWidget(makeWidgetState());
62
- const component = factory(mockTUI(), mockTheme());
63
- assert.strictEqual(typeof component.render, "function");
64
- assert.strictEqual(typeof component.dispose, "function");
65
- component.dispose!();
66
- });
67
-
68
- it("renders header with workflow name and step count", () => {
69
- const widgetState = makeWidgetState({ currentStep: "explore" });
70
- const factory = createProgressWidget(widgetState);
71
- const component = factory(mockTUI(), mockTheme());
72
- const lines = component.render(120);
73
- component.dispose!();
74
-
75
- assert.ok(lines[0].includes("test-workflow"), "header should include workflow name");
76
- assert.ok(lines[0].includes("1/3"), "header should show step 1/3");
77
- });
78
-
79
- it("renders pending steps with dot indicator", () => {
80
- const widgetState = makeWidgetState();
81
- const factory = createProgressWidget(widgetState);
82
- const component = factory(mockTUI(), mockTheme());
83
- const lines = component.render(120);
84
- component.dispose!();
85
-
86
- assert.ok(lines.some(l => l.includes("·") && l.includes("explore")));
87
- assert.ok(lines.some(l => l.includes("·") && l.includes("analyze")));
88
- assert.ok(lines.some(l => l.includes("·") && l.includes("report")));
89
- });
90
-
91
- it("renders running step with triangle indicator", () => {
92
- const widgetState = makeWidgetState({ currentStep: "analyze" });
93
- const factory = createProgressWidget(widgetState);
94
- const component = factory(mockTUI(), mockTheme());
95
- const lines = component.render(120);
96
- component.dispose!();
97
-
98
- assert.ok(lines.some(l => l.includes("▸") && l.includes("analyze")));
99
- });
100
-
101
- it("renders completed step with checkmark, duration, cost, tokens", () => {
102
- const widgetState = makeWidgetState({ currentStep: "analyze" });
103
- widgetState.state.steps.explore = {
104
- step: "explore",
105
- agent: "explorer",
106
- status: "completed",
107
- usage: { input: 5000, output: 2000, cacheRead: 0, cacheWrite: 0, cost: 0.03, turns: 1 },
108
- durationMs: 42000,
109
- };
110
- const factory = createProgressWidget(widgetState);
111
- const component = factory(mockTUI(), mockTheme());
112
- const lines = component.render(120);
113
- component.dispose!();
114
-
115
- const exploreLine = lines.find(l => l.includes("explore"));
116
- assert.ok(exploreLine, "should have explore line");
117
- assert.ok(exploreLine.includes("✓"), "completed should have checkmark");
118
- assert.ok(exploreLine.includes("42s"), "should show duration");
119
- assert.ok(exploreLine.includes("$0.03"), "should show cost");
120
- assert.ok(exploreLine.includes("7.0k tok"), "should show token count");
121
- });
122
-
123
- it("renders failed step with X indicator and error", () => {
124
- const widgetState = makeWidgetState();
125
- widgetState.state.steps.explore = {
126
- step: "explore",
127
- agent: "explorer",
128
- status: "failed",
129
- usage: zeroUsage(),
130
- durationMs: 5000,
131
- error: "Agent crashed",
132
- };
133
- const factory = createProgressWidget(widgetState);
134
- const component = factory(mockTUI(), mockTheme());
135
- const lines = component.render(120);
136
- component.dispose!();
137
-
138
- const exploreLine = lines.find(l => l.includes("explore"));
139
- assert.ok(exploreLine, "should have explore line");
140
- assert.ok(exploreLine.includes("✗"), "failed should have X");
141
- assert.ok(exploreLine.includes("Agent crashed"), "should show error");
142
- });
143
-
144
- it("renders skipped step with circle indicator", () => {
145
- const widgetState = makeWidgetState();
146
- widgetState.state.steps.explore = {
147
- step: "explore",
148
- agent: "skipped",
149
- status: "skipped",
150
- usage: zeroUsage(),
151
- durationMs: 0,
152
- };
153
- const factory = createProgressWidget(widgetState);
154
- const component = factory(mockTUI(), mockTheme());
155
- const lines = component.render(120);
156
- component.dispose!();
157
-
158
- const exploreLine = lines.find(l => l.includes("explore"));
159
- assert.ok(exploreLine, "should have explore line");
160
- assert.ok(exploreLine.includes("⊘"), "skipped should have ⊘");
161
- assert.ok(exploreLine.includes("[skipped]"), "should show [skipped] tag");
162
- });
163
-
164
- it("renders gate step with [gate] tag", () => {
165
- const widgetState = makeWidgetState();
166
- widgetState.state.steps.explore = {
167
- step: "explore",
168
- agent: "gate",
169
- status: "completed",
170
- output: { passed: true, exitCode: 0, output: "ok" },
171
- usage: zeroUsage(),
172
- durationMs: 100,
173
- };
174
- const factory = createProgressWidget(widgetState);
175
- const component = factory(mockTUI(), mockTheme());
176
- const lines = component.render(120);
177
- component.dispose!();
178
-
179
- const exploreLine = lines.find(l => l.includes("explore"));
180
- assert.ok(exploreLine?.includes("[gate]"), "gate step should show [gate] tag");
181
- });
182
-
183
- it("renders transform step with [transform] tag", () => {
184
- const widgetState = makeWidgetState();
185
- widgetState.state.steps.explore = {
186
- step: "explore",
187
- agent: "transform",
188
- status: "completed",
189
- output: { merged: true },
190
- usage: zeroUsage(),
191
- durationMs: 1,
192
- };
193
- const factory = createProgressWidget(widgetState);
194
- const component = factory(mockTUI(), mockTheme());
195
- const lines = component.render(120);
196
- component.dispose!();
197
-
198
- const exploreLine = lines.find(l => l.includes("explore"));
199
- assert.ok(exploreLine?.includes("[transform]"), "transform step should show [transform] tag");
200
- });
201
-
202
- it("shows parallel count in header for multiple concurrent steps", () => {
203
- const widgetState = makeWidgetState({ currentStep: "explore, analyze" });
204
- const factory = createProgressWidget(widgetState);
205
- const component = factory(mockTUI(), mockTheme());
206
- const lines = component.render(120);
207
- component.dispose!();
208
-
209
- assert.ok(lines[0].includes("[2 parallel]"), "header should show parallel count");
210
- });
211
-
212
- it("shows multiple running indicators for parallel steps", () => {
213
- const widgetState = makeWidgetState({ currentStep: "explore, analyze" });
214
- const factory = createProgressWidget(widgetState);
215
- const component = factory(mockTUI(), mockTheme());
216
- const lines = component.render(120);
217
- component.dispose!();
218
-
219
- const runningLines = lines.filter(l => l.includes("▸"));
220
- assert.strictEqual(runningLines.length, 2, "should have 2 running step indicators");
221
- });
222
-
223
- it("renders correct number of lines (header + one per step)", () => {
224
- const widgetState = makeWidgetState();
225
- const factory = createProgressWidget(widgetState);
226
- const component = factory(mockTUI(), mockTheme());
227
- const lines = component.render(120);
228
- component.dispose!();
229
-
230
- // 1 header + 3 steps = 4 lines
231
- assert.strictEqual(lines.length, 4);
232
- });
233
-
234
- it("truncates lines to width", () => {
235
- const widgetState = makeWidgetState({ currentStep: "explore" });
236
- const factory = createProgressWidget(widgetState);
237
- const component = factory(mockTUI(), mockTheme());
238
- const lines = component.render(20);
239
- component.dispose!();
240
-
241
- for (const line of lines) {
242
- assert.ok(line.length <= 20, `line exceeds width: "${line}" (${line.length})`);
243
- }
244
- });
245
-
246
- it("renders resumed indicator when resumedSteps is set", () => {
247
- const widgetState = makeWidgetState({ resumedSteps: 3, currentStep: "report" });
248
- widgetState.state.steps.explore = {
249
- step: "explore",
250
- agent: "explorer",
251
- status: "completed",
252
- usage: zeroUsage(),
253
- durationMs: 10000,
254
- };
255
- widgetState.state.steps.analyze = {
256
- step: "analyze",
257
- agent: "analyzer",
258
- status: "completed",
259
- usage: zeroUsage(),
260
- durationMs: 20000,
261
- };
262
- const factory = createProgressWidget(widgetState);
263
- const component = factory(mockTUI(), mockTheme());
264
- const lines = component.render(120);
265
- component.dispose!();
266
-
267
- const resumedLine = lines.find(l => l.includes("Resumed"));
268
- assert.ok(resumedLine, "should have a resumed indicator line");
269
- assert.ok(resumedLine.includes("↻"), "should have ↻ symbol");
270
- assert.ok(resumedLine.includes("3 steps from prior run"), "should show step count");
271
- // Header + resumed line + 3 steps = 5 lines
272
- assert.strictEqual(lines.length, 5);
273
- });
274
-
275
- it("renders paused indicator when state status is paused", () => {
276
- const widgetState = makeWidgetState();
277
- widgetState.state.status = "paused";
278
- widgetState.state.steps.explore = {
279
- step: "explore",
280
- agent: "explorer",
281
- status: "completed",
282
- usage: zeroUsage(),
283
- durationMs: 10000,
284
- };
285
- const factory = createProgressWidget(widgetState);
286
- const component = factory(mockTUI(), mockTheme());
287
- const lines = component.render(120);
288
- component.dispose!();
289
-
290
- const pausedLine = lines.find(l => l.includes("Paused"));
291
- assert.ok(pausedLine, "should have a paused indicator line");
292
- assert.ok(pausedLine.includes("⏸"), "should have ⏸ symbol");
293
- });
294
-
295
- it("does not render resumed indicator when resumedSteps is not set", () => {
296
- const widgetState = makeWidgetState();
297
- const factory = createProgressWidget(widgetState);
298
- const component = factory(mockTUI(), mockTheme());
299
- const lines = component.render(120);
300
- component.dispose!();
301
-
302
- assert.ok(!lines.some(l => l.includes("Resumed")), "should not have resumed indicator");
303
- });
304
-
305
- it("shows [truncated] indicator for truncated step", () => {
306
- const widgetState = makeWidgetState({ currentStep: "analyze" });
307
- widgetState.state.steps.explore = {
308
- step: "explore",
309
- agent: "explorer",
310
- status: "completed",
311
- usage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 1 },
312
- durationMs: 5000,
313
- truncated: true,
314
- warnings: ["Stdout exceeded 10MB limit"],
315
- };
316
- const factory = createProgressWidget(widgetState);
317
- const component = factory(mockTUI(), mockTheme());
318
- const lines = component.render(120);
319
- component.dispose!();
320
-
321
- // Find the explore line and verify it contains a truncated indicator
322
- const exploreLine = lines.find(l => l.includes("explore"));
323
- assert.ok(exploreLine, "explore line should exist");
324
- assert.ok(exploreLine.includes("truncated"), "Truncated step should show [truncated] indicator");
325
- });
326
-
327
- it("does not show [truncated] indicator for non-truncated step", () => {
328
- const widgetState = makeWidgetState({ currentStep: "analyze" });
329
- widgetState.state.steps.explore = {
330
- step: "explore",
331
- agent: "explorer",
332
- status: "completed",
333
- usage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 1 },
334
- durationMs: 5000,
335
- };
336
- const factory = createProgressWidget(widgetState);
337
- const component = factory(mockTUI(), mockTheme());
338
- const lines = component.render(120);
339
- component.dispose!();
340
-
341
- const exploreLine = lines.find(l => l.includes("explore"));
342
- assert.ok(exploreLine, "explore line should exist");
343
- assert.ok(!exploreLine.includes("truncated"), "Non-truncated step should not show [truncated]");
344
- });
345
-
346
- it("dispose clears the pulse interval", () => {
347
- const widgetState = makeWidgetState();
348
- const factory = createProgressWidget(widgetState);
349
- const component = factory(mockTUI(), mockTheme());
350
-
351
- // Should not throw; if dispose didn't clear the interval,
352
- // the test process would hang (node:test waits for timers to drain)
353
- component.dispose!();
354
- });
355
- });