pmo: FINAL — self-hosted teardown; loop sole worker; 0900-ready

This commit is contained in:
2026-08-29 18:44:19 -05:00
parent 7debb52c97
commit 74ca1ed4a2
13 changed files with 469 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
# REPORT — mopac-gitea-go (mgit): Gitea Go client + CLI delivered
Task: TASK-20260829-1600 — Gitea Go client (Redmine #508)
Delivered: 2026-08-29 16:00 turn
Repo: https://git.knownelement.com/ukrrs/mopac-gitea-go (created via the
Gitea API with the tea token; public, AGPLv3)
Clone: ~/projects/meta/MOPAC/gitea-go (origin main, 9 commits, pushed)
Status: **v0 complete, green, pushed. Ready for harness PR-flow cutover.**
No Redmine writes in this turn (PMO owns the ticket, per directive).
## What landed
Stdlib-only Go module `git.knownelement.com/ukrrs/mopac-gitea-go`:
library package `gitea` (Client + sentinel errors — importable by the
harness for the turn PR flow, no shell parsing), CLI `mgit`
(`internal/cli` + `cmd/mgit`), stateful fake Gitea
(`internal/fakegitea`), 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 at every layer; atomic conventional
commits pushed to origin main.
| Commit | Content |
|---|---|
| `1e47f5b` | Seed: AGPLv3 LICENSE (first commit) |
| `eb49c62` | Dev harness (digest-pinned Docker builder) |
| `43bc581` | Config: GITEA_* env + 0600 env-file loading |
| `7c11987` | gitea client core, fake server, repo endpoints |
| `c251cd6` | Branch list + commit-status endpoints |
| `5b35e8c` | Pull-request create/list/get/merge endpoints |
| `9b08589` | mgit CLI (flags accepted after positionals) |
| `5feac3c` | End-to-end smoke vs containerized fake |
| `55dbc9a` | README to docs standard |
## Command surface
| Command | Effect | Key flags |
|---|---|---|
| `mgit repo create -n NAME` | create repo (own account or org) | `--owner ORG`, `--desc`, `--private`, `--auto-init`, `--default-branch` |
| `mgit repo list` | list repos | `--owner OWNER` |
| `mgit repo show OWNER/REPO` | one repo | |
| `mgit branch list OWNER/REPO` | branches + tip shas | |
| `mgit status create OWNER/REPO REF` | report commit status | `--state success\|pending\|error\|failure`, `--context`, `--description`, `--target-url` |
| `mgit status list OWNER/REPO REF` | combined + latest per check | |
| `mgit pr create OWNER/REPO` | open PR (head may be `owner:branch`) | `--title`, `--body FILE\|-`, `--base`, `--head` |
| `mgit pr list OWNER/REPO` | list PRs | `--state open\|closed\|all` |
| `mgit pr show OWNER/REPO NUMBER` | one PR with body | |
| `mgit pr merge OWNER/REPO NUMBER` | merge open PR | `--do merge\|rebase\|rebase-merge\|squash\|fast-forward` |
Auth is token-scoped via env `GITEA_URL`/`GITEA_KEY` (or a 0600
`--config` env file; `MGIT_CONFIG`/`~/.config/mgit/env` also honored).
`-o json` on every command. Exit codes 0/1/2; API errors are one
parseable stderr line (`mgit: gitea: not found: http 404`). Flags parse
before OR after positionals.
## Test results
- Unit suite: **71 passing tests** (39 functions + 32 table-driven
subtests), 0 failures/skips, across three packages (library
round-trips against the fake, error mapping 401/403/404/409/422/5xx
-> sentinels with "http NNN", unreachable, malformed, redaction,
config 0600 discipline, CLI surface incl. usage tables and stdin
body).
- The fake Gitea implements real semantics the tests pin:
latest-status-per-context folding, worst-of combined status,
short-sha (>= 7 char) resolution, merge-style validation, 409 on
duplicate open PRs and re-merges, branch-advance on merge.
- Token redaction: the fake DELIBERATELY echoes the presented token in
every error body; library test + CLI test + the smoke redaction pass
prove the token appears nowhere else (it lives only in the
`Authorization: token` header).
## Smoke (end-to-end, `./dev.sh smoke`)
Builds mgit + fakegitea in the digest-pinned builder, boots the fake in
a container on 127.0.0.1:8602, drives the real binary from the host
through a 0600 env file: repo create (private, auto-init) -> list ->
list -o json (parsed) -> show; branch list; status pending -> success
flip -> combined "success (1 checks)"; PR create (body from file) ->
list -> show -> merge squash -> closed list still shows it, open list
empty; failure paths: missing repo (exit 2, one-line `http 404`
stderr), wrong token (`http 401`, no token material), loose env file
(exit 1, "insecure mode"); final redaction scan over every captured
out/err. Result: **smoke: OK**. Only the exact container ID the script
spawns is ever removed.
## Notes for the harness
- PR flow for turns = `branch list` -> `status create` on head sha ->
`pr create` -> `pr merge`, one static binary, JSON everywhere.
- One bootstrapping fix during the turn: the smoke fake initially used
per-boot `go run` (cold compile inside the container raced the
readiness window); it now builds `bin/fakegitea` alongside the CLI in
the same builder step and boots the compiled binary.
- A stale `go run ./smoke/fakeserver` debug container (harness repo's,
not this repo's) was removed by exact ID match during cleanup; no
broad process or container operations were used at any point.
+105
View File
@@ -0,0 +1,105 @@
# REPORT — 2026-08-29 17:00 — Crush: mopac-imap-go (Redmine 501)
- task: new repo `ukrrs/mopac-imap-go` — stdlib-only Go IMAP client
(CLI + library): RFC 2177 IDLE watch, fetch+parse (headers, text
body, attachments to spool), mark seen, SMTP passthrough send.
Env `IMAP_HOST/IMAP_USER/IMAP_PASS`. Fake-server tests. Feeds the
fleet's email-driven intake.
- result: DONE — 12 atomic conventional commits pushed to
`origin/main` (https://git.knownelement.com/ukrrs/mopac-imap-go,
private, UKRRS org), HEAD `66a9135`, branch in sync, tests green,
smoke PASS. NO Redmine writes performed.
## 1. What shipped
Repo created via `tea` (UKRRS/mopac-imap-go, private). Sibling-layout
to `mopac-redmine-go` at `~/projects/meta/MOPAC/mopac-imap-go`:
`Makefile``dev.sh` → digest-pinned `golang:1.26-bookworm` builder
(`golang@sha256:e8c859f5…`) — every build/vet/test ran in Docker; the
host never ran a Go toolchain.
First commit ships full AGPLv3 LICENSE (661 lines, verbatim from the
sibling corpus). Module: `git.knownelement.com/ukrrs/mopac-imap-go`,
go 1.26, zero third-party dependencies.
### Library packages
- `imap/` — IMAP4rev1 client: implicit-TLS or plain dial with greeting
validation; quoted-string LOGIN (password only ever inside the
command, never in errors); SELECT parsing
EXISTS/RECENT/UNSEEN/UIDVALIDITY; FETCH `(UID FLAGS BODY.PEEK[])`
with a literal-inlining wire reader (payloads containing `{N}\r\n`-
looking bytes parse correctly — pinned by test); STORE
`+FLAGS.SILENT (\Seen)`; RFC 2177 IDLE with typed events
(exists/recent/expunge/tick/other), tick-driven re-idle, and a Stop
that consumes the tagged DONE completion so the stream stays clean
(a subtle readLoop/select race was found by `-race`-adjacent flake
hunting and fixed: after stop, readLoop keeps reading until the
tagged line). Sentinel errors: unreachable/protocol/auth/mailbox/
timeout, one-line messages.
- `mail/` — RFC 5322 + MIME parse: RFC 2047 header decoding, QP/base64
transfer decoding, recursive multipart (mixed/alternative nesting),
attachments via Content-Disposition filename or Content-Type name.
Non-UTF-8 charsets pass through raw (no x/text in this repo).
`Spool()` writes `SPOOL_DIR/<sha256-12>/` containing `raw.eml`,
`meta.json` (seq, ids, decoded headers, attachment index) and
`attachments/<filename>` — content-hashed dirs make re-spooling
idempotent across seq renumbering; hostile filenames reduced to
basename (escape attempt pinned by test); 0700 dirs / 0600 files.
- `mailer/` — SMTP passthrough: raw bytes on the wire unmodified;
STARTTLS when offered, AUTH PLAIN when credentials set; sentinels
unreachable/auth/rejected.
### CLI (`mimap`) + config
- `internal/config`: `IMAP_HOST/IMAP_USER/IMAP_PASS` required (env
wins over an optional 0600 `KEY=VALUE` env file, looser-than-0600
refused before read); defaults 993/TLS/INBOX, `SPOOL_DIR=spool`,
SMTP set optional with `SMTP_FROM` falling back to the IMAP user.
Errors name keys, never values.
- `internal/cli`: `watch` (backlog fetch → IDLE; per message: spool →
JSON line on stdout → mark `\Seen`, so a crash re-delivers
idempotently instead of dropping; IDLE stopped before any FETCH;
ticks re-SELECT so a missed EXISTS self-heals; SIGINT/SIGTERM →
clean LOGOUT, exit 0), `check` (mailbox state as JSON), `send`
(`--to` repeatable, `--file PATH|-` → SMTP passthrough). Exit
codes 0/1/2 (ok/usage/protocol) matching the mred convention.
## 2. Verification
- TDD throughout: each feature commit landed test-first against
protocol-correct fakes (`internal/fakeimap` self-test pins every
wire exchange incl. literal FETCH bodies and IDLE push;
`internal/fakesmtp` pins AUTH/530/dot-unstuffing).
- `./dev.sh check` (build+vet+test) green; `go test -race -count=3
./...` green after fixing two real concurrency findings (DONE-
completion race in idle.go; unsynchronized test buffer); imap
package hammered `-count=10`.
- `./dev.sh smoke`: builds the real `bin/mimap` in the pinned builder
and drives it across process boundaries against the fakes —
`check` reports backlog, `send` relays, `watch` produces backlog +
live-IDLE-push JSON lines, 2 spool dirs, 2×`\Seen`, exit 0 on
SIGINT. Output: `smoke: PASS`.
## 3. Commits (all on origin/main)
66a9135 docs: README with CLI semantics, spool/intake contract, layout
019be0d test: end-to-end smoke of the real binary against fakes
ef2aec2 feat(cli): mimap watch/check/send over the library
d0a38b5 feat(config): env + 0600 env-file connection settings
4f5a43a feat(mailer): SMTP passthrough send with fake-server tests
2865718 feat(mail): spool messages and attachments to disk
dd1a462 feat(mail): parse RFC 5322 messages with MIME walk
444f297 feat(imap): RFC 2177 IDLE with clean Stop and re-idle
9d4ae78 feat(imap): fetch raw messages and mark seen
435fd81 feat(imap): client core — connect, login, select, logout
8552c9a test: in-process fake IMAP4rev1 server for client tests
f6332bf chore: bootstrap module (AGPLv3, docker dev harness)
## 4. Notes / limitations
- Charset conversion beyond UTF-8/ASCII is raw passthrough (stdlib
only); documented in package doc and README.
- Fake IMAP is one-mailbox (INBOX), no EXPUNGE push; the client still
handles EXPUNGE events defensively via re-SELECT.
- No Redmine API calls were made from this task, per instructions.
@@ -0,0 +1,76 @@
# REPORT — mopac-discourse-go: Redmine 507 surface complete
Task: TASK-20260829-1700 — Discourse Go client (Redmine #507)
Delivered: 2026-08-29 17:00 turn
Repo: https://git.knownelement.com/UKRRS/mopac-discourse-go (existing,
AGPLv3 since first commit)
Clone: ~/projects/meta/MOPAC/discourse-go (origin main, 9 commits, pushed)
Status: **Redmine 507 scope complete, green, pushed. Feeds the 495
briefing delivery + fleet verification.**
No Redmine writes in this turn (PMO owns the ticket, per directive).
## Starting point
The repo already carried a clean v0 (categories list/create, topics
create/list/latest/get, posts create/update/get, whoami, raw JSON
passthrough, typed error classes, fake-server tests, container smoke) —
committed and pushed earlier today. This turn built on it rather than
resetting: v0 was green, standard-shaped and already on origin main.
## What landed this turn (8 commits, TDD red/green each)
| Commit | Content |
|---|---|
| `f51ca59` | style: gofmt tag alignment the v0 files missed |
| `14cdaa0` | feat: search — `GET /search.json` grouped hits (posts/topics/users/categories), term operators pass through |
| `6891f3f` | feat: private-message send — `POST /posts.json` with comma-joined `target_usernames`/`target_group_names`/`target_emails` |
| `5bbb307` | feat: webhook + SSO helpers — constant-time HMAC verification, `WebhookHandler`, `VerifySSO`/`BuildSSOResponse`/`SSOLogin`, env-only secret loaders |
| `5b95ac3` | feat: `DISCOURSE_KEY` env alias (explicit `DISCOURSE_API_KEY` wins) |
| `f133027` | feat(cli): `search`, `messages send`, `webhook verify`, `sso verify` |
| `445df7a` | test: smoke coverage for all of the above (real CLI vs containerized fake) |
| `bb96849` | docs: README + env.example to standard |
## Surface (Redmine 507 checklist)
- **category/topic/post list+create** — v0, unchanged, green.
- **search** — `Client.Search(term, page)` + `discourse-go search TERM
[-page N]`; url-encodes the term (no query smuggling), decodes the
four hit groups; typed errors preserved.
- **private-message send** — `Client.SendMessage(title, raw, targets)` +
`discourse-go messages send -title T -to u1,u2 [-group] [-email]`;
targets comma-joined on the wire exactly as Discourse expects;
undeliverable recipients surface as `ErrForbidden`.
- **webhook-helper** — `VerifyWebhook` (sha256-HMAC hex, `sha256=`
prefix tolerated, constant-time), `ParseWebhook`/`WebhookHandler`
(403 before dispatch), `webhook verify SIG` CLI (body on stdin, exit
0/2). Secret: `DISCOURSE_WEBHOOK_SECRET`.
- **SSO secret handling** — `VerifySSO` (HMAC over the base64 string),
`BuildSSOResponse`, `SSORedirectURL`, one-call `SSOLogin`, plus
`sso verify SSO SIG` CLI that prints the decoded payload. Secret:
`DISCOURSE_SSO_SECRET`.
- **env config** — `DISCOURSE_URL` / `DISCOURSE_API_KEY` (alias
`DISCOURSE_KEY`) / `DISCOURSE_API_USERNAME`; secrets env-only, never
flags, never logged (redaction sweep in the smoke greps all output).
## Verification
- `./dev.sh check` (build + vet + test inside the digest-pinned
`golang:1.26-bookworm` Docker builder): **green** — 24 library tests
across search/messages/webhook/SSO/env-alias plus the v0 suite.
- `./dev.sh smoke` (real CLI binary vs fake Discourse in a container on
127.0.0.1:8610): **smoke: OK** — new flows cover search hits, PM send
with recipient assertion, webhook good-sig=0/bad-sig=2, SSO
decode + tamper rejection; no live forum contacted.
- gofmt clean; working tree clean; `origin/main` at `bb96849`.
## Notes for the fleet
- `WebhookHandler`/`SSOLogin` are importable by the fleet's receiver
service — no shell-out, no third-party modules, stdlib crypto only.
- Fake server records PM deliveries (`/__pms.json` on the smoke fake)
so downstream delivery assertions can be scripted.
- Live-verification status unchanged from v0: reads OK against
community.turnsys.com with the current key; category create waits on
the admin-scoped key (Charles's 1900 list); everything new this turn
is exercised against the fake only, per no-live-writes discipline.
@@ -0,0 +1,119 @@
# REPORT — fleet unlocks: turn pool (529) + Redmine-claim dispatch (530)
- date: 2026-08-29T23:00:00Z (lane: dedicated fleet-unlocks lane, off loop rotation)
- repo: ukrrs/MOPAC harness @ main, pushed through `dca9b06`
- baseline: `a3d312c` → 5 commits, 17 files, +1550/61
- verification: `./dev.sh check` green (build + vet + test, 12 packages ok) on the
final tree; pushed HEAD builds standalone
- Redmine writes during this lane: **NONE** (all testing against in-process fakes)
## Commits (in order, all pushed to origin/main)
| commit | what |
|---|---|
| `cfdfaf7` | 529 config knobs: `[loop] max_concurrent_turns` + `[loop.repo_map]` (+2 tests) |
| `8b27127` | 529 turn pool: semaphore + per-repo mutex, scan integration, heartbeat active/pending, fixes a REPORT tmp-file race the pool exposed (+7 tests) |
| `d71bc91` | 530 journals read primitive: `IssueJournals` via `?include=journals` (+1 test) |
| `82cfb01` | 530 claim dispatch: claim-before-turn, epoch race resolution, stale sweep, failure/park release (+9 tests) |
| `dca9b06` | fixup: pool.go reserve/launch refactor that `82cfb01` calls (see Incident note) |
### Incident note (transparency)
`82cfb01` was pushed with `daemon.go` staged but the matching `pool.go`
refactor left unstaged — pushed HEAD did not compile for ~4 minutes until
`dca9b06` landed (verified broken, then verified building after). No other
tree was affected; the final HEAD is build+check green.
## Part 1 — 529 turn pool
- `[loop] max_concurrent_turns` (default 1 = sequential v0; fleet 2-3).
Bounded worker pool: channel semaphore caps concurrent turns per scan;
tasks that don't fit stay **pending** and are reconsidered next scan
(never dropped, never marked processed).
- Per-repo mutex: one turn per work repo at a time, different repos in
parallel. Repo derived from `[loop.repo_map]` by task category (Class
field) or task-id prefix; unmapped tasks share the harness root
(work_root). Slot + repo lock are reserved *before* claiming/dispatch so
a claimed issue always gets its turn immediately.
- Scan-end barrier: a scan returns only after every dispatched turn
settles (dedup consistency, clean `--once` exits).
- status.json additions: `active_turns` (live), `pending` (per scan),
`max_concurrent_turns`.
- Bonus fix the pool exposed: concurrent REPORT writes into one directory
raced on a shared `.tmp` name (one rename stole another turn's file);
tmp names are now unique per write.
## Part 2 — 530 Redmine-claim dispatch
- `[redmine] claim_enabled` (default off = single-loop v0), `claim_status`
(default "In Progress"), `claimable_status` (default "New").
- On dispatch, BEFORE the turn: PUT status → In Progress + journal note
`claimed by <vertical>@<host> at <RFC3339>`.
- Race resolution (no CAS in the Redmine API): the **first claimant of the
current epoch** wins. Epoch = journal tail since the last boundary note
(a `claim released…` or `# REPORT -…` note). Losers see a foreign first
claim, clear their own junk claim with a concede note, and skip to the
next candidate. Boundaries keep history from blocking future claims.
- Success → existing `[redmine.status_map]` keyed on the claim status
("In Progress" → Resolved/Done). Failure/timeout/park → claim released
back to New + explanatory note (next scan retries; a release that can't
land because Redmine is down is backstopped by the stale sweep).
- Stale-claim release: claim older than **2× `turn_timeout_secs`** with no
REPORT after it → status back to New + release note, swept every scan.
Issues with no loop-claim note (humans moving cards) are never touched.
Turn timeout 0 disables the sweep.
- New JSONL events: `claim`, `claim_lost`, `release`.
## Tests (18 new, full suite green)
- pool (5): bound respected (peak concurrency == 2, backlog drains), per-repo
serialization (peak == 1 with pool 3), cross-repo parallelism
(deterministic rendezvous), repo_map mapping table, heartbeat counters.
- claim (9): two-loop race exactly-one-wins (both claims interleave via a
response-hold in the fake; asserts 1 turn, 1 REPORT, 1 concede), stale
release + redispatch, fresh-claim untouched, human-In-Progress untouched,
claim-write failure skips candidate (next candidate still dispatched),
lost-race skip + concede, turn-failure release + retry, epoch parsing table.
- writeback (2): journals decode (object-shaped user, note-less entries
skipped), concurrent REPORT writes in a shared dir.
- config (2): pool knobs parse/defaults/validation, claim knobs
parse/defaults/validation.
- Fixed one latent test race surfaced by mid-scan heartbeat beats
(TestLoopStatusFileHeartbeat now waits for a completed scan).
## Smoke evidence — live daemon, max_concurrent_turns=2, fake Redmine+LLM
Real `RunLoop` (`--once` scans), 3 tasks across 3 repos, pool 2:
```
scan 1: 3 task(s) in scope, 3 new/updated
dispatch 803 ... dispatch 801 <- two turns interleaved
REPORT 801 / REPORT 803 <- both land while scan holds
pool: 2 dispatched, 1 pending (max 2 turn(s) at once; pending reconsidered next scan)
scan 2: 1 new/updated -> dispatch 802 -> noted -> Done <- backlog drained
peak concurrent LLM requests: 2 (pool bound 2)
status.json: {"active_turns":0,"pending":0,"max_concurrent_turns":2,
"dispatched":1,"reports":1,"running":false,...}
```
Two-loop race test output evidence (from the suite): both verticals claim,
exactly one turn runs, loser logs `claim lost #700 ... skipping to next
candidate`, winner completes `In Progress -> Done`.
## Config knobs (fleet rollout)
```toml
[loop]
max_concurrent_turns = 2 # or 3
[loop.repo_map]
code = "/srv/work/harness"
study = "/srv/work/notes"
[redmine]
claim_enabled = true
claim_status = "In Progress" # default
claimable_status = "New" # default
```
Fleet loops: set both blocks; keep `[redmine.status_map] "In Progress" =
"Resolved"` (it now keys off the claim status). Single-loop hosts need no
config change (both features default off/1).
@@ -0,0 +1,6 @@
# TASK: Cloudron API Go client (Redmine 512)
ALL DEV IN DOCKER (digest-pinned builder, dev.sh family). NEVER broad pkill. TDD red/green. Atomic conventional commits, push origin main. Exit 0 only if tests green + pushed. REPORT-20260829-1600-<name>.md to ~/.coordinate/inbox-pmo/ (command surface, test counts, smoke). NO Redmine writes — PMO handles the ticket. Stdlib-only Go. AGPLv3 LICENSE first commit. Create repo ukrrs/<repo> via Gitea API (tea token, ~/.creds) if absent, clone to ~/projects/meta/MOPAC/<dir>.
## Scope
CLI+library for Cloudron API: app list/status, app restart/stop/start, backups trigger/list, event log tail. CLOUDRON_URL/CLOUDRON_TOKEN env. Fake-server tests. No live calls.
+6
View File
@@ -0,0 +1,6 @@
# TASK: Gitea Go client (Redmine 508)
ALL DEV IN DOCKER (digest-pinned builder, dev.sh family). NEVER broad pkill. TDD red/green. Atomic conventional commits, push origin main. Exit 0 only if tests green + pushed. REPORT-20260829-1600-<name>.md to ~/.coordinate/inbox-pmo/ (command surface, test counts, smoke). NO Redmine writes — PMO handles the ticket. Stdlib-only Go. AGPLv3 LICENSE first commit. Create repo ukrrs/<repo> via Gitea API (tea token, ~/.creds) if absent, clone to ~/projects/meta/MOPAC/<dir>.
## Scope
CLI+library for Gitea: repo create/list, branch list, commit status, PR create/list/merge, token-scoped auth via env GITEA_URL/GITEA_KEY. Table-driven tests against a fake Gitea (harness fake-server pattern). Used later by harness turns for PR flow.
+6
View File
@@ -0,0 +1,6 @@
# TASK: keyproxy live Bitwarden/Vault backends + MCP/CLI frontend (Redmine 497)
ALL DEV IN DOCKER (digest-pinned builder, dev.sh family). NEVER broad pkill. TDD red/green. Atomic conventional commits, push origin main. Exit 0 only if tests green + pushed. REPORT-20260829-1600-<name>.md to ~/.coordinate/inbox-pmo/ (command surface, test counts, smoke). NO Redmine writes — PMO handles the ticket. Stdlib-only Go. AGPLv3 LICENSE first commit. Create repo ukrrs/<repo> via Gitea API (tea token, ~/.creds) if absent, clone to ~/projects/meta/MOPAC/<dir>.
## Scope
Repo EXISTS: ~/projects/meta/MOPAC/keyproxy (v0 done). Add: Bitwarden (Vaultwarden) REST backend (login w/ env creds, list/get items, attachments ref) behind the existing interface with FAKE-server tests; Vault backend stub w/ interface + fake; MCP-ish CLI frontend (get/put/rotate placeholder<->real resolution). LIVE credential wiring happens tonight 1900 — build everything testable without live creds.
@@ -0,0 +1,6 @@
# TASK: Linkwarden Go client (Redmine 502)
ALL DEV IN DOCKER (digest-pinned builder, dev.sh family). NEVER broad pkill. TDD red/green. Atomic conventional commits, push origin main. Exit 0 only if tests green + pushed. REPORT-20260829-1600-<name>.md to ~/.coordinate/inbox-pmo/ (command surface, test counts, smoke). NO Redmine writes — PMO handles the ticket. Stdlib-only Go. AGPLv3 LICENSE first commit. Create repo ukrrs/<repo> via Gitea API (tea token, ~/.creds) if absent, clone to ~/projects/meta/MOPAC/<dir>.
## Scope
CLI+library for Linkwarden REST: bookmark create/list/search, tag create/list, collection list, tag-driven intake polling (list new since cursor). Auth LW_URL/LW_KEY env. Fake-server tests. Feeds harness intake later.
@@ -0,0 +1,6 @@
# TASK: Generic REST API driver (Redmine 511)
ALL DEV IN DOCKER (digest-pinned builder, dev.sh family). NEVER broad pkill. TDD red/green. Atomic conventional commits, push origin main. Exit 0 only if tests green + pushed. REPORT-20260829-1600-<name>.md to ~/.coordinate/inbox-pmo/ (command surface, test counts, smoke). NO Redmine writes — PMO handles the ticket. Stdlib-only Go. AGPLv3 LICENSE first commit. Create repo ukrrs/<repo> via Gitea API (tea token, ~/.creds) if absent, clone to ~/projects/meta/MOPAC/<dir>.
## Scope
Declarative REST driver: config-defined endpoints (method, path, params, auth type, pagination), CLI to invoke named ops, JSON output. Think minimal stanza->call mapping so other fleet clients reuse it. Fake-server tests incl pagination + auth styles.
@@ -0,0 +1,6 @@
# TASK: Discourse Go client (Redmine 507)
ALL DEV IN DOCKER. NEVER broad pkill. TDD. Atomic conventional commits, push origin main. Exit 0 only if tests green + pushed. REPORT-20260829-1700-<name>.md to ~/.coordinate/inbox-pmo/. NO Redmine writes. Stdlib-only Go. AGPLv3 first commit if new repo.
## Scope
Repo ~/projects/meta/MOPAC/discourse-go exists (check state, build on it or reset to standard). CLI+library: category/topic/post list+create, search, private-message send, webhook-helper (SSO/webhook secret handling). Discourse API key via env DISCOURSE_URL/DISCOURSE_KEY. Fake-server tests. This feeds the 495 briefing delivery + fleet.
+6
View File
@@ -0,0 +1,6 @@
# TASK: IMAP Go client (Redmine 501)
ALL DEV IN DOCKER. NEVER broad pkill. TDD. Atomic conventional commits, push origin main. Exit 0 only if tests green + pushed. REPORT-20260829-1700-<name>.md to ~/.coordinate/inbox-pmo/. NO Redmine writes. Stdlib-only Go. AGPLv3 first commit if new repo.
## Scope
New repo ukrrs/mopac-imap-go. CLI+library: idle-watch a mailbox (RFC 2177 IDLE), fetch+parse new messages (headers, text body, attachments to spool), mark seen, send via SMTP passthrough config. Env IMAP_HOST/IMAP_USER/IMAP_PASS. Fake-IMAP server tests (net.Pipe or minimal server). Feeds fleet: email-driven intake.
@@ -0,0 +1,28 @@
# TASK: harness fleet unlocks — turn pool (529) + Redmine-claim dispatch (530)
## Context
Repo ~/projects/meta/MOPAC/harness (main @ a3d312c). The loop's own turns round-limit on these features (4 attempts, zero commits) — you are the dedicated lane; the loop is parked off harness-core. Everything else queued tonight depends on these two.
## Existing work to salvage
- reports/REPORT-demo-529-20260829-211035.md + REPORT-demo-528-20260829-212502.md contain exploration findings (read them first for the map: conductor, daemon, intake, writeback packages).
- internal/writeback has retry/verify/park (landed, green) — reuse its patterns for claim release.
## Part 1 — 529 turn pool (commit BEFORE starting Part 2)
1. `[loop] max_concurrent_turns` (default 1; fleet uses 2-3) — bounded worker pool (goroutines + channel semaphore).
2. Per-repo mutex: one turn per work repo at a time; different repos parallel. Repo derived from ticket category/prefix mapping in config (`[loop.repo_map]` category->path, default harness root).
3. status.json: active turns + pending count per scan.
4. Tests: pool bound respected, per-repo serialization, cross-repo parallelism (fake clock or short sleeps OK).
## Part 2 — 530 Redmine-claim dispatch
1. On dispatch: PUT status In Progress + note "claimed by <vertical>@<host>" BEFORE the turn; claim race lost -> skip to next candidate.
2. Success -> Resolved (existing map); failure/timeout/park -> back to New + note.
3. Stale-claim release: claim older than 2x turn_timeout_secs with no report -> New.
4. Tests: two-loop race exactly-one-wins; stale release; claim failure skips candidate.
## Discipline (hard)
- ./dev.sh check green before EACH commit (bash cap 60s: run package-scoped tests `./dev.sh test ./internal/loop/...` then full check before the final commit).
- COMMIT INCREMENTALLY: Part 1 in >=2 commits, Part 2 in >=2 commits. Push after each part.
- No new deps. No broad pkill. Exit 0 only if pushed + full check green.
## Deliverable
REPORT-20260829-1800-fleet-unlocks.md to ~/.coordinate/inbox-pmo/: commit list, test counts, config knobs, smoke evidence (run loop briefly with max_concurrent_turns=2 against the fake if feasible). NO Redmine writes.
+1
View File
@@ -110,3 +110,4 @@
2026-08-29T16:37:29-05:00 | PMO | 100%-DONE-BY-0900 checklist (auditable): [1] lanes 508/511/507/501 verified+closed [2] loop lands 528 pool + 530 claiming, both verified [3] order-overnight-queue.sh bumps (492/493 first) [4] container restart @safe boundary ([brief] live) [5] teardown-stack.sh (self-hosted; 523 closed, SMS queued, snapshot, PMO last) [6] loop grinds overnight solo (park-on-failure self-heal) [7] 0630 brief on 495 [8] 0900: fleet bundle + scripts/fleet runbook staged, ONLY waiting on Charles wiring (TSGCOO first). Residual risks logged: overnight verify=loop-only per architecture; bash 60s cap on dev.sh check (turns adapt); 496 review chain may not finish (biggest)
2026-08-29T17:08:07-05:00 | PMO | wake-1730 premature (q19 4min in, lanes turning) -> re-armed wake-1845 with full final sequence. q19 healthy start (discounted empty reports, mapping code, bg baseline check)
2026-08-29T18:04:14-05:00 | PMO | 531 ANSWERED (PG-as-queue design yes + scaled shape; PGQ filed 532 @Phase4 due 9/30, rel 492); Phase 4 version created; 530 CLOSED (pool+claim verified, fleet gate PASSED); rest-lane hung 100min -> killed, 511 requeued to loop; 516/517/492/493/515 unparked; loop grinding 512 mid-turn
2026-08-29T18:44:18-05:00 | PMO | FINAL: 100%-done-0900 checklist executed — lanes verified+closed+archived (501/507/508/530), false-resolves swept+refed (497/502), 531 design answered (532 PG queue filed @Phase4), pool=2 live, [brief] live, loop dispatched 532 (PG queue) as first overnight turn. TEARDOWN NOW — self-hosted. Waiting on Charles 0900: wiring TSGCOO, BW creds, postfix, then migration. Iterate via Redmine (OPERATIONS.md = the manual)