harness: LiteLLM client, Redmine intake, REPORT writeback, gated bash tool
OpenAI-compatible chat client for the LiteLLM proxy (base_url normalization,
Bearer auth, retry/backoff on 429/5xx/transport, usage accounting) - stdlib
http only. Intake lists issues in scope from /issues.json with the task
class read from a configurable custom field. Writeback lands each turn as
REPORT-<vertical>-<task>-<ts>.md plus REPORT-latest.md (atomic rename) with
model/tier/token telemetry. The bash tool ports maki's permission semantics
without tree-sitter: segment-by-segment compound-command checks, deny beats
allow, word-boundary "cmd *" matching, $()/backtick/subshell denied, output
truncation, per-command timeout with process-group cleanup.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// Package writeback persists turn results as REPORT files. The REPORT is
|
||||
// both the human-facing record and the telemetry trail (model, tier, class,
|
||||
// tokens, rounds) that makes routing decisions auditable.
|
||||
package writeback
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.knownelement.com/reachableceo/MOPAC/harness/internal/task"
|
||||
)
|
||||
|
||||
// Report is everything that lands in a REPORT file.
|
||||
type Report struct {
|
||||
Time time.Time
|
||||
Vertical string
|
||||
Task task.Task
|
||||
Class string
|
||||
Tier string
|
||||
Model string
|
||||
PromptTokens int
|
||||
CompletionTokens int
|
||||
TotalTokens int
|
||||
Rounds int
|
||||
ToolCalls int
|
||||
Denied int
|
||||
Duration time.Duration
|
||||
StopReason string
|
||||
Content string
|
||||
}
|
||||
|
||||
// Write renders r to <dir>/REPORT-<vertical>-<taskID>-<timestamp>.md and
|
||||
// refreshes <dir>/REPORT-latest.md. Both writes are atomic (tmp + rename).
|
||||
func Write(dir string, r Report) (string, error) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("writeback: %w", err)
|
||||
}
|
||||
name := fmt.Sprintf("REPORT-%s-%s-%s.md",
|
||||
sanitize(r.Vertical), sanitize(r.Task.ID), r.Time.Format("20060102-150405"))
|
||||
body := r.Render()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := atomicWrite(path, body); err != nil {
|
||||
return "", fmt.Errorf("writeback: %w", err)
|
||||
}
|
||||
if err := atomicWrite(filepath.Join(dir, "REPORT-latest.md"), body); err != nil {
|
||||
return "", fmt.Errorf("writeback: %w", err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Render formats the REPORT markdown.
|
||||
func (r Report) Render() string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "# REPORT - %s - %s: %s\n\n", r.Vertical, r.Task.ID, r.Task.Subject)
|
||||
fmt.Fprintf(&b, "- date: %s\n", r.Time.Format(time.RFC3339))
|
||||
fmt.Fprintf(&b, "- task source: %s\n", r.Task.Source)
|
||||
fmt.Fprintf(&b, "- model: %s (tier %s, class %s)\n", r.Model, r.Tier, r.Class)
|
||||
fmt.Fprintf(&b, "- usage: prompt=%d completion=%d total=%d tokens\n",
|
||||
r.PromptTokens, r.CompletionTokens, r.TotalTokens)
|
||||
fmt.Fprintf(&b, "- turn: rounds=%d tool_calls=%d denied=%d stop=%s\n",
|
||||
r.Rounds, r.ToolCalls, r.Denied, r.StopReason)
|
||||
fmt.Fprintf(&b, "- duration: %s\n", r.Duration.Round(time.Millisecond))
|
||||
fmt.Fprintf(&b, "\n## Result\n\n")
|
||||
content := r.Content
|
||||
if content == "" {
|
||||
content = "(no content produced)"
|
||||
}
|
||||
b.WriteString(content)
|
||||
b.WriteString("\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func atomicWrite(path, body string) error {
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, []byte(body), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
func sanitize(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '_', r == '-':
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
b.WriteByte('-')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user