pmo: FINAL — self-hosted teardown; loop sole worker; 0900-ready
This commit is contained in:
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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-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-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: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)
|
||||||
|
|||||||
Reference in New Issue
Block a user