@harms-haus/pi-workflows 1.0.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 +113 -0
- package/docs/architecture.md +318 -0
- package/docs/configuration-reference.md +427 -0
- package/docs/contributing.md +132 -0
- package/docs/examples.md +1242 -0
- package/docs/hook-lifecycle.md +380 -0
- package/docs/state-management.md +534 -0
- package/docs/subworkflows.md +428 -0
- package/docs/template-variables.md +383 -0
- package/docs/testing.md +479 -0
- package/package.json +69 -0
- package/skills/workflow-generation/SKILL.md +272 -0
- package/src/TimerManager.ts +67 -0
- package/src/command.ts +199 -0
- package/src/config/index.ts +11 -0
- package/src/config/loading-parse.ts +205 -0
- package/src/config/loading-phases.ts +78 -0
- package/src/config/loading-resolve.ts +82 -0
- package/src/config/loading.ts +202 -0
- package/src/config/templates.ts +25 -0
- package/src/config/validation.ts +258 -0
- package/src/hooks.ts +265 -0
- package/src/index.ts +98 -0
- package/src/prompts.ts +141 -0
- package/src/renderers.ts +46 -0
- package/src/state.ts +426 -0
- package/src/tool.ts +364 -0
- package/src/types.ts +211 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-generation
|
|
3
|
+
description: Build pi-workflow definitions (workflows, phases, subworkflows, agent-profiles). Use when asked to create or modify a workflow for the pi-workflows extension. Covers the full file-based schema, directory layout, phase frontmatter, subworkflow references, looping, tool blacklists/whitelists, and agent-profile authoring.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Workflow Generation
|
|
7
|
+
|
|
8
|
+
This skill teaches how to build **pi-workflows** — file-based workflow definitions loaded from `~/.pi/agent/workflows/` (global) or `.pi/workflows/` (project-local) by the `pi-workflows` extension.
|
|
9
|
+
|
|
10
|
+
Read this entire file before building any workflow.
|
|
11
|
+
|
|
12
|
+
## Directory Layout
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
~/.pi/agent/workflows/
|
|
16
|
+
├── my-workflow/ # One directory per user-facing workflow
|
|
17
|
+
│ └── workflow.yaml # Entry point: name, commandName, phases list
|
|
18
|
+
├── _shared/ # Convention: underscore-prefixed dirs hold reusable phases
|
|
19
|
+
│ ├── scouting.md
|
|
20
|
+
│ └── planning.md
|
|
21
|
+
└── my-other-workflow/
|
|
22
|
+
└── workflow.yaml # Can reference ../_shared/*.md phases
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Each workflow is a **directory** containing a `workflow.yaml`.
|
|
26
|
+
- Phase instructions live in **`.md` files** with YAML frontmatter.
|
|
27
|
+
- Directories starting with `_` are convention for shared phase libraries (not loaded as standalone workflows since they lack `workflow.yaml`).
|
|
28
|
+
- Phase `.md` files are referenced by **relative path** from the workflow directory.
|
|
29
|
+
|
|
30
|
+
## Agent Profiles
|
|
31
|
+
|
|
32
|
+
Agent profiles live in `~/.pi/agent/agent-profiles/` as `.md` files with YAML frontmatter:
|
|
33
|
+
|
|
34
|
+
```markdown
|
|
35
|
+
---
|
|
36
|
+
name: my-profile
|
|
37
|
+
provider: openai # Provider ID
|
|
38
|
+
model: gpt-4o # Model ID
|
|
39
|
+
thinkingLevel: medium # low | medium | high
|
|
40
|
+
tools: read,bash,edit,write,lsp-diagnostics,lsp-find-references,lsp-goto-definition
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
System prompt for the agent goes here as free-form markdown.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Key fields:**
|
|
47
|
+
|
|
48
|
+
- `name` — must match the filename (without `.md`)
|
|
49
|
+
- `tools` — comma-separated list of tools the subagent can use. Omitting a tool prevents the subagent from using it.
|
|
50
|
+
- `thinkingLevel` — controls reasoning effort
|
|
51
|
+
|
|
52
|
+
### Designing Profiles
|
|
53
|
+
|
|
54
|
+
1. **One responsibility per profile.** A scout scouts. A writer writes. A reviewer reviews. Never combine.
|
|
55
|
+
2. **Restrict tools to the minimum needed.** Scouts don't need `edit`/`write`. Writers do. Reviewers only need `read`-family tools.
|
|
56
|
+
3. **Set thinking level appropriately.** Low for fast scouting. Medium for implementation/writing. High for reviewing and planning.
|
|
57
|
+
4. **Reuse before creating.** Check `~/.pi/agent/agent-profiles/` for existing profiles before creating new ones.
|
|
58
|
+
|
|
59
|
+
## workflow.yaml Schema
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
name: Human-Readable Workflow Name # Required
|
|
63
|
+
commandName: my-command # Required for user-facing workflows. Used as: /workflow my-command <desc>
|
|
64
|
+
sessionNamePrefix: "Prefix: " # Optional. Shown in TUI session name
|
|
65
|
+
sessionNameMaxLength: 50 # Optional. Default 50
|
|
66
|
+
initialMessage: | # Required for user-facing workflows
|
|
67
|
+
Start the {workflowName} for: "{description}"
|
|
68
|
+
Begin with Phase 1 ({firstPhaseName}).
|
|
69
|
+
show: user # Optional. "user" (default) or "workflows" (subworkflow-only)
|
|
70
|
+
phases: # Required. Array of phase file paths or subworkflow references
|
|
71
|
+
- ../_shared/scouting.md
|
|
72
|
+
- ../_shared/planning.md
|
|
73
|
+
- ./implementing.md
|
|
74
|
+
- { subworkflow: my-sub-workflow } # Delegates to another workflow
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Template variables** available in `initialMessage`:
|
|
78
|
+
|
|
79
|
+
- `{workflowName}`, `{description}`, `{firstPhaseId}`, `{firstPhaseName}`, `{firstPhaseEmoji}`, `{firstPhaseProfiles}`
|
|
80
|
+
|
|
81
|
+
**Optional workflow-level fields:**
|
|
82
|
+
|
|
83
|
+
- `show: "workflows"` — makes the workflow invisible to `/workflow` command; only usable as a subworkflow
|
|
84
|
+
- `loopable: false` — prevents looping (restarting from phase 0). Default is `true`.
|
|
85
|
+
|
|
86
|
+
## Phase .md File Schema
|
|
87
|
+
|
|
88
|
+
```markdown
|
|
89
|
+
---
|
|
90
|
+
id: my-phase-id # Required. Unique within the workflow.
|
|
91
|
+
name: My Phase # Required. Display name.
|
|
92
|
+
emoji: "🔍" # Required. Single emoji.
|
|
93
|
+
tools: # Optional. Exactly one of blacklist or whitelist.
|
|
94
|
+
blacklist:
|
|
95
|
+
- edit
|
|
96
|
+
- write
|
|
97
|
+
availableProfiles: # Optional. Profiles the agent may delegate to.
|
|
98
|
+
- vertical-scout
|
|
99
|
+
- horizontal-scout
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
Phase instructions go here as free-form markdown.
|
|
103
|
+
|
|
104
|
+
These instructions are injected into the agent's context during this phase.
|
|
105
|
+
They tell the agent WHAT to do and HOW to use subagents.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Tool Configuration
|
|
109
|
+
|
|
110
|
+
Each phase can restrict tools via `tools`:
|
|
111
|
+
|
|
112
|
+
- **`blacklist`**: Block these specific tools. Everything else is allowed.
|
|
113
|
+
- **`whitelist`**: Allow ONLY these tools. Everything else is blocked.
|
|
114
|
+
- Cannot use both simultaneously.
|
|
115
|
+
- `workflow_step` is always allowed regardless of configuration.
|
|
116
|
+
|
|
117
|
+
Common pattern: read-only phases use `blacklist: [edit, write]` so the agent can scout/plan/review but not modify files directly — it must delegate to subagent profiles that have those tools.
|
|
118
|
+
|
|
119
|
+
### Phase Instructions Best Practices
|
|
120
|
+
|
|
121
|
+
Phase instructions are the core of your workflow. They must be:
|
|
122
|
+
|
|
123
|
+
1. **Self-contained** — The agent only sees the current phase's instructions. Include all context the agent needs.
|
|
124
|
+
2. **Actionable** — Tell the agent exactly what to do. Specify when to use `delegate_to_subagents`, `workflow_step next`, `workflow_step loop`.
|
|
125
|
+
3. **Profile-aware** — List which profiles to use and what prompts to send them.
|
|
126
|
+
4. **Terminal** — Every phase MUST end with either `workflow_step next` (advance) or `workflow_step loop` (restart the current workflow scope from phase 0).
|
|
127
|
+
|
|
128
|
+
Example instruction patterns:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Spawn 1-4 parallel subagents:
|
|
132
|
+
delegate_to_subagents: [{ name: "scout-N", prompt: "Investigate: [topic]", profile: "vertical-scout" }]
|
|
133
|
+
|
|
134
|
+
Collect results:
|
|
135
|
+
get_subagent_output for each sessionId
|
|
136
|
+
|
|
137
|
+
Advance:
|
|
138
|
+
workflow_step next
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Subworkflows
|
|
142
|
+
|
|
143
|
+
A subworkflow delegates a phase to an entire other workflow. In `workflow.yaml`:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
phases:
|
|
147
|
+
- { subworkflow: my-sub-workflow }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This resolves `my-sub-workflow` by its **directory name** in the workflows root. The parent workflow's phase becomes the entire subworkflow's phase sequence.
|
|
151
|
+
|
|
152
|
+
### Subworkflow Rules
|
|
153
|
+
|
|
154
|
+
1. The referenced key is the **directory name**, not the workflow's `name` field.
|
|
155
|
+
2. Subworkflows can be marked `show: "workflows"` to hide them from `/workflow`.
|
|
156
|
+
3. Subworkflow nesting is supported (a subworkflow can reference another subworkflow).
|
|
157
|
+
4. **Cycles are detected and rejected** — the reference graph must be a DAG.
|
|
158
|
+
5. If a referenced subworkflow doesn't exist, the parent workflow is skipped with a warning.
|
|
159
|
+
|
|
160
|
+
### When to Use Subworkflows
|
|
161
|
+
|
|
162
|
+
Use subworkflows when:
|
|
163
|
+
|
|
164
|
+
- Multiple top-level workflows share the same phase sequence (e.g., research → plan → implement → review)
|
|
165
|
+
- You want to reuse a "loop" boundary (looping restarts the current scope)
|
|
166
|
+
|
|
167
|
+
Example — a shared implementation+review loop used by two workflows:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
workflows/
|
|
171
|
+
├── rpir-dev/
|
|
172
|
+
│ └── workflow.yaml # phases: [research, plan, {subworkflow: rpir-implement}]
|
|
173
|
+
├── rpir-improve/
|
|
174
|
+
│ └── workflow.yaml # phases: [research, plan, {subworkflow: rpir-implement}]
|
|
175
|
+
└── rpir-implement/
|
|
176
|
+
└── workflow.yaml # show: workflows
|
|
177
|
+
# phases: [implement.md, review.md] ← loopable unit
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Looping
|
|
181
|
+
|
|
182
|
+
When a phase calls `workflow_step loop`, the **current workflow scope** restarts from its **phase 0**. This is how iterative refinement works:
|
|
183
|
+
|
|
184
|
+
1. An "implement" phase writes code
|
|
185
|
+
2. A "review" phase checks it
|
|
186
|
+
3. If review finds issues → `workflow_step loop` → back to implement
|
|
187
|
+
4. If review is clean → `workflow_step next` → advance past the loop
|
|
188
|
+
|
|
189
|
+
**Loop scope** is determined by the workflow boundary:
|
|
190
|
+
|
|
191
|
+
- In a flat workflow, `loop` restarts the entire workflow
|
|
192
|
+
- In a subworkflow reference, `loop` restarts only the subworkflow's phases
|
|
193
|
+
|
|
194
|
+
Set `loopable: false` to prevent looping (useful for planning phases that should run once).
|
|
195
|
+
|
|
196
|
+
## Phase Reuse Pattern
|
|
197
|
+
|
|
198
|
+
Avoid copy-pasting phase `.md` files across workflows. Instead:
|
|
199
|
+
|
|
200
|
+
1. Create a shared directory (convention: `_myshared/` with leading underscore)
|
|
201
|
+
2. Put reusable phases there
|
|
202
|
+
3. Reference them with relative paths: `../_myshared/scouting.md`
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
workflows/
|
|
206
|
+
├── workflow-a/
|
|
207
|
+
│ └── workflow.yaml # phases: [../_shared/scouting.md, ./a-specific.md]
|
|
208
|
+
├── workflow-b/
|
|
209
|
+
│ └── workflow.yaml # phases: [../_shared/scouting.md, ./b-specific.md]
|
|
210
|
+
└── _shared/
|
|
211
|
+
├── scouting.md # Shared phase
|
|
212
|
+
└── planning.md # Shared phase
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Building a New Workflow — Checklist
|
|
216
|
+
|
|
217
|
+
1. **Define the workflow** — What is the goal? What phases does it need?
|
|
218
|
+
2. **Check for reusable phases** — Look in `~/.pi/agent/workflows/` for `_`-prefixed shared directories with phases you can reference.
|
|
219
|
+
3. **Check for reusable profiles** — Look in `~/.pi/agent/agent-profiles/` before creating new ones.
|
|
220
|
+
4. **Create the directory** — `~/.pi/agent/workflows/my-workflow/`
|
|
221
|
+
5. **Write `workflow.yaml`** — name, commandName, initialMessage, phases list
|
|
222
|
+
6. **Write phase `.md` files** — Each with frontmatter (id, name, emoji) and instructions. Place shared phases in a `_shared/` directory and reference with `../_shared/...` paths.
|
|
223
|
+
7. **Create agent profiles** — Only if no existing profile fits. Place in `~/.pi/agent/agent-profiles/`.
|
|
224
|
+
8. **Validate** — Ensure every phase `.md` has `id`, `name`, `emoji`. Ensure all referenced profiles exist. Ensure YAML is valid.
|
|
225
|
+
|
|
226
|
+
## Common Patterns
|
|
227
|
+
|
|
228
|
+
### Read-Only Phase (Scouting/Planning/Reviewing)
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
# frontmatter
|
|
232
|
+
tools:
|
|
233
|
+
blacklist:
|
|
234
|
+
- edit
|
|
235
|
+
- write
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The agent delegates to subagent profiles that have write tools. The orchestrator itself cannot modify files.
|
|
239
|
+
|
|
240
|
+
### Implementation Phase
|
|
241
|
+
|
|
242
|
+
```yaml
|
|
243
|
+
# frontmatter
|
|
244
|
+
tools:
|
|
245
|
+
blacklist:
|
|
246
|
+
- edit
|
|
247
|
+
- write
|
|
248
|
+
availableProfiles:
|
|
249
|
+
- task-worker
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Same pattern — the orchestrator delegates to `task-worker` which has edit/write tools.
|
|
253
|
+
|
|
254
|
+
### Skip-if-Clean Phase
|
|
255
|
+
|
|
256
|
+
In the phase instructions, include:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
If there are no issues, immediately perform `workflow_step next` to skip.
|
|
260
|
+
Otherwise, fix issues and use `workflow_step loop` to re-review.
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Parallel Subagent Delegation
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
Spawn 1-4 parallel subagents:
|
|
267
|
+
delegate_to_subagents: [
|
|
268
|
+
{ name: "task-1", prompt: "...", profile: "my-profile" },
|
|
269
|
+
{ name: "task-2", prompt: "...", profile: "my-profile" }
|
|
270
|
+
]
|
|
271
|
+
Collect results with get_subagent_output for each sessionId.
|
|
272
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimerManager: tracks a single active setInterval and setTimeout.
|
|
3
|
+
*
|
|
4
|
+
* Prevents stale callbacks by verifying the stored handle still matches
|
|
5
|
+
* the one that was set at the time the callback was scheduled.
|
|
6
|
+
*/
|
|
7
|
+
export class TimerManager {
|
|
8
|
+
private intervalHandle: ReturnType<typeof setInterval> | null = null;
|
|
9
|
+
private timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Start a new interval. Any previously tracked interval is cleared first.
|
|
13
|
+
*/
|
|
14
|
+
startInterval(delay: number, callback: () => void): void {
|
|
15
|
+
this.clearInterval();
|
|
16
|
+
|
|
17
|
+
const handle = setInterval(() => {
|
|
18
|
+
// Only execute if the handle hasn't been replaced since scheduling
|
|
19
|
+
if (this.intervalHandle === handle) {
|
|
20
|
+
callback();
|
|
21
|
+
}
|
|
22
|
+
}, delay);
|
|
23
|
+
|
|
24
|
+
this.intervalHandle = handle;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start a new timeout. Any previously tracked timeout is cleared first.
|
|
29
|
+
*/
|
|
30
|
+
startTimeout(delay: number, callback: () => void): void {
|
|
31
|
+
this.clearTimeout();
|
|
32
|
+
|
|
33
|
+
const handle = setTimeout(() => {
|
|
34
|
+
// Only execute if the handle hasn't been replaced since scheduling
|
|
35
|
+
if (this.timeoutHandle === handle) {
|
|
36
|
+
callback();
|
|
37
|
+
}
|
|
38
|
+
}, delay);
|
|
39
|
+
|
|
40
|
+
this.timeoutHandle = handle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Clear all tracked timers and set handles to null.
|
|
45
|
+
*/
|
|
46
|
+
clearAll(): void {
|
|
47
|
+
this.clearInterval();
|
|
48
|
+
this.clearTimeout();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private clearInterval(): void {
|
|
52
|
+
if (this.intervalHandle !== null) {
|
|
53
|
+
clearInterval(this.intervalHandle);
|
|
54
|
+
this.intervalHandle = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private clearTimeout(): void {
|
|
59
|
+
if (this.timeoutHandle !== null) {
|
|
60
|
+
clearTimeout(this.timeoutHandle);
|
|
61
|
+
this.timeoutHandle = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Module-level singleton. */
|
|
67
|
+
export const timerManager = new TimerManager();
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { WorkflowState, SetState, ReloadDefinitions, WorkflowDefinition } from "./types";
|
|
3
|
+
import { isSubworkflowRef } from "./types";
|
|
4
|
+
import {
|
|
5
|
+
createInitialState,
|
|
6
|
+
persistState,
|
|
7
|
+
isActive,
|
|
8
|
+
resolveActive,
|
|
9
|
+
autoEnterSubworkflowRefs,
|
|
10
|
+
resolveFirstPhase,
|
|
11
|
+
} from "./state";
|
|
12
|
+
import { loadWorkflows, findWorkflowByCommandName, resolveTemplate } from "./config";
|
|
13
|
+
|
|
14
|
+
/** Show available workflows to the user. */
|
|
15
|
+
function showAvailableWorkflows(ctx: ExtensionCommandContext): void {
|
|
16
|
+
const workflows = loadWorkflows(ctx.cwd);
|
|
17
|
+
const entries = Object.entries(workflows)
|
|
18
|
+
.filter(([_key, def]) => (def.show ?? "user") === "user")
|
|
19
|
+
.map(([_key, def]) => ` ${def.commandName} — ${def.name}`);
|
|
20
|
+
ctx.ui.notify(
|
|
21
|
+
"Usage: /workflow {name} {description}\n\nAvailable workflows:\n" + entries.join("\n"),
|
|
22
|
+
"info",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Set the session name and send the initial workflow message. */
|
|
27
|
+
function startWorkflowSession(
|
|
28
|
+
pi: ExtensionAPI,
|
|
29
|
+
definition: WorkflowDefinition,
|
|
30
|
+
workflowKey: string,
|
|
31
|
+
description: string,
|
|
32
|
+
): void {
|
|
33
|
+
const prefix = definition.sessionNamePrefix ?? "Workflow: ";
|
|
34
|
+
const maxLen = definition.sessionNameMaxLength ?? 50;
|
|
35
|
+
pi.setSessionName(
|
|
36
|
+
`${prefix}${description.slice(0, maxLen)}${description.length > maxLen ? "…" : ""}`,
|
|
37
|
+
);
|
|
38
|
+
const firstPhase = resolveFirstPhase(definition.phases);
|
|
39
|
+
if (!firstPhase) return;
|
|
40
|
+
const initialMessage = resolveTemplate(definition.initialMessage, {
|
|
41
|
+
workflowName: definition.name,
|
|
42
|
+
workflowKey,
|
|
43
|
+
description,
|
|
44
|
+
firstPhaseId: firstPhase.id,
|
|
45
|
+
firstPhaseName: firstPhase.name,
|
|
46
|
+
firstPhaseEmoji: firstPhase.emoji,
|
|
47
|
+
firstPhaseProfiles: firstPhase.availableProfiles?.join(", ") ?? "(none)",
|
|
48
|
+
});
|
|
49
|
+
pi.sendUserMessage(initialMessage);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Register the /workflow command.
|
|
54
|
+
* Usage: /workflow {commandName} {description}
|
|
55
|
+
*/
|
|
56
|
+
export function registerWorkflowCommand(
|
|
57
|
+
pi: ExtensionAPI,
|
|
58
|
+
getState: () => WorkflowState | null,
|
|
59
|
+
reloadDefinitions: ReloadDefinitions,
|
|
60
|
+
setState: SetState,
|
|
61
|
+
): void {
|
|
62
|
+
pi.registerCommand("workflow", {
|
|
63
|
+
description: "Start a configured workflow. Usage: /workflow {name} {description}",
|
|
64
|
+
getArgumentCompletions(prefix: string) {
|
|
65
|
+
const workflows = loadWorkflows();
|
|
66
|
+
const names = Object.values(workflows)
|
|
67
|
+
.filter((w) => (w.show ?? "user") === "user")
|
|
68
|
+
.map((w) => w.commandName);
|
|
69
|
+
const filtered = names.filter((n) => n.startsWith(prefix));
|
|
70
|
+
return filtered.length > 0 ? filtered.map((n) => ({ value: n, label: n })) : null;
|
|
71
|
+
},
|
|
72
|
+
handler: async (args, ctx) => {
|
|
73
|
+
// Parse: split on first whitespace to get commandName and description
|
|
74
|
+
const input = typeof args === "string" ? args : "";
|
|
75
|
+
const parts = input.trim().match(/^(\S+)\s*(.*)/s);
|
|
76
|
+
if (!parts) {
|
|
77
|
+
showAvailableWorkflows(ctx);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const commandName = parts[1];
|
|
82
|
+
const description = parts[2];
|
|
83
|
+
if (commandName === undefined || description === undefined) {
|
|
84
|
+
showAvailableWorkflows(ctx);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!description || description.trim() === "") {
|
|
88
|
+
ctx.ui.notify(`Usage: /workflow ${commandName} {description}`, "warning");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Reload definitions to get latest
|
|
93
|
+
const definitions = await reloadDefinitions(ctx.cwd);
|
|
94
|
+
|
|
95
|
+
// Find the workflow
|
|
96
|
+
const match = findWorkflowByCommandName(definitions, commandName);
|
|
97
|
+
if (!match) {
|
|
98
|
+
const available = Object.values(definitions)
|
|
99
|
+
.map((d) => d.commandName)
|
|
100
|
+
.join(", ");
|
|
101
|
+
ctx.ui.notify(
|
|
102
|
+
`Unknown workflow: "${commandName}". Available: ${available || "(none)"}`,
|
|
103
|
+
"error",
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const [workflowKey, definition] = match;
|
|
109
|
+
|
|
110
|
+
// Safety check: reject workflows not shown to users
|
|
111
|
+
if (definition.show === "workflows") {
|
|
112
|
+
ctx.ui.notify(
|
|
113
|
+
`"${commandName}" is a subworkflow that can only run as part of another workflow. It cannot be started directly.`,
|
|
114
|
+
"error",
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const state = getState();
|
|
119
|
+
|
|
120
|
+
// Check for existing active workflow
|
|
121
|
+
if (isActive(state)) {
|
|
122
|
+
const active = resolveActive(state, definitions);
|
|
123
|
+
const phaseName = active ? active.currentPhase.name : "unknown";
|
|
124
|
+
const existingDesc = state.taskDescription;
|
|
125
|
+
const ok = await ctx.ui.confirm(
|
|
126
|
+
"Workflow already active",
|
|
127
|
+
`Phase: ${phaseName}\nTask: ${existingDesc}\n\nStart a new one?`,
|
|
128
|
+
);
|
|
129
|
+
if (!ok) return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Create new state
|
|
133
|
+
const newState = createInitialState(workflowKey, description.trim());
|
|
134
|
+
|
|
135
|
+
// Auto-enter subworkflow if first phase is a SubworkflowRef
|
|
136
|
+
const firstEntry = definition.phases[0];
|
|
137
|
+
let stateToSet = newState;
|
|
138
|
+
if (isSubworkflowRef(firstEntry)) {
|
|
139
|
+
const { newState: enteredState } = autoEnterSubworkflowRefs(newState, firstEntry);
|
|
140
|
+
stateToSet = enteredState;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setState(stateToSet);
|
|
144
|
+
persistState(pi, stateToSet);
|
|
145
|
+
ctx.ui.setStatus("workflow", undefined);
|
|
146
|
+
startWorkflowSession(pi, definition, workflowKey, description.trim());
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Register the /cancel-workflow command.
|
|
153
|
+
* Immediately jumps the active workflow to DONE, bypassing the not-done reminder loop.
|
|
154
|
+
*/
|
|
155
|
+
export function registerCancelWorkflowCommand(
|
|
156
|
+
pi: ExtensionAPI,
|
|
157
|
+
getState: () => WorkflowState | null,
|
|
158
|
+
setState: (s: WorkflowState | null) => void,
|
|
159
|
+
): void {
|
|
160
|
+
pi.registerCommand("cancel-workflow", {
|
|
161
|
+
description:
|
|
162
|
+
"Cancel the active workflow and jump to DONE. Bypasses the not-done reminder loop.",
|
|
163
|
+
handler: (_args, ctx) => {
|
|
164
|
+
const state = getState();
|
|
165
|
+
if (!state || !state.active) {
|
|
166
|
+
ctx.ui.notify("No active workflow to cancel.", "info");
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Jump straight to DONE state
|
|
171
|
+
const doneState: WorkflowState = {
|
|
172
|
+
...state,
|
|
173
|
+
currentPath: state.currentPath.map((seg) => ({ ...seg })),
|
|
174
|
+
active: false,
|
|
175
|
+
cancelled: true,
|
|
176
|
+
completionNotified: false,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Persist so session resume knows it was cancelled
|
|
180
|
+
persistState(pi, doneState);
|
|
181
|
+
|
|
182
|
+
// Clear the status
|
|
183
|
+
ctx.ui.setStatus("workflow", undefined);
|
|
184
|
+
|
|
185
|
+
// Send cancellation notification immediately (bypass agent_end hook)
|
|
186
|
+
const msg = `❌ **Workflow Cancelled**\n\n**Task:** ${state.taskDescription}\n**Task ID:** ${state.taskId}`;
|
|
187
|
+
pi.sendMessage(
|
|
188
|
+
{ customType: "workflow:complete", content: msg, display: true },
|
|
189
|
+
{ triggerTurn: false },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Unload immediately so agent_end hook sees null state and does nothing
|
|
193
|
+
setState(null);
|
|
194
|
+
|
|
195
|
+
ctx.ui.notify("Workflow cancelled.", "info");
|
|
196
|
+
return Promise.resolve();
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Barrel file — re-exports the public API from config modules.
|
|
2
|
+
// All existing imports from "./config" or "../config" resolve through this file.
|
|
3
|
+
|
|
4
|
+
export { resolveTemplate, getBlockedTools, getWhitelist } from "./templates";
|
|
5
|
+
export { validateWorkflowDefinition, detectCycles, VALID_COMMAND_NAME_RE } from "./validation";
|
|
6
|
+
export {
|
|
7
|
+
findWorkflowByCommandName,
|
|
8
|
+
loadWorkflowFromDir,
|
|
9
|
+
loadWorkflowsFromDir,
|
|
10
|
+
loadWorkflows,
|
|
11
|
+
} from "./loading";
|