@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.
- package/CHANGELOG.md +28 -0
- package/README.md +169 -0
- package/agents/audit-fixer.agent.yaml +18 -0
- package/agents/code-explorer.agent.yaml +11 -0
- package/agents/decomposer.agent.yaml +51 -0
- package/agents/investigator.agent.yaml +49 -0
- package/agents/pattern-analyzer.agent.yaml +21 -0
- package/agents/phase-author.agent.yaml +16 -0
- package/agents/plan-decomposer.agent.yaml +12 -0
- package/agents/quality-analyzer.agent.yaml +21 -0
- package/agents/researcher.agent.yaml +38 -0
- package/agents/spec-implementer.agent.yaml +12 -0
- package/agents/structure-analyzer.agent.yaml +21 -0
- package/agents/synthesizer.agent.yaml +23 -0
- package/agents/verifier.agent.yaml +12 -0
- package/package.json +40 -0
- package/schemas/decomposition-specs.schema.json +50 -0
- package/schemas/execution-results.schema.json +50 -0
- package/schemas/investigation-findings.schema.json +45 -0
- package/schemas/pattern-analysis.schema.json +43 -0
- package/schemas/plan-breakdown.schema.json +22 -0
- package/schemas/quality-analysis.schema.json +35 -0
- package/schemas/research-findings.schema.json +44 -0
- package/schemas/structure-analysis.schema.json +42 -0
- package/schemas/synthesis.schema.json +33 -0
- package/schemas/verifier-output.schema.json +96 -0
- package/src/agent-spec.test.ts +289 -0
- package/src/agent-spec.ts +122 -0
- package/src/block-validation.test.ts +350 -0
- package/src/checkpoint.test.ts +237 -0
- package/src/checkpoint.ts +139 -0
- package/src/completion.test.ts +143 -0
- package/src/completion.ts +68 -0
- package/src/dag.test.ts +350 -0
- package/src/dag.ts +219 -0
- package/src/dispatch.test.ts +473 -0
- package/src/dispatch.ts +353 -0
- package/src/expression.test.ts +353 -0
- package/src/expression.ts +332 -0
- package/src/format.test.ts +80 -0
- package/src/format.ts +41 -0
- package/src/graduated-failure.test.ts +556 -0
- package/src/index.test.ts +114 -0
- package/src/index.ts +488 -0
- package/src/loop.test.ts +549 -0
- package/src/output.test.ts +213 -0
- package/src/output.ts +70 -0
- package/src/parallel-integration.test.ts +175 -0
- package/src/resume.test.ts +192 -0
- package/src/state.test.ts +192 -0
- package/src/state.ts +253 -0
- package/src/step-agent.test.ts +327 -0
- package/src/step-agent.ts +178 -0
- package/src/step-command.test.ts +330 -0
- package/src/step-command.ts +132 -0
- package/src/step-foreach.test.ts +647 -0
- package/src/step-foreach.ts +148 -0
- package/src/step-gate.test.ts +185 -0
- package/src/step-gate.ts +116 -0
- package/src/step-loop.test.ts +626 -0
- package/src/step-loop.ts +323 -0
- package/src/step-parallel.test.ts +475 -0
- package/src/step-parallel.ts +168 -0
- package/src/step-pause.test.ts +30 -0
- package/src/step-pause.ts +27 -0
- package/src/step-shared.test.ts +355 -0
- package/src/step-shared.ts +157 -0
- package/src/step-transform.test.ts +155 -0
- package/src/step-transform.ts +47 -0
- package/src/template-integration.test.ts +72 -0
- package/src/template.test.ts +162 -0
- package/src/template.ts +92 -0
- package/src/test-helpers.ts +51 -0
- package/src/tui.test.ts +355 -0
- package/src/tui.ts +182 -0
- package/src/types.ts +195 -0
- package/src/verifier-schema.test.ts +226 -0
- package/src/workflow-discovery.test.ts +105 -0
- package/src/workflow-discovery.ts +113 -0
- package/src/workflow-executor.test.ts +1530 -0
- package/src/workflow-executor.ts +810 -0
- package/src/workflow-sdk.test.ts +238 -0
- package/src/workflow-sdk.ts +262 -0
- package/src/workflow-spec.test.ts +576 -0
- package/src/workflow-spec.ts +471 -0
- package/templates/analyzers/base-analyzer.md +9 -0
- package/templates/analyzers/macros.md +1 -0
- package/templates/analyzers/patterns-task.md +11 -0
- package/templates/analyzers/patterns.md +15 -0
- package/templates/analyzers/quality-task.md +11 -0
- package/templates/analyzers/quality.md +15 -0
- package/templates/analyzers/structure-task.md +17 -0
- package/templates/analyzers/structure.md +15 -0
- package/templates/audit-fixer/task.md +67 -0
- package/templates/decomposer/task.md +56 -0
- package/templates/explorer/system.md +3 -0
- package/templates/explorer/task.md +9 -0
- package/templates/investigator/task.md +30 -0
- package/templates/phase-author/task.md +74 -0
- package/templates/plan-decomposer/task.md +46 -0
- package/templates/researcher/task.md +26 -0
- package/templates/spec-implementer/task.md +53 -0
- package/templates/synthesizer/system.md +3 -0
- package/templates/synthesizer/task.md +38 -0
- package/templates/verifier/task.md +57 -0
- package/workflows/create-phase.workflow.yaml +67 -0
- package/workflows/do-gap.workflow.yaml +153 -0
- package/workflows/fix-audit.workflow.yaml +217 -0
- package/workflows/gap-to-phase.workflow.yaml +98 -0
- package/workflows/parallel-analysis.workflow.yaml +62 -0
- package/workflows/parallel-explicit.workflow.yaml +42 -0
- package/workflows/pausable-analysis.workflow.yaml +44 -0
- package/workflows/resumable-analysis.workflow.yaml +42 -0
- package/workflows/self-implement.workflow.yaml +63 -0
- 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
|
+
});
|