docs + dev.sh: loop in README/REPORT, ./dev.sh loop runner, scan-failure exit codes

README gains the self-host loop section (flow diagram, config reference
rows for [loop]/[redmine.status_map]/[keyproxy]/[gitea], CLI flags,
status table); REPORT.md records phase 3a including the die list for the
bash stack it replaces. dev.sh grows a `loop` runner (repo bind-mounted,
key env passthrough). `harness loop --once` now exits 2 when the intake
scan itself fails so cron setups alert loudly; daemon mode still logs
and retries.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-08-28 22:21:54 -05:00
parent ecc0ee874b
commit 2901bb8cae
6 changed files with 203 additions and 27 deletions
+9 -3
View File
@@ -74,13 +74,19 @@ func (c *Conductor) RunLoop(ctx context.Context, opts LoopOpts) error {
}
for {
if err := c.scanOnce(ctx, state, writer, gitea, opts.DryRun); err != nil {
scanErr := c.scanOnce(ctx, state, writer, gitea, opts.DryRun)
if scanErr != nil {
// Scan failures (redmine down, decode hiccups) are logged and
// retried on the next tick; only ctx cancel ends the daemon.
fmt.Fprintf(c.out, "harness: loop: scan error: %v (retrying next interval)\n", err)
_ = state.log(loopEvent{Type: evError, Detail: "scan: " + err.Error()})
fmt.Fprintf(c.out, "harness: loop: scan error: %v (retrying next interval)\n", scanErr)
_ = state.log(loopEvent{Type: evError, Detail: "scan: " + scanErr.Error()})
}
if opts.Once {
if scanErr != nil {
// A cron-style scan that could not even read the intake
// must fail loudly, not exit 0.
return scanErr
}
return nil
}
select {