workplane 0.2.0 → 0.3.1
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 +127 -34
- package/dist/cli.js +430 -1
- package/dist/cli.js.map +1 -1
- package/dist/node.js.map +1 -1
- package/dist/server.js +174 -92
- package/dist/server.js.map +1 -1
- package/package.json +5 -6
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# workplane
|
|
2
2
|
|
|
3
|
-
[](https://mindstaq.github.io/workplane/)
|
|
4
4
|
[](https://www.npmjs.com/package/workplane)
|
|
5
|
-
[](https://github.com/MindStaq/workplane)
|
|
6
6
|
[](https://github.com/MindStaq/workplane/blob/main/packages/workplane/LICENSE)
|
|
7
7
|
|
|
8
|
-
> **
|
|
8
|
+
> **Alpha software.** APIs and data models are unstable and will change between releases. Not recommended for production use.
|
|
9
9
|
|
|
10
|
-
Route durable work
|
|
10
|
+
Route durable work to capable nodes on your private network. Compose multi-step AI workplans that mix local Ollama with frontier APIs. Supports shell commands, local inference, and AI coding agents (Codex, Claude Code, Aider) — both **batch** (one-shot) and **interactive** (multi-turn PTY/stdin sessions mediated through the control plane with no direct client-to-node connection).
|
|
11
11
|
|
|
12
|
-
Workplane is a control plane + polling node runtime backed by Postgres
|
|
12
|
+
Workplane is a control plane + polling node runtime backed by Postgres. You submit tasks from a CLI; nodes pull work, run it in isolated workspaces, and stream logs and artifacts back.
|
|
13
13
|
|
|
14
14
|
**Requires Node.js 20+** and **Postgres**.
|
|
15
15
|
|
|
@@ -54,7 +54,7 @@ workplane-node
|
|
|
54
54
|
**4. Submit a task** (fourth terminal):
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
export WORKPLANE_OPERATOR_TOKEN=your-operator-secret
|
|
57
|
+
export WORKPLANE_OPERATOR_TOKEN=your-operator-secret
|
|
58
58
|
|
|
59
59
|
workplane task submit shell --command "echo hello"
|
|
60
60
|
workplane tasks
|
|
@@ -64,18 +64,18 @@ workplane logs <runId>
|
|
|
64
64
|
|
|
65
65
|
## Authentication
|
|
66
66
|
|
|
67
|
-
When `WORKPLANE_NODE_TOKEN` is set on the **server**, every node must use the same value. When `WORKPLANE_OPERATOR_TOKEN` is set, the CLI must send it for `task submit`, `retry`, and `cancel`.
|
|
68
|
-
|
|
69
67
|
| Variable | Used by |
|
|
70
68
|
|----------|---------|
|
|
71
|
-
| `WORKPLANE_NODE_TOKEN` | `workplane-node` |
|
|
72
|
-
| `WORKPLANE_OPERATOR_TOKEN` | `workplane` CLI (
|
|
69
|
+
| `WORKPLANE_NODE_TOKEN` | `workplane-node` (register, poll, report) |
|
|
70
|
+
| `WORKPLANE_OPERATOR_TOKEN` | `workplane` CLI (task submit / retry / cancel / send input) |
|
|
73
71
|
|
|
74
|
-
Read-only CLI commands (`tasks`, `logs`, `runs`, …) work without the operator token
|
|
72
|
+
Read-only CLI commands (`tasks`, `logs`, `runs`, …) work without the operator token.
|
|
75
73
|
|
|
76
74
|
## CLI examples
|
|
77
75
|
|
|
78
76
|
```bash
|
|
77
|
+
export WORKPLANE_OPERATOR_TOKEN=...
|
|
78
|
+
|
|
79
79
|
# Shell
|
|
80
80
|
workplane task submit shell --command "npm test" \
|
|
81
81
|
--repo https://github.com/you/your-repo.git
|
|
@@ -83,19 +83,33 @@ workplane task submit shell --command "npm test" \
|
|
|
83
83
|
# Local inference (node must advertise ollama capability)
|
|
84
84
|
workplane task submit inference --model llama3.2 --prompt "Say hello"
|
|
85
85
|
|
|
86
|
-
#
|
|
87
|
-
workplane task submit harness --harness
|
|
88
|
-
--repo git@github.com:you/app.git \
|
|
89
|
-
--
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
--prompt "
|
|
94
|
-
--
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
workplane
|
|
86
|
+
# Batch harness (one-shot)
|
|
87
|
+
workplane task submit harness --harness claude-code \
|
|
88
|
+
--repo git@github.com:you/app.git --prompt "Refactor auth middleware" \
|
|
89
|
+
--requires claude-code,git
|
|
90
|
+
|
|
91
|
+
# Interactive harness (multi-turn PTY/stdin session)
|
|
92
|
+
workplane task submit harness --harness claude-code \
|
|
93
|
+
--repo git@github.com:you/app.git --prompt "Start exploring the codebase" \
|
|
94
|
+
--interactive --requires claude-code,git
|
|
95
|
+
|
|
96
|
+
# Send input to a running interactive session
|
|
97
|
+
workplane run input <runId> --stdin "Focus on the auth module"
|
|
98
|
+
workplane run input <runId> --signal SIGINT
|
|
99
|
+
workplane run input <runId> --resize 220x50
|
|
100
|
+
|
|
101
|
+
# Skills (v0.3.0)
|
|
102
|
+
workplane skill list
|
|
103
|
+
workplane skill run code-review --repo . --model claude-haiku-4-5-20251001
|
|
104
|
+
|
|
105
|
+
# Inspect
|
|
106
|
+
workplane tasks [--status running]
|
|
107
|
+
workplane runs --task-id <taskId>
|
|
108
|
+
workplane run show <runId>
|
|
109
|
+
workplane logs <runId>
|
|
98
110
|
workplane artifacts <runId>
|
|
111
|
+
workplane task cancel <taskId>
|
|
112
|
+
workplane task retry <taskId>
|
|
99
113
|
```
|
|
100
114
|
|
|
101
115
|
## Node capabilities
|
|
@@ -109,27 +123,106 @@ workplane-node
|
|
|
109
123
|
|
|
110
124
|
Tasks declare `requires`; the control plane assigns work to a node whose capabilities are a superset.
|
|
111
125
|
|
|
112
|
-
| Adapter |
|
|
113
|
-
|
|
114
|
-
| Shell | `shell` |
|
|
115
|
-
| Ollama | `ollama` |
|
|
116
|
-
| Aider | `
|
|
117
|
-
| Codex | `
|
|
118
|
-
| Claude Code | `
|
|
126
|
+
| Adapter | Capability tags | Modes |
|
|
127
|
+
|---------|----------------|-------|
|
|
128
|
+
| Shell | `shell` | batch |
|
|
129
|
+
| Ollama | `ollama` | batch |
|
|
130
|
+
| Aider | `aider`, `git` | batch |
|
|
131
|
+
| Codex | `codex`, `git` | batch, interactive (stdio) |
|
|
132
|
+
| Claude Code | `claude-code`, `git` | batch, interactive (PTY) |
|
|
119
133
|
|
|
120
134
|
Override harness binaries: `WORKPLANE_CODEX_BIN`, `WORKPLANE_CLAUDE_CODE_BIN`.
|
|
121
135
|
|
|
136
|
+
## Workplans (v0.3.0)
|
|
137
|
+
|
|
138
|
+
Compose multi-step AI pipelines with the `@workplane/workplans` DSL:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { SequentialWorkplanRunner, LocalWorkplanContext } from "@workplane/workplans";
|
|
142
|
+
|
|
143
|
+
const plan = {
|
|
144
|
+
id: "review", name: "Code Review",
|
|
145
|
+
steps: [
|
|
146
|
+
{
|
|
147
|
+
id: "diff", adapter: "shell", provider: "shell",
|
|
148
|
+
payload: { command: "git diff HEAD~1", cwd: "./my-repo" },
|
|
149
|
+
output: { dest: "next" },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "summarize", adapter: "ollama", provider: "ollama", model: "llama3",
|
|
153
|
+
payload: { prompt: "Summarize these changes:\n{{prevOutput}}" },
|
|
154
|
+
output: { dest: "next" },
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "critique", adapter: "anthropic", provider: "anthropic",
|
|
158
|
+
model: "claude-haiku-4-5-20251001",
|
|
159
|
+
payload: { prompt: "Review for correctness and security:\n{{prevOutput}}" },
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
await new SequentialWorkplanRunner().run(plan, new LocalWorkplanContext());
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Inline providers (`anthropic`, `openai`, `ollama`, `shell`, `file`) run without dispatching to a fleet node.
|
|
168
|
+
|
|
169
|
+
## Agent skills (v0.3.0)
|
|
170
|
+
|
|
171
|
+
Pre-built workplans via `@workplane/agent-skills`:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { createDefaultRegistry } from "@workplane/agent-skills";
|
|
175
|
+
import { SequentialWorkplanRunner, LocalWorkplanContext } from "@workplane/workplans";
|
|
176
|
+
|
|
177
|
+
const skill = createDefaultRegistry().get("code-review");
|
|
178
|
+
const plan = skill.buildPlan({ repo: ".", model: "claude-haiku-4-5-20251001" });
|
|
179
|
+
await new SequentialWorkplanRunner().run(plan, new LocalWorkplanContext());
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Or via CLI: `workplane skill run code-review --repo . --model claude-haiku-4-5-20251001`
|
|
183
|
+
|
|
184
|
+
## Library packages
|
|
185
|
+
|
|
186
|
+
All packages are published under `@workplane/` and can be imported independently:
|
|
187
|
+
|
|
188
|
+
| Package | Purpose |
|
|
189
|
+
|---------|---------|
|
|
190
|
+
| `@workplane/workplans` | Workplan DSL, sequential runner, inline providers |
|
|
191
|
+
| `@workplane/agent-skills` | Pre-built skills, SkillRegistry, CanonicalSkillWorkflow |
|
|
192
|
+
| `@workplane/adapter-sdk` | Build custom adapters — WorkAdapter, WorkContext, cancellable exec |
|
|
193
|
+
| `@workplane/types` | Shared TypeScript types |
|
|
194
|
+
| `@workplane/core` | Config, HTTP client, auth, git utilities |
|
|
195
|
+
| `@workplane/adapter-shell` | Shell command adapter |
|
|
196
|
+
| `@workplane/adapter-ollama` | Ollama inference adapter |
|
|
197
|
+
| `@workplane/adapter-aider` | Aider coding agent adapter |
|
|
198
|
+
| `@workplane/adapter-harness` | Base harness adapter (extended by codex/claude-code) |
|
|
199
|
+
| `@workplane/adapter-claude-code` | Claude Code adapter (PTY interactive) |
|
|
200
|
+
| `@workplane/adapter-codex` | Codex adapter (stdio interactive) |
|
|
201
|
+
| `@workplane/dbos` | Optional DBOS durability layer (step checkpointing + replay) |
|
|
202
|
+
|
|
203
|
+
## DBOS (optional)
|
|
204
|
+
|
|
205
|
+
DBOS durability is opt-in. The server boots without it by default:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Default — no DBOS
|
|
209
|
+
workplane-server
|
|
210
|
+
|
|
211
|
+
# With DBOS durability
|
|
212
|
+
WORKPLANE_USE_DBOS=true workplane-server
|
|
213
|
+
```
|
|
214
|
+
|
|
122
215
|
## Personal fleet (home + office)
|
|
123
216
|
|
|
124
|
-
Run the control plane on an always-on host (VPS, NAS, or home server). Run nodes on each machine where tools or GPUs live.
|
|
217
|
+
Run the control plane on an always-on host (VPS, NAS, or home server). Run nodes on each machine where tools or GPUs live. Nodes poll outbound — no inbound ports required. Use Tailscale, WireGuard, or a trusted LAN.
|
|
125
218
|
|
|
126
|
-
Set `WORKPLANE_SERVER_URL` to the control plane
|
|
219
|
+
Set `WORKPLANE_SERVER_URL` to the control plane's reachable address on every node and CLI client.
|
|
127
220
|
|
|
128
|
-
Full deployment guide: [
|
|
221
|
+
Full deployment guide: [docs/deployment/FLEET.md](https://github.com/MindStaq/workplane/blob/main/docs/deployment/FLEET.md)
|
|
129
222
|
|
|
130
223
|
## Documentation
|
|
131
224
|
|
|
132
|
-
- [v0.
|
|
225
|
+
- [v0.3.0 specification](https://github.com/MindStaq/workplane/blob/main/docs/specs/v0.3.0/WORKPLANE_SPEC.md)
|
|
133
226
|
- [Source repository](https://github.com/MindStaq/workplane)
|
|
134
227
|
- [Issues](https://github.com/MindStaq/workplane/issues)
|
|
135
228
|
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,110 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../workplans/src/providers/anthropic.ts
|
|
13
|
+
var anthropic_exports = {};
|
|
14
|
+
__export(anthropic_exports, {
|
|
15
|
+
runAnthropicStep: () => runAnthropicStep
|
|
16
|
+
});
|
|
17
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
18
|
+
async function runAnthropicStep(opts) {
|
|
19
|
+
const client = new Anthropic({ apiKey: opts.apiKey ?? process.env.ANTHROPIC_API_KEY });
|
|
20
|
+
const message = await client.messages.create({
|
|
21
|
+
model: opts.model,
|
|
22
|
+
max_tokens: 4096,
|
|
23
|
+
messages: [{ role: "user", content: opts.prompt }]
|
|
24
|
+
});
|
|
25
|
+
const block = message.content[0];
|
|
26
|
+
return block.type === "text" ? block.text : "";
|
|
27
|
+
}
|
|
28
|
+
var init_anthropic = __esm({
|
|
29
|
+
"../workplans/src/providers/anthropic.ts"() {
|
|
30
|
+
"use strict";
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ../workplans/src/providers/openai.ts
|
|
35
|
+
var openai_exports = {};
|
|
36
|
+
__export(openai_exports, {
|
|
37
|
+
runOpenAIStep: () => runOpenAIStep
|
|
38
|
+
});
|
|
39
|
+
import OpenAI from "openai";
|
|
40
|
+
async function runOpenAIStep(opts) {
|
|
41
|
+
const client = new OpenAI({ apiKey: opts.apiKey ?? process.env.OPENAI_API_KEY });
|
|
42
|
+
const completion = await client.chat.completions.create({
|
|
43
|
+
model: opts.model,
|
|
44
|
+
messages: [{ role: "user", content: opts.prompt }]
|
|
45
|
+
});
|
|
46
|
+
return completion.choices[0]?.message.content ?? "";
|
|
47
|
+
}
|
|
48
|
+
var init_openai = __esm({
|
|
49
|
+
"../workplans/src/providers/openai.ts"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ../workplans/src/providers/ollama.ts
|
|
55
|
+
var ollama_exports = {};
|
|
56
|
+
__export(ollama_exports, {
|
|
57
|
+
runOllamaStep: () => runOllamaStep
|
|
58
|
+
});
|
|
59
|
+
import { Ollama } from "ollama";
|
|
60
|
+
async function runOllamaStep(opts) {
|
|
61
|
+
const host = opts.host ?? process.env.OLLAMA_HOST ?? "http://localhost:11434";
|
|
62
|
+
const client = new Ollama({ host });
|
|
63
|
+
const response = await client.generate({ model: opts.model, prompt: opts.prompt });
|
|
64
|
+
return response.response;
|
|
65
|
+
}
|
|
66
|
+
var init_ollama = __esm({
|
|
67
|
+
"../workplans/src/providers/ollama.ts"() {
|
|
68
|
+
"use strict";
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ../workplans/src/providers/shell.ts
|
|
73
|
+
var shell_exports = {};
|
|
74
|
+
__export(shell_exports, {
|
|
75
|
+
runShellStep: () => runShellStep
|
|
76
|
+
});
|
|
77
|
+
import { exec } from "child_process";
|
|
78
|
+
import { promisify } from "util";
|
|
79
|
+
async function runShellStep(opts) {
|
|
80
|
+
const { stdout } = await execAsync(opts.command, {
|
|
81
|
+
cwd: opts.cwd,
|
|
82
|
+
maxBuffer: 10 * 1024 * 1024
|
|
83
|
+
});
|
|
84
|
+
return stdout;
|
|
85
|
+
}
|
|
86
|
+
var execAsync;
|
|
87
|
+
var init_shell = __esm({
|
|
88
|
+
"../workplans/src/providers/shell.ts"() {
|
|
89
|
+
"use strict";
|
|
90
|
+
execAsync = promisify(exec);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ../workplans/src/providers/file.ts
|
|
95
|
+
var file_exports = {};
|
|
96
|
+
__export(file_exports, {
|
|
97
|
+
runFileStep: () => runFileStep
|
|
98
|
+
});
|
|
99
|
+
import { readFile } from "fs/promises";
|
|
100
|
+
async function runFileStep(opts) {
|
|
101
|
+
return readFile(opts.path, opts.encoding ?? "utf8");
|
|
102
|
+
}
|
|
103
|
+
var init_file = __esm({
|
|
104
|
+
"../workplans/src/providers/file.ts"() {
|
|
105
|
+
"use strict";
|
|
106
|
+
}
|
|
107
|
+
});
|
|
2
108
|
|
|
3
109
|
// ../core/src/env.ts
|
|
4
110
|
import { config as dotenvConfig } from "dotenv";
|
|
@@ -64,6 +170,292 @@ async function workplaneFetch(serverUrl2, path, options = {}) {
|
|
|
64
170
|
return await response.json();
|
|
65
171
|
}
|
|
66
172
|
|
|
173
|
+
// ../agent-skills/src/skills/code-review.ts
|
|
174
|
+
function codeReviewPlan(opts = {}) {
|
|
175
|
+
const { repoPath = ".", model = "claude-haiku-4-5-20251001", branch } = opts;
|
|
176
|
+
const diffCmd = branch ? `git diff ${branch}..HEAD` : "git diff HEAD~1";
|
|
177
|
+
return {
|
|
178
|
+
id: "code-review",
|
|
179
|
+
name: "Code Review",
|
|
180
|
+
steps: [
|
|
181
|
+
{
|
|
182
|
+
id: "diff",
|
|
183
|
+
name: "Git Diff",
|
|
184
|
+
adapter: "shell",
|
|
185
|
+
provider: "shell",
|
|
186
|
+
payload: { command: diffCmd, cwd: repoPath },
|
|
187
|
+
output: { dest: "next" }
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "summarize",
|
|
191
|
+
name: "Summarize",
|
|
192
|
+
adapter: "ollama",
|
|
193
|
+
provider: "ollama",
|
|
194
|
+
model: "llama3",
|
|
195
|
+
payload: { prompt: "Summarize these code changes concisely:\n{{prevOutput}}" },
|
|
196
|
+
output: { dest: "next" }
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "critique",
|
|
200
|
+
name: "Critique",
|
|
201
|
+
adapter: "anthropic",
|
|
202
|
+
provider: "anthropic",
|
|
203
|
+
model,
|
|
204
|
+
payload: { prompt: "Review this summary for correctness, security issues, and code quality:\n{{prevOutput}}" }
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ../agent-skills/src/skills/summarize-file.ts
|
|
211
|
+
function summarizeFilePlan(opts) {
|
|
212
|
+
const { filePath, provider = "ollama", model = "llama3" } = opts;
|
|
213
|
+
return {
|
|
214
|
+
id: "summarize-file",
|
|
215
|
+
name: "Summarize File",
|
|
216
|
+
steps: [
|
|
217
|
+
{
|
|
218
|
+
id: "read",
|
|
219
|
+
name: "Read File",
|
|
220
|
+
adapter: "file",
|
|
221
|
+
provider: "file",
|
|
222
|
+
payload: { path: filePath },
|
|
223
|
+
output: { dest: "next" }
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "summarize",
|
|
227
|
+
name: "Summarize",
|
|
228
|
+
adapter: provider,
|
|
229
|
+
provider,
|
|
230
|
+
model,
|
|
231
|
+
payload: { prompt: "Summarize the following file content concisely:\n{{prevOutput}}" }
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ../agent-skills/src/skills/index.ts
|
|
238
|
+
var SkillRegistry = class {
|
|
239
|
+
skills = /* @__PURE__ */ new Map();
|
|
240
|
+
register(entry) {
|
|
241
|
+
this.skills.set(entry.name, entry);
|
|
242
|
+
}
|
|
243
|
+
get(name) {
|
|
244
|
+
return this.skills.get(name);
|
|
245
|
+
}
|
|
246
|
+
list() {
|
|
247
|
+
return Array.from(this.skills.values());
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
function createDefaultRegistry() {
|
|
251
|
+
const registry = new SkillRegistry();
|
|
252
|
+
registry.register({
|
|
253
|
+
name: "code-review",
|
|
254
|
+
description: "Git diff \u2192 local summarize (ollama) \u2192 frontier critique (anthropic)",
|
|
255
|
+
buildPlan: (opts) => codeReviewPlan({
|
|
256
|
+
repoPath: typeof opts.repo === "string" ? opts.repo : void 0,
|
|
257
|
+
model: typeof opts.model === "string" ? opts.model : void 0,
|
|
258
|
+
branch: typeof opts.branch === "string" ? opts.branch : void 0
|
|
259
|
+
})
|
|
260
|
+
});
|
|
261
|
+
registry.register({
|
|
262
|
+
name: "summarize-file",
|
|
263
|
+
description: "Read file \u2192 summarize with configurable provider (default: ollama)",
|
|
264
|
+
buildPlan: (opts) => {
|
|
265
|
+
const filePath = typeof opts.file === "string" ? opts.file : "";
|
|
266
|
+
if (!filePath) throw new Error("--file is required for summarize-file");
|
|
267
|
+
return summarizeFilePlan({
|
|
268
|
+
filePath,
|
|
269
|
+
provider: typeof opts.provider === "string" ? opts.provider : void 0,
|
|
270
|
+
model: typeof opts.model === "string" ? opts.model : void 0
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return registry;
|
|
275
|
+
}
|
|
276
|
+
function listSkills(registry) {
|
|
277
|
+
for (const skill of registry.list()) {
|
|
278
|
+
process.stdout.write(` ${skill.name.padEnd(20)} ${skill.description}
|
|
279
|
+
`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ../workplans/src/template.ts
|
|
284
|
+
function applyTemplate(payload, prevOutput) {
|
|
285
|
+
return Object.fromEntries(
|
|
286
|
+
Object.entries(payload).map(([k, v]) => [
|
|
287
|
+
k,
|
|
288
|
+
typeof v === "string" ? v.replaceAll("{{prevOutput}}", prevOutput) : v
|
|
289
|
+
])
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ../workplans/src/runner.ts
|
|
294
|
+
var INLINE_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "openai", "ollama", "shell", "file"]);
|
|
295
|
+
async function runInlineStep(step, prompt) {
|
|
296
|
+
const provider = step.provider;
|
|
297
|
+
if (provider === "anthropic") {
|
|
298
|
+
const { runAnthropicStep: runAnthropicStep2 } = await Promise.resolve().then(() => (init_anthropic(), anthropic_exports));
|
|
299
|
+
return runAnthropicStep2({ model: step.model ?? "claude-haiku-4-5-20251001", prompt });
|
|
300
|
+
}
|
|
301
|
+
if (provider === "openai") {
|
|
302
|
+
const { runOpenAIStep: runOpenAIStep2 } = await Promise.resolve().then(() => (init_openai(), openai_exports));
|
|
303
|
+
return runOpenAIStep2({ model: step.model ?? "gpt-4o-mini", prompt });
|
|
304
|
+
}
|
|
305
|
+
if (provider === "ollama") {
|
|
306
|
+
const { runOllamaStep: runOllamaStep2 } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
|
|
307
|
+
return runOllamaStep2({ model: step.model ?? "llama3", prompt });
|
|
308
|
+
}
|
|
309
|
+
if (provider === "shell") {
|
|
310
|
+
const { runShellStep: runShellStep2 } = await Promise.resolve().then(() => (init_shell(), shell_exports));
|
|
311
|
+
return runShellStep2({
|
|
312
|
+
command: String(step.payload.command ?? ""),
|
|
313
|
+
cwd: typeof step.payload.cwd === "string" ? step.payload.cwd : void 0
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (provider === "file") {
|
|
317
|
+
const { runFileStep: runFileStep2 } = await Promise.resolve().then(() => (init_file(), file_exports));
|
|
318
|
+
return runFileStep2({ path: String(step.payload.path ?? "") });
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`unknown inline provider: ${provider}`);
|
|
321
|
+
}
|
|
322
|
+
var SequentialWorkplanRunner = class {
|
|
323
|
+
async run(plan, ctx) {
|
|
324
|
+
const results = [];
|
|
325
|
+
let prevOutput = "";
|
|
326
|
+
let planSucceeded = true;
|
|
327
|
+
for (const step of plan.steps) {
|
|
328
|
+
const payload = prevOutput ? applyTemplate(step.payload, prevOutput) : step.payload;
|
|
329
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
330
|
+
const start = Date.now();
|
|
331
|
+
let output = "";
|
|
332
|
+
let exitCode = 0;
|
|
333
|
+
try {
|
|
334
|
+
if (step.provider && INLINE_PROVIDERS.has(step.provider)) {
|
|
335
|
+
output = await runInlineStep({ ...step, payload }, prompt);
|
|
336
|
+
} else {
|
|
337
|
+
const node = await ctx.resolveNode(step.requires ?? [], step.provider);
|
|
338
|
+
const result = await node.execute({ ...step, payload });
|
|
339
|
+
output = result.output;
|
|
340
|
+
exitCode = result.exitCode;
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
exitCode = 1;
|
|
344
|
+
output = err instanceof Error ? err.message : String(err);
|
|
345
|
+
planSucceeded = false;
|
|
346
|
+
const stepResult2 = {
|
|
347
|
+
stepId: step.id,
|
|
348
|
+
output,
|
|
349
|
+
exitCode,
|
|
350
|
+
durationMs: Date.now() - start
|
|
351
|
+
};
|
|
352
|
+
results.push(stepResult2);
|
|
353
|
+
ctx.emitResult(step.id, step.name, output);
|
|
354
|
+
if (!step.continueOnError) {
|
|
355
|
+
return {
|
|
356
|
+
planId: plan.id,
|
|
357
|
+
status: "step_failed",
|
|
358
|
+
steps: results,
|
|
359
|
+
succeeded: false
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
prevOutput = "";
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const dest = step.output?.dest ?? "stdout";
|
|
366
|
+
const stepResult = {
|
|
367
|
+
stepId: step.id,
|
|
368
|
+
output,
|
|
369
|
+
exitCode,
|
|
370
|
+
durationMs: Date.now() - start
|
|
371
|
+
};
|
|
372
|
+
results.push(stepResult);
|
|
373
|
+
if (dest === "stdout" || dest === "artifact") {
|
|
374
|
+
ctx.emitResult(step.id, step.name, output);
|
|
375
|
+
prevOutput = "";
|
|
376
|
+
} else if (dest === "next") {
|
|
377
|
+
prevOutput = output;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
planId: plan.id,
|
|
382
|
+
status: planSucceeded ? "completed" : "failed",
|
|
383
|
+
steps: results,
|
|
384
|
+
succeeded: planSucceeded
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// ../workplans/src/sinks/stdout.ts
|
|
390
|
+
function printStepResult(stepName, output) {
|
|
391
|
+
process.stdout.write(`=== step: ${stepName} ===
|
|
392
|
+
${output}
|
|
393
|
+
|
|
394
|
+
`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ../workplans/src/context.ts
|
|
398
|
+
var WorkplaneNodeHandle = class {
|
|
399
|
+
constructor(serverUrl2, nodeToken) {
|
|
400
|
+
this.serverUrl = serverUrl2;
|
|
401
|
+
this.nodeToken = nodeToken;
|
|
402
|
+
}
|
|
403
|
+
serverUrl;
|
|
404
|
+
nodeToken;
|
|
405
|
+
async execute(step) {
|
|
406
|
+
const start = Date.now();
|
|
407
|
+
const taskRes = await fetch(`${this.serverUrl}/tasks`, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: {
|
|
410
|
+
"content-type": "application/json",
|
|
411
|
+
authorization: `Bearer ${this.nodeToken}`
|
|
412
|
+
},
|
|
413
|
+
body: JSON.stringify({
|
|
414
|
+
kind: "workplan.step",
|
|
415
|
+
adapter: step.adapter,
|
|
416
|
+
payload: step.payload,
|
|
417
|
+
requires: step.requires ?? []
|
|
418
|
+
})
|
|
419
|
+
});
|
|
420
|
+
if (!taskRes.ok) {
|
|
421
|
+
throw new Error(`failed to submit step ${step.id}: ${taskRes.status}`);
|
|
422
|
+
}
|
|
423
|
+
const task = await taskRes.json();
|
|
424
|
+
const output = await this.pollUntilDone(task.id);
|
|
425
|
+
return { stepId: step.id, output, exitCode: 0, durationMs: Date.now() - start };
|
|
426
|
+
}
|
|
427
|
+
async pollUntilDone(taskId) {
|
|
428
|
+
const terminalStatuses = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
|
|
429
|
+
for (; ; ) {
|
|
430
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
431
|
+
const res = await fetch(`${this.serverUrl}/tasks/${taskId}`, {
|
|
432
|
+
headers: { authorization: `Bearer ${this.nodeToken}` }
|
|
433
|
+
});
|
|
434
|
+
const task = await res.json();
|
|
435
|
+
if (terminalStatuses.has(task.status)) {
|
|
436
|
+
if (task.status !== "succeeded") {
|
|
437
|
+
throw new Error(`step task ${taskId} ended with status: ${task.status}`);
|
|
438
|
+
}
|
|
439
|
+
return typeof task.payload?.output === "string" ? task.payload.output : "";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
var LocalWorkplanContext = class {
|
|
445
|
+
serverUrl;
|
|
446
|
+
nodeToken;
|
|
447
|
+
constructor(opts = {}) {
|
|
448
|
+
this.serverUrl = opts.serverUrl ?? process.env.WORKPLANE_SERVER_URL ?? "http://localhost:8787";
|
|
449
|
+
this.nodeToken = opts.nodeToken ?? process.env.WORKPLANE_NODE_TOKEN ?? "";
|
|
450
|
+
}
|
|
451
|
+
async resolveNode(_requires, _provider) {
|
|
452
|
+
return new WorkplaneNodeHandle(this.serverUrl, this.nodeToken);
|
|
453
|
+
}
|
|
454
|
+
emitResult(_stepId, stepName, output) {
|
|
455
|
+
printStepResult(stepName, output);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
67
459
|
// ../cli/src/index.ts
|
|
68
460
|
loadLocalEnv();
|
|
69
461
|
var serverUrl = process.env.WORKPLANE_SERVER_URL ?? "http://localhost:8787";
|
|
@@ -120,7 +512,9 @@ function usage() {
|
|
|
120
512
|
" run input <runId> --signal <SIGTERM|SIGKILL|SIGINT>",
|
|
121
513
|
" run input <runId> --resize <COLSxROWS>",
|
|
122
514
|
" logs <runId|taskId>",
|
|
123
|
-
" artifacts <runId>"
|
|
515
|
+
" artifacts <runId>",
|
|
516
|
+
" skill list",
|
|
517
|
+
" skill run <name> [--key value ...]"
|
|
124
518
|
].join("\n")
|
|
125
519
|
);
|
|
126
520
|
}
|
|
@@ -364,6 +758,41 @@ async function main() {
|
|
|
364
758
|
printJson(artifacts);
|
|
365
759
|
return;
|
|
366
760
|
}
|
|
761
|
+
if (command === "skill" && subcommand === "list") {
|
|
762
|
+
const registry = createDefaultRegistry();
|
|
763
|
+
process.stdout.write("Available skills:\n");
|
|
764
|
+
listSkills(registry);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (command === "skill" && subcommand === "run") {
|
|
768
|
+
const skillName = rest[0];
|
|
769
|
+
if (!skillName) {
|
|
770
|
+
throw new Error("skill name is required");
|
|
771
|
+
}
|
|
772
|
+
const registry = createDefaultRegistry();
|
|
773
|
+
const skill = registry.get(skillName);
|
|
774
|
+
if (!skill) {
|
|
775
|
+
throw new Error(`unknown skill: ${skillName}. Run 'workplane skill list' to see available skills.`);
|
|
776
|
+
}
|
|
777
|
+
const optArgs = rest.slice(1);
|
|
778
|
+
const options = {};
|
|
779
|
+
for (let i = 0; i < optArgs.length - 1; i++) {
|
|
780
|
+
if (optArgs[i].startsWith("--")) {
|
|
781
|
+
options[optArgs[i].slice(2)] = optArgs[i + 1];
|
|
782
|
+
i++;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const plan = skill.buildPlan(options);
|
|
786
|
+
const runner = new SequentialWorkplanRunner();
|
|
787
|
+
const ctx = new LocalWorkplanContext();
|
|
788
|
+
const result = await runner.run(plan, ctx);
|
|
789
|
+
if (!result.succeeded) {
|
|
790
|
+
process.stderr.write(`skill '${skillName}' failed at step '${result.steps.at(-1)?.stepId ?? "unknown"}'
|
|
791
|
+
`);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
367
796
|
usage();
|
|
368
797
|
}
|
|
369
798
|
main().catch((error) => {
|