diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..54ef22a --- /dev/null +++ b/REPORT.md @@ -0,0 +1,83 @@ +# REPORT — harness skeleton (build phase 2) — 2026-08-28 + +## Works + +- **Config**: `harness.toml` via a stdlib-only TOML-subset parser (tables, + bare keys incl. hyphens, strings/ints/bools, single- and multi-line + arrays; trailing commas tolerated; anything richer fails loudly with + line numbers). Defaults + validation; secrets are refs only + (`env:NAME` / `file:PATH` / `literal:VALUE`; `bw:` reserved) and redacted + from every error path. `harness.toml.example` is tracked and load-tested + so it can never rot; real configs are gitignored. +- **Model routing v0**: `[models]` tier map + `[models.classes]` class map; + requests go out with the CONCRETE model resolved from the map + (mopac-study→glm-4.7-flash, mopac-code→glm-5.2, mopac-review→glm-5-turbo, + mopac-primary→glm-5.3, + mopac-vision→glm-4.6v). No heuristic code; + unknown class = hard error naming the config section. +- **Conductor single-shot**: `harness once` = intake → routing → bounded + turn → REPORT, then exit 0. Chainable, no daemon. `--dry-run` = intake + + plan (resolved model, tool bounds) with ZERO LLM calls (test-asserted). + `--task-id` filter for chaining. Exit codes: 0 ok/no-tasks, 1 + config/usage, 2 intake, 4 llm/turn. +- **Intake**: Redmine `/issues.json` scope query (raw filter params or + saved query id), task class from a configurable custom field with + default-class fallback; `--demo` builds the issue from `[demo]` (no + Redmine needed). +- **Bounded turn**: OpenAI-compatible chat via LiteLLM (stdlib http; + base_url ± `/v1` normalized; Bearer auth; retry/backoff on 429/5xx/ + transport; usage accounting), tool-calling loop capped at `max_rounds`; + gate denials feed back to the model as tool results and are counted, not + fatal. On mid-turn LLM failure after content exists, a partial REPORT is + still written with the error as stop reason. +- **Exec tool**: allow-listed bash. maki-derived scope semantics without + tree-sitter: compound commands split segment-by-segment (`&&`/`||`/`;`/ + `|`, quote-aware), `cmd *` word-boundary, `pfx*` raw prefix, `pfx/**` + path prefix, `*` universal; deny beats allow; `$()`/backticks/subshells + always denied (headless has no prompt channel). Per-command timeout with + process-group cleanup on unix; output truncation. +- **Writeback**: `REPORT---.md` + `REPORT-latest.md` + (atomic tmp+rename) with model/tier/class/tokens/rounds/denied/stop + telemetry per the DESIGN auditability bar. +- **Tests**: table-driven, stdlib `testing` only — TOML subset (valid doc + + 9 error cases), config defaults/validation/key refs (incl. leak check), + routing decisions, scope matcher + gate (14 cases) + exec (timeout, + truncation, deny), writeback, Redmine intake (httptest), LLM client + (auth/model/retry/4xx/empty-choices), and loop end-to-end against a + scripted fake OpenAI server (demo turn, dry-run zero-call, tool + round-trip with message-shape assertions, denial counting, round limit, + error-class mapping). `go build ./...`, `go vet ./...`, `go test ./...` + all clean on go1.26.7; every intermediate commit builds standalone. + +## MVP demo bar status + +`harness once --demo` is wired end-to-end: prompt "tell me about yourself" +→ LiteLLM (glm-5.3 via mopac-primary) → GLM self-description → REPORT. +Proven against a scripted fake in tests. The live proxy +(http://192.168.3.78:4001) is reachable from this host and answers 401 +without a key, so the live run needs exactly one thing: + + export HARNESS_LITELLM_KEY= + ./bin/harness once --demo + +## Stubbed / known gaps + +- Redmine issue-note writeback (SoR note after the REPORT) — file only. +- Budget/semaphore GATE (LiteLLM spend APIs, class-aware slots) and cost + in REPORT (tokens only today). +- `bw:` key refs error until the bitwarden wrapper (phase 3). +- Streaming + turn resume on truncation — retry is request-level today. +- Session persistence + read-time repair (crush notes §2) — not started. +- Write confinement to declared roots + symlink-aware canonicalization; + redirects currently match by segment text (phase 3 permission layer). +- Task selection is first-in-scope; no P1-first ordering or status + transitions, so chained `once` re-picks the same issue (use --task-id). +- Local inbox intake (DESIGN core-loop step 1, second half) — not started. + +## Next chunk (phase 3) + +1. bitwarden-go wrapper + `bw:` refs (unblocks secret posture). +2. Full permission layer: tree-sitter bash scopes, write roots, symlink + checks, per-vertical allow/deny presets. +3. Redmine note writeback + status transition so chaining advances scope. +4. Budget gate via LiteLLM spend APIs; cost line in REPORT. +5. Streaming with truncation retry + turn resume.