@juicesharp/rpiv-workflow 1.14.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/LICENSE +21 -0
- package/README.md +449 -0
- package/api.ts +557 -0
- package/audit.ts +217 -0
- package/built-ins.ts +65 -0
- package/command.ts +137 -0
- package/docs/cover.png +0 -0
- package/docs/cover.svg +120 -0
- package/docs/workflow-authoring.md +629 -0
- package/docs/workflow-basics.md +122 -0
- package/docs-protocol.ts +106 -0
- package/fanout.ts +96 -0
- package/host.ts +97 -0
- package/index.ts +230 -0
- package/internal-utils.ts +69 -0
- package/internal.ts +27 -0
- package/layers.ts +33 -0
- package/lifecycle.ts +274 -0
- package/load/cache.test.ts +82 -0
- package/load/cache.ts +40 -0
- package/load/index.ts +159 -0
- package/load/merge.ts +136 -0
- package/load/normalize.ts +73 -0
- package/load/paths.ts +32 -0
- package/load/resolve-default.ts +43 -0
- package/load/shape-guards.test.ts +74 -0
- package/load/shape-guards.ts +42 -0
- package/messages.ts +185 -0
- package/outcomes/collectors/directory-path.test.ts +64 -0
- package/outcomes/collectors/directory-path.ts +40 -0
- package/outcomes/collectors/index.ts +21 -0
- package/outcomes/collectors/tool-call.test.ts +110 -0
- package/outcomes/collectors/tool-call.ts +63 -0
- package/outcomes/collectors/transcript-path.test.ts +70 -0
- package/outcomes/collectors/transcript-path.ts +53 -0
- package/outcomes/collectors/union.test.ts +59 -0
- package/outcomes/collectors/union.ts +55 -0
- package/outcomes/collectors/url.test.ts +67 -0
- package/outcomes/collectors/url.ts +45 -0
- package/outcomes/collectors/workspace-diff.test.ts +107 -0
- package/outcomes/collectors/workspace-diff.ts +123 -0
- package/outcomes/git-commit.test.ts +194 -0
- package/outcomes/git-commit.ts +192 -0
- package/outcomes/index.ts +22 -0
- package/outcomes/parsers/index.ts +11 -0
- package/outcomes/parsers/json-body.test.ts +80 -0
- package/outcomes/parsers/json-body.ts +50 -0
- package/outcomes/side-effect.ts +26 -0
- package/output-spec.ts +170 -0
- package/output.ts +98 -0
- package/package.json +83 -0
- package/preview.ts +120 -0
- package/routing.ts +79 -0
- package/runner/chain-advance.ts +185 -0
- package/runner/index.ts +7 -0
- package/runner/runner.ts +356 -0
- package/runner/script-stage.ts +240 -0
- package/runner/stage-lifecycle.ts +447 -0
- package/sessions/extraction.ts +297 -0
- package/sessions/index.ts +7 -0
- package/sessions/sessions.ts +269 -0
- package/sessions/spawn.ts +135 -0
- package/state/index.ts +27 -0
- package/state/paths.ts +46 -0
- package/state/reads.ts +190 -0
- package/state/state.ts +115 -0
- package/state/writes.ts +58 -0
- package/transcript.ts +156 -0
- package/triggers.ts +27 -0
- package/typebox-adapter.ts +48 -0
- package/types.ts +237 -0
- package/validate-output.ts +120 -0
- package/validate-workflow.ts +491 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Workflow Basics
|
|
2
|
+
|
|
3
|
+
A workflow chains Pi skills into a typed multi-stage graph with audited JSONL state, predicate routing, and per-stage output validation. Workflows are **skill-agnostic** — they name skills by their installed name, and the runner dispatches `/skill:<name>` via Pi's native skill loader.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Running workflows](#running-workflows)
|
|
8
|
+
- [File structure](#file-structure)
|
|
9
|
+
- [Layer merging](#layer-merging)
|
|
10
|
+
- [Config files](#config-files)
|
|
11
|
+
- [Pack files](#pack-files)
|
|
12
|
+
- [Example](#example)
|
|
13
|
+
|
|
14
|
+
## Running workflows
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
/wf # Preview every loaded workflow
|
|
18
|
+
/wf <name> # Preview one workflow's stage graph
|
|
19
|
+
/wf <name> <input> # Run a workflow with <input> piped to the start stage
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Running `/wf` without arguments shows a list of every loaded workflow and its stages. Running `/wf <name>` without input shows that workflow's stage graph in detail. The `<input>` string becomes the start stage's prompt.
|
|
23
|
+
|
|
24
|
+
## File structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
<cwd>/.rpiv-workflow/
|
|
28
|
+
├── workflows.config.ts # The project's workflow config (hand-edited)
|
|
29
|
+
└── workflows/ # Pack files (installable bundles)
|
|
30
|
+
├── my-pipeline.ts
|
|
31
|
+
└── ship.ts
|
|
32
|
+
|
|
33
|
+
~/.config/rpiv-workflow/
|
|
34
|
+
├── workflows.config.ts # User-level config
|
|
35
|
+
└── workflows/ # User-level packs
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every workflow file is TypeScript, loaded via `jiti` (no build step required). Import the authoring DSL from `@juicesharp/rpiv-workflow`:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { defineWorkflow, produces, acts, gate, gt, eq } from "@juicesharp/rpiv-workflow";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Layer merging
|
|
45
|
+
|
|
46
|
+
The loader merges workflows from five layers. Each later layer overrides earlier by workflow name:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
built-in (registered by sibling packages like rpiv-pi)
|
|
50
|
+
← user packs (~/.config/rpiv-workflow/workflows/*.ts, alpha-sorted)
|
|
51
|
+
← user config (~/.config/rpiv-workflow/workflows.config.ts)
|
|
52
|
+
← project packs (<cwd>/.rpiv-workflow/workflows/*.ts, alpha-sorted)
|
|
53
|
+
← project config (<cwd>/.rpiv-workflow/workflows.config.ts)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Within a layer, the config file wins by workflow name over pack files. Only the config file may set the `default` workflow (the one `/wf <input>` runs without specifying a name). Defaults cascade: `project config > user config > first registered workflow`.
|
|
57
|
+
|
|
58
|
+
## Config files
|
|
59
|
+
|
|
60
|
+
The config file (`workflows.config.ts`) is the one TypeScript file you hand-edit. It accepts three default-export shapes:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// 1. A single Workflow
|
|
64
|
+
import { defineWorkflow, produces, acts } from "@juicesharp/rpiv-workflow";
|
|
65
|
+
export default defineWorkflow({
|
|
66
|
+
name: "ship",
|
|
67
|
+
start: "implement",
|
|
68
|
+
stages: { implement: acts(), commit: acts() },
|
|
69
|
+
edges: { implement: "commit", commit: "stop" },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 2. A Workflow[] with a single entry
|
|
73
|
+
export default [/* one workflow */];
|
|
74
|
+
|
|
75
|
+
// 3. The envelope form — required when shipping multiple workflows
|
|
76
|
+
export default {
|
|
77
|
+
workflows: [/* many */],
|
|
78
|
+
default: "ship", // which one `/wf <input>` runs without a name
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Pack files
|
|
83
|
+
|
|
84
|
+
Pack files (`workflows/*.ts`) are installable bundles others can drop in. They accept only `Workflow | Workflow[]`. Packs **cannot** set `default` — that lives in the config file.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// workflows/my-pipeline.ts
|
|
88
|
+
import { defineWorkflow, produces, acts } from "@juicesharp/rpiv-workflow";
|
|
89
|
+
export default defineWorkflow({
|
|
90
|
+
name: "my-pipeline",
|
|
91
|
+
start: "research",
|
|
92
|
+
stages: { research: produces({ outcome: myOutcome }), implement: acts() },
|
|
93
|
+
edges: { research: "implement", implement: "stop" },
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This is what makes installable workflow packs safe: a pack contributes new workflows without overriding the user's default.
|
|
98
|
+
|
|
99
|
+
## Example
|
|
100
|
+
|
|
101
|
+
A minimal workflow that chains two skills:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { defineWorkflow, produces, acts } from "@juicesharp/rpiv-workflow";
|
|
105
|
+
|
|
106
|
+
export default defineWorkflow({
|
|
107
|
+
name: "review-and-ship",
|
|
108
|
+
start: "code-review",
|
|
109
|
+
stages: {
|
|
110
|
+
"code-review": produces({ outcome: myOutcome }),
|
|
111
|
+
commit: acts(),
|
|
112
|
+
},
|
|
113
|
+
edges: {
|
|
114
|
+
"code-review": "commit",
|
|
115
|
+
commit: "stop",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Save this as `.rpiv-workflow/workflows.config.ts` in your project, then run `/wf review-and-ship implement auth feature`.
|
|
121
|
+
|
|
122
|
+
For the full DSL reference (all stage factories, routing, outcomes, validators), see [workflow-authoring.md](./workflow-authoring.md).
|
package/docs-protocol.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompt protocol for rpiv-workflow documentation delivery.
|
|
3
|
+
*
|
|
4
|
+
* Prepends a documentation reference block to the system prompt every turn
|
|
5
|
+
* via `before_agent_start`, following the same pattern as rpiv-args'
|
|
6
|
+
* skill-invocation protocol (`packages/rpiv-args/args.ts:439`). The block
|
|
7
|
+
* tells the agent where to find rpiv-workflow's authoring docs and when to
|
|
8
|
+
* read them — the agent reads the docs on-demand via the `read` tool.
|
|
9
|
+
*
|
|
10
|
+
* The protocol is small (~200 bytes) and identical every turn, so it
|
|
11
|
+
* benefits from Pi's prompt caching (same amortization as
|
|
12
|
+
* `SKILL_INVOCATION_PROTOCOL` in rpiv-args).
|
|
13
|
+
*
|
|
14
|
+
* This module has zero Pi type imports — the host interface declares only
|
|
15
|
+
* the `on("before_agent_start", ...)` shape needed for registration.
|
|
16
|
+
* At runtime, Pi passes the full ExtensionAPI which structurally satisfies
|
|
17
|
+
* this interface.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Path resolution
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Absolute path to the rpiv-workflow package root. Resolved from this
|
|
29
|
+
* file's location via `import.meta.url` — works in both development
|
|
30
|
+
* (monorepo source) and production (installed npm package).
|
|
31
|
+
*/
|
|
32
|
+
const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const DOCS_DIR = join(PACKAGE_ROOT, "docs");
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Protocol
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* System prompt protocol prepended every turn. Cached after first build
|
|
41
|
+
* so bytes are identical every turn → prompt-cache hit after turn 1
|
|
42
|
+
* (same posture as rpiv-args' `SKILL_INVOCATION_PROTOCOL`).
|
|
43
|
+
*
|
|
44
|
+
* Mirrors Pi's own "When asked about:" routing table pattern — path
|
|
45
|
+
* references + routing instructions, no inline content.
|
|
46
|
+
*/
|
|
47
|
+
let protocolCache: string | undefined;
|
|
48
|
+
|
|
49
|
+
export function getDocsProtocol(): string {
|
|
50
|
+
if (protocolCache) return protocolCache;
|
|
51
|
+
|
|
52
|
+
protocolCache = [
|
|
53
|
+
"",
|
|
54
|
+
"rpiv-workflow documentation (read only when the user asks about creating, modifying, or troubleshooting workflows):",
|
|
55
|
+
`- Workflow basics: ${join(DOCS_DIR, "workflow-basics.md")}`,
|
|
56
|
+
`- Authoring DSL reference: ${join(DOCS_DIR, "workflow-authoring.md")}`,
|
|
57
|
+
"- When asked about workflows, read the relevant docs file(s) before generating any workflow code",
|
|
58
|
+
"- Generated workflow files must pass validateWorkflow() — verify before writing",
|
|
59
|
+
"",
|
|
60
|
+
].join("\n");
|
|
61
|
+
|
|
62
|
+
return protocolCache;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Host interface
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Minimal host interface for docs-protocol registration. Declares only the
|
|
71
|
+
* `on` shape needed for `before_agent_start`. At runtime, Pi passes the
|
|
72
|
+
* full ExtensionAPI which structurally satisfies this.
|
|
73
|
+
*
|
|
74
|
+
* Not exported as part of rpiv-workflow's public type surface — internal
|
|
75
|
+
* to the extension wiring in `index.ts`.
|
|
76
|
+
*/
|
|
77
|
+
export interface DocsProtocolHost {
|
|
78
|
+
on(event: "before_agent_start", handler: (event: { systemPrompt: string }) => { systemPrompt: string }): void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Handler
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `before_agent_start` handler. Read-then-prepend — never replace,
|
|
87
|
+
* replacement clobbers prior extensions (rpiv-args, rpiv-pi guidance, etc.).
|
|
88
|
+
*
|
|
89
|
+
* Exported for testing.
|
|
90
|
+
*/
|
|
91
|
+
export function handleBeforeAgentStart(event: { systemPrompt: string }): { systemPrompt: string } {
|
|
92
|
+
return { systemPrompt: getDocsProtocol() + event.systemPrompt };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Registration
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Register the docs-protocol `before_agent_start` hook with the host.
|
|
101
|
+
* Called from the extension's default export alongside
|
|
102
|
+
* `registerWorkflowCommand`.
|
|
103
|
+
*/
|
|
104
|
+
export function registerDocsProtocol(host: DocsProtocolHost): void {
|
|
105
|
+
host.on("before_agent_start", handleBeforeAgentStart);
|
|
106
|
+
}
|
package/fanout.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fanout iteration. When a stage opts in via `StageDef.fanout`, the runner
|
|
3
|
+
* calls the user's `FanoutFn` to get a list of units and iterates one Pi
|
|
4
|
+
* session per unit. This module owns the iteration loop; `runner.ts`
|
|
5
|
+
* injects its primitives via `FanoutDeps` so the module never imports
|
|
6
|
+
* back (cycle-free).
|
|
7
|
+
*
|
|
8
|
+
* No markdown regex, no per-convention counter, no cap — rpiv-workflow
|
|
9
|
+
* stays convention-agnostic. Consumers (rpiv-pi etc.) own the FanoutFn
|
|
10
|
+
* body and any safety bounds it needs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { FanoutUnit } from "./api.js";
|
|
14
|
+
import { buildLifecycleContext, skillStageRef } from "./lifecycle.js";
|
|
15
|
+
import { MSG_STAGE_COMPLETE, STATUS_FANOUT_UNIT, STATUS_KEY } from "./messages.js";
|
|
16
|
+
import type { FanoutSession, RunContext, RunnerCtx } from "./types.js";
|
|
17
|
+
|
|
18
|
+
export interface FanoutDeps {
|
|
19
|
+
runFanoutSession: (ctx: RunnerCtx, session: FanoutSession) => Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Resume the chain after the fanout node's units finish. Receives the
|
|
22
|
+
* fanout node's name so the routing layer can look up the outgoing
|
|
23
|
+
* edge from it.
|
|
24
|
+
*/
|
|
25
|
+
advanceAfter: (curCtx: RunnerCtx, completedName: string, completedIdx: number, run: RunContext) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `skill` is the bundled skill body (threaded by the runner), not the node
|
|
30
|
+
* name. Aliased nodes (e.g. `implement-after-revise` invoking `implement`)
|
|
31
|
+
* tag unit rows + prompts with the skill body so audit consumers don't see
|
|
32
|
+
* two labels for the same work. Caller verifies node + fanout shape before
|
|
33
|
+
* invoking (see `runStage`).
|
|
34
|
+
*
|
|
35
|
+
* `currentName` is the fanout node's name in the workflow — passed to
|
|
36
|
+
* `advanceAfter` once the final unit completes so the routing layer can
|
|
37
|
+
* look up the outgoing edge from it.
|
|
38
|
+
*
|
|
39
|
+
* `units` was already produced by the user's `FanoutFn`; the iteration loop
|
|
40
|
+
* walks it in order. `p` is the 1-based index into `units` for the next
|
|
41
|
+
* session to run — the continuation-style self-call increments it.
|
|
42
|
+
*/
|
|
43
|
+
export async function runFanout(
|
|
44
|
+
curCtx: RunnerCtx,
|
|
45
|
+
stageIdx: number,
|
|
46
|
+
currentName: string,
|
|
47
|
+
skill: string,
|
|
48
|
+
p: number,
|
|
49
|
+
units: readonly FanoutUnit[],
|
|
50
|
+
run: RunContext,
|
|
51
|
+
deps: FanoutDeps,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const { cwd, runId, totalStages, state } = run;
|
|
54
|
+
|
|
55
|
+
if (p > units.length) {
|
|
56
|
+
curCtx.ui.notify(MSG_STAGE_COMPLETE(skill), "info");
|
|
57
|
+
await deps.advanceAfter(curCtx, currentName, stageIdx, run);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const unit = units[p - 1]!;
|
|
62
|
+
curCtx.ui.setStatus(STATUS_KEY, STATUS_FANOUT_UNIT(stageIdx + 1, totalStages, skill, unit.label));
|
|
63
|
+
|
|
64
|
+
const lifecycleCtx = buildLifecycleContext({
|
|
65
|
+
cwd,
|
|
66
|
+
runId,
|
|
67
|
+
workflow: run.workflow.name,
|
|
68
|
+
totalStages: run.totalStages,
|
|
69
|
+
trigger: run.trigger,
|
|
70
|
+
state,
|
|
71
|
+
});
|
|
72
|
+
await run.lifecycle.fire(
|
|
73
|
+
curCtx,
|
|
74
|
+
"onFanoutUnitStart",
|
|
75
|
+
skillStageRef(currentName, stageIdx + 1, skill),
|
|
76
|
+
unit,
|
|
77
|
+
p,
|
|
78
|
+
lifecycleCtx,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await deps.runFanoutSession(curCtx, {
|
|
82
|
+
cwd,
|
|
83
|
+
runId,
|
|
84
|
+
state,
|
|
85
|
+
prompt: `/skill:${skill} ${unit.prompt}`,
|
|
86
|
+
stageName: currentName,
|
|
87
|
+
skill,
|
|
88
|
+
lifecycle: run.lifecycle,
|
|
89
|
+
runIdentity: { workflow: run.workflow.name, totalStages: run.totalStages, trigger: run.trigger },
|
|
90
|
+
unitIndex: p,
|
|
91
|
+
label: unit.label,
|
|
92
|
+
id: unit.id,
|
|
93
|
+
stageIndex: stageIdx,
|
|
94
|
+
onSuccess: (freshCtx) => runFanout(freshCtx, stageIdx, currentName, skill, p + 1, units, run, deps),
|
|
95
|
+
});
|
|
96
|
+
}
|
package/host.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host ports — the contract the workflow runtime needs from its host
|
|
3
|
+
* environment, expressed in workflow-domain vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* The package never re-exports `@earendil-works/pi-coding-agent` types
|
|
6
|
+
* from its public surface. Pi's `ExtensionAPI` / `ExtensionCommandContext`
|
|
7
|
+
* / `ReplacedSessionContext` structurally satisfy these ports, so
|
|
8
|
+
* embedders pass their Pi handles directly without casting; consumers
|
|
9
|
+
* wanting to drive the runtime from a non-Pi adapter implement these two
|
|
10
|
+
* interfaces.
|
|
11
|
+
*
|
|
12
|
+
* - `WorkflowHost` — registry-level host (default-export ctor + continue-policy sender).
|
|
13
|
+
* - `WorkflowContext` — per-command ctx passed into `runWorkflow`, also the
|
|
14
|
+
* replacement ctx delivered to `newSession`'s `withSession`
|
|
15
|
+
* callback. `sendUserMessage` is optional at the type
|
|
16
|
+
* level (the outer command ctx may not carry one) but
|
|
17
|
+
* the runtime guarantees it is present inside
|
|
18
|
+
* `withSession`.
|
|
19
|
+
*
|
|
20
|
+
* Compile-time tripwire: `host.test.ts` asserts Pi's concrete types
|
|
21
|
+
* extend these ports. If Pi's API drifts (a method renames, a signature
|
|
22
|
+
* tightens), `npm run check` fails immediately on that file.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Registry-level host. Default-exported function receives this; the
|
|
27
|
+
* runner also uses it for continue-policy stages (sends into the
|
|
28
|
+
* already-streaming agent) and for skill-registration preflight.
|
|
29
|
+
*
|
|
30
|
+
* The three methods we touch on Pi's `ExtensionAPI`. Anything beyond
|
|
31
|
+
* these is invisible to the runtime.
|
|
32
|
+
*/
|
|
33
|
+
export interface WorkflowHost {
|
|
34
|
+
/** Register a slash command. Used by the `/wf` entry point. */
|
|
35
|
+
registerCommand(
|
|
36
|
+
name: string,
|
|
37
|
+
options: {
|
|
38
|
+
description?: string;
|
|
39
|
+
handler: (args: string, ctx: WorkflowContext) => Promise<void>;
|
|
40
|
+
},
|
|
41
|
+
): void;
|
|
42
|
+
/**
|
|
43
|
+
* Send a user message into the active agent stream. Used by the
|
|
44
|
+
* continue-policy session handler.
|
|
45
|
+
*
|
|
46
|
+
* Pi declares this `void` at the type level but returns a Promise at
|
|
47
|
+
* runtime; we declare `void | Promise<void>` so `await` is safe in
|
|
48
|
+
* either world.
|
|
49
|
+
*/
|
|
50
|
+
sendUserMessage(content: string): void | Promise<void>;
|
|
51
|
+
/** Enumerate currently registered slash commands. Used by skill-registration preflight. */
|
|
52
|
+
getCommands(): ReadonlyArray<{ name: string; source: string }>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Per-command host ctx. Embedders hand this to `runWorkflow`; the
|
|
57
|
+
* runner threads it (and any replacement ctx returned by `newSession`)
|
|
58
|
+
* through stages.
|
|
59
|
+
*
|
|
60
|
+
* Exhaustive list of members the runtime touches — adding any reach
|
|
61
|
+
* outside this list is a port-widening decision, not an oversight.
|
|
62
|
+
*
|
|
63
|
+
* `sendUserMessage` is declared optional because the outer command ctx
|
|
64
|
+
* Pi delivers to a `/wf` handler does not carry one — only the
|
|
65
|
+
* replacement ctx inside `newSession`'s `withSession` callback does. The
|
|
66
|
+
* runtime guarantees it is present in that callback; callers outside
|
|
67
|
+
* `withSession` must not rely on it.
|
|
68
|
+
*/
|
|
69
|
+
export interface WorkflowContext {
|
|
70
|
+
cwd: string;
|
|
71
|
+
hasUI: boolean;
|
|
72
|
+
ui: {
|
|
73
|
+
notify(message: string, level?: "info" | "warning" | "error"): void;
|
|
74
|
+
setStatus(key: string, text: string | undefined): void;
|
|
75
|
+
};
|
|
76
|
+
sessionManager: {
|
|
77
|
+
getBranch(): unknown;
|
|
78
|
+
};
|
|
79
|
+
isIdle(): boolean;
|
|
80
|
+
waitForIdle(): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Open a fresh session and run `withSession` on the replacement ctx.
|
|
83
|
+
* Returns `{ cancelled: true }` if the host declined to spawn (user
|
|
84
|
+
* dismissed the swap, etc.). `cancelled: false` implies the outer
|
|
85
|
+
* ctx is now invalidated — all further work runs on the replacement
|
|
86
|
+
* delivered to `withSession`.
|
|
87
|
+
*/
|
|
88
|
+
newSession(options: {
|
|
89
|
+
withSession: (replacement: WorkflowContext) => Promise<void>;
|
|
90
|
+
}): Promise<{ cancelled: boolean }>;
|
|
91
|
+
/**
|
|
92
|
+
* Present on the replacement ctx delivered inside `withSession`; not
|
|
93
|
+
* present on the outer command ctx. The runtime narrows internally
|
|
94
|
+
* before calling.
|
|
95
|
+
*/
|
|
96
|
+
sendUserMessage?(content: string): Promise<void>;
|
|
97
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rpiv-workflow — Pi extension entry point.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `/wf` slash command and (optionally) exposes the workflow
|
|
5
|
+
* runtime as a programmatic API for sibling packages that want to
|
|
6
|
+
* contribute built-in workflows via `registerBuiltIns(...)`.
|
|
7
|
+
*
|
|
8
|
+
* Skill-agnostic: the runner sends `/skill:<name>` via Pi's native skill
|
|
9
|
+
* dispatch — workflows can name any skill installed in Pi's search path
|
|
10
|
+
* (`~/.pi/agent/skills/`, `<cwd>/.pi/skills/`, or settings-declared
|
|
11
|
+
* `skillPaths[]`). This package ships ZERO built-in workflows. Bundles
|
|
12
|
+
* like `@juicesharp/rpiv-pi` opt in by calling `registerBuiltIns(...)`
|
|
13
|
+
* from their own extension entry.
|
|
14
|
+
*
|
|
15
|
+
* ─── Public surface, grouped by audience ────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* 1. Authoring DSL — `./api.js`, `./predicates.js`, `./typebox-adapter.js`
|
|
18
|
+
* What a `workflows.config.ts` author imports to declare a workflow:
|
|
19
|
+
* `defineWorkflow`, `produces`, `acts`, `terminal`, `defineRoute`, `gate`,
|
|
20
|
+
* `Workflow`, `StageDef`, `EdgeFn`, `EdgeTarget`, `EdgeContext`,
|
|
21
|
+
* `StageSchema`, `StageKind`, `SessionPolicy`, `OutputSpec`,
|
|
22
|
+
* `READS_DATA`, the runtime-mirror `*_VALUES` arrays, the
|
|
23
|
+
* `gt`/`gte`/`lt`/`lte`/`eq` predicate helpers, and `typeboxSchema`
|
|
24
|
+
* (the TypeBox adapter).
|
|
25
|
+
*
|
|
26
|
+
* 2. Runner (programmatic embedders) — `./runner/index.js`, `./host.js`
|
|
27
|
+
* Drive a workflow from outside `/wf`: `runWorkflow`,
|
|
28
|
+
* `RunWorkflowOptions`, `RunWorkflowResult`. Embedders type their
|
|
29
|
+
* host handles against `WorkflowHost` / `WorkflowContext` (the host
|
|
30
|
+
* ports) — Pi's `ExtensionAPI` / `ExtensionCommandContext` /
|
|
31
|
+
* `ReplacedSessionContext` structurally satisfy them, so the
|
|
32
|
+
* values pass through without casting.
|
|
33
|
+
*
|
|
34
|
+
* 3. Loader (programmatic embedders) — `./load/index.js`
|
|
35
|
+
* Materialise the merged workflow registry: `loadWorkflows`,
|
|
36
|
+
* `LoadedWorkflows`, `Issue`, `LoadIssue`, `ConfigLayer`,
|
|
37
|
+
* `OverlayPaths`, `projectOverlayPaths`, `userOverlayPaths`.
|
|
38
|
+
*
|
|
39
|
+
* 4. Built-in registry (sibling packages) — `./built-ins.js`
|
|
40
|
+
* Contribute workflows to the lowest config layer:
|
|
41
|
+
* `registerBuiltIns`. (`getBuiltIns` is test-only and lives on
|
|
42
|
+
* `@juicesharp/rpiv-workflow/internal`.)
|
|
43
|
+
*
|
|
44
|
+
* 5. Output envelope + bundled outcomes — `./output.js`,
|
|
45
|
+
* `./outcomes/index.js`, `./handle.js`
|
|
46
|
+
* Inter-stage data channel (`Output<K, D>`, `OutputMeta`,
|
|
47
|
+
* `Artifact`, `ArtifactHandle` + constructors `fs`/`url`/
|
|
48
|
+
* `opaque`/`inline`/`handleToString`) + bundled outcomes
|
|
49
|
+
* (`sideEffectOutcome`, `gitCommitOutcome`, `GitCommitData`,
|
|
50
|
+
* `gitHeadSnapshot`, `GitHeadSnapshot`) + the bundled
|
|
51
|
+
* collector/parser catalog wireable into any custom `OutputSpec`:
|
|
52
|
+
* - collectors: `transcriptPathCollector` (regex over assistant
|
|
53
|
+
* text), `toolCallCollector` (universal tool_use observer),
|
|
54
|
+
* `workspaceDiffCollector` (git status diff pre/post),
|
|
55
|
+
* `gitCommitCollector` (commit detection), the wrappers
|
|
56
|
+
* `directoryPathCollector` / `urlCollector`, plus composition
|
|
57
|
+
* `unionCollectors` and the empty-list primitive `noopCollector`.
|
|
58
|
+
* - parsers: `jsonBodyParser` (parses primary fs body),
|
|
59
|
+
* `gitCommitParser`.
|
|
60
|
+
* The `.rpiv/artifacts/<bucket>/<file>.md` outcome + the
|
|
61
|
+
* markdown-frontmatter parser live in `@juicesharp/rpiv-pi`
|
|
62
|
+
* (`rpivArtifactMdOutcome` / `frontmatterParser`) — those are
|
|
63
|
+
* rpiv conventions, not framework defaults.
|
|
64
|
+
*
|
|
65
|
+
* 6. Custom-outcome authoring surface — `./output.js`
|
|
66
|
+
* `OutputSpec<Snapshot, Kind, Data>` (collector + optional parser),
|
|
67
|
+
* `ArtifactCollector`, `ArtifactParser`, `CollectCtx`,
|
|
68
|
+
* `CollectResult`, `ParseCtx`, `ParseResult`, `SnapshotCtx`,
|
|
69
|
+
* `SnapshotFn`. Sugar: `defineCollector` / `defineParser`.
|
|
70
|
+
*
|
|
71
|
+
* 7. Validation surfaces — `./validate-workflow.js`,
|
|
72
|
+
* `./validate-output.js`
|
|
73
|
+
* `validateWorkflow`, `WorkflowValidationIssue`,
|
|
74
|
+
* `validateOutputData`, `SchemaValidationFailure`.
|
|
75
|
+
*
|
|
76
|
+
* 8. Persistence (low-level — JSONL inspect) — `./state/index.js`
|
|
77
|
+
* Read past runs at `<cwd>/.rpiv/workflows/<run-id>.jsonl`:
|
|
78
|
+
* `listRuns`, `readHeader`, `readLastStage`, `listArtifacts`,
|
|
79
|
+
* `stateFilePath`, `workflowsDir`, `RunSummary`,
|
|
80
|
+
* `WorkflowHeader`, `WorkflowStage`. `recordStage` lives on
|
|
81
|
+
* `@juicesharp/rpiv-workflow/internal` (test-only — rpiv-pi's
|
|
82
|
+
* `[I3]` regression test pokes it directly; runner owns row
|
|
83
|
+
* writes, embedders never need it).
|
|
84
|
+
*
|
|
85
|
+
* 9. Runtime types — `./types.js`
|
|
86
|
+
* `RunState`.
|
|
87
|
+
*
|
|
88
|
+
* Per-module deep imports (`from "@juicesharp/rpiv-workflow/api.js"`)
|
|
89
|
+
* are NOT supported across the package boundary.
|
|
90
|
+
*
|
|
91
|
+
* ─── Pi-coupling boundary ───────────────────────────────────────────────
|
|
92
|
+
*
|
|
93
|
+
* The package's public type surface names ZERO `@earendil-works/pi-coding-agent`
|
|
94
|
+
* types. Every host capability the runtime needs is declared as a
|
|
95
|
+
* workflow-owned port in `./host.js`:
|
|
96
|
+
*
|
|
97
|
+
* • `WorkflowHost` — registry-level (default export + continue sends)
|
|
98
|
+
* • `WorkflowContext` — per-command ctx for `runWorkflow`; also the
|
|
99
|
+
* replacement ctx delivered to `newSession`'s
|
|
100
|
+
* `withSession` callback. `sendUserMessage` is
|
|
101
|
+
* optional at the type level (the outer command
|
|
102
|
+
* ctx omits it); the runtime guarantees it is
|
|
103
|
+
* present inside `withSession`.
|
|
104
|
+
*
|
|
105
|
+
* Pi's `ExtensionAPI` / `ExtensionCommandContext` are structurally
|
|
106
|
+
* compatible with these ports — embedders pass their existing Pi handles
|
|
107
|
+
* directly. A future non-Pi host implements the three port interfaces.
|
|
108
|
+
*
|
|
109
|
+
* The package no longer imports any value from
|
|
110
|
+
* `@earendil-works/pi-coding-agent` — `parseFrontmatter` moved to
|
|
111
|
+
* `@juicesharp/rpiv-pi` along with the rpiv-flavoured outcome
|
|
112
|
+
* (`rpivArtifactMdOutcome`). The peer dep stays for `pi-tui` types
|
|
113
|
+
* structural-compatibility only.
|
|
114
|
+
*
|
|
115
|
+
* `host.test.ts` carries a compile-time tripwire that fails immediately
|
|
116
|
+
* if Pi's types drift below the port's required shape.
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
import { registerWorkflowCommand } from "./command.js";
|
|
120
|
+
import { type DocsProtocolHost, registerDocsProtocol } from "./docs-protocol.js";
|
|
121
|
+
import type { WorkflowHost } from "./host.js";
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
type ActsScriptFn,
|
|
125
|
+
acts,
|
|
126
|
+
type DefineRouteOptions,
|
|
127
|
+
defineRoute,
|
|
128
|
+
defineWorkflow,
|
|
129
|
+
type EdgeContext,
|
|
130
|
+
type EdgeFn,
|
|
131
|
+
type EdgeTarget,
|
|
132
|
+
type FanoutContext,
|
|
133
|
+
type FanoutFn,
|
|
134
|
+
type FanoutUnit,
|
|
135
|
+
gate,
|
|
136
|
+
marksReadsData,
|
|
137
|
+
ON_INVALID_VALUES,
|
|
138
|
+
type OnInvalid,
|
|
139
|
+
type ProducesScriptFn,
|
|
140
|
+
produces,
|
|
141
|
+
READS_DATA,
|
|
142
|
+
type ScriptContext,
|
|
143
|
+
SESSION_POLICIES,
|
|
144
|
+
type SessionPolicy,
|
|
145
|
+
STAGE_KINDS,
|
|
146
|
+
type StageDef,
|
|
147
|
+
type StageKind,
|
|
148
|
+
type StageSchema,
|
|
149
|
+
terminal,
|
|
150
|
+
type Workflow,
|
|
151
|
+
} from "./api.js";
|
|
152
|
+
export { registerBuiltIns } from "./built-ins.js";
|
|
153
|
+
export {
|
|
154
|
+
type Artifact,
|
|
155
|
+
type ArtifactHandle,
|
|
156
|
+
fs,
|
|
157
|
+
handleToString,
|
|
158
|
+
inline,
|
|
159
|
+
opaque,
|
|
160
|
+
url,
|
|
161
|
+
} from "./handle.js";
|
|
162
|
+
export type { WorkflowContext, WorkflowHost } from "./host.js";
|
|
163
|
+
export { type LifecycleContext, type LifecycleListeners, registerLifecycle, type StageRef } from "./lifecycle.js";
|
|
164
|
+
export type { ConfigLayer, Issue, LoadedWorkflows, LoadIssue, OverlayPaths } from "./load/index.js";
|
|
165
|
+
export { loadWorkflows, projectOverlayPaths, userOverlayPaths } from "./load/index.js";
|
|
166
|
+
export {
|
|
167
|
+
type DirectoryPathCollectorOpts,
|
|
168
|
+
directoryPathCollector,
|
|
169
|
+
type GitCommitData,
|
|
170
|
+
type GitHeadSnapshot,
|
|
171
|
+
gitCommitCollector,
|
|
172
|
+
gitCommitOutcome,
|
|
173
|
+
gitCommitParser,
|
|
174
|
+
gitHeadSnapshot,
|
|
175
|
+
jsonBodyParser,
|
|
176
|
+
noopCollector,
|
|
177
|
+
sideEffectOutcome,
|
|
178
|
+
type ToolCall,
|
|
179
|
+
type ToolCallCollectorOpts,
|
|
180
|
+
type TranscriptPathCollectorOpts,
|
|
181
|
+
toolCallCollector,
|
|
182
|
+
transcriptPathCollector,
|
|
183
|
+
type UrlCollectorOpts,
|
|
184
|
+
unionCollectors,
|
|
185
|
+
urlCollector,
|
|
186
|
+
type WorkspaceDiffCollectorOpts,
|
|
187
|
+
type WorkspaceDiffSnapshot,
|
|
188
|
+
workspaceDiffCollector,
|
|
189
|
+
} from "./outcomes/index.js";
|
|
190
|
+
export type {
|
|
191
|
+
ArtifactCollector,
|
|
192
|
+
ArtifactParser,
|
|
193
|
+
CollectCtx,
|
|
194
|
+
CollectResult,
|
|
195
|
+
Output,
|
|
196
|
+
OutputMeta,
|
|
197
|
+
OutputSpec,
|
|
198
|
+
ParseCtx,
|
|
199
|
+
ParseResult,
|
|
200
|
+
SnapshotCtx,
|
|
201
|
+
SnapshotFn,
|
|
202
|
+
} from "./output.js";
|
|
203
|
+
export { defineCollector, defineParser } from "./output-spec.js";
|
|
204
|
+
export { eq, gt, gte, lt, lte, type Predicate } from "./predicates.js";
|
|
205
|
+
export { type RunWorkflowOptions, type RunWorkflowResult, runWorkflow } from "./runner/index.js";
|
|
206
|
+
export {
|
|
207
|
+
listArtifacts,
|
|
208
|
+
listRuns,
|
|
209
|
+
type RunSummary,
|
|
210
|
+
readHeader,
|
|
211
|
+
readLastStage,
|
|
212
|
+
stateFilePath,
|
|
213
|
+
type WorkflowHeader,
|
|
214
|
+
type WorkflowStage,
|
|
215
|
+
workflowsDir,
|
|
216
|
+
} from "./state/index.js";
|
|
217
|
+
export { DEFAULT_TRIGGER, type RunTrigger } from "./triggers.js";
|
|
218
|
+
export { typeboxSchema } from "./typebox-adapter.js";
|
|
219
|
+
export type { RunState } from "./types.js";
|
|
220
|
+
export { type SchemaValidationFailure, validateOutputData } from "./validate-output.js";
|
|
221
|
+
export { validateWorkflow, type WorkflowValidationIssue } from "./validate-workflow.js";
|
|
222
|
+
|
|
223
|
+
export default function (host: WorkflowHost): void {
|
|
224
|
+
registerWorkflowCommand(host);
|
|
225
|
+
// Pi passes the full ExtensionAPI at runtime, which has on().
|
|
226
|
+
// WorkflowHost doesn't declare on() to keep the programmatic-embedder
|
|
227
|
+
// surface narrow. Cast to satisfy the type — Pi always provides the
|
|
228
|
+
// full API.
|
|
229
|
+
registerDocsProtocol(host as unknown as DocsProtocolHost);
|
|
230
|
+
}
|