pmo: 07:02 heartbeat — q14 report imminent, q15 mid-B4, q16 gated, no flakes
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
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 ...`).
|
||||
|
||||
@@ -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
|
||||
|
||||
+16
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user