@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,80 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { fs as fsHandle, opaque } from "../../handle.js";
|
|
6
|
+
import type { ParseCtx } from "../../output-spec.js";
|
|
7
|
+
import { jsonBodyParser } from "./json-body.js";
|
|
8
|
+
|
|
9
|
+
const ctxOf = (cwd: string, artifacts: ParseCtx["artifacts"]): ParseCtx => ({
|
|
10
|
+
cwd,
|
|
11
|
+
runId: "test",
|
|
12
|
+
stageIndex: 0,
|
|
13
|
+
state: {} as never,
|
|
14
|
+
branch: [],
|
|
15
|
+
branchOffset: undefined,
|
|
16
|
+
snapshot: undefined,
|
|
17
|
+
skill: "test",
|
|
18
|
+
artifacts,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("jsonBodyParser", () => {
|
|
22
|
+
let tmpDir: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmpDir = mkdtempSync(join(tmpdir(), "rpiv-jsonbody-"));
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("parses the primary fs artifact's body and emits kind:'json'", async () => {
|
|
32
|
+
writeFileSync(join(tmpDir, "out.json"), JSON.stringify({ ok: true, count: 3 }));
|
|
33
|
+
const ctx = ctxOf(tmpDir, [{ handle: fsHandle("out.json") }]);
|
|
34
|
+
const result = await jsonBodyParser.parse(ctx);
|
|
35
|
+
expect(result.kind).toBe("ok");
|
|
36
|
+
if (result.kind !== "ok") return;
|
|
37
|
+
expect(result.payload.kind).toBe("json");
|
|
38
|
+
expect(result.payload.data).toEqual({ ok: true, count: 3 });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("accepts absolute paths unchanged", async () => {
|
|
42
|
+
writeFileSync(join(tmpDir, "abs.json"), JSON.stringify({ x: 1 }));
|
|
43
|
+
const ctx = ctxOf(tmpDir, [{ handle: fsHandle(join(tmpDir, "abs.json")) }]);
|
|
44
|
+
const result = await jsonBodyParser.parse(ctx);
|
|
45
|
+
expect(result.kind === "ok" && result.payload.data).toEqual({ x: 1 });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("fatals when the primary artifact isn't an fs handle", async () => {
|
|
49
|
+
const ctx = ctxOf(tmpDir, [{ handle: opaque("not-fs") }]);
|
|
50
|
+
const result = await jsonBodyParser.parse(ctx);
|
|
51
|
+
expect(result.kind).toBe("fatal");
|
|
52
|
+
if (result.kind !== "fatal") return;
|
|
53
|
+
expect(result.message).toMatch(/requires an fs artifact/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("fatals when no artifacts are present", async () => {
|
|
57
|
+
const ctx = ctxOf(tmpDir, []);
|
|
58
|
+
const result = await jsonBodyParser.parse(ctx);
|
|
59
|
+
expect(result.kind).toBe("fatal");
|
|
60
|
+
if (result.kind !== "fatal") return;
|
|
61
|
+
expect(result.message).toMatch(/got none/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("fatals when the file doesn't exist", async () => {
|
|
65
|
+
const ctx = ctxOf(tmpDir, [{ handle: fsHandle("missing.json") }]);
|
|
66
|
+
const result = await jsonBodyParser.parse(ctx);
|
|
67
|
+
expect(result.kind).toBe("fatal");
|
|
68
|
+
if (result.kind !== "fatal") return;
|
|
69
|
+
expect(result.message).toMatch(/does not exist on disk/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("fatals on malformed JSON", async () => {
|
|
73
|
+
writeFileSync(join(tmpDir, "bad.json"), "{not json");
|
|
74
|
+
const ctx = ctxOf(tmpDir, [{ handle: fsHandle("bad.json") }]);
|
|
75
|
+
const result = await jsonBodyParser.parse(ctx);
|
|
76
|
+
expect(result.kind).toBe("fatal");
|
|
77
|
+
if (result.kind !== "fatal") return;
|
|
78
|
+
expect(result.message).toMatch(/failed to parse JSON/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-body parser — parses the primary fs artifact's body via
|
|
3
|
+
* `JSON.parse`. The companion to `transcriptPathCollector` (or any
|
|
4
|
+
* fs-emitting collector) for stages whose output is a JSON document
|
|
5
|
+
* the next stage validates against an `inputSchema`.
|
|
6
|
+
*
|
|
7
|
+
* Fail cases:
|
|
8
|
+
* - primary artifact is not an `fs` handle → fatal
|
|
9
|
+
* - file announced but missing on disk → fatal
|
|
10
|
+
* - body does not parse as JSON → fatal
|
|
11
|
+
*
|
|
12
|
+
* Authors who want to read only the frontmatter of a markdown file
|
|
13
|
+
* use rpiv-pi's `frontmatterParser` (or write their own); this parser
|
|
14
|
+
* intentionally does no Markdown handling.
|
|
15
|
+
*
|
|
16
|
+
* `kind` is `"json"`; `data` is the parsed value (typed `unknown` —
|
|
17
|
+
* narrow it via the stage's `outputSchema` for typed downstream
|
|
18
|
+
* narrowing through `output.data`).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
|
+
import { isAbsolute, join } from "node:path";
|
|
23
|
+
import type { ArtifactParser } from "../../output-spec.js";
|
|
24
|
+
import { defineParser } from "../../output-spec.js";
|
|
25
|
+
|
|
26
|
+
export const jsonBodyParser: ArtifactParser<unknown, "json", unknown> = defineParser({
|
|
27
|
+
parse: (ctx) => {
|
|
28
|
+
const primary = ctx.artifacts[0];
|
|
29
|
+
if (primary?.handle.kind !== "fs") {
|
|
30
|
+
return {
|
|
31
|
+
kind: "fatal",
|
|
32
|
+
message: `${ctx.skill}: jsonBodyParser requires an fs artifact (got ${primary?.handle.kind ?? "none"})`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const abs = isAbsolute(primary.handle.path) ? primary.handle.path : join(ctx.cwd, primary.handle.path);
|
|
36
|
+
if (!existsSync(abs)) {
|
|
37
|
+
return { kind: "fatal", message: `agent announced ${primary.handle.path} but file does not exist on disk` };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const data = JSON.parse(readFileSync(abs, "utf-8"));
|
|
41
|
+
return { kind: "ok", payload: { kind: "json", data } };
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
44
|
+
return {
|
|
45
|
+
kind: "fatal",
|
|
46
|
+
message: `${ctx.skill}: failed to parse JSON from ${primary.handle.path} — ${reason}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default outcome for side-effect stages — and the framework's
|
|
3
|
+
* "no artifacts produced" primitive.
|
|
4
|
+
*
|
|
5
|
+
* Collector always returns `{ kind: "ok", artifacts: [] }`. The chain
|
|
6
|
+
* semantics (see `runner/stage-lifecycle.ts:inputForStage`) then
|
|
7
|
+
* inherit the upstream artifact list forward — an action skill
|
|
8
|
+
* between two produces skills doesn't need its own collector.
|
|
9
|
+
*
|
|
10
|
+
* No parser: with `artifacts: []` the output's `data` is the empty
|
|
11
|
+
* list and `kind` is the literal `"artifacts"`. Stages that need a
|
|
12
|
+
* different discriminator wire their own outcome.
|
|
13
|
+
*
|
|
14
|
+
* No snapshot — side-effect stages have no pre-stage state to capture.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ArtifactCollector, OutputSpec } from "../output-spec.js";
|
|
18
|
+
|
|
19
|
+
/** Collector primitive: always returns zero artifacts, never fatal. */
|
|
20
|
+
export const noopCollector: ArtifactCollector = {
|
|
21
|
+
collect: () => ({ kind: "ok", artifacts: [] }),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const sideEffectOutcome: OutputSpec = {
|
|
25
|
+
collector: noopCollector,
|
|
26
|
+
};
|
package/output-spec.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutputSpec authoring surface — the contract a stage's data-channel
|
|
3
|
+
* implementation satisfies. Decomposed into two orthogonal halves so
|
|
4
|
+
* authors can mix-and-match:
|
|
5
|
+
*
|
|
6
|
+
* - `ArtifactCollector<B>` — ENUMERATE: which artifacts did the
|
|
7
|
+
* stage produce? (text scan, tool-call
|
|
8
|
+
* observation, fs diff, git, custom.)
|
|
9
|
+
* - `ArtifactParser<B, K, D>` — INTERPRET: given the artifacts, what
|
|
10
|
+
* typed data does downstream see?
|
|
11
|
+
*
|
|
12
|
+
* `OutputSpec` is the wired-up pair — `{ collector, parser? }` — that
|
|
13
|
+
* stages declare via `StageDef.outcome`. When `parser` is omitted the
|
|
14
|
+
* output data IS the artifact list (kind = `"artifacts"`).
|
|
15
|
+
*
|
|
16
|
+
* Companion to `output.ts` (the envelope `Output<K, D>` that flows to
|
|
17
|
+
* downstream stages, predicates, and the JSONL audit log). The split:
|
|
18
|
+
* output-spec authors implement the producer side here; output
|
|
19
|
+
* consumers read the envelope shape there.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Artifact } from "./handle.js";
|
|
23
|
+
import type { BranchEntry } from "./transcript.js";
|
|
24
|
+
import type { RunState } from "./types.js";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Snapshot — pre-stage reference capture (shared by collector + parser)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface SnapshotCtx {
|
|
31
|
+
cwd: string;
|
|
32
|
+
runId: string;
|
|
33
|
+
stageIndex: number;
|
|
34
|
+
state: Readonly<RunState>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Fail-soft: implementations catch and return undefined rather than throwing. */
|
|
38
|
+
export type SnapshotFn<Snapshot = unknown> = (ctx: SnapshotCtx) => Promise<Snapshot> | Snapshot;
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Collector — discover what the stage produced
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Context handed to a collector's `collect`. Includes the full unsliced
|
|
46
|
+
* branch (`branch`) plus a policy-derived `branchOffset` — for
|
|
47
|
+
* continue-policy stages the offset lets the collector ignore prior-stage
|
|
48
|
+
* prefix without re-materialising a slice. `snapshot` is whatever the
|
|
49
|
+
* collector's optional `snapshot` hook returned.
|
|
50
|
+
*/
|
|
51
|
+
export interface CollectCtx<Snapshot = unknown> extends SnapshotCtx {
|
|
52
|
+
branch: BranchEntry[];
|
|
53
|
+
branchOffset?: number;
|
|
54
|
+
snapshot: Snapshot;
|
|
55
|
+
/** Filled by the runner; collectors MUST NOT set this themselves. */
|
|
56
|
+
skill: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Three-way return from `collect`:
|
|
61
|
+
*
|
|
62
|
+
* `kind: "ok"` + `artifacts: []` — stage produced nothing.
|
|
63
|
+
* For produces stages the runner halts;
|
|
64
|
+
* for side-effect stages the chain inherits
|
|
65
|
+
* the upstream artifact list forward.
|
|
66
|
+
* `kind: "ok"` + `artifacts: [...]` — N>=1 artifacts; parser (or default) shapes the data.
|
|
67
|
+
* `kind: "fatal"` — collector cannot satisfy its contract;
|
|
68
|
+
* runner halts with the carried message.
|
|
69
|
+
*/
|
|
70
|
+
export type CollectResult = { kind: "ok"; artifacts: readonly Artifact[] } | { kind: "fatal"; message: string };
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The user-supplyable primitive. A collector enumerates artifacts; that's
|
|
74
|
+
* its single job. Authors compose `snapshot?` (pre-stage capture) +
|
|
75
|
+
* `collect` (post-stage enumeration). Side-effect-only stages use a
|
|
76
|
+
* collector that always returns `{ kind: "ok", artifacts: [] }` — see
|
|
77
|
+
* `outcomes/side-effect.ts`.
|
|
78
|
+
*
|
|
79
|
+
* Method shorthand (vs. function-property) so specialised
|
|
80
|
+
* `ArtifactCollector<MySnapshot>` is assignable to the runner's
|
|
81
|
+
* `ArtifactCollector` (default `Snapshot = unknown`) without explicit
|
|
82
|
+
* widening at every call site.
|
|
83
|
+
*/
|
|
84
|
+
export interface ArtifactCollector<Snapshot = unknown> {
|
|
85
|
+
snapshot?(ctx: SnapshotCtx): Promise<Snapshot> | Snapshot;
|
|
86
|
+
collect(ctx: CollectCtx<Snapshot>): Promise<CollectResult> | CollectResult;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Parser — interpret collected artifacts into a typed data channel
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Context handed to a parser's `parse`. Extends `CollectCtx` with the
|
|
95
|
+
* `artifacts` the matching collector just returned, so parsers can
|
|
96
|
+
* narrow on `artifacts[0].handle.kind` and inspect any `meta` the
|
|
97
|
+
* collector attached. `snapshot` flows through unchanged.
|
|
98
|
+
*/
|
|
99
|
+
export interface ParseCtx<Snapshot = unknown> extends CollectCtx<Snapshot> {
|
|
100
|
+
artifacts: readonly Artifact[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Two-way return from `parse`. `ok` produces the typed data channel
|
|
105
|
+
* downstream stages see on `output.data`; `fatal` halts the stage
|
|
106
|
+
* with the carried message — same posture as `CollectResult`.
|
|
107
|
+
*/
|
|
108
|
+
export type ParseResult<Kind extends string = string, Data = unknown> =
|
|
109
|
+
| { kind: "ok"; payload: { kind: Kind; data: Data } }
|
|
110
|
+
| { kind: "fatal"; message: string };
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Optional companion to a collector. When omitted, the output's
|
|
114
|
+
* `data` is the artifact list itself and `kind` is the literal
|
|
115
|
+
* `"artifacts"` — a stage that only needs to enumerate files doesn't
|
|
116
|
+
* have to write a parser.
|
|
117
|
+
*
|
|
118
|
+
* Method shorthand for the same bivariance reason as `ArtifactCollector`.
|
|
119
|
+
*/
|
|
120
|
+
export interface ArtifactParser<Snapshot = unknown, Kind extends string = string, Data = unknown> {
|
|
121
|
+
parse(ctx: ParseCtx<Snapshot>): Promise<ParseResult<Kind, Data>> | ParseResult<Kind, Data>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// OutputSpec — wired-up pair on `StageDef.outcome`
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A stage's collector+parser bundle. `parser` is optional; when omitted
|
|
130
|
+
* the output emits `kind: "artifacts"` with `data = artifacts`.
|
|
131
|
+
*
|
|
132
|
+
* Generic over `<Snapshot, Kind, Data>` so specialised output specs
|
|
133
|
+
* (`OutputSpec<GitHeadSnapshot, "git-commit", GitCommitData>`) flow types
|
|
134
|
+
* end-to-end from snapshot through collect into the downstream
|
|
135
|
+
* `output.data`.
|
|
136
|
+
*/
|
|
137
|
+
export interface OutputSpec<Snapshot = unknown, Kind extends string = string, Data = unknown> {
|
|
138
|
+
/**
|
|
139
|
+
* Categorical name this outcome publishes under in `state.named`. When set,
|
|
140
|
+
* the runner uses it as the default publish name for any stage wired with
|
|
141
|
+
* this outcome — multiple stages sharing the same outcome converge to one
|
|
142
|
+
* `state.named[name]` slot. Resolution order at write time:
|
|
143
|
+
* `stage.publishes ?? outcome.name ?? stage.<record-key>`.
|
|
144
|
+
*
|
|
145
|
+
* Optional. Outcomes that omit it cause stages to publish under their
|
|
146
|
+
* record key by default; downstream `reads:` references stage names
|
|
147
|
+
* directly.
|
|
148
|
+
*/
|
|
149
|
+
name?: string;
|
|
150
|
+
collector: ArtifactCollector<Snapshot>;
|
|
151
|
+
parser?: ArtifactParser<Snapshot, Kind, Data>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Author helpers — `define*` shorthands match `defineWorkflow` /
|
|
156
|
+
// `defineRoute` in api.ts. Pure passthroughs: they exist for type
|
|
157
|
+
// inference + uniform shape at the call site.
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/** Identity passthrough; lets authors annotate snapshot-generic collectors without re-stating `<Snapshot>`. */
|
|
161
|
+
export function defineCollector<Snapshot = unknown>(spec: ArtifactCollector<Snapshot>): ArtifactCollector<Snapshot> {
|
|
162
|
+
return spec;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Identity passthrough; same idiom as `defineCollector`. */
|
|
166
|
+
export function defineParser<Snapshot = unknown, Kind extends string = string, Data = unknown>(
|
|
167
|
+
spec: ArtifactParser<Snapshot, Kind, Data>,
|
|
168
|
+
): ArtifactParser<Snapshot, Kind, Data> {
|
|
169
|
+
return spec;
|
|
170
|
+
}
|
package/output.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output envelope — the inter-stage data channel a stage's collector +
|
|
3
|
+
* parser produce on settlement. Flows through `RunState`, persists to
|
|
4
|
+
* the JSONL audit log, and is read by downstream predicates / stages.
|
|
5
|
+
*
|
|
6
|
+
* Audience: predicate authors and downstream-stage authors reading
|
|
7
|
+
* `output.artifacts` (the storage references) and `output.data`
|
|
8
|
+
* (the typed channel a parser shaped). The producer-side surface
|
|
9
|
+
* (`ArtifactCollector` / `ArtifactParser` / `OutputSpec`) lives in
|
|
10
|
+
* `output-spec.ts`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Artifact } from "./handle.js";
|
|
14
|
+
import type { GitCommitData } from "./outcomes/git-commit.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Output envelope
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface OutputMeta {
|
|
21
|
+
/** Workflow stage record key — matches `WorkflowStage.stage`. */
|
|
22
|
+
stage: string;
|
|
23
|
+
/** Pi skill body when the producing stage was skill-based; absent for script stages. Matches `WorkflowStage.skill?`. */
|
|
24
|
+
skill?: string;
|
|
25
|
+
/** 1-based; matches `WorkflowStage.stageNumber`. */
|
|
26
|
+
stageNumber: number;
|
|
27
|
+
/** ISO-8601. */
|
|
28
|
+
ts: string;
|
|
29
|
+
/** Duplicated from header for ergonomic JSONL reads. */
|
|
30
|
+
runId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One stage's contribution to the chain. `artifacts` is always present
|
|
35
|
+
* (possibly empty for side-effect stages); `data` is whatever the parser
|
|
36
|
+
* shaped (or the artifact list itself when no parser is wired).
|
|
37
|
+
*
|
|
38
|
+
* `kind` discriminates the data shape so downstream consumers narrow
|
|
39
|
+
* via `output.kind === "git-commit"` etc. The literal `"artifacts"`
|
|
40
|
+
* is the default parser-less shape.
|
|
41
|
+
*/
|
|
42
|
+
export interface Output<K extends string = string, D = unknown> {
|
|
43
|
+
kind: K;
|
|
44
|
+
artifacts: readonly Artifact[];
|
|
45
|
+
data: D;
|
|
46
|
+
meta: OutputMeta;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Built-in output kind aliases
|
|
51
|
+
//
|
|
52
|
+
// Tagged-union narrowing convenience for consumers. Data shapes live
|
|
53
|
+
// with their producing outcomes; `GitCommitData` is type-only imported
|
|
54
|
+
// from `outcomes/git-commit.ts` (no runtime cycle).
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export type ArtifactsOutput = Output<"artifacts", readonly Artifact[]>;
|
|
58
|
+
export type SideEffectOutput = Output<"side-effect", Record<string, never>>;
|
|
59
|
+
export type GitCommitOutput = Output<"git-commit", GitCommitData>;
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// OutputSpec types — re-exported so consumers can `import { OutputSpec,
|
|
63
|
+
// CollectCtx, ... } from "../output.js"` without rewriting every
|
|
64
|
+
// site. Canonical definitions live in `output-spec.ts`.
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export type {
|
|
68
|
+
ArtifactCollector,
|
|
69
|
+
ArtifactParser,
|
|
70
|
+
CollectCtx,
|
|
71
|
+
CollectResult,
|
|
72
|
+
OutputSpec,
|
|
73
|
+
ParseCtx,
|
|
74
|
+
ParseResult,
|
|
75
|
+
SnapshotCtx,
|
|
76
|
+
SnapshotFn,
|
|
77
|
+
} from "./output-spec.js";
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Output construction
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Single source of output metadata authorship. The runner calls this
|
|
85
|
+
* after a stage's collector returned `artifacts` and the parser (or
|
|
86
|
+
* parser-less default) returned `{ kind, data }`.
|
|
87
|
+
*/
|
|
88
|
+
export function finalizeOutput<K extends string, D>(
|
|
89
|
+
args: { kind: K; artifacts: readonly Artifact[]; data: D },
|
|
90
|
+
meta: OutputMeta,
|
|
91
|
+
): Output<K, D> {
|
|
92
|
+
return {
|
|
93
|
+
kind: args.kind,
|
|
94
|
+
artifacts: args.artifacts,
|
|
95
|
+
data: args.data,
|
|
96
|
+
meta,
|
|
97
|
+
};
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juicesharp/rpiv-workflow",
|
|
3
|
+
"version": "1.14.0",
|
|
4
|
+
"description": "Pi extension. Chain skills into typed multi-stage workflows with audited JSONL state, predicate routing, and per-stage output validation. Skill-agnostic — bring your own skills.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"rpiv",
|
|
9
|
+
"workflow",
|
|
10
|
+
"skills",
|
|
11
|
+
"orchestration",
|
|
12
|
+
"dag",
|
|
13
|
+
"pipeline"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "juicesharp",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/juicesharp/rpiv-mono.git",
|
|
21
|
+
"directory": "packages/rpiv-workflow"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-workflow#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/juicesharp/rpiv-mono/issues"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.ts",
|
|
35
|
+
"internal.ts",
|
|
36
|
+
"api.ts",
|
|
37
|
+
"artifacts-layout.ts",
|
|
38
|
+
"audit.ts",
|
|
39
|
+
"built-ins.ts",
|
|
40
|
+
"command.ts",
|
|
41
|
+
"fanout.ts",
|
|
42
|
+
"host.ts",
|
|
43
|
+
"internal-utils.ts",
|
|
44
|
+
"layers.ts",
|
|
45
|
+
"lifecycle.ts",
|
|
46
|
+
"load",
|
|
47
|
+
"output.ts",
|
|
48
|
+
"messages.ts",
|
|
49
|
+
"output-spec.ts",
|
|
50
|
+
"outcomes",
|
|
51
|
+
"preview.ts",
|
|
52
|
+
"routing.ts",
|
|
53
|
+
"runner",
|
|
54
|
+
"sessions",
|
|
55
|
+
"state",
|
|
56
|
+
"transcript.ts",
|
|
57
|
+
"triggers.ts",
|
|
58
|
+
"typebox-adapter.ts",
|
|
59
|
+
"types.ts",
|
|
60
|
+
"validate-output.ts",
|
|
61
|
+
"validate-workflow.ts",
|
|
62
|
+
"docs-protocol.ts",
|
|
63
|
+
"docs",
|
|
64
|
+
"README.md",
|
|
65
|
+
"LICENSE"
|
|
66
|
+
],
|
|
67
|
+
"exports": {
|
|
68
|
+
".": "./index.ts",
|
|
69
|
+
"./internal": "./internal.ts"
|
|
70
|
+
},
|
|
71
|
+
"pi": {
|
|
72
|
+
"extensions": [
|
|
73
|
+
"./index.ts"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
78
|
+
"@juicesharp/rpiv-config": "*",
|
|
79
|
+
"@standard-schema/spec": "*",
|
|
80
|
+
"jiti": "*",
|
|
81
|
+
"typebox": "*"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/preview.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty-print workflow lists (no-args path) and per-workflow details
|
|
3
|
+
* (workflow-name-only path). Read-only against `LoadedWorkflows` — no I/O,
|
|
4
|
+
* no mutation. Two public formatters, both returning a single multiline
|
|
5
|
+
* string that `command.ts` hands straight to `ctx.ui.notify(..., "info")`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { StageDef, Workflow } from "./api.js";
|
|
9
|
+
import { type ConfigLayer, renderConfigLayer } from "./layers.js";
|
|
10
|
+
import type { LoadedWorkflows } from "./load/index.js";
|
|
11
|
+
import { CMD_USAGE_LIST, CMD_USAGE_PREVIEW, CMD_USAGE_RUN } from "./messages.js";
|
|
12
|
+
|
|
13
|
+
// ===========================================================================
|
|
14
|
+
// Public formatters
|
|
15
|
+
// ===========================================================================
|
|
16
|
+
|
|
17
|
+
/** Truncate a description to `maxLen` characters, appending "..." if truncated. */
|
|
18
|
+
function truncateDescription(desc: string, maxLen = 50): string {
|
|
19
|
+
if (desc.length <= maxLen) return desc;
|
|
20
|
+
return `${desc.slice(0, maxLen - 3)}...`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** No-args listing: every loaded workflow, its stage count, and its source. */
|
|
24
|
+
export function formatWorkflowList(loaded: LoadedWorkflows): string {
|
|
25
|
+
const rows = loaded.workflows.map((w) => {
|
|
26
|
+
const layer = loaded.workflowSources.get(w.name) ?? "built-in";
|
|
27
|
+
const stages = Object.keys(w.stages).length;
|
|
28
|
+
const tags = [`[${renderConfigLayer(layer)}]`];
|
|
29
|
+
if (w.name === loaded.default) tags.push("(default)");
|
|
30
|
+
const desc = w.description ? ` ${truncateDescription(w.description)}` : "";
|
|
31
|
+
return ` ${w.name} ${stages} stages ${tags.join(" ")}${desc}`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
"Available workflows:",
|
|
36
|
+
...rows,
|
|
37
|
+
"",
|
|
38
|
+
formatLayerBanner(loaded.layers),
|
|
39
|
+
CMD_USAGE_LIST,
|
|
40
|
+
CMD_USAGE_PREVIEW,
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Workflow-name-only path: full stage list + edges for one workflow. */
|
|
45
|
+
export function formatWorkflowDetails(loaded: LoadedWorkflows, name: string): string {
|
|
46
|
+
const workflow = loaded.workflows.find((w) => w.name === name);
|
|
47
|
+
if (!workflow) {
|
|
48
|
+
throw new Error(`formatWorkflowDetails: workflow "${name}" not found in loaded set`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const layer = loaded.workflowSources.get(name) ?? "built-in";
|
|
52
|
+
const heading = formatWorkflowHeading(name, layer, name === loaded.default);
|
|
53
|
+
const descriptionLine = workflow.description ? [workflow.description] : [];
|
|
54
|
+
const stageRows = Object.entries(workflow.stages).map(([stageName, stage], i) =>
|
|
55
|
+
formatStageRow(i + 1, stageName, stage, workflow),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return [heading, ...descriptionLine, "", ...stageRows, "", CMD_USAGE_RUN(name)].join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ===========================================================================
|
|
62
|
+
// Stage / source rendering
|
|
63
|
+
// ===========================================================================
|
|
64
|
+
|
|
65
|
+
/** `workflow: <name> (<layer>[, default])` — header line for details view. */
|
|
66
|
+
function formatWorkflowHeading(name: string, layer: ConfigLayer, isDefault: boolean): string {
|
|
67
|
+
const tags: string[] = [renderConfigLayer(layer)];
|
|
68
|
+
if (isDefault) tags.push("default");
|
|
69
|
+
return `workflow: ${name} (${tags.join(", ")})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Numbered row showing the stage + its outgoing edge target(s). */
|
|
73
|
+
function formatStageRow(idx: number, stageName: string, stage: StageDef, workflow: Workflow): string {
|
|
74
|
+
const num = `${idx}.`.padEnd(3);
|
|
75
|
+
const decorations = [stage.kind.padEnd(13), stage.sessionPolicy, outcomeTag(stage)];
|
|
76
|
+
if (stage.inputSchema) decorations.push("in-schema");
|
|
77
|
+
if (stage.outputSchema) decorations.push("out-schema");
|
|
78
|
+
|
|
79
|
+
const displayName = stage.skill && stage.skill !== stageName ? `${stageName} (skill: ${stage.skill})` : stageName;
|
|
80
|
+
const arrow = formatEdge(workflow, stageName);
|
|
81
|
+
const trailer = arrow ? ` → ${arrow}` : "";
|
|
82
|
+
|
|
83
|
+
return ` ${num} ${displayName.padEnd(36)} ${decorations.join(" · ")}${trailer}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
/**
|
|
88
|
+
* Single tag per stage encoding the outcome shape. Custom outcomes
|
|
89
|
+
* report `custom` (+`snapshot` when the collector declares a snapshot
|
|
90
|
+
* hook, +`parser` when a parser is wired). Stages without an outcome
|
|
91
|
+
* fall through to the framework default: `side-effect` for
|
|
92
|
+
* side-effect stages (the only kind that has a default); `???` for
|
|
93
|
+
* `produces` (load-time validation rejects this — the tag is for
|
|
94
|
+
* defensive rendering only).
|
|
95
|
+
*/
|
|
96
|
+
function outcomeTag(stage: StageDef): string {
|
|
97
|
+
if (stage.outcome) {
|
|
98
|
+
const tags = ["custom"];
|
|
99
|
+
if (stage.outcome.collector.snapshot) tags.push("snapshot");
|
|
100
|
+
if (stage.outcome.parser) tags.push("parser");
|
|
101
|
+
return tags.join("+");
|
|
102
|
+
}
|
|
103
|
+
return stage.kind === "produces" ? "???" : "side-effect";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Render the outgoing edge as a human-readable trailer (string or predicate target set). */
|
|
107
|
+
function formatEdge(workflow: Workflow, from: string): string | undefined {
|
|
108
|
+
const target = workflow.edges[from];
|
|
109
|
+
if (target === undefined) return "(terminal — no edge declared)";
|
|
110
|
+
if (target === "stop") return "stop";
|
|
111
|
+
if (typeof target === "string") return target;
|
|
112
|
+
const targets = target.targets;
|
|
113
|
+
if (Array.isArray(targets) && targets.length > 0) return `predicate(${targets.join(" | ")})`;
|
|
114
|
+
return "predicate";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** "Sources: built-in + user + project" — single-line layer banner. */
|
|
118
|
+
function formatLayerBanner(layers: readonly ConfigLayer[]): string {
|
|
119
|
+
return `Sources: ${layers.map(renderConfigLayer).join(" + ")}`;
|
|
120
|
+
}
|