@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.
- package/CHANGELOG.md +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +172 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +156 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +57 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +125 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +726 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +438 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +464 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +84 -0
- package/skills/pi-workflows/SKILL.md +247 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +74 -0
- package/workflows/create-handoff.workflow.yaml +58 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +55 -0
- package/workflows/plan-from-requirements.workflow.yaml +51 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- package/src/workflow-spec.ts +0 -471
|
@@ -26,7 +26,7 @@ steps:
|
|
|
26
26
|
path: ${{ input.path }}
|
|
27
27
|
output:
|
|
28
28
|
format: json
|
|
29
|
-
schema: schemas/structure-analysis.schema.json
|
|
29
|
+
schema: ../schemas/structure-analysis.schema.json
|
|
30
30
|
|
|
31
31
|
analyze-quality:
|
|
32
32
|
agent: quality-analyzer
|
|
@@ -35,7 +35,7 @@ steps:
|
|
|
35
35
|
path: ${{ input.path }}
|
|
36
36
|
output:
|
|
37
37
|
format: json
|
|
38
|
-
schema: schemas/quality-analysis.schema.json
|
|
38
|
+
schema: ../schemas/quality-analysis.schema.json
|
|
39
39
|
|
|
40
40
|
analyze-patterns:
|
|
41
41
|
agent: pattern-analyzer
|
|
@@ -44,7 +44,7 @@ steps:
|
|
|
44
44
|
path: ${{ input.path }}
|
|
45
45
|
output:
|
|
46
46
|
format: json
|
|
47
|
-
schema: schemas/pattern-analysis.schema.json
|
|
47
|
+
schema: ../schemas/pattern-analysis.schema.json
|
|
48
48
|
|
|
49
49
|
synthesize:
|
|
50
50
|
agent: synthesizer
|
|
@@ -54,7 +54,7 @@ steps:
|
|
|
54
54
|
patterns: ${{ steps.analyze-patterns.output }}
|
|
55
55
|
output:
|
|
56
56
|
format: json
|
|
57
|
-
schema: schemas/synthesis.schema.json
|
|
57
|
+
schema: ../schemas/synthesis.schema.json
|
|
58
58
|
|
|
59
59
|
completion:
|
|
60
60
|
message: |
|
package/src/agent-spec.test.ts
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { 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
|
-
});
|
package/src/agent-spec.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent spec loading — YAML specs are the source of truth.
|
|
3
|
-
*
|
|
4
|
-
* Agent specs are declarative YAML files that define typed functions:
|
|
5
|
-
* InputSchema → OutputSchema, with template references for prompt
|
|
6
|
-
* composition. The .md that pi consumes is compiled at dispatch time
|
|
7
|
-
* from spec + templates + typed input. It exists in memory only.
|
|
8
|
-
*
|
|
9
|
-
* Search order (first match wins):
|
|
10
|
-
* 1. .pi/agents/<name>.agent.yaml (project)
|
|
11
|
-
* 2. ~/.pi/agent/agents/<name>.agent.yaml (user)
|
|
12
|
-
* 3. <package>/agents/<name>.agent.yaml (builtin)
|
|
13
|
-
*/
|
|
14
|
-
import fs from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
|
|
17
|
-
/** Check if a prompt.system value looks like a template file path vs inline text. */
|
|
18
|
-
function isTemplatePath(value: string | undefined): boolean {
|
|
19
|
-
if (!value) return false;
|
|
20
|
-
return value.endsWith(".md") || value.endsWith(".txt") || (value.includes("/") && !value.includes("\n"));
|
|
21
|
-
}
|
|
22
|
-
import os from "node:os";
|
|
23
|
-
import { parse as parseYaml } from "yaml";
|
|
24
|
-
import type { AgentSpec } from "./types.ts";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Thrown when an agent spec file is not found in any search path.
|
|
28
|
-
*/
|
|
29
|
-
export class AgentNotFoundError extends Error {
|
|
30
|
-
public readonly agentName: string;
|
|
31
|
-
public readonly searchPaths: string[];
|
|
32
|
-
|
|
33
|
-
constructor(agentName: string, searchPaths: string[]) {
|
|
34
|
-
const pathList = searchPaths.map(p => ` - ${p}`).join("\n");
|
|
35
|
-
super(`Agent '${agentName}' not found. Searched:\n${pathList}`);
|
|
36
|
-
this.name = "AgentNotFoundError";
|
|
37
|
-
this.agentName = agentName;
|
|
38
|
-
this.searchPaths = searchPaths;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Thrown when an agent spec file exists but cannot be read or parsed.
|
|
44
|
-
*/
|
|
45
|
-
export class AgentParseError extends Error {
|
|
46
|
-
public readonly agentName: string;
|
|
47
|
-
public readonly filePath: string;
|
|
48
|
-
public readonly cause: Error;
|
|
49
|
-
|
|
50
|
-
constructor(agentName: string, filePath: string, cause: Error) {
|
|
51
|
-
super(`Agent '${agentName}' at ${filePath}: ${cause.message}`);
|
|
52
|
-
this.name = "AgentParseError";
|
|
53
|
-
this.agentName = agentName;
|
|
54
|
-
this.filePath = filePath;
|
|
55
|
-
this.cause = cause;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Parse a YAML agent spec file into an AgentSpec.
|
|
61
|
-
*/
|
|
62
|
-
export function parseAgentYaml(filePath: string): AgentSpec {
|
|
63
|
-
const name = path.basename(filePath, ".agent.yaml");
|
|
64
|
-
|
|
65
|
-
let content: string;
|
|
66
|
-
try {
|
|
67
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
68
|
-
} catch (err) {
|
|
69
|
-
throw new AgentParseError(name, filePath, err instanceof Error ? err : new Error(String(err)));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let spec: any;
|
|
73
|
-
try {
|
|
74
|
-
spec = parseYaml(content);
|
|
75
|
-
} catch (err) {
|
|
76
|
-
throw new AgentParseError(name, filePath, err instanceof Error ? err : new Error(String(err)));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Handle null/undefined from parsing empty file or non-mapping YAML
|
|
80
|
-
if (!spec || typeof spec !== "object") {
|
|
81
|
-
throw new AgentParseError(name, filePath, new Error("File is empty or does not contain a YAML mapping"));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
name: spec.name || name,
|
|
86
|
-
description: spec.description,
|
|
87
|
-
role: spec.role,
|
|
88
|
-
model: spec.model,
|
|
89
|
-
thinking: spec.thinking,
|
|
90
|
-
tools: spec.tools,
|
|
91
|
-
extensions: spec.extensions,
|
|
92
|
-
skills: spec.skills,
|
|
93
|
-
output: spec.output?.file,
|
|
94
|
-
promptTemplate: isTemplatePath(spec.prompt?.system) ? spec.prompt?.system : undefined,
|
|
95
|
-
systemPrompt: isTemplatePath(spec.prompt?.system) ? undefined : spec.prompt?.system,
|
|
96
|
-
taskTemplate: spec.prompt?.task,
|
|
97
|
-
inputSchema: spec.input,
|
|
98
|
-
outputFormat: spec.output?.format,
|
|
99
|
-
outputSchema: spec.output?.schema,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Create an agent loader that finds .agent.yaml specs.
|
|
105
|
-
*/
|
|
106
|
-
export function createAgentLoader(cwd: string, builtinDir?: string): (name: string) => AgentSpec {
|
|
107
|
-
const defaultBuiltinDir = builtinDir ?? path.resolve(import.meta.dirname, "..", "agents");
|
|
108
|
-
|
|
109
|
-
return (name: string): AgentSpec => {
|
|
110
|
-
const searchPaths = [
|
|
111
|
-
path.join(cwd, ".pi", "agents", `${name}.agent.yaml`),
|
|
112
|
-
path.join(os.homedir(), ".pi", "agent", "agents", `${name}.agent.yaml`),
|
|
113
|
-
path.join(defaultBuiltinDir, `${name}.agent.yaml`),
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
for (const p of searchPaths) {
|
|
117
|
-
if (fs.existsSync(p)) return parseAgentYaml(p);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
throw new AgentNotFoundError(name, searchPaths);
|
|
121
|
-
};
|
|
122
|
-
}
|