diff --git a/archive/NOTICE-crossfeed-founder.md b/archive/NOTICE-crossfeed-founder.md new file mode 100644 index 0000000..8b376ce --- /dev/null +++ b/archive/NOTICE-crossfeed-founder.md @@ -0,0 +1,3 @@ +Founder directive: a fifth crossfeed dir is LIVE: /home/_crossfeed/founder +(owner reachableceo). Add it to your turn-start peer reads; the founder PMO publishes +STATUS.md there. Same ambient-only rules. Archive this notice after reading. diff --git a/archive/REPORT-20260828-2130-keyproxy-v0.md b/archive/REPORT-20260828-2130-keyproxy-v0.md new file mode 100644 index 0000000..43eb095 --- /dev/null +++ b/archive/REPORT-20260828-2130-keyproxy-v0.md @@ -0,0 +1,101 @@ +# REPORT — keyproxy v0 finish (commit + push of the self-killed turn) + +Date: 2026-08-28 21:30 +Repo: https://git.knownelement.com/ukrrs/mopac-keyproxy (branch `main`, pushed through `47343d5`) +Task: verify the keyproxy-v0 working tree left uncommitted by the previous +turn (it self-SIGTERM'd via a broad pkill before committing), re-test in +the Docker builder, commit in logical chunks, push, report. + +## What shipped (v0, per the TASK spec) + +- **Two shapes, one resolver**: `keyproxy serve` (localhost HTTP resolve + hop) + `keyproxy get REF` (exec-style CLI, bare value, no newline). +- **Endpoints** (`internal/server/server.go`): + - `POST /v1/resolve` — bearer-token auth (constant-time compare, + bootstrapped at startup from the file backend), body + `{"ref":"mpk-"}` → `{"value":"..."}`. + - `GET /healthz` — unauthenticated liveness. + - Status codes: 200 / 401 / 400 (malformed body or non-`mpk-` ref, + never echoed) / 404 / 405 / 413 (4 KiB cap) / 500 (recovered panic, + detail suppressed) / 501 (stub backends) / 502. Failure bodies carry + ref + backend + fixed reason enum only. +- **Backends** (`internal/backend/`, one `Backend` interface): + - `file` — 0600 KEY=VALUE env files, parsed in pure Go (never + sourced), `~` expansion, permission mask enforced before read + (default 0600, stricter passes, looser refused). + - `env` — process-environment indirection (source IS the var name). + - `bitwarden`, `vault` — **explicit not-implemented stubs** returning + 501 `not_implemented` behind the same interface; phase-3 drop-ins. +- **Config** (`internal/config/`): strict stdlib-only TOML subset parser; + unknown keys and backend-specific misconfigurations fail loudly at + startup; `keyproxy.toml.example` tracked, real `keyproxy.toml` + gitignored (holds the ref MAP — locations only, never material). +- **Redaction, no persistence, no admin UI**: memory-only material; every + log line naming a ref masks it `=***`; auth failures log remote + address only; parse errors carry line numbers, never contents; panic + values discarded. Enforced by tests + smoke log scan. +- **Build discipline**: all dev in Docker via digest-pinned + `golang:1.26-bookworm` + (`golang@sha256:e8c859f5...e5514`); `dev.sh build|vet|test|check|smoke`, + `make` is a thin front door. Host runs no Go toolchain. + +## Verification (re-run this turn, all in the Docker builder) + +- `./dev.sh check` (= build + vet + test): PASS — + `ok internal/backend`, `ok internal/config`, `ok internal/server`, + `go vet` clean, binary built. +- `./dev.sh smoke` (end-to-end, containerized): **10/10 PASS** — + healthz; 401 no-token; 401 wrong-token; 200 file resolve; + 200 env resolve; 404 unknown ref; 400 invalid ref without echo; + 501 bitwarden stub; 501 vault stub; 405 GET resolve. + Server log verified redacted: no material strings, refs masked + `ref=mpk-smoke=***`. Smoke container torn down by exact name + (`docker rm -f keyproxy-smoke`); no pkill used this turn. + +## Commits pushed to origin/main + +Previous turn left everything uncommitted. Committed in 5 logical chunks +(hashes post-rebase, see note below): + +| Commit | Content | +|---|---| +| `b43bc55` | backend interface + file/env backends + bitwarden/vault stubs + env-file parser, with tests | +| `7bec2a1` | config loader + stdlib TOML subset parser, with tests | +| `82861b6` | HTTP resolve hop (auth, redaction, reason enums), with tests | +| `24a57c1` | CLI (`serve`, `get`, `help`; exit codes 0/1/2) | +| `47343d5` | dev.sh/Makefile Docker tooling, smoke/, keyproxy.toml.example, .gitignore, README | + +Note: first push was rejected — origin/main had been re-seeded +(`6c88e65` + `325a36c`), an unrelated history whose README/LICENSE are +byte-identical to our never-pushed local seed. Resolved by rebasing the 5 +implementation commits onto `325a36c` (dropping the redundant local seed; +resulting tree verified identical to the pre-rebase tree). Push: +`325a36c..47343d5 main -> main`, working tree clean. + +## How to start it + +See README "Quickstart" (repo root, verified verbatim on 2026-08-28): +`./dev.sh check` → create 0600 env files + `cp keyproxy.toml.example +keyproxy.toml` → `./bin/keyproxy serve` (binds 127.0.0.1:8082) → +`POST /v1/resolve` with bearer token, or `./bin/keyproxy get mpk-example`. + +## Left for phase 3 + +- `bitwarden` backend: replace stub with Bitwarden Secrets Manager REST + (machine accounts, stdlib-first — official SDK license is + AGPL-incompatible). +- `vault` backend: replace stub with HashiCorp Vault KV v2 + AppRole + (official Go api pkg, MPL-2.0, vendored). +- Optional: short-TTL memory cache (per README "Today vs planned"). + +Both stubs already fail loudly (501 `not_implemented`) naming ref + +backend; connectors slot into the single `backend.Backend` interface and +`newRegistry()` in `cmd/keyproxy/main.go`. + +## Hygiene + +- No broad pkill this turn; the only process handling was + `docker rm -f keyproxy-smoke` (exact container name) in the smoke + script's cleanup trap. +- Nothing seeded beyond the repo; real `keyproxy.toml`, `*.env`, + `bin/`, `.smoke/` all gitignored; working tree clean after push. diff --git a/archive/REPORT-20260828-2131-selfhost-core.md b/archive/REPORT-20260828-2131-selfhost-core.md new file mode 100644 index 0000000..c609141 --- /dev/null +++ b/archive/REPORT-20260828-2131-selfhost-core.md @@ -0,0 +1,108 @@ +# REPORT — self-host core: `harness loop` replaces the bash stack — 2026-08-28 21:31 + +Deliverable for "get to be self hosting asap — smallest core that can +bootstrap itself (redmine/gitea interaction fine, discourse later)". +Pushed to ukrrs/MOPAC main as +`86c39c8` (module rename) → `c869ac1` (keyproxy/config) → `ecc0ee8` +(loop daemon) → `2901bb8` (docs + dev.sh loop runner). + +## What runs now + +The MOPAC harness (Go, stdlib only, built in the digest-pinned Docker +builder — host stays toolchain-free) drives ITSELF. Redmine is the SoR, +the conductor is the worker. Per issue in the released scope whose +(id, updated_on) has not been processed: + +1. one bounded turn (same conductor `harness once` uses; sequential, + one at a time — v0, no concurrency knob yet); +2. REPORT file lands in `reports/` (as before); +3. REPORT body is POSTed back onto the issue as a Redmine journal note + (the previously-stubbed note writeback is now implemented); +4. status transitions per `[redmine.status_map]` (default shipped: + `"In Progress" = "Done"`; names resolved via `/issue_statuses.json`); +5. optional Gitea REPORT commit (`[gitea] commit_reports`, OFF by + default); +6. dedup marker refreshed to the post-writeback `updated_on`, so the + loop's own note never re-triggers itself (self-retrigger hazard + handled + test-asserted). + +State: append-only `state/loop/loop.jsonl` — one JSON line per action +(dispatch / report / note / status / commit / refresh / error), dedup +index rebuilt at startup, restarts keep exactly-once-reaction semantics. +One line per action goes to stdout as well. A FAILED turn is recorded, +NOT retried (update the issue to re-release it) — a down proxy cannot +hot-loop the poll. + +Secrets: `key_ref = "mpk:mpk-redmine"` style refs resolve through +mopac-keyproxy `POST /v1/resolve` (bearer from `[keyproxy] token_ref`, +60s in-memory cache). `env:`/`file:`/`literal:` refs keep working, so +the loop runs with or without keyproxy up. No hosts hardcoded — all in +harness.toml. Also folded in: module renamed +`git.knownelement.com/reachableceo/MOPAC/harness` → `ukrrs.com/mopac/harness` +(the doc-rot finding), verified build+vet+test in the builder. + +Tests: loop end-to-end against a fake Redmine (mutable: notes bump +updated_on like the real SoR) + fake OpenAI server — dispatch/note/ +status happy path, dedup on rescan, re-dispatch on issue update, failed +turn without hot-loop, dry-run zero-trace, restart dedup survival, +status-map miss, gitea create/update, keyproxy 401/404/cache. Full +`./dev.sh check` (build + vet + test) clean on go1.26. + +## Exact command to start the loop + +From `/home/reachableceo/projects/meta/MOPAC/harness` (harness.toml is +already in place; it is gitignored): + + ./dev.sh build + HARNESS_REDMINE_KEY= HARNESS_LITELLM_KEY= \ + ./dev.sh loop + +That runs the daemon in the Docker builder container with the repo +bind-mounted (REPORTs + state land in the repo). Verify first with a +zero-cost scan: + + HARNESS_REDMINE_KEY= HARNESS_LITELLM_KEY= \ + ./dev.sh loop --dry-run # prints "would dispatch " per new issue, no turns, no state + ./dev.sh loop --once # single scan (cron-able), exit 2 if the intake itself fails + +Stop: Ctrl-C (SIGINT) in the foreground, or `docker stop mopac-loop`. +Interval default 120s (`[loop] poll_interval_secs`), override with +`-interval 60s`. With keyproxy up instead of env keys: +`[keyproxy] url/token_ref` in harness.toml + `mpk:` refs (see +harness.toml.example, all commented). + +## What it replaces — the die list (NOT deleted yet, pending your sign-off) + +1. `/home/_crossfeed/tooling/agent-stack/semaphore.sh` + slot-file + concurrency cap → replaced by the loop's in-process discipline + (v0: one turn at a time; concurrency knob later if load justifies). +2. `~/.coordinate/scripts/queue-next.sh`, `queue-after.sh`, + `pmo-heartbeat.sh` → replaced by the loop + its own wake discipline + (poll interval; `--once` for cron). The heartbeat screen + (`pmo-heartbeat`, PID 2924085) can be killed whenever you say so. +3. `dispatch-turn.sh` doorbell `screen -S reachableceo-PMO -X stuff` — + note: ALREADY dead. The screen is named `RCEO-PMO`, so the message + was silently dropped all along. Mentioned for the record; deletion + after your sign-off. + +Nothing in `/home/_crossfeed` or `~/.coordinate/scripts` was touched +this turn. `harness once` (manual/chained path) also unchanged in +behavior — file-only writeback, no notes. + +## Rollback + +Just stop the loop (`Ctrl-C` / `docker stop mopac-loop`). The bash stack +is fully present and untouched — relaunch it exactly as before. The only +new artifacts are `state/loop/loop.jsonl` (delete to forget all dedup +history) and REPORT files. No config the old stack reads was modified. + +## Suggested verification (PMO) + +1. `./dev.sh loop --dry-run` with the real Redmine — should list the + released-scope issues it would dispatch, zero LLM calls. +2. Pick one low-stakes issue, drop it to "In Progress" in scope, run + `./dev.sh loop --once` with keys: expect turn → REPORT file → + journal note on the issue → status → Done. +3. Check `state/loop/loop.jsonl` for the one-line-per-action trail. + +— Work stack, 2026-08-28 ~22:30 diff --git a/archive/REPORT-20260828-2330-bitwarden-go.md b/archive/REPORT-20260828-2330-bitwarden-go.md new file mode 100644 index 0000000..fb0324c --- /dev/null +++ b/archive/REPORT-20260828-2330-bitwarden-go.md @@ -0,0 +1,172 @@ +# REPORT — mopac-bitwarden-go v0: Bitwarden Secrets Manager REST client (fake-server tested) + +- Date: 2026-08-28 23:30 (work session 2026-08-28 late) +- Repo: https://git.knownelement.com/ukrrs/mopac-bitwarden-go (main, pushed: + `643de0f..9b730af`, 6 commits after the spec seed) +- Local: ~/projects/meta/MOPAC/bitwarden-go +- Status: v0 COMPLETE and green. Stdlib-only Go (zero third-party + modules), ALL dev in Docker (digest-pinned golang:1.26-bookworm, same + image as keyproxy), built and tested entirely against a fake Secrets + Manager — no live vault access exists on this account yet; live creds + attach later with zero code change (config only). + +## 1. What was built + +A plain-REST Bitwarden Secrets Manager client for machine accounts + +thin CLI. No official SDK imported (AGPL-incompatible); the wire protocol +was reconstructed from Bitwarden's public API behavior and pinned to +published SDK test vectors. + +Wire protocol implemented (all verified against the fake server): + +| Step | Wire | +|---|---| +| login | POST /identity/connect/token — grant_type=client_credentials, scope=api.secrets, machine client_id/client_secret | +| org key | response's `encrypted_payload`: type-2 EncString sealed for the credential's embedded 16-byte key (HKDF-SHA256, salt `bitwarden-accesstoken`, info `sm-access-token`); unwraps to the organization key | +| refresh | grant_type=refresh_token, fired automatically 30s before expiry (memory-only) | +| secrets | GET /api/accounts/{id}/secrets, GET /api/secrets/{id}, GET /api/projects/{id}/secrets; names/values are EncStrings decrypted in memory with the org key | +| projects | GET /api/accounts/{id}/projects | + +Machine credential format handled: `0..:` as +printed by Secrets Manager (uuid -> client_id; secret -> client_secret; +key -> payload-unwrap derivation). Split BW_CLIENTID/BW_CLIENTSECRET also +accepted (plaintext-mode servers only). + +## 2. Library surface (the exact keyproxy integration contract) + +```go +import bw "git.knownelement.com/ukrrs/mopac-bitwarden-go" + +tok, err := bw.Authenticate(ctx, bw.Credentials{BaseURL: url, AccessToken: cred}) +defer tok.Zero() +value, err := bw.GetSecret(ctx, tok, "redmine-api-key") // secret NAME or uuid +secrets, err := bw.ListSecrets(ctx, tok) // metadata only, no values +projects, err := bw.ListProjects(ctx, tok) +``` + +- Errors are `errors.Is`-friendly sentinels: ErrAuthFailed, + ErrTokenExpired, ErrSecretNotFound, ErrMalformedResponse, ErrDecrypt, + ErrServer, ErrUnreachable, ErrInvalidCredentials. Error strings carry + fixed reason enums + uuids/names ONLY — never values, tokens, + ciphertext, or response bodies. +- Token: memory-only, auto-refresh-before-expiry, `Zero()` wipes key + material, `String()` is log-safe (expiry + ids only). +- Decryption fails closed: MAC verified before decrypt; tampering is a + typed error, never passthrough of ciphertext. + +## 3. CLI usage + +``` +bitwarden-go login # credential check; prints summary only +bitwarden-go projects # " " per line +bitwarden-go secrets list # " " per line +bitwarden-go get # bare value to stdout, no newline +``` + +Config: BW_* env vars or 0600 env file ($BITWARDENGO_CONFIG, default +~/.config/bitwarden-go/env). NEVER flags/args. Keys: BW_SERVER_URL +(default https://vault.bitwarden.com), BW_ACCESS_TOKEN (full credential) +or BW_CLIENTID+BW_CLIENTSECRET. Exit codes 0/1/2 mirror keyproxy +(ok / usage-config / auth-or-resolution). + +## 4. Fake-server test results + +`./dev.sh check` (in the Docker builder): build + vet + gofmt clean, +**53 tests pass, 0 fail** across 3 packages (38 top-level + 15 subtests). +Coverage areas: + +- Crypto pinned to published SDK vectors: credential parsing, HKDF + shareable-key derivation (3 vectors), type-2 EncString AE vector, + round-trips, tamper detection (MAC + ciphertext). +- Auth table vs fake server: happy path, wrong secret, server 500, + unreachable, malformed token body, split-creds-on-encrypted-server + (fails loudly), plaintext server mode. +- Token lifecycle: refresh-before-expiry (short-TTL), refresh rejection + surfacing, expiry without refresh token, omitted-value list fallback to + by-id fetch. +- Reads: by name, by uuid, lists with decrypted names, missing secret + (name + uuid), malformed list body, tampered value MAC, rejected + bearer. +- Config: 0600 enforcement (0644/0666 refused BEFORE read; 0400 passes), + env-over-file precedence, pure-Go KEY=VALUE discipline, missing-file + behavior, no-credentials error. +- CLI (end-to-end via real 0600 env file): login/get/lists/failure + paths/usage. +- Redaction sweeps: every error path and every CLI run asserted free of + client secret, credential key, access/refresh tokens, secret values; + stdout carries material only in `get` (pinned to the exact value). + +Smoke (`./dev.sh smoke`): fake Secrets Manager booted in a container on +127.0.0.1:8600 (full crypto chain, random org key per boot); real binary +driven from host: login/projects/secrets list/get by name/get by +uuid/missing-secret/bad-credential + redaction sweep. Result: `smoke: OK`. +Exact container IDs cleaned up; no broad kills. + +## 5. Open questions from PORTING-NOTES-secrets.md §3 + +Answered by code (v0 decision, reversible): + +1. Substrate: direct REST in Go, zero Node/bw. The client implements the + machine-account flow including client-side decryption, so shelling to + `bw` is unnecessary for the harness's read needs. +3. Ref syntax (read side): `bw:` maps to GetSecret(name) = Secrets + Manager secret by NAME (uuid also accepted). `#field`/`#totp` are + vault-item concepts; Secrets Manager secrets have no fields — code + answer: not needed for the SM path. +4. Session lifetime: memory-only for the process lifetime; a long-lived + keyproxy keeps its Token and auto-refreshes before expiry (30s skew). + No disk persistence exists in the library at all. + +Still needing Charles: + +2. Machine account: dedicated service account for keyproxy/harness (not + coo@turnsys.com), with access scoped to a harness project; the vault + side must exist before phase 3 flips on. +5. Secret inventory: exact secret names/uuids for Redmine + LiteLLM keys + (current fake names redmine-api-key/litellm-key are placeholders). +6. Scope: strictly the SM read path consumed via keyproxy, or also absorb + KNELCredsManager's ~/.creds/*.env contract. +7. Subprocess injection: does anything need secrets as child-process env + (LiteLLM), or HTTP-header-only as today? ++ NEW — module wiring: keyproxy must consume this module from the private + gitea; recommend `go mod vendor` (matches "one auditable vendored + module tree") or GOPRIVATE=git.knownelement.com. Founder's call. ++ NEW — live verification checklist (first day with real creds): run + login/get against Vaultwarden pwvault.turnsys.com and against Bitwarden + cloud to confirm the accounts-path list endpoint and encrypted_payload + shapes match the fake exactly (protocol reconstructed from public + behavior + SDK vectors; end-to-end vs a live server is the one thing + tests cannot prove). + +## 6. Exact keyproxy integration point + +`keyproxy/internal/backend/bitwarden.go` (currently the 501 stub, +`Resolve` at bitwarden.go:20). Phase 3 replacement sketch: + +```go +// keyproxy: internal/backend/bitwarden.go +func (b *Bitwarden) Resolve(ctx context.Context, ref Ref) (string, error) { + cfg, err := config.Load(ref.Source) // ref.Source = 0600 env file path (BW_* keys) + if err != nil { return "", Err(ref, b.Name(), ReasonUnreadableSource) } + tok, err := bw.Authenticate(ctx, bw.Credentials{ + BaseURL: cfg.ServerURL, AccessToken: cfg.AccessToken, + ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, + }) + if err != nil { return "", Err(ref, b.Name(), ReasonUnreadableSource) } // auth -> typed reason + defer tok.Zero() + return bw.GetSecret(ctx, tok, ref.Key) // ref.Key = secret name or uuid +} +``` + +keyproxy.toml ref shape (existing stub semantics): backend `bitwarden`, +source = path to the 0600 env file, key = secret name/uuid. The library's +typed errors map 1:1 onto keyproxy's Reason enums. Wiring requires adding +the module dependency (see open question: vendor vs GOPRIVATE). + +## 7. Reproduce + +``` +cd ~/projects/meta/MOPAC/bitwarden-go +./dev.sh check # build + vet + test (Docker builder) +./dev.sh smoke # containerized fake server + real CLI end-to-end +``` diff --git a/archive/REPORT-20260828-docs-standards.md b/archive/REPORT-20260828-docs-standards.md new file mode 100644 index 0000000..4eab24b --- /dev/null +++ b/archive/REPORT-20260828-docs-standards.md @@ -0,0 +1,114 @@ +# REPORT: docs standards pass — READMEs and docs/ across ukrrs repos + +Date: 2026-08-28 +Operator: docs-standards turn (TASK-20260828-2100-docs-standards) +Status: COMPLETE — 4 repos committed and pushed; verification-first throughout + +## Method + +Read code/help/config before writing a word. Ran every documented command: +build/vet/test inside the Docker builder (golang:1.26-bookworm, pulled, +pre-authorized), `./bin/harness help`, `./bin/harness once --dry-run --demo`, +and the no-key live-demo error path (exit 1, clean message). Gitea anchor +slugs for the DESIGN.md TOC were taken from the rendered page, not guessed. + +## Per-repo summary + +### ukrrs/MOPAC (harness) — commit c6da05f, pushed to main + +- README.md: full rewrite from the 16-line skeleton. H1 + purpose paragraph + + Status line; verified quickstart (Docker builder, cp harness.toml.example, + dry-run with real output, live-demo bar incl. verified no-key failure + output); mermaid conductor flowchart (INTAKE -> ROUTING -> bounded TURN -> + REPORT, chaining by re-invocation) + prose; reference tables for CLI + (subcommands, flags, exit codes 0/1/2/4 from actual help), config (every + harness.toml section + defaults from internal/config), model routing + (tier table + class -> tier -> model table); Status table (Works/Stubbed/ + Next) sourced from REPORT.md; docs/link section; AGPLv3. Stale + `reachableceo/MOPAC` path replaced with git.knownelement.com/ukrrs/MOPAC. +- docs/: all three PORTING-NOTES-*.md got Status front-matter lines + (crush/maki: complete, drift possible; secrets: complete, 7 open questions + pending Charles, feeds mopac-bitwarden-go). PORTING-NOTES-secrets.md was + untracked in the working tree (pre-existing) — now committed. +- DESIGN.md: Contents section (19 anchor links, verified against Gitea's + rendered ids) after the intro. No sections rewritten, reordered, or added + beyond the TOC. + +### ukrrs/mopac-keyproxy — commit 325a36c, pushed to master + +- Cloned fresh from Gitea. README upgraded, purpose prose kept: Status line + (spec seed, no code yet); Scope (mpk_ placeholders, BW Secrets Manager + REST + Vault KV v2/AppRole backends, HTTP hop + `keyproxy get` CLI); + Non-goals (no admin UI ever, not a secrets manager, no disk persistence, + no org assumptions); Today-vs-planned table; Design references linking + into ukrrs/MOPAC DESIGN.md key-proxy + FLOSS-repos sections (anchors + verified) and the sibling repos; AGPLv3. + +### ukrrs/mopac-bitwarden-go — commit d8c781d, pushed to main + +- Cloned fresh from Gitea. README upgraded, purpose prose kept: Status line + (spec seed, reference study complete, open questions pending Charles); + Scope (stdlib REST, no SDK, machine-account auth, porcelain/plumbing, + memory-only session handling); Non-goals (no admin UI, not a vault server, + no disk cache of values); Today-vs-planned table naming the + KNELSecretsManager ADR-002 wrapper as the production interim; Design + references into PORTING-NOTES-secrets.md (incl. its 7 open questions), + DESIGN.md toolchain-policy + key-proxy anchors, keyproxy sibling; AGPLv3. + +### ukrrs/KNEL-AIMiddleware — commit 6476304, pushed to main + +- Audit-only pass (working clone: projects/meta/MOPAC/siblings/ + KNEL-AIMiddleware; the task's ~/projects/siblings path does not exist). + Fixed only wrong/stale facts: + - License badge + footer said MIT; LICENSE is AGPLv3. Corrected both. + - Production numbers said 31/42 (MCP 27/38); PRODUCTION-READY.md says + 33/42 (MCP 29/38). Badge + summary table corrected. + - Clone URL was github.com/KNEL/...; corrected to + git.knownelement.com/ukrrs/KNEL-AIMiddleware.git. + - Project structure tree predated tooling-cli/ (redmine, discourse, dns, + KNELCredsManager), the 40+ mcp-/lsp-wrapper scripts, run.sh, + maintenance.sh, PRODUCTION-READY.md, LSP_SETUP.md — all added. + - scripts/ section missing validate-mcp.sh / validate-all.sh — added. + - Status front-matter line added (org standard) without restructuring. + +## Checklist applied + +- Front matter (H1, purpose paragraph, Status line): all 4 repos. +- Quickstart verified by running: MOPAC (build/vet/test in Docker builder, + help, dry-run, no-key live path; with-key path proven live 2026-08-28 + 19:29 per log + reports/REPORT-latest.md). Seeds have no runnable code — + quickstarts intentionally absent until commands exist (standard: never + document an unverified command). +- Architecture mermaid + prose: MOPAC (conductor loop). +- Reference tables (CLI/flags/exit codes, config sections, routing map): + MOPAC. +- Status table from REPORT.md: MOPAC. +- Terminology (harness/conductor/turn/REPORT), fenced code with language + tags, no heading-level skips, no ownerless TODOs: all 4. +- Links checked: relative files exist; Gitea anchors verified against + rendered HTML; no dead links introduced. +- Correct repo URLs (git.knownelement.com/ukrrs/...): all 4. + +## Doc rot found, NOT fixed in this pass (follow-up list) + +1. **go.mod module path stale**: `git.knownelement.com/reachableceo/MOPAC/ + harness` (and all internal import paths) predate the ukrrs transfer. + Source change — excluded from a docs-only pass. Fix in a code turn, + then drop the parenthetical note in the harness README quickstart. +2. **DESIGN.md line ~154** still lists mopac-bitwarden-go as "later" — it + is created and seeded now. DESIGN edits were out of scope (TOC only); + fold into the next DESIGN append turn. +3. **Duplicate KNEL-AIMiddleware clone**: ~/projects/KNEL-AIMiddleware + (older location) is now one commit behind the siblings/ clone I pushed. + Someone should `git pull` it or remove it (MIGRATION-projects.md + territory). +4. **KNEL-AIMiddleware STATUS.md / PRODUCTION-READY.md / LSP_SETUP.md / + JOURNAL.md**: not audited this pass (scope was the README); README now + trusts PRODUCTION-READY.md's 33/42, which matched its own internal + tables. A deeper docs pass could reconcile STATUS.md the same way. +5. **golang:1.26-alpine builder breaks the harness test suite** (no bash in + the image; internal/tools tests fail). Bookworm image works and is what + the README documents. Worth pinning digest + documenting image choice + when the builder container is formalized (DESIGN all-dev-in-docker rule). +6. **Harness bin/ is gitignored but README quickstart builds into it** — + correct as designed; noting only so nobody "fixes" it away. diff --git a/archive/REPORT-20260828-events-receiver.md b/archive/REPORT-20260828-events-receiver.md new file mode 100644 index 0000000..0e4454f --- /dev/null +++ b/archive/REPORT-20260828-events-receiver.md @@ -0,0 +1,117 @@ +# REPORT: mopac events — webhook receiver (build phase 2b, V1 scope) + +Date: 2026-08-28 +Operator: phase-2b events turn (DESIGN "Events are V1 scope" section) +Status: COMPLETE — 5 commits on main, pushed to ukrrs/MOPAC; build/vet/test +clean in the digest-pinned Docker builder; live smoke proven on LAN port +4100 (401 without secret, 200 + JSONL record with). + +## What shipped + +`harness events` — Go stdlib net/http receiver (no frameworks, zero new +deps) that lets Redmine/Discourse/Gitea punch the harness: + +- Routes: `POST /hooks/{redmine,discourse,gitea}`, `GET /healthz`; GET on a + hook = 405, unknown path = 404, body > 1 MiB = 413. +- Verification, deny-first: gitea = hex HMAC-SHA256 of the raw body in + `X-Gitea-Signature` (constant-time hmac.Equal); redmine/discourse = + shared-secret header, constant-time compare, header names configurable + (defaults `X-Redmine-Webhook-Secret` / `X-Discourse-Webhook-Secret`). + Unsigned/unverified = 401 with ONE generic error body; rejection logs + name the failure class only. +- Normalization: every provider payload becomes one internal Event + (source, kind, actor, canonical subject id — `redmine:issue:42`, + `discourse:topic:7`, `gitea:pr:ukrrs/MOPAC#5` — title, repo, provider + event id, sha256 payload digest, received-at). Tolerant across known + payload variants (redmine_webhooks + flat shapes, Discourse headers or + payload, Gitea action/pull_request/issue incl. closed+merged → + pr_merged). +- Action mapping per DESIGN: redmine issue update/note/journal → + dispatch_turn; discourse post reply → respond_turn; gitea PR + approved/merged → pipeline_step; rest stored-but-ignored. +- Persistence: append-only `state/events/events.jsonl` (file 0600, dir + 0700), dedup by provider event id (`X-Gitea-Delivery`, + `X-Discourse-Event-Id`, `X-Redmine-Delivery`; payload-digest fallback); + dedup index rebuilt from the file at startup (torn tail skipped) so + replays across restarts still dedup — at-least-once delivery, + exactly-once reaction. +- Config: `[events]` listen (default `:4100`), state_dir, per-source + secret refs (`env:NAME` / `file:` / `literal:`), resolved at startup; + refuses to start with zero secrets; ref values never echoed. +- Secrets posture: never logged, never persisted; audit lines carry + normalized fields + truncated digest only. Test-asserted (log + JSONL + scanned for secret material). + +## What is stubbed + +- Event → turn dispatch: stored actionable events reach + `Conductor.DispatchEvent` (internal/loop/loop.go), which today prints + the would-be action. Real wiring (dispatch/redirect turn for Redmine, + context update + response for Discourse, pipeline step for Gitea) is + the next chunk, per plan. +- Cross-host forwarding/replication of the event log (T1 later); JSONL + is local-only today. +- Discourse HMAC signature mode (their `X-Discourse-Event-Signature`) is + not implemented; the spec asked for the secret-header mode, which is + what shipped. + +## Dev-in-docker compliance + +Every compile/vet/test path routes through `dev.sh`, which pins the +builder by digest: `golang@sha256:e8c859f5632...` (= golang:1.26-bookworm). +Note: golang:1.26-alpine was tried first per the task text but its image +has no bash, and the phase-2 exec tool's tests exec bash — bookworm keeps +the pin and the suite green. Host ran only docker + python3 stdlib urllib +(curl is banned on host). Exact commands used: + +- Gate: `./dev.sh check` → + `docker run --rm -v "$PWD:/h" -w /h -u "$(id -u):$(id -g)" -e HOME=/tmp + -e GOFLAGS=-buildvcs=false golang@sha256:e8c859f... sh -c 'go build -o + bin/harness ./cmd/harness && go vet ./... && go test ./...'` +- Targeted runs: `./dev.sh test -run TestNormalize -v` (same container). +- Smoke: `./dev.sh smoke` → builds via the same builder, then + `docker run -d --name mopac-events-smoke -p 4100:4100 -v "$PWD:/h" -w /h + -u "$(id -u):$(id -g)" -e HOME=/tmp golang@sha256:e8c859f... + /h/bin/harness events -config /h/.smoke/harness.toml -listen ":4100"`, + probed from the host with `BASE_URL=http://127.0.0.1:4100 python3 + smoke/probe.py`, torn down with `docker rm -f`. + +## Smoke results (port 4100, all PASS) + +- healthz answers; GET hook = 405. +- gitea unsigned → 401 `{"error":"unverified webhook"}`; bad signature → + 401; valid HMAC → 200 + `{"action":"pipeline_step","id":"gitea:smoke-delivery-1","status":"stored"}`; + replay → 200 `status:duplicate`. +- redmine shared secret → 200 stored, `dispatch_turn`; wrong secret → 401. +- discourse shared secret → 200 stored, `respond_turn`. +- JSONL: 3 lines (gitea/redmine/discourse), correct fields, no secret + material; container log shows one audit line per delivery + the stub + dispatch firing exactly once per stored actionable event. + +## Tests + +Table-driven, stdlib only: verification (7 HMAC + 5 shared-secret cases), +normalization (13 shape cases + 12-row action map + digest stability), +store (dedup, restart persistence, torn tail, 0600 perms), end-to-end +httptest server (401/400/200/405/413 paths, replay dedup, +ignore-not-dispatched, secret-leak scans of responses/log/JSONL), config +(events defaults, parse, bad-ref validation). `go build ./...`, `go vet +./...`, `go test ./...` all clean; every commit builds standalone. + +## Commits (main, pushed c6da05f..f89b694) + +1. `164b145` events: config surface for the webhook receiver +2. `05ec1a4` events: verify, normalize, append-only store with + provider-id dedup +3. `043e03b` events: HTTP receiver, CLI wiring, conductor dispatch stub +4. `88e7b7b` dev.sh + smoke: docker-routed dev loop and webhook smoke test +5. `f89b694` docs: events receiver in README, phase 2b status in REPORT + +## Open for Charles + +- Confirm the Cloudron apps can be configured with the three secret + headers (Redmine needs a webhook plugin — redmine_webhooks works with + its secret setting; header name is configurable if the plugin differs). +- Port choice 4100 is a default, not a decision — override via + `[events] listen` or `-listen` / `docker -p` when the real stacks land. diff --git a/archive/REPORT-20260829-0000-serve.md b/archive/REPORT-20260829-0000-serve.md new file mode 100644 index 0000000..8cff785 --- /dev/null +++ b/archive/REPORT-20260829-0000-serve.md @@ -0,0 +1,126 @@ +# REPORT — `harness serve`: OpenAI-compatible front door (OWUI is the client) + +Date: 2026-08-29 ~01:30 | Repo: ukrrs/MOPAC (harness/) | Status: DONE, live on LAN port 8090, pushed to origin main (2901bb8..704f905) + +## What it is + +`harness serve` = the OWUI front door from DESIGN (hermes killed; OpenWebUI = +interactive surface with Cloudron SSO + admin RBAC we do not build). OWUI +adds MOPAC as an OpenAI connection; each task class registers as a "model". +OWUI chat and webhooks (`harness events`, port 4100) are now two doors into +the same conductor loop. v0: stateless, non-streaming, tools off. + +## Endpoints (port 8090, own port — coexists with events on 4100) + +| Route | Auth | Behavior | +|---|---|---| +| `POST /v1/chat/completions` | Bearer vkey | ONE bounded stateless conductor turn over the full history OWUI sends; reply = single assistant message + usage. `temperature`/`max_tokens` forwarded. `stream:true` = explicit 400. | +| `GET /v1/models` | Bearer vkey | the servable catalog (below) | +| `GET /healthz` | none | liveness | + +Auth = constant-time SHA-256 digest compare of the Bearer vkey; missing / +wrong / malformed keys all get one byte-identical generic 401. vkey is a +config ref (`[serve] vkey_ref = "env:HARNESS_SERVE_VKEY"`), never logged. +Unknown model = 400 naming the valid ones. Upstream failures = terse 502. + +## Model catalog (as configured in harness.toml right now) + +Every `[models.classes]` class is exposed as model `mopac-`, routed +through the existing `[models]` tier table to the concrete proxy model: + +| OWUI model name | Tier | Concrete model (sent to LiteLLM) | +|---|---|---| +| `mopac-primary` | mopac-primary | glm-5.3 (default / flagship+) | +| `mopac-code` | mopac-code | glm-5.2 (flagship) | +| `mopac-architecture` | mopac-code | glm-5.2 | +| `mopac-review` | mopac-review | glm-5-turbo (mid) | +| `mopac-summarize` | mopac-review | glm-5-turbo | +| `mopac-writeback` | mopac-review | glm-5-turbo | +| `mopac-study` | mopac-study | glm-4.7-flash (flash) | +| `mopac-read` | mopac-study | glm-4.7-flash | +| `mopac-vision` | mopac-vision | glm-4.6v | + +`[serve] enabled_models` can narrow this to a subset if you want fewer +buttons in OWUI (validated at config load). + +## OWUI connection settings (plug these in) + +- **Base URL**: `http://192.168.3.78:8090/v1` +- **API key (vkey)**: `mopac-owui-f2fa1879c71b8d7c83098c7838f8859d3b85d3bf` + (canonical copy: `~/.coordinate/secrets/mopac-serve-vkey.env`, 0600 — + same value as env var `HARNESS_SERVE_VKEY`; revoke = mint a new one + + restart) +- In OWUI: Admin Settings -> Connections -> add OpenAI API connection with + the above, then pull the model list (the 9 `mopac-*` models appear); + per-user/group model access is OWUI's RBAC, not ours. +- OWUI must NOT request streaming for this connection (v0 returns an + explicit 400 on `stream:true`; OWUI tolerates non-streaming providers — + toggle in the connection settings if a client defaults to streaming). + +## How to run / restart + +```sh +cd ~/projects/meta/MOPAC/harness +set -a; . ~/.coordinate/secrets/mopac-harness-vkey.env # HARNESS_LITELLM_KEY + . ~/.coordinate/secrets/mopac-serve-vkey.env # HARNESS_SERVE_VKEY +set +a +./dev.sh serve # container mopac-serve, port 8090 on the LAN +# stop: docker rm -f mopac-serve (exact container, never pkill) +``` + +It is RUNNING now (container `mopac-serve`, up as of this report). + +## Implementation notes + +- Turn machinery REUSED, not copied: `loop.turn` refactored into a shared + bounded-turn core (`runTurn`) that takes arbitrary message history, an + optional tool palette, and forwarded max_tokens/temperature. `once`/`loop` + behavior unchanged (all prior tests pass untouched); the serve path calls + the same core with tools hard-off (a hallucinated tool call is refused as + a tool result, never executed). +- History assembly: OWUI's own leading system message is preserved verbatim + (OWUI personas win); only when none is sent does the harness prepend a + minimal vertical-identity prompt. +- Commits (logical chunks, all Docker-built via digest-pinned builder): + 8614827 loop refactor, c54a5a4 [serve] config, c9e86ee serve + CLI + + dev.sh runner, 704f905 docs. Pushed to origin main. + +## Test results + +`./dev.sh check` (build + vet + test, golang:1.26 builder): ALL GREEN — + +``` +ok internal/config ok internal/events ok internal/intake +ok internal/llm ok internal/loop ok internal/models +ok internal/serve ok internal/tools ok internal/writeback +``` + +- Serve tests: scripted fake OpenAI upstream against the REAL server — auth + matrix (missing/malformed/wrong key 401, generic body, no vkey leak), + catalog + enabled_models subset, end-to-end chat (concrete model on the + wire, no tools, OpenAI response shape, usage), history assembly both + ways, multi-round usage accounting (refused tool call fed back, sums + 40/30/70), temperature/max_tokens forwarding, unknown-model 400 naming + valid ones, stream 400, empty/bad-role/malformed 400s, upstream 502, + fail-fast construction. +- Loop tests: ServeTurn system-prompt prepend/preserve, tools-off refusal + (allow-listed `pwd` NOT executed), knob forwarding, Router accessor. +- Live LAN proof (python urllib, curl banned on host): healthz 200; wrong + key 401; catalog 200 (all 9 models); unknown model 400 listing them; + stream 400; REAL turns through LiteLLM — `mopac-primary` -> glm-5.3 + ("MOPAC demo door" reply, 255 tokens, finish stop) and `mopac-study` -> + glm-4.7-flash (166 tokens). Audit lines log routing + counters only. + +## Next (per TASK: streaming, tools; plus the obvious follow-ons) + +1. **Streaming**: SSE `stream:true` path on the serve door (needs the llm + client's streaming + truncation-resume work, already on the phase 3 + list). +2. **Tools in serve turns**: per-vertical gated tool palette inside serve + turns (the machinery already supports a palette; it is deliberately nil + for v0). +3. Per-connection vkeys (LiteLLM-vkey-style minting via keyproxy) instead + of one vkey per harness instance. +4. Each onstage/offstage stack gets its own `harness serve` instance with + its own vertical name + class map — that is how "each stack registers + as a model in OWUI" scales past one box. diff --git a/archive/REPORT-harness-skeleton.md b/archive/REPORT-harness-skeleton.md new file mode 100644 index 0000000..54ef22a --- /dev/null +++ b/archive/REPORT-harness-skeleton.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. diff --git a/archive/REPORT-knel-aimiddleware-consolidation.md b/archive/REPORT-knel-aimiddleware-consolidation.md new file mode 100644 index 0000000..44799cc --- /dev/null +++ b/archive/REPORT-knel-aimiddleware-consolidation.md @@ -0,0 +1,67 @@ +# REPORT: KNEL-AIMiddleware consolidated into _crossfeed/tooling + +Date: 2026-08-28 +Operator: reachableceo agent +Status: COMPLETE — all deliverables verified + +## Method + +1. Verified source and target are on the same filesystem (both device 2049, /dev/sda1). +2. Verified the stale target had no unique work before deletion: no extra branches + (main only), no stashes, no commits not on origin/main. Its only uncommitted + change was a deleted `.env` (see Skipped). +3. `rm -rf` stale copy at /home/_crossfeed/tooling/knel-aimiddleware (2.4M). +4. Atomic same-filesystem `mv` of /home/reachableceo/projects/KNEL-AIMiddleware + (512M, incl. vendor/ and .git) into the target path. No data copied; inode + rename only, so no disk-space impact (45G free confirmed before move). +5. Created symlink /home/reachableceo/projects/KNEL-AIMiddleware -> + /home/_crossfeed/tooling/knel-aimiddleware so existing path references keep working. + +## File counts (before / after) + +| Item | Files | Dirs | Size | +|-------------------------------|--------|-------|------| +| Source before move | 21,590 | 4,240 | 512M | +| Stale target before removal | 283 | 171 | 2.4M | +| Target after move | 21,590 | 4,240 | 512M | + +Counts match exactly; nothing lost in transit. + +## Git (verified at final path, via both real and symlink paths) + +- `git status --porcelain`: clean (empty). Branch main, up to date with origin/main. +- HEAD unchanged: 2440188 2026-08-10 "feat(tooling): containerize dns-cli in tooling-cli/dns" +- Remote intact: origin ssh://git@git.knownelement.com:29418/KNEL/KNEL-AIMiddleware.git (fetch + push) +- History preserved (full .git moved with the tree). + +## Symlink + +/home/reachableceo/projects/KNEL-AIMiddleware -> /home/_crossfeed/tooling/knel-aimiddleware +Resolves: yes (spot-checked file read through old path). + +## Permissions applied + +- Top level: reachableceo:users, mode 775. +- Recursive on all reachableceo-owned entries: chgrp users; dirs g+rx; files g+r + (incl. .git, so group members can run read-only git commands). +- Audit: 21,126 entries now group `users`; 0 owned entries missing required group bits. + +## Skipped / exceptions + +1. 4,753 entries under vendor/discourse-mcp/.pnpm-store are root:root (created by + root during containerized builds). chgrp/chmod impossible without sudo. All are + mode 755 (dirs) / 644 (files), i.e. world-readable, so group `users` read access + is unaffected. Cosmetic only. +2. Stale copy's one uncommitted change (deletion of .env) was discarded with it. + The surviving copy contains .env intact, so nothing of value was lost. +3. Two dangling symlinks in vendor test fixtures (bash-language-server, + terraform-ls testdata) could not be chgrp'd via dereference; `chgrp -h` applied + to the link inodes instead. They were dangling in the source already. +4. Minor: .git/index gets rewritten by git commands and may drift back to group + `reachableceo` over time; harmless for read access (mode stays group/world + readable). Re-run `chgrp -R users` if strict group ownership is ever required. + +## Result + +/home/_crossfeed/tooling/knel-aimiddleware is now the single current shared copy +(full working tree + vendor/ + .git + remote). Old path is a working symlink. diff --git a/archive/REPORT-litellm.md b/archive/REPORT-litellm.md new file mode 100644 index 0000000..948337e --- /dev/null +++ b/archive/REPORT-litellm.md @@ -0,0 +1,100 @@ +# REPORT: LiteLLM proxy deployed + RCEO pilot wired + +Date: 2026-08-28 (turn TASK-20260828-1716-litellm) +Operator: RCEO-Work +Status: COMPLETE (one deviation, pre-approved — see "DB deviation") + +## Containers + +| name | image | status | port | +|---|---|---|---| +| litellm | ghcr.io/berriai/litellm-database:main-stable | Up, restart unless-stopped | **192.168.3.78:4001** -> 4000 | +| litellm-db | postgres:alpine | Up, restart unless-stopped | none (docker-internal only) | + +Port note: host 4000 is taken (kneldevstack-supportstack-demo-homepage) -> used +4001 per task instruction. No existing container was stopped/restarted/touched. + +## DB deviation (pre-approved fallback) + +v2.2 specified "embedded SQLite". Upstream fact: LiteLLM has NEVER supported +SQLite for virtual keys/spend — Prisma schema is postgresql-pinned in every +release tag I checked (v1.53.7 Apr 2025, v1.63.14, v1.67.0, main); current +main-stable fails fast: "database features ... require PostgreSQL". No sqlite +code path exists in the installed package. Deployed the fallback PMO +pre-approved in STATE-SNAPSHOT-20260828-1720.md: "dedicated litellm-db postgres +container (standard minimal deploy, not overbuild)". Everything else per spec. +Full recipe was validated with throwaway containers first, then deployed. + +## Virtual keys (8) — NAMES only, values are secrets + +Values: /home/_crossfeed/tooling/credentials/litellm-virtual-keys.md (mode 600, +gitignored). Budgets are USD per 30d window, enforced at the proxy. + +| key_alias | budget | scope | +|---|---|---| +| rceo | $50 / 30d | onstage pilot (this host, crush) — ACTIVE | +| bod | $30 / 30d | onstage BOD | +| cco | $30 / 30d | onstage CCO | +| cto | $30 / 30d | onstage CTO | +| coo | $30 / 30d | onstage COO | +| offstage-1 | $15 / 30d | offstage (capped lower per v2.2) | +| offstage-2 | $15 / 30d | offstage | +| offstage-3 | $15 / 30d | offstage | + +All keys allow all 12 z.ai models (glm-5.3, 5.2, 5.1, 5-turbo, 5, 4.7, +4.7-flash, 4.6, 4.6v, 4.5, 4.5-air, 4.5v). Global hard cap: 3 concurrent +(config.yaml max_parallel_requests). Offstage hosts reach the proxy at +http://192.168.3.78:4001/v1 (OpenAI-compatible). + +## Config / secrets layout + +- /home/_crossfeed/tooling/litellm/config.yaml — 12 z.ai models via + https://api.z.ai/api/coding/paas/v4 (OpenAI-compatible), master_key from env, + per-token USD pricing mirrored from the crush catalog, drop_params on. +- /home/_crossfeed/tooling/credentials/litellm-secrets.env (600, gitignored) — + LITELLM_MASTER_KEY, ZAI_API_KEY (real key; only place outside crush state), + DATABASE_URL. Passed via --env-file, never in config or logs. +- /home/_crossfeed/tooling/credentials/litellm-db.env (600, gitignored) — + postgres creds for litellm-db. +- /home/_crossfeed/tooling/litellm/data/pg — postgres data (persists across + container recreation). + +## RCEO pilot wiring + +- Backups: /home/reachableceo/.local/share/crush/providers.json.pre-litellm.bak + (task-required) and crush.json.pre-litellm.bak (companion; holds the direct + z.ai key override). +- Actual wiring lives in /home/reachableceo/.config/crush/crush.json (600): + custom provider "litellm" -> base_url http://192.168.3.78:4001/v1 with the + rceo virtual key; models large=glm-5.2, small=glm-5-turbo. +- WHY not providers.json: crush auto-regenerates providers.json from its + bundled catalog on startup (verified — my endpoint edit there was silently + reverted, which caused a transient 401 during wiring). ~/.local/share/crush/ + is machine state; the durable override point is the config file. + +## Verification (all redacted, no key values) + +1. Headless crush via proxy: `crush run --quiet -m litellm/glm-5.2 "Reply with + exactly: PROXY-OK..."` -> **PROXY-OK**; flagless default run -> "READY"; + proxy access log shows the corresponding `POST /v1/chat/completions 200`. +2. Usage/key-info API sample (GET /key/info, master auth): + `{"key_alias":"rceo","spend_usd":2.035e-05,"max_budget_usd":50.0, + "budget_duration":"30d","budget_reset_at":"2026-09-01T00:00:00+00:00"}` + GET /global/spend/logs: `[{"date":"2026-08-28","spend":2.035e-05}]` + -> per-key attribution + dollar spend recording confirmed end-to-end. + +## Emergency fallback (restore direct z.ai) + +1. cp -p ~/.local/share/crush/providers.json.pre-litellm.bak ~/.local/share/crush/providers.json +2. rm ~/.config/crush/crush.json (removes proxy wiring; model reverts to zai) + (crush.json.pre-litellm.bak also restorable if state was touched) +3. Optional teardown: docker rm -f litellm litellm-db +Direct z.ai key remains intact in crush state the whole time. + +## Notes / caveats + +- glm-5.2 is priced 0/0 in the crush catalog (coding-plan included), so its + proxy spend tracks $0 by design; other models track real USD. +- Test artifacts from validation (throwaway containers/network/envs) fully + removed; no residue. Images kept locally for fast redeploys. +- credentials/.gitignore extended with the three litellm secret filenames. diff --git a/archive/REPORT-lsp-stack.md b/archive/REPORT-lsp-stack.md new file mode 100644 index 0000000..c24efea --- /dev/null +++ b/archive/REPORT-lsp-stack.md @@ -0,0 +1,102 @@ +# REPORT — LSP stack in docker, wired into crush (MOPAC dev env) + +Date: 2026-08-28 20:15 | Home: ~/projects/meta/MOPAC/lsp/ | Status: DONE (4.5/5 servers with diagnostics; bash wired but server emits none upstream) + +## What runs + +Five language servers, all containerized, built from `MOPAC/lsp/docker-compose.yml` +(`docker compose --profile build build`), on the dedicated bridge network +`mopac-lsp` (isolated from other stacks; outbound HTTPS allowed because gopls +fetches Go modules via proxy.golang.org at analysis time). stdio servers: one +`docker run -i --rm` container per crush session via wrapper scripts +(KNEL-AIMiddleware pattern, PID-suffixed names for concurrent sessions). +No ports published (stdio over pipes). No host mounts beyond the workspace: +gopls mounts the go.mod project root at its host path (URI parity) + named +cache volumes `mopac-lsp-gomodcache` / `mopac-lsp-gocache`; marksman mounts +cwd read-only; node servers mount nothing. + +| Server | Version | Base (digest-pinned) | Diagnostics verified | +|---|---|---|---| +| gopls | v0.23.0 | golang:1.26-alpine sha256:28d89ee9... | YES — semantic (see proof) | +| yaml-language-server | 1.24.0 | node:22-alpine sha256:c610fcdf... | YES — "Flow sequence ... must end with ]" on bad yaml | +| dockerfile-language-server | 0.15.0 | node:22-alpine sha256:c610fcdf... | YES — "Unknown instruction: FRM" on bad Dockerfile | +| marksman | 2026-02-08 | alpine:3.22 sha256:14358309... | YES — lsp_symbols on .md (server emits few diagnostics by design) | +| bash-language-server | 5.6.0 | node:22-alpine sha256:c610fcdf... | WIRED — container starts, initialize+publishDiagnostics flow; **v5.6 upstream does not publish tree-sitter parse errors as diagnostics** (same behavior in the KNEL stack). Hover/symbols/completion work. | + +marksman correction to the TASK text: it is an F# project, not a Go binary. +No Go module exists to build; used the official musl release binary, pinned +by release tag + build-time sha256 verification, DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 +(no ICU package needed). + +## Crush wiring (how) + +`lsp` block merged into `~/.config/crush/crush.json` (global config; backup at +`~/.config/crush/crush.json.bak-mopaclsp`). Canonical snippet kept in sync at +`MOPAC/lsp/crush-lsp.json`. Entries: gopls (filetypes go/mod/sum/work, +root_markers go.mod|go.work, timeout 120 — cold start may download modules), +bash-language-server, yaml-language-server, dockerfile-language-server +(root_markers Dockerfile*), markdown (= marksman). **No per-project setup is +needed**: crush auto-starts the matching server when a matching file is +opened/viewed/edited in a matching root. Version bumps require only editing +the Dockerfile ARG + compose arg + wrapper image tag, then rebuild. + +Wiring gotchas (empirical, crush v0.87.0): +- marksman entry must be keyed `markdown` (KNEL-style); keyed `marksman` it + never matches .md files and no container ever spawns. +- An LSP config entry keyed by catalog name inherits powernap defaults; + explicit filetypes/root_markers in the entry are respected. +- Fixed a real blocker in harness: go.mod was a module-less stub ("go 1.24" + only) — gopls returns "no module path" and zero semantic diagnostics. + Added `module git.knownelement.com/reachableceo/MOPAC/harness` (absorbed + into harness commit 591d345 by the parallel phase-2 session). + +## Proof (headless crush turns, 2026-08-28) + +1. gopls on real harness code (`cmd/harness/main.go` with a temp injected + error at line 113, restored after): + `113:25 [gopls compiler][UnusedVar] declared and not used: x` + `113:37 [gopls compiler][UndeclaredName] undefined: yy` + (Earlier, pre-fix: `undefined: undefindedVariable` Error 7:14 on the + scratch file — full pipeline incl. container spawn confirmed by docker ps + polling during the turn.) +2. yaml: `line 3:1 Flow sequence in block collection must be sufficiently + indented and end with a ]` (yaml-language-server). +3. dockerfile: 2 errors on `FRM alpine:3.22` — `No source image provided + with FROM` + `Unknown instruction: FRM` (dockerfile-language-server). +4. markdown: `lsp_symbols` returned `String Title (line 1)` for the test .md. +5. bash: container spawns on .sh access; raw probe shows the server parses + and logs `syntax error` via window/logMessage but publishes an empty + diagnostics array (upstream behavior, not a wiring failure). + +Scratch files for re-verification kept in `MOPAC/lsp/verify-scratch/`; +raw probes: `lsp-handshake-test.sh`, `lsp-diagnostics-probe.sh`. + +## Node-exception list (supply-chain ledger) + +Sanctioned node-based exceptions — containerized only, never installed on +host; base node:22-alpine digest-pinned (sha256:c610fcdfb1d5b4740dd70c284 +ed3cb16bb857e0f7166196e36a5501df7a3aa32); npm pins installed at build time: +- bash-language-server@5.6.0 (npm integrity sha512-DCuV+/BZAAozsp5...) +- yaml-language-server@1.24.0 (sha512-+HGcwu4M7IC...) +- dockerfile-language-server-nodejs@0.15.0 (sha512-iS3GUUAn1bC3s...) + +## Failures / deviations + +- marksman mislabeled "Go binary" in TASK — it's F#; handled via pinned + prebuilt musl binary (sha256-verified) instead of building from source. +- gopls v0.23.0 requires go >= 1.26; initial golang:1.25-alpine base failed + (`requires go >= 1.26.0`), switched to golang:1.26-alpine (digest recorded). +- marksman musl binary needs libgcc/libstdc++ on alpine (added) and .NET ICU + check bypassed via invariant-globalization env var. +- Compose parses unquoted `2026-02-08` args as timestamps ("2026-02-08 + 00:00:00 +0000 UTC") — quote date-like build args (fixed in compose). +- bash-language-server publishes no parse diagnostics (upstream); noted, + options.bashIde.sourceErrorDiagnostics=true set anyway for future-proofing. +- MOPAC/lsp is not yet under version control (MOPAC root has no git repo; + harness/.git is separate). Not committed — awaiting founder instruction + on which repo should own dev-env tooling. + +## Ports + +None. All servers are stdio (docker run -i, no published ports). Network +`mopac-lsp` exists for isolation + gopls egress only. diff --git a/archive/REPORT-pmo-ops.md b/archive/REPORT-pmo-ops.md new file mode 100644 index 0000000..ebccefc --- /dev/null +++ b/archive/REPORT-pmo-ops.md @@ -0,0 +1,8 @@ +# REPORT — PMO ops batch (2026-08-28 ~19:27, RCEO-Work) + +1. DESIGN.md: 6 sections appended (keyproxy/vault placeholders, FLOSS repo coupling, dev-in-docker BIG RULE, events V1 webhook receiver, OWUI front door / Hermes killed, budget+quota). Commit ab29d8a pushed to ukrrs/MOPAC main. +2. Redmine: issue 480 (study-crush) + 481 (study-maki) -> status_id 3 (closed) with journal notes citing deliverables. Env sourced in bash, passed via env to python3/urllib; no exec() of env file. +3. REPORT-study-crush.md + REPORT-study-maki.md written to inbox-pmo; deliverables verified in git (a606810, a1623e1) + pushed. +4. log.md: PMO line appended (19:27-05:00). + +ALL DONE. diff --git a/archive/REPORT-ready.md b/archive/REPORT-ready.md new file mode 100644 index 0000000..272192a --- /dev/null +++ b/archive/REPORT-ready.md @@ -0,0 +1,5 @@ +REPORT — reachableceo-Work online (2026-08-28) + +- Read INTRO-WORK.md and PROTOCOL.md; checklist run (BOARD, inbox-work, log tail). +- inbox-work/ empty — no TASKs yet. +- Awaiting first TASK dispatch. diff --git a/archive/REPORT-study-crush.md b/archive/REPORT-study-crush.md new file mode 100644 index 0000000..8351365 --- /dev/null +++ b/archive/REPORT-study-crush.md @@ -0,0 +1,7 @@ +# REPORT — study-crush (PMO verification, 2026-08-28) + +- Deliverable: /home/reachableceo/projects/meta/MOPAC/harness/docs/PORTING-NOTES-crush.md +- Covers: crush internals — MCP via go-sdk, session model, skip-list, exposure-minimization protocol (env/VAR indirection, redaction, canonical-source regeneration). +- PMO-verified: file exists; committed (a606810) and pushed to ukrrs/MOPAC on main. +- Redmine issue 480 (Study crush internals) set to status_id 3 (closed) with journal note. +- STATUS: DONE. diff --git a/archive/REPORT-study-maki.md b/archive/REPORT-study-maki.md new file mode 100644 index 0000000..141f501 --- /dev/null +++ b/archive/REPORT-study-maki.md @@ -0,0 +1,7 @@ +# REPORT — study-maki (PMO verification, 2026-08-28) + +- Deliverable: /home/reachableceo/projects/meta/MOPAC/harness/docs/PORTING-NOTES-maki.md +- Covers: maki internals — tree-sitter usage, bash permission scopes, token reduction, subagent tiers. +- PMO-verified: file exists; committed (a1623e1) and pushed to ukrrs/MOPAC on main. +- Redmine issue 481 (Study maki internals) set to status_id 3 (closed) with journal note. +- STATUS: DONE. diff --git a/archive/REPORT-study-secrets.md b/archive/REPORT-study-secrets.md new file mode 100644 index 0000000..65d5064 --- /dev/null +++ b/archive/REPORT-study-secrets.md @@ -0,0 +1,118 @@ +# Porting notes: KNELSecretsManager (reference/KNELSecretsManager) + +Scope for MOPAC harness: Bitwarden secret retrieval for `bw:` key refs +(reserved in `internal/config/keys.go:34-35`, errors until "phase 3"). +DESIGN.md: Go `bitwarden-go` wrapper replaces KNELSecretsManager; secrets +never in logs. Line refs from study 2026-08-28, drift possible. + +## 1. Current surface + +Two generations ship in the repo; ADR-002 (`docs/ADR-002-ContainerBW.md`) +supersedes the standalone script with a containerized `bw` wrapper. + +**Legacy `secrets-manager.sh` (v2.0)** — single bash script, CLI only: +`install | get | list | test`, `-c/--config FILE`. Every invocation +redoes the full lifecycle: source plaintext config (BW_SERVER_URL, +BW_CLIENTID, BW_CLIENTSECRET, BW_PASSWORD) -> `bw config server` -> +`bw logout` -> `bw login --apikey` -> `bw unlock --raw` -> `bw get password +` (secrets-manager.sh:119-159). Exit codes 10/20/30/40/50/60/70 +(config/bw-missing/install/server/session/secret/login). Logs to +`/tmp/secrets-manager.sh.log` (names, not values). Designed to be vendored +into shell frameworks; Makefile lint/test/vendor-test targets. + +**Current container wrapper (production since 2026-08-13, ADR-002)**: +- `bin/bw-install.sh` — downloads pre-compiled bw `2026.7.0` (inside an + alpine container; no host Node/wget), builds `reachableceo-bw-native: + 2026.7.0` (debian:bookworm-slim + ca-certificates, `docker/bw-native/ + Dockerfile`), installs wrapper to `~/.local/bin/bw` + entrypoint to + `~/.local/share/bw/entrypoint.sh`. +- `bin/bw-cli.sh` (host) — loads `~/.config/bw/env` (single-quoted values, + mode 600; the only secrets-on-disk allowed org-wide), then `docker run` + with named volume `tsys-bw-cli-state` mounted at `/root/.config/Bitwarden + CLI` so login state persists across calls. +- `bin/bw-entrypoint.sh` (container) — idempotent: `bw config server` -> + `bw login --apikey` (no TOTP; API key minted from an authenticated web + session) -> `bw unlock --passwordfile` (password briefly in /tmp) -> + `bw sync` -> `exec bw "$@"` with BW_SESSION. +- Interface is transparent `bw`: `status | list items/collections | get + password|totp|item | generate`. Lookup is by item **name**; the + default secret is the item's password field (e.g. `APIKEY-pushover`). +- Backend: Vaultwarden at `https://pwvault.turnsys.com`, machine account + `coo@turnsys.com`, API-key auth + master-password unlock. + +**Consumers**: nothing in the MOPAC harness consumes it yet — sole planned +integration is the `bw:` key-ref prefix (`internal/config/keys.go`, +asserted by `config_test.go:126,130`; `harness.toml.example:5`). First +real secrets needed: Redmine API key + LiteLLM key (`[redmine] key_ref`, +`[litellm] key_ref`). Siblings still on Bitwarden: KNELCredsManager +(`~/.creds/*.env` feeder) and `mcp-bitwarden-wrapper.sh` in +KNEL-AIMiddleware. + +**Weaknesses to fix in the port**: plaintext master password + API secret +on disk; full login/unlock/sync per call (slow, racy logout in legacy); +secret values echoed to stdout and captured by callers into env; docker +required per host; "native" bw binary is secretly a Node SEA (ADR-002 +caveat); legacy script `bw logout`s any concurrent session. + +## 2. Replacement design sketch (Go) + +**Shape**: `internal/secrets` package + thin `MOPAC/tools/bitwarden-go` +CLI (per DESIGN.md:88) so both harness and humans share one resolver. +Config gains `ResolveKeyRef` case for `bw:REF` (keys.go already reserves +it; keep `redact()` semantics). + +**bw execution**: exec `bw` from PATH (the ADR-002 wrapper makes it +transparent whether bw is native or containerized). Treat as opaque +subprocess: `bw get password --session --nointeraction`, +JSON mode where useful (`bw get item `), 30s timeout, capture +stdout into a `[]byte` not a logged string. + +**Machine credentials** (bootstrapping only): prefer env +`BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD`; fallback `~/.config/bw/env` +parsed by hand (single-quoted values, require mode 0600). Never in +harness.toml, never in logs. Server URL from env `BW_SERVER` with +Vaultwarden default. + +**Unlock strategy**: once per process — config server (idempotent), +`bw login --apikey` (skip if `bw status` says logged in), `bw unlock +--passwordenv BW_PASSWORD --raw` -> session key held **in memory only**; +single `bw sync` after login. On session-expired error mid-run: re-unlock +once, then fail. Never `bw logout` (breaks concurrent clients). + +**Lookup by key**: `bw:` -> password field (matches current +usage). Optional `bw:#` and `bw:#totp` if non-password +fields are needed. Not-found => typed `ErrSecretNotFound`; auth failures +=> `ErrUnlockFailed` (mirrors exit codes 60/50/70 for callers). + +**Caching**: per-run in-memory `map[ref]string` + mutex (multi-vertical +workers may resolve concurrently); negative results cached too. Session +token cached for process lifetime, zeroed on exit. No disk cache of +values; optional disk persistence of BW_SESSION is an open question. + +**Never-log rules** (extends keys.go:70-78): log refs only as +`bw:****`; error strings never embed values; subprocess stderr from +bw is scrubbed/dropped (bw can echo fragments); secrets never written to +REPORT.md, spans, or crash dumps; `go test` uses fake bw stub, never the +real vault. + +## 3. Open questions for Charles + +1. **Substrate**: keep shelling out to `bw` (containerized per ADR-002, + Docker on every harness host) or implement the Vaultwarden REST API + directly in Go (truly zero Node/bw, but reimplements crypto + sync)? + DESIGN.md:84 says "replaces Node bw CLI dependency" — which side of + that line do you want? +2. **Machine account**: dedicated service account for the harness instead + of `coo@turnsys.com`, with a vault collection scoped to only the + secrets the harness needs? +3. **Ref syntax**: is `bw:` (password field) sufficient for + v1, or do you want `#field` / `#totp` / collection-scoped refs now? +4. **Session lifetime**: unlock per `harness once` run only, or persist + BW_SESSION (0600, TTL) across runs to avoid unlock cost each start? +5. **Secret inventory + names**: confirm exact vault item names for + Redmine and LiteLLM keys so `key_ref` values can be pinned in + harness.toml.example. +6. **Scope of port**: absorb KNELCredsManager's `~/.creds/*.env` + contract too, or strictly the `bw:` refs used by the harness? +7. **Subprocess injection**: any consumers needing secrets as env vars + for child processes (LiteLLM?), vs HTTP-header-only as today? diff --git a/archive/TASK-20260828-1710-knel-migrate.md b/archive/TASK-20260828-1710-knel-migrate.md new file mode 100644 index 0000000..90e663a --- /dev/null +++ b/archive/TASK-20260828-1710-knel-migrate.md @@ -0,0 +1,23 @@ +# TASK: Consolidate KNEL-AIMiddleware into _crossfeed/tooling + +## Objective +Make /home/_crossfeed/tooling/knel-aimiddleware the single current shared copy of KNEL-AIMiddleware, replacing the stale Aug-10 copy. + +## Context +- Source (current): /home/reachableceo/projects/KNEL-AIMiddleware — 512M, 21,590 files, git repo, remote ssh://git@git.knownelement.com:29418/KNEL/KNEL-AIMiddleware.git, vendor/ = 508M. +- Target (stale): /home/_crossfeed/tooling/knel-aimiddleware — 2.4M, 283 files, git repo, no vendor/, contains since-removed JOURNAL.md. Dated 2026-08-10. +- Target parent is reachableceo-writable; no sudo needed. +- Charles is splitting work from all-reachableceo to multiple accounts; other agent accounts (group `users`) need read access. + +## Constraints +- No sudo. +- Preserve git history + remote in the surviving copy. +- Do not break path references: leave symlink /home/reachableceo/projects/KNEL-AIMiddleware -> /home/_crossfeed/tooling/knel-aimiddleware after the move. +- Keep vendor/ (disk has 45G free; move is same-filesystem rename). +- Verify target git status clean after move. + +## Deliverables +1. _crossfeed/tooling/knel-aimiddleware holds the current working tree incl vendor/ and .git. +2. Symlink from old path in place and resolves. +3. Perms: group users, dirs g+rx, files g+r (top-level chgrp users). +4. REPORT to inbox-pmo with: method used, file counts before/after, git status, remote intact, perms applied, anything skipped. diff --git a/archive/TASK-20260828-1712-governance-gate.md b/archive/TASK-20260828-1712-governance-gate.md new file mode 100644 index 0000000..24ceb24 --- /dev/null +++ b/archive/TASK-20260828-1712-governance-gate.md @@ -0,0 +1,19 @@ +# TASK: Amend agent-stack GOVERNANCE.md release policy (mechanical gate) + +## Objective +Replace the per-release founder gate in agent-stack GOVERNANCE.md with a mechanical release gate modeled on TSYSGroupAIOS. + +## Context +- Current GOVERNANCE.md (~L33): "reachableceo: capture anywhere, anytime. Approve releases (founder gate)." +- TSYSGroupAIOS (~/projects/TSYSGroupAIOS, per Charles 2026-08-28) establishes the model: policy in AGENTS.md prose, enforcement via git hooks + scripts/check-rules.sh audit; audit-clean = shippable. +- Proposed policy: agent-stack tooling releases are PRE-APPROVED when the repo's mechanical checks pass (bash -n on scripts, git commit hooks, no changes outside tooling/). Founder gate is RETAINED for: systemd units, relaunchers/stack-touching scripts, multiuser/perms changes, credentials/. + +## Constraints +- Edit only /home/_crossfeed/tooling/agent-stack/GOVERNANCE.md; commit to that repo; no other files. +- Wording must stay plain-language (Charles reads this doc). +- Do not rename or touch /home/_crossfeed/founder/ references (separate pending decision). + +## Deliverables +1. GOVERNANCE.md amended with the mechanical-gate release policy. +2. Commit with clear message. +3. REPORT to inbox-pmo with the diff. diff --git a/archive/TASK-20260828-1716-litellm.md b/archive/TASK-20260828-1716-litellm.md new file mode 100644 index 0000000..46ff750 --- /dev/null +++ b/archive/TASK-20260828-1716-litellm.md @@ -0,0 +1,20 @@ +# TASK: Deploy LiteLLM proxy + wire RCEO pilot stack + +## Objective +One LiteLLM container fronting the shared z.ai key on this host; virtual keys issued; RCEO crush pointed at it; usage API verified. Implements DESIGN-24x7-execution.md §v2.2. + +## Context +- z.ai provider + key currently live in /home/reachableceo/.local/share/crush/providers.json (NEVER print key values in REPORT/logs). +- reachableceo is in the docker group — containers allowed, no sudo needed. +- This host already runs docker (25 containers). Offstage (other machine) must reach the proxy later, so bind to the LAN interface, not just loopback. + +## Deliverables +1. Config: /home/_crossfeed/tooling/litellm/config.yaml — z.ai models via OpenAI-compatible endpoint (api.z.ai), master_key from credentials/, embedded SQLite. Secrets file mounted 600. +2. Container: name `litellm`, restart unless-stopped, port 4000 on LAN interface (4001+ if busy, note it). +3. Virtual keys (8): rceo, bod, cco, cto, coo, offstage-1, offstage-2, offstage-3. Names + budgets documented; key VALUES only in /home/_crossfeed/tooling/credentials/litellm-virtual-keys.md (mode 600). +4. RCEO pilot wiring: backup providers.json -> providers.json.pre-litellm.bak; point base_url at http://:4000 with the rceo virtual key. +5. Verify: one headless crush call through the proxy succeeds; hit the LiteLLM usage/key-info endpoint and capture (redacted) output. +6. REPORT to inbox-pmo: container name/health, port, virtual key NAMES (not values), backup path, redacted usage sample, emergency fallback = restore providers.json.pre-litellm.bak. + +## Constraints +- No sudo. No secrets in any REPORT/log. Do not touch TSG account homes. Do not stop or restart any existing container. If any step fails, BLOCKED report, do not improvise credentials handling. diff --git a/archive/TASK-20260828-1900-study-crush.md b/archive/TASK-20260828-1900-study-crush.md new file mode 100644 index 0000000..7046b63 --- /dev/null +++ b/archive/TASK-20260828-1900-study-crush.md @@ -0,0 +1,13 @@ +# TASK: Reference study — crush internals (PORTING-NOTES-crush.md) + +## Objective +Study /home/reachableceo/projects/meta/MOPAC/reference/crush. Produce +/home/reachableceo/projects/meta/MOPAC/harness/docs/PORTING-NOTES-crush.md. + +## Deliverables (notes file, <=180 lines, dense, no fluff) +1. MCP plumbing: config format, stdio/http transports, tool registration — what to port to Go. +2. Session/persistence model: session files, --continue, resume semantics. +3. Provider layer: how providers.json/crush.json merge, model routing, retries/stream handling (note where stream-truncation errors would surface). +4. Headless mode: how crush run/--print works, exit codes. +5. OAuth flows: where they live (deferred for v1 but map the code path). +6. Top 10 packages/files worth reading in detail during build; top 5 gotchas. diff --git a/archive/TASK-20260828-1901-study-maki.md b/archive/TASK-20260828-1901-study-maki.md new file mode 100644 index 0000000..3950399 --- /dev/null +++ b/archive/TASK-20260828-1901-study-maki.md @@ -0,0 +1,12 @@ +# TASK: Reference study — maki internals (PORTING-NOTES-maki.md) + +## Objective +Study /home/reachableceo/projects/meta/MOPAC/reference/maki. Produce +/home/reachableceo/projects/meta/MOPAC/harness/docs/PORTING-NOTES-maki.md. + +## Deliverables (notes file, <=180 lines, dense) +1. Tree-sitter bash permission parsing: how commands/subshells/pipes are decomposed -> allow/deny rules. Go equivalents (github.com/tree-sitter/tree-sitter-bash). +2. Token reduction: index/skeleton reads, MCP tool_search hiding, sandboxed code_execution — what's portable to a headless Go harness, what to skip. +3. Subagent model (weak/medium/strong, read-only vs full tools) — map to our TASK/dispatch pattern. +4. Headless/--print mode + session parallelism. +5. Top 10 files to re-read during build; top 5 gotchas. diff --git a/archive/TASK-20260828-1902-study-secrets.md b/archive/TASK-20260828-1902-study-secrets.md new file mode 100644 index 0000000..53e005b --- /dev/null +++ b/archive/TASK-20260828-1902-study-secrets.md @@ -0,0 +1,10 @@ +# TASK: Reference study — KNELSecretsManager (PORTING-NOTES-secrets.md) + +## Objective +Study /home/reachableceo/projects/meta/MOPAC/reference/KNELSecretsManager. Produce +/home/reachableceo/projects/meta/MOPAC/harness/docs/PORTING-NOTES-secrets.md. + +## Deliverables (notes file, <=120 lines) +1. Current surface: what it stores/serves, interface (CLI? API?), consumers. +2. Replacement design sketch (Go): Bitwarden CLI (bw) wrapper — unlock strategy, machine credentials, secret lookup by key, caching, never-log-secrets rules. +3. Open questions list for Charles (be specific). diff --git a/archive/TASK-20260828-1903-harness-skeleton.md b/archive/TASK-20260828-1903-harness-skeleton.md new file mode 100644 index 0000000..dbb49f8 --- /dev/null +++ b/archive/TASK-20260828-1903-harness-skeleton.md @@ -0,0 +1,26 @@ +# TASK: Harness skeleton (build phase 2) + +## Objective +Initial Go implementation in /home/reachableceo/projects/meta/MOPAC/harness per +its DESIGN.md (READ IT FIRST, esp. "MVP demo bar" + "Model selection layer" +sections, appended 2026-08-28 ~19:00): config load, conductor loop (single-shot +mode), redmine-pull intake (list issues in scope), bounded turn via LiteLLM +(OpenAI-compatible, base_url+key from config), REPORT file writeback, exec tool +(bash, allow-listed). + +## Constraints +- Read docs/PORTING-NOTES-*.md first if present. +- `go build ./...` and `go vet ./...` clean; no external deps beyond stdlib + + tree-sitter/gomft if genuinely needed (prefer stdlib for v1). +- Config: harness.toml (scope query, litellm url/key-ref, paths) + example file. +- Single-shot: `harness once` runs ONE iteration then exits (chainable, no daemon). +- Dry-run mode: `harness once --dry-run` = intake+plan only, no LLM call. +- MODEL ROUTING v0: harness.toml `[models]` tier map (aliases `mopac-study` + -> glm-4.7-flash, `mopac-code` -> glm-5.2, `mopac-review` -> glm-5-turbo, + `mopac-primary` -> glm-5.3) + per-TASK `class` field; requests go out with + the concrete model name resolved from the map. No heuristic code in v0. +- MVP DEMO (the acceptance bar): `harness once` on a demo issue whose prompt + is "tell me about yourself" through LiteLLM; the GLM self-description comes + back and lands as the REPORT. Wire this as an example/smoke path. +- Tests for loop/gate logic (table-driven, stdlib testing). +- Commit incrementally; REPORT lists what works, what's stubbed, next chunk. diff --git a/archive/TASK-20260828-1915-lsp-stack.md b/archive/TASK-20260828-1915-lsp-stack.md new file mode 100644 index 0000000..ba32a37 --- /dev/null +++ b/archive/TASK-20260828-1915-lsp-stack.md @@ -0,0 +1,26 @@ +# TASK: LSP stack in docker, wired into crush (MOPAC dev env) + +## Objective +Language servers containerized + wired into crush config so agent/human Go +dev on MOPAC gets diagnostics. Home: /home/reachableceo/projects/meta/MOPAC/lsp/ +(wrappers + compose + README). + +## Deliverables +1. Containers (compose file, isolated network, no host mounts beyond workspace): + - gopls (Go toolchain image; the critical one for MOPAC/harness) + - dockerfile-language-server (node-based: sanctioned exception, containerized) + - marksman (markdown; Go binary) + - yaml-language-server + bash-language-server (node-based: same exception) +2. Wrapper scripts per KNEL-AIMiddleware lsp-*wrapper.sh pattern (see + ../siblings/KNEL-AIMiddleware/lsp-bash-wrapper.sh) adapted to MOPAC/lsp/. +3. Crush wiring: add servers to crush config (study how crush declares LSP: + reference/crush docs/code) for gopls at minimum; others best-effort. +4. Verify: open/edit a .go file in a MOPAC project via a headless crush turn -> + diagnostics actually flow (capture proof in REPORT). +5. REPORT to inbox-pmo: what runs, ports, how crush is wired, node-exception + list (for supply-chain ledger), failures if any. + +## Constraints +No secrets involved. Reuse golang official images, pin digests. If crush LSP +config needs per-project setup, document exactly where. Do not touch live +screens. Node-based servers stay containerized and unpinned-to-host. diff --git a/archive/TASK-20260828-1930-pmo-ops.md b/archive/TASK-20260828-1930-pmo-ops.md new file mode 100644 index 0000000..355901c --- /dev/null +++ b/archive/TASK-20260828-1930-pmo-ops.md @@ -0,0 +1,117 @@ +# TASK: PMO ops batch (traffic-cop delegation, fresh session) + +You are RCEO-Work executing a PMO ops batch. Do ALL steps, terse REPORT at end +to /home/reachableceo/.coordinate/inbox-pmo/REPORT-pmo-ops.md. + +1. APPEND to /home/reachableceo/projects/meta/MOPAC/harness/DESIGN.md: + +## Key proxy: placeholders only, material never leaves the vault (Charles, 2026-08-28 evening) +- PROBLEM with existing gateways: they HOLD real keys in a webui/api and + serve placeholders. We invert that: key material lives ONLY in Bitwarden + (Secrets Manager) + HashiCorp Vault. A Go proxy (MOPAC/tools/keyproxy or + bitwarden-go evolution) resolves placeholder -> real at the wire, holds + material in memory only (fetch-on-demand, short TTL, never persisted, + never in any admin UI; there is no admin surface — config only). +- FRONTEND: consumers (harness, CLIs, containers) receive placeholder keys + (mpk_... opaque refs). If one leaks, it's a placeholder: revoke/re-map in + vault, zero upstream exposure. Defense in depth. +- BACKEND connectors: Bitwarden Secrets Manager REST (machine accounts; + AVOID the official SDK — source-available license conflicts with AGPL; + plain REST via stdlib keeps AGPLv3 clean) + HashiCorp Vault KV v2 + + AppRole (official Go api pkg is MPL-2.0, AGPL-compatible, vendor it). + License: AGPLv3; MIT fallback only if a conflict still arises. +- ADOPT crush's exposure-minimization protocol everywhere (per + docs/PORTING-NOTES-crush.md + house practice): keys via os.environ/VAR + indirection in configs (never literals), 0600 on secret files, redaction + of key material in all logs/output, canonical-source regeneration + (crush.json -> providers.json pattern), never exec env files inside + scripting languages, session scrubbing. +- Also feeds: study-secrets TASK (KNELSecretsManager notes) in flight. + LiteLLM stays a sanctioned service exception (its z.ai key can later be + injected at container start from the vault; not tonight). + +## Tooling = standalone public FLOSS repos, loosely coupled (Charles, 2026-08-28 evening) +- Dedicated CLIs/services each get their OWN public Gitea repo under the + UKRRS org (org for ALL LLM work; AGPLv3), composable by anyone: + ukrrs/mopac-keyproxy (created, seeded), later mopac-redmine-cli, + mopac-discourse-cli, mopac-bitwarden-go, ... ukrrs/MOPAC (transferred + 2026-08-28) is the harness core. MOPAC references them; it does not + contain them. +- LOOSE COUPLING RULES (FLOSS without support burden): config-driven — + no org-specific hosts/paths/defaults baked into code; contracts are + exec + JSON in/out (Unix plumbing); each tool standalone-usable by a + human; TSYS-specific policy/policy repos (agent-stack) live OUTSIDE the + tools. We build for our needs first; genericness comes from clean + contracts, not from supporting foreign requirements. + +## ALL dev work in Docker (Charles, 2026-08-28 evening) — BIG RULE +- No dev toolchains on the host: compiling, vetting, testing, hacking all + happen inside containers (e.g. digest-pinned golang:1.26 builder with the + repo bind-mounted, `go build/vet/test` inside, artifacts out via the + mount). Host runs containers + screens; nothing else. +- docker pull of anything needed is PRE-AUTHORIZED, no asking. +- Exception cleanup: the harness-skeleton turn installed go1.26.7 to + host ~/.local before this rule landed — remove it (follow-up TASK) once + that turn completes; all future builds go through the builder container. +- Rationale: reproducible, supply-chain-clean (CMMC posture), host stays + pristine; same pattern as the LSP stack (digest-pinned containers). + +## Events are V1 scope: webhook receiver now (Charles, 2026-08-28 evening) +- NOT deferred. `mopac events` (Go HTTP receiver, in ukrrs/MOPAC) ships with + the first build. The interaction layer (Redmine/Discourse/Gitea) must be + able to PUNCH the harness: when Charles replies on Discourse, updates a + Redmine ticket, or approves a Gitea PR, the harness knows and acts. +- Trigger -> action semantics (dedup + replay, at-least-once): + Redmine issue update/note on an active stack's issue -> dispatch/redirect + turn for that stack; Discourse post reply in a stack's category/topic -> + context update + response turn; Gitea PR approved/merged -> next pipeline + step (build/test/deploy per repo config). +- Signatures: Gitea HMAC-SHA256 secret header; Discourse webhook secret + header; Redmine shared-secret header (verify, reject unsigned). Normalize + to internal event record -> Redmine issue (SoR) -> conductor picks up. +- Runs containerized (dev-in-docker rule), port published on host LAN; + Cloudron apps get the webhook URL configured per-app. + +## Hermes killed; OpenWebUI is the interactive front door (Charles, 2026-08-28 evening) +- Hermes was Node — removed (dashboard/gateway/searxng, 2026-08-28 ~19:10). + No Node middleware anywhere in MOPAC's path. +- OWUI (Cloudron + SSO) becomes the ONLY interactive surface: MOPAC serves + an OpenAI-compatible /v1/chat/completions endpoint (Go stdlib HTTP, in + this repo, e.g. `mopac serve`). OWUI adds it as a connection; each stack/ + vertical registers as a "model". OWUI admin UI + Cloudron SSO = RBAC we + do not build. Keys minted per OWUI connection (LiteLLM vkeys pattern). +- Event path unchanged: Redmine/Discourse/Gitea webhooks -> `mopac events` + (Go T1 receiver). OWUI chat and webhooks are two doors into the same loop. + +## Budget + quota reality (Charles, 2026-08-28 evening) +- Until 2026-09-22: legacy z.ai coding plan = FLAT RATE. Real budget = plan + quota, not USD. Smaller/flash models stretch quota ~3x. Routing policy: + grind/study/summarize -> flash tier (glm-5.3-flash now live on proxy, + 13 models); flagship (glm-5.3/5.2) only when the work needs it. Maximize + usage of what we already pay for. +- After 2026-09-22: org-wide AI cap $200/mo across all 8 stacks: + 2x $80 z.ai max plans + $40 OpenRouter. Design all budgets/vkeys to fit. + Then: cd harness && git add DESIGN.md && git commit -m "DESIGN: budget + + quota reality (flat-rate now, $200/mo after 09-22; flash-stretch routing)" + && git push origin main + +2. REDMINE SoR mirror (https://projects.knownelement.com, project MOPAC): + source ~/.creds/redmine.env in bash, pass vars to python3 via env — + NEVER exec() the env file inside python (key-leak pattern). Use urllib: + GET /issues.json?project_id=MOPAC to find issue ids by subject + (study-crush, study-maki). Set each to status_id for Done via + PUT /issues/.json {"issue":{"status_id":3}} with X-Redmine-API-Key. + Add a short journal note (notes param) on each: deliverable = + harness/docs/PORTING-NOTES-.md, committed+pushed. + +3. WRITE two brief verification REPORTs in + /home/reachableceo/.coordinate/inbox-pmo/: + REPORT-study-crush.md + REPORT-study-maki.md — 5 lines each: deliverable + path, what it covers, PMO-verified (files exist, in git). + +4. APPEND to /home/reachableceo/.coordinate/log.md (one line, format + "2026-08-28T-05:00 | PMO | ..."): litellm restarted cleanly, + glm-5.3-flash live (13 models); chain halt at study-secrets = known z.ai + flake, redispatched; skeleton turn + queue running; Charles directive: + PMO = traffic cop only (write TASKs, farm to crush -p screens), PMO + context must stay tiny — no meaty work in PMO session. diff --git a/archive/TASK-20260828-1975-events-receiver.md b/archive/TASK-20260828-1975-events-receiver.md new file mode 100644 index 0000000..d98ad44 --- /dev/null +++ b/archive/TASK-20260828-1975-events-receiver.md @@ -0,0 +1,31 @@ +# TASK: mopac events — webhook receiver (build phase 2b, V1 scope) + +## Objective +Implement `mopac events` in /home/reachableceo/projects/meta/MOPAC/harness +per DESIGN.md ("Events are V1 scope" section — READ FIRST). A Go HTTP +webhook receiver that makes Redmine/Discourse/Gitea able to punch the +harness: replies, ticket updates, PR approvals -> harness knows and acts. + +## Constraints +- ALL DEV IN DOCKER (hard rule): no host toolchains. Pull digest-pinned + golang:1.26(-alpine) builder; bind-mount the repo; build/vet/test inside + the container. If a Makefile/dev.sh wrapper helps, add it, but every + compile/test path routes through docker run. +- Go stdlib net/http only (no frameworks); no new deps without need. +- Receivers: /hooks/redmine, /hooks/discourse, /hooks/gitea. + - Verify secrets: Gitea HMAC-SHA256 header; Discourse secret header; + Redmine shared-secret header; reject unsigned/unverified with 401. + - Normalize each into one internal Event struct (source, kind, actor, + subject/topic/PR id, payload digest, received-at). + - Persist events (append-only JSONL + dedup by provider event id) under + a state dir from config; NEVER log secret/header values. + - Event -> action mapping per DESIGN (turn dispatch is a stub call into + the conductor interface for now; wiring comes after skeleton lands). +- Config: webhook secrets via env refs (os.environ/VAR pattern), port + + state dir in harness.toml; sensible defaults. +- Table-driven tests for signature verification + normalization + dedup. +- Run it: brief docker run smoke on a high LAN port (e.g. 4100) with a + curl-equivalent POST (python urllib — curl is banned on host) using a + test secret; show 401 without secret, 200 + JSONL record with. +- Commit incrementally to main; push. REPORT to inbox-pmo lists what + works, what's stubbed, exact container commands used. diff --git a/archive/TASK-20260828-2100-docs-standards.md b/archive/TASK-20260828-2100-docs-standards.md new file mode 100644 index 0000000..f818c88 --- /dev/null +++ b/archive/TASK-20260828-2100-docs-standards.md @@ -0,0 +1,60 @@ +# TASK: Docs standards pass — READMEs and docs/ across ukrrs repos + +## Context +Charles decreed (2026-08-28 ~21:00): ALL project docs must be informative, +well-formatted, beautiful, and useful. Current MOPAC harness README is 16 +skeleton lines with a stale Gitea path (says reachableceo/MOPAC; org moved to +ukrrs/MOPAC). This standard applies to every repo we own from now on. + +## Scope (in priority order) +1. `~/projects/meta/MOPAC/harness/` — README.md full rewrite; docs/ polish. +2. `~/projects/meta/MOPAC/harness/` siblings cloned from Gitea: clone + `https://git.knownelement.com/ukrrs/mopac-keyproxy` and + `https://git.knownelement.com/ukrrs/mopac-bitwarden-go` under + `~/projects/meta/MOPAC/` and upgrade their READMEs (they are spec seeds). +3. `~/projects/siblings/KNEL-AIMiddleware` (if present) — README audit: + fix only what is wrong/stale; do not rewrite working docs wholesale. + +## The doc standard (apply to every README; checklist) +- **Front matter**: H1 title, one-paragraph purpose (what/why/who-for, no + throat-clearing), then a `Status:` line — date + one-word state + (e.g. `Status: 2026-08-28 — skeleton, MVP demo path live`). +- **Quickstart**: copy-paste commands that actually work, each block followed + by a short expected-output snippet. Verify every command by RUNNING it + (e.g. `./bin/harness once --dry-run --demo`, `./bin/harness help`). + Never document an unverified command. +- **Architecture**: a mermaid flowchart of the conductor loop + (intake -> routing -> bounded turn -> REPORT) plus one paragraph of prose. +- **Reference tables** (not prose walls): + - CLI: subcommands, flags, exit codes (0/1/2/4) — from actual help output. + - Config: every `harness.toml` section with one-line meaning (from + harness.toml.example + internal/config). + - Model routing: the `[models]` tier map as a table (class -> model). +- **Status table**: Works / Stubbed / Next — sourced from REPORT.md, kept + honest. +- **Formatting**: fenced code with language tags; headings never skip levels; + consistent terminology (harness, conductor, turn, REPORT capitalized as + shown); no dead links; no TODO markers without an owner; correct repo + URLs (git.knownelement.com/ukrrs/...). +- For keyproxy/bitwarden-go seeds: purpose, scope, non-goals (no admin UI, + memory-only), license (AGPLv3), status line, and what exists today vs + planned — the spec READMEs already carry the design; make them navigable. + +## docs/ polish (MOPAC only) +- Each PORTING-NOTES-*.md gets the Status front-matter line. +- DESIGN.md gets a short table of contents after its intro (section list, + anchor links). Do NOT rewrite or reorder existing sections. + +## Constraints +- DOCS ONLY. No source code changes. No new sections in DESIGN.md. +- This turn runs AFTER pmo-ops and events-receiver complete (dispatch is + gated); the repo working tree will be clean and pushed when you start. +- Verify-then-write: read the code/help/config you are documenting. +- Commit per repo with clear messages; push to ukrrs remotes. + +## Deliverables +- Commits + pushes: MOPAC (README + docs/), mopac-keyproxy, mopac-bitwarden-go, + (KNEL-AIMiddleware if touched). +- `REPORT-20260828-docs-standards.md` in `~/.coordinate/inbox-pmo/`: per-repo + summary of changes, the checklist items applied, any doc rot found that you + could NOT fix in this pass (list for follow-up). diff --git a/archive/TASK-20260828-2130-keyproxy-v0.md b/archive/TASK-20260828-2130-keyproxy-v0.md new file mode 100644 index 0000000..aeef95f --- /dev/null +++ b/archive/TASK-20260828-2130-keyproxy-v0.md @@ -0,0 +1,49 @@ +# TASK: keyproxy v0 — file backend (mpk_ placeholder interface, live) + +## Context +Charles (2026-08-28 ~21:30): the credential gateway is key infrastructure. +This account has NO Bitwarden access, so v0 runs on a FILE backend over +~/.creds-style env files. The `mpk_...` placeholder interface ships NOW; +Bitwarden REST + Vault KV v2/AppRole backends swap in later behind the same +interface (phase 3, already specced in the repo README — read it first). +Consumers only ever see `mpk-` refs; real material crosses the wire +at resolve time, memory-only, never persisted, never logged. + +## Scope +1. Clone `https://git.knownelement.com/ukrrs/mopac-keyproxy` to + `~/projects/meta/MOPAC/keyproxy` (spec README + LICENSE already there). +2. Implement v0 in Go (stdlib only): + - `keyproxy serve` — localhost HTTP (default :8082, configurable): + `POST /v1/resolve` body `{"ref":"mpk-redmine"}` → `{"value":"..."}` + (mask value in any log line as `mpk-redmine=***`). + - Backends behind one interface: + - `file:` — env-file format (KEY=VALUE lines, 0600) at a configured + path; ref maps to file+key. Sources tonight: `~/.creds/redmine.env`, + `/home/_crossfeed/tooling/credentials/litellm-secrets.env`, + `~/.coordinate/secrets/mopac-harness-vkey.env`. + - `env:` — process env indirection. + - `bitwarden:` / `vault:` — stubs returning explicit "not implemented + in v0" errors (interface + tests only, so phase 3 is a drop-in). + - Auth: bearer token for /v1/resolve, bootstrapped from the file backend + itself (a `mpk-keyproxy-self` entry), 0600 file, never logged. + - Config: `keyproxy.toml` — port + ref map (ref → backend/source/key). + `keyproxy.toml.example` tracked; real config gitignored. +3. Rules (non-negotiable, from DESIGN.md keyproxy section): + - No admin UI. No persistence. No key material in logs, errors, or + crash paths (assert redaction in tests). + - Never exec() env files; parse KEY=VALUE in Go. + - Resolution failures name the ref and backend, never the value. + +## Build discipline +- ALL DEV IN DOCKER: digest-pinned golang:1.26 builder container, repo + bind-mounted; Makefile targets (build/vet/test) route through docker run. + No host Go toolchain use. + +## Deliverables +- Commits pushed to ukrrs/mopac-keyproxy main. +- Docs per standard (Charles's decree): README quickstart with verified + copy-paste curl-equivalent (python3 urllib) resolve example, config + reference table, status table. +- Table-driven tests incl. redaction + fake-HTTP end-to-end. +- `REPORT-20260828-2130-keyproxy-v0.md` in `~/.coordinate/inbox-pmo/`: + what runs, how to start it, the ref map seeded, stub status. diff --git a/archive/TASK-20260828-2131-selfhost-core.md b/archive/TASK-20260828-2131-selfhost-core.md new file mode 100644 index 0000000..c0020c4 --- /dev/null +++ b/archive/TASK-20260828-2131-selfhost-core.md @@ -0,0 +1,54 @@ +# TASK: MOPAC self-host core — `harness loop` replaces the bash stack + +## Context +Charles (2026-08-28 ~21:30): "get to be self hosting asap — smallest core +that can bootstrap itself (redmine/gitea interaction fine, discourse later)" +and "moving away from the brittle _crossfeed setup and doorbell PMO<->work +channel". The MVP bar is already met (`harness once --demo` live, REPORT in +reports/). This turn makes the harness drive ITSELF: Redmine is the SoR, +the conductor is the worker, no bash middle layer. + +## What dies (list explicitly in REPORT, do not delete yet) +- `/home/_crossfeed/tooling/agent-stack/semaphore.sh` + slot-file cap + (replaced by in-process concurrency limit) +- `~/.coordinate/scripts/queue-next.sh`, `queue-after.sh`, `pmo-heartbeat.sh` + (replaced by the loop + its own wake discipline) +- dispatch-turn.sh doorbell `screen -S reachableceo-PMO -X stuff` — note: + this doorbell is ALREADY dead (screen is named RCEO-PMO, message silently + dropped). Mention in REPORT; deletion happens after Charles signs off. + +## Scope — smallest self-bootstrapping core +1. `harness loop` subcommand (Go, stdlib only, chainable with `once`): + - Poll Redmine intake (existing `/issues.json` query) on interval + (config, default 120s). `--once` flag = single scan (cron-able). + - For each new/updated task issue not yet processed: run the bounded + turn (existing conductor), then write REPORT back (existing Redmine + note writeback — currently stubbed; implement the note POST) and + update issue status per config map (e.g. In Progress -> Done). + - State: append-only JSONL (one event per dispatch/report), dedup by + issue id + updated_on. No _crossfeed paths, no slot files. + - v0 = SEQUENTIAL turns (one at a time). Concurrency knob can come later. + - Gitea: optional REPORT commit step behind config flag (off by default). + Discourse: out of scope. +2. Credentials: harness.toml `key_ref` gains `mpk:` scheme → POST to + keyproxy `/v1/resolve` (see ukrrs/mopac-keyproxy, built tonight). + `env:` / `file:` refs keep working as fallback, so the loop runs with + or without keyproxy up. Do not hardcode hosts — all in harness.toml. +3. `harness loop` supervises ITS OWN turns; PMO (me) verifies REPORTs and + archives. The loop logs one line per action to stdout + JSONL. + +## Build discipline +- ALL DEV IN DOCKER: digest-pinned golang:1.26 builder, bind-mounted repo, + Makefile build/vet/test through docker run. No host Go toolchain. +- Docs standard applies (README status table + config reference update). +- Tests: loop end-to-end against a fake Redmine HTTP server (pattern + exists in internal/loop tests) — dispatch, dedup, writeback, status map. + +## Deliverables +- Commits + push to ukrrs/MOPAC main. +- `REPORT-20260828-2131-selfhost-core.md` in `~/.coordinate/inbox-pmo/`: + what runs, exact command to start the loop, what it replaces (the die + list), how to roll back (just stop the loop, bash stack still present). + +## Added by PMO (21:30, post docs-turn rot finding) +- Rename Go module path `reachableceo/...` -> `ukrrs.com/mopac/...` or similar (go.mod module line + all internal imports). Mechanical, verify with build+vet+test in the Docker builder. Cited as doc rot in docs-standards REPORT; folding here since it is a source change. diff --git a/archive/TASK-20260828-2300-keyproxy-finish.md b/archive/TASK-20260828-2300-keyproxy-finish.md new file mode 100644 index 0000000..87f7905 --- /dev/null +++ b/archive/TASK-20260828-2300-keyproxy-finish.md @@ -0,0 +1,31 @@ +# TASK: keyproxy-v0 finish — commit, push, report (turn was self-killed) + +## Context +The keyproxy-v0 turn completed the implementation but self-terminated +(SIGTERM via its own broad pkill while restarting the smoke server) right +before committing. Everything sits UNCOMMITTED in the working tree of +`~/projects/meta/MOPAC/keyproxy` (README.md modified; cmd/ internal/ +Makefile dev.sh go.mod keyproxy.toml.example .gitignore untracked). +Its log (per PMO read): tests were green, docker smoke 10/10 PASS, +live 401/200/400 verified, README quickstart verified verbatim. + +## Steps +1. Read the working tree (code + README + example config). Confirm it + matches the v0 TASK spec (file/env backends, bitwarden/vault stubs, + auth, redaction, no persistence, no admin UI). +2. Re-verify: `make test` (or equivalent) inside the digest-pinned + golang:1.26 Docker builder — ALL DEV IN DOCKER, no host toolchain. + Fix anything that fails; keep changes minimal. +3. Commit in logical chunks (backend interface + parsers, config, server, + cmd/CLI, build tooling + docs) with clear messages. +4. Push to `origin main` (ukrrs/mopac-keyproxy; default branch is main — + clone is already on it). +5. NEVER use a broad pkill. If you must stop a server process, kill the + exact PID you spawned (save `$!`), never pattern-match. +6. Seed nothing else; real config/keyproxy.toml stays gitignored. + +## Deliverable +`REPORT-20260828-2130-keyproxy-v0.md` in `~/.coordinate/inbox-pmo/`: +what shipped (endpoints, backends, ref map), test results, the commit +hashes, how to start it (README quickstart already written — just cite), +and anything left for phase 3 (bitwarden/vault stubs status). diff --git a/archive/TASK-20260828-2330-bitwarden-go.md b/archive/TASK-20260828-2330-bitwarden-go.md new file mode 100644 index 0000000..e57b256 --- /dev/null +++ b/archive/TASK-20260828-2330-bitwarden-go.md @@ -0,0 +1,49 @@ +# TASK: mopac-bitwarden-go v0 — Secrets Manager REST client (fake-server tested) + +## Context +keyproxy v0 is LIVE (ukrrs/mopac-keyproxy: file/env backends, `bitwarden:` +backend is a 501 stub awaiting this library). Charles: the credential +gateway is key infrastructure; Bitwarden backend is phase 3. This account +has NO Bitwarden access today — so v0 is built and tested entirely against +a FAKE Bitwarden Secrets Manager REST server in tests; live creds attach +later with zero code change. + +## Inputs to read first +- `~/projects/meta/MOPAC/harness/docs/PORTING-NOTES-secrets.md` — design + sketch + 7 open questions; answer what code can answer, list the rest. +- The spec README already seeded in the repo. +- Public Bitwarden Secrets Manager API docs (machine accounts: client_id + + client_secret -> access token via /identity/connect/token, + client_credentials grant; then /api/accounts/{id}/secrets etc.). + Plain REST only — the official SDK is source-available and + AGPL-incompatible (DESIGN.md), do NOT import it. + +## Scope +1. Clone `https://git.knownelement.com/ukrrs/mopac-bitwarden-go` to + `~/projects/meta/MOPAC/bitwarden-go` (default branch main). +2. Go package + thin CLI (`bitwarden-go`): + - `login` — exchange machine credentials (from env refs or 0600 file, + NEVER flags/args) for an access token; memory-only, refresh before + expiry; never logged. + - `projects` / `secrets list` / `get ` — read paths keyproxy needs. + - Library surface: `Authenticate(ctx, creds) (Token, error)`, + `GetSecret(ctx, token, key) (string, error)` — keyproxy's `bitwarden:` + backend will call exactly this. +3. Fake server in tests: table-driven, covers auth failure, expiry, + missing secret, malformed payloads, redaction assertions (token/secret + values never in logs or errors). +4. Config via env-file (0600) — same KEY=VALUE discipline as keyproxy. + No admin UI, no persistence of tokens to disk. EVER. +5. Docs per the standard: README (purpose, status line, verified + quickstart against the fake server, config table). + +## Build discipline +- ALL DEV IN DOCKER: digest-pinned golang:1.26 builder, repo bind-mounted; + Makefile/dev.sh pattern copied from ukrrs/mopac-keyproxy. +- NEVER broad pkill — kill exact PIDs you spawned. +- Commits in logical chunks, push to origin main. + +## Deliverable +`REPORT-20260828-2330-bitwarden-go.md` in `~/.coordinate/inbox-pmo/`: +library surface, CLI usage, fake-server test results, the open questions +still needing Charles, and the exact keyproxy integration point. diff --git a/archive/TASK-20260829-0000-serve.md b/archive/TASK-20260829-0000-serve.md new file mode 100644 index 0000000..4706b03 --- /dev/null +++ b/archive/TASK-20260829-0000-serve.md @@ -0,0 +1,44 @@ +# TASK: `harness serve` — OpenAI-compatible front door (OWUI is the client) + +## Context +DESIGN.md "OWUI front door" section (hermes killed): OpenWebUI = interactive +front door with Cloudron SSO + admin RBAC we don't build. MOPAC serves +OpenAI-compatible `/v1/chat/completions`; each stack/vertical registers as +a "model" in OWUI. OWUI chat and webhooks (`harness events`, already live) +are two doors into the same conductor loop. + +## Scope +1. `harness serve` subcommand (Go stdlib net/http, same style as events): + - `POST /v1/chat/completions` — OpenAI request/response shapes + (model, messages, temperature, max_tokens; reply = single assistant + message + usage). NON-streaming v0; return an explicit error if + `stream:true` is requested (OWUI tolerates non-streaming providers). + - `GET /v1/models` — lists servable "models" from config (the class -> + tier map is the catalog: each class is exposed as model + `mopac-`). + - Bearer auth (vkey), constant-time; unauthenticated = 401 generic. + - v0 STATELESS: OWUI sends full conversation history each call — run + one bounded conductor turn over the assembled history, return the + final text. No session storage. + - Model routing: request.model (e.g. `mopac-primary`) maps through the + existing `[models]` tier map; unknown model = 400 naming valid ones. +2. Config: `[serve]` section (port, default 8090; `enabled_models` + optional subset). Coexists with `harness events` on its own port. +3. Reuse the conductor turn machinery `once` uses — no copy-paste; if the + turn package needs a small refactor to accept history, do it cleanly. +4. Tool use inside serve turns: v0 = tools OFF (pure chat path), noted in + README as Next. + +## Build discipline +- ALL DEV IN DOCKER (digest-pinned builder, dev.sh gets a `serve` runner). +- Tests: fake OpenAI client against the real server — auth, model map, + history assembly, usage accounting, unknown-model 400. +- Docs standard: README section + config table rows + status table update. +- NEVER broad pkill; kill exact PIDs. +- Commits in logical chunks, push to origin main. + +## Deliverable +`REPORT-20260829-0000-serve.md` in `~/.coordinate/inbox-pmo/`: endpoints, +the model catalog as configured, test results, exact OWUI connection +settings (base URL + vkey) for Charles to plug in, and what's Next +(streaming, tools). diff --git a/inbox-work/TASK-20260829-0500-quota.md b/inbox-work/TASK-20260829-0500-quota.md new file mode 100644 index 0000000..be4175a --- /dev/null +++ b/inbox-work/TASK-20260829-0500-quota.md @@ -0,0 +1,54 @@ +# TASK: Quota monitoring + back-pressure + usage mgmt + resource gate (Redmine 490+491) + +## Context +Charles's spec of record: `docs/SPEC-20260829-charles-brief.md` (READ IT +FIRST, esp. "It's a marathon" + Roadmap 1). Last night the z.ai quota wall +killed two dispatched turns at 19:00 — this system prevents that class of +failure and maximizes the flat-rate plan 24x7 (legacy plan ends 9/22). +Redmine tickets: 490 (quota) + 491 (resource monitor). + +## Scope — `internal/quota` + `harness quota` + loop integration +1. **z.ai usage polling**: research the z.ai coding-plan usage/limits + endpoint (it exists per Charles; find the API surface from public docs + + the z.ai provider behavior we already see). Poll on interval; parse + buckets/limits/reset times; expose `QuotaSnapshot` (struct: per-bucket + used/limit/window-reset). + - Auth via key_ref (never logged, same discipline as keyproxy). + - If the endpoint proves unreachable/undocumented: build against a + documented interface + fake server, flag LIVE VERIFICATION as open. +2. **Central state decision** (Charles leaves it to us): Redis container + vs LiteLLM-native (postgres spend tracking already live at + 192.168.3.78:4001). Decide with rationale in the REPORT; implement the + chosen one. Requirements: multiple harness instances (9 accounts, 2 + hosts) share quota state; redis (if chosen) runs as a docker container + on this host, config in harness.toml, no host packages. +3. **Back-pressure in the loop** (internal/loop): before dispatching a + turn, consult quota snapshot + schedule: + - Peak window (config, default 0100-0500 CST — VERIFY empirically if + possible; Charles sleeps 2300-0500): restrict to flash-tier classes; + defer heavy classes with a logged reason; prefer LLM-lite work. + - Quota nearly exhausted: defer + surface status; never hard-fail the + loop. + - Schedule + thresholds all in `[quota]` harness.toml section (TZ-aware, + CST default). +4. **Resource gate (ticket 491)**: read-only system monitor (load avg, + mem available, disk free, IO delay from /proc + /sys) with + configurable busy thresholds; loop defers dispatch when busy. cgroup + enforcement is deploy-time — document the runbook section only. +5. **Usage accounting**: per vertical/class token+model accounting into + the JSONL state (extend loop state) — feeds the eventual + per-instance Discourse usage reports. +6. TDD red/green: quota parser, schedule logic (peak/offpeak across TZ + edges), back-pressure decisions, resource thresholds — table-driven + + fake clock. Docker dev discipline. Docs standard (README + config + table). Push to origin main. + +## Deliverable +`REPORT-20260829-0500-quota.md` in `~/.coordinate/inbox-pmo/`: the Redis +vs LiteLLM decision + rationale, endpoint findings, config surface, test +results, and how the loop now behaves at quota exhaustion (the 19:00-wall +scenario replayed as a test). + +## ADDENDUM (redispatch, ~05:10 CST) +- Previous attempt died on a sourcegraph.com timeout — do NOT use sourcegraph; research the z.ai usage endpoint via direct fetch of z.ai docs / the API itself. +- BE QUOTA-LEAN (weekly bucket at 93%): minimal exploration, write code in large correct chunks, one test pass, no gold-plating, finish and report. diff --git a/inbox-work/TASK-20260829-0700-dispatcher.md b/inbox-work/TASK-20260829-0700-dispatcher.md new file mode 100644 index 0000000..cd880a1 --- /dev/null +++ b/inbox-work/TASK-20260829-0700-dispatcher.md @@ -0,0 +1,43 @@ +# TASK: Smart dispatcher — phase-aware model selection + queue selection (Redmine 492+493) + +## Context +Spec of record: docs/SPEC-20260829-charles-brief.md (Roadmap 2+3). The +quota/back-pressure turn (internal/quota) lands in the repo BEFORE this +turn starts — build on it, do not re-implement. + +## Part A — Phase-aware model selection (ticket 492) +- Routing v1: work phases get different tiers. Convention: a task's phase + is derived v1 from explicit fields first, fallbacks second: + 1. Redmine custom field (configurable field name, e.g. "phase", + values: plan/implement/review/grind) + 2. harness.toml `[models.phases]` map: phase -> class (plan=primary, + implement=code, review=review, grind=flash-class default) + 3. fallback: existing class map (unchanged behavior). +- The conductor exposes which phase it ran in the REPORT header (model + + phase + class) — telemetry for the eventual auto-classifier (v2, NOT + this turn). +- Unknown phase value = config error naming valid phases. + +## Part B — Queue selection (ticket 493) +- Loop intake query becomes selection-aware: + - Redmine sort: priority desc, due_date asc (config keys in + `[redmine.selection]`); respect relations (issue blocked while + relates/blocks parent open — use /relations or issue relations field; + skip blocked, log reason). + - Quota gate: consult internal/quota snapshot (from the prior turn) — + heavy classes deferred when quota-low or peak-window per its config. + - Resource gate: same pattern for the busy-system gate. + - Pick order = sort order; one turn at a time (v0 concurrency), scan + continues after each. +- Selection decisions land in the loop JSONL (task id, chosen/deferred + + reason) — auditable. + +## Discipline +TDD red/green (fake Redmine server pattern exists; add phase-routing and +blocked-relation cases), ALL DEV IN DOCKER, docs standard (README config +tables updated), push to origin main, NEVER broad pkill. + +## Deliverable +`REPORT-20260829-0700-dispatcher.md` in `~/.coordinate/inbox-pmo/`: +phase map surface, selection rules + defer reasons, test results, how a +priority-5 due-today ticket beats a priority-3 no-due one (test cited). diff --git a/inbox-work/TASK-20260829-0900-deploy.md b/inbox-work/TASK-20260829-0900-deploy.md new file mode 100644 index 0000000..01c1db5 --- /dev/null +++ b/inbox-work/TASK-20260829-0900-deploy.md @@ -0,0 +1,43 @@ +# TASK: Multi-account deploy — packaging + runbook, 9 accounts / 2 hosts (Redmine 494) + +## Context +Spec: docs/SPEC-20260829-charles-brief.md (account list inside). The PMO +runtime CANNOT ssh or sudo — this turn produces everything so that +deployment is a ~10-minute Charles window executing one runbook. + +## Accounts +- ultix-streaming: reachableceo, TSGBOD, TSGCOO, TSGCTO, TSGCCO +- ultix-offstage (ssh reachableceo-offstage, passwordless sudo there): + reachableceo-offstage, COSRCEO-Personal, COSRCEO-Biz, COSWFO + +## Scope +1. `deploy/` directory in the harness repo: + - `deploy/accounts.tsv` — account, host, vertical, redmine project + scope, port assignments (events/serve per account: derive a scheme, + e.g. base port 4100+ / 8090+ offset by account index). + - `deploy/install-account.sh ` — idempotent, runs AS the + target user (no root needed if binary path is ~/.local/bin): creates + ~/.mopac/{bin,state,reports}, installs static binary (built in the + Docker builder for linux/amd64), writes harness.toml from a template + (per-account substitutions), refuses to overwrite existing + harness.toml or secrets. + - `deploy/runbook.md` — the exact Charles sequence: build once + (docker), copy/ssh loop per host+account, run install script, start + command per account (nohup or cron @reboot line — NO systemd claims + without root; document both options), verify (healthz curl-equivalent + + first `loop --once` dry output), rollback (stop + rm ~/.mopac/bin). + - Per-account harness.toml template with vertical/redmine-scope/ports + substituted; secrets stay env refs (0600 env files per account, + bootstrap instructions). +2. Static binary build: Makefile target `release` (docker builder, + GOOS=linux GOARCH=amd64, CGO=0) producing `bin/harness-linux-amd64`. +3. Concurrency guard: distinct state dirs + ports per account so multiple + daemons on one host never collide; document the account-port table. +4. Tests where feasible (template substitution, port derivation, + idempotence of install script in a fake HOME). Docs standard. + +## Deliverable +Commits pushed to ukrrs/MOPAC main (deploy/ + Makefile release target + +README deploy section). `REPORT-20260829-0900-deploy.md` in +`~/.coordinate/inbox-pmo/`: what Charles executes, per-host time estimate, +verification steps, rollback. diff --git a/inbox-work/TASK-20260829-1100-briefing.md b/inbox-work/TASK-20260829-1100-briefing.md new file mode 100644 index 0000000..81a3371 --- /dev/null +++ b/inbox-work/TASK-20260829-1100-briefing.md @@ -0,0 +1,39 @@ +# TASK: COS morning briefing — discourse client + `harness brief` (Redmine 495) + +## Context +Spec: docs/SPEC-20260829-charles-brief.md. Target: first live 3-COS +briefing 2026-09-01 06:30 CST (beta 8/31 0600). Briefing = daily digest +via Discourse (fallback Redmine note) assembled from: Redmine activity +(scope), Gitea PR inbox, quota usage. Exact format TBD by Charles — ship +a clean v0 format he can react to. + +## Part A — mopac-discourse-go client (new repo) +1. Create repo via Gitea API if absent (PMO pattern: tea token from + ~/.config/tea/config.yml, POST /api/v1/orgs/ukrrs/repos), clone to + ~/projects/meta/MOPAC/discourse-go. AGPLv3 LICENSE. +2. Stdlib-only Go client: categories (list/create), topics (create/list), + posts (create/update), raw JSON passthrough for anything else. API key + via key_ref/env (never logged). Fake-server tests. Docs standard. + (Current Discourse key 403s on category creation — client supports it, + live verification may wait for the admin-scoped key; note in REPORT.) + +## Part B — `harness brief` subcommand +1. Digest window: since last briefing marker (state/brief/brief.jsonl). +2. Sources (read-only): Redmine issues in scope created/updated/closed in + window (reuse intake client); Gitea open PRs across ukrrs repos (API); + quota snapshot summary (internal/quota if landed). +3. Render: clean markdown — per-vertical sections: shipped (closed), + moved (updated w/ status), incoming (new), PRs awaiting review, quota + line. Beautiful tables (docs standard). +4. Delivery v0: post to Discourse category (configurable id/slug); if + Discourse fails (403 etc.), fallback = journal note on a configured + Redmine "briefing" issue — NEVER silently drop; log delivery path. +5. Scheduling v0: `harness brief` is one-shot (cron/loop calls it at + 0630); config `[brief]` section (category, redmine fallback issue, + window tz CST). Idempotent: one briefing per window (marker). +6. TDD (fake servers for both), docker dev, docs, push. + +## Deliverable +`REPORT-20260829-1100-briefing.md` in `~/.coordinate/inbox-pmo/`: client +surface, the v0 briefing format (render a sample with fake data in the +REPORT), delivery verification status, what Charles should tweak. diff --git a/inbox-work/TASK-20260829-1300-pdf.md b/inbox-work/TASK-20260829-1300-pdf.md new file mode 100644 index 0000000..142c366 --- /dev/null +++ b/inbox-work/TASK-20260829-1300-pdf.md @@ -0,0 +1,48 @@ +# TASK: mopac-pdf v0 — beautiful PDF generation pipeline (Redmine 499) + +## Context +Spec docs/SPEC-20260829-charles-brief.md "Workflows": beautifully +formatted PDFs of budgets, travel itineraries, project plans, proposals, +consulting reports, multi-year financial plans, business plans, +engineering docs — with charts/diagrams. This feeds the COS briefing +world (reports) and TSYS client deliverables. + +## Constraints +- OUR code: Go only (no python/node/rust — prebuilt engine BINARIES in + digest-pinned docker images are tooling, fine). +- ALL DEV IN DOCKER. AGPLv3. Docs standard. + +## Scope +1. New repo `ukrrs/mopac-pdf` (create via Gitea API if absent — PMO + pattern, tea token; clone to ~/projects/meta/MOPAC/pdf). LICENSE + AGPLv3 first commit. +2. Engine decision (document rationale in REPORT): typst (docker image, + typst compile) vs pandoc+latex — evaluate compile speed, chart/table + quality, template ergonomics. Typst expected to win on modern output. +3. Go package + CLI `mopac-pdf`: + - Input: markdown (the composable currency of this whole stack — + Redmine notes, Discourse posts, briefing output) + front-matter + (title, subtitle, date, classification footer, template name). + - Templates dir: 2 shipped v0 — "report" (clean business report: + title page, TOC, headers/footers, page numbers) and "brief" + (1-3 page exec summary, dense tables). + - Charts: pure-Go rendering option (go-chart or similar permissive + lib) embedded as images from fenced code blocks with data blocks; + keep v0 minimal — one chart type proven end-to-end. + - Output: PDF bytes to stdout or -o file; exit codes like harness + (0/1/2 usage/engine). +4. Docker runner: `mopac-pdf` shells the engine container (digest- + pinned); host stays toolchain-free. Makefile/dev.sh per family + pattern. +5. Tests: template rendering golden tests (fixture .md -> PDF, assert + non-empty + page count via a tiny Go PDF header parse or engine + stdout), CLI flags, front-matter parsing. NEVER broad pkill. + +## Deliverable +Commits pushed to ukrrs/mopac-pdf main. +`REPORT-20260829-1300-pdf.md` in `~/.coordinate/inbox-pmo/`: engine +decision + rationale, sample outputs listed (paths), how the briefing +pipeline will call it, what's Next (chart types, more templates). + +> CLARIFIED (Charles): supply chain MAY include prebuilt Rust tools (typst +> etc.) in digest-pinned images. OUR code: Go/PHP/C/Java only.