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.
Files changed (36) hide show
  1. hermes_workflow-0.1.3/LICENSE +21 -0
  2. hermes_workflow-0.1.3/PKG-INFO +192 -0
  3. hermes_workflow-0.1.3/README.md +170 -0
  4. hermes_workflow-0.1.3/pyproject.toml +41 -0
  5. hermes_workflow-0.1.3/setup.cfg +4 -0
  6. hermes_workflow-0.1.3/src/hermes_workflow/__init__.py +37 -0
  7. hermes_workflow-0.1.3/src/hermes_workflow/board.py +126 -0
  8. hermes_workflow-0.1.3/src/hermes_workflow/engine/__init__.py +0 -0
  9. hermes_workflow-0.1.3/src/hermes_workflow/engine/graph.py +92 -0
  10. hermes_workflow-0.1.3/src/hermes_workflow/engine/interpolate.py +26 -0
  11. hermes_workflow-0.1.3/src/hermes_workflow/engine/model.py +60 -0
  12. hermes_workflow-0.1.3/src/hermes_workflow/engine/provenance.py +93 -0
  13. hermes_workflow-0.1.3/src/hermes_workflow/engine/reconcile.py +30 -0
  14. hermes_workflow-0.1.3/src/hermes_workflow/engine/template.py +111 -0
  15. hermes_workflow-0.1.3/src/hermes_workflow/hooks.py +137 -0
  16. hermes_workflow-0.1.3/src/hermes_workflow/lanes/__init__.py +0 -0
  17. hermes_workflow-0.1.3/src/hermes_workflow/lanes/presets.py +19 -0
  18. hermes_workflow-0.1.3/src/hermes_workflow/materialize.py +144 -0
  19. hermes_workflow-0.1.3/src/hermes_workflow/plugin.yaml +13 -0
  20. hermes_workflow-0.1.3/src/hermes_workflow/preflight.py +123 -0
  21. hermes_workflow-0.1.3/src/hermes_workflow/runview.py +100 -0
  22. hermes_workflow-0.1.3/src/hermes_workflow/skills/workflow-author/SKILL.md +176 -0
  23. hermes_workflow-0.1.3/src/hermes_workflow/skills/workflow-orchestrator/SKILL.md +165 -0
  24. hermes_workflow-0.1.3/src/hermes_workflow/sweep.py +30 -0
  25. hermes_workflow-0.1.3/src/hermes_workflow/tools.py +672 -0
  26. hermes_workflow-0.1.3/src/hermes_workflow/version.py +6 -0
  27. hermes_workflow-0.1.3/src/hermes_workflow/veto.py +85 -0
  28. hermes_workflow-0.1.3/src/hermes_workflow/worktree.py +74 -0
  29. hermes_workflow-0.1.3/src/hermes_workflow.egg-info/PKG-INFO +192 -0
  30. hermes_workflow-0.1.3/src/hermes_workflow.egg-info/SOURCES.txt +34 -0
  31. hermes_workflow-0.1.3/src/hermes_workflow.egg-info/dependency_links.txt +1 -0
  32. hermes_workflow-0.1.3/src/hermes_workflow.egg-info/entry_points.txt +2 -0
  33. hermes_workflow-0.1.3/src/hermes_workflow.egg-info/requires.txt +4 -0
  34. hermes_workflow-0.1.3/src/hermes_workflow.egg-info/top_level.txt +1 -0
  35. hermes_workflow-0.1.3/tests/test_preflight_eval.py +45 -0
  36. 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: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
28
+ [![Python: >=3.11](https://img.shields.io/badge/python-%3E%3D3.11-blue.svg)](pyproject.toml)
29
+ [![CI](https://github.com/carlosraphael/hermes-workflow/actions/workflows/ci.yml/badge.svg)](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: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
+ [![Python: >=3.11](https://img.shields.io/badge/python-%3E%3D3.11-blue.svg)](pyproject.toml)
7
+ [![CI](https://github.com/carlosraphael/hermes-workflow/actions/workflows/ci.yml/badge.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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."""