docs: rewrite README to the docs standard; polish docs/
README: verified quickstart (Docker builder, dry-run, live demo), mermaid
architecture, CLI/config/routing reference tables, status table from
REPORT.md, correct ukrrs repo URLs. docs/: Status front-matter lines on
all PORTING-NOTES (secrets notes now tracked). DESIGN: table of contents
with anchor links; sections untouched.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -1,16 +1,248 @@
|
||||
# MOPAC harness
|
||||
|
||||
Headless multi-vertical agent harness in Go. AGPLv3. SoR: Redmine project MOPAC; docs: Discourse; code: Gitea reachableceo/MOPAC.
|
||||
Spec: DESIGN.md. Status: see Redmine MOPAC project issues and REPORT.md.
|
||||
The MOPAC harness is a headless agent conductor written in Go: it pulls a task
|
||||
from Redmine (or a demo issue), routes it to the right model through LiteLLM,
|
||||
runs ONE bounded turn with an allow-listed bash tool, and writes the result as
|
||||
a REPORT file. There is no daemon and no TUI — callers chain turns by
|
||||
re-invoking `harness once`. It is the execution core for the org's vertical
|
||||
stacks: humans and other agents interact with a stack only through Redmine
|
||||
(SoR), Discourse (docs) and Gitea (code), never by attaching to the loop.
|
||||
|
||||
## Build & run (v0 skeleton)
|
||||
Status: 2026-08-28 — skeleton; MVP demo path live (single GLM turn through
|
||||
LiteLLM to REPORT, proven live 2026-08-28).
|
||||
|
||||
go build ./... && go vet ./... && go test ./...
|
||||
cp harness.toml.example harness.toml # then set the key refs
|
||||
./bin/harness once --dry-run --demo # intake + plan, no LLM call
|
||||
HARNESS_LITELLM_KEY=sk-... ./bin/harness once --demo # MVP demo turn
|
||||
## Quickstart
|
||||
|
||||
`harness once` runs ONE conductor iteration (intake -> model routing ->
|
||||
bounded turn via LiteLLM -> REPORT file) and exits; chain iterations by
|
||||
re-invoking. There is no daemon. Exit codes: 0 ok, 1 config/usage,
|
||||
2 intake, 4 llm/turn. See `harness help`.
|
||||
All dev work happens inside a Docker builder (host stays toolchain-free);
|
||||
`docker pull` of the builder is pre-authorized. Commands below were verified
|
||||
on 2026-08-28 from a fresh clone.
|
||||
|
||||
### Build and test
|
||||
|
||||
```sh
|
||||
docker run --rm -v "$PWD:/h" -w /h \
|
||||
-u "$(id -u):$(id -g)" -e HOME=/tmp \
|
||||
golang:1.26-bookworm \
|
||||
sh -c 'go build -o bin/harness ./cmd/harness && go vet ./... && go test ./...'
|
||||
```
|
||||
|
||||
Expected output (tail):
|
||||
|
||||
```text
|
||||
ok git.knownelement.com/reachableceo/MOPAC/harness/internal/config
|
||||
ok git.knownelement.com/reachableceo/MOPAC/harness/internal/intake
|
||||
ok git.knownelement.com/reachableceo/MOPAC/harness/internal/llm
|
||||
ok git.knownelement.com/reachableceo/MOPAC/harness/internal/loop
|
||||
ok git.knownelement.com/reachableceo/MOPAC/harness/internal/models
|
||||
ok git.knownelement.com/reachableceo/MOPAC/harness/internal/tools
|
||||
ok git.knownelement.com/reachableceo/MOPAC/harness/internal/writeback
|
||||
```
|
||||
|
||||
(The module path still carries the old `reachableceo` org prefix; the repo
|
||||
lives at [git.knownelement.com/ukrrs/MOPAC](https://git.knownelement.com/ukrrs/MOPAC).)
|
||||
|
||||
### Configure
|
||||
|
||||
```sh
|
||||
cp harness.toml.example harness.toml
|
||||
```
|
||||
|
||||
`harness.toml` is gitignored. It holds no secrets — only key refs
|
||||
(`env:NAME`, `file:PATH`, `literal:VALUE`; `bw:` is reserved for the
|
||||
bitwarden wrapper, build phase 3) that are resolved at runtime and redacted
|
||||
from every error path.
|
||||
|
||||
### Dry-run (no LLM call, no REPORT)
|
||||
|
||||
```sh
|
||||
./bin/harness once --dry-run --demo
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
harness: intake: demo issue from harness.toml [demo]
|
||||
harness: task demo-1 (demo): "MVP demo: GLM self-description" class=primary -> mopac-primary -> glm-5.3
|
||||
PLAN (dry-run)
|
||||
vertical: demo
|
||||
task: [demo-1] MVP demo: GLM self-description (source demo)
|
||||
routing: class "primary" -> tier mopac-primary -> model glm-5.3
|
||||
tools: bash (allow=18 deny=7 default=deny timeout=60s)
|
||||
bound: max 8 rounds
|
||||
harness: dry-run complete, no LLM call made
|
||||
```
|
||||
|
||||
### Live demo turn (the MVP bar)
|
||||
|
||||
```sh
|
||||
HARNESS_LITELLM_KEY=<vertical virtual key> ./bin/harness once --demo
|
||||
```
|
||||
|
||||
The model's reply lands verbatim in `reports/REPORT-latest.md` with model /
|
||||
tier / class / tokens / rounds telemetry. Without the key the run fails fast
|
||||
and clean (verified):
|
||||
|
||||
```text
|
||||
harness: litellm key: environment variable HARNESS_LITELLM_KEY is not set
|
||||
```
|
||||
|
||||
exit code 1 (config error). The with-key path was proven live on 2026-08-28:
|
||||
the `[demo]` prompt "tell me about yourself" routed to `glm-5.3` via tier
|
||||
`mopac-primary` and the GLM self-description landed as the REPORT.
|
||||
|
||||
### Help
|
||||
|
||||
```sh
|
||||
./bin/harness help
|
||||
```
|
||||
|
||||
Prints the full usage block reproduced under [CLI reference](#cli-reference).
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["harness once"]) --> I["INTAKE<br/>Redmine /issues.json scope query<br/>or the [demo] issue"]
|
||||
I -->|"task + class"| RT["ROUTING<br/>class -> tier -> concrete model<br/>[models] + [models.classes]"]
|
||||
RT --> T["BOUNDED TURN<br/>OpenAI-compatible chat via LiteLLM<br/>gated bash tool, max_rounds cap"]
|
||||
T --> W["REPORT<br/>reports/REPORT-<vertical>-<task>-<ts>.md<br/>+ REPORT-latest.md, atomic write"]
|
||||
W --> E(["exit 0"])
|
||||
E -.->|"re-invoke to chain the next turn"| S
|
||||
```
|
||||
|
||||
One conductor iteration per process: INTAKE resolves the released scope (a
|
||||
Redmine query, or the `[demo]` issue with `--demo`) into a task with a class;
|
||||
ROUTING maps the class through the config-only tier table to a concrete proxy
|
||||
model; the bounded TURN drives an OpenAI-compatible chat loop through LiteLLM
|
||||
(retry/backoff on 429/5xx/transport, usage accounting) with a tool-calling
|
||||
loop capped at `max_rounds`, where the only tool today is an allow-listed
|
||||
bash exec whose gate denials feed back to the model as tool results instead
|
||||
of failing the turn; WRITEBACK persists the final assistant reply as a REPORT
|
||||
file plus `REPORT-latest.md` (atomic tmp+rename). On a mid-turn LLM failure
|
||||
after content exists, a partial REPORT is still written with the error as the
|
||||
stop reason. Chaining is by re-invocation — there is no daemon.
|
||||
|
||||
## CLI reference
|
||||
|
||||
Subcommands (from `harness help`):
|
||||
|
||||
| Subcommand | Purpose |
|
||||
|---|---|
|
||||
| `harness help` | print usage (also `-h`, `--help`) |
|
||||
| `harness once` | run ONE conductor iteration, then exit |
|
||||
|
||||
Flags for `once`:
|
||||
|
||||
| Flag | Meaning |
|
||||
|---|---|
|
||||
| `-config PATH` | config file (default `$HARNESS_CONFIG`, then `./harness.toml`) |
|
||||
| `-dry-run` | intake + plan only; no LLM call, no REPORT |
|
||||
| `-demo` | run the `[demo]` issue instead of Redmine intake |
|
||||
| `-task-id ID` | run only the task/issue with this id |
|
||||
|
||||
Exit codes:
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| 0 | ok (including "no tasks in scope") |
|
||||
| 1 | usage / config / routing / writeback error |
|
||||
| 2 | intake error |
|
||||
| 4 | llm / turn error |
|
||||
|
||||
## Configuration
|
||||
|
||||
Every `harness.toml` section (see `harness.toml.example`, load-tested so it
|
||||
cannot rot):
|
||||
|
||||
| Section | Keys | Meaning |
|
||||
|---|---|---|
|
||||
| top level | `vertical` | vertical/stack identity for this harness instance |
|
||||
| | `work_root` | where the bash tool executes (default `.`) |
|
||||
| | `report_dir` | where REPORT files land (default `reports`) |
|
||||
| `[loop]` | `max_rounds` | max LLM round trips per turn, tool calls included (default 8) |
|
||||
| `[redmine]` | `url`, `key_ref` | SoR issues endpoint + auth key ref |
|
||||
| | `scope_query` / `scope_query_id` | released-scope filter: raw `/issues.json` params, or a saved query id (`scope_query` wins when both are set) |
|
||||
| | `class_field`, `default_class` | custom field carrying the task class; fallback for issues without it (default `Class` / `primary`) |
|
||||
| | `limit` | max issues fetched per intake (default 50) |
|
||||
| `[litellm]` | `base_url`, `key_ref` | OpenAI-compatible proxy endpoint + key ref |
|
||||
| | `timeout_secs`, `max_retries` | per-request timeout (default 120) and retry count (default 2) |
|
||||
| `[models]` | tier keys, `default_tier` | tier alias → concrete proxy model (see below) |
|
||||
| `[models.classes]` | class keys | task class → tier alias (see below) |
|
||||
| `[tools.bash]` | `enabled`, `timeout_secs`, `max_output_bytes` | bash gate on/off, per-command timeout (default 60s), output truncation (default 100000) |
|
||||
| | `default` | verdict for commands matching no rule: `allow` or `deny` (org preset: deny) |
|
||||
| | `allow`, `deny` | rule lists; deny beats allow; `cmd *` word-boundary, `pfx*` raw prefix, `pfx/**` path prefix, `*` universal; compound commands checked segment by segment; command substitution and subshells always denied |
|
||||
| `[demo]` | `id`, `subject`, `prompt`, `class` | the issue `--demo` runs instead of Redmine intake |
|
||||
|
||||
Key refs accepted anywhere a `*_ref` appears: `env:NAME`, `file:PATH`,
|
||||
`literal:VALUE` (last resort), and `bw:REF` (reserved; errors until the
|
||||
bitwarden wrapper lands in build phase 3).
|
||||
|
||||
## Model routing
|
||||
|
||||
Static, config-only, no heuristics: a task's class maps to a tier alias, the
|
||||
tier alias maps to the concrete model actually sent to the proxy. Unknown
|
||||
class = hard error naming the config section.
|
||||
|
||||
Tiers (`[models]`):
|
||||
|
||||
| Tier | Concrete model | Role |
|
||||
|---|---|---|
|
||||
| `mopac-study` | `glm-4.7-flash` | flash tier |
|
||||
| `mopac-code` | `glm-5.2` | flagship |
|
||||
| `mopac-review` | `glm-5-turbo` | mid |
|
||||
| `mopac-primary` | `glm-5.3` | default / flagship+ |
|
||||
| `mopac-vision` | `glm-4.6v` | vision when needed |
|
||||
|
||||
Classes (`[models.classes]`):
|
||||
|
||||
| Task class | Tier | Concrete model |
|
||||
|---|---|---|
|
||||
| `study` | `mopac-study` | `glm-4.7-flash` |
|
||||
| `read` | `mopac-study` | `glm-4.7-flash` |
|
||||
| `code` | `mopac-code` | `glm-5.2` |
|
||||
| `architecture` | `mopac-code` | `glm-5.2` |
|
||||
| `review` | `mopac-review` | `glm-5-turbo` |
|
||||
| `summarize` | `mopac-review` | `glm-5-turbo` |
|
||||
| `writeback` | `mopac-review` | `glm-5-turbo` |
|
||||
| `vision` | `mopac-vision` | `glm-4.6v` |
|
||||
| `primary` | `mopac-primary` | `glm-5.3` |
|
||||
| (no class) | `default_tier` = `mopac-primary` | `glm-5.3` |
|
||||
|
||||
## Status
|
||||
|
||||
Sourced from [REPORT.md](REPORT.md) — keep both in sync.
|
||||
|
||||
| Area | State | Detail |
|
||||
|---|---|---|
|
||||
| Config | Works | TOML-subset parser, defaults + validation, secret refs only, redacted errors |
|
||||
| Model routing v0 | Works | tier + class maps; concrete model resolved before the request leaves |
|
||||
| Conductor single-shot | Works | `harness once` chainable; `--dry-run` makes zero LLM calls (test-asserted) |
|
||||
| Redmine intake | Works | `/issues.json` scope query, class custom field, `--demo` fallback |
|
||||
| Bounded turn | Works | LiteLLM chat, retry/backoff, tool loop capped at `max_rounds`, partial REPORT on mid-turn failure |
|
||||
| Exec tool | Works | allow-listed bash, segment-wise compound checks, timeout + truncation |
|
||||
| REPORT writeback | Works | timestamped file + `REPORT-latest.md`, atomic, full telemetry |
|
||||
| Tests | Works | table-driven, stdlib only; build/vet/test clean on go1.26 |
|
||||
| Redmine note writeback | Stubbed | REPORT is file-only today |
|
||||
| Budget/semaphore gate | Stubbed | tokens in REPORT, no cost/spend enforcement yet |
|
||||
| `bw:` key refs | Stubbed | error until the bitwarden wrapper (phase 3) |
|
||||
| Streaming + turn resume | Stubbed | retry is request-level today |
|
||||
| Session persistence/repair | Stubbed | not started |
|
||||
| Write confinement | Stubbed | declared roots + symlink checks land with the permission layer |
|
||||
| Task ordering | Stubbed | first-in-scope; chain with `--task-id` until status transitions land |
|
||||
| Local inbox intake | Stubbed | not started |
|
||||
| Next (phase 3) | Next | bitwarden wrapper, full permission layer, Redmine note writeback + status transitions, budget gate via LiteLLM spend APIs, streaming with resume |
|
||||
|
||||
## Docs and links
|
||||
|
||||
- [DESIGN.md](DESIGN.md) — design spec (hard rules, build order, org model)
|
||||
- [REPORT.md](REPORT.md) — current build status
|
||||
- [docs/PORTING-NOTES-crush.md](docs/PORTING-NOTES-crush.md) — sessions/MCP/provider study of the crush agent
|
||||
- [docs/PORTING-NOTES-maki.md](docs/PORTING-NOTES-maki.md) — permission parsing and token-reduction study of maki
|
||||
- [docs/PORTING-NOTES-secrets.md](docs/PORTING-NOTES-secrets.md) — Bitwarden secrets study; feeds the bitwarden-go tool
|
||||
- Repository: <https://git.knownelement.com/ukrrs/MOPAC>
|
||||
- Sibling tool repos (spec seeds): [mopac-keyproxy](https://git.knownelement.com/ukrrs/mopac-keyproxy), [mopac-bitwarden-go](https://git.knownelement.com/ukrrs/mopac-bitwarden-go)
|
||||
- SoR: Redmine project MOPAC; docs: Discourse
|
||||
|
||||
## License
|
||||
|
||||
AGPLv3 — see [LICENSE](LICENSE).
|
||||
|
||||
Reference in New Issue
Block a user