@davidorex/pi-workflows 0.1.0 → 0.2.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/README.md +21 -5
- package/package.json +1 -1
- package/src/checkpoint.test.ts +4 -4
- package/src/checkpoint.ts +3 -2
- package/src/dispatch.test.ts +8 -6
- package/src/graduated-failure.test.ts +1 -1
- package/src/index.ts +33 -5
- package/src/loop.test.ts +8 -6
- package/src/state.ts +8 -7
- package/src/types.ts +1 -1
- package/src/workflow-discovery.test.ts +4 -4
- package/src/workflow-discovery.ts +3 -2
- package/src/workflow-executor.test.ts +10 -8
- package/src/workflows-dir.ts +2 -0
package/README.md
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
# pi-workflows
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Schema-driven workflow orchestration for [Pi](https://github.com/badlogic/pi-mono).
|
|
4
|
+
|
|
5
|
+
Data flows through workflows as typed JSON, not strings. Each agent step declares an `output.schema` — the agent's output is validated against a JSON Schema before it's accepted into the pipeline. The expression engine passes typed fields between steps (`${{ steps.investigate.output.findings }}`), not raw text. Templates compose typed data into agent prompts, so agents receive structured context. The entire pipeline is schema-governed: **schema defines shape → agent produces to shape → validator enforces shape → next step consumes typed fields.**
|
|
6
|
+
|
|
7
|
+
The schemas in `schemas/` (investigation-findings, decomposition-specs, execution-results, etc.) are the typed contracts between workflow steps. They're not metadata — they're the enforcement boundary.
|
|
4
8
|
|
|
5
9
|
## Install
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
|
-
pi install pi-
|
|
12
|
+
pi install npm:@davidorex/pi-project # peer dependency
|
|
13
|
+
pi install npm:@davidorex/pi-workflows
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or install both at once: `pi install npm:@davidorex/pi-project-workflows`
|
|
17
|
+
|
|
18
|
+
## Getting Started
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
/workflow init
|
|
9
22
|
```
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
Creates `.workflows/` for run state. Workflow YAML specs are discovered automatically from the package's bundled workflows — no setup needed to start running them.
|
|
12
25
|
|
|
13
26
|
## What It Does
|
|
14
27
|
|
|
15
28
|
pi-workflows replaces ad-hoc agent chaining with composable, typed workflow orchestration. Workflows are YAML specs. Steps run as subprocesses (`pi --mode json`) with their own context windows. The main conversation is the control plane; workflows are subordinate.
|
|
16
29
|
|
|
30
|
+
Workflows consume project blocks as typed input via `readBlock()` — structured data, not raw files. When workflow agents write back to project blocks, pi-project's schema validation enforces the shape at write time. The two extensions form a typed loop: project state → workflow input → agent output → validated project state.
|
|
31
|
+
|
|
17
32
|
**Tool registered:**
|
|
18
33
|
- `workflow` — run a named workflow with typed input
|
|
19
34
|
|
|
20
35
|
**Commands registered:**
|
|
36
|
+
- `/workflow init` — scaffold `.workflows/` directory for run state
|
|
21
37
|
- `/workflow list` — discover and select a workflow to run
|
|
22
38
|
- `/workflow run <name> [--input '<json>']` — execute a workflow
|
|
23
39
|
- `/workflow resume <name>` — resume an incomplete run from checkpoint
|
|
@@ -28,7 +44,7 @@ pi-workflows replaces ad-hoc agent chaining with composable, typed workflow orch
|
|
|
28
44
|
|
|
29
45
|
## Workflow Spec Format
|
|
30
46
|
|
|
31
|
-
Workflows are `.workflow.yaml` files discovered from `.
|
|
47
|
+
Workflows are `.workflow.yaml` files discovered from `.workflows/` (project), `~/.pi/agent/workflows/` (user), or the package's `workflows/` directory (builtin).
|
|
32
48
|
|
|
33
49
|
```yaml
|
|
34
50
|
name: my-workflow
|
|
@@ -143,7 +159,7 @@ declaredSchemaRefs(spec): string[]
|
|
|
143
159
|
- DAG planner infers parallelism from `${{ steps.X }}` references — no manual dependency declaration
|
|
144
160
|
- Agent specs are `.agent.yaml` only. Compiled to prompts via Nunjucks at dispatch time.
|
|
145
161
|
- State persisted atomically after each step (tmp + rename). State write failure is fatal.
|
|
146
|
-
- Three-tier resource search: project `.pi/` > user `~/.pi/agent/` > package builtin
|
|
162
|
+
- Three-tier resource search: project (`.workflows/`, `.pi/agents/`, `.pi/templates/`) > user `~/.pi/agent/` > package builtin
|
|
147
163
|
- `completion` field controls what message is sent back to the main conversation after a workflow finishes
|
|
148
164
|
|
|
149
165
|
## For LLMs
|
package/package.json
CHANGED
package/src/checkpoint.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ describe("findIncompleteRun", () => {
|
|
|
18
18
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
|
|
19
19
|
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
20
20
|
|
|
21
|
-
const runDir = path.join(tmpDir, ".
|
|
21
|
+
const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
|
|
22
22
|
fs.mkdirSync(runDir, { recursive: true });
|
|
23
23
|
writeState(runDir, { input: {}, steps: {}, status: "completed" });
|
|
24
24
|
|
|
@@ -30,7 +30,7 @@ describe("findIncompleteRun", () => {
|
|
|
30
30
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
|
|
31
31
|
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
32
32
|
|
|
33
|
-
const runDir = path.join(tmpDir, ".
|
|
33
|
+
const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
|
|
34
34
|
fs.mkdirSync(runDir, { recursive: true });
|
|
35
35
|
|
|
36
36
|
const state: ExecutionState = {
|
|
@@ -57,7 +57,7 @@ describe("findIncompleteRun", () => {
|
|
|
57
57
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
|
|
58
58
|
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
59
59
|
|
|
60
|
-
const runDir = path.join(tmpDir, ".
|
|
60
|
+
const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
|
|
61
61
|
fs.mkdirSync(runDir, { recursive: true });
|
|
62
62
|
|
|
63
63
|
const state: ExecutionState = {
|
|
@@ -88,7 +88,7 @@ describe("findIncompleteRun", () => {
|
|
|
88
88
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
|
|
89
89
|
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
90
90
|
|
|
91
|
-
const runsBase = path.join(tmpDir, ".
|
|
91
|
+
const runsBase = path.join(tmpDir, ".workflows", "runs", "test", "runs");
|
|
92
92
|
|
|
93
93
|
// Older run (completed)
|
|
94
94
|
const oldDir = path.join(runsBase, "test-20260314-100000-aaaa");
|
package/src/checkpoint.ts
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import type { ExecutionState, WorkflowSpec } from "./types.ts";
|
|
10
10
|
import { readState } from "./state.ts";
|
|
11
|
+
import { WORKFLOWS_DIR } from "./workflows-dir.ts";
|
|
11
12
|
|
|
12
13
|
export interface IncompleteRun {
|
|
13
14
|
runId: string;
|
|
@@ -21,14 +22,14 @@ export interface IncompleteRun {
|
|
|
21
22
|
/**
|
|
22
23
|
* Find the most recent incomplete run for a workflow.
|
|
23
24
|
*
|
|
24
|
-
* Scans .
|
|
25
|
+
* Scans .workflows/runs/<name>/runs/ for state.json files
|
|
25
26
|
* with status "running" or "failed". Returns the most recent one
|
|
26
27
|
* (by directory name, which encodes timestamp).
|
|
27
28
|
*
|
|
28
29
|
* Returns null if no incomplete runs exist.
|
|
29
30
|
*/
|
|
30
31
|
export function findIncompleteRun(cwd: string, workflowName: string): IncompleteRun | null {
|
|
31
|
-
const runsDir = path.join(cwd,
|
|
32
|
+
const runsDir = path.join(cwd, WORKFLOWS_DIR, "runs", workflowName, "runs");
|
|
32
33
|
if (!fs.existsSync(runsDir)) return null;
|
|
33
34
|
|
|
34
35
|
const entries = fs.readdirSync(runsDir)
|
package/src/dispatch.test.ts
CHANGED
|
@@ -6,13 +6,15 @@ import fs from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
|
|
9
|
-
// Skip integration tests
|
|
9
|
+
// Skip integration tests unless RUN_INTEGRATION=1 and pi is available
|
|
10
10
|
let hasPi = false;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
if (process.env.RUN_INTEGRATION === "1") {
|
|
12
|
+
try {
|
|
13
|
+
const { execSync } = await import("node:child_process");
|
|
14
|
+
execSync("pi --version", { stdio: "ignore" });
|
|
15
|
+
hasPi = true;
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
16
18
|
|
|
17
19
|
// ── Unit tests: extractText ──
|
|
18
20
|
|
|
@@ -199,7 +199,7 @@ describe("graduated failure retry", () => {
|
|
|
199
199
|
beforeEach(() => {
|
|
200
200
|
tmpDir = makeTmpDir();
|
|
201
201
|
// Create minimal run dir structure
|
|
202
|
-
const runDir = path.join(tmpDir, ".
|
|
202
|
+
const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs");
|
|
203
203
|
fs.mkdirSync(runDir, { recursive: true });
|
|
204
204
|
});
|
|
205
205
|
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { executeWorkflow, requestPause } from "./workflow-executor.ts";
|
|
|
7
7
|
import { findIncompleteRun, validateResumeCompatibility, formatIncompleteRun } from "./checkpoint.ts";
|
|
8
8
|
import { createAgentLoader } from "./agent-spec.ts";
|
|
9
9
|
import type { WorkflowResult } from "./types.ts";
|
|
10
|
+
import { WORKFLOWS_DIR } from "./workflows-dir.ts";
|
|
10
11
|
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
11
13
|
|
|
12
14
|
import { Key } from "@mariozechner/pi-tui";
|
|
13
15
|
import { Type } from "@sinclair/typebox";
|
|
@@ -126,7 +128,7 @@ function formatToolResult(result: WorkflowResult): string {
|
|
|
126
128
|
async function handleList(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
127
129
|
const workflows = discoverWorkflows(ctx.cwd);
|
|
128
130
|
if (workflows.length === 0) {
|
|
129
|
-
ctx.ui.notify("No workflows found in .
|
|
131
|
+
ctx.ui.notify("No workflows found in .workflows/ or ~/.pi/agent/workflows/", "info");
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
132
134
|
|
|
@@ -333,6 +335,30 @@ async function handleResume(rawArgs: string, ctx: ExtensionCommandContext, pi: E
|
|
|
333
335
|
}
|
|
334
336
|
}
|
|
335
337
|
|
|
338
|
+
/**
|
|
339
|
+
* /workflow init — scaffold .workflows/ directory for user workflow specs
|
|
340
|
+
* and run state. Idempotent.
|
|
341
|
+
*/
|
|
342
|
+
function handleWorkflowInit(ctx: ExtensionCommandContext): void {
|
|
343
|
+
const workflowsDir = path.join(ctx.cwd, WORKFLOWS_DIR);
|
|
344
|
+
const runsDir = path.join(workflowsDir, "runs");
|
|
345
|
+
|
|
346
|
+
const created: string[] = [];
|
|
347
|
+
|
|
348
|
+
for (const dir of [workflowsDir, runsDir]) {
|
|
349
|
+
if (!fs.existsSync(dir)) {
|
|
350
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
351
|
+
created.push(path.relative(ctx.cwd, dir) + "/");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (created.length > 0) {
|
|
356
|
+
ctx.ui.notify(`Workflows initialized: created ${created.join(", ")}`, "info");
|
|
357
|
+
} else {
|
|
358
|
+
ctx.ui.notify("Workflows already initialized — nothing to do.", "info");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
336
362
|
// ── Extension factory ───────────────────────────────────────────────────────
|
|
337
363
|
|
|
338
364
|
const extension = (pi: ExtensionAPI) => {
|
|
@@ -341,7 +367,7 @@ const extension = (pi: ExtensionAPI) => {
|
|
|
341
367
|
pi.registerTool({
|
|
342
368
|
name: "workflow",
|
|
343
369
|
label: "Workflow",
|
|
344
|
-
description: "Run a named workflow with typed input. Discovers workflows from .
|
|
370
|
+
description: "Run a named workflow with typed input. Discovers workflows from .workflows/ and ~/.pi/agent/workflows/.",
|
|
345
371
|
promptSnippet: "Run a multi-step workflow with typed data flow between agents",
|
|
346
372
|
parameters: Type.Object({
|
|
347
373
|
workflow: Type.String({ description: "Name of the workflow to run" }),
|
|
@@ -403,7 +429,7 @@ const extension = (pi: ExtensionAPI) => {
|
|
|
403
429
|
pi.registerCommand("workflow", {
|
|
404
430
|
description: "List and run workflows",
|
|
405
431
|
getArgumentCompletions: (prefix: string) => {
|
|
406
|
-
const subcommands = ["run", "list", "resume"];
|
|
432
|
+
const subcommands = ["init", "run", "list", "resume"];
|
|
407
433
|
return subcommands
|
|
408
434
|
.filter((s) => s.startsWith(prefix))
|
|
409
435
|
.map((s) => ({ value: s, label: s }));
|
|
@@ -415,14 +441,16 @@ const extension = (pi: ExtensionAPI) => {
|
|
|
415
441
|
const subcommand = spaceIdx === -1 ? trimmed || "list" : trimmed.slice(0, spaceIdx);
|
|
416
442
|
const rest = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1);
|
|
417
443
|
|
|
418
|
-
if (subcommand === "
|
|
444
|
+
if (subcommand === "init") {
|
|
445
|
+
handleWorkflowInit(ctx);
|
|
446
|
+
} else if (subcommand === "list") {
|
|
419
447
|
await handleList(ctx, pi);
|
|
420
448
|
} else if (subcommand === "run") {
|
|
421
449
|
await handleRun(rest, ctx, pi);
|
|
422
450
|
} else if (subcommand === "resume") {
|
|
423
451
|
await handleResume(rest, ctx, pi);
|
|
424
452
|
} else {
|
|
425
|
-
ctx.ui.notify(`Unknown subcommand: ${subcommand}. Use: list, run, resume`, "warning");
|
|
453
|
+
ctx.ui.notify(`Unknown subcommand: ${subcommand}. Use: init, list, run, resume`, "warning");
|
|
426
454
|
}
|
|
427
455
|
},
|
|
428
456
|
});
|
package/src/loop.test.ts
CHANGED
|
@@ -17,13 +17,15 @@ function defaultOptions(tmpDir?: string) {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// Skip
|
|
20
|
+
// Skip integration tests unless RUN_INTEGRATION=1 and pi is available
|
|
21
21
|
let hasPi = false;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
if (process.env.RUN_INTEGRATION === "1") {
|
|
23
|
+
try {
|
|
24
|
+
const { execSync } = await import("node:child_process");
|
|
25
|
+
execSync("pi --version", { stdio: "ignore" });
|
|
26
|
+
hasPi = true;
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
27
29
|
|
|
28
30
|
describe("loop steps", () => {
|
|
29
31
|
it("breaks on gate pass", async (t: any) => {
|
package/src/state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import type { ExecutionState, WorkflowResult, WorkflowSpec, StepResult, StepUsage } from "./types.ts";
|
|
5
5
|
import { formatDuration, formatCost } from "./format.ts";
|
|
6
|
+
import { WORKFLOWS_DIR } from "./workflows-dir.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Generate a unique run ID.
|
|
@@ -24,11 +25,11 @@ export function generateRunId(workflowName: string): string {
|
|
|
24
25
|
/**
|
|
25
26
|
* Initialize the run directory structure.
|
|
26
27
|
* Creates:
|
|
27
|
-
* .
|
|
28
|
-
* .
|
|
29
|
-
* .
|
|
28
|
+
* .workflows/runs/<workflowName>/runs/<runId>/
|
|
29
|
+
* .workflows/runs/<workflowName>/runs/<runId>/sessions/
|
|
30
|
+
* .workflows/runs/<workflowName>/runs/<runId>/outputs/
|
|
30
31
|
*
|
|
31
|
-
* Each workflow owns a directory under .
|
|
32
|
+
* Each workflow owns a directory under .workflows/runs/<name>/.
|
|
32
33
|
* Run state goes in runs/<runId>/; artifacts live at the workflow level.
|
|
33
34
|
*
|
|
34
35
|
* @param cwd - project root
|
|
@@ -37,7 +38,7 @@ export function generateRunId(workflowName: string): string {
|
|
|
37
38
|
* @returns absolute path to the run directory
|
|
38
39
|
*/
|
|
39
40
|
export function initRunDir(cwd: string, workflowName: string, runId: string): string {
|
|
40
|
-
const runDir = path.join(cwd,
|
|
41
|
+
const runDir = path.join(cwd, WORKFLOWS_DIR, "runs", workflowName, "runs", runId);
|
|
41
42
|
fs.mkdirSync(path.join(runDir, "sessions"), { recursive: true });
|
|
42
43
|
fs.mkdirSync(path.join(runDir, "outputs"), { recursive: true });
|
|
43
44
|
return runDir;
|
|
@@ -49,10 +50,10 @@ export function initRunDir(cwd: string, workflowName: string, runId: string): st
|
|
|
49
50
|
*
|
|
50
51
|
* @param cwd - project root
|
|
51
52
|
* @param workflowName - name of the workflow
|
|
52
|
-
* @returns absolute path to .
|
|
53
|
+
* @returns absolute path to .workflows/runs/<workflowName>/
|
|
53
54
|
*/
|
|
54
55
|
export function getWorkflowDir(cwd: string, workflowName: string): string {
|
|
55
|
-
return path.join(cwd,
|
|
56
|
+
return path.join(cwd, WORKFLOWS_DIR, "runs", workflowName);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
package/src/types.ts
CHANGED
|
@@ -159,7 +159,7 @@ export interface WorkflowResult {
|
|
|
159
159
|
output?: unknown; // final workflow output (last step's output, or explicit)
|
|
160
160
|
totalUsage: StepUsage;
|
|
161
161
|
totalDurationMs: number;
|
|
162
|
-
runDir: string; // absolute path to .
|
|
162
|
+
runDir: string; // absolute path to .workflows/runs/<name>/runs/<run-id>/
|
|
163
163
|
artifacts?: Record<string, string>; // name → absolute path of written artifact files
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -11,7 +11,7 @@ const NO_BUILTINS = path.join(os.tmpdir(), "wf-no-builtins-nonexistent");
|
|
|
11
11
|
describe("discoverWorkflows", () => {
|
|
12
12
|
it("discovers workflows from project directory", () => {
|
|
13
13
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-test-"));
|
|
14
|
-
const wfDir = path.join(tmpDir, ".
|
|
14
|
+
const wfDir = path.join(tmpDir, ".workflows");
|
|
15
15
|
fs.mkdirSync(wfDir, { recursive: true });
|
|
16
16
|
fs.writeFileSync(
|
|
17
17
|
path.join(wfDir, "test.workflow.yaml"),
|
|
@@ -35,7 +35,7 @@ describe("discoverWorkflows", () => {
|
|
|
35
35
|
|
|
36
36
|
it("skips invalid specs with warning (no throw)", () => {
|
|
37
37
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-test-"));
|
|
38
|
-
const wfDir = path.join(tmpDir, ".
|
|
38
|
+
const wfDir = path.join(tmpDir, ".workflows");
|
|
39
39
|
fs.mkdirSync(wfDir, { recursive: true });
|
|
40
40
|
fs.writeFileSync(path.join(wfDir, "bad.workflow.yaml"), "not: valid: workflow");
|
|
41
41
|
fs.writeFileSync(
|
|
@@ -52,7 +52,7 @@ describe("discoverWorkflows", () => {
|
|
|
52
52
|
|
|
53
53
|
it("project specs shadow user specs with same name", () => {
|
|
54
54
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-test-"));
|
|
55
|
-
const projectDir = path.join(tmpDir, ".
|
|
55
|
+
const projectDir = path.join(tmpDir, ".workflows");
|
|
56
56
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
57
57
|
fs.writeFileSync(
|
|
58
58
|
path.join(projectDir, "shared.workflow.yaml"),
|
|
@@ -83,7 +83,7 @@ describe("discoverWorkflows", () => {
|
|
|
83
83
|
describe("findWorkflow", () => {
|
|
84
84
|
it("finds a workflow by name", () => {
|
|
85
85
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-test-"));
|
|
86
|
-
const wfDir = path.join(tmpDir, ".
|
|
86
|
+
const wfDir = path.join(tmpDir, ".workflows");
|
|
87
87
|
fs.mkdirSync(wfDir, { recursive: true });
|
|
88
88
|
fs.writeFileSync(
|
|
89
89
|
path.join(wfDir, "bugfix.workflow.yaml"),
|
|
@@ -3,12 +3,13 @@ import path from "node:path";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import type { WorkflowSpec } from "./types.ts";
|
|
5
5
|
import { parseWorkflowSpec } from "./workflow-spec.ts";
|
|
6
|
+
import { WORKFLOWS_DIR } from "./workflows-dir.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Discover all workflow specs from project, user, and builtin directories.
|
|
9
10
|
*
|
|
10
11
|
* Scans (highest priority first):
|
|
11
|
-
* 1. .
|
|
12
|
+
* 1. .workflows/ (project-level, source: "project")
|
|
12
13
|
* 2. ~/.pi/agent/workflows/ (user-level, source: "user")
|
|
13
14
|
* 3. <package>/workflows/ (builtin workflows, source: "user")
|
|
14
15
|
*
|
|
@@ -20,7 +21,7 @@ import { parseWorkflowSpec } from "./workflow-spec.ts";
|
|
|
20
21
|
* skipped with a warning (logged to stderr), not thrown.
|
|
21
22
|
*/
|
|
22
23
|
export function discoverWorkflows(cwd: string, builtinDir?: string): WorkflowSpec[] {
|
|
23
|
-
const projectDir = path.join(cwd,
|
|
24
|
+
const projectDir = path.join(cwd, WORKFLOWS_DIR);
|
|
24
25
|
const userDir = path.join(os.homedir(), ".pi", "agent", "workflows");
|
|
25
26
|
const demoDir = builtinDir ?? path.resolve(import.meta.dirname, "..", "workflows");
|
|
26
27
|
|
|
@@ -7,13 +7,15 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
|
|
10
|
-
// Skip
|
|
10
|
+
// Skip integration tests unless RUN_INTEGRATION=1 and pi is available
|
|
11
11
|
let hasPi = false;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
if (process.env.RUN_INTEGRATION === "1") {
|
|
13
|
+
try {
|
|
14
|
+
const { execSync } = await import("node:child_process");
|
|
15
|
+
execSync("pi --version", { stdio: "ignore" });
|
|
16
|
+
hasPi = true;
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
17
19
|
|
|
18
20
|
describe("executeWorkflow", { skip: !hasPi ? "pi not available" : undefined }, () => {
|
|
19
21
|
it("runs a single-step workflow", async () => {
|
|
@@ -529,8 +531,8 @@ describe("artifacts", () => {
|
|
|
529
531
|
|
|
530
532
|
assert.strictEqual(result.status, "completed");
|
|
531
533
|
assert.ok(result.artifacts);
|
|
532
|
-
// Artifact path should be under .
|
|
533
|
-
const workflowDir = path.join(tmpDir, ".
|
|
534
|
+
// Artifact path should be under .workflows/runs/<workflow-name>/
|
|
535
|
+
const workflowDir = path.join(tmpDir, ".workflows", "runs", "test-artifact-rel");
|
|
534
536
|
const expectedPath = path.resolve(workflowDir, "latest.json");
|
|
535
537
|
assert.strictEqual(result.artifacts!.report, expectedPath);
|
|
536
538
|
assert.ok(fs.existsSync(expectedPath));
|