@davidorex/pi-workflows 0.1.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 (115) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +169 -0
  3. package/agents/audit-fixer.agent.yaml +18 -0
  4. package/agents/code-explorer.agent.yaml +11 -0
  5. package/agents/decomposer.agent.yaml +51 -0
  6. package/agents/investigator.agent.yaml +49 -0
  7. package/agents/pattern-analyzer.agent.yaml +21 -0
  8. package/agents/phase-author.agent.yaml +16 -0
  9. package/agents/plan-decomposer.agent.yaml +12 -0
  10. package/agents/quality-analyzer.agent.yaml +21 -0
  11. package/agents/researcher.agent.yaml +38 -0
  12. package/agents/spec-implementer.agent.yaml +12 -0
  13. package/agents/structure-analyzer.agent.yaml +21 -0
  14. package/agents/synthesizer.agent.yaml +23 -0
  15. package/agents/verifier.agent.yaml +12 -0
  16. package/package.json +40 -0
  17. package/schemas/decomposition-specs.schema.json +50 -0
  18. package/schemas/execution-results.schema.json +50 -0
  19. package/schemas/investigation-findings.schema.json +45 -0
  20. package/schemas/pattern-analysis.schema.json +43 -0
  21. package/schemas/plan-breakdown.schema.json +22 -0
  22. package/schemas/quality-analysis.schema.json +35 -0
  23. package/schemas/research-findings.schema.json +44 -0
  24. package/schemas/structure-analysis.schema.json +42 -0
  25. package/schemas/synthesis.schema.json +33 -0
  26. package/schemas/verifier-output.schema.json +96 -0
  27. package/src/agent-spec.test.ts +289 -0
  28. package/src/agent-spec.ts +122 -0
  29. package/src/block-validation.test.ts +350 -0
  30. package/src/checkpoint.test.ts +237 -0
  31. package/src/checkpoint.ts +139 -0
  32. package/src/completion.test.ts +143 -0
  33. package/src/completion.ts +68 -0
  34. package/src/dag.test.ts +350 -0
  35. package/src/dag.ts +219 -0
  36. package/src/dispatch.test.ts +473 -0
  37. package/src/dispatch.ts +353 -0
  38. package/src/expression.test.ts +353 -0
  39. package/src/expression.ts +332 -0
  40. package/src/format.test.ts +80 -0
  41. package/src/format.ts +41 -0
  42. package/src/graduated-failure.test.ts +556 -0
  43. package/src/index.test.ts +114 -0
  44. package/src/index.ts +488 -0
  45. package/src/loop.test.ts +549 -0
  46. package/src/output.test.ts +213 -0
  47. package/src/output.ts +70 -0
  48. package/src/parallel-integration.test.ts +175 -0
  49. package/src/resume.test.ts +192 -0
  50. package/src/state.test.ts +192 -0
  51. package/src/state.ts +253 -0
  52. package/src/step-agent.test.ts +327 -0
  53. package/src/step-agent.ts +178 -0
  54. package/src/step-command.test.ts +330 -0
  55. package/src/step-command.ts +132 -0
  56. package/src/step-foreach.test.ts +647 -0
  57. package/src/step-foreach.ts +148 -0
  58. package/src/step-gate.test.ts +185 -0
  59. package/src/step-gate.ts +116 -0
  60. package/src/step-loop.test.ts +626 -0
  61. package/src/step-loop.ts +323 -0
  62. package/src/step-parallel.test.ts +475 -0
  63. package/src/step-parallel.ts +168 -0
  64. package/src/step-pause.test.ts +30 -0
  65. package/src/step-pause.ts +27 -0
  66. package/src/step-shared.test.ts +355 -0
  67. package/src/step-shared.ts +157 -0
  68. package/src/step-transform.test.ts +155 -0
  69. package/src/step-transform.ts +47 -0
  70. package/src/template-integration.test.ts +72 -0
  71. package/src/template.test.ts +162 -0
  72. package/src/template.ts +92 -0
  73. package/src/test-helpers.ts +51 -0
  74. package/src/tui.test.ts +355 -0
  75. package/src/tui.ts +182 -0
  76. package/src/types.ts +195 -0
  77. package/src/verifier-schema.test.ts +226 -0
  78. package/src/workflow-discovery.test.ts +105 -0
  79. package/src/workflow-discovery.ts +113 -0
  80. package/src/workflow-executor.test.ts +1530 -0
  81. package/src/workflow-executor.ts +810 -0
  82. package/src/workflow-sdk.test.ts +238 -0
  83. package/src/workflow-sdk.ts +262 -0
  84. package/src/workflow-spec.test.ts +576 -0
  85. package/src/workflow-spec.ts +471 -0
  86. package/templates/analyzers/base-analyzer.md +9 -0
  87. package/templates/analyzers/macros.md +1 -0
  88. package/templates/analyzers/patterns-task.md +11 -0
  89. package/templates/analyzers/patterns.md +15 -0
  90. package/templates/analyzers/quality-task.md +11 -0
  91. package/templates/analyzers/quality.md +15 -0
  92. package/templates/analyzers/structure-task.md +17 -0
  93. package/templates/analyzers/structure.md +15 -0
  94. package/templates/audit-fixer/task.md +67 -0
  95. package/templates/decomposer/task.md +56 -0
  96. package/templates/explorer/system.md +3 -0
  97. package/templates/explorer/task.md +9 -0
  98. package/templates/investigator/task.md +30 -0
  99. package/templates/phase-author/task.md +74 -0
  100. package/templates/plan-decomposer/task.md +46 -0
  101. package/templates/researcher/task.md +26 -0
  102. package/templates/spec-implementer/task.md +53 -0
  103. package/templates/synthesizer/system.md +3 -0
  104. package/templates/synthesizer/task.md +38 -0
  105. package/templates/verifier/task.md +57 -0
  106. package/workflows/create-phase.workflow.yaml +67 -0
  107. package/workflows/do-gap.workflow.yaml +153 -0
  108. package/workflows/fix-audit.workflow.yaml +217 -0
  109. package/workflows/gap-to-phase.workflow.yaml +98 -0
  110. package/workflows/parallel-analysis.workflow.yaml +62 -0
  111. package/workflows/parallel-explicit.workflow.yaml +42 -0
  112. package/workflows/pausable-analysis.workflow.yaml +44 -0
  113. package/workflows/resumable-analysis.workflow.yaml +42 -0
  114. package/workflows/self-implement.workflow.yaml +63 -0
  115. package/workflows/typed-analysis.workflow.yaml +63 -0
@@ -0,0 +1,45 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["affected_files", "constraints", "risks", "needs_research", "complexity", "summary"],
4
+ "properties": {
5
+ "affected_files": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "object",
9
+ "required": ["path", "role", "current_behavior"],
10
+ "properties": {
11
+ "path": { "type": "string" },
12
+ "role": { "type": "string", "description": "What this file does in the context of the gap" },
13
+ "current_behavior": { "type": "string", "description": "What it currently does that's relevant to the gap" }
14
+ }
15
+ }
16
+ },
17
+ "constraints": {
18
+ "type": "array",
19
+ "items": { "type": "string" },
20
+ "description": "Technical constraints, invariants, or requirements that the solution must respect"
21
+ },
22
+ "risks": {
23
+ "type": "array",
24
+ "items": { "type": "string" },
25
+ "description": "What could go wrong, edge cases, regression risks"
26
+ },
27
+ "needs_research": {
28
+ "type": "boolean",
29
+ "description": "Whether the gap requires external knowledge beyond what codebase analysis and LLM training data provide"
30
+ },
31
+ "research_questions": {
32
+ "type": "array",
33
+ "items": { "type": "string" },
34
+ "description": "Specific questions for the research step to answer. Only meaningful if needs_research is true."
35
+ },
36
+ "complexity": {
37
+ "type": "string",
38
+ "enum": ["low", "medium", "high"]
39
+ },
40
+ "summary": {
41
+ "type": "string",
42
+ "description": "Concise summary of the investigation — what's the problem, what's involved, what's the path forward"
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["patterns", "conventions", "recommendations"],
4
+ "properties": {
5
+ "patterns": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "object",
9
+ "required": ["name", "usage", "files"],
10
+ "properties": {
11
+ "name": { "type": "string" },
12
+ "usage": { "type": "string" },
13
+ "files": { "type": "array", "items": { "type": "string" } },
14
+ "correct": { "type": "boolean" }
15
+ }
16
+ }
17
+ },
18
+ "conventions": {
19
+ "type": "array",
20
+ "items": {
21
+ "type": "object",
22
+ "required": ["convention", "followed"],
23
+ "properties": {
24
+ "convention": { "type": "string" },
25
+ "followed": { "type": "boolean" },
26
+ "violations": { "type": "array", "items": { "type": "string" } }
27
+ }
28
+ }
29
+ },
30
+ "recommendations": {
31
+ "type": "array",
32
+ "items": {
33
+ "type": "object",
34
+ "required": ["pattern", "suggestion"],
35
+ "properties": {
36
+ "pattern": { "type": "string" },
37
+ "suggestion": { "type": "string" },
38
+ "priority": { "type": "string", "enum": ["high", "medium", "low"] }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["plans"],
4
+ "properties": {
5
+ "plans": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "object",
9
+ "required": ["name", "intent", "tasks", "acceptance_criteria"],
10
+ "properties": {
11
+ "name": { "type": "string" },
12
+ "intent": { "type": "string" },
13
+ "tasks": { "type": "array", "items": { "type": "string" } },
14
+ "files_to_change": { "type": "array", "items": { "type": "string" } },
15
+ "acceptance_criteria": { "type": "array", "items": { "type": "string" } },
16
+ "context_needed": { "type": "array", "items": { "type": "string" } },
17
+ "parallel_group": { "type": "string" }
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["concerns", "testCoverage", "maintainability"],
4
+ "properties": {
5
+ "concerns": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "object",
9
+ "required": ["description", "severity"],
10
+ "properties": {
11
+ "description": { "type": "string" },
12
+ "severity": { "type": "string", "enum": ["info", "warning", "critical"] },
13
+ "file": { "type": "string" },
14
+ "suggestion": { "type": "string" }
15
+ }
16
+ }
17
+ },
18
+ "testCoverage": {
19
+ "type": "object",
20
+ "required": ["tested", "untested"],
21
+ "properties": {
22
+ "tested": { "type": "array", "items": { "type": "string" } },
23
+ "untested": { "type": "array", "items": { "type": "string" } }
24
+ }
25
+ },
26
+ "maintainability": {
27
+ "type": "object",
28
+ "required": ["score", "factors"],
29
+ "properties": {
30
+ "score": { "type": "string", "enum": ["high", "medium", "low"] },
31
+ "factors": { "type": "array", "items": { "type": "string" } }
32
+ }
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["questions", "patterns", "recommendations"],
4
+ "properties": {
5
+ "questions": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "object",
9
+ "required": ["question", "answer", "confidence"],
10
+ "properties": {
11
+ "question": { "type": "string" },
12
+ "answer": { "type": "string" },
13
+ "sources": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "description": "Where the answer came from — docs, other projects, known patterns"
17
+ },
18
+ "confidence": {
19
+ "type": "string",
20
+ "enum": ["high", "medium", "low"],
21
+ "description": "How confident the answer is — high = well-known pattern, low = best guess"
22
+ }
23
+ }
24
+ }
25
+ },
26
+ "patterns": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "object",
30
+ "required": ["name", "description", "applicability"],
31
+ "properties": {
32
+ "name": { "type": "string" },
33
+ "description": { "type": "string" },
34
+ "applicability": { "type": "string", "description": "How this pattern applies to the specific gap" }
35
+ }
36
+ }
37
+ },
38
+ "recommendations": {
39
+ "type": "array",
40
+ "items": { "type": "string" },
41
+ "description": "Concrete recommendations based on research findings"
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["architecture", "modules", "dependencies"],
4
+ "properties": {
5
+ "architecture": {
6
+ "type": "object",
7
+ "required": ["patterns", "organization"],
8
+ "properties": {
9
+ "patterns": { "type": "array", "items": { "type": "string" } },
10
+ "organization": { "type": "string" }
11
+ }
12
+ },
13
+ "modules": {
14
+ "type": "array",
15
+ "items": {
16
+ "type": "object",
17
+ "required": ["name", "responsibility", "files"],
18
+ "properties": {
19
+ "name": { "type": "string" },
20
+ "responsibility": { "type": "string" },
21
+ "files": { "type": "array", "items": { "type": "string" } }
22
+ }
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "object",
29
+ "required": ["from", "to", "type"],
30
+ "properties": {
31
+ "from": { "type": "string" },
32
+ "to": { "type": "string" },
33
+ "type": { "type": "string", "enum": ["import", "runtime", "config"] }
34
+ }
35
+ }
36
+ },
37
+ "entryPoints": {
38
+ "type": "array",
39
+ "items": { "type": "string" }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["summary", "findings", "recommendations"],
4
+ "properties": {
5
+ "summary": { "type": "string" },
6
+ "findings": {
7
+ "type": "array",
8
+ "items": {
9
+ "type": "object",
10
+ "required": ["category", "description", "severity"],
11
+ "properties": {
12
+ "category": { "type": "string", "enum": ["structure", "quality", "patterns"] },
13
+ "description": { "type": "string" },
14
+ "severity": { "type": "string", "enum": ["info", "warning", "critical"] },
15
+ "files": { "type": "array", "items": { "type": "string" } }
16
+ }
17
+ }
18
+ },
19
+ "recommendations": {
20
+ "type": "array",
21
+ "items": {
22
+ "type": "object",
23
+ "required": ["action", "rationale", "priority"],
24
+ "properties": {
25
+ "action": { "type": "string" },
26
+ "rationale": { "type": "string" },
27
+ "priority": { "type": "string", "enum": ["high", "medium", "low"] },
28
+ "affectedFiles": { "type": "array", "items": { "type": "string" } }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,96 @@
1
+ {
2
+ "type": "object",
3
+ "required": ["status", "score", "truths", "criteria_results"],
4
+ "properties": {
5
+ "status": {
6
+ "type": "string",
7
+ "enum": ["passed", "gaps_found", "human_needed"]
8
+ },
9
+ "score": {
10
+ "type": "string",
11
+ "description": "Score in N/M format (e.g. '5/7')"
12
+ },
13
+ "truths": {
14
+ "type": "array",
15
+ "items": {
16
+ "type": "object",
17
+ "required": ["truth", "status", "evidence"],
18
+ "properties": {
19
+ "truth": { "type": "string" },
20
+ "status": { "type": "string", "enum": ["verified", "failed", "uncertain"] },
21
+ "evidence": { "type": "string" }
22
+ }
23
+ }
24
+ },
25
+ "artifacts": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "object",
29
+ "required": ["path", "status"],
30
+ "properties": {
31
+ "path": { "type": "string" },
32
+ "status": { "type": "string", "enum": ["verified", "stub", "missing"] },
33
+ "exists": { "type": "boolean" },
34
+ "substantive": { "type": "boolean" },
35
+ "wired": { "type": "boolean" },
36
+ "details": { "type": "string" }
37
+ }
38
+ }
39
+ },
40
+ "requirements_coverage": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "required": ["requirement_id", "status"],
45
+ "properties": {
46
+ "requirement_id": { "type": "string" },
47
+ "status": { "type": "string", "enum": ["satisfied", "blocked", "needs_human"] },
48
+ "supporting_truths": {
49
+ "type": "array",
50
+ "items": { "type": "string" }
51
+ }
52
+ }
53
+ }
54
+ },
55
+ "criteria_results": {
56
+ "type": "array",
57
+ "items": {
58
+ "type": "object",
59
+ "required": ["criterion", "verify_method", "status"],
60
+ "properties": {
61
+ "criterion": { "type": "string" },
62
+ "verify_method": { "type": "string", "enum": ["command", "grep", "inspect"] },
63
+ "status": { "type": "string", "enum": ["passed", "failed", "uncertain", "skipped"] },
64
+ "expected_outcome": { "type": "string" },
65
+ "actual_outcome": { "type": "string" },
66
+ "evidence": { "type": "string" }
67
+ }
68
+ }
69
+ },
70
+ "human_verification": {
71
+ "type": "array",
72
+ "items": {
73
+ "type": "object",
74
+ "required": ["name", "test", "expected", "why_human"],
75
+ "properties": {
76
+ "name": { "type": "string" },
77
+ "test": { "type": "string" },
78
+ "expected": { "type": "string" },
79
+ "why_human": { "type": "string" }
80
+ }
81
+ }
82
+ },
83
+ "gaps": {
84
+ "type": "array",
85
+ "items": {
86
+ "type": "object",
87
+ "required": ["truth", "status", "reason"],
88
+ "properties": {
89
+ "truth": { "type": "string" },
90
+ "status": { "type": "string" },
91
+ "reason": { "type": "string" }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,289 @@
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 { parseAgentYaml, createAgentLoader, AgentNotFoundError, AgentParseError } from "./agent-spec.ts";
7
+ import { compileAgentSpec } from "./step-shared.ts";
8
+ import { createTemplateEnv } from "./template.ts";
9
+
10
+ describe("parseAgentYaml", () => {
11
+ it("parses YAML agent spec with all fields", (t) => {
12
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
13
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
14
+
15
+ const specPath = path.join(tmpDir, "test.agent.yaml");
16
+ fs.writeFileSync(specPath, `
17
+ name: test-agent
18
+ role: sensor
19
+ description: A test agent
20
+ model: anthropic/claude-sonnet-4-6
21
+ thinking: low
22
+ tools: [read, bash]
23
+ extensions: [./ext.ts]
24
+ skills: [coding]
25
+ input:
26
+ type: object
27
+ required: [path]
28
+ properties:
29
+ path: { type: string }
30
+ output:
31
+ format: json
32
+ schema: test.schema.json
33
+ file: result.json
34
+ prompt:
35
+ system: test/system.md
36
+ task: test/task.md
37
+ `);
38
+
39
+ const spec = parseAgentYaml(specPath);
40
+ assert.strictEqual(spec.name, "test-agent");
41
+ assert.strictEqual(spec.role, "sensor");
42
+ assert.strictEqual(spec.description, "A test agent");
43
+ assert.strictEqual(spec.model, "anthropic/claude-sonnet-4-6");
44
+ assert.strictEqual(spec.thinking, "low");
45
+ assert.deepStrictEqual(spec.tools, ["read", "bash"]);
46
+ assert.deepStrictEqual(spec.extensions, ["./ext.ts"]);
47
+ assert.deepStrictEqual(spec.skills, ["coding"]);
48
+ assert.strictEqual(spec.promptTemplate, "test/system.md");
49
+ assert.strictEqual(spec.taskTemplate, "test/task.md");
50
+ assert.strictEqual(spec.outputFormat, "json");
51
+ assert.strictEqual(spec.outputSchema, "test.schema.json");
52
+ assert.strictEqual(spec.output, "result.json");
53
+ assert.deepStrictEqual(spec.inputSchema?.required, ["path"]);
54
+ });
55
+
56
+ it("uses filename as name when name field is missing", (t) => {
57
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
58
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
59
+
60
+ const specPath = path.join(tmpDir, "my-agent.agent.yaml");
61
+ fs.writeFileSync(specPath, "tools: [read]\n");
62
+
63
+ const spec = parseAgentYaml(specPath);
64
+ assert.strictEqual(spec.name, "my-agent");
65
+ });
66
+
67
+ it("throws AgentParseError for malformed YAML", (t) => {
68
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
69
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
70
+
71
+ const specPath = path.join(tmpDir, "bad.agent.yaml");
72
+ fs.writeFileSync(specPath, `
73
+ name: bad-agent
74
+ tools: [read
75
+ this is not valid yaml: {{{}}}
76
+ `);
77
+
78
+ assert.throws(
79
+ () => parseAgentYaml(specPath),
80
+ (err: any) => {
81
+ assert.strictEqual(err.name, "AgentParseError");
82
+ assert.strictEqual(err.agentName, "bad");
83
+ assert.strictEqual(err.filePath, specPath);
84
+ assert.ok(err.message.includes("bad"), "error message should include agent name");
85
+ assert.ok(err.message.includes(specPath), "error message should include file path");
86
+ return true;
87
+ },
88
+ );
89
+ });
90
+
91
+ it("throws AgentParseError for empty file", (t) => {
92
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
93
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
94
+
95
+ const specPath = path.join(tmpDir, "empty.agent.yaml");
96
+ fs.writeFileSync(specPath, "");
97
+
98
+ assert.throws(
99
+ () => parseAgentYaml(specPath),
100
+ (err: any) => {
101
+ assert.strictEqual(err.name, "AgentParseError");
102
+ assert.ok(err.message.includes("empty") || err.message.includes("does not contain"));
103
+ return true;
104
+ },
105
+ );
106
+ });
107
+
108
+ it("throws AgentParseError for file with only document markers", (t) => {
109
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
110
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
111
+
112
+ const specPath = path.join(tmpDir, "marker.agent.yaml");
113
+ fs.writeFileSync(specPath, "---\n---\n");
114
+
115
+ assert.throws(
116
+ () => parseAgentYaml(specPath),
117
+ (err: any) => {
118
+ assert.strictEqual(err.name, "AgentParseError");
119
+ return true;
120
+ },
121
+ );
122
+ });
123
+
124
+ it("throws AgentParseError for scalar YAML content", (t) => {
125
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
126
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
127
+
128
+ const specPath = path.join(tmpDir, "scalar.agent.yaml");
129
+ fs.writeFileSync(specPath, "just a string, not a mapping\n");
130
+
131
+ assert.throws(
132
+ () => parseAgentYaml(specPath),
133
+ (err: any) => {
134
+ assert.strictEqual(err.name, "AgentParseError");
135
+ assert.ok(err.message.includes("does not contain a YAML mapping"));
136
+ return true;
137
+ },
138
+ );
139
+ });
140
+ });
141
+
142
+ describe("createAgentLoader", () => {
143
+ it("finds .agent.yaml specs in .pi/agents/", (t) => {
144
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
145
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
146
+
147
+ const agentDir = path.join(tmpDir, ".pi", "agents");
148
+ fs.mkdirSync(agentDir, { recursive: true });
149
+ fs.writeFileSync(path.join(agentDir, "test.agent.yaml"), "name: test\nrole: sensor\ntools: [read]\n");
150
+
151
+ const loader = createAgentLoader(tmpDir);
152
+ const spec = loader("test");
153
+ assert.strictEqual(spec.name, "test");
154
+ assert.strictEqual(spec.role, "sensor");
155
+ assert.deepStrictEqual(spec.tools, ["read"]);
156
+ });
157
+
158
+ it("finds specs in builtin directory", (t) => {
159
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
160
+ const builtinDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-builtin-"));
161
+ t.after(() => {
162
+ fs.rmSync(tmpDir, { recursive: true, force: true });
163
+ fs.rmSync(builtinDir, { recursive: true, force: true });
164
+ });
165
+
166
+ fs.writeFileSync(path.join(builtinDir, "builtin.agent.yaml"), "name: builtin\nrole: quality\n");
167
+
168
+ const loader = createAgentLoader(tmpDir, builtinDir);
169
+ const spec = loader("builtin");
170
+ assert.strictEqual(spec.name, "builtin");
171
+ assert.strictEqual(spec.role, "quality");
172
+ });
173
+
174
+ it("throws AgentNotFoundError for missing agent", (t) => {
175
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
176
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
177
+
178
+ const loader = createAgentLoader(tmpDir, tmpDir); // builtinDir also empty
179
+ assert.throws(
180
+ () => loader("nonexistent-agent"),
181
+ (err: any) => {
182
+ assert.strictEqual(err.name, "AgentNotFoundError");
183
+ assert.strictEqual(err.agentName, "nonexistent-agent");
184
+ assert.ok(Array.isArray(err.searchPaths));
185
+ assert.ok(err.searchPaths.length >= 2, "should list at least project and user search paths");
186
+ assert.ok(err.message.includes("nonexistent-agent"), "error message should include agent name");
187
+ assert.ok(err.message.includes("Searched"), "error message should indicate search was performed");
188
+ return true;
189
+ },
190
+ );
191
+ });
192
+
193
+ it("throws AgentParseError when found file has invalid YAML", (t) => {
194
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-"));
195
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
196
+
197
+ const agentDir = path.join(tmpDir, ".pi", "agents");
198
+ fs.mkdirSync(agentDir, { recursive: true });
199
+ fs.writeFileSync(path.join(agentDir, "broken.agent.yaml"), "name: broken\ntools: [read\n");
200
+
201
+ const loader = createAgentLoader(tmpDir, tmpDir);
202
+ assert.throws(
203
+ () => loader("broken"),
204
+ (err: any) => {
205
+ assert.strictEqual(err.name, "AgentParseError");
206
+ assert.strictEqual(err.agentName, "broken");
207
+ assert.ok(err.filePath.includes("broken.agent.yaml"));
208
+ return true;
209
+ },
210
+ );
211
+ });
212
+ });
213
+
214
+ describe("compileAgentSpec", () => {
215
+ it("renders plain text system prompt unchanged", () => {
216
+ const env = createTemplateEnv("/nonexistent");
217
+ const spec = { name: "test", systemPrompt: "Plain text prompt." };
218
+ const result = compileAgentSpec(spec, {}, env);
219
+ assert.strictEqual(result.systemPrompt, "Plain text prompt.");
220
+ });
221
+
222
+ it("renders system prompt with template variables", () => {
223
+ const env = createTemplateEnv("/nonexistent");
224
+ const spec = { name: "test", systemPrompt: "Analyze {{ path }} for {{ concern }}." };
225
+ const result = compileAgentSpec(spec, { path: "src/index.ts", concern: "security" }, env);
226
+ assert.strictEqual(result.systemPrompt, "Analyze src/index.ts for security.");
227
+ });
228
+
229
+ it("renders system prompt from file template", (t) => {
230
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-"));
231
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
232
+
233
+ fs.writeFileSync(path.join(tmpDir, "system.md"), "You analyze {{ focus }} code.");
234
+
235
+ const env = createTemplateEnv("/nonexistent", tmpDir);
236
+ const spec = { name: "test", promptTemplate: "system.md" };
237
+ const result = compileAgentSpec(spec, { focus: "TypeScript" }, env);
238
+ assert.strictEqual(result.systemPrompt, "You analyze TypeScript code.");
239
+ assert.strictEqual(result.promptTemplate, undefined);
240
+ });
241
+
242
+ it("renders task template from typed input", (t) => {
243
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-"));
244
+ t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
245
+
246
+ fs.writeFileSync(path.join(tmpDir, "task.md"), "Fix {{ diagnosis.rootCause }} in {{ diagnosis.file }}.");
247
+
248
+ const env = createTemplateEnv("/nonexistent", tmpDir);
249
+ const spec = { name: "test", taskTemplate: "task.md" };
250
+ const result = compileAgentSpec(spec, { diagnosis: { rootCause: "null ref", file: "auth.ts" } }, env);
251
+ assert.strictEqual(result.taskTemplate, "Fix null ref in auth.ts.");
252
+ });
253
+
254
+ it("returns spec unchanged when no templateEnv provided", () => {
255
+ const spec = { name: "test", systemPrompt: "Hello {{ name }}", taskTemplate: "task.md" };
256
+ const result = compileAgentSpec(spec, { name: "world" });
257
+ assert.strictEqual(result.systemPrompt, "Hello {{ name }}");
258
+ assert.strictEqual(result.taskTemplate, "task.md");
259
+ });
260
+ });
261
+
262
+ describe("verifier agent spec", () => {
263
+ const builtinDir = path.resolve(import.meta.dirname, "..", "agents");
264
+
265
+ it("parses verifier.agent.yaml correctly", () => {
266
+ const specPath = path.join(builtinDir, "verifier.agent.yaml");
267
+ const spec = parseAgentYaml(specPath);
268
+ assert.strictEqual(spec.name, "verifier");
269
+ assert.strictEqual(spec.role, "quality");
270
+ assert.strictEqual(spec.description, "Verifies step outputs against declared intent and success criteria");
271
+ assert.strictEqual(spec.model, undefined); // model comes from .project/model-config.json, not agent spec
272
+ assert.strictEqual(spec.outputFormat, "json");
273
+ assert.strictEqual(spec.outputSchema, "schemas/verifier-output.schema.json");
274
+ assert.strictEqual(spec.taskTemplate, "verifier/task.md");
275
+ });
276
+
277
+ it("has correct tools for verification", () => {
278
+ const specPath = path.join(builtinDir, "verifier.agent.yaml");
279
+ const spec = parseAgentYaml(specPath);
280
+ assert.deepStrictEqual(spec.tools, ["read", "bash", "grep", "find"]);
281
+ });
282
+
283
+ it("output schema path resolves to a valid file", () => {
284
+ const specPath = path.join(builtinDir, "verifier.agent.yaml");
285
+ const spec = parseAgentYaml(specPath);
286
+ const schemaPath = path.resolve(path.dirname(specPath), "..", "schemas", spec.outputSchema!.replace("schemas/", ""));
287
+ assert.ok(fs.existsSync(schemaPath), `Schema file not found at ${schemaPath}`);
288
+ });
289
+ });