@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/api.ts +557 -0
  4. package/audit.ts +217 -0
  5. package/built-ins.ts +65 -0
  6. package/command.ts +137 -0
  7. package/docs/cover.png +0 -0
  8. package/docs/cover.svg +120 -0
  9. package/docs/workflow-authoring.md +629 -0
  10. package/docs/workflow-basics.md +122 -0
  11. package/docs-protocol.ts +106 -0
  12. package/fanout.ts +96 -0
  13. package/host.ts +97 -0
  14. package/index.ts +230 -0
  15. package/internal-utils.ts +69 -0
  16. package/internal.ts +27 -0
  17. package/layers.ts +33 -0
  18. package/lifecycle.ts +274 -0
  19. package/load/cache.test.ts +82 -0
  20. package/load/cache.ts +40 -0
  21. package/load/index.ts +159 -0
  22. package/load/merge.ts +136 -0
  23. package/load/normalize.ts +73 -0
  24. package/load/paths.ts +32 -0
  25. package/load/resolve-default.ts +43 -0
  26. package/load/shape-guards.test.ts +74 -0
  27. package/load/shape-guards.ts +42 -0
  28. package/messages.ts +185 -0
  29. package/outcomes/collectors/directory-path.test.ts +64 -0
  30. package/outcomes/collectors/directory-path.ts +40 -0
  31. package/outcomes/collectors/index.ts +21 -0
  32. package/outcomes/collectors/tool-call.test.ts +110 -0
  33. package/outcomes/collectors/tool-call.ts +63 -0
  34. package/outcomes/collectors/transcript-path.test.ts +70 -0
  35. package/outcomes/collectors/transcript-path.ts +53 -0
  36. package/outcomes/collectors/union.test.ts +59 -0
  37. package/outcomes/collectors/union.ts +55 -0
  38. package/outcomes/collectors/url.test.ts +67 -0
  39. package/outcomes/collectors/url.ts +45 -0
  40. package/outcomes/collectors/workspace-diff.test.ts +107 -0
  41. package/outcomes/collectors/workspace-diff.ts +123 -0
  42. package/outcomes/git-commit.test.ts +194 -0
  43. package/outcomes/git-commit.ts +192 -0
  44. package/outcomes/index.ts +22 -0
  45. package/outcomes/parsers/index.ts +11 -0
  46. package/outcomes/parsers/json-body.test.ts +80 -0
  47. package/outcomes/parsers/json-body.ts +50 -0
  48. package/outcomes/side-effect.ts +26 -0
  49. package/output-spec.ts +170 -0
  50. package/output.ts +98 -0
  51. package/package.json +83 -0
  52. package/preview.ts +120 -0
  53. package/routing.ts +79 -0
  54. package/runner/chain-advance.ts +185 -0
  55. package/runner/index.ts +7 -0
  56. package/runner/runner.ts +356 -0
  57. package/runner/script-stage.ts +240 -0
  58. package/runner/stage-lifecycle.ts +447 -0
  59. package/sessions/extraction.ts +297 -0
  60. package/sessions/index.ts +7 -0
  61. package/sessions/sessions.ts +269 -0
  62. package/sessions/spawn.ts +135 -0
  63. package/state/index.ts +27 -0
  64. package/state/paths.ts +46 -0
  65. package/state/reads.ts +190 -0
  66. package/state/state.ts +115 -0
  67. package/state/writes.ts +58 -0
  68. package/transcript.ts +156 -0
  69. package/triggers.ts +27 -0
  70. package/typebox-adapter.ts +48 -0
  71. package/types.ts +237 -0
  72. package/validate-output.ts +120 -0
  73. 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
+ }