harness loop: self-hosting daemon — Redmine SoR drives its own turns

`harness loop` polls the /issues.json intake on an interval (default
120s, --once for cron-style single scans), runs one bounded turn per
new/updated issue — deduped by issue id + updated_on in an append-only
loop.jsonl — then writes the REPORT back as a Redmine journal note,
transitions status per [redmine.status_map] (In Progress -> Done by
name, resolved via /issue_statuses.json), and refreshes the dedup marker
to the post-writeback updated_on so its own notes never re-trigger it.
Turns are sequential (v0); failed turns are recorded, not retried, so a
down proxy cannot hot-loop the poll. The optional Gitea REPORT commit
([gitea] commit_reports, off) rides the same flow. No slot files, no
doorbell screens, no queue scripts — the bash middle layer is replaced,
not wrapped.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-08-28 22:17:03 -05:00
parent c869ac1b06
commit ecc0ee874b
10 changed files with 1359 additions and 22 deletions
+11 -5
View File
@@ -70,6 +70,10 @@ func (c *RedmineClient) ListTasks(ctx context.Context) ([]task.Task, error) {
ID int `json:"id"`
Subject string `json:"subject"`
Description string `json:"description"`
UpdatedOn string `json:"updated_on"`
Status struct {
Name string `json:"name"`
} `json:"status"`
CustomFields []struct {
Name string `json:"name"`
Value string `json:"value"`
@@ -93,11 +97,13 @@ func (c *RedmineClient) ListTasks(ctx context.Context) ([]task.Task, error) {
prompt = is.Subject
}
tasks = append(tasks, task.Task{
ID: strconv.Itoa(is.ID),
Subject: is.Subject,
Prompt: prompt,
Class: class,
Source: "redmine",
ID: strconv.Itoa(is.ID),
Subject: is.Subject,
Prompt: prompt,
Class: class,
Source: "redmine",
UpdatedOn: is.UpdatedOn,
Status: is.Status.Name,
})
}
return tasks, nil