foreman-plane 0.1.0__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.
- foreman_plane-0.1.0/CHANGELOG.md +37 -0
- foreman_plane-0.1.0/LICENSE +21 -0
- foreman_plane-0.1.0/MANIFEST.in +10 -0
- foreman_plane-0.1.0/PKG-INFO +253 -0
- foreman_plane-0.1.0/README.md +230 -0
- foreman_plane-0.1.0/README.zh.md +216 -0
- foreman_plane-0.1.0/docs/IMPLEMENTATION_REPORT.md +184 -0
- foreman_plane-0.1.0/docs/PROTOCOL.md +304 -0
- foreman_plane-0.1.0/docs/SPEC.md +327 -0
- foreman_plane-0.1.0/examples/task_card.md +43 -0
- foreman_plane-0.1.0/pyproject.toml +38 -0
- foreman_plane-0.1.0/scripts/publish.sh +44 -0
- foreman_plane-0.1.0/setup.cfg +4 -0
- foreman_plane-0.1.0/src/foreman/__init__.py +13 -0
- foreman_plane-0.1.0/src/foreman/acceptance.py +140 -0
- foreman_plane-0.1.0/src/foreman/cli.py +292 -0
- foreman_plane-0.1.0/src/foreman/config.py +175 -0
- foreman_plane-0.1.0/src/foreman/contract.py +234 -0
- foreman_plane-0.1.0/src/foreman/errors.py +51 -0
- foreman_plane-0.1.0/src/foreman/gate.py +202 -0
- foreman_plane-0.1.0/src/foreman/ledger.py +238 -0
- foreman_plane-0.1.0/src/foreman/mcp_server.py +233 -0
- foreman_plane-0.1.0/src/foreman/runner.py +398 -0
- foreman_plane-0.1.0/src/foreman_plane.egg-info/PKG-INFO +253 -0
- foreman_plane-0.1.0/src/foreman_plane.egg-info/SOURCES.txt +33 -0
- foreman_plane-0.1.0/src/foreman_plane.egg-info/dependency_links.txt +1 -0
- foreman_plane-0.1.0/src/foreman_plane.egg-info/entry_points.txt +2 -0
- foreman_plane-0.1.0/src/foreman_plane.egg-info/top_level.txt +1 -0
- foreman_plane-0.1.0/tests/test_acceptance.py +88 -0
- foreman_plane-0.1.0/tests/test_cli.py +158 -0
- foreman_plane-0.1.0/tests/test_contract.py +104 -0
- foreman_plane-0.1.0/tests/test_gate.py +115 -0
- foreman_plane-0.1.0/tests/test_ledger.py +183 -0
- foreman_plane-0.1.0/tests/test_mcp_server.py +142 -0
- foreman_plane-0.1.0/tests/test_runner.py +115 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
|
+
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
First release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Task card contract** (`foreman.contract`): four markers — `【id】`,
|
|
14
|
+
`【depends_on】`, `【needs_api】`, `【验收】` — tolerant of full-width/half-width
|
|
15
|
+
brackets, casing and colon variants. A marker that yields no legal token is
|
|
16
|
+
reported through an explicit `*_unparsed` flag instead of being silently
|
|
17
|
+
treated as "no dependency".
|
|
18
|
+
- **Two-level dependency gate** (`foreman.gate`): hard dependencies must be
|
|
19
|
+
terminal, `pass` and accepted; soft dependencies only have to exist and are
|
|
20
|
+
reported as `needs_api_pending`. Six blocking reasons: `task_id_missing`,
|
|
21
|
+
`dependency_unparsed`, `dependency_cycle`, `dependency_unknown`,
|
|
22
|
+
`dependency_pending`, `dependency_failed`, `dependency_unaccepted`.
|
|
23
|
+
- **Append-only ledger** (`foreman.ledger`): JSONL events (`dispatch`,
|
|
24
|
+
`running`, `blocked`, `done`, `cancelled`, `replan`), state folding, task
|
|
25
|
+
id idempotency and `UnknownTaskError` for unknown task ids.
|
|
26
|
+
- **Objective artefact acceptance** (`foreman.acceptance`):
|
|
27
|
+
`path::min_bytes=N::sha256=<hex>::contains=<substr>::exit_code=N`.
|
|
28
|
+
- **Execution body adapters** (`foreman.runner`): `claude`, `codex`, `dsh`,
|
|
29
|
+
a `generic` template and custom agents declared in `agents.json`.
|
|
30
|
+
- **MCP server** (`foreman.mcp_server`): JSON-RPC 2.0 over stdio with NDJSON
|
|
31
|
+
framing, three tools (`foreman_dispatch`, `foreman_status`,
|
|
32
|
+
`foreman_cancel`).
|
|
33
|
+
- **CLI** (`foreman.cli`): `dispatch`, `status`, `ledger`, `verify`, `mcp`,
|
|
34
|
+
`config`, with `--json` output and stable exit codes.
|
|
35
|
+
- Zero third-party dependencies, Python >= 3.10, `unittest` test suite.
|
|
36
|
+
|
|
37
|
+
[0.1.0]: https://example.invalid/foreman-plane/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Foreman Plane contributors
|
|
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,10 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include README.zh.md
|
|
4
|
+
include CHANGELOG.md
|
|
5
|
+
recursive-include docs *.md
|
|
6
|
+
recursive-include examples *.md
|
|
7
|
+
recursive-include scripts *.sh
|
|
8
|
+
recursive-include tests *.py
|
|
9
|
+
global-exclude __pycache__/*
|
|
10
|
+
global-exclude *.py[cod]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foreman-plane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A task governance plane that runs outside the agent harness: dispatch, dependency gates, objective acceptance, append-only ledger.
|
|
5
|
+
Author: Foreman Plane contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Anthony-Vivalink/foreman-plane
|
|
8
|
+
Project-URL: Source, https://github.com/Anthony-Vivalink/foreman-plane
|
|
9
|
+
Project-URL: Issues, https://github.com/Anthony-Vivalink/foreman-plane/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Anthony-Vivalink/foreman-plane/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: agents,mcp,orchestration,task-governance,audit
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# foreman-plane
|
|
25
|
+
|
|
26
|
+
**A task governance plane that runs *outside* your agent harness.**
|
|
27
|
+
|
|
28
|
+
foreman-plane (the "foreman") is the layer *above* the agent, not inside it.
|
|
29
|
+
Claude Code, Codex, DeepSeek Harness or any CLI agent are pluggable
|
|
30
|
+
execution bodies; the plane owns the parts an agent session cannot own:
|
|
31
|
+
dispatch, dependency gating, objective acceptance and an append-only audit
|
|
32
|
+
trail.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌───────────────────────────────────────────┐
|
|
36
|
+
│ foreman-plane (the foreman) │
|
|
37
|
+
│ contract → gate → ledger → acceptance │
|
|
38
|
+
└───┬───────────────┬───────────────┬───────┘
|
|
39
|
+
│ MCP/stdio │ MCP/stdio │ MCP/stdio
|
|
40
|
+
┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
|
|
41
|
+
│ Claude │ │ Codex │ │ DSH │ ← execution bodies
|
|
42
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
* **Cross session** — the ledger outlives every conversation.
|
|
46
|
+
* **Cross instance** — several agents share one task graph.
|
|
47
|
+
* **Cross day** — a task dispatched today can be resumed tomorrow by any body.
|
|
48
|
+
* **Outside the harness** — the plane is a plain process; no agent framework
|
|
49
|
+
has to be patched to use it.
|
|
50
|
+
|
|
51
|
+
Zero third-party dependencies. Python ≥ 3.10. Standard library only.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# 1. run it straight from the checkout
|
|
59
|
+
cd foreman-plane
|
|
60
|
+
export PYTHONPATH="$PWD/src"
|
|
61
|
+
export FOREMAN_HOME="$PWD/.foreman"
|
|
62
|
+
|
|
63
|
+
# 2. declare one execution body
|
|
64
|
+
cat > "$FOREMAN_HOME/agents.json" <<'JSON'
|
|
65
|
+
{"agents": {"mybot": {"command": "mybot run --prompt {task}"}}}
|
|
66
|
+
JSON
|
|
67
|
+
|
|
68
|
+
# 3. dispatch a task card
|
|
69
|
+
cat > card.md <<'MD'
|
|
70
|
+
【id】T-20261109-20
|
|
71
|
+
【depends_on】T-20261109-01
|
|
72
|
+
【验收】reports/report.md::min_bytes=200
|
|
73
|
+
写一份报告。
|
|
74
|
+
MD
|
|
75
|
+
|
|
76
|
+
python3 -m foreman.cli dispatch --file card.md --agent mybot --json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`python -m foreman.cli` and the `foreman` console script are equivalent.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## The task card
|
|
84
|
+
|
|
85
|
+
A task card is plain text. Four markers may appear anywhere in it:
|
|
86
|
+
|
|
87
|
+
| marker | meaning |
|
|
88
|
+
| --------------- | ------------------------------------------------------------ |
|
|
89
|
+
| `【id】` | task id, `T-YYYYMMDD-NN` |
|
|
90
|
+
| `【depends_on】` | **hard** dependency — the upstream must be done and `pass` |
|
|
91
|
+
| `【needs_api】` | **soft** dependency — an artefact that only has to exist |
|
|
92
|
+
| `【验收】` | acceptance artefact plus optional objective criteria |
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
【id】T-20261109-20
|
|
96
|
+
【depends_on】T-20261109-01
|
|
97
|
+
【needs_api】T-20261109-01#/var/lib/foreman/api/order_book.json
|
|
98
|
+
【验收】reports/order_book_20261109.md::min_bytes=200::contains=结论
|
|
99
|
+
|
|
100
|
+
请读取 order_book.json,产出分析报告并给出结论小节。
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Marker syntax is forgiving (`【id】`, `[id]`, `ID:`, full-width spaces and
|
|
104
|
+
colons all work), but a dependency that cannot be parsed is **never**
|
|
105
|
+
silently dropped: the dispatch is refused with `dependency_unparsed`.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## What the plane guarantees
|
|
110
|
+
|
|
111
|
+
### Hard gate — a task never runs too early
|
|
112
|
+
|
|
113
|
+
A task with `【depends_on】` only starts when every upstream task is
|
|
114
|
+
terminal, passed **and** accepted. Otherwise the plane writes a `blocked`
|
|
115
|
+
event and stops. There is no `dispatch` event for a task that did not run,
|
|
116
|
+
so the ledger cannot show a ghost dispatch.
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
【depends_on】T-20261109-01 → T-20261109-01 done, verdict=pass → dispatch
|
|
120
|
+
→ T-20261109-01 still running → blocked(dependency_pending)
|
|
121
|
+
→ T-20261109-01 unknown → blocked(dependency_unknown)
|
|
122
|
+
→ T-20261109-01 verdict=fail → blocked(dependency_failed)
|
|
123
|
+
→ unparsable dependency marker → blocked(dependency_unparsed)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Soft gate — a task may start without a missing artefact
|
|
127
|
+
|
|
128
|
+
`【needs_api】` marks artefacts that must be *ready* but do not have to be
|
|
129
|
+
finished. A missing artefact is reported as `needs_api_pending` and the
|
|
130
|
+
task still runs.
|
|
131
|
+
|
|
132
|
+
### Objective acceptance — the artefact decides, not the model
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
【验收】reports/report.md::min_bytes=200::sha256=<hex>::contains=结论
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Supported criteria: `min_bytes`, `sha256`, `contains`, `exit_code`. All
|
|
139
|
+
criteria are ANDed, relative paths resolve against the working directory,
|
|
140
|
+
and an unknown criterion fails closed.
|
|
141
|
+
|
|
142
|
+
### Append-only audit trail
|
|
143
|
+
|
|
144
|
+
Every decision is one JSON line in `${FOREMAN_HOME}/ledger.jsonl`:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{"ts":"2026-11-09T09:41:02.113456+00:00","event":"dispatch","task_id":"T-20261109-20","actor":"foreman","depends_on":["T-20261109-01"],"needs_api":[],"acceptance":"reports/report.md::min_bytes=200","needs_api_pending":false,"detail":{}}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Event types: `dispatch`, `running`, `blocked`, `done`, `cancelled`,
|
|
151
|
+
`replan`. History is never rewritten or deleted.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## MCP integration
|
|
156
|
+
|
|
157
|
+
The plane ships an MCP server that speaks JSON-RPC 2.0 over stdio with
|
|
158
|
+
NDJSON framing (one JSON object per line). Point any MCP client at it:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python3 -m foreman.mcp_server
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
For a harness that loads MCP servers from a patch file, the entry looks
|
|
165
|
+
like this (the exact field names follow the MCP client in use — only the
|
|
166
|
+
stdio JSON-RPC handshake is fixed by the plane):
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
# ~/.dsh/profiles/headless/cordis.patch.yml
|
|
170
|
+
mcp:
|
|
171
|
+
servers:
|
|
172
|
+
- name: foreman
|
|
173
|
+
command: python3
|
|
174
|
+
args:
|
|
175
|
+
- -m
|
|
176
|
+
- foreman.mcp_server
|
|
177
|
+
env:
|
|
178
|
+
FOREMAN_HOME: /var/lib/foreman
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Three tools are exposed:
|
|
182
|
+
|
|
183
|
+
| tool | purpose |
|
|
184
|
+
| ----------------- | ---------------------------------------------------------- |
|
|
185
|
+
| `foreman_dispatch`| dispatch a task card (parse → gate → run → accept) |
|
|
186
|
+
| `foreman_status` | ledger status of one task or of everything |
|
|
187
|
+
| `foreman_cancel` | cancel a task (unknown ids are rejected) |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## CLI
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
foreman dispatch --file TASK.md [--agent NAME] [--workdir DIR] [--force] [--json]
|
|
195
|
+
foreman status [--task-id ID] [--json]
|
|
196
|
+
foreman ledger list | show | ready | blocked | cancel | replan
|
|
197
|
+
foreman verify --acceptance "<验收串>"
|
|
198
|
+
foreman mcp
|
|
199
|
+
foreman config
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Exit codes: `0` success · `2` blocked by the gate · `3` artefact acceptance
|
|
203
|
+
failed · `1` any other error.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Configuration
|
|
208
|
+
|
|
209
|
+
| variable | meaning |
|
|
210
|
+
| ----------------- | ---------------------------------------------------- |
|
|
211
|
+
| `FOREMAN_HOME` | plane home (ledger, runs, agents.json) |
|
|
212
|
+
| `FOREMAN_TIMEOUT` | execution timeout in seconds (default 900) |
|
|
213
|
+
| `FOREMAN_AGENT` | default execution body (default `generic`) |
|
|
214
|
+
| `FOREMAN_ACTOR` | actor recorded in the ledger (default `foreman`) |
|
|
215
|
+
|
|
216
|
+
`FOREMAN_HOME` is resolved as: the environment variable → `$HOME/.foreman`
|
|
217
|
+
→ `<cwd>/.foreman`. When `HOME` is not set the plane still works.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## How this differs from other approaches
|
|
222
|
+
|
|
223
|
+
* **Against "the agent will remember"** — a session that forgets cannot
|
|
224
|
+
enforce a dependency. The gate is a separate process with its own
|
|
225
|
+
durable state, so it keeps working across sessions, agents and days.
|
|
226
|
+
* **Against "the agent will self-report"** — the verdict is derived from an
|
|
227
|
+
artefact on disk (`min_bytes`, `sha256`, `contains`, `exit_code`), not from
|
|
228
|
+
a sentence in the transcript. A task that promises a report but writes no
|
|
229
|
+
report fails.
|
|
230
|
+
* **Against "one big agent"** — the plane does not choose a model and does
|
|
231
|
+
not care which agent runs a task. Execution bodies are interchangeable
|
|
232
|
+
commands; governance stays in one place.
|
|
233
|
+
* **Against "a workflow engine"** — there is no DAG runtime, no scheduler and
|
|
234
|
+
no background worker. The plane is a ledger plus a gate: it decides
|
|
235
|
+
*whether* a task may start, and leaves *how* to the body that is wired in.
|
|
236
|
+
* **Against "a chat transcript as state"** — the ledger is append-only and
|
|
237
|
+
machine readable, so it can be queried, diffed and audited by something
|
|
238
|
+
that is not an LLM.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The specification lives in [`docs/SPEC.md`](docs/SPEC.md), the wire
|
|
249
|
+
contract in [`docs/PROTOCOL.md`](docs/PROTOCOL.md).
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# foreman-plane
|
|
2
|
+
|
|
3
|
+
**A task governance plane that runs *outside* your agent harness.**
|
|
4
|
+
|
|
5
|
+
foreman-plane (the "foreman") is the layer *above* the agent, not inside it.
|
|
6
|
+
Claude Code, Codex, DeepSeek Harness or any CLI agent are pluggable
|
|
7
|
+
execution bodies; the plane owns the parts an agent session cannot own:
|
|
8
|
+
dispatch, dependency gating, objective acceptance and an append-only audit
|
|
9
|
+
trail.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌───────────────────────────────────────────┐
|
|
13
|
+
│ foreman-plane (the foreman) │
|
|
14
|
+
│ contract → gate → ledger → acceptance │
|
|
15
|
+
└───┬───────────────┬───────────────┬───────┘
|
|
16
|
+
│ MCP/stdio │ MCP/stdio │ MCP/stdio
|
|
17
|
+
┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
|
|
18
|
+
│ Claude │ │ Codex │ │ DSH │ ← execution bodies
|
|
19
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
* **Cross session** — the ledger outlives every conversation.
|
|
23
|
+
* **Cross instance** — several agents share one task graph.
|
|
24
|
+
* **Cross day** — a task dispatched today can be resumed tomorrow by any body.
|
|
25
|
+
* **Outside the harness** — the plane is a plain process; no agent framework
|
|
26
|
+
has to be patched to use it.
|
|
27
|
+
|
|
28
|
+
Zero third-party dependencies. Python ≥ 3.10. Standard library only.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 1. run it straight from the checkout
|
|
36
|
+
cd foreman-plane
|
|
37
|
+
export PYTHONPATH="$PWD/src"
|
|
38
|
+
export FOREMAN_HOME="$PWD/.foreman"
|
|
39
|
+
|
|
40
|
+
# 2. declare one execution body
|
|
41
|
+
cat > "$FOREMAN_HOME/agents.json" <<'JSON'
|
|
42
|
+
{"agents": {"mybot": {"command": "mybot run --prompt {task}"}}}
|
|
43
|
+
JSON
|
|
44
|
+
|
|
45
|
+
# 3. dispatch a task card
|
|
46
|
+
cat > card.md <<'MD'
|
|
47
|
+
【id】T-20261109-20
|
|
48
|
+
【depends_on】T-20261109-01
|
|
49
|
+
【验收】reports/report.md::min_bytes=200
|
|
50
|
+
写一份报告。
|
|
51
|
+
MD
|
|
52
|
+
|
|
53
|
+
python3 -m foreman.cli dispatch --file card.md --agent mybot --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`python -m foreman.cli` and the `foreman` console script are equivalent.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## The task card
|
|
61
|
+
|
|
62
|
+
A task card is plain text. Four markers may appear anywhere in it:
|
|
63
|
+
|
|
64
|
+
| marker | meaning |
|
|
65
|
+
| --------------- | ------------------------------------------------------------ |
|
|
66
|
+
| `【id】` | task id, `T-YYYYMMDD-NN` |
|
|
67
|
+
| `【depends_on】` | **hard** dependency — the upstream must be done and `pass` |
|
|
68
|
+
| `【needs_api】` | **soft** dependency — an artefact that only has to exist |
|
|
69
|
+
| `【验收】` | acceptance artefact plus optional objective criteria |
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
【id】T-20261109-20
|
|
73
|
+
【depends_on】T-20261109-01
|
|
74
|
+
【needs_api】T-20261109-01#/var/lib/foreman/api/order_book.json
|
|
75
|
+
【验收】reports/order_book_20261109.md::min_bytes=200::contains=结论
|
|
76
|
+
|
|
77
|
+
请读取 order_book.json,产出分析报告并给出结论小节。
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Marker syntax is forgiving (`【id】`, `[id]`, `ID:`, full-width spaces and
|
|
81
|
+
colons all work), but a dependency that cannot be parsed is **never**
|
|
82
|
+
silently dropped: the dispatch is refused with `dependency_unparsed`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## What the plane guarantees
|
|
87
|
+
|
|
88
|
+
### Hard gate — a task never runs too early
|
|
89
|
+
|
|
90
|
+
A task with `【depends_on】` only starts when every upstream task is
|
|
91
|
+
terminal, passed **and** accepted. Otherwise the plane writes a `blocked`
|
|
92
|
+
event and stops. There is no `dispatch` event for a task that did not run,
|
|
93
|
+
so the ledger cannot show a ghost dispatch.
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
【depends_on】T-20261109-01 → T-20261109-01 done, verdict=pass → dispatch
|
|
97
|
+
→ T-20261109-01 still running → blocked(dependency_pending)
|
|
98
|
+
→ T-20261109-01 unknown → blocked(dependency_unknown)
|
|
99
|
+
→ T-20261109-01 verdict=fail → blocked(dependency_failed)
|
|
100
|
+
→ unparsable dependency marker → blocked(dependency_unparsed)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Soft gate — a task may start without a missing artefact
|
|
104
|
+
|
|
105
|
+
`【needs_api】` marks artefacts that must be *ready* but do not have to be
|
|
106
|
+
finished. A missing artefact is reported as `needs_api_pending` and the
|
|
107
|
+
task still runs.
|
|
108
|
+
|
|
109
|
+
### Objective acceptance — the artefact decides, not the model
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
【验收】reports/report.md::min_bytes=200::sha256=<hex>::contains=结论
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Supported criteria: `min_bytes`, `sha256`, `contains`, `exit_code`. All
|
|
116
|
+
criteria are ANDed, relative paths resolve against the working directory,
|
|
117
|
+
and an unknown criterion fails closed.
|
|
118
|
+
|
|
119
|
+
### Append-only audit trail
|
|
120
|
+
|
|
121
|
+
Every decision is one JSON line in `${FOREMAN_HOME}/ledger.jsonl`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{"ts":"2026-11-09T09:41:02.113456+00:00","event":"dispatch","task_id":"T-20261109-20","actor":"foreman","depends_on":["T-20261109-01"],"needs_api":[],"acceptance":"reports/report.md::min_bytes=200","needs_api_pending":false,"detail":{}}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Event types: `dispatch`, `running`, `blocked`, `done`, `cancelled`,
|
|
128
|
+
`replan`. History is never rewritten or deleted.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## MCP integration
|
|
133
|
+
|
|
134
|
+
The plane ships an MCP server that speaks JSON-RPC 2.0 over stdio with
|
|
135
|
+
NDJSON framing (one JSON object per line). Point any MCP client at it:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python3 -m foreman.mcp_server
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
For a harness that loads MCP servers from a patch file, the entry looks
|
|
142
|
+
like this (the exact field names follow the MCP client in use — only the
|
|
143
|
+
stdio JSON-RPC handshake is fixed by the plane):
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
# ~/.dsh/profiles/headless/cordis.patch.yml
|
|
147
|
+
mcp:
|
|
148
|
+
servers:
|
|
149
|
+
- name: foreman
|
|
150
|
+
command: python3
|
|
151
|
+
args:
|
|
152
|
+
- -m
|
|
153
|
+
- foreman.mcp_server
|
|
154
|
+
env:
|
|
155
|
+
FOREMAN_HOME: /var/lib/foreman
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Three tools are exposed:
|
|
159
|
+
|
|
160
|
+
| tool | purpose |
|
|
161
|
+
| ----------------- | ---------------------------------------------------------- |
|
|
162
|
+
| `foreman_dispatch`| dispatch a task card (parse → gate → run → accept) |
|
|
163
|
+
| `foreman_status` | ledger status of one task or of everything |
|
|
164
|
+
| `foreman_cancel` | cancel a task (unknown ids are rejected) |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## CLI
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
foreman dispatch --file TASK.md [--agent NAME] [--workdir DIR] [--force] [--json]
|
|
172
|
+
foreman status [--task-id ID] [--json]
|
|
173
|
+
foreman ledger list | show | ready | blocked | cancel | replan
|
|
174
|
+
foreman verify --acceptance "<验收串>"
|
|
175
|
+
foreman mcp
|
|
176
|
+
foreman config
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Exit codes: `0` success · `2` blocked by the gate · `3` artefact acceptance
|
|
180
|
+
failed · `1` any other error.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Configuration
|
|
185
|
+
|
|
186
|
+
| variable | meaning |
|
|
187
|
+
| ----------------- | ---------------------------------------------------- |
|
|
188
|
+
| `FOREMAN_HOME` | plane home (ledger, runs, agents.json) |
|
|
189
|
+
| `FOREMAN_TIMEOUT` | execution timeout in seconds (default 900) |
|
|
190
|
+
| `FOREMAN_AGENT` | default execution body (default `generic`) |
|
|
191
|
+
| `FOREMAN_ACTOR` | actor recorded in the ledger (default `foreman`) |
|
|
192
|
+
|
|
193
|
+
`FOREMAN_HOME` is resolved as: the environment variable → `$HOME/.foreman`
|
|
194
|
+
→ `<cwd>/.foreman`. When `HOME` is not set the plane still works.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## How this differs from other approaches
|
|
199
|
+
|
|
200
|
+
* **Against "the agent will remember"** — a session that forgets cannot
|
|
201
|
+
enforce a dependency. The gate is a separate process with its own
|
|
202
|
+
durable state, so it keeps working across sessions, agents and days.
|
|
203
|
+
* **Against "the agent will self-report"** — the verdict is derived from an
|
|
204
|
+
artefact on disk (`min_bytes`, `sha256`, `contains`, `exit_code`), not from
|
|
205
|
+
a sentence in the transcript. A task that promises a report but writes no
|
|
206
|
+
report fails.
|
|
207
|
+
* **Against "one big agent"** — the plane does not choose a model and does
|
|
208
|
+
not care which agent runs a task. Execution bodies are interchangeable
|
|
209
|
+
commands; governance stays in one place.
|
|
210
|
+
* **Against "a workflow engine"** — there is no DAG runtime, no scheduler and
|
|
211
|
+
no background worker. The plane is a ledger plus a gate: it decides
|
|
212
|
+
*whether* a task may start, and leaves *how* to the body that is wired in.
|
|
213
|
+
* **Against "a chat transcript as state"** — the ledger is append-only and
|
|
214
|
+
machine readable, so it can be queried, diffed and audited by something
|
|
215
|
+
that is not an LLM.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The specification lives in [`docs/SPEC.md`](docs/SPEC.md), the wire
|
|
226
|
+
contract in [`docs/PROTOCOL.md`](docs/PROTOCOL.md).
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT — see [`LICENSE`](LICENSE).
|