@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 harms-haus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# pi-workflows
|
|
2
|
+
|
|
3
|
+
A configurable pi extension for defining and running named multi-phase workflows with tool control, subworkflow nesting, and state persistence.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Named workflows** — Define any number of workflows as standalone directories with a `workflow.yaml` file, each invoked by a unique slash command.
|
|
8
|
+
- **Configurable phases** — Each phase specifies its own instructions, emoji, and available subagent profiles.
|
|
9
|
+
- **Per-phase tool control** — Restrict tools per phase using a blacklist (block specific tools) or whitelist (allow only specific tools).
|
|
10
|
+
- **Subworkflow nesting** — Compose workflows by referencing other workflows as phases, with cycle detection and breadcrumb navigation.
|
|
11
|
+
- **State persistence** — Workflow state survives session restarts via `pi.appendEntry`.
|
|
12
|
+
- **Auto-continue enforcement** — The agent cannot finish until the workflow reaches DONE; a configurable reminder is injected on premature `agent_end`.
|
|
13
|
+
- **Template variables** — Use `{workflowName}`, `{phaseName}`, `{taskDescription}`, and more in messages and instructions.
|
|
14
|
+
- **Slash commands** — Start workflows with `/workflow`, cancel with `/cancel-workflow`, and inspect progress with the `workflow_step` tool.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pi install git:github.com/harms-haus/pi-workflows
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
**1. Create a workflow directory** under `.pi/workflows/` in your project (or `~/.pi/agent/workflows/` globally):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
mkdir -p .pi/workflows/my-workflow
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**2. Write `workflow.yaml`** with two phases referencing markdown files:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
name: My Workflow
|
|
34
|
+
commandName: my-workflow
|
|
35
|
+
initialMessage: 'Starting {workflowName} for: "{description}"'
|
|
36
|
+
phases:
|
|
37
|
+
- gather.md
|
|
38
|
+
- execute.md
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Create the phase files alongside `workflow.yaml`:
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
## <!-- gather.md -->
|
|
45
|
+
|
|
46
|
+
id: gather
|
|
47
|
+
name: Gather
|
|
48
|
+
emoji: "🔍"
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
Research the task and summarize findings.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
## <!-- execute.md -->
|
|
57
|
+
|
|
58
|
+
id: execute
|
|
59
|
+
name: Execute
|
|
60
|
+
emoji: "🔨"
|
|
61
|
+
tools:
|
|
62
|
+
whitelist: - edit - write - workflow_step
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
Implement the solution based on gathered research.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
See [docs/configuration-reference.md](docs/configuration-reference.md) for the full schema.
|
|
70
|
+
|
|
71
|
+
**3. Run the workflow** in your pi session:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
/workflow my-workflow add user authentication
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
### Commands
|
|
80
|
+
|
|
81
|
+
| Command | Description |
|
|
82
|
+
| --------------------------------------- | ----------------------------------------------------------- |
|
|
83
|
+
| `/workflow {commandName} {description}` | Start a workflow |
|
|
84
|
+
| `/cancel-workflow` | Cancel the active workflow (bypasses the not-done reminder) |
|
|
85
|
+
|
|
86
|
+
### `workflow_step` Tool
|
|
87
|
+
|
|
88
|
+
| Action | Description |
|
|
89
|
+
| -------- | ----------------------------------------------------------------------- |
|
|
90
|
+
| `status` | Show current workflow state, phase instructions, and available profiles |
|
|
91
|
+
| `next` | Advance to the next phase (or DONE if on the last phase) |
|
|
92
|
+
| `cancel` | Cancel the active workflow (requires two calls to confirm) |
|
|
93
|
+
| `loop` | Restart the current scope from phase 0 (if the workflow is `loopable`) |
|
|
94
|
+
|
|
95
|
+
For complete examples, see [docs/examples.md](docs/examples.md).
|
|
96
|
+
|
|
97
|
+
## Documentation
|
|
98
|
+
|
|
99
|
+
| Document | Description |
|
|
100
|
+
| ---------------------------------------------------------- | -------------------------------------------------------- |
|
|
101
|
+
| [Configuration Reference](docs/configuration-reference.md) | Full schema for `workflow.yaml` and phase markdown files |
|
|
102
|
+
| [Template Variables](docs/template-variables.md) | All available `{variables}` and where they can be used |
|
|
103
|
+
| [Subworkflows](docs/subworkflows.md) | Composing workflows from other workflows |
|
|
104
|
+
| [Examples](docs/examples.md) | Complete workflow definitions and usage patterns |
|
|
105
|
+
| [Architecture](docs/architecture.md) | Extension structure, hooks, and data flow |
|
|
106
|
+
| [Hook Lifecycle](docs/hook-lifecycle.md) | How hooks intercept agent turns and tool calls |
|
|
107
|
+
| [State Management](docs/state-management.md) | How workflow state is tracked and persisted |
|
|
108
|
+
| [Testing](docs/testing.md) | Running and writing tests for the extension |
|
|
109
|
+
| [Contributing](docs/contributing.md) | Development setup and contribution guidelines |
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Technical reference for extension developers working on or extending pi-workflows internals.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## System Overview
|
|
8
|
+
|
|
9
|
+
pi-workflows is a **pi coding agent extension** that transforms the agent into a phase-driven orchestrator. It hooks into 6 framework events to inject workflow context into the agent's conversation, enforce per-phase tool restrictions, and manage linear phase progression through arbitrarily nested subworkflows.
|
|
10
|
+
|
|
11
|
+
The extension has no HTTP server, no database, and no background processes. It is entirely event-driven: the pi agent runtime calls into registered callbacks, and the extension responds by mutating closure-captured state, blocking tool calls, injecting hidden messages, and sending user-visible notifications.
|
|
12
|
+
|
|
13
|
+
**Core responsibilities:**
|
|
14
|
+
|
|
15
|
+
1. **Context injection** — Before each agent turn, the `before_agent_start` hook inserts a hidden message containing the current phase instructions, tool restrictions, progress, and advance reminders.
|
|
16
|
+
2. **Tool gating** — The `tool_call` hook blocks or allows tool usage per the active phase's `blacklist`/`whitelist` configuration. `workflow_step` is always exempt.
|
|
17
|
+
3. **Phase advancement** — The `workflow_step` tool and `agent_end` hook drive phase transitions, subworkflow entry/exit, looping, and completion.
|
|
18
|
+
4. **State persistence** — Every state mutation is appended to the session branch via `pi.appendEntry`, enabling full reconstruction on session resume or branch switch.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Module Map
|
|
23
|
+
|
|
24
|
+
| File | Responsibility | Key Exports | Internal Dependencies |
|
|
25
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
26
|
+
| `index.ts` | Entry point; wires all event subscriptions and registrations | `default` (extension function) | All other modules |
|
|
27
|
+
| `types.ts` | Type definitions and discriminated-union guards | `PhaseToolConfig`, `PathSegment`, `PhaseDefinition`, `SubworkflowReference`, `PhaseEntry`, `WorkflowDefinition`, `WorkflowState`, `ActiveWorkflow`, `HookStateMutation`, `GetState`, `SetState`, `GetDefinitions`, `ReloadDefinitions`, `isSubworkflowRef()`, `isPhaseDefinition()` | — |
|
|
28
|
+
| `config/index.ts` | Barrel module — re-exports the public API from all config submodules | (re-exports only) | `config/templates.ts`, `config/validation.ts`, `config/loading.ts` |
|
|
29
|
+
| `config/templates.ts` | Template variable resolution and phase tool-accessor helpers | `resolveTemplate()`, `getBlockedTools()`, `getWhitelist()` | `types.ts` |
|
|
30
|
+
| `config/validation.ts` | Workflow definition validation and subworkflow cycle detection (iterative DFS) | `validateWorkflowDefinition()`, `detectCycles()`, `VALID_COMMAND_NAME_RE` | `types.ts` |
|
|
31
|
+
| `config/loading.ts` | Orchestrator: top-level discovery, directory scanning, command-name lookup | `findWorkflowByCommandName()`, `loadWorkflowFromDir()`, `loadWorkflowsFromDir()`, `loadWorkflows()` | `types.ts`, `config/validation.ts`, `config/loading-parse.ts`, `config/loading-phases.ts`, `config/loading-resolve.ts` |
|
|
32
|
+
| `config/loading-parse.ts` | YAML parsing, field extraction from `workflow.yaml` and phase frontmatter | `parseWorkflowYaml()`, `extractPhaseMetadata()` | `types.ts` |
|
|
33
|
+
| `config/loading-phases.ts` | Path safety checks and phase loading from markdown files | `checkPathSafety()`, `loadPhaseFromMarkdown()` | `types.ts`, `config/loading-parse.ts` |
|
|
34
|
+
| `config/loading-resolve.ts` | Cycle removal, subworkflow reference resolution, duplicate command-name detection | `removeCycles()`, `resolveSubworkflowRefs()`, `checkDuplicateCommandNames()` | `types.ts`, `config/validation.ts` |
|
|
35
|
+
| `state.ts` | State creation, copy-on-write phase advancement, subworkflow navigation, persistence, reconstruction | `createInitialState()`, `cloneState()`, `advancePhase()`, `loopPhase()`, `resolveActive()`, `persistState()`, `reconstructState()`, `isActive()`, `autoEnterSubworkflowRefs()`, `resolveFirstPhase()`, `phaseEntryName()` | `types.ts` |
|
|
36
|
+
| `TimerManager.ts` | Timer management singleton tracking `setInterval`/`setTimeout` with stale-callback prevention | `TimerManager` (class), `timerManager` (singleton) | — |
|
|
37
|
+
| `tool.ts` | Registers the `workflow_step` tool (status, next, cancel, loop actions) | `registerWorkflowTool()` | `types.ts`, `state.ts`, `config/` |
|
|
38
|
+
| `command.ts` | Registers `/workflow` and `/cancel-workflow` slash commands | `registerWorkflowCommand()`, `registerCancelWorkflowCommand()` | `types.ts`, `state.ts`, `config/` |
|
|
39
|
+
| `hooks.ts` | Lifecycle hook handlers — exports 4 functions used across 6 event registrations (`session_start`/`session_tree` are handled inline in `index.ts`) | `updateStatus()`, `handleToolCall()`, `handleBeforeAgentStart()`, `handleAgentEnd()` | `types.ts`, `state.ts`, `config/`, `prompts.ts`, `TimerManager.ts` |
|
|
40
|
+
| `prompts.ts` | Context prompt construction and default message templates | `buildContextPrompt()`, `DEFAULT_NOT_DONE_REMINDER`, `DEFAULT_COMPLETION_MESSAGE`, `DEFAULT_CANCELLED_MESSAGE` | `types.ts`, `config/`, `state.ts` |
|
|
41
|
+
| `renderers.ts` | TUI message renderers for workflow message types | `registerRenderers()` | — |
|
|
42
|
+
|
|
43
|
+
### Dependency Graph
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
index.ts
|
|
47
|
+
├── config/ ────────────── types.ts
|
|
48
|
+
│ ├── loading.ts ──────── types.ts, config/validation.ts, config/loading-parse.ts,
|
|
49
|
+
│ │ config/loading-phases.ts, config/loading-resolve.ts
|
|
50
|
+
│ ├── loading-parse.ts ── types.ts
|
|
51
|
+
│ ├── loading-phases.ts ─ types.ts, config/loading-parse.ts
|
|
52
|
+
│ ├── loading-resolve.ts ─ types.ts, config/validation.ts
|
|
53
|
+
│ ├── validation.ts ───── types.ts
|
|
54
|
+
│ └── templates.ts ────── types.ts
|
|
55
|
+
├── state.ts ──────────── types.ts
|
|
56
|
+
├── hooks.ts ──────────── types.ts, config/, state.ts, prompts.ts, TimerManager.ts
|
|
57
|
+
│ └── prompts.ts ────── types.ts, config/, state.ts
|
|
58
|
+
├── tool.ts ───────────── types.ts, config/, state.ts
|
|
59
|
+
├── command.ts ────────── types.ts, config/, state.ts
|
|
60
|
+
├── TimerManager.ts
|
|
61
|
+
└── renderers.ts
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Data Flow Diagram
|
|
67
|
+
|
|
68
|
+
The diagram below traces the complete lifecycle from user invocation through each event hook to workflow completion.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
User
|
|
72
|
+
│
|
|
73
|
+
▼
|
|
74
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
75
|
+
│ /workflow {name} {description} │
|
|
76
|
+
│ (command.ts → createInitialState → persistState → │
|
|
77
|
+
│ setSessionName → sendUserMessage with initialMessage) │
|
|
78
|
+
└──────────────────────┬───────────────────────────────────────┘
|
|
79
|
+
│
|
|
80
|
+
▼
|
|
81
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
82
|
+
│ session_start / session_tree │
|
|
83
|
+
│ (index.ts → loadWorkflows + reconstructState → updateStatus)│
|
|
84
|
+
│ │
|
|
85
|
+
│ Loads definitions from ~/.pi/agent/workflows/ and │
|
|
86
|
+
│ .pi/workflows/. Replays persisted state from session branch.│
|
|
87
|
+
└──────────────────────┬───────────────────────────────────────┘
|
|
88
|
+
│
|
|
89
|
+
┌────────────▼────────────┐
|
|
90
|
+
│ Agent turn begins │◄───────────────────────┐
|
|
91
|
+
└────────────┬────────────┘ │
|
|
92
|
+
▼ │
|
|
93
|
+
┌──────────────────────────────────────┐ │
|
|
94
|
+
│ before_agent_start │ │
|
|
95
|
+
│ (hooks.ts → handleBeforeAgentStart) │ │
|
|
96
|
+
│ │ │
|
|
97
|
+
│ If workflow active: │ │
|
|
98
|
+
│ resolveActive → buildContextPrompt│ │
|
|
99
|
+
│ → inject hidden message │ │
|
|
100
|
+
│ (customType: "workflow:context") │ │
|
|
101
|
+
└──────────────┬───────────────────────┘ │
|
|
102
|
+
▼ │
|
|
103
|
+
┌──────────────────────────────────────┐ │
|
|
104
|
+
│ tool_call │ │
|
|
105
|
+
│ (hooks.ts → handleToolCall) │ │
|
|
106
|
+
│ │ │
|
|
107
|
+
│ If workflow active: │ │
|
|
108
|
+
│ Check blacklist → block if listed │ │
|
|
109
|
+
│ Check whitelist → block if absent │ │
|
|
110
|
+
│ workflow_step always allowed │ │
|
|
111
|
+
└──────────────┬───────────────────────┘ │
|
|
112
|
+
▼ │
|
|
113
|
+
┌──────────────────────────────────────┐ │
|
|
114
|
+
│ workflow_step tool invocation │ │
|
|
115
|
+
│ (tool.ts → execute) │ │
|
|
116
|
+
│ │ │
|
|
117
|
+
│ action=status → report phase info │ │
|
|
118
|
+
│ action=next → advancePhase() │─────┐ │
|
|
119
|
+
│ action=loop → loopPhase() │ │ │
|
|
120
|
+
│ action=cancel → two-step cancel │ │ │
|
|
121
|
+
└──────────────┬───────────────────────┘ │ │
|
|
122
|
+
│ │ │
|
|
123
|
+
▼ │ │
|
|
124
|
+
┌──────────────────────────────────────┐ │ │
|
|
125
|
+
│ agent_end │ │ │
|
|
126
|
+
│ (hooks.ts → handleAgentEnd) │ │ │
|
|
127
|
+
│ │ │ │
|
|
128
|
+
│ If DONE (not notified): │ │ │
|
|
129
|
+
│ Send completion message │ │ │
|
|
130
|
+
│ → unload state │ │ │
|
|
131
|
+
│ │ │ │
|
|
132
|
+
│ If still active (not aborted): │ │ │
|
|
133
|
+
│ Send not-done reminder │ │ │
|
|
134
|
+
│ → auto-continue after 3s delay │─────┘ │
|
|
135
|
+
└──────────────┬───────────────────────┘ │
|
|
136
|
+
▼ │
|
|
137
|
+
┌──────────────────────────────────────┐ │
|
|
138
|
+
│ turn_end │ │
|
|
139
|
+
│ (hooks.ts → updateStatus) │ │
|
|
140
|
+
│ │ │
|
|
141
|
+
│ Update status bar with current │ │
|
|
142
|
+
│ phase name, emoji, and progress. │ │
|
|
143
|
+
└──────────────────────────────────────┘ │
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Closure State Model
|
|
149
|
+
|
|
150
|
+
The extension's runtime state lives as **closure variables** inside the default export function in `index.ts`. Because pi extensions are single-file modules that export one initialization function, all event handlers, tools, and commands share state through these closures — no global variables or singletons are used.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
export default function (pi: ExtensionAPI): void {
|
|
154
|
+
┌─────────────────────────────────────────────┐
|
|
155
|
+
│ let state: WorkflowState | null = null; │ ← mutable closure variable
|
|
156
|
+
│ let definitions: Record< │
|
|
157
|
+
│ string, WorkflowDefinition> = {}; │ ← mutable closure variable
|
|
158
|
+
│ │
|
|
159
|
+
│ const getState = () => state; │ ─┐
|
|
160
|
+
│ const setState = (s) => { state = s; }; │ │ accessor callbacks
|
|
161
|
+
│ const getDefinitions = () => definitions; │ │ passed to tool/command
|
|
162
|
+
│ const reloadDefinitions = () => { │ │ registrations
|
|
163
|
+
│ definitions = loadWorkflows(); │ │
|
|
164
|
+
│ return Promise.resolve(definitions); │ ─┘
|
|
165
|
+
│ }; │
|
|
166
|
+
└─────────────────────────────────────────────┘
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Why accessor callbacks?
|
|
171
|
+
|
|
172
|
+
Tools and commands are registered at initialization time and receive the accessor functions as arguments. They cannot import or directly reference the closure variables. This pattern:
|
|
173
|
+
|
|
174
|
+
- **Decouples** registration from state ownership — `tool.ts` and `command.ts` never import from `index.ts`.
|
|
175
|
+
- **Enables mutation** — `setState` allows tools/commands to replace the state object (e.g., on cancel or completion).
|
|
176
|
+
- **Supports hot reload** — `reloadDefinitions` re-reads workflow files from disk and updates the shared `definitions` closure.
|
|
177
|
+
|
|
178
|
+
### Where state is mutated
|
|
179
|
+
|
|
180
|
+
| Site | How | Effect |
|
|
181
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
|
182
|
+
| `session_start` / `session_tree` handlers | Direct assignment to `state` and `definitions` | Loads fresh definitions; reconstructs persisted state |
|
|
183
|
+
| `agent_end` handler | Reads `HookStateMutation` result; assigns `state = null` or `state = mutation.state` | Handles completion unload or state update |
|
|
184
|
+
| `workflow_step` tool (cancel, next→DONE) | Calls `setState(newState)` | Immediate state swap from within tool execution |
|
|
185
|
+
| `/workflow` command | Calls `setState(newState)` | Starts a new workflow, replacing any prior active state |
|
|
186
|
+
| `/cancel-workflow` command | Calls `setState(null)` | Unloads workflow immediately |
|
|
187
|
+
|
|
188
|
+
> **Copy-on-write semantics:** `advancePhase()` and `loopPhase()` in `state.ts` are pure functions — they call `cloneState()` to produce a deep copy, mutate the copy, and return it as `newState` in their result object. The caller (typically `tool.ts` or `hooks.ts`) is responsible for passing the new state to `setState()`. The original state object is never mutated in place.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Event Subscription Map
|
|
193
|
+
|
|
194
|
+
### Agent Lifecycle Events
|
|
195
|
+
|
|
196
|
+
| Event | Handler | Purpose | Returns |
|
|
197
|
+
| -------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
|
|
198
|
+
| `session_start` | Inline in `index.ts` | Load definitions from disk; reconstruct state from session branch; update status bar | `void` |
|
|
199
|
+
| `session_tree` | Inline in `index.ts` | Same as `session_start` — fires when the user switches session branches. Captures `ctx.cwd` synchronously before any async gap. | `void` |
|
|
200
|
+
| `tool_call` | `handleToolCall()` in `hooks.ts` | Block tool calls that violate the active phase's blacklist/whitelist. `workflow_step` is always exempt. | `{ block: true; reason: string }` or `void` |
|
|
201
|
+
| `before_agent_start` | `handleBeforeAgentStart()` in `hooks.ts` | Inject a hidden `workflow:context` message containing the full context prompt for the current phase. | `{ message: {...} }` or `void` |
|
|
202
|
+
| `agent_end` | `handleAgentEnd()` in `hooks.ts` | Detect completion (send notification, unload state) or mid-workflow stop (send not-done reminder, auto-continue after 3s). Skips auto-continue if the agent was aborted by the user. | `HookStateMutation` (returned inline, not via event return) |
|
|
203
|
+
| `turn_end` | `updateStatus()` in `hooks.ts` | Refresh the status bar with the current phase name, emoji, and progress indicator. | `void` |
|
|
204
|
+
|
|
205
|
+
### Registrations (non-event)
|
|
206
|
+
|
|
207
|
+
| Registration | Module | Description |
|
|
208
|
+
| --------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
209
|
+
| `registerWorkflowTool()` | `tool.ts` | Registers the `workflow_step` tool with actions: `next`, `status`, `cancel`, `loop`. Includes TUI renderers for call/result. |
|
|
210
|
+
| `registerWorkflowCommand()` | `command.ts` | Registers the `/workflow` slash command. Supports argument completions for available workflow names. |
|
|
211
|
+
| `registerCancelWorkflowCommand()` | `command.ts` | Registers the `/cancel-workflow` slash command for immediate cancellation. |
|
|
212
|
+
| `registerRenderers()` | `renderers.ts` | Registers TUI renderers for three custom message types: `workflow:context`, `workflow:complete`, `workflow:countdown`. |
|
|
213
|
+
|
|
214
|
+
### Event Handler Signatures
|
|
215
|
+
|
|
216
|
+
Event handlers receive the event object and a context object. Return values depend on the event:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
session_start(event, ctx) → void
|
|
220
|
+
session_tree(event, ctx) → void
|
|
221
|
+
tool_call(event, ctx) → { block: true; reason: string } | void
|
|
222
|
+
before_agent_start(event, ctx) → { message: { customType, content, display } } | void
|
|
223
|
+
agent_end(event, ctx) → (mutation applied inline by index.ts)
|
|
224
|
+
turn_end(event, ctx) → void
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
> **Note:** The `agent_end` handler is unique — it returns a `HookStateMutation` that `index.ts` applies to the closure state. This is because the handler runs in `hooks.ts` which has no access to the closure; the mutation pattern bridges that gap.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Stale Error Handling
|
|
232
|
+
|
|
233
|
+
When the user replaces or reloads a session while an async handler is still executing, the pi runtime marks the context as **stale**. Any subsequent API call on that context throws an error whose message contains the string `"stale"`.
|
|
234
|
+
|
|
235
|
+
### `withStaleGuard()` and `initSession()`
|
|
236
|
+
|
|
237
|
+
Event handlers that call `pi.*` methods or access `ctx` are wrapped in `withStaleGuard()`, a higher-order function defined in `index.ts` that catches stale-context errors and silently discards them:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
function withStaleGuard(fn: () => void): void {
|
|
241
|
+
try {
|
|
242
|
+
fn();
|
|
243
|
+
} catch (e) {
|
|
244
|
+
if (isStaleError(e)) return;
|
|
245
|
+
throw e;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The `isStaleError` helper checks for the stale marker:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
function isStaleError(e: unknown): boolean {
|
|
254
|
+
return e instanceof Error && e.message.includes("stale");
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Both `session_start` and `session_tree` share a common `initSession()` function that handles definition loading, timer cleanup, state reconstruction, and status update:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
function initSession(ctx: { ... }) {
|
|
262
|
+
timerManager.clearAll();
|
|
263
|
+
definitions = loadWorkflows(ctx.cwd);
|
|
264
|
+
state = reconstructState(ctx);
|
|
265
|
+
updateStatus(ctx, state, definitions);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
This ensures timers from a previous session are always cancelled before loading new state.
|
|
270
|
+
|
|
271
|
+
### Handlers with stale guards
|
|
272
|
+
|
|
273
|
+
| Handler | Guard wrapper | Why it needs the guard |
|
|
274
|
+
| --------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
275
|
+
| `session_start` | `withStaleGuard` | Calls `loadWorkflows(ctx.cwd)`, `reconstructState(ctx)`, and `updateStatus(ctx, ...)` — `ctx` may be stale. |
|
|
276
|
+
| `session_tree` | `withStaleGuard` | Same as `session_start`. Also clears timers via `timerManager.clearAll()`. |
|
|
277
|
+
| `agent_end` | `withStaleGuard` | Calls `pi.sendMessage()`, `persistState(pi, ...)`, and `ctx.ui.setStatus()` — all of which can throw on a replaced session. |
|
|
278
|
+
| `turn_end` | `withStaleGuard` | Calls `updateStatus(ctx, ...)` which uses `ctx.ui.setStatus()`. |
|
|
279
|
+
|
|
280
|
+
Handlers that are **not** guarded (`tool_call`, `before_agent_start`) operate on the closure-captured `state` and `definitions` without calling async `pi.*` or `ctx.*` methods, so they cannot encounter stale-context errors.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Tech Stack
|
|
285
|
+
|
|
286
|
+
### Runtime Packages
|
|
287
|
+
|
|
288
|
+
Only `@earendil-works/pi-coding-agent` is a **direct dependency** listed in `package.json`. The remaining packages are **transitive dependencies** — they are imported directly by pi-workflows source code but resolved through the `@earendil-works/pi-coding-agent` dependency chain.
|
|
289
|
+
|
|
290
|
+
| Package | Type | Purpose | Used In |
|
|
291
|
+
| --------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
292
|
+
| `@earendil-works/pi-coding-agent` | Direct | Extension API (`ExtensionAPI`, `ExtensionContext`, event types, `parseFrontmatter`), tool registration, command registration, message rendering | All modules except `types.ts` |
|
|
293
|
+
| `@earendil-works/pi-tui` | Transitive | Terminal UI rendering — `Text` component for custom tool call/result and message renderers | `tool.ts`, `renderers.ts` |
|
|
294
|
+
| `@earendil-works/pi-ai` | Transitive | `StringEnum` for TypeBox schema generation of the `action` parameter in `workflow_step` | `tool.ts` |
|
|
295
|
+
| `typebox` | Transitive | Runtime type schema construction (`Type.Object`, `Type.String`, `Type.Optional`) for tool parameter validation | `tool.ts` |
|
|
296
|
+
| `yaml` | Transitive | YAML parsing for `workflow.yaml` files during definition loading | `config/loading.ts` |
|
|
297
|
+
|
|
298
|
+
### Standard Library Usage
|
|
299
|
+
|
|
300
|
+
| Module | Purpose |
|
|
301
|
+
| ----------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
302
|
+
| `node:fs` | `readdirSync`, `readFileSync`, `existsSync`, `realpathSync` — synchronous file I/O for workflow discovery and phase loading |
|
|
303
|
+
| `node:path` | `join`, `resolve`, `sep` — path construction and path-safety checks |
|
|
304
|
+
| `node:os` | `homedir` — resolving the global `~/.pi/agent/workflows/` directory |
|
|
305
|
+
|
|
306
|
+
### Key convention: synchronous file I/O
|
|
307
|
+
|
|
308
|
+
Workflow loading in `config/loading.ts` uses **synchronous** filesystem operations. This is intentional — the `loadWorkflows()` function itself is synchronous. The `reloadDefinitions` wrapper returns a `Promise` via `Promise.resolve()` for API consistency (the `ReloadDefinitions` type). The agent runtime calls `reloadDefinitions` during `session_start` / `session_tree`, so blocking the microtask queue briefly is acceptable and avoids the complexity of managing concurrent reads.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Further Reading
|
|
313
|
+
|
|
314
|
+
- **[Hook Lifecycle](hook-lifecycle.md)** — Deep-dive into each event hook's execution order, return value semantics, and interaction with the agent loop.
|
|
315
|
+
- **[State Management](state-management.md)** — Detailed walkthrough of the state machine, path stack navigation, subworkflow enter/exit, and persistence/reconstruction.
|
|
316
|
+
- **[Configuration Reference](configuration-reference.md)** — Complete field-by-field reference for `workflow.yaml` and phase `.md` frontmatter.
|
|
317
|
+
- **[Subworkflows](subworkflows.md)** — Guide to nested workflow references, cycle detection, and the two-pass resolution algorithm.
|
|
318
|
+
- **[Template Variables](template-variables.md)** — Full catalog of `{variable}` placeholders available in every template field.
|