hermes-workflow 0.1.3__tar.gz
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.
- hermes_workflow-0.1.3/LICENSE +21 -0
- hermes_workflow-0.1.3/PKG-INFO +192 -0
- hermes_workflow-0.1.3/README.md +170 -0
- hermes_workflow-0.1.3/pyproject.toml +41 -0
- hermes_workflow-0.1.3/setup.cfg +4 -0
- hermes_workflow-0.1.3/src/hermes_workflow/__init__.py +37 -0
- hermes_workflow-0.1.3/src/hermes_workflow/board.py +126 -0
- hermes_workflow-0.1.3/src/hermes_workflow/engine/__init__.py +0 -0
- hermes_workflow-0.1.3/src/hermes_workflow/engine/graph.py +92 -0
- hermes_workflow-0.1.3/src/hermes_workflow/engine/interpolate.py +26 -0
- hermes_workflow-0.1.3/src/hermes_workflow/engine/model.py +60 -0
- hermes_workflow-0.1.3/src/hermes_workflow/engine/provenance.py +93 -0
- hermes_workflow-0.1.3/src/hermes_workflow/engine/reconcile.py +30 -0
- hermes_workflow-0.1.3/src/hermes_workflow/engine/template.py +111 -0
- hermes_workflow-0.1.3/src/hermes_workflow/hooks.py +137 -0
- hermes_workflow-0.1.3/src/hermes_workflow/lanes/__init__.py +0 -0
- hermes_workflow-0.1.3/src/hermes_workflow/lanes/presets.py +19 -0
- hermes_workflow-0.1.3/src/hermes_workflow/materialize.py +144 -0
- hermes_workflow-0.1.3/src/hermes_workflow/plugin.yaml +13 -0
- hermes_workflow-0.1.3/src/hermes_workflow/preflight.py +123 -0
- hermes_workflow-0.1.3/src/hermes_workflow/runview.py +100 -0
- hermes_workflow-0.1.3/src/hermes_workflow/skills/workflow-author/SKILL.md +176 -0
- hermes_workflow-0.1.3/src/hermes_workflow/skills/workflow-orchestrator/SKILL.md +165 -0
- hermes_workflow-0.1.3/src/hermes_workflow/sweep.py +30 -0
- hermes_workflow-0.1.3/src/hermes_workflow/tools.py +672 -0
- hermes_workflow-0.1.3/src/hermes_workflow/version.py +6 -0
- hermes_workflow-0.1.3/src/hermes_workflow/veto.py +85 -0
- hermes_workflow-0.1.3/src/hermes_workflow/worktree.py +74 -0
- hermes_workflow-0.1.3/src/hermes_workflow.egg-info/PKG-INFO +192 -0
- hermes_workflow-0.1.3/src/hermes_workflow.egg-info/SOURCES.txt +34 -0
- hermes_workflow-0.1.3/src/hermes_workflow.egg-info/dependency_links.txt +1 -0
- hermes_workflow-0.1.3/src/hermes_workflow.egg-info/entry_points.txt +2 -0
- hermes_workflow-0.1.3/src/hermes_workflow.egg-info/requires.txt +4 -0
- hermes_workflow-0.1.3/src/hermes_workflow.egg-info/top_level.txt +1 -0
- hermes_workflow-0.1.3/tests/test_preflight_eval.py +45 -0
- hermes_workflow-0.1.3/tests/test_sweep.py +31 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carlos Raphael
|
|
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.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermes-workflow
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Declarative multi-stage workflow primitive over the Hermes Kanban board.
|
|
5
|
+
Author: Carlos Raphael
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/carlosraphael/hermes-workflow
|
|
8
|
+
Project-URL: Repository, https://github.com/carlosraphael/hermes-workflow
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: PyYAML<7,>=6
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest<9,>=8; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# hermes-workflow
|
|
24
|
+
|
|
25
|
+
A declarative, versioned, multi-stage workflow primitive over the Hermes Kanban board.
|
|
26
|
+
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
[](pyproject.toml)
|
|
29
|
+
[](https://github.com/carlosraphael/hermes-workflow/actions)
|
|
30
|
+
|
|
31
|
+
A `*.workflow.yaml` template — params, abstract roles bound to lanes, stages with
|
|
32
|
+
`needs`, a single-level `expand` fan-out plus an `expand_out` shape gate, a human
|
|
33
|
+
`gate`, and workspaces — is instantiated into a Kanban card-graph that Hermes'
|
|
34
|
+
existing dispatcher and worker lanes execute. It turns "hand-wire an orchestrator
|
|
35
|
+
from scratch every run" into a repeatable, durable, inspectable run on the board
|
|
36
|
+
you already use.
|
|
37
|
+
|
|
38
|
+
## What it is
|
|
39
|
+
|
|
40
|
+
A template compiles to a linked card-graph. Materialization is **lazy and
|
|
41
|
+
single-level**:
|
|
42
|
+
|
|
43
|
+
- `workflow_start` validates the template + bindings, runs a per-profile
|
|
44
|
+
pre-flight, and creates the **dynamic-free prefix** (the static stages up to
|
|
45
|
+
the first fan-out) plus a write-once compiled-snapshot **blackboard root**.
|
|
46
|
+
- The `post_tool_call` **fan-out hook** fires as stages complete: when a source
|
|
47
|
+
stage finishes, it creates that fan-out's children **and** its join in one
|
|
48
|
+
atomic create, wiring the join as a child of every instance.
|
|
49
|
+
|
|
50
|
+
Completion-gating is **deterministic and injection-free**, enforced by a
|
|
51
|
+
`pre_tool_call` veto: it checks the `expand_out` shape + `max`, and commit-clean
|
|
52
|
+
for worktree stages, before a stage may complete. There is no LLM in the gate.
|
|
53
|
+
|
|
54
|
+
All state lives on the board — body sentinels, links, and statuses — and the run
|
|
55
|
+
root is a write-once compiled-snapshot blackboard. Nothing touches raw SQLite —
|
|
56
|
+
every mutation goes through Hermes' board APIs (`kanban_*` from workers, `kb.*`
|
|
57
|
+
from the host).
|
|
58
|
+
|
|
59
|
+
## Requirements
|
|
60
|
+
|
|
61
|
+
- **Hermes Agent v0.15.x**
|
|
62
|
+
- **Python ≥ 3.11**
|
|
63
|
+
- For any role with `lane: codex`: the `codex` binary on `PATH`, and Hermes'
|
|
64
|
+
**bundled `kanban-codex-lane` skill** present in that role's bound profile.
|
|
65
|
+
This plugin **reuses** that skill — it does **not** re-ship it.
|
|
66
|
+
|
|
67
|
+
## Quick Install
|
|
68
|
+
|
|
69
|
+
The intended primary path is PyPI:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pip install hermes-workflow
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
From source:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
git clone https://github.com/carlosraphael/hermes-workflow
|
|
79
|
+
cd hermes-workflow
|
|
80
|
+
pip install -e .
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then **enable it in EVERY bound profile** — not just the orchestrator's. This is
|
|
84
|
+
a pip / entry-point plugin, so `hermes plugins enable` does **not** apply to it
|
|
85
|
+
(that command only sees user-dir and bundled plugins). Instead add
|
|
86
|
+
`hermes-workflow` to the `plugins.enabled` list in each bound profile's
|
|
87
|
+
`config.yaml` — open it with `hermes -p <profile> config edit` (or edit
|
|
88
|
+
`<that profile's HERMES_HOME>/config.yaml` directly) and add:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
plugins:
|
|
92
|
+
enabled:
|
|
93
|
+
- hermes-workflow
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The change takes effect on the **next session**. (Do **not** use
|
|
97
|
+
`hermes config set plugins.enabled …` — it writes a scalar string, which breaks
|
|
98
|
+
the loader's list check.)
|
|
99
|
+
|
|
100
|
+
**Per-profile enablement is the #1 failure mode.** Workers read the plugin's
|
|
101
|
+
load status from the *assignee profile's* home, so the plugin must be enabled
|
|
102
|
+
**and loadable** in every profile a role binds to. `workflow_start` runs a
|
|
103
|
+
per-profile loadability pre-flight and **fails fast — creating zero cards** —
|
|
104
|
+
with a per-profile remediation map if any bound profile cannot load it (or a
|
|
105
|
+
codex role is missing its `codex` binary / `kanban-codex-lane` skill).
|
|
106
|
+
|
|
107
|
+
## Quickstart
|
|
108
|
+
|
|
109
|
+
Using `examples/fix-flaky-tests.workflow.yaml`. There are two surfaces; both take
|
|
110
|
+
the same arguments.
|
|
111
|
+
|
|
112
|
+
In-session slash command:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
/workflow start --template examples/fix-flaky-tests.workflow.yaml \
|
|
116
|
+
--params '{"repo":"/abs/path/to/repo"}' \
|
|
117
|
+
--bindings '{"scout":"designer","fixer":"coder","reporter":"writer"}'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
CLI:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
hermes workflow start --template examples/fix-flaky-tests.workflow.yaml \
|
|
124
|
+
--params '{"repo":"/abs/path/to/repo"}' \
|
|
125
|
+
--bindings '{"scout":"designer","fixer":"coder","reporter":"writer"}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Run lifecycle
|
|
129
|
+
|
|
130
|
+
1. The `scan` worker (role `scout`) inspects the repo and returns
|
|
131
|
+
`metadata.flaky = [{test_id, file}, …]`.
|
|
132
|
+
2. The fan-out hook creates one `fix` card per flaky test — each on its own
|
|
133
|
+
pre-provisioned worktree, gated by commit-clean — and wires the `approve`
|
|
134
|
+
human gate as a child of all of them.
|
|
135
|
+
3. Once every `fix` card is `done`, the gate promotes to `ready`.
|
|
136
|
+
4. The operator runs `hermes workflow approve <gate_card>`. Find the gate card
|
|
137
|
+
with `hermes workflow status <root_id>` (it is listed under
|
|
138
|
+
`awaiting_approval`).
|
|
139
|
+
5. `report` (role `reporter`) then runs, summarizing the fixes from the
|
|
140
|
+
handoffs and listing the branches.
|
|
141
|
+
|
|
142
|
+
## Commands
|
|
143
|
+
|
|
144
|
+
Both surfaces — the CLI (`hermes workflow <cmd>`) and the slash command
|
|
145
|
+
(`/workflow <cmd>`) — dispatch the same tools (`workflow_start` … `workflow_abandon`).
|
|
146
|
+
|
|
147
|
+
| Command | Form | What it does |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `start` | `--template <path> --params <json> --bindings <json> [--board]` | Validate, pre-flight every bound profile, then seed the run (root blackboard + dynamic-free prefix). Fails closed with zero cards if any profile can't load. |
|
|
150
|
+
| `status` | `<root_id> [--board]` | Read-only run summary: per-stage rollup plus `blocked_stages`, `awaiting_approval` (the gate signal), and `review_required`. |
|
|
151
|
+
| `validate` | `--template <path>` | Deterministic, read-only template check. Rejects verify/retry and nested-expand. |
|
|
152
|
+
| `reconcile` | `<root_id> [--board]` | Re-drive a partial fan-out: create missing children, re-link to the join, progress-aware dedup, and surface review-required stalls. |
|
|
153
|
+
| `approve` | `<gate_card> [--board]` | Complete a human gate card so its downstream stages promote natively. |
|
|
154
|
+
| `abandon` | `<root_id> [--board]` | Hazard-free teardown: reclaim running workers, then archive the run reverse-topologically (leaves-first). Worktrees are preserved. |
|
|
155
|
+
|
|
156
|
+
The four mutating commands (`start`, `reconcile`, `approve`, `abandon`) refuse to
|
|
157
|
+
run inside a dispatcher-spawned worker — they are orchestrator-context only.
|
|
158
|
+
|
|
159
|
+
## Bundled skills
|
|
160
|
+
|
|
161
|
+
Two skills are auto-registered on plugin load:
|
|
162
|
+
|
|
163
|
+
- **`workflow-author`** — how to write a `*.workflow.yaml` template within the
|
|
164
|
+
0.1.0 constraints; validate it with `workflow_validate`.
|
|
165
|
+
- **`workflow-orchestrator`** — start / status / approve / abandon / reconcile,
|
|
166
|
+
plus the review-required backstop for recovering a stalled stage.
|
|
167
|
+
|
|
168
|
+
## 0.1.0 scope
|
|
169
|
+
|
|
170
|
+
Lanes are `{profile, codex}` only — `claude-code` is deferred to 0.2.x.
|
|
171
|
+
`verify.command` / `retry` and granular `reject` / `modify` are deferred to
|
|
172
|
+
0.2.x. Nested fan-out (an `expand` stage that is itself an `expand` source) is
|
|
173
|
+
**forbidden** in 0.1.0.
|
|
174
|
+
|
|
175
|
+
## Documentation
|
|
176
|
+
|
|
177
|
+
| Document | What it covers |
|
|
178
|
+
|---|---|
|
|
179
|
+
| [`AGENTS.md`](AGENTS.md) | Development guide for AI coding assistants and contributors. |
|
|
180
|
+
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Development setup, PR process, and code style. |
|
|
181
|
+
| [`docs/operations.md`](docs/operations.md) | Upgrade / version-gate procedure and operational caveats. |
|
|
182
|
+
| [`CONTEXT.md`](CONTEXT.md) | The naming taxonomy (distribution / import / plugin / runtime layers). |
|
|
183
|
+
| [`docs/adr/`](docs/adr/) | Architecture decision records. |
|
|
184
|
+
|
|
185
|
+
## Contributing
|
|
186
|
+
|
|
187
|
+
Contributions are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md) for the
|
|
188
|
+
development setup, PR process, and code style.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT — Copyright (c) 2026 Carlos Raphael. See [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# hermes-workflow
|
|
2
|
+
|
|
3
|
+
A declarative, versioned, multi-stage workflow primitive over the Hermes Kanban board.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](pyproject.toml)
|
|
7
|
+
[](https://github.com/carlosraphael/hermes-workflow/actions)
|
|
8
|
+
|
|
9
|
+
A `*.workflow.yaml` template — params, abstract roles bound to lanes, stages with
|
|
10
|
+
`needs`, a single-level `expand` fan-out plus an `expand_out` shape gate, a human
|
|
11
|
+
`gate`, and workspaces — is instantiated into a Kanban card-graph that Hermes'
|
|
12
|
+
existing dispatcher and worker lanes execute. It turns "hand-wire an orchestrator
|
|
13
|
+
from scratch every run" into a repeatable, durable, inspectable run on the board
|
|
14
|
+
you already use.
|
|
15
|
+
|
|
16
|
+
## What it is
|
|
17
|
+
|
|
18
|
+
A template compiles to a linked card-graph. Materialization is **lazy and
|
|
19
|
+
single-level**:
|
|
20
|
+
|
|
21
|
+
- `workflow_start` validates the template + bindings, runs a per-profile
|
|
22
|
+
pre-flight, and creates the **dynamic-free prefix** (the static stages up to
|
|
23
|
+
the first fan-out) plus a write-once compiled-snapshot **blackboard root**.
|
|
24
|
+
- The `post_tool_call` **fan-out hook** fires as stages complete: when a source
|
|
25
|
+
stage finishes, it creates that fan-out's children **and** its join in one
|
|
26
|
+
atomic create, wiring the join as a child of every instance.
|
|
27
|
+
|
|
28
|
+
Completion-gating is **deterministic and injection-free**, enforced by a
|
|
29
|
+
`pre_tool_call` veto: it checks the `expand_out` shape + `max`, and commit-clean
|
|
30
|
+
for worktree stages, before a stage may complete. There is no LLM in the gate.
|
|
31
|
+
|
|
32
|
+
All state lives on the board — body sentinels, links, and statuses — and the run
|
|
33
|
+
root is a write-once compiled-snapshot blackboard. Nothing touches raw SQLite —
|
|
34
|
+
every mutation goes through Hermes' board APIs (`kanban_*` from workers, `kb.*`
|
|
35
|
+
from the host).
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- **Hermes Agent v0.15.x**
|
|
40
|
+
- **Python ≥ 3.11**
|
|
41
|
+
- For any role with `lane: codex`: the `codex` binary on `PATH`, and Hermes'
|
|
42
|
+
**bundled `kanban-codex-lane` skill** present in that role's bound profile.
|
|
43
|
+
This plugin **reuses** that skill — it does **not** re-ship it.
|
|
44
|
+
|
|
45
|
+
## Quick Install
|
|
46
|
+
|
|
47
|
+
The intended primary path is PyPI:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pip install hermes-workflow
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
From source:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
git clone https://github.com/carlosraphael/hermes-workflow
|
|
57
|
+
cd hermes-workflow
|
|
58
|
+
pip install -e .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then **enable it in EVERY bound profile** — not just the orchestrator's. This is
|
|
62
|
+
a pip / entry-point plugin, so `hermes plugins enable` does **not** apply to it
|
|
63
|
+
(that command only sees user-dir and bundled plugins). Instead add
|
|
64
|
+
`hermes-workflow` to the `plugins.enabled` list in each bound profile's
|
|
65
|
+
`config.yaml` — open it with `hermes -p <profile> config edit` (or edit
|
|
66
|
+
`<that profile's HERMES_HOME>/config.yaml` directly) and add:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
plugins:
|
|
70
|
+
enabled:
|
|
71
|
+
- hermes-workflow
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The change takes effect on the **next session**. (Do **not** use
|
|
75
|
+
`hermes config set plugins.enabled …` — it writes a scalar string, which breaks
|
|
76
|
+
the loader's list check.)
|
|
77
|
+
|
|
78
|
+
**Per-profile enablement is the #1 failure mode.** Workers read the plugin's
|
|
79
|
+
load status from the *assignee profile's* home, so the plugin must be enabled
|
|
80
|
+
**and loadable** in every profile a role binds to. `workflow_start` runs a
|
|
81
|
+
per-profile loadability pre-flight and **fails fast — creating zero cards** —
|
|
82
|
+
with a per-profile remediation map if any bound profile cannot load it (or a
|
|
83
|
+
codex role is missing its `codex` binary / `kanban-codex-lane` skill).
|
|
84
|
+
|
|
85
|
+
## Quickstart
|
|
86
|
+
|
|
87
|
+
Using `examples/fix-flaky-tests.workflow.yaml`. There are two surfaces; both take
|
|
88
|
+
the same arguments.
|
|
89
|
+
|
|
90
|
+
In-session slash command:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
/workflow start --template examples/fix-flaky-tests.workflow.yaml \
|
|
94
|
+
--params '{"repo":"/abs/path/to/repo"}' \
|
|
95
|
+
--bindings '{"scout":"designer","fixer":"coder","reporter":"writer"}'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
CLI:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
hermes workflow start --template examples/fix-flaky-tests.workflow.yaml \
|
|
102
|
+
--params '{"repo":"/abs/path/to/repo"}' \
|
|
103
|
+
--bindings '{"scout":"designer","fixer":"coder","reporter":"writer"}'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Run lifecycle
|
|
107
|
+
|
|
108
|
+
1. The `scan` worker (role `scout`) inspects the repo and returns
|
|
109
|
+
`metadata.flaky = [{test_id, file}, …]`.
|
|
110
|
+
2. The fan-out hook creates one `fix` card per flaky test — each on its own
|
|
111
|
+
pre-provisioned worktree, gated by commit-clean — and wires the `approve`
|
|
112
|
+
human gate as a child of all of them.
|
|
113
|
+
3. Once every `fix` card is `done`, the gate promotes to `ready`.
|
|
114
|
+
4. The operator runs `hermes workflow approve <gate_card>`. Find the gate card
|
|
115
|
+
with `hermes workflow status <root_id>` (it is listed under
|
|
116
|
+
`awaiting_approval`).
|
|
117
|
+
5. `report` (role `reporter`) then runs, summarizing the fixes from the
|
|
118
|
+
handoffs and listing the branches.
|
|
119
|
+
|
|
120
|
+
## Commands
|
|
121
|
+
|
|
122
|
+
Both surfaces — the CLI (`hermes workflow <cmd>`) and the slash command
|
|
123
|
+
(`/workflow <cmd>`) — dispatch the same tools (`workflow_start` … `workflow_abandon`).
|
|
124
|
+
|
|
125
|
+
| Command | Form | What it does |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `start` | `--template <path> --params <json> --bindings <json> [--board]` | Validate, pre-flight every bound profile, then seed the run (root blackboard + dynamic-free prefix). Fails closed with zero cards if any profile can't load. |
|
|
128
|
+
| `status` | `<root_id> [--board]` | Read-only run summary: per-stage rollup plus `blocked_stages`, `awaiting_approval` (the gate signal), and `review_required`. |
|
|
129
|
+
| `validate` | `--template <path>` | Deterministic, read-only template check. Rejects verify/retry and nested-expand. |
|
|
130
|
+
| `reconcile` | `<root_id> [--board]` | Re-drive a partial fan-out: create missing children, re-link to the join, progress-aware dedup, and surface review-required stalls. |
|
|
131
|
+
| `approve` | `<gate_card> [--board]` | Complete a human gate card so its downstream stages promote natively. |
|
|
132
|
+
| `abandon` | `<root_id> [--board]` | Hazard-free teardown: reclaim running workers, then archive the run reverse-topologically (leaves-first). Worktrees are preserved. |
|
|
133
|
+
|
|
134
|
+
The four mutating commands (`start`, `reconcile`, `approve`, `abandon`) refuse to
|
|
135
|
+
run inside a dispatcher-spawned worker — they are orchestrator-context only.
|
|
136
|
+
|
|
137
|
+
## Bundled skills
|
|
138
|
+
|
|
139
|
+
Two skills are auto-registered on plugin load:
|
|
140
|
+
|
|
141
|
+
- **`workflow-author`** — how to write a `*.workflow.yaml` template within the
|
|
142
|
+
0.1.0 constraints; validate it with `workflow_validate`.
|
|
143
|
+
- **`workflow-orchestrator`** — start / status / approve / abandon / reconcile,
|
|
144
|
+
plus the review-required backstop for recovering a stalled stage.
|
|
145
|
+
|
|
146
|
+
## 0.1.0 scope
|
|
147
|
+
|
|
148
|
+
Lanes are `{profile, codex}` only — `claude-code` is deferred to 0.2.x.
|
|
149
|
+
`verify.command` / `retry` and granular `reject` / `modify` are deferred to
|
|
150
|
+
0.2.x. Nested fan-out (an `expand` stage that is itself an `expand` source) is
|
|
151
|
+
**forbidden** in 0.1.0.
|
|
152
|
+
|
|
153
|
+
## Documentation
|
|
154
|
+
|
|
155
|
+
| Document | What it covers |
|
|
156
|
+
|---|---|
|
|
157
|
+
| [`AGENTS.md`](AGENTS.md) | Development guide for AI coding assistants and contributors. |
|
|
158
|
+
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Development setup, PR process, and code style. |
|
|
159
|
+
| [`docs/operations.md`](docs/operations.md) | Upgrade / version-gate procedure and operational caveats. |
|
|
160
|
+
| [`CONTEXT.md`](CONTEXT.md) | The naming taxonomy (distribution / import / plugin / runtime layers). |
|
|
161
|
+
| [`docs/adr/`](docs/adr/) | Architecture decision records. |
|
|
162
|
+
|
|
163
|
+
## Contributing
|
|
164
|
+
|
|
165
|
+
Contributions are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md) for the
|
|
166
|
+
development setup, PR process, and code style.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT — Copyright (c) 2026 Carlos Raphael. See [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hermes-workflow"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "Declarative multi-stage workflow primitive over the Hermes Kanban board."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Carlos Raphael" }]
|
|
13
|
+
dependencies = ["PyYAML>=6,<7"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/carlosraphael/hermes-workflow"
|
|
25
|
+
Repository = "https://github.com/carlosraphael/hermes-workflow"
|
|
26
|
+
|
|
27
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
28
|
+
hermes-workflow = "hermes_workflow"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest>=8,<9"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
hermes_workflow = ["plugin.yaml", "skills/*/SKILL.md"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
markers = ["integration: requires a real Hermes board + plugin load (no LLM)"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# src/hermes_workflow/__init__.py
|
|
2
|
+
"""hermes-workflow: declarative workflow primitive over Hermes Kanban."""
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
from hermes_workflow.version import PLUGIN_VERSION
|
|
6
|
+
from hermes_workflow import tools, hooks
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register(ctx):
|
|
10
|
+
"""Wire every hermes-workflow surface into the Hermes plugin context.
|
|
11
|
+
|
|
12
|
+
Tools + CLI + slash are orchestrator-context mutators (they self-refuse inside
|
|
13
|
+
a worker). Both hooks are bound to ``ctx`` via a closure because Hermes' hook
|
|
14
|
+
invoke does NOT pass ctx (post_tool_call fan-out + pre_tool_call completion gate).
|
|
15
|
+
"""
|
|
16
|
+
for name, fn, schema in tools.TOOL_SPECS:
|
|
17
|
+
ctx.register_tool(name=name, toolset="workflow", schema=schema,
|
|
18
|
+
handler=tools.make_tool_handler(ctx, fn))
|
|
19
|
+
|
|
20
|
+
ctx.register_hook("post_tool_call", lambda **kw: hooks.on_tool_done(ctx=ctx, **kw))
|
|
21
|
+
ctx.register_hook("pre_tool_call", lambda **kw: hooks.on_tool_pre(ctx=ctx, **kw))
|
|
22
|
+
|
|
23
|
+
ctx.register_cli_command("workflow", help="manage hermes-workflow runs",
|
|
24
|
+
setup_fn=tools.cli_setup,
|
|
25
|
+
handler_fn=lambda args: tools.cli_dispatch(ctx, args))
|
|
26
|
+
ctx.register_command("workflow", lambda raw: tools.slash_dispatch(ctx, raw),
|
|
27
|
+
description="Manage hermes-workflow runs",
|
|
28
|
+
args_hint="<start|status|validate|reconcile|approve|abandon>")
|
|
29
|
+
|
|
30
|
+
# Bundled skills are shipped in Phase 4; guard the (currently absent) dir so
|
|
31
|
+
# register stays crash-free until then.
|
|
32
|
+
skills_dir = pathlib.Path(__file__).parent / "skills"
|
|
33
|
+
if skills_dir.is_dir():
|
|
34
|
+
for child in sorted(p for p in skills_dir.iterdir() if (p / "SKILL.md").exists()):
|
|
35
|
+
ctx.register_skill(child.name, child / "SKILL.md")
|
|
36
|
+
|
|
37
|
+
getattr(ctx, "log", None) and ctx.log.info("hermes-workflow %s registered", PLUGIN_VERSION)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# src/hermes_workflow/board.py
|
|
2
|
+
"""Board adapter: two surfaces over the Hermes Kanban.
|
|
3
|
+
|
|
4
|
+
WorkerBoard wraps the worker/hook *model-tool* surface (kanban_create/link/
|
|
5
|
+
comment/show) via ``ctx.dispatch_tool(name, args) -> JSON string``. Every call
|
|
6
|
+
gets an explicit ``board`` injected so it never relies on ambient env routing.
|
|
7
|
+
|
|
8
|
+
HostBoard wraps the orchestrator/CLI *host* surface (kb.* functions) for the
|
|
9
|
+
operations that have NO model tool — archive/unlink (Hermes finding Y) — plus a
|
|
10
|
+
board-wide list/reclaim. It is constructed with an already board-scoped conn.
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
from hermes_workflow.version import SENTINEL_ROOT_ASSIGNEE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkerBoard:
|
|
18
|
+
"""Model-tool surface, reached through a dispatch context.
|
|
19
|
+
|
|
20
|
+
``ctx`` only needs ``dispatch_tool(tool_name, args, **kwargs) -> str``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, ctx, board):
|
|
24
|
+
self.ctx = ctx
|
|
25
|
+
self.board = board
|
|
26
|
+
|
|
27
|
+
def _call(self, tool, args):
|
|
28
|
+
"""Dispatch a kanban tool with an explicit board, return parsed JSON.
|
|
29
|
+
|
|
30
|
+
Immutability: build a NEW args dict — never mutate the caller's.
|
|
31
|
+
"""
|
|
32
|
+
payload = {**args, "board": self.board}
|
|
33
|
+
return json.loads(self.ctx.dispatch_tool(tool, payload))
|
|
34
|
+
|
|
35
|
+
def create(self, *, title, parents=(), assignee=SENTINEL_ROOT_ASSIGNEE,
|
|
36
|
+
workspace_kind="scratch", workspace_path=None, body="",
|
|
37
|
+
skills=None, idempotency_key=None):
|
|
38
|
+
# Always pass workspace_kind explicitly: if BOTH workspace_kind and
|
|
39
|
+
# workspace_path are omitted, _handle_create INHERITS the calling
|
|
40
|
+
# worker's workspace (kanban_tools.py:746-794). Passing them keeps the
|
|
41
|
+
# child's workspace deterministic.
|
|
42
|
+
r = self._call("kanban_create", {
|
|
43
|
+
"title": title,
|
|
44
|
+
"assignee": assignee,
|
|
45
|
+
"parents": list(parents) if parents else [],
|
|
46
|
+
"workspace_kind": workspace_kind,
|
|
47
|
+
"workspace_path": workspace_path,
|
|
48
|
+
"body": body,
|
|
49
|
+
"skills": skills,
|
|
50
|
+
"idempotency_key": idempotency_key,
|
|
51
|
+
})
|
|
52
|
+
if not r.get("ok"):
|
|
53
|
+
raise BoardError(f"kanban_create failed: {r}")
|
|
54
|
+
return r["task_id"]
|
|
55
|
+
|
|
56
|
+
def link(self, parent_id, child_id):
|
|
57
|
+
r = self._call("kanban_link", {"parent_id": parent_id, "child_id": child_id})
|
|
58
|
+
if not r.get("ok"):
|
|
59
|
+
raise BoardError(f"kanban_link failed: {r}")
|
|
60
|
+
return r
|
|
61
|
+
|
|
62
|
+
def comment(self, task_id, body):
|
|
63
|
+
r = self._call("kanban_comment", {"task_id": task_id, "body": body})
|
|
64
|
+
if not r.get("ok"):
|
|
65
|
+
raise BoardError(f"kanban_comment failed: {r}")
|
|
66
|
+
return r
|
|
67
|
+
|
|
68
|
+
def complete(self, *, task_id, summary=None):
|
|
69
|
+
r = self._call("kanban_complete", {"task_id": task_id, "summary": summary})
|
|
70
|
+
if not r.get("ok"):
|
|
71
|
+
raise BoardError(f"kanban_complete failed: {r}")
|
|
72
|
+
return r
|
|
73
|
+
|
|
74
|
+
def show(self, task_id):
|
|
75
|
+
"""Return a FLAT card dict.
|
|
76
|
+
|
|
77
|
+
kanban_show (kanban_tools.py:339-412) returns a NESTED shape:
|
|
78
|
+
``{"task": {...}, "parents": [...], "children": [...], "runs": [...],
|
|
79
|
+
"comments": [...], ...}`` — NOT an _ok envelope. Downstream Phase-2 code
|
|
80
|
+
(RunView/materializer/hooks) wants a single flat card, so this is the
|
|
81
|
+
one place we normalize: merge the ``task`` sub-dict up to the top level
|
|
82
|
+
and re-attach the link/run/comment lists.
|
|
83
|
+
"""
|
|
84
|
+
raw = self._call("kanban_show", {"task_id": task_id})
|
|
85
|
+
if "task" not in raw:
|
|
86
|
+
# Error envelope ({"error": ...}/{"ok": false}) or a missing card.
|
|
87
|
+
raise BoardError(f"kanban_show returned no task for {task_id!r}: {raw}")
|
|
88
|
+
task = raw["task"]
|
|
89
|
+
return {
|
|
90
|
+
**task,
|
|
91
|
+
"parents": raw.get("parents", []),
|
|
92
|
+
"children": raw.get("children", []),
|
|
93
|
+
"runs": raw.get("runs", []),
|
|
94
|
+
"comments": raw.get("comments", []),
|
|
95
|
+
"events": raw.get("events", []),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class HostBoard:
|
|
100
|
+
"""Host kb.* surface for orchestrator/CLI-only operations.
|
|
101
|
+
|
|
102
|
+
The ``conn`` is already board-scoped, so kb.* calls take no ``board=``
|
|
103
|
+
kwarg. ``board`` is retained for identity/logging.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, kb, conn, board):
|
|
107
|
+
self.kb = kb
|
|
108
|
+
self.conn = conn
|
|
109
|
+
self.board = board
|
|
110
|
+
|
|
111
|
+
def list(self):
|
|
112
|
+
"""Board-wide sweep (the conn is already board-scoped)."""
|
|
113
|
+
return self.kb.list_tasks(self.conn)
|
|
114
|
+
|
|
115
|
+
def archive(self, task_id):
|
|
116
|
+
return self.kb.archive_task(self.conn, task_id)
|
|
117
|
+
|
|
118
|
+
def unlink(self, parent_id, child_id):
|
|
119
|
+
return self.kb.unlink_tasks(self.conn, parent_id, child_id)
|
|
120
|
+
|
|
121
|
+
def reclaim(self, task_id):
|
|
122
|
+
return self.kb.reclaim_task(self.conn, task_id)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class BoardError(RuntimeError):
|
|
126
|
+
"""Raised when a kanban model-tool call returns a non-ok / error payload."""
|
|
File without changes
|