diff --git a/inbox-pmo/REPORT-20260829-1400-redmine-cli.md b/inbox-pmo/REPORT-20260829-1400-redmine-cli.md new file mode 100644 index 0000000..69f6f0d --- /dev/null +++ b/inbox-pmo/REPORT-20260829-1400-redmine-cli.md @@ -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 --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. diff --git a/log.md b/log.md index 613283c..623eade 100644 --- a/log.md +++ b/log.md @@ -89,3 +89,4 @@ 2026-08-29T06:21:06-05:00 | PMO | identity model FINAL: 1:1 linux->BW vault (cloudron+3rd-party entries); acting identity selected per task/turn, stamped on all actions + usage 2026-08-29T06:26:47-05:00 | PMO | ALPHA LIVE: loop autonomous (520 done New->Resolved+note, 519 running); q12 killed; q14 CLI keeps running; tool-widening ticket filed Immediate 2026-08-29T06:29:44-05:00 | PMO | bootstrap turn dispatched (q15: container+tool-widening, FINAL crush turn); ping-charles.sh ready (needs SMTP creds tonight); loop alpha continues autonomous +2026-08-29T07:04:03-05:00 | PMO | hb 07:02 chain check: no REPORTs ready — q14 README done 07:03, REPORT imminent (wake-q14 fuse armed 15m); q15 mid-B4, loop.test running 10m-timeout; q16 correctly gated on q15 screen; loop idle-scan; no flakes; chain full — nothing to dispatch diff --git a/logs/heartbeat.log b/logs/heartbeat.log index 049db63..bc3763c 100644 --- a/logs/heartbeat.log +++ b/logs/heartbeat.log @@ -15,3 +15,4 @@ 2026-08-29T04:55:19-05:00 wake sent (pending=4 active=1) 2026-08-29T05:02:15-05:00 wake sent (pending=4 active=1) 2026-08-29T06:02:15-05:00 wake sent (pending=4 active=1) +2026-08-29T07:02:15-05:00 wake sent (pending=5 active=1) diff --git a/logs/loop-alpha.log b/logs/loop-alpha.log index 8c93038..a71cafb 100644 --- a/logs/loop-alpha.log +++ b/logs/loop-alpha.log @@ -40,3 +40,33 @@ harness: loop: status New -> Resolved on #518 harness: loop: noted REPORT on #518 harness: intake: 1 task(s) in redmine scope harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated +harness: intake: 1 task(s) in redmine scope +harness: loop: scan: 1 task(s) in scope, 0 new/updated diff --git a/logs/q-bootstrap.log b/logs/q-bootstrap.log index e69de29..a882cb5 100644 --- a/logs/q-bootstrap.log +++ b/logs/q-bootstrap.log @@ -0,0 +1 @@ +Now Part B1 — file tools. Checking the llm tool types:Writing the file tools test first (TDD red):Now the implementation:File tools green. Now wiring config `[tools.files]` + `[loop] turn_timeout_secs` (TDD):Now the config implementation:Now the conductor wiring. Checking writeback render for empty content handling:Writing loop tests for file tools, timeout, and e2e turn (TDD red):Now wire the conductor (loop.go):Now runTurn timeout handling + execTool dispatch:All three failures are the expected red. Fixing: unknown-tool counts as denial, double-%w wrapping, and extending the shipped allow-list:Now extend the shipped allow-list in both example and live config:All green. Now B4 — end-to-end fake-Redmine turn test + status-file heartbeat tests:Now implement the heartbeat in daemon.go:Now wire hb into the Conductor struct and daemon flow: \ No newline at end of file diff --git a/logs/q-redmine-cli.log b/logs/q-redmine-cli.log index 858d932..b6a952a 100644 --- a/logs/q-redmine-cli.log +++ b/logs/q-redmine-cli.log @@ -1 +1,6 @@ -I have full context: sibling pattern (mopac-bitwarden-go), harness fake-server test style, PMO python glue to replace, creds at `~/.creds/redmine.env`, Redmine at projects.knownelement.com. Setting up the work plan.Toolchain verified. Creating the repo via Gitea API (idempotent check first):Repo created. Cloning and seeding with LICENSE:Harness done. Now TDD config package — tests first (red):Red confirmed. Implementation:Green. Two atomic commits:Now the core piece. First the stateful fake Redmine server (test infrastructure, harness-style):Now the library test suite (red first):Red confirmed (library absent). Fixing fake's query type, then writing the library:All three failures are test-side: fake returns issues in map order, `*` is URL-encoded, and one assertion is inverted. Fixing:Now sort fake issue listings for determinism: \ No newline at end of file +I have full context: sibling pattern (mopac-bitwarden-go), harness fake-server test style, PMO python glue to replace, creds at `~/.creds/redmine.env`, Redmine at projects.knownelement.com. Setting up the work plan.Toolchain verified. Creating the repo via Gitea API (idempotent check first):Repo created. Cloning and seeding with LICENSE:Harness done. Now TDD config package — tests first (red):Red confirmed. Implementation:Green. Two atomic commits:Now the core piece. First the stateful fake Redmine server (test infrastructure, harness-style):Now the library test suite (red first):Red confirmed (library absent). Fixing fake's query type, then writing the library:All three failures are test-side: fake returns issues in map order, `*` is URL-encoded, and one assertion is inverted. Fixing:Now sort fake issue listings for determinism:All green. Committing the library:Library committed. Now the CLI — tests first:Red. Implementing the CLI:Fixing the update-path project resolution (numeric project id, no MOPAC hardcode, single fetch):Now the main entrypoint and run tests:Only relation failed — but flag stops at positionals, so update should have tripped too. Investigating before patching:All four failures share one root cause: flags after positionals. Checking the list-test detail first:Confirmed. Adding a loose arg parser that accepts flags anywhere (task surface requires `update ID --note`, `relation create FROM TO --type`):Now switch all subcommands to `parseArgs`:Guarding the lone `-` (stdin convention) against an infinite loop in parseArgs:Found it: the fake's issue list returns stored write-shape issues instead of the read projection. Fixing:My projection edits never landed (stale content mismatch). Applying against current text:All green. Committing CLI:Smoke pattern clear. Adding a fixed-address listener to the fake, the smoke server main, and the smoke script:Three fixes before running: hoist leading `--config` in Run, clean the readiness probe, and extract ID2 by subject:Smoke failed at issue create. Reproducing manually to see the error:Fake is up (transient reset). Retrying the failing steps manually:Found it: the smoke's create command is missing `-o json`, so the `"id":` grep fails silently. Fixing:Smoke fully green. Committing, then README per docs standard:Verifying Redmine 506 reference (read-only) before committing:506 confirmed as this task. Adding env.example, final check, push:All pushed. Now the PMO report with exact replacement mappings:Verifying the report's cutover commands live (read-only): sed transform + real `issue list`:Live read verified against the real tracker (issues 522/521/519, exit 0). Updating docs to record it:Delivered and pushed (8 commits, `ukrrs/mopac-redmine-go` @ `788f6fe`): + +- **Library + CLI**: `redmine.Client` (issues/notes-journals, versions, categories, relations, name→id resolution) and `mred` with the full specced surface, `-o json`, flags-after-positionals, exit 0/1/2 with one-line `http NNN` stderr. +- **Tests**: 62 unit tests green (round-trips, filters, error mapping, key-redaction — the fake echoes the key in error bodies to prove nothing leaks) + end-to-end smoke `OK` against the containerized fake; all builds via the digest-pinned Docker builder. +- **Live-verified read-only**: `mred issue list -p MOPAC` against projects.knownelement.com returned 522/521/519, exit 0, zero code change. Live writes left to PMO acceptance. +- **Report**: `~/.coordinate/inbox-pmo/REPORT-20260829-1400-redmine-cli.md` — command surface, test results, and row-per-operation python→mred mappings for all four glue scripts, plus the one-time setup block and cutover checklist (#506 closes via `mred issue update 506 --status done --done-ratio 100 --note ...`). diff --git a/outbox/ping-queue.txt b/outbox/ping-queue.txt index 10875b3..b495dcc 100644 --- a/outbox/ping-queue.txt +++ b/outbox/ping-queue.txt @@ -1,2 +1,3 @@ [info 08-29 06:29 MOPAC] alpha loop live, bootstrap turn dispatched, CLI building [info 08-29 06:33 MOPAC] relay cascade live: postfix first, cloudron fallback, queue last +[info 08-29 06:34 MOPAC] cascade test: expect queue until postfix up diff --git a/scripts/ping-charles.sh b/scripts/ping-charles.sh index 5d9f142..a539ea1 100755 --- a/scripts/ping-charles.sh +++ b/scripts/ping-charles.sh @@ -17,21 +17,25 @@ SEV="${2:-info}" STAMP="$(date '+%m-%d %H:%M')" LINE="[$SEV $STAMP MOPAC] $MSG" -if [ ! -f "$CONF" ]; then - mkdir -p "$(dirname "$QUEUE")" - echo "$LINE" >> "$QUEUE" - echo "ping queued (no relay config): $LINE" >&2 - exit 0 -fi -set -a; . "$CONF"; set +a +# Relay cascade (Charles 2026-08-29: local postfix relay presumed; failing +# that, authenticated Cloudron submission if configured; else queue). +FROM_ADDR="mopac@ultix-streaming.local" +if [ -f "$CONF" ]; then set -a; . "$CONF"; set +a; fi send_one() { printf 'From: %s\nTo: %s\nSubject: MOPAC %s\nContent-Type: text/plain; charset=UTF-8\n\n%s\n' \ - "$PING_FROM" "$DEST" "$SEV" "$1" \ - | curl -sS --max-time 30 --ssl-reqd --url "smtp://$PING_SMTP_HOST" \ - --mail-from "$PING_FROM" --mail-rcpt "$DEST" \ - -u "$PING_SMTP_USER:$PING_SMTP_PASS" \ - -T - && return 0 || return 1 + "$FROM_ADDR" "$DEST" "$SEV" "$1" \ + | curl -sS --max-time 15 --url "smtp://localhost:25" \ + --mail-from "$FROM_ADDR" --mail-rcpt "$DEST" -T - 2>/dev/null && return 0 + if [ -n "${PING_SMTP_HOST:-}" ] && [ "${PING_SMTP_USER:-}" != "fill-me@knownelement.com" ]; then + printf 'From: %s\nTo: %s\nSubject: MOPAC %s\nContent-Type: text/plain; charset=UTF-8\n\n%s\n' \ + "$PING_FROM" "$DEST" "$SEV" "$1" \ + | curl -sS --max-time 30 --ssl-reqd --url "smtp://$PING_SMTP_HOST" \ + --mail-from "$PING_FROM" --mail-rcpt "$DEST" \ + -u "$PING_SMTP_USER:$PING_SMTP_PASS" \ + -T - 2>/dev/null && return 0 + fi + return 1 } if [ -f "$QUEUE" ] && [ -s "$QUEUE" ]; then