@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
package/audit.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit / bookkeeping — JSONL writes, status-line clears, notify, and
|
|
3
|
+
* `state.termination.error` for terminal outcomes. Shared by runner.ts + sessions.ts;
|
|
4
|
+
* neither imports back. Depends only on state + messages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { handleToString } from "./handle.js";
|
|
8
|
+
import { assertNever } from "./internal-utils.js";
|
|
9
|
+
import { buildLifecycleContext, scriptStageRef, skillStageRef } from "./lifecycle.js";
|
|
10
|
+
import {
|
|
11
|
+
ERR_STAGE_ABORTED,
|
|
12
|
+
ERR_STAGE_NO_RESPONSE,
|
|
13
|
+
ERR_STAGE_TOOL_STALLED,
|
|
14
|
+
ERR_STAGE_TRUNCATED,
|
|
15
|
+
MSG_PARTIAL_ARTIFACTS,
|
|
16
|
+
MSG_STAGE_ABORTED,
|
|
17
|
+
MSG_STAGE_FAILED,
|
|
18
|
+
MSG_STAGE_NO_RESPONSE,
|
|
19
|
+
MSG_STAGE_TOOL_STALLED,
|
|
20
|
+
MSG_STAGE_TRUNCATED,
|
|
21
|
+
MSG_WORKFLOW_CANCELLED,
|
|
22
|
+
STATUS_KEY,
|
|
23
|
+
} from "./messages.js";
|
|
24
|
+
import { appendStage, listArtifacts, type WorkflowStage } from "./state/index.js";
|
|
25
|
+
import type { StopSignal } from "./transcript.js";
|
|
26
|
+
import type { FanoutSession, RunnerCtx, RunState, SessionContext } from "./types.js";
|
|
27
|
+
|
|
28
|
+
/** Single source of ISO-8601 timestamps for audit rows + output meta. */
|
|
29
|
+
export const nowIso = (): string => new Date().toISOString();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Minimal bookkeeping ctx. Structurally derived from `SessionContext` so any
|
|
33
|
+
* future field added to the base lands here too — no duplicate
|
|
34
|
+
* maintenance. Both `StageSession` and `FanoutSession` collapse to this.
|
|
35
|
+
*
|
|
36
|
+
* `isScript` toggles the `onStageError` ref construction in
|
|
37
|
+
* `recordTerminalFailure` from `skillStageRef` to `scriptStageRef` (the
|
|
38
|
+
* script branch carries no `skill` field). Defaulting to `undefined`
|
|
39
|
+
* preserves the skill-path behaviour for every existing caller.
|
|
40
|
+
*/
|
|
41
|
+
export type AuditCtx = Pick<
|
|
42
|
+
SessionContext,
|
|
43
|
+
"cwd" | "runId" | "state" | "stageName" | "skill" | "lifecycle" | "runIdentity"
|
|
44
|
+
> & {
|
|
45
|
+
isScript?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* JSONL `WorkflowStage.stage` value for fanout-unit rows — built from
|
|
50
|
+
* the parent stage's record key (`stageName`) suffixed with the
|
|
51
|
+
* user-supplied `id` when present, falling back to `label`
|
|
52
|
+
* (e.g. `"implement (phase-2)"` or `"implement (phase 2/4)"`) so
|
|
53
|
+
* post-hoc readers can distinguish loop iterations. Owned by the audit
|
|
54
|
+
* layer because the JSONL row shape is its concern; the runner stays
|
|
55
|
+
* neutral about the wording.
|
|
56
|
+
*/
|
|
57
|
+
export const fanoutRowStage = (s: FanoutSession): string => `${s.stageName} (${s.id ?? s.label})`;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Allocates the next `stageNumber`, attempts the append, and returns the
|
|
61
|
+
* assigned number on success (or undefined on I/O failure). `lastAllocatedStageNumber`
|
|
62
|
+
* advances monotonically — once per call — so a transient failure doesn't
|
|
63
|
+
* cause the next stage to reuse the lost row's number. Higher-level counters
|
|
64
|
+
* (e.g. `stagesCompleted`) gate on the returned value being defined.
|
|
65
|
+
*
|
|
66
|
+
* `wrapOutput`'s `state.lastAllocatedStageNumber + 1` peek aligns with this allocation
|
|
67
|
+
* because the output is built BEFORE recordStage is called.
|
|
68
|
+
*/
|
|
69
|
+
export function recordStage(
|
|
70
|
+
cwd: string,
|
|
71
|
+
runId: string,
|
|
72
|
+
stage: Omit<WorkflowStage, "stageNumber">,
|
|
73
|
+
state: RunState,
|
|
74
|
+
): number | undefined {
|
|
75
|
+
state.lastAllocatedStageNumber += 1;
|
|
76
|
+
const stageNumber = state.lastAllocatedStageNumber;
|
|
77
|
+
return appendStage(cwd, runId, { stageNumber, ...stage }) ? stageNumber : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Surface every artifact recorded so far — recap on stage failure. */
|
|
81
|
+
export function notifyPartialArtifacts(ctx: RunnerCtx, cwd: string, runId: string): void {
|
|
82
|
+
const items = listArtifacts(cwd, runId);
|
|
83
|
+
if (items.length === 0) return;
|
|
84
|
+
const artifactList = items.map((i) => ` • ${i.stage}: ${handleToString(i.artifact.handle)}`).join("\n");
|
|
85
|
+
ctx.ui.notify(MSG_PARTIAL_ARTIFACTS(artifactList), "info");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function recordTerminalFailure(
|
|
89
|
+
ctx: RunnerCtx,
|
|
90
|
+
audit: AuditCtx,
|
|
91
|
+
args: {
|
|
92
|
+
status: "failed" | "aborted";
|
|
93
|
+
notifyMsg: string;
|
|
94
|
+
notifyLevel: "warning" | "error";
|
|
95
|
+
errMsg: string;
|
|
96
|
+
},
|
|
97
|
+
onFailure?: (ctx: RunnerCtx) => void,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
recordStage(
|
|
100
|
+
audit.cwd,
|
|
101
|
+
audit.runId,
|
|
102
|
+
// Script-stage failure rows omit `skill` (the row split landed in A.0);
|
|
103
|
+
// skill rows continue to carry it. `undefined` is dropped by JSON.stringify.
|
|
104
|
+
// `errMsg` mirrors `state.termination.error` so the failure reason
|
|
105
|
+
// survives in JSONL even when the `ctx.ui.notify` toast is missed.
|
|
106
|
+
{
|
|
107
|
+
stage: audit.stageName,
|
|
108
|
+
skill: audit.isScript ? undefined : audit.skill,
|
|
109
|
+
status: args.status,
|
|
110
|
+
ts: nowIso(),
|
|
111
|
+
errMsg: args.errMsg,
|
|
112
|
+
},
|
|
113
|
+
audit.state,
|
|
114
|
+
);
|
|
115
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
116
|
+
ctx.ui.notify(args.notifyMsg, args.notifyLevel);
|
|
117
|
+
onFailure?.(ctx);
|
|
118
|
+
audit.state.termination.error = args.errMsg;
|
|
119
|
+
const ref = audit.isScript
|
|
120
|
+
? scriptStageRef(audit.stageName, audit.state.lastAllocatedStageNumber)
|
|
121
|
+
: skillStageRef(audit.stageName, audit.state.lastAllocatedStageNumber, audit.skill);
|
|
122
|
+
await audit.lifecycle.fire(
|
|
123
|
+
ctx,
|
|
124
|
+
"onStageError",
|
|
125
|
+
ref,
|
|
126
|
+
args.errMsg,
|
|
127
|
+
buildLifecycleContext({
|
|
128
|
+
cwd: audit.cwd,
|
|
129
|
+
runId: audit.runId,
|
|
130
|
+
workflow: audit.runIdentity.workflow,
|
|
131
|
+
totalStages: audit.runIdentity.totalStages,
|
|
132
|
+
trigger: audit.runIdentity.trigger,
|
|
133
|
+
state: audit.state,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One arm per StopSignal variant (minus `"stop"`, the success path).
|
|
140
|
+
* JSONL `status` stays `"aborted" | "failed"` for downstream-reader
|
|
141
|
+
* compatibility; the per-signal distinction surfaces via MSG_STAGE_*
|
|
142
|
+
* and state.termination.error.
|
|
143
|
+
*/
|
|
144
|
+
export async function recordStopFailure(
|
|
145
|
+
ctx: RunnerCtx,
|
|
146
|
+
audit: AuditCtx,
|
|
147
|
+
stop: Exclude<StopSignal, "stop">,
|
|
148
|
+
errorMessage: string,
|
|
149
|
+
onFailure?: (ctx: RunnerCtx) => void,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
await recordTerminalFailure(ctx, audit, stopFailureArgs(audit.skill, stop, errorMessage), onFailure);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function stopFailureArgs(
|
|
155
|
+
skill: string,
|
|
156
|
+
stop: Exclude<StopSignal, "stop">,
|
|
157
|
+
errorMessage: string,
|
|
158
|
+
): {
|
|
159
|
+
status: "failed" | "aborted";
|
|
160
|
+
notifyMsg: string;
|
|
161
|
+
notifyLevel: "warning" | "error";
|
|
162
|
+
errMsg: string;
|
|
163
|
+
} {
|
|
164
|
+
switch (stop) {
|
|
165
|
+
case "aborted":
|
|
166
|
+
return {
|
|
167
|
+
status: "aborted",
|
|
168
|
+
notifyMsg: MSG_STAGE_ABORTED(skill),
|
|
169
|
+
notifyLevel: "warning",
|
|
170
|
+
errMsg: ERR_STAGE_ABORTED(skill),
|
|
171
|
+
};
|
|
172
|
+
case "length":
|
|
173
|
+
return {
|
|
174
|
+
status: "failed",
|
|
175
|
+
notifyMsg: MSG_STAGE_TRUNCATED(skill),
|
|
176
|
+
notifyLevel: "error",
|
|
177
|
+
errMsg: ERR_STAGE_TRUNCATED(skill),
|
|
178
|
+
};
|
|
179
|
+
case "toolUse":
|
|
180
|
+
return {
|
|
181
|
+
status: "failed",
|
|
182
|
+
notifyMsg: MSG_STAGE_TOOL_STALLED(skill),
|
|
183
|
+
notifyLevel: "error",
|
|
184
|
+
errMsg: ERR_STAGE_TOOL_STALLED(skill),
|
|
185
|
+
};
|
|
186
|
+
case "noResponse":
|
|
187
|
+
return {
|
|
188
|
+
status: "failed",
|
|
189
|
+
notifyMsg: MSG_STAGE_NO_RESPONSE(skill),
|
|
190
|
+
notifyLevel: "error",
|
|
191
|
+
errMsg: ERR_STAGE_NO_RESPONSE(skill),
|
|
192
|
+
};
|
|
193
|
+
case "error":
|
|
194
|
+
return {
|
|
195
|
+
status: "failed",
|
|
196
|
+
notifyMsg: MSG_STAGE_FAILED(skill),
|
|
197
|
+
notifyLevel: "error",
|
|
198
|
+
errMsg: errorMessage,
|
|
199
|
+
};
|
|
200
|
+
default:
|
|
201
|
+
return assertNever(stop);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function recordCancellation(ctx: RunnerCtx, audit: AuditCtx): void {
|
|
206
|
+
recordStage(
|
|
207
|
+
audit.cwd,
|
|
208
|
+
audit.runId,
|
|
209
|
+
{ stage: audit.stageName, skill: audit.skill, status: "skipped", ts: nowIso() },
|
|
210
|
+
audit.state,
|
|
211
|
+
);
|
|
212
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
213
|
+
ctx.ui.notify(MSG_WORKFLOW_CANCELLED, "info");
|
|
214
|
+
// `success: false` alone can't distinguish "cancelled" from "never started";
|
|
215
|
+
// the error string is the signal.
|
|
216
|
+
audit.state.termination.error = `${audit.skill} cancelled by user`;
|
|
217
|
+
}
|
package/built-ins.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic built-in workflow registry.
|
|
3
|
+
*
|
|
4
|
+
* Sibling packages contribute their workflows at extension load via
|
|
5
|
+
* `registerBuiltIns(...)`. The loader treats the union as the lowest layer
|
|
6
|
+
* — user and project overlays still override by name.
|
|
7
|
+
*
|
|
8
|
+
* The runner itself ships ZERO built-in workflows. That's deliberate: this
|
|
9
|
+
* package is skill-agnostic, and shipping examples that name skills the
|
|
10
|
+
* user may not have installed would surface as confusing "skill not found"
|
|
11
|
+
* errors. Packages like `@juicesharp/rpiv-pi` opt in by calling
|
|
12
|
+
* `registerBuiltIns(...)` from their extension entry point with workflows
|
|
13
|
+
* that name their own bundled skills.
|
|
14
|
+
*
|
|
15
|
+
* The registry array is anchored on a `Symbol.for` slot on `globalThis`.
|
|
16
|
+
* Pi may load this module more than once — once for the rpiv-workflow
|
|
17
|
+
* extension itself, and once via the rpiv-pi `import { registerBuiltIns }
|
|
18
|
+
* from "@juicesharp/rpiv-workflow"` cross-package resolution — and
|
|
19
|
+
* module-local state would be siloed between those copies.
|
|
20
|
+
* `globalThis[KEY]` is process-wide and survives the dup load.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Workflow } from "./api.js";
|
|
24
|
+
|
|
25
|
+
const REGISTRY_KEY = Symbol.for("@juicesharp/rpiv-workflow:built-ins");
|
|
26
|
+
|
|
27
|
+
type Global = Record<symbol, unknown>;
|
|
28
|
+
|
|
29
|
+
function getRegistry(): Workflow[] {
|
|
30
|
+
const g = globalThis as unknown as Global;
|
|
31
|
+
let registry = g[REGISTRY_KEY] as Workflow[] | undefined;
|
|
32
|
+
if (!registry) {
|
|
33
|
+
registry = [];
|
|
34
|
+
g[REGISTRY_KEY] = registry;
|
|
35
|
+
}
|
|
36
|
+
return registry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Register one or more workflows into the `built-in` layer. Idempotent on
|
|
41
|
+
* `Workflow.name` — re-registering an existing name replaces the prior
|
|
42
|
+
* entry. Safe to call multiple times from the same extension load if the
|
|
43
|
+
* extension is re-loaded by Pi's `/reload`.
|
|
44
|
+
*/
|
|
45
|
+
export function registerBuiltIns(workflows: readonly Workflow[]): void {
|
|
46
|
+
const registry = getRegistry();
|
|
47
|
+
for (const w of workflows) {
|
|
48
|
+
const existing = registry.findIndex((r) => r.name === w.name);
|
|
49
|
+
if (existing >= 0) registry[existing] = w;
|
|
50
|
+
else registry.push(w);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read-only view of the registry — consumed by `load.ts`. */
|
|
55
|
+
export function getBuiltIns(): readonly Workflow[] {
|
|
56
|
+
return getRegistry();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Test reset. Wired into the repo-wide test setup so cross-test
|
|
61
|
+
* registration leaks don't bias the next case.
|
|
62
|
+
*/
|
|
63
|
+
export function __resetBuiltIns(): void {
|
|
64
|
+
getRegistry().length = 0;
|
|
65
|
+
}
|
package/command.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** /wf slash command: parse → loadWorkflows → runWorkflow. */
|
|
2
|
+
|
|
3
|
+
import type { WorkflowContext, WorkflowHost } from "./host.js";
|
|
4
|
+
import { renderConfigLayer } from "./layers.js";
|
|
5
|
+
import { findWorkflow, type Issue, loadWorkflows } from "./load/index.js";
|
|
6
|
+
import {
|
|
7
|
+
CMD_DESCRIPTION,
|
|
8
|
+
MSG_INTERACTIVE_ONLY,
|
|
9
|
+
MSG_LOAD_ABORTED,
|
|
10
|
+
MSG_NO_WORKFLOWS_REGISTERED,
|
|
11
|
+
MSG_WORKFLOW_NOT_FOUND,
|
|
12
|
+
MSG_WORKFLOW_THREW,
|
|
13
|
+
} from "./messages.js";
|
|
14
|
+
import { formatWorkflowDetails, formatWorkflowList } from "./preview.js";
|
|
15
|
+
import { runWorkflow } from "./runner/index.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Public entry
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export function registerWorkflowCommand(host: WorkflowHost): void {
|
|
22
|
+
host.registerCommand("wf", {
|
|
23
|
+
description: CMD_DESCRIPTION,
|
|
24
|
+
handler: (args: string, ctx: WorkflowContext) => handleWorkflowCommand(host, args, ctx),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Orchestrator
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
async function handleWorkflowCommand(host: WorkflowHost, args: string, ctx: WorkflowContext): Promise<void> {
|
|
33
|
+
if (!ctx.hasUI) {
|
|
34
|
+
ctx.ui.notify(MSG_INTERACTIVE_ONLY, "error");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const loaded = await loadWorkflows(ctx.cwd);
|
|
39
|
+
surfaceIssues(ctx, loaded.issues);
|
|
40
|
+
|
|
41
|
+
const workflowNames = new Set(loaded.workflows.map((w) => w.name));
|
|
42
|
+
const { workflow: workflowName, input } = parseArgs(args, { workflowNames, default: loaded.default });
|
|
43
|
+
|
|
44
|
+
if (!input) {
|
|
45
|
+
const trimmed = args.trim();
|
|
46
|
+
const previewing = trimmed.length > 0 && workflowNames.has(trimmed);
|
|
47
|
+
ctx.ui.notify(previewing ? formatWorkflowDetails(loaded, trimmed) : formatWorkflowList(loaded), "info");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Block execution on load errors — running a partially-loaded workflow set
|
|
52
|
+
// would silently mask the user's intent (e.g. their preferred workflow
|
|
53
|
+
// failed to import).
|
|
54
|
+
const errorCount = loaded.issues.filter((i) => i.severity === "error").length;
|
|
55
|
+
if (errorCount > 0) {
|
|
56
|
+
ctx.ui.notify(MSG_LOAD_ABORTED(errorCount), "error");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Standalone install: rpiv-workflow ships zero workflows; if nothing else
|
|
61
|
+
// registered one, there's nothing to run. parseArgs returns "" for the
|
|
62
|
+
// workflow name in this case (no default + first token didn't match) —
|
|
63
|
+
// surface the empty-registry verdict instead of falling through to a
|
|
64
|
+
// generic not-found notify.
|
|
65
|
+
if (!workflowName) {
|
|
66
|
+
ctx.ui.notify(MSG_NO_WORKFLOWS_REGISTERED, "error");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const workflow = findWorkflow(loaded, workflowName);
|
|
71
|
+
if (!workflow) {
|
|
72
|
+
ctx.ui.notify(MSG_WORKFLOW_NOT_FOUND(workflowName), "error");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// runWorkflow returns a result envelope rather than throwing — but a
|
|
77
|
+
// thrown predicate or invariant could still bubble. Catch so Pi's
|
|
78
|
+
// dispatcher doesn't print a raw stack.
|
|
79
|
+
try {
|
|
80
|
+
await runWorkflow(ctx, { workflow, input, host, trigger: { kind: "command", name: "wf" } });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
83
|
+
ctx.ui.notify(MSG_WORKFLOW_THREW(reason), "error");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Arg parsing (exported for tests)
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* First token is a workflow name iff recognised; otherwise the whole arg is
|
|
93
|
+
* input bound to the resolved default. When no default is registered (the
|
|
94
|
+
* empty-registry case), the returned `workflow` is `""` and the orchestrator
|
|
95
|
+
* surfaces `MSG_NO_WORKFLOWS_REGISTERED`.
|
|
96
|
+
*/
|
|
97
|
+
export function parseArgs(
|
|
98
|
+
args: string,
|
|
99
|
+
loaded: { workflowNames: ReadonlySet<string>; default: string | undefined },
|
|
100
|
+
): { workflow: string; input: string } {
|
|
101
|
+
const trimmed = args.trim();
|
|
102
|
+
if (!trimmed) {
|
|
103
|
+
return { workflow: loaded.default ?? "", input: "" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
107
|
+
const firstToken = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
|
|
108
|
+
|
|
109
|
+
if (loaded.workflowNames.has(firstToken)) {
|
|
110
|
+
const remaining = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1).trim();
|
|
111
|
+
return { workflow: firstToken, input: remaining };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { workflow: loaded.default ?? "", input: trimmed };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Private helpers
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/** Surface every load + validation issue as a notify, prefixed by severity. */
|
|
122
|
+
function surfaceIssues(ctx: WorkflowContext, issues: readonly Issue[]): void {
|
|
123
|
+
for (const issue of issues) {
|
|
124
|
+
const level: "warning" | "error" = issue.severity === "error" ? "error" : "warning";
|
|
125
|
+
ctx.ui.notify(formatIssue(issue), level);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatIssue(issue: Issue): string {
|
|
130
|
+
if (issue.kind === "load") {
|
|
131
|
+
const where = issue.path ? ` (${issue.path})` : "";
|
|
132
|
+
return `[${renderConfigLayer(issue.layer)} config${where}] ${issue.message}`;
|
|
133
|
+
}
|
|
134
|
+
const stageTag = issue.stage ? ` — stage "${issue.stage}"` : "";
|
|
135
|
+
const pathTag = issue.path ? ` (${issue.path})` : "";
|
|
136
|
+
return `[${renderConfigLayer(issue.layer)} config${pathTag}] workflow "${issue.workflow}"${stageTag}: ${issue.message}`;
|
|
137
|
+
}
|
package/docs/cover.png
ADDED
|
Binary file
|
package/docs/cover.svg
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 800" role="img" aria-label="rpiv-workflow — Chain Pi skills into typed multi-stage workflows" preserveAspectRatio="xMidYMid meet">
|
|
2
|
+
<title>rpiv-workflow — Chain Pi skills into typed multi-stage workflows</title>
|
|
3
|
+
<defs>
|
|
4
|
+
<style>
|
|
5
|
+
.mono { font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
6
|
+
.serif { font-family: 'Iowan Old Style', 'Hoefler Text', Charter, 'Source Serif Pro', Georgia, serif; }
|
|
7
|
+
.ink { fill: #ede6d3; }
|
|
8
|
+
.text-quiet { fill: #a8a29e; }
|
|
9
|
+
.text-distant { fill: #9c968d; }
|
|
10
|
+
.ochre { fill: #a8896c; }
|
|
11
|
+
.sage { fill: #8a9580; }
|
|
12
|
+
.sage-deep { fill: #5e6b56; }
|
|
13
|
+
</style>
|
|
14
|
+
<radialGradient id="wash-ochre" cx="25%" cy="10%" r="65%">
|
|
15
|
+
<stop offset="0%" stop-color="#a8896c" stop-opacity="0.10"/>
|
|
16
|
+
<stop offset="100%" stop-color="#a8896c" stop-opacity="0"/>
|
|
17
|
+
</radialGradient>
|
|
18
|
+
<radialGradient id="wash-sage" cx="85%" cy="90%" r="55%">
|
|
19
|
+
<stop offset="0%" stop-color="#8a9580" stop-opacity="0.07"/>
|
|
20
|
+
<stop offset="100%" stop-color="#8a9580" stop-opacity="0"/>
|
|
21
|
+
</radialGradient>
|
|
22
|
+
|
|
23
|
+
</defs>
|
|
24
|
+
|
|
25
|
+
<rect width="1280" height="800" fill="#1c1a17"/>
|
|
26
|
+
<rect width="1280" height="800" fill="url(#wash-ochre)"/>
|
|
27
|
+
<rect width="1280" height="800" fill="url(#wash-sage)"/>
|
|
28
|
+
|
|
29
|
+
<g fill="none" stroke="#8a9580" stroke-linecap="round" opacity="0.45">
|
|
30
|
+
<path d="M1120 60 q36 -52 88 -60 q40 -5 68 14" stroke-width="1.1"/>
|
|
31
|
+
<path d="M1140 80 q28 -18 60 -14 q24 3 42 14" stroke-width="0.7" opacity="0.6"/>
|
|
32
|
+
<circle cx="1210" cy="32" r="2" opacity="0.5"/>
|
|
33
|
+
<circle cx="1160" cy="60" r="1.4" opacity="0.4"/>
|
|
34
|
+
</g>
|
|
35
|
+
|
|
36
|
+
<text class="mono text-distant" x="80" y="64" font-size="11" letter-spacing="0.18em">PI AGENT EXTENSION</text>
|
|
37
|
+
|
|
38
|
+
<text class="serif ink" x="80" y="152" font-size="80" font-weight="700" letter-spacing="-2.5">rpiv<tspan class="ochre">-</tspan>workflow</text>
|
|
39
|
+
<text class="mono text-quiet" x="80" y="196" font-size="19" letter-spacing="0.2">Chain Pi skills into typed multi-stage workflows.</text>
|
|
40
|
+
|
|
41
|
+
<line x1="80" y1="228" x2="1200" y2="228" stroke="#2e2b27" stroke-width="1"/>
|
|
42
|
+
|
|
43
|
+
<rect x="80" y="264" width="1120" height="440" rx="3" fill="#1c1a17" stroke="#3a3631" stroke-width="1"/>
|
|
44
|
+
|
|
45
|
+
<!-- Main timeline (research → plan → review → gate → commit → stop) -->
|
|
46
|
+
<line x1="200" y1="468" x2="800" y2="468" stroke="#a8896c" stroke-width="2"/>
|
|
47
|
+
<line x1="836" y1="468" x2="1000" y2="468" stroke="#a8896c" stroke-width="2"/>
|
|
48
|
+
<line x1="1000" y1="468" x2="1124" y2="468" stroke="#a8896c" stroke-width="2"/>
|
|
49
|
+
<path d="M1114 459 L1128 468 L1114 477" fill="none" stroke="#a8896c" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
|
|
50
|
+
|
|
51
|
+
<!-- Loopback edge: gate → (down) → (left) → plan -->
|
|
52
|
+
<path d="M800 484 Q800 580 720 580 L480 580 Q400 580 400 500 L400 484"
|
|
53
|
+
fill="none" stroke="#5e6b56" stroke-width="1.6" stroke-dasharray="4 4" stroke-linecap="round"/>
|
|
54
|
+
<path d="M392 494 L400 480 L408 494" fill="none" stroke="#5e6b56" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
|
|
55
|
+
|
|
56
|
+
<!-- Loopback label -->
|
|
57
|
+
<text class="mono sage" x="600" y="572" font-size="11" letter-spacing="0.18em" text-anchor="middle" font-weight="700">REVISE · blockers > 0</text>
|
|
58
|
+
|
|
59
|
+
<!-- Branch label on straight path to commit -->
|
|
60
|
+
<text class="mono text-distant" x="900" y="456" font-size="11" letter-spacing="0.16em" text-anchor="middle">blockers == 0</text>
|
|
61
|
+
|
|
62
|
+
<!-- Node 01: Research (produces) -->
|
|
63
|
+
<g transform="translate(200 468)">
|
|
64
|
+
<circle r="14" fill="#1c1a17" stroke="#a8896c" stroke-width="2"/>
|
|
65
|
+
<circle r="5" fill="#a8896c"/>
|
|
66
|
+
<text class="mono text-distant" x="0" y="-36" font-size="11" letter-spacing="0.12em" text-anchor="middle">01 · produces</text>
|
|
67
|
+
<text class="mono ink" x="0" y="52" font-size="15" letter-spacing="0.12em" text-anchor="middle" font-weight="700">RESEARCH</text>
|
|
68
|
+
</g>
|
|
69
|
+
|
|
70
|
+
<!-- Node 02: Plan (produces) -->
|
|
71
|
+
<g transform="translate(400 468)">
|
|
72
|
+
<circle r="14" fill="#1c1a17" stroke="#a8896c" stroke-width="2"/>
|
|
73
|
+
<circle r="5" fill="#a8896c"/>
|
|
74
|
+
<text class="mono text-distant" x="0" y="-36" font-size="11" letter-spacing="0.12em" text-anchor="middle">02 · produces</text>
|
|
75
|
+
<text class="mono ink" x="0" y="52" font-size="15" letter-spacing="0.12em" text-anchor="middle" font-weight="700">PLAN</text>
|
|
76
|
+
</g>
|
|
77
|
+
|
|
78
|
+
<!-- Node 03: Review (produces, emits blockers count) -->
|
|
79
|
+
<g transform="translate(600 468)">
|
|
80
|
+
<circle r="14" fill="#1c1a17" stroke="#a8896c" stroke-width="2"/>
|
|
81
|
+
<circle r="5" fill="#a8896c"/>
|
|
82
|
+
<text class="mono text-distant" x="0" y="-36" font-size="11" letter-spacing="0.12em" text-anchor="middle">03 · produces</text>
|
|
83
|
+
<text class="mono ink" x="0" y="52" font-size="15" letter-spacing="0.12em" text-anchor="middle" font-weight="700">REVIEW</text>
|
|
84
|
+
</g>
|
|
85
|
+
|
|
86
|
+
<!-- Node 04: Gate (predicate routing — diamond) -->
|
|
87
|
+
<g transform="translate(800 468)">
|
|
88
|
+
<polygon points="0,-18 18,0 0,18 -18,0" fill="#1c1a17" stroke="#8a9580" stroke-width="2"/>
|
|
89
|
+
<circle r="3.5" fill="#8a9580"/>
|
|
90
|
+
<text class="mono text-distant" x="0" y="-40" font-size="11" letter-spacing="0.12em" text-anchor="middle">04 · gate</text>
|
|
91
|
+
<text class="mono sage" x="0" y="54" font-size="15" letter-spacing="0.12em" text-anchor="middle" font-weight="700">blockers_count</text>
|
|
92
|
+
</g>
|
|
93
|
+
|
|
94
|
+
<!-- Node 05: Commit (acts) -->
|
|
95
|
+
<g transform="translate(1000 468)">
|
|
96
|
+
<circle r="16" fill="#1c1a17" stroke="#8a9580" stroke-width="2"/>
|
|
97
|
+
<circle r="6" fill="#8a9580"/>
|
|
98
|
+
<text class="mono text-distant" x="0" y="-38" font-size="11" letter-spacing="0.12em" text-anchor="middle">05 · acts</text>
|
|
99
|
+
<text class="mono sage" x="0" y="54" font-size="15" letter-spacing="0.12em" text-anchor="middle" font-weight="700">COMMIT</text>
|
|
100
|
+
</g>
|
|
101
|
+
|
|
102
|
+
<!-- Stop terminator -->
|
|
103
|
+
<g transform="translate(1140 468)">
|
|
104
|
+
<rect x="-6" y="-9" width="12" height="18" fill="#5e6b56" stroke="#5e6b56" stroke-width="1"/>
|
|
105
|
+
<text class="mono text-distant" x="0" y="34" font-size="11" letter-spacing="0.18em" text-anchor="middle">STOP</text>
|
|
106
|
+
</g>
|
|
107
|
+
|
|
108
|
+
<!-- Caption -->
|
|
109
|
+
<text class="mono text-distant" x="640" y="660" font-size="12" letter-spacing="0.2em" text-anchor="middle">TYPED STAGES · PREDICATE ROUTING · TS SCRIPTING · .rpiv/workflows/</text>
|
|
110
|
+
|
|
111
|
+
<line x1="80" y1="740" x2="1200" y2="740" stroke="#2e2b27" stroke-width="1"/>
|
|
112
|
+
|
|
113
|
+
<text class="mono text-distant" x="80" y="776" font-size="12" letter-spacing="0.12em">
|
|
114
|
+
<tspan class="ochre" font-weight="700">▌ rpiv-workflow</tspan>
|
|
115
|
+
<tspan dx="12">│</tspan>
|
|
116
|
+
<tspan dx="12" class="sage">/wf · defineWorkflow() · gate()</tspan>
|
|
117
|
+
<tspan dx="12">│</tspan>
|
|
118
|
+
<tspan dx="12">npm:@juicesharp/rpiv-workflow</tspan>
|
|
119
|
+
</text>
|
|
120
|
+
</svg>
|