pmo: 07:02 heartbeat — q14 report imminent, q15 mid-B4, q16 gated, no flakes

This commit is contained in:
2026-08-29 07:04:03 -05:00
parent d4ed0b6132
commit 54751ae623
8 changed files with 181 additions and 13 deletions
@@ -0,0 +1,125 @@
# REPORT — mopac-redmine-go (mred): Redmine Go CLI delivered
Task: TASK-20260829-1400-redmine-cli (Redmine #506, top priority by
direct order 2026-08-29)
Delivered: 2026-08-29 12:05
Repo: https://git.knownelement.com/ukrrs/mopac-redmine-go (public, AGPLv3)
Clone: ~/projects/meta/MOPAC/redmine-go (origin main, 7 commits)
Status: **v0 complete, green, pushed. Ready for PMO cutover on acceptance.**
## What landed
Stdlib-only Go module `git.knownelement.com/ukrrs/mopac-redmine-go`:
library package `redmine` (Client + sentinel errors, usable by the
harness directly — no copy-paste surface), CLI `mred`
(`internal/cli` + `cmd/mred`), stateful fake Redmine
(`internal/fakeredmine`), 0600-disciplined config loader
(`internal/config`). ALL dev work ran inside the digest-pinned
`golang:1.26-bookworm` builder via `./dev.sh`/`Makefile` (host never
runs a toolchain). TDD red/green throughout; atomic conventional
commits pushed to origin main.
| Commit | Content |
|---|---|
| `4b9aa5f` | Seed: AGPLv3 LICENSE (first commit) |
| `c3b86b1` | Dev harness (digest-pinned Docker builder) |
| `a3c83bd` | Config: MRED_* env + 0600 env-file loading |
| `20dbcf6` | Redmine REST client + fake-server test suite |
| `d98f0ae` | mred CLI (flags accepted after positionals) |
| `bbd85fa` | End-to-end smoke vs containerized fake |
| `4ad9f6f` | README to docs standard |
## Command surface
| Command | Effect | Key flags |
|---|---|---|
| `mred issue list -p MOPAC` | list issues (open by default) | `--status open\|all\|closed\|NAME`, `--version NAME`, `--limit N`, `-o json` |
| `mred issue show ID` | full issue + description | `--with journals` |
| `mred issue create -p PROJ -s SUBJECT` | create; returns new issue | `--desc FILE\|-`, `--tracker`, `--priority`, `--category`, `--version`, `--due DATE`, `--parent ID`, `--est HOURS`, `--note TEXT` |
| `mred issue update ID` | partial update (only set fields sent) | `--status`, `--priority`, `--category`, `--version`, `--due`, `--done-ratio`, `--desc FILE\|-`, `--note TEXT` (= journal) |
| `mred version list -p PROJ` / `version create -p PROJ -n NAME` | milestones | `--due DATE`, `--status open\|closed` (sharing=descendants) |
| `mred category list -p PROJ` / `category create -p PROJ -n NAME` | categories | |
| `mred relation create FROM TO --type blocks\|relates` | issue relations | |
Names resolve case-insensitively against the server's own enumerations
(`--tracker feature` -> id 2, `--priority immediate` -> id 5,
`--status done`, `--category`/`--version` by name; no id lookups in
PMO scripts ever again). Flags parse before OR after positionals.
Exit codes: 0 ok, 1 usage/config, 2 API error — stderr is one
parseable line: `mred: redmine: not found: http 404`.
## Test results
- Unit suite: **62 passing tests** (34 functions + 28 table/subtests)
across library round-trips, list filters, notes-as-journals,
relations, versions, categories, name resolution, error mapping
(401/403/404/422/5xx -> sentinels; unreachable; malformed),
config 0600 discipline.
- Key redaction: the fake DELIBERATELY echoes the presented API key in
every error body; `TestKeyNeverLeaks` (library) + the smoke redaction
pass (CLI) prove the key appears nowhere except the
`X-Redmine-API-Key` header.
- End-to-end smoke: **smoke: OK** — builds in the builder, boots the
fake in a container on 127.0.0.1:8601, drives the real binary through
a 0600 env file: version/category create+list, issue create with full
flags + desc from stdin (`-o json`), list + filters, update with
status/done-ratio/note then show-with-journals, relation create,
404/401/loose-file failure paths with exact exit codes, redaction
sweep. Only the exact container ID is ever removed; no broad pkill.
- `./dev.sh check` (build+vet+test) green at HEAD.
- **Live tracker verified (read-only)** 2026-08-29: with
`~/.creds/redmine.env` key-renamed to a 0600 `--config` file,
`mred issue list -p MOPAC --limit 3` returned real issues (522/521/519,
text and `-o json`, exit 0) from projects.knownelement.com — zero code
change from the fake-tested path. Live WRITES await PMO acceptance
(step 3 below).
## PMO replacement mappings (python ops today -> mred one-liners)
Setup once (then every row below is one command):
```sh
sudo install -m 755 ~/projects/meta/MOPAC/redmine-go/bin/mred /usr/local/bin/mred
install -d -m 700 ~/.config/mred
sed -e 's/^REDMINE_URL=/MRED_URL=/' -e 's/^REDMINE_API_KEY=/MRED_KEY=/' \
~/.creds/redmine.env > ~/.config/mred/env && chmod 600 ~/.config/mred/env
mred issue list -p MOPAC --limit 5 # live verify vs projects.knownelement.com
```
| Python op today (file) | mred one-liner |
|---|---|
| Create roadmap version: `POST /projects/MOPAC/versions.json {"name","due_date","status","sharing":"descendants"}` (roadmap-buildout.py §1) | `mred version create -p MOPAC -n "Beta" --due 2026-08-31 --status open` |
| List versions to map name->id (roadmap-buildout.py §1 fallback) | `mred version list -p MOPAC -o json` (name->id lookup no longer needed; `--version` takes the NAME) |
| Create category (roadmap-buildout.py §2) | `mred category create -p MOPAC -n "Quota & Backpressure"` |
| List categories (roadmap-buildout.py §2 fallback) | `mred category list -p MOPAC -o json` |
| Update issue category+version+estimate: `PUT /issues/490.json {"category_id","fixed_version_id","estimated_hours"}` (roadmap-buildout.py §3) | `mred issue update 490 --category "Quota & Backpressure" --version Beta --est 8` |
| Relation: `POST /issues/490/relations.json {"issue_to_id":492,"relation_type":"blocks"}` (roadmap-buildout.py §4) | `mred relation create 490 492 --type blocks` |
| Create child issue with tracker/priority/category/version/est/desc (roadmap-buildout.py §5) | `mred issue create -p MOPAC -s "Google suite MCP/CLI" --tracker task --priority normal --category Integrations --version "Phase 3 - Integrations" --est 8 --parent 498 --desc -` (desc via heredoc/file) |
| Fetch issue 517, rewrite description at marker, PUT desc + note (amend-517.py) | `mred issue show 517 -o json \| jq -r '.issue.description' \| { editor pipeline } > /tmp/d.md && mred issue update 517 --desc /tmp/d.md --note "Hard requirement added: ..."` |
| Append-desc + note to 497 (finalize-497.py §1) | `mred issue show 497 -o json \| jq -r '.issue.description' \| head ... > /tmp/d.md && mred issue update 497 --desc /tmp/d.md --note "FINAL model: ..."` |
| Cross-issue note (finalize-497.py §2, note on 517) | `mred issue update 517 --note "Clarification per #497 final: ..."` |
| Read category/version maps then create identity ticket (file-identity-ticket.py) | `mred issue create -p MOPAC -s "Two-level identity model: Linux account (runtime) + Cloudron accounts (acting)" --tracker feature --priority urgent --category Infrastructure --version Production --est 5 --desc identity-desc.md` |
| Generic list/read the PMO does inline (status sweeps) | `mred issue list -p MOPAC --status all -o json` / `mred issue show ID --with journals -o json` |
Notes for the cutover:
- `--note` is idempotent-safe to skip: omit it and no journal entry is
written. Notes are one-per-call (Redmine journals are append-only).
- Text output is stable/grep-able (`ID STATUS SUBJECT @Version`);
`-o json` is the machine contract (`{"issues":[...]}` etc.).
- No python process, no urllib timeout handling, no key material in
scripts: the key sits in a 0600 env file mred refuses to read looser.
## Acceptance checklist (PMO)
1. Run the setup block above; `mred issue list -p MOPAC --limit 5`
returns live issues (exit 0). (Pre-verified 2026-08-29: this exact
command returned 522/521/519 from the live tracker.)
2. Spot-map one recurring op (e.g. the finalize-497 note pattern) with
`mred issue update <id> --note "cutover test"` and verify the journal
in the web UI.
3. On pass: python glue in ~/.coordinate/scripts is retired for Redmine
writes; comment/close #506 via `mred issue update 506 --status done
--done-ratio 100 --note "Delivered: ukrrs/mopac-redmine-go v0"`.
Zero downtime: python glue keeps working until step 3 — mred adds no
server-side change; both clients can run side by side indefinitely.