@osolmaz/pi-workflows 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -16
- package/dist/builtins/autodevise.workflow.d.ts +58 -0
- package/dist/builtins/autodevise.workflow.js +190 -0
- package/dist/builtins/autodevise.workflow.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +154 -0
- package/dist/builtins/autoimplement.workflow.js +729 -0
- package/dist/builtins/autoimplement.workflow.js.map +1 -0
- package/dist/builtins/catalog.js +5 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +3 -0
- package/dist/builtins/index.js +4 -0
- package/dist/builtins/index.js.map +1 -0
- package/dist/builtins/monitor.workflow.d.ts +25 -3
- package/dist/builtins/monitor.workflow.js +200 -13
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/render/graph-render.js +13 -2
- package/dist/render/graph-render.js.map +1 -1
- package/dist/workflows/catalog.d.ts +1 -0
- package/dist/workflows/catalog.js +6 -0
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.d.ts +45 -0
- package/dist/workflows/composition.js +471 -0
- package/dist/workflows/composition.js.map +1 -0
- package/dist/workflows/decision.d.ts +11 -5
- package/dist/workflows/decision.js.map +1 -1
- package/dist/workflows/definition.d.ts +22 -3
- package/dist/workflows/definition.js +46 -3
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.js +115 -16
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/graph.js +8 -6
- package/dist/workflows/graph.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +5 -4
- package/dist/workflows/loader.js +118 -18
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.d.ts +3 -1
- package/dist/workflows/schema.js +49 -2
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +32 -2
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +77 -2
- package/docs/CONTROLLERS.md +1 -1
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/MONITOR.md +35 -18
- package/docs/WORKFLOW_COMPOSITION.md +326 -0
- package/docs/plans/2026-08-19-workflow-composition-plan.md +300 -0
- package/docs/run-bundles.md +24 -10
- package/docs/workflows.md +65 -12
- package/examples/workflows/autodevise.workflow.ts +1 -0
- package/examples/workflows/autoimplement.workflow.ts +1 -92
- package/herdr-plugin.toml +1 -1
- package/package.json +5 -1
- package/skills/monitor/SKILL.md +6 -1
- package/skills/pi-workflows/SKILL.md +3 -1
- package/src/builtins/autodevise.workflow.ts +231 -0
- package/src/builtins/autoimplement.workflow.ts +856 -0
- package/src/builtins/catalog.ts +5 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/monitor.workflow.ts +242 -15
- package/src/render/graph-render.ts +14 -2
- package/src/workflows/catalog.ts +7 -0
- package/src/workflows/composition.ts +627 -0
- package/src/workflows/decision.ts +12 -5
- package/src/workflows/definition.ts +118 -8
- package/src/workflows/engine.ts +151 -18
- package/src/workflows/graph.ts +8 -6
- package/src/workflows/index.ts +20 -0
- package/src/workflows/loader.ts +186 -18
- package/src/workflows/schema.ts +62 -2
- package/src/workflows/store.ts +37 -2
- package/src/workflows/types.ts +109 -2
- package/examples/workflows/elegant-solution.workflow.ts +0 -95
|
@@ -53,6 +53,8 @@ A workflow is a `.workflow.ts`, `.workflow.js`, `.workflow.mts`, or `.workflow.m
|
|
|
53
53
|
Follow these rules:
|
|
54
54
|
|
|
55
55
|
- Compose the existing node and edge primitives before adding a new primitive.
|
|
56
|
+
- Reuse a finite workflow with a direct typed `includeWorkflow()` mount. Use a controller only when the child needs an independent run or indefinite reconciliation.
|
|
57
|
+
- Give included workflows named exits, map their input explicitly, and keep parent edges out of child internals.
|
|
56
58
|
- Keep `compute` pure. Put external effects in agent, function-action, or shell-action nodes.
|
|
57
59
|
- Use structured node outputs for routing.
|
|
58
60
|
- Use a checkpoint when progress requires human input.
|
|
@@ -62,7 +64,7 @@ Follow these rules:
|
|
|
62
64
|
- Keep presentation separate from execution. Use `presentationPrompt` only when a final assistant response is needed.
|
|
63
65
|
- Preserve the single active workflow rule in one Pi session.
|
|
64
66
|
|
|
65
|
-
Read [../../docs/workflows.md](../../docs/workflows.md) before creating or changing a workflow. Read [../../docs/DESIGN_PHILOSOPHY.md](../../docs/DESIGN_PHILOSOPHY.md) before adding public primitives. Use the examples under [../../examples/workflows](../../examples/workflows) as starting points.
|
|
67
|
+
Read [../../docs/workflows.md](../../docs/workflows.md) before creating or changing a workflow. Read [../../docs/WORKFLOW_COMPOSITION.md](../../docs/WORKFLOW_COMPOSITION.md) for nested workflows. Read [../../docs/DESIGN_PHILOSOPHY.md](../../docs/DESIGN_PHILOSOPHY.md) before adding public primitives. Use the examples under [../../examples/workflows](../../examples/workflows) as starting points.
|
|
66
68
|
|
|
67
69
|
## Verify changes
|
|
68
70
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { agent, compute, defineWorkflow } from "../workflows/definition.js";
|
|
3
|
+
|
|
4
|
+
export type AutodeviseInput = {
|
|
5
|
+
problem: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
constraints?: string[];
|
|
8
|
+
previousPlan?: unknown;
|
|
9
|
+
newEvidence?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AutodeviseReady = {
|
|
13
|
+
status: "ready";
|
|
14
|
+
frame: unknown;
|
|
15
|
+
proposal: unknown;
|
|
16
|
+
ideal: unknown;
|
|
17
|
+
selection: unknown;
|
|
18
|
+
plan: unknown;
|
|
19
|
+
planDigest: string;
|
|
20
|
+
previousPlanDigest?: string;
|
|
21
|
+
changed: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AutodeviseBlocked = {
|
|
25
|
+
status: "blocked";
|
|
26
|
+
frame: unknown;
|
|
27
|
+
proposal: unknown;
|
|
28
|
+
ideal: unknown;
|
|
29
|
+
selection: unknown;
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
34
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
35
|
+
throw new Error(`${label} must be an object`);
|
|
36
|
+
}
|
|
37
|
+
return value as Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireString(value: unknown, label: string): string {
|
|
41
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
42
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
43
|
+
}
|
|
44
|
+
return value.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseInput(value: unknown): AutodeviseInput {
|
|
48
|
+
const input = requireRecord(value, "autodevise input");
|
|
49
|
+
const constraints = input.constraints;
|
|
50
|
+
if (
|
|
51
|
+
constraints !== undefined &&
|
|
52
|
+
(!Array.isArray(constraints) || constraints.some((item) => typeof item !== "string"))
|
|
53
|
+
) {
|
|
54
|
+
throw new Error("autodevise constraints must be an array of strings");
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
problem: requireString(input.problem, "autodevise problem"),
|
|
58
|
+
...(input.scope !== undefined ? { scope: requireString(input.scope, "autodevise scope") } : {}),
|
|
59
|
+
...(constraints !== undefined ? { constraints: [...constraints] as string[] } : {}),
|
|
60
|
+
...(input.previousPlan !== undefined ? { previousPlan: input.previousPlan } : {}),
|
|
61
|
+
...(input.newEvidence !== undefined ? { newEvidence: input.newEvidence } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseSelection(value: unknown): Record<string, unknown> {
|
|
66
|
+
const selection = requireRecord(value, "autodevise selection");
|
|
67
|
+
if (selection.status !== "ready" && selection.status !== "blocked") {
|
|
68
|
+
throw new Error("autodevise selection status must be ready or blocked");
|
|
69
|
+
}
|
|
70
|
+
requireString(selection.selected, "autodevise selected solution");
|
|
71
|
+
requireString(selection.why, "autodevise selection reason");
|
|
72
|
+
if (selection.status === "blocked") requireString(selection.blocker, "autodevise blocker");
|
|
73
|
+
return selection;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function digest(value: unknown): string {
|
|
77
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const autodeviseWorkflow = defineWorkflow({
|
|
81
|
+
source: import.meta.url,
|
|
82
|
+
contractId: "pi-workflows.autodevise.v1",
|
|
83
|
+
name: "autodevise",
|
|
84
|
+
input: parseInput,
|
|
85
|
+
title: ({ input }) => `autodevise: ${input.problem.slice(0, 60)}`,
|
|
86
|
+
presentationPrompt: [
|
|
87
|
+
"Present the selected practical solution and its implementation plan.",
|
|
88
|
+
"Briefly state how the ideal informed the choice and what was excluded as outside scope.",
|
|
89
|
+
"Do not ask the user to choose between the options.",
|
|
90
|
+
].join("\n"),
|
|
91
|
+
startAt: "frame",
|
|
92
|
+
maxSteps: 10,
|
|
93
|
+
exits: {
|
|
94
|
+
ready: {
|
|
95
|
+
from: "finalize",
|
|
96
|
+
validate: (value: unknown): AutodeviseReady => value as AutodeviseReady,
|
|
97
|
+
},
|
|
98
|
+
blocked: {
|
|
99
|
+
from: "blocked",
|
|
100
|
+
validate: (value: unknown): AutodeviseBlocked => value as AutodeviseBlocked,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
nodes: {
|
|
104
|
+
frame: agent({
|
|
105
|
+
statusDetail: "framing the problem",
|
|
106
|
+
prompt: ({ input }) => {
|
|
107
|
+
const request = input as AutodeviseInput;
|
|
108
|
+
return [
|
|
109
|
+
`Frame this problem: ${request.problem}`,
|
|
110
|
+
`Authorized scope: ${request.scope ?? "infer it conservatively from the request and current project"}.`,
|
|
111
|
+
`Constraints: ${JSON.stringify(request.constraints ?? [])}.`,
|
|
112
|
+
`Previous plan: ${JSON.stringify(request.previousPlan ?? null)}.`,
|
|
113
|
+
`New evidence: ${JSON.stringify(request.newEvidence ?? null)}.`,
|
|
114
|
+
"Identify the goal, observable success criteria, systems in scope, systems outside scope, and interfaces we control.",
|
|
115
|
+
"Do not invent permission to change an upstream project, external service, or unrelated repository.",
|
|
116
|
+
].join("\n");
|
|
117
|
+
},
|
|
118
|
+
expectedOutput: `{ "problem": "concise statement", "success": ["criterion"], "inScope": ["change"], "outOfScope": ["change"], "constraints": ["constraint"], "controlBoundary": "what can change" }`,
|
|
119
|
+
validate: (value) => requireRecord(value, "autodevise frame"),
|
|
120
|
+
}),
|
|
121
|
+
propose: agent({
|
|
122
|
+
statusDetail: "devising a solution",
|
|
123
|
+
prompt: ({ outputs }) =>
|
|
124
|
+
[
|
|
125
|
+
"Devise the most elegant, long-term production-ready solution within the framed scope.",
|
|
126
|
+
"Prefer a small number of general parts, clear ownership boundaries, and existing public interfaces.",
|
|
127
|
+
"Avoid one-off mechanisms and unnecessary infrastructure.",
|
|
128
|
+
"Do not implement anything.",
|
|
129
|
+
`Problem frame: ${JSON.stringify(outputs.frame)}`,
|
|
130
|
+
].join("\n"),
|
|
131
|
+
expectedOutput: `{ "solution": "proposal", "rationale": "why", "parts": ["part"], "tradeoffs": ["trade-off"] }`,
|
|
132
|
+
validate: (value) => requireRecord(value, "autodevise proposal"),
|
|
133
|
+
}),
|
|
134
|
+
ideal: agent({
|
|
135
|
+
statusDetail: "describing the ideal end state",
|
|
136
|
+
prompt: ({ outputs, input }) =>
|
|
137
|
+
[
|
|
138
|
+
"Set the proposal aside and describe the holy grail for this problem.",
|
|
139
|
+
"The holy grail can match the proposal or exceed the current scope.",
|
|
140
|
+
"Name dependencies outside our authority instead of assuming they can change.",
|
|
141
|
+
"Explain the practical value beyond the proposal.",
|
|
142
|
+
`Problem frame: ${JSON.stringify(outputs.frame)}`,
|
|
143
|
+
`Proposal: ${JSON.stringify(outputs.propose)}`,
|
|
144
|
+
`New evidence: ${JSON.stringify((input as AutodeviseInput).newEvidence ?? null)}`,
|
|
145
|
+
].join("\n"),
|
|
146
|
+
expectedOutput: `{ "ideal": "ideal end state", "outsideDependencies": ["dependency"], "additionalValue": ["benefit"] }`,
|
|
147
|
+
validate: (value) => requireRecord(value, "autodevise ideal"),
|
|
148
|
+
}),
|
|
149
|
+
choose: agent({
|
|
150
|
+
statusDetail: "choosing the practical solution",
|
|
151
|
+
prompt: ({ outputs }) =>
|
|
152
|
+
[
|
|
153
|
+
"Choose the right solution without asking the user to decide.",
|
|
154
|
+
"Choose the ideal when it is production-ready, proportionate, in scope, and implementable through interfaces we control.",
|
|
155
|
+
"Otherwise choose the strongest practical in-scope solution with a clear path toward the ideal.",
|
|
156
|
+
"Do not block only because the ideal depends on work outside our authority.",
|
|
157
|
+
"Do not make an upstream change, unrelated repository, new service, or unapproved resource a requirement.",
|
|
158
|
+
"Prefer the simpler choice when options give materially equivalent results.",
|
|
159
|
+
"Return blocked only when no truthful in-scope solution can meet the success criteria.",
|
|
160
|
+
`Frame: ${JSON.stringify(outputs.frame)}`,
|
|
161
|
+
`Proposal: ${JSON.stringify(outputs.propose)}`,
|
|
162
|
+
`Ideal: ${JSON.stringify(outputs.ideal)}`,
|
|
163
|
+
].join("\n"),
|
|
164
|
+
expectedOutput: `{ "status": "ready" | "blocked", "selected": "solution", "why": "reason", "relationshipToIdeal": "relationship", "excluded": ["excluded work"], "compromises": ["compromise"], "blocker": "required only when blocked" }`,
|
|
165
|
+
validate: parseSelection,
|
|
166
|
+
}),
|
|
167
|
+
plan: agent({
|
|
168
|
+
timeoutMs: 30 * 60_000,
|
|
169
|
+
statusDetail: "writing the implementation plan",
|
|
170
|
+
prompt: ({ outputs, input }) =>
|
|
171
|
+
[
|
|
172
|
+
"Write a detailed implementation-ready plan for the selected solution.",
|
|
173
|
+
"Keep every step inside the framed scope and authority.",
|
|
174
|
+
"For each step, state what changes, where it changes, and how to verify it.",
|
|
175
|
+
"Include contract changes, compatibility boundaries, tests, rollout or migration work, and failure handling when they apply.",
|
|
176
|
+
"Use the new evidence to correct the previous plan when one exists.",
|
|
177
|
+
"Do not implement the plan.",
|
|
178
|
+
`Frame: ${JSON.stringify(outputs.frame)}`,
|
|
179
|
+
`Selection: ${JSON.stringify(outputs.choose)}`,
|
|
180
|
+
`Previous plan: ${JSON.stringify((input as AutodeviseInput).previousPlan ?? null)}`,
|
|
181
|
+
`New evidence: ${JSON.stringify((input as AutodeviseInput).newEvidence ?? null)}`,
|
|
182
|
+
].join("\n"),
|
|
183
|
+
expectedOutput: `{ "summary": "approach", "steps": [{ "change": "change", "where": "location", "verification": "evidence" }], "contracts": ["impact"], "tests": ["test"], "risks": [{ "risk": "risk", "mitigation": "mitigation" }], "boundaries": ["excluded work"] }`,
|
|
184
|
+
validate: (value) => requireRecord(value, "autodevise plan"),
|
|
185
|
+
}),
|
|
186
|
+
blocked: compute({
|
|
187
|
+
run: ({ outputs }) => {
|
|
188
|
+
const selection = outputs.choose as Record<string, unknown>;
|
|
189
|
+
return {
|
|
190
|
+
status: "blocked",
|
|
191
|
+
frame: outputs.frame,
|
|
192
|
+
proposal: outputs.propose,
|
|
193
|
+
ideal: outputs.ideal,
|
|
194
|
+
selection,
|
|
195
|
+
reason: requireString(selection.blocker, "autodevise blocker"),
|
|
196
|
+
} satisfies AutodeviseBlocked;
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
finalize: compute({
|
|
200
|
+
run: ({ outputs, input }) => {
|
|
201
|
+
const request = input as AutodeviseInput;
|
|
202
|
+
const planDigest = digest(outputs.plan);
|
|
203
|
+
const previousPlanDigest =
|
|
204
|
+
request.previousPlan === undefined ? undefined : digest(request.previousPlan);
|
|
205
|
+
return {
|
|
206
|
+
status: "ready",
|
|
207
|
+
frame: outputs.frame,
|
|
208
|
+
proposal: outputs.propose,
|
|
209
|
+
ideal: outputs.ideal,
|
|
210
|
+
selection: outputs.choose,
|
|
211
|
+
plan: outputs.plan,
|
|
212
|
+
planDigest,
|
|
213
|
+
...(previousPlanDigest !== undefined ? { previousPlanDigest } : {}),
|
|
214
|
+
changed: previousPlanDigest === undefined || previousPlanDigest !== planDigest,
|
|
215
|
+
} satisfies AutodeviseReady;
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
edges: [
|
|
220
|
+
{ from: "frame", to: "propose" },
|
|
221
|
+
{ from: "propose", to: "ideal" },
|
|
222
|
+
{ from: "ideal", to: "choose" },
|
|
223
|
+
{
|
|
224
|
+
from: "choose",
|
|
225
|
+
switch: { on: "$.status", cases: { ready: "plan", blocked: "blocked" } },
|
|
226
|
+
},
|
|
227
|
+
{ from: "plan", to: "finalize" },
|
|
228
|
+
],
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export default autodeviseWorkflow;
|