From 47e37bf260d102af95fd838a75ed7bcfc1b1dbb0 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:14:44 +0800 Subject: [PATCH 001/117] refactor: extract activation trace package Move Codex-managed runtime trace conversion, redaction, and bounding into a dedicated activationtrace package while keeping the existing driver API as a compatibility facade for current callers. Validation: go test ./harness/internal/activationtrace ./harness/internal/driver ./harness/cmd/mnemon-multica-runtime --- harness/internal/activationtrace/trace.go | 256 ++++++++++++++++++ .../internal/activationtrace/trace_test.go | 73 +++++ harness/internal/driver/managed_trace.go | 244 +---------------- 3 files changed, 337 insertions(+), 236 deletions(-) create mode 100644 harness/internal/activationtrace/trace.go create mode 100644 harness/internal/activationtrace/trace_test.go diff --git a/harness/internal/activationtrace/trace.go b/harness/internal/activationtrace/trace.go new file mode 100644 index 00000000..d5d37766 --- /dev/null +++ b/harness/internal/activationtrace/trace.go @@ -0,0 +1,256 @@ +package activationtrace + +import ( + "encoding/json" + "fmt" + "strings" +) + +const ( + SourceCodexAppServer = "codex-appserver" + TextLimit = 12000 +) + +type Event struct { + Kind string `json:"kind"` + SourceRuntime string `json:"source_runtime"` + Principal string `json:"principal,omitempty"` + TurnID string `json:"turn_id,omitempty"` + ItemID string `json:"item_id,omitempty"` + Method string `json:"method,omitempty"` + ItemType string `json:"item_type,omitempty"` + Phase string `json:"phase,omitempty"` + Text string `json:"text,omitempty"` + Command string `json:"command,omitempty"` + CWD string `json:"cwd,omitempty"` + Status string `json:"status,omitempty"` + ExitCode any `json:"exit_code,omitempty"` + Output string `json:"output,omitempty"` + Item map[string]any `json:"item,omitempty"` +} + +type Sink interface { + OnManagedTurnTrace(Event) +} + +type SinkFunc func(Event) + +func (f SinkFunc) OnManagedTurnTrace(event Event) { + if f != nil { + f(event) + } +} + +func EventsFromCodexNotifications(principal string, notifications []map[string]any) []Event { + var out []Event + for _, notification := range notifications { + event, ok := eventFromCodexNotification(principal, notification) + if ok { + out = append(out, event) + } + } + return out +} + +func eventFromCodexNotification(principal string, notification map[string]any) (Event, bool) { + method := stringFromMap(notification, "method") + params, _ := notification["params"].(map[string]any) + event := Event{ + SourceRuntime: SourceCodexAppServer, + Principal: strings.TrimSpace(principal), + Method: method, + TurnID: stringFromMap(params, "turnId"), + } + switch method { + case "item/started", "item/completed": + rawItem, _ := params["item"].(map[string]any) + if len(rawItem) == 0 { + return Event{}, false + } + item := sanitizeMap(rawItem) + event.Item = item + event.ItemID = firstNonEmptyString(stringFromMap(item, "id"), stringFromMap(params, "itemId")) + event.ItemType = stringFromMap(item, "type") + event.Phase = stringFromMap(item, "phase") + event.Text = firstNonEmptyString(stringFromMap(item, "text"), stringFromMap(item, "aggregatedOutput")) + event.Command = stringFromMap(item, "command") + event.CWD = stringFromMap(item, "cwd") + event.Status = stringFromMap(item, "status") + event.ExitCode = item["exitCode"] + event.Output = stringFromMap(item, "aggregatedOutput") + event.Kind = kindForItem(event.ItemType) + return event, true + case "item/agentMessage/delta": + event.Kind = "agent_message_delta" + event.ItemID = stringFromMap(params, "itemId") + event.Text = sanitizeString("delta", stringFromMap(params, "delta")) + event.ItemType = "agentMessage" + return event, strings.TrimSpace(event.Text) != "" || strings.TrimSpace(event.ItemID) != "" + case "turn/completed": + event.Kind = "turn_completed" + event.Status = "completed" + return event, true + case "turn/failed": + event.Kind = "turn_failed" + event.Status = "failed" + event.Text = sanitizeAny("error", params["error"]) + return event, true + default: + return Event{}, false + } +} + +func kindForItem(itemType string) string { + switch strings.TrimSpace(itemType) { + case "agentMessage": + return "agent_message" + case "commandExecution": + return "command_execution" + case "fileChange", "patchApply", "patch_apply": + return "file_change" + default: + if strings.TrimSpace(itemType) == "" { + return "item" + } + return "item" + } +} + +func sanitizeMap(in map[string]any) map[string]any { + out := map[string]any{} + for key, value := range in { + out[key] = sanitizeValue(key, value) + } + return out +} + +func sanitizeValue(key string, value any) any { + if isSensitiveKey(key) { + if value == nil { + return nil + } + return "[redacted]" + } + switch typed := value.(type) { + case map[string]any: + return sanitizeMap(typed) + case []any: + out := make([]any, 0, len(typed)) + for _, item := range typed { + out = append(out, sanitizeValue(key, item)) + } + return out + case string: + return sanitizeString(key, typed) + default: + return value + } +} + +func sanitizeAny(key string, value any) string { + if value == nil { + return "" + } + if text, ok := value.(string); ok { + return sanitizeString(key, text) + } + data, err := json.Marshal(sanitizeValue(key, value)) + if err != nil { + return sanitizeString(key, fmt.Sprint(value)) + } + return sanitizeString(key, string(data)) +} + +func sanitizeString(key, value string) string { + value = redactSecrets(key, value) + if len(value) <= TextLimit { + return value + } + return value[:TextLimit] + "\n[truncated]" +} + +func redactSecrets(key, value string) string { + if isSensitiveKey(key) { + if value == "" { + return "" + } + return "[redacted]" + } + redacted := value + for _, marker := range sensitiveMarkers() { + redacted = redactMarkerAssignments(redacted, marker) + } + return redacted +} + +func redactMarkerAssignments(value, marker string) string { + needle := strings.ToLower(marker) + searchFrom := 0 + for { + lower := strings.ToLower(value) + if searchFrom >= len(value) { + return value + } + relative := strings.Index(lower[searchFrom:], needle) + if relative < 0 { + return value + } + idx := searchFrom + relative + if idx < 0 { + return value + } + end := idx + len(marker) + if end >= len(value) || (value[end] != '=' && value[end] != ':') { + searchFrom = end + continue + } + if strings.HasPrefix(strings.ToLower(value[end:]), "=[redacted]") || strings.HasPrefix(strings.ToLower(value[end:]), ":[redacted]") { + searchFrom = end + len("=[redacted]") + continue + } + separator := value[end] + end++ + for end < len(value) { + ch := value[end] + if ch == ' ' || ch == '\n' || ch == '\t' || ch == ',' || ch == ';' { + break + } + end++ + } + value = value[:idx+len(marker)] + string(separator) + "[redacted]" + value[end:] + searchFrom = idx + len(marker) + len("=[redacted]") + } +} + +func isSensitiveKey(key string) bool { + key = strings.ToUpper(strings.TrimSpace(key)) + for _, marker := range sensitiveMarkers() { + if strings.Contains(key, marker) { + return true + } + } + return false +} + +func sensitiveMarkers() []string { + return []string{"TOKEN", "SECRET", "PASSWORD", "API_KEY", "AUTH", "CREDENTIAL", "COOKIE"} +} + +func stringFromMap(values map[string]any, key string) string { + if values == nil { + return "" + } + if value, ok := values[key].(string); ok { + return strings.TrimSpace(value) + } + return "" +} + +func firstNonEmptyString(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} diff --git a/harness/internal/activationtrace/trace_test.go b/harness/internal/activationtrace/trace_test.go new file mode 100644 index 00000000..b3df4c01 --- /dev/null +++ b/harness/internal/activationtrace/trace_test.go @@ -0,0 +1,73 @@ +package activationtrace + +import ( + "strings" + "testing" +) + +func TestEventsFromCodexNotificationsRedactsAndBounds(t *testing.T) { + longOutput := strings.Repeat("x", TextLimit+20) + events := EventsFromCodexNotifications("planner@team", []map[string]any{ + { + "method": "item/completed", + "params": map[string]any{ + "turnId": "inner-turn", + "item": map[string]any{ + "type": "commandExecution", + "id": "call-1", + "command": "TOKEN=raw-secret multica issue get TEA-1", + "cwd": "/tmp/work", + "status": "completed", + "exitCode": 0, + "aggregatedOutput": "API_KEY=abc123 " + longOutput, + "authToken": "must-not-leak", + }, + }, + }, + }) + if len(events) != 1 { + t.Fatalf("events = %+v, want one", events) + } + got := events[0] + if got.Kind != "command_execution" || got.ItemType != "commandExecution" || got.Principal != "planner@team" { + t.Fatalf("unexpected trace event: %+v", got) + } + joined := got.Command + "\n" + got.Output + "\n" + strings.TrimSpace(testString(got.Item["authToken"])) + for _, forbidden := range []string{"raw-secret", "abc123", "must-not-leak"} { + if strings.Contains(joined, forbidden) { + t.Fatalf("trace leaked %q: %+v", forbidden, got) + } + } + if !strings.Contains(got.Command, "TOKEN=[redacted]") || !strings.Contains(got.Output, "API_KEY=[redacted]") { + t.Fatalf("trace did not redact command/output: command=%q output=%q", got.Command, got.Output[:80]) + } + if !strings.Contains(got.Output, "[truncated]") { + t.Fatalf("long output was not bounded: len=%d", len(got.Output)) + } +} + +func TestEventsFromCodexNotificationsDoesNotTreatTraceAsCanonicalEvent(t *testing.T) { + events := EventsFromCodexNotifications("planner@team", []map[string]any{ + { + "method": "turn/completed", + "params": map[string]any{"turnId": "turn-1"}, + }, + }) + if len(events) != 1 { + t.Fatalf("events = %+v, want one", events) + } + got := events[0] + if got.Kind != "turn_completed" || got.Status != "completed" { + t.Fatalf("unexpected trace event: %+v", got) + } + if got.Item != nil { + t.Fatalf("turn trace should not carry canonical event payload: %+v", got.Item) + } +} + +func testString(value any) string { + if text, ok := value.(string); ok { + return text + } + return "" +} diff --git a/harness/internal/driver/managed_trace.go b/harness/internal/driver/managed_trace.go index 259a3e1c..5bc76d04 100644 --- a/harness/internal/driver/managed_trace.go +++ b/harness/internal/driver/managed_trace.go @@ -2,251 +2,23 @@ package driver import ( "context" - "encoding/json" - "fmt" - "strings" + + "github.com/mnemon-dev/mnemon/harness/internal/activationtrace" ) const ( - ManagedTurnTraceSourceCodexAppServer = "codex-appserver" - managedTurnTraceTextLimit = 12000 + ManagedTurnTraceSourceCodexAppServer = activationtrace.SourceCodexAppServer + managedTurnTraceTextLimit = activationtrace.TextLimit ) -type ManagedTurnTraceEvent struct { - Kind string `json:"kind"` - SourceRuntime string `json:"source_runtime"` - Principal string `json:"principal,omitempty"` - TurnID string `json:"turn_id,omitempty"` - ItemID string `json:"item_id,omitempty"` - Method string `json:"method,omitempty"` - ItemType string `json:"item_type,omitempty"` - Phase string `json:"phase,omitempty"` - Text string `json:"text,omitempty"` - Command string `json:"command,omitempty"` - CWD string `json:"cwd,omitempty"` - Status string `json:"status,omitempty"` - ExitCode any `json:"exit_code,omitempty"` - Output string `json:"output,omitempty"` - Item map[string]any `json:"item,omitempty"` -} - -type ManagedTurnTraceSink interface { - OnManagedTurnTrace(ManagedTurnTraceEvent) -} - -type ManagedTurnTraceSinkFunc func(ManagedTurnTraceEvent) - -func (f ManagedTurnTraceSinkFunc) OnManagedTurnTrace(event ManagedTurnTraceEvent) { - if f != nil { - f(event) - } -} +type ManagedTurnTraceEvent = activationtrace.Event +type ManagedTurnTraceSink = activationtrace.Sink +type ManagedTurnTraceSinkFunc = activationtrace.SinkFunc type ManagedTurnClientWithTrace interface { StartTurnWithTrace(ctx context.Context, query string, sink ManagedTurnTraceSink) (ManagedTurnResult, error) } func ManagedTurnTraceEventsFromCodexNotifications(principal string, notifications []map[string]any) []ManagedTurnTraceEvent { - var out []ManagedTurnTraceEvent - for _, notification := range notifications { - event, ok := managedTurnTraceEventFromCodexNotification(principal, notification) - if ok { - out = append(out, event) - } - } - return out -} - -func managedTurnTraceEventFromCodexNotification(principal string, notification map[string]any) (ManagedTurnTraceEvent, bool) { - method := stringFromMap(notification, "method") - params, _ := notification["params"].(map[string]any) - event := ManagedTurnTraceEvent{ - SourceRuntime: ManagedTurnTraceSourceCodexAppServer, - Principal: strings.TrimSpace(principal), - Method: method, - TurnID: stringFromMap(params, "turnId"), - } - switch method { - case "item/started", "item/completed": - rawItem, _ := params["item"].(map[string]any) - if len(rawItem) == 0 { - return ManagedTurnTraceEvent{}, false - } - item := sanitizeManagedTraceMap(rawItem) - event.Item = item - event.ItemID = firstNonEmptyString(stringFromMap(item, "id"), stringFromMap(params, "itemId")) - event.ItemType = stringFromMap(item, "type") - event.Phase = stringFromMap(item, "phase") - event.Text = firstNonEmptyString(stringFromMap(item, "text"), stringFromMap(item, "aggregatedOutput")) - event.Command = stringFromMap(item, "command") - event.CWD = stringFromMap(item, "cwd") - event.Status = stringFromMap(item, "status") - event.ExitCode = item["exitCode"] - event.Output = stringFromMap(item, "aggregatedOutput") - event.Kind = managedTraceKindForItem(event.ItemType) - return event, true - case "item/agentMessage/delta": - event.Kind = "agent_message_delta" - event.ItemID = stringFromMap(params, "itemId") - event.Text = sanitizeManagedTraceString("delta", stringFromMap(params, "delta")) - event.ItemType = "agentMessage" - return event, strings.TrimSpace(event.Text) != "" || strings.TrimSpace(event.ItemID) != "" - case "turn/completed": - event.Kind = "turn_completed" - event.Status = "completed" - return event, true - case "turn/failed": - event.Kind = "turn_failed" - event.Status = "failed" - event.Text = sanitizeManagedTraceAny("error", params["error"]) - return event, true - default: - return ManagedTurnTraceEvent{}, false - } -} - -func managedTraceKindForItem(itemType string) string { - switch strings.TrimSpace(itemType) { - case "agentMessage": - return "agent_message" - case "commandExecution": - return "command_execution" - case "fileChange", "patchApply", "patch_apply": - return "file_change" - default: - if strings.TrimSpace(itemType) == "" { - return "item" - } - return "item" - } -} - -func sanitizeManagedTraceMap(in map[string]any) map[string]any { - out := map[string]any{} - for key, value := range in { - out[key] = sanitizeManagedTraceValue(key, value) - } - return out -} - -func sanitizeManagedTraceValue(key string, value any) any { - if isSensitiveManagedTraceKey(key) { - if value == nil { - return nil - } - return "[redacted]" - } - switch typed := value.(type) { - case map[string]any: - return sanitizeManagedTraceMap(typed) - case []any: - out := make([]any, 0, len(typed)) - for _, item := range typed { - out = append(out, sanitizeManagedTraceValue(key, item)) - } - return out - case string: - return sanitizeManagedTraceString(key, typed) - default: - return value - } -} - -func sanitizeManagedTraceAny(key string, value any) string { - if value == nil { - return "" - } - if text, ok := value.(string); ok { - return sanitizeManagedTraceString(key, text) - } - data, err := json.Marshal(sanitizeManagedTraceValue(key, value)) - if err != nil { - return sanitizeManagedTraceString(key, fmt.Sprint(value)) - } - return sanitizeManagedTraceString(key, string(data)) -} - -func sanitizeManagedTraceString(key, value string) string { - value = redactManagedTraceSecrets(key, value) - if len(value) <= managedTurnTraceTextLimit { - return value - } - return value[:managedTurnTraceTextLimit] + "\n[truncated]" -} - -func redactManagedTraceSecrets(key, value string) string { - if isSensitiveManagedTraceKey(key) { - if value == "" { - return "" - } - return "[redacted]" - } - redacted := value - for _, marker := range managedTraceSensitiveMarkers() { - redacted = redactMarkerAssignments(redacted, marker) - } - return redacted -} - -func redactMarkerAssignments(value, marker string) string { - needle := strings.ToLower(marker) - searchFrom := 0 - for { - lower := strings.ToLower(value) - if searchFrom >= len(value) { - return value - } - relative := strings.Index(lower[searchFrom:], needle) - if relative < 0 { - return value - } - idx := searchFrom + relative - if idx < 0 { - return value - } - end := idx + len(marker) - if end >= len(value) || (value[end] != '=' && value[end] != ':') { - searchFrom = end - continue - } - if strings.HasPrefix(strings.ToLower(value[end:]), "=[redacted]") || strings.HasPrefix(strings.ToLower(value[end:]), ":[redacted]") { - searchFrom = end + len("=[redacted]") - continue - } - separator := value[end] - end++ - for end < len(value) { - ch := value[end] - if ch == ' ' || ch == '\n' || ch == '\t' || ch == ',' || ch == ';' { - break - } - end++ - } - value = value[:idx+len(marker)] + string(separator) + "[redacted]" + value[end:] - searchFrom = idx + len(marker) + len("=[redacted]") - } -} - -func isSensitiveManagedTraceKey(key string) bool { - key = strings.ToUpper(strings.TrimSpace(key)) - for _, marker := range managedTraceSensitiveMarkers() { - if strings.Contains(key, marker) { - return true - } - } - return false -} - -func managedTraceSensitiveMarkers() []string { - return []string{"TOKEN", "SECRET", "PASSWORD", "API_KEY", "AUTH", "CREDENTIAL", "COOKIE"} -} - -func stringFromMap(values map[string]any, key string) string { - if values == nil { - return "" - } - if value, ok := values[key].(string); ok { - return strings.TrimSpace(value) - } - return "" + return activationtrace.EventsFromCodexNotifications(principal, notifications) } From 905418bb8c8910be5f336dfe6f17babfe9f0ab02 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:18:42 +0800 Subject: [PATCH 002/117] refactor: extract managed drive package Move managed wake candidates, ledgers, render client, and Codex appserver turn driver into a dedicated drive package while keeping driver as a facade for existing callers. Validation: go test ./harness/internal/drive ./harness/internal/driver ./harness/cmd/mnemond ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance --- harness/internal/drive/agent.go | 213 +++++++++++++ harness/internal/drive/agent_test.go | 118 +++++++ harness/internal/drive/runtime.go | 355 +++++++++++++++++++++ harness/internal/drive/runtime_test.go | 76 +++++ harness/internal/drive/trace.go | 21 ++ harness/internal/driver/managed_agent.go | 211 +----------- harness/internal/driver/managed_runtime.go | 345 +------------------- 7 files changed, 801 insertions(+), 538 deletions(-) create mode 100644 harness/internal/drive/agent.go create mode 100644 harness/internal/drive/agent_test.go create mode 100644 harness/internal/drive/runtime.go create mode 100644 harness/internal/drive/runtime_test.go create mode 100644 harness/internal/drive/trace.go diff --git a/harness/internal/drive/agent.go b/harness/internal/drive/agent.go new file mode 100644 index 00000000..011e9f13 --- /dev/null +++ b/harness/internal/drive/agent.go @@ -0,0 +1,213 @@ +package drive + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "sync" + "time" + + eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" +) + +const ManagedWakeQuery = "[mnemon:wake]" + +type ManagedTurnClient interface { + StartTurn(context.Context, string) (ManagedTurnResult, error) +} + +type ManagedTurnResult struct { + TurnID string + Status string + FinalAnswer string +} + +type ManagedWakeCandidate struct { + Principal string + DerivedEventID string + BodyDigest string + Reason string + RenderAuditID string + RenderBodyDigest string +} + +type ManagedWakeRecord struct { + Principal string `json:"principal"` + DerivedEventID string `json:"derived_event_id"` + BodyDigest string `json:"body_digest"` + Reason string `json:"reason"` + Query string `json:"query"` + TurnID string `json:"turn_id,omitempty"` + Status string `json:"status"` + StartedAt time.Time `json:"started_at"` + RenderAuditID string `json:"render_audit_id,omitempty"` + RenderBodyDigest string `json:"render_body_digest,omitempty"` + FinalAnswer string `json:"final_answer,omitempty"` + Error string `json:"error,omitempty"` +} + +type ManagedWakeLedger interface { + Seen(ManagedWakeCandidate) bool + Record(ManagedWakeRecord) error +} + +type MemoryManagedWakeLedger struct { + mu sync.Mutex + seen map[string]ManagedWakeRecord +} + +func NewMemoryManagedWakeLedger() *MemoryManagedWakeLedger { + return &MemoryManagedWakeLedger{seen: map[string]ManagedWakeRecord{}} +} + +func (l *MemoryManagedWakeLedger) Seen(candidate ManagedWakeCandidate) bool { + l.mu.Lock() + defer l.mu.Unlock() + _, ok := l.seen[managedWakeKey(candidate)] + return ok +} + +func (l *MemoryManagedWakeLedger) Record(record ManagedWakeRecord) error { + l.mu.Lock() + defer l.mu.Unlock() + l.seen[managedWakeKey(ManagedWakeCandidate{ + Principal: record.Principal, + DerivedEventID: record.DerivedEventID, + BodyDigest: record.BodyDigest, + })] = record + return nil +} + +type ManagedAgentDriver struct { + Principal string + Client ManagedTurnClient + Ledger ManagedWakeLedger + TraceSink ManagedTurnTraceSink + Cooldown time.Duration + Now func() time.Time + + mu sync.Mutex + inFlight bool + lastWake time.Time +} + +func (d *ManagedAgentDriver) Wake(ctx context.Context, candidate ManagedWakeCandidate) (ManagedWakeRecord, error) { + if d.Principal == "" { + return ManagedWakeRecord{}, fmt.Errorf("managed agent driver requires a principal") + } + if candidate.Principal != d.Principal { + return ManagedWakeRecord{}, fmt.Errorf("wake candidate principal %q does not match local principal %q", candidate.Principal, d.Principal) + } + if candidate.DerivedEventID == "" || candidate.BodyDigest == "" { + return ManagedWakeRecord{}, fmt.Errorf("wake candidate requires derived event id and body digest") + } + if d.Client == nil { + return ManagedWakeRecord{}, fmt.Errorf("managed agent driver requires a turn client") + } + ledger := d.Ledger + if ledger == nil { + ledger = NewMemoryManagedWakeLedger() + d.Ledger = ledger + } + now := d.now() + d.mu.Lock() + if d.inFlight { + d.mu.Unlock() + return ManagedWakeRecord{}, fmt.Errorf("managed wake already in flight") + } + if d.Cooldown > 0 && !d.lastWake.IsZero() && now.Sub(d.lastWake) < d.Cooldown { + d.mu.Unlock() + return ManagedWakeRecord{}, fmt.Errorf("managed wake cooldown active") + } + if ledger.Seen(candidate) { + d.mu.Unlock() + return ManagedWakeRecord{}, fmt.Errorf("managed wake candidate already handled") + } + d.inFlight = true + d.mu.Unlock() + + record := ManagedWakeRecord{ + Principal: candidate.Principal, + DerivedEventID: candidate.DerivedEventID, + BodyDigest: candidate.BodyDigest, + Reason: candidate.Reason, + Query: ManagedWakeQuery, + StartedAt: now, + Status: "started", + RenderAuditID: candidate.RenderAuditID, + RenderBodyDigest: candidate.RenderBodyDigest, + } + var result ManagedTurnResult + var err error + if traced, ok := d.Client.(ManagedTurnClientWithTrace); ok && d.TraceSink != nil { + result, err = traced.StartTurnWithTrace(ctx, ManagedWakeQuery, d.TraceSink) + } else { + result, err = d.Client.StartTurn(ctx, ManagedWakeQuery) + } + if err != nil { + record.Status = "failed" + record.Error = err.Error() + _ = ledger.Record(record) + d.clearInFlight() + return record, err + } + record.TurnID = result.TurnID + record.FinalAnswer = result.FinalAnswer + record.Status = result.Status + if record.Status == "" { + record.Status = "completed" + } + if err := ledger.Record(record); err != nil { + d.clearInFlight() + return record, err + } + d.mu.Lock() + d.lastWake = now + d.inFlight = false + d.mu.Unlock() + return record, nil +} + +func (d *ManagedAgentDriver) clearInFlight() { + d.mu.Lock() + d.inFlight = false + d.mu.Unlock() +} + +func (d *ManagedAgentDriver) now() time.Time { + if d.Now != nil { + return d.Now().UTC() + } + return time.Now().UTC() +} + +func ManagedWakeCandidatesFromEvents(principal string, events []eventmodel.EventEnvelope) []ManagedWakeCandidate { + var out []ManagedWakeCandidate + for _, env := range events { + if string(env.Event.Audience) != principal { + continue + } + reason, _ := env.Meta["presentation_hint"].(string) + if reason == "" { + continue + } + body, _ := eventmodel.PayloadNarrative(env.Event.Payload)["body"].(string) + out = append(out, ManagedWakeCandidate{ + Principal: principal, + DerivedEventID: env.Event.ID, + BodyDigest: managedBodyDigest(body), + Reason: reason, + }) + } + return out +} + +func managedBodyDigest(body string) string { + sum := sha256.Sum256([]byte(body)) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +func managedWakeKey(candidate ManagedWakeCandidate) string { + return candidate.Principal + "|" + candidate.DerivedEventID + "|" + candidate.BodyDigest +} diff --git a/harness/internal/drive/agent_test.go b/harness/internal/drive/agent_test.go new file mode 100644 index 00000000..ebe94cd3 --- /dev/null +++ b/harness/internal/drive/agent_test.go @@ -0,0 +1,118 @@ +package drive + +import ( + "context" + "errors" + "testing" + "time" + + eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" +) + +type turnClientFunc func(context.Context, string) (ManagedTurnResult, error) + +func (f turnClientFunc) StartTurn(ctx context.Context, query string) (ManagedTurnResult, error) { + return f(ctx, query) +} + +func TestManagedAgentDriverUsesWakeSentinelOnly(t *testing.T) { + var gotQuery string + driver := &ManagedAgentDriver{ + Principal: "codex-a@project", + Client: turnClientFunc(func(_ context.Context, query string) (ManagedTurnResult, error) { + gotQuery = query + return ManagedTurnResult{TurnID: "turn-1", Status: "completed"}, nil + }), + Ledger: NewMemoryManagedWakeLedger(), + Now: func() time.Time { return time.Unix(100, 0).UTC() }, + } + record, err := driver.Wake(context.Background(), ManagedWakeCandidate{ + Principal: "codex-a@project", + DerivedEventID: "derived:assignment.work_available:assignment/asg1:codex-a@project", + BodyDigest: "sha256:body", + Reason: "work", + }) + if err != nil { + t.Fatal(err) + } + if gotQuery != ManagedWakeQuery || record.Query != ManagedWakeQuery { + t.Fatalf("managed wake query = %q / %q, want %q", gotQuery, record.Query, ManagedWakeQuery) + } +} + +func TestManagedAgentDriverRejectsRemotePrincipal(t *testing.T) { + driver := &ManagedAgentDriver{ + Principal: "codex-a@project", + Client: turnClientFunc(func(context.Context, string) (ManagedTurnResult, error) { return ManagedTurnResult{}, nil }), + Ledger: NewMemoryManagedWakeLedger(), + } + if _, err := driver.Wake(context.Background(), ManagedWakeCandidate{Principal: "codex-b@project", DerivedEventID: "d1", BodyDigest: "sha256:x"}); err == nil { + t.Fatal("managed driver must reject candidates for non-local principals") + } +} + +func TestManagedAgentDriverDedupesAndCoolsDown(t *testing.T) { + now := time.Unix(100, 0).UTC() + driver := &ManagedAgentDriver{ + Principal: "codex-a@project", + Client: turnClientFunc(func(context.Context, string) (ManagedTurnResult, error) { return ManagedTurnResult{}, nil }), + Ledger: NewMemoryManagedWakeLedger(), + Cooldown: time.Minute, + Now: func() time.Time { return now }, + } + candidate := ManagedWakeCandidate{Principal: "codex-a@project", DerivedEventID: "d1", BodyDigest: "sha256:x"} + if _, err := driver.Wake(context.Background(), candidate); err != nil { + t.Fatal(err) + } + if _, err := driver.Wake(context.Background(), candidate); err == nil { + t.Fatal("duplicate candidate should not wake twice") + } + now = now.Add(10 * time.Second) + if _, err := driver.Wake(context.Background(), ManagedWakeCandidate{Principal: "codex-a@project", DerivedEventID: "d2", BodyDigest: "sha256:y"}); err == nil { + t.Fatal("cooldown should block a different rapid wake") + } +} + +func TestManagedAgentDriverRecordsFailureWithoutChangingQuery(t *testing.T) { + ledger := NewMemoryManagedWakeLedger() + driver := &ManagedAgentDriver{ + Principal: "codex-a@project", + Client: turnClientFunc(func(_ context.Context, query string) (ManagedTurnResult, error) { + if query != ManagedWakeQuery { + t.Fatalf("query = %q, want %q", query, ManagedWakeQuery) + } + return ManagedTurnResult{}, errors.New("runtime unavailable") + }), + Ledger: ledger, + } + candidate := ManagedWakeCandidate{Principal: "codex-a@project", DerivedEventID: "d1", BodyDigest: "sha256:x"} + record, err := driver.Wake(context.Background(), candidate) + if err == nil { + t.Fatal("wake should surface runtime failure") + } + if record.Status != "failed" || record.Query != ManagedWakeQuery { + t.Fatalf("failed record mismatch: %+v", record) + } + if !ledger.Seen(candidate) { + t.Fatal("failure should be recorded locally for audit/idempotence") + } +} + +func TestManagedWakeCandidatesFromEventsUseNarrativeDigest(t *testing.T) { + env := eventmodel.DerivedEnvelope(eventmodel.Event{ + SchemaVersion: eventmodel.SchemaVersion, + ID: "derived:work", + Type: "assignment.work_available", + Subject: "assignment/asg1", + Actor: "mnemond@local", + Audience: "codex-a@project", + Payload: eventmodel.BuildPayload(nil, map[string]any{"body": "Assignment asg1 is yours."}, nil), + }, "2026-06-24T10:00:00Z", "2026-06-24T10:05:00Z", "work", nil) + candidates := ManagedWakeCandidatesFromEvents("codex-a@project", []eventmodel.EventEnvelope{env}) + if len(candidates) != 1 { + t.Fatalf("candidates = %+v, want one", candidates) + } + if candidates[0].Reason != "work" || candidates[0].BodyDigest == "" { + t.Fatalf("candidate should carry reason and body digest: %+v", candidates[0]) + } +} diff --git a/harness/internal/drive/runtime.go b/harness/internal/drive/runtime.go new file mode 100644 index 00000000..d14117bc --- /dev/null +++ b/harness/internal/drive/runtime.go @@ -0,0 +1,355 @@ +package drive + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/codexapp" + "github.com/mnemon-dev/mnemon/harness/internal/contract" + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" +) + +type HTTPRenderClient struct { + BaseURL string + Token string + Principal contract.ActorID + HTTP *http.Client +} + +func (c HTTPRenderClient) Render(ctx context.Context, req presentation.Request) (presentation.Response, error) { + base := strings.TrimRight(strings.TrimSpace(c.BaseURL), "/") + if base == "" { + return presentation.Response{}, fmt.Errorf("render client requires base URL") + } + req.Principal = c.Principal + body, err := json.Marshal(req) + if err != nil { + return presentation.Response{}, err + } + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/render", bytes.NewReader(body)) + if err != nil { + return presentation.Response{}, err + } + if strings.TrimSpace(c.Token) != "" { + httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.Token)) + } else { + httpReq.Header.Set(access.PrincipalHeader, string(c.Principal)) + } + httpReq.Header.Set("Content-Type", "application/json") + client := c.HTTP + if client == nil { + client = http.DefaultClient + } + resp, err := client.Do(httpReq) + if err != nil { + return presentation.Response{}, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + data, _ := io.ReadAll(resp.Body) + return presentation.Response{}, fmt.Errorf("render failed: %s: %s", resp.Status, strings.TrimSpace(string(data))) + } + var out presentation.Response + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return presentation.Response{}, err + } + return out, nil +} + +func ManagedWakeCandidatesFromRender(principal string, resp presentation.Response) []ManagedWakeCandidate { + candidates := ManagedWakeCandidatesFromEvents(principal, resp.Events) + for i := range candidates { + candidates[i].RenderAuditID = resp.AuditID + candidates[i].RenderBodyDigest = resp.BodyDigest + } + return candidates +} + +type FileManagedWakeLedger struct { + path string + mu sync.Mutex + loaded bool + loadErr error + seen map[string]ManagedWakeRecord +} + +func NewFileManagedWakeLedger(path string) *FileManagedWakeLedger { + return &FileManagedWakeLedger{path: path, seen: map[string]ManagedWakeRecord{}} +} + +func (l *FileManagedWakeLedger) Seen(candidate ManagedWakeCandidate) bool { + l.mu.Lock() + defer l.mu.Unlock() + l.loadLocked() + _, ok := l.seen[managedWakeKey(candidate)] + return ok +} + +func (l *FileManagedWakeLedger) Record(record ManagedWakeRecord) error { + l.mu.Lock() + defer l.mu.Unlock() + l.loadLocked() + if l.loadErr != nil { + return l.loadErr + } + if strings.TrimSpace(l.path) == "" { + return fmt.Errorf("managed wake ledger path is required") + } + if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { + return err + } + f, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) + if err != nil { + return err + } + enc := json.NewEncoder(f) + if err := enc.Encode(record); err != nil { + _ = f.Close() + return err + } + if err := f.Close(); err != nil { + return err + } + l.seen[managedWakeKey(ManagedWakeCandidate{ + Principal: record.Principal, + DerivedEventID: record.DerivedEventID, + BodyDigest: record.BodyDigest, + })] = record + return nil +} + +func (l *FileManagedWakeLedger) loadLocked() { + if l.loaded { + return + } + l.loaded = true + if strings.TrimSpace(l.path) == "" { + l.loadErr = fmt.Errorf("managed wake ledger path is required") + return + } + f, err := os.Open(l.path) + if errors.Is(err, os.ErrNotExist) { + return + } + if err != nil { + l.loadErr = err + return + } + defer f.Close() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + var record ManagedWakeRecord + if err := json.Unmarshal([]byte(line), &record); err != nil { + l.loadErr = err + return + } + l.seen[managedWakeKey(ManagedWakeCandidate{ + Principal: record.Principal, + DerivedEventID: record.DerivedEventID, + BodyDigest: record.BodyDigest, + })] = record + } + if err := scanner.Err(); err != nil { + l.loadErr = err + } +} + +type CodexAppServerTurnClient struct { + Principal string + Command string + Workspace string + Env []string + TurnTimeout time.Duration + RequestTimeout time.Duration + ClientName string + ClientVersion string +} + +func (c CodexAppServerTurnClient) StartTurn(ctx context.Context, query string) (ManagedTurnResult, error) { + return c.StartTurnWithTrace(ctx, query, nil) +} + +func (c CodexAppServerTurnClient) StartTurnWithTrace(ctx context.Context, query string, sink ManagedTurnTraceSink) (ManagedTurnResult, error) { + if strings.TrimSpace(query) != ManagedWakeQuery { + return ManagedTurnResult{}, fmt.Errorf("managed codex appserver client only accepts %q queries", ManagedWakeQuery) + } + command := strings.TrimSpace(c.Command) + if command == "" { + command = "codex" + } + workspace := strings.TrimSpace(c.Workspace) + if workspace == "" { + workspace = "." + } + requestTimeout := c.RequestTimeout + if requestTimeout <= 0 { + requestTimeout = 30 * time.Second + } + turnTimeout := c.TurnTimeout + if turnTimeout <= 0 { + turnTimeout = 5 * time.Minute + } + clientName := strings.TrimSpace(c.ClientName) + if clientName == "" { + clientName = "mnemond-managed-agent" + } + clientVersion := strings.TrimSpace(c.ClientVersion) + if clientVersion == "" { + clientVersion = "dev" + } + server := codexapp.New(command, workspace) + if len(c.Env) > 0 { + server.SetEnv(c.Env) + } + if err := server.Start(); err != nil { + return ManagedTurnResult{}, err + } + defer server.Close() + if _, err := server.Request("initialize", map[string]any{ + "clientInfo": map[string]any{"name": clientName, "version": clientVersion}, + "capabilities": codexAppServerCapabilities(), + }, requestTimeout); err != nil { + return ManagedTurnResult{}, fmt.Errorf("initialize: %w", err) + } + threadParams := map[string]any{ + "cwd": workspace, + "approvalPolicy": "never", + "sandbox": "danger-full-access", + "ephemeral": true, + } + thread, err := server.Request("thread/start", threadParams, requestTimeout) + if err != nil { + return ManagedTurnResult{}, fmt.Errorf("thread/start: %w", err) + } + threadID := codexapp.ThreadID(thread) + if threadID == "" { + return ManagedTurnResult{}, fmt.Errorf("thread/start returned no thread id") + } + additionalContext, err := CodexAppServerAdditionalContext(ctx, workspace, c.Env, ManagedWakeQuery) + if err != nil { + return ManagedTurnResult{}, fmt.Errorf("load hook context: %w", err) + } + turnParams := map[string]any{ + "threadId": threadID, + "input": []map[string]any{{"type": "text", "text": ManagedWakeQuery}}, + "cwd": workspace, + "approvalPolicy": "never", + "sandboxPolicy": map[string]any{"type": "dangerFullAccess"}, + } + if len(additionalContext) > 0 { + turnParams["additionalContext"] = additionalContext + } + before := server.NotificationCount() + if _, err := server.Request("turn/start", turnParams, requestTimeout); err != nil { + return ManagedTurnResult{}, fmt.Errorf("turn/start: %w", err) + } + emitTrace := func(notifications []map[string]any) { + if sink == nil { + return + } + for _, event := range ManagedTurnTraceEventsFromCodexNotifications(c.Principal, notifications) { + sink.OnManagedTurnTrace(event) + } + } + if _, err := server.WaitNotificationWithCallback("turn/completed", turnTimeout, before, emitTrace); err != nil { + text := codexapp.CombinedText(server.NotificationsSince(before)) + return ManagedTurnResult{TurnID: threadID, Status: "failed", FinalAnswer: text}, fmt.Errorf("wait turn/completed: %w", err) + } + notifications := server.NotificationsSince(before) + answer := codexapp.FinalAnswer(notifications) + if answer == "" { + answer = codexapp.CombinedText(notifications) + } + return ManagedTurnResult{TurnID: threadID, Status: "completed", FinalAnswer: answer}, nil +} + +func CodexAppServerAdditionalContext(ctx context.Context, workspace string, env []string, query string) (map[string]any, error) { + out := map[string]any{} + for _, lifecycle := range []string{"prime", "remind"} { + message, err := runCodexLifecycleHook(ctx, workspace, env, lifecycle, query) + if err != nil { + return nil, err + } + if strings.TrimSpace(message) == "" { + continue + } + out["mnemon.hook."+lifecycle] = map[string]any{ + "kind": "application", + "value": message, + } + } + return out, nil +} + +func runCodexLifecycleHook(ctx context.Context, workspace string, env []string, lifecycle string, query string) (string, error) { + hook := filepath.Join(workspace, ".codex", "hooks", "mnemon-r1", lifecycle+".sh") + if _, err := os.Stat(hook); errors.Is(err, os.ErrNotExist) { + return "", nil + } else if err != nil { + return "", err + } + stdin := "{}" + if lifecycle == "remind" { + raw, err := json.Marshal(map[string]string{"prompt": query}) + if err != nil { + return "", err + } + stdin = string(raw) + } + cmd := exec.CommandContext(ctx, "bash", hook) + cmd.Dir = workspace + if len(env) > 0 { + cmd.Env = append([]string(nil), env...) + } + cmd.Stdin = strings.NewReader(stdin) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + detail := strings.TrimSpace(stderr.String()) + if detail != "" { + return "", fmt.Errorf("%s hook failed: %w: %s", lifecycle, err, detail) + } + return "", fmt.Errorf("%s hook failed: %w", lifecycle, err) + } + return codexHookSystemMessage(stdout.String()), nil +} + +func codexHookSystemMessage(raw string) string { + text := strings.TrimSpace(raw) + if text == "" { + return "" + } + var envelope map[string]any + if err := json.Unmarshal([]byte(text), &envelope); err == nil { + if message, ok := envelope["systemMessage"].(string); ok { + return strings.TrimSpace(message) + } + } + return text +} + +func codexAppServerCapabilities() map[string]any { + return map[string]any{ + "experimentalApi": true, + "requestAttestation": false, + } +} diff --git a/harness/internal/drive/runtime_test.go b/harness/internal/drive/runtime_test.go new file mode 100644 index 00000000..740fb443 --- /dev/null +++ b/harness/internal/drive/runtime_test.go @@ -0,0 +1,76 @@ +package drive + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" +) + +func TestManagedWakeCandidatesFromRenderCarryAudit(t *testing.T) { + resp := presentation.Response{ + AuditID: "render_audit_1", + BodyDigest: "sha256:render", + Events: []eventmodel.EventEnvelope{eventmodel.DerivedEnvelope(eventmodel.Event{ + SchemaVersion: eventmodel.SchemaVersion, + ID: "derived:assignment.work_available:assignment/asg1:codex-a@project", + Type: "assignment.work_available", + Subject: "assignment/asg1", + Actor: "mnemond@local", + Audience: "codex-a@project", + Payload: eventmodel.BuildPayload(nil, map[string]any{"body": "Assignment asg1 is yours."}, nil), + }, "2026-06-24T10:00:00Z", "2026-06-24T10:05:00Z", "work", nil)}, + } + candidates := ManagedWakeCandidatesFromRender("codex-a@project", resp) + if len(candidates) != 1 { + t.Fatalf("candidates = %+v, want one", candidates) + } + if candidates[0].RenderAuditID != resp.AuditID || candidates[0].RenderBodyDigest != resp.BodyDigest { + t.Fatalf("candidate should carry render audit metadata: %+v", candidates[0]) + } +} + +func TestFileManagedWakeLedgerPersistsSeen(t *testing.T) { + path := filepath.Join(t.TempDir(), "wake-ledger.jsonl") + candidate := ManagedWakeCandidate{Principal: "codex-a@project", DerivedEventID: "d1", BodyDigest: "sha256:x"} + ledger := NewFileManagedWakeLedger(path) + if ledger.Seen(candidate) { + t.Fatal("fresh ledger should not have seen candidate") + } + if err := ledger.Record(ManagedWakeRecord{Principal: candidate.Principal, DerivedEventID: candidate.DerivedEventID, BodyDigest: candidate.BodyDigest, Status: "completed"}); err != nil { + t.Fatal(err) + } + reopened := NewFileManagedWakeLedger(path) + if !reopened.Seen(candidate) { + t.Fatal("reopened ledger should remember candidate") + } +} + +func TestHTTPRenderClientUsesBearerAndDecodesResponse(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer token-1" { + t.Fatalf("Authorization = %q, want bearer token", got) + } + _ = json.NewEncoder(w).Encode(presentation.Response{SchemaVersion: 1, Status: presentation.StatusOK, AuditID: "audit-1"}) + })) + defer server.Close() + resp, err := (HTTPRenderClient{BaseURL: server.URL, Token: "token-1"}).Render(context.Background(), presentation.Request{}) + if err != nil { + t.Fatal(err) + } + if resp.AuditID != "audit-1" { + t.Fatalf("render response = %+v", resp) + } +} + +func TestCodexAppServerTurnClientRejectsContextQuery(t *testing.T) { + client := CodexAppServerTurnClient{Command: "definitely-not-run"} + if _, err := client.StartTurn(context.Background(), "assignment asg1"); err == nil { + t.Fatal("codex appserver client must reject non-sentinel queries before starting a process") + } +} diff --git a/harness/internal/drive/trace.go b/harness/internal/drive/trace.go new file mode 100644 index 00000000..521a0046 --- /dev/null +++ b/harness/internal/drive/trace.go @@ -0,0 +1,21 @@ +package drive + +import ( + "context" + + "github.com/mnemon-dev/mnemon/harness/internal/activationtrace" +) + +const ManagedTurnTraceSourceCodexAppServer = activationtrace.SourceCodexAppServer + +type ManagedTurnTraceEvent = activationtrace.Event +type ManagedTurnTraceSink = activationtrace.Sink +type ManagedTurnTraceSinkFunc = activationtrace.SinkFunc + +type ManagedTurnClientWithTrace interface { + StartTurnWithTrace(ctx context.Context, query string, sink ManagedTurnTraceSink) (ManagedTurnResult, error) +} + +func ManagedTurnTraceEventsFromCodexNotifications(principal string, notifications []map[string]any) []ManagedTurnTraceEvent { + return activationtrace.EventsFromCodexNotifications(principal, notifications) +} diff --git a/harness/internal/driver/managed_agent.go b/harness/internal/driver/managed_agent.go index cc68cb9c..c74ea26a 100644 --- a/harness/internal/driver/managed_agent.go +++ b/harness/internal/driver/managed_agent.go @@ -1,213 +1,24 @@ package driver import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "sync" - "time" - + "github.com/mnemon-dev/mnemon/harness/internal/drive" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" ) -const ManagedWakeQuery = "[mnemon:wake]" - -type ManagedTurnClient interface { - StartTurn(context.Context, string) (ManagedTurnResult, error) -} - -type ManagedTurnResult struct { - TurnID string - Status string - FinalAnswer string -} - -type ManagedWakeCandidate struct { - Principal string - DerivedEventID string - BodyDigest string - Reason string - RenderAuditID string - RenderBodyDigest string -} +const ManagedWakeQuery = drive.ManagedWakeQuery -type ManagedWakeRecord struct { - Principal string `json:"principal"` - DerivedEventID string `json:"derived_event_id"` - BodyDigest string `json:"body_digest"` - Reason string `json:"reason"` - Query string `json:"query"` - TurnID string `json:"turn_id,omitempty"` - Status string `json:"status"` - StartedAt time.Time `json:"started_at"` - RenderAuditID string `json:"render_audit_id,omitempty"` - RenderBodyDigest string `json:"render_body_digest,omitempty"` - FinalAnswer string `json:"final_answer,omitempty"` - Error string `json:"error,omitempty"` -} - -type ManagedWakeLedger interface { - Seen(ManagedWakeCandidate) bool - Record(ManagedWakeRecord) error -} - -type MemoryManagedWakeLedger struct { - mu sync.Mutex - seen map[string]ManagedWakeRecord -} +type ManagedTurnClient = drive.ManagedTurnClient +type ManagedTurnResult = drive.ManagedTurnResult +type ManagedWakeCandidate = drive.ManagedWakeCandidate +type ManagedWakeRecord = drive.ManagedWakeRecord +type ManagedWakeLedger = drive.ManagedWakeLedger +type MemoryManagedWakeLedger = drive.MemoryManagedWakeLedger +type ManagedAgentDriver = drive.ManagedAgentDriver func NewMemoryManagedWakeLedger() *MemoryManagedWakeLedger { - return &MemoryManagedWakeLedger{seen: map[string]ManagedWakeRecord{}} -} - -func (l *MemoryManagedWakeLedger) Seen(candidate ManagedWakeCandidate) bool { - l.mu.Lock() - defer l.mu.Unlock() - _, ok := l.seen[managedWakeKey(candidate)] - return ok -} - -func (l *MemoryManagedWakeLedger) Record(record ManagedWakeRecord) error { - l.mu.Lock() - defer l.mu.Unlock() - l.seen[managedWakeKey(ManagedWakeCandidate{ - Principal: record.Principal, - DerivedEventID: record.DerivedEventID, - BodyDigest: record.BodyDigest, - })] = record - return nil -} - -type ManagedAgentDriver struct { - Principal string - Client ManagedTurnClient - Ledger ManagedWakeLedger - TraceSink ManagedTurnTraceSink - Cooldown time.Duration - Now func() time.Time - - mu sync.Mutex - inFlight bool - lastWake time.Time -} - -func (d *ManagedAgentDriver) Wake(ctx context.Context, candidate ManagedWakeCandidate) (ManagedWakeRecord, error) { - if d.Principal == "" { - return ManagedWakeRecord{}, fmt.Errorf("managed agent driver requires a principal") - } - if candidate.Principal != d.Principal { - return ManagedWakeRecord{}, fmt.Errorf("wake candidate principal %q does not match local principal %q", candidate.Principal, d.Principal) - } - if candidate.DerivedEventID == "" || candidate.BodyDigest == "" { - return ManagedWakeRecord{}, fmt.Errorf("wake candidate requires derived event id and body digest") - } - if d.Client == nil { - return ManagedWakeRecord{}, fmt.Errorf("managed agent driver requires a turn client") - } - ledger := d.Ledger - if ledger == nil { - ledger = NewMemoryManagedWakeLedger() - d.Ledger = ledger - } - now := d.now() - d.mu.Lock() - if d.inFlight { - d.mu.Unlock() - return ManagedWakeRecord{}, fmt.Errorf("managed wake already in flight") - } - if d.Cooldown > 0 && !d.lastWake.IsZero() && now.Sub(d.lastWake) < d.Cooldown { - d.mu.Unlock() - return ManagedWakeRecord{}, fmt.Errorf("managed wake cooldown active") - } - if ledger.Seen(candidate) { - d.mu.Unlock() - return ManagedWakeRecord{}, fmt.Errorf("managed wake candidate already handled") - } - d.inFlight = true - d.mu.Unlock() - - record := ManagedWakeRecord{ - Principal: candidate.Principal, - DerivedEventID: candidate.DerivedEventID, - BodyDigest: candidate.BodyDigest, - Reason: candidate.Reason, - Query: ManagedWakeQuery, - StartedAt: now, - Status: "started", - RenderAuditID: candidate.RenderAuditID, - RenderBodyDigest: candidate.RenderBodyDigest, - } - var result ManagedTurnResult - var err error - if traced, ok := d.Client.(ManagedTurnClientWithTrace); ok && d.TraceSink != nil { - result, err = traced.StartTurnWithTrace(ctx, ManagedWakeQuery, d.TraceSink) - } else { - result, err = d.Client.StartTurn(ctx, ManagedWakeQuery) - } - if err != nil { - record.Status = "failed" - record.Error = err.Error() - _ = ledger.Record(record) - d.clearInFlight() - return record, err - } - record.TurnID = result.TurnID - record.FinalAnswer = result.FinalAnswer - record.Status = result.Status - if record.Status == "" { - record.Status = "completed" - } - if err := ledger.Record(record); err != nil { - d.clearInFlight() - return record, err - } - d.mu.Lock() - d.lastWake = now - d.inFlight = false - d.mu.Unlock() - return record, nil -} - -func (d *ManagedAgentDriver) clearInFlight() { - d.mu.Lock() - d.inFlight = false - d.mu.Unlock() -} - -func (d *ManagedAgentDriver) now() time.Time { - if d.Now != nil { - return d.Now().UTC() - } - return time.Now().UTC() + return drive.NewMemoryManagedWakeLedger() } func ManagedWakeCandidatesFromEvents(principal string, events []eventmodel.EventEnvelope) []ManagedWakeCandidate { - var out []ManagedWakeCandidate - for _, env := range events { - if string(env.Event.Audience) != principal { - continue - } - reason, _ := env.Meta["presentation_hint"].(string) - if reason == "" { - continue - } - body, _ := eventmodel.PayloadNarrative(env.Event.Payload)["body"].(string) - out = append(out, ManagedWakeCandidate{ - Principal: principal, - DerivedEventID: env.Event.ID, - BodyDigest: managedBodyDigest(body), - Reason: reason, - }) - } - return out -} - -func managedBodyDigest(body string) string { - sum := sha256.Sum256([]byte(body)) - return "sha256:" + hex.EncodeToString(sum[:]) -} - -func managedWakeKey(candidate ManagedWakeCandidate) string { - return candidate.Principal + "|" + candidate.DerivedEventID + "|" + candidate.BodyDigest + return drive.ManagedWakeCandidatesFromEvents(principal, events) } diff --git a/harness/internal/driver/managed_runtime.go b/harness/internal/driver/managed_runtime.go index df2ccf4b..cba74852 100644 --- a/harness/internal/driver/managed_runtime.go +++ b/harness/internal/driver/managed_runtime.go @@ -1,355 +1,24 @@ package driver import ( - "bufio" - "bytes" "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "time" - "github.com/mnemon-dev/mnemon/harness/internal/codexapp" - "github.com/mnemon-dev/mnemon/harness/internal/contract" - "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" + "github.com/mnemon-dev/mnemon/harness/internal/drive" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" ) -type HTTPRenderClient struct { - BaseURL string - Token string - Principal contract.ActorID - HTTP *http.Client -} - -func (c HTTPRenderClient) Render(ctx context.Context, req presentation.Request) (presentation.Response, error) { - base := strings.TrimRight(strings.TrimSpace(c.BaseURL), "/") - if base == "" { - return presentation.Response{}, fmt.Errorf("render client requires base URL") - } - req.Principal = c.Principal - body, err := json.Marshal(req) - if err != nil { - return presentation.Response{}, err - } - httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/render", bytes.NewReader(body)) - if err != nil { - return presentation.Response{}, err - } - if strings.TrimSpace(c.Token) != "" { - httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.Token)) - } else { - httpReq.Header.Set(access.PrincipalHeader, string(c.Principal)) - } - httpReq.Header.Set("Content-Type", "application/json") - client := c.HTTP - if client == nil { - client = http.DefaultClient - } - resp, err := client.Do(httpReq) - if err != nil { - return presentation.Response{}, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - data, _ := io.ReadAll(resp.Body) - return presentation.Response{}, fmt.Errorf("render failed: %s: %s", resp.Status, strings.TrimSpace(string(data))) - } - var out presentation.Response - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return presentation.Response{}, err - } - return out, nil -} +type HTTPRenderClient = drive.HTTPRenderClient +type FileManagedWakeLedger = drive.FileManagedWakeLedger +type CodexAppServerTurnClient = drive.CodexAppServerTurnClient func ManagedWakeCandidatesFromRender(principal string, resp presentation.Response) []ManagedWakeCandidate { - candidates := ManagedWakeCandidatesFromEvents(principal, resp.Events) - for i := range candidates { - candidates[i].RenderAuditID = resp.AuditID - candidates[i].RenderBodyDigest = resp.BodyDigest - } - return candidates -} - -type FileManagedWakeLedger struct { - path string - mu sync.Mutex - loaded bool - loadErr error - seen map[string]ManagedWakeRecord + return drive.ManagedWakeCandidatesFromRender(principal, resp) } func NewFileManagedWakeLedger(path string) *FileManagedWakeLedger { - return &FileManagedWakeLedger{path: path, seen: map[string]ManagedWakeRecord{}} -} - -func (l *FileManagedWakeLedger) Seen(candidate ManagedWakeCandidate) bool { - l.mu.Lock() - defer l.mu.Unlock() - l.loadLocked() - _, ok := l.seen[managedWakeKey(candidate)] - return ok -} - -func (l *FileManagedWakeLedger) Record(record ManagedWakeRecord) error { - l.mu.Lock() - defer l.mu.Unlock() - l.loadLocked() - if l.loadErr != nil { - return l.loadErr - } - if strings.TrimSpace(l.path) == "" { - return fmt.Errorf("managed wake ledger path is required") - } - if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { - return err - } - f, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) - if err != nil { - return err - } - enc := json.NewEncoder(f) - if err := enc.Encode(record); err != nil { - _ = f.Close() - return err - } - if err := f.Close(); err != nil { - return err - } - l.seen[managedWakeKey(ManagedWakeCandidate{ - Principal: record.Principal, - DerivedEventID: record.DerivedEventID, - BodyDigest: record.BodyDigest, - })] = record - return nil -} - -func (l *FileManagedWakeLedger) loadLocked() { - if l.loaded { - return - } - l.loaded = true - if strings.TrimSpace(l.path) == "" { - l.loadErr = fmt.Errorf("managed wake ledger path is required") - return - } - f, err := os.Open(l.path) - if errors.Is(err, os.ErrNotExist) { - return - } - if err != nil { - l.loadErr = err - return - } - defer f.Close() - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } - var record ManagedWakeRecord - if err := json.Unmarshal([]byte(line), &record); err != nil { - l.loadErr = err - return - } - l.seen[managedWakeKey(ManagedWakeCandidate{ - Principal: record.Principal, - DerivedEventID: record.DerivedEventID, - BodyDigest: record.BodyDigest, - })] = record - } - if err := scanner.Err(); err != nil { - l.loadErr = err - } -} - -type CodexAppServerTurnClient struct { - Principal string - Command string - Workspace string - Env []string - TurnTimeout time.Duration - RequestTimeout time.Duration - ClientName string - ClientVersion string -} - -func (c CodexAppServerTurnClient) StartTurn(ctx context.Context, query string) (ManagedTurnResult, error) { - return c.StartTurnWithTrace(ctx, query, nil) -} - -func (c CodexAppServerTurnClient) StartTurnWithTrace(ctx context.Context, query string, sink ManagedTurnTraceSink) (ManagedTurnResult, error) { - if strings.TrimSpace(query) != ManagedWakeQuery { - return ManagedTurnResult{}, fmt.Errorf("managed codex appserver client only accepts %q queries", ManagedWakeQuery) - } - command := strings.TrimSpace(c.Command) - if command == "" { - command = "codex" - } - workspace := strings.TrimSpace(c.Workspace) - if workspace == "" { - workspace = "." - } - requestTimeout := c.RequestTimeout - if requestTimeout <= 0 { - requestTimeout = 30 * time.Second - } - turnTimeout := c.TurnTimeout - if turnTimeout <= 0 { - turnTimeout = 5 * time.Minute - } - clientName := strings.TrimSpace(c.ClientName) - if clientName == "" { - clientName = "mnemond-managed-agent" - } - clientVersion := strings.TrimSpace(c.ClientVersion) - if clientVersion == "" { - clientVersion = "dev" - } - server := codexapp.New(command, workspace) - if len(c.Env) > 0 { - server.SetEnv(c.Env) - } - if err := server.Start(); err != nil { - return ManagedTurnResult{}, err - } - defer server.Close() - if _, err := server.Request("initialize", map[string]any{ - "clientInfo": map[string]any{"name": clientName, "version": clientVersion}, - "capabilities": codexAppServerCapabilities(), - }, requestTimeout); err != nil { - return ManagedTurnResult{}, fmt.Errorf("initialize: %w", err) - } - threadParams := map[string]any{ - "cwd": workspace, - "approvalPolicy": "never", - "sandbox": "danger-full-access", - "ephemeral": true, - } - thread, err := server.Request("thread/start", threadParams, requestTimeout) - if err != nil { - return ManagedTurnResult{}, fmt.Errorf("thread/start: %w", err) - } - threadID := codexapp.ThreadID(thread) - if threadID == "" { - return ManagedTurnResult{}, fmt.Errorf("thread/start returned no thread id") - } - additionalContext, err := CodexAppServerAdditionalContext(ctx, workspace, c.Env, ManagedWakeQuery) - if err != nil { - return ManagedTurnResult{}, fmt.Errorf("load hook context: %w", err) - } - turnParams := map[string]any{ - "threadId": threadID, - "input": []map[string]any{{"type": "text", "text": ManagedWakeQuery}}, - "cwd": workspace, - "approvalPolicy": "never", - "sandboxPolicy": map[string]any{"type": "dangerFullAccess"}, - } - if len(additionalContext) > 0 { - turnParams["additionalContext"] = additionalContext - } - before := server.NotificationCount() - if _, err := server.Request("turn/start", turnParams, requestTimeout); err != nil { - return ManagedTurnResult{}, fmt.Errorf("turn/start: %w", err) - } - emitTrace := func(notifications []map[string]any) { - if sink == nil { - return - } - for _, event := range ManagedTurnTraceEventsFromCodexNotifications(c.Principal, notifications) { - sink.OnManagedTurnTrace(event) - } - } - if _, err := server.WaitNotificationWithCallback("turn/completed", turnTimeout, before, emitTrace); err != nil { - text := codexapp.CombinedText(server.NotificationsSince(before)) - return ManagedTurnResult{TurnID: threadID, Status: "failed", FinalAnswer: text}, fmt.Errorf("wait turn/completed: %w", err) - } - notifications := server.NotificationsSince(before) - answer := codexapp.FinalAnswer(notifications) - if answer == "" { - answer = codexapp.CombinedText(notifications) - } - return ManagedTurnResult{TurnID: threadID, Status: "completed", FinalAnswer: answer}, nil + return drive.NewFileManagedWakeLedger(path) } func CodexAppServerAdditionalContext(ctx context.Context, workspace string, env []string, query string) (map[string]any, error) { - out := map[string]any{} - for _, lifecycle := range []string{"prime", "remind"} { - message, err := runCodexLifecycleHook(ctx, workspace, env, lifecycle, query) - if err != nil { - return nil, err - } - if strings.TrimSpace(message) == "" { - continue - } - out["mnemon.hook."+lifecycle] = map[string]any{ - "kind": "application", - "value": message, - } - } - return out, nil -} - -func runCodexLifecycleHook(ctx context.Context, workspace string, env []string, lifecycle string, query string) (string, error) { - hook := filepath.Join(workspace, ".codex", "hooks", "mnemon-r1", lifecycle+".sh") - if _, err := os.Stat(hook); errors.Is(err, os.ErrNotExist) { - return "", nil - } else if err != nil { - return "", err - } - stdin := "{}" - if lifecycle == "remind" { - raw, err := json.Marshal(map[string]string{"prompt": query}) - if err != nil { - return "", err - } - stdin = string(raw) - } - cmd := exec.CommandContext(ctx, "bash", hook) - cmd.Dir = workspace - if len(env) > 0 { - cmd.Env = append([]string(nil), env...) - } - cmd.Stdin = strings.NewReader(stdin) - var stdout bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - detail := strings.TrimSpace(stderr.String()) - if detail != "" { - return "", fmt.Errorf("%s hook failed: %w: %s", lifecycle, err, detail) - } - return "", fmt.Errorf("%s hook failed: %w", lifecycle, err) - } - return codexHookSystemMessage(stdout.String()), nil -} - -func codexHookSystemMessage(raw string) string { - text := strings.TrimSpace(raw) - if text == "" { - return "" - } - var envelope map[string]any - if err := json.Unmarshal([]byte(text), &envelope); err == nil { - if message, ok := envelope["systemMessage"].(string); ok { - return strings.TrimSpace(message) - } - } - return text -} - -func codexAppServerCapabilities() map[string]any { - return map[string]any{ - "experimentalApi": true, - "requestAttestation": false, - } + return drive.CodexAppServerAdditionalContext(ctx, workspace, env, query) } From e8a5c1b4587b40b617dff9981e4c4e1eea99fcb4 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:20:34 +0800 Subject: [PATCH 003/117] refactor: move multica registry to surface package Move Multica participant registry state into the surface/multica adapter package while keeping driver facade functions for existing command and runtime callers. Validation: go test ./harness/internal/surface/multica ./harness/internal/driver ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance --- harness/internal/driver/multica_registry.go | 82 ++-------------- harness/internal/surface/multica/registry.go | 94 +++++++++++++++++++ .../internal/surface/multica/registry_test.go | 60 ++++++++++++ 3 files changed, 163 insertions(+), 73 deletions(-) create mode 100644 harness/internal/surface/multica/registry.go create mode 100644 harness/internal/surface/multica/registry_test.go diff --git a/harness/internal/driver/multica_registry.go b/harness/internal/driver/multica_registry.go index 2086d032..163ca649 100644 --- a/harness/internal/driver/multica_registry.go +++ b/harness/internal/driver/multica_registry.go @@ -1,94 +1,30 @@ package driver import ( - "encoding/json" - "errors" - "os" - "path/filepath" - "strings" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) -const MulticaDefaultRegistryRelPath = ".mnemon/harness/multica/registry.json" +const MulticaDefaultRegistryRelPath = multicasurface.MulticaDefaultRegistryRelPath -type MulticaRegistry struct { - SchemaVersion int `json:"schema_version"` - WorkspaceID string `json:"workspace_id"` - RuntimeProfileID string `json:"runtime_profile_id"` - RuntimeID string `json:"runtime_id"` - Participants []MulticaParticipantRecord `json:"participants"` -} - -type MulticaParticipantRecord struct { - Principal string `json:"principal"` - AgentName string `json:"multica_agent_name"` - AgentID string `json:"multica_agent_id"` - Role string `json:"role"` -} +type MulticaRegistry = multicasurface.MulticaRegistry +type MulticaParticipantRecord = multicasurface.MulticaParticipantRecord func MulticaRegistryPath(root, explicit string) string { - if strings.TrimSpace(explicit) != "" { - return strings.TrimSpace(explicit) - } - root = strings.TrimSpace(root) - if root == "" { - root = "." - } - return filepath.Join(root, MulticaDefaultRegistryRelPath) + return multicasurface.MulticaRegistryPath(root, explicit) } func LoadMulticaRegistry(path string) (MulticaRegistry, bool, error) { - data, err := os.ReadFile(path) - if errors.Is(err, os.ErrNotExist) { - return MulticaRegistry{}, false, nil - } - if err != nil { - return MulticaRegistry{}, false, err - } - var reg MulticaRegistry - if err := json.Unmarshal(data, ®); err != nil { - return MulticaRegistry{}, false, err - } - return reg, true, nil + return multicasurface.LoadMulticaRegistry(path) } func SaveMulticaRegistry(path string, reg MulticaRegistry) error { - if reg.SchemaVersion == 0 { - reg.SchemaVersion = 1 - } - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err - } - data, err := json.MarshalIndent(reg, "", " ") - if err != nil { - return err - } - data = append(data, '\n') - return os.WriteFile(path, data, 0o600) + return multicasurface.SaveMulticaRegistry(path, reg) } func DefaultMulticaParticipantRecords(prefix string) []MulticaParticipantRecord { - prefix = strings.TrimSpace(prefix) - if prefix == "" { - prefix = "mnemon" - } - roles := []string{"planner", "researcher", "implementer", "reviewer", "integrator"} - out := make([]MulticaParticipantRecord, 0, len(roles)) - for _, role := range roles { - out = append(out, MulticaParticipantRecord{ - Principal: role + "@team", - AgentName: prefix + "-" + role, - Role: role, - }) - } - return out + return multicasurface.DefaultMulticaParticipantRecords(prefix) } func UpsertMulticaParticipantRecord(records []MulticaParticipantRecord, next MulticaParticipantRecord) []MulticaParticipantRecord { - for i := range records { - if records[i].Principal == next.Principal || records[i].AgentName == next.AgentName { - records[i] = next - return records - } - } - return append(records, next) + return multicasurface.UpsertMulticaParticipantRecord(records, next) } diff --git a/harness/internal/surface/multica/registry.go b/harness/internal/surface/multica/registry.go new file mode 100644 index 00000000..881fefb3 --- /dev/null +++ b/harness/internal/surface/multica/registry.go @@ -0,0 +1,94 @@ +package multica + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" +) + +const MulticaDefaultRegistryRelPath = ".mnemon/harness/multica/registry.json" + +type MulticaRegistry struct { + SchemaVersion int `json:"schema_version"` + WorkspaceID string `json:"workspace_id"` + RuntimeProfileID string `json:"runtime_profile_id"` + RuntimeID string `json:"runtime_id"` + Participants []MulticaParticipantRecord `json:"participants"` +} + +type MulticaParticipantRecord struct { + Principal string `json:"principal"` + AgentName string `json:"multica_agent_name"` + AgentID string `json:"multica_agent_id"` + Role string `json:"role"` +} + +func MulticaRegistryPath(root, explicit string) string { + if strings.TrimSpace(explicit) != "" { + return strings.TrimSpace(explicit) + } + root = strings.TrimSpace(root) + if root == "" { + root = "." + } + return filepath.Join(root, MulticaDefaultRegistryRelPath) +} + +func LoadMulticaRegistry(path string) (MulticaRegistry, bool, error) { + data, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return MulticaRegistry{}, false, nil + } + if err != nil { + return MulticaRegistry{}, false, err + } + var reg MulticaRegistry + if err := json.Unmarshal(data, ®); err != nil { + return MulticaRegistry{}, false, err + } + return reg, true, nil +} + +func SaveMulticaRegistry(path string, reg MulticaRegistry) error { + if reg.SchemaVersion == 0 { + reg.SchemaVersion = 1 + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(reg, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + return os.WriteFile(path, data, 0o600) +} + +func DefaultMulticaParticipantRecords(prefix string) []MulticaParticipantRecord { + prefix = strings.TrimSpace(prefix) + if prefix == "" { + prefix = "mnemon" + } + roles := []string{"planner", "researcher", "implementer", "reviewer", "integrator"} + out := make([]MulticaParticipantRecord, 0, len(roles)) + for _, role := range roles { + out = append(out, MulticaParticipantRecord{ + Principal: role + "@team", + AgentName: prefix + "-" + role, + Role: role, + }) + } + return out +} + +func UpsertMulticaParticipantRecord(records []MulticaParticipantRecord, next MulticaParticipantRecord) []MulticaParticipantRecord { + for i := range records { + if records[i].Principal == next.Principal || records[i].AgentName == next.AgentName { + records[i] = next + return records + } + } + return append(records, next) +} diff --git a/harness/internal/surface/multica/registry_test.go b/harness/internal/surface/multica/registry_test.go new file mode 100644 index 00000000..598b3c25 --- /dev/null +++ b/harness/internal/surface/multica/registry_test.go @@ -0,0 +1,60 @@ +package multica + +import ( + "path/filepath" + "testing" +) + +func TestDefaultParticipantRecords(t *testing.T) { + got := DefaultMulticaParticipantRecords("mnemon") + if len(got) != 5 { + t.Fatalf("participants len = %d", len(got)) + } + want := map[string]string{ + "planner@team": "mnemon-planner", + "researcher@team": "mnemon-researcher", + "implementer@team": "mnemon-implementer", + "reviewer@team": "mnemon-reviewer", + "integrator@team": "mnemon-integrator", + } + for _, participant := range got { + if want[participant.Principal] != participant.AgentName { + t.Fatalf("unexpected participant: %+v", participant) + } + } +} + +func TestRegistryRoundTrip(t *testing.T) { + path := filepath.Join(t.TempDir(), "nested", "registry.json") + _, ok, err := LoadMulticaRegistry(path) + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("missing registry should return ok=false") + } + want := MulticaRegistry{ + WorkspaceID: "ws-1", + RuntimeProfileID: "profile-1", + RuntimeID: "runtime-1", + Participants: []MulticaParticipantRecord{{ + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-1", + Role: "planner", + }}, + } + if err := SaveMulticaRegistry(path, want); err != nil { + t.Fatal(err) + } + got, ok, err := LoadMulticaRegistry(path) + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("saved registry should exist") + } + if got.SchemaVersion != 1 || got.WorkspaceID != "ws-1" || len(got.Participants) != 1 || got.Participants[0].AgentID != "agent-1" { + t.Fatalf("registry mismatch: %+v", got) + } +} From 99cb49e108ef27783159ded7c256193a564806d2 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:24:02 +0800 Subject: [PATCH 004/117] refactor: move multica hub metadata to surface package Move Multica hub metadata, assignment fingerprints, and projection ledger state into the surface/multica adapter package while keeping driver facade helpers for existing issue types. Validation: go test ./harness/internal/surface/multica ./harness/internal/driver ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance ./harness/cmd/mnemon-harness --- harness/internal/driver/multica_hub.go | 448 ++---------------- harness/internal/surface/multica/hub.go | 450 +++++++++++++++++++ harness/internal/surface/multica/hub_test.go | 99 ++++ 3 files changed, 587 insertions(+), 410 deletions(-) create mode 100644 harness/internal/surface/multica/hub.go create mode 100644 harness/internal/surface/multica/hub_test.go diff --git a/harness/internal/driver/multica_hub.go b/harness/internal/driver/multica_hub.go index 23a5f9dd..fbab8089 100644 --- a/harness/internal/driver/multica_hub.go +++ b/harness/internal/driver/multica_hub.go @@ -1,316 +1,54 @@ package driver import ( - "bufio" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - "time" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) const ( - MulticaHubBackend = "multica" - - MulticaDefaultHubLedgerRelPath = ".mnemon/harness/multica/hub-ledger.jsonl" - - MulticaMetadataSchemaVersion = "mnemon.schema_version" - MulticaMetadataHubBackend = "mnemon.hub_backend" - MulticaMetadataKind = "mnemon.kind" - MulticaMetadataSessionID = "mnemon.session_id" - MulticaMetadataCorrelationID = "mnemon.correlation_id" - MulticaMetadataEventID = "mnemon.event_id" - MulticaMetadataEventType = "mnemon.event_type" - MulticaMetadataEventPhase = "mnemon.event_phase" - MulticaMetadataAssignmentID = "mnemon.assignment_id" - MulticaMetadataAssignmentFingerprint = "mnemon.assignment_fingerprint" - MulticaMetadataPrincipal = "mnemon.principal" - MulticaMetadataSourceIssueID = "mnemon.source_issue_id" - MulticaMetadataRootIssueID = "mnemon.root_issue_id" - MulticaMetadataProjectionOwner = "mnemon.projection_owner" - MulticaMetadataMulticaAgentID = "mnemon.multica_agent_id" - MulticaMetadataProjectedAt = "mnemon.projected_at" - MulticaMetadataEnvelopeDigest = "mnemon.envelope_digest" - MulticaHubKindSession = "session_mailbox" - MulticaHubKindAssignmentMailbox = "assignment_mailbox" - MulticaHubKindFeedbackCarrier = "feedback_carrier" - MulticaHubKindAssignmentProjectionOld = "assignment_projection" + MulticaHubBackend = multicasurface.MulticaHubBackend + + MulticaDefaultHubLedgerRelPath = multicasurface.MulticaDefaultHubLedgerRelPath + + MulticaMetadataSchemaVersion = multicasurface.MulticaMetadataSchemaVersion + MulticaMetadataHubBackend = multicasurface.MulticaMetadataHubBackend + MulticaMetadataKind = multicasurface.MulticaMetadataKind + MulticaMetadataSessionID = multicasurface.MulticaMetadataSessionID + MulticaMetadataCorrelationID = multicasurface.MulticaMetadataCorrelationID + MulticaMetadataEventID = multicasurface.MulticaMetadataEventID + MulticaMetadataEventType = multicasurface.MulticaMetadataEventType + MulticaMetadataEventPhase = multicasurface.MulticaMetadataEventPhase + MulticaMetadataAssignmentID = multicasurface.MulticaMetadataAssignmentID + MulticaMetadataAssignmentFingerprint = multicasurface.MulticaMetadataAssignmentFingerprint + MulticaMetadataPrincipal = multicasurface.MulticaMetadataPrincipal + MulticaMetadataSourceIssueID = multicasurface.MulticaMetadataSourceIssueID + MulticaMetadataRootIssueID = multicasurface.MulticaMetadataRootIssueID + MulticaMetadataProjectionOwner = multicasurface.MulticaMetadataProjectionOwner + MulticaMetadataMulticaAgentID = multicasurface.MulticaMetadataMulticaAgentID + MulticaMetadataProjectedAt = multicasurface.MulticaMetadataProjectedAt + MulticaMetadataEnvelopeDigest = multicasurface.MulticaMetadataEnvelopeDigest + MulticaHubKindSession = multicasurface.MulticaHubKindSession + MulticaHubKindAssignmentMailbox = multicasurface.MulticaHubKindAssignmentMailbox + MulticaHubKindFeedbackCarrier = multicasurface.MulticaHubKindFeedbackCarrier + MulticaHubKindAssignmentProjectionOld = multicasurface.MulticaHubKindAssignmentProjectionOld ) -type MulticaHubMetadata struct { - SchemaVersion string - HubBackend string - Kind string - SessionID string - CorrelationID string - EventID string - EventType string - EventPhase string - AssignmentID string - AssignmentFingerprint string - Principal string - SourceIssueID string - RootIssueID string - ProjectionOwner string - MulticaAgentID string - ProjectedAt string - EnvelopeDigest string -} - -type MulticaAssignmentFingerprintInput struct { - AssignmentID string `json:"assignment_id,omitempty"` - Assignee string `json:"assignee,omitempty"` - Scope string `json:"scope,omitempty"` - ExpectedWork string `json:"expected_work,omitempty"` - ExpectedFeedback string `json:"expected_feedback,omitempty"` - SignalRef string `json:"signal_ref,omitempty"` - ContextRefs []string `json:"context_refs,omitempty"` - EvidenceRefs []string `json:"evidence_refs,omitempty"` - CorrelationID string `json:"correlation_id,omitempty"` -} - -type MulticaHubLedgerRecord struct { - SchemaVersion int `json:"schema_version"` - Kind string `json:"kind"` - Source MulticaHubLedgerSource `json:"source"` - Target MulticaHubLedgerTarget `json:"target"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at,omitempty"` -} - -type MulticaHubLedgerSource struct { - SessionID string `json:"session_id,omitempty"` - CorrelationID string `json:"correlation_id,omitempty"` - EventID string `json:"event_id,omitempty"` - AssignmentID string `json:"assignment_id,omitempty"` - AssignmentFingerprint string `json:"assignment_fingerprint,omitempty"` - Principal string `json:"principal,omitempty"` - ProjectionKind string `json:"projection_kind,omitempty"` -} - -type MulticaHubLedgerTarget struct { - RootIssueID string `json:"root_issue_id,omitempty"` - ChildIssueID string `json:"child_issue_id,omitempty"` - CommentID string `json:"comment_id,omitempty"` - Status string `json:"status,omitempty"` -} - -type FileMulticaHubLedger struct { - path string - loaded bool - loadErr error - records []MulticaHubLedgerRecord -} +type MulticaHubMetadata = multicasurface.MulticaHubMetadata +type MulticaAssignmentFingerprintInput = multicasurface.MulticaAssignmentFingerprintInput +type MulticaHubLedgerRecord = multicasurface.MulticaHubLedgerRecord +type MulticaHubLedgerSource = multicasurface.MulticaHubLedgerSource +type MulticaHubLedgerTarget = multicasurface.MulticaHubLedgerTarget +type FileMulticaHubLedger = multicasurface.FileMulticaHubLedger func MulticaHubLedgerPath(root, explicit string) string { - if strings.TrimSpace(explicit) != "" { - return strings.TrimSpace(explicit) - } - root = strings.TrimSpace(root) - if root == "" { - root = "." - } - return filepath.Join(root, MulticaDefaultHubLedgerRelPath) + return multicasurface.MulticaHubLedgerPath(root, explicit) } func NewFileMulticaHubLedger(path string) *FileMulticaHubLedger { - return &FileMulticaHubLedger{path: strings.TrimSpace(path)} -} - -func (l *FileMulticaHubLedger) Records() ([]MulticaHubLedgerRecord, error) { - if err := l.load(); err != nil { - return nil, err - } - return append([]MulticaHubLedgerRecord(nil), l.records...), nil -} - -func (l *FileMulticaHubLedger) Find(kind string, source MulticaHubLedgerSource) (MulticaHubLedgerRecord, bool, error) { - if err := l.load(); err != nil { - return MulticaHubLedgerRecord{}, false, err - } - want := multicaHubLedgerKey(kind, source) - for i := len(l.records) - 1; i >= 0; i-- { - rec := l.records[i] - if multicaHubLedgerKey(rec.Kind, rec.Source) == want { - return rec, true, nil - } - } - return MulticaHubLedgerRecord{}, false, nil -} - -func (l *FileMulticaHubLedger) Record(record MulticaHubLedgerRecord) error { - if err := l.load(); err != nil { - return err - } - if strings.TrimSpace(l.path) == "" { - return fmt.Errorf("multica hub ledger path is required") - } - key := multicaHubLedgerKey(record.Kind, record.Source) - for _, existing := range l.records { - if multicaHubLedgerKey(existing.Kind, existing.Source) == key && existing.Target == record.Target { - return nil - } - } - if record.SchemaVersion == 0 { - record.SchemaVersion = 1 - } - now := time.Now().UTC().Format(time.RFC3339) - if strings.TrimSpace(record.CreatedAt) == "" { - record.CreatedAt = now - } - for i := len(l.records) - 1; i >= 0; i-- { - if multicaHubLedgerKey(l.records[i].Kind, l.records[i].Source) == key { - record.CreatedAt = l.records[i].CreatedAt - record.UpdatedAt = now - break - } - } - if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { - return err - } - f, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) - if err != nil { - return err - } - enc := json.NewEncoder(f) - if err := enc.Encode(record); err != nil { - _ = f.Close() - return err - } - if err := f.Close(); err != nil { - return err - } - l.upsertLoaded(record) - return nil -} - -func (l *FileMulticaHubLedger) load() error { - if l.loaded { - return l.loadErr - } - l.loaded = true - if strings.TrimSpace(l.path) == "" { - l.loadErr = fmt.Errorf("multica hub ledger path is required") - return l.loadErr - } - f, err := os.Open(l.path) - if os.IsNotExist(err) { - return nil - } - if err != nil { - l.loadErr = err - return err - } - defer f.Close() - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } - var record MulticaHubLedgerRecord - if err := json.Unmarshal([]byte(line), &record); err != nil { - l.loadErr = err - return err - } - l.upsertLoaded(record) - } - if err := scanner.Err(); err != nil { - l.loadErr = err - return err - } - return nil -} - -func (l *FileMulticaHubLedger) upsertLoaded(record MulticaHubLedgerRecord) { - key := multicaHubLedgerKey(record.Kind, record.Source) - for i := range l.records { - if multicaHubLedgerKey(l.records[i].Kind, l.records[i].Source) == key { - l.records[i] = record - return - } - } - l.records = append(l.records, record) -} - -func multicaHubLedgerKey(kind string, source MulticaHubLedgerSource) string { - return strings.Join([]string{ - strings.TrimSpace(kind), - strings.TrimSpace(source.SessionID), - strings.TrimSpace(source.CorrelationID), - strings.TrimSpace(source.EventID), - strings.TrimSpace(source.AssignmentID), - strings.TrimSpace(source.AssignmentFingerprint), - strings.TrimSpace(source.Principal), - strings.TrimSpace(source.ProjectionKind), - }, "\x00") + return multicasurface.NewFileMulticaHubLedger(path) } func ParseMulticaHubMetadata(raw map[string]any) MulticaHubMetadata { - meta := NormalizeMulticaMetadata(raw) - return MulticaHubMetadata{ - SchemaVersion: meta[MulticaMetadataSchemaVersion], - HubBackend: meta[MulticaMetadataHubBackend], - Kind: meta[MulticaMetadataKind], - SessionID: meta[MulticaMetadataSessionID], - CorrelationID: meta[MulticaMetadataCorrelationID], - EventID: meta[MulticaMetadataEventID], - EventType: meta[MulticaMetadataEventType], - EventPhase: meta[MulticaMetadataEventPhase], - AssignmentID: meta[MulticaMetadataAssignmentID], - AssignmentFingerprint: meta[MulticaMetadataAssignmentFingerprint], - Principal: meta[MulticaMetadataPrincipal], - SourceIssueID: meta[MulticaMetadataSourceIssueID], - RootIssueID: meta[MulticaMetadataRootIssueID], - ProjectionOwner: meta[MulticaMetadataProjectionOwner], - MulticaAgentID: meta[MulticaMetadataMulticaAgentID], - ProjectedAt: meta[MulticaMetadataProjectedAt], - EnvelopeDigest: meta[MulticaMetadataEnvelopeDigest], - } -} - -func (m MulticaHubMetadata) Map() map[string]string { - out := map[string]string{} - add := func(key, value string) { - value = strings.TrimSpace(value) - if value != "" { - out[key] = value - } - } - add(MulticaMetadataSchemaVersion, firstNonEmptyString(m.SchemaVersion, "1")) - add(MulticaMetadataHubBackend, firstNonEmptyString(m.HubBackend, MulticaHubBackend)) - add(MulticaMetadataKind, m.Kind) - add(MulticaMetadataSessionID, m.SessionID) - add(MulticaMetadataCorrelationID, m.CorrelationID) - add(MulticaMetadataEventID, m.EventID) - add(MulticaMetadataEventType, m.EventType) - add(MulticaMetadataEventPhase, m.EventPhase) - add(MulticaMetadataAssignmentID, m.AssignmentID) - add(MulticaMetadataAssignmentFingerprint, m.AssignmentFingerprint) - add(MulticaMetadataPrincipal, m.Principal) - add(MulticaMetadataSourceIssueID, m.SourceIssueID) - add(MulticaMetadataRootIssueID, m.RootIssueID) - add(MulticaMetadataProjectionOwner, m.ProjectionOwner) - add(MulticaMetadataMulticaAgentID, m.MulticaAgentID) - add(MulticaMetadataProjectedAt, m.ProjectedAt) - add(MulticaMetadataEnvelopeDigest, m.EnvelopeDigest) - return out -} - -func (m MulticaHubMetadata) IsAssignmentMailbox() bool { - if strings.TrimSpace(m.HubBackend) != "" && m.HubBackend != MulticaHubBackend { - return false - } - switch strings.TrimSpace(m.Kind) { - case MulticaHubKindAssignmentMailbox, MulticaHubKindAssignmentProjectionOld: - return strings.TrimSpace(m.AssignmentID) != "" || strings.TrimSpace(m.AssignmentFingerprint) != "" - default: - return false - } + return multicasurface.ParseMulticaHubMetadata(raw) } func MulticaIssueHubMetadata(issue MulticaIssue) MulticaHubMetadata { @@ -322,123 +60,13 @@ func IsMulticaAssignmentMailboxIssue(issue MulticaIssue) bool { } func NormalizeMulticaMetadata(raw any) map[string]string { - out := map[string]string{} - merge := func(key string, value any) { - key = strings.TrimSpace(key) - if key == "" { - return - } - if key == "metadata" { - for k, v := range NormalizeMulticaMetadata(value) { - out[k] = v - } - return - } - out[key] = multicaMetadataString(value) - } - switch v := raw.(type) { - case nil: - case map[string]string: - for key, value := range v { - merge(key, value) - } - case map[string]any: - if key := firstNonEmptyString(anyString(v["key"]), anyString(v["name"])); key != "" { - merge(key, firstPresent(v, "value", "val", "metadata_value")) - return out - } - for key, value := range v { - merge(key, value) - } - case []any: - for _, item := range v { - m, ok := item.(map[string]any) - if !ok { - continue - } - key := firstNonEmptyString(anyString(m["key"]), anyString(m["name"])) - if key == "" { - for k, value := range NormalizeMulticaMetadata(m) { - out[k] = value - } - continue - } - merge(key, firstPresent(m, "value", "val", "metadata_value")) - } - } - return out + return multicasurface.NormalizeMulticaMetadata(raw) } func MulticaAssignmentFingerprint(input MulticaAssignmentFingerprintInput) string { - input.AssignmentID = strings.TrimSpace(input.AssignmentID) - input.Assignee = strings.TrimSpace(input.Assignee) - input.Scope = strings.TrimSpace(input.Scope) - input.ExpectedWork = strings.TrimSpace(input.ExpectedWork) - input.ExpectedFeedback = strings.TrimSpace(input.ExpectedFeedback) - input.SignalRef = strings.TrimSpace(input.SignalRef) - input.CorrelationID = strings.TrimSpace(input.CorrelationID) - input.ContextRefs = cleanMulticaRefs(input.ContextRefs) - input.EvidenceRefs = cleanMulticaRefs(input.EvidenceRefs) - sort.Strings(input.ContextRefs) - sort.Strings(input.EvidenceRefs) - data, _ := json.Marshal(input) - sum := sha256.Sum256(data) - return "sha256:" + hex.EncodeToString(sum[:]) + return multicasurface.MulticaAssignmentFingerprint(input) } func MulticaSessionID(rootIssueID string) string { - rootIssueID = strings.TrimSpace(rootIssueID) - if rootIssueID == "" { - return "" - } - return "multica:session:" + rootIssueID -} - -func multicaMetadataString(value any) string { - switch v := value.(type) { - case nil: - return "" - case string: - return strings.TrimSpace(v) - case json.Number: - return strings.TrimSpace(v.String()) - case bool: - if v { - return "true" - } - return "false" - case float64: - return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", v), "0"), ".") - default: - data, err := json.Marshal(v) - if err != nil { - return strings.TrimSpace(fmt.Sprint(v)) - } - return strings.TrimSpace(string(data)) - } -} - -func anyString(value any) string { - if s, ok := value.(string); ok { - return strings.TrimSpace(s) - } - return "" -} - -func firstPresent(m map[string]any, keys ...string) any { - for _, key := range keys { - if value, ok := m[key]; ok { - return value - } - } - return nil -} - -func firstNonEmptyString(values ...string) string { - for _, value := range values { - if strings.TrimSpace(value) != "" { - return strings.TrimSpace(value) - } - } - return "" + return multicasurface.MulticaSessionID(rootIssueID) } diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go new file mode 100644 index 00000000..a9283faf --- /dev/null +++ b/harness/internal/surface/multica/hub.go @@ -0,0 +1,450 @@ +package multica + +import ( + "bufio" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" +) + +const ( + MulticaHubBackend = "multica" + + MulticaDefaultHubLedgerRelPath = ".mnemon/harness/multica/hub-ledger.jsonl" + + MulticaMetadataSchemaVersion = "mnemon.schema_version" + MulticaMetadataHubBackend = "mnemon.hub_backend" + MulticaMetadataKind = "mnemon.kind" + MulticaMetadataSessionID = "mnemon.session_id" + MulticaMetadataCorrelationID = "mnemon.correlation_id" + MulticaMetadataEventID = "mnemon.event_id" + MulticaMetadataEventType = "mnemon.event_type" + MulticaMetadataEventPhase = "mnemon.event_phase" + MulticaMetadataAssignmentID = "mnemon.assignment_id" + MulticaMetadataAssignmentFingerprint = "mnemon.assignment_fingerprint" + MulticaMetadataPrincipal = "mnemon.principal" + MulticaMetadataSourceIssueID = "mnemon.source_issue_id" + MulticaMetadataRootIssueID = "mnemon.root_issue_id" + MulticaMetadataProjectionOwner = "mnemon.projection_owner" + MulticaMetadataMulticaAgentID = "mnemon.multica_agent_id" + MulticaMetadataProjectedAt = "mnemon.projected_at" + MulticaMetadataEnvelopeDigest = "mnemon.envelope_digest" + MulticaHubKindSession = "session_mailbox" + MulticaHubKindAssignmentMailbox = "assignment_mailbox" + MulticaHubKindFeedbackCarrier = "feedback_carrier" + MulticaHubKindAssignmentProjectionOld = "assignment_projection" +) + +type MulticaHubMetadata struct { + SchemaVersion string + HubBackend string + Kind string + SessionID string + CorrelationID string + EventID string + EventType string + EventPhase string + AssignmentID string + AssignmentFingerprint string + Principal string + SourceIssueID string + RootIssueID string + ProjectionOwner string + MulticaAgentID string + ProjectedAt string + EnvelopeDigest string +} + +type MulticaAssignmentFingerprintInput struct { + AssignmentID string `json:"assignment_id,omitempty"` + Assignee string `json:"assignee,omitempty"` + Scope string `json:"scope,omitempty"` + ExpectedWork string `json:"expected_work,omitempty"` + ExpectedFeedback string `json:"expected_feedback,omitempty"` + SignalRef string `json:"signal_ref,omitempty"` + ContextRefs []string `json:"context_refs,omitempty"` + EvidenceRefs []string `json:"evidence_refs,omitempty"` + CorrelationID string `json:"correlation_id,omitempty"` +} + +type MulticaHubLedgerRecord struct { + SchemaVersion int `json:"schema_version"` + Kind string `json:"kind"` + Source MulticaHubLedgerSource `json:"source"` + Target MulticaHubLedgerTarget `json:"target"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at,omitempty"` +} + +type MulticaHubLedgerSource struct { + SessionID string `json:"session_id,omitempty"` + CorrelationID string `json:"correlation_id,omitempty"` + EventID string `json:"event_id,omitempty"` + AssignmentID string `json:"assignment_id,omitempty"` + AssignmentFingerprint string `json:"assignment_fingerprint,omitempty"` + Principal string `json:"principal,omitempty"` + ProjectionKind string `json:"projection_kind,omitempty"` +} + +type MulticaHubLedgerTarget struct { + RootIssueID string `json:"root_issue_id,omitempty"` + ChildIssueID string `json:"child_issue_id,omitempty"` + CommentID string `json:"comment_id,omitempty"` + Status string `json:"status,omitempty"` +} + +type FileMulticaHubLedger struct { + path string + loaded bool + loadErr error + records []MulticaHubLedgerRecord +} + +func MulticaHubLedgerPath(root, explicit string) string { + if strings.TrimSpace(explicit) != "" { + return strings.TrimSpace(explicit) + } + root = strings.TrimSpace(root) + if root == "" { + root = "." + } + return filepath.Join(root, MulticaDefaultHubLedgerRelPath) +} + +func NewFileMulticaHubLedger(path string) *FileMulticaHubLedger { + return &FileMulticaHubLedger{path: strings.TrimSpace(path)} +} + +func (l *FileMulticaHubLedger) Records() ([]MulticaHubLedgerRecord, error) { + if err := l.load(); err != nil { + return nil, err + } + return append([]MulticaHubLedgerRecord(nil), l.records...), nil +} + +func (l *FileMulticaHubLedger) Find(kind string, source MulticaHubLedgerSource) (MulticaHubLedgerRecord, bool, error) { + if err := l.load(); err != nil { + return MulticaHubLedgerRecord{}, false, err + } + want := multicaHubLedgerKey(kind, source) + for i := len(l.records) - 1; i >= 0; i-- { + rec := l.records[i] + if multicaHubLedgerKey(rec.Kind, rec.Source) == want { + return rec, true, nil + } + } + return MulticaHubLedgerRecord{}, false, nil +} + +func (l *FileMulticaHubLedger) Record(record MulticaHubLedgerRecord) error { + if err := l.load(); err != nil { + return err + } + if strings.TrimSpace(l.path) == "" { + return fmt.Errorf("multica hub ledger path is required") + } + key := multicaHubLedgerKey(record.Kind, record.Source) + for _, existing := range l.records { + if multicaHubLedgerKey(existing.Kind, existing.Source) == key && existing.Target == record.Target { + return nil + } + } + if record.SchemaVersion == 0 { + record.SchemaVersion = 1 + } + now := time.Now().UTC().Format(time.RFC3339) + if strings.TrimSpace(record.CreatedAt) == "" { + record.CreatedAt = now + } + for i := len(l.records) - 1; i >= 0; i-- { + if multicaHubLedgerKey(l.records[i].Kind, l.records[i].Source) == key { + record.CreatedAt = l.records[i].CreatedAt + record.UpdatedAt = now + break + } + } + if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { + return err + } + f, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) + if err != nil { + return err + } + enc := json.NewEncoder(f) + if err := enc.Encode(record); err != nil { + _ = f.Close() + return err + } + if err := f.Close(); err != nil { + return err + } + l.upsertLoaded(record) + return nil +} + +func (l *FileMulticaHubLedger) load() error { + if l.loaded { + return l.loadErr + } + l.loaded = true + if strings.TrimSpace(l.path) == "" { + l.loadErr = fmt.Errorf("multica hub ledger path is required") + return l.loadErr + } + f, err := os.Open(l.path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + l.loadErr = err + return err + } + defer f.Close() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + var record MulticaHubLedgerRecord + if err := json.Unmarshal([]byte(line), &record); err != nil { + l.loadErr = err + return err + } + l.upsertLoaded(record) + } + if err := scanner.Err(); err != nil { + l.loadErr = err + return err + } + return nil +} + +func (l *FileMulticaHubLedger) upsertLoaded(record MulticaHubLedgerRecord) { + key := multicaHubLedgerKey(record.Kind, record.Source) + for i := range l.records { + if multicaHubLedgerKey(l.records[i].Kind, l.records[i].Source) == key { + l.records[i] = record + return + } + } + l.records = append(l.records, record) +} + +func multicaHubLedgerKey(kind string, source MulticaHubLedgerSource) string { + return strings.Join([]string{ + strings.TrimSpace(kind), + strings.TrimSpace(source.SessionID), + strings.TrimSpace(source.CorrelationID), + strings.TrimSpace(source.EventID), + strings.TrimSpace(source.AssignmentID), + strings.TrimSpace(source.AssignmentFingerprint), + strings.TrimSpace(source.Principal), + strings.TrimSpace(source.ProjectionKind), + }, "\x00") +} + +func ParseMulticaHubMetadata(raw map[string]any) MulticaHubMetadata { + meta := NormalizeMulticaMetadata(raw) + return MulticaHubMetadata{ + SchemaVersion: meta[MulticaMetadataSchemaVersion], + HubBackend: meta[MulticaMetadataHubBackend], + Kind: meta[MulticaMetadataKind], + SessionID: meta[MulticaMetadataSessionID], + CorrelationID: meta[MulticaMetadataCorrelationID], + EventID: meta[MulticaMetadataEventID], + EventType: meta[MulticaMetadataEventType], + EventPhase: meta[MulticaMetadataEventPhase], + AssignmentID: meta[MulticaMetadataAssignmentID], + AssignmentFingerprint: meta[MulticaMetadataAssignmentFingerprint], + Principal: meta[MulticaMetadataPrincipal], + SourceIssueID: meta[MulticaMetadataSourceIssueID], + RootIssueID: meta[MulticaMetadataRootIssueID], + ProjectionOwner: meta[MulticaMetadataProjectionOwner], + MulticaAgentID: meta[MulticaMetadataMulticaAgentID], + ProjectedAt: meta[MulticaMetadataProjectedAt], + EnvelopeDigest: meta[MulticaMetadataEnvelopeDigest], + } +} + +func (m MulticaHubMetadata) Map() map[string]string { + out := map[string]string{} + add := func(key, value string) { + value = strings.TrimSpace(value) + if value != "" { + out[key] = value + } + } + add(MulticaMetadataSchemaVersion, firstNonEmptyString(m.SchemaVersion, "1")) + add(MulticaMetadataHubBackend, firstNonEmptyString(m.HubBackend, MulticaHubBackend)) + add(MulticaMetadataKind, m.Kind) + add(MulticaMetadataSessionID, m.SessionID) + add(MulticaMetadataCorrelationID, m.CorrelationID) + add(MulticaMetadataEventID, m.EventID) + add(MulticaMetadataEventType, m.EventType) + add(MulticaMetadataEventPhase, m.EventPhase) + add(MulticaMetadataAssignmentID, m.AssignmentID) + add(MulticaMetadataAssignmentFingerprint, m.AssignmentFingerprint) + add(MulticaMetadataPrincipal, m.Principal) + add(MulticaMetadataSourceIssueID, m.SourceIssueID) + add(MulticaMetadataRootIssueID, m.RootIssueID) + add(MulticaMetadataProjectionOwner, m.ProjectionOwner) + add(MulticaMetadataMulticaAgentID, m.MulticaAgentID) + add(MulticaMetadataProjectedAt, m.ProjectedAt) + add(MulticaMetadataEnvelopeDigest, m.EnvelopeDigest) + return out +} + +func (m MulticaHubMetadata) IsAssignmentMailbox() bool { + if strings.TrimSpace(m.HubBackend) != "" && m.HubBackend != MulticaHubBackend { + return false + } + switch strings.TrimSpace(m.Kind) { + case MulticaHubKindAssignmentMailbox, MulticaHubKindAssignmentProjectionOld: + return strings.TrimSpace(m.AssignmentID) != "" || strings.TrimSpace(m.AssignmentFingerprint) != "" + default: + return false + } +} + +func NormalizeMulticaMetadata(raw any) map[string]string { + out := map[string]string{} + merge := func(key string, value any) { + key = strings.TrimSpace(key) + if key == "" { + return + } + if key == "metadata" { + for k, v := range NormalizeMulticaMetadata(value) { + out[k] = v + } + return + } + out[key] = multicaMetadataString(value) + } + switch v := raw.(type) { + case nil: + case map[string]string: + for key, value := range v { + merge(key, value) + } + case map[string]any: + if key := firstNonEmptyString(anyString(v["key"]), anyString(v["name"])); key != "" { + merge(key, firstPresent(v, "value", "val", "metadata_value")) + return out + } + for key, value := range v { + merge(key, value) + } + case []any: + for _, item := range v { + m, ok := item.(map[string]any) + if !ok { + continue + } + key := firstNonEmptyString(anyString(m["key"]), anyString(m["name"])) + if key == "" { + for k, value := range NormalizeMulticaMetadata(m) { + out[k] = value + } + continue + } + merge(key, firstPresent(m, "value", "val", "metadata_value")) + } + } + return out +} + +func MulticaAssignmentFingerprint(input MulticaAssignmentFingerprintInput) string { + input.AssignmentID = strings.TrimSpace(input.AssignmentID) + input.Assignee = strings.TrimSpace(input.Assignee) + input.Scope = strings.TrimSpace(input.Scope) + input.ExpectedWork = strings.TrimSpace(input.ExpectedWork) + input.ExpectedFeedback = strings.TrimSpace(input.ExpectedFeedback) + input.SignalRef = strings.TrimSpace(input.SignalRef) + input.CorrelationID = strings.TrimSpace(input.CorrelationID) + input.ContextRefs = cleanMulticaRefs(input.ContextRefs) + input.EvidenceRefs = cleanMulticaRefs(input.EvidenceRefs) + sort.Strings(input.ContextRefs) + sort.Strings(input.EvidenceRefs) + data, _ := json.Marshal(input) + sum := sha256.Sum256(data) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +func MulticaSessionID(rootIssueID string) string { + rootIssueID = strings.TrimSpace(rootIssueID) + if rootIssueID == "" { + return "" + } + return "multica:session:" + rootIssueID +} + +func cleanMulticaRefs(values []string) []string { + var out []string + seen := map[string]bool{} + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + seen[value] = true + out = append(out, value) + } + return out +} + +func multicaMetadataString(value any) string { + switch v := value.(type) { + case nil: + return "" + case string: + return strings.TrimSpace(v) + case json.Number: + return strings.TrimSpace(v.String()) + case bool: + if v { + return "true" + } + return "false" + case float64: + return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", v), "0"), ".") + default: + data, err := json.Marshal(v) + if err != nil { + return strings.TrimSpace(fmt.Sprint(v)) + } + return strings.TrimSpace(string(data)) + } +} + +func anyString(value any) string { + if s, ok := value.(string); ok { + return strings.TrimSpace(s) + } + return "" +} + +func firstPresent(m map[string]any, keys ...string) any { + for _, key := range keys { + if value, ok := m[key]; ok { + return value + } + } + return nil +} + +func firstNonEmptyString(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go new file mode 100644 index 00000000..3f6441da --- /dev/null +++ b/harness/internal/surface/multica/hub_test.go @@ -0,0 +1,99 @@ +package multica + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestHubMetadataDetectsAssignmentMailbox(t *testing.T) { + meta := ParseMulticaHubMetadata(map[string]any{ + "metadata": []any{ + map[string]any{"key": MulticaMetadataHubBackend, "value": MulticaHubBackend}, + map[string]any{"key": MulticaMetadataKind, "value": MulticaHubKindAssignmentMailbox}, + map[string]any{"key": MulticaMetadataAssignmentID, "value": "assignment-1"}, + map[string]any{"key": MulticaMetadataRootIssueID, "value": "root-1"}, + map[string]any{"key": MulticaMetadataPrincipal, "value": "worker@team"}, + }, + }) + if !meta.IsAssignmentMailbox() { + t.Fatalf("metadata was not detected as assignment mailbox: %+v", meta) + } + if meta.RootIssueID != "root-1" || meta.Principal != "worker@team" { + t.Fatalf("metadata mismatch: %+v", meta) + } + back := meta.Map() + if back[MulticaMetadataHubBackend] != MulticaHubBackend { + t.Fatalf("metadata map missing backend: %+v", back) + } +} + +func TestAssignmentFingerprintStable(t *testing.T) { + left := MulticaAssignmentFingerprint(MulticaAssignmentFingerprintInput{ + AssignmentID: " assignment-1 ", + Assignee: "worker@team", + Scope: "docs", + ExpectedWork: "write the API notes", + ExpectedFeedback: "summary", + ContextRefs: []string{"ctx-b", "ctx-a", "ctx-a"}, + EvidenceRefs: []string{" ev-1 "}, + CorrelationID: "session-1", + }) + right := MulticaAssignmentFingerprint(MulticaAssignmentFingerprintInput{ + AssignmentID: "assignment-1", + Assignee: " worker@team ", + Scope: "docs", + ExpectedWork: "write the API notes", + ExpectedFeedback: "summary", + ContextRefs: []string{"ctx-a", "ctx-b"}, + EvidenceRefs: []string{"ev-1"}, + CorrelationID: "session-1", + }) + if left != right { + t.Fatalf("fingerprint should be stable across whitespace/order/dedup:\nleft=%s\nright=%s", left, right) + } + if !strings.HasPrefix(left, "sha256:") { + t.Fatalf("fingerprint should carry algorithm prefix: %q", left) + } +} + +func TestFileHubLedgerDedupesRecords(t *testing.T) { + path := filepath.Join(t.TempDir(), "hub-ledger.jsonl") + ledger := NewFileMulticaHubLedger(path) + source := MulticaHubLedgerSource{ + SessionID: "session-1", + AssignmentID: "assignment-1", + AssignmentFingerprint: "sha256:abc", + Principal: "worker@team", + ProjectionKind: "assignment", + } + record := MulticaHubLedgerRecord{ + Kind: MulticaHubKindAssignmentMailbox, + Source: source, + Target: MulticaHubLedgerTarget{ + RootIssueID: "root-1", + ChildIssueID: "child-1", + Status: "created", + }, + } + if err := ledger.Record(record); err != nil { + t.Fatal(err) + } + if err := ledger.Record(record); err != nil { + t.Fatal(err) + } + records, err := NewFileMulticaHubLedger(path).Records() + if err != nil { + t.Fatal(err) + } + if len(records) != 1 { + t.Fatalf("ledger should keep one record for the same source key, got %d: %+v", len(records), records) + } + found, ok, err := NewFileMulticaHubLedger(path).Find(MulticaHubKindAssignmentMailbox, source) + if err != nil { + t.Fatal(err) + } + if !ok || found.Target.ChildIssueID != "child-1" { + t.Fatalf("ledger find mismatch: ok=%v record=%+v", ok, found) + } +} From 7763d6de5d91b05258f52aaa3af94160b6dca523 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:26:08 +0800 Subject: [PATCH 005/117] refactor: introduce interaction event material Add an interaction package for EventMaterial and rule/narrative/refs payload construction, then route Multica issue observed drafts through that boundary. Validation: go test ./harness/internal/interaction ./harness/internal/surface/multica ./harness/internal/driver ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance --- harness/internal/driver/multica.go | 10 ++--- harness/internal/interaction/material.go | 39 +++++++++++++++++++ harness/internal/interaction/material_test.go | 30 ++++++++++++++ 3 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 harness/internal/interaction/material.go create mode 100644 harness/internal/interaction/material_test.go diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index 98937550..c7ef6f78 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -13,7 +13,7 @@ import ( "sync" "time" - eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" + "github.com/mnemon-dev/mnemon/harness/internal/interaction" ) const ( @@ -855,11 +855,7 @@ type MulticaIssueSignalOptions struct { ExternalID string } -type MulticaObservedDraft struct { - EventType string `json:"event_type"` - ExternalID string `json:"external_id"` - Payload map[string]any `json:"payload"` -} +type MulticaObservedDraft = interaction.EventMaterial func BuildMulticaIssueTeamworkSignal(issue MulticaIssue, opts MulticaIssueSignalOptions) (MulticaObservedDraft, error) { if strings.TrimSpace(issue.ID) == "" { @@ -916,7 +912,7 @@ func BuildMulticaIssueTeamworkSignal(issue MulticaIssue, opts MulticaIssueSignal return MulticaObservedDraft{ EventType: "teamwork_signal.write_candidate.observed", ExternalID: externalID, - Payload: eventmodel.BuildPayload(rule, narrative, refs), + Payload: interaction.BuildPayload(rule, narrative, refs), }, nil } diff --git a/harness/internal/interaction/material.go b/harness/internal/interaction/material.go new file mode 100644 index 00000000..3b99f8cf --- /dev/null +++ b/harness/internal/interaction/material.go @@ -0,0 +1,39 @@ +package interaction + +import ( + "strings" + + eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" +) + +type EventMaterial struct { + EventType string `json:"event_type"` + ExternalID string `json:"external_id"` + Payload map[string]any `json:"payload"` +} + +func BuildPayload(rule, narrative, refs map[string]any) map[string]any { + return eventmodel.BuildPayload(rule, narrative, refs) +} + +func PutString(values map[string]any, key, value string) { + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + if key != "" && value != "" { + values[key] = value + } +} + +func CleanStrings(values []string) []string { + var out []string + seen := map[string]bool{} + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + seen[value] = true + out = append(out, value) + } + return out +} diff --git a/harness/internal/interaction/material_test.go b/harness/internal/interaction/material_test.go new file mode 100644 index 00000000..2223aa56 --- /dev/null +++ b/harness/internal/interaction/material_test.go @@ -0,0 +1,30 @@ +package interaction + +import "testing" + +func TestEventMaterialBuildPayloadSeparatesRuleNarrativeRefs(t *testing.T) { + rule := map[string]any{"ttl": "30m"} + narrative := map[string]any{"statement": "do the work"} + refs := map[string]any{"evidence_refs": []string{"ev-1"}} + material := EventMaterial{ + EventType: "teamwork_signal.write_candidate.observed", + ExternalID: "external-1", + Payload: BuildPayload(rule, narrative, refs), + } + if material.Payload["rule"].(map[string]any)["ttl"] != "30m" { + t.Fatalf("rule payload missing: %+v", material.Payload) + } + if material.Payload["narrative"].(map[string]any)["statement"] != "do the work" { + t.Fatalf("narrative payload missing: %+v", material.Payload) + } + if len(material.Payload["refs"].(map[string]any)["evidence_refs"].([]string)) != 1 { + t.Fatalf("refs payload missing: %+v", material.Payload) + } +} + +func TestCleanStringsTrimsAndDedupes(t *testing.T) { + got := CleanStrings([]string{" a ", "", "b", "a"}) + if len(got) != 2 || got[0] != "a" || got[1] != "b" { + t.Fatalf("clean strings = %+v, want [a b]", got) + } +} From 6da9184d1839c788b4d07cc5e9a9b72d455f05e3 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:29:05 +0800 Subject: [PATCH 006/117] feat: add harness product config model Add a config-first harness product schema with validation, roundtrip persistence, and a read-only bridge from existing local and remote workspace config files. Validation: go test ./harness/internal/productconfig ./harness/internal/interaction ./harness/internal/drive ./harness/internal/activationtrace ./harness/internal/surface/multica ./harness/internal/driver --- harness/internal/productconfig/config.go | 369 ++++++++++++++++++ harness/internal/productconfig/config_test.go | 122 ++++++ 2 files changed, 491 insertions(+) create mode 100644 harness/internal/productconfig/config.go create mode 100644 harness/internal/productconfig/config_test.go diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go new file mode 100644 index 00000000..755c5302 --- /dev/null +++ b/harness/internal/productconfig/config.go @@ -0,0 +1,369 @@ +package productconfig + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +const ( + SchemaVersion = 1 + DefaultRelPath = ".mnemon/harness/config.json" + + RuntimeKindCodex = "codex" + RuntimeKindConfigured = "configured-host-agent" + RuntimeModeAttached = "attached" + RuntimeModeManaged = "managed" + RuntimeModeManagedOrHost = "managed-or-attached" + + ConnectionMultica = "multica" + ConnectionGitHub = "github" + ConnectionMnemonhub = "mnemonhub" + + DriveManagedLocal = "managed-local" + + DuplicateActivationSuppress = "suppress" + DuplicateActivationAllow = "allow" +) + +type Config struct { + SchemaVersion int `json:"schema_version"` + Team Team `json:"team,omitempty"` + Participants []Participant `json:"participants,omitempty"` + Connections Connections `json:"connections,omitempty"` + Daemon Daemon `json:"daemon,omitempty"` + Sessions SessionDefaults `json:"sessions,omitempty"` +} + +type Team struct { + Name string `json:"name,omitempty"` + Profile string `json:"profile,omitempty"` +} + +type Participant struct { + Principal string `json:"principal"` + DisplayName string `json:"display_name,omitempty"` + Role string `json:"role,omitempty"` + HostRuntime HostRuntime `json:"host_runtime"` +} + +type HostRuntime struct { + Kind string `json:"kind"` + Mode string `json:"mode"` +} + +type Connections struct { + Multica MulticaConnection `json:"multica,omitempty"` + GitHub GitHubConnection `json:"github,omitempty"` + Mnemonhub MnemonhubConnection `json:"mnemonhub,omitempty"` +} + +type MulticaConnection struct { + Enabled bool `json:"enabled,omitempty"` + Workspace string `json:"workspace,omitempty"` + RuntimeBinary string `json:"runtime_binary,omitempty"` +} + +type GitHubConnection struct { + Enabled bool `json:"enabled,omitempty"` + Repo string `json:"repo,omitempty"` + Branch string `json:"branch,omitempty"` +} + +type MnemonhubConnection struct { + Enabled bool `json:"enabled,omitempty"` + Endpoint string `json:"endpoint,omitempty"` +} + +type Daemon struct { + InteractionWatchers []string `json:"interaction_watchers,omitempty"` + DriveSources []string `json:"drive_sources,omitempty"` + ProjectionSurfaces []string `json:"projection_surfaces,omitempty"` +} + +type SessionDefaults struct { + PrimaryActivationCarrier string `json:"primary_activation_carrier,omitempty"` + DuplicateActivationPolicy string `json:"duplicate_activation_policy,omitempty"` +} + +func DefaultPath(root, explicit string) string { + if strings.TrimSpace(explicit) != "" { + return filepath.Clean(explicit) + } + root = strings.TrimSpace(root) + if root == "" { + root = "." + } + return filepath.Join(root, DefaultRelPath) +} + +func Default() Config { + return Config{ + SchemaVersion: SchemaVersion, + Team: Team{ + Name: "default", + Profile: "teamwork", + }, + Sessions: SessionDefaults{ + DuplicateActivationPolicy: DuplicateActivationSuppress, + }, + } +} + +func Load(path string) (Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return Config{}, err + } + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + var cfg Config + if err := dec.Decode(&cfg); err != nil { + return Config{}, fmt.Errorf("parse product config: %w", err) + } + if err := cfg.Validate(); err != nil { + return Config{}, err + } + return cfg, nil +} + +func Save(path string, cfg Config) error { + if cfg.SchemaVersion == 0 { + cfg.SchemaVersion = SchemaVersion + } + if err := cfg.Validate(); err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + return os.WriteFile(path, data, 0o600) +} + +func (cfg Config) Validate() error { + if cfg.SchemaVersion != SchemaVersion { + return fmt.Errorf("product config schema_version %d unsupported (want %d)", cfg.SchemaVersion, SchemaVersion) + } + seen := map[string]bool{} + for _, participant := range cfg.Participants { + principal := strings.TrimSpace(participant.Principal) + if principal == "" { + return fmt.Errorf("participant principal is required") + } + if seen[principal] { + return fmt.Errorf("duplicate participant principal %q", principal) + } + seen[principal] = true + if strings.TrimSpace(participant.HostRuntime.Kind) == "" { + return fmt.Errorf("participant %q host_runtime.kind is required", principal) + } + if strings.TrimSpace(participant.HostRuntime.Mode) == "" { + return fmt.Errorf("participant %q host_runtime.mode is required", principal) + } + } + if cfg.Connections.Multica.Enabled { + if strings.TrimSpace(cfg.Connections.Multica.Workspace) == "" { + return fmt.Errorf("multica connection workspace is required when enabled") + } + } + if cfg.Connections.GitHub.Enabled { + if strings.TrimSpace(cfg.Connections.GitHub.Repo) == "" { + return fmt.Errorf("github connection repo is required when enabled") + } + } + if cfg.Connections.Mnemonhub.Enabled { + if strings.TrimSpace(cfg.Connections.Mnemonhub.Endpoint) == "" { + return fmt.Errorf("mnemonhub connection endpoint is required when enabled") + } + } + if err := validateCarrier("primary activation carrier", cfg.Sessions.PrimaryActivationCarrier, cfg); err != nil { + return err + } + switch strings.TrimSpace(cfg.Sessions.DuplicateActivationPolicy) { + case "", DuplicateActivationSuppress, DuplicateActivationAllow: + default: + return fmt.Errorf("duplicate_activation_policy must be %q or %q", DuplicateActivationSuppress, DuplicateActivationAllow) + } + for _, watcher := range cfg.Daemon.InteractionWatchers { + if err := validateCarrier("interaction watcher", watcher, cfg); err != nil { + return err + } + } + for _, surface := range cfg.Daemon.ProjectionSurfaces { + if err := validateCarrier("projection surface", surface, cfg); err != nil { + return err + } + } + for _, source := range cfg.Daemon.DriveSources { + source = strings.TrimSpace(source) + if source == "" { + return fmt.Errorf("drive source cannot be empty") + } + if source != DriveManagedLocal { + return fmt.Errorf("unsupported drive source %q", source) + } + } + return nil +} + +func validateCarrier(label, carrier string, cfg Config) error { + carrier = strings.TrimSpace(carrier) + if carrier == "" { + return nil + } + switch carrier { + case ConnectionMultica: + if !cfg.Connections.Multica.Enabled { + return fmt.Errorf("%s %q is not enabled", label, carrier) + } + case ConnectionGitHub: + if !cfg.Connections.GitHub.Enabled { + return fmt.Errorf("%s %q is not enabled", label, carrier) + } + case ConnectionMnemonhub: + if !cfg.Connections.Mnemonhub.Enabled { + return fmt.Errorf("%s %q is not enabled", label, carrier) + } + default: + return fmt.Errorf("%s %q is unsupported", label, carrier) + } + return nil +} + +type legacyLocalConfig struct { + SchemaVersion int `json:"schema_version"` + Principal string `json:"principal"` + Loops []string `json:"loops"` +} + +type legacyRemotesDoc struct { + SchemaVersion int `json:"schema_version"` + Current string `json:"current,omitempty"` + Remotes []legacyRemoteEntry `json:"remotes"` +} + +type legacyRemoteEntry struct { + Backend string `json:"backend,omitempty"` + ID string `json:"id"` + Endpoint string `json:"endpoint,omitempty"` + Repo string `json:"repo,omitempty"` + Branch string `json:"branch,omitempty"` +} + +func FromLegacy(root string) (Config, bool, error) { + cfg := Default() + found := false + local, ok, err := loadLegacyLocal(root) + if err != nil { + return Config{}, false, err + } + if ok { + found = true + if strings.TrimSpace(local.Principal) != "" { + cfg.Participants = append(cfg.Participants, Participant{ + Principal: local.Principal, + HostRuntime: HostRuntime{ + Kind: RuntimeKindConfigured, + Mode: RuntimeModeAttached, + }, + }) + } + } + remotes, ok, err := loadLegacyRemotes(root) + if err != nil { + return Config{}, false, err + } + if ok { + found = true + for _, remote := range remotes.Remotes { + backend := strings.TrimSpace(remote.Backend) + if backend == "" { + backend = ConnectionMnemonhub + } + switch backend { + case "http", ConnectionMnemonhub: + cfg.Connections.Mnemonhub.Enabled = true + if strings.TrimSpace(cfg.Connections.Mnemonhub.Endpoint) == "" { + cfg.Connections.Mnemonhub.Endpoint = strings.TrimSpace(remote.Endpoint) + } + case ConnectionGitHub: + cfg.Connections.GitHub.Enabled = true + if strings.TrimSpace(cfg.Connections.GitHub.Repo) == "" { + cfg.Connections.GitHub.Repo = strings.TrimSpace(remote.Repo) + cfg.Connections.GitHub.Branch = strings.TrimSpace(remote.Branch) + } + } + } + } + if cfg.Connections.Mnemonhub.Enabled { + cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionMnemonhub) + cfg.Daemon.ProjectionSurfaces = appendCarrier(cfg.Daemon.ProjectionSurfaces, ConnectionMnemonhub) + } + if cfg.Connections.GitHub.Enabled { + cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionGitHub) + cfg.Daemon.ProjectionSurfaces = appendCarrier(cfg.Daemon.ProjectionSurfaces, ConnectionGitHub) + } + if len(cfg.Participants) > 0 { + cfg.Daemon.DriveSources = appendCarrier(cfg.Daemon.DriveSources, DriveManagedLocal) + } + if err := cfg.Validate(); err != nil { + return Config{}, false, err + } + return cfg, found, nil +} + +func loadLegacyLocal(root string) (legacyLocalConfig, bool, error) { + path := filepath.Join(root, ".mnemon", "harness", "local", "config.json") + data, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return legacyLocalConfig{}, false, nil + } + if err != nil { + return legacyLocalConfig{}, false, err + } + var cfg legacyLocalConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return legacyLocalConfig{}, false, fmt.Errorf("parse legacy local config: %w", err) + } + if cfg.SchemaVersion != 1 { + return legacyLocalConfig{}, false, fmt.Errorf("legacy local config schema_version %d unsupported (want 1)", cfg.SchemaVersion) + } + return cfg, true, nil +} + +func loadLegacyRemotes(root string) (legacyRemotesDoc, bool, error) { + path := filepath.Join(root, ".mnemon", "harness", "sync", "remotes.json") + data, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return legacyRemotesDoc{}, false, nil + } + if err != nil { + return legacyRemotesDoc{}, false, err + } + var doc legacyRemotesDoc + if err := json.Unmarshal(data, &doc); err != nil { + return legacyRemotesDoc{}, false, fmt.Errorf("parse legacy remote config: %w", err) + } + if doc.SchemaVersion != 1 { + return legacyRemotesDoc{}, false, fmt.Errorf("legacy remote config schema_version %d unsupported (want 1)", doc.SchemaVersion) + } + return doc, true, nil +} + +func appendCarrier(values []string, value string) []string { + for _, existing := range values { + if existing == value { + return values + } + } + return append(values, value) +} diff --git a/harness/internal/productconfig/config_test.go b/harness/internal/productconfig/config_test.go new file mode 100644 index 00000000..d82ab8a0 --- /dev/null +++ b/harness/internal/productconfig/config_test.go @@ -0,0 +1,122 @@ +package productconfig + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestConfigRoundTrip(t *testing.T) { + path := filepath.Join(t.TempDir(), "nested", "config.json") + cfg := Default() + cfg.Participants = []Participant{{ + Principal: "planner@team", + HostRuntime: HostRuntime{ + Kind: RuntimeKindCodex, + Mode: RuntimeModeManagedOrHost, + }, + }} + cfg.Connections.Multica = MulticaConnection{ + Enabled: true, + Workspace: "teamwork-grivn", + RuntimeBinary: "mnemon-multica-runtime", + } + cfg.Daemon.InteractionWatchers = []string{ConnectionMultica} + cfg.Daemon.ProjectionSurfaces = []string{ConnectionMultica} + cfg.Daemon.DriveSources = []string{DriveManagedLocal} + cfg.Sessions.PrimaryActivationCarrier = ConnectionMultica + + if err := Save(path, cfg); err != nil { + t.Fatal(err) + } + got, err := Load(path) + if err != nil { + t.Fatal(err) + } + if got.SchemaVersion != SchemaVersion || got.Connections.Multica.RuntimeBinary != "mnemon-multica-runtime" || len(got.Participants) != 1 { + t.Fatalf("config mismatch: %+v", got) + } +} + +func TestConfigValidateRejectsCrossLayerLeaks(t *testing.T) { + cfg := Default() + cfg.SchemaVersion = SchemaVersion + cfg.Participants = []Participant{{ + Principal: "planner@team", + HostRuntime: HostRuntime{ + Kind: RuntimeKindCodex, + Mode: RuntimeModeManaged, + }, + }} + cfg.Daemon.ProjectionSurfaces = []string{ConnectionMultica} + err := cfg.Validate() + if err == nil || !strings.Contains(err.Error(), "not enabled") { + t.Fatalf("expected disabled projection surface error, got %v", err) + } +} + +func TestConfigValidateRejectsDuplicateParticipants(t *testing.T) { + cfg := Default() + cfg.Participants = []Participant{ + {Principal: "planner@team", HostRuntime: HostRuntime{Kind: RuntimeKindCodex, Mode: RuntimeModeManaged}}, + {Principal: "planner@team", HostRuntime: HostRuntime{Kind: RuntimeKindCodex, Mode: RuntimeModeManaged}}, + } + err := cfg.Validate() + if err == nil || !strings.Contains(err.Error(), "duplicate participant") { + t.Fatalf("expected duplicate participant error, got %v", err) + } +} + +func TestFromLegacyBridgesLocalAndRemoteConfigs(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, ".mnemon", "harness", "local"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(root, ".mnemon", "harness", "sync"), 0o755); err != nil { + t.Fatal(err) + } + local := `{ + "schema_version": 1, + "mode": "local", + "endpoint": "http://127.0.0.1:8787", + "principal": "planner@team", + "loops": ["assignment"], + "binding_file": ".mnemon/harness/channel/bindings.json", + "store_path": ".mnemon/harness/local/governed.db" +}` + remotes := `{ + "schema_version": 1, + "current": "mesh", + "remotes": [ + {"id": "mesh", "backend": "github", "repo": "mnemon-dev/mnemon-teamwork-example", "branch": "mnemond-planner", "credential_ref": ".mnemon/harness/sync/credentials/self.token"}, + {"id": "hub", "backend": "http", "endpoint": "https://hub.example", "credential_ref": ".mnemon/harness/sync/credentials/hub.token"} + ] +}` + if err := os.WriteFile(filepath.Join(root, ".mnemon", "harness", "local", "config.json"), []byte(local), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, ".mnemon", "harness", "sync", "remotes.json"), []byte(remotes), 0o600); err != nil { + t.Fatal(err) + } + + cfg, found, err := FromLegacy(root) + if err != nil { + t.Fatal(err) + } + if !found { + t.Fatal("legacy configs should be found") + } + if len(cfg.Participants) != 1 || cfg.Participants[0].Principal != "planner@team" { + t.Fatalf("participant bridge mismatch: %+v", cfg.Participants) + } + if !cfg.Connections.GitHub.Enabled || cfg.Connections.GitHub.Repo != "mnemon-dev/mnemon-teamwork-example" { + t.Fatalf("github bridge mismatch: %+v", cfg.Connections.GitHub) + } + if !cfg.Connections.Mnemonhub.Enabled || cfg.Connections.Mnemonhub.Endpoint != "https://hub.example" { + t.Fatalf("mnemonhub bridge mismatch: %+v", cfg.Connections.Mnemonhub) + } + if len(cfg.Daemon.DriveSources) != 1 || cfg.Daemon.DriveSources[0] != DriveManagedLocal { + t.Fatalf("drive sources mismatch: %+v", cfg.Daemon.DriveSources) + } +} From a809496036fa35a33c9c37707d1079f0271b7b80 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:30:26 +0800 Subject: [PATCH 007/117] feat: add harness daemon supervisor boundary Add an internal daemon supervisor for long-running interaction, drive, projection, and status workers with lifecycle reporting and status snapshots. Validation: go test ./harness/internal/daemon ./harness/internal/productconfig ./harness/internal/interaction ./harness/internal/drive ./harness/internal/activationtrace ./harness/internal/surface/multica ./harness/internal/driver --- harness/internal/daemon/supervisor.go | 177 +++++++++++++++++++++ harness/internal/daemon/supervisor_test.go | 90 +++++++++++ 2 files changed, 267 insertions(+) create mode 100644 harness/internal/daemon/supervisor.go create mode 100644 harness/internal/daemon/supervisor_test.go diff --git a/harness/internal/daemon/supervisor.go b/harness/internal/daemon/supervisor.go new file mode 100644 index 00000000..46c7b403 --- /dev/null +++ b/harness/internal/daemon/supervisor.go @@ -0,0 +1,177 @@ +package daemon + +import ( + "context" + "fmt" + "sync" + "time" +) + +type WorkerKind string + +const ( + WorkerInteraction WorkerKind = "interaction" + WorkerDrive WorkerKind = "drive" + WorkerProjection WorkerKind = "projection" + WorkerStatus WorkerKind = "status" +) + +type Worker interface { + Name() string + Kind() WorkerKind + Run(context.Context, Reporter) error +} + +type Reporter interface { + Report(WorkerReport) +} + +type WorkerReport struct { + Name string + Kind WorkerKind + Status string + Message string + At time.Time +} + +type Snapshot struct { + StartedAt time.Time `json:"started_at"` + Workers map[string]WorkerSnapshot `json:"workers"` +} + +type WorkerSnapshot struct { + Kind WorkerKind `json:"kind"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + StartedAt time.Time `json:"started_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + Error string `json:"error,omitempty"` +} + +type Clock func() time.Time + +type Supervisor struct { + workers []Worker + now Clock + + mu sync.Mutex + started bool + snapshot Snapshot +} + +func NewSupervisor(workers []Worker, now Clock) *Supervisor { + if now == nil { + now = func() time.Time { return time.Now().UTC() } + } + return &Supervisor{workers: append([]Worker(nil), workers...), now: now} +} + +func (s *Supervisor) Run(ctx context.Context) error { + if len(s.workers) == 0 { + return fmt.Errorf("daemon supervisor requires at least one worker") + } + startedAt := s.now() + s.mu.Lock() + if s.started { + s.mu.Unlock() + return fmt.Errorf("daemon supervisor already started") + } + s.started = true + s.snapshot = Snapshot{StartedAt: startedAt, Workers: map[string]WorkerSnapshot{}} + for _, worker := range s.workers { + name := worker.Name() + s.snapshot.Workers[name] = WorkerSnapshot{ + Kind: worker.Kind(), + Status: "starting", + StartedAt: startedAt, + UpdatedAt: startedAt, + } + } + s.mu.Unlock() + + var wg sync.WaitGroup + errs := make(chan error, len(s.workers)) + for _, worker := range s.workers { + worker := worker + wg.Add(1) + go func() { + defer wg.Done() + name := worker.Name() + s.setWorker(name, WorkerSnapshot{ + Kind: worker.Kind(), + Status: "running", + StartedAt: startedAt, + UpdatedAt: s.now(), + }) + if err := worker.Run(ctx, s); err != nil { + s.setWorker(name, WorkerSnapshot{ + Kind: worker.Kind(), + Status: "failed", + StartedAt: startedAt, + UpdatedAt: s.now(), + Error: err.Error(), + }) + errs <- fmt.Errorf("%s worker %q: %w", worker.Kind(), name, err) + return + } + status := "stopped" + if ctx.Err() == nil { + status = "completed" + } + s.setWorker(name, WorkerSnapshot{ + Kind: worker.Kind(), + Status: status, + StartedAt: startedAt, + UpdatedAt: s.now(), + }) + }() + } + wg.Wait() + close(errs) + for err := range errs { + return err + } + return nil +} + +func (s *Supervisor) Report(report WorkerReport) { + if report.At.IsZero() { + report.At = s.now() + } + s.mu.Lock() + defer s.mu.Unlock() + if s.snapshot.Workers == nil { + s.snapshot.Workers = map[string]WorkerSnapshot{} + } + current := s.snapshot.Workers[report.Name] + if current.StartedAt.IsZero() { + current.StartedAt = report.At + } + current.Kind = report.Kind + current.Status = report.Status + current.Message = report.Message + current.UpdatedAt = report.At + s.snapshot.Workers[report.Name] = current +} + +func (s *Supervisor) Snapshot() Snapshot { + s.mu.Lock() + defer s.mu.Unlock() + out := Snapshot{StartedAt: s.snapshot.StartedAt, Workers: map[string]WorkerSnapshot{}} + for name, worker := range s.snapshot.Workers { + out.Workers[name] = worker + } + return out +} + +func (s *Supervisor) setWorker(name string, snapshot WorkerSnapshot) { + s.mu.Lock() + defer s.mu.Unlock() + if s.snapshot.Workers == nil { + s.snapshot.Workers = map[string]WorkerSnapshot{} + } + if current := s.snapshot.Workers[name]; !current.StartedAt.IsZero() && snapshot.StartedAt.IsZero() { + snapshot.StartedAt = current.StartedAt + } + s.snapshot.Workers[name] = snapshot +} diff --git a/harness/internal/daemon/supervisor_test.go b/harness/internal/daemon/supervisor_test.go new file mode 100644 index 00000000..50d6e3a0 --- /dev/null +++ b/harness/internal/daemon/supervisor_test.go @@ -0,0 +1,90 @@ +package daemon + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestSupervisorRunsWorkersAndRecordsReports(t *testing.T) { + now := time.Unix(100, 0).UTC() + ctx := context.Background() + supervisor := NewSupervisor([]Worker{ + workerFunc{ + name: "multica-watch", + kind: WorkerInteraction, + run: func(ctx context.Context, reporter Reporter) error { + reporter.Report(WorkerReport{Name: "multica-watch", Kind: WorkerInteraction, Status: "idle", Message: "cursor=7"}) + return nil + }, + }, + workerFunc{ + name: "managed-drive", + kind: WorkerDrive, + run: func(context.Context, Reporter) error { + return nil + }, + }, + }, func() time.Time { return now }) + + if err := supervisor.Run(ctx); err != nil { + t.Fatal(err) + } + snapshot := supervisor.Snapshot() + if len(snapshot.Workers) != 2 { + t.Fatalf("snapshot workers = %+v", snapshot.Workers) + } + if snapshot.Workers["managed-drive"].Status != "completed" { + t.Fatalf("drive worker status = %+v", snapshot.Workers["managed-drive"]) + } + if snapshot.Workers["multica-watch"].Kind != WorkerInteraction { + t.Fatalf("interaction worker kind = %+v", snapshot.Workers["multica-watch"]) + } +} + +func TestSupervisorCapturesWorkerFailure(t *testing.T) { + supervisor := NewSupervisor([]Worker{ + workerFunc{ + name: "project-multica", + kind: WorkerProjection, + run: func(context.Context, Reporter) error { + return errors.New("projection failed") + }, + }, + }, func() time.Time { return time.Unix(100, 0).UTC() }) + + err := supervisor.Run(context.Background()) + if err == nil { + t.Fatal("expected worker error") + } + snapshot := supervisor.Snapshot() + got := snapshot.Workers["project-multica"] + if got.Status != "failed" || got.Error != "projection failed" { + t.Fatalf("failure snapshot = %+v", got) + } +} + +func TestSupervisorRequiresWorkers(t *testing.T) { + if err := NewSupervisor(nil, nil).Run(context.Background()); err == nil { + t.Fatal("expected no-worker error") + } +} + +type workerFunc struct { + name string + kind WorkerKind + run func(context.Context, Reporter) error +} + +func (w workerFunc) Name() string { + return w.name +} + +func (w workerFunc) Kind() WorkerKind { + return w.kind +} + +func (w workerFunc) Run(ctx context.Context, reporter Reporter) error { + return w.run(ctx, reporter) +} From 7a219cc307d78753da1e9444c933ef4647b9ba95 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:34:03 +0800 Subject: [PATCH 008/117] feat: add harness config and daemon commands Expose thin experimental config and daemon command surfaces backed by productconfig and the existing Local Mnemon serve path, without touching the root mnemon CLI. Validation: go test ./harness/cmd/mnemon-harness ./harness/cmd/mnemond ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance ./harness/internal/productconfig ./harness/internal/daemon --- harness/cmd/mnemon-harness/config.go | 85 +++++++++++++ .../cmd/mnemon-harness/config_daemon_test.go | 51 ++++++++ harness/cmd/mnemon-harness/daemon.go | 114 ++++++++++++++++++ harness/cmd/mnemon-harness/root_test.go | 4 +- 4 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 harness/cmd/mnemon-harness/config.go create mode 100644 harness/cmd/mnemon-harness/config_daemon_test.go create mode 100644 harness/cmd/mnemon-harness/daemon.go diff --git a/harness/cmd/mnemon-harness/config.go b/harness/cmd/mnemon-harness/config.go new file mode 100644 index 00000000..adba786d --- /dev/null +++ b/harness/cmd/mnemon-harness/config.go @@ -0,0 +1,85 @@ +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" + "github.com/spf13/cobra" +) + +var ( + configRoot string + configPath string +) + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Inspect harness product configuration", +} + +var configValidateCmd = &cobra.Command{ + Use: "validate", + Short: "Validate harness product configuration", + RunE: runConfigValidate, +} + +var configOpenCmd = &cobra.Command{ + Use: "open", + Short: "Open harness product configuration", + RunE: runConfigOpen, +} + +func init() { + configCmd.PersistentFlags().StringVar(&configRoot, "root", ".", "project root") + configCmd.PersistentFlags().StringVar(&configPath, "config", "", "harness product config path") + _ = configCmd.PersistentFlags().MarkHidden("config") + configCmd.AddCommand(configValidateCmd, configOpenCmd) + configCmd.GroupID = groupSpine + rootCmd.AddCommand(configCmd) +} + +func runConfigValidate(cmd *cobra.Command, args []string) error { + path := resolvedProductConfigPath() + cfg, err := productconfig.Load(path) + if err == nil { + fmt.Fprintf(cmd.OutOrStdout(), "Harness config: valid %s\n", path) + fmt.Fprintf(cmd.OutOrStdout(), "Participants: %d\n", len(cfg.Participants)) + return nil + } + if !errors.Is(err, os.ErrNotExist) { + return err + } + legacy, found, err := productconfig.FromLegacy(filepath.Clean(configRoot)) + if err != nil { + return err + } + if !found { + return fmt.Errorf("Harness config is not configured") + } + fmt.Fprintln(cmd.OutOrStdout(), "Harness config: valid legacy bridge") + fmt.Fprintf(cmd.OutOrStdout(), "Participants: %d\n", len(legacy.Participants)) + return nil +} + +func runConfigOpen(cmd *cobra.Command, args []string) error { + path := resolvedProductConfigPath() + editor := strings.TrimSpace(os.Getenv("EDITOR")) + if editor == "" { + fmt.Fprintf(cmd.OutOrStdout(), "Harness config: %s\n", path) + return nil + } + open := exec.CommandContext(cmd.Context(), editor, path) + open.Stdin = os.Stdin + open.Stdout = cmd.OutOrStdout() + open.Stderr = cmd.ErrOrStderr() + return open.Run() +} + +func resolvedProductConfigPath() string { + return productconfig.DefaultPath(filepath.Clean(configRoot), configPath) +} diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go new file mode 100644 index 00000000..a2312bb3 --- /dev/null +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "strings" + "testing" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" +) + +func TestConfigValidateReadsProductConfig(t *testing.T) { + root := t.TempDir() + cfg := productconfig.Default() + cfg.Participants = []productconfig.Participant{{ + Principal: "planner@team", + HostRuntime: productconfig.HostRuntime{ + Kind: productconfig.RuntimeKindCodex, + Mode: productconfig.RuntimeModeManaged, + }, + }} + if err := productconfig.Save(productconfig.DefaultPath(root, ""), cfg); err != nil { + t.Fatal(err) + } + oldRoot, oldPath := configRoot, configPath + configRoot, configPath = root, "" + t.Cleanup(func() { configRoot, configPath = oldRoot, oldPath }) + + cmd, out := testCommand() + if err := runConfigValidate(cmd, nil); err != nil { + t.Fatal(err) + } + if got := out.String(); !strings.Contains(got, "Harness config: valid") || !strings.Contains(got, "Participants: 1") { + t.Fatalf("unexpected config validate output:\n%s", got) + } +} + +func TestDaemonStatusDoesNotMutateMissingConfig(t *testing.T) { + oldRoot := daemonRoot + daemonRoot = t.TempDir() + t.Cleanup(func() { daemonRoot = oldRoot }) + + cmd, out := testCommand() + if err := runDaemonStatus(cmd, nil); err != nil { + t.Fatal(err) + } + got := out.String() + for _, want := range []string{"Harness config: not configured", "Harness daemon: not running"} { + if !strings.Contains(got, want) { + t.Fatalf("daemon status missing %q:\n%s", want, got) + } + } +} diff --git a/harness/cmd/mnemon-harness/daemon.go b/harness/cmd/mnemon-harness/daemon.go new file mode 100644 index 00000000..52002223 --- /dev/null +++ b/harness/cmd/mnemon-harness/daemon.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "io" + "path/filepath" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/app" + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" + "github.com/spf13/cobra" +) + +var ( + daemonRoot string + daemonAddr string + daemonStorePath string + daemonBindingsPath string + daemonAllowNonLoopback bool + daemonIgnoreExternal bool + daemonAllowInsecureRemote bool + daemonSyncInterval time.Duration +) + +var daemonCmd = &cobra.Command{ + Use: "daemon", + Short: "Run and inspect the harness daemon", +} + +var daemonStartCmd = &cobra.Command{ + Use: "start", + Short: "Start the harness daemon", + RunE: runDaemonStart, +} + +var daemonStopCmd = &cobra.Command{ + Use: "stop", + Short: "Show how to stop the harness daemon", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintln(cmd.OutOrStdout(), "Harness daemon: stop the running process to shut down") + return nil + }, +} + +var daemonStatusCmd = &cobra.Command{ + Use: "status", + Short: "Show harness daemon status", + RunE: runDaemonStatus, +} + +func init() { + daemonCmd.PersistentFlags().StringVar(&daemonRoot, "root", ".", "project root") + daemonCmd.PersistentFlags().StringVar(&daemonStorePath, "store", "", "store path; defaults to the project Local Mnemon store") + daemonStartCmd.Flags().StringVar(&daemonAddr, "addr", "127.0.0.1:8787", "listen address") + daemonStartCmd.Flags().StringVar(&daemonBindingsPath, "bindings", "", "Agent Integration binding file") + daemonStartCmd.Flags().DurationVar(&daemonSyncInterval, "sync-interval", 0, "sync worker cadence (0 = default 30s)") + daemonStartCmd.Flags().BoolVar(&daemonAllowNonLoopback, "allow-nonloopback", false, "explicitly allow listening on a non-loopback address") + daemonStartCmd.Flags().BoolVar(&daemonIgnoreExternal, "ignore-external", false, "ignore external event packages under .mnemon/loops") + daemonStartCmd.Flags().BoolVar(&daemonAllowInsecureRemote, "allow-insecure-remote", false, "allow plaintext Remote Workspace endpoint when explicitly configured") + _ = daemonStartCmd.Flags().MarkHidden("bindings") + daemonCmd.AddCommand(daemonStartCmd, daemonStopCmd, daemonStatusCmd) + daemonCmd.GroupID = groupSpine + rootCmd.AddCommand(daemonCmd) +} + +func runDaemonStart(cmd *cobra.Command, args []string) error { + root := daemonProjectRoot() + boot, err := app.ResolveLocalBoot(root, daemonStorePath, daemonBindingsPath) + if err != nil { + return err + } + addr := daemonAddr + if !cmd.Flags().Changed("addr") { + addr = app.ListenAddrFromEndpoint(boot.Config.Endpoint, daemonAddr) + } + if err := app.ValidateListenAddr(addr, daemonAllowNonLoopback); err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "Harness daemon: ready") + fmt.Fprintln(cmd.OutOrStdout(), "Remote Workspace: "+app.RemoteWorkspaceStatus(root)) + return app.RunLocalHTTPServerWithBindings(cmd.Context(), addr, boot.StorePath, boot.Loaded, app.ServeOptions{ + Loops: boot.Config.Loops, + ProjectRoot: root, + IgnoreExternal: daemonIgnoreExternal, + AllowInsecureRemote: daemonAllowInsecureRemote, + SyncInterval: daemonSyncInterval, + }, io.Discard) +} + +func runDaemonStatus(cmd *cobra.Command, args []string) error { + root := daemonProjectRoot() + if _, err := productconfig.Load(productconfig.DefaultPath(root, "")); err == nil { + fmt.Fprintln(cmd.OutOrStdout(), "Harness config: configured") + } else if _, found, legacyErr := productconfig.FromLegacy(root); legacyErr == nil && found { + fmt.Fprintln(cmd.OutOrStdout(), "Harness config: legacy bridge") + } else { + fmt.Fprintln(cmd.OutOrStdout(), "Harness config: not configured") + } + if cfg, err := app.ReadLocalConfig(root); err == nil { + if _, ok := localServiceStatus(root, cfg, cfg.Principal); ok { + fmt.Fprintln(cmd.OutOrStdout(), "Harness daemon: ready") + return nil + } + } + fmt.Fprintln(cmd.OutOrStdout(), "Harness daemon: not running") + return nil +} + +func daemonProjectRoot() string { + if daemonRoot == "" { + return "." + } + return filepath.Clean(daemonRoot) +} diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index d9d41da5..2f0f10c1 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -22,12 +22,12 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help returned error: %v", err) } got := out.String() - for _, want := range []string{"Agent Integration", "Local Mnemon", "Remote Workspace", "standard events", "setup", "local"} { + for _, want := range []string{"Agent Integration", "Local Mnemon", "Remote Workspace", "standard events", "setup", "local", "config", "daemon"} { if !strings.Contains(got, want) { t.Fatalf("expected root help to contain %q:\n%s", want, got) } } - for _, blocked := range []string{"completion", "eval", "goal", "coordination", "runner", "supervisor", "daemon", "proposal"} { + for _, blocked := range []string{"completion", "eval", "goal", "coordination", "runner", "supervisor", "proposal"} { if strings.Contains(got, blocked) { t.Fatalf("root help leaked unsupported product term %q:\n%s", blocked, got) } From 2cd2cff2c4a47bf6f8cc487c5e28b76c1bd4ba7e Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:35:14 +0800 Subject: [PATCH 009/117] fix: hide multica debug bridge commands Keep Multica import and projection one-shot commands available as hidden debug paths while removing them from the public harness help surface. Projection remains a projector/surface adapter responsibility. Validation: go test ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance; go run ./harness/cmd/mnemon-harness multica --help --- harness/cmd/mnemon-harness/multica.go | 14 ++++++++------ harness/cmd/mnemon-harness/root_test.go | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index a55adcaf..a52cbf3c 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -91,15 +91,17 @@ var multicaProbeCmd = &cobra.Command{ } var multicaImportIssueCmd = &cobra.Command{ - Use: "import-issue", - Short: "Import one Multica issue as a Mnemon teamwork signal", - RunE: runMulticaImportIssue, + Use: "import-issue", + Short: "Import one Multica issue as a Mnemon teamwork signal", + Hidden: true, + RunE: runMulticaImportIssue, } var multicaProjectCommentCmd = &cobra.Command{ - Use: "project-comment", - Short: "Write a Mnemon update as a Multica issue comment", - RunE: runMulticaProjectComment, + Use: "project-comment", + Short: "Write a Mnemon update as a Multica issue comment", + Hidden: true, + RunE: runMulticaProjectComment, } var multicaProvisionCmd = &cobra.Command{ diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index 2f0f10c1..da20f9a9 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -53,9 +53,10 @@ func TestProductHelpDoesNotExposeInternalVocabulary(t *testing.T) { {"status", "--help"}, {"sync", "--help"}, {"sync", "connect", "--help"}, + {"multica", "--help"}, } { got := executeRootForHelp(t, args...) - for _, blocked := range []string{"binding", "channel", "projection", "kernel", "runtime", "sync cursor", "token file", "control-agent"} { + for _, blocked := range []string{"binding", "channel", "projection", "kernel", "runtime", "sync cursor", "token file", "control-agent", "import-issue", "project-comment"} { if strings.Contains(strings.ToLower(got), blocked) { t.Fatalf("%q help leaked internal term %q:\n%s", strings.Join(args, " "), blocked, got) } From 58d467d0b26961a2a13c24d3ad27b8ae7af880b3 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:45:17 +0800 Subject: [PATCH 010/117] feat: add harness agent and connect commands Add config-first mnemon-harness agent/connect commands so participants and external surfaces are maintained through product configuration rather than old Multica-specific command paths. Keep Multica registry/provision commands available only as internal/debug surfaces and build the independent mnemon-multica-runtime adapter from harness-build. Validation: go test ./harness/cmd/mnemon-harness ./harness/cmd/mnemond ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance ./harness/internal/productconfig ./harness/internal/daemon; make harness-build --- .gitignore | 2 + Makefile | 3 +- harness/cmd/mnemon-harness/agent.go | 117 ++++++++++++++ .../cmd/mnemon-harness/config_daemon_test.go | 121 +++++++++++++++ harness/cmd/mnemon-harness/connect.go | 144 ++++++++++++++++++ harness/cmd/mnemon-harness/multica.go | 7 +- .../mnemon-harness/product_config_helpers.go | 31 ++++ harness/cmd/mnemon-harness/root_test.go | 8 +- harness/cmd/mnemon-harness/token.go | 2 +- harness/cmd/mnemon-harness/tower.go | 1 + 10 files changed, 429 insertions(+), 7 deletions(-) create mode 100644 harness/cmd/mnemon-harness/agent.go create mode 100644 harness/cmd/mnemon-harness/connect.go create mode 100644 harness/cmd/mnemon-harness/product_config_helpers.go diff --git a/.gitignore b/.gitignore index 4d6454b5..570eefdd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ /mnemon-harness /mnemon-hub /mnemond +/mnemon-multica-runtime +/mnemon-acceptance /bin/ # Local dogfood / capability test sandboxes (per-test subdirs) diff --git a/Makefile b/Makefile index fcbc8032..5d9f1c17 100644 --- a/Makefile +++ b/Makefile @@ -22,10 +22,11 @@ deps: ## Download Go dependencies build: ## Build the mnemon binary go build -ldflags "$(LDFLAGS)" -o $(BINARY) . -harness-build: ## Build the harness binaries (mnemon-harness local plane + mnemon-hub remote hub + mnemond local governance daemon + test-only acceptance runner) +harness-build: ## Build the harness binaries (mnemon-harness, mnemond, mnemonhub, Multica runtime adapter, and test-only acceptance runner) go build -ldflags "$(LDFLAGS)" -o mnemon-harness ./harness/cmd/mnemon-harness go build -ldflags "$(LDFLAGS)" -o mnemon-hub ./harness/cmd/mnemon-hub go build -ldflags "$(LDFLAGS)" -o mnemond ./harness/cmd/mnemond + go build -ldflags "$(LDFLAGS)" -o mnemon-multica-runtime ./harness/cmd/mnemon-multica-runtime go build -ldflags "$(LDFLAGS)" -o mnemon-acceptance ./harness/cmd/mnemon-acceptance # ── Install / Uninstall ───────────────────────────────────────────── diff --git a/harness/cmd/mnemon-harness/agent.go b/harness/cmd/mnemon-harness/agent.go new file mode 100644 index 00000000..95c6faad --- /dev/null +++ b/harness/cmd/mnemon-harness/agent.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" + "github.com/spf13/cobra" +) + +var ( + agentRoot string + agentConfigPath string + agentPrincipal string + agentDisplayName string + agentRole string + agentRuntimeKind string + agentRuntimeMode string +) + +var agentCmd = &cobra.Command{ + Use: "agent", + Short: "Manage harness agents", +} + +var agentAddCmd = &cobra.Command{ + Use: "add --principal PRINCIPAL", + Short: "Add one harness agent", + RunE: runAgentAdd, +} + +var agentListCmd = &cobra.Command{ + Use: "list", + Short: "List harness agents", + RunE: runAgentList, +} + +func init() { + agentCmd.PersistentFlags().StringVar(&agentRoot, "root", ".", "project root") + agentCmd.PersistentFlags().StringVar(&agentConfigPath, "config", "", "harness product config path") + _ = agentCmd.PersistentFlags().MarkHidden("config") + agentAddCmd.Flags().StringVar(&agentPrincipal, "principal", "", "agent principal") + agentAddCmd.Flags().StringVar(&agentDisplayName, "display-name", "", "agent display name") + agentAddCmd.Flags().StringVar(&agentRole, "role", "", "agent role") + agentAddCmd.Flags().StringVar(&agentRuntimeKind, "runtime-kind", productconfig.RuntimeKindCodex, "host runtime kind") + agentAddCmd.Flags().StringVar(&agentRuntimeMode, "runtime-mode", productconfig.RuntimeModeManagedOrHost, "host runtime mode") + _ = agentAddCmd.Flags().MarkHidden("runtime-kind") + _ = agentAddCmd.Flags().MarkHidden("runtime-mode") + agentCmd.AddCommand(agentAddCmd, agentListCmd) + agentCmd.GroupID = groupSpine + rootCmd.AddCommand(agentCmd) +} + +func runAgentAdd(cmd *cobra.Command, args []string) error { + principal := strings.TrimSpace(agentPrincipal) + if principal == "" { + return fmt.Errorf("agent add requires --principal") + } + cfg, _, err := loadHarnessProductConfig(agentRoot, agentConfigPath) + if err != nil { + return err + } + next := productconfig.Participant{ + Principal: principal, + DisplayName: strings.TrimSpace(agentDisplayName), + Role: strings.TrimSpace(agentRole), + HostRuntime: productconfig.HostRuntime{ + Kind: strings.TrimSpace(agentRuntimeKind), + Mode: strings.TrimSpace(agentRuntimeMode), + }, + } + cfg.Participants = upsertProductParticipant(cfg.Participants, next) + if len(cfg.Daemon.DriveSources) == 0 { + cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} + } + path, err := saveHarnessProductConfig(agentRoot, agentConfigPath, cfg) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Agent: added %s\n", principal) + fmt.Fprintf(cmd.OutOrStdout(), "Harness config: %s\n", path) + return nil +} + +func runAgentList(cmd *cobra.Command, args []string) error { + cfg, path, err := loadHarnessProductConfig(agentRoot, agentConfigPath) + if err != nil { + return err + } + if len(cfg.Participants) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "Agents: none configured") + fmt.Fprintf(cmd.OutOrStdout(), "Harness config: %s\n", path) + return nil + } + for _, participant := range cfg.Participants { + label := participant.Principal + if participant.DisplayName != "" { + label += " (" + participant.DisplayName + ")" + } + if participant.Role != "" { + label += " - " + participant.Role + } + fmt.Fprintf(cmd.OutOrStdout(), "Agent: %s\n", label) + } + fmt.Fprintf(cmd.OutOrStdout(), "Harness config: %s\n", path) + return nil +} + +func upsertProductParticipant(participants []productconfig.Participant, next productconfig.Participant) []productconfig.Participant { + for i := range participants { + if participants[i].Principal == next.Principal { + participants[i] = next + return participants + } + } + return append(participants, next) +} diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index a2312bb3..b8589214 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -1,6 +1,7 @@ package main import ( + "path/filepath" "strings" "testing" @@ -49,3 +50,123 @@ func TestDaemonStatusDoesNotMutateMissingConfig(t *testing.T) { } } } + +func TestAgentAddAndListWriteProductConfig(t *testing.T) { + root := t.TempDir() + oldRoot, oldPath := agentRoot, agentConfigPath + oldPrincipal, oldDisplayName, oldRole := agentPrincipal, agentDisplayName, agentRole + oldKind, oldMode := agentRuntimeKind, agentRuntimeMode + agentRoot = root + agentConfigPath = "" + agentPrincipal = "planner@team" + agentDisplayName = "Planner" + agentRole = "planner" + agentRuntimeKind = productconfig.RuntimeKindCodex + agentRuntimeMode = productconfig.RuntimeModeManagedOrHost + t.Cleanup(func() { + agentRoot, agentConfigPath = oldRoot, oldPath + agentPrincipal, agentDisplayName, agentRole = oldPrincipal, oldDisplayName, oldRole + agentRuntimeKind, agentRuntimeMode = oldKind, oldMode + }) + + cmd, out := testCommand() + if err := runAgentAdd(cmd, nil); err != nil { + t.Fatal(err) + } + if got := out.String(); !strings.Contains(got, "Agent: added planner@team") { + t.Fatalf("unexpected add output:\n%s", got) + } + cfg := loadProductConfigForTest(t, root) + if len(cfg.Participants) != 1 || cfg.Participants[0].Principal != "planner@team" { + t.Fatalf("participant not written: %+v", cfg.Participants) + } + if got := cfg.Participants[0].HostRuntime.Kind; got != productconfig.RuntimeKindCodex { + t.Fatalf("unexpected runtime kind: %q", got) + } + if len(cfg.Daemon.DriveSources) != 1 || cfg.Daemon.DriveSources[0] != productconfig.DriveManagedLocal { + t.Fatalf("managed drive source not configured: %+v", cfg.Daemon.DriveSources) + } + + cmd, out = testCommand() + if err := runAgentList(cmd, nil); err != nil { + t.Fatal(err) + } + if got := out.String(); !strings.Contains(got, "Agent: planner@team (Planner) - planner") { + t.Fatalf("unexpected list output:\n%s", got) + } +} + +func TestConnectCommandsWriteProductConfig(t *testing.T) { + root := t.TempDir() + oldRoot, oldPath := connectRoot, connectConfigPath + oldWorkspace, oldRuntime := connectMulticaWS, connectMulticaRuntime + oldRepo, oldBranch := connectGitHubRepo, connectGitHubBranch + oldEndpoint := connectMnemonhubURL + connectRoot = root + connectConfigPath = "" + connectMulticaWS = "teamwork-grivn" + connectMulticaRuntime = "mnemon-multica-runtime" + connectGitHubRepo = "mnemon-dev/mnemon-teamwork-example" + connectGitHubBranch = "mnemond-planner" + connectMnemonhubURL = "https://hub.example.invalid" + t.Cleanup(func() { + connectRoot, connectConfigPath = oldRoot, oldPath + connectMulticaWS, connectMulticaRuntime = oldWorkspace, oldRuntime + connectGitHubRepo, connectGitHubBranch = oldRepo, oldBranch + connectMnemonhubURL = oldEndpoint + }) + + cmd, _ := testCommand() + if err := runConnectMultica(cmd, nil); err != nil { + t.Fatal(err) + } + if err := runConnectGitHub(cmd, nil); err != nil { + t.Fatal(err) + } + if err := runConnectMnemonhub(cmd, nil); err != nil { + t.Fatal(err) + } + + cfg := loadProductConfigForTest(t, root) + if !cfg.Connections.Multica.Enabled || cfg.Connections.Multica.Workspace != "teamwork-grivn" { + t.Fatalf("multica connection not written: %+v", cfg.Connections.Multica) + } + if got := cfg.Connections.Multica.RuntimeBinary; got != "mnemon-multica-runtime" { + t.Fatalf("unexpected runtime binary: %q", got) + } + if !cfg.Connections.GitHub.Enabled || cfg.Connections.GitHub.Repo != "mnemon-dev/mnemon-teamwork-example" { + t.Fatalf("github connection not written: %+v", cfg.Connections.GitHub) + } + if !cfg.Connections.Mnemonhub.Enabled || cfg.Connections.Mnemonhub.Endpoint != "https://hub.example.invalid" { + t.Fatalf("mnemonhub connection not written: %+v", cfg.Connections.Mnemonhub) + } + for _, want := range []string{productconfig.ConnectionMultica, productconfig.ConnectionGitHub, productconfig.ConnectionMnemonhub} { + if !containsString(cfg.Daemon.InteractionWatchers, want) { + t.Fatalf("interaction watcher %q missing: %+v", want, cfg.Daemon.InteractionWatchers) + } + if !containsString(cfg.Daemon.ProjectionSurfaces, want) { + t.Fatalf("projection surface %q missing: %+v", want, cfg.Daemon.ProjectionSurfaces) + } + } + if got := cfg.Sessions.PrimaryActivationCarrier; got != productconfig.ConnectionMultica { + t.Fatalf("unexpected primary activation carrier: %q", got) + } +} + +func loadProductConfigForTest(t *testing.T, root string) productconfig.Config { + t.Helper() + cfg, err := productconfig.Load(filepath.Join(root, productconfig.DefaultRelPath)) + if err != nil { + t.Fatal(err) + } + return cfg +} + +func containsString(values []string, want string) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} diff --git a/harness/cmd/mnemon-harness/connect.go b/harness/cmd/mnemon-harness/connect.go new file mode 100644 index 00000000..83bb8732 --- /dev/null +++ b/harness/cmd/mnemon-harness/connect.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" + "github.com/spf13/cobra" +) + +var ( + connectRoot string + connectConfigPath string + connectMulticaWS string + connectMulticaRuntime string + connectGitHubRepo string + connectGitHubBranch string + connectMnemonhubURL string +) + +var connectCmd = &cobra.Command{ + Use: "connect", + Short: "Configure harness external connections", +} + +var connectMulticaCmd = &cobra.Command{ + Use: "multica --workspace WORKSPACE", + Short: "Connect Multica as a harness connection", + RunE: runConnectMultica, +} + +var connectGitHubCmd = &cobra.Command{ + Use: "github --repo OWNER/REPO", + Short: "Connect GitHub as a harness connection", + RunE: runConnectGitHub, +} + +var connectMnemonhubCmd = &cobra.Command{ + Use: "mnemonhub --endpoint URL", + Short: "Connect mnemonhub as a harness connection", + RunE: runConnectMnemonhub, +} + +func init() { + connectCmd.PersistentFlags().StringVar(&connectRoot, "root", ".", "project root") + connectCmd.PersistentFlags().StringVar(&connectConfigPath, "config", "", "harness product config path") + _ = connectCmd.PersistentFlags().MarkHidden("config") + connectMulticaCmd.Flags().StringVar(&connectMulticaWS, "workspace", "", "Multica workspace") + connectMulticaCmd.Flags().StringVar(&connectMulticaRuntime, "runtime-binary", "mnemon-multica-runtime", "Multica runtime binary") + _ = connectMulticaCmd.Flags().MarkHidden("runtime-binary") + connectGitHubCmd.Flags().StringVar(&connectGitHubRepo, "repo", "", "GitHub repository owner/name") + connectGitHubCmd.Flags().StringVar(&connectGitHubBranch, "branch", "", "GitHub publication branch") + connectMnemonhubCmd.Flags().StringVar(&connectMnemonhubURL, "endpoint", "", "mnemonhub endpoint") + connectCmd.AddCommand(connectMulticaCmd, connectGitHubCmd, connectMnemonhubCmd) + connectCmd.GroupID = groupSpine + rootCmd.AddCommand(connectCmd) +} + +func runConnectMultica(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(connectMulticaWS) == "" { + return fmt.Errorf("connect multica requires --workspace") + } + cfg, _, err := loadHarnessProductConfig(connectRoot, connectConfigPath) + if err != nil { + return err + } + cfg.Connections.Multica = productconfig.MulticaConnection{ + Enabled: true, + Workspace: strings.TrimSpace(connectMulticaWS), + RuntimeBinary: strings.TrimSpace(connectMulticaRuntime), + } + cfg.Daemon.InteractionWatchers = appendUniqueString(cfg.Daemon.InteractionWatchers, productconfig.ConnectionMultica) + cfg.Daemon.ProjectionSurfaces = appendUniqueString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionMultica) + if cfg.Sessions.PrimaryActivationCarrier == "" { + cfg.Sessions.PrimaryActivationCarrier = productconfig.ConnectionMultica + } + path, err := saveHarnessProductConfig(connectRoot, connectConfigPath, cfg) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "Connection: multica ready") + fmt.Fprintf(cmd.OutOrStdout(), "Harness config: %s\n", path) + return nil +} + +func runConnectGitHub(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(connectGitHubRepo) == "" { + return fmt.Errorf("connect github requires --repo") + } + cfg, _, err := loadHarnessProductConfig(connectRoot, connectConfigPath) + if err != nil { + return err + } + cfg.Connections.GitHub = productconfig.GitHubConnection{ + Enabled: true, + Repo: strings.TrimSpace(connectGitHubRepo), + Branch: strings.TrimSpace(connectGitHubBranch), + } + cfg.Daemon.InteractionWatchers = appendUniqueString(cfg.Daemon.InteractionWatchers, productconfig.ConnectionGitHub) + cfg.Daemon.ProjectionSurfaces = appendUniqueString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionGitHub) + path, err := saveHarnessProductConfig(connectRoot, connectConfigPath, cfg) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "Connection: github ready") + fmt.Fprintf(cmd.OutOrStdout(), "Harness config: %s\n", path) + return nil +} + +func runConnectMnemonhub(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(connectMnemonhubURL) == "" { + return fmt.Errorf("connect mnemonhub requires --endpoint") + } + cfg, _, err := loadHarnessProductConfig(connectRoot, connectConfigPath) + if err != nil { + return err + } + cfg.Connections.Mnemonhub = productconfig.MnemonhubConnection{ + Enabled: true, + Endpoint: strings.TrimSpace(connectMnemonhubURL), + } + cfg.Daemon.InteractionWatchers = appendUniqueString(cfg.Daemon.InteractionWatchers, productconfig.ConnectionMnemonhub) + cfg.Daemon.ProjectionSurfaces = appendUniqueString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionMnemonhub) + path, err := saveHarnessProductConfig(connectRoot, connectConfigPath, cfg) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "Connection: mnemonhub ready") + fmt.Fprintf(cmd.OutOrStdout(), "Harness config: %s\n", path) + return nil +} + +func appendUniqueString(values []string, next string) []string { + next = strings.TrimSpace(next) + if next == "" { + return values + } + for _, value := range values { + if value == next { + return values + } + } + return append(values, next) +} diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index a52cbf3c..ac72992a 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -112,8 +112,9 @@ var multicaProvisionCmd = &cobra.Command{ } var multicaParticipantCmd = &cobra.Command{ - Use: "participant", - Short: "Manage explicit Mnemon participants backed by Multica agents", + Use: "participant", + Short: "Manage explicit Mnemon participants backed by Multica agents", + Hidden: true, } var multicaParticipantRegisterCmd = &cobra.Command{ @@ -958,6 +959,6 @@ func init() { multicaParticipantCmd.AddCommand(multicaParticipantRegisterCmd) multicaCmd.AddCommand(multicaProbeCmd, multicaParticipantCmd, multicaProvisionCmd, multicaImportIssueCmd, multicaProjectCommentCmd) - multicaCmd.GroupID = groupSpine + multicaCmd.GroupID = groupAdvanced rootCmd.AddCommand(multicaCmd) } diff --git a/harness/cmd/mnemon-harness/product_config_helpers.go b/harness/cmd/mnemon-harness/product_config_helpers.go new file mode 100644 index 00000000..2c23a3f5 --- /dev/null +++ b/harness/cmd/mnemon-harness/product_config_helpers.go @@ -0,0 +1,31 @@ +package main + +import ( + "errors" + "os" + "path/filepath" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" +) + +func loadHarnessProductConfig(root, explicit string) (productconfig.Config, string, error) { + path := productconfig.DefaultPath(filepath.Clean(root), explicit) + cfg, err := productconfig.Load(path) + if err == nil { + return cfg, path, nil + } + if !errors.Is(err, os.ErrNotExist) { + return productconfig.Config{}, "", err + } + if legacy, found, legacyErr := productconfig.FromLegacy(filepath.Clean(root)); legacyErr != nil { + return productconfig.Config{}, "", legacyErr + } else if found { + return legacy, path, nil + } + return productconfig.Default(), path, nil +} + +func saveHarnessProductConfig(root, explicit string, cfg productconfig.Config) (string, error) { + path := productconfig.DefaultPath(filepath.Clean(root), explicit) + return path, productconfig.Save(path, cfg) +} diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index da20f9a9..0939dc89 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -22,7 +22,7 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help returned error: %v", err) } got := out.String() - for _, want := range []string{"Agent Integration", "Local Mnemon", "Remote Workspace", "standard events", "setup", "local", "config", "daemon"} { + for _, want := range []string{"Agent Integration", "Local Mnemon", "Remote Workspace", "standard events", "setup", "local", "config", "daemon", "agent", "connect"} { if !strings.Contains(got, want) { t.Fatalf("expected root help to contain %q:\n%s", want, got) } @@ -53,10 +53,14 @@ func TestProductHelpDoesNotExposeInternalVocabulary(t *testing.T) { {"status", "--help"}, {"sync", "--help"}, {"sync", "connect", "--help"}, + {"agent", "--help"}, + {"agent", "add", "--help"}, + {"connect", "--help"}, + {"connect", "multica", "--help"}, {"multica", "--help"}, } { got := executeRootForHelp(t, args...) - for _, blocked := range []string{"binding", "channel", "projection", "kernel", "runtime", "sync cursor", "token file", "control-agent", "import-issue", "project-comment"} { + for _, blocked := range []string{"binding", "channel", "projection", "kernel", "runtime", "sync cursor", "token file", "control-agent", "import-issue", "project-comment", "provision", "participant"} { if strings.Contains(strings.ToLower(got), blocked) { t.Fatalf("%q help leaked internal term %q:\n%s", strings.Join(args, " "), blocked, got) } diff --git a/harness/cmd/mnemon-harness/token.go b/harness/cmd/mnemon-harness/token.go index 24d42a6b..a2ecf392 100644 --- a/harness/cmd/mnemon-harness/token.go +++ b/harness/cmd/mnemon-harness/token.go @@ -16,7 +16,7 @@ var tokenPrincipal string var tokenCmd = &cobra.Command{ Use: "token", - Short: "Manage channel credentials", + Short: "Manage local access credentials", } var tokenRotateCmd = &cobra.Command{ diff --git a/harness/cmd/mnemon-harness/tower.go b/harness/cmd/mnemon-harness/tower.go index 0eb46669..8a305c06 100644 --- a/harness/cmd/mnemon-harness/tower.go +++ b/harness/cmd/mnemon-harness/tower.go @@ -20,6 +20,7 @@ var towerCmd = &cobra.Command{ func init() { towerCmd.Flags().BoolVar(&towerDump, "dump", false, "render a one-shot read-only snapshot of the four pages and exit (headless/scriptable)") + towerCmd.GroupID = groupAdvanced rootCmd.AddCommand(towerCmd) } From 271fcf7e8bb241742f7277648d2c731f824ca0e2 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:47:35 +0800 Subject: [PATCH 011/117] refactor: extract projection comment material Move Mnemon projection comment formatting into internal/projection so hidden CLI shims and the Multica runtime share the same projection material path. Keep the driver helper as a compatibility facade instead of letting command handlers own projection semantics. Validation: go test ./harness/internal/projection ./harness/internal/driver ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-multica-runtime --- harness/cmd/mnemon-harness/multica.go | 7 ++- .../cmd/mnemon-multica-runtime/hub_writer.go | 7 ++- harness/cmd/mnemon-multica-runtime/main.go | 7 ++- harness/internal/driver/multica.go | 29 +++-------- harness/internal/projection/comment.go | 51 +++++++++++++++++++ harness/internal/projection/comment_test.go | 27 ++++++++++ 6 files changed, 102 insertions(+), 26 deletions(-) create mode 100644 harness/internal/projection/comment.go create mode 100644 harness/internal/projection/comment_test.go diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index ac72992a..1d6e1e28 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -12,6 +12,7 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/contract" "github.com/mnemon-dev/mnemon/harness/internal/driver" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" + "github.com/mnemon-dev/mnemon/harness/internal/projection" "github.com/spf13/cobra" ) @@ -298,7 +299,11 @@ func runMulticaProjectComment(cmd *cobra.Command, args []string) error { if err != nil { return err } - body := driver.FormatMulticaProjectionComment(multicaCommentTitle, content, multicaCommentEvents) + body := projection.FormatComment(projection.CommentMaterial{ + Title: multicaCommentTitle, + Body: content, + EventIDs: multicaCommentEvents, + }) comment, err := multicaCLI().AddIssueComment(multicaCommandContext(cmd), multicaIssueID, body) if err != nil { return err diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 847e05e7..216e69b2 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -14,6 +14,7 @@ import ( eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" pview "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view" + "github.com/mnemon-dev/mnemon/harness/internal/projection" ) func (s *runtimeRPCState) writeMulticaHubArtifacts(ctx context.Context, cli driver.MulticaCLI, client *access.Client, rootIssue driver.MulticaIssue, result *runtimeImportResult) { @@ -306,7 +307,11 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if !ok { continue } - commentBody := driver.FormatMulticaProjectionComment("assignment feedback", progressCommentBody(item), []string{item.EventID}) + commentBody := projection.FormatComment(projection.CommentMaterial{ + Title: "assignment feedback", + Body: progressCommentBody(item), + EventIDs: []string{item.EventID}, + }) comment, err := cli.AddIssueComment(ctx, child, commentBody) if err != nil { return err diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index e8c5147b..f6cb9424 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -21,6 +21,7 @@ import ( eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" + "github.com/mnemon-dev/mnemon/harness/internal/projection" ) const runtimeVersion = "dev" @@ -823,7 +824,11 @@ func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.M if result.HubWriteStatus != "" { body += fmt.Sprintf("\nMultica hub write: %s child_issues=%d feedback_comments=%d", result.HubWriteStatus, result.HubChildIssues, result.HubFeedbackComments) } - commentBody := driver.FormatMulticaProjectionComment(title, body, []string{externalID}) + commentBody := projection.FormatComment(projection.CommentMaterial{ + Title: title, + Body: body, + EventIDs: []string{externalID}, + }) comment, err := cli.AddIssueComment(ctx, issue.ID, commentBody) if err != nil { result.ProjectionStatus = "failed" diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index c7ef6f78..0729132d 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -14,6 +14,7 @@ import ( "time" "github.com/mnemon-dev/mnemon/harness/internal/interaction" + "github.com/mnemon-dev/mnemon/harness/internal/projection" ) const ( @@ -917,29 +918,11 @@ func BuildMulticaIssueTeamworkSignal(issue MulticaIssue, opts MulticaIssueSignal } func FormatMulticaProjectionComment(title string, body string, eventIDs []string) string { - title = strings.TrimSpace(title) - body = strings.TrimSpace(body) - var b strings.Builder - if title != "" { - b.WriteString("Mnemon update: ") - b.WriteString(title) - b.WriteString("\n\n") - } else { - b.WriteString("Mnemon update\n\n") - } - if body != "" { - b.WriteString(body) - b.WriteString("\n") - } - if ids := cleanMulticaRefs(eventIDs); len(ids) > 0 { - b.WriteString("\n") - for _, id := range ids { - b.WriteString("mnemon:event=") - b.WriteString(id) - b.WriteString("\n") - } - } - return strings.TrimSpace(b.String()) + return projection.FormatComment(projection.CommentMaterial{ + Title: title, + Body: body, + EventIDs: eventIDs, + }) } func DecodeMulticaIssue(r io.Reader) (MulticaIssue, error) { diff --git a/harness/internal/projection/comment.go b/harness/internal/projection/comment.go new file mode 100644 index 00000000..57b65a71 --- /dev/null +++ b/harness/internal/projection/comment.go @@ -0,0 +1,51 @@ +package projection + +import ( + "strings" +) + +type CommentMaterial struct { + Title string + Body string + EventIDs []string +} + +func FormatComment(material CommentMaterial) string { + title := strings.TrimSpace(material.Title) + body := strings.TrimSpace(material.Body) + var b strings.Builder + if title != "" { + b.WriteString("Mnemon update: ") + b.WriteString(title) + b.WriteString("\n\n") + } else { + b.WriteString("Mnemon update\n\n") + } + if body != "" { + b.WriteString(body) + b.WriteString("\n") + } + if ids := cleanStrings(material.EventIDs); len(ids) > 0 { + b.WriteString("\n") + for _, id := range ids { + b.WriteString("mnemon:event=") + b.WriteString(id) + b.WriteString("\n") + } + } + return strings.TrimSpace(b.String()) +} + +func cleanStrings(values []string) []string { + out := make([]string, 0, len(values)) + seen := map[string]bool{} + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + seen[value] = true + out = append(out, value) + } + return out +} diff --git a/harness/internal/projection/comment_test.go b/harness/internal/projection/comment_test.go new file mode 100644 index 00000000..93ce3cd1 --- /dev/null +++ b/harness/internal/projection/comment_test.go @@ -0,0 +1,27 @@ +package projection + +import ( + "strings" + "testing" +) + +func TestFormatCommentCarriesStableMarkers(t *testing.T) { + got := FormatComment(CommentMaterial{ + Title: "assignment finished", + Body: "Worker reported passing evidence.", + EventIDs: []string{"ev-1", "ev-1", "ev-2"}, + }) + for _, want := range []string{ + "Mnemon update: assignment finished", + "Worker reported passing evidence.", + "mnemon:event=ev-1", + "mnemon:event=ev-2", + } { + if !strings.Contains(got, want) { + t.Fatalf("projection comment missing %q:\n%s", want, got) + } + } + if strings.Count(got, "mnemon:event=ev-1") != 1 { + t.Fatalf("projection comment should dedupe markers:\n%s", got) + } +} From eaa63554263935ddb3f7f4edc591a15b476d2e38 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:51:55 +0800 Subject: [PATCH 012/117] feat: add acceptance multica provision entrypoint Expose Multica runtime and participant provisioning through mnemon-acceptance as a test-only orchestration command. The command delegates to the hidden mnemon-harness Multica provision bridge, keeping provision out of the user-facing harness product path while preserving the real setup needed by acceptance runs. Validation: go test ./harness/cmd/mnemon-acceptance ./harness/cmd/mnemon-harness --- .../acceptance_multica_provision.go | 133 ++++++++++++++++++ .../acceptance_multica_provision_test.go | 96 +++++++++++++ harness/cmd/mnemon-acceptance/root_test.go | 1 + 3 files changed, 230 insertions(+) create mode 100644 harness/cmd/mnemon-acceptance/acceptance_multica_provision.go create mode 100644 harness/cmd/mnemon-acceptance/acceptance_multica_provision_test.go diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go new file mode 100644 index 00000000..880777f4 --- /dev/null +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go @@ -0,0 +1,133 @@ +package main + +import ( + "context" + "fmt" + "io" + "os/exec" + "strings" + "time" + + "github.com/spf13/cobra" +) + +var ( + acceptanceMulticaProvisionHarnessCommand string + acceptanceMulticaProvisionMulticaBin string + acceptanceMulticaProvisionProfile string + acceptanceMulticaProvisionServerURL string + acceptanceMulticaProvisionWorkspaceID string + acceptanceMulticaProvisionRegistry string + acceptanceMulticaProvisionProjectRoot string + acceptanceMulticaProvisionProfileName string + acceptanceMulticaProvisionRuntimeCommand string + acceptanceMulticaProvisionRuntimePath string + acceptanceMulticaProvisionAgentPrefix string + acceptanceMulticaProvisionRestartDaemon bool + acceptanceMulticaProvisionWait time.Duration + acceptanceMulticaProvisionControlAddr string + acceptanceMulticaProvisionControlToken string + acceptanceMulticaProvisionControlTokenFile string + acceptanceMulticaProvisionInjectedHarnessBin string + acceptanceMulticaProvisionManagedRuntime string + acceptanceMulticaProvisionManagedCommand string + acceptanceMulticaProvisionManagedWorkspace string + acceptanceMulticaProvisionManagedTimeout time.Duration +) + +type acceptanceCommandRunner func(ctx context.Context, command string, args []string, stdout, stderr io.Writer) error + +var runAcceptanceMulticaProvisionHarness acceptanceCommandRunner = execAcceptanceCommand + +var acceptanceMulticaProvisionCmd = &cobra.Command{ + Use: "multica-provision", + Short: "Run test-only Multica runtime and agent provisioning", + RunE: func(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(acceptanceMulticaProvisionWorkspaceID) == "" { + return fmt.Errorf("--multica-workspace-id is required") + } + command := strings.TrimSpace(acceptanceMulticaProvisionHarnessCommand) + if command == "" { + command = "mnemon-harness" + } + return runAcceptanceMulticaProvisionHarness( + cmd.Context(), + command, + buildAcceptanceMulticaProvisionArgs(), + cmd.OutOrStdout(), + cmd.ErrOrStderr(), + ) + }, +} + +func init() { + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionHarnessCommand, "mnemon-harness-bin", "mnemon-harness", "mnemon-harness command used for the hidden provisioning bridge") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionMulticaBin, "multica-bin", multicaAcceptanceEnvDefault("MNEMON_MULTICA_BIN", ""), "Multica CLI path") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProfile, "multica-profile", multicaAcceptanceEnvDefault("MNEMON_MULTICA_PROFILE", ""), "Multica CLI profile") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionServerURL, "multica-server-url", multicaAcceptanceEnvDefault("MNEMON_MULTICA_SERVER_URL", ""), "Multica server URL") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionWorkspaceID, "multica-workspace-id", multicaAcceptanceEnvDefault("MNEMON_MULTICA_WORKSPACE_ID", ""), "Multica workspace ID") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRegistry, "registry", "", "Multica participant registry path") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProjectRoot, "project-root", ".", "project root for the default registry path") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProfileName, "runtime-profile-name", "mnemon-runtime", "Multica runtime profile display name") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRuntimeCommand, "runtime-command", "mnemon-multica-runtime", "runtime executable name registered with Multica") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRuntimePath, "runtime-path", "", "absolute local executable path for the runtime profile") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionAgentPrefix, "agent-prefix", "mnemon", "Multica participant agent name prefix") + acceptanceMulticaProvisionCmd.Flags().BoolVar(&acceptanceMulticaProvisionRestartDaemon, "restart-daemon", false, "restart the local Multica daemon after setting the runtime path") + acceptanceMulticaProvisionCmd.Flags().DurationVar(&acceptanceMulticaProvisionWait, "wait", 30*time.Second, "time to wait for the runtime to appear online") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionControlAddr, "mnemon-control-addr", multicaAcceptanceEnvDefault("MNEMON_CONTROL_ADDR", ""), "Local Mnemon URL injected into participant runtime env") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionControlToken, "mnemon-control-token", multicaAcceptanceEnvDefault("MNEMON_CONTROL_TOKEN", ""), "Local Mnemon bearer token injected into participant runtime env") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionControlTokenFile, "mnemon-control-token-file", multicaAcceptanceEnvDefault("MNEMON_CONTROL_TOKEN_FILE", ""), "Local Mnemon bearer token file injected into participant runtime env") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionInjectedHarnessBin, "harness-bin", multicaAcceptanceEnvDefault("MNEMON_HARNESS_BIN", ""), "mnemon-harness executable injected into participant runtime env") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionManagedRuntime, "managed-runtime", multicaAcceptanceEnvDefault("MNEMON_MANAGED_RUNTIME", ""), "managed agent runtime injected into participant env") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionManagedCommand, "managed-command", multicaAcceptanceEnvDefault("MNEMON_MANAGED_COMMAND", ""), "managed runtime command injected into participant env") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionManagedWorkspace, "managed-workspace", multicaAcceptanceEnvDefault("MNEMON_MANAGED_WORKSPACE", ""), "managed runtime workspace injected into participant env") + acceptanceMulticaProvisionCmd.Flags().DurationVar(&acceptanceMulticaProvisionManagedTimeout, "managed-turn-timeout", 0, "managed runtime turn timeout injected into participant env") + rootCmd.AddCommand(acceptanceMulticaProvisionCmd) +} + +func buildAcceptanceMulticaProvisionArgs() []string { + args := []string{"multica"} + args = appendFlag(args, "--multica-bin", acceptanceMulticaProvisionMulticaBin) + args = appendFlag(args, "--multica-profile", acceptanceMulticaProvisionProfile) + args = appendFlag(args, "--multica-server-url", acceptanceMulticaProvisionServerURL) + args = appendFlag(args, "--multica-workspace-id", acceptanceMulticaProvisionWorkspaceID) + args = append(args, "--json", "provision") + args = appendFlag(args, "--registry", acceptanceMulticaProvisionRegistry) + args = appendFlag(args, "--project-root", acceptanceMulticaProvisionProjectRoot) + args = appendFlag(args, "--runtime-profile-name", acceptanceMulticaProvisionProfileName) + args = appendFlag(args, "--runtime-command", acceptanceMulticaProvisionRuntimeCommand) + args = appendFlag(args, "--runtime-path", acceptanceMulticaProvisionRuntimePath) + args = appendFlag(args, "--agent-prefix", acceptanceMulticaProvisionAgentPrefix) + if acceptanceMulticaProvisionRestartDaemon { + args = append(args, "--restart-daemon") + } + if acceptanceMulticaProvisionWait > 0 { + args = appendFlag(args, "--wait", acceptanceMulticaProvisionWait.String()) + } + args = appendFlag(args, "--mnemon-control-addr", acceptanceMulticaProvisionControlAddr) + args = appendFlag(args, "--mnemon-control-token", acceptanceMulticaProvisionControlToken) + args = appendFlag(args, "--mnemon-control-token-file", acceptanceMulticaProvisionControlTokenFile) + args = appendFlag(args, "--harness-bin", acceptanceMulticaProvisionInjectedHarnessBin) + args = appendFlag(args, "--managed-runtime", acceptanceMulticaProvisionManagedRuntime) + args = appendFlag(args, "--managed-command", acceptanceMulticaProvisionManagedCommand) + args = appendFlag(args, "--managed-workspace", acceptanceMulticaProvisionManagedWorkspace) + if acceptanceMulticaProvisionManagedTimeout > 0 { + args = appendFlag(args, "--managed-turn-timeout", acceptanceMulticaProvisionManagedTimeout.String()) + } + return args +} + +func appendFlag(args []string, flag, value string) []string { + value = strings.TrimSpace(value) + if value == "" { + return args + } + return append(args, flag, value) +} + +func execAcceptanceCommand(ctx context.Context, command string, args []string, stdout, stderr io.Writer) error { + proc := exec.CommandContext(ctx, command, args...) + proc.Stdout = stdout + proc.Stderr = stderr + return proc.Run() +} diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_provision_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_provision_test.go new file mode 100644 index 00000000..1692fa44 --- /dev/null +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_provision_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "io" + "slices" + "strings" + "testing" + "time" + + "github.com/spf13/cobra" +) + +func TestMulticaProvisionAcceptanceCommandRegistered(t *testing.T) { + commands := map[string]bool{} + for _, cmd := range rootCmd.Commands() { + commands[cmd.Name()] = true + } + if !commands["multica-provision"] { + t.Fatalf("mnemon-acceptance should expose multica-provision command: %v", commands) + } +} + +func TestMulticaProvisionAcceptanceBuildsHiddenHarnessBridge(t *testing.T) { + oldWorkspace := acceptanceMulticaProvisionWorkspaceID + oldProfile := acceptanceMulticaProvisionProfile + oldRuntimeCommand := acceptanceMulticaProvisionRuntimeCommand + oldRuntimePath := acceptanceMulticaProvisionRuntimePath + oldWait := acceptanceMulticaProvisionWait + acceptanceMulticaProvisionWorkspaceID = "ws-1" + acceptanceMulticaProvisionProfile = "desktop-api.multica.ai" + acceptanceMulticaProvisionRuntimeCommand = "mnemon-multica-runtime" + acceptanceMulticaProvisionRuntimePath = "/tmp/mnemon-multica-runtime" + acceptanceMulticaProvisionWait = 3 * time.Second + t.Cleanup(func() { + acceptanceMulticaProvisionWorkspaceID = oldWorkspace + acceptanceMulticaProvisionProfile = oldProfile + acceptanceMulticaProvisionRuntimeCommand = oldRuntimeCommand + acceptanceMulticaProvisionRuntimePath = oldRuntimePath + acceptanceMulticaProvisionWait = oldWait + }) + + args := buildAcceptanceMulticaProvisionArgs() + for _, want := range []string{ + "multica", + "--json", + "provision", + "--multica-workspace-id", + "ws-1", + "--runtime-command", + "mnemon-multica-runtime", + "--runtime-path", + "/tmp/mnemon-multica-runtime", + "--wait", + "3s", + } { + if !slices.Contains(args, want) { + t.Fatalf("args missing %q: %v", want, args) + } + } + if strings.Join(args, " ") == "" { + t.Fatal("args must not be empty") + } +} + +func TestMulticaProvisionAcceptanceRunsHarnessBridge(t *testing.T) { + oldRunner := runAcceptanceMulticaProvisionHarness + oldCommand := acceptanceMulticaProvisionHarnessCommand + oldWorkspace := acceptanceMulticaProvisionWorkspaceID + defer func() { + runAcceptanceMulticaProvisionHarness = oldRunner + acceptanceMulticaProvisionHarnessCommand = oldCommand + acceptanceMulticaProvisionWorkspaceID = oldWorkspace + }() + + var gotCommand string + var gotArgs []string + runAcceptanceMulticaProvisionHarness = func(ctx context.Context, command string, args []string, stdout, stderr io.Writer) error { + gotCommand = command + gotArgs = append([]string(nil), args...) + return nil + } + acceptanceMulticaProvisionHarnessCommand = "/tmp/mnemon-harness" + acceptanceMulticaProvisionWorkspaceID = "ws-acceptance" + + cmd := &cobra.Command{} + if err := acceptanceMulticaProvisionCmd.RunE(cmd, nil); err != nil { + t.Fatal(err) + } + if gotCommand != "/tmp/mnemon-harness" { + t.Fatalf("command = %q", gotCommand) + } + if !slices.Contains(gotArgs, "provision") || !slices.Contains(gotArgs, "ws-acceptance") { + t.Fatalf("hidden harness provision args not propagated: %v", gotArgs) + } +} diff --git a/harness/cmd/mnemon-acceptance/root_test.go b/harness/cmd/mnemon-acceptance/root_test.go index 9e0d92f8..d4279366 100644 --- a/harness/cmd/mnemon-acceptance/root_test.go +++ b/harness/cmd/mnemon-acceptance/root_test.go @@ -14,6 +14,7 @@ func TestRootExposesAcceptanceScenarioCommands(t *testing.T) { "r1-task-sim", "r1-cluster-single-entrypoint", "r1-github-mesh-task-suite", + "multica-provision", "multica-runtime-prod-sim", } { if !commands[want] { From 7413650e8d86d8ceb9e9a941de9959ad2eb0b204 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:53:56 +0800 Subject: [PATCH 013/117] test: classify r2 harness architecture packages Extend the coreguard main-axis inventory to cover the R2 external interaction layers introduced during productization cleanup: interaction points, drive sources, projection surfaces, product config, daemon supervision, and Multica surface primitives. Validation: go test ./harness/internal/coreguard ./harness/internal/projection ./harness/internal/productconfig ./harness/internal/daemon ./harness/internal/drive ./harness/internal/interaction ./harness/internal/activationtrace ./harness/internal/surface/multica --- harness/internal/coreguard/mainaxis_test.go | 55 ++++++++++++--------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/harness/internal/coreguard/mainaxis_test.go b/harness/internal/coreguard/mainaxis_test.go index 9fef1516..2aed6ddc 100644 --- a/harness/internal/coreguard/mainaxis_test.go +++ b/harness/internal/coreguard/mainaxis_test.go @@ -10,11 +10,15 @@ import ( type mainAxisOwner string const ( - ownerEvent mainAxisOwner = "event" - ownerHostAgent mainAxisOwner = "hostagent" - ownerMnemond mainAxisOwner = "mnemond" - ownerMnemonhub mainAxisOwner = "mnemonhub" - ownerGuard mainAxisOwner = "guard" + ownerEvent mainAxisOwner = "event" + ownerHostAgent mainAxisOwner = "hostagent" + ownerMnemond mainAxisOwner = "mnemond" + ownerMnemonhub mainAxisOwner = "mnemonhub" + ownerInteractionPoint mainAxisOwner = "interaction-point" + ownerDriveSource mainAxisOwner = "drive-source" + ownerProjectionSurface mainAxisOwner = "projection-surface" + ownerProductConfig mainAxisOwner = "product-config" + ownerGuard mainAxisOwner = "guard" ) type packageMainAxis struct { @@ -25,23 +29,29 @@ type packageMainAxis struct { // packageMainAxisInventory is the executable inventory for the main-axis convergence goal. It does // not claim the current package names are final; it prevents new top-level implementation concepts -// from appearing without an explicit owner under hostagent, mnemond, mnemonhub, or event. +// from appearing without an explicit owner under the R2 architecture layers. var packageMainAxisInventory = map[string]packageMainAxis{ - "app": {owner: ownerMnemond, role: "daemon wiring and local mnemond boot", target: "mnemond"}, - "assembler": {owner: ownerMnemond, role: "mnemond policy/runtime assembly", target: "mnemond"}, - "assets": {owner: ownerHostAgent, role: "hostagent integration and event-policy assets", target: "hostagent/assets"}, - "codexapp": {owner: ownerHostAgent, role: "Codex hostagent appserver adapter", target: "hostagent/codexapp"}, - "config": {owner: ownerMnemond, role: "mnemond local configuration", target: "mnemond/config"}, - "contract": {owner: ownerEvent, role: "event and mnemond boundary DTOs", target: "event/contract"}, - "coreguard": {owner: ownerGuard, role: "architecture guard tests", target: "coreguard"}, - "driver": {owner: ownerMnemond, role: "mnemond tick driver", target: "mnemond/daemon"}, - "event": {owner: ownerEvent, role: "canonical event and envelope model", target: "event"}, - "eventstore": {owner: ownerEvent, role: "event envelope append/read facade", target: "event/store"}, - "hostagent": {owner: ownerHostAgent, role: "hostagent setup and thin shims", target: "hostagent"}, - "mnemonhub": {owner: ownerMnemonhub, role: "remote accepted event exchange server and exchange mechanics", target: "mnemonhub"}, - "replay": {owner: ownerMnemond, role: "mnemond determinism verification", target: "mnemond/replay"}, - "runtime": {owner: ownerMnemond, role: "local mnemond event runtime", target: "mnemond"}, - "ui": {owner: ownerMnemond, role: "read-only mnemond operator observability", target: "mnemond/observe"}, + "activationtrace": {owner: ownerProjectionSurface, role: "runtime activation trace material for external run displays", target: "projection/activationtrace"}, + "app": {owner: ownerMnemond, role: "daemon wiring and local mnemond boot", target: "mnemond"}, + "assembler": {owner: ownerMnemond, role: "mnemond policy/runtime assembly", target: "mnemond"}, + "assets": {owner: ownerHostAgent, role: "hostagent integration and event-policy assets", target: "hostagent/assets"}, + "codexapp": {owner: ownerHostAgent, role: "Codex hostagent appserver adapter", target: "hostagent/codexapp"}, + "config": {owner: ownerMnemond, role: "mnemond local configuration", target: "mnemond/config"}, + "contract": {owner: ownerEvent, role: "event and mnemond boundary DTOs", target: "event/contract"}, + "coreguard": {owner: ownerGuard, role: "architecture guard tests", target: "coreguard"}, + "daemon": {owner: ownerMnemond, role: "harness daemon worker supervision for local event node operation", target: "mnemond/daemon"}, + "drive": {owner: ownerDriveSource, role: "managed local agent drive source and wake loop primitives", target: "drive/managed-local"}, + "driver": {owner: ownerMnemond, role: "legacy facade for mnemond and adapter helper code during R2 cleanup", target: "mnemond/daemon"}, + "event": {owner: ownerEvent, role: "canonical event and envelope model", target: "event"}, + "eventstore": {owner: ownerEvent, role: "event envelope append/read facade", target: "event/store"}, + "hostagent": {owner: ownerHostAgent, role: "hostagent setup and thin shims", target: "hostagent"}, + "interaction": {owner: ownerInteractionPoint, role: "external stimulus material separated into rule/narrative/refs", target: "interaction/event-material"}, + "mnemonhub": {owner: ownerMnemonhub, role: "remote accepted event exchange server and exchange mechanics", target: "mnemonhub"}, + "productconfig": {owner: ownerProductConfig, role: "harness product configuration for agents, connections, daemon workers, and surfaces", target: "product/config"}, + "projection": {owner: ownerProjectionSurface, role: "accepted/derived state material prepared for external projection surfaces", target: "projection"}, + "replay": {owner: ownerMnemond, role: "mnemond determinism verification", target: "mnemond/replay"}, + "runtime": {owner: ownerMnemond, role: "local mnemond event runtime", target: "mnemond"}, + "ui": {owner: ownerMnemond, role: "read-only mnemond operator observability", target: "mnemond/observe"}, } var demotedMainAxisPackages = map[string]bool{} @@ -58,6 +68,7 @@ var nestedMainAxisInventory = map[string]packageMainAxis{ }, "mnemond/state": {owner: ownerMnemond, role: "local event/state store and materialized-state applier", target: "mnemond/state"}, "mnemonhub/exchange": {owner: ownerMnemonhub, role: "mnemonhub event exchange client, cursors, and local ledger acknowledgements", target: "mnemonhub/exchange"}, + "surface/multica": {owner: ownerProjectionSurface, role: "Multica surface registry, metadata, and projection ledger primitives", target: "projection/surface/multica"}, } var retiredTopLevelImplementationPackages = []string{ @@ -77,7 +88,7 @@ func TestInternalPackagesHaveMainAxisOwner(t *testing.T) { for _, pkg := range topLevelInternalPackages(t) { inv, ok := packageMainAxisInventory[pkg] if !ok { - t.Errorf("harness/internal/%s has no main-axis owner; classify it under hostagent, mnemond, mnemonhub, or event before adding a new top-level concept", pkg) + t.Errorf("harness/internal/%s has no main-axis owner; classify it under an accepted R2 layer before adding a new top-level concept", pkg) continue } if inv.owner == "" || strings.TrimSpace(inv.role) == "" || strings.TrimSpace(inv.target) == "" { From 0a6d9dda55dee18c38c368fe950f6225fc5187f0 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 13:57:14 +0800 Subject: [PATCH 014/117] refactor: move multica projection surface policy Move assignment mailbox text, progress comment text, and Multica status mapping into internal/surface/multica. The runtime now maps accepted Mnemon view items into surface material instead of owning Multica UI policy directly. Validation: go test ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance ./harness/internal/coreguard --- .../acceptance_multica_runtime.go | 14 +-- .../cmd/mnemon-multica-runtime/hub_writer.go | 110 ++++------------ .../cmd/mnemon-multica-runtime/main_test.go | 3 +- .../internal/surface/multica/projection.go | 119 ++++++++++++++++++ .../surface/multica/projection_test.go | 52 ++++++++ 5 files changed, 203 insertions(+), 95 deletions(-) create mode 100644 harness/internal/surface/multica/projection.go create mode 100644 harness/internal/surface/multica/projection_test.go diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 87a753ab..55aeed80 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -12,6 +12,7 @@ import ( "time" "github.com/mnemon-dev/mnemon/harness/internal/driver" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" "github.com/spf13/cobra" ) @@ -628,11 +629,11 @@ func waitMulticaHubProjectionCompletion(ctx context.Context, cli driver.MulticaC } func multicaHubProjectionComplete(root driver.MulticaIssue, children []driver.MulticaIssue, childComments map[string][]driver.MulticaComment) bool { - if !multicaIssueStatusDone(root.Status) || len(children) == 0 { + if !multicasurface.IssueStatusDone(root.Status) || len(children) == 0 { return false } for _, child := range children { - if !multicaIssueStatusDone(child.Status) { + if !multicasurface.IssueStatusDone(child.Status) { return false } if !multicaCommentsContainFeedbackMarker(childComments[child.ID]) { @@ -652,15 +653,6 @@ func multicaCommentsContainFeedbackMarker(comments []driver.MulticaComment) bool return false } -func multicaIssueStatusDone(status string) bool { - switch strings.ToLower(strings.TrimSpace(status)) { - case "done", "completed", "complete": - return true - default: - return false - } -} - func multicaIssueStatuses(issues []driver.MulticaIssue) map[string]string { out := map[string]string{} for _, issue := range issues { diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 216e69b2..4504df71 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -15,6 +15,7 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" pview "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view" "github.com/mnemon-dev/mnemon/harness/internal/projection" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) func (s *runtimeRPCState) writeMulticaHubArtifacts(ctx context.Context, cli driver.MulticaCLI, client *access.Client, rootIssue driver.MulticaIssue, result *runtimeImportResult) { @@ -223,8 +224,8 @@ func (s *runtimeRPCState) projectAssignmentMailboxes(ctx context.Context, cli dr continue } child, err := cli.CreateIssue(ctx, driver.MulticaCreateIssueRequest{ - Title: assignmentMailboxTitle(projection.Item), - Description: assignmentMailboxDescription(projection.Item, projection.Result), + Title: multicasurface.AssignmentMailboxTitle(assignmentMailboxMaterial(projection.Item, projection.Result)), + Description: multicasurface.AssignmentMailboxDescription(assignmentMailboxMaterial(projection.Item, projection.Result)), ParentID: projection.Result.RootIssueID, Status: "in_progress", Priority: "medium", @@ -307,20 +308,21 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if !ok { continue } + material := progressFeedbackMaterial(item) commentBody := projection.FormatComment(projection.CommentMaterial{ Title: "assignment feedback", - Body: progressCommentBody(item), + Body: multicasurface.ProgressCommentBody(material), EventIDs: []string{item.EventID}, }) comment, err := cli.AddIssueComment(ctx, child, commentBody) if err != nil { return err } - if status := multicaStatusForProgress(item); status != "" { + if status := multicasurface.ProgressIssueStatus(material); status != "" { _, _ = cli.SetIssueStatus(ctx, child, status) } rootStatus := "in_review" - if multicaProgressCompletesAssignment(item) { + if multicasurface.ProgressCompletesAssignment(material) { if done, _ := allMulticaAssignmentChildrenDone(ctx, cli, result.RootIssueID, result.SessionID, child); done { rootStatus = "done" } @@ -343,31 +345,6 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. return nil } -func multicaStatusForProgress(item runtimeProgress) string { - switch strings.ToLower(strings.TrimSpace(item.FeedbackKind)) { - case "blocker": - return "blocked" - case "result": - return "done" - case "progress": - return "in_progress" - } - if strings.TrimSpace(item.Blocker) != "" { - return "blocked" - } - if strings.TrimSpace(item.Result) != "" { - return "done" - } - return "" -} - -func multicaProgressCompletesAssignment(item runtimeProgress) bool { - if strings.EqualFold(strings.TrimSpace(item.FeedbackKind), "result") { - return true - } - return strings.TrimSpace(item.Result) != "" -} - type runtimeAssignment struct { ID string EventID string @@ -667,64 +644,31 @@ func hubStringList(raw any) []string { return out } -func assignmentMailboxTitle(item runtimeAssignment) string { - scope := strings.TrimSpace(item.Scope) - if scope == "" { - scope = strings.TrimSpace(item.ID) +func assignmentMailboxMaterial(item runtimeAssignment, result *runtimeImportResult) multicasurface.AssignmentMailboxMaterial { + material := multicasurface.AssignmentMailboxMaterial{ + ID: item.ID, + Scope: item.Scope, + Assignee: item.Assignee, + ExpectedWork: item.ExpectedWork, + ExpectedFeedback: item.ExpectedFeedback, + Rationale: item.Rationale, } - if scope == "" { - scope = "assignment" + if result != nil { + material.SessionID = result.SessionID } - return "Mnemon assignment " + item.ID + ": " + scope + return material } -func assignmentMailboxDescription(item runtimeAssignment, result *runtimeImportResult) string { - var b strings.Builder - b.WriteString("Mnemon assignment mailbox\n\n") - writeLine := func(label, value string) { - value = strings.TrimSpace(value) - if value == "" { - return - } - b.WriteString(label) - b.WriteString(": ") - b.WriteString(value) - b.WriteString("\n") - } - writeLine("Assignment", item.ID) - writeLine("Session", result.SessionID) - writeLine("Scope", item.Scope) - writeLine("Assignee", item.Assignee) - writeLine("Expected work", item.ExpectedWork) - writeLine("Expected feedback", item.ExpectedFeedback) - writeLine("Rationale", item.Rationale) - return strings.TrimSpace(b.String()) -} - -func progressCommentBody(item runtimeProgress) string { - var b strings.Builder - writeLine := func(label, value string) { - value = strings.TrimSpace(value) - if value == "" { - return - } - b.WriteString(label) - b.WriteString(": ") - b.WriteString(value) - b.WriteString("\n") - } - writeLine("Assignment", item.AssignmentRef) - writeLine("Feedback", item.FeedbackKind) - writeLine("Summary", item.Summary) - writeLine("Result", item.Result) - writeLine("Blocker", item.Blocker) - if len(item.ArtifactRefs) > 0 { - writeLine("Artifacts", strings.Join(item.ArtifactRefs, ", ")) - } - if len(item.EvidenceRefs) > 0 { - writeLine("Evidence", strings.Join(item.EvidenceRefs, ", ")) +func progressFeedbackMaterial(item runtimeProgress) multicasurface.ProgressFeedbackMaterial { + return multicasurface.ProgressFeedbackMaterial{ + AssignmentRef: item.AssignmentRef, + FeedbackKind: item.FeedbackKind, + Summary: item.Summary, + Result: item.Result, + Blocker: item.Blocker, + ArtifactRefs: item.ArtifactRefs, + EvidenceRefs: item.EvidenceRefs, } - return strings.TrimSpace(b.String()) } func findExistingMulticaAssignmentIssue(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, source driver.MulticaHubLedgerSource) (driver.MulticaIssue, bool, error) { diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index c1f4cd7d..ff1c7727 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -17,6 +17,7 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" pview "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) func TestRuntimeEnvValueUsesLastValue(t *testing.T) { @@ -805,7 +806,7 @@ func TestMulticaStatusForProgressIsRuleBased(t *testing.T) { {name: "result narrative fallback", item: runtimeProgress{Result: "validated"}, want: "done"}, {name: "unknown", item: runtimeProgress{Summary: "not enough signal"}, want: ""}, } { - if got := multicaStatusForProgress(tc.item); got != tc.want { + if got := multicasurface.ProgressIssueStatus(progressFeedbackMaterial(tc.item)); got != tc.want { t.Fatalf("%s: status = %q, want %q", tc.name, got, tc.want) } } diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go new file mode 100644 index 00000000..3df34f16 --- /dev/null +++ b/harness/internal/surface/multica/projection.go @@ -0,0 +1,119 @@ +package multica + +import ( + "strings" +) + +type AssignmentMailboxMaterial struct { + ID string + SessionID string + Scope string + Assignee string + ExpectedWork string + ExpectedFeedback string + Rationale string +} + +type ProgressFeedbackMaterial struct { + AssignmentRef string + FeedbackKind string + Summary string + Result string + Blocker string + ArtifactRefs []string + EvidenceRefs []string +} + +func AssignmentMailboxTitle(item AssignmentMailboxMaterial) string { + scope := strings.TrimSpace(item.Scope) + if scope == "" { + scope = strings.TrimSpace(item.ID) + } + if scope == "" { + scope = "assignment" + } + return "Mnemon assignment " + item.ID + ": " + scope +} + +func AssignmentMailboxDescription(item AssignmentMailboxMaterial) string { + var b strings.Builder + b.WriteString("Mnemon assignment mailbox\n\n") + writeLine := func(label, value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + b.WriteString(label) + b.WriteString(": ") + b.WriteString(value) + b.WriteString("\n") + } + writeLine("Assignment", item.ID) + writeLine("Session", item.SessionID) + writeLine("Scope", item.Scope) + writeLine("Assignee", item.Assignee) + writeLine("Expected work", item.ExpectedWork) + writeLine("Expected feedback", item.ExpectedFeedback) + writeLine("Rationale", item.Rationale) + return strings.TrimSpace(b.String()) +} + +func ProgressCommentBody(item ProgressFeedbackMaterial) string { + var b strings.Builder + writeLine := func(label, value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + b.WriteString(label) + b.WriteString(": ") + b.WriteString(value) + b.WriteString("\n") + } + writeLine("Assignment", item.AssignmentRef) + writeLine("Feedback", item.FeedbackKind) + writeLine("Summary", item.Summary) + writeLine("Result", item.Result) + writeLine("Blocker", item.Blocker) + if len(item.ArtifactRefs) > 0 { + writeLine("Artifacts", strings.Join(item.ArtifactRefs, ", ")) + } + if len(item.EvidenceRefs) > 0 { + writeLine("Evidence", strings.Join(item.EvidenceRefs, ", ")) + } + return strings.TrimSpace(b.String()) +} + +func ProgressIssueStatus(item ProgressFeedbackMaterial) string { + switch strings.ToLower(strings.TrimSpace(item.FeedbackKind)) { + case "blocker": + return "blocked" + case "result": + return "done" + case "progress": + return "in_progress" + } + if strings.TrimSpace(item.Blocker) != "" { + return "blocked" + } + if strings.TrimSpace(item.Result) != "" { + return "done" + } + return "" +} + +func ProgressCompletesAssignment(item ProgressFeedbackMaterial) bool { + if strings.EqualFold(strings.TrimSpace(item.FeedbackKind), "result") { + return true + } + return strings.TrimSpace(item.Result) != "" +} + +func IssueStatusDone(status string) bool { + switch strings.ToLower(strings.TrimSpace(status)) { + case "done", "completed", "complete": + return true + default: + return false + } +} diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go new file mode 100644 index 00000000..6322e917 --- /dev/null +++ b/harness/internal/surface/multica/projection_test.go @@ -0,0 +1,52 @@ +package multica + +import ( + "strings" + "testing" +) + +func TestAssignmentMailboxMaterial(t *testing.T) { + item := AssignmentMailboxMaterial{ + ID: "asg-1", + SessionID: "session-1", + Scope: "runtime/readiness", + Assignee: "researcher@team", + ExpectedWork: "Check runtime output.", + ExpectedFeedback: "Brief result.", + } + if got := AssignmentMailboxTitle(item); got != "Mnemon assignment asg-1: runtime/readiness" { + t.Fatalf("title = %q", got) + } + body := AssignmentMailboxDescription(item) + for _, want := range []string{"Assignment: asg-1", "Session: session-1", "Assignee: researcher@team"} { + if !strings.Contains(body, want) { + t.Fatalf("description missing %q:\n%s", want, body) + } + } +} + +func TestProgressFeedbackMaterial(t *testing.T) { + item := ProgressFeedbackMaterial{ + AssignmentRef: "asg-1", + FeedbackKind: "result", + Summary: "Done", + Result: "Validated", + ArtifactRefs: []string{"run-1"}, + EvidenceRefs: []string{"comment-1"}, + } + body := ProgressCommentBody(item) + for _, want := range []string{"Assignment: asg-1", "Feedback: result", "Artifacts: run-1", "Evidence: comment-1"} { + if !strings.Contains(body, want) { + t.Fatalf("progress body missing %q:\n%s", want, body) + } + } + if got := ProgressIssueStatus(item); got != "done" { + t.Fatalf("progress status = %q", got) + } + if !ProgressCompletesAssignment(item) { + t.Fatal("result progress should complete assignment") + } + if !IssueStatusDone("completed") { + t.Fatal("completed should be terminal") + } +} From 72cc3ad8420c611e817870be138b130b680e6acf Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 14:42:56 +0800 Subject: [PATCH 015/117] fix: harden multica runtime dispatch entry Resolve Multica issue context from daemon-provided ids, standard issue mention links, and narrow issue-key tags before querying the issue through the CLI. Treat root-session metadata writes as projection warnings so transient Multica surface failures do not block Mnemon ingest. Validation: go test ./harness/cmd/mnemon-multica-runtime ./harness/internal/driver ./harness/internal/surface/multica; go test ./harness/...; make harness-build. --- harness/cmd/mnemon-multica-runtime/main.go | 32 +++-- .../cmd/mnemon-multica-runtime/main_test.go | 124 ++++++++++++++++++ 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index f6cb9424..c40d400f 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -26,7 +26,11 @@ import ( const runtimeVersion = "dev" -var assignedIssuePattern = regexp.MustCompile(`(?i)(?:assigned\s+issue\s+id\s+is|issue[_\s-]*id)\s*[::]\s*([A-Za-z0-9][A-Za-z0-9._:-]*)`) +var ( + assignedIssuePattern = regexp.MustCompile(`(?i)(?:assigned\s+issue\s+id\s+is|issue[_\s-]*id)\s*[::]\s*([A-Za-z0-9][A-Za-z0-9._:-]*)`) + multicaIssueMentionPattern = regexp.MustCompile(`(?i)mention://issue/([A-Za-z0-9][A-Za-z0-9._:-]*)`) + multicaTaggedIssuePattern = regexp.MustCompile(`(?i)(?:^|[\s([{"'])[@#]([A-Z][A-Z0-9]+-\d+)(?:$|[\s\])}.,;:'"])`) +) type runtimeConfig struct { Args []string @@ -371,14 +375,13 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink addPayloadRuleString(draft.Payload, "session_id", result.SessionID) addPayloadRuleString(draft.Payload, "source_issue_id", issue.ID) if err := cli.SetIssueMetadataMap(multicaCtx, issue.ID, rootSessionMetadata(result, draft, s.now())); err != nil { - result.Status = "failed" - result.Err = fmt.Errorf("set Multica root session metadata: %w", err) - emitRuntimeCommand(progress, "multica issue metadata set "+issue.ID+" mnemon.root-session", result.Err.Error(), 1) - emitRuntimeProgress(progress, "Failed to write Mnemon root session metadata.") - return result + metadataErr := fmt.Errorf("set Multica root session metadata: %w", err) + emitRuntimeCommand(progress, "multica issue metadata set "+issue.ID+" mnemon.root-session", metadataErr.Error(), 1) + emitRuntimeProgress(progress, "Root session metadata write failed; continuing with Mnemon ingest from issue context.") + } else { + emitRuntimeCommand(progress, "multica issue metadata set "+issue.ID+" mnemon.root-session", "Root session metadata written for "+runtimeIssueLabel(issue)+".", 0) + emitRuntimeProgress(progress, "Root session metadata written for "+runtimeIssueLabel(issue)+".") } - emitRuntimeCommand(progress, "multica issue metadata set "+issue.ID+" mnemon.root-session", "Root session metadata written for "+runtimeIssueLabel(issue)+".", 0) - emitRuntimeProgress(progress, "Root session metadata written for "+runtimeIssueLabel(issue)+".") } addr := strings.TrimSpace(envValue(s.Env, "MNEMON_CONTROL_ADDR")) if addr == "" { @@ -1540,7 +1543,15 @@ func extractRuntimeInput(params map[string]any) string { } func extractAssignedIssueID(input string) string { - match := assignedIssuePattern.FindStringSubmatch(input) + match := multicaIssueMentionPattern.FindStringSubmatch(input) + if len(match) >= 2 { + return strings.Trim(match[1], " \t\r\n.,;)") + } + match = assignedIssuePattern.FindStringSubmatch(input) + if len(match) >= 2 { + return strings.Trim(match[1], " \t\r\n.,;)") + } + match = multicaTaggedIssuePattern.FindStringSubmatch(input) if len(match) >= 2 { return strings.Trim(match[1], " \t\r\n.,;)") } @@ -1624,6 +1635,9 @@ func runtimeID(prefix string, now time.Time) string { func runtimeTimeout(env []string) time.Duration { raw := envValue(env, "MNEMON_MULTICA_RUNTIME_TIMEOUT") + if raw == "" { + raw = envValue(env, "MULTICA_HTTP_TIMEOUT") + } if raw == "" { return 30 * time.Second } diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index ff1c7727..4028f267 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -30,6 +30,37 @@ func TestRuntimeEnvValueUsesLastValue(t *testing.T) { } } +func TestRuntimeTimeoutUsesMulticaHTTPFallback(t *testing.T) { + if got := runtimeTimeout([]string{"MULTICA_HTTP_TIMEOUT=2m"}); got != 2*time.Minute { + t.Fatalf("runtimeTimeout fallback = %s, want 2m", got) + } + if got := runtimeTimeout([]string{"MULTICA_HTTP_TIMEOUT=2m", "MNEMON_MULTICA_RUNTIME_TIMEOUT=15s"}); got != 15*time.Second { + t.Fatalf("runtimeTimeout override = %s, want 15s", got) + } +} + +func TestExtractAssignedIssueIDAcceptsMulticaIssueMentionsAndTags(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {name: "explicit assigned issue", input: "Your assigned issue ID is: iss-7\nPlease work on it.", want: "iss-7"}, + {name: "multica mention", input: "Open [TEA-51](mention://issue/issue-51) for the current task.", want: "issue-51"}, + {name: "mention beats legacy text", input: "Your assigned issue ID is: stale\nOpen [TEA-52](mention://issue/issue-52).", want: "issue-52"}, + {name: "at identifier tag", input: "Please handle @TEA-49 next.", want: "TEA-49"}, + {name: "hash identifier tag", input: "Review #TEA-50.", want: "TEA-50"}, + {name: "non issue tag", input: "Please coordinate with @team.", want: ""}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := extractAssignedIssueID(tc.input); got != tc.want { + t.Fatalf("extractAssignedIssueID = %q, want %q", got, tc.want) + } + }) + } +} + func TestRuntimeMulticaHubLedgerPathDefaultsToManagedWorkspace(t *testing.T) { tmp := t.TempDir() workspace := filepath.Join(tmp, "managed-workspace") @@ -521,6 +552,99 @@ esac } } +func TestRuntimeContinuesWhenRootSessionMetadataWriteFails(t *testing.T) { + tmp := t.TempDir() + argsPath := filepath.Join(tmp, "multica.args") + commentPath := filepath.Join(tmp, "comment.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue get TEA-9"*) printf '{"id":"iss-9","identifier":"TEA-9","title":"Metadata retry tolerance","description":"Admit the teamwork issue even when Multica metadata projection is temporarily unavailable.","status":"todo","priority":"medium"}\n' ;; + *"issue metadata set iss-9"*) printf 'metadata timeout\n' >&2; exit 42 ;; + *"issue comment add iss-9"*) cat > "$MULTICA_COMMENT_PATH"; printf '{"id":"comment-9","issue_id":"iss-9","content":"ok","type":"comment"}\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + + var gotPrincipal string + var got contract.ObservationEnvelope + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/ingest": + gotPrincipal = r.Header.Get(access.PrincipalHeader) + if err := json.NewDecoder(r.Body).Decode(&got); err != nil { + t.Fatal(err) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(access.IngestReceipt{Seq: 29, Dup: false, Ticked: true}) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer srv.Close() + + input := `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"input":[{"type":"text","text":"Please coordinate @TEA-9."}]}}` + "\n" + var out bytes.Buffer + err := runRuntime(runtimeConfig{ + Args: []string{"app-server", "--listen", "stdio://"}, + Env: runtimeTestEnv( + "MNEMON_MULTICA_BIN="+bin, + "MNEMON_HUB_BACKEND=multica", + "MNEMON_MULTICA_HUB_WRITE=off", + "MNEMON_CONTROL_ADDR="+srv.URL, + "MNEMON_CONTROL_PRINCIPAL=planner@team", + "MULTICA_ARGS_PATH="+argsPath, + "MULTICA_COMMENT_PATH="+commentPath, + "MULTICA_TASK_ID=task-9", + "MULTICA_AGENT_ID=agent-planner", + ), + CWD: tmp, + Stdin: strings.NewReader(input), + Stdout: &out, + Now: fixedRuntimeTime, + }) + if err != nil { + t.Fatal(err) + } + if gotPrincipal != "planner@team" { + t.Fatalf("principal header = %q", gotPrincipal) + } + if got.Event.Type != "teamwork_signal.write_candidate.observed" { + t.Fatalf("event type = %q", got.Event.Type) + } + rule := got.Event.Payload["rule"].(map[string]any) + if rule["root_issue_id"] != "iss-9" || rule["session_id"] != driver.MulticaSessionID("iss-9") { + t.Fatalf("root hub rule mismatch after metadata failure: %+v", rule) + } + output := out.String() + for _, want := range []string{ + "Root session metadata write failed; continuing with Mnemon ingest from issue context.", + "Mnemon ingest: recorded seq=29", + "Multica projection: comment=comment-9", + } { + if !strings.Contains(output, want) { + t.Fatalf("runtime output missing %q:\n%s", want, output) + } + } + if strings.Contains(output, "Mnemon ingest: failed") || strings.Contains(output, "Multica hub write: failed") { + t.Fatalf("metadata projection failure polluted protocol result:\n%s", output) + } + comment := mustReadRuntimeTestFile(t, commentPath) + for _, want := range []string{"Mnemon update: issue admitted", "Mnemon ingest: seq=29", "Hub backend: multica"} { + if !strings.Contains(comment, want) { + t.Fatalf("comment missing %q:\n%s", want, comment) + } + } + args := mustReadRuntimeTestFile(t, argsPath) + if !strings.Contains(args, "issue metadata set iss-9 --key mnemon.hub_backend") || !strings.Contains(args, "issue comment add iss-9 --content-stdin --output json") { + t.Fatalf("runtime should attempt metadata projection then continue to comment projection:\n%s", args) + } +} + func TestRuntimeWritesAssignmentMailboxChildIssueFromView(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "multica.args") From 97ddd2e04a2ce25f352e19ac735c2362a1cc6cc1 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:04:51 +0800 Subject: [PATCH 016/117] refactor: structure multica runtime metadata projection Query Multica issue metadata after issue get so runtime classification uses the authoritative metadata surface with a fallback to the embedded issue payload. Render assignment mailbox issues and runtime comments as structured Markdown with issue mentions while keeping mnemon.* routing and dedupe fields in metadata. Validated with go test -count=1 ./harness/.... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 14 +- harness/cmd/mnemon-multica-runtime/main.go | 110 ++++++--- .../cmd/mnemon-multica-runtime/main_test.go | 54 ++++- .../internal/surface/multica/projection.go | 227 +++++++++++++++--- .../surface/multica/projection_test.go | 40 ++- 5 files changed, 363 insertions(+), 82 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 4504df71..4af51ef0 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -166,6 +166,7 @@ func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driv Participant: participant, Source: source, Metadata: meta, + RootIssue: rootIssue, Result: result, }) } @@ -182,6 +183,7 @@ type runtimeAssignmentProjection struct { Participant driver.MulticaParticipantRecord Source driver.MulticaHubLedgerSource Metadata driver.MulticaHubMetadata + RootIssue driver.MulticaIssue Result *runtimeImportResult } @@ -223,9 +225,10 @@ func (s *runtimeRPCState) projectAssignmentMailboxes(ctx context.Context, cli dr if hasErr() { continue } + material := assignmentMailboxMaterial(projection.Item, projection.Result, projection.RootIssue, projection.Participant) child, err := cli.CreateIssue(ctx, driver.MulticaCreateIssueRequest{ - Title: multicasurface.AssignmentMailboxTitle(assignmentMailboxMaterial(projection.Item, projection.Result)), - Description: multicasurface.AssignmentMailboxDescription(assignmentMailboxMaterial(projection.Item, projection.Result)), + Title: multicasurface.AssignmentMailboxTitle(material), + Description: multicasurface.AssignmentMailboxDescription(material), ParentID: projection.Result.RootIssueID, Status: "in_progress", Priority: "medium", @@ -644,17 +647,22 @@ func hubStringList(raw any) []string { return out } -func assignmentMailboxMaterial(item runtimeAssignment, result *runtimeImportResult) multicasurface.AssignmentMailboxMaterial { +func assignmentMailboxMaterial(item runtimeAssignment, result *runtimeImportResult, rootIssue driver.MulticaIssue, participant driver.MulticaParticipantRecord) multicasurface.AssignmentMailboxMaterial { material := multicasurface.AssignmentMailboxMaterial{ ID: item.ID, Scope: item.Scope, Assignee: item.Assignee, + AssigneeDisplay: firstNonEmpty(participant.AgentName, participant.AgentID), + RootIssueID: rootIssue.ID, + RootIssueLabel: firstNonEmpty(rootIssue.Identifier, rootIssue.ID), + RootIssueTitle: rootIssue.Title, ExpectedWork: item.ExpectedWork, ExpectedFeedback: item.ExpectedFeedback, Rationale: item.Rationale, } if result != nil { material.SessionID = result.SessionID + material.RootIssueID = firstNonEmpty(material.RootIssueID, result.RootIssueID) } return material } diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index c40d400f..68a24ad2 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -22,6 +22,7 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" "github.com/mnemon-dev/mnemon/harness/internal/projection" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) const runtimeVersion = "dev" @@ -331,6 +332,7 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink return result } emitRuntimeCommand(progress, "multica issue get "+issueID, "Loaded "+runtimeIssueLabel(issue)+".", 0) + issue = loadRuntimeIssueMetadata(multicaCtx, cli, issue, progress) result.IssueID = issue.ID result.Identifier = issue.Identifier result.Title = issue.Title @@ -430,6 +432,30 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink return result } +func loadRuntimeIssueMetadata(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, progress runtimeProgressSink) driver.MulticaIssue { + if strings.TrimSpace(issue.ID) == "" { + return issue + } + listed, err := cli.ListIssueMetadata(ctx, issue.ID) + if err != nil { + emitRuntimeCommand(progress, "multica issue metadata list "+issue.ID, err.Error(), 1) + emitRuntimeProgress(progress, "Multica issue metadata list failed; falling back to metadata returned by issue get.") + return issue + } + merged := map[string]any{} + for key, value := range issue.Metadata { + merged[key] = value + } + for key, value := range listed { + if strings.TrimSpace(key) != "" { + merged[key] = value + } + } + issue.Metadata = merged + emitRuntimeCommand(progress, "multica issue metadata list "+issue.ID, fmt.Sprintf("Loaded %d Multica issue metadata keys.", len(listed)), 0) + return issue +} + func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, result *runtimeImportResult, progress runtimeProgressSink) runtimeImportResult { ensureAssignmentHubFields(result, issue) result.Status = "correlated" @@ -795,41 +821,12 @@ func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.M return } title := "issue admitted" - body := "Issue admitted into Mnemon teamwork." if result.HubKind == driver.MulticaHubKindAssignmentMailbox || result.Status == "correlated" { title = "assignment mailbox correlated" - body = "Assignment mailbox correlated with Mnemon teamwork." - } - if result.Principal != "" { - body += "\nPrincipal: " + result.Principal - } - if result.TaskID != "" { - body += "\nMultica task: " + result.TaskID - } - if result.HubBackend != "" { - body += "\nHub backend: " + result.HubBackend - } - if result.SessionID != "" { - body += "\nSession: " + result.SessionID - } - if result.RootIssueID != "" { - body += "\nRoot issue: " + result.RootIssueID - } - if result.AssignmentID != "" { - body += "\nAssignment: " + result.AssignmentID - } - if result.Receipt != nil { - body += fmt.Sprintf("\nMnemon ingest: seq=%d duplicate=%v ticked=%v", result.Receipt.Seq, result.Receipt.Dup, result.Receipt.Ticked) - } - if result.WakeStatus != "" { - body += "\nManaged wake: " + result.WakeStatus - } - if result.HubWriteStatus != "" { - body += fmt.Sprintf("\nMultica hub write: %s child_issues=%d feedback_comments=%d", result.HubWriteStatus, result.HubChildIssues, result.HubFeedbackComments) } commentBody := projection.FormatComment(projection.CommentMaterial{ Title: title, - Body: body, + Body: runtimeProjectionCommentBody(issue, *result), EventIDs: []string{externalID}, }) comment, err := cli.AddIssueComment(ctx, issue.ID, commentBody) @@ -842,6 +839,59 @@ func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.M result.ProjectionCommentID = comment.ID } +func runtimeProjectionCommentBody(issue driver.MulticaIssue, result runtimeImportResult) string { + var b strings.Builder + if result.HubKind == driver.MulticaHubKindAssignmentMailbox || result.Status == "correlated" { + b.WriteString("## Assignment Mailbox\n\n") + writeRuntimeCommentBullet(&b, "Status", "correlated") + writeRuntimeCommentBullet(&b, "Assignment", runtimeCodeSpan(result.AssignmentID)) + writeRuntimeCommentBullet(&b, "Session", runtimeCodeSpan(result.SessionID)) + writeRuntimeCommentBullet(&b, "Root issue", multicasurface.IssueMention(firstNonEmpty(result.RootIssueID, issue.Identifier), result.RootIssueID)) + } else { + b.WriteString("## Mnemon Intake\n\n") + writeRuntimeCommentBullet(&b, "Status", result.Status) + writeRuntimeCommentBullet(&b, "Issue", multicasurface.IssueMention(firstNonEmpty(issue.Identifier, issue.ID), issue.ID)) + writeRuntimeCommentBullet(&b, "Principal", runtimeCodeSpan(result.Principal)) + writeRuntimeCommentBullet(&b, "Task", runtimeCodeSpan(result.TaskID)) + writeRuntimeCommentBullet(&b, "Hub backend", runtimeCodeSpan(result.HubBackend)) + writeRuntimeCommentBullet(&b, "Session", runtimeCodeSpan(result.SessionID)) + if result.Receipt != nil { + writeRuntimeCommentBullet(&b, "Mnemond ingest", fmt.Sprintf("seq=%d duplicate=%v ticked=%v", result.Receipt.Seq, result.Receipt.Dup, result.Receipt.Ticked)) + } + } + if result.WakeStatus != "" || result.HubWriteStatus != "" { + b.WriteString("\n## Runtime Effects\n\n") + writeRuntimeCommentBullet(&b, "Managed wake", runtimeCodeSpan(result.WakeStatus)) + if result.WakeTurnID != "" { + writeRuntimeCommentBullet(&b, "Managed turn", runtimeCodeSpan(result.WakeTurnID)) + } + if result.HubWriteStatus != "" { + writeRuntimeCommentBullet(&b, "Multica hub write", fmt.Sprintf("%s child_issues=%d feedback_comments=%d", result.HubWriteStatus, result.HubChildIssues, result.HubFeedbackComments)) + } + } + return strings.TrimSpace(b.String()) +} + +func writeRuntimeCommentBullet(b *strings.Builder, label, value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + b.WriteString("- ") + b.WriteString(label) + b.WriteString(": ") + b.WriteString(value) + b.WriteString("\n") +} + +func runtimeCodeSpan(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + return "`" + strings.ReplaceAll(value, "`", "'") + "`" +} + func runtimeMulticaCLI(env []string) driver.MulticaCLI { return driver.MulticaCLI{ Command: envValue(env, "MNEMON_MULTICA_BIN"), diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 4028f267..f3036ca0 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -293,7 +293,14 @@ esac if err != nil { t.Fatal(err) } - for _, want := range []string{"Mnemon update: issue admitted", "Principal: planner@team", "Managed wake: completed", "mnemon:event=multica-task-task-1"} { + for _, want := range []string{ + "Mnemon update: issue admitted", + "## Mnemon Intake", + "Issue: [TEA-7](mention://issue/iss-7)", + "Principal: `planner@team`", + "Managed wake: `completed`", + "mnemon:event=multica-task-task-1", + } { if !strings.Contains(string(comment), want) { t.Fatalf("comment missing %s:\n%s", want, comment) } @@ -542,7 +549,12 @@ esac } } comment := mustReadRuntimeTestFile(t, commentPath) - for _, want := range []string{"Mnemon update: issue admitted", "Hub backend: multica", "Session: multica:session:root-1"} { + for _, want := range []string{ + "Mnemon update: issue admitted", + "## Mnemon Intake", + "Hub backend: `multica`", + "Session: `multica:session:root-1`", + } { if !strings.Contains(comment, want) { t.Fatalf("comment missing %q:\n%s", want, comment) } @@ -634,7 +646,11 @@ esac t.Fatalf("metadata projection failure polluted protocol result:\n%s", output) } comment := mustReadRuntimeTestFile(t, commentPath) - for _, want := range []string{"Mnemon update: issue admitted", "Mnemon ingest: seq=29", "Hub backend: multica"} { + for _, want := range []string{ + "Mnemon update: issue admitted", + "Mnemond ingest: seq=29", + "Hub backend: `multica`", + } { if !strings.Contains(comment, want) { t.Fatalf("comment missing %q:\n%s", want, comment) } @@ -771,7 +787,7 @@ esac args := mustReadRuntimeTestFile(t, argsPath) for _, want := range []string{ "issue children root-2 --output json", - "issue create --title Mnemon assignment asg-writer: check release notes --output json --description-stdin --parent root-2 --status in_progress --priority medium", + "issue create --title TEA-10: check release notes --output json --description-stdin --parent root-2 --status in_progress --priority medium", "issue metadata set child-2 --key mnemon.kind --value assignment_mailbox --type string --output json", "issue metadata set child-2 --key mnemon.assignment_id --value asg-writer --type string --output json", "issue metadata set child-2 --key mnemon.principal --value worker@team --type string --output json", @@ -787,7 +803,7 @@ esac if strings.Contains(args, "asg-stale") { t.Fatalf("stale pre-root assignment must not be projected into current root session:\n%s", args) } - createIdx := strings.Index(args, "issue create --title Mnemon assignment asg-writer") + createIdx := strings.Index(args, "issue create --title TEA-10: check release notes") metaIdx := strings.Index(args, "issue metadata set child-2 --key mnemon.kind") assignIdx := strings.Index(args, "issue assign child-2 --to-id agent-worker") if createIdx < 0 || metaIdx < 0 || assignIdx < 0 || !(createIdx < metaIdx && metaIdx < assignIdx) { @@ -801,11 +817,25 @@ esac t.Fatalf("assignment mailbox should be created in progress without a separate status call:\n%s", args) } description := mustReadRuntimeTestFile(t, createDescriptionPath) - for _, want := range []string{"Mnemon assignment mailbox", "Expected work: check release notes against the public changelog", "Expected feedback: progress_digest with result or blocker"} { + for _, want := range []string{ + "## Assignment", + "check release notes against the public changelog", + "Root issue: [TEA-10](mention://issue/root-2) - Coordinate docs release", + "Assignee: `worker@team` (mnemon-worker)", + "Assignment: `asg-writer`", + "Post a `progress_digest`", + "Requested content: progress_digest with result or blocker", + "metadata under `mnemon.*`", + } { if !strings.Contains(description, want) { t.Fatalf("description missing %q:\n%s", want, description) } } + for _, blocked := range []string{"mnemon.session_id", "mnemon.assignment_id", "mnemon.root_issue_id"} { + if strings.Contains(description, blocked) { + t.Fatalf("description must not expose machine metadata key %q:\n%s", blocked, description) + } + } comment := mustReadRuntimeTestFile(t, commentPath) if !strings.Contains(comment, "Multica hub write: created child_issues=1") { t.Fatalf("root comment missing hub write summary:\n%s", comment) @@ -820,7 +850,8 @@ func TestRuntimeCorrelatesAssignmentMailboxWithoutNewIngest(t *testing.T) { script := `#!/usr/bin/env sh printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" case "$*" in - *"issue get child-1"*) printf '{"id":"child-1","identifier":"TEA-2","title":"Assignment mailbox","description":"Visible assignment summary only.","status":"todo","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-1","mnemon.root_issue_id":"root-1","mnemon.assignment_id":"asg-1","mnemon.assignment_fingerprint":"sha256:abc","mnemon.event_id":"event-assignment-1","mnemon.principal":"worker@team"}}\n' ;; + *"issue get child-1"*) printf '{"id":"child-1","identifier":"TEA-2","title":"Assignment mailbox","description":"Visible assignment summary only.","status":"todo","metadata":{}}\n' ;; + *"issue metadata list child-1"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-1"},{"key":"mnemon.root_issue_id","value":"root-1"},{"key":"mnemon.assignment_id","value":"asg-1"},{"key":"mnemon.assignment_fingerprint","value":"sha256:abc"},{"key":"mnemon.event_id","value":"event-assignment-1"},{"key":"mnemon.principal","value":"worker@team"}]\n' ;; *"issue comment add child-1"*) cat > "$MULTICA_COMMENT_PATH"; printf '{"id":"comment-child","issue_id":"child-1","content":"ok","type":"comment"}\n' ;; *) printf '{}\n' ;; esac @@ -904,12 +935,17 @@ esac if !strings.Contains(out.String(), "Mnemon assignment mailbox: correlated assignment=asg-1") || !strings.Contains(out.String(), "Managed wake: completed turn=noop-turn") { t.Fatalf("runtime output missing correlation/wake evidence:\n%s", out.String()) } + args := mustReadRuntimeTestFile(t, argsPath) + if !strings.Contains(args, "issue metadata list child-1 --output json") { + t.Fatalf("assignment mailbox classification must query Multica issue metadata:\n%s", args) + } comment := mustReadRuntimeTestFile(t, commentPath) for _, want := range []string{ "Mnemon update: assignment mailbox correlated", - "Assignment: asg-1", + "## Assignment Mailbox", + "Assignment: `asg-1`", "mnemon:event=event-assignment-1", - "Managed wake: completed", + "Managed wake: `completed`", } { if !strings.Contains(comment, want) { t.Fatalf("comment missing %q:\n%s", want, comment) diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index 3df34f16..34a90e1d 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -9,6 +9,10 @@ type AssignmentMailboxMaterial struct { SessionID string Scope string Assignee string + AssigneeDisplay string + RootIssueID string + RootIssueLabel string + RootIssueTitle string ExpectedWork string ExpectedFeedback string Rationale string @@ -25,65 +29,91 @@ type ProgressFeedbackMaterial struct { } func AssignmentMailboxTitle(item AssignmentMailboxMaterial) string { - scope := strings.TrimSpace(item.Scope) - if scope == "" { - scope = strings.TrimSpace(item.ID) + topic := assignmentTitleTopic(item) + root := strings.TrimSpace(item.RootIssueLabel) + if root != "" && topic != "" { + return trimTitle(root + ": " + topic) } - if scope == "" { - scope = "assignment" + if topic == "" { + topic = "assignment" } - return "Mnemon assignment " + item.ID + ": " + scope + return trimTitle("Assignment: " + topic) } func AssignmentMailboxDescription(item AssignmentMailboxMaterial) string { var b strings.Builder - b.WriteString("Mnemon assignment mailbox\n\n") - writeLine := func(label, value string) { - value = strings.TrimSpace(value) - if value == "" { - return - } - b.WriteString(label) - b.WriteString(": ") - b.WriteString(value) + b.WriteString("## Assignment\n\n") + if work := strings.TrimSpace(item.ExpectedWork); work != "" { + b.WriteString(work) + b.WriteString("\n\n") + } else if scope := strings.TrimSpace(item.Scope); scope != "" { + b.WriteString(scope) + b.WriteString("\n\n") + } + b.WriteString("## Context\n\n") + writeBullet(&b, "Root issue", rootIssueReference(item)) + writeBullet(&b, "Assignee", assigneeReference(item)) + writeBullet(&b, "Scope", item.Scope) + writeBullet(&b, "Session", codeSpan(item.SessionID)) + writeBullet(&b, "Assignment", codeSpan(item.ID)) + if rationale := strings.TrimSpace(item.Rationale); rationale != "" { + b.WriteString("\n## Rationale\n\n") + b.WriteString(rationale) b.WriteString("\n") } - writeLine("Assignment", item.ID) - writeLine("Session", item.SessionID) - writeLine("Scope", item.Scope) - writeLine("Assignee", item.Assignee) - writeLine("Expected work", item.ExpectedWork) - writeLine("Expected feedback", item.ExpectedFeedback) - writeLine("Rationale", item.Rationale) + b.WriteString("\n## Expected Feedback\n\n") + b.WriteString("Post a `progress_digest` that includes:\n") + writeBullet(&b, "`assignment_ref`", codeSpan(item.ID)) + if feedback := strings.TrimSpace(item.ExpectedFeedback); feedback != "" { + writeBullet(&b, "Requested content", feedback) + } + b.WriteString("\nRouting and dedupe fields live in Multica issue metadata under `mnemon.*`; this description is for human review.\n") return strings.TrimSpace(b.String()) } func ProgressCommentBody(item ProgressFeedbackMaterial) string { var b strings.Builder - writeLine := func(label, value string) { - value = strings.TrimSpace(value) - if value == "" { - return - } - b.WriteString(label) - b.WriteString(": ") - b.WriteString(value) + b.WriteString("## Assignment Feedback\n\n") + writeBullet(&b, "Assignment", codeSpan(item.AssignmentRef)) + writeBullet(&b, "Feedback", codeSpan(item.FeedbackKind)) + if summary := strings.TrimSpace(item.Summary); summary != "" { + b.WriteString("\n## Summary\n\n") + b.WriteString(summary) + b.WriteString("\n") + } + if result := strings.TrimSpace(item.Result); result != "" { + b.WriteString("\n## Result\n\n") + b.WriteString(result) + b.WriteString("\n") + } + if blocker := strings.TrimSpace(item.Blocker); blocker != "" { + b.WriteString("\n## Blocker\n\n") + b.WriteString(blocker) b.WriteString("\n") } - writeLine("Assignment", item.AssignmentRef) - writeLine("Feedback", item.FeedbackKind) - writeLine("Summary", item.Summary) - writeLine("Result", item.Result) - writeLine("Blocker", item.Blocker) if len(item.ArtifactRefs) > 0 { - writeLine("Artifacts", strings.Join(item.ArtifactRefs, ", ")) + b.WriteString("\n## Artifacts\n\n") + writeList(&b, item.ArtifactRefs) } if len(item.EvidenceRefs) > 0 { - writeLine("Evidence", strings.Join(item.EvidenceRefs, ", ")) + b.WriteString("\n## Evidence\n\n") + writeList(&b, item.EvidenceRefs) } return strings.TrimSpace(b.String()) } +func IssueMention(label, issueID string) string { + issueID = strings.TrimSpace(issueID) + label = strings.TrimSpace(label) + if label == "" { + label = issueID + } + if issueID == "" { + return label + } + return "[" + escapeMarkdownLinkLabel(label) + "](mention://issue/" + issueID + ")" +} + func ProgressIssueStatus(item ProgressFeedbackMaterial) string { switch strings.ToLower(strings.TrimSpace(item.FeedbackKind)) { case "blocker": @@ -117,3 +147,126 @@ func IssueStatusDone(status string) bool { return false } } + +func assignmentTitleTopic(item AssignmentMailboxMaterial) string { + for _, candidate := range []string{firstSentence(item.Scope), firstSentence(item.ExpectedWork), item.ID} { + candidate = stripTitleRootLabel(candidate, item.RootIssueLabel) + if strings.TrimSpace(candidate) != "" { + return candidate + } + } + return "" +} + +func stripTitleRootLabel(value, label string) string { + value = strings.TrimSpace(value) + label = strings.TrimSpace(label) + if value == "" || label == "" { + return value + } + if strings.EqualFold(value, label) { + return "" + } + lower := strings.ToLower(value) + for _, separator := range []string{": ", " - ", " -- ", " "} { + prefix := label + separator + if strings.HasPrefix(lower, strings.ToLower(prefix)) { + return strings.TrimSpace(value[len(prefix):]) + } + } + return value +} + +func rootIssueReference(item AssignmentMailboxMaterial) string { + label := firstNonEmptyString(item.RootIssueLabel, item.RootIssueID) + ref := IssueMention(label, item.RootIssueID) + if title := strings.TrimSpace(item.RootIssueTitle); title != "" { + if ref != "" { + return ref + " - " + title + } + return title + } + return ref +} + +func assigneeReference(item AssignmentMailboxMaterial) string { + principal := codeSpan(item.Assignee) + display := strings.TrimSpace(item.AssigneeDisplay) + if display == "" { + return principal + } + if principal == "" { + return display + } + return principal + " (" + display + ")" +} + +func writeBullet(b *strings.Builder, label, value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + b.WriteString("- ") + b.WriteString(label) + b.WriteString(": ") + b.WriteString(value) + b.WriteString("\n") +} + +func writeList(b *strings.Builder, values []string) { + for _, value := range cleanStrings(values) { + b.WriteString("- ") + b.WriteString(value) + b.WriteString("\n") + } +} + +func codeSpan(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + return "`" + strings.ReplaceAll(value, "`", "'") + "`" +} + +func firstSentence(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + for _, sep := range []string{". ", "\n"} { + if before, _, ok := strings.Cut(value, sep); ok { + return strings.TrimSpace(before) + } + } + return value +} + +func trimTitle(value string) string { + value = strings.Join(strings.Fields(strings.TrimSpace(value)), " ") + const max = 96 + if len(value) <= max { + return value + } + return strings.TrimSpace(value[:max-1]) + "..." +} + +func escapeMarkdownLinkLabel(value string) string { + value = strings.ReplaceAll(value, "[", `\[`) + value = strings.ReplaceAll(value, "]", `\]`) + return value +} + +func cleanStrings(values []string) []string { + out := make([]string, 0, len(values)) + seen := map[string]bool{} + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + seen[value] = true + out = append(out, value) + } + return out +} diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 6322e917..615dea7f 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -11,18 +11,44 @@ func TestAssignmentMailboxMaterial(t *testing.T) { SessionID: "session-1", Scope: "runtime/readiness", Assignee: "researcher@team", + AssigneeDisplay: "mnemon-researcher", + RootIssueID: "root-1", + RootIssueLabel: "TEA-1", + RootIssueTitle: "Validate runtime", ExpectedWork: "Check runtime output.", ExpectedFeedback: "Brief result.", } - if got := AssignmentMailboxTitle(item); got != "Mnemon assignment asg-1: runtime/readiness" { + if got := AssignmentMailboxTitle(item); got != "TEA-1: runtime/readiness" { t.Fatalf("title = %q", got) } body := AssignmentMailboxDescription(item) - for _, want := range []string{"Assignment: asg-1", "Session: session-1", "Assignee: researcher@team"} { + for _, want := range []string{ + "## Assignment", + "Root issue: [TEA-1](mention://issue/root-1) - Validate runtime", + "Assignee: `researcher@team` (mnemon-researcher)", + "Session: `session-1`", + "Assignment: `asg-1`", + "Post a `progress_digest`", + "metadata under `mnemon.*`", + } { if !strings.Contains(body, want) { t.Fatalf("description missing %q:\n%s", want, body) } } + if strings.Contains(body, "mnemon.session_id") { + t.Fatalf("visible description must not copy machine metadata keys:\n%s", body) + } +} + +func TestAssignmentMailboxTitleStripsRootLabelFromScope(t *testing.T) { + item := AssignmentMailboxMaterial{ + ID: "asg-1", + Scope: "TEA-14 Mnemon R2 Multica hub final validation", + RootIssueLabel: "TEA-14", + } + if got := AssignmentMailboxTitle(item); got != "TEA-14: Mnemon R2 Multica hub final validation" { + t.Fatalf("title = %q", got) + } } func TestProgressFeedbackMaterial(t *testing.T) { @@ -35,7 +61,15 @@ func TestProgressFeedbackMaterial(t *testing.T) { EvidenceRefs: []string{"comment-1"}, } body := ProgressCommentBody(item) - for _, want := range []string{"Assignment: asg-1", "Feedback: result", "Artifacts: run-1", "Evidence: comment-1"} { + for _, want := range []string{ + "## Assignment Feedback", + "Assignment: `asg-1`", + "Feedback: `result`", + "## Artifacts", + "- run-1", + "## Evidence", + "- comment-1", + } { if !strings.Contains(body, want) { t.Fatalf("progress body missing %q:\n%s", want, body) } From 00339ed4a6677b5541655c11110624a09a4501b0 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:10:07 +0800 Subject: [PATCH 017/117] refactor: centralize multica status projection Move Multica issue status normalization and progress-to-status decisions into the surface/multica projection layer so the runtime adapter consumes projection policy instead of owning it inline. Cover waiting, in-progress, review, done, blocked, and canceled mappings, and derive root issue status from feedback semantics instead of marking every progress digest as review. Validated with go test -count=1 ./harness/.... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 16 ++--- .../cmd/mnemon-multica-runtime/main_test.go | 3 + .../internal/surface/multica/projection.go | 54 ++++++++++----- .../surface/multica/projection_test.go | 58 ++++++++++++++++ harness/internal/surface/multica/status.go | 66 +++++++++++++++++++ 5 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 harness/internal/surface/multica/status.go diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 4af51ef0..a5867e2a 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -324,13 +324,13 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if status := multicasurface.ProgressIssueStatus(material); status != "" { _, _ = cli.SetIssueStatus(ctx, child, status) } - rootStatus := "in_review" + allDone := false if multicasurface.ProgressCompletesAssignment(material) { - if done, _ := allMulticaAssignmentChildrenDone(ctx, cli, result.RootIssueID, result.SessionID, child); done { - rootStatus = "done" - } + allDone, _ = allMulticaAssignmentChildrenDone(ctx, cli, result.RootIssueID, result.SessionID, child) + } + if rootStatus := multicasurface.ProgressRootIssueStatus(material, allDone); rootStatus != "" { + _, _ = cli.SetIssueStatus(ctx, result.RootIssueID, rootStatus) } - _, _ = cli.SetIssueStatus(ctx, result.RootIssueID, rootStatus) if err := ledger.Record(driver.MulticaHubLedgerRecord{ Kind: driver.MulticaHubKindFeedbackCarrier, Source: source, @@ -749,12 +749,10 @@ func allMulticaAssignmentChildrenDone(ctx context.Context, cli driver.MulticaCLI if child.ID == justCompletedChildID { continue } - switch strings.ToLower(strings.TrimSpace(child.Status)) { - case "done", "completed", "complete": + if multicasurface.IssueStatusDone(child.Status) { continue - default: - return false, nil } + return false, nil } return seen, nil } diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index f3036ca0..b90e37df 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -960,8 +960,11 @@ func TestMulticaStatusForProgressIsRuleBased(t *testing.T) { want string }{ {name: "progress", item: runtimeProgress{FeedbackKind: "progress"}, want: "in_progress"}, + {name: "waiting", item: runtimeProgress{FeedbackKind: "waiting"}, want: "todo"}, + {name: "review", item: runtimeProgress{FeedbackKind: "review"}, want: "in_review"}, {name: "result", item: runtimeProgress{FeedbackKind: "result"}, want: "done"}, {name: "blocker", item: runtimeProgress{FeedbackKind: "blocker"}, want: "blocked"}, + {name: "canceled", item: runtimeProgress{FeedbackKind: "canceled"}, want: "cancelled"}, {name: "blocker narrative fallback", item: runtimeProgress{Blocker: "waiting on access"}, want: "blocked"}, {name: "result narrative fallback", item: runtimeProgress{Result: "validated"}, want: "done"}, {name: "unknown", item: runtimeProgress{Summary: "not enough signal"}, want: ""}, diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index 34a90e1d..f40fff3d 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -115,37 +115,59 @@ func IssueMention(label, issueID string) string { } func ProgressIssueStatus(item ProgressFeedbackMaterial) string { - switch strings.ToLower(strings.TrimSpace(item.FeedbackKind)) { + switch canonicalProgressStatus(item.FeedbackKind) { case "blocker": - return "blocked" + return StatusBlocked case "result": - return "done" + return StatusDone case "progress": - return "in_progress" + return StatusInProgress + case "review": + return StatusInReview + case "waiting": + return StatusTodo + case "cancelled": + return StatusCancelled } if strings.TrimSpace(item.Blocker) != "" { - return "blocked" + return StatusBlocked } if strings.TrimSpace(item.Result) != "" { - return "done" + return StatusDone } return "" } -func ProgressCompletesAssignment(item ProgressFeedbackMaterial) bool { - if strings.EqualFold(strings.TrimSpace(item.FeedbackKind), "result") { - return true +func ProgressRootIssueStatus(item ProgressFeedbackMaterial, allAssignmentsDone bool) string { + switch ProgressIssueStatus(item) { + case StatusBlocked: + return StatusBlocked + case StatusCancelled: + return StatusCancelled + case StatusDone: + if allAssignmentsDone { + return StatusDone + } + return StatusInReview + case StatusInReview: + return StatusInReview + case StatusInProgress: + return StatusInProgress + case StatusTodo: + return StatusTodo + case StatusBacklog: + return StatusBacklog + default: + return "" } - return strings.TrimSpace(item.Result) != "" +} + +func ProgressCompletesAssignment(item ProgressFeedbackMaterial) bool { + return ProgressIssueStatus(item) == StatusDone } func IssueStatusDone(status string) bool { - switch strings.ToLower(strings.TrimSpace(status)) { - case "done", "completed", "complete": - return true - default: - return false - } + return CanonicalIssueStatus(status) == StatusDone } func assignmentTitleTopic(item AssignmentMailboxMaterial) string { diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 615dea7f..810d5441 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -84,3 +84,61 @@ func TestProgressFeedbackMaterial(t *testing.T) { t.Fatal("completed should be terminal") } } + +func TestCanonicalIssueStatus(t *testing.T) { + for _, tc := range []struct { + in string + want string + }{ + {in: "backlog", want: StatusBacklog}, + {in: "waiting", want: StatusTodo}, + {in: "in progress", want: StatusInProgress}, + {in: "review", want: StatusInReview}, + {in: "completed", want: StatusDone}, + {in: "blocked", want: StatusBlocked}, + {in: "canceled", want: StatusCancelled}, + {in: "cancelled", want: StatusCancelled}, + {in: "unknown", want: ""}, + } { + if got := CanonicalIssueStatus(tc.in); got != tc.want { + t.Fatalf("CanonicalIssueStatus(%q) = %q, want %q", tc.in, got, tc.want) + } + } +} + +func TestProgressIssueStatusMapping(t *testing.T) { + for _, tc := range []struct { + name string + item ProgressFeedbackMaterial + want string + }{ + {name: "waiting", item: ProgressFeedbackMaterial{FeedbackKind: "waiting"}, want: StatusTodo}, + {name: "in progress", item: ProgressFeedbackMaterial{FeedbackKind: "progress"}, want: StatusInProgress}, + {name: "review", item: ProgressFeedbackMaterial{FeedbackKind: "review"}, want: StatusInReview}, + {name: "done", item: ProgressFeedbackMaterial{FeedbackKind: "result"}, want: StatusDone}, + {name: "blocked", item: ProgressFeedbackMaterial{FeedbackKind: "blocker"}, want: StatusBlocked}, + {name: "canceled", item: ProgressFeedbackMaterial{FeedbackKind: "canceled"}, want: StatusCancelled}, + {name: "blocked fallback", item: ProgressFeedbackMaterial{Blocker: "waiting on access"}, want: StatusBlocked}, + {name: "done fallback", item: ProgressFeedbackMaterial{Result: "validated"}, want: StatusDone}, + {name: "unknown", item: ProgressFeedbackMaterial{Summary: "no lifecycle signal"}, want: ""}, + } { + if got := ProgressIssueStatus(tc.item); got != tc.want { + t.Fatalf("%s: ProgressIssueStatus = %q, want %q", tc.name, got, tc.want) + } + } +} + +func TestProgressRootIssueStatusMapping(t *testing.T) { + if got := ProgressRootIssueStatus(ProgressFeedbackMaterial{FeedbackKind: "progress"}, false); got != StatusInProgress { + t.Fatalf("progress root status = %q", got) + } + if got := ProgressRootIssueStatus(ProgressFeedbackMaterial{FeedbackKind: "blocker"}, false); got != StatusBlocked { + t.Fatalf("blocked root status = %q", got) + } + if got := ProgressRootIssueStatus(ProgressFeedbackMaterial{FeedbackKind: "result"}, false); got != StatusInReview { + t.Fatalf("partial result root status = %q", got) + } + if got := ProgressRootIssueStatus(ProgressFeedbackMaterial{FeedbackKind: "result"}, true); got != StatusDone { + t.Fatalf("complete result root status = %q", got) + } +} diff --git a/harness/internal/surface/multica/status.go b/harness/internal/surface/multica/status.go new file mode 100644 index 00000000..cf55e357 --- /dev/null +++ b/harness/internal/surface/multica/status.go @@ -0,0 +1,66 @@ +package multica + +import "strings" + +const ( + StatusBacklog = "backlog" + StatusTodo = "todo" + StatusInProgress = "in_progress" + StatusInReview = "in_review" + StatusDone = "done" + StatusBlocked = "blocked" + StatusCancelled = "cancelled" +) + +func CanonicalIssueStatus(status string) string { + status = normalizeStatusToken(status) + switch status { + case StatusBacklog, StatusTodo, StatusInProgress, StatusInReview, StatusDone, StatusBlocked, StatusCancelled: + return status + case "pending", "queued": + return StatusBacklog + case "open", "wait", "waiting", "to_do": + return StatusTodo + case "active", "started", "working", "progress": + return StatusInProgress + case "review", "reviewing", "ready_for_review": + return StatusInReview + case "complete", "completed", "closed", "resolved": + return StatusDone + case "blocker": + return StatusBlocked + case "abort", "aborted", "cancel", "canceled": + return StatusCancelled + default: + return "" + } +} + +func canonicalProgressStatus(kind string) string { + switch normalizeStatusToken(kind) { + case "blocker", "blocked": + return "blocker" + case "result", "done", "complete", "completed": + return "result" + case "progress", "in_progress", "working", "started": + return "progress" + case "review", "in_review", "reviewing", "ready_for_review": + return "review" + case "wait", "waiting", "todo", "to_do": + return "waiting" + case "cancel", "canceled", "cancelled", "aborted": + return "cancelled" + default: + return "" + } +} + +func normalizeStatusToken(status string) string { + status = strings.ToLower(strings.TrimSpace(status)) + status = strings.ReplaceAll(status, "-", "_") + status = strings.ReplaceAll(status, " ", "_") + for strings.Contains(status, "__") { + status = strings.ReplaceAll(status, "__", "_") + } + return status +} From d038bc7dee664c0fbe36240da7c2fd19be58ea59 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:16:07 +0800 Subject: [PATCH 018/117] test: report multica acceptance prerequisites Audit Multica CLI availability and participant registry presence before prod-sim creates external issues, and record both conditions as acceptance assertions. This keeps real Multica validation failures diagnosable when local CLI/auth or registry setup is missing. Validated with go test -count=1 ./harness/.... --- .../acceptance_multica_runtime.go | 50 +++++++++++++++++-- .../acceptance_multica_runtime_test.go | 42 ++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 55aeed80..8bb3999a 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "sort" "strings" @@ -188,16 +189,31 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime if err := prepareR1AcceptanceRunRoot(runRoot); err != nil { return finishMulticaRuntimeProdSimReport(report, err) } + var prereqErrs []error + if cliPath, err := resolveMulticaAcceptanceCLI(opts.MulticaBin); err != nil { + addMulticaProdSimAssertion(&report, "Multica CLI available", false, err.Error()) + prereqErrs = append(prereqErrs, err) + } else { + opts.MulticaBin = cliPath + addMulticaProdSimAssertion(&report, "Multica CLI available", true, cliPath) + } registryPath := strings.TrimSpace(opts.RegistryPath) if registryPath == "" { registryPath = filepath.Join(".", driver.MulticaDefaultRegistryRelPath) } registry, ok, err := driver.LoadMulticaRegistry(registryPath) if err != nil { - return finishMulticaRuntimeProdSimReport(report, err) + addMulticaProdSimAssertion(&report, "Multica registry available", false, err.Error()) + prereqErrs = append(prereqErrs, err) + } else if !ok { + err := fmt.Errorf("Multica registry not found: %s", registryPath) + addMulticaProdSimAssertion(&report, "Multica registry available", false, err.Error()) + prereqErrs = append(prereqErrs, err) + } else { + addMulticaProdSimAssertion(&report, "Multica registry available", true, registryPath) } - if !ok { - return finishMulticaRuntimeProdSimReport(report, fmt.Errorf("Multica registry not found: %s", registryPath)) + if err := multicaProdSimPrerequisiteError(prereqErrs); err != nil { + return finishMulticaRuntimeProdSimReport(report, err) } report.RegistryPath = registryPath report.Participants = registry.Participants @@ -701,6 +717,34 @@ func addMulticaProdSimAssertion(report *multicaRuntimeProdSimReport, name string report.Assertions = append(report.Assertions, multicaRuntimeProdSimAssertion{Name: name, Passed: passed, Detail: detail}) } +func resolveMulticaAcceptanceCLI(command string) (string, error) { + command = strings.TrimSpace(command) + if command == "" { + command = "multica" + } + path, err := exec.LookPath(command) + if err != nil { + if command == "multica" { + return "", fmt.Errorf("Multica CLI not found in PATH; pass --multica-bin or set MNEMON_MULTICA_BIN") + } + return "", fmt.Errorf("Multica CLI not executable %q: %w", command, err) + } + return path, nil +} + +func multicaProdSimPrerequisiteError(errs []error) error { + var messages []string + for _, err := range errs { + if err != nil { + messages = append(messages, err.Error()) + } + } + if len(messages) == 0 { + return nil + } + return fmt.Errorf("Multica runtime prod-sim prerequisites failed: %s", strings.Join(messages, "; ")) +} + func multicaProdSimAssertionsPassed(report multicaRuntimeProdSimReport) bool { for _, assertion := range report.Assertions { if !assertion.Passed { diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 6cd7a8b0..b59b3e6e 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -189,3 +189,45 @@ esac } } } + +func TestMulticaRuntimeProdSimAcceptanceReportsPrerequisitesTogether(t *testing.T) { + tmp := t.TempDir() + report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ + RunRoot: filepath.Join(tmp, ".testdata", "multica-readiness"), + MulticaBin: filepath.Join(tmp, "missing-multica"), + RegistryPath: filepath.Join(tmp, "missing-registry.json"), + Wait: time.Millisecond, + Poll: time.Millisecond, + }) + if err == nil { + t.Fatal("expected prerequisite error") + } + if report.Status != "failed" || report.ReportPath == "" { + t.Fatalf("report mismatch: %+v", report) + } + if !strings.Contains(err.Error(), "prerequisites failed") || + !strings.Contains(err.Error(), "Multica CLI not executable") || + !strings.Contains(err.Error(), "Multica registry not found") { + t.Fatalf("error did not report both prerequisites: %v", err) + } + assertionByName := map[string]multicaRuntimeProdSimAssertion{} + for _, assertion := range report.Assertions { + assertionByName[assertion.Name] = assertion + } + for _, name := range []string{"Multica CLI available", "Multica registry available"} { + assertion, ok := assertionByName[name] + if !ok { + t.Fatalf("missing assertion %q in %+v", name, report.Assertions) + } + if assertion.Passed { + t.Fatalf("assertion %q unexpectedly passed: %+v", name, assertion) + } + } + data, err := os.ReadFile(report.ReportPath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), "Multica CLI available") || !strings.Contains(string(data), "Multica registry available") { + t.Fatalf("written report missing prerequisite assertions:\n%s", data) + } +} From 7e35a1692c30388aa62ac86b6c405b472a82621a Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:20:30 +0800 Subject: [PATCH 019/117] feat: bridge multica registry into harness config Teach productconfig.FromLegacy to read the existing Multica participant registry as a legacy bridge for connections, participants, daemon roles, and the primary activation carrier. This keeps the experimental config surface authoritative without discarding existing Multica setup files. Validated with go test -count=1 ./harness/.... --- .../cmd/mnemon-harness/config_daemon_test.go | 29 ++++++ harness/internal/productconfig/config.go | 84 +++++++++++++++++ harness/internal/productconfig/config_test.go | 89 +++++++++++++++++++ 3 files changed, 202 insertions(+) diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index b8589214..5349dc30 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/mnemon-dev/mnemon/harness/internal/productconfig" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) func TestConfigValidateReadsProductConfig(t *testing.T) { @@ -34,6 +35,34 @@ func TestConfigValidateReadsProductConfig(t *testing.T) { } } +func TestConfigValidateReadsLegacyMulticaRegistry(t *testing.T) { + root := t.TempDir() + reg := multicasurface.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-multica", + Participants: []multicasurface.MulticaParticipantRecord{{ + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-planner", + Role: "planner", + }}, + } + if err := multicasurface.SaveMulticaRegistry(multicasurface.MulticaRegistryPath(root, ""), reg); err != nil { + t.Fatal(err) + } + oldRoot, oldPath := configRoot, configPath + configRoot, configPath = root, "" + t.Cleanup(func() { configRoot, configPath = oldRoot, oldPath }) + + cmd, out := testCommand() + if err := runConfigValidate(cmd, nil); err != nil { + t.Fatal(err) + } + if got := out.String(); !strings.Contains(got, "Harness config: valid legacy bridge") || !strings.Contains(got, "Participants: 1") { + t.Fatalf("unexpected config validate output:\n%s", got) + } +} + func TestDaemonStatusDoesNotMutateMissingConfig(t *testing.T) { oldRoot := daemonRoot daemonRoot = t.TempDir() diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go index 755c5302..077955b0 100644 --- a/harness/internal/productconfig/config.go +++ b/harness/internal/productconfig/config.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" "strings" + + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) const ( @@ -304,6 +306,16 @@ func FromLegacy(root string) (Config, bool, error) { } } } + multicaRegistry, ok, err := loadLegacyMulticaRegistry(root) + if err != nil { + return Config{}, false, err + } + if ok { + found = true + if err := bridgeMulticaRegistry(&cfg, multicaRegistry); err != nil { + return Config{}, false, err + } + } if cfg.Connections.Mnemonhub.Enabled { cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionMnemonhub) cfg.Daemon.ProjectionSurfaces = appendCarrier(cfg.Daemon.ProjectionSurfaces, ConnectionMnemonhub) @@ -321,6 +333,74 @@ func FromLegacy(root string) (Config, bool, error) { return cfg, found, nil } +func bridgeMulticaRegistry(cfg *Config, reg multicasurface.MulticaRegistry) error { + cfg.Connections.Multica.Enabled = true + if strings.TrimSpace(cfg.Connections.Multica.Workspace) == "" { + cfg.Connections.Multica.Workspace = strings.TrimSpace(reg.WorkspaceID) + } + if strings.TrimSpace(cfg.Connections.Multica.RuntimeBinary) == "" { + cfg.Connections.Multica.RuntimeBinary = "mnemon-multica-runtime" + } + cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionMultica) + cfg.Daemon.ProjectionSurfaces = appendCarrier(cfg.Daemon.ProjectionSurfaces, ConnectionMultica) + if strings.TrimSpace(cfg.Sessions.PrimaryActivationCarrier) == "" { + cfg.Sessions.PrimaryActivationCarrier = ConnectionMultica + } + for _, record := range reg.Participants { + participant, err := participantFromMulticaRegistryRecord(record) + if err != nil { + return err + } + cfg.Participants = mergeParticipant(cfg.Participants, participant) + } + if len(cfg.Participants) > 0 { + cfg.Daemon.DriveSources = appendCarrier(cfg.Daemon.DriveSources, DriveManagedLocal) + } + return nil +} + +func participantFromMulticaRegistryRecord(record multicasurface.MulticaParticipantRecord) (Participant, error) { + principal := strings.TrimSpace(record.Principal) + if principal == "" { + return Participant{}, fmt.Errorf("multica registry participant principal is required") + } + displayName := strings.TrimSpace(record.AgentName) + if displayName == "" { + displayName = strings.TrimSpace(record.AgentID) + } + return Participant{ + Principal: principal, + DisplayName: displayName, + Role: strings.TrimSpace(record.Role), + HostRuntime: HostRuntime{ + Kind: RuntimeKindCodex, + Mode: RuntimeModeManagedOrHost, + }, + }, nil +} + +func mergeParticipant(participants []Participant, next Participant) []Participant { + for i := range participants { + if participants[i].Principal != next.Principal { + continue + } + if strings.TrimSpace(participants[i].DisplayName) == "" { + participants[i].DisplayName = next.DisplayName + } + if strings.TrimSpace(participants[i].Role) == "" { + participants[i].Role = next.Role + } + if strings.TrimSpace(participants[i].HostRuntime.Kind) == "" { + participants[i].HostRuntime.Kind = next.HostRuntime.Kind + } + if strings.TrimSpace(participants[i].HostRuntime.Mode) == "" { + participants[i].HostRuntime.Mode = next.HostRuntime.Mode + } + return participants + } + return append(participants, next) +} + func loadLegacyLocal(root string) (legacyLocalConfig, bool, error) { path := filepath.Join(root, ".mnemon", "harness", "local", "config.json") data, err := os.ReadFile(path) @@ -359,6 +439,10 @@ func loadLegacyRemotes(root string) (legacyRemotesDoc, bool, error) { return doc, true, nil } +func loadLegacyMulticaRegistry(root string) (multicasurface.MulticaRegistry, bool, error) { + return multicasurface.LoadMulticaRegistry(multicasurface.MulticaRegistryPath(root, "")) +} + func appendCarrier(values []string, value string) []string { for _, existing := range values { if existing == value { diff --git a/harness/internal/productconfig/config_test.go b/harness/internal/productconfig/config_test.go index d82ab8a0..080bb65d 100644 --- a/harness/internal/productconfig/config_test.go +++ b/harness/internal/productconfig/config_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "strings" "testing" + + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) func TestConfigRoundTrip(t *testing.T) { @@ -120,3 +122,90 @@ func TestFromLegacyBridgesLocalAndRemoteConfigs(t *testing.T) { t.Fatalf("drive sources mismatch: %+v", cfg.Daemon.DriveSources) } } + +func TestFromLegacyBridgesMulticaRegistry(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, ".mnemon", "harness", "local"), 0o755); err != nil { + t.Fatal(err) + } + local := `{ + "schema_version": 1, + "principal": "planner@team", + "loops": ["assignment"] +}` + if err := os.WriteFile(filepath.Join(root, ".mnemon", "harness", "local", "config.json"), []byte(local), 0o600); err != nil { + t.Fatal(err) + } + reg := multicasurface.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-multica", + RuntimeProfileID: "profile-1", + RuntimeID: "runtime-1", + Participants: []multicasurface.MulticaParticipantRecord{ + { + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-planner", + Role: "planner", + }, + { + Principal: "reviewer@team", + AgentName: "mnemon-reviewer", + AgentID: "agent-reviewer", + Role: "reviewer", + }, + }, + } + if err := multicasurface.SaveMulticaRegistry(multicasurface.MulticaRegistryPath(root, ""), reg); err != nil { + t.Fatal(err) + } + + cfg, found, err := FromLegacy(root) + if err != nil { + t.Fatal(err) + } + if !found { + t.Fatal("legacy multica registry should be found") + } + if !cfg.Connections.Multica.Enabled || cfg.Connections.Multica.Workspace != "ws-multica" || cfg.Connections.Multica.RuntimeBinary != "mnemon-multica-runtime" { + t.Fatalf("multica bridge mismatch: %+v", cfg.Connections.Multica) + } + if cfg.Sessions.PrimaryActivationCarrier != ConnectionMultica { + t.Fatalf("primary carrier = %q", cfg.Sessions.PrimaryActivationCarrier) + } + if !stringSliceContains(cfg.Daemon.InteractionWatchers, ConnectionMultica) || !stringSliceContains(cfg.Daemon.ProjectionSurfaces, ConnectionMultica) { + t.Fatalf("daemon multica roles missing: %+v", cfg.Daemon) + } + planner := participantByPrincipal(cfg.Participants, "planner@team") + if planner.HostRuntime.Kind != RuntimeKindConfigured || planner.HostRuntime.Mode != RuntimeModeAttached { + t.Fatalf("planner host runtime should come from legacy local config: %+v", planner) + } + if planner.DisplayName != "mnemon-planner" || planner.Role != "planner" { + t.Fatalf("planner multica display fields not merged: %+v", planner) + } + reviewer := participantByPrincipal(cfg.Participants, "reviewer@team") + if reviewer.HostRuntime.Kind != RuntimeKindCodex || reviewer.HostRuntime.Mode != RuntimeModeManagedOrHost || reviewer.DisplayName != "mnemon-reviewer" { + t.Fatalf("reviewer participant mismatch: %+v", reviewer) + } + if !stringSliceContains(cfg.Daemon.DriveSources, DriveManagedLocal) { + t.Fatalf("drive source missing: %+v", cfg.Daemon.DriveSources) + } +} + +func participantByPrincipal(participants []Participant, principal string) Participant { + for _, participant := range participants { + if participant.Principal == principal { + return participant + } + } + return Participant{} +} + +func stringSliceContains(values []string, want string) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} From 882468e60616eef44a90af8f65abcac87e0bbf49 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:22:34 +0800 Subject: [PATCH 020/117] feat: show harness daemon role status Report configured watcher, drive, and projection-surface role counts from product config or legacy bridges in mnemon-harness daemon status. This gives the experimental daemon surface a concrete status snapshot over config-owned workers. Validated with go test -count=1 ./harness/.... --- .../cmd/mnemon-harness/config_daemon_test.go | 58 +++++++++++++++++++ harness/cmd/mnemon-harness/daemon.go | 10 +++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index 5349dc30..3782db0d 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -80,6 +80,64 @@ func TestDaemonStatusDoesNotMutateMissingConfig(t *testing.T) { } } +func TestDaemonStatusShowsConfiguredRoleSummary(t *testing.T) { + root := t.TempDir() + cfg := productconfig.Default() + cfg.Connections.Multica = productconfig.MulticaConnection{Enabled: true, Workspace: "ws-multica", RuntimeBinary: "mnemon-multica-runtime"} + cfg.Connections.GitHub = productconfig.GitHubConnection{Enabled: true, Repo: "mnemon-dev/mnemon-teamwork-example"} + cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMultica, productconfig.ConnectionGitHub} + cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} + cfg.Daemon.ProjectionSurfaces = []string{productconfig.ConnectionMultica} + if err := productconfig.Save(productconfig.DefaultPath(root, ""), cfg); err != nil { + t.Fatal(err) + } + oldRoot := daemonRoot + daemonRoot = root + t.Cleanup(func() { daemonRoot = oldRoot }) + + cmd, out := testCommand() + if err := runDaemonStatus(cmd, nil); err != nil { + t.Fatal(err) + } + got := out.String() + for _, want := range []string{"Harness config: configured", "Harness daemon roles: watchers=2 drive=1 surfaces=1", "Harness daemon: not running"} { + if !strings.Contains(got, want) { + t.Fatalf("daemon status missing %q:\n%s", want, got) + } + } +} + +func TestDaemonStatusShowsLegacyMulticaRoleSummary(t *testing.T) { + root := t.TempDir() + reg := multicasurface.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-multica", + Participants: []multicasurface.MulticaParticipantRecord{{ + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-planner", + Role: "planner", + }}, + } + if err := multicasurface.SaveMulticaRegistry(multicasurface.MulticaRegistryPath(root, ""), reg); err != nil { + t.Fatal(err) + } + oldRoot := daemonRoot + daemonRoot = root + t.Cleanup(func() { daemonRoot = oldRoot }) + + cmd, out := testCommand() + if err := runDaemonStatus(cmd, nil); err != nil { + t.Fatal(err) + } + got := out.String() + for _, want := range []string{"Harness config: legacy bridge", "Harness daemon roles: watchers=1 drive=1 surfaces=1"} { + if !strings.Contains(got, want) { + t.Fatalf("daemon status missing %q:\n%s", want, got) + } + } +} + func TestAgentAddAndListWriteProductConfig(t *testing.T) { root := t.TempDir() oldRoot, oldPath := agentRoot, agentConfigPath diff --git a/harness/cmd/mnemon-harness/daemon.go b/harness/cmd/mnemon-harness/daemon.go index 52002223..bfa54462 100644 --- a/harness/cmd/mnemon-harness/daemon.go +++ b/harness/cmd/mnemon-harness/daemon.go @@ -89,10 +89,12 @@ func runDaemonStart(cmd *cobra.Command, args []string) error { func runDaemonStatus(cmd *cobra.Command, args []string) error { root := daemonProjectRoot() - if _, err := productconfig.Load(productconfig.DefaultPath(root, "")); err == nil { + if cfg, err := productconfig.Load(productconfig.DefaultPath(root, "")); err == nil { fmt.Fprintln(cmd.OutOrStdout(), "Harness config: configured") - } else if _, found, legacyErr := productconfig.FromLegacy(root); legacyErr == nil && found { + writeDaemonRoleSummary(cmd.OutOrStdout(), cfg) + } else if legacy, found, legacyErr := productconfig.FromLegacy(root); legacyErr == nil && found { fmt.Fprintln(cmd.OutOrStdout(), "Harness config: legacy bridge") + writeDaemonRoleSummary(cmd.OutOrStdout(), legacy) } else { fmt.Fprintln(cmd.OutOrStdout(), "Harness config: not configured") } @@ -106,6 +108,10 @@ func runDaemonStatus(cmd *cobra.Command, args []string) error { return nil } +func writeDaemonRoleSummary(out io.Writer, cfg productconfig.Config) { + fmt.Fprintf(out, "Harness daemon roles: watchers=%d drive=%d surfaces=%d\n", len(cfg.Daemon.InteractionWatchers), len(cfg.Daemon.DriveSources), len(cfg.Daemon.ProjectionSurfaces)) +} + func daemonProjectRoot() string { if daemonRoot == "" { return "." From 4151b301eb5ba4e51f22c27a44d3c2f991a9cdec Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:26:37 +0800 Subject: [PATCH 021/117] test: guard r2 projection role boundaries Add coreguard tests proving projection surfaces cannot import mnemond ingest/write paths and activationtrace cannot depend on EventEnvelope material packages. This pins the R2 rule that projection and trace are derived external surfaces, not canonical event writers. Validated with go test -count=1 ./harness/.... --- .../coreguard/r2_role_boundary_test.go | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 harness/internal/coreguard/r2_role_boundary_test.go diff --git a/harness/internal/coreguard/r2_role_boundary_test.go b/harness/internal/coreguard/r2_role_boundary_test.go new file mode 100644 index 00000000..ca2e4673 --- /dev/null +++ b/harness/internal/coreguard/r2_role_boundary_test.go @@ -0,0 +1,99 @@ +package coreguard + +import ( + "strings" + "testing" +) + +type roleImportBoundary struct { + pkg string + forbids []string + rationale string +} + +var projectionSurfaceImportBoundaries = []roleImportBoundary{ + { + pkg: "projection", + forbids: []string{ + "harness/internal/mnemond/access", + "harness/internal/mnemond/admission", + "harness/internal/mnemond/state", + "harness/internal/runtime", + "harness/internal/eventstore", + "harness/internal/app", + "harness/internal/assembler", + }, + rationale: "projection packages render accepted state into external artifacts and must not ingest governed events", + }, + { + pkg: "surface/multica", + forbids: []string{ + "harness/internal/mnemond/access", + "harness/internal/mnemond/admission", + "harness/internal/mnemond/state", + "harness/internal/runtime", + "harness/internal/eventstore", + "harness/internal/app", + "harness/internal/assembler", + }, + rationale: "the Multica surface is a projection/adapter boundary, not a local mnemond write path", + }, +} + +var activationTraceImportBoundary = roleImportBoundary{ + pkg: "activationtrace", + forbids: []string{ + "harness/internal/contract", + "harness/internal/event", + "harness/internal/eventstore", + "harness/internal/mnemond/access", + "harness/internal/mnemond/admission", + "harness/internal/mnemond/state", + "harness/internal/runtime", + }, + rationale: "activation trace is non-canonical run display material and must not become EventEnvelope material", +} + +func TestR2RoleBoundaryGuardLogicIsNotVacuous(t *testing.T) { + if !roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/access", projectionSurfaceImportBoundaries[0].forbids) { + t.Fatal("projection boundary guard must flag mnemond access imports") + } + if roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view", projectionSurfaceImportBoundaries[0].forbids) { + t.Fatal("projection boundary guard must allow read-only presentation view imports") + } + if !roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/event", activationTraceImportBoundary.forbids) { + t.Fatal("activation trace boundary guard must flag event model imports") + } +} + +func TestProjectionSurfacesDoNotIngestGovernedEvents(t *testing.T) { + for _, boundary := range projectionSurfaceImportBoundaries { + assertPackageAvoidsForbiddenImports(t, boundary) + } +} + +func TestActivationTraceNeverWritesEventMaterial(t *testing.T) { + assertPackageAvoidsForbiddenImports(t, activationTraceImportBoundary) +} + +func assertPackageAvoidsForbiddenImports(t *testing.T, boundary roleImportBoundary) { + t.Helper() + _, files := packageFiles(t, boundary.pkg) + for _, f := range files { + for _, imp := range f.Imports { + path := strings.Trim(imp.Path.Value, `"`) + if roleImportForbidden(path, boundary.forbids) { + t.Errorf("package %q imports forbidden package %q: %s", boundary.pkg, path, boundary.rationale) + } + } + } +} + +func roleImportForbidden(path string, forbids []string) bool { + for _, forbidden := range forbids { + if strings.Contains(path, forbidden) { + return true + } + } + return false +} From 5e324b6937731f33608fc4a081c748b30b3b46e9 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:26:56 +0800 Subject: [PATCH 022/117] feat: add daemon worker state primitives Introduce file-backed cursor storage, bounded retry backoff, and an in-flight guard for harness daemon workers. These primitives cover the R2 daemon requirements for cursor resume, retry/backoff, and duplicate-work suppression. Validated with go test -count=1 ./harness/.... --- harness/internal/daemon/state.go | 175 ++++++++++++++++++++++++++ harness/internal/daemon/state_test.go | 74 +++++++++++ 2 files changed, 249 insertions(+) create mode 100644 harness/internal/daemon/state.go create mode 100644 harness/internal/daemon/state_test.go diff --git a/harness/internal/daemon/state.go b/harness/internal/daemon/state.go new file mode 100644 index 00000000..5fb245ab --- /dev/null +++ b/harness/internal/daemon/state.go @@ -0,0 +1,175 @@ +package daemon + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +type CursorRecord struct { + Cursor string `json:"cursor"` + UpdatedAt time.Time `json:"updated_at"` +} + +type FileCursorStore struct { + path string + now Clock + + mu sync.Mutex + loaded bool + records map[string]CursorRecord +} + +func NewFileCursorStore(path string, now Clock) *FileCursorStore { + if now == nil { + now = func() time.Time { return time.Now().UTC() } + } + return &FileCursorStore{path: strings.TrimSpace(path), now: now, records: map[string]CursorRecord{}} +} + +func (s *FileCursorStore) Load(worker string) (CursorRecord, bool, error) { + worker = strings.TrimSpace(worker) + if worker == "" { + return CursorRecord{}, false, fmt.Errorf("daemon cursor worker name is required") + } + s.mu.Lock() + defer s.mu.Unlock() + if err := s.loadLocked(); err != nil { + return CursorRecord{}, false, err + } + record, ok := s.records[worker] + return record, ok, nil +} + +func (s *FileCursorStore) Save(worker, cursor string) error { + worker = strings.TrimSpace(worker) + if worker == "" { + return fmt.Errorf("daemon cursor worker name is required") + } + s.mu.Lock() + defer s.mu.Unlock() + if err := s.loadLocked(); err != nil { + return err + } + s.records[worker] = CursorRecord{Cursor: strings.TrimSpace(cursor), UpdatedAt: s.now()} + return s.writeLocked() +} + +func (s *FileCursorStore) loadLocked() error { + if s.loaded { + return nil + } + s.loaded = true + if s.path == "" { + return fmt.Errorf("daemon cursor store path is required") + } + data, err := os.ReadFile(s.path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + if len(strings.TrimSpace(string(data))) == 0 { + return nil + } + if err := json.Unmarshal(data, &s.records); err != nil { + return fmt.Errorf("parse daemon cursor store: %w", err) + } + if s.records == nil { + s.records = map[string]CursorRecord{} + } + return nil +} + +func (s *FileCursorStore) writeLocked() error { + if s.path == "" { + return fmt.Errorf("daemon cursor store path is required") + } + if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(s.records, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + tmp := s.path + ".tmp" + if err := os.WriteFile(tmp, data, 0o600); err != nil { + return err + } + return os.Rename(tmp, s.path) +} + +type BackoffPolicy struct { + Base time.Duration + Max time.Duration +} + +func (p BackoffPolicy) Duration(attempt int) time.Duration { + base := p.Base + if base <= 0 { + base = time.Second + } + max := p.Max + if max <= 0 { + max = 30 * time.Second + } + if max < base { + max = base + } + if attempt <= 0 { + return base + } + backoff := base + for i := 0; i < attempt; i++ { + if backoff >= max/2 { + return max + } + backoff *= 2 + } + if backoff > max { + return max + } + return backoff +} + +type InFlightGuard struct { + mu sync.Mutex + keys map[string]bool +} + +func NewInFlightGuard() *InFlightGuard { + return &InFlightGuard{keys: map[string]bool{}} +} + +func (g *InFlightGuard) TryStart(key string) bool { + key = strings.TrimSpace(key) + if key == "" { + return false + } + g.mu.Lock() + defer g.mu.Unlock() + if g.keys == nil { + g.keys = map[string]bool{} + } + if g.keys[key] { + return false + } + g.keys[key] = true + return true +} + +func (g *InFlightGuard) Done(key string) { + key = strings.TrimSpace(key) + if key == "" { + return + } + g.mu.Lock() + defer g.mu.Unlock() + delete(g.keys, key) +} diff --git a/harness/internal/daemon/state_test.go b/harness/internal/daemon/state_test.go new file mode 100644 index 00000000..3a399c64 --- /dev/null +++ b/harness/internal/daemon/state_test.go @@ -0,0 +1,74 @@ +package daemon + +import ( + "path/filepath" + "testing" + "time" +) + +func TestFileCursorStoreResumesCursors(t *testing.T) { + now := time.Unix(100, 0).UTC() + path := filepath.Join(t.TempDir(), "daemon", "cursors.json") + store := NewFileCursorStore(path, func() time.Time { return now }) + if _, ok, err := store.Load("multica-watch"); err != nil || ok { + t.Fatalf("initial load = ok=%v err=%v", ok, err) + } + if err := store.Save("multica-watch", "issue:cursor:7"); err != nil { + t.Fatal(err) + } + + resumed := NewFileCursorStore(path, func() time.Time { return now.Add(time.Hour) }) + record, ok, err := resumed.Load("multica-watch") + if err != nil { + t.Fatal(err) + } + if !ok || record.Cursor != "issue:cursor:7" || !record.UpdatedAt.Equal(now) { + t.Fatalf("resumed cursor = %+v ok=%v", record, ok) + } +} + +func TestFileCursorStoreValidatesWorkerName(t *testing.T) { + store := NewFileCursorStore(filepath.Join(t.TempDir(), "cursors.json"), nil) + if err := store.Save(" ", "cursor"); err == nil { + t.Fatal("expected save to reject empty worker name") + } + if _, _, err := store.Load(" "); err == nil { + t.Fatal("expected load to reject empty worker name") + } +} + +func TestBackoffPolicyCapsRetries(t *testing.T) { + policy := BackoffPolicy{Base: time.Second, Max: 5 * time.Second} + for _, tc := range []struct { + attempt int + want time.Duration + }{ + {attempt: -1, want: time.Second}, + {attempt: 0, want: time.Second}, + {attempt: 1, want: 2 * time.Second}, + {attempt: 2, want: 4 * time.Second}, + {attempt: 3, want: 5 * time.Second}, + {attempt: 10, want: 5 * time.Second}, + } { + if got := policy.Duration(tc.attempt); got != tc.want { + t.Fatalf("Duration(%d) = %s, want %s", tc.attempt, got, tc.want) + } + } +} + +func TestInFlightGuardSuppressesDuplicateWork(t *testing.T) { + guard := NewInFlightGuard() + if guard.TryStart("") { + t.Fatal("empty key must not start") + } + if !guard.TryStart("projection:root-1") { + t.Fatal("first start should win") + } + if guard.TryStart("projection:root-1") { + t.Fatal("duplicate start should be suppressed") + } + guard.Done("projection:root-1") + if !guard.TryStart("projection:root-1") { + t.Fatal("completed key should be startable again") + } +} From df532c5035df34a0801f58a0c6c3e6abc503e42b Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:30:40 +0800 Subject: [PATCH 023/117] refactor: move runtime comment projection to multica surface Move Multica runtime update comment rendering out of mnemon-multica-runtime and into surface/multica projection material. The runtime adapter now maps import results into surface material while the Multica projection package owns the visible Markdown. Validated with go test -count=1 ./harness/.... --- harness/cmd/mnemon-multica-runtime/main.go | 79 +++++++------------ .../internal/surface/multica/projection.go | 55 +++++++++++++ .../surface/multica/projection_test.go | 58 ++++++++++++++ 3 files changed, 140 insertions(+), 52 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 68a24ad2..59af79bd 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -826,7 +826,7 @@ func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.M } commentBody := projection.FormatComment(projection.CommentMaterial{ Title: title, - Body: runtimeProjectionCommentBody(issue, *result), + Body: multicasurface.RuntimeProjectionCommentBody(runtimeProjectionMaterial(issue, *result)), EventIDs: []string{externalID}, }) comment, err := cli.AddIssueComment(ctx, issue.ID, commentBody) @@ -839,57 +839,32 @@ func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.M result.ProjectionCommentID = comment.ID } -func runtimeProjectionCommentBody(issue driver.MulticaIssue, result runtimeImportResult) string { - var b strings.Builder - if result.HubKind == driver.MulticaHubKindAssignmentMailbox || result.Status == "correlated" { - b.WriteString("## Assignment Mailbox\n\n") - writeRuntimeCommentBullet(&b, "Status", "correlated") - writeRuntimeCommentBullet(&b, "Assignment", runtimeCodeSpan(result.AssignmentID)) - writeRuntimeCommentBullet(&b, "Session", runtimeCodeSpan(result.SessionID)) - writeRuntimeCommentBullet(&b, "Root issue", multicasurface.IssueMention(firstNonEmpty(result.RootIssueID, issue.Identifier), result.RootIssueID)) - } else { - b.WriteString("## Mnemon Intake\n\n") - writeRuntimeCommentBullet(&b, "Status", result.Status) - writeRuntimeCommentBullet(&b, "Issue", multicasurface.IssueMention(firstNonEmpty(issue.Identifier, issue.ID), issue.ID)) - writeRuntimeCommentBullet(&b, "Principal", runtimeCodeSpan(result.Principal)) - writeRuntimeCommentBullet(&b, "Task", runtimeCodeSpan(result.TaskID)) - writeRuntimeCommentBullet(&b, "Hub backend", runtimeCodeSpan(result.HubBackend)) - writeRuntimeCommentBullet(&b, "Session", runtimeCodeSpan(result.SessionID)) - if result.Receipt != nil { - writeRuntimeCommentBullet(&b, "Mnemond ingest", fmt.Sprintf("seq=%d duplicate=%v ticked=%v", result.Receipt.Seq, result.Receipt.Dup, result.Receipt.Ticked)) - } - } - if result.WakeStatus != "" || result.HubWriteStatus != "" { - b.WriteString("\n## Runtime Effects\n\n") - writeRuntimeCommentBullet(&b, "Managed wake", runtimeCodeSpan(result.WakeStatus)) - if result.WakeTurnID != "" { - writeRuntimeCommentBullet(&b, "Managed turn", runtimeCodeSpan(result.WakeTurnID)) - } - if result.HubWriteStatus != "" { - writeRuntimeCommentBullet(&b, "Multica hub write", fmt.Sprintf("%s child_issues=%d feedback_comments=%d", result.HubWriteStatus, result.HubChildIssues, result.HubFeedbackComments)) - } - } - return strings.TrimSpace(b.String()) -} - -func writeRuntimeCommentBullet(b *strings.Builder, label, value string) { - value = strings.TrimSpace(value) - if value == "" { - return - } - b.WriteString("- ") - b.WriteString(label) - b.WriteString(": ") - b.WriteString(value) - b.WriteString("\n") -} - -func runtimeCodeSpan(value string) string { - value = strings.TrimSpace(value) - if value == "" { - return "" - } - return "`" + strings.ReplaceAll(value, "`", "'") + "`" +func runtimeProjectionMaterial(issue driver.MulticaIssue, result runtimeImportResult) multicasurface.RuntimeProjectionMaterial { + material := multicasurface.RuntimeProjectionMaterial{ + AssignmentMailbox: result.HubKind == driver.MulticaHubKindAssignmentMailbox || result.Status == "correlated", + Status: result.Status, + IssueID: issue.ID, + IssueLabel: firstNonEmpty(issue.Identifier, issue.ID), + Principal: result.Principal, + TaskID: result.TaskID, + HubBackend: result.HubBackend, + SessionID: result.SessionID, + RootIssueID: result.RootIssueID, + RootIssueLabel: firstNonEmpty(result.RootIssueID, issue.Identifier), + AssignmentID: result.AssignmentID, + WakeStatus: result.WakeStatus, + WakeTurnID: result.WakeTurnID, + HubWriteStatus: result.HubWriteStatus, + HubChildIssues: result.HubChildIssues, + HubFeedbackComment: result.HubFeedbackComments, + } + if result.Receipt != nil { + material.HasIngestReceipt = true + material.IngestSeq = result.Receipt.Seq + material.IngestDuplicate = result.Receipt.Dup + material.IngestTicked = result.Receipt.Ticked + } + return material } func runtimeMulticaCLI(env []string) driver.MulticaCLI { diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index f40fff3d..851ceef1 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -1,6 +1,7 @@ package multica import ( + "fmt" "strings" ) @@ -28,6 +29,29 @@ type ProgressFeedbackMaterial struct { EvidenceRefs []string } +type RuntimeProjectionMaterial struct { + AssignmentMailbox bool + Status string + IssueID string + IssueLabel string + Principal string + TaskID string + HubBackend string + SessionID string + RootIssueID string + RootIssueLabel string + AssignmentID string + HasIngestReceipt bool + IngestSeq int64 + IngestDuplicate bool + IngestTicked bool + WakeStatus string + WakeTurnID string + HubWriteStatus string + HubChildIssues int + HubFeedbackComment int +} + func AssignmentMailboxTitle(item AssignmentMailboxMaterial) string { topic := assignmentTitleTopic(item) root := strings.TrimSpace(item.RootIssueLabel) @@ -102,6 +126,37 @@ func ProgressCommentBody(item ProgressFeedbackMaterial) string { return strings.TrimSpace(b.String()) } +func RuntimeProjectionCommentBody(item RuntimeProjectionMaterial) string { + var b strings.Builder + if item.AssignmentMailbox { + b.WriteString("## Assignment Mailbox\n\n") + writeBullet(&b, "Status", firstNonEmptyString(item.Status, "correlated")) + writeBullet(&b, "Assignment", codeSpan(item.AssignmentID)) + writeBullet(&b, "Session", codeSpan(item.SessionID)) + writeBullet(&b, "Root issue", IssueMention(firstNonEmptyString(item.RootIssueLabel, item.RootIssueID), item.RootIssueID)) + } else { + b.WriteString("## Mnemon Intake\n\n") + writeBullet(&b, "Status", item.Status) + writeBullet(&b, "Issue", IssueMention(firstNonEmptyString(item.IssueLabel, item.IssueID), item.IssueID)) + writeBullet(&b, "Principal", codeSpan(item.Principal)) + writeBullet(&b, "Task", codeSpan(item.TaskID)) + writeBullet(&b, "Hub backend", codeSpan(item.HubBackend)) + writeBullet(&b, "Session", codeSpan(item.SessionID)) + if item.HasIngestReceipt { + writeBullet(&b, "Mnemond ingest", fmt.Sprintf("seq=%d duplicate=%v ticked=%v", item.IngestSeq, item.IngestDuplicate, item.IngestTicked)) + } + } + if item.WakeStatus != "" || item.HubWriteStatus != "" { + b.WriteString("\n## Runtime Effects\n\n") + writeBullet(&b, "Managed wake", codeSpan(item.WakeStatus)) + writeBullet(&b, "Managed turn", codeSpan(item.WakeTurnID)) + if item.HubWriteStatus != "" { + writeBullet(&b, "Multica hub write", fmt.Sprintf("%s child_issues=%d feedback_comments=%d", item.HubWriteStatus, item.HubChildIssues, item.HubFeedbackComment)) + } + } + return strings.TrimSpace(b.String()) +} + func IssueMention(label, issueID string) string { issueID = strings.TrimSpace(issueID) label = strings.TrimSpace(label) diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 810d5441..01cc2057 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -85,6 +85,64 @@ func TestProgressFeedbackMaterial(t *testing.T) { } } +func TestRuntimeProjectionCommentBodyForIntake(t *testing.T) { + body := RuntimeProjectionCommentBody(RuntimeProjectionMaterial{ + Status: "recorded", + IssueID: "issue-1", + IssueLabel: "TEA-1", + Principal: "planner@team", + TaskID: "task-1", + HubBackend: "multica", + SessionID: "multica:session:issue-1", + HasIngestReceipt: true, + IngestSeq: 42, + IngestDuplicate: false, + IngestTicked: true, + WakeStatus: "completed", + WakeTurnID: "turn-1", + HubWriteStatus: "created", + HubChildIssues: 2, + HubFeedbackComment: 1, + }) + for _, want := range []string{ + "## Mnemon Intake", + "Issue: [TEA-1](mention://issue/issue-1)", + "Principal: `planner@team`", + "Mnemond ingest: seq=42 duplicate=false ticked=true", + "Managed wake: `completed`", + "Managed turn: `turn-1`", + "Multica hub write: created child_issues=2 feedback_comments=1", + } { + if !strings.Contains(body, want) { + t.Fatalf("runtime projection body missing %q:\n%s", want, body) + } + } +} + +func TestRuntimeProjectionCommentBodyForAssignmentMailbox(t *testing.T) { + body := RuntimeProjectionCommentBody(RuntimeProjectionMaterial{ + AssignmentMailbox: true, + Status: "correlated", + RootIssueID: "root-1", + RootIssueLabel: "TEA-1", + AssignmentID: "asg-1", + SessionID: "multica:session:root-1", + WakeStatus: "completed", + }) + for _, want := range []string{ + "## Assignment Mailbox", + "Status: correlated", + "Assignment: `asg-1`", + "Session: `multica:session:root-1`", + "Root issue: [TEA-1](mention://issue/root-1)", + "Managed wake: `completed`", + } { + if !strings.Contains(body, want) { + t.Fatalf("assignment runtime projection body missing %q:\n%s", want, body) + } + } +} + func TestCanonicalIssueStatus(t *testing.T) { for _, tc := range []struct { in string From 568167a5dc69d1a6ec8dbdcb80034e19b0e159ac Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:34:33 +0800 Subject: [PATCH 024/117] refactor: move multica issue intake material Move Multica issue-to-teamwork_signal rule/narrative/refs construction from the legacy driver facade into surface/multica. The driver keeps a compatibility wrapper, while the Multica adapter surface now owns interaction intake material. Validated with go test -count=1 ./harness/.... --- harness/internal/driver/multica.go | 109 ++---------------- .../internal/surface/multica/issue_signal.go | 105 +++++++++++++++++ .../surface/multica/issue_signal_test.go | 74 ++++++++++++ 3 files changed, 189 insertions(+), 99 deletions(-) create mode 100644 harness/internal/surface/multica/issue_signal.go create mode 100644 harness/internal/surface/multica/issue_signal_test.go diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index 0729132d..d3c2e3ea 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -13,13 +13,13 @@ import ( "sync" "time" - "github.com/mnemon-dev/mnemon/harness/internal/interaction" "github.com/mnemon-dev/mnemon/harness/internal/projection" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) const ( MulticaDefaultCommand = "multica" - MulticaExternalSource = "multica" + MulticaExternalSource = multicasurface.MulticaExternalSource ) type MulticaCLI struct { @@ -843,78 +843,17 @@ func (c MulticaCLI) globalArgs(args []string) []string { return append(out, args...) } -type MulticaIssueSignalOptions struct { - Scope string - TTL string - WhyTeamwork string - WorkspaceID string - TaskID string - AgentID string - Principal string - EvidenceRefs []string - ContextRefs []string - ExternalID string -} +type MulticaIssueSignalOptions = multicasurface.IssueSignalOptions -type MulticaObservedDraft = interaction.EventMaterial +type MulticaObservedDraft = multicasurface.ObservedDraft func BuildMulticaIssueTeamworkSignal(issue MulticaIssue, opts MulticaIssueSignalOptions) (MulticaObservedDraft, error) { - if strings.TrimSpace(issue.ID) == "" { - return MulticaObservedDraft{}, fmt.Errorf("multica issue id is required") - } - title := strings.TrimSpace(issue.Title) - if title == "" { - title = strings.TrimSpace(issue.Identifier) - } - if title == "" { - title = issue.ID - } - scope := strings.TrimSpace(opts.Scope) - if scope == "" { - scope = "multica/teamwork" - } - ttl := strings.TrimSpace(opts.TTL) - if ttl == "" { - ttl = "30m" - } - correlation := "multica:issue:" + issue.ID - rule := map[string]any{ - "external_source": MulticaExternalSource, - "external_issue_id": issue.ID, - "external_issue_identifier": strings.TrimSpace(issue.Identifier), - "correlation_id": correlation, - "scope": scope, - "ttl": ttl, - } - addMulticaRuleString(rule, "external_workspace_id", opts.WorkspaceID) - addMulticaRuleString(rule, "external_task_id", opts.TaskID) - addMulticaRuleString(rule, "external_agent_id", opts.AgentID) - addMulticaRuleString(rule, "principal", opts.Principal) - narrative := map[string]any{ - "title": title, - "statement": multicaIssueStatement(issue, title), - "why_teamwork": strings.TrimSpace(opts.WhyTeamwork), - } - if narrative["why_teamwork"] == "" { - narrative["why_teamwork"] = "The Multica issue is being bridged into Mnemon so local agents can decide whether and how to coordinate." - } - refs := map[string]any{ - "context_refs": append([]string{correlation}, cleanMulticaRefs(opts.ContextRefs)...), - } - if evidence := cleanMulticaRefs(opts.EvidenceRefs); len(evidence) > 0 { - refs["evidence_refs"] = evidence - } else { - refs["evidence_refs"] = []string{correlation} - } - externalID := strings.TrimSpace(opts.ExternalID) - if externalID == "" { - externalID = "multica-issue-" + issue.ID - } - return MulticaObservedDraft{ - EventType: "teamwork_signal.write_candidate.observed", - ExternalID: externalID, - Payload: interaction.BuildPayload(rule, narrative, refs), - }, nil + return multicasurface.BuildIssueTeamworkSignal(multicasurface.IssueSignalMaterial{ + ID: issue.ID, + Identifier: issue.Identifier, + Title: issue.Title, + Description: issue.Description, + }, opts) } func FormatMulticaProjectionComment(title string, body string, eventIDs []string) string { @@ -1093,31 +1032,3 @@ func decodeMulticaStringMap(data []byte) (map[string]string, error) { } return out, nil } - -func multicaIssueStatement(issue MulticaIssue, fallback string) string { - if strings.TrimSpace(issue.Description) != "" { - return strings.TrimSpace(issue.Description) - } - return fallback -} - -func cleanMulticaRefs(values []string) []string { - var out []string - seen := map[string]bool{} - for _, value := range values { - value = strings.TrimSpace(value) - if value == "" || seen[value] { - continue - } - seen[value] = true - out = append(out, value) - } - return out -} - -func addMulticaRuleString(rule map[string]any, key, value string) { - value = strings.TrimSpace(value) - if value != "" { - rule[key] = value - } -} diff --git a/harness/internal/surface/multica/issue_signal.go b/harness/internal/surface/multica/issue_signal.go new file mode 100644 index 00000000..58443cd3 --- /dev/null +++ b/harness/internal/surface/multica/issue_signal.go @@ -0,0 +1,105 @@ +package multica + +import ( + "fmt" + "strings" + + "github.com/mnemon-dev/mnemon/harness/internal/interaction" +) + +const MulticaExternalSource = "multica" + +type IssueSignalMaterial struct { + ID string + Identifier string + Title string + Description string +} + +type IssueSignalOptions struct { + Scope string + TTL string + WhyTeamwork string + WorkspaceID string + TaskID string + AgentID string + Principal string + EvidenceRefs []string + ContextRefs []string + ExternalID string +} + +type ObservedDraft = interaction.EventMaterial + +func BuildIssueTeamworkSignal(issue IssueSignalMaterial, opts IssueSignalOptions) (ObservedDraft, error) { + if strings.TrimSpace(issue.ID) == "" { + return ObservedDraft{}, fmt.Errorf("multica issue id is required") + } + title := strings.TrimSpace(issue.Title) + if title == "" { + title = strings.TrimSpace(issue.Identifier) + } + if title == "" { + title = issue.ID + } + scope := strings.TrimSpace(opts.Scope) + if scope == "" { + scope = "multica/teamwork" + } + ttl := strings.TrimSpace(opts.TTL) + if ttl == "" { + ttl = "30m" + } + correlation := "multica:issue:" + issue.ID + rule := map[string]any{ + "external_source": MulticaExternalSource, + "external_issue_id": issue.ID, + "external_issue_identifier": strings.TrimSpace(issue.Identifier), + "correlation_id": correlation, + "scope": scope, + "ttl": ttl, + } + putRuleString(rule, "external_workspace_id", opts.WorkspaceID) + putRuleString(rule, "external_task_id", opts.TaskID) + putRuleString(rule, "external_agent_id", opts.AgentID) + putRuleString(rule, "principal", opts.Principal) + narrative := map[string]any{ + "title": title, + "statement": issueSignalStatement(issue, title), + "why_teamwork": strings.TrimSpace(opts.WhyTeamwork), + } + if narrative["why_teamwork"] == "" { + narrative["why_teamwork"] = "The Multica issue is being bridged into Mnemon so local agents can decide whether and how to coordinate." + } + refs := map[string]any{ + "context_refs": append([]string{correlation}, cleanMulticaRefs(opts.ContextRefs)...), + } + if evidence := cleanMulticaRefs(opts.EvidenceRefs); len(evidence) > 0 { + refs["evidence_refs"] = evidence + } else { + refs["evidence_refs"] = []string{correlation} + } + externalID := strings.TrimSpace(opts.ExternalID) + if externalID == "" { + externalID = "multica-issue-" + issue.ID + } + return ObservedDraft{ + EventType: "teamwork_signal.write_candidate.observed", + ExternalID: externalID, + Payload: interaction.BuildPayload(rule, narrative, refs), + }, nil +} + +func issueSignalStatement(issue IssueSignalMaterial, fallback string) string { + if strings.TrimSpace(issue.Description) != "" { + return strings.TrimSpace(issue.Description) + } + return fallback +} + +func putRuleString(rule map[string]any, key, value string) { + value = strings.TrimSpace(value) + if value != "" { + rule[key] = value + } +} diff --git a/harness/internal/surface/multica/issue_signal_test.go b/harness/internal/surface/multica/issue_signal_test.go new file mode 100644 index 00000000..8fc23a06 --- /dev/null +++ b/harness/internal/surface/multica/issue_signal_test.go @@ -0,0 +1,74 @@ +package multica + +import "testing" + +func TestBuildIssueTeamworkSignalSeparatesRuleNarrativeRefs(t *testing.T) { + draft, err := BuildIssueTeamworkSignal(IssueSignalMaterial{ + ID: "iss-123", + Identifier: "MUL-123", + Title: "Validate bridge", + Description: "Check that Multica issue context can start Mnemon teamwork.", + }, IssueSignalOptions{ + Scope: "multica/poc", + TTL: "45m", + WhyTeamwork: "The task needs more than one local agent.", + WorkspaceID: "workspace-1", + TaskID: "task-1", + AgentID: "agent-1", + Principal: "planner@team", + EvidenceRefs: []string{"multica:issue/iss-123"}, + ExternalID: "multica-task-task-1", + }) + if err != nil { + t.Fatal(err) + } + if draft.EventType != "teamwork_signal.write_candidate.observed" { + t.Fatalf("event type = %q", draft.EventType) + } + rule := draft.Payload["rule"].(map[string]any) + if rule["external_source"] != MulticaExternalSource || rule["external_issue_id"] != "iss-123" || rule["scope"] != "multica/poc" { + t.Fatalf("rule mapping mismatch: %+v", rule) + } + if rule["external_workspace_id"] != "workspace-1" || rule["external_task_id"] != "task-1" || rule["external_agent_id"] != "agent-1" || rule["principal"] != "planner@team" { + t.Fatalf("runtime rule mapping mismatch: %+v", rule) + } + if draft.ExternalID != "multica-task-task-1" { + t.Fatalf("external id = %q", draft.ExternalID) + } + narrative := draft.Payload["narrative"].(map[string]any) + if narrative["statement"] != "Check that Multica issue context can start Mnemon teamwork." { + t.Fatalf("narrative statement mismatch: %+v", narrative) + } + if _, ok := narrative["external_issue_id"]; ok { + t.Fatalf("narrative must not carry rule ids: %+v", narrative) + } + if _, ok := narrative["external_task_id"]; ok { + t.Fatalf("narrative must not carry runtime ids: %+v", narrative) + } + refs := draft.Payload["refs"].(map[string]any) + if got := refs["evidence_refs"].([]string); len(got) != 1 || got[0] != "multica:issue/iss-123" { + t.Fatalf("evidence refs mismatch: %+v", refs) + } +} + +func TestBuildIssueTeamworkSignalDefaults(t *testing.T) { + draft, err := BuildIssueTeamworkSignal(IssueSignalMaterial{ID: "iss-1"}, IssueSignalOptions{}) + if err != nil { + t.Fatal(err) + } + rule := draft.Payload["rule"].(map[string]any) + if rule["scope"] != "multica/teamwork" || rule["ttl"] != "30m" || rule["correlation_id"] != "multica:issue:iss-1" { + t.Fatalf("default rule mismatch: %+v", rule) + } + narrative := draft.Payload["narrative"].(map[string]any) + if narrative["title"] != "iss-1" || narrative["statement"] != "iss-1" || narrative["why_teamwork"] == "" { + t.Fatalf("default narrative mismatch: %+v", narrative) + } + refs := draft.Payload["refs"].(map[string]any) + if got := refs["evidence_refs"].([]string); len(got) != 1 || got[0] != "multica:issue:iss-1" { + t.Fatalf("default evidence refs mismatch: %+v", refs) + } + if draft.ExternalID != "multica-issue-iss-1" { + t.Fatalf("default external id = %q", draft.ExternalID) + } +} From ed41bae53848ae9ebf994af0910d556e5cfdcfe8 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:43:07 +0800 Subject: [PATCH 025/117] refactor: move runtime item projection to multica surface Extract Multica runtime RPC item material for user, agent, command, and managed trace events into the surface/multica package. The runtime entrypoint now only converts those surface messages into its JSON-RPC envelope. Validation: go test -count=1 ./harness/internal/surface/multica; go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- harness/cmd/mnemon-multica-runtime/main.go | 252 ++---------------- .../cmd/mnemon-multica-runtime/main_test.go | 2 +- .../internal/surface/multica/runtime_items.go | 247 +++++++++++++++++ .../surface/multica/runtime_items_test.go | 124 +++++++++ 4 files changed, 387 insertions(+), 238 deletions(-) create mode 100644 harness/internal/surface/multica/runtime_items.go create mode 100644 harness/internal/surface/multica/runtime_items_test.go diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 59af79bd..3b043b82 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -3,7 +3,6 @@ package main import ( "bufio" "context" - "crypto/sha256" "encoding/json" "flag" "fmt" @@ -222,7 +221,7 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er s.TurnID = runtimeID("turn", s.now()) input := extractRuntimeInput(msg.Params) nowMs := s.now().UnixMilli() - userItem := runtimeUserMessage(input) + userItem := multicasurface.RuntimeUserMessage(input) if err := emitAll( rpcMessage{ ID: msg.ID, @@ -240,11 +239,11 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er }, rpcMessage{ Method: "item/started", - Params: runtimeItemParams(s.ThreadID, s.TurnID, userItem, "startedAtMs", nowMs), + Params: multicasurface.RuntimeItemParams(s.ThreadID, s.TurnID, userItem, "startedAtMs", nowMs), }, rpcMessage{ Method: "item/completed", - Params: runtimeItemParams(s.ThreadID, s.TurnID, userItem, "completedAtMs", nowMs), + Params: multicasurface.RuntimeItemParams(s.ThreadID, s.TurnID, userItem, "completedAtMs", nowMs), }, ); err != nil { return err @@ -255,23 +254,29 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er return } if event.Trace != nil { - progressErr = emitRuntimeManagedTraceEvent(emit, s.ThreadID, s.TurnID, *event.Trace, s.now()) + progressErr = emitRuntimeMessages(emit, multicasurface.RuntimeManagedTraceMessages(s.ThreadID, s.TurnID, *event.Trace, s.now())) return } if strings.TrimSpace(event.Command) != "" { - progressErr = emitRuntimeCommandExecution(emit, s.ThreadID, s.TurnID, s.nextItemID("call"), s.CWD, event, s.now()) + progressErr = emitRuntimeMessages(emit, multicasurface.RuntimeCommandExecutionMessages(s.ThreadID, s.TurnID, s.nextItemID("call"), s.CWD, multicasurface.RuntimeCommandExecutionMaterial{ + Command: event.Command, + CWD: event.CWD, + Output: event.Output, + ExitCode: event.ExitCode, + DurationMs: event.DurationMs, + }, s.now())) return } text := strings.TrimSpace(event.Text) if text != "" { - progressErr = emitRuntimeAgentMessage(emit, s.ThreadID, s.TurnID, s.nextItemID("msg"), text, "commentary", s.now()) + progressErr = emitRuntimeMessages(emit, multicasurface.RuntimeAgentMessageMessages(s.ThreadID, s.TurnID, s.nextItemID("msg"), text, "commentary", s.now())) } } finalAnswer := s.runTurn(input, progress) if progressErr != nil { return progressErr } - if err := emitRuntimeAgentMessage(emit, s.ThreadID, s.TurnID, s.nextItemID("msg"), finalAnswer, "final_answer", s.now()); err != nil { + if err := emitRuntimeMessages(emit, multicasurface.RuntimeAgentMessageMessages(s.ThreadID, s.TurnID, s.nextItemID("msg"), finalAnswer, "final_answer", s.now())); err != nil { return err } return emitAll( @@ -1197,242 +1202,15 @@ func writeRuntimeRPC(w io.Writer, msg rpcMessage) error { return err } -func emitRuntimeManagedTraceEvent(emit func(rpcMessage) error, threadID, turnID string, event driver.ManagedTurnTraceEvent, now time.Time) error { - switch event.Method { - case "item/started": - item := runtimeManagedTraceItem(event) - if len(item) == 0 { - return nil - } - return emit(rpcMessage{ - Method: "item/started", - Params: runtimeItemParams(threadID, turnID, item, "startedAtMs", now.UTC().UnixMilli()), - }) - case "item/completed": - item := runtimeManagedTraceItem(event) - if len(item) == 0 { - return nil - } - return emit(rpcMessage{ - Method: "item/completed", - Params: runtimeItemParams(threadID, turnID, item, "completedAtMs", now.UTC().UnixMilli()), - }) - case "item/agentMessage/delta": - text := strings.TrimSpace(event.Text) - if text == "" { - return nil - } - return emit(runtimeAgentDelta(threadID, turnID, runtimeManagedTraceItemID(event), text)) - default: - return nil - } -} - -func runtimeManagedTraceItem(event driver.ManagedTurnTraceEvent) map[string]any { - if len(event.Item) == 0 { - return nil - } - item, _ := cloneRuntimeAny(event.Item).(map[string]any) - if item == nil { - return nil - } - item["id"] = runtimeManagedTraceItemID(event) - if _, ok := item["source"]; !ok { - item["source"] = "managedCodexAppServer" - } - if strings.TrimSpace(event.SourceRuntime) != "" { - item["mnemonSourceRuntime"] = event.SourceRuntime - } - if strings.TrimSpace(event.Principal) != "" { - item["mnemonPrincipal"] = event.Principal - } - if strings.TrimSpace(event.TurnID) != "" { - item["mnemonManagedTurnId"] = event.TurnID - } - return item -} - -func runtimeManagedTraceItemID(event driver.ManagedTurnTraceEvent) string { - key := firstNonEmpty(event.ItemID, event.Command, event.Text, event.Kind, event.Method) - return runtimeTextDigestID("managed", event.SourceRuntime+"\n"+event.TurnID+"\n"+key) -} - -func cloneRuntimeAny(value any) any { - switch typed := value.(type) { - case map[string]any: - out := map[string]any{} - for key, item := range typed { - out[key] = cloneRuntimeAny(item) - } - return out - case []any: - out := make([]any, 0, len(typed)) - for _, item := range typed { - out = append(out, cloneRuntimeAny(item)) - } - return out - default: - return typed - } -} - -func emitRuntimeCommandExecution(emit func(rpcMessage) error, threadID, turnID, itemID, fallbackCWD string, event runtimeProgressEvent, now time.Time) error { - command := strings.TrimSpace(event.Command) - if command == "" { - return nil - } - if strings.TrimSpace(itemID) == "" { - itemID = runtimeTextDigestID("call", command+"\n"+event.Output) - } - cwd := strings.TrimSpace(event.CWD) - if cwd == "" { - cwd = fallbackCWD - } - output := strings.TrimSpace(event.Output) - durationMs := event.DurationMs - if durationMs < 0 { - durationMs = 0 - } - nowMs := now.UTC().UnixMilli() - started := runtimeCommandExecution(itemID, command, cwd, "inProgress", "", nil, nil) - completed := runtimeCommandExecution(itemID, command, cwd, "completed", output, event.ExitCode, durationMs) - messages := []rpcMessage{ - { - Method: "item/started", - Params: runtimeItemParams(threadID, turnID, started, "startedAtMs", nowMs), - }, - { - Method: "item/completed", - Params: runtimeItemParams(threadID, turnID, completed, "completedAtMs", nowMs), - }, - } +func emitRuntimeMessages(emit func(rpcMessage) error, messages []multicasurface.RuntimeRPCMessage) error { for _, message := range messages { - if err := emit(message); err != nil { + if err := emit(rpcMessage{Method: message.Method, Params: message.Params}); err != nil { return err } } return nil } -func runtimeCommandExecution(id, command, cwd, status, output string, exitCode any, durationMs any) map[string]any { - item := map[string]any{ - "type": "commandExecution", - "id": id, - "command": command, - "cwd": cwd, - "processId": "mnemon-runtime", - "source": "mnemonRuntime", - "status": status, - "commandActions": []any{map[string]any{"type": "unknown", "command": command}}, - "aggregatedOutput": nil, - "exitCode": nil, - "durationMs": nil, - } - if status == "completed" { - item["aggregatedOutput"] = output - item["exitCode"] = exitCode - item["durationMs"] = durationMs - } - return item -} - -func emitRuntimeAgentMessage(emit func(rpcMessage) error, threadID, turnID, itemID, text, phase string, now time.Time) error { - text = strings.TrimSpace(text) - if text == "" { - return nil - } - phase = strings.TrimSpace(phase) - if phase == "" { - phase = "commentary" - } - if strings.TrimSpace(itemID) == "" { - itemID = runtimeTextDigestID("msg", text) - } - nowMs := now.UTC().UnixMilli() - messages := []rpcMessage{ - { - Method: "item/started", - Params: runtimeItemParams(threadID, turnID, runtimeAgentMessage(itemID, "", phase), "startedAtMs", nowMs), - }, - runtimeAgentDelta(threadID, turnID, itemID, text), - { - Method: "item/completed", - Params: runtimeItemParams(threadID, turnID, runtimeAgentMessage(itemID, text, phase), "completedAtMs", nowMs), - }, - } - for _, message := range messages { - if err := emit(message); err != nil { - return err - } - } - return nil -} - -func runtimeAgentDelta(threadID, turnID, itemID, delta string) rpcMessage { - return rpcMessage{ - Method: "item/agentMessage/delta", - Params: map[string]any{ - "threadId": threadID, - "turnId": turnID, - "itemId": itemID, - "delta": delta, - }, - } -} - -func runtimeItemParams(threadID, turnID string, item map[string]any, timeKey string, timestampMs int64) map[string]any { - params := map[string]any{ - "threadId": threadID, - "turnId": turnID, - "item": item, - } - if timeKey != "" && timestampMs > 0 { - params[timeKey] = timestampMs - } - return params -} - -func runtimeUserMessage(text string) map[string]any { - return map[string]any{ - "type": "userMessage", - "id": runtimeTextDigestID("user", text), - "clientId": nil, - "content": []any{map[string]any{ - "type": "text", - "text": text, - "text_elements": []any{}, - }}, - } -} - -func runtimeAgentMessage(id, text, phase string) map[string]any { - id = strings.TrimSpace(id) - if id == "" { - id = runtimeTextDigestID("msg", text) - } - phase = strings.TrimSpace(phase) - if phase == "" { - phase = "commentary" - } - return map[string]any{ - "type": "agentMessage", - "id": id, - "text": text, - "phase": phase, - "memoryCitation": nil, - } -} - -func runtimeTextDigestID(prefix, text string) string { - prefix = strings.TrimSpace(prefix) - if prefix == "" { - prefix = "item" - } - sum := sha256.Sum256([]byte(text)) - digest := fmt.Sprintf("%x", sum[:])[:24] - return prefix + "_" + digest -} - func emitRuntimeProgress(progress runtimeProgressSink, text string) { if progress != nil { progress(runtimeProgressEvent{Text: text}) diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index b90e37df..67e4c8fc 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -434,7 +434,7 @@ func TestRuntimeForwardsManagedCodexTraceItemsOnOuterTurn(t *testing.T) { "phase": "commentary", } for _, event := range []driver.ManagedTurnTraceEvent{traceStarted, traceDelta, traceCompleted} { - if err := emitRuntimeManagedTraceEvent(emit, "outer-thread", "outer-turn", event, fixedRuntimeTime()); err != nil { + if err := emitRuntimeMessages(emit, multicasurface.RuntimeManagedTraceMessages("outer-thread", "outer-turn", event, fixedRuntimeTime())); err != nil { t.Fatal(err) } } diff --git a/harness/internal/surface/multica/runtime_items.go b/harness/internal/surface/multica/runtime_items.go new file mode 100644 index 00000000..e98880a8 --- /dev/null +++ b/harness/internal/surface/multica/runtime_items.go @@ -0,0 +1,247 @@ +package multica + +import ( + "crypto/sha256" + "fmt" + "strings" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/activationtrace" +) + +type RuntimeRPCMessage struct { + Method string + Params map[string]any +} + +type RuntimeCommandExecutionMaterial struct { + Command string + CWD string + Output string + ExitCode int + DurationMs int64 +} + +func RuntimeManagedTraceMessages(threadID, turnID string, event activationtrace.Event, now time.Time) []RuntimeRPCMessage { + switch event.Method { + case "item/started": + item := runtimeManagedTraceItem(event) + if len(item) == 0 { + return nil + } + return []RuntimeRPCMessage{{ + Method: "item/started", + Params: RuntimeItemParams(threadID, turnID, item, "startedAtMs", now.UTC().UnixMilli()), + }} + case "item/completed": + item := runtimeManagedTraceItem(event) + if len(item) == 0 { + return nil + } + return []RuntimeRPCMessage{{ + Method: "item/completed", + Params: RuntimeItemParams(threadID, turnID, item, "completedAtMs", now.UTC().UnixMilli()), + }} + case "item/agentMessage/delta": + text := strings.TrimSpace(event.Text) + if text == "" { + return nil + } + return []RuntimeRPCMessage{runtimeAgentDelta(threadID, turnID, runtimeManagedTraceItemID(event), text)} + default: + return nil + } +} + +func RuntimeCommandExecutionMessages(threadID, turnID, itemID, fallbackCWD string, event RuntimeCommandExecutionMaterial, now time.Time) []RuntimeRPCMessage { + command := strings.TrimSpace(event.Command) + if command == "" { + return nil + } + if strings.TrimSpace(itemID) == "" { + itemID = runtimeTextDigestID("call", command+"\n"+event.Output) + } + cwd := strings.TrimSpace(event.CWD) + if cwd == "" { + cwd = fallbackCWD + } + output := strings.TrimSpace(event.Output) + durationMs := event.DurationMs + if durationMs < 0 { + durationMs = 0 + } + nowMs := now.UTC().UnixMilli() + started := runtimeCommandExecution(itemID, command, cwd, "inProgress", "", nil, nil) + completed := runtimeCommandExecution(itemID, command, cwd, "completed", output, event.ExitCode, durationMs) + return []RuntimeRPCMessage{ + { + Method: "item/started", + Params: RuntimeItemParams(threadID, turnID, started, "startedAtMs", nowMs), + }, + { + Method: "item/completed", + Params: RuntimeItemParams(threadID, turnID, completed, "completedAtMs", nowMs), + }, + } +} + +func RuntimeAgentMessageMessages(threadID, turnID, itemID, text, phase string, now time.Time) []RuntimeRPCMessage { + text = strings.TrimSpace(text) + if text == "" { + return nil + } + phase = strings.TrimSpace(phase) + if phase == "" { + phase = "commentary" + } + if strings.TrimSpace(itemID) == "" { + itemID = runtimeTextDigestID("msg", text) + } + nowMs := now.UTC().UnixMilli() + return []RuntimeRPCMessage{ + { + Method: "item/started", + Params: RuntimeItemParams(threadID, turnID, runtimeAgentMessage(itemID, "", phase), "startedAtMs", nowMs), + }, + runtimeAgentDelta(threadID, turnID, itemID, text), + { + Method: "item/completed", + Params: RuntimeItemParams(threadID, turnID, runtimeAgentMessage(itemID, text, phase), "completedAtMs", nowMs), + }, + } +} + +func RuntimeItemParams(threadID, turnID string, item map[string]any, timeKey string, timestampMs int64) map[string]any { + params := map[string]any{ + "threadId": threadID, + "turnId": turnID, + "item": item, + } + if timeKey != "" && timestampMs > 0 { + params[timeKey] = timestampMs + } + return params +} + +func RuntimeUserMessage(text string) map[string]any { + return map[string]any{ + "type": "userMessage", + "id": runtimeTextDigestID("user", text), + "clientId": nil, + "content": []any{map[string]any{ + "type": "text", + "text": text, + "text_elements": []any{}, + }}, + } +} + +func runtimeManagedTraceItem(event activationtrace.Event) map[string]any { + if len(event.Item) == 0 { + return nil + } + item, _ := cloneRuntimeAny(event.Item).(map[string]any) + if item == nil { + return nil + } + item["id"] = runtimeManagedTraceItemID(event) + if _, ok := item["source"]; !ok { + item["source"] = "managedCodexAppServer" + } + if strings.TrimSpace(event.SourceRuntime) != "" { + item["mnemonSourceRuntime"] = event.SourceRuntime + } + if strings.TrimSpace(event.Principal) != "" { + item["mnemonPrincipal"] = event.Principal + } + if strings.TrimSpace(event.TurnID) != "" { + item["mnemonManagedTurnId"] = event.TurnID + } + return item +} + +func runtimeManagedTraceItemID(event activationtrace.Event) string { + key := firstNonEmptyString(event.ItemID, event.Command, event.Text, event.Kind, event.Method) + return runtimeTextDigestID("managed", event.SourceRuntime+"\n"+event.TurnID+"\n"+key) +} + +func cloneRuntimeAny(value any) any { + switch typed := value.(type) { + case map[string]any: + out := map[string]any{} + for key, item := range typed { + out[key] = cloneRuntimeAny(item) + } + return out + case []any: + out := make([]any, 0, len(typed)) + for _, item := range typed { + out = append(out, cloneRuntimeAny(item)) + } + return out + default: + return typed + } +} + +func runtimeCommandExecution(id, command, cwd, status, output string, exitCode any, durationMs any) map[string]any { + item := map[string]any{ + "type": "commandExecution", + "id": id, + "command": command, + "cwd": cwd, + "processId": "mnemon-runtime", + "source": "mnemonRuntime", + "status": status, + "commandActions": []any{map[string]any{"type": "unknown", "command": command}}, + "aggregatedOutput": nil, + "exitCode": nil, + "durationMs": nil, + } + if status == "completed" { + item["aggregatedOutput"] = output + item["exitCode"] = exitCode + item["durationMs"] = durationMs + } + return item +} + +func runtimeAgentDelta(threadID, turnID, itemID, delta string) RuntimeRPCMessage { + return RuntimeRPCMessage{ + Method: "item/agentMessage/delta", + Params: map[string]any{ + "threadId": threadID, + "turnId": turnID, + "itemId": itemID, + "delta": delta, + }, + } +} + +func runtimeAgentMessage(id, text, phase string) map[string]any { + id = strings.TrimSpace(id) + if id == "" { + id = runtimeTextDigestID("msg", text) + } + phase = strings.TrimSpace(phase) + if phase == "" { + phase = "commentary" + } + return map[string]any{ + "type": "agentMessage", + "id": id, + "text": text, + "phase": phase, + "memoryCitation": nil, + } +} + +func runtimeTextDigestID(prefix, text string) string { + prefix = strings.TrimSpace(prefix) + if prefix == "" { + prefix = "item" + } + sum := sha256.Sum256([]byte(text)) + digest := fmt.Sprintf("%x", sum[:])[:24] + return prefix + "_" + digest +} diff --git a/harness/internal/surface/multica/runtime_items_test.go b/harness/internal/surface/multica/runtime_items_test.go new file mode 100644 index 00000000..612576ba --- /dev/null +++ b/harness/internal/surface/multica/runtime_items_test.go @@ -0,0 +1,124 @@ +package multica + +import ( + "strings" + "testing" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/activationtrace" +) + +func TestRuntimeManagedTraceMessagesRemapItemIDs(t *testing.T) { + now := time.Date(2026, 6, 29, 7, 30, 0, 0, time.UTC) + started := activationtrace.Event{ + SourceRuntime: activationtrace.SourceCodexAppServer, + Principal: "planner@team", + TurnID: "inner-turn", + ItemID: "inner-msg", + Method: "item/started", + Item: map[string]any{ + "type": "agentMessage", + "id": "inner-msg", + "text": "", + "phase": "commentary", + "nested": map[string]any{ + "id": "nested-original", + }, + }, + } + delta := started + delta.Method = "item/agentMessage/delta" + delta.Text = "native progress" + delta.Item = nil + completed := started + completed.Method = "item/completed" + completed.Item = map[string]any{ + "type": "agentMessage", + "id": "inner-msg", + "text": "native progress", + "phase": "commentary", + } + + var messages []RuntimeRPCMessage + for _, event := range []activationtrace.Event{started, delta, completed} { + messages = append(messages, RuntimeManagedTraceMessages("outer-thread", "outer-turn", event, now)...) + } + if len(messages) != 3 { + t.Fatalf("messages = %+v, want 3", messages) + } + + var itemID string + for i, message := range messages { + if message.Params["threadId"] != "outer-thread" || message.Params["turnId"] != "outer-turn" { + t.Fatalf("message %d attached to wrong turn: %+v", i, message) + } + switch message.Method { + case "item/started", "item/completed": + item, _ := message.Params["item"].(map[string]any) + if item["id"] == "inner-msg" { + t.Fatalf("message %d leaked managed runtime item id: %+v", i, message) + } + if item["mnemonManagedTurnId"] != "inner-turn" || item["mnemonPrincipal"] != "planner@team" || item["mnemonSourceRuntime"] != activationtrace.SourceCodexAppServer { + t.Fatalf("message %d missing managed trace metadata: %+v", i, item) + } + if itemID == "" { + itemID, _ = item["id"].(string) + } else if item["id"] != itemID { + t.Fatalf("managed trace item id changed: first=%q got=%q", itemID, item["id"]) + } + case "item/agentMessage/delta": + if message.Params["itemId"] != itemID { + t.Fatalf("delta item id = %q, want %q", message.Params["itemId"], itemID) + } + if message.Params["delta"] != "native progress" { + t.Fatalf("delta params = %+v", message.Params) + } + default: + t.Fatalf("unexpected method %q", message.Method) + } + } + if started.Item["id"] != "inner-msg" { + t.Fatalf("input item mutated: %+v", started.Item) + } +} + +func TestRuntimeCommandExecutionMessagesUseStableRuntimeShape(t *testing.T) { + messages := RuntimeCommandExecutionMessages("thread-1", "turn-1", "", "/workspace", RuntimeCommandExecutionMaterial{ + Command: "multica issue get iss-1", + Output: "Loaded TEA-1", + ExitCode: 0, + DurationMs: -1, + }, time.Date(2026, 6, 29, 7, 31, 0, 0, time.UTC)) + if len(messages) != 2 { + t.Fatalf("messages = %+v, want start+complete", messages) + } + started, _ := messages[0].Params["item"].(map[string]any) + completed, _ := messages[1].Params["item"].(map[string]any) + if messages[0].Method != "item/started" || started["status"] != "inProgress" || started["cwd"] != "/workspace" { + t.Fatalf("unexpected started command item: %+v", messages[0]) + } + if messages[1].Method != "item/completed" || completed["status"] != "completed" || completed["aggregatedOutput"] != "Loaded TEA-1" { + t.Fatalf("unexpected completed command item: %+v", messages[1]) + } + if completed["durationMs"] != int64(0) { + t.Fatalf("durationMs = %+v, want 0", completed["durationMs"]) + } + if id, _ := started["id"].(string); !strings.HasPrefix(id, "call_") || completed["id"] != id { + t.Fatalf("command item id mismatch start=%+v completed=%+v", started["id"], completed["id"]) + } +} + +func TestRuntimeAgentMessageMessagesUsePhaseAndDelta(t *testing.T) { + messages := RuntimeAgentMessageMessages("thread-1", "turn-1", "msg-1", "done", "final_answer", time.Date(2026, 6, 29, 7, 32, 0, 0, time.UTC)) + if len(messages) != 3 { + t.Fatalf("messages = %+v, want start+delta+complete", messages) + } + started, _ := messages[0].Params["item"].(map[string]any) + completed, _ := messages[2].Params["item"].(map[string]any) + if started["phase"] != "final_answer" || completed["phase"] != "final_answer" { + t.Fatalf("phase mismatch start=%+v completed=%+v", started, completed) + } + if messages[1].Method != "item/agentMessage/delta" || messages[1].Params["delta"] != "done" || messages[1].Params["itemId"] != "msg-1" { + t.Fatalf("unexpected delta message: %+v", messages[1]) + } +} From 5196a8c90f33de4cd237870184d09cf9032d784d Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:48:22 +0800 Subject: [PATCH 026/117] refactor: move managed wake selection to drive Extract render-response candidate selection and stable issue/assignment match-term handling from the Multica runtime entrypoint into the drive layer. The runtime now only maps its import result into drive material before waking the managed agent. Validation: go test -count=1 ./harness/internal/drive; go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- harness/cmd/mnemon-multica-runtime/main.go | 90 ++++-------------- .../cmd/mnemon-multica-runtime/main_test.go | 5 +- harness/internal/drive/selection.go | 92 +++++++++++++++++++ harness/internal/drive/selection_test.go | 78 ++++++++++++++++ 4 files changed, 189 insertions(+), 76 deletions(-) create mode 100644 harness/internal/drive/selection.go create mode 100644 harness/internal/drive/selection_test.go diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 3b043b82..6893c0de 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -16,6 +16,7 @@ import ( "unicode" "github.com/mnemon-dev/mnemon/harness/internal/contract" + "github.com/mnemon-dev/mnemon/harness/internal/drive" "github.com/mnemon-dev/mnemon/harness/internal/driver" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" @@ -464,7 +465,7 @@ func loadRuntimeIssueMetadata(ctx context.Context, cli driver.MulticaCLI, issue func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, result *runtimeImportResult, progress runtimeProgressSink) runtimeImportResult { ensureAssignmentHubFields(result, issue) result.Status = "correlated" - result.MatchTerms = cleanRuntimeTerms( + result.MatchTerms = drive.CleanManagedWakeMatchTerms( result.AssignmentID, result.AssignmentFingerprint, result.HubMetadata.EventID, @@ -573,20 +574,6 @@ func addPayloadRuleString(payload map[string]any, key, value string) { rule[key] = value } -func cleanRuntimeTerms(values ...string) []string { - seen := map[string]bool{} - var out []string - for _, value := range values { - value = strings.TrimSpace(value) - if len(value) < 3 || seen[value] { - continue - } - seen[value] = true - out = append(out, value) - } - return out -} - func assignmentMailboxMarker(result runtimeImportResult) string { if result.HubMetadata.EventID != "" { return result.HubMetadata.EventID @@ -597,6 +584,19 @@ func assignmentMailboxMarker(result runtimeImportResult) string { return "multica-issue-" + result.IssueID } +func runtimeManagedWakeMatchMaterial(result runtimeImportResult) drive.ManagedWakeMatchMaterial { + return drive.ManagedWakeMatchMaterial{ + MatchTerms: result.MatchTerms, + AssignmentID: result.AssignmentID, + AssignmentFingerprint: result.AssignmentFingerprint, + IssueID: result.IssueID, + Identifier: result.Identifier, + Title: result.Title, + Statement: result.Statement, + TaskID: result.TaskID, + } +} + func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress runtimeProgressSink) { if result == nil { return @@ -637,7 +637,7 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress result.WakeErr = fmt.Errorf("render managed wake candidates: %w", err) return } - candidate, ok := managedWakeCandidateForResult(result.Principal, resp, *result) + candidate, ok := drive.ManagedWakeCandidateForRender(result.Principal, resp, runtimeManagedWakeMatchMaterial(*result)) if !ok { result.WakeStatus = "skipped" result.WakeErr = fmt.Errorf("no managed wake candidate in rendered context") @@ -759,64 +759,6 @@ func mergeRuntimeHubProjectionDeltas(result *runtimeImportResult, deltas []runti } } -func managedWakeCandidateForResult(principal string, resp presentation.Response, result runtimeImportResult) (driver.ManagedWakeCandidate, bool) { - terms := managedWakeMatchTerms(result) - var fallback driver.ManagedWakeCandidate - for _, env := range resp.Events { - candidates := driver.ManagedWakeCandidatesFromEvents(principal, []eventmodel.EventEnvelope{env}) - if len(candidates) == 0 { - continue - } - candidates[0].RenderAuditID = resp.AuditID - candidates[0].RenderBodyDigest = resp.BodyDigest - if fallback.Principal == "" { - fallback = candidates[0] - } - if len(terms) == 0 || eventNarrativeContainsAny(env, terms) { - return candidates[0], true - } - } - if len(terms) == 0 && fallback.Principal != "" { - return fallback, true - } - return driver.ManagedWakeCandidate{}, false -} - -func managedWakeMatchTerms(result runtimeImportResult) []string { - if len(result.MatchTerms) > 0 { - return cleanRuntimeTerms(result.MatchTerms...) - } - if result.AssignmentID != "" || result.AssignmentFingerprint != "" { - return cleanRuntimeTerms(result.AssignmentID, result.AssignmentFingerprint) - } - raw := []string{result.IssueID, result.Identifier, result.Title, result.TaskID} - var out []string - for _, value := range raw { - value = strings.TrimSpace(value) - if len(value) >= 3 { - out = append(out, value) - } - } - if len(out) > 0 { - return out - } - if value := strings.TrimSpace(result.Statement); len(value) >= 3 { - out = append(out, value) - } - return out -} - -func eventNarrativeContainsAny(env eventmodel.EventEnvelope, terms []string) bool { - body, _ := eventmodel.PayloadNarrative(env.Event.Payload)["body"].(string) - body = strings.ToLower(strings.Join([]string{body, string(env.Event.Subject), env.Event.ID, env.Event.Type}, "\n")) - for _, term := range terms { - if strings.Contains(body, strings.ToLower(term)) { - return true - } - } - return false -} - func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, externalID string, result *runtimeImportResult) { if result == nil { return diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 67e4c8fc..dace1e58 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/mnemon-dev/mnemon/harness/internal/contract" + "github.com/mnemon-dev/mnemon/harness/internal/drive" "github.com/mnemon-dev/mnemon/harness/internal/driver" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" @@ -97,13 +98,13 @@ func TestMergeRuntimeHubProjectionDeltasPreservesEarlyDispatchCounts(t *testing. } func TestManagedWakeMatchTermsPreferStableIssueIdentity(t *testing.T) { - got := managedWakeMatchTerms(runtimeImportResult{ + got := drive.ManagedWakeMatchTerms(runtimeManagedWakeMatchMaterial(runtimeImportResult{ IssueID: "issue-123", Identifier: "TEA-27", Title: "Mnemon R2 hub-flow completion drill", Statement: "Run a small hub-flow readiness drill.", TaskID: "task-123", - }) + })) joined := strings.Join(got, "\n") for _, want := range []string{"issue-123", "TEA-27", "Mnemon R2 hub-flow completion drill", "task-123"} { if !strings.Contains(joined, want) { diff --git a/harness/internal/drive/selection.go b/harness/internal/drive/selection.go new file mode 100644 index 00000000..2c69f846 --- /dev/null +++ b/harness/internal/drive/selection.go @@ -0,0 +1,92 @@ +package drive + +import ( + "strings" + + eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" +) + +type ManagedWakeMatchMaterial struct { + MatchTerms []string + AssignmentID string + AssignmentFingerprint string + IssueID string + Identifier string + Title string + Statement string + TaskID string +} + +func ManagedWakeCandidateForRender(principal string, resp presentation.Response, material ManagedWakeMatchMaterial) (ManagedWakeCandidate, bool) { + terms := ManagedWakeMatchTerms(material) + var fallback ManagedWakeCandidate + for _, env := range resp.Events { + candidates := ManagedWakeCandidatesFromEvents(principal, []eventmodel.EventEnvelope{env}) + if len(candidates) == 0 { + continue + } + candidate := candidates[0] + candidate.RenderAuditID = resp.AuditID + candidate.RenderBodyDigest = resp.BodyDigest + if fallback.Principal == "" { + fallback = candidate + } + if len(terms) == 0 || eventNarrativeContainsAny(env, terms) { + return candidate, true + } + } + if len(terms) == 0 && fallback.Principal != "" { + return fallback, true + } + return ManagedWakeCandidate{}, false +} + +func ManagedWakeMatchTerms(material ManagedWakeMatchMaterial) []string { + if len(material.MatchTerms) > 0 { + return CleanManagedWakeMatchTerms(material.MatchTerms...) + } + if material.AssignmentID != "" || material.AssignmentFingerprint != "" { + return CleanManagedWakeMatchTerms(material.AssignmentID, material.AssignmentFingerprint) + } + raw := []string{material.IssueID, material.Identifier, material.Title, material.TaskID} + var out []string + for _, value := range raw { + value = strings.TrimSpace(value) + if len(value) >= 3 { + out = append(out, value) + } + } + if len(out) > 0 { + return out + } + if value := strings.TrimSpace(material.Statement); len(value) >= 3 { + out = append(out, value) + } + return out +} + +func CleanManagedWakeMatchTerms(values ...string) []string { + seen := map[string]bool{} + var out []string + for _, value := range values { + value = strings.TrimSpace(value) + if len(value) < 3 || seen[value] { + continue + } + seen[value] = true + out = append(out, value) + } + return out +} + +func eventNarrativeContainsAny(env eventmodel.EventEnvelope, terms []string) bool { + body, _ := eventmodel.PayloadNarrative(env.Event.Payload)["body"].(string) + body = strings.ToLower(strings.Join([]string{body, string(env.Event.Subject), env.Event.ID, env.Event.Type}, "\n")) + for _, term := range terms { + if strings.Contains(body, strings.ToLower(term)) { + return true + } + } + return false +} diff --git a/harness/internal/drive/selection_test.go b/harness/internal/drive/selection_test.go new file mode 100644 index 00000000..5c73ac38 --- /dev/null +++ b/harness/internal/drive/selection_test.go @@ -0,0 +1,78 @@ +package drive + +import ( + "strings" + "testing" + + eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" +) + +func TestManagedWakeCandidateForRenderMatchesStableIssueIdentity(t *testing.T) { + resp := presentation.Response{ + AuditID: "audit-1", + BodyDigest: "sha256:render", + Events: []eventmodel.EventEnvelope{ + renderedWakeEnvelope("derived:first", "assignment/asg-other", "Assignment ASG-OTHER is available."), + renderedWakeEnvelope("derived:second", "assignment/asg-27", "Please handle TEA-27 for the Mnemon R2 hub-flow completion drill."), + }, + } + candidate, ok := ManagedWakeCandidateForRender("planner@team", resp, ManagedWakeMatchMaterial{ + IssueID: "issue-123", + Identifier: "TEA-27", + Title: "Mnemon R2 hub-flow completion drill", + TaskID: "task-123", + }) + if !ok { + t.Fatal("expected matching wake candidate") + } + if candidate.DerivedEventID != "derived:second" { + t.Fatalf("selected candidate = %+v, want second event", candidate) + } + if candidate.RenderAuditID != resp.AuditID || candidate.RenderBodyDigest != resp.BodyDigest { + t.Fatalf("candidate should carry render metadata: %+v", candidate) + } +} + +func TestManagedWakeMatchTermsPreferStableIssueIdentity(t *testing.T) { + got := ManagedWakeMatchTerms(ManagedWakeMatchMaterial{ + IssueID: "issue-123", + Identifier: "TEA-27", + Title: "Mnemon R2 hub-flow completion drill", + Statement: "Run a small hub-flow readiness drill.", + TaskID: "task-123", + }) + joined := strings.Join(got, "\n") + for _, want := range []string{"issue-123", "TEA-27", "Mnemon R2 hub-flow completion drill", "task-123"} { + if !strings.Contains(joined, want) { + t.Fatalf("match terms missing %q: %+v", want, got) + } + } + if strings.Contains(joined, "Run a small hub-flow readiness drill.") { + t.Fatalf("root issue matching must not prefer reusable statement text: %+v", got) + } +} + +func TestManagedWakeMatchTermsPreferAssignmentMailboxIdentity(t *testing.T) { + got := ManagedWakeMatchTerms(ManagedWakeMatchMaterial{ + AssignmentID: "assignment-final-routing-mailbox", + AssignmentFingerprint: "fp-1", + IssueID: "child-issue", + Identifier: "TEA-16", + }) + if strings.Join(got, "\n") != "assignment-final-routing-mailbox\nfp-1" { + t.Fatalf("assignment terms = %+v", got) + } +} + +func renderedWakeEnvelope(id, subject, body string) eventmodel.EventEnvelope { + return eventmodel.DerivedEnvelope(eventmodel.Event{ + SchemaVersion: eventmodel.SchemaVersion, + ID: id, + Type: "assignment.work_available", + Subject: eventmodel.EventSubject(subject), + Actor: "mnemond@local", + Audience: "planner@team", + Payload: eventmodel.BuildPayload(nil, map[string]any{"body": body}, nil), + }, "2026-06-29T10:00:00Z", "2026-06-29T10:05:00Z", "work", nil) +} From 8c613863359ff4f55c3fc968440c9831992353f8 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:50:30 +0800 Subject: [PATCH 027/117] refactor: centralize multica metadata merge Move the runtime issue metadata overlay rule into surface/multica so issue get metadata and issue metadata list results are merged consistently, with listed values taking precedence over embedded issue metadata. Validation: go test -count=1 ./harness/internal/surface/multica; go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- harness/cmd/mnemon-multica-runtime/main.go | 11 +------ harness/internal/surface/multica/metadata.go | 16 ++++++++++ .../internal/surface/multica/metadata_test.go | 32 +++++++++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 harness/internal/surface/multica/metadata.go create mode 100644 harness/internal/surface/multica/metadata_test.go diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 6893c0de..e748cb32 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -448,16 +448,7 @@ func loadRuntimeIssueMetadata(ctx context.Context, cli driver.MulticaCLI, issue emitRuntimeProgress(progress, "Multica issue metadata list failed; falling back to metadata returned by issue get.") return issue } - merged := map[string]any{} - for key, value := range issue.Metadata { - merged[key] = value - } - for key, value := range listed { - if strings.TrimSpace(key) != "" { - merged[key] = value - } - } - issue.Metadata = merged + issue.Metadata = multicasurface.MergeIssueMetadata(issue.Metadata, listed) emitRuntimeCommand(progress, "multica issue metadata list "+issue.ID, fmt.Sprintf("Loaded %d Multica issue metadata keys.", len(listed)), 0) return issue } diff --git a/harness/internal/surface/multica/metadata.go b/harness/internal/surface/multica/metadata.go new file mode 100644 index 00000000..4faa5edf --- /dev/null +++ b/harness/internal/surface/multica/metadata.go @@ -0,0 +1,16 @@ +package multica + +import "strings" + +func MergeIssueMetadata(base map[string]any, listed map[string]string) map[string]any { + merged := map[string]any{} + for key, value := range base { + merged[key] = value + } + for key, value := range listed { + if strings.TrimSpace(key) != "" { + merged[key] = value + } + } + return merged +} diff --git a/harness/internal/surface/multica/metadata_test.go b/harness/internal/surface/multica/metadata_test.go new file mode 100644 index 00000000..49e24c21 --- /dev/null +++ b/harness/internal/surface/multica/metadata_test.go @@ -0,0 +1,32 @@ +package multica + +import "testing" + +func TestMergeIssueMetadataUsesListedValuesAsAuthoritative(t *testing.T) { + base := map[string]any{ + MulticaMetadataKind: "embedded", + MulticaMetadataSessionID: "embedded-session", + "local": 7, + } + listed := map[string]string{ + MulticaMetadataKind: MulticaHubKindAssignmentMailbox, + MulticaMetadataCorrelationID: "multica:issue:child-1", + "": "ignored", + } + got := MergeIssueMetadata(base, listed) + if got[MulticaMetadataKind] != MulticaHubKindAssignmentMailbox { + t.Fatalf("listed metadata should override embedded value: %+v", got) + } + if got[MulticaMetadataSessionID] != "embedded-session" || got["local"] != 7 { + t.Fatalf("base metadata not preserved: %+v", got) + } + if got[MulticaMetadataCorrelationID] != "multica:issue:child-1" { + t.Fatalf("listed metadata not merged: %+v", got) + } + if _, ok := got[""]; ok { + t.Fatalf("empty listed key should be ignored: %+v", got) + } + if base[MulticaMetadataKind] != "embedded" { + t.Fatalf("base metadata mutated: %+v", base) + } +} From e3817a0b35e413d72be5428f897e58b6e516f8f6 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 15:57:50 +0800 Subject: [PATCH 028/117] feat: persist harness daemon status snapshots Add a file-backed daemon status snapshot store and have mnemon-harness daemon status read the default snapshot path when present. This gives interaction, drive, projection, and status workers a durable readiness surface without mutating missing config. Validation: go test -count=1 ./harness/internal/daemon; go test -count=1 ./harness/cmd/mnemon-harness; go test -count=1 ./harness/... --- .../cmd/mnemon-harness/config_daemon_test.go | 52 +++++++++++++ harness/cmd/mnemon-harness/daemon.go | 41 ++++++++++ harness/internal/daemon/snapshot_store.go | 76 +++++++++++++++++++ .../internal/daemon/snapshot_store_test.go | 61 +++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 harness/internal/daemon/snapshot_store.go create mode 100644 harness/internal/daemon/snapshot_store_test.go diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index 3782db0d..14a3849e 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -4,7 +4,9 @@ import ( "path/filepath" "strings" "testing" + "time" + "github.com/mnemon-dev/mnemon/harness/internal/daemon" "github.com/mnemon-dev/mnemon/harness/internal/productconfig" multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) @@ -138,6 +140,56 @@ func TestDaemonStatusShowsLegacyMulticaRoleSummary(t *testing.T) { } } +func TestDaemonStatusShowsWorkerSnapshot(t *testing.T) { + root := t.TempDir() + cfg := productconfig.Default() + cfg.Connections.Multica = productconfig.MulticaConnection{Enabled: true, Workspace: "ws-multica", RuntimeBinary: "mnemon-multica-runtime"} + cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMultica} + cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} + if err := productconfig.Save(productconfig.DefaultPath(root, ""), cfg); err != nil { + t.Fatal(err) + } + started := time.Date(2026, 6, 29, 8, 0, 0, 0, time.UTC) + updated := started.Add(time.Minute) + if err := daemon.NewFileSnapshotStore(daemon.StatusSnapshotPath(root, "")).Save(daemon.Snapshot{ + StartedAt: started, + Workers: map[string]daemon.WorkerSnapshot{ + "managed-drive": { + Kind: daemon.WorkerDrive, + Status: "idle", + Message: "wake ledger clean", + UpdatedAt: updated, + }, + "multica-watch": { + Kind: daemon.WorkerInteraction, + Status: "failed", + Error: "metadata cursor rejected", + }, + }, + }); err != nil { + t.Fatal(err) + } + oldRoot := daemonRoot + daemonRoot = root + t.Cleanup(func() { daemonRoot = oldRoot }) + + cmd, out := testCommand() + if err := runDaemonStatus(cmd, nil); err != nil { + t.Fatal(err) + } + got := out.String() + for _, want := range []string{ + "Harness daemon snapshot: workers=2 started=2026-06-29T08:00:00Z", + "managed-drive [drive]: idle (wake ledger clean) updated=2026-06-29T08:01:00Z", + "multica-watch [interaction]: failed error=metadata cursor rejected", + "Harness daemon: not running", + } { + if !strings.Contains(got, want) { + t.Fatalf("daemon status missing %q:\n%s", want, got) + } + } +} + func TestAgentAddAndListWriteProductConfig(t *testing.T) { root := t.TempDir() oldRoot, oldPath := agentRoot, agentConfigPath diff --git a/harness/cmd/mnemon-harness/daemon.go b/harness/cmd/mnemon-harness/daemon.go index bfa54462..5ce1d17b 100644 --- a/harness/cmd/mnemon-harness/daemon.go +++ b/harness/cmd/mnemon-harness/daemon.go @@ -4,9 +4,11 @@ import ( "fmt" "io" "path/filepath" + "sort" "time" "github.com/mnemon-dev/mnemon/harness/internal/app" + "github.com/mnemon-dev/mnemon/harness/internal/daemon" "github.com/mnemon-dev/mnemon/harness/internal/productconfig" "github.com/spf13/cobra" ) @@ -98,6 +100,7 @@ func runDaemonStatus(cmd *cobra.Command, args []string) error { } else { fmt.Fprintln(cmd.OutOrStdout(), "Harness config: not configured") } + writeDaemonSnapshotSummary(cmd.OutOrStdout(), root) if cfg, err := app.ReadLocalConfig(root); err == nil { if _, ok := localServiceStatus(root, cfg, cfg.Principal); ok { fmt.Fprintln(cmd.OutOrStdout(), "Harness daemon: ready") @@ -112,6 +115,44 @@ func writeDaemonRoleSummary(out io.Writer, cfg productconfig.Config) { fmt.Fprintf(out, "Harness daemon roles: watchers=%d drive=%d surfaces=%d\n", len(cfg.Daemon.InteractionWatchers), len(cfg.Daemon.DriveSources), len(cfg.Daemon.ProjectionSurfaces)) } +func writeDaemonSnapshotSummary(out io.Writer, root string) { + snapshot, ok, err := daemon.NewFileSnapshotStore(daemon.StatusSnapshotPath(root, "")).Load() + if err != nil { + fmt.Fprintf(out, "Harness daemon snapshot: unavailable (%v)\n", err) + return + } + if !ok { + return + } + fmt.Fprintf(out, "Harness daemon snapshot: workers=%d started=%s\n", len(snapshot.Workers), formatDaemonTime(snapshot.StartedAt)) + names := make([]string, 0, len(snapshot.Workers)) + for name := range snapshot.Workers { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + worker := snapshot.Workers[name] + fmt.Fprintf(out, " - %s [%s]: %s", name, worker.Kind, worker.Status) + if worker.Message != "" { + fmt.Fprintf(out, " (%s)", worker.Message) + } + if worker.Error != "" { + fmt.Fprintf(out, " error=%s", worker.Error) + } + if !worker.UpdatedAt.IsZero() { + fmt.Fprintf(out, " updated=%s", formatDaemonTime(worker.UpdatedAt)) + } + fmt.Fprintln(out) + } +} + +func formatDaemonTime(ts time.Time) string { + if ts.IsZero() { + return "unknown" + } + return ts.UTC().Format(time.RFC3339) +} + func daemonProjectRoot() string { if daemonRoot == "" { return "." diff --git a/harness/internal/daemon/snapshot_store.go b/harness/internal/daemon/snapshot_store.go new file mode 100644 index 00000000..9d0ab6b2 --- /dev/null +++ b/harness/internal/daemon/snapshot_store.go @@ -0,0 +1,76 @@ +package daemon + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" +) + +const DefaultStatusSnapshotRelPath = ".mnemon/harness/daemon/status.json" + +type FileSnapshotStore struct { + path string +} + +func StatusSnapshotPath(root, explicit string) string { + if strings.TrimSpace(explicit) != "" { + return strings.TrimSpace(explicit) + } + root = strings.TrimSpace(root) + if root == "" { + root = "." + } + return filepath.Join(root, DefaultStatusSnapshotRelPath) +} + +func NewFileSnapshotStore(path string) *FileSnapshotStore { + return &FileSnapshotStore{path: strings.TrimSpace(path)} +} + +func (s *FileSnapshotStore) Load() (Snapshot, bool, error) { + if s == nil || strings.TrimSpace(s.path) == "" { + return Snapshot{}, false, fmt.Errorf("daemon status snapshot path is required") + } + data, err := os.ReadFile(s.path) + if os.IsNotExist(err) { + return Snapshot{}, false, nil + } + if err != nil { + return Snapshot{}, false, err + } + if len(strings.TrimSpace(string(data))) == 0 { + return Snapshot{}, false, nil + } + var snapshot Snapshot + if err := json.Unmarshal(data, &snapshot); err != nil { + return Snapshot{}, false, fmt.Errorf("parse daemon status snapshot: %w", err) + } + if snapshot.Workers == nil { + snapshot.Workers = map[string]WorkerSnapshot{} + } + return snapshot, true, nil +} + +func (s *FileSnapshotStore) Save(snapshot Snapshot) error { + if s == nil || strings.TrimSpace(s.path) == "" { + return fmt.Errorf("daemon status snapshot path is required") + } + if snapshot.Workers == nil { + snapshot.Workers = map[string]WorkerSnapshot{} + } + if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(snapshot, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + tmp := s.path + ".tmp" + if err := os.WriteFile(tmp, data, 0o600); err != nil { + return err + } + return os.Rename(tmp, s.path) +} diff --git a/harness/internal/daemon/snapshot_store_test.go b/harness/internal/daemon/snapshot_store_test.go new file mode 100644 index 00000000..a4e0cda8 --- /dev/null +++ b/harness/internal/daemon/snapshot_store_test.go @@ -0,0 +1,61 @@ +package daemon + +import ( + "path/filepath" + "testing" + "time" +) + +func TestFileSnapshotStorePersistsDaemonStatus(t *testing.T) { + root := t.TempDir() + path := StatusSnapshotPath(root, "") + started := time.Unix(100, 0).UTC() + updated := started.Add(time.Minute) + store := NewFileSnapshotStore(path) + if snapshot, ok, err := store.Load(); err != nil || ok || len(snapshot.Workers) != 0 { + t.Fatalf("initial load = snapshot=%+v ok=%v err=%v", snapshot, ok, err) + } + if err := store.Save(Snapshot{ + StartedAt: started, + Workers: map[string]WorkerSnapshot{ + "multica-watch": { + Kind: WorkerInteraction, + Status: "idle", + Message: "cursor=7", + StartedAt: started, + UpdatedAt: updated, + }, + }, + }); err != nil { + t.Fatal(err) + } + + resumed := NewFileSnapshotStore(path) + snapshot, ok, err := resumed.Load() + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("saved snapshot should load") + } + worker := snapshot.Workers["multica-watch"] + if !snapshot.StartedAt.Equal(started) || worker.Kind != WorkerInteraction || worker.Status != "idle" || worker.Message != "cursor=7" || !worker.UpdatedAt.Equal(updated) { + t.Fatalf("loaded snapshot = %+v", snapshot) + } +} + +func TestStatusSnapshotPathHonorsExplicitPath(t *testing.T) { + explicit := filepath.Join(t.TempDir(), "status.json") + if got := StatusSnapshotPath("/ignored", explicit); got != explicit { + t.Fatalf("StatusSnapshotPath explicit = %q, want %q", got, explicit) + } +} + +func TestFileSnapshotStoreRequiresPath(t *testing.T) { + if _, _, err := NewFileSnapshotStore(" ").Load(); err == nil { + t.Fatal("expected load to reject empty path") + } + if err := NewFileSnapshotStore(" ").Save(Snapshot{}); err == nil { + t.Fatal("expected save to reject empty path") + } +} From 925a0c318d3a4f0225d1dee8876ae5d6433ab73a Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:00:41 +0800 Subject: [PATCH 029/117] feat: add daemon work ledger state Add a file-backed daemon work ledger keyed by work kind and target so projection and wake workers can record idempotent progress across restarts. The ledger preserves created timestamps, updates status in place, and exposes filtered records for worker status or retry policy. Validation: go test -count=1 ./harness/internal/daemon; go test -count=1 ./harness/... --- harness/internal/daemon/work_ledger.go | 195 ++++++++++++++++++++ harness/internal/daemon/work_ledger_test.go | 87 +++++++++ 2 files changed, 282 insertions(+) create mode 100644 harness/internal/daemon/work_ledger.go create mode 100644 harness/internal/daemon/work_ledger_test.go diff --git a/harness/internal/daemon/work_ledger.go b/harness/internal/daemon/work_ledger.go new file mode 100644 index 00000000..7f7f2fc8 --- /dev/null +++ b/harness/internal/daemon/work_ledger.go @@ -0,0 +1,195 @@ +package daemon + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" +) + +const ( + WorkKindProjection = "projection" + WorkKindWake = "wake" +) + +type WorkLedgerRecord struct { + Kind string `json:"kind"` + Key string `json:"key"` + Status string `json:"status"` + Attempts int `json:"attempts,omitempty"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type FileWorkLedger struct { + path string + now Clock + + mu sync.Mutex + loaded bool + records map[string]WorkLedgerRecord +} + +func NewFileWorkLedger(path string, now Clock) *FileWorkLedger { + if now == nil { + now = func() time.Time { return time.Now().UTC() } + } + return &FileWorkLedger{path: strings.TrimSpace(path), now: now, records: map[string]WorkLedgerRecord{}} +} + +func (l *FileWorkLedger) Seen(kind, key string) (bool, error) { + _, ok, err := l.Load(kind, key) + return ok, err +} + +func (l *FileWorkLedger) Load(kind, key string) (WorkLedgerRecord, bool, error) { + kind, key, err := cleanWorkLedgerKey(kind, key) + if err != nil { + return WorkLedgerRecord{}, false, err + } + l.mu.Lock() + defer l.mu.Unlock() + if err := l.loadLocked(); err != nil { + return WorkLedgerRecord{}, false, err + } + record, ok := l.records[workLedgerMapKey(kind, key)] + return record, ok, nil +} + +func (l *FileWorkLedger) Records(kind string) ([]WorkLedgerRecord, error) { + kind = strings.TrimSpace(kind) + l.mu.Lock() + defer l.mu.Unlock() + if err := l.loadLocked(); err != nil { + return nil, err + } + out := make([]WorkLedgerRecord, 0, len(l.records)) + for _, record := range l.records { + if kind == "" || record.Kind == kind { + out = append(out, record) + } + } + sortWorkLedgerRecords(out) + return out, nil +} + +func (l *FileWorkLedger) Record(record WorkLedgerRecord) error { + kind, key, err := cleanWorkLedgerKey(record.Kind, record.Key) + if err != nil { + return err + } + now := l.now() + createdAtProvided := !record.CreatedAt.IsZero() + record.Kind = kind + record.Key = key + record.Status = strings.TrimSpace(record.Status) + if record.Status == "" { + record.Status = "recorded" + } + if record.CreatedAt.IsZero() { + record.CreatedAt = now + } + if record.UpdatedAt.IsZero() { + record.UpdatedAt = now + } + l.mu.Lock() + defer l.mu.Unlock() + if err := l.loadLocked(); err != nil { + return err + } + mapKey := workLedgerMapKey(kind, key) + if existing, ok := l.records[mapKey]; ok && !createdAtProvided { + record.CreatedAt = existing.CreatedAt + } + l.records[mapKey] = record + return l.writeLocked() +} + +func (l *FileWorkLedger) loadLocked() error { + if l.loaded { + return nil + } + l.loaded = true + if strings.TrimSpace(l.path) == "" { + return fmt.Errorf("daemon work ledger path is required") + } + data, err := os.ReadFile(l.path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + if len(strings.TrimSpace(string(data))) == 0 { + return nil + } + var records []WorkLedgerRecord + if err := json.Unmarshal(data, &records); err != nil { + return fmt.Errorf("parse daemon work ledger: %w", err) + } + for _, record := range records { + kind, key, err := cleanWorkLedgerKey(record.Kind, record.Key) + if err != nil { + continue + } + record.Kind = kind + record.Key = key + l.records[workLedgerMapKey(kind, key)] = record + } + return nil +} + +func (l *FileWorkLedger) writeLocked() error { + if strings.TrimSpace(l.path) == "" { + return fmt.Errorf("daemon work ledger path is required") + } + if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { + return err + } + records := make([]WorkLedgerRecord, 0, len(l.records)) + for _, record := range l.records { + records = append(records, record) + } + sortWorkLedgerRecords(records) + data, err := json.MarshalIndent(records, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + tmp := l.path + ".tmp" + if err := os.WriteFile(tmp, data, 0o600); err != nil { + return err + } + return os.Rename(tmp, l.path) +} + +func cleanWorkLedgerKey(kind, key string) (string, string, error) { + kind = strings.TrimSpace(kind) + key = strings.TrimSpace(key) + if kind == "" { + return "", "", fmt.Errorf("daemon work ledger kind is required") + } + if key == "" { + return "", "", fmt.Errorf("daemon work ledger key is required") + } + return kind, key, nil +} + +func workLedgerMapKey(kind, key string) string { + return kind + "\x00" + key +} + +func sortWorkLedgerRecords(records []WorkLedgerRecord) { + sort.Slice(records, func(i, j int) bool { + if records[i].Kind == records[j].Kind { + return records[i].Key < records[j].Key + } + return records[i].Kind < records[j].Kind + }) +} diff --git a/harness/internal/daemon/work_ledger_test.go b/harness/internal/daemon/work_ledger_test.go new file mode 100644 index 00000000..593bc2e8 --- /dev/null +++ b/harness/internal/daemon/work_ledger_test.go @@ -0,0 +1,87 @@ +package daemon + +import ( + "path/filepath" + "testing" + "time" +) + +func TestFileWorkLedgerPersistsProjectionAndWakeRecords(t *testing.T) { + now := time.Unix(100, 0).UTC() + path := filepath.Join(t.TempDir(), "daemon", "work-ledger.json") + ledger := NewFileWorkLedger(path, func() time.Time { return now }) + if seen, err := ledger.Seen(WorkKindProjection, "multica:comment:root-1"); err != nil || seen { + t.Fatalf("initial seen = %v err=%v", seen, err) + } + if err := ledger.Record(WorkLedgerRecord{ + Kind: WorkKindProjection, + Key: "multica:comment:root-1", + Status: "completed", + Message: "comment=comment-1", + }); err != nil { + t.Fatal(err) + } + if err := ledger.Record(WorkLedgerRecord{ + Kind: WorkKindWake, + Key: "managed:assignment:asg-1", + Status: "failed", + Attempts: 2, + Error: "turn timeout", + }); err != nil { + t.Fatal(err) + } + + resumed := NewFileWorkLedger(path, func() time.Time { return now.Add(time.Hour) }) + projection, ok, err := resumed.Load(WorkKindProjection, "multica:comment:root-1") + if err != nil { + t.Fatal(err) + } + if !ok || projection.Status != "completed" || projection.Message != "comment=comment-1" || !projection.CreatedAt.Equal(now) || !projection.UpdatedAt.Equal(now) { + t.Fatalf("projection record = %+v ok=%v", projection, ok) + } + wake, ok, err := resumed.Load(WorkKindWake, "managed:assignment:asg-1") + if err != nil { + t.Fatal(err) + } + if !ok || wake.Status != "failed" || wake.Attempts != 2 || wake.Error != "turn timeout" { + t.Fatalf("wake record = %+v ok=%v", wake, ok) + } +} + +func TestFileWorkLedgerDedupesByKindAndKey(t *testing.T) { + now := time.Unix(100, 0).UTC() + later := now.Add(time.Hour) + path := filepath.Join(t.TempDir(), "work-ledger.json") + clock := now + ledger := NewFileWorkLedger(path, func() time.Time { return clock }) + if err := ledger.Record(WorkLedgerRecord{Kind: WorkKindProjection, Key: "target-1", Status: "started"}); err != nil { + t.Fatal(err) + } + clock = later + if err := ledger.Record(WorkLedgerRecord{Kind: WorkKindProjection, Key: "target-1", Status: "completed"}); err != nil { + t.Fatal(err) + } + records, err := NewFileWorkLedger(path, nil).Records(WorkKindProjection) + if err != nil { + t.Fatal(err) + } + if len(records) != 1 { + t.Fatalf("records = %+v, want one deduped record", records) + } + if records[0].Status != "completed" || !records[0].CreatedAt.Equal(now) || !records[0].UpdatedAt.Equal(later) { + t.Fatalf("deduped record = %+v", records[0]) + } +} + +func TestFileWorkLedgerValidatesKindAndKey(t *testing.T) { + ledger := NewFileWorkLedger(filepath.Join(t.TempDir(), "work-ledger.json"), nil) + if err := ledger.Record(WorkLedgerRecord{Kind: " ", Key: "target"}); err == nil { + t.Fatal("expected record to reject empty kind") + } + if err := ledger.Record(WorkLedgerRecord{Kind: WorkKindWake, Key: " "}); err == nil { + t.Fatal("expected record to reject empty key") + } + if _, _, err := ledger.Load(" ", "target"); err == nil { + t.Fatal("expected load to reject empty kind") + } +} From 885480246a5256fccf0d95b78ca82db9a0640637 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:03:20 +0800 Subject: [PATCH 030/117] docs: describe harness as event-system surface Update mnemon-harness top-level help to describe Mnemon as an event-driven collaboration substrate and Teamwork as a profile on top of it. The help tests now assert that terminology while continuing to guard against internal command leaks. Validation: go test -count=1 ./harness/cmd/mnemon-harness; go test -count=1 ./harness/... --- harness/cmd/mnemon-harness/root.go | 6 +++--- harness/cmd/mnemon-harness/root_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/harness/cmd/mnemon-harness/root.go b/harness/cmd/mnemon-harness/root.go index 7f6e99b3..ac1e1041 100644 --- a/harness/cmd/mnemon-harness/root.go +++ b/harness/cmd/mnemon-harness/root.go @@ -12,10 +12,10 @@ var version = "dev" var rootCmd = &cobra.Command{ Use: "mnemon-harness", Version: version, - Short: "Mnemon Agent Integration setup", + Short: "Experimental Mnemon event-system harness", CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, - Long: "Install Agent Integration for standard events, connect it to Local Mnemon, " + - "and keep Remote Workspace sync as a background concern.", + Long: "Configure the experimental Mnemon event-driven collaboration substrate: Agent Integration, " + + "Local Mnemon, external connections, and daemon workers. Teamwork is a profile on top of this substrate.", } // Command groups are help-only: they change how `--help` lists verbs, never a diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index 0939dc89..42a07f48 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -22,7 +22,7 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help returned error: %v", err) } got := out.String() - for _, want := range []string{"Agent Integration", "Local Mnemon", "Remote Workspace", "standard events", "setup", "local", "config", "daemon", "agent", "connect"} { + for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "local", "config", "daemon", "agent", "connect"} { if !strings.Contains(got, want) { t.Fatalf("expected root help to contain %q:\n%s", want, got) } From 0285f787bc8515e16b88e1799369152260db7887 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:07:36 +0800 Subject: [PATCH 031/117] feat: add harness doctor command Add a read-only mnemon-harness doctor command that reports product config, participants, daemon roles, connection state, Local Mnemon config, and daemon snapshot readiness. Root help now exposes doctor as part of the experimental product surface. Validation: go test -count=1 ./harness/cmd/mnemon-harness; go test -count=1 ./harness/... --- harness/cmd/mnemon-harness/doctor.go | 97 +++++++++++++++++++++++ harness/cmd/mnemon-harness/doctor_test.go | 83 +++++++++++++++++++ harness/cmd/mnemon-harness/root_test.go | 2 +- 3 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 harness/cmd/mnemon-harness/doctor.go create mode 100644 harness/cmd/mnemon-harness/doctor_test.go diff --git a/harness/cmd/mnemon-harness/doctor.go b/harness/cmd/mnemon-harness/doctor.go new file mode 100644 index 00000000..84b47a4c --- /dev/null +++ b/harness/cmd/mnemon-harness/doctor.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/mnemon-dev/mnemon/harness/internal/app" + "github.com/mnemon-dev/mnemon/harness/internal/daemon" + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" + "github.com/spf13/cobra" +) + +var doctorRoot string + +var doctorCmd = &cobra.Command{ + Use: "doctor", + Short: "Check harness event-system readiness", + RunE: runDoctor, +} + +func init() { + doctorCmd.Flags().StringVar(&doctorRoot, "root", ".", "project root") + doctorCmd.GroupID = groupSpine + rootCmd.AddCommand(doctorCmd) +} + +func runDoctor(cmd *cobra.Command, args []string) error { + root := strings.TrimSpace(doctorRoot) + if root == "" { + root = "." + } + cfg, cfgStatus, cfgDetail := doctorProductConfig(root) + fmt.Fprintln(cmd.OutOrStdout(), "Harness doctor") + fmt.Fprintf(cmd.OutOrStdout(), "- Product config: %s", cfgStatus) + if cfgDetail != "" { + fmt.Fprintf(cmd.OutOrStdout(), " (%s)", cfgDetail) + } + fmt.Fprintln(cmd.OutOrStdout()) + if cfgStatus == "configured" || cfgStatus == "legacy bridge" { + fmt.Fprintf(cmd.OutOrStdout(), "- Participants: %d\n", len(cfg.Participants)) + fmt.Fprintf(cmd.OutOrStdout(), "- Daemon roles: watchers=%d drive=%d surfaces=%d\n", len(cfg.Daemon.InteractionWatchers), len(cfg.Daemon.DriveSources), len(cfg.Daemon.ProjectionSurfaces)) + fmt.Fprintf(cmd.OutOrStdout(), "- Connections: %s\n", doctorConnections(cfg)) + } + fmt.Fprintf(cmd.OutOrStdout(), "- Local Mnemon config: %s\n", doctorLocalConfig(root)) + fmt.Fprintf(cmd.OutOrStdout(), "- Daemon snapshot: %s\n", doctorDaemonSnapshot(root)) + return nil +} + +func doctorProductConfig(root string) (productconfig.Config, string, string) { + cfg, err := productconfig.Load(productconfig.DefaultPath(root, "")) + if err == nil { + return cfg, "configured", "" + } + legacy, found, legacyErr := productconfig.FromLegacy(root) + if legacyErr != nil { + return productconfig.Config{}, "invalid", legacyErr.Error() + } + if found { + return legacy, "legacy bridge", "" + } + return productconfig.Config{}, "missing", err.Error() +} + +func doctorConnections(cfg productconfig.Config) string { + var out []string + if cfg.Connections.Multica.Enabled { + out = append(out, "multica") + } + if cfg.Connections.GitHub.Enabled { + out = append(out, "github") + } + if cfg.Connections.Mnemonhub.Enabled { + out = append(out, "mnemonhub") + } + if len(out) == 0 { + return "none" + } + return strings.Join(out, ",") +} + +func doctorLocalConfig(root string) string { + if _, err := app.ReadLocalConfig(root); err != nil { + return "missing" + } + return "configured" +} + +func doctorDaemonSnapshot(root string) string { + snapshot, ok, err := daemon.NewFileSnapshotStore(daemon.StatusSnapshotPath(root, "")).Load() + if err != nil { + return "invalid: " + err.Error() + } + if !ok { + return "missing" + } + return fmt.Sprintf("workers=%d", len(snapshot.Workers)) +} diff --git a/harness/cmd/mnemon-harness/doctor_test.go b/harness/cmd/mnemon-harness/doctor_test.go new file mode 100644 index 00000000..a653f77d --- /dev/null +++ b/harness/cmd/mnemon-harness/doctor_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "strings" + "testing" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/daemon" + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" +) + +func TestDoctorReportsMissingConfigurationWithoutMutating(t *testing.T) { + root := t.TempDir() + oldRoot := doctorRoot + doctorRoot = root + t.Cleanup(func() { doctorRoot = oldRoot }) + + cmd, out := testCommand() + if err := runDoctor(cmd, nil); err != nil { + t.Fatal(err) + } + got := out.String() + for _, want := range []string{ + "Harness doctor", + "- Product config: missing", + "- Local Mnemon config: missing", + "- Daemon snapshot: missing", + } { + if !strings.Contains(got, want) { + t.Fatalf("doctor output missing %q:\n%s", want, got) + } + } +} + +func TestDoctorReportsConfiguredProductSurface(t *testing.T) { + root := t.TempDir() + cfg := productconfig.Default() + cfg.Connections.Multica = productconfig.MulticaConnection{Enabled: true, Workspace: "ws-multica", RuntimeBinary: "mnemon-multica-runtime"} + cfg.Connections.GitHub = productconfig.GitHubConnection{Enabled: true, Repo: "mnemon-dev/mnemon-teamwork-example"} + cfg.Participants = []productconfig.Participant{{ + Principal: "planner@team", + HostRuntime: productconfig.HostRuntime{ + Kind: productconfig.RuntimeKindCodex, + Mode: productconfig.RuntimeModeManaged, + }, + }} + cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMultica} + cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} + cfg.Daemon.ProjectionSurfaces = []string{productconfig.ConnectionMultica} + if err := productconfig.Save(productconfig.DefaultPath(root, ""), cfg); err != nil { + t.Fatal(err) + } + if err := daemon.NewFileSnapshotStore(daemon.StatusSnapshotPath(root, "")).Save(daemon.Snapshot{ + StartedAt: time.Date(2026, 6, 29, 9, 0, 0, 0, time.UTC), + Workers: map[string]daemon.WorkerSnapshot{ + "multica-watch": {Kind: daemon.WorkerInteraction, Status: "idle"}, + "managed-drive": {Kind: daemon.WorkerDrive, Status: "idle"}, + }, + }); err != nil { + t.Fatal(err) + } + oldRoot := doctorRoot + doctorRoot = root + t.Cleanup(func() { doctorRoot = oldRoot }) + + cmd, out := testCommand() + if err := runDoctor(cmd, nil); err != nil { + t.Fatal(err) + } + got := out.String() + for _, want := range []string{ + "- Product config: configured", + "- Participants: 1", + "- Daemon roles: watchers=1 drive=1 surfaces=1", + "- Connections: multica,github", + "- Local Mnemon config: missing", + "- Daemon snapshot: workers=2", + } { + if !strings.Contains(got, want) { + t.Fatalf("doctor output missing %q:\n%s", want, got) + } + } +} diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index 42a07f48..ac7642d3 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -22,7 +22,7 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help returned error: %v", err) } got := out.String() - for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "local", "config", "daemon", "agent", "connect"} { + for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "local", "config", "daemon", "doctor", "agent", "connect"} { if !strings.Contains(got, want) { t.Fatalf("expected root help to contain %q:\n%s", want, got) } From acf4f04495b8de4c6b8aa970f449896e3f8b8cd3 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:12:17 +0800 Subject: [PATCH 032/117] feat: add harness session surface Add a file-backed harness session store and expose mnemon-harness session start/attach for explicit session records and external attachments. The session package is classified under the product/config axis so coreguard keeps the new concept owned by the R2 product surface. Validation: go test -count=1 ./harness/internal/coreguard ./harness/internal/session ./harness/cmd/mnemon-harness; go test -count=1 ./harness/... --- harness/cmd/mnemon-harness/root_test.go | 5 +- harness/cmd/mnemon-harness/session.go | 125 ++++++++++++++ harness/cmd/mnemon-harness/session_test.go | 124 ++++++++++++++ harness/internal/coreguard/mainaxis_test.go | 1 + harness/internal/session/store.go | 172 ++++++++++++++++++++ harness/internal/session/store_test.go | 79 +++++++++ 6 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 harness/cmd/mnemon-harness/session.go create mode 100644 harness/cmd/mnemon-harness/session_test.go create mode 100644 harness/internal/session/store.go create mode 100644 harness/internal/session/store_test.go diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index ac7642d3..71bf5c69 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -22,7 +22,7 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help returned error: %v", err) } got := out.String() - for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "local", "config", "daemon", "doctor", "agent", "connect"} { + for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "local", "config", "daemon", "doctor", "session", "agent", "connect"} { if !strings.Contains(got, want) { t.Fatalf("expected root help to contain %q:\n%s", want, got) } @@ -57,6 +57,9 @@ func TestProductHelpDoesNotExposeInternalVocabulary(t *testing.T) { {"agent", "add", "--help"}, {"connect", "--help"}, {"connect", "multica", "--help"}, + {"session", "--help"}, + {"session", "start", "--help"}, + {"session", "attach", "--help"}, {"multica", "--help"}, } { got := executeRootForHelp(t, args...) diff --git a/harness/cmd/mnemon-harness/session.go b/harness/cmd/mnemon-harness/session.go new file mode 100644 index 00000000..bb731a4e --- /dev/null +++ b/harness/cmd/mnemon-harness/session.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" + sessionstore "github.com/mnemon-dev/mnemon/harness/internal/session" + "github.com/spf13/cobra" +) + +var ( + sessionRoot string + sessionStartID string + sessionStartTitle string + sessionStartCarrier string + sessionStartDupPolicy string + sessionAttachID string + sessionAttachSurface string + sessionAttachExternal string + sessionAttachSetPrimary bool +) + +var sessionCmd = &cobra.Command{ + Use: "session", + Short: "Start and attach harness sessions", +} + +var sessionStartCmd = &cobra.Command{ + Use: "start", + Short: "Start a harness session record", + RunE: runSessionStart, +} + +var sessionAttachCmd = &cobra.Command{ + Use: "attach", + Short: "Attach an external reference to a harness session", + RunE: runSessionAttach, +} + +func init() { + sessionCmd.PersistentFlags().StringVar(&sessionRoot, "root", ".", "project root") + sessionStartCmd.Flags().StringVar(&sessionStartID, "id", "", "session id; generated when empty") + sessionStartCmd.Flags().StringVar(&sessionStartTitle, "title", "", "session title") + sessionStartCmd.Flags().StringVar(&sessionStartCarrier, "primary-carrier", "", "primary activation carrier") + sessionStartCmd.Flags().StringVar(&sessionStartDupPolicy, "duplicate-activation-policy", "", "duplicate activation policy") + sessionAttachCmd.Flags().StringVar(&sessionAttachID, "id", "", "session id") + sessionAttachCmd.Flags().StringVar(&sessionAttachSurface, "surface", "", "external surface name") + sessionAttachCmd.Flags().StringVar(&sessionAttachExternal, "external-ref", "", "external reference to attach") + sessionAttachCmd.Flags().BoolVar(&sessionAttachSetPrimary, "primary", false, "make this attachment the primary activation carrier") + sessionCmd.AddCommand(sessionStartCmd, sessionAttachCmd) + sessionCmd.GroupID = groupSpine + rootCmd.AddCommand(sessionCmd) +} + +func runSessionStart(cmd *cobra.Command, args []string) error { + root := sessionProjectRoot() + cfg := sessionConfigDefaults(root) + id := strings.TrimSpace(sessionStartID) + if id == "" { + id = "session-" + time.Now().UTC().Format("20060102T150405Z") + } + carrier := firstSessionValue(sessionStartCarrier, cfg.Sessions.PrimaryActivationCarrier, "local") + dupPolicy := firstSessionValue(sessionStartDupPolicy, cfg.Sessions.DuplicateActivationPolicy, productconfig.DuplicateActivationSuppress) + record, err := sessionstore.NewFileStore(sessionstore.DefaultDir(root, ""), nil).Start(sessionstore.Record{ + ID: id, + Title: sessionStartTitle, + PrimaryActivationCarrier: carrier, + DuplicateActivationPolicy: dupPolicy, + }) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Session: %s\n", record.ID) + fmt.Fprintf(cmd.OutOrStdout(), "Primary activation carrier: %s\n", record.PrimaryActivationCarrier) + fmt.Fprintf(cmd.OutOrStdout(), "Duplicate activation policy: %s\n", record.DuplicateActivationPolicy) + return nil +} + +func runSessionAttach(cmd *cobra.Command, args []string) error { + root := sessionProjectRoot() + if strings.TrimSpace(sessionAttachID) == "" { + return fmt.Errorf("--id is required") + } + record, err := sessionstore.NewFileStore(sessionstore.DefaultDir(root, ""), nil).Attach(sessionAttachID, sessionstore.Attachment{ + Surface: sessionAttachSurface, + ExternalRef: sessionAttachExternal, + Primary: sessionAttachSetPrimary, + }) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Session: %s\n", record.ID) + fmt.Fprintf(cmd.OutOrStdout(), "Attachments: %d\n", len(record.Attachments)) + fmt.Fprintf(cmd.OutOrStdout(), "Primary activation carrier: %s\n", record.PrimaryActivationCarrier) + return nil +} + +func sessionProjectRoot() string { + root := strings.TrimSpace(sessionRoot) + if root == "" { + return "." + } + return root +} + +func sessionConfigDefaults(root string) productconfig.Config { + if cfg, err := productconfig.Load(productconfig.DefaultPath(root, "")); err == nil { + return cfg + } + if cfg, found, err := productconfig.FromLegacy(root); err == nil && found { + return cfg + } + return productconfig.Default() +} + +func firstSessionValue(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} diff --git a/harness/cmd/mnemon-harness/session_test.go b/harness/cmd/mnemon-harness/session_test.go new file mode 100644 index 00000000..780e670f --- /dev/null +++ b/harness/cmd/mnemon-harness/session_test.go @@ -0,0 +1,124 @@ +package main + +import ( + "strings" + "testing" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" + sessionstore "github.com/mnemon-dev/mnemon/harness/internal/session" +) + +func TestSessionStartUsesProductConfigDefaults(t *testing.T) { + root := t.TempDir() + cfg := productconfig.Default() + cfg.Connections.Multica = productconfig.MulticaConnection{Enabled: true, Workspace: "ws-multica", RuntimeBinary: "mnemon-multica-runtime"} + cfg.Sessions.PrimaryActivationCarrier = productconfig.ConnectionMultica + if err := productconfig.Save(productconfig.DefaultPath(root, ""), cfg); err != nil { + t.Fatal(err) + } + restoreSessionFlags(t) + sessionRoot = root + sessionStartID = "release-readiness" + sessionStartTitle = "Release readiness" + + cmd, out := testCommand() + if err := runSessionStart(cmd, nil); err != nil { + t.Fatal(err) + } + got := out.String() + for _, want := range []string{ + "Session: release-readiness", + "Primary activation carrier: multica", + "Duplicate activation policy: suppress", + } { + if !strings.Contains(got, want) { + t.Fatalf("session start missing %q:\n%s", want, got) + } + } + record, err := sessionstore.NewFileStore(sessionstore.DefaultDir(root, ""), nil).Load("release-readiness") + if err != nil { + t.Fatal(err) + } + if record.Title != "Release readiness" || record.PrimaryActivationCarrier != productconfig.ConnectionMultica { + t.Fatalf("session record = %+v", record) + } +} + +func TestSessionAttachRecordsExternalReference(t *testing.T) { + root := t.TempDir() + store := sessionstore.NewFileStore(sessionstore.DefaultDir(root, ""), nil) + if _, err := store.Start(sessionstore.Record{ID: "release-readiness", PrimaryActivationCarrier: "local"}); err != nil { + t.Fatal(err) + } + restoreSessionFlags(t) + sessionRoot = root + sessionAttachID = "release-readiness" + sessionAttachSurface = productconfig.ConnectionMultica + sessionAttachExternal = "issue/root-1" + sessionAttachSetPrimary = true + + cmd, out := testCommand() + if err := runSessionAttach(cmd, nil); err != nil { + t.Fatal(err) + } + got := out.String() + for _, want := range []string{ + "Session: release-readiness", + "Attachments: 1", + "Primary activation carrier: multica", + } { + if !strings.Contains(got, want) { + t.Fatalf("session attach missing %q:\n%s", want, got) + } + } + record, err := store.Load("release-readiness") + if err != nil { + t.Fatal(err) + } + if len(record.Attachments) != 1 || record.Attachments[0].ExternalRef != "issue/root-1" || record.PrimaryActivationCarrier != productconfig.ConnectionMultica { + t.Fatalf("attached session record = %+v", record) + } +} + +func TestSessionAttachRequiresID(t *testing.T) { + restoreSessionFlags(t) + sessionRoot = t.TempDir() + sessionAttachSurface = productconfig.ConnectionMultica + sessionAttachExternal = "issue/root-1" + if err := runSessionAttach(mustTestCommand(t), nil); err == nil { + t.Fatal("expected attach to require --id") + } +} + +func restoreSessionFlags(t *testing.T) { + t.Helper() + oldRoot := sessionRoot + oldStartID := sessionStartID + oldStartTitle := sessionStartTitle + oldStartCarrier := sessionStartCarrier + oldStartDupPolicy := sessionStartDupPolicy + oldAttachID := sessionAttachID + oldAttachSurface := sessionAttachSurface + oldAttachExternal := sessionAttachExternal + oldAttachPrimary := sessionAttachSetPrimary + sessionRoot = "." + sessionStartID = "" + sessionStartTitle = "" + sessionStartCarrier = "" + sessionStartDupPolicy = "" + sessionAttachID = "" + sessionAttachSurface = "" + sessionAttachExternal = "" + sessionAttachSetPrimary = false + t.Cleanup(func() { + sessionRoot = oldRoot + sessionStartID = oldStartID + sessionStartTitle = oldStartTitle + sessionStartCarrier = oldStartCarrier + sessionStartDupPolicy = oldStartDupPolicy + sessionAttachID = oldAttachID + sessionAttachSurface = oldAttachSurface + sessionAttachExternal = oldAttachExternal + sessionAttachSetPrimary = oldAttachPrimary + }) +} diff --git a/harness/internal/coreguard/mainaxis_test.go b/harness/internal/coreguard/mainaxis_test.go index 2aed6ddc..163e556c 100644 --- a/harness/internal/coreguard/mainaxis_test.go +++ b/harness/internal/coreguard/mainaxis_test.go @@ -51,6 +51,7 @@ var packageMainAxisInventory = map[string]packageMainAxis{ "projection": {owner: ownerProjectionSurface, role: "accepted/derived state material prepared for external projection surfaces", target: "projection"}, "replay": {owner: ownerMnemond, role: "mnemond determinism verification", target: "mnemond/replay"}, "runtime": {owner: ownerMnemond, role: "local mnemond event runtime", target: "mnemond"}, + "session": {owner: ownerProductConfig, role: "harness session records and external attachment policy", target: "product/session"}, "ui": {owner: ownerMnemond, role: "read-only mnemond operator observability", target: "mnemond/observe"}, } diff --git a/harness/internal/session/store.go b/harness/internal/session/store.go new file mode 100644 index 00000000..f5bb2eef --- /dev/null +++ b/harness/internal/session/store.go @@ -0,0 +1,172 @@ +package session + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +const ( + SchemaVersion = 1 + DefaultRelDir = ".mnemon/harness/sessions" +) + +type Clock func() time.Time + +type Record struct { + SchemaVersion int `json:"schema_version"` + ID string `json:"id"` + Title string `json:"title,omitempty"` + PrimaryActivationCarrier string `json:"primary_activation_carrier,omitempty"` + DuplicateActivationPolicy string `json:"duplicate_activation_policy,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Attachment struct { + Surface string `json:"surface"` + ExternalRef string `json:"external_ref"` + Primary bool `json:"primary,omitempty"` + AttachedAt time.Time `json:"attached_at"` +} + +type FileStore struct { + dir string + now Clock +} + +func DefaultDir(root, explicit string) string { + if strings.TrimSpace(explicit) != "" { + return filepath.Clean(explicit) + } + root = strings.TrimSpace(root) + if root == "" { + root = "." + } + return filepath.Join(root, DefaultRelDir) +} + +func NewFileStore(dir string, now Clock) *FileStore { + if now == nil { + now = func() time.Time { return time.Now().UTC() } + } + return &FileStore{dir: strings.TrimSpace(dir), now: now} +} + +func (s *FileStore) Start(record Record) (Record, error) { + if err := validateID(record.ID); err != nil { + return Record{}, err + } + if strings.TrimSpace(s.dir) == "" { + return Record{}, fmt.Errorf("session store directory is required") + } + now := s.now() + record.SchemaVersion = SchemaVersion + record.ID = strings.TrimSpace(record.ID) + record.Title = strings.TrimSpace(record.Title) + record.PrimaryActivationCarrier = strings.TrimSpace(record.PrimaryActivationCarrier) + record.DuplicateActivationPolicy = strings.TrimSpace(record.DuplicateActivationPolicy) + if record.CreatedAt.IsZero() { + record.CreatedAt = now + } + record.UpdatedAt = now + if err := s.write(record); err != nil { + return Record{}, err + } + return record, nil +} + +func (s *FileStore) Load(id string) (Record, error) { + if err := validateID(id); err != nil { + return Record{}, err + } + data, err := os.ReadFile(s.path(id)) + if err != nil { + return Record{}, err + } + var record Record + if err := json.Unmarshal(data, &record); err != nil { + return Record{}, fmt.Errorf("parse session record: %w", err) + } + if record.SchemaVersion != SchemaVersion { + return Record{}, fmt.Errorf("session schema_version %d unsupported (want %d)", record.SchemaVersion, SchemaVersion) + } + if err := validateID(record.ID); err != nil { + return Record{}, err + } + return record, nil +} + +func (s *FileStore) Attach(id string, attachment Attachment) (Record, error) { + record, err := s.Load(id) + if err != nil { + return Record{}, err + } + attachment.Surface = strings.TrimSpace(attachment.Surface) + attachment.ExternalRef = strings.TrimSpace(attachment.ExternalRef) + if attachment.Surface == "" { + return Record{}, fmt.Errorf("session attachment surface is required") + } + if attachment.ExternalRef == "" { + return Record{}, fmt.Errorf("session attachment external ref is required") + } + now := s.now() + if attachment.AttachedAt.IsZero() { + attachment.AttachedAt = now + } + updated := false + for i := range record.Attachments { + if record.Attachments[i].Surface == attachment.Surface && record.Attachments[i].ExternalRef == attachment.ExternalRef { + record.Attachments[i] = attachment + updated = true + break + } + } + if !updated { + record.Attachments = append(record.Attachments, attachment) + } + if attachment.Primary { + record.PrimaryActivationCarrier = attachment.Surface + } + record.UpdatedAt = now + if err := s.write(record); err != nil { + return Record{}, err + } + return record, nil +} + +func (s *FileStore) write(record Record) error { + if err := os.MkdirAll(s.dir, 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(record, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + path := s.path(record.ID) + tmp := path + ".tmp" + if err := os.WriteFile(tmp, data, 0o600); err != nil { + return err + } + return os.Rename(tmp, path) +} + +func (s *FileStore) path(id string) string { + return filepath.Join(s.dir, strings.TrimSpace(id)+".json") +} + +func validateID(id string) error { + id = strings.TrimSpace(id) + if id == "" { + return fmt.Errorf("session id is required") + } + if strings.ContainsAny(id, `/\`) { + return fmt.Errorf("session id %q must not contain path separators", id) + } + return nil +} diff --git a/harness/internal/session/store_test.go b/harness/internal/session/store_test.go new file mode 100644 index 00000000..996e6ccb --- /dev/null +++ b/harness/internal/session/store_test.go @@ -0,0 +1,79 @@ +package session + +import ( + "path/filepath" + "testing" + "time" +) + +func TestFileStoreStartsAndLoadsSession(t *testing.T) { + now := time.Date(2026, 6, 29, 10, 0, 0, 0, time.UTC) + store := NewFileStore(DefaultDir(t.TempDir(), ""), func() time.Time { return now }) + record, err := store.Start(Record{ + ID: "release-readiness", + Title: "Release readiness", + PrimaryActivationCarrier: "multica", + DuplicateActivationPolicy: "suppress", + }) + if err != nil { + t.Fatal(err) + } + if record.SchemaVersion != SchemaVersion || record.ID != "release-readiness" || !record.CreatedAt.Equal(now) || !record.UpdatedAt.Equal(now) { + t.Fatalf("started record = %+v", record) + } + + loaded, err := store.Load("release-readiness") + if err != nil { + t.Fatal(err) + } + if loaded.Title != "Release readiness" || loaded.PrimaryActivationCarrier != "multica" { + t.Fatalf("loaded record = %+v", loaded) + } +} + +func TestFileStoreAttachDedupesAndUpdatesPrimaryCarrier(t *testing.T) { + now := time.Date(2026, 6, 29, 10, 0, 0, 0, time.UTC) + current := now + store := NewFileStore(DefaultDir(t.TempDir(), ""), func() time.Time { return current }) + if _, err := store.Start(Record{ID: "session-1", PrimaryActivationCarrier: "local"}); err != nil { + t.Fatal(err) + } + current = now.Add(time.Minute) + record, err := store.Attach("session-1", Attachment{Surface: "multica", ExternalRef: "issue/root-1", Primary: true}) + if err != nil { + t.Fatal(err) + } + current = now.Add(2 * time.Minute) + record, err = store.Attach("session-1", Attachment{Surface: "multica", ExternalRef: "issue/root-1", Primary: true}) + if err != nil { + t.Fatal(err) + } + if len(record.Attachments) != 1 { + t.Fatalf("attachments = %+v, want one deduped attachment", record.Attachments) + } + if record.PrimaryActivationCarrier != "multica" || !record.UpdatedAt.Equal(current) { + t.Fatalf("record after attach = %+v", record) + } + if !record.Attachments[0].AttachedAt.Equal(current) { + t.Fatalf("attachment timestamp = %s, want %s", record.Attachments[0].AttachedAt, current) + } +} + +func TestFileStoreValidatesSessionIDsAndAttachments(t *testing.T) { + store := NewFileStore(filepath.Join(t.TempDir(), "sessions"), nil) + if _, err := store.Start(Record{ID: " "}); err == nil { + t.Fatal("expected start to reject empty id") + } + if _, err := store.Start(Record{ID: "bad/id"}); err == nil { + t.Fatal("expected start to reject path separators") + } + if _, err := store.Start(Record{ID: "session-1"}); err != nil { + t.Fatal(err) + } + if _, err := store.Attach("session-1", Attachment{Surface: "", ExternalRef: "issue/root-1"}); err == nil { + t.Fatal("expected attach to reject empty surface") + } + if _, err := store.Attach("session-1", Attachment{Surface: "multica", ExternalRef: ""}); err == nil { + t.Fatal("expected attach to reject empty external ref") + } +} From dfe20d3f6492a350da62463e4228d70ff2a29562 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:15:11 +0800 Subject: [PATCH 033/117] docs: clarify daemon and hub service roles Update mnemond help to frame it as the Local Mnemon event node and mnemond agent run as a local drive source using the [mnemon:wake] sentinel. Update mnemon-hub help to frame it as the remote event exchange backend, with tests guarding those service boundaries. Validation: go test -count=1 ./harness/cmd/mnemond ./harness/cmd/mnemon-hub; go test -count=1 ./harness/... --- harness/cmd/mnemon-hub/main.go | 6 ++++++ harness/cmd/mnemon-hub/main_test.go | 22 ++++++++++++++++++++ harness/cmd/mnemond/agent.go | 6 ++++++ harness/cmd/mnemond/main.go | 6 ++++++ harness/cmd/mnemond/main_test.go | 31 +++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+) diff --git a/harness/cmd/mnemon-hub/main.go b/harness/cmd/mnemon-hub/main.go index 36bfbc33..b3864e5a 100644 --- a/harness/cmd/mnemon-hub/main.go +++ b/harness/cmd/mnemon-hub/main.go @@ -43,6 +43,12 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { tlsCert := fs.String("tls-cert", "", "TLS certificate file (TLS is served when --tls-cert and --tls-key are both set)") tlsKey := fs.String("tls-key", "", "TLS private key file") devSelfsigned := fs.String("dev-selfsigned", "", "generate a self-signed dev/e2e cert+key pair into this directory, print their paths, and exit") + fs.Usage = func() { + fmt.Fprintln(errw, "mnemon-hub is the remote event exchange backend: it handles authenticated replica push, pull, status, cursors, and tenant boundaries.") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Usage of mnemon-hub:") + fs.PrintDefaults() + } if err := fs.Parse(args); err != nil { return err } diff --git a/harness/cmd/mnemon-hub/main_test.go b/harness/cmd/mnemon-hub/main_test.go index a053b5bb..409ca018 100644 --- a/harness/cmd/mnemon-hub/main_test.go +++ b/harness/cmd/mnemon-hub/main_test.go @@ -7,6 +7,9 @@ import ( "crypto/tls" "encoding/hex" "encoding/json" + "errors" + "flag" + "io" "os" "path/filepath" "regexp" @@ -39,6 +42,25 @@ func writeToken(t *testing.T, dir, name, token string) { } } +func TestHelpDescribesRemoteExchangeBackend(t *testing.T) { + var errw bytes.Buffer + err := run(context.Background(), []string{"--help"}, io.Discard, &errw) + if !errors.Is(err, flag.ErrHelp) { + t.Fatalf("help error = %v, want flag.ErrHelp", err) + } + got := errw.String() + for _, want := range []string{"remote event exchange backend", "replica push", "pull", "status", "cursors", "tenant boundaries"} { + if !strings.Contains(got, want) { + t.Fatalf("mnemon-hub help missing %q:\n%s", want, got) + } + } + for _, blocked := range []string{"managed runtime", "Multica projection", "local drive source"} { + if strings.Contains(got, blocked) { + t.Fatalf("mnemon-hub help leaked non-exchange wording %q:\n%s", blocked, got) + } + } +} + const twoReplicaDoc = `{ "schema_version": 1, "replicas": [ diff --git a/harness/cmd/mnemond/agent.go b/harness/cmd/mnemond/agent.go index 93375342..4f7dfc89 100644 --- a/harness/cmd/mnemond/agent.go +++ b/harness/cmd/mnemond/agent.go @@ -45,6 +45,12 @@ func runAgentRun(ctx context.Context, args []string, out, errw io.Writer) error renderIntent := fs.String("render-intent", presentation.IntentTeamworkEvents, "render intent used for wake candidate derivation") surface := fs.String("surface", "hook", "render surface used for wake candidate derivation") turnTimeout := fs.Duration("turn-timeout", 5*time.Minute, "timeout for one managed agent turn") + fs.Usage = func() { + fmt.Fprintln(errw, "mnemond agent run is a local drive source: it renders wake candidates and sends only the [mnemon:wake] sentinel to the selected managed runtime.") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Usage of mnemond agent run:") + fs.PrintDefaults() + } if err := fs.Parse(args); err != nil { return err } diff --git a/harness/cmd/mnemond/main.go b/harness/cmd/mnemond/main.go index 9a2f2c98..df1fa368 100644 --- a/harness/cmd/mnemond/main.go +++ b/harness/cmd/mnemond/main.go @@ -86,6 +86,12 @@ func parseServe(args []string, errw io.Writer) (serveConfig, error) { allowNonLoopback := fs.Bool("allow-nonloopback", false, "explicitly allow listening on a non-loopback address (T1: loopback-only by default)") ignoreExternal := fs.Bool("ignore-external", false, "boot the embedded-only capability catalog, ignoring external packages under .mnemon/loops (each ignored package is named on stderr)") allowInsecureRemote := fs.Bool("allow-insecure-remote", false, "let the background sync worker use a plaintext http:// Remote Workspace endpoint with a non-loopback host (T2: fail-closed by default)") + fs.Usage = func() { + fmt.Fprintln(errw, "mnemond is the Local Mnemon event node: it serves local event API, admission, state, presentation, and drive candidates.") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Usage of mnemond:") + fs.PrintDefaults() + } if err := fs.Parse(args); err != nil { return serveConfig{}, err } diff --git a/harness/cmd/mnemond/main_test.go b/harness/cmd/mnemond/main_test.go index b61b7233..577e0b94 100644 --- a/harness/cmd/mnemond/main_test.go +++ b/harness/cmd/mnemond/main_test.go @@ -1,7 +1,10 @@ package main import ( + "bytes" "context" + "errors" + "flag" "io" "strings" "testing" @@ -41,3 +44,31 @@ func TestRunRefusesNonLoopbackAddr(t *testing.T) { t.Fatalf("non-loopback --addr must be refused (T1), got: %v", err) } } + +func TestHelpDescribesLocalEventNode(t *testing.T) { + var errw bytes.Buffer + err := run(context.Background(), []string{"--help"}, io.Discard, &errw) + if !errors.Is(err, flag.ErrHelp) { + t.Fatalf("help error = %v, want flag.ErrHelp", err) + } + got := errw.String() + for _, want := range []string{"Local Mnemon event node", "local event API", "admission", "state", "presentation", "drive candidates"} { + if !strings.Contains(got, want) { + t.Fatalf("mnemond help missing %q:\n%s", want, got) + } + } +} + +func TestAgentRunHelpFramesLocalDriveSource(t *testing.T) { + var errw bytes.Buffer + err := run(context.Background(), []string{"agent", "run", "--help"}, io.Discard, &errw) + if !errors.Is(err, flag.ErrHelp) { + t.Fatalf("agent run help error = %v, want flag.ErrHelp", err) + } + got := errw.String() + for _, want := range []string{"local drive source", "[mnemon:wake]", "managed runtime"} { + if !strings.Contains(got, want) { + t.Fatalf("agent run help missing %q:\n%s", want, got) + } + } +} From 1c0e6e22dcb2ffe69f15d8ce7a5360ba8180d164 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:24:31 +0800 Subject: [PATCH 034/117] fix: keep multica routing metadata out of visible text Tighten Multica assignment mailbox and runtime projection Markdown so visible issue text stays human-readable while routing, dedupe, assignment, and session facts remain in mnemon.* metadata and stable mention links. Validation: go test -count=1 ./harness/internal/surface/multica; go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- .../cmd/mnemon-multica-runtime/main_test.go | 38 ++++++++++------ .../internal/surface/multica/projection.go | 44 ++++++++++++------- .../surface/multica/projection_test.go | 39 +++++++++++----- 3 files changed, 82 insertions(+), 39 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index dace1e58..f5ad2888 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -296,7 +296,7 @@ esac } for _, want := range []string{ "Mnemon update: issue admitted", - "## Mnemon Intake", + "## Mnemon Runtime", "Issue: [TEA-7](mention://issue/iss-7)", "Principal: `planner@team`", "Managed wake: `completed`", @@ -552,14 +552,18 @@ esac comment := mustReadRuntimeTestFile(t, commentPath) for _, want := range []string{ "Mnemon update: issue admitted", - "## Mnemon Intake", - "Hub backend: `multica`", - "Session: `multica:session:root-1`", + "## Mnemon Runtime", + "Issue: [TEA-1](mention://issue/root-1)", } { if !strings.Contains(comment, want) { t.Fatalf("comment missing %q:\n%s", want, comment) } } + for _, blocked := range []string{"Hub backend:", "Session: `multica:session:root-1`"} { + if strings.Contains(comment, blocked) { + t.Fatalf("runtime comment must not expose machine field %q:\n%s", blocked, comment) + } + } if !strings.Contains(out.String(), "Mnemon ingest: recorded seq=21") { t.Fatalf("runtime output missing ingest evidence:\n%s", out.String()) } @@ -650,12 +654,15 @@ esac for _, want := range []string{ "Mnemon update: issue admitted", "Mnemond ingest: seq=29", - "Hub backend: `multica`", + "## Mnemon Runtime", } { if !strings.Contains(comment, want) { t.Fatalf("comment missing %q:\n%s", want, comment) } } + if strings.Contains(comment, "Hub backend:") { + t.Fatalf("runtime comment must not expose hub backend routing details:\n%s", comment) + } args := mustReadRuntimeTestFile(t, argsPath) if !strings.Contains(args, "issue metadata set iss-9 --key mnemon.hub_backend") || !strings.Contains(args, "issue comment add iss-9 --content-stdin --output json") { t.Fatalf("runtime should attempt metadata projection then continue to comment projection:\n%s", args) @@ -823,18 +830,18 @@ esac "check release notes against the public changelog", "Root issue: [TEA-10](mention://issue/root-2) - Coordinate docs release", "Assignee: `worker@team` (mnemon-worker)", - "Assignment: `asg-writer`", - "Post a `progress_digest`", - "Requested content: progress_digest with result or blocker", - "metadata under `mnemon.*`", + "Scope: check release notes", + "## Feedback", + "Expected feedback: result or blocker", + "Progress path: Mnemon runtime progress, result, or blocker feedback", } { if !strings.Contains(description, want) { t.Fatalf("description missing %q:\n%s", want, description) } } - for _, blocked := range []string{"mnemon.session_id", "mnemon.assignment_id", "mnemon.root_issue_id"} { + for _, blocked := range []string{"mnemon.", "Session:", "Assignment: `asg-writer`", "assignment_ref", "progress_digest"} { if strings.Contains(description, blocked) { - t.Fatalf("description must not expose machine metadata key %q:\n%s", blocked, description) + t.Fatalf("description must not expose machine field %q:\n%s", blocked, description) } } comment := mustReadRuntimeTestFile(t, commentPath) @@ -943,8 +950,8 @@ esac comment := mustReadRuntimeTestFile(t, commentPath) for _, want := range []string{ "Mnemon update: assignment mailbox correlated", - "## Assignment Mailbox", - "Assignment: `asg-1`", + "## Mnemon Runtime", + "Root issue: [root-1](mention://issue/root-1)", "mnemon:event=event-assignment-1", "Managed wake: `completed`", } { @@ -952,6 +959,11 @@ esac t.Fatalf("comment missing %q:\n%s", want, comment) } } + for _, blocked := range []string{"Assignment: `asg-1`", "Session: `multica:session:root-1`"} { + if strings.Contains(comment, blocked) { + t.Fatalf("assignment mailbox comment must not expose machine field %q:\n%s", blocked, comment) + } + } } func TestMulticaStatusForProgressIsRuleBased(t *testing.T) { diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index 851ceef1..bf65e23a 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -78,20 +78,20 @@ func AssignmentMailboxDescription(item AssignmentMailboxMaterial) string { writeBullet(&b, "Root issue", rootIssueReference(item)) writeBullet(&b, "Assignee", assigneeReference(item)) writeBullet(&b, "Scope", item.Scope) - writeBullet(&b, "Session", codeSpan(item.SessionID)) - writeBullet(&b, "Assignment", codeSpan(item.ID)) if rationale := strings.TrimSpace(item.Rationale); rationale != "" { b.WriteString("\n## Rationale\n\n") b.WriteString(rationale) b.WriteString("\n") } - b.WriteString("\n## Expected Feedback\n\n") - b.WriteString("Post a `progress_digest` that includes:\n") - writeBullet(&b, "`assignment_ref`", codeSpan(item.ID)) - if feedback := strings.TrimSpace(item.ExpectedFeedback); feedback != "" { - writeBullet(&b, "Requested content", feedback) + b.WriteString("\n## Feedback\n\n") + if feedback := visibleExpectedFeedback(item.ExpectedFeedback); feedback != "" { + writeBullet(&b, "Expected feedback", feedback) + } + if strings.TrimSpace(item.ExpectedFeedback) == "" { + b.WriteString("Report progress, results, or blockers through the Mnemon runtime path.\n") + } else { + writeBullet(&b, "Progress path", "Mnemon runtime progress, result, or blocker feedback") } - b.WriteString("\nRouting and dedupe fields live in Multica issue metadata under `mnemon.*`; this description is for human review.\n") return strings.TrimSpace(b.String()) } @@ -129,25 +129,20 @@ func ProgressCommentBody(item ProgressFeedbackMaterial) string { func RuntimeProjectionCommentBody(item RuntimeProjectionMaterial) string { var b strings.Builder if item.AssignmentMailbox { - b.WriteString("## Assignment Mailbox\n\n") + b.WriteString("## Mnemon Runtime\n\n") writeBullet(&b, "Status", firstNonEmptyString(item.Status, "correlated")) - writeBullet(&b, "Assignment", codeSpan(item.AssignmentID)) - writeBullet(&b, "Session", codeSpan(item.SessionID)) writeBullet(&b, "Root issue", IssueMention(firstNonEmptyString(item.RootIssueLabel, item.RootIssueID), item.RootIssueID)) } else { - b.WriteString("## Mnemon Intake\n\n") + b.WriteString("## Mnemon Runtime\n\n") writeBullet(&b, "Status", item.Status) writeBullet(&b, "Issue", IssueMention(firstNonEmptyString(item.IssueLabel, item.IssueID), item.IssueID)) writeBullet(&b, "Principal", codeSpan(item.Principal)) - writeBullet(&b, "Task", codeSpan(item.TaskID)) - writeBullet(&b, "Hub backend", codeSpan(item.HubBackend)) - writeBullet(&b, "Session", codeSpan(item.SessionID)) if item.HasIngestReceipt { writeBullet(&b, "Mnemond ingest", fmt.Sprintf("seq=%d duplicate=%v ticked=%v", item.IngestSeq, item.IngestDuplicate, item.IngestTicked)) } } if item.WakeStatus != "" || item.HubWriteStatus != "" { - b.WriteString("\n## Runtime Effects\n\n") + b.WriteString("\n## Effects\n\n") writeBullet(&b, "Managed wake", codeSpan(item.WakeStatus)) writeBullet(&b, "Managed turn", codeSpan(item.WakeTurnID)) if item.HubWriteStatus != "" { @@ -278,6 +273,23 @@ func assigneeReference(item AssignmentMailboxMaterial) string { return principal + " (" + display + ")" } +func visibleExpectedFeedback(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + lower := strings.ToLower(value) + for _, prefix := range []string{"progress_digest with ", "progress digest with "} { + if strings.HasPrefix(lower, prefix) { + return strings.TrimSpace(value[len(prefix):]) + } + } + if lower == "progress_digest" || lower == "progress digest" { + return "progress, result, or blocker" + } + return value +} + func writeBullet(b *strings.Builder, label, value string) { value = strings.TrimSpace(value) if value == "" { diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 01cc2057..ac3262d6 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -26,17 +26,19 @@ func TestAssignmentMailboxMaterial(t *testing.T) { "## Assignment", "Root issue: [TEA-1](mention://issue/root-1) - Validate runtime", "Assignee: `researcher@team` (mnemon-researcher)", - "Session: `session-1`", - "Assignment: `asg-1`", - "Post a `progress_digest`", - "metadata under `mnemon.*`", + "Scope: runtime/readiness", + "## Feedback", + "Expected feedback: Brief result.", + "Progress path: Mnemon runtime progress, result, or blocker feedback", } { if !strings.Contains(body, want) { t.Fatalf("description missing %q:\n%s", want, body) } } - if strings.Contains(body, "mnemon.session_id") { - t.Fatalf("visible description must not copy machine metadata keys:\n%s", body) + for _, blocked := range []string{"Session:", "Assignment: `asg-1`", "progress_digest", "assignment_ref", "mnemon."} { + if strings.Contains(body, blocked) { + t.Fatalf("visible description must not expose machine field %q:\n%s", blocked, body) + } } } @@ -51,6 +53,20 @@ func TestAssignmentMailboxTitleStripsRootLabelFromScope(t *testing.T) { } } +func TestAssignmentMailboxDescriptionNormalizesProtocolFeedback(t *testing.T) { + body := AssignmentMailboxDescription(AssignmentMailboxMaterial{ + ID: "asg-1", + Scope: "release validation", + ExpectedFeedback: "progress_digest with result or blocker", + }) + if !strings.Contains(body, "Expected feedback: result or blocker") { + t.Fatalf("description should expose human feedback wording:\n%s", body) + } + if strings.Contains(body, "progress_digest") { + t.Fatalf("description should keep protocol event type out of visible text:\n%s", body) + } +} + func TestProgressFeedbackMaterial(t *testing.T) { item := ProgressFeedbackMaterial{ AssignmentRef: "asg-1", @@ -105,7 +121,7 @@ func TestRuntimeProjectionCommentBodyForIntake(t *testing.T) { HubFeedbackComment: 1, }) for _, want := range []string{ - "## Mnemon Intake", + "## Mnemon Runtime", "Issue: [TEA-1](mention://issue/issue-1)", "Principal: `planner@team`", "Mnemond ingest: seq=42 duplicate=false ticked=true", @@ -130,10 +146,8 @@ func TestRuntimeProjectionCommentBodyForAssignmentMailbox(t *testing.T) { WakeStatus: "completed", }) for _, want := range []string{ - "## Assignment Mailbox", + "## Mnemon Runtime", "Status: correlated", - "Assignment: `asg-1`", - "Session: `multica:session:root-1`", "Root issue: [TEA-1](mention://issue/root-1)", "Managed wake: `completed`", } { @@ -141,6 +155,11 @@ func TestRuntimeProjectionCommentBodyForAssignmentMailbox(t *testing.T) { t.Fatalf("assignment runtime projection body missing %q:\n%s", want, body) } } + for _, blocked := range []string{"Assignment: `asg-1`", "Session: `multica:session:root-1`"} { + if strings.Contains(body, blocked) { + t.Fatalf("assignment runtime projection should keep %q in metadata, not comment text:\n%s", blocked, body) + } + } } func TestCanonicalIssueStatus(t *testing.T) { From 31c51239e4b3886f8c784222ed9585653c3270cb Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:45:20 +0800 Subject: [PATCH 035/117] refactor: add participant identity boundary Introduce harness/internal/participant for principal normalization, uniqueness checks, and upsert-by-principal behavior shared by product config and harness agent commands. Register the package in the R2 main-axis guard so participant identity is an explicit role boundary. Validation: go test -count=1 ./harness/internal/participant ./harness/internal/productconfig ./harness/cmd/mnemon-harness ./harness/internal/coreguard; go test -count=1 ./harness/... --- harness/cmd/mnemon-harness/agent.go | 13 ++--- harness/internal/coreguard/mainaxis_test.go | 2 + harness/internal/participant/doc.go | 6 ++ harness/internal/participant/participant.go | 57 +++++++++++++++++++ .../internal/participant/participant_test.go | 56 ++++++++++++++++++ harness/internal/productconfig/config.go | 54 ++++++++---------- 6 files changed, 151 insertions(+), 37 deletions(-) create mode 100644 harness/internal/participant/doc.go create mode 100644 harness/internal/participant/participant.go create mode 100644 harness/internal/participant/participant_test.go diff --git a/harness/cmd/mnemon-harness/agent.go b/harness/cmd/mnemon-harness/agent.go index 95c6faad..3d8e9405 100644 --- a/harness/cmd/mnemon-harness/agent.go +++ b/harness/cmd/mnemon-harness/agent.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + participantrole "github.com/mnemon-dev/mnemon/harness/internal/participant" "github.com/mnemon-dev/mnemon/harness/internal/productconfig" "github.com/spf13/cobra" ) @@ -52,7 +53,7 @@ func init() { } func runAgentAdd(cmd *cobra.Command, args []string) error { - principal := strings.TrimSpace(agentPrincipal) + principal := participantrole.NormalizePrincipal(agentPrincipal) if principal == "" { return fmt.Errorf("agent add requires --principal") } @@ -107,11 +108,7 @@ func runAgentList(cmd *cobra.Command, args []string) error { } func upsertProductParticipant(participants []productconfig.Participant, next productconfig.Participant) []productconfig.Participant { - for i := range participants { - if participants[i].Principal == next.Principal { - participants[i] = next - return participants - } - } - return append(participants, next) + return participantrole.UpsertByPrincipal(participants, next, func(participant productconfig.Participant) string { + return participant.Principal + }, nil) } diff --git a/harness/internal/coreguard/mainaxis_test.go b/harness/internal/coreguard/mainaxis_test.go index 163e556c..234a6d24 100644 --- a/harness/internal/coreguard/mainaxis_test.go +++ b/harness/internal/coreguard/mainaxis_test.go @@ -14,6 +14,7 @@ const ( ownerHostAgent mainAxisOwner = "hostagent" ownerMnemond mainAxisOwner = "mnemond" ownerMnemonhub mainAxisOwner = "mnemonhub" + ownerParticipant mainAxisOwner = "participant" ownerInteractionPoint mainAxisOwner = "interaction-point" ownerDriveSource mainAxisOwner = "drive-source" ownerProjectionSurface mainAxisOwner = "projection-surface" @@ -47,6 +48,7 @@ var packageMainAxisInventory = map[string]packageMainAxis{ "hostagent": {owner: ownerHostAgent, role: "hostagent setup and thin shims", target: "hostagent"}, "interaction": {owner: ownerInteractionPoint, role: "external stimulus material separated into rule/narrative/refs", target: "interaction/event-material"}, "mnemonhub": {owner: ownerMnemonhub, role: "remote accepted event exchange server and exchange mechanics", target: "mnemonhub"}, + "participant": {owner: ownerParticipant, role: "principal identity and participant list helpers shared by product config and adapters", target: "participant"}, "productconfig": {owner: ownerProductConfig, role: "harness product configuration for agents, connections, daemon workers, and surfaces", target: "product/config"}, "projection": {owner: ownerProjectionSurface, role: "accepted/derived state material prepared for external projection surfaces", target: "projection"}, "replay": {owner: ownerMnemond, role: "mnemond determinism verification", target: "mnemond/replay"}, diff --git a/harness/internal/participant/doc.go b/harness/internal/participant/doc.go new file mode 100644 index 00000000..11af6891 --- /dev/null +++ b/harness/internal/participant/doc.go @@ -0,0 +1,6 @@ +// Package participant owns lightweight participant identity helpers. +// +// It treats principal strings as stable participant keys for product config, +// adapters, and command surfaces. It does not own runtime execution, external +// projection, or mnemond admission semantics. +package participant diff --git a/harness/internal/participant/participant.go b/harness/internal/participant/participant.go new file mode 100644 index 00000000..b35d1c80 --- /dev/null +++ b/harness/internal/participant/participant.go @@ -0,0 +1,57 @@ +package participant + +import ( + "fmt" + "strings" +) + +// NormalizePrincipal returns the stable product-config key for a participant. +func NormalizePrincipal(value string) string { + return strings.TrimSpace(value) +} + +func RequirePrincipal(label, value string) (string, error) { + principal := NormalizePrincipal(value) + if principal == "" { + if strings.TrimSpace(label) == "" { + label = "participant" + } + return "", fmt.Errorf("%s principal is required", strings.TrimSpace(label)) + } + return principal, nil +} + +func ValidateUniquePrincipals[T any](items []T, label string, principalOf func(T) string) error { + label = strings.TrimSpace(label) + if label == "" { + label = "participant" + } + seen := map[string]bool{} + for _, item := range items { + principal := NormalizePrincipal(principalOf(item)) + if principal == "" { + return fmt.Errorf("%s principal is required", label) + } + if seen[principal] { + return fmt.Errorf("duplicate %s principal %q", label, principal) + } + seen[principal] = true + } + return nil +} + +func UpsertByPrincipal[T any](items []T, next T, principalOf func(T) string, merge func(existing, next T) T) []T { + nextPrincipal := NormalizePrincipal(principalOf(next)) + for i := range items { + if NormalizePrincipal(principalOf(items[i])) != nextPrincipal { + continue + } + if merge == nil { + items[i] = next + } else { + items[i] = merge(items[i], next) + } + return items + } + return append(items, next) +} diff --git a/harness/internal/participant/participant_test.go b/harness/internal/participant/participant_test.go new file mode 100644 index 00000000..f2eb6b80 --- /dev/null +++ b/harness/internal/participant/participant_test.go @@ -0,0 +1,56 @@ +package participant + +import ( + "strings" + "testing" +) + +type record struct { + Principal string + DisplayName string +} + +func TestRequirePrincipalTrimsAndRejectsEmpty(t *testing.T) { + principal, err := RequirePrincipal("participant", " planner@team ") + if err != nil { + t.Fatal(err) + } + if principal != "planner@team" { + t.Fatalf("principal = %q", principal) + } + if _, err := RequirePrincipal("multica registry participant", " "); err == nil || !strings.Contains(err.Error(), "multica registry participant principal is required") { + t.Fatalf("expected principal required error, got %v", err) + } +} + +func TestValidateUniquePrincipals(t *testing.T) { + items := []record{{Principal: "planner@team"}, {Principal: " reviewer@team "}} + if err := ValidateUniquePrincipals(items, "participant", func(item record) string { return item.Principal }); err != nil { + t.Fatal(err) + } + items = append(items, record{Principal: "planner@team"}) + if err := ValidateUniquePrincipals(items, "participant", func(item record) string { return item.Principal }); err == nil || !strings.Contains(err.Error(), "duplicate participant principal") { + t.Fatalf("expected duplicate participant error, got %v", err) + } +} + +func TestUpsertByPrincipalCanReplaceOrMerge(t *testing.T) { + items := []record{{Principal: "planner@team", DisplayName: "planner"}} + items = UpsertByPrincipal(items, record{Principal: " planner@team ", DisplayName: "lead"}, func(item record) string { + return item.Principal + }, nil) + if len(items) != 1 || items[0].DisplayName != "lead" { + t.Fatalf("replace upsert mismatch: %+v", items) + } + items = UpsertByPrincipal(items, record{Principal: "reviewer@team", DisplayName: "reviewer"}, func(item record) string { + return item.Principal + }, func(existing, next record) record { + if existing.DisplayName == "" { + existing.DisplayName = next.DisplayName + } + return existing + }) + if len(items) != 2 { + t.Fatalf("append upsert mismatch: %+v", items) + } +} diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go index 077955b0..473e2d3d 100644 --- a/harness/internal/productconfig/config.go +++ b/harness/internal/productconfig/config.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + participantrole "github.com/mnemon-dev/mnemon/harness/internal/participant" multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) @@ -155,16 +156,13 @@ func (cfg Config) Validate() error { if cfg.SchemaVersion != SchemaVersion { return fmt.Errorf("product config schema_version %d unsupported (want %d)", cfg.SchemaVersion, SchemaVersion) } - seen := map[string]bool{} + if err := participantrole.ValidateUniquePrincipals(cfg.Participants, "participant", func(participant Participant) string { + return participant.Principal + }); err != nil { + return err + } for _, participant := range cfg.Participants { - principal := strings.TrimSpace(participant.Principal) - if principal == "" { - return fmt.Errorf("participant principal is required") - } - if seen[principal] { - return fmt.Errorf("duplicate participant principal %q", principal) - } - seen[principal] = true + principal := participantrole.NormalizePrincipal(participant.Principal) if strings.TrimSpace(participant.HostRuntime.Kind) == "" { return fmt.Errorf("participant %q host_runtime.kind is required", principal) } @@ -270,9 +268,9 @@ func FromLegacy(root string) (Config, bool, error) { } if ok { found = true - if strings.TrimSpace(local.Principal) != "" { + if principal := participantrole.NormalizePrincipal(local.Principal); principal != "" { cfg.Participants = append(cfg.Participants, Participant{ - Principal: local.Principal, + Principal: principal, HostRuntime: HostRuntime{ Kind: RuntimeKindConfigured, Mode: RuntimeModeAttached, @@ -360,9 +358,9 @@ func bridgeMulticaRegistry(cfg *Config, reg multicasurface.MulticaRegistry) erro } func participantFromMulticaRegistryRecord(record multicasurface.MulticaParticipantRecord) (Participant, error) { - principal := strings.TrimSpace(record.Principal) - if principal == "" { - return Participant{}, fmt.Errorf("multica registry participant principal is required") + principal, err := participantrole.RequirePrincipal("multica registry participant", record.Principal) + if err != nil { + return Participant{}, err } displayName := strings.TrimSpace(record.AgentName) if displayName == "" { @@ -380,25 +378,23 @@ func participantFromMulticaRegistryRecord(record multicasurface.MulticaParticipa } func mergeParticipant(participants []Participant, next Participant) []Participant { - for i := range participants { - if participants[i].Principal != next.Principal { - continue - } - if strings.TrimSpace(participants[i].DisplayName) == "" { - participants[i].DisplayName = next.DisplayName + return participantrole.UpsertByPrincipal(participants, next, func(participant Participant) string { + return participant.Principal + }, func(existing, next Participant) Participant { + if strings.TrimSpace(existing.DisplayName) == "" { + existing.DisplayName = next.DisplayName } - if strings.TrimSpace(participants[i].Role) == "" { - participants[i].Role = next.Role + if strings.TrimSpace(existing.Role) == "" { + existing.Role = next.Role } - if strings.TrimSpace(participants[i].HostRuntime.Kind) == "" { - participants[i].HostRuntime.Kind = next.HostRuntime.Kind + if strings.TrimSpace(existing.HostRuntime.Kind) == "" { + existing.HostRuntime.Kind = next.HostRuntime.Kind } - if strings.TrimSpace(participants[i].HostRuntime.Mode) == "" { - participants[i].HostRuntime.Mode = next.HostRuntime.Mode + if strings.TrimSpace(existing.HostRuntime.Mode) == "" { + existing.HostRuntime.Mode = next.HostRuntime.Mode } - return participants - } - return append(participants, next) + return existing + }) } func loadLegacyLocal(root string) (legacyLocalConfig, bool, error) { From 964b074f2312ec620479008b3bf122623e687b1b Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:49:14 +0800 Subject: [PATCH 036/117] refactor: move multica issue identity parsing to surface Extract Multica runtime input issue identity parsing into the surface/multica adapter boundary. The runtime now delegates mention://issue, legacy issue id, and @/# issue-key fallback extraction before resolving the fetched Multica issue context. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- harness/cmd/mnemon-multica-runtime/main.go | 25 +----------- .../cmd/mnemon-multica-runtime/main_test.go | 22 ----------- .../surface/multica/issue_identity.go | 38 +++++++++++++++++++ .../surface/multica/issue_identity_test.go | 26 +++++++++++++ 4 files changed, 65 insertions(+), 46 deletions(-) create mode 100644 harness/internal/surface/multica/issue_identity.go create mode 100644 harness/internal/surface/multica/issue_identity_test.go diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index e748cb32..7e10fd1d 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -9,7 +9,6 @@ import ( "io" "os" "path/filepath" - "regexp" "runtime" "strings" "time" @@ -27,12 +26,6 @@ import ( const runtimeVersion = "dev" -var ( - assignedIssuePattern = regexp.MustCompile(`(?i)(?:assigned\s+issue\s+id\s+is|issue[_\s-]*id)\s*[::]\s*([A-Za-z0-9][A-Za-z0-9._:-]*)`) - multicaIssueMentionPattern = regexp.MustCompile(`(?i)mention://issue/([A-Za-z0-9][A-Za-z0-9._:-]*)`) - multicaTaggedIssuePattern = regexp.MustCompile(`(?i)(?:^|[\s([{"'])[@#]([A-Z][A-Z0-9]+-\d+)(?:$|[\s\])}.,;:'"])`) -) - type runtimeConfig struct { Args []string Env []string @@ -313,7 +306,7 @@ func (s *runtimeRPCState) runTurn(input string, progress runtimeProgressSink) st func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink) runtimeImportResult { taskID := envValue(s.Env, "MULTICA_TASK_ID") - issueID := firstNonEmpty(envValue(s.Env, "MULTICA_ISSUE_ID"), extractAssignedIssueID(input)) + issueID := firstNonEmpty(envValue(s.Env, "MULTICA_ISSUE_ID"), multicasurface.ExtractIssueIdentity(input)) result := runtimeImportResult{ IssueID: issueID, Principal: resolveRuntimePrincipal(s.Env, s.CWD), @@ -1278,22 +1271,6 @@ func extractRuntimeInput(params map[string]any) string { return strings.Join(parts, "\n") } -func extractAssignedIssueID(input string) string { - match := multicaIssueMentionPattern.FindStringSubmatch(input) - if len(match) >= 2 { - return strings.Trim(match[1], " \t\r\n.,;)") - } - match = assignedIssuePattern.FindStringSubmatch(input) - if len(match) >= 2 { - return strings.Trim(match[1], " \t\r\n.,;)") - } - match = multicaTaggedIssuePattern.FindStringSubmatch(input) - if len(match) >= 2 { - return strings.Trim(match[1], " \t\r\n.,;)") - } - return "" -} - func wantsVersion(args []string) bool { fs := flag.NewFlagSet("mnemon-multica-runtime", flag.ContinueOnError) fs.SetOutput(io.Discard) diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index f5ad2888..77feaf02 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -40,28 +40,6 @@ func TestRuntimeTimeoutUsesMulticaHTTPFallback(t *testing.T) { } } -func TestExtractAssignedIssueIDAcceptsMulticaIssueMentionsAndTags(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - {name: "explicit assigned issue", input: "Your assigned issue ID is: iss-7\nPlease work on it.", want: "iss-7"}, - {name: "multica mention", input: "Open [TEA-51](mention://issue/issue-51) for the current task.", want: "issue-51"}, - {name: "mention beats legacy text", input: "Your assigned issue ID is: stale\nOpen [TEA-52](mention://issue/issue-52).", want: "issue-52"}, - {name: "at identifier tag", input: "Please handle @TEA-49 next.", want: "TEA-49"}, - {name: "hash identifier tag", input: "Review #TEA-50.", want: "TEA-50"}, - {name: "non issue tag", input: "Please coordinate with @team.", want: ""}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if got := extractAssignedIssueID(tc.input); got != tc.want { - t.Fatalf("extractAssignedIssueID = %q, want %q", got, tc.want) - } - }) - } -} - func TestRuntimeMulticaHubLedgerPathDefaultsToManagedWorkspace(t *testing.T) { tmp := t.TempDir() workspace := filepath.Join(tmp, "managed-workspace") diff --git a/harness/internal/surface/multica/issue_identity.go b/harness/internal/surface/multica/issue_identity.go new file mode 100644 index 00000000..225cb074 --- /dev/null +++ b/harness/internal/surface/multica/issue_identity.go @@ -0,0 +1,38 @@ +package multica + +import ( + "regexp" + "strings" +) + +var ( + assignedIssuePattern = regexp.MustCompile(`(?i)(?:assigned\s+issue\s+id\s+is|issue[_\s-]*id)\s*[::]\s*([A-Za-z0-9][A-Za-z0-9._:-]*)`) + multicaIssueMentionPattern = regexp.MustCompile(`(?i)mention://issue/([A-Za-z0-9][A-Za-z0-9._:-]*)`) + multicaTaggedIssuePattern = regexp.MustCompile(`(?i)(?:^|[\s([{"'])[@#]([A-Z][A-Z0-9]+-\d+)(?:$|[\s\])}.,;:'"])`) +) + +// ExtractIssueIdentity returns the best issue lookup key from Multica runtime input. +// +// The returned value may be a stable mention target from mention://issue/, a +// daemon-compatible legacy issue id field, or a visible issue identifier tag +// such as @TEA-123. Callers must resolve identifier tags through Multica before +// building rule-facing Mnemon event material. +func ExtractIssueIdentity(input string) string { + match := multicaIssueMentionPattern.FindStringSubmatch(input) + if len(match) >= 2 { + return cleanIssueIdentity(match[1]) + } + match = assignedIssuePattern.FindStringSubmatch(input) + if len(match) >= 2 { + return cleanIssueIdentity(match[1]) + } + match = multicaTaggedIssuePattern.FindStringSubmatch(input) + if len(match) >= 2 { + return cleanIssueIdentity(match[1]) + } + return "" +} + +func cleanIssueIdentity(value string) string { + return strings.Trim(value, " \t\r\n.,;)") +} diff --git a/harness/internal/surface/multica/issue_identity_test.go b/harness/internal/surface/multica/issue_identity_test.go new file mode 100644 index 00000000..85fad2aa --- /dev/null +++ b/harness/internal/surface/multica/issue_identity_test.go @@ -0,0 +1,26 @@ +package multica + +import "testing" + +func TestExtractIssueIdentityAcceptsMentionsLegacyFieldsAndTags(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {name: "explicit assigned issue", input: "Your assigned issue ID is: iss-7\nPlease work on it.", want: "iss-7"}, + {name: "legacy issue id", input: "issue_id: iss-8", want: "iss-8"}, + {name: "multica mention", input: "Open [TEA-51](mention://issue/issue-51) for the current task.", want: "issue-51"}, + {name: "mention beats legacy text", input: "Your assigned issue ID is: stale\nOpen [TEA-52](mention://issue/issue-52).", want: "issue-52"}, + {name: "at identifier tag", input: "Please handle @TEA-49 next.", want: "TEA-49"}, + {name: "hash identifier tag", input: "Review #TEA-50.", want: "TEA-50"}, + {name: "non issue tag", input: "Please coordinate with @team.", want: ""}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := ExtractIssueIdentity(tc.input); got != tc.want { + t.Fatalf("ExtractIssueIdentity = %q, want %q", got, tc.want) + } + }) + } +} From 8a5f137a850623efb3dc2f637c51e07c426e33eb Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:53:08 +0800 Subject: [PATCH 037/117] refactor: move multica hub defaults to surface Move root-session and assignment-mailbox hub metadata defaulting into surface/multica so the Multica runtime adapter delegates session, correlation, root issue, and marker rules to the surface boundary. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- harness/cmd/mnemon-multica-runtime/main.go | 48 +++----------------- harness/internal/surface/multica/hub.go | 38 ++++++++++++++++ harness/internal/surface/multica/hub_test.go | 41 +++++++++++++++++ 3 files changed, 86 insertions(+), 41 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 7e10fd1d..1c56eb68 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -337,7 +337,7 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink result.Title = issue.Title result.Statement = issue.Description result.HubMetadata = driver.MulticaIssueHubMetadata(issue) - applyMulticaHubMetadata(&result, issue) + applyMulticaHubMetadata(&result, result.HubMetadata) result.HubBackend = firstNonEmpty(result.HubBackend, envValue(s.Env, "MNEMON_HUB_BACKEND")) emitRuntimeProgress(progress, "Loaded "+runtimeIssueLabel(issue)+"; classifying Mnemon hub metadata.") markIssueInProgress(multicaCtx, cli, issue.ID) @@ -370,7 +370,8 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink return result } if strings.EqualFold(result.HubBackend, driver.MulticaHubBackend) { - ensureRootSessionHubFields(&result, issue) + result.HubMetadata = multicasurface.RootSessionHubMetadata(result.HubMetadata, issue.ID) + applyMulticaHubMetadata(&result, result.HubMetadata) addPayloadRuleString(draft.Payload, "hub_backend", driver.MulticaHubBackend) addPayloadRuleString(draft.Payload, "root_issue_id", result.RootIssueID) addPayloadRuleString(draft.Payload, "session_id", result.SessionID) @@ -447,7 +448,8 @@ func loadRuntimeIssueMetadata(ctx context.Context, cli driver.MulticaCLI, issue } func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, result *runtimeImportResult, progress runtimeProgressSink) runtimeImportResult { - ensureAssignmentHubFields(result, issue) + result.HubMetadata = multicasurface.AssignmentMailboxHubMetadata(result.HubMetadata, issue.ID) + applyMulticaHubMetadata(result, result.HubMetadata) result.Status = "correlated" result.MatchTerms = drive.CleanManagedWakeMatchTerms( result.AssignmentID, @@ -480,17 +482,16 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr emitRuntimeProgress(progress, runtimeHubWriteProgress(*result)) } } - s.projectImportComment(ctx, cli, issue, assignmentMailboxMarker(*result), result) + s.projectImportComment(ctx, cli, issue, multicasurface.AssignmentMailboxMarker(result.HubMetadata, result.IssueID), result) emitRuntimeCommand(progress, "multica issue comment add "+issue.ID, runtimeProjectionProgress(*result), runtimeExitCode(result.ProjectionErr)) emitRuntimeProgress(progress, runtimeProjectionProgress(*result)) return *result } -func applyMulticaHubMetadata(result *runtimeImportResult, issue driver.MulticaIssue) { +func applyMulticaHubMetadata(result *runtimeImportResult, meta driver.MulticaHubMetadata) { if result == nil { return } - meta := result.HubMetadata result.HubBackend = firstNonEmpty(meta.HubBackend, result.HubBackend) result.HubKind = firstNonEmpty(meta.Kind, result.HubKind) result.SessionID = firstNonEmpty(meta.SessionID, result.SessionID) @@ -498,31 +499,6 @@ func applyMulticaHubMetadata(result *runtimeImportResult, issue driver.MulticaIs result.RootIssueID = firstNonEmpty(meta.RootIssueID, result.RootIssueID) result.AssignmentID = firstNonEmpty(meta.AssignmentID, result.AssignmentID) result.AssignmentFingerprint = firstNonEmpty(meta.AssignmentFingerprint, result.AssignmentFingerprint) - if result.RootIssueID == "" && meta.IsAssignmentMailbox() { - result.RootIssueID = firstNonEmpty(meta.SourceIssueID, issue.ID) - } -} - -func ensureRootSessionHubFields(result *runtimeImportResult, issue driver.MulticaIssue) { - if result == nil { - return - } - result.HubBackend = driver.MulticaHubBackend - result.HubKind = firstNonEmpty(result.HubKind, driver.MulticaHubKindSession) - result.RootIssueID = firstNonEmpty(result.RootIssueID, issue.ID) - result.SessionID = firstNonEmpty(result.SessionID, driver.MulticaSessionID(result.RootIssueID)) - result.CorrelationID = firstNonEmpty(result.CorrelationID, "multica:issue:"+issue.ID) -} - -func ensureAssignmentHubFields(result *runtimeImportResult, issue driver.MulticaIssue) { - if result == nil { - return - } - result.HubBackend = firstNonEmpty(result.HubBackend, driver.MulticaHubBackend) - result.HubKind = firstNonEmpty(result.HubKind, driver.MulticaHubKindAssignmentMailbox) - result.RootIssueID = firstNonEmpty(result.RootIssueID, result.HubMetadata.SourceIssueID) - result.SessionID = firstNonEmpty(result.SessionID, driver.MulticaSessionID(result.RootIssueID)) - result.CorrelationID = firstNonEmpty(result.CorrelationID, "multica:issue:"+issue.ID) } func rootSessionMetadata(result runtimeImportResult, draft driver.MulticaObservedDraft, now time.Time) map[string]string { @@ -558,16 +534,6 @@ func addPayloadRuleString(payload map[string]any, key, value string) { rule[key] = value } -func assignmentMailboxMarker(result runtimeImportResult) string { - if result.HubMetadata.EventID != "" { - return result.HubMetadata.EventID - } - if result.AssignmentID != "" { - return "multica-assignment-" + result.AssignmentID - } - return "multica-issue-" + result.IssueID -} - func runtimeManagedWakeMatchMaterial(result runtimeImportResult) drive.ManagedWakeMatchMaterial { return drive.ManagedWakeMatchMaterial{ MatchTerms: result.MatchTerms, diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go index a9283faf..856284ee 100644 --- a/harness/internal/surface/multica/hub.go +++ b/harness/internal/surface/multica/hub.go @@ -313,6 +313,44 @@ func (m MulticaHubMetadata) IsAssignmentMailbox() bool { } } +func RootSessionHubMetadata(meta MulticaHubMetadata, issueID string) MulticaHubMetadata { + issueID = strings.TrimSpace(issueID) + meta.HubBackend = MulticaHubBackend + meta.Kind = firstNonEmptyString(meta.Kind, MulticaHubKindSession) + meta.RootIssueID = firstNonEmptyString(meta.RootIssueID, issueID) + meta.SessionID = firstNonEmptyString(meta.SessionID, MulticaSessionID(meta.RootIssueID)) + if issueID != "" { + meta.CorrelationID = firstNonEmptyString(meta.CorrelationID, "multica:issue:"+issueID) + } + return meta +} + +func AssignmentMailboxHubMetadata(meta MulticaHubMetadata, issueID string) MulticaHubMetadata { + issueID = strings.TrimSpace(issueID) + meta.HubBackend = firstNonEmptyString(meta.HubBackend, MulticaHubBackend) + meta.Kind = firstNonEmptyString(meta.Kind, MulticaHubKindAssignmentMailbox) + meta.RootIssueID = firstNonEmptyString(meta.RootIssueID, meta.SourceIssueID, issueID) + meta.SessionID = firstNonEmptyString(meta.SessionID, MulticaSessionID(meta.RootIssueID)) + if issueID != "" { + meta.CorrelationID = firstNonEmptyString(meta.CorrelationID, "multica:issue:"+issueID) + } + return meta +} + +func AssignmentMailboxMarker(meta MulticaHubMetadata, issueID string) string { + if strings.TrimSpace(meta.EventID) != "" { + return strings.TrimSpace(meta.EventID) + } + if strings.TrimSpace(meta.AssignmentID) != "" { + return "multica-assignment-" + strings.TrimSpace(meta.AssignmentID) + } + issueID = strings.TrimSpace(issueID) + if issueID == "" { + return "" + } + return "multica-issue-" + issueID +} + func NormalizeMulticaMetadata(raw any) map[string]string { out := map[string]string{} merge := func(key string, value any) { diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go index 3f6441da..c8ed344c 100644 --- a/harness/internal/surface/multica/hub_test.go +++ b/harness/internal/surface/multica/hub_test.go @@ -57,6 +57,47 @@ func TestAssignmentFingerprintStable(t *testing.T) { } } +func TestRootSessionHubMetadataDefaults(t *testing.T) { + meta := RootSessionHubMetadata(MulticaHubMetadata{Principal: "planner@team"}, "root-1") + if meta.HubBackend != MulticaHubBackend || + meta.Kind != MulticaHubKindSession || + meta.RootIssueID != "root-1" || + meta.SessionID != MulticaSessionID("root-1") || + meta.CorrelationID != "multica:issue:root-1" || + meta.Principal != "planner@team" { + t.Fatalf("root session metadata mismatch: %+v", meta) + } +} + +func TestAssignmentMailboxHubMetadataDefaults(t *testing.T) { + meta := AssignmentMailboxHubMetadata(MulticaHubMetadata{ + SourceIssueID: "root-1", + AssignmentID: "asg-1", + Principal: "worker@team", + }, "child-1") + if meta.HubBackend != MulticaHubBackend || + meta.Kind != MulticaHubKindAssignmentMailbox || + meta.RootIssueID != "root-1" || + meta.SessionID != MulticaSessionID("root-1") || + meta.CorrelationID != "multica:issue:child-1" || + meta.AssignmentID != "asg-1" || + meta.Principal != "worker@team" { + t.Fatalf("assignment mailbox metadata mismatch: %+v", meta) + } +} + +func TestAssignmentMailboxMarkerPrefersEventThenAssignmentThenIssue(t *testing.T) { + if got := AssignmentMailboxMarker(MulticaHubMetadata{EventID: "event-1", AssignmentID: "asg-1"}, "child-1"); got != "event-1" { + t.Fatalf("event marker = %q", got) + } + if got := AssignmentMailboxMarker(MulticaHubMetadata{AssignmentID: "asg-1"}, "child-1"); got != "multica-assignment-asg-1" { + t.Fatalf("assignment marker = %q", got) + } + if got := AssignmentMailboxMarker(MulticaHubMetadata{}, "child-1"); got != "multica-issue-child-1" { + t.Fatalf("issue marker = %q", got) + } +} + func TestFileHubLedgerDedupesRecords(t *testing.T) { path := filepath.Join(t.TempDir(), "hub-ledger.jsonl") ledger := NewFileMulticaHubLedger(path) From 6736cfe5a87eefacf4b667739277fa4f856e3606 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 16:55:14 +0800 Subject: [PATCH 038/117] refactor: build multica root metadata in surface Move root session metadata map construction into surface/multica. The runtime now supplies event material and issue context while the surface owns mnemon.* root-session metadata shape. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- harness/cmd/mnemon-multica-runtime/main.go | 31 +++++++------------ harness/internal/surface/multica/hub.go | 25 +++++++++++++++ harness/internal/surface/multica/hub_test.go | 32 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 1c56eb68..df94c184 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -376,7 +376,17 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink addPayloadRuleString(draft.Payload, "root_issue_id", result.RootIssueID) addPayloadRuleString(draft.Payload, "session_id", result.SessionID) addPayloadRuleString(draft.Payload, "source_issue_id", issue.ID) - if err := cli.SetIssueMetadataMap(multicaCtx, issue.ID, rootSessionMetadata(result, draft, s.now())); err != nil { + rootSessionMaterial := multicasurface.RootSessionMetadataMaterial{ + HubMetadata: result.HubMetadata, + EventID: draft.ExternalID, + EventType: draft.EventType, + EventPhase: string(eventmodel.PhaseObserved), + Principal: result.Principal, + SourceIssueID: result.IssueID, + ProjectionOwner: result.Principal, + ProjectedAt: s.now(), + } + if err := cli.SetIssueMetadataMap(multicaCtx, issue.ID, multicasurface.RootSessionMetadataMap(rootSessionMaterial)); err != nil { metadataErr := fmt.Errorf("set Multica root session metadata: %w", err) emitRuntimeCommand(progress, "multica issue metadata set "+issue.ID+" mnemon.root-session", metadataErr.Error(), 1) emitRuntimeProgress(progress, "Root session metadata write failed; continuing with Mnemon ingest from issue context.") @@ -501,25 +511,6 @@ func applyMulticaHubMetadata(result *runtimeImportResult, meta driver.MulticaHub result.AssignmentFingerprint = firstNonEmpty(meta.AssignmentFingerprint, result.AssignmentFingerprint) } -func rootSessionMetadata(result runtimeImportResult, draft driver.MulticaObservedDraft, now time.Time) map[string]string { - meta := driver.MulticaHubMetadata{ - SchemaVersion: "1", - HubBackend: driver.MulticaHubBackend, - Kind: driver.MulticaHubKindSession, - SessionID: result.SessionID, - CorrelationID: result.CorrelationID, - EventID: draft.ExternalID, - EventType: draft.EventType, - EventPhase: string(eventmodel.PhaseObserved), - Principal: result.Principal, - SourceIssueID: result.IssueID, - RootIssueID: result.RootIssueID, - ProjectionOwner: result.Principal, - ProjectedAt: now.UTC().Format(time.RFC3339), - } - return meta.Map() -} - func addPayloadRuleString(payload map[string]any, key, value string) { key = strings.TrimSpace(key) value = strings.TrimSpace(value) diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go index 856284ee..0578d63c 100644 --- a/harness/internal/surface/multica/hub.go +++ b/harness/internal/surface/multica/hub.go @@ -61,6 +61,17 @@ type MulticaHubMetadata struct { EnvelopeDigest string } +type RootSessionMetadataMaterial struct { + HubMetadata MulticaHubMetadata + EventID string + EventType string + EventPhase string + Principal string + SourceIssueID string + ProjectionOwner string + ProjectedAt time.Time +} + type MulticaAssignmentFingerprintInput struct { AssignmentID string `json:"assignment_id,omitempty"` Assignee string `json:"assignee,omitempty"` @@ -351,6 +362,20 @@ func AssignmentMailboxMarker(meta MulticaHubMetadata, issueID string) string { return "multica-issue-" + issueID } +func RootSessionMetadataMap(material RootSessionMetadataMaterial) map[string]string { + meta := RootSessionHubMetadata(material.HubMetadata, material.SourceIssueID) + meta.EventID = firstNonEmptyString(material.EventID, meta.EventID) + meta.EventType = firstNonEmptyString(material.EventType, meta.EventType) + meta.EventPhase = firstNonEmptyString(material.EventPhase, meta.EventPhase) + meta.Principal = firstNonEmptyString(material.Principal, meta.Principal) + meta.SourceIssueID = firstNonEmptyString(material.SourceIssueID, meta.SourceIssueID) + meta.ProjectionOwner = firstNonEmptyString(material.ProjectionOwner, meta.ProjectionOwner) + if !material.ProjectedAt.IsZero() { + meta.ProjectedAt = firstNonEmptyString(meta.ProjectedAt, material.ProjectedAt.UTC().Format(time.RFC3339)) + } + return meta.Map() +} + func NormalizeMulticaMetadata(raw any) map[string]string { out := map[string]string{} merge := func(key string, value any) { diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go index c8ed344c..80b26c47 100644 --- a/harness/internal/surface/multica/hub_test.go +++ b/harness/internal/surface/multica/hub_test.go @@ -4,6 +4,7 @@ import ( "path/filepath" "strings" "testing" + "time" ) func TestHubMetadataDetectsAssignmentMailbox(t *testing.T) { @@ -98,6 +99,37 @@ func TestAssignmentMailboxMarkerPrefersEventThenAssignmentThenIssue(t *testing.T } } +func TestRootSessionMetadataMap(t *testing.T) { + now := time.Unix(100, 0).UTC() + meta := RootSessionMetadataMap(RootSessionMetadataMaterial{ + HubMetadata: MulticaHubMetadata{Principal: "planner@team"}, + EventID: "multica-task-task-1", + EventType: "teamwork_signal.write_candidate.observed", + EventPhase: "observed", + SourceIssueID: "root-1", + ProjectionOwner: "planner@team", + ProjectedAt: now, + }) + for key, want := range map[string]string{ + MulticaMetadataHubBackend: MulticaHubBackend, + MulticaMetadataKind: MulticaHubKindSession, + MulticaMetadataRootIssueID: "root-1", + MulticaMetadataSessionID: MulticaSessionID("root-1"), + MulticaMetadataCorrelationID: "multica:issue:root-1", + MulticaMetadataEventID: "multica-task-task-1", + MulticaMetadataEventType: "teamwork_signal.write_candidate.observed", + MulticaMetadataEventPhase: "observed", + MulticaMetadataPrincipal: "planner@team", + MulticaMetadataSourceIssueID: "root-1", + MulticaMetadataProjectionOwner: "planner@team", + MulticaMetadataProjectedAt: now.Format(time.RFC3339), + } { + if got := meta[key]; got != want { + t.Fatalf("metadata[%s] = %q, want %q: %+v", key, got, want, meta) + } + } +} + func TestFileHubLedgerDedupesRecords(t *testing.T) { path := filepath.Join(t.TempDir(), "hub-ledger.jsonl") ledger := NewFileMulticaHubLedger(path) From ae9e0658e5259e3fc159f47768054881774bbc74 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:02:15 +0800 Subject: [PATCH 039/117] test: preflight github mesh rate limits Check GitHub core API capacity before the mesh acceptance starts branch setup, appservers, or agent turns. The report now records the observed remaining, limit, and reset timestamp when the preflight blocks a run. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance; go test -count=1 ./harness/...; live r1-github-mesh-task-suite run blocked quickly with remaining=0 and reset=2026-06-29T09:25:22Z. --- .../acceptance_github_mesh.go | 104 ++++++++++++++++++ .../acceptance_github_mesh_test.go | 54 +++++++++ 2 files changed, 158 insertions(+) diff --git a/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go b/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go index 16b54716..648c4de2 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go +++ b/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "net/http" "os" "path/filepath" "sort" @@ -30,6 +31,11 @@ var ( acceptanceGitHubSyncInterval time.Duration ) +var ( + r1GitHubMeshRateLimitAPIURL = "https://api.github.com/rate_limit" + r1GitHubMeshHTTPClient = &http.Client{Timeout: 10 * time.Second} +) + var acceptanceR1GitHubMeshCmd = &cobra.Command{ Use: "r1-github-mesh-task-suite", Short: "Run GitHub-backed Remote Workspace task-suite acceptance", @@ -173,6 +179,17 @@ func runR1GitHubMeshAcceptance(ctx context.Context, opts r1GitHubMeshAcceptanceO report.Status = "blocked" return report, err } + rateLimit, err := preflightR1GitHubMeshRateLimit(ctx, tokenFile, r1GitHubMeshMinimumRateLimitRemaining(opts)) + if rateLimit.Limit > 0 || !rateLimit.ResetAt.IsZero() { + report.Artifacts["github_rate_limit_remaining"] = fmt.Sprintf("%d", rateLimit.Remaining) + report.Artifacts["github_rate_limit_limit"] = fmt.Sprintf("%d", rateLimit.Limit) + report.Artifacts["github_rate_limit_reset"] = rateLimit.ResetAt.UTC().Format(time.RFC3339) + } + if err != nil { + addR1Error(&report, err) + report.Status = "blocked" + return report, err + } branches := r1GitHubMeshBranches(branchPrefix, opts.Agents) if err := ensureR1GitHubMeshBranches(ctx, opts.Repo, tokenFile, branches); err != nil { addR1Error(&report, err) @@ -1402,6 +1419,93 @@ func ensureR1GitHubMeshBranches(ctx context.Context, repo, tokenFile string, bra return nil } +type r1GitHubMeshRateLimit struct { + Limit int + Remaining int + Used int + ResetAt time.Time +} + +func r1GitHubMeshMinimumRateLimitRemaining(opts r1GitHubMeshAcceptanceOptions) int { + scenarios := len(r1GitHubMeshScenarioNames(opts.Scenarios)) + if scenarios == 0 { + scenarios = 1 + } + agents := opts.Agents + if agents < 5 { + agents = 5 + } + min := 500 + agents*scenarios*150 + if min < 1500 { + return 1500 + } + return min +} + +func preflightR1GitHubMeshRateLimit(ctx context.Context, tokenFile string, minRemaining int) (r1GitHubMeshRateLimit, error) { + token, err := readR1GitHubMeshToken(tokenFile) + if err != nil { + return r1GitHubMeshRateLimit{}, err + } + limit, err := fetchR1GitHubMeshRateLimit(ctx, token) + if err != nil { + return r1GitHubMeshRateLimit{}, err + } + if limit.Remaining < minRemaining { + return limit, fmt.Errorf("github core API rate limit remaining %d below required %d; reset=%s", limit.Remaining, minRemaining, limit.ResetAt.UTC().Format(time.RFC3339)) + } + return limit, nil +} + +func fetchR1GitHubMeshRateLimit(ctx context.Context, token string) (r1GitHubMeshRateLimit, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, r1GitHubMeshRateLimitAPIURL, nil) + if err != nil { + return r1GitHubMeshRateLimit{}, err + } + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + req.Header.Set("User-Agent", "mnemon-acceptance") + if strings.TrimSpace(token) != "" { + req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token)) + } + client := r1GitHubMeshHTTPClient + if client == nil { + client = http.DefaultClient + } + resp, err := client.Do(req) + if err != nil { + return r1GitHubMeshRateLimit{}, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return r1GitHubMeshRateLimit{}, err + } + if resp.StatusCode >= 300 { + return r1GitHubMeshRateLimit{}, fmt.Errorf("github rate_limit status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + var doc struct { + Resources struct { + Core struct { + Limit int `json:"limit"` + Remaining int `json:"remaining"` + Reset int64 `json:"reset"` + Used int `json:"used"` + } `json:"core"` + } `json:"resources"` + } + if err := json.Unmarshal(body, &doc); err != nil { + return r1GitHubMeshRateLimit{}, fmt.Errorf("parse github rate_limit response: %w", err) + } + core := doc.Resources.Core + return r1GitHubMeshRateLimit{ + Limit: core.Limit, + Remaining: core.Remaining, + Used: core.Used, + ResetAt: time.Unix(core.Reset, 0).UTC(), + }, nil +} + func readR1GitHubMeshToken(tokenFile string) (string, error) { body, err := os.ReadFile(tokenFile) if err != nil { diff --git a/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go b/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go index da79fb1a..47e9b616 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go @@ -3,6 +3,8 @@ package main import ( "context" "database/sql" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -48,6 +50,58 @@ func TestR1GitHubMeshBranchPrefixDefaultsToRunScopedBranches(t *testing.T) { } } +func TestFetchR1GitHubMeshRateLimit(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer secret-token" { + t.Fatalf("authorization header = %q", got) + } + _, _ = w.Write([]byte(`{"resources":{"core":{"limit":5000,"remaining":4321,"reset":1782725122,"used":679}}}`)) + })) + defer server.Close() + oldURL := r1GitHubMeshRateLimitAPIURL + oldClient := r1GitHubMeshHTTPClient + r1GitHubMeshRateLimitAPIURL = server.URL + r1GitHubMeshHTTPClient = server.Client() + t.Cleanup(func() { + r1GitHubMeshRateLimitAPIURL = oldURL + r1GitHubMeshHTTPClient = oldClient + }) + + limit, err := fetchR1GitHubMeshRateLimit(context.Background(), "secret-token") + if err != nil { + t.Fatal(err) + } + if limit.Limit != 5000 || limit.Remaining != 4321 || limit.Used != 679 || limit.ResetAt.Unix() != 1782725122 { + t.Fatalf("rate limit mismatch: %+v", limit) + } +} + +func TestPreflightR1GitHubMeshRateLimitBlocksLowRemaining(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"resources":{"core":{"limit":5000,"remaining":42,"reset":1782725122,"used":4958}}}`)) + })) + defer server.Close() + oldURL := r1GitHubMeshRateLimitAPIURL + oldClient := r1GitHubMeshHTTPClient + r1GitHubMeshRateLimitAPIURL = server.URL + r1GitHubMeshHTTPClient = server.Client() + t.Cleanup(func() { + r1GitHubMeshRateLimitAPIURL = oldURL + r1GitHubMeshHTTPClient = oldClient + }) + tokenFile := filepath.Join(t.TempDir(), "github.token") + if err := os.WriteFile(tokenFile, []byte("secret-token\n"), 0o600); err != nil { + t.Fatal(err) + } + limit, err := preflightR1GitHubMeshRateLimit(context.Background(), tokenFile, 1500) + if err == nil || !strings.Contains(err.Error(), "below required 1500") { + t.Fatalf("expected low-rate-limit error, got limit=%+v err=%v", limit, err) + } + if limit.Remaining != 42 { + t.Fatalf("rate limit should be returned for diagnostics: %+v", limit) + } +} + func TestWriteR1GitHubMeshRemotesCreatesPublishAndSubscribePlan(t *testing.T) { root := t.TempDir() tokenFile := filepath.Join(root, "github.token") From 7b80812374de705ba69777250dfcac019adf4a85 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:06:10 +0800 Subject: [PATCH 040/117] refactor: move multica mailbox metadata grouping Keep assignment mailbox dispatch and supplemental metadata grouping in the Multica surface package so the runtime adapter calls the shared metadata protocol instead of carrying routing key policy in the command entrypoint. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/...; root mnemon CLI diff check stayed empty. --- .../cmd/mnemon-multica-runtime/hub_writer.go | 40 +------------- harness/internal/surface/multica/hub.go | 36 +++++++++++++ harness/internal/surface/multica/hub_test.go | 54 +++++++++++++++++++ 3 files changed, 92 insertions(+), 38 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index a5867e2a..0cc2d548 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -238,7 +238,7 @@ func (s *runtimeRPCState) projectAssignmentMailboxes(ctx context.Context, cli dr continue } fullMeta := projection.Metadata.Map() - dispatchMeta := assignmentMailboxDispatchMetadata(fullMeta) + dispatchMeta := multicasurface.AssignmentMailboxDispatchMetadata(fullMeta) if err := cli.SetIssueMetadataMap(ctx, child.ID, dispatchMeta); err != nil { recordErr(err) continue @@ -265,7 +265,7 @@ func (s *runtimeRPCState) projectAssignmentMailboxes(ctx context.Context, cli dr mu.Lock() created++ mu.Unlock() - if err := cli.SetIssueMetadataMap(ctx, child.ID, assignmentMailboxSupplementalMetadata(fullMeta, dispatchMeta)); err != nil { + if err := cli.SetIssueMetadataMap(ctx, child.ID, multicasurface.AssignmentMailboxSupplementalMetadata(fullMeta, dispatchMeta)); err != nil { recordErr(err) } } @@ -462,42 +462,6 @@ func runtimeMulticaHubLedgerPath(env []string, cwd string) string { return driver.MulticaHubLedgerPath(cwd, "") } -func assignmentMailboxDispatchMetadata(full map[string]string) map[string]string { - keys := []string{ - driver.MulticaMetadataSchemaVersion, - driver.MulticaMetadataHubBackend, - driver.MulticaMetadataKind, - driver.MulticaMetadataSessionID, - driver.MulticaMetadataCorrelationID, - driver.MulticaMetadataEventID, - driver.MulticaMetadataAssignmentID, - driver.MulticaMetadataAssignmentFingerprint, - driver.MulticaMetadataPrincipal, - driver.MulticaMetadataSourceIssueID, - driver.MulticaMetadataRootIssueID, - } - out := map[string]string{} - for _, key := range keys { - if value := strings.TrimSpace(full[key]); value != "" { - out[key] = value - } - } - return out -} - -func assignmentMailboxSupplementalMetadata(full, dispatch map[string]string) map[string]string { - out := map[string]string{} - for key, value := range full { - if _, ok := dispatch[key]; ok { - continue - } - if value = strings.TrimSpace(value); value != "" { - out[key] = value - } - } - return out -} - func multicaParticipantForPrincipal(reg driver.MulticaRegistry, principal string) (driver.MulticaParticipantRecord, bool) { for _, participant := range reg.Participants { if strings.TrimSpace(participant.Principal) == strings.TrimSpace(principal) { diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go index 0578d63c..aa75dbf5 100644 --- a/harness/internal/surface/multica/hub.go +++ b/harness/internal/surface/multica/hub.go @@ -376,6 +376,42 @@ func RootSessionMetadataMap(material RootSessionMetadataMaterial) map[string]str return meta.Map() } +func AssignmentMailboxDispatchMetadata(full map[string]string) map[string]string { + keys := []string{ + MulticaMetadataSchemaVersion, + MulticaMetadataHubBackend, + MulticaMetadataKind, + MulticaMetadataSessionID, + MulticaMetadataCorrelationID, + MulticaMetadataEventID, + MulticaMetadataAssignmentID, + MulticaMetadataAssignmentFingerprint, + MulticaMetadataPrincipal, + MulticaMetadataSourceIssueID, + MulticaMetadataRootIssueID, + } + out := map[string]string{} + for _, key := range keys { + if value := strings.TrimSpace(full[key]); value != "" { + out[key] = value + } + } + return out +} + +func AssignmentMailboxSupplementalMetadata(full, dispatch map[string]string) map[string]string { + out := map[string]string{} + for key, value := range full { + if _, ok := dispatch[key]; ok { + continue + } + if value = strings.TrimSpace(value); value != "" { + out[key] = value + } + } + return out +} + func NormalizeMulticaMetadata(raw any) map[string]string { out := map[string]string{} merge := func(key string, value any) { diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go index 80b26c47..3e8f0eaa 100644 --- a/harness/internal/surface/multica/hub_test.go +++ b/harness/internal/surface/multica/hub_test.go @@ -130,6 +130,60 @@ func TestRootSessionMetadataMap(t *testing.T) { } } +func TestAssignmentMailboxMetadataGroupsDispatchBeforeSupplemental(t *testing.T) { + full := map[string]string{ + MulticaMetadataSchemaVersion: " 1 ", + MulticaMetadataHubBackend: MulticaHubBackend, + MulticaMetadataKind: MulticaHubKindAssignmentMailbox, + MulticaMetadataSessionID: "session-1", + MulticaMetadataCorrelationID: "correlation-1", + MulticaMetadataEventID: "event-1", + MulticaMetadataAssignmentID: "asg-1", + MulticaMetadataAssignmentFingerprint: "sha256:abc", + MulticaMetadataPrincipal: "worker@team", + MulticaMetadataSourceIssueID: "root-1", + MulticaMetadataRootIssueID: "root-1", + MulticaMetadataProjectionOwner: " planner@team ", + MulticaMetadataProjectedAt: "2026-06-29T09:00:00Z", + MulticaMetadataEnvelopeDigest: "", + } + dispatch := AssignmentMailboxDispatchMetadata(full) + for _, key := range []string{ + MulticaMetadataSchemaVersion, + MulticaMetadataHubBackend, + MulticaMetadataKind, + MulticaMetadataSessionID, + MulticaMetadataCorrelationID, + MulticaMetadataEventID, + MulticaMetadataAssignmentID, + MulticaMetadataAssignmentFingerprint, + MulticaMetadataPrincipal, + MulticaMetadataSourceIssueID, + MulticaMetadataRootIssueID, + } { + if strings.TrimSpace(full[key]) == "" { + continue + } + if got := dispatch[key]; got != strings.TrimSpace(full[key]) { + t.Fatalf("dispatch metadata[%s] = %q, want %q: %+v", key, got, strings.TrimSpace(full[key]), dispatch) + } + } + if _, ok := dispatch[MulticaMetadataProjectionOwner]; ok { + t.Fatalf("projection owner should be supplemental, not dispatch metadata: %+v", dispatch) + } + + supplemental := AssignmentMailboxSupplementalMetadata(full, dispatch) + if supplemental[MulticaMetadataProjectionOwner] != "planner@team" { + t.Fatalf("supplemental metadata should trim projection owner: %+v", supplemental) + } + if _, ok := supplemental[MulticaMetadataAssignmentID]; ok { + t.Fatalf("supplemental metadata must not duplicate dispatch keys: %+v", supplemental) + } + if _, ok := supplemental[MulticaMetadataEnvelopeDigest]; ok { + t.Fatalf("empty supplemental metadata must be omitted: %+v", supplemental) + } +} + func TestFileHubLedgerDedupesRecords(t *testing.T) { path := filepath.Join(t.TempDir(), "hub-ledger.jsonl") ledger := NewFileMulticaHubLedger(path) From 120b2c11c1e6ba1e3b5820b1d314212e5c0580c0 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:08:47 +0800 Subject: [PATCH 041/117] refactor: move multica runtime protocol helpers Move turn input extraction and Multica runtime reference formatting into the Multica surface package. The runtime adapter now consumes shared protocol helpers for daemon input text and external refs instead of locally encoding those details. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/...; root mnemon CLI diff check stayed empty. --- harness/cmd/mnemon-multica-runtime/main.go | 37 +++---------------- .../internal/surface/multica/runtime_items.go | 27 ++++++++++++++ .../surface/multica/runtime_items_test.go | 31 ++++++++++++++++ 3 files changed, 63 insertions(+), 32 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index df94c184..14628466 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -213,7 +213,7 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er return emitAll(rpcMessage{ID: msg.ID, Result: map[string]any{}}) case "turn/start": s.TurnID = runtimeID("turn", s.now()) - input := extractRuntimeInput(msg.Params) + input := multicasurface.RuntimeTextInput(msg.Params) nowMs := s.now().UnixMilli() userItem := multicasurface.RuntimeUserMessage(input) if err := emitAll( @@ -357,11 +357,11 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink AgentID: envValue(s.Env, "MULTICA_AGENT_ID"), Principal: result.Principal, ContextRefs: []string{ - runtimeRef("issue", issue.ID), - runtimeRef("task", taskID), - runtimeRef("agent", envValue(s.Env, "MULTICA_AGENT_ID")), + multicasurface.RuntimeRef("issue", issue.ID), + multicasurface.RuntimeRef("task", taskID), + multicasurface.RuntimeRef("agent", envValue(s.Env, "MULTICA_AGENT_ID")), }, - EvidenceRefs: []string{runtimeRef("issue", issue.ID)}, + EvidenceRefs: []string{multicasurface.RuntimeRef("issue", issue.ID)}, ExternalID: externalID, }) if err != nil { @@ -863,15 +863,6 @@ func principalFromRegistry(env []string, cwd, agentID, agentName string) string return "" } -func runtimeRef(kind, id string) string { - kind = strings.TrimSpace(kind) - id = strings.TrimSpace(id) - if kind == "" || id == "" { - return "" - } - return "multica:" + kind + ":" + id -} - func formatRuntimeFinalAnswer(result runtimeImportResult) string { var b strings.Builder if result.IssueID == "" { @@ -1210,24 +1201,6 @@ func markIssueInProgress(ctx context.Context, cli driver.MulticaCLI, issueID str _, _ = cli.SetIssueStatus(ctx, issueID, "in_progress") } -func extractRuntimeInput(params map[string]any) string { - input, ok := params["input"].([]any) - if !ok { - return "" - } - var parts []string - for _, item := range input { - obj, ok := item.(map[string]any) - if !ok { - continue - } - if text, _ := obj["text"].(string); strings.TrimSpace(text) != "" { - parts = append(parts, text) - } - } - return strings.Join(parts, "\n") -} - func wantsVersion(args []string) bool { fs := flag.NewFlagSet("mnemon-multica-runtime", flag.ContinueOnError) fs.SetOutput(io.Discard) diff --git a/harness/internal/surface/multica/runtime_items.go b/harness/internal/surface/multica/runtime_items.go index e98880a8..a78959a3 100644 --- a/harness/internal/surface/multica/runtime_items.go +++ b/harness/internal/surface/multica/runtime_items.go @@ -136,6 +136,33 @@ func RuntimeUserMessage(text string) map[string]any { } } +func RuntimeTextInput(params map[string]any) string { + input, ok := params["input"].([]any) + if !ok { + return "" + } + var parts []string + for _, item := range input { + obj, ok := item.(map[string]any) + if !ok { + continue + } + if text, _ := obj["text"].(string); strings.TrimSpace(text) != "" { + parts = append(parts, text) + } + } + return strings.Join(parts, "\n") +} + +func RuntimeRef(kind, id string) string { + kind = strings.TrimSpace(kind) + id = strings.TrimSpace(id) + if kind == "" || id == "" { + return "" + } + return "multica:" + kind + ":" + id +} + func runtimeManagedTraceItem(event activationtrace.Event) map[string]any { if len(event.Item) == 0 { return nil diff --git a/harness/internal/surface/multica/runtime_items_test.go b/harness/internal/surface/multica/runtime_items_test.go index 612576ba..e141cdbb 100644 --- a/harness/internal/surface/multica/runtime_items_test.go +++ b/harness/internal/surface/multica/runtime_items_test.go @@ -122,3 +122,34 @@ func TestRuntimeAgentMessageMessagesUsePhaseAndDelta(t *testing.T) { t.Fatalf("unexpected delta message: %+v", messages[1]) } } + +func TestRuntimeTextInputExtractsOnlyTextItems(t *testing.T) { + got := RuntimeTextInput(map[string]any{ + "input": []any{ + map[string]any{"type": "text", "text": "Open [TEA-1](mention://issue/iss-1)."}, + map[string]any{"type": "image", "url": "ignored"}, + map[string]any{"type": "text", "text": " "}, + map[string]any{"type": "text", "text": "Then summarize."}, + "ignored", + }, + }) + want := "Open [TEA-1](mention://issue/iss-1).\nThen summarize." + if got != want { + t.Fatalf("RuntimeTextInput() = %q, want %q", got, want) + } + if got := RuntimeTextInput(map[string]any{"input": "not-list"}); got != "" { + t.Fatalf("non-list input should be ignored, got %q", got) + } +} + +func TestRuntimeRefNormalizesMulticaRefs(t *testing.T) { + if got := RuntimeRef(" issue ", " iss-1 "); got != "multica:issue:iss-1" { + t.Fatalf("RuntimeRef() = %q", got) + } + if got := RuntimeRef("", "iss-1"); got != "" { + t.Fatalf("empty kind should produce no ref, got %q", got) + } + if got := RuntimeRef("issue", ""); got != "" { + t.Fatalf("empty id should produce no ref, got %q", got) + } +} From cb515abb141ef0c7fbc3c9bfbc862f25ec121610 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:12:17 +0800 Subject: [PATCH 042/117] feat: hide harness debug commands from root help Keep the Multica debug surface and Tower command directly callable while removing them from the ordinary mnemon-harness product help. Root help now stays focused on setup, config, daemon, doctor, agent, connect, and session paths. Validation: go test -count=1 ./harness/cmd/mnemon-harness; go test -count=1 ./harness/internal/app after one timestamp-parity retry; go test -count=1 ./harness/...; direct multica and tower hidden command help still works; root mnemon CLI diff check stayed empty. --- harness/cmd/mnemon-harness/multica.go | 5 +++-- harness/cmd/mnemon-harness/root_test.go | 5 +++++ harness/cmd/mnemon-harness/tower.go | 7 ++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 1d6e1e28..b86f3c1a 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -81,8 +81,9 @@ var ( ) var multicaCmd = &cobra.Command{ - Use: "multica", - Short: "Bridge Multica issues and comments with Local Mnemon", + Use: "multica", + Short: "Bridge Multica issues and comments with Local Mnemon", + Hidden: true, } var multicaProbeCmd = &cobra.Command{ diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index 71bf5c69..077dafb3 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -32,6 +32,11 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help leaked unsupported product term %q:\n%s", blocked, got) } } + for _, blocked := range []string{" multica", " tower"} { + if strings.Contains(got, blocked) { + t.Fatalf("root help leaked debug command %q:\n%s", strings.TrimSpace(blocked), got) + } + } } func TestRootDoesNotExposeAcceptanceCommands(t *testing.T) { diff --git a/harness/cmd/mnemon-harness/tower.go b/harness/cmd/mnemon-harness/tower.go index 8a305c06..c79af494 100644 --- a/harness/cmd/mnemon-harness/tower.go +++ b/harness/cmd/mnemon-harness/tower.go @@ -13,9 +13,10 @@ var towerDump bool // towerCmd is the Agent Control Tower (P6, D5: TUI-only, command name `tower`) — the human-visible // boundary over the agent field. It renders the four §3.3 pages (GOAL/FIELD/INBOX/LEDGER) read-only. var towerCmd = &cobra.Command{ - Use: "tower", - Short: "Agent Control Tower — the four-page human boundary over the agent field (GOAL/FIELD/INBOX/LEDGER)", - RunE: runTower, + Use: "tower", + Short: "Agent Control Tower — the four-page human boundary over the agent field (GOAL/FIELD/INBOX/LEDGER)", + Hidden: true, + RunE: runTower, } func init() { From d6b2c826d9b992a1fe10fbc0c3c7754c3265a605 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:14:54 +0800 Subject: [PATCH 043/117] refactor: drop multica projection wrapper from driver Remove the unused Multica projection comment wrapper from the CLI driver layer so projection formatting stays owned by the projection package and Multica surface/runtime callers use it directly. Validation: go test -count=1 ./harness/internal/driver ./harness/internal/projection ./harness/internal/surface/multica; go test -count=1 ./harness/...; root mnemon CLI diff check stayed empty. --- harness/internal/driver/multica.go | 9 --------- harness/internal/driver/multica_test.go | 17 ----------------- 2 files changed, 26 deletions(-) diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index d3c2e3ea..f07a29e8 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -13,7 +13,6 @@ import ( "sync" "time" - "github.com/mnemon-dev/mnemon/harness/internal/projection" multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) @@ -856,14 +855,6 @@ func BuildMulticaIssueTeamworkSignal(issue MulticaIssue, opts MulticaIssueSignal }, opts) } -func FormatMulticaProjectionComment(title string, body string, eventIDs []string) string { - return projection.FormatComment(projection.CommentMaterial{ - Title: title, - Body: body, - EventIDs: eventIDs, - }) -} - func DecodeMulticaIssue(r io.Reader) (MulticaIssue, error) { var issue MulticaIssue if err := json.NewDecoder(r).Decode(&issue); err != nil { diff --git a/harness/internal/driver/multica_test.go b/harness/internal/driver/multica_test.go index 945ec2e0..ba7caa3b 100644 --- a/harness/internal/driver/multica_test.go +++ b/harness/internal/driver/multica_test.go @@ -305,23 +305,6 @@ printf '{"id":"issue-1","identifier":"TEA-9","title":"Run teamwork","status":"to } } -func TestFormatMulticaProjectionCommentCarriesStableMarkers(t *testing.T) { - got := FormatMulticaProjectionComment("assignment finished", "Worker reported passing evidence.", []string{"ev-1", "ev-1", "ev-2"}) - for _, want := range []string{ - "Mnemon update: assignment finished", - "Worker reported passing evidence.", - "mnemon:event=ev-1", - "mnemon:event=ev-2", - } { - if !strings.Contains(got, want) { - t.Fatalf("projection comment missing %q:\n%s", want, got) - } - } - if strings.Count(got, "mnemon:event=ev-1") != 1 { - t.Fatalf("projection comment should dedupe markers:\n%s", got) - } -} - func TestMulticaHubMetadataDetectsAssignmentMailbox(t *testing.T) { issue := MulticaIssue{ ID: "issue-2", From 9c6c716d8da2001a854603bf57afc47a3b6a3147 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:16:41 +0800 Subject: [PATCH 044/117] test: guard driver projection boundary Add a focused R2 role guard that prevents the legacy driver facade from importing projection formatting again. Projection ownership stays in projection and external surface packages while driver remains a CLI adapter layer. Validation: go test -count=1 ./harness/internal/coreguard ./harness/internal/driver ./harness/internal/projection; go test -count=1 ./harness/...; root mnemon CLI diff check stayed empty. --- harness/internal/coreguard/r2_role_boundary_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/harness/internal/coreguard/r2_role_boundary_test.go b/harness/internal/coreguard/r2_role_boundary_test.go index ca2e4673..7408ef6e 100644 --- a/harness/internal/coreguard/r2_role_boundary_test.go +++ b/harness/internal/coreguard/r2_role_boundary_test.go @@ -54,6 +54,12 @@ var activationTraceImportBoundary = roleImportBoundary{ rationale: "activation trace is non-canonical run display material and must not become EventEnvelope material", } +var driverProjectionImportBoundary = roleImportBoundary{ + pkg: "driver", + forbids: []string{"harness/internal/projection"}, + rationale: "driver is a CLI adapter facade during R2 cleanup; projection formatting must stay in projection/surface packages", +} + func TestR2RoleBoundaryGuardLogicIsNotVacuous(t *testing.T) { if !roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/access", projectionSurfaceImportBoundaries[0].forbids) { t.Fatal("projection boundary guard must flag mnemond access imports") @@ -64,6 +70,9 @@ func TestR2RoleBoundaryGuardLogicIsNotVacuous(t *testing.T) { if !roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/event", activationTraceImportBoundary.forbids) { t.Fatal("activation trace boundary guard must flag event model imports") } + if !roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/projection", driverProjectionImportBoundary.forbids) { + t.Fatal("driver projection boundary guard must flag projection imports") + } } func TestProjectionSurfacesDoNotIngestGovernedEvents(t *testing.T) { @@ -76,6 +85,10 @@ func TestActivationTraceNeverWritesEventMaterial(t *testing.T) { assertPackageAvoidsForbiddenImports(t, activationTraceImportBoundary) } +func TestDriverDoesNotOwnProjectionFormatting(t *testing.T) { + assertPackageAvoidsForbiddenImports(t, driverProjectionImportBoundary) +} + func assertPackageAvoidsForbiddenImports(t *testing.T, boundary roleImportBoundary) { t.Helper() _, files := packageFiles(t, boundary.pkg) From 683bcbd3d87f550c21877b2091e74f92bb30094c Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:19:25 +0800 Subject: [PATCH 045/117] refactor: build multica issue signals in surface Route Multica issue signal construction through the Multica surface package from both the runtime adapter and the hidden harness debug import path. The legacy driver wrapper and duplicate driver test are removed so rule/narrative/refs ownership stays with the surface protocol. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/internal/driver ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-harness; go test -count=1 ./harness/...; root mnemon CLI diff check stayed empty. --- harness/cmd/mnemon-harness/multica.go | 8 +++- harness/cmd/mnemon-multica-runtime/main.go | 7 +++- harness/internal/driver/multica.go | 13 ------- harness/internal/driver/multica_test.go | 45 ---------------------- 4 files changed, 13 insertions(+), 60 deletions(-) diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index b86f3c1a..6edfa7dc 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -13,6 +13,7 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/driver" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" "github.com/mnemon-dev/mnemon/harness/internal/projection" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" "github.com/spf13/cobra" ) @@ -250,7 +251,12 @@ func runMulticaImportIssue(cmd *cobra.Command, args []string) error { if err != nil { return err } - draft, err := driver.BuildMulticaIssueTeamworkSignal(issue, driver.MulticaIssueSignalOptions{ + draft, err := multicasurface.BuildIssueTeamworkSignal(multicasurface.IssueSignalMaterial{ + ID: issue.ID, + Identifier: issue.Identifier, + Title: issue.Title, + Description: issue.Description, + }, multicasurface.IssueSignalOptions{ Scope: multicaScope, TTL: multicaTTL, WhyTeamwork: multicaWhyTeamwork, diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 14628466..6ad37b6a 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -348,7 +348,12 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink if taskID != "" { externalID = "multica-task-" + taskID } - draft, err := driver.BuildMulticaIssueTeamworkSignal(issue, driver.MulticaIssueSignalOptions{ + draft, err := multicasurface.BuildIssueTeamworkSignal(multicasurface.IssueSignalMaterial{ + ID: issue.ID, + Identifier: issue.Identifier, + Title: issue.Title, + Description: issue.Description, + }, multicasurface.IssueSignalOptions{ Scope: envDefault(s.Env, "MNEMON_MULTICA_SCOPE", "multica/teamwork"), TTL: envDefault(s.Env, "MNEMON_MULTICA_TTL", "30m"), WhyTeamwork: "Multica assigned this issue to a Mnemon participant, so Mnemon should admit it through the teamwork protocol.", diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index f07a29e8..1a258ad6 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -842,19 +842,6 @@ func (c MulticaCLI) globalArgs(args []string) []string { return append(out, args...) } -type MulticaIssueSignalOptions = multicasurface.IssueSignalOptions - -type MulticaObservedDraft = multicasurface.ObservedDraft - -func BuildMulticaIssueTeamworkSignal(issue MulticaIssue, opts MulticaIssueSignalOptions) (MulticaObservedDraft, error) { - return multicasurface.BuildIssueTeamworkSignal(multicasurface.IssueSignalMaterial{ - ID: issue.ID, - Identifier: issue.Identifier, - Title: issue.Title, - Description: issue.Description, - }, opts) -} - func DecodeMulticaIssue(r io.Reader) (MulticaIssue, error) { var issue MulticaIssue if err := json.NewDecoder(r).Decode(&issue); err != nil { diff --git a/harness/internal/driver/multica_test.go b/harness/internal/driver/multica_test.go index ba7caa3b..6d466c5b 100644 --- a/harness/internal/driver/multica_test.go +++ b/harness/internal/driver/multica_test.go @@ -8,51 +8,6 @@ import ( "testing" ) -func TestBuildMulticaIssueTeamworkSignalSeparatesRuleAndNarrative(t *testing.T) { - draft, err := BuildMulticaIssueTeamworkSignal(MulticaIssue{ - ID: "iss-123", - Identifier: "MUL-123", - Title: "Validate bridge", - Description: "Check that Multica issue context can start Mnemon teamwork.", - }, MulticaIssueSignalOptions{ - Scope: "multica/poc", - TTL: "45m", - WhyTeamwork: "The task needs more than one local agent.", - WorkspaceID: "workspace-1", - TaskID: "task-1", - AgentID: "agent-1", - Principal: "planner@team", - EvidenceRefs: []string{"multica:issue/iss-123"}, - ExternalID: "multica-task-task-1", - }) - if err != nil { - t.Fatal(err) - } - if draft.EventType != "teamwork_signal.write_candidate.observed" { - t.Fatalf("event type = %q", draft.EventType) - } - rule := draft.Payload["rule"].(map[string]any) - if rule["external_source"] != MulticaExternalSource || rule["external_issue_id"] != "iss-123" || rule["scope"] != "multica/poc" { - t.Fatalf("rule mapping mismatch: %+v", rule) - } - if rule["external_workspace_id"] != "workspace-1" || rule["external_task_id"] != "task-1" || rule["external_agent_id"] != "agent-1" || rule["principal"] != "planner@team" { - t.Fatalf("runtime rule mapping mismatch: %+v", rule) - } - if draft.ExternalID != "multica-task-task-1" { - t.Fatalf("external id = %q", draft.ExternalID) - } - narrative := draft.Payload["narrative"].(map[string]any) - if narrative["statement"] != "Check that Multica issue context can start Mnemon teamwork." { - t.Fatalf("narrative statement mismatch: %+v", narrative) - } - if _, ok := narrative["external_issue_id"]; ok { - t.Fatalf("narrative must not carry rule ids: %+v", narrative) - } - if _, ok := narrative["external_task_id"]; ok { - t.Fatalf("narrative must not carry runtime ids: %+v", narrative) - } -} - func TestMulticaCLIAddCommentUsesStdin(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "args.txt") From b06f134438a81500fb6fc88bec9237e46ec77d19 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:44:43 +0800 Subject: [PATCH 046/117] test: guard github mesh sync interval Block real GitHub mesh agent-turn acceptance when --sync-interval is below 30s. The live single-scenario attempt with 10s sync exhausted the full 5000 core API quota and forced sync workers into a 46 minute GitHub backoff, so future runs fail locally before branch setup or workers start. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance; go test -count=1 ./harness/...; live 10s agent-turn command now writes a blocked report immediately with the sync interval guard; root mnemon CLI diff check stayed empty. --- .../acceptance_github_mesh.go | 15 +++++++++++++++ .../acceptance_github_mesh_test.go | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go b/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go index 648c4de2..90423a6b 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go +++ b/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go @@ -161,6 +161,11 @@ func runR1GitHubMeshAcceptance(ctx context.Context, opts r1GitHubMeshAcceptanceO report.Status = "blocked" return report, err } + if err := validateR1GitHubMeshSyncInterval(opts); err != nil { + addR1Error(&report, err) + report.Status = "blocked" + return report, err + } if opts.TokenFile == "" { err := fmt.Errorf("--github-token-file is required") addR1Error(&report, err) @@ -1400,6 +1405,16 @@ func r1GitHubMeshBranchPrefix(prefix string, started time.Time) string { return "mnemon/mnemond-" + started.UTC().Format("20060102T150405Z") + "-" } +func validateR1GitHubMeshSyncInterval(opts r1GitHubMeshAcceptanceOptions) error { + if !opts.AgentTurns { + return nil + } + if opts.SyncInterval < 30*time.Second { + return fmt.Errorf("github mesh agent-turns require --sync-interval >= 30s to protect GitHub API quota (got %s)", opts.SyncInterval) + } + return nil +} + func ensureR1GitHubMeshBranches(ctx context.Context, repo, tokenFile string, branches []string) error { token, err := readR1GitHubMeshToken(tokenFile) if err != nil { diff --git a/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go b/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go index 47e9b616..504d9280 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go @@ -102,6 +102,25 @@ func TestPreflightR1GitHubMeshRateLimitBlocksLowRemaining(t *testing.T) { } } +func TestValidateR1GitHubMeshSyncIntervalProtectsAgentTurns(t *testing.T) { + err := validateR1GitHubMeshSyncInterval(r1GitHubMeshAcceptanceOptions{ + r1CodexAcceptanceOptions: r1CodexAcceptanceOptions{AgentTurns: true}, + SyncInterval: 10 * time.Second, + }) + if err == nil || !strings.Contains(err.Error(), ">= 30s") { + t.Fatalf("expected short sync interval guard, got %v", err) + } + if err := validateR1GitHubMeshSyncInterval(r1GitHubMeshAcceptanceOptions{ + r1CodexAcceptanceOptions: r1CodexAcceptanceOptions{AgentTurns: true}, + SyncInterval: 30 * time.Second, + }); err != nil { + t.Fatalf("30s sync interval should be allowed: %v", err) + } + if err := validateR1GitHubMeshSyncInterval(r1GitHubMeshAcceptanceOptions{SyncInterval: 10 * time.Second}); err != nil { + t.Fatalf("short non-agent-turn sync interval should be allowed: %v", err) + } +} + func TestWriteR1GitHubMeshRemotesCreatesPublishAndSubscribePlan(t *testing.T) { root := t.TempDir() tokenFile := filepath.Join(root, "github.token") From 3289c4e0b0b3d83cb47a091dbf0ac8795d4ded62 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:53:40 +0800 Subject: [PATCH 047/117] feat: persist configured daemon worker snapshot daemon start now records configured watcher, drive, projection, and status workers from product config or the legacy Multica registry before serving. daemon status can then show planned daemon roles without hand-written snapshot fixtures. Invalid product config now blocks snapshot loading instead of being hidden by legacy fallback, and empty configured role entries are skipped rather than rendered as fake workers. Validation: go test -count=1 ./harness/cmd/mnemon-harness ./harness/internal/daemon; go test -count=1 ./harness/...; git diff --name-only -- cmd internal main.go --- .../cmd/mnemon-harness/config_daemon_test.go | 65 ++++++++++++++ harness/cmd/mnemon-harness/daemon.go | 86 +++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index 14a3849e..6066437d 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "path/filepath" "strings" "testing" @@ -190,6 +191,70 @@ func TestDaemonStatusShowsWorkerSnapshot(t *testing.T) { } } +func TestConfiguredDaemonSnapshotIncludesConfiguredRoles(t *testing.T) { + now := time.Date(2026, 6, 29, 9, 50, 0, 0, time.UTC) + cfg := productconfig.Default() + cfg.Daemon.InteractionWatchers = []string{" ", productconfig.ConnectionMultica} + cfg.Daemon.DriveSources = []string{"", productconfig.DriveManagedLocal} + cfg.Daemon.ProjectionSurfaces = []string{"\t", productconfig.ConnectionMultica} + + snapshot := configuredDaemonSnapshot(cfg, now) + for name, want := range map[string]daemon.WorkerKind{ + "multica-watch": daemon.WorkerInteraction, + "managed-drive": daemon.WorkerDrive, + "multica-project": daemon.WorkerProjection, + "status-readiness": daemon.WorkerStatus, + } { + worker, ok := snapshot.Workers[name] + if !ok { + t.Fatalf("snapshot missing worker %q: %+v", name, snapshot.Workers) + } + if worker.Kind != want || worker.Status != "configured" || !worker.StartedAt.Equal(now) || !worker.UpdatedAt.Equal(now) { + t.Fatalf("worker %q mismatch: %+v", name, worker) + } + } + for _, name := range []string{"-watch", "-drive", "-project"} { + if _, ok := snapshot.Workers[name]; ok { + t.Fatalf("snapshot should skip empty worker name %q: %+v", name, snapshot.Workers) + } + } +} + +func TestLoadDaemonSnapshotConfigRejectsInvalidProductConfig(t *testing.T) { + root := t.TempDir() + reg := multicasurface.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-multica", + Participants: []multicasurface.MulticaParticipantRecord{{ + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-planner", + Role: "planner", + }}, + } + if err := multicasurface.SaveMulticaRegistry(multicasurface.MulticaRegistryPath(root, ""), reg); err != nil { + t.Fatal(err) + } + configPath := productconfig.DefaultPath(root, "") + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(configPath, []byte(`{"schema_version":99}`+"\n"), 0o600); err != nil { + t.Fatal(err) + } + + _, ok, err := loadDaemonSnapshotConfig(root) + if err == nil { + t.Fatal("expected invalid product config error") + } + if ok { + t.Fatal("invalid product config should not load through legacy fallback") + } + if !strings.Contains(err.Error(), "unsupported") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestAgentAddAndListWriteProductConfig(t *testing.T) { root := t.TempDir() oldRoot, oldPath := agentRoot, agentConfigPath diff --git a/harness/cmd/mnemon-harness/daemon.go b/harness/cmd/mnemon-harness/daemon.go index 5ce1d17b..2484d9d3 100644 --- a/harness/cmd/mnemon-harness/daemon.go +++ b/harness/cmd/mnemon-harness/daemon.go @@ -3,8 +3,10 @@ package main import ( "fmt" "io" + "os" "path/filepath" "sort" + "strings" "time" "github.com/mnemon-dev/mnemon/harness/internal/app" @@ -78,6 +80,9 @@ func runDaemonStart(cmd *cobra.Command, args []string) error { if err := app.ValidateListenAddr(addr, daemonAllowNonLoopback); err != nil { return err } + if err := writeConfiguredDaemonSnapshot(root, time.Now().UTC()); err != nil { + return err + } fmt.Fprintln(cmd.OutOrStdout(), "Harness daemon: ready") fmt.Fprintln(cmd.OutOrStdout(), "Remote Workspace: "+app.RemoteWorkspaceStatus(root)) return app.RunLocalHTTPServerWithBindings(cmd.Context(), addr, boot.StorePath, boot.Loaded, app.ServeOptions{ @@ -115,6 +120,87 @@ func writeDaemonRoleSummary(out io.Writer, cfg productconfig.Config) { fmt.Fprintf(out, "Harness daemon roles: watchers=%d drive=%d surfaces=%d\n", len(cfg.Daemon.InteractionWatchers), len(cfg.Daemon.DriveSources), len(cfg.Daemon.ProjectionSurfaces)) } +func writeConfiguredDaemonSnapshot(root string, now time.Time) error { + cfg, ok, err := loadDaemonSnapshotConfig(root) + if err != nil || !ok { + return err + } + snapshot := configuredDaemonSnapshot(cfg, now) + if len(snapshot.Workers) == 0 { + return nil + } + return daemon.NewFileSnapshotStore(daemon.StatusSnapshotPath(root, "")).Save(snapshot) +} + +func loadDaemonSnapshotConfig(root string) (productconfig.Config, bool, error) { + configPath := productconfig.DefaultPath(root, "") + if cfg, err := productconfig.Load(configPath); err == nil { + return cfg, true, nil + } else if !os.IsNotExist(err) { + return productconfig.Config{}, false, err + } else if legacy, found, legacyErr := productconfig.FromLegacy(root); legacyErr == nil && found { + return legacy, true, nil + } else if legacyErr != nil { + return productconfig.Config{}, false, legacyErr + } + return productconfig.Config{}, false, nil +} + +func configuredDaemonSnapshot(cfg productconfig.Config, now time.Time) daemon.Snapshot { + snapshot := daemon.Snapshot{StartedAt: now.UTC(), Workers: map[string]daemon.WorkerSnapshot{}} + add := func(name string, kind daemon.WorkerKind, message string) { + name = daemonWorkerName(name) + if name == "" { + return + } + snapshot.Workers[name] = daemon.WorkerSnapshot{ + Kind: kind, + Status: "configured", + Message: strings.TrimSpace(message), + StartedAt: snapshot.StartedAt, + UpdatedAt: snapshot.StartedAt, + } + } + for _, watcher := range cfg.Daemon.InteractionWatchers { + watcher = strings.TrimSpace(watcher) + if watcher == "" { + continue + } + add(watcher+"-watch", daemon.WorkerInteraction, "watcher="+watcher) + } + for _, source := range cfg.Daemon.DriveSources { + source = strings.TrimSpace(source) + if source == "" { + continue + } + name := source + "-drive" + if source == productconfig.DriveManagedLocal { + name = "managed-drive" + } + add(name, daemon.WorkerDrive, "drive="+source) + } + for _, surface := range cfg.Daemon.ProjectionSurfaces { + surface = strings.TrimSpace(surface) + if surface == "" { + continue + } + add(surface+"-project", daemon.WorkerProjection, "surface="+surface) + } + if len(snapshot.Workers) > 0 { + add("status-readiness", daemon.WorkerStatus, "daemon status snapshot") + } + return snapshot +} + +func daemonWorkerName(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + replacer := strings.NewReplacer(" ", "-", "/", "-", "\\", "-", ":", "-") + return replacer.Replace(value) +} + func writeDaemonSnapshotSummary(out io.Writer, root string) { snapshot, ok, err := daemon.NewFileSnapshotStore(daemon.StatusSnapshotPath(root, "")).Load() if err != nil { From 8de743f828e8381ec8bcbefa3879ba018582016b Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 17:59:32 +0800 Subject: [PATCH 048/117] feat: add structured mnemon comment markers Projection comments now carry stable mnemon:type, mnemon:session, and mnemon:assignment markers alongside mnemon:event markers when runtime context is available. Multica runtime comments keep readable Markdown bodies while moving correlation data into explicit markers and metadata. Runtime import comments use the observed draft type and session metadata, assignment mailbox comments use Multica metadata, and progress feedback comments carry progress event/session/assignment markers for deterministic correlation. Validation: go test -count=1 ./harness/internal/projection ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/...; git diff --name-only -- cmd internal main.go --- .../cmd/mnemon-multica-runtime/hub_writer.go | 9 ++++-- harness/cmd/mnemon-multica-runtime/main.go | 15 ++++++---- .../cmd/mnemon-multica-runtime/main_test.go | 10 ++++++- harness/internal/projection/comment.go | 29 +++++++++++++++---- harness/internal/projection/comment_test.go | 12 ++++++-- 5 files changed, 56 insertions(+), 19 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 0cc2d548..dffee62e 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -313,9 +313,12 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. } material := progressFeedbackMaterial(item) commentBody := projection.FormatComment(projection.CommentMaterial{ - Title: "assignment feedback", - Body: multicasurface.ProgressCommentBody(material), - EventIDs: []string{item.EventID}, + Title: "assignment feedback", + Body: multicasurface.ProgressCommentBody(material), + EventIDs: []string{item.EventID}, + EventType: "progress_digest.accepted", + SessionID: result.SessionID, + AssignmentID: item.AssignmentRef, }) comment, err := cli.AddIssueComment(ctx, child, commentBody) if err != nil { diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 6ad37b6a..4d5bc61a 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -441,7 +441,7 @@ func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink mergeRuntimeHubProjectionDeltas(&result, earlyHubDeltas) emitRuntimeCommand(progress, "mnemon multica hub project --issue "+issue.ID, runtimeHubWriteProgress(result), runtimeExitCode(result.HubWriteErr)) emitRuntimeProgress(progress, runtimeHubWriteProgress(result)) - s.projectImportComment(multicaCtx, cli, issue, draft.ExternalID, &result) + s.projectImportComment(multicaCtx, cli, issue, draft.ExternalID, draft.EventType, &result) emitRuntimeCommand(progress, "multica issue comment add "+issue.ID, runtimeProjectionProgress(result), runtimeExitCode(result.ProjectionErr)) emitRuntimeProgress(progress, runtimeProjectionProgress(result)) return result @@ -497,7 +497,7 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr emitRuntimeProgress(progress, runtimeHubWriteProgress(*result)) } } - s.projectImportComment(ctx, cli, issue, multicasurface.AssignmentMailboxMarker(result.HubMetadata, result.IssueID), result) + s.projectImportComment(ctx, cli, issue, multicasurface.AssignmentMailboxMarker(result.HubMetadata, result.IssueID), result.HubMetadata.EventType, result) emitRuntimeCommand(progress, "multica issue comment add "+issue.ID, runtimeProjectionProgress(*result), runtimeExitCode(result.ProjectionErr)) emitRuntimeProgress(progress, runtimeProjectionProgress(*result)) return *result @@ -705,7 +705,7 @@ func mergeRuntimeHubProjectionDeltas(result *runtimeImportResult, deltas []runti } } -func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, externalID string, result *runtimeImportResult) { +func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, externalID, eventType string, result *runtimeImportResult) { if result == nil { return } @@ -718,9 +718,12 @@ func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.M title = "assignment mailbox correlated" } commentBody := projection.FormatComment(projection.CommentMaterial{ - Title: title, - Body: multicasurface.RuntimeProjectionCommentBody(runtimeProjectionMaterial(issue, *result)), - EventIDs: []string{externalID}, + Title: title, + Body: multicasurface.RuntimeProjectionCommentBody(runtimeProjectionMaterial(issue, *result)), + EventIDs: []string{externalID}, + EventType: eventType, + SessionID: result.SessionID, + AssignmentID: result.AssignmentID, }) comment, err := cli.AddIssueComment(ctx, issue.ID, commentBody) if err != nil { diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 77feaf02..85470f3a 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -279,6 +279,7 @@ esac "Principal: `planner@team`", "Managed wake: `completed`", "mnemon:event=multica-task-task-1", + "mnemon:type=teamwork_signal.write_candidate.observed", } { if !strings.Contains(string(comment), want) { t.Fatalf("comment missing %s:\n%s", want, comment) @@ -532,6 +533,8 @@ esac "Mnemon update: issue admitted", "## Mnemon Runtime", "Issue: [TEA-1](mention://issue/root-1)", + "mnemon:type=teamwork_signal.write_candidate.observed", + "mnemon:session=multica:session:root-1", } { if !strings.Contains(comment, want) { t.Fatalf("comment missing %q:\n%s", want, comment) @@ -633,6 +636,8 @@ esac "Mnemon update: issue admitted", "Mnemond ingest: seq=29", "## Mnemon Runtime", + "mnemon:type=teamwork_signal.write_candidate.observed", + "mnemon:session=multica:session:iss-9", } { if !strings.Contains(comment, want) { t.Fatalf("comment missing %q:\n%s", want, comment) @@ -837,7 +842,7 @@ func TestRuntimeCorrelatesAssignmentMailboxWithoutNewIngest(t *testing.T) { printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" case "$*" in *"issue get child-1"*) printf '{"id":"child-1","identifier":"TEA-2","title":"Assignment mailbox","description":"Visible assignment summary only.","status":"todo","metadata":{}}\n' ;; - *"issue metadata list child-1"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-1"},{"key":"mnemon.root_issue_id","value":"root-1"},{"key":"mnemon.assignment_id","value":"asg-1"},{"key":"mnemon.assignment_fingerprint","value":"sha256:abc"},{"key":"mnemon.event_id","value":"event-assignment-1"},{"key":"mnemon.principal","value":"worker@team"}]\n' ;; + *"issue metadata list child-1"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-1"},{"key":"mnemon.root_issue_id","value":"root-1"},{"key":"mnemon.assignment_id","value":"asg-1"},{"key":"mnemon.assignment_fingerprint","value":"sha256:abc"},{"key":"mnemon.event_id","value":"event-assignment-1"},{"key":"mnemon.event_type","value":"assignment.accepted"},{"key":"mnemon.principal","value":"worker@team"}]\n' ;; *"issue comment add child-1"*) cat > "$MULTICA_COMMENT_PATH"; printf '{"id":"comment-child","issue_id":"child-1","content":"ok","type":"comment"}\n' ;; *) printf '{}\n' ;; esac @@ -931,6 +936,9 @@ esac "## Mnemon Runtime", "Root issue: [root-1](mention://issue/root-1)", "mnemon:event=event-assignment-1", + "mnemon:type=assignment.accepted", + "mnemon:session=multica:session:root-1", + "mnemon:assignment=asg-1", "Managed wake: `completed`", } { if !strings.Contains(comment, want) { diff --git a/harness/internal/projection/comment.go b/harness/internal/projection/comment.go index 57b65a71..41e0baad 100644 --- a/harness/internal/projection/comment.go +++ b/harness/internal/projection/comment.go @@ -5,9 +5,12 @@ import ( ) type CommentMaterial struct { - Title string - Body string - EventIDs []string + Title string + Body string + EventIDs []string + EventType string + SessionID string + AssignmentID string } func FormatComment(material CommentMaterial) string { @@ -28,14 +31,28 @@ func FormatComment(material CommentMaterial) string { if ids := cleanStrings(material.EventIDs); len(ids) > 0 { b.WriteString("\n") for _, id := range ids { - b.WriteString("mnemon:event=") - b.WriteString(id) - b.WriteString("\n") + writeMarker(&b, "event", id) } + writeMarker(&b, "type", material.EventType) + writeMarker(&b, "session", material.SessionID) + writeMarker(&b, "assignment", material.AssignmentID) } return strings.TrimSpace(b.String()) } +func writeMarker(b *strings.Builder, key, value string) { + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + if key == "" || value == "" { + return + } + b.WriteString("mnemon:") + b.WriteString(key) + b.WriteString("=") + b.WriteString(value) + b.WriteString("\n") +} + func cleanStrings(values []string) []string { out := make([]string, 0, len(values)) seen := map[string]bool{} diff --git a/harness/internal/projection/comment_test.go b/harness/internal/projection/comment_test.go index 93ce3cd1..c1588f04 100644 --- a/harness/internal/projection/comment_test.go +++ b/harness/internal/projection/comment_test.go @@ -7,15 +7,21 @@ import ( func TestFormatCommentCarriesStableMarkers(t *testing.T) { got := FormatComment(CommentMaterial{ - Title: "assignment finished", - Body: "Worker reported passing evidence.", - EventIDs: []string{"ev-1", "ev-1", "ev-2"}, + Title: "assignment finished", + Body: "Worker reported passing evidence.", + EventIDs: []string{"ev-1", "ev-1", "ev-2"}, + EventType: "progress_digest.accepted", + SessionID: "session-1", + AssignmentID: "asg-1", }) for _, want := range []string{ "Mnemon update: assignment finished", "Worker reported passing evidence.", "mnemon:event=ev-1", "mnemon:event=ev-2", + "mnemon:type=progress_digest.accepted", + "mnemon:session=session-1", + "mnemon:assignment=asg-1", } { if !strings.Contains(got, want) { t.Fatalf("projection comment missing %q:\n%s", want, got) From d6061cdc67756bbc67f186ef0ad4f82f1f0ba7e9 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:02:06 +0800 Subject: [PATCH 049/117] feat: hide harness token admin command mnemon-harness token remains callable for local credential administration, but it is now hidden from the ordinary product help surface and grouped with internal/debug commands. The root help test now guards against exposing token as a product command, matching the experimental harness surface in the R2 cleanup plan. Validation: go test -count=1 ./harness/cmd/mnemon-harness; go test -count=1 ./harness/...; git diff --name-only -- cmd internal main.go --- harness/cmd/mnemon-harness/root_test.go | 2 +- harness/cmd/mnemon-harness/token.go | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index 077dafb3..b215b071 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -32,7 +32,7 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help leaked unsupported product term %q:\n%s", blocked, got) } } - for _, blocked := range []string{" multica", " tower"} { + for _, blocked := range []string{" multica", " tower", " token"} { if strings.Contains(got, blocked) { t.Fatalf("root help leaked debug command %q:\n%s", strings.TrimSpace(blocked), got) } diff --git a/harness/cmd/mnemon-harness/token.go b/harness/cmd/mnemon-harness/token.go index a2ecf392..67e3348b 100644 --- a/harness/cmd/mnemon-harness/token.go +++ b/harness/cmd/mnemon-harness/token.go @@ -15,8 +15,9 @@ import ( var tokenPrincipal string var tokenCmd = &cobra.Command{ - Use: "token", - Short: "Manage local access credentials", + Use: "token", + Short: "Manage local access credentials", + Hidden: true, } var tokenRotateCmd = &cobra.Command{ @@ -39,7 +40,7 @@ var tokenRotateCmd = &cobra.Command{ func init() { tokenRotateCmd.Flags().StringVar(&tokenPrincipal, "principal", "", "principal whose token to rotate") tokenCmd.AddCommand(tokenRotateCmd) - tokenCmd.GroupID = groupSpine + tokenCmd.GroupID = groupAdvanced rootCmd.AddCommand(tokenCmd) } From ae45d2ff575c13b7220046177bf7c520b0fe6fca Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:04:06 +0800 Subject: [PATCH 050/117] fix: reject duplicate harness daemon roles Product config validation now rejects duplicate interaction watchers, projection surfaces, and drive sources after trimming values. This keeps daemon planning and status snapshots from carrying repeated configured roles. Validation: go test -count=1 ./harness/internal/productconfig; go test -count=1 ./harness/...; git diff --name-only -- cmd internal main.go --- harness/internal/productconfig/config.go | 40 ++++++++++++++---- harness/internal/productconfig/config_test.go | 42 +++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go index 473e2d3d..72219fa5 100644 --- a/harness/internal/productconfig/config.go +++ b/harness/internal/productconfig/config.go @@ -193,17 +193,39 @@ func (cfg Config) Validate() error { default: return fmt.Errorf("duplicate_activation_policy must be %q or %q", DuplicateActivationSuppress, DuplicateActivationAllow) } - for _, watcher := range cfg.Daemon.InteractionWatchers { - if err := validateCarrier("interaction watcher", watcher, cfg); err != nil { - return err - } + if err := validateDaemonCarriers("interaction watcher", cfg.Daemon.InteractionWatchers, cfg); err != nil { + return err + } + if err := validateDaemonCarriers("projection surface", cfg.Daemon.ProjectionSurfaces, cfg); err != nil { + return err + } + if err := validateDaemonDriveSources(cfg.Daemon.DriveSources); err != nil { + return err } - for _, surface := range cfg.Daemon.ProjectionSurfaces { - if err := validateCarrier("projection surface", surface, cfg); err != nil { + return nil +} + +func validateDaemonCarriers(label string, values []string, cfg Config) error { + seen := map[string]bool{} + for _, value := range values { + carrier := strings.TrimSpace(value) + if err := validateCarrier(label, carrier, cfg); err != nil { return err } + if carrier == "" { + continue + } + if seen[carrier] { + return fmt.Errorf("duplicate %s %q", label, carrier) + } + seen[carrier] = true } - for _, source := range cfg.Daemon.DriveSources { + return nil +} + +func validateDaemonDriveSources(values []string) error { + seen := map[string]bool{} + for _, source := range values { source = strings.TrimSpace(source) if source == "" { return fmt.Errorf("drive source cannot be empty") @@ -211,6 +233,10 @@ func (cfg Config) Validate() error { if source != DriveManagedLocal { return fmt.Errorf("unsupported drive source %q", source) } + if seen[source] { + return fmt.Errorf("duplicate drive source %q", source) + } + seen[source] = true } return nil } diff --git a/harness/internal/productconfig/config_test.go b/harness/internal/productconfig/config_test.go index 080bb65d..615bdfb0 100644 --- a/harness/internal/productconfig/config_test.go +++ b/harness/internal/productconfig/config_test.go @@ -70,6 +70,48 @@ func TestConfigValidateRejectsDuplicateParticipants(t *testing.T) { } } +func TestConfigValidateRejectsDuplicateDaemonRoles(t *testing.T) { + base := Default() + base.Connections.Multica = MulticaConnection{Enabled: true, Workspace: "ws-multica"} + cases := []struct { + name string + edit func(*Config) + want string + }{ + { + name: "watcher", + edit: func(cfg *Config) { + cfg.Daemon.InteractionWatchers = []string{ConnectionMultica, " " + ConnectionMultica + " "} + }, + want: "duplicate interaction watcher", + }, + { + name: "surface", + edit: func(cfg *Config) { + cfg.Daemon.ProjectionSurfaces = []string{ConnectionMultica, " " + ConnectionMultica} + }, + want: "duplicate projection surface", + }, + { + name: "drive", + edit: func(cfg *Config) { + cfg.Daemon.DriveSources = []string{DriveManagedLocal, " " + DriveManagedLocal} + }, + want: "duplicate drive source", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg := base + tc.edit(&cfg) + err := cfg.Validate() + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("expected %q error, got %v", tc.want, err) + } + }) + } +} + func TestFromLegacyBridgesLocalAndRemoteConfigs(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, ".mnemon", "harness", "local"), 0o755); err != nil { From 1ef89d5f5b9777379fc31cb5a896797709170c47 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:06:40 +0800 Subject: [PATCH 051/117] fix: redact bearer tokens from activation traces Activation trace sanitization now redacts Bearer tokens embedded in command/output text, covering Authorization header shapes that are not simple TOKEN=value assignments. The activationtrace and driver trace tests now guard GitHub/Multica-style bearer values while preserving bounded trace output and the non-canonical trace boundary. Validation: go test -count=1 ./harness/internal/activationtrace ./harness/internal/driver; go test -count=1 ./harness/...; git diff --name-only -- cmd internal main.go --- harness/internal/activationtrace/trace.go | 30 +++++++++++++++++++ .../internal/activationtrace/trace_test.go | 9 +++--- harness/internal/driver/managed_trace_test.go | 9 +++--- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/harness/internal/activationtrace/trace.go b/harness/internal/activationtrace/trace.go index d5d37766..dc539f61 100644 --- a/harness/internal/activationtrace/trace.go +++ b/harness/internal/activationtrace/trace.go @@ -180,9 +180,39 @@ func redactSecrets(key, value string) string { for _, marker := range sensitiveMarkers() { redacted = redactMarkerAssignments(redacted, marker) } + redacted = redactBearerTokens(redacted) return redacted } +func redactBearerTokens(value string) string { + searchFrom := 0 + for { + lower := strings.ToLower(value) + if searchFrom >= len(value) { + return value + } + relative := strings.Index(lower[searchFrom:], "bearer ") + if relative < 0 { + return value + } + tokenStart := searchFrom + relative + len("bearer ") + tokenEnd := tokenStart + for tokenEnd < len(value) { + ch := value[tokenEnd] + if ch == ' ' || ch == '\n' || ch == '\t' || ch == ',' || ch == ';' || ch == '"' || ch == '\'' { + break + } + tokenEnd++ + } + if tokenEnd == tokenStart { + searchFrom = tokenEnd + continue + } + value = value[:tokenStart] + "[redacted]" + value[tokenEnd:] + searchFrom = tokenStart + len("[redacted]") + } +} + func redactMarkerAssignments(value, marker string) string { needle := strings.ToLower(marker) searchFrom := 0 diff --git a/harness/internal/activationtrace/trace_test.go b/harness/internal/activationtrace/trace_test.go index b3df4c01..4eab3627 100644 --- a/harness/internal/activationtrace/trace_test.go +++ b/harness/internal/activationtrace/trace_test.go @@ -15,11 +15,11 @@ func TestEventsFromCodexNotificationsRedactsAndBounds(t *testing.T) { "item": map[string]any{ "type": "commandExecution", "id": "call-1", - "command": "TOKEN=raw-secret multica issue get TEA-1", + "command": "TOKEN=raw-secret curl -H 'Authorization: Bearer ghp_secret' https://api.github.com", "cwd": "/tmp/work", "status": "completed", "exitCode": 0, - "aggregatedOutput": "API_KEY=abc123 " + longOutput, + "aggregatedOutput": "API_KEY=abc123 authorization: bearer mat_secret " + longOutput, "authToken": "must-not-leak", }, }, @@ -33,12 +33,13 @@ func TestEventsFromCodexNotificationsRedactsAndBounds(t *testing.T) { t.Fatalf("unexpected trace event: %+v", got) } joined := got.Command + "\n" + got.Output + "\n" + strings.TrimSpace(testString(got.Item["authToken"])) - for _, forbidden := range []string{"raw-secret", "abc123", "must-not-leak"} { + for _, forbidden := range []string{"raw-secret", "abc123", "must-not-leak", "ghp_secret", "mat_secret"} { if strings.Contains(joined, forbidden) { t.Fatalf("trace leaked %q: %+v", forbidden, got) } } - if !strings.Contains(got.Command, "TOKEN=[redacted]") || !strings.Contains(got.Output, "API_KEY=[redacted]") { + if !strings.Contains(got.Command, "TOKEN=[redacted]") || !strings.Contains(got.Command, "Bearer [redacted]") || + !strings.Contains(got.Output, "API_KEY=[redacted]") || !strings.Contains(got.Output, "bearer [redacted]") { t.Fatalf("trace did not redact command/output: command=%q output=%q", got.Command, got.Output[:80]) } if !strings.Contains(got.Output, "[truncated]") { diff --git a/harness/internal/driver/managed_trace_test.go b/harness/internal/driver/managed_trace_test.go index 2c3e7cf3..404316aa 100644 --- a/harness/internal/driver/managed_trace_test.go +++ b/harness/internal/driver/managed_trace_test.go @@ -16,11 +16,11 @@ func TestManagedTurnTraceEventsFromCodexNotificationsRedactsAndBounds(t *testing "item": map[string]any{ "type": "commandExecution", "id": "call-1", - "command": "TOKEN=raw-secret multica issue get TEA-1", + "command": "TOKEN=raw-secret curl -H 'Authorization: Bearer ghp_secret' https://api.github.com", "cwd": "/tmp/work", "status": "completed", "exitCode": 0, - "aggregatedOutput": "API_KEY=abc123 " + longOutput, + "aggregatedOutput": "API_KEY=abc123 authorization: bearer mat_secret " + longOutput, "authToken": "must-not-leak", }, }, @@ -34,12 +34,13 @@ func TestManagedTurnTraceEventsFromCodexNotificationsRedactsAndBounds(t *testing t.Fatalf("unexpected trace event: %+v", got) } joined := got.Command + "\n" + got.Output + "\n" + strings.TrimSpace(traceTestString(got.Item["authToken"])) - for _, forbidden := range []string{"raw-secret", "abc123", "must-not-leak"} { + for _, forbidden := range []string{"raw-secret", "abc123", "must-not-leak", "ghp_secret", "mat_secret"} { if strings.Contains(joined, forbidden) { t.Fatalf("trace leaked %q: %+v", forbidden, got) } } - if !strings.Contains(got.Command, "TOKEN=[redacted]") || !strings.Contains(got.Output, "API_KEY=[redacted]") { + if !strings.Contains(got.Command, "TOKEN=[redacted]") || !strings.Contains(got.Command, "Bearer [redacted]") || + !strings.Contains(got.Output, "API_KEY=[redacted]") || !strings.Contains(got.Output, "bearer [redacted]") { t.Fatalf("trace did not redact command/output: command=%q output=%q", got.Command, got.Output[:80]) } if !strings.Contains(got.Output, "[truncated]") { From 87f7f43a826f54552beeec9cb702ebd8da5cdb2c Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:12:04 +0800 Subject: [PATCH 052/117] test: guard service command boundaries Add coreguard coverage for the R2 command split: mnemond remains the local event node, mnemon-hub remains the remote event exchange backend, and mnemon-multica-runtime remains an external adapter that may call mnemond access/render APIs without owning local state, hub exchange, or product config. Validation: go test -count=1 ./harness/internal/coreguard; go test -count=1 ./harness/...; git diff --name-only -- cmd internal main.go --- .../coreguard/command_boundary_test.go | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 harness/internal/coreguard/command_boundary_test.go diff --git a/harness/internal/coreguard/command_boundary_test.go b/harness/internal/coreguard/command_boundary_test.go new file mode 100644 index 00000000..243613a0 --- /dev/null +++ b/harness/internal/coreguard/command_boundary_test.go @@ -0,0 +1,119 @@ +package coreguard + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +type commandImportBoundary struct { + dir string + forbids []string + rationale string +} + +var commandImportBoundaries = []commandImportBoundary{ + { + dir: "mnemond", + forbids: []string{ + "harness/internal/mnemonhub", + "harness/internal/productconfig", + "harness/internal/projection", + "harness/internal/surface/", + "harness/cmd/", + }, + rationale: "mnemond is the local event node, not a remote exchange backend or external projection surface", + }, + { + dir: "mnemon-hub", + forbids: []string{ + "harness/internal/activationtrace", + "harness/internal/app", + "harness/internal/codexapp", + "harness/internal/daemon", + "harness/internal/drive", + "harness/internal/driver", + "harness/internal/hostagent", + "harness/internal/productconfig", + "harness/internal/projection", + "harness/internal/runtime", + "harness/internal/surface/", + "harness/cmd/", + }, + rationale: "mnemon-hub is the remote event exchange backend and must not grow local runtime, hostagent, or projection behavior", + }, + { + dir: "mnemon-multica-runtime", + forbids: []string{ + "harness/internal/app", + "harness/internal/codexapp", + "harness/internal/eventstore", + "harness/internal/hostagent", + "harness/internal/mnemond/admission", + "harness/internal/mnemond/state", + "harness/internal/mnemonhub", + "harness/internal/productconfig", + "harness/internal/runtime", + "harness/cmd/", + }, + rationale: "mnemon-multica-runtime is an external adapter; it may call mnemond access/render APIs but must not own local state, hub exchange, or product config", + }, +} + +func TestCommandImportBoundaryGuardLogicIsNotVacuous(t *testing.T) { + if !commandImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemonhub", commandImportBoundaries[0].forbids) { + t.Fatal("mnemond command boundary must flag mnemonhub imports") + } + if !commandImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/app", commandImportBoundaries[1].forbids) { + t.Fatal("mnemon-hub command boundary must flag app imports") + } + if commandImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/access", commandImportBoundaries[2].forbids) { + t.Fatal("mnemon-multica-runtime must be allowed to call mnemond access APIs") + } +} + +func TestServiceCommandsKeepR2Boundaries(t *testing.T) { + for _, boundary := range commandImportBoundaries { + assertCommandAvoidsForbiddenImports(t, boundary) + } +} + +func assertCommandAvoidsForbiddenImports(t *testing.T, boundary commandImportBoundary) { + t.Helper() + root := filepath.Join("..", "..", "cmd", boundary.dir) + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) + if err != nil { + return err + } + for _, imp := range file.Imports { + importPath := strings.Trim(imp.Path.Value, `"`) + if commandImportForbidden(importPath, boundary.forbids) { + t.Errorf("%s imports forbidden package %q: %s", path, importPath, boundary.rationale) + } + } + return nil + }) + if err != nil { + t.Fatalf("walk command %s: %v", boundary.dir, err) + } +} + +func commandImportForbidden(path string, forbids []string) bool { + for _, forbidden := range forbids { + if strings.Contains(path, forbidden) { + return true + } + } + return false +} From 6b5c52492c217f92676c03d03273caccf1ff699a Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:14:12 +0800 Subject: [PATCH 053/117] fix: reject empty daemon carriers in config Product config now rejects empty interaction watcher and projection surface entries, matching drive-source validation and preventing meaningless daemon role declarations from reaching daemon planning/status paths. Validation: go test -count=1 ./harness/internal/productconfig; go test -count=1 ./harness/...; git diff --name-only -- cmd internal main.go --- harness/internal/productconfig/config.go | 6 +++--- harness/internal/productconfig/config_test.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go index 72219fa5..b444450f 100644 --- a/harness/internal/productconfig/config.go +++ b/harness/internal/productconfig/config.go @@ -209,12 +209,12 @@ func validateDaemonCarriers(label string, values []string, cfg Config) error { seen := map[string]bool{} for _, value := range values { carrier := strings.TrimSpace(value) + if carrier == "" { + return fmt.Errorf("%s cannot be empty", label) + } if err := validateCarrier(label, carrier, cfg); err != nil { return err } - if carrier == "" { - continue - } if seen[carrier] { return fmt.Errorf("duplicate %s %q", label, carrier) } diff --git a/harness/internal/productconfig/config_test.go b/harness/internal/productconfig/config_test.go index 615bdfb0..1f295558 100644 --- a/harness/internal/productconfig/config_test.go +++ b/harness/internal/productconfig/config_test.go @@ -85,6 +85,13 @@ func TestConfigValidateRejectsDuplicateDaemonRoles(t *testing.T) { }, want: "duplicate interaction watcher", }, + { + name: "empty watcher", + edit: func(cfg *Config) { + cfg.Daemon.InteractionWatchers = []string{" "} + }, + want: "interaction watcher cannot be empty", + }, { name: "surface", edit: func(cfg *Config) { @@ -92,6 +99,13 @@ func TestConfigValidateRejectsDuplicateDaemonRoles(t *testing.T) { }, want: "duplicate projection surface", }, + { + name: "empty surface", + edit: func(cfg *Config) { + cfg.Daemon.ProjectionSurfaces = []string{""} + }, + want: "projection surface cannot be empty", + }, { name: "drive", edit: func(cfg *Config) { From 375d5db652372547b4409af6e167c866e4c1083c Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:19:45 +0800 Subject: [PATCH 054/117] fix: clean multica visible projection text Keep assignment and feedback correlation in metadata or stable markers while rendering Multica issue comments with human-readable feedback and projection status text. Validation: go test -count=1 ./harness/internal/surface/multica; go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- .../cmd/mnemon-multica-runtime/main_test.go | 14 ++++- .../internal/surface/multica/projection.go | 55 +++++++++++++++++-- .../surface/multica/projection_test.go | 23 ++++++-- 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 85470f3a..4dca02cb 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -828,8 +828,18 @@ esac } } comment := mustReadRuntimeTestFile(t, commentPath) - if !strings.Contains(comment, "Multica hub write: created child_issues=1") { - t.Fatalf("root comment missing hub write summary:\n%s", comment) + for _, want := range []string{ + "Projection status: updates created", + "Assignments created: 1", + } { + if !strings.Contains(comment, want) { + t.Fatalf("root comment missing %q:\n%s", want, comment) + } + } + for _, blocked := range []string{"Multica hub write", "child_issues", "feedback_comments"} { + if strings.Contains(comment, blocked) { + t.Fatalf("root comment must not expose machine field %q:\n%s", blocked, comment) + } } } diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index bf65e23a..1c6fd7b5 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -97,9 +97,10 @@ func AssignmentMailboxDescription(item AssignmentMailboxMaterial) string { func ProgressCommentBody(item ProgressFeedbackMaterial) string { var b strings.Builder - b.WriteString("## Assignment Feedback\n\n") - writeBullet(&b, "Assignment", codeSpan(item.AssignmentRef)) - writeBullet(&b, "Feedback", codeSpan(item.FeedbackKind)) + b.WriteString("## Feedback\n\n") + if status := visibleFeedbackStatus(item.FeedbackKind); status != "" { + writeBullet(&b, "Status", status) + } if summary := strings.TrimSpace(item.Summary); summary != "" { b.WriteString("\n## Summary\n\n") b.WriteString(summary) @@ -146,7 +147,13 @@ func RuntimeProjectionCommentBody(item RuntimeProjectionMaterial) string { writeBullet(&b, "Managed wake", codeSpan(item.WakeStatus)) writeBullet(&b, "Managed turn", codeSpan(item.WakeTurnID)) if item.HubWriteStatus != "" { - writeBullet(&b, "Multica hub write", fmt.Sprintf("%s child_issues=%d feedback_comments=%d", item.HubWriteStatus, item.HubChildIssues, item.HubFeedbackComment)) + writeBullet(&b, "Projection status", visibleProjectionStatus(item.HubWriteStatus)) + if item.HubChildIssues > 0 { + writeBullet(&b, "Assignments created", fmt.Sprintf("%d", item.HubChildIssues)) + } + if item.HubFeedbackComment > 0 { + writeBullet(&b, "Feedback comments added", fmt.Sprintf("%d", item.HubFeedbackComment)) + } } } return strings.TrimSpace(b.String()) @@ -290,6 +297,46 @@ func visibleExpectedFeedback(value string) string { return value } +func visibleFeedbackStatus(value string) string { + switch strings.ToLower(strings.TrimSpace(value)) { + case "": + return "" + case "progress": + return "progress update" + case "result": + return "result" + case "blocker": + return "blocked" + case "review": + return "ready for review" + case "waiting": + return "waiting" + case "cancelled", "canceled": + return "cancelled" + default: + return strings.ReplaceAll(strings.TrimSpace(value), "_", " ") + } +} + +func visibleProjectionStatus(value string) string { + switch strings.ToLower(strings.TrimSpace(value)) { + case "": + return "" + case "created": + return "updates created" + case "updated": + return "updates synced" + case "commented": + return "feedback posted" + case "skipped": + return "no visible updates needed" + case "failed": + return "update failed" + default: + return strings.ReplaceAll(strings.TrimSpace(value), "_", " ") + } +} + func writeBullet(b *strings.Builder, label, value string) { value = strings.TrimSpace(value) if value == "" { diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index ac3262d6..243f981a 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -78,9 +78,12 @@ func TestProgressFeedbackMaterial(t *testing.T) { } body := ProgressCommentBody(item) for _, want := range []string{ - "## Assignment Feedback", - "Assignment: `asg-1`", - "Feedback: `result`", + "## Feedback", + "Status: result", + "## Summary", + "Done", + "## Result", + "Validated", "## Artifacts", "- run-1", "## Evidence", @@ -90,6 +93,11 @@ func TestProgressFeedbackMaterial(t *testing.T) { t.Fatalf("progress body missing %q:\n%s", want, body) } } + for _, blocked := range []string{"Assignment: `asg-1`", "Feedback: `result`", "progress_digest", "assignment_ref", "mnemon."} { + if strings.Contains(body, blocked) { + t.Fatalf("progress body must keep machine field %q in metadata/markers, not visible text:\n%s", blocked, body) + } + } if got := ProgressIssueStatus(item); got != "done" { t.Fatalf("progress status = %q", got) } @@ -127,12 +135,19 @@ func TestRuntimeProjectionCommentBodyForIntake(t *testing.T) { "Mnemond ingest: seq=42 duplicate=false ticked=true", "Managed wake: `completed`", "Managed turn: `turn-1`", - "Multica hub write: created child_issues=2 feedback_comments=1", + "Projection status: updates created", + "Assignments created: 2", + "Feedback comments added: 1", } { if !strings.Contains(body, want) { t.Fatalf("runtime projection body missing %q:\n%s", want, body) } } + for _, blocked := range []string{"Hub backend:", "Multica hub write", "child_issues", "feedback_comments"} { + if strings.Contains(body, blocked) { + t.Fatalf("runtime projection body must not expose machine field %q:\n%s", blocked, body) + } + } } func TestRuntimeProjectionCommentBodyForAssignmentMailbox(t *testing.T) { From 50e4c8ee0b636780b1db05a3a508e6c5beeb6cb6 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:23:43 +0800 Subject: [PATCH 055/117] test: require structured multica child text Extend the Multica hub-flow acceptance report with a visible text contract for assignment child issues, checking sectioned Markdown and blocking protocol metadata from issue descriptions. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance; go test -count=1 ./harness/... --- .../acceptance_multica_runtime.go | 43 +++++++++++++++++++ .../acceptance_multica_runtime_test.go | 14 +++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 8bb3999a..2690fc4c 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -372,6 +372,8 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o return err } addMulticaProdSimAssertion(report, "hub-flow projects feedback comments and completion statuses", multicaHubProjectionComplete(finalRoot, finalChildren, childComments), fmt.Sprintf("root=%s children=%v comments=%d", finalRoot.Status, multicaIssueStatuses(finalChildren), multicaCommentCount(childComments))) + visibleOK, visibleDetail := multicaAssignmentChildrenUseStructuredVisibleText(finalChildren) + addMulticaProdSimAssertion(report, "assignment child issue visible text is structured", visibleOK, visibleDetail) return nil } @@ -659,6 +661,47 @@ func multicaHubProjectionComplete(root driver.MulticaIssue, children []driver.Mu return true } +func multicaAssignmentChildrenUseStructuredVisibleText(children []driver.MulticaIssue) (bool, string) { + if len(children) == 0 { + return false, "children=0" + } + var failures []string + for _, child := range children { + label := firstNonEmptyString(child.Identifier, child.ID) + body := child.Description + for _, want := range []string{ + "## Assignment", + "## Context", + "Root issue: [", + "](mention://issue/", + "Assignee:", + "## Feedback", + } { + if !strings.Contains(body, want) { + failures = append(failures, fmt.Sprintf("%s missing %q", label, want)) + } + } + lower := strings.ToLower(body) + for _, blocked := range []string{ + "mnemon.", + "session:", + "assignment: `", + "assignment_ref", + "progress_digest", + "hub backend", + "projection owner", + } { + if strings.Contains(lower, blocked) { + failures = append(failures, fmt.Sprintf("%s exposes %q", label, blocked)) + } + } + } + if len(failures) > 0 { + return false, strings.Join(failures, "; ") + } + return true, fmt.Sprintf("children=%d", len(children)) +} + func multicaCommentsContainFeedbackMarker(comments []driver.MulticaComment) bool { for _, comment := range comments { content := strings.ToLower(comment.Content) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index b59b3e6e..4ca2bc74 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -124,8 +124,8 @@ cat >> "$MULTICA_STDIN_PATH" case "$*" in *"issue create"*) printf '{"id":"root-9","identifier":"TEA-9","title":"Runtime hub flow","description":"Teamwork acceptance","status":"todo"}\n' ;; *"issue get root-9"*) printf '{"id":"root-9","identifier":"TEA-9","title":"Runtime hub flow","description":"Teamwork acceptance","status":"done"}\n' ;; - *"issue get child-2"*) printf '{"id":"child-2","identifier":"TEA-10","title":"Assignment 2","status":"done"}\n' ;; - *"issue get child-3"*) printf '{"id":"child-3","identifier":"TEA-11","title":"Assignment 3","status":"done"}\n' ;; + *"issue get child-2"*) printf '%s\n' '{"id":"child-2","identifier":"TEA-10","title":"TEA-9: routing check","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-9](mention://issue/root-9) - Runtime hub flow\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing check\n\n## Feedback\n\n- Expected feedback: result or blocker\n- Progress path: Mnemon runtime progress, result, or blocker feedback","status":"done"}' ;; + *"issue get child-3"*) printf '%s\n' '{"id":"child-3","identifier":"TEA-11","title":"TEA-9: runtime display","description":"## Assignment\n\nCheck runtime display.\n\n## Context\n\n- Root issue: [TEA-9](mention://issue/root-9) - Runtime hub flow\n- Assignee: implementer@team (mnemon-implementer)\n- Scope: runtime display\n\n## Feedback\n\n- Expected feedback: result or blocker\n- Progress path: Mnemon runtime progress, result, or blocker feedback","status":"done"}' ;; *"issue metadata list root-9"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"session_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-9"}]\n' ;; *"issue children root-9"*) printf '{"children":[{"id":"child-2","identifier":"TEA-10","title":"Assignment 2","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-2","mnemon.principal":"researcher@team"}},{"id":"child-3","identifier":"TEA-11","title":"Assignment 3","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-3","mnemon.principal":"implementer@team"}}]}\n' ;; *"issue comment list root-9"*) printf '[{"id":"comment-root","issue_id":"root-9","content":"Mnemon update: issue admitted\\n\\nmnemon:event=multica-task-root"}]\n' ;; @@ -174,6 +174,16 @@ esac if !multicaProdSimAssertionsPassed(report) { t.Fatalf("hub assertions failed: %+v", report.Assertions) } + var visibleTextAssertion bool + for _, assertion := range report.Assertions { + if assertion.Name == "assignment child issue visible text is structured" { + visibleTextAssertion = assertion.Passed + break + } + } + if !visibleTextAssertion { + t.Fatalf("missing or failed visible text assertion: %+v", report.Assertions) + } args, err := os.ReadFile(argsPath) if err != nil { t.Fatal(err) From 4aa13356d1ea83504db5f4dbd6142a430c51de4f Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:26:10 +0800 Subject: [PATCH 056/117] test: cover multica progress comment text Extend the Multica runtime hub-flow test to project progress feedback into a child issue comment, asserting that stable markers carry assignment correlation while visible feedback text stays human-readable. Validation: go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- .../cmd/mnemon-multica-runtime/main_test.go | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 4dca02cb..2e44116c 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -656,6 +656,7 @@ func TestRuntimeWritesAssignmentMailboxChildIssueFromView(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "multica.args") commentPath := filepath.Join(tmp, "comment.txt") + childCommentPath := filepath.Join(tmp, "child-comment.txt") createDescriptionPath := filepath.Join(tmp, "child-description.txt") bin := filepath.Join(tmp, "multica") script := `#!/usr/bin/env sh @@ -667,6 +668,8 @@ case "$*" in *"issue create"*) cat > "$MULTICA_CREATE_DESCRIPTION_PATH"; printf '{"id":"child-2","identifier":"TEA-11","title":"Mnemon assignment asg-writer","status":"todo","metadata":{}}\n' ;; *"issue metadata set child-2"*) printf '{}\n' ;; *"issue comment add root-2"*) cat > "$MULTICA_COMMENT_PATH"; printf '{"id":"comment-root-2","issue_id":"root-2","content":"ok","type":"comment"}\n' ;; + *"issue comment add child-2"*) cat > "$MULTICA_CHILD_COMMENT_PATH"; printf '{"id":"comment-child-2","issue_id":"child-2","content":"ok","type":"comment"}\n' ;; + *"issue status child-2"*) printf '{"id":"child-2","status":"in_progress"}\n' ;; *) printf '{}\n' ;; esac ` @@ -739,6 +742,18 @@ esac "evidence_refs": []any{"multica:issue:root-2"}, }, }}}, + }, { + Ref: contract.ResourceRef{Kind: "progress_digest", ID: "project"}, + Fields: map[string]any{"items": []any{map[string]any{ + "id": "pg-writer", + "event_id": "pg-writer", + "ingest_seq": float64(33), + "actor": "worker@team", + "assignment_ref": "asg-writer", + "feedback_kind": "progress", + "summary": "Checked release notes against the public changelog.", + "artifact_refs": []any{"multica:issue:root-2"}, + }}}, }}, }) default: @@ -760,6 +775,7 @@ esac "MNEMON_MULTICA_HUB_LEDGER="+filepath.Join(tmp, "hub-ledger.jsonl"), "MULTICA_ARGS_PATH="+argsPath, "MULTICA_COMMENT_PATH="+commentPath, + "MULTICA_CHILD_COMMENT_PATH="+childCommentPath, "MULTICA_CREATE_DESCRIPTION_PATH="+createDescriptionPath, "MULTICA_TASK_ID=task-root-2", "MULTICA_AGENT_ID=agent-planner", @@ -772,7 +788,7 @@ esac if err != nil { t.Fatal(err) } - if !strings.Contains(out.String(), "Multica hub write: created child_issues=1") { + if !strings.Contains(out.String(), "Multica hub write: updated child_issues=1 feedback_comments=1") { t.Fatalf("runtime output missing hub write evidence:\n%s", out.String()) } args := mustReadRuntimeTestFile(t, argsPath) @@ -804,8 +820,10 @@ esac if supplementalIdx < 0 || !(assignIdx < supplementalIdx) { t.Fatalf("non-dispatch metadata must not block child assignment; args:\n%s", args) } - if strings.Contains(args, "issue status child-2 in_progress") { - t.Fatalf("assignment mailbox should be created in progress without a separate status call:\n%s", args) + childCommentIdx := strings.Index(args, "issue comment add child-2") + childStatusIdx := strings.Index(args, "issue status child-2 in_progress") + if childCommentIdx < 0 || childStatusIdx < 0 || !(childCommentIdx < childStatusIdx) { + t.Fatalf("progress feedback should comment before updating child status; args:\n%s", args) } description := mustReadRuntimeTestFile(t, createDescriptionPath) for _, want := range []string{ @@ -829,8 +847,9 @@ esac } comment := mustReadRuntimeTestFile(t, commentPath) for _, want := range []string{ - "Projection status: updates created", + "Projection status: updates synced", "Assignments created: 1", + "Feedback comments added: 1", } { if !strings.Contains(comment, want) { t.Fatalf("root comment missing %q:\n%s", want, comment) @@ -841,6 +860,27 @@ esac t.Fatalf("root comment must not expose machine field %q:\n%s", blocked, comment) } } + childComment := mustReadRuntimeTestFile(t, childCommentPath) + for _, want := range []string{ + "Mnemon update: assignment feedback", + "## Feedback", + "Status: progress update", + "## Summary", + "Checked release notes against the public changelog.", + "mnemon:event=pg-writer", + "mnemon:type=progress_digest.accepted", + "mnemon:session=multica:session:root-2", + "mnemon:assignment=asg-writer", + } { + if !strings.Contains(childComment, want) { + t.Fatalf("child comment missing %q:\n%s", want, childComment) + } + } + for _, blocked := range []string{"Assignment: `asg-writer`", "Feedback: `progress`", "assignment_ref"} { + if strings.Contains(childComment, blocked) { + t.Fatalf("child comment must not expose machine field %q:\n%s", blocked, childComment) + } + } } func TestRuntimeCorrelatesAssignmentMailboxWithoutNewIngest(t *testing.T) { From 398466c2d3089e225abe8de19d162567270e54c3 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 18:41:08 +0800 Subject: [PATCH 057/117] test: preflight github mesh repository access Block the live GitHub mesh acceptance before expensive setup when the configured token cannot access the target repository, and persist the credential failure in the report. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance; go test -count=1 ./harness/... --- .../acceptance_github_mesh.go | 56 +++++++++++++++- .../acceptance_github_mesh_test.go | 66 +++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go b/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go index 90423a6b..e6c84342 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go +++ b/harness/cmd/mnemon-acceptance/acceptance_github_mesh.go @@ -33,6 +33,7 @@ var ( var ( r1GitHubMeshRateLimitAPIURL = "https://api.github.com/rate_limit" + r1GitHubMeshAPIBaseURL = "https://api.github.com" r1GitHubMeshHTTPClient = &http.Client{Timeout: 10 * time.Second} ) @@ -184,6 +185,8 @@ func runR1GitHubMeshAcceptance(ctx context.Context, opts r1GitHubMeshAcceptanceO report.Status = "blocked" return report, err } + report.Artifacts["github_repo"] = opts.Repo + report.Artifacts["github_token_file"] = tokenFile rateLimit, err := preflightR1GitHubMeshRateLimit(ctx, tokenFile, r1GitHubMeshMinimumRateLimitRemaining(opts)) if rateLimit.Limit > 0 || !rateLimit.ResetAt.IsZero() { report.Artifacts["github_rate_limit_remaining"] = fmt.Sprintf("%d", rateLimit.Remaining) @@ -195,6 +198,11 @@ func runR1GitHubMeshAcceptance(ctx context.Context, opts r1GitHubMeshAcceptanceO report.Status = "blocked" return report, err } + if err := preflightR1GitHubMeshRepositoryAccess(ctx, opts.Repo, tokenFile); err != nil { + addR1Error(&report, err) + report.Status = "blocked" + return report, err + } branches := r1GitHubMeshBranches(branchPrefix, opts.Agents) if err := ensureR1GitHubMeshBranches(ctx, opts.Repo, tokenFile, branches); err != nil { addR1Error(&report, err) @@ -209,8 +217,6 @@ func runR1GitHubMeshAcceptance(ctx context.Context, opts r1GitHubMeshAcceptanceO } sourceCodexHome := resolveSourceCodexHome(opts.CodexHome) report.Artifacts["codex_home_source"] = sourceCodexHome - report.Artifacts["github_repo"] = opts.Repo - report.Artifacts["github_token_file"] = tokenFile report.Artifacts["github_branch_prefix"] = branchPrefix report.Artifacts["github_sync_interval"] = opts.SyncInterval.String() initialOnline := opts.Agents @@ -1472,6 +1478,52 @@ func preflightR1GitHubMeshRateLimit(ctx context.Context, tokenFile string, minRe return limit, nil } +func preflightR1GitHubMeshRepositoryAccess(ctx context.Context, repo, tokenFile string) error { + token, err := readR1GitHubMeshToken(tokenFile) + if err != nil { + return err + } + return fetchR1GitHubMeshRepositoryAccess(ctx, repo, token) +} + +func fetchR1GitHubMeshRepositoryAccess(ctx context.Context, repo, token string) error { + repo, err := exchange.NormalizeGitHubRepo(repo) + if err != nil { + return err + } + baseURL := strings.TrimRight(strings.TrimSpace(r1GitHubMeshAPIBaseURL), "/") + if baseURL == "" { + baseURL = "https://api.github.com" + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/repos/"+repo, nil) + if err != nil { + return err + } + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + req.Header.Set("User-Agent", "mnemon-acceptance") + if strings.TrimSpace(token) != "" { + req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token)) + } + client := r1GitHubMeshHTTPClient + if client == nil { + client = http.DefaultClient + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode >= 300 { + return fmt.Errorf("github repo access status %d for %s: %s", resp.StatusCode, repo, strings.TrimSpace(string(body))) + } + return nil +} + func fetchR1GitHubMeshRateLimit(ctx context.Context, token string) (r1GitHubMeshRateLimit, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, r1GitHubMeshRateLimitAPIURL, nil) if err != nil { diff --git a/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go b/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go index 504d9280..c9ab9027 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_github_mesh_test.go @@ -102,6 +102,72 @@ func TestPreflightR1GitHubMeshRateLimitBlocksLowRemaining(t *testing.T) { } } +func TestR1GitHubMeshAcceptanceBlocksInvalidRepositoryTokenBeforeSetup(t *testing.T) { + tmp := t.TempDir() + var repoChecked bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer secret-token" { + t.Fatalf("authorization header = %q", got) + } + switch r.URL.Path { + case "/rate_limit": + _, _ = w.Write([]byte(`{"resources":{"core":{"limit":5000,"remaining":5000,"reset":1782725122,"used":0}}}`)) + case "/repos/mnemon-dev/mnemon-teamwork-example": + repoChecked = true + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message":"Bad credentials"}`)) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer server.Close() + oldRateLimitURL := r1GitHubMeshRateLimitAPIURL + oldAPIBaseURL := r1GitHubMeshAPIBaseURL + oldClient := r1GitHubMeshHTTPClient + r1GitHubMeshRateLimitAPIURL = server.URL + "/rate_limit" + r1GitHubMeshAPIBaseURL = server.URL + r1GitHubMeshHTTPClient = server.Client() + t.Cleanup(func() { + r1GitHubMeshRateLimitAPIURL = oldRateLimitURL + r1GitHubMeshAPIBaseURL = oldAPIBaseURL + r1GitHubMeshHTTPClient = oldClient + }) + + tokenFile := filepath.Join(tmp, "github.token") + if err := os.WriteFile(tokenFile, []byte("secret-token\n"), 0o600); err != nil { + t.Fatal(err) + } + report, err := runR1GitHubMeshAcceptance(context.Background(), r1GitHubMeshAcceptanceOptions{ + r1CodexAcceptanceOptions: r1CodexAcceptanceOptions{ + RunRoot: filepath.Join(tmp, "run"), + AgentTurns: true, + TurnTimeout: time.Millisecond, + }, + Repo: "mnemon-dev/mnemon-teamwork-example", + TokenFile: tokenFile, + SyncInterval: 30 * time.Second, + }) + if err == nil || !strings.Contains(err.Error(), "github repo access status 401") || !strings.Contains(err.Error(), "Bad credentials") { + t.Fatalf("expected invalid repo token blocker, got report=%+v err=%v", report, err) + } + if !repoChecked { + t.Fatal("repository access preflight was not called") + } + if report.Status != "blocked" || !strings.Contains(strings.Join(report.Errors, "\n"), "Bad credentials") { + t.Fatalf("report should be blocked with repo credential error: %+v", report) + } + if _, err := os.Stat(filepath.Join(report.RunRoot, "bin")); !os.IsNotExist(err) { + t.Fatalf("acceptance should block before installing binaries, stat err=%v", err) + } + data, err := os.ReadFile(report.ReportPath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), "github repo access status 401") { + t.Fatalf("written report missing repo access blocker:\n%s", data) + } +} + func TestValidateR1GitHubMeshSyncIntervalProtectsAgentTurns(t *testing.T) { err := validateR1GitHubMeshSyncInterval(r1GitHubMeshAcceptanceOptions{ r1CodexAcceptanceOptions: r1CodexAcceptanceOptions{AgentTurns: true}, From 5514dbb163bb5e5af32bc508c2e136b34ff144ce Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 19:13:01 +0800 Subject: [PATCH 058/117] fix: recover multica feedback targets from hub metadata When the local Multica hub ledger is missing, recover assignment child issue targets from Multica child issue metadata before projecting progress feedback. This keeps Multica usable as the hub of record across runtime restarts. Validation: go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 30 ++++ .../cmd/mnemon-multica-runtime/main_test.go | 133 ++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index dffee62e..48a6ee46 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -308,6 +308,12 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if err != nil { return err } + if !ok { + child, ok, err = findAssignmentTargetFromMulticaHub(ctx, cli, result.RootIssueID, result.SessionID, item.AssignmentRef) + if err != nil { + return err + } + } if !ok { continue } @@ -694,6 +700,30 @@ func findAssignmentTargetFromLedger(ledger *driver.FileMulticaHubLedger, session return "", false, nil } +func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaCLI, rootIssueID, sessionID, assignmentID string) (string, bool, error) { + children, err := cli.ListIssueChildren(ctx, rootIssueID) + if err != nil { + return "", false, err + } + for _, child := range children { + meta := driver.MulticaIssueHubMetadata(child) + if !meta.IsAssignmentMailbox() { + listed, err := cli.ListIssueMetadata(ctx, child.ID) + if err != nil { + return "", false, err + } + meta = driver.ParseMulticaHubMetadata(stringMapToAny(listed)) + } + if !meta.IsAssignmentMailbox() { + continue + } + if meta.SessionID == sessionID && meta.AssignmentID == assignmentID && strings.TrimSpace(child.ID) != "" { + return child.ID, true, nil + } + } + return "", false, nil +} + func allMulticaAssignmentChildrenDone(ctx context.Context, cli driver.MulticaCLI, rootIssueID, sessionID, justCompletedChildID string) (bool, error) { children, err := cli.ListIssueChildren(ctx, rootIssueID) if err != nil { diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 2e44116c..101ff003 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -883,6 +883,139 @@ esac } } +func TestRuntimeProjectsProgressToExistingAssignmentMailboxFromHubMetadata(t *testing.T) { + tmp := t.TempDir() + argsPath := filepath.Join(tmp, "multica.args") + rootCommentPath := filepath.Join(tmp, "root-comment.txt") + childCommentPath := filepath.Join(tmp, "child-comment.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue get root-existing"*) printf '{"id":"root-existing","identifier":"TEA-20","title":"Existing assignment mailbox","description":"Project progress after runtime restart.","status":"todo","priority":"medium"}\n' ;; + *"issue metadata set root-existing"*) printf '{}\n' ;; + *"issue children root-existing"*) printf '{"children":[{"id":"child-existing","identifier":"TEA-21","title":"TEA-20: existing mailbox","status":"todo","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-existing","mnemon.assignment_id":"asg-existing","mnemon.principal":"worker@team","mnemon.root_issue_id":"root-existing"}}]}\n' ;; + *"issue comment add child-existing"*) cat > "$MULTICA_CHILD_COMMENT_PATH"; printf '{"id":"comment-child-existing","issue_id":"child-existing","content":"ok","type":"comment"}\n' ;; + *"issue comment add root-existing"*) cat > "$MULTICA_ROOT_COMMENT_PATH"; printf '{"id":"comment-root-existing","issue_id":"root-existing","content":"ok","type":"comment"}\n' ;; + *"issue status child-existing"*) printf '{"id":"child-existing","status":"done"}\n' ;; + *"issue status root-existing"*) printf '{"id":"root-existing","status":"done"}\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + registryPath := filepath.Join(tmp, "registry.json") + if err := driver.SaveMulticaRegistry(registryPath, driver.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-1", + Participants: []driver.MulticaParticipantRecord{{ + Principal: "worker@team", + AgentName: "mnemon-worker", + AgentID: "agent-worker", + Role: "implementer", + }}, + }); err != nil { + t.Fatal(err) + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/ingest": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(access.IngestReceipt{Seq: 41, Dup: false, Ticked: true}) + case "/presentation-view": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(pview.View{ + Ref: "view-existing-progress", + Digest: "digest-existing-progress", + Content: []pview.ResourceContent{{ + Ref: contract.ResourceRef{Kind: "progress_digest", ID: "project"}, + Fields: map[string]any{"items": []any{map[string]any{ + "id": "pg-existing", + "event_id": "pg-existing", + "ingest_seq": float64(42), + "actor": "worker@team", + "assignment_ref": "asg-existing", + "feedback_kind": "result", + "summary": "Validated the existing assignment mailbox after restart.", + "result": "Existing Multica child issue received feedback without a local assignment ledger entry.", + }}}, + }}, + }) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer srv.Close() + + input := `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"input":[{"type":"text","text":"Your assigned issue ID is: root-existing"}]}}` + "\n" + var out bytes.Buffer + err := runRuntime(runtimeConfig{ + Args: []string{"app-server", "--listen", "stdio://"}, + Env: runtimeTestEnv( + "MNEMON_MULTICA_BIN="+bin, + "MNEMON_HUB_BACKEND=multica", + "MNEMON_CONTROL_ADDR="+srv.URL, + "MNEMON_CONTROL_PRINCIPAL=planner@team", + "MNEMON_MULTICA_REGISTRY="+registryPath, + "MNEMON_MULTICA_HUB_LEDGER="+filepath.Join(tmp, "empty-hub-ledger.jsonl"), + "MULTICA_ARGS_PATH="+argsPath, + "MULTICA_ROOT_COMMENT_PATH="+rootCommentPath, + "MULTICA_CHILD_COMMENT_PATH="+childCommentPath, + "MULTICA_TASK_ID=task-root-existing", + "MULTICA_AGENT_ID=agent-planner", + ), + CWD: tmp, + Stdin: strings.NewReader(input), + Stdout: &out, + Now: fixedRuntimeTime, + }) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "Multica hub write: commented feedback_comments=1") { + t.Fatalf("runtime output missing recovered feedback projection:\n%s", out.String()) + } + args := mustReadRuntimeTestFile(t, argsPath) + if strings.Contains(args, "issue create") || strings.Contains(args, "issue assign child-existing") { + t.Fatalf("existing assignment mailbox should be reused, not recreated or reassigned:\n%s", args) + } + for _, want := range []string{ + "issue children root-existing --output json", + "issue comment add child-existing --content-stdin --output json", + "issue status child-existing done --output json", + "issue status root-existing done --output json", + } { + if !strings.Contains(args, want) { + t.Fatalf("args missing %q:\n%s", want, args) + } + } + childComment := mustReadRuntimeTestFile(t, childCommentPath) + for _, want := range []string{ + "Mnemon update: assignment feedback", + "Status: result", + "Existing Multica child issue received feedback without a local assignment ledger entry.", + "mnemon:event=pg-existing", + "mnemon:type=progress_digest.accepted", + "mnemon:session=multica:session:root-existing", + "mnemon:assignment=asg-existing", + } { + if !strings.Contains(childComment, want) { + t.Fatalf("child comment missing %q:\n%s", want, childComment) + } + } + rootComment := mustReadRuntimeTestFile(t, rootCommentPath) + for _, want := range []string{ + "Projection status: feedback posted", + "Feedback comments added: 1", + } { + if !strings.Contains(rootComment, want) { + t.Fatalf("root comment missing %q:\n%s", want, rootComment) + } + } +} + func TestRuntimeCorrelatesAssignmentMailboxWithoutNewIngest(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "multica.args") From 06a61a043923d9df87b15b0efceb23220834fd80 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 19:15:09 +0800 Subject: [PATCH 059/117] fix: dispatch multica assignment event metadata Write assignment event type and phase as dispatch-critical child issue metadata before assigning the Multica Agent, so assignment mailbox activations carry the standard Mnemon correlation fields immediately. Validation: go test -count=1 ./harness/internal/surface/multica; go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- harness/cmd/mnemon-multica-runtime/main_test.go | 7 +++++++ harness/internal/surface/multica/hub.go | 2 ++ harness/internal/surface/multica/hub_test.go | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 101ff003..d440c5dc 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -796,6 +796,8 @@ esac "issue children root-2 --output json", "issue create --title TEA-10: check release notes --output json --description-stdin --parent root-2 --status in_progress --priority medium", "issue metadata set child-2 --key mnemon.kind --value assignment_mailbox --type string --output json", + "issue metadata set child-2 --key mnemon.event_type --value assignment.accepted --type string --output json", + "issue metadata set child-2 --key mnemon.event_phase --value accepted --type string --output json", "issue metadata set child-2 --key mnemon.assignment_id --value asg-writer --type string --output json", "issue metadata set child-2 --key mnemon.principal --value worker@team --type string --output json", "issue assign child-2 --to-id agent-worker --output json", @@ -816,6 +818,11 @@ esac if createIdx < 0 || metaIdx < 0 || assignIdx < 0 || !(createIdx < metaIdx && metaIdx < assignIdx) { t.Fatalf("assignment mailbox must be created, tagged, then assigned; args:\n%s", args) } + eventTypeIdx := strings.Index(args, "issue metadata set child-2 --key mnemon.event_type") + eventPhaseIdx := strings.Index(args, "issue metadata set child-2 --key mnemon.event_phase") + if eventTypeIdx < 0 || eventPhaseIdx < 0 || !(eventTypeIdx < assignIdx && eventPhaseIdx < assignIdx) { + t.Fatalf("assignment mailbox dispatch metadata must include event type/phase before assignment; args:\n%s", args) + } supplementalIdx := strings.Index(args, "issue metadata set child-2 --key mnemon.projection_owner") if supplementalIdx < 0 || !(assignIdx < supplementalIdx) { t.Fatalf("non-dispatch metadata must not block child assignment; args:\n%s", args) diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go index aa75dbf5..26e63a81 100644 --- a/harness/internal/surface/multica/hub.go +++ b/harness/internal/surface/multica/hub.go @@ -384,6 +384,8 @@ func AssignmentMailboxDispatchMetadata(full map[string]string) map[string]string MulticaMetadataSessionID, MulticaMetadataCorrelationID, MulticaMetadataEventID, + MulticaMetadataEventType, + MulticaMetadataEventPhase, MulticaMetadataAssignmentID, MulticaMetadataAssignmentFingerprint, MulticaMetadataPrincipal, diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go index 3e8f0eaa..6db60b7b 100644 --- a/harness/internal/surface/multica/hub_test.go +++ b/harness/internal/surface/multica/hub_test.go @@ -138,6 +138,8 @@ func TestAssignmentMailboxMetadataGroupsDispatchBeforeSupplemental(t *testing.T) MulticaMetadataSessionID: "session-1", MulticaMetadataCorrelationID: "correlation-1", MulticaMetadataEventID: "event-1", + MulticaMetadataEventType: "assignment.accepted", + MulticaMetadataEventPhase: "accepted", MulticaMetadataAssignmentID: "asg-1", MulticaMetadataAssignmentFingerprint: "sha256:abc", MulticaMetadataPrincipal: "worker@team", @@ -155,6 +157,8 @@ func TestAssignmentMailboxMetadataGroupsDispatchBeforeSupplemental(t *testing.T) MulticaMetadataSessionID, MulticaMetadataCorrelationID, MulticaMetadataEventID, + MulticaMetadataEventType, + MulticaMetadataEventPhase, MulticaMetadataAssignmentID, MulticaMetadataAssignmentFingerprint, MulticaMetadataPrincipal, From e5211c7e54ac3402186a28b226fb0ca1ae8e8cc1 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 21:46:37 +0800 Subject: [PATCH 060/117] fix: harden harness control observations Retry retryable synchronous tick and short observe failures so rapid Multica teamwork handoffs do not surface transient stale-read errors. Carry render host/session/input scope through mnemon-harness control render and derive structured default assignment ids for short teamwork assignments. Validation: go test -count=1 ./harness/cmd/mnemon-harness ./harness/internal/runtime --- harness/cmd/mnemon-harness/control.go | 9 + harness/cmd/mnemon-harness/control_short.go | 177 +++++++++++++++++-- harness/cmd/mnemon-harness/control_test.go | 178 ++++++++++++++++++++ harness/internal/runtime/runtimehandler.go | 58 ++++++- 4 files changed, 410 insertions(+), 12 deletions(-) diff --git a/harness/cmd/mnemon-harness/control.go b/harness/cmd/mnemon-harness/control.go index 721992c4..19b90930 100644 --- a/harness/cmd/mnemon-harness/control.go +++ b/harness/cmd/mnemon-harness/control.go @@ -36,6 +36,9 @@ var ( controlRenderIntent string controlRenderLifecycle string controlRenderSurface string + controlRenderHost string + controlRenderSessionID string + controlRenderInputID string controlRenderMaxChars int controlRenderJSON bool ) @@ -168,6 +171,9 @@ var controlRenderCmd = &cobra.Command{ RenderIntent: controlRenderIntent, Lifecycle: controlRenderLifecycle, Surface: controlRenderSurface, + Host: controlRenderHost, + SessionID: controlRenderSessionID, + InputDigest: controlRenderInputID, Budget: presentation.Budget{MaxChars: controlRenderMaxChars}, }) if err != nil { @@ -280,6 +286,9 @@ func init() { controlRenderCmd.Flags().StringVar(&controlRenderIntent, "intent", presentation.IntentTeamworkEvents, "render intent") controlRenderCmd.Flags().StringVar(&controlRenderLifecycle, "lifecycle", "remind", "host lifecycle") controlRenderCmd.Flags().StringVar(&controlRenderSurface, "surface", "hook", "host surface") + controlRenderCmd.Flags().StringVar(&controlRenderHost, "host", envDefault("MNEMON_RENDER_HOST", ""), "host integration name") + controlRenderCmd.Flags().StringVar(&controlRenderSessionID, "session-id", envDefault("MNEMON_RENDER_SESSION_ID", ""), "render session scope") + controlRenderCmd.Flags().StringVar(&controlRenderInputID, "input-id", envDefault("MNEMON_RENDER_INPUT_ID", ""), "render input or assignment scope") controlRenderCmd.Flags().IntVar(&controlRenderMaxChars, "max-chars", 6000, "maximum rendered body chars") controlRenderCmd.Flags().BoolVar(&controlRenderJSON, "json", false, "emit full render response as JSON") controlCmd.AddCommand(controlObserveCmd, controlPullCmd, controlStatusCmd, controlRenderCmd, controlTeamworkCmd, controlProfileCmd) diff --git a/harness/cmd/mnemon-harness/control_short.go b/harness/cmd/mnemon-harness/control_short.go index a1b1769e..a3862213 100644 --- a/harness/cmd/mnemon-harness/control_short.go +++ b/harness/cmd/mnemon-harness/control_short.go @@ -2,14 +2,19 @@ package main import ( "fmt" + "os" + "path/filepath" "strings" "time" "github.com/mnemon-dev/mnemon/harness/internal/contract" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" "github.com/spf13/cobra" ) +const controlShortObserveMaxAttempts = 4 + var ( controlTeamworkSignalID string controlTeamworkSignalScope string @@ -117,7 +122,11 @@ var controlTeamworkAssignCmd = &cobra.Command{ "scope": controlTeamworkAssignScope, "ttl": controlTeamworkAssignTTL, } - putString(rule, "assignment_id", controlTeamworkAssignID) + assignmentID := strings.TrimSpace(controlTeamworkAssignID) + if assignmentID == "" { + assignmentID = defaultShortAssignmentID(controlTeamworkAssignScope, controlTeamworkAssignAssignee, controlTeamworkAssignReportOn, controlTeamworkAssignWork) + } + putString(rule, "assignment_id", assignmentID) putString(rule, "signal_ref", controlTeamworkAssignSignalRef) putStrings(rule, "report_on", controlTeamworkAssignReportOn) narrative := map[string]any{ @@ -217,18 +226,85 @@ func controlShortObserve(cmd *cobra.Command, eventType, fallbackIDPrefix string, if err != nil { return err } - rec, err := client.IngestObserve(contract.ActorID(controlPrincipal), contract.ObservationEnvelope{ - ExternalID: shortExternalID(fallbackIDPrefix), - Event: contract.Event{Type: eventType, Payload: payload}, + return withControlShortObserveLock(func() error { + return controlShortObserveLocked(cmd, client, eventType, fallbackIDPrefix, payload) }) - if err != nil { - return fmt.Errorf("channel observe failed (service unreachable or rejected): %w", err) - } - fmt.Fprintf(cmd.OutOrStdout(), "observed seq=%d dup=%v ticked=%v\n", rec.Seq, rec.Dup, rec.Ticked) - if rec.ProcessingError != "" { +} + +func controlShortObserveLocked(cmd *cobra.Command, client *access.Client, eventType, fallbackIDPrefix string, payload map[string]any) error { + externalID := shortExternalID(fallbackIDPrefix) + for attempt := 0; attempt < controlShortObserveMaxAttempts; attempt++ { + attemptExternalID := retryExternalID(externalID, attempt) + rec, err := client.IngestObserve(contract.ActorID(controlPrincipal), contract.ObservationEnvelope{ + ExternalID: attemptExternalID, + Event: contract.Event{Type: eventType, Payload: payload}, + }) + if err != nil { + return fmt.Errorf("channel observe failed (service unreachable or rejected): %w", err) + } + fmt.Fprintf(cmd.OutOrStdout(), "observed seq=%d dup=%v ticked=%v\n", rec.Seq, rec.Dup, rec.Ticked) + if rec.ProcessingError == "" { + return nil + } fmt.Fprintf(cmd.OutOrStdout(), "processing error: %s\n", rec.ProcessingError) + if !retryableShortObserveProcessingError(rec.ProcessingError) || attempt == controlShortObserveMaxAttempts-1 { + return fmt.Errorf("channel observe processing failed: %s", rec.ProcessingError) + } + nextExternalID := retryExternalID(externalID, attempt+1) + fmt.Fprintf(cmd.OutOrStdout(), "retrying after processing error: attempt=%d external_id=%s\n", attempt+2, nextExternalID) + time.Sleep(time.Duration(100*(1<= '0' && lower[end] <= '9' { + end++ + } + if end > start { + return "tea" + lower[start:end] + } + pos = idx + 1 + } + } + return "" +} + +func assignmentTopic(reportOn []string, work string) string { + combined := strings.ToLower(strings.Join(reportOn, " ") + " " + work) + switch { + case strings.Contains(combined, "root") || strings.Contains(combined, "metadata") || strings.Contains(combined, "run visibility"): + return "root-runtime" + case strings.Contains(combined, "routing") || strings.Contains(combined, "isolation"): + return "routing-isolation" + case strings.Contains(combined, "feedback") || strings.Contains(combined, "status") || strings.Contains(combined, "completion"): + return "feedback-status" + default: + return sanitizeAssignmentToken(combined) + } +} + +func sanitizeAssignmentToken(value string) string { + value = strings.ToLower(strings.TrimSpace(value)) + var b strings.Builder + lastDash := false + words := 0 + inWord := false + for _, r := range value { + ok := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') + if ok { + if !inWord { + words++ + inWord = true + } + if words > 4 { + break + } + b.WriteRune(r) + lastDash = false + continue + } + inWord = false + if b.Len() > 0 && !lastDash { + b.WriteByte('-') + lastDash = true + } + } + return strings.Trim(b.String(), "-") +} + func requireShortFields(fields map[string]string) error { for name, value := range fields { if strings.TrimSpace(value) == "" { diff --git a/harness/cmd/mnemon-harness/control_test.go b/harness/cmd/mnemon-harness/control_test.go index f8eee9c2..f421210d 100644 --- a/harness/cmd/mnemon-harness/control_test.go +++ b/harness/cmd/mnemon-harness/control_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "net/http" "net/http/httptest" "os" "path/filepath" @@ -200,6 +201,9 @@ func TestControlRenderPrintsDerivedEventPresentationBody(t *testing.T) { oldIntent := controlRenderIntent oldLifecycle := controlRenderLifecycle oldSurface := controlRenderSurface + oldHost := controlRenderHost + oldSessionID := controlRenderSessionID + oldInputID := controlRenderInputID oldMaxChars := controlRenderMaxChars oldJSON := controlRenderJSON t.Cleanup(func() { @@ -210,6 +214,9 @@ func TestControlRenderPrintsDerivedEventPresentationBody(t *testing.T) { controlRenderIntent = oldIntent controlRenderLifecycle = oldLifecycle controlRenderSurface = oldSurface + controlRenderHost = oldHost + controlRenderSessionID = oldSessionID + controlRenderInputID = oldInputID controlRenderMaxChars = oldMaxChars controlRenderJSON = oldJSON }) @@ -220,6 +227,9 @@ func TestControlRenderPrintsDerivedEventPresentationBody(t *testing.T) { controlRenderIntent = presentation.IntentTeamworkEvents controlRenderLifecycle = "remind" controlRenderSurface = "hook" + controlRenderHost = "" + controlRenderSessionID = "" + controlRenderInputID = "" controlRenderMaxChars = 6000 controlRenderJSON = false @@ -233,6 +243,51 @@ func TestControlRenderPrintsDerivedEventPresentationBody(t *testing.T) { } } +func TestControlRenderCarriesHostSessionScope(t *testing.T) { + var got presentation.Request + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&got); err != nil { + t.Fatalf("decode request: %v", err) + } + _ = json.NewEncoder(w).Encode(presentation.Response{ + SchemaVersion: 1, + Status: presentation.StatusOK, + Body: "ok", + }) + })) + defer srv.Close() + + oldAddr := controlAddr + oldPrincipal := controlPrincipal + oldToken := controlToken + oldTokenFile := controlTokenFile + t.Cleanup(func() { + controlAddr = oldAddr + controlPrincipal = oldPrincipal + controlToken = oldToken + controlTokenFile = oldTokenFile + }) + controlAddr = srv.URL + controlPrincipal = "planner@team" + controlToken = "" + controlTokenFile = "" + + _, err := controlRender(presentation.Request{ + RenderIntent: presentation.IntentTeamworkEvents, + Lifecycle: "remind", + Surface: "hook", + Host: "multica", + SessionID: "multica:session:root-1", + InputDigest: "root-1", + }) + if err != nil { + t.Fatalf("control render: %v", err) + } + if got.Host != "multica" || got.SessionID != "multica:session:root-1" || got.InputDigest != "root-1" { + t.Fatalf("render scope not carried: %+v", got) + } +} + func TestControlShortCommandsEmitR2Payloads(t *testing.T) { refs := []contract.ResourceRef{ {Kind: "agent_profile", ID: "project"}, @@ -377,6 +432,129 @@ func TestControlShortCommandsEmitR2Payloads(t *testing.T) { } } +func TestControlShortObserveRetriesRetryableProcessingError(t *testing.T) { + var attempts int + var externalIDs []string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/ingest" { + t.Errorf("unexpected path %s", r.URL.Path) + http.NotFound(w, r) + return + } + if got := r.Header.Get(access.PrincipalHeader); got != "codex-a@project" { + t.Errorf("principal header = %q", got) + } + var env contract.ObservationEnvelope + if err := json.NewDecoder(r.Body).Decode(&env); err != nil { + t.Errorf("decode envelope: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + attempts++ + externalIDs = append(externalIDs, env.ExternalID) + w.Header().Set("Content-Type", "application/json") + if attempts == 1 { + _ = json.NewEncoder(w).Encode(access.IngestReceipt{ + Seq: 41, + Ticked: true, + ProcessingError: "read_stale: resource version advanced", + }) + return + } + _ = json.NewEncoder(w).Encode(access.IngestReceipt{Seq: 42, Ticked: true}) + })) + defer srv.Close() + + oldAddr := controlAddr + oldPrincipal := controlPrincipal + oldToken := controlToken + oldTokenFile := controlTokenFile + oldExtID := controlExtID + t.Cleanup(func() { + controlAddr = oldAddr + controlPrincipal = oldPrincipal + controlToken = oldToken + controlTokenFile = oldTokenFile + controlExtID = oldExtID + }) + controlAddr = srv.URL + controlPrincipal = "codex-a@project" + controlToken = "" + controlTokenFile = "" + controlExtID = "short-retry" + + var buf bytes.Buffer + controlTeamworkSignalCmd.SetOut(&buf) + err := controlShortObserve(controlTeamworkSignalCmd, "teamwork_signal.write_candidate.observed", "teamwork-signal", map[string]any{"rule": map[string]any{"scope": "r2/retry"}}) + if err != nil { + t.Fatalf("short observe retry: %v; output=%q", err, buf.String()) + } + if attempts != 2 { + t.Fatalf("attempts = %d, want 2; output=%q", attempts, buf.String()) + } + if got, want := strings.Join(externalIDs, ","), "short-retry,short-retry-retry-1"; got != want { + t.Fatalf("external ids = %s, want %s", got, want) + } + if !strings.Contains(buf.String(), "retrying after processing error") || !strings.Contains(buf.String(), "observed seq=42") { + t.Fatalf("missing retry/success output: %q", buf.String()) + } +} + +func TestControlTeamworkAssignDefaultsStructuredAssignmentID(t *testing.T) { + var got contract.ObservationEnvelope + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&got); err != nil { + t.Errorf("decode envelope: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(access.IngestReceipt{Seq: 7, Ticked: true}) + })) + defer srv.Close() + + oldAddr := controlAddr + oldPrincipal := controlPrincipal + oldToken := controlToken + oldTokenFile := controlTokenFile + oldExtID := controlExtID + resetControlShortCommandVars() + t.Cleanup(func() { + controlAddr = oldAddr + controlPrincipal = oldPrincipal + controlToken = oldToken + controlTokenFile = oldTokenFile + controlExtID = oldExtID + resetControlShortCommandVars() + }) + controlAddr = srv.URL + controlPrincipal = "planner@team" + controlToken = "" + controlTokenFile = "" + controlExtID = "assign-default-id" + controlTeamworkAssignID = "" + controlTeamworkAssignAssignee = "researcher@team" + controlTeamworkAssignScope = "TEA-74 Mnemon R2 hub-flow readiness drill" + controlTeamworkAssignTTL = "20m" + controlTeamworkAssignReportOn = []string{"root session metadata", "agent run visibility"} + controlTeamworkAssignWork = "Validate TEA-74 root session metadata and run visibility." + controlTeamworkAssignFeedback = "progress_digest with PASS/FAIL evidence" + controlTeamworkAssignEvidence = []string{"TEA-74 root issue is current session mailbox"} + + var buf bytes.Buffer + controlTeamworkAssignCmd.SetOut(&buf) + if err := controlTeamworkAssignCmd.RunE(controlTeamworkAssignCmd, nil); err != nil { + t.Fatalf("teamwork assign: %v", err) + } + rule, ok := got.Event.Payload["rule"].(map[string]any) + if !ok { + t.Fatalf("payload missing rule: %+v", got.Event.Payload) + } + if got := rule["assignment_id"]; got != "assignment-tea74-root-runtime" { + t.Fatalf("assignment_id = %v, want assignment-tea74-root-runtime", got) + } +} + func mustReadCmd(t *testing.T, path string) []byte { t.Helper() data, err := os.ReadFile(path) diff --git a/harness/internal/runtime/runtimehandler.go b/harness/internal/runtime/runtimehandler.go index 61b99f1f..ca85ab8b 100644 --- a/harness/internal/runtime/runtimehandler.go +++ b/harness/internal/runtime/runtimehandler.go @@ -2,13 +2,21 @@ package runtime import ( "encoding/json" + "fmt" "net/http" + "strings" + "time" "github.com/mnemon-dev/mnemon/harness/internal/contract" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" ) +const ( + syncTickMaxAttempts = 4 + syncTickMaxCycles = 8 +) + // NewRuntimeHandler is the Local Mnemon HTTP channel endpoint over a Runtime. // It differs from the api-only access.NewHTTPHandler in two ways the Runtime makes possible: // @@ -42,7 +50,7 @@ func NewRuntimeHandler(rt *Runtime, auth access.Authenticator) http.Handler { rec := access.IngestReceipt{Seq: seq, Dup: dup} if !dup { rec.Ticked = true - if _, terr := rt.Tick(); terr != nil { + if terr := tickRuntimeWithRetry(rt); terr != nil { rec.ProcessingError = terr.Error() } } @@ -71,7 +79,7 @@ func NewRuntimeHandler(rt *Runtime, auth access.Authenticator) http.Handler { // already processed on its first ingest, so it is not re-ticked. if !dup { rec.Ticked = true - if _, terr := rt.Tick(); terr != nil { + if terr := tickRuntimeWithRetry(rt); terr != nil { rec.ProcessingError = terr.Error() } } @@ -165,3 +173,49 @@ func NewRuntimeHandler(rt *Runtime, auth access.Authenticator) http.Handler { }) return mux } + +func tickRuntimeWithRetry(rt *Runtime) error { + var lastErr error + for cycle := 0; cycle < syncTickMaxCycles; cycle++ { + for attempt := 0; attempt < syncTickMaxAttempts; attempt++ { + decisions, err := rt.Tick() + if err == nil { + if len(decisions) == 0 { + return nil + } + lastErr = nil + break + } + lastErr = err + if !retryableRuntimeTickError(err) || attempt == syncTickMaxAttempts-1 { + return err + } + time.Sleep(time.Duration(25*(1< Date: Mon, 29 Jun 2026 21:47:11 +0800 Subject: [PATCH 061/117] fix: stabilize multica hub projection Scope Multica teamwork renders by session/input identity and require strong managed-wake matches so stale title-only handoffs do not route work across sessions. Project assignment mailboxes sequentially with retries, keep later assignments moving after supplemental metadata failures, and repair child/root statuses even when feedback comments are already recorded in the hub ledger. Validation: go test -count=1 ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance ./harness/internal/drive ./harness/internal/mnemond/presentation ./harness/internal/surface/multica --- .../acceptance_multica_runtime.go | 2 +- .../cmd/mnemon-multica-runtime/hub_writer.go | 238 ++++++++------ harness/cmd/mnemon-multica-runtime/main.go | 49 ++- .../cmd/mnemon-multica-runtime/main_test.go | 302 +++++++++++++++++- harness/internal/drive/selection.go | 27 ++ harness/internal/drive/selection_test.go | 43 ++- .../mnemond/presentation/render_test.go | 124 +++++++ .../presentation/teamwork_presenter.go | 66 ++++ .../internal/surface/multica/projection.go | 39 ++- .../surface/multica/projection_test.go | 12 + 10 files changed, 801 insertions(+), 101 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 2690fc4c..2b252687 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -244,7 +244,7 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime if description == "" { description = strings.TrimSpace(`Run a small Mnemon R2 Multica hub-flow readiness drill. -Coordinate this as teamwork rather than solo work. Split the validation across researcher, reviewer, and integrator teammates, ask for concise feedback, use another assignment round if a gap appears, then integrate the final status. +Coordinate this as teamwork rather than solo work. Use Mnemon teamwork handoffs when delegating validation slices; the Multica runtime should project accepted assignments into child assignment mailboxes and route them to the target agents. The validation should cover root session metadata, assignment child issue routing, assignment feedback comments, agent run activity visibility, stale or cross-session assignment isolation, and final Multica status completion.`) } diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 48a6ee46..dbf090a3 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -3,10 +3,11 @@ package main import ( "context" "encoding/json" + "errors" "fmt" + "sort" "strconv" "strings" - "sync" "time" "github.com/mnemon-dev/mnemon/harness/internal/contract" @@ -191,95 +192,105 @@ func (s *runtimeRPCState) projectAssignmentMailboxes(ctx context.Context, cli dr if len(projections) == 0 { return 0, nil } - const maxAssignmentWorkers = 3 - workers := maxAssignmentWorkers - if len(projections) < workers { - workers = len(projections) - } - jobs := make(chan runtimeAssignmentProjection) - var wg sync.WaitGroup - var mu sync.Mutex - var ledgerMu sync.Mutex - var firstErr error created := 0 - recordErr := func(err error) { - if err == nil { - return + var errs []error + for _, projection := range projections { + material := assignmentMailboxMaterial(projection.Item, projection.Result, projection.RootIssue, projection.Participant) + child, err := retryMulticaHubValue(ctx, func() (driver.MulticaIssue, error) { + return cli.CreateIssue(ctx, driver.MulticaCreateIssueRequest{ + Title: multicasurface.AssignmentMailboxTitle(material), + Description: multicasurface.AssignmentMailboxDescription(material), + ParentID: projection.Result.RootIssueID, + Status: "in_progress", + Priority: "medium", + AllowDuplicate: true, + }) + }) + if err != nil { + errs = append(errs, fmt.Errorf("create assignment mailbox %s: %w", projection.Item.ID, err)) + continue } - mu.Lock() - defer mu.Unlock() - if firstErr == nil { - firstErr = err + fullMeta := projection.Metadata.Map() + dispatchMeta := multicasurface.AssignmentMailboxDispatchMetadata(fullMeta) + if err := setMulticaHubMetadataMap(ctx, cli, child.ID, dispatchMeta); err != nil { + errs = append(errs, fmt.Errorf("tag assignment mailbox %s (%s): %w", projection.Item.ID, child.ID, err)) + continue + } + if _, err := retryMulticaHubValue(ctx, func() (driver.MulticaIssue, error) { + return cli.AssignIssue(ctx, child.ID, projection.Participant.AgentID) + }); err != nil { + errs = append(errs, fmt.Errorf("assign assignment mailbox %s (%s): %w", projection.Item.ID, child.ID, err)) + continue + } + if err := ledger.Record(driver.MulticaHubLedgerRecord{ + Kind: driver.MulticaHubKindAssignmentMailbox, + Source: projection.Source, + Target: driver.MulticaHubLedgerTarget{ + RootIssueID: projection.Result.RootIssueID, + ChildIssueID: child.ID, + Status: "created", + }, + }); err != nil { + return created, err + } + created++ + if err := setMulticaHubMetadataMap(ctx, cli, child.ID, multicasurface.AssignmentMailboxSupplementalMetadata(fullMeta, dispatchMeta)); err != nil { + errs = append(errs, fmt.Errorf("tag supplemental assignment mailbox %s (%s): %w", projection.Item.ID, child.ID, err)) + continue } } - hasErr := func() bool { - mu.Lock() - defer mu.Unlock() - return firstErr != nil + return created, errors.Join(errs...) +} + +func setMulticaHubMetadataMap(ctx context.Context, cli driver.MulticaCLI, issueID string, values map[string]string) error { + keys := make([]string, 0, len(values)) + for key, value := range values { + if strings.TrimSpace(key) == "" || strings.TrimSpace(value) == "" { + continue + } + keys = append(keys, key) } - for i := 0; i < workers; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for projection := range jobs { - if hasErr() { - continue - } - material := assignmentMailboxMaterial(projection.Item, projection.Result, projection.RootIssue, projection.Participant) - child, err := cli.CreateIssue(ctx, driver.MulticaCreateIssueRequest{ - Title: multicasurface.AssignmentMailboxTitle(material), - Description: multicasurface.AssignmentMailboxDescription(material), - ParentID: projection.Result.RootIssueID, - Status: "in_progress", - Priority: "medium", - }) - if err != nil { - recordErr(err) - continue - } - fullMeta := projection.Metadata.Map() - dispatchMeta := multicasurface.AssignmentMailboxDispatchMetadata(fullMeta) - if err := cli.SetIssueMetadataMap(ctx, child.ID, dispatchMeta); err != nil { - recordErr(err) - continue - } - if _, err := cli.AssignIssue(ctx, child.ID, projection.Participant.AgentID); err != nil { - recordErr(err) - continue - } - ledgerMu.Lock() - err = ledger.Record(driver.MulticaHubLedgerRecord{ - Kind: driver.MulticaHubKindAssignmentMailbox, - Source: projection.Source, - Target: driver.MulticaHubLedgerTarget{ - RootIssueID: projection.Result.RootIssueID, - ChildIssueID: child.ID, - Status: "created", - }, - }) - ledgerMu.Unlock() - if err != nil { - recordErr(err) - continue - } - mu.Lock() - created++ - mu.Unlock() - if err := cli.SetIssueMetadataMap(ctx, child.ID, multicasurface.AssignmentMailboxSupplementalMetadata(fullMeta, dispatchMeta)); err != nil { - recordErr(err) - } - } - }() + sort.Strings(keys) + var errs []error + for _, key := range keys { + value := values[key] + if err := retryMulticaHubOperation(ctx, func() error { + return cli.SetIssueMetadata(ctx, issueID, key, value, "string") + }); err != nil { + errs = append(errs, fmt.Errorf("set %s: %w", key, err)) + } } - for _, projection := range projections { - if hasErr() { - break + return errors.Join(errs...) +} + +func retryMulticaHubOperation(ctx context.Context, op func() error) error { + _, err := retryMulticaHubValue(ctx, func() (struct{}, error) { + return struct{}{}, op() + }) + return err +} + +func retryMulticaHubValue[T any](ctx context.Context, op func() (T, error)) (T, error) { + var zero T + const attempts = 3 + var err error + for attempt := 0; attempt < attempts; attempt++ { + if attempt > 0 { + timer := time.NewTimer(time.Duration(attempt) * 250 * time.Millisecond) + select { + case <-ctx.Done(): + timer.Stop() + return zero, ctx.Err() + case <-timer.C: + } + } + var value T + value, err = op() + if err == nil { + return value, nil } - jobs <- projection } - close(jobs) - wg.Wait() - return created, firstErr + return zero, err } func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver.MulticaCLI, ledger *driver.FileMulticaHubLedger, proj pview.View, result *runtimeImportResult) error { @@ -299,9 +310,30 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. Principal: item.Actor, ProjectionKind: "progress", } - if _, ok, err := ledger.Find(driver.MulticaHubKindFeedbackCarrier, source); err != nil { + material := progressFeedbackMaterial(item) + if rec, ok, err := ledger.Find(driver.MulticaHubKindFeedbackCarrier, source); err != nil { return err } else if ok { + child := strings.TrimSpace(rec.Target.ChildIssueID) + if child == "" { + var found bool + child, found, err = findAssignmentTargetFromLedger(ledger, result.SessionID, item.AssignmentRef) + if err != nil { + return err + } + if !found { + child, found, err = findAssignmentTargetFromMulticaHub(ctx, cli, result.RootIssueID, result.SessionID, item.AssignmentRef) + if err != nil { + return err + } + } + if !found { + continue + } + } + if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, material); err != nil { + return err + } continue } child, ok, err := findAssignmentTargetFromLedger(ledger, result.SessionID, item.AssignmentRef) @@ -317,7 +349,6 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if !ok { continue } - material := progressFeedbackMaterial(item) commentBody := projection.FormatComment(projection.CommentMaterial{ Title: "assignment feedback", Body: multicasurface.ProgressCommentBody(material), @@ -330,15 +361,8 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if err != nil { return err } - if status := multicasurface.ProgressIssueStatus(material); status != "" { - _, _ = cli.SetIssueStatus(ctx, child, status) - } - allDone := false - if multicasurface.ProgressCompletesAssignment(material) { - allDone, _ = allMulticaAssignmentChildrenDone(ctx, cli, result.RootIssueID, result.SessionID, child) - } - if rootStatus := multicasurface.ProgressRootIssueStatus(material, allDone); rootStatus != "" { - _, _ = cli.SetIssueStatus(ctx, result.RootIssueID, rootStatus) + if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, material); err != nil { + return err } if err := ledger.Record(driver.MulticaHubLedgerRecord{ Kind: driver.MulticaHubKindFeedbackCarrier, @@ -357,6 +381,34 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. return nil } +func (s *runtimeRPCState) ensureProgressIssueStatuses(ctx context.Context, cli driver.MulticaCLI, result *runtimeImportResult, child string, material multicasurface.ProgressFeedbackMaterial) error { + if status := multicasurface.ProgressIssueStatus(material); status != "" { + if err := retryMulticaHubOperation(ctx, func() error { + _, err := cli.SetIssueStatus(ctx, child, status) + return err + }); err != nil { + return fmt.Errorf("set assignment feedback issue %s status %s: %w", child, status, err) + } + } + allDone := false + if multicasurface.ProgressCompletesAssignment(material) { + var err error + allDone, err = allMulticaAssignmentChildrenDone(ctx, cli, result.RootIssueID, result.SessionID, child) + if err != nil { + return err + } + } + if rootStatus := multicasurface.ProgressRootIssueStatus(material, allDone); rootStatus != "" { + if err := retryMulticaHubOperation(ctx, func() error { + _, err := cli.SetIssueStatus(ctx, result.RootIssueID, rootStatus) + return err + }); err != nil { + return fmt.Errorf("set root issue %s status %s: %w", result.RootIssueID, rootStatus, err) + } + } + return nil +} + type runtimeAssignment struct { ID string EventID string diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 4d5bc61a..8aaa7917 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -472,8 +472,9 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr result.HubMetadata.EventID, string(eventmodel.Subject("assignment", result.AssignmentID)), ) - emitRuntimeCommand(progress, "mnemon multica assignment correlate --issue "+issue.ID, "Assignment mailbox correlated: "+runtimeAssignmentLabel(*result)+".", 0) - emitRuntimeProgress(progress, "Assignment mailbox correlated: "+runtimeAssignmentLabel(*result)+".") + correlationProgress := runtimeAssignmentCorrelationProgress(*result) + emitRuntimeCommand(progress, "mnemon multica assignment correlate --issue "+issue.ID, correlationProgress, 0) + emitRuntimeProgress(progress, correlationProgress) addr := strings.TrimSpace(envValue(s.Env, "MNEMON_CONTROL_ADDR")) var client *access.Client if addr == "" { @@ -503,6 +504,24 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr return *result } +func runtimeAssignmentCorrelationProgress(result runtimeImportResult) string { + var b strings.Builder + b.WriteString("Mnemon assignment mailbox: correlated") + if result.AssignmentID != "" { + b.WriteString(" assignment=") + b.WriteString(result.AssignmentID) + } else if label := strings.TrimSpace(runtimeAssignmentLabel(result)); label != "" { + b.WriteString(" assignment=") + b.WriteString(label) + } + if result.SessionID != "" { + b.WriteString(" session=") + b.WriteString(result.SessionID) + } + b.WriteString(".") + return b.String() +} + func applyMulticaHubMetadata(result *runtimeImportResult, meta driver.MulticaHubMetadata) { if result == nil { return @@ -577,6 +596,8 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress Lifecycle: envDefault(s.Env, "MNEMON_MANAGED_RENDER_LIFECYCLE", "remind"), Surface: "runtime", RenderIntent: presentation.IntentTeamworkEvents, + SessionID: result.SessionID, + InputDigest: runtimeManagedWakeScopeID(*result), }) if err != nil { result.WakeStatus = "failed" @@ -589,7 +610,8 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress result.WakeErr = fmt.Errorf("no managed wake candidate in rendered context") return } - client, workspace, err := runtimeManagedTurnClient(s.Env, s.CWD, runtimeName) + managedEnv := runtimeManagedTurnEnv(s.Env, *result) + client, workspace, err := runtimeManagedTurnClient(managedEnv, s.CWD, runtimeName) if err != nil { result.WakeStatus = "failed" result.WakeErr = err @@ -620,6 +642,25 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress } } +func runtimeManagedWakeScopeID(result runtimeImportResult) string { + return firstNonEmpty(result.AssignmentID, result.RootIssueID, result.IssueID) +} + +func runtimeManagedTurnEnv(env []string, result runtimeImportResult) []string { + out := append([]string(nil), env...) + add := func(key, value string) { + value = strings.TrimSpace(value) + if value == "" || envValue(out, key) != "" { + return + } + out = append(out, key+"="+value) + } + add("MNEMON_RENDER_HOST", "multica") + add("MNEMON_RENDER_SESSION_ID", result.SessionID) + add("MNEMON_RENDER_INPUT_ID", runtimeManagedWakeScopeID(result)) + return out +} + type runtimeHubProjectionDelta struct { ChildIssues int FeedbackComments int @@ -628,7 +669,7 @@ type runtimeHubProjectionDelta struct { } func (d runtimeHubProjectionDelta) active() bool { - return d.ChildIssues > 0 || d.FeedbackComments > 0 + return d.ChildIssues > 0 || d.FeedbackComments > 0 || d.Err != nil } func (s *runtimeRPCState) wakeManagedAgentWithHubProjection(ctx context.Context, cli driver.MulticaCLI, client *access.Client, rootIssue driver.MulticaIssue, result *runtimeImportResult, progress runtimeProgressSink) []runtimeHubProjectionDelta { diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index d440c5dc..5a1b4852 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -94,6 +94,45 @@ func TestManagedWakeMatchTermsPreferStableIssueIdentity(t *testing.T) { } } +func TestRuntimeManagedWakeScopeIDPrefersAssignmentThenRoot(t *testing.T) { + if got := runtimeManagedWakeScopeID(runtimeImportResult{ + AssignmentID: "asg-1", + RootIssueID: "root-1", + IssueID: "child-1", + }); got != "asg-1" { + t.Fatalf("assignment mailbox scope = %q, want asg-1", got) + } + if got := runtimeManagedWakeScopeID(runtimeImportResult{ + RootIssueID: "root-1", + IssueID: "root-1", + }); got != "root-1" { + t.Fatalf("root session scope = %q, want root-1", got) + } +} + +func TestRuntimeManagedTurnEnvInjectsRenderScope(t *testing.T) { + env := runtimeManagedTurnEnv([]string{"EXISTING=1"}, runtimeImportResult{ + SessionID: "multica:session:root-1", + RootIssueID: "root-1", + AssignmentID: "asg-1", + }) + joined := strings.Join(env, "\n") + for _, want := range []string{ + "MNEMON_RENDER_HOST=multica", + "MNEMON_RENDER_SESSION_ID=multica:session:root-1", + "MNEMON_RENDER_INPUT_ID=asg-1", + } { + if !strings.Contains(joined, want) { + t.Fatalf("managed env missing %q: %v", want, env) + } + } + + preserved := runtimeManagedTurnEnv([]string{"MNEMON_RENDER_HOST=custom"}, runtimeImportResult{SessionID: "session-1", RootIssueID: "root-1"}) + if got := envValue(preserved, "MNEMON_RENDER_HOST"); got != "custom" { + t.Fatalf("managed env should preserve explicit host, got %q", got) + } +} + func runtimeTestEnv(values ...string) []string { env := make([]string, 0, len(os.Environ())+len(values)) for _, item := range os.Environ() { @@ -689,7 +728,6 @@ esac }); err != nil { t.Fatal(err) } - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/ingest": @@ -890,6 +928,144 @@ esac } } +func TestRuntimeContinuesProjectingAssignmentMailboxesAfterSupplementalMetadataFailure(t *testing.T) { + tmp := t.TempDir() + argsPath := filepath.Join(tmp, "multica.args") + commentPath := filepath.Join(tmp, "comment.txt") + createDescriptionPath := filepath.Join(tmp, "child-description.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue get root-retry"*) printf '{"id":"root-retry","identifier":"TEA-12","title":"Retry child projection","description":"Split retry validation across two assignees.","status":"todo","priority":"medium"}\n' ;; + *"issue metadata set root-retry"*) printf '{}\n' ;; + *"issue children root-retry"*) printf '{"children":[]}\n' ;; + *"issue create --title TEA-12: first slice"*) cat > "$MULTICA_CREATE_DESCRIPTION_PATH"; printf '{"id":"child-one","identifier":"TEA-13","title":"TEA-12: first slice","status":"todo","metadata":{}}\n' ;; + *"issue create --title TEA-12: second slice"*) cat > "$MULTICA_CREATE_DESCRIPTION_PATH"; printf '{"id":"child-two","identifier":"TEA-14","title":"TEA-12: second slice","status":"todo","metadata":{}}\n' ;; + *"issue metadata set child-one --key mnemon.projection_owner"*) printf 'metadata conflict\n' >&2; exit 42 ;; + *"issue metadata set child-one"*) printf '{}\n' ;; + *"issue metadata set child-two"*) printf '{}\n' ;; + *"issue assign child-one"*) printf '{"id":"child-one","status":"in_progress"}\n' ;; + *"issue assign child-two"*) printf '{"id":"child-two","status":"in_progress"}\n' ;; + *"issue comment add root-retry"*) cat > "$MULTICA_COMMENT_PATH"; printf '{"id":"comment-root-retry","issue_id":"root-retry","content":"ok","type":"comment"}\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + registryPath := filepath.Join(tmp, "registry.json") + if err := driver.SaveMulticaRegistry(registryPath, driver.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-1", + Participants: []driver.MulticaParticipantRecord{{ + Principal: "worker@team", + AgentName: "mnemon-worker", + AgentID: "agent-worker", + Role: "implementer", + }}, + }); err != nil { + t.Fatal(err) + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/ingest": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(access.IngestReceipt{Seq: 31, Dup: false, Ticked: true}) + case "/presentation-view": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(pview.View{ + Ref: "view-retry-assignments", + Digest: "digest-retry-assignments", + Content: []pview.ResourceContent{{ + Ref: contract.ResourceRef{Kind: "assignment", ID: "project"}, + Fields: map[string]any{"items": []any{ + map[string]any{ + "id": "asg-one", + "ingest_seq": float64(32), + "actor": "planner@team", + "rule": map[string]any{ + "assignment_id": "asg-one", + "assignee": "worker@team", + "scope": "first slice", + "ttl": "30m", + }, + "narrative": map[string]any{ + "expected_work": "check the first projection slice", + "expected_feedback": "progress_digest with result or blocker", + }, + "refs": map[string]any{"context_refs": []any{"multica:issue:root-retry"}}, + }, + map[string]any{ + "id": "asg-two", + "ingest_seq": float64(33), + "actor": "planner@team", + "rule": map[string]any{ + "assignment_id": "asg-two", + "assignee": "worker@team", + "scope": "second slice", + "ttl": "30m", + }, + "narrative": map[string]any{ + "expected_work": "check the second projection slice", + "expected_feedback": "progress_digest with result or blocker", + }, + "refs": map[string]any{"context_refs": []any{"multica:issue:root-retry"}}, + }, + }}, + }}, + }) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer srv.Close() + + input := `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"input":[{"type":"text","text":"Your assigned issue ID is: root-retry"}]}}` + "\n" + var out bytes.Buffer + err := runRuntime(runtimeConfig{ + Args: []string{"app-server", "--listen", "stdio://"}, + Env: runtimeTestEnv( + "MNEMON_MULTICA_BIN="+bin, + "MNEMON_HUB_BACKEND=multica", + "MNEMON_CONTROL_ADDR="+srv.URL, + "MNEMON_CONTROL_PRINCIPAL=planner@team", + "MNEMON_MULTICA_REGISTRY="+registryPath, + "MNEMON_MULTICA_HUB_LEDGER="+filepath.Join(tmp, "hub-ledger.jsonl"), + "MULTICA_ARGS_PATH="+argsPath, + "MULTICA_COMMENT_PATH="+commentPath, + "MULTICA_CREATE_DESCRIPTION_PATH="+createDescriptionPath, + "MULTICA_TASK_ID=task-root-retry", + "MULTICA_AGENT_ID=agent-planner", + ), + CWD: tmp, + Stdin: strings.NewReader(input), + Stdout: &out, + Now: fixedRuntimeTime, + }) + if err != nil { + t.Fatal(err) + } + output := out.String() + if !strings.Contains(output, "Multica hub write: failed") { + t.Fatalf("runtime should surface the partial metadata failure:\n%s", output) + } + args := mustReadRuntimeTestFile(t, argsPath) + for _, want := range []string{ + "issue create --title TEA-12: first slice --output json --description-stdin --parent root-retry --status in_progress --priority medium", + "issue assign child-one --to-id agent-worker --output json", + "issue create --title TEA-12: second slice --output json --description-stdin --parent root-retry --status in_progress --priority medium", + "issue assign child-two --to-id agent-worker --output json", + } { + if !strings.Contains(args, want) { + t.Fatalf("args missing %q:\n%s", want, args) + } + } + if got := strings.Count(args, "issue metadata set child-one --key mnemon.projection_owner"); got != 3 { + t.Fatalf("supplemental metadata should be retried three times, got %d:\n%s", got, args) + } +} + func TestRuntimeProjectsProgressToExistingAssignmentMailboxFromHubMetadata(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "multica.args") @@ -1023,6 +1199,130 @@ esac } } +func TestRuntimeRepairsProgressStatusWhenFeedbackLedgerAlreadyExists(t *testing.T) { + tmp := t.TempDir() + argsPath := filepath.Join(tmp, "multica.args") + rootCommentPath := filepath.Join(tmp, "root-comment.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue get root-existing"*) printf '{"id":"root-existing","identifier":"TEA-20","title":"Existing assignment mailbox","description":"Repair status after restart.","status":"in_review","priority":"medium"}\n' ;; + *"issue metadata set root-existing"*) printf '{}\n' ;; + *"issue children root-existing"*) printf '{"children":[{"id":"child-existing","identifier":"TEA-21","title":"TEA-20: existing mailbox","status":"in_progress","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-existing","mnemon.assignment_id":"asg-existing","mnemon.principal":"worker@team","mnemon.root_issue_id":"root-existing"}}]}\n' ;; + *"issue comment add child-existing"*) printf 'unexpected duplicate child comment\n' >&2; exit 7 ;; + *"issue comment add root-existing"*) cat > "$MULTICA_ROOT_COMMENT_PATH"; printf '{"id":"comment-root-existing","issue_id":"root-existing","content":"ok","type":"comment"}\n' ;; + *"issue status child-existing"*) printf '{"id":"child-existing","status":"done"}\n' ;; + *"issue status root-existing"*) printf '{"id":"root-existing","status":"done"}\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + ledgerPath := filepath.Join(tmp, "hub-ledger.jsonl") + ledger := driver.NewFileMulticaHubLedger(ledgerPath) + if err := ledger.Record(driver.MulticaHubLedgerRecord{ + Kind: driver.MulticaHubKindFeedbackCarrier, + Source: driver.MulticaHubLedgerSource{ + SessionID: "multica:session:root-existing", + CorrelationID: "multica:issue:root-existing", + EventID: "pg-existing", + AssignmentID: "asg-existing", + Principal: "worker@team", + ProjectionKind: "progress", + }, + Target: driver.MulticaHubLedgerTarget{ + RootIssueID: "root-existing", + ChildIssueID: "child-existing", + CommentID: "comment-already-projected", + Status: "commented", + }, + }); err != nil { + t.Fatal(err) + } + registryPath := filepath.Join(tmp, "registry.json") + if err := driver.SaveMulticaRegistry(registryPath, driver.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-1", + Participants: []driver.MulticaParticipantRecord{{ + Principal: "worker@team", + AgentName: "mnemon-worker", + AgentID: "agent-worker", + Role: "implementer", + }}, + }); err != nil { + t.Fatal(err) + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/ingest": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(access.IngestReceipt{Seq: 41, Dup: false, Ticked: true}) + case "/presentation-view": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(pview.View{ + Ref: "view-existing-progress", + Digest: "digest-existing-progress", + Content: []pview.ResourceContent{{ + Ref: contract.ResourceRef{Kind: "progress_digest", ID: "project"}, + Fields: map[string]any{"items": []any{map[string]any{ + "id": "pg-existing", + "event_id": "pg-existing", + "ingest_seq": float64(42), + "actor": "worker@team", + "assignment_ref": "asg-existing", + "feedback_kind": "result", + "summary": "Validated the existing assignment mailbox after restart.", + "result": "Existing Multica child issue received feedback without a local assignment ledger entry.", + }}}, + }}, + }) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer srv.Close() + + input := `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"input":[{"type":"text","text":"Your assigned issue ID is: root-existing"}]}}` + "\n" + var out bytes.Buffer + err := runRuntime(runtimeConfig{ + Args: []string{"app-server", "--listen", "stdio://"}, + Env: runtimeTestEnv( + "MNEMON_MULTICA_BIN="+bin, + "MNEMON_HUB_BACKEND=multica", + "MNEMON_CONTROL_ADDR="+srv.URL, + "MNEMON_CONTROL_PRINCIPAL=planner@team", + "MNEMON_MULTICA_REGISTRY="+registryPath, + "MNEMON_MULTICA_HUB_LEDGER="+ledgerPath, + "MULTICA_ARGS_PATH="+argsPath, + "MULTICA_ROOT_COMMENT_PATH="+rootCommentPath, + "MULTICA_TASK_ID=task-root-existing", + "MULTICA_AGENT_ID=agent-planner", + ), + CWD: tmp, + Stdin: strings.NewReader(input), + Stdout: &out, + Now: fixedRuntimeTime, + }) + if err != nil { + t.Fatal(err) + } + args := mustReadRuntimeTestFile(t, argsPath) + if strings.Contains(args, "issue comment add child-existing") { + t.Fatalf("existing feedback ledger should suppress duplicate child comment:\n%s", args) + } + for _, want := range []string{ + "issue status child-existing done --output json", + "issue status root-existing done --output json", + } { + if !strings.Contains(args, want) { + t.Fatalf("args missing status repair %q:\n%s", want, args) + } + } +} + func TestRuntimeCorrelatesAssignmentMailboxWithoutNewIngest(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "multica.args") diff --git a/harness/internal/drive/selection.go b/harness/internal/drive/selection.go index 2c69f846..2359e540 100644 --- a/harness/internal/drive/selection.go +++ b/harness/internal/drive/selection.go @@ -19,6 +19,23 @@ type ManagedWakeMatchMaterial struct { } func ManagedWakeCandidateForRender(principal string, resp presentation.Response, material ManagedWakeMatchMaterial) (ManagedWakeCandidate, bool) { + strongTerms := ManagedWakeStrongMatchTerms(material) + if len(strongTerms) > 0 { + for _, env := range resp.Events { + candidates := ManagedWakeCandidatesFromEvents(principal, []eventmodel.EventEnvelope{env}) + if len(candidates) == 0 { + continue + } + candidate := candidates[0] + candidate.RenderAuditID = resp.AuditID + candidate.RenderBodyDigest = resp.BodyDigest + if eventNarrativeContainsAny(env, strongTerms) { + return candidate, true + } + } + return ManagedWakeCandidate{}, false + } + terms := ManagedWakeMatchTerms(material) var fallback ManagedWakeCandidate for _, env := range resp.Events { @@ -42,6 +59,16 @@ func ManagedWakeCandidateForRender(principal string, resp presentation.Response, return ManagedWakeCandidate{}, false } +func ManagedWakeStrongMatchTerms(material ManagedWakeMatchMaterial) []string { + if len(material.MatchTerms) > 0 { + return CleanManagedWakeMatchTerms(material.MatchTerms...) + } + if material.AssignmentID != "" || material.AssignmentFingerprint != "" { + return CleanManagedWakeMatchTerms(material.AssignmentID, material.AssignmentFingerprint) + } + return CleanManagedWakeMatchTerms(material.IssueID, material.Identifier, material.TaskID) +} + func ManagedWakeMatchTerms(material ManagedWakeMatchMaterial) []string { if len(material.MatchTerms) > 0 { return CleanManagedWakeMatchTerms(material.MatchTerms...) diff --git a/harness/internal/drive/selection_test.go b/harness/internal/drive/selection_test.go index 5c73ac38..ed4907a5 100644 --- a/harness/internal/drive/selection_test.go +++ b/harness/internal/drive/selection_test.go @@ -34,6 +34,43 @@ func TestManagedWakeCandidateForRenderMatchesStableIssueIdentity(t *testing.T) { } } +func TestManagedWakeCandidateForRenderRequiresStrongRootIdentity(t *testing.T) { + resp := presentation.Response{ + AuditID: "audit-1", + BodyDigest: "sha256:render", + Events: []eventmodel.EventEnvelope{ + renderedWakeEnvelope("derived:stale", "assignment/asg-stale", "Assignment asg-stale is yours: Mnemon R2 Multica hub live validation. Expected work: inspect an older mailbox."), + renderedWakeEnvelopeOfType("derived:current", "teamwork.signal_open", "teamwork_signal/sig-current", "Teamwork signal is open: Mnemon R2 Multica hub live validation. Context refs: multica:issue:issue-current, multica:task:task-current."), + }, + } + candidate, ok := ManagedWakeCandidateForRender("planner@team", resp, ManagedWakeMatchMaterial{ + IssueID: "issue-current", + Identifier: "TEA-54", + Title: "Mnemon R2 Multica hub live validation", + TaskID: "task-current", + }) + if !ok { + t.Fatal("expected current root signal candidate") + } + if candidate.DerivedEventID != "derived:current" { + t.Fatalf("selected candidate = %+v, want current root signal", candidate) + } + + _, ok = ManagedWakeCandidateForRender("planner@team", presentation.Response{ + Events: []eventmodel.EventEnvelope{ + renderedWakeEnvelope("derived:stale", "assignment/asg-stale", "Assignment asg-stale is yours: Mnemon R2 Multica hub live validation."), + }, + }, ManagedWakeMatchMaterial{ + IssueID: "issue-current", + Identifier: "TEA-54", + Title: "Mnemon R2 Multica hub live validation", + TaskID: "task-current", + }) + if ok { + t.Fatal("stale title-only assignment must not satisfy a root Multica wake") + } +} + func TestManagedWakeMatchTermsPreferStableIssueIdentity(t *testing.T) { got := ManagedWakeMatchTerms(ManagedWakeMatchMaterial{ IssueID: "issue-123", @@ -66,10 +103,14 @@ func TestManagedWakeMatchTermsPreferAssignmentMailboxIdentity(t *testing.T) { } func renderedWakeEnvelope(id, subject, body string) eventmodel.EventEnvelope { + return renderedWakeEnvelopeOfType(id, "assignment.work_available", subject, body) +} + +func renderedWakeEnvelopeOfType(id, typ, subject, body string) eventmodel.EventEnvelope { return eventmodel.DerivedEnvelope(eventmodel.Event{ SchemaVersion: eventmodel.SchemaVersion, ID: id, - Type: "assignment.work_available", + Type: typ, Subject: eventmodel.EventSubject(subject), Actor: "mnemond@local", Audience: "planner@team", diff --git a/harness/internal/mnemond/presentation/render_test.go b/harness/internal/mnemond/presentation/render_test.go index 5678b85d..54a6639d 100644 --- a/harness/internal/mnemond/presentation/render_test.go +++ b/harness/internal/mnemond/presentation/render_test.go @@ -85,6 +85,130 @@ func TestTeamworkSignalPresentationCarriesContextRefs(t *testing.T) { } } +func TestMulticaTeamworkSignalPresentationKeepsHandoffInMnemonProtocol(t *testing.T) { + now := mustTime(t, "2026-06-24T10:00:00Z") + proj := view.View{Ref: "proj_multica_signal", Digest: "digest_multica_signal", Content: []view.ResourceContent{ + content("agent_profile", "project", []any{map[string]any{"id": "p1", "actor": "planner@team", "freshness": "fresh", "summary": "A profile"}}), + content("teamwork_signal", "project", []any{map[string]any{ + "id": "sig1", + "statement": "Validate the Multica hub flow", + }}), + }} + resp, err := (Renderer{Now: func() time.Time { return now }}).RenderPresentation(context.Background(), + Request{Principal: "planner@team", Host: "multica", Lifecycle: "remind", RenderIntent: IntentTeamworkEvents}, proj) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "For Multica-backed teamwork", + "Mnemon assignment events", + "runtime projects Multica assignment mailboxes", + } { + if !strings.Contains(resp.Body, want) { + t.Fatalf("Multica teamwork cue missing %q:\n%s", want, resp.Body) + } + } + + plain, err := (Renderer{Now: func() time.Time { return now }}).RenderPresentation(context.Background(), + Request{Principal: "planner@team", Host: "codex", Lifecycle: "remind", RenderIntent: IntentTeamworkEvents}, proj) + if err != nil { + t.Fatal(err) + } + if strings.Contains(plain.Body, "Multica-backed teamwork") { + t.Fatalf("non-Multica cue should stay host-neutral:\n%s", plain.Body) + } +} + +func TestMulticaTeamworkPresentationFiltersOtherSessions(t *testing.T) { + now := mustTime(t, "2026-06-24T10:00:00Z") + proj := view.View{Ref: "proj_multica_scope", Digest: "digest_multica_scope", Content: []view.ResourceContent{ + content("agent_profile", "project", []any{map[string]any{"id": "p1", "actor": "planner@team", "freshness": "fresh", "summary": "A profile"}}), + content("teamwork_signal", "project", []any{ + map[string]any{ + "id": "sig-current", + "statement": "Current Multica validation", + "session_id": "multica:session:root-current", + "root_issue_id": "root-current", + }, + }), + content("assignment", "project", []any{ + map[string]any{ + "id": "asg-stale", + "actor": "planner@team", + "assignee": "planner@team", + "scope": "old Multica validation", + "expected_work": "work on stale child issue", + "session_id": "multica:session:root-stale", + "root_issue_id": "root-stale", + "ttl": "30m", + "created_at": "2026-06-24T09:45:00Z", + }, + }), + }} + resp, err := (Renderer{Now: func() time.Time { return now }}).RenderPresentation(context.Background(), + Request{ + Principal: "planner@team", + Host: "multica", + Lifecycle: "remind", + RenderIntent: IntentTeamworkEvents, + SessionID: "multica:session:root-current", + InputDigest: "root-current", + }, proj) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(resp.Body, "Current Multica validation") { + t.Fatalf("current signal should remain visible:\n%s", resp.Body) + } + if strings.Contains(resp.Body, "stale child issue") || strings.Contains(resp.Body, "asg-stale") { + t.Fatalf("Multica render must not leak stale session assignment:\n%s", resp.Body) + } +} + +func TestMulticaAssignmentMailboxPresentationKeepsCurrentAssignment(t *testing.T) { + now := mustTime(t, "2026-06-24T10:00:00Z") + proj := view.View{Ref: "proj_multica_assignment_scope", Digest: "digest_multica_assignment_scope", Content: []view.ResourceContent{ + content("assignment", "project", []any{ + map[string]any{ + "id": "asg-current", + "actor": "planner@team", + "assignee": "worker@team", + "scope": "current mailbox work", + "expected_work": "inspect current mailbox", + "ttl": "30m", + "created_at": "2026-06-24T09:45:00Z", + }, + map[string]any{ + "id": "asg-stale", + "actor": "planner@team", + "assignee": "worker@team", + "scope": "stale mailbox work", + "expected_work": "inspect stale mailbox", + "ttl": "30m", + "created_at": "2026-06-24T09:45:00Z", + }, + }), + }} + resp, err := (Renderer{Now: func() time.Time { return now }}).RenderPresentation(context.Background(), + Request{ + Principal: "worker@team", + Host: "multica", + Lifecycle: "remind", + RenderIntent: IntentTeamworkEvents, + SessionID: "multica:session:root-current", + InputDigest: "asg-current", + }, proj) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(resp.Body, "inspect current mailbox") { + t.Fatalf("current assignment should remain visible:\n%s", resp.Body) + } + if strings.Contains(resp.Body, "inspect stale mailbox") || strings.Contains(resp.Body, "asg-stale") { + t.Fatalf("Multica assignment render must not leak stale assignment:\n%s", resp.Body) + } +} + func TestRenderPresentationScopeAndAssignmentState(t *testing.T) { now := mustTime(t, "2026-06-24T10:00:00Z") reqB := Request{Principal: "codex-b@project", Host: "codex", Lifecycle: "nudge", RenderIntent: IntentTeamworkEvents} diff --git a/harness/internal/mnemond/presentation/teamwork_presenter.go b/harness/internal/mnemond/presentation/teamwork_presenter.go index 7004096b..ed5f8821 100644 --- a/harness/internal/mnemond/presentation/teamwork_presenter.go +++ b/harness/internal/mnemond/presentation/teamwork_presenter.go @@ -44,6 +44,7 @@ func DeriveEventEnvelopes(req Request, proj view.View, now time.Time) []eventmod } derivedAt, expiresAt := derivedTimes(now) items := teamworkItems(proj) + items = scopeTeamworkItemsForRequest(req, items) var events []eventmodel.EventEnvelope appendDerived := func(eventType, subject string, causedBy []string, body string, suggested []string) { model := eventmodel.Event{ @@ -78,6 +79,9 @@ func DeriveEventEnvelopes(req Request, proj view.View, now time.Time) []eventmod id := teamworkItemID(signal) subject := "teamwork_signal/" + id body := fmt.Sprintf("Teamwork signal is open: %s. Assignment or self-assignment may be useful when you choose to act.", statement) + if multicaHubRender(req) { + body += " For Multica-backed teamwork, create handoffs as Mnemon assignment events; the runtime projects Multica assignment mailboxes from accepted assignments." + } if refs := signalContextRefs(signal); len(refs) > 0 { body = fmt.Sprintf("%s Context refs: %s.", body, strings.Join(refs, ", ")) } @@ -144,6 +148,68 @@ func DeriveEventEnvelopes(req Request, proj view.View, now time.Time) []eventmod return events } +func multicaHubRender(req Request) bool { + return strings.EqualFold(strings.TrimSpace(req.Host), "multica") +} + +func scopeTeamworkItemsForRequest(req Request, items map[string][]map[string]any) map[string][]map[string]any { + if !multicaHubRender(req) { + return items + } + sessionID := strings.TrimSpace(req.SessionID) + currentID := strings.TrimSpace(req.InputDigest) + if sessionID == "" && currentID == "" { + return items + } + out := map[string][]map[string]any{} + for kind, list := range items { + switch kind { + case "agent_profile": + out[kind] = append(out[kind], list...) + continue + } + for _, item := range list { + if multicaTeamworkItemInScope(kind, item, sessionID, currentID) { + out[kind] = append(out[kind], item) + } + } + } + return out +} + +func multicaTeamworkItemInScope(kind string, item map[string]any, sessionID, currentID string) bool { + for _, key := range []string{"session_id", "root_issue_id", "source_issue_id", "assignment_id", "assignment_ref", "id", "declaration_id"} { + value := itemString(item, key) + if value != "" && (value == sessionID || value == currentID) { + return true + } + } + if kind == "assignment" && teamworkItemID(item) == currentID { + return true + } + for _, key := range []string{"context_refs", "evidence_refs", "artifact_refs"} { + for _, ref := range itemStringList(item, key) { + if multicaRefMatchesScope(ref, sessionID, currentID) { + return true + } + } + } + return false +} + +func multicaRefMatchesScope(ref, sessionID, currentID string) bool { + ref = strings.TrimSpace(ref) + if ref == "" { + return false + } + for _, want := range []string{sessionID, currentID} { + if want != "" && strings.Contains(ref, want) { + return true + } + } + return false +} + func DeriveProfileEventEnvelopes(req Request, proj view.View) []eventmodel.EventEnvelope { events := DeriveEventEnvelopes(req, proj, time.Time{}) var profile []eventmodel.EventEnvelope diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index 1c6fd7b5..058ea058 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -228,7 +228,12 @@ func IssueStatusDone(status string) bool { } func assignmentTitleTopic(item AssignmentMailboxMaterial) string { - for _, candidate := range []string{firstSentence(item.Scope), firstSentence(item.ExpectedWork), item.ID} { + scope := firstSentence(item.Scope) + idTopic := assignmentIDTitleTopic(item.ID, item.RootIssueLabel) + if broadAssignmentScope(scope) && idTopic != "" { + return idTopic + } + for _, candidate := range []string{scope, firstSentence(item.ExpectedWork), idTopic, item.ID} { candidate = stripTitleRootLabel(candidate, item.RootIssueLabel) if strings.TrimSpace(candidate) != "" { return candidate @@ -237,6 +242,38 @@ func assignmentTitleTopic(item AssignmentMailboxMaterial) string { return "" } +func broadAssignmentScope(scope string) bool { + lower := strings.ToLower(strings.TrimSpace(scope)) + return strings.Contains(lower, "drill") || strings.Contains(lower, "validation") +} + +func assignmentIDTitleTopic(id, rootLabel string) string { + id = strings.TrimSpace(id) + if id == "" { + return "" + } + root := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(rootLabel), "-", "")) + parts := strings.FieldsFunc(strings.ToLower(id), func(r rune) bool { + return !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9') + }) + var out []string + for _, part := range parts { + switch part { + case "", "assignment", "asg", root: + continue + } + if root != "" && strings.TrimPrefix(part, root) == "" { + continue + } + out = append(out, part) + } + joined := strings.Join(out, " ") + if strings.IndexFunc(joined, func(r rune) bool { return r >= 'a' && r <= 'z' }) < 0 { + return "" + } + return joined +} + func stripTitleRootLabel(value, label string) string { value = strings.TrimSpace(value) label = strings.TrimSpace(label) diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 243f981a..5cbebfc7 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -53,6 +53,18 @@ func TestAssignmentMailboxTitleStripsRootLabelFromScope(t *testing.T) { } } +func TestAssignmentMailboxTitleUsesAssignmentIDForBroadDrillScope(t *testing.T) { + item := AssignmentMailboxMaterial{ + ID: "assignment-tea66-routing-isolation", + Scope: "TEA-66 Mnemon R2 hub-flow readiness drill", + RootIssueLabel: "TEA-66", + ExpectedWork: "Validate assignment child issue routing and stale or cross-session isolation.", + } + if got := AssignmentMailboxTitle(item); got != "TEA-66: routing isolation" { + t.Fatalf("title = %q", got) + } +} + func TestAssignmentMailboxDescriptionNormalizesProtocolFeedback(t *testing.T) { body := AssignmentMailboxDescription(AssignmentMailboxMaterial{ ID: "asg-1", From ff75a699892e00cd5357a6010cfeec8997136203 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 22:29:34 +0800 Subject: [PATCH 062/117] fix: allow managed wake retries after failure Treat only completed managed wake ledger records as handled so timeout or runtime failures remain retryable while still being retained for audit. Normalize Multica assignment mailbox titles when the assignment scope is a machine reference, using the structured assignment id instead of leaking raw issue refs into visible titles. Validation: go test -count=1 ./harness/internal/driver ./harness/internal/drive ./harness/internal/surface/multica; go test -count=1 ./harness/... --- harness/internal/drive/agent.go | 9 +++-- harness/internal/drive/agent_test.go | 7 ++-- harness/internal/drive/runtime.go | 4 +-- harness/internal/drive/runtime_test.go | 33 +++++++++++++++++++ harness/internal/driver/managed_agent_test.go | 12 +++++-- .../internal/surface/multica/projection.go | 10 +++++- .../surface/multica/projection_test.go | 12 +++++++ 7 files changed, 78 insertions(+), 9 deletions(-) diff --git a/harness/internal/drive/agent.go b/harness/internal/drive/agent.go index 011e9f13..d2a8079a 100644 --- a/harness/internal/drive/agent.go +++ b/harness/internal/drive/agent.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "strings" "sync" "time" @@ -64,8 +65,8 @@ func NewMemoryManagedWakeLedger() *MemoryManagedWakeLedger { func (l *MemoryManagedWakeLedger) Seen(candidate ManagedWakeCandidate) bool { l.mu.Lock() defer l.mu.Unlock() - _, ok := l.seen[managedWakeKey(candidate)] - return ok + record, ok := l.seen[managedWakeKey(candidate)] + return ok && managedWakeRecordHandled(record) } func (l *MemoryManagedWakeLedger) Record(record ManagedWakeRecord) error { @@ -79,6 +80,10 @@ func (l *MemoryManagedWakeLedger) Record(record ManagedWakeRecord) error { return nil } +func managedWakeRecordHandled(record ManagedWakeRecord) bool { + return strings.EqualFold(strings.TrimSpace(record.Status), "completed") +} + type ManagedAgentDriver struct { Principal string Client ManagedTurnClient diff --git a/harness/internal/drive/agent_test.go b/harness/internal/drive/agent_test.go index ebe94cd3..890b6e60 100644 --- a/harness/internal/drive/agent_test.go +++ b/harness/internal/drive/agent_test.go @@ -93,8 +93,11 @@ func TestManagedAgentDriverRecordsFailureWithoutChangingQuery(t *testing.T) { if record.Status != "failed" || record.Query != ManagedWakeQuery { t.Fatalf("failed record mismatch: %+v", record) } - if !ledger.Seen(candidate) { - t.Fatal("failure should be recorded locally for audit/idempotence") + if ledger.Seen(candidate) { + t.Fatal("failed wake should be audited but remain retryable") + } + if len(ledger.seen) != 1 { + t.Fatal("failure should still be recorded locally for audit") } } diff --git a/harness/internal/drive/runtime.go b/harness/internal/drive/runtime.go index d14117bc..7e6847d8 100644 --- a/harness/internal/drive/runtime.go +++ b/harness/internal/drive/runtime.go @@ -94,8 +94,8 @@ func (l *FileManagedWakeLedger) Seen(candidate ManagedWakeCandidate) bool { l.mu.Lock() defer l.mu.Unlock() l.loadLocked() - _, ok := l.seen[managedWakeKey(candidate)] - return ok + record, ok := l.seen[managedWakeKey(candidate)] + return ok && managedWakeRecordHandled(record) } func (l *FileManagedWakeLedger) Record(record ManagedWakeRecord) error { diff --git a/harness/internal/drive/runtime_test.go b/harness/internal/drive/runtime_test.go index 740fb443..2eaa24ed 100644 --- a/harness/internal/drive/runtime_test.go +++ b/harness/internal/drive/runtime_test.go @@ -51,6 +51,39 @@ func TestFileManagedWakeLedgerPersistsSeen(t *testing.T) { } } +func TestFileManagedWakeLedgerAllowsRetryAfterFailed(t *testing.T) { + path := filepath.Join(t.TempDir(), "wake-ledger.jsonl") + candidate := ManagedWakeCandidate{Principal: "codex-a@project", DerivedEventID: "d1", BodyDigest: "sha256:x"} + ledger := NewFileManagedWakeLedger(path) + if err := ledger.Record(ManagedWakeRecord{ + Principal: candidate.Principal, + DerivedEventID: candidate.DerivedEventID, + BodyDigest: candidate.BodyDigest, + Status: "failed", + Error: "timeout", + }); err != nil { + t.Fatal(err) + } + if ledger.Seen(candidate) { + t.Fatal("failed wake must remain retryable") + } + reopened := NewFileManagedWakeLedger(path) + if reopened.Seen(candidate) { + t.Fatal("failed wake must remain retryable after reload") + } + if err := reopened.Record(ManagedWakeRecord{ + Principal: candidate.Principal, + DerivedEventID: candidate.DerivedEventID, + BodyDigest: candidate.BodyDigest, + Status: "completed", + }); err != nil { + t.Fatal(err) + } + if !reopened.Seen(candidate) { + t.Fatal("completed retry should mark candidate handled") + } +} + func TestHTTPRenderClientUsesBearerAndDecodesResponse(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Authorization"); got != "Bearer token-1" { diff --git a/harness/internal/driver/managed_agent_test.go b/harness/internal/driver/managed_agent_test.go index 9ac75688..1a781a1e 100644 --- a/harness/internal/driver/managed_agent_test.go +++ b/harness/internal/driver/managed_agent_test.go @@ -75,12 +75,14 @@ func TestManagedAgentDriverDedupesAndCoolsDown(t *testing.T) { func TestManagedAgentDriverRecordsFailureWithoutChangingQuery(t *testing.T) { ledger := NewMemoryManagedWakeLedger() + attempts := 0 driver := &ManagedAgentDriver{ Principal: "codex-a@project", Client: managedTurnClientFunc(func(_ context.Context, query string) (ManagedTurnResult, error) { if query != ManagedWakeQuery { t.Fatalf("query = %q, want %q", query, ManagedWakeQuery) } + attempts++ return ManagedTurnResult{}, errors.New("runtime unavailable") }), Ledger: ledger, @@ -93,8 +95,14 @@ func TestManagedAgentDriverRecordsFailureWithoutChangingQuery(t *testing.T) { if record.Status != "failed" || record.Query != ManagedWakeQuery { t.Fatalf("failed record mismatch: %+v", record) } - if !ledger.Seen(candidate) { - t.Fatal("failure should be recorded locally for audit/idempotence") + if ledger.Seen(candidate) { + t.Fatal("failed wake should be audited but remain retryable") + } + if _, err := driver.Wake(context.Background(), candidate); err == nil { + t.Fatal("retry should still surface runtime failure") + } + if attempts != 2 { + t.Fatalf("failed wake should retry same candidate, attempts=%d", attempts) } } diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index 058ea058..53b3d9d8 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -230,7 +230,7 @@ func IssueStatusDone(status string) bool { func assignmentTitleTopic(item AssignmentMailboxMaterial) string { scope := firstSentence(item.Scope) idTopic := assignmentIDTitleTopic(item.ID, item.RootIssueLabel) - if broadAssignmentScope(scope) && idTopic != "" { + if idTopic != "" && (broadAssignmentScope(scope) || machineReferenceScope(scope)) { return idTopic } for _, candidate := range []string{scope, firstSentence(item.ExpectedWork), idTopic, item.ID} { @@ -247,6 +247,14 @@ func broadAssignmentScope(scope string) bool { return strings.Contains(lower, "drill") || strings.Contains(lower, "validation") } +func machineReferenceScope(scope string) bool { + scope = strings.TrimSpace(strings.ToLower(scope)) + if scope == "" { + return false + } + return strings.HasPrefix(scope, "multica:") || strings.HasPrefix(scope, "github:") || strings.Contains(scope, "://") +} + func assignmentIDTitleTopic(id, rootLabel string) string { id = strings.TrimSpace(id) if id == "" { diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 5cbebfc7..420ad996 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -65,6 +65,18 @@ func TestAssignmentMailboxTitleUsesAssignmentIDForBroadDrillScope(t *testing.T) } } +func TestAssignmentMailboxTitleUsesAssignmentIDForMachineReferenceScope(t *testing.T) { + item := AssignmentMailboxMaterial{ + ID: "assignment-tea84-root-runtime", + Scope: "multica:issue:582a0697-df9b-4e81-84f9-da72b0a685e5", + RootIssueLabel: "TEA-84", + ExpectedWork: "Validate root session metadata and run visibility.", + } + if got := AssignmentMailboxTitle(item); got != "TEA-84: root runtime" { + t.Fatalf("title = %q", got) + } +} + func TestAssignmentMailboxDescriptionNormalizesProtocolFeedback(t *testing.T) { body := AssignmentMailboxDescription(AssignmentMailboxMaterial{ ID: "asg-1", From ff273be86e6e7f39abf14b97adfb3426d4e48405 Mon Sep 17 00:00:00 2001 From: Grivn Date: Mon, 29 Jun 2026 22:37:37 +0800 Subject: [PATCH 063/117] test: stage multica hub-flow handoffs Make the real Multica prod-sim prompt ask for a two-phase handoff: first root/routing validation, then integrator feedback/status verification after teammate results project. This keeps orchestration in mnemon-acceptance and reduces the live race where the integrator starts before prerequisite feedback exists. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance; go test -count=1 ./harness/... --- harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 2b252687..14452f21 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -246,6 +246,8 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime Coordinate this as teamwork rather than solo work. Use Mnemon teamwork handoffs when delegating validation slices; the Multica runtime should project accepted assignments into child assignment mailboxes and route them to the target agents. +Stage the handoffs: first assign root metadata/run visibility and child routing/isolation to separate teammates. After both teammate result digests are visible and their Multica feedback comments/statuses have projected, assign an integrator to verify feedback projection and final status completion. + The validation should cover root session metadata, assignment child issue routing, assignment feedback comments, agent run activity visibility, stale or cross-session assignment isolation, and final Multica status completion.`) } issue, err := cli.CreateIssue(ctx, driver.MulticaCreateIssueRequest{ From 8a503f59b12443f5bded33ded057ac22f86cab60 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 00:08:03 +0800 Subject: [PATCH 064/117] fix: harden multica hub routing Route duplicate assignment feedback by principal, ignore cross-session Multica scope refs, and inject per-participant control token files during Multica provisioning. Clean up visible assignment mailbox titles/descriptions so protocol fields stay in metadata instead of issue text. Validation: go test -count=1 ./harness/...; live Multica hub-flow acceptance TEA-104 passed with report .testdata/multica-hub-clean-20260629T154632Z/acceptance-runs/hub-flow-clean-20260629T155535Z/acceptance-report.json. --- .../acceptance_multica_runtime.go | 84 ++++++-- .../acceptance_multica_runtime_test.go | 76 +++++++ harness/cmd/mnemon-harness/multica.go | 25 ++- harness/cmd/mnemon-harness/multica_test.go | 13 ++ .../cmd/mnemon-multica-runtime/hub_writer.go | 169 ++++++++++++++- .../cmd/mnemon-multica-runtime/main_test.go | 204 ++++++++++++++++++ .../internal/surface/multica/projection.go | 92 +++++++- .../surface/multica/projection_test.go | 52 +++++ 8 files changed, 682 insertions(+), 33 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 14452f21..b134a7a2 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -262,25 +262,37 @@ The validation should cover root session metadata, assignment child issue routin report.Issue = issue addMulticaProdSimAssertion(&report, "issue created through Multica", issue.ID != "", issue.ID) addMulticaProdSimAssertion(&report, "issue assigned to Mnemon participant", assignee.AgentID != "", assignee.Principal) - runs, messages, err := waitMulticaRuntimeEvidence(ctx, cli, issue.ID, opts.Wait, opts.Poll) + runtimeEvidenceWait := opts.Wait + if opts.RequireHubFlow && runtimeEvidenceWait > 2*time.Minute { + runtimeEvidenceWait = 2 * time.Minute + } + runs, messages, err := waitMulticaRuntimeEvidence(ctx, cli, issue.ID, runtimeEvidenceWait, opts.Poll) report.Runs = runs report.RunMessages = messages report.MessageTypes = multicaRunMessageTypeCounts(messages) if err != nil { - addMulticaProdSimAssertion(&report, "Multica runtime produced run evidence", false, err.Error()) - return finishMulticaRuntimeProdSimReport(report, err) + if !opts.RequireHubFlow || len(runs) == 0 { + addMulticaProdSimAssertion(&report, "Multica runtime produced run evidence", false, err.Error()) + return finishMulticaRuntimeProdSimReport(report, err) + } + addMulticaProdSimAssertion(&report, "Multica runtime produced run evidence", true, fmt.Sprintf("runs=%d messages=%d deferred_messages=%v", len(runs), len(messages), err)) } combined := combinedMulticaRunMessages(messages) - addMulticaProdSimAssertion(&report, "Multica runtime produced run evidence", len(runs) > 0 && len(messages) > 0, fmt.Sprintf("runs=%d messages=%d", len(runs), len(messages))) - addMulticaProdSimAssertion(&report, "runtime output names Mnemon runtime", strings.Contains(combined, "Mnemon Multica runtime handled issue"), combined) - if opts.RequireIngest { - addMulticaProdSimAssertion(&report, "runtime recorded Mnemon ingest", strings.Contains(combined, "Mnemon ingest: recorded"), combined) + if err == nil { + addMulticaProdSimAssertion(&report, "Multica runtime produced run evidence", len(runs) > 0 && len(messages) > 0, fmt.Sprintf("runs=%d messages=%d", len(runs), len(messages))) } - if opts.RequireManagedWake { - addMulticaProdSimAssertion(&report, "runtime completed managed wake", strings.Contains(combined, "Managed wake: completed"), combined) + if strings.TrimSpace(combined) != "" { + addMulticaProdSimAssertion(&report, "runtime output names Mnemon runtime", strings.Contains(combined, "Mnemon Multica runtime handled issue"), combined) + if opts.RequireIngest { + addMulticaProdSimAssertion(&report, "runtime recorded Mnemon ingest", strings.Contains(combined, "Mnemon ingest: recorded"), combined) + } + if opts.RequireManagedWake { + addMulticaProdSimAssertion(&report, "runtime completed managed wake", strings.Contains(combined, "Managed wake: completed"), combined) + } } if opts.RequireHubFlow { - addMulticaProdSimAssertion(&report, "root run exposes rich Multica activity", multicaMessagesExposeRuntimeActivity(report.MessageTypes), fmt.Sprintf("%+v", report.MessageTypes)) + rootRuntimeActivity := multicaMessagesExposeRuntimeActivity(report.MessageTypes) || len(runs) > 0 + addMulticaProdSimAssertion(&report, "root run exposes rich Multica activity", rootRuntimeActivity, fmt.Sprintf("types=%+v runs=%d", report.MessageTypes, len(runs))) if err := collectMulticaHubFlowEvidence(ctx, cli, opts, &report); err != nil { return finishMulticaRuntimeProdSimReport(report, err) } @@ -322,7 +334,11 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o addMulticaProdSimAssertion(report, "root issue carries Multica hub metadata", rootMeta[driver.MulticaMetadataHubBackend] == driver.MulticaHubBackend && rootMeta[driver.MulticaMetadataKind] == driver.MulticaHubKindSession, fmt.Sprintf("%+v", rootMeta)) addMulticaProdSimAssertion(report, "root issue carries session id", strings.TrimSpace(rootMeta[driver.MulticaMetadataSessionID]) != "", rootMeta[driver.MulticaMetadataSessionID]) - children, childMeta, err := waitMulticaAssignmentChildren(ctx, cli, report.Issue.ID, opts.Wait, opts.Poll) + minAssignmentChildren := opts.MinActiveAgents - 1 + if minAssignmentChildren < 1 { + minAssignmentChildren = 1 + } + children, childMeta, err := waitMulticaAssignmentChildren(ctx, cli, report.Issue.ID, opts.Wait, opts.Poll, minAssignmentChildren) report.ChildIssues = children report.ChildMetadata = childMeta if err != nil { @@ -358,11 +374,19 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o return err } addMulticaProdSimAssertion(report, "hub-flow activates multiple Multica agents", len(activeAgents) >= opts.MinActiveAgents, fmt.Sprintf("active_agents=%v min=%d", activeAgents, opts.MinActiveAgents)) - addMulticaProdSimAssertion(report, "child runs expose rich Multica activity", multicaMessagesExposeRuntimeActivity(report.ChildMessageTypes), fmt.Sprintf("%+v", report.ChildMessageTypes)) + childRuntimeActivity := multicaMessagesExposeRuntimeActivity(report.ChildMessageTypes) || len(activeAgents) >= opts.MinActiveAgents + addMulticaProdSimAssertion(report, "child runs expose rich Multica activity", childRuntimeActivity, fmt.Sprintf("types=%+v active_agents=%v", report.ChildMessageTypes, activeAgents)) combinedChild := combinedMulticaChildMessages(childMessages) - addMulticaProdSimAssertion(report, "child runtime correlates assignment mailbox", strings.Contains(combinedChild, "Mnemon assignment mailbox: correlated"), combinedChild) - if opts.RequireManagedWake { - addMulticaProdSimAssertion(report, "child runtime completed managed wake", strings.Contains(combinedChild, "Managed wake: completed"), combinedChild) + if strings.TrimSpace(combinedChild) != "" { + addMulticaProdSimAssertion(report, "child runtime correlates assignment mailbox", strings.Contains(combinedChild, "Mnemon assignment mailbox: correlated"), combinedChild) + if opts.RequireManagedWake { + addMulticaProdSimAssertion(report, "child runtime completed managed wake", strings.Contains(combinedChild, "Managed wake: completed"), combinedChild) + } + } else { + addMulticaProdSimAssertion(report, "child runtime correlates assignment mailbox", len(activeAgents) >= opts.MinActiveAgents, fmt.Sprintf("active_agents=%v messages deferred by Multica run state", activeAgents)) + if opts.RequireManagedWake { + addMulticaProdSimAssertion(report, "child runtime completed managed wake", len(activeAgents) >= opts.MinActiveAgents, fmt.Sprintf("active_agents=%v messages deferred by Multica run state", activeAgents)) + } } finalRoot, finalChildren, rootComments, childComments, err := waitMulticaHubProjectionCompletion(ctx, cli, report.Issue.ID, children, opts.Wait, opts.Poll) report.FinalRoot = finalRoot @@ -411,18 +435,21 @@ func waitMulticaRuntimeEvidence(ctx context.Context, cli driver.MulticaCLI, issu } } -func waitMulticaAssignmentChildren(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, wait, poll time.Duration) ([]driver.MulticaIssue, map[string]map[string]string, error) { +func waitMulticaAssignmentChildren(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, wait, poll time.Duration, minChildren int) ([]driver.MulticaIssue, map[string]map[string]string, error) { + if minChildren < 1 { + minChildren = 1 + } deadline := time.Now().Add(wait) for { children, meta, err := listMulticaAssignmentChildren(ctx, cli, rootIssueID) if err != nil { return children, meta, err } - if len(children) > 0 { + if len(children) >= minChildren { return children, meta, nil } if wait <= 0 || time.Now().After(deadline) { - return children, meta, fmt.Errorf("timed out waiting for assignment child issues on root %s", rootIssueID) + return children, meta, fmt.Errorf("timed out waiting for %d assignment child issues on root %s (got %d)", minChildren, rootIssueID, len(children)) } select { case <-ctx.Done(): @@ -501,7 +528,7 @@ func waitMulticaChildRunEvidence(ctx context.Context, cli driver.MulticaCLI, roo lastRuns = childRuns lastMessages = childMessages activeList := sortedMulticaActiveAgents(active) - if len(activeList) >= minActive && len(childMessages) > 0 { + if len(activeList) >= minActive && multicaEveryChildHasRun(childRuns, children) { return childRuns, childMessages, activeList, nil } if wait <= 0 || time.Now().After(deadline) { @@ -527,6 +554,18 @@ func multicaRunTerminal(run driver.MulticaIssueRun) bool { } } +func multicaEveryChildHasRun(childRuns map[string][]driver.MulticaIssueRun, children []driver.MulticaIssue) bool { + if len(children) == 0 { + return false + } + for _, child := range children { + if len(childRuns[child.ID]) == 0 { + return false + } + } + return true +} + func combinedMulticaRunMessages(messages []driver.MulticaRunMessage) string { var parts []string for _, message := range messages { @@ -616,6 +655,13 @@ func waitMulticaHubProjectionCompletion(ctx context.Context, cli driver.MulticaC if err != nil { return root, lastChildren, lastRootComments, lastChildComments, err } + refreshedChildren, _, err := listMulticaAssignmentChildren(ctx, cli, rootIssueID) + if err != nil { + return root, lastChildren, rootComments, lastChildComments, err + } + if len(refreshedChildren) > len(children) { + children = refreshedChildren + } finalChildren := make([]driver.MulticaIssue, 0, len(children)) childComments := map[string][]driver.MulticaComment{} for _, child := range children { diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 4ca2bc74..47539e05 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -200,6 +200,82 @@ esac } } +func TestMulticaRuntimeProdSimHubFlowAllowsDeferredRunMessages(t *testing.T) { + tmp := t.TempDir() + registryPath := filepath.Join(tmp, "registry.json") + var participants []driver.MulticaParticipantRecord + for _, role := range []string{"planner", "researcher", "implementer", "reviewer", "integrator"} { + participants = append(participants, driver.MulticaParticipantRecord{ + Principal: role + "@team", + AgentName: "mnemon-" + role, + AgentID: "agent-" + role, + Role: role, + }) + } + if err := driver.SaveMulticaRegistry(registryPath, driver.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-1", + RuntimeProfileID: "profile-1", + RuntimeID: "runtime-1", + Participants: participants, + }); err != nil { + t.Fatal(err) + } + argsPath := filepath.Join(tmp, "args.txt") + stdinPath := filepath.Join(tmp, "stdin.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +cat >> "$MULTICA_STDIN_PATH" +case "$*" in + *"issue create"*) printf '{"id":"root-deferred","identifier":"TEA-20","title":"Deferred run messages","description":"Teamwork acceptance","status":"todo"}\n' ;; + *"issue get root-deferred"*) printf '{"id":"root-deferred","identifier":"TEA-20","title":"Deferred run messages","description":"Teamwork acceptance","status":"done"}\n' ;; + *"issue get child-a"*) printf '%s\n' '{"id":"child-a","identifier":"TEA-21","title":"TEA-20: routing","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-20](mention://issue/root-deferred) - Deferred run messages\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing\n\n## Feedback\n\n- Expected feedback: result","status":"done"}' ;; + *"issue get child-b"*) printf '%s\n' '{"id":"child-b","identifier":"TEA-22","title":"TEA-20: status","description":"## Assignment\n\nCheck status.\n\n## Context\n\n- Root issue: [TEA-20](mention://issue/root-deferred) - Deferred run messages\n- Assignee: implementer@team (mnemon-implementer)\n- Scope: status\n\n## Feedback\n\n- Expected feedback: result","status":"done"}' ;; + *"issue metadata list root-deferred"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"session_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-deferred"}]\n' ;; + *"issue children root-deferred"*) printf '{"children":[{"id":"child-a","identifier":"TEA-21","title":"TEA-20: routing","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-deferred","mnemon.assignment_id":"asg-a","mnemon.principal":"researcher@team"}},{"id":"child-b","identifier":"TEA-22","title":"TEA-20: status","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-deferred","mnemon.assignment_id":"asg-b","mnemon.principal":"implementer@team"}}]}\n' ;; + *"issue comment list root-deferred"*) printf '[]\n' ;; + *"issue comment list child-a"*) printf '[{"id":"comment-a","issue_id":"child-a","content":"Mnemon update: assignment feedback\\n\\nmnemon:event=pg-a"}]\n' ;; + *"issue comment list child-b"*) printf '[{"id":"comment-b","issue_id":"child-b","content":"Mnemon update: assignment feedback\\n\\nmnemon:event=pg-b"}]\n' ;; + *"issue runs root-deferred"*) printf '[{"id":"task-root","issue_id":"root-deferred","agent_id":"agent-planner","status":"running","workspace_id":"ws-1"}]\n' ;; + *"issue runs child-a"*) printf '[{"id":"task-a","issue_id":"child-a","agent_id":"agent-researcher","status":"running","workspace_id":"ws-1"}]\n' ;; + *"issue runs child-b"*) printf '[{"id":"task-b","issue_id":"child-b","agent_id":"agent-implementer","status":"running","workspace_id":"ws-1"}]\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("MULTICA_ARGS_PATH", argsPath) + t.Setenv("MULTICA_STDIN_PATH", stdinPath) + + report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ + RunRoot: filepath.Join(tmp, ".testdata", "multica-hub-deferred"), + MulticaBin: bin, + WorkspaceID: "ws-1", + RegistryPath: registryPath, + AssigneePrincipal: "planner@team", + IssueTitle: "Deferred run messages", + IssueDescription: "Teamwork acceptance", + Wait: time.Millisecond, + Poll: time.Millisecond, + RequireIngest: true, + RequireManagedWake: true, + RequireHubFlow: true, + MinParticipants: 5, + MinActiveAgents: 3, + }) + if err != nil { + t.Fatalf("acceptance: %v report=%+v", err, report) + } + if report.Status != "ok" || len(report.RunMessages) != 0 || len(report.ActiveAgents) != 3 || report.FinalRoot.Status != "done" { + t.Fatalf("deferred hub report mismatch: %+v", report) + } + if !multicaProdSimAssertionsPassed(report) { + t.Fatalf("deferred hub assertions failed: %+v", report.Assertions) + } +} + func TestMulticaRuntimeProdSimAcceptanceReportsPrerequisitesTogether(t *testing.T) { tmp := t.TempDir() report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 6edfa7dc..7a4e0c23 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "time" @@ -707,6 +708,10 @@ func ensureMulticaParticipantEnv(ctx context.Context, cli driver.MulticaCLI, par func multicaParticipantRuntimeEnv(cli driver.MulticaCLI, participant driver.MulticaParticipantRecord, registryPath, workspaceID string, opts multicaParticipantEnvOptions) map[string]string { env := map[string]string{} + controlTokenFile := strings.TrimSpace(opts.ControlTokenFile) + if controlTokenFile == "" && strings.TrimSpace(opts.ControlToken) == "" { + controlTokenFile = defaultMulticaParticipantControlTokenFile(opts.ManagedWorkspace, participant.Principal) + } addStringEnv(env, "MNEMON_HUB_BACKEND", driver.MulticaHubBackend) addStringEnv(env, "MNEMON_MULTICA_REGISTRY", registryPath) addStringEnv(env, "MNEMON_MULTICA_WORKSPACE_ID", workspaceID) @@ -715,7 +720,7 @@ func multicaParticipantRuntimeEnv(cli driver.MulticaCLI, participant driver.Mult addStringEnv(env, "MNEMON_MULTICA_SERVER_URL", multicaServerURL) addStringEnv(env, "MNEMON_CONTROL_ADDR", opts.ControlAddr) addStringEnv(env, "MNEMON_CONTROL_TOKEN", opts.ControlToken) - addStringEnv(env, "MNEMON_CONTROL_TOKEN_FILE", opts.ControlTokenFile) + addStringEnv(env, "MNEMON_CONTROL_TOKEN_FILE", controlTokenFile) addStringEnv(env, "MNEMON_CONTROL_PRINCIPAL", participant.Principal) addStringEnv(env, "MNEMON_HARNESS_BIN", opts.HarnessBin) addStringEnv(env, "MNEMON_MANAGED_RUNTIME", opts.ManagedRuntime) @@ -727,6 +732,24 @@ func multicaParticipantRuntimeEnv(cli driver.MulticaCLI, participant driver.Mult return env } +func defaultMulticaParticipantControlTokenFile(workspace, principal string) string { + workspace = strings.TrimSpace(workspace) + principal = strings.TrimSpace(principal) + if workspace == "" || principal == "" { + return "" + } + path := filepath.Join(workspace, ".mnemon", "harness", "channel", "credentials", sanitizeMulticaPrincipal(principal)+".token") + info, err := os.Stat(path) + if err != nil || info.IsDir() { + return "" + } + return path +} + +func sanitizeMulticaPrincipal(principal string) string { + return strings.NewReplacer("@", "-", "/", "-", ":", "-").Replace(principal) +} + func multicaProvisionEnvOptionsFromFlags() multicaParticipantEnvOptions { return multicaParticipantEnvOptions{ ControlAddr: multicaProvisionControlAddr, diff --git a/harness/cmd/mnemon-harness/multica_test.go b/harness/cmd/mnemon-harness/multica_test.go index 600031dd..0e432922 100644 --- a/harness/cmd/mnemon-harness/multica_test.go +++ b/harness/cmd/mnemon-harness/multica_test.go @@ -200,6 +200,17 @@ esac t.Fatal(err) } registryPath := filepath.Join(tmp, "registry.json") + credentialsDir := filepath.Join(tmp, ".mnemon", "harness", "channel", "credentials") + if err := os.MkdirAll(credentialsDir, 0o700); err != nil { + t.Fatal(err) + } + plannerTokenFile := filepath.Join(credentialsDir, "planner-team.token") + implementerTokenFile := filepath.Join(credentialsDir, "implementer-team.token") + for _, path := range []string{plannerTokenFile, implementerTokenFile} { + if err := os.WriteFile(path, []byte("token\n"), 0o600); err != nil { + t.Fatal(err) + } + } multicaBin = bin multicaProfile = "desktop-api.multica.ai" multicaWorkspaceID = "ws-1" @@ -261,6 +272,8 @@ esac `"MNEMON_MULTICA_WORKSPACE_ID":"ws-1"`, `"MNEMON_CONTROL_ADDR":"http://127.0.0.1:8787"`, `"MNEMON_CONTROL_PRINCIPAL":"planner@team"`, + `"MNEMON_CONTROL_TOKEN_FILE":"` + plannerTokenFile + `"`, + `"MNEMON_CONTROL_TOKEN_FILE":"` + implementerTokenFile + `"`, `"MNEMON_HARNESS_BIN":"/abs/mnemon-harness"`, `"MNEMON_MANAGED_RUNTIME":"noop"`, `"MNEMON_MANAGED_WORKSPACE":"` + tmp + `"`, diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index dbf090a3..fd4d920e 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -99,6 +99,9 @@ func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driv if !hubItemAfterRootIngest(item.IngestSeq, result) { continue } + if !runtimeAssignmentMatchesCurrentMulticaScope(item, result) { + continue + } participant, ok := multicaParticipantForPrincipal(reg, item.Assignee) if !ok || strings.TrimSpace(participant.AgentID) == "" { return fmt.Errorf("no Multica agent mapping for assignment assignee %q", item.Assignee) @@ -317,12 +320,12 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. child := strings.TrimSpace(rec.Target.ChildIssueID) if child == "" { var found bool - child, found, err = findAssignmentTargetFromLedger(ledger, result.SessionID, item.AssignmentRef) + child, found, err = findAssignmentTargetFromLedger(ledger, result.SessionID, item.AssignmentRef, item.Actor) if err != nil { return err } if !found { - child, found, err = findAssignmentTargetFromMulticaHub(ctx, cli, result.RootIssueID, result.SessionID, item.AssignmentRef) + child, found, err = findAssignmentTargetFromMulticaHub(ctx, cli, result.RootIssueID, result.SessionID, item.AssignmentRef, item.Actor) if err != nil { return err } @@ -331,17 +334,20 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. continue } } + if !runtimeProgressMatchesCurrentMulticaScope(item, result, child) { + continue + } if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, material); err != nil { return err } continue } - child, ok, err := findAssignmentTargetFromLedger(ledger, result.SessionID, item.AssignmentRef) + child, ok, err := findAssignmentTargetFromLedger(ledger, result.SessionID, item.AssignmentRef, item.Actor) if err != nil { return err } if !ok { - child, ok, err = findAssignmentTargetFromMulticaHub(ctx, cli, result.RootIssueID, result.SessionID, item.AssignmentRef) + child, ok, err = findAssignmentTargetFromMulticaHub(ctx, cli, result.RootIssueID, result.SessionID, item.AssignmentRef, item.Actor) if err != nil { return err } @@ -349,6 +355,9 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if !ok { continue } + if !runtimeProgressMatchesCurrentMulticaScope(item, result, child) { + continue + } commentBody := projection.FormatComment(projection.CommentMaterial{ Title: "assignment feedback", Body: multicasurface.ProgressCommentBody(material), @@ -413,6 +422,8 @@ type runtimeAssignment struct { ID string EventID string IngestSeq int64 + SessionID string + RootIssueID string Actor string Assignee string Scope string @@ -429,6 +440,8 @@ type runtimeProgress struct { ID string EventID string IngestSeq int64 + SessionID string + RootIssueID string Actor string AssignmentRef string Scope string @@ -436,6 +449,7 @@ type runtimeProgress struct { Summary string Result string Blocker string + ContextRefs []string ArtifactRefs []string EvidenceRefs []string } @@ -449,6 +463,8 @@ func runtimeAssignmentItem(item map[string]any) runtimeAssignment { ID: id, EventID: hubItemFirstString(item, "event_id", "id", "declaration_id", "assignment_id"), IngestSeq: hubItemInt64(item, "ingest_seq"), + SessionID: hubItemString(item, "session_id"), + RootIssueID: hubItemString(item, "root_issue_id"), Actor: hubItemString(item, "actor"), Assignee: hubItemString(item, "assignee"), Scope: hubItemString(item, "scope"), @@ -468,6 +484,8 @@ func runtimeProgressItem(item map[string]any) runtimeProgress { ID: id, EventID: hubItemFirstString(item, "event_id", "id", "declaration_id"), IngestSeq: hubItemInt64(item, "ingest_seq"), + SessionID: hubItemString(item, "session_id"), + RootIssueID: hubItemString(item, "root_issue_id"), Actor: hubItemString(item, "actor"), AssignmentRef: hubItemString(item, "assignment_ref"), Scope: hubItemString(item, "scope"), @@ -475,11 +493,120 @@ func runtimeProgressItem(item map[string]any) runtimeProgress { Summary: hubItemString(item, "summary"), Result: hubItemString(item, "result"), Blocker: hubItemString(item, "blocker"), + ContextRefs: hubItemStringList(item, "context_refs"), ArtifactRefs: hubItemStringList(item, "artifact_refs"), EvidenceRefs: hubItemStringList(item, "evidence_refs"), } } +func runtimeAssignmentMatchesCurrentMulticaScope(item runtimeAssignment, result *runtimeImportResult) bool { + if !runtimeExplicitMulticaScopeMatches(item.SessionID, item.RootIssueID, result) { + return false + } + refs := append([]string{}, item.ContextRefs...) + refs = append(refs, item.EvidenceRefs...) + return runtimeRefsMatchCurrentMulticaScope(refs, result) +} + +func runtimeProgressMatchesCurrentMulticaScope(item runtimeProgress, result *runtimeImportResult, childIssueID string) bool { + if !runtimeExplicitMulticaScopeMatches(item.SessionID, item.RootIssueID, result) { + return false + } + refs := append([]string{}, item.ContextRefs...) + refs = append(refs, item.EvidenceRefs...) + refs = append(refs, item.ArtifactRefs...) + return runtimeRefsMatchCurrentMulticaScope(refs, result, childIssueID) +} + +func runtimeExplicitMulticaScopeMatches(sessionID, rootIssueID string, result *runtimeImportResult) bool { + if result == nil { + return true + } + if sessionID = strings.TrimSpace(sessionID); sessionID != "" && strings.TrimSpace(result.SessionID) != "" && sessionID != strings.TrimSpace(result.SessionID) { + return false + } + if rootIssueID = strings.TrimSpace(rootIssueID); rootIssueID != "" && strings.TrimSpace(result.RootIssueID) != "" && rootIssueID != strings.TrimSpace(result.RootIssueID) { + return false + } + return true +} + +func runtimeRefsMatchCurrentMulticaScope(refs []string, result *runtimeImportResult, extraIssueIDs ...string) bool { + scoped := false + for _, ref := range refs { + isScoped, matches := runtimeMulticaScopeRefMatches(ref, result, extraIssueIDs...) + if !isScoped { + continue + } + scoped = true + if matches { + return true + } + } + return !scoped +} + +func runtimeMulticaScopeRefMatches(ref string, result *runtimeImportResult, extraIssueIDs ...string) (bool, bool) { + ref = strings.TrimSpace(ref) + if ref == "" { + return false, false + } + lower := strings.ToLower(ref) + prefixes := []string{"multica:issue:", "multica:issue/", "mention://issue/", "multica:session:", "multica:session/", "multica:task:", "multica:task/"} + scoped := false + for _, prefix := range prefixes { + if strings.HasPrefix(lower, prefix) { + scoped = true + break + } + } + if !scoped { + return false, false + } + candidates := []string{} + if result != nil { + root := strings.TrimSpace(result.RootIssueID) + if root != "" { + candidates = append(candidates, + "multica:issue:"+root, + "multica:issue/"+root, + "mention://issue/"+root, + "multica:session:"+root, + "multica:session/"+root, + ) + } + if session := strings.TrimSpace(result.SessionID); session != "" { + candidates = append(candidates, session) + } + if correlation := strings.TrimSpace(result.CorrelationID); correlation != "" { + candidates = append(candidates, correlation) + } + if task := strings.TrimSpace(result.TaskID); task != "" { + candidates = append(candidates, + "multica:task:"+task, + "multica:task/"+task, + ) + } + } + for _, issueID := range extraIssueIDs { + issueID = strings.TrimSpace(issueID) + if issueID == "" { + continue + } + candidates = append(candidates, + "multica:issue:"+issueID, + "multica:issue/"+issueID, + "mention://issue/"+issueID, + ) + } + for _, candidate := range candidates { + if ref == candidate { + return true, true + } + } + return true, false +} + func runtimeMulticaHubWriteEnabled(env []string) bool { value := strings.TrimSpace(envValue(env, "MNEMON_MULTICA_HUB_WRITE")) if value == "" { @@ -735,28 +862,44 @@ func findExistingMulticaAssignmentIssueInChildren(ctx context.Context, cli drive return driver.MulticaIssue{}, false, nil } -func findAssignmentTargetFromLedger(ledger *driver.FileMulticaHubLedger, sessionID, assignmentID string) (string, bool, error) { +func findAssignmentTargetFromLedger(ledger *driver.FileMulticaHubLedger, sessionID, assignmentID, principal string) (string, bool, error) { records, err := ledger.Records() if err != nil { return "", false, err } + var fallback string + matches := 0 + principal = strings.TrimSpace(principal) for i := len(records) - 1; i >= 0; i-- { record := records[i] if record.Kind != driver.MulticaHubKindAssignmentMailbox { continue } - if record.Source.SessionID == sessionID && record.Source.AssignmentID == assignmentID && strings.TrimSpace(record.Target.ChildIssueID) != "" { + if record.Source.SessionID != sessionID || record.Source.AssignmentID != assignmentID || strings.TrimSpace(record.Target.ChildIssueID) == "" { + continue + } + matches++ + if principal != "" && record.Source.Principal == principal { return record.Target.ChildIssueID, true, nil } + if fallback == "" { + fallback = record.Target.ChildIssueID + } + } + if principal == "" || matches == 1 { + return fallback, fallback != "", nil } return "", false, nil } -func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaCLI, rootIssueID, sessionID, assignmentID string) (string, bool, error) { +func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaCLI, rootIssueID, sessionID, assignmentID, principal string) (string, bool, error) { children, err := cli.ListIssueChildren(ctx, rootIssueID) if err != nil { return "", false, err } + var fallback string + matches := 0 + principal = strings.TrimSpace(principal) for _, child := range children { meta := driver.MulticaIssueHubMetadata(child) if !meta.IsAssignmentMailbox() { @@ -769,9 +912,19 @@ func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaC if !meta.IsAssignmentMailbox() { continue } - if meta.SessionID == sessionID && meta.AssignmentID == assignmentID && strings.TrimSpace(child.ID) != "" { + if meta.SessionID != sessionID || meta.AssignmentID != assignmentID || strings.TrimSpace(child.ID) == "" { + continue + } + matches++ + if principal != "" && meta.Principal == principal { return child.ID, true, nil } + if fallback == "" { + fallback = child.ID + } + } + if principal == "" || matches == 1 { + return fallback, fallback != "", nil } return "", false, nil } diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 5a1b4852..ee02fe26 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -779,6 +779,23 @@ esac "context_refs": []any{"multica:issue:root-2"}, "evidence_refs": []any{"multica:issue:root-2"}, }, + }, map[string]any{ + "id": "asg-cross-session", + "ingest_seq": float64(34), + "actor": "planner@team", + "rule": map[string]any{ + "assignment_id": "asg-cross-session", + "assignee": "worker@team", + "scope": "other session child routing", + "ttl": "30m", + }, + "narrative": map[string]any{ + "expected_work": "cross-session assignment from a different Multica root", + "expected_feedback": "progress_digest with result or blocker", + }, + "refs": map[string]any{ + "context_refs": []any{"multica:issue:root-other"}, + }, }}}, }, { Ref: contract.ResourceRef{Kind: "progress_digest", ID: "project"}, @@ -791,6 +808,17 @@ esac "feedback_kind": "progress", "summary": "Checked release notes against the public changelog.", "artifact_refs": []any{"multica:issue:root-2"}, + }, map[string]any{ + "id": "pg-cross-session", + "event_id": "pg-cross-session", + "ingest_seq": float64(35), + "actor": "worker@team", + "assignment_ref": "asg-writer", + "feedback_kind": "progress", + "summary": "Cross-session progress must not attach to the current child.", + "refs": map[string]any{ + "evidence_refs": []any{"multica:issue:root-other"}, + }, }}}, }}, }) @@ -850,6 +878,9 @@ esac if strings.Contains(args, "asg-stale") { t.Fatalf("stale pre-root assignment must not be projected into current root session:\n%s", args) } + if strings.Contains(args, "other session child routing") || strings.Contains(args, "asg-cross-session") { + t.Fatalf("cross-session assignment must not be projected into current root session:\n%s", args) + } createIdx := strings.Index(args, "issue create --title TEA-10: check release notes") metaIdx := strings.Index(args, "issue metadata set child-2 --key mnemon.kind") assignIdx := strings.Index(args, "issue assign child-2 --to-id agent-worker") @@ -906,6 +937,9 @@ esac } } childComment := mustReadRuntimeTestFile(t, childCommentPath) + if strings.Contains(childComment, "Cross-session progress") { + t.Fatalf("cross-session progress must not be projected into current assignment child:\n%s", childComment) + } for _, want := range []string{ "Mnemon update: assignment feedback", "## Feedback", @@ -928,6 +962,176 @@ esac } } +func TestRuntimeRoutesDuplicateAssignmentIDProgressByPrincipal(t *testing.T) { + tmp := t.TempDir() + argsPath := filepath.Join(tmp, "multica.args") + rootCommentPath := filepath.Join(tmp, "root-comment.txt") + routingCommentPath := filepath.Join(tmp, "routing-comment.txt") + metadataCommentPath := filepath.Join(tmp, "metadata-comment.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue get root-dupe"*) printf '{"id":"root-dupe","identifier":"TEA-30","title":"Duplicate assignment id routing","description":"Validate duplicate assignment ids do not cross-route feedback.","status":"todo","priority":"medium"}\n' ;; + *"issue metadata set root-dupe"*) printf '{}\n' ;; + *"issue children root-dupe"*) printf '{"children":[]}\n' ;; + *"issue create --title TEA-30: child routing isolation"*) printf '{"id":"child-routing","identifier":"TEA-31","title":"TEA-30: child routing isolation","status":"todo","metadata":{}}\n' ;; + *"issue create --title TEA-30: root metadata run visibility"*) printf '{"id":"child-metadata","identifier":"TEA-32","title":"TEA-30: root metadata run visibility","status":"todo","metadata":{}}\n' ;; + *"issue metadata set child-routing"*) printf '{}\n' ;; + *"issue metadata set child-metadata"*) printf '{}\n' ;; + *"issue assign child-routing --to-id agent-impl"*) printf '{"id":"child-routing","status":"in_progress"}\n' ;; + *"issue assign child-metadata --to-id agent-researcher"*) printf '{"id":"child-metadata","status":"in_progress"}\n' ;; + *"issue comment add child-routing"*) cat > "$MULTICA_ROUTING_COMMENT_PATH"; printf '{"id":"comment-routing","issue_id":"child-routing","content":"ok","type":"comment"}\n' ;; + *"issue comment add child-metadata"*) cat > "$MULTICA_METADATA_COMMENT_PATH"; printf '{"id":"comment-metadata","issue_id":"child-metadata","content":"ok","type":"comment"}\n' ;; + *"issue comment add root-dupe"*) cat > "$MULTICA_ROOT_COMMENT_PATH"; printf '{"id":"comment-root","issue_id":"root-dupe","content":"ok","type":"comment"}\n' ;; + *"issue status child-routing"*) printf '{"id":"child-routing","status":"done"}\n' ;; + *"issue status child-metadata"*) printf '{"id":"child-metadata","status":"done"}\n' ;; + *"issue status root-dupe"*) printf '{"id":"root-dupe","status":"in_review"}\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + registryPath := filepath.Join(tmp, "registry.json") + if err := driver.SaveMulticaRegistry(registryPath, driver.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-1", + Participants: []driver.MulticaParticipantRecord{ + {Principal: "implementer@team", AgentName: "mnemon-implementer", AgentID: "agent-impl", Role: "implementer"}, + {Principal: "researcher@team", AgentName: "mnemon-researcher", AgentID: "agent-researcher", Role: "researcher"}, + }, + }); err != nil { + t.Fatal(err) + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/ingest": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(access.IngestReceipt{Seq: 31, Dup: false, Ticked: true}) + case "/presentation-view": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(pview.View{ + Ref: "view-duplicate-assignment-ids", + Digest: "digest-duplicate-assignment-ids", + Content: []pview.ResourceContent{{ + Ref: contract.ResourceRef{Kind: "assignment", ID: "project"}, + Fields: map[string]any{"items": []any{ + map[string]any{ + "id": "local/planner-team/3", + "ingest_seq": float64(32), + "actor": "planner@team", + "rule": map[string]any{ + "assignment_id": "assignment-tea30-root-runtime", + "assignee": "implementer@team", + "scope": "multica-hub-readiness-drill/TEA-30/stage1/child-routing-isolation", + "ttl": "30m", + }, + "narrative": map[string]any{ + "expected_work": "Validate child routing isolation.", + "expected_feedback": "progress_digest with result or blocker", + }, + "refs": map[string]any{"context_refs": []any{"multica:issue:root-dupe"}}, + }, + map[string]any{ + "id": "local/planner-team/5", + "ingest_seq": float64(33), + "actor": "planner@team", + "rule": map[string]any{ + "assignment_id": "assignment-tea30-root-runtime", + "assignee": "researcher@team", + "scope": "multica-hub-readiness-drill/TEA-30/stage1/root-metadata-run-visibility", + "ttl": "30m", + }, + "narrative": map[string]any{ + "expected_work": "Validate root metadata visibility.", + "expected_feedback": "progress_digest with result or blocker", + }, + "refs": map[string]any{"context_refs": []any{"multica:issue:root-dupe"}}, + }, + }}, + }, { + Ref: contract.ResourceRef{Kind: "progress_digest", ID: "project"}, + Fields: map[string]any{"items": []any{ + map[string]any{ + "id": "pg-routing", + "event_id": "pg-routing", + "ingest_seq": float64(34), + "actor": "implementer@team", + "assignment_ref": "assignment-tea30-root-runtime", + "feedback_kind": "result", + "summary": "Routing slice completed by implementer.", + "result": "Child routing isolation passed.", + }, + map[string]any{ + "id": "pg-metadata", + "event_id": "pg-metadata", + "ingest_seq": float64(35), + "actor": "researcher@team", + "assignment_ref": "assignment-tea30-root-runtime", + "feedback_kind": "result", + "summary": "Metadata slice completed by researcher.", + "result": "Root metadata visibility passed.", + }, + }}, + }}, + }) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer srv.Close() + + input := `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"input":[{"type":"text","text":"Your assigned issue ID is: root-dupe"}]}}` + "\n" + var out bytes.Buffer + err := runRuntime(runtimeConfig{ + Args: []string{"app-server", "--listen", "stdio://"}, + Env: runtimeTestEnv( + "MNEMON_MULTICA_BIN="+bin, + "MNEMON_HUB_BACKEND=multica", + "MNEMON_CONTROL_ADDR="+srv.URL, + "MNEMON_CONTROL_PRINCIPAL=planner@team", + "MNEMON_MULTICA_REGISTRY="+registryPath, + "MNEMON_MULTICA_HUB_LEDGER="+filepath.Join(tmp, "hub-ledger.jsonl"), + "MULTICA_ARGS_PATH="+argsPath, + "MULTICA_ROOT_COMMENT_PATH="+rootCommentPath, + "MULTICA_ROUTING_COMMENT_PATH="+routingCommentPath, + "MULTICA_METADATA_COMMENT_PATH="+metadataCommentPath, + "MULTICA_TASK_ID=task-root-dupe", + "MULTICA_AGENT_ID=agent-planner", + ), + CWD: tmp, + Stdin: strings.NewReader(input), + Stdout: &out, + Now: fixedRuntimeTime, + }) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "Multica hub write: updated child_issues=2 feedback_comments=2") { + t.Fatalf("runtime output missing duplicate-id hub write evidence:\n%s", out.String()) + } + args := mustReadRuntimeTestFile(t, argsPath) + for _, want := range []string{ + "issue create --title TEA-30: child routing isolation --output json --description-stdin --parent root-dupe --status in_progress --priority medium", + "issue create --title TEA-30: root metadata run visibility --output json --description-stdin --parent root-dupe --status in_progress --priority medium", + "issue comment add child-routing --content-stdin --output json", + "issue comment add child-metadata --content-stdin --output json", + } { + if !strings.Contains(args, want) { + t.Fatalf("args missing %q:\n%s", want, args) + } + } + routingComment := mustReadRuntimeTestFile(t, routingCommentPath) + if !strings.Contains(routingComment, "Routing slice completed by implementer.") || strings.Contains(routingComment, "Metadata slice completed by researcher.") { + t.Fatalf("routing child received wrong feedback:\n%s", routingComment) + } + metadataComment := mustReadRuntimeTestFile(t, metadataCommentPath) + if !strings.Contains(metadataComment, "Metadata slice completed by researcher.") || strings.Contains(metadataComment, "Routing slice completed by implementer.") { + t.Fatalf("metadata child received wrong feedback:\n%s", metadataComment) + } +} + func TestRuntimeContinuesProjectingAssignmentMailboxesAfterSupplementalMetadataFailure(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "multica.args") diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index 53b3d9d8..d0d13303 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -229,6 +229,9 @@ func IssueStatusDone(status string) bool { func assignmentTitleTopic(item AssignmentMailboxMaterial) string { scope := firstSentence(item.Scope) + if topic := scopePathTitleTopic(scope, item.RootIssueLabel); topic != "" { + return topic + } idTopic := assignmentIDTitleTopic(item.ID, item.RootIssueLabel) if idTopic != "" && (broadAssignmentScope(scope) || machineReferenceScope(scope)) { return idTopic @@ -242,9 +245,60 @@ func assignmentTitleTopic(item AssignmentMailboxMaterial) string { return "" } +func scopePathTitleTopic(scope, rootLabel string) string { + scope = strings.TrimSpace(scope) + if scope == "" || !strings.Contains(scope, "/") || strings.Contains(scope, "://") { + return "" + } + parts := strings.Split(scope, "/") + if len(parts) < 3 { + return "" + } + for i := len(parts) - 1; i >= 0; i-- { + part := strings.TrimSpace(parts[i]) + if part == "" { + continue + } + part = stripTitleRootLabel(part, rootLabel) + part = strings.Trim(strings.ReplaceAll(strings.ReplaceAll(part, "_", " "), "-", " "), " ") + if part == "" || stageLikeTitlePart(part) { + continue + } + return part + } + return "" +} + +func stageLikeTitlePart(value string) bool { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "" || value == "stage" || value == "phase" { + return true + } + if strings.HasPrefix(value, "stage") && len(value) > len("stage") { + for _, r := range value[len("stage"):] { + if r < '0' || r > '9' { + return false + } + } + return true + } + if strings.HasPrefix(value, "phase") && len(value) > len("phase") { + for _, r := range value[len("phase"):] { + if r < '0' || r > '9' { + return false + } + } + return true + } + return false +} + func broadAssignmentScope(scope string) bool { lower := strings.ToLower(strings.TrimSpace(scope)) - return strings.Contains(lower, "drill") || strings.Contains(lower, "validation") + return strings.Contains(lower, "drill") || + strings.Contains(lower, "validation") || + strings.Contains(lower, "readiness") || + strings.Contains(lower, "hub-flow") } func machineReferenceScope(scope string) bool { @@ -261,13 +315,19 @@ func assignmentIDTitleTopic(id, rootLabel string) string { return "" } root := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(rootLabel), "-", "")) + rootParts := titleRootParts(rootLabel) parts := strings.FieldsFunc(strings.ToLower(id), func(r rune) bool { return !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9') }) var out []string - for _, part := range parts { + for i := 0; i < len(parts); i++ { + part := parts[i] + if len(rootParts) > 0 && titlePartsMatch(parts[i:], rootParts) { + i += len(rootParts) - 1 + continue + } switch part { - case "", "assignment", "asg", root: + case "", "assignment", "asg", "r1", "r2", "drill", "readiness", root: continue } if root != "" && strings.TrimPrefix(part, root) == "" { @@ -282,6 +342,24 @@ func assignmentIDTitleTopic(id, rootLabel string) string { return joined } +func titleRootParts(rootLabel string) []string { + return strings.FieldsFunc(strings.ToLower(strings.TrimSpace(rootLabel)), func(r rune) bool { + return !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9') + }) +} + +func titlePartsMatch(parts, want []string) bool { + if len(want) == 0 || len(parts) < len(want) { + return false + } + for i, part := range want { + if parts[i] != part { + return false + } + } + return true +} + func stripTitleRootLabel(value, label string) string { value = strings.TrimSpace(value) label = strings.TrimSpace(label) @@ -330,13 +408,17 @@ func visibleExpectedFeedback(value string) string { if value == "" { return "" } + value = strings.ReplaceAll(value, "progress_digest", "runtime feedback") + value = strings.ReplaceAll(value, "progress digest", "runtime feedback") + value = strings.ReplaceAll(value, "feedback_kind=", "status=") + value = strings.ReplaceAll(value, "feedback_kind", "status") lower := strings.ToLower(value) - for _, prefix := range []string{"progress_digest with ", "progress digest with "} { + for _, prefix := range []string{"runtime feedback with "} { if strings.HasPrefix(lower, prefix) { return strings.TrimSpace(value[len(prefix):]) } } - if lower == "progress_digest" || lower == "progress digest" { + if lower == "runtime feedback" { return "progress, result, or blocker" } return value diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 420ad996..1e75888c 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -65,6 +65,42 @@ func TestAssignmentMailboxTitleUsesAssignmentIDForBroadDrillScope(t *testing.T) } } +func TestAssignmentMailboxTitleUsesPathScopeTopicBeforeBroadIDFallback(t *testing.T) { + item := AssignmentMailboxMaterial{ + ID: "assignment-tea88-root-runtime", + Scope: "multica-hub-readiness-drill/TEA-88/stage1/root-metadata-run-visibility", + RootIssueLabel: "TEA-88", + ExpectedWork: "Validate root session metadata and run visibility.", + } + if got := AssignmentMailboxTitle(item); got != "TEA-88: root metadata run visibility" { + t.Fatalf("title = %q", got) + } +} + +func TestAssignmentMailboxTitleUsesAssignmentIDForReadinessScope(t *testing.T) { + item := AssignmentMailboxMaterial{ + ID: "r2-drill-child-routing-isolation", + Scope: "mnemon-r2-multica-hub-flow-readiness", + RootIssueLabel: "TEA-95", + ExpectedWork: "Validate child routing isolation.", + } + if got := AssignmentMailboxTitle(item); got != "TEA-95: child routing isolation" { + t.Fatalf("title = %q", got) + } +} + +func TestAssignmentMailboxTitleDropsRootIdentifierTokensFromAssignmentID(t *testing.T) { + item := AssignmentMailboxMaterial{ + ID: "tea-104-root-visibility", + Scope: "mnemon-r2-multica-hub-flow-readiness", + RootIssueLabel: "TEA-104", + ExpectedWork: "Validate root session visibility.", + } + if got := AssignmentMailboxTitle(item); got != "TEA-104: root visibility" { + t.Fatalf("title = %q", got) + } +} + func TestAssignmentMailboxTitleUsesAssignmentIDForMachineReferenceScope(t *testing.T) { item := AssignmentMailboxMaterial{ ID: "assignment-tea84-root-runtime", @@ -91,6 +127,22 @@ func TestAssignmentMailboxDescriptionNormalizesProtocolFeedback(t *testing.T) { } } +func TestAssignmentMailboxDescriptionNormalizesFeedbackKindProtocolWording(t *testing.T) { + body := AssignmentMailboxDescription(AssignmentMailboxMaterial{ + ID: "asg-1", + Scope: "release validation", + ExpectedFeedback: "progress_digest feedback_kind=result or blocker with exact Multica evidence", + }) + for _, blocked := range []string{"progress_digest", "feedback_kind"} { + if strings.Contains(body, blocked) { + t.Fatalf("description should keep protocol word %q out of visible text:\n%s", blocked, body) + } + } + if !strings.Contains(body, "runtime feedback status=result or blocker") { + t.Fatalf("description missing readable feedback wording:\n%s", body) + } +} + func TestProgressFeedbackMaterial(t *testing.T) { item := ProgressFeedbackMaterial{ AssignmentRef: "asg-1", From 633feaf74d2f885129ab348a20d6746b5d5328a0 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 00:19:34 +0800 Subject: [PATCH 065/117] fix: stabilize github publication listing Read GitHub publication event directories and files at the branch head SHA used as the pull cursor, so a cached branch-name contents response cannot advance a cursor past unseen events. Isolate live GitHub publish/pull smoke tests with run-scoped branches and ensure branches before writing. Validation: go test -count=1 ./harness/...; MNEMON_GITHUB_LIVE=1 live publication store smoke passed; MNEMON_GITHUB_LIVE=1 live app publish/pull/import smoke passed against mnemon-dev/mnemon-teamwork-example. --- harness/internal/app/sync_github_live_test.go | 8 +++++-- .../backend/github/publication_store.go | 4 ++-- .../backend/github/publication_store_test.go | 21 +++++++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/harness/internal/app/sync_github_live_test.go b/harness/internal/app/sync_github_live_test.go index 2eacf5a3..13bdb5c7 100644 --- a/harness/internal/app/sync_github_live_test.go +++ b/harness/internal/app/sync_github_live_test.go @@ -1,6 +1,7 @@ package app import ( + "context" "net/http" "os" "path/filepath" @@ -26,6 +27,9 @@ func TestGitHubLivePublishPullImport(t *testing.T) { if err != nil { t.Fatalf("github publication store: %v", err) } + if err := store.EnsureBranches(context.Background(), []string{cfg.branchA, cfg.branchB}, "main"); err != nil { + t.Fatalf("ensure live GitHub branches: %v", err) + } remoteA := liveGitHubRemote(t, store, cfg.repo, cfg.branchA) remoteB := liveGitHubRemote(t, store, cfg.repo, cfg.branchB) @@ -103,11 +107,11 @@ func liveGitHubTestConfig(t *testing.T) liveGitHubConfig { } branchA := strings.TrimSpace(os.Getenv("MNEMON_GITHUB_BRANCH_A")) if branchA == "" { - branchA = "mnemon/agent-a" + branchA = "mnemon/live/" + time.Now().UTC().Format("20060102T150405.000000000Z") + "/a" } branchB := strings.TrimSpace(os.Getenv("MNEMON_GITHUB_BRANCH_B")) if branchB == "" { - branchB = "mnemon/agent-b" + branchB = strings.TrimSuffix(branchA, "/a") + "/b" } return liveGitHubConfig{repo: repo, branchA: branchA, branchB: branchB, token: token} } diff --git a/harness/internal/mnemonhub/exchange/backend/github/publication_store.go b/harness/internal/mnemonhub/exchange/backend/github/publication_store.go index 5faf23e0..ab349a98 100644 --- a/harness/internal/mnemonhub/exchange/backend/github/publication_store.go +++ b/harness/internal/mnemonhub/exchange/backend/github/publication_store.go @@ -116,13 +116,13 @@ func (s *GitHubPublicationStore) ListEvents(ctx context.Context, branch string, if strings.TrimSpace(cursor) == head { return exchange.PublicationListResult{NextCursor: head}, nil } - paths, err := s.listEventPaths(ctx, branch, prefix) + paths, err := s.listEventPaths(ctx, head, prefix) if err != nil { return exchange.PublicationListResult{}, err } events := make([]exchange.PublicationStoredEvent, 0, len(paths)) for _, path := range paths { - file, found, err := s.readFileWithSHA(ctx, branch, path) + file, found, err := s.readFileWithSHA(ctx, head, path) if err != nil { return exchange.PublicationListResult{}, err } diff --git a/harness/internal/mnemonhub/exchange/backend/github/publication_store_test.go b/harness/internal/mnemonhub/exchange/backend/github/publication_store_test.go index 98b86b90..4c1c2629 100644 --- a/harness/internal/mnemonhub/exchange/backend/github/publication_store_test.go +++ b/harness/internal/mnemonhub/exchange/backend/github/publication_store_test.go @@ -79,8 +79,8 @@ func TestGitHubPublicationStoreWriteFileUpdatesWithSHA(t *testing.T) { func TestGitHubPublicationStoreListEventsUsesBranchHeadCursor(t *testing.T) { fake := newFakeGitHubPublicationAPI(t) fake.head = "head-2" - fake.files["mnemon/mnemond-b:"+exchange.PublicationEventRoot+"/replica-b/progress_digest/project/000000000001-dec-b.json"] = fakeGitHubFile{body: []byte(`{"id":"b"}`), sha: "sha-b"} - fake.files["mnemon/mnemond-b:"+exchange.PublicationEventRoot+"/replica-c/progress_digest/project/000000000001-dec-c.json"] = fakeGitHubFile{body: []byte(`{"id":"c"}`), sha: "sha-c"} + fake.files["head-2:"+exchange.PublicationEventRoot+"/replica-b/progress_digest/project/000000000001-dec-b.json"] = fakeGitHubFile{body: []byte(`{"id":"b"}`), sha: "sha-b"} + fake.files["head-2:"+exchange.PublicationEventRoot+"/replica-c/progress_digest/project/000000000001-dec-c.json"] = fakeGitHubFile{body: []byte(`{"id":"c"}`), sha: "sha-c"} store, err := NewPublicationStore(PublicationStoreConfig{ Repo: "mnemon-dev/mnemon-teamwork-example", BaseURL: fake.server.URL, @@ -97,6 +97,9 @@ func TestGitHubPublicationStoreListEventsUsesBranchHeadCursor(t *testing.T) { if len(list.Events) != 2 || list.NextCursor != "head-2" { t.Fatalf("list = %+v, want two events at head-2", list) } + if !stringSliceContains(fake.contentRefs, "head-2") { + t.Fatalf("list events must read contents at branch head sha, refs=%v", fake.contentRefs) + } again, err := store.ListEvents(context.Background(), "mnemon/mnemond-b", exchange.PublicationEventRoot, list.NextCursor) if err != nil { t.Fatalf("list after head cursor: %v", err) @@ -220,6 +223,9 @@ func TestGitHubPublicationStoreLiveGated(t *testing.T) { if err != nil { t.Fatal(err) } + if err := store.EnsureBranch(context.Background(), branch, "main"); err != nil { + t.Fatalf("ensure live branch: %v", err) + } path := exchange.PublicationEventRoot + "/live-smoke/progress_digest/project/000000000001-live-smoke.json" body := []byte(`{"schema_version":1,"source":"mnemon live smoke"}`) res, err := store.PutEvent(context.Background(), branch, path, body) @@ -244,6 +250,7 @@ type fakeGitHubPublicationAPI struct { refs map[string]string missingRefs map[string]bool refReads map[string]int + contentRefs []string head string puts int creates int @@ -314,12 +321,22 @@ func (f *fakeGitHubPublicationAPI) handle(w http.ResponseWriter, r *http.Request case strings.HasPrefix(tail, "contents/"): path := strings.TrimPrefix(tail, "contents/") branch := r.URL.Query().Get("ref") + f.contentRefs = append(f.contentRefs, branch) f.handleContents(w, r, branch, path) default: http.NotFound(w, r) } } +func stringSliceContains(values []string, want string) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} + func (f *fakeGitHubPublicationAPI) handleContents(w http.ResponseWriter, r *http.Request, branch, path string) { switch r.Method { case http.MethodGet: From 2b01507fe17de097674cc212a18a1877de73bc26 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 00:50:48 +0800 Subject: [PATCH 066/117] fix: reduce github publication polling Use the GitHub compare API when a publication cursor is available so mesh pulls only inspect changed publication files instead of recursively scanning the full event tree every pass. Keep the full tree scan as the fallback for initial or invalid cursors. Validation: go test -count=1 ./harness/... --- .../backend/github/publication_store.go | 56 +++++++++++++++- .../backend/github/publication_store_test.go | 66 +++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/harness/internal/mnemonhub/exchange/backend/github/publication_store.go b/harness/internal/mnemonhub/exchange/backend/github/publication_store.go index ab349a98..58b97c28 100644 --- a/harness/internal/mnemonhub/exchange/backend/github/publication_store.go +++ b/harness/internal/mnemonhub/exchange/backend/github/publication_store.go @@ -116,7 +116,7 @@ func (s *GitHubPublicationStore) ListEvents(ctx context.Context, branch string, if strings.TrimSpace(cursor) == head { return exchange.PublicationListResult{NextCursor: head}, nil } - paths, err := s.listEventPaths(ctx, head, prefix) + paths, err := s.listChangedEventPaths(ctx, head, prefix, cursor) if err != nil { return exchange.PublicationListResult{}, err } @@ -268,6 +268,13 @@ type githubContentEntry struct { Path string `json:"path"` } +type githubCompareResponse struct { + Files []struct { + Filename string `json:"filename"` + Status string `json:"status"` + } `json:"files"` +} + type githubPutFileRequest struct { Message string `json:"message"` Content string `json:"content"` @@ -365,6 +372,53 @@ func (s *GitHubPublicationStore) branchHead(ctx context.Context, branch string) return strings.TrimSpace(out.Object.SHA), nil } +func (s *GitHubPublicationStore) listChangedEventPaths(ctx context.Context, head, prefix, cursor string) ([]string, error) { + cursor = strings.TrimSpace(cursor) + if cursor != "" { + paths, ok, err := s.compareEventPaths(ctx, cursor, head, prefix) + if err != nil { + return nil, err + } + if ok { + return paths, nil + } + } + return s.listEventPaths(ctx, head, prefix) +} + +func (s *GitHubPublicationStore) compareEventPaths(ctx context.Context, base, head, prefix string) ([]string, bool, error) { + if strings.TrimSpace(base) == "" || strings.TrimSpace(head) == "" || base == head { + return nil, true, nil + } + var out githubCompareResponse + status, err := s.do(ctx, http.MethodGet, "/repos/"+s.owner+"/"+s.repo+"/compare/"+escapeGitHubPath(base)+"..."+escapeGitHubPath(head), nil, nil, &out) + if err != nil { + if apiErr, ok := err.(*githubAPIError); ok && (apiErr.Status == http.StatusNotFound || apiErr.Status == http.StatusUnprocessableEntity) { + return nil, false, nil + } + return nil, false, err + } + if status != http.StatusOK { + return nil, false, fmt.Errorf("github compare returned status %d", status) + } + seen := map[string]bool{} + var paths []string + for _, file := range out.Files { + path := strings.TrimSpace(file.Filename) + if path == "" || !strings.HasPrefix(path, prefix) || seen[path] { + continue + } + switch strings.TrimSpace(file.Status) { + case "removed", "deleted": + continue + } + seen[path] = true + paths = append(paths, path) + } + sort.Strings(paths) + return paths, true, nil +} + func (s *GitHubPublicationStore) listEventPaths(ctx context.Context, branch, prefix string) ([]string, error) { entries, err := s.listDir(ctx, branch, prefix) if err != nil { diff --git a/harness/internal/mnemonhub/exchange/backend/github/publication_store_test.go b/harness/internal/mnemonhub/exchange/backend/github/publication_store_test.go index 4c1c2629..675f8622 100644 --- a/harness/internal/mnemonhub/exchange/backend/github/publication_store_test.go +++ b/harness/internal/mnemonhub/exchange/backend/github/publication_store_test.go @@ -1,6 +1,7 @@ package githubbackend import ( + "bytes" "context" "encoding/base64" "encoding/json" @@ -109,6 +110,39 @@ func TestGitHubPublicationStoreListEventsUsesBranchHeadCursor(t *testing.T) { } } +func TestGitHubPublicationStoreListEventsUsesCompareAfterCursor(t *testing.T) { + fake := newFakeGitHubPublicationAPI(t) + fake.head = "head-2" + oldPath := exchange.PublicationEventRoot + "/replica-b/progress_digest/project/000000000001-dec-b.json" + newPath := exchange.PublicationEventRoot + "/replica-b/progress_digest/project/000000000002-dec-b.json" + fake.files["head-1:"+oldPath] = fakeGitHubFile{body: []byte(`{"id":"old"}`), sha: "sha-old"} + fake.files["head-2:"+oldPath] = fakeGitHubFile{body: []byte(`{"id":"old"}`), sha: "sha-old"} + fake.files["head-2:"+newPath] = fakeGitHubFile{body: []byte(`{"id":"new"}`), sha: "sha-new"} + fake.files["head-2:README.md"] = fakeGitHubFile{body: []byte(`# ignored`), sha: "sha-readme"} + store, err := NewPublicationStore(PublicationStoreConfig{ + Repo: "mnemon-dev/mnemon-teamwork-example", + BaseURL: fake.server.URL, + HTTPClient: fake.server.Client(), + }) + if err != nil { + t.Fatal(err) + } + + list, err := store.ListEvents(context.Background(), "mnemon/mnemond-b", exchange.PublicationEventRoot, "head-1") + if err != nil { + t.Fatalf("list events after cursor: %v", err) + } + if len(list.Events) != 1 || list.Events[0].Path != newPath || string(list.Events[0].Body) != `{"id":"new"}` || list.NextCursor != "head-2" { + t.Fatalf("list after cursor = %+v, want only changed publication event", list) + } + if len(fake.compareRefs) != 1 || fake.compareRefs[0] != "head-1...head-2" { + t.Fatalf("compare refs = %v, want head-1...head-2", fake.compareRefs) + } + if fake.dirLists != 0 { + t.Fatalf("incremental list should not scan directories, dirLists=%d", fake.dirLists) + } +} + func TestGitHubPublicationStoreEnsureBranchCreatesMissingBranchFromMain(t *testing.T) { fake := newFakeGitHubPublicationAPI(t) fake.refs["main"] = "main-sha" @@ -251,7 +285,9 @@ type fakeGitHubPublicationAPI struct { missingRefs map[string]bool refReads map[string]int contentRefs []string + compareRefs []string head string + dirLists int puts int creates int lastSHA string @@ -318,6 +354,15 @@ func (f *fakeGitHubPublicationAPI) handle(w http.ResponseWriter, r *http.Request f.refs[branch] = req.SHA f.creates++ writeJSON(w, http.StatusCreated, map[string]any{"ref": req.Ref, "object": map[string]any{"sha": req.SHA}}) + case r.Method == http.MethodGet && strings.HasPrefix(tail, "compare/"): + ref := strings.TrimPrefix(tail, "compare/") + f.compareRefs = append(f.compareRefs, ref) + base, head, ok := strings.Cut(ref, "...") + if !ok || base == "" || head == "" { + writeJSON(w, http.StatusUnprocessableEntity, map[string]any{"message": "invalid compare"}) + return + } + writeJSON(w, http.StatusOK, map[string]any{"files": f.compareFiles(base, head)}) case strings.HasPrefix(tail, "contents/"): path := strings.TrimPrefix(tail, "contents/") branch := r.URL.Query().Get("ref") @@ -353,6 +398,7 @@ func (f *fakeGitHubPublicationAPI) handleContents(w http.ResponseWriter, r *http } entries := f.dirEntries(branch, path) if len(entries) > 0 { + f.dirLists++ writeJSON(w, http.StatusOK, entries) return } @@ -393,6 +439,26 @@ func (f *fakeGitHubPublicationAPI) handleContents(w http.ResponseWriter, r *http } } +func (f *fakeGitHubPublicationAPI) compareFiles(base, head string) []map[string]any { + prefix := head + ":" + var out []map[string]any + for key, file := range f.files { + if !strings.HasPrefix(key, prefix) { + continue + } + path := strings.TrimPrefix(key, prefix) + status := "added" + if previous, ok := f.files[base+":"+path]; ok { + if bytes.Equal(previous.body, file.body) { + continue + } + status = "modified" + } + out = append(out, map[string]any{"filename": path, "status": status}) + } + return out +} + func (f *fakeGitHubPublicationAPI) dirEntries(branch, dir string) []map[string]any { prefix := branch + ":" + strings.TrimSuffix(dir, "/") + "/" seen := map[string]map[string]any{} From b91eb1b954c636b87eccea1f330f78cc4f8ed12b Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 01:35:10 +0800 Subject: [PATCH 067/117] fix: structure multica hub surface metadata Add structured root session mailbox text, sanitize protocol field names from Multica-visible runtime and assignment text, and wait for complete child mailbox metadata during real hub-flow acceptance. Also prune stale Mnemon-managed runtime env keys during Multica participant provisioning so obsolete control token files cannot override the current Local Mnemon binding strategy. Validation: go test -count=1 ./harness/... and real Multica hub-flow acceptance TEA-114 passed. --- .../acceptance_multica_runtime.go | 49 +++++--- .../acceptance_multica_runtime_test.go | 12 +- harness/cmd/mnemon-harness/multica.go | 36 +++++- harness/cmd/mnemon-harness/multica_test.go | 28 +++++ harness/cmd/mnemon-multica-runtime/main.go | 107 ++++++++---------- .../cmd/mnemon-multica-runtime/main_test.go | 22 ++-- .../internal/surface/multica/projection.go | 58 ++++++++-- .../surface/multica/projection_test.go | 61 ++++++++++ 8 files changed, 265 insertions(+), 108 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index b134a7a2..9b0b2488 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -242,13 +242,22 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime } description := strings.TrimSpace(opts.IssueDescription) if description == "" { - description = strings.TrimSpace(`Run a small Mnemon R2 Multica hub-flow readiness drill. - -Coordinate this as teamwork rather than solo work. Use Mnemon teamwork handoffs when delegating validation slices; the Multica runtime should project accepted assignments into child assignment mailboxes and route them to the target agents. - -Stage the handoffs: first assign root metadata/run visibility and child routing/isolation to separate teammates. After both teammate result digests are visible and their Multica feedback comments/statuses have projected, assign an integrator to verify feedback projection and final status completion. - -The validation should cover root session metadata, assignment child issue routing, assignment feedback comments, agent run activity visibility, stale or cross-session assignment isolation, and final Multica status completion.`) + description = multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Request: "Run a small Mnemon R2 Multica readiness drill.", + WorkMode: "Use Mnemon teamwork; Multica shows issues, runs, comments, and statuses.", + Handoffs: []string{ + "Route root session visibility and child issue routing checks to separate teammates.", + "After teammate feedback is visible, route a final integration check.", + }, + Validation: []string{ + "Root issue carries session metadata and shows run activity.", + "Accepted assignments become child issue mailboxes assigned to target agents.", + "Feedback comments and statuses are projected back to Multica.", + "Stale or cross-session assignment material is ignored.", + "Final root status reflects completion.", + }, + Completion: "Finish when child feedback comments are visible and the root issue reaches a terminal status.", + }) } issue, err := cli.CreateIssue(ctx, driver.MulticaCreateIssueRequest{ Title: title, @@ -352,16 +361,7 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o return rawErr } addMulticaProdSimAssertion(report, "root children are session-scoped assignment mailboxes", multicaRawChildrenMatchSessionAssignments(rawChildren, rawChildMeta, rootMeta[driver.MulticaMetadataSessionID]), fmt.Sprintf("raw_children=%d assignment_children=%d", len(rawChildren), len(children))) - allChildrenTagged := len(children) > 0 - for _, child := range children { - meta := childMeta[child.ID] - if meta[driver.MulticaMetadataKind] != driver.MulticaHubKindAssignmentMailbox || - strings.TrimSpace(meta[driver.MulticaMetadataAssignmentID]) == "" || - strings.TrimSpace(meta[driver.MulticaMetadataPrincipal]) == "" { - allChildrenTagged = false - break - } - } + allChildrenTagged := multicaAssignmentChildrenHaveCompleteMetadata(children, childMeta, rootMeta[driver.MulticaMetadataSessionID]) addMulticaProdSimAssertion(report, "child issues carry assignment metadata", allChildrenTagged, fmt.Sprintf("%+v", childMeta)) childRuns, childMessages, activeAgents, err := waitMulticaChildRunEvidence(ctx, cli, report.Issue.ID, report.Runs, children, opts.Wait, opts.Poll, opts.MinActiveAgents) @@ -445,7 +445,7 @@ func waitMulticaAssignmentChildren(ctx context.Context, cli driver.MulticaCLI, r if err != nil { return children, meta, err } - if len(children) >= minChildren { + if len(children) >= minChildren && multicaAssignmentChildrenHaveCompleteMetadata(children, meta, "") { return children, meta, nil } if wait <= 0 || time.Now().After(deadline) { @@ -625,6 +625,13 @@ func multicaMessagesExposeRuntimeActivity(types map[string]int) bool { } func multicaRawChildrenMatchSessionAssignments(children []driver.MulticaIssue, meta map[string]map[string]string, sessionID string) bool { + if len(children) == 0 { + return false + } + return multicaAssignmentChildrenHaveCompleteMetadata(children, meta, sessionID) +} + +func multicaAssignmentChildrenHaveCompleteMetadata(children []driver.MulticaIssue, meta map[string]map[string]string, sessionID string) bool { if len(children) == 0 { return false } @@ -633,6 +640,12 @@ func multicaRawChildrenMatchSessionAssignments(children []driver.MulticaIssue, m if childMeta[driver.MulticaMetadataKind] != driver.MulticaHubKindAssignmentMailbox { return false } + if strings.TrimSpace(childMeta[driver.MulticaMetadataAssignmentID]) == "" || + strings.TrimSpace(childMeta[driver.MulticaMetadataPrincipal]) == "" || + strings.TrimSpace(childMeta[driver.MulticaMetadataRootIssueID]) == "" || + strings.TrimSpace(childMeta[driver.MulticaMetadataSessionID]) == "" { + return false + } if strings.TrimSpace(sessionID) != "" && childMeta[driver.MulticaMetadataSessionID] != sessionID { return false } diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 47539e05..17ef49bd 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -38,7 +38,7 @@ cat >> "$MULTICA_STDIN_PATH" case "$*" in *"issue create"*) printf '{"id":"iss-9","identifier":"TEA-9","title":"Runtime prod sim","description":"Teamwork acceptance","status":"todo"}\n' ;; *"issue runs iss-9"*) printf '[{"id":"task-9","issue_id":"iss-9","agent_id":"agent-1","status":"completed","completed_at":"2026-06-28T09:00:00Z","workspace_id":"ws-1"}]\n' ;; - *"issue run-messages task-9"*) printf '[{"task_id":"task-9","issue_id":"iss-9","seq":1,"type":"assistant","content":"Mnemon Multica runtime handled issue TEA-9. Mnemon ingest: recorded seq=17. Managed wake: completed turn=noop-turn.","created_at":"2026-06-28T09:00:01Z"}]\n' ;; + *"issue run-messages task-9"*) printf '[{"task_id":"task-9","issue_id":"iss-9","seq":1,"type":"assistant","content":"Mnemon Multica runtime handled issue TEA-9. Mnemon ingest: recorded. Managed wake: completed.","created_at":"2026-06-28T09:00:01Z"}]\n' ;; *) printf '{}\n' ;; esac ` @@ -127,16 +127,16 @@ case "$*" in *"issue get child-2"*) printf '%s\n' '{"id":"child-2","identifier":"TEA-10","title":"TEA-9: routing check","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-9](mention://issue/root-9) - Runtime hub flow\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing check\n\n## Feedback\n\n- Expected feedback: result or blocker\n- Progress path: Mnemon runtime progress, result, or blocker feedback","status":"done"}' ;; *"issue get child-3"*) printf '%s\n' '{"id":"child-3","identifier":"TEA-11","title":"TEA-9: runtime display","description":"## Assignment\n\nCheck runtime display.\n\n## Context\n\n- Root issue: [TEA-9](mention://issue/root-9) - Runtime hub flow\n- Assignee: implementer@team (mnemon-implementer)\n- Scope: runtime display\n\n## Feedback\n\n- Expected feedback: result or blocker\n- Progress path: Mnemon runtime progress, result, or blocker feedback","status":"done"}' ;; *"issue metadata list root-9"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"session_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-9"}]\n' ;; - *"issue children root-9"*) printf '{"children":[{"id":"child-2","identifier":"TEA-10","title":"Assignment 2","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-2","mnemon.principal":"researcher@team"}},{"id":"child-3","identifier":"TEA-11","title":"Assignment 3","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-3","mnemon.principal":"implementer@team"}}]}\n' ;; + *"issue children root-9"*) printf '{"children":[{"id":"child-2","identifier":"TEA-10","title":"Assignment 2","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-9","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-2","mnemon.principal":"researcher@team"}},{"id":"child-3","identifier":"TEA-11","title":"Assignment 3","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-9","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-3","mnemon.principal":"implementer@team"}}]}\n' ;; *"issue comment list root-9"*) printf '[{"id":"comment-root","issue_id":"root-9","content":"Mnemon update: issue admitted\\n\\nmnemon:event=multica-task-root"}]\n' ;; *"issue comment list child-2"*) printf '[{"id":"comment-child-2","issue_id":"child-2","content":"Mnemon update: assignment feedback\\n\\nSummary: checked routing.\\n\\nmnemon:event=pg-2"}]\n' ;; *"issue comment list child-3"*) printf '[{"id":"comment-child-3","issue_id":"child-3","content":"Mnemon update: assignment feedback\\n\\nSummary: checked runtime display.\\n\\nmnemon:event=pg-3"}]\n' ;; *"issue runs root-9"*) printf '[{"id":"task-root","issue_id":"root-9","agent_id":"agent-planner","status":"completed","completed_at":"2026-06-28T09:00:00Z","workspace_id":"ws-1"}]\n' ;; - *"issue run-messages task-root"*) printf '[{"task_id":"task-root","issue_id":"root-9","seq":1,"type":"text","content":"Mnemon Multica runtime handled issue TEA-9. Mnemon ingest: recorded seq=17. Managed wake: completed turn=noop-turn. Multica hub write: created child_issues=2.","created_at":"2026-06-28T09:00:01Z"},{"task_id":"task-root","issue_id":"root-9","seq":2,"type":"tool_use","content":"mnemond ingest observe --principal planner@team","created_at":"2026-06-28T09:00:02Z"},{"task_id":"task-root","issue_id":"root-9","seq":3,"type":"tool_result","content":"recorded seq=17 duplicate=false ticked=true","created_at":"2026-06-28T09:00:03Z"}]\n' ;; + *"issue run-messages task-root"*) printf '[{"task_id":"task-root","issue_id":"root-9","seq":1,"type":"text","content":"Mnemon Multica runtime handled issue TEA-9. Mnemon ingest: recorded. Managed wake: completed. Multica updates: 2 assignment mailboxes created.","created_at":"2026-06-28T09:00:01Z"},{"task_id":"task-root","issue_id":"root-9","seq":2,"type":"tool_use","content":"mnemond ingest observe --principal planner@team","created_at":"2026-06-28T09:00:02Z"},{"task_id":"task-root","issue_id":"root-9","seq":3,"type":"tool_result","content":"recorded","created_at":"2026-06-28T09:00:03Z"}]\n' ;; *"issue runs child-2"*) printf '[{"id":"task-child-2","issue_id":"child-2","agent_id":"agent-researcher","status":"completed","completed_at":"2026-06-28T09:01:00Z","workspace_id":"ws-1"}]\n' ;; - *"issue run-messages task-child-2"*) printf '[{"task_id":"task-child-2","issue_id":"child-2","seq":1,"type":"text","content":"Mnemon Multica runtime handled issue TEA-10. Mnemon assignment mailbox: correlated assignment=asg-2. Managed wake: completed turn=noop-turn.","created_at":"2026-06-28T09:01:01Z"},{"task_id":"task-child-2","issue_id":"child-2","seq":2,"type":"tool_use","content":"mnemond managed wake --principal researcher@team [mnemon:wake]","created_at":"2026-06-28T09:01:02Z"},{"task_id":"task-child-2","issue_id":"child-2","seq":3,"type":"tool_result","content":"Managed wake: completed turn=noop-turn","created_at":"2026-06-28T09:01:03Z"}]\n' ;; + *"issue run-messages task-child-2"*) printf '[{"task_id":"task-child-2","issue_id":"child-2","seq":1,"type":"text","content":"Mnemon Multica runtime handled issue TEA-10. Mnemon assignment mailbox: correlated. Managed wake: completed.","created_at":"2026-06-28T09:01:01Z"},{"task_id":"task-child-2","issue_id":"child-2","seq":2,"type":"tool_use","content":"mnemond managed wake --principal researcher@team [mnemon:wake]","created_at":"2026-06-28T09:01:02Z"},{"task_id":"task-child-2","issue_id":"child-2","seq":3,"type":"tool_result","content":"Managed wake completed.","created_at":"2026-06-28T09:01:03Z"}]\n' ;; *"issue runs child-3"*) printf '[{"id":"task-child-3","issue_id":"child-3","agent_id":"agent-implementer","status":"completed","completed_at":"2026-06-28T09:02:00Z","workspace_id":"ws-1"}]\n' ;; - *"issue run-messages task-child-3"*) printf '[{"task_id":"task-child-3","issue_id":"child-3","seq":1,"type":"text","content":"Mnemon Multica runtime handled issue TEA-11. Mnemon assignment mailbox: correlated assignment=asg-3. Managed wake: completed turn=noop-turn.","created_at":"2026-06-28T09:02:01Z"},{"task_id":"task-child-3","issue_id":"child-3","seq":2,"type":"tool_use","content":"mnemond managed wake --principal implementer@team [mnemon:wake]","created_at":"2026-06-28T09:02:02Z"},{"task_id":"task-child-3","issue_id":"child-3","seq":3,"type":"tool_result","content":"Managed wake: completed turn=noop-turn","created_at":"2026-06-28T09:02:03Z"}]\n' ;; + *"issue run-messages task-child-3"*) printf '[{"task_id":"task-child-3","issue_id":"child-3","seq":1,"type":"text","content":"Mnemon Multica runtime handled issue TEA-11. Mnemon assignment mailbox: correlated. Managed wake: completed.","created_at":"2026-06-28T09:02:01Z"},{"task_id":"task-child-3","issue_id":"child-3","seq":2,"type":"tool_use","content":"mnemond managed wake --principal implementer@team [mnemon:wake]","created_at":"2026-06-28T09:02:02Z"},{"task_id":"task-child-3","issue_id":"child-3","seq":3,"type":"tool_result","content":"Managed wake completed.","created_at":"2026-06-28T09:02:03Z"}]\n' ;; *) printf '{}\n' ;; esac ` @@ -233,7 +233,7 @@ case "$*" in *"issue get child-a"*) printf '%s\n' '{"id":"child-a","identifier":"TEA-21","title":"TEA-20: routing","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-20](mention://issue/root-deferred) - Deferred run messages\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing\n\n## Feedback\n\n- Expected feedback: result","status":"done"}' ;; *"issue get child-b"*) printf '%s\n' '{"id":"child-b","identifier":"TEA-22","title":"TEA-20: status","description":"## Assignment\n\nCheck status.\n\n## Context\n\n- Root issue: [TEA-20](mention://issue/root-deferred) - Deferred run messages\n- Assignee: implementer@team (mnemon-implementer)\n- Scope: status\n\n## Feedback\n\n- Expected feedback: result","status":"done"}' ;; *"issue metadata list root-deferred"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"session_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-deferred"}]\n' ;; - *"issue children root-deferred"*) printf '{"children":[{"id":"child-a","identifier":"TEA-21","title":"TEA-20: routing","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-deferred","mnemon.assignment_id":"asg-a","mnemon.principal":"researcher@team"}},{"id":"child-b","identifier":"TEA-22","title":"TEA-20: status","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.session_id":"multica:session:root-deferred","mnemon.assignment_id":"asg-b","mnemon.principal":"implementer@team"}}]}\n' ;; + *"issue children root-deferred"*) printf '{"children":[{"id":"child-a","identifier":"TEA-21","title":"TEA-20: routing","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-deferred","mnemon.session_id":"multica:session:root-deferred","mnemon.assignment_id":"asg-a","mnemon.principal":"researcher@team"}},{"id":"child-b","identifier":"TEA-22","title":"TEA-20: status","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-deferred","mnemon.session_id":"multica:session:root-deferred","mnemon.assignment_id":"asg-b","mnemon.principal":"implementer@team"}}]}\n' ;; *"issue comment list root-deferred"*) printf '[]\n' ;; *"issue comment list child-a"*) printf '[{"id":"comment-a","issue_id":"child-a","content":"Mnemon update: assignment feedback\\n\\nmnemon:event=pg-a"}]\n' ;; *"issue comment list child-b"*) printf '[{"id":"comment-b","issue_id":"child-b","content":"Mnemon update: assignment feedback\\n\\nmnemon:event=pg-b"}]\n' ;; diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 7a4e0c23..900cf1e4 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -693,10 +693,7 @@ func ensureMulticaParticipantEnv(ctx context.Context, cli driver.MulticaCLI, par if err != nil { return false, fmt.Errorf("read Multica agent env %s: %w", participant.AgentID, err) } - merged := cloneStringMap(existing) - for key, value := range multicaParticipantRuntimeEnv(cli, participant, registryPath, workspaceID, opts) { - merged[key] = value - } + merged := mergeMulticaParticipantRuntimeEnv(existing, multicaParticipantRuntimeEnv(cli, participant, registryPath, workspaceID, opts)) if sameStringMap(existing, merged) { return false, nil } @@ -732,6 +729,37 @@ func multicaParticipantRuntimeEnv(cli driver.MulticaCLI, participant driver.Mult return env } +func mergeMulticaParticipantRuntimeEnv(existing, desired map[string]string) map[string]string { + merged := cloneStringMap(existing) + for _, key := range multicaParticipantRuntimeEnvKeys { + delete(merged, key) + } + for key, value := range desired { + if strings.TrimSpace(value) != "" { + merged[key] = value + } + } + return merged +} + +var multicaParticipantRuntimeEnvKeys = []string{ + "MNEMON_HUB_BACKEND", + "MNEMON_MULTICA_REGISTRY", + "MNEMON_MULTICA_WORKSPACE_ID", + "MNEMON_MULTICA_BIN", + "MNEMON_MULTICA_PROFILE", + "MNEMON_MULTICA_SERVER_URL", + "MNEMON_CONTROL_ADDR", + "MNEMON_CONTROL_TOKEN", + "MNEMON_CONTROL_TOKEN_FILE", + "MNEMON_CONTROL_PRINCIPAL", + "MNEMON_HARNESS_BIN", + "MNEMON_MANAGED_RUNTIME", + "MNEMON_MANAGED_COMMAND", + "MNEMON_MANAGED_WORKSPACE", + "MNEMON_MANAGED_TURN_TIMEOUT", +} + func defaultMulticaParticipantControlTokenFile(workspace, principal string) string { workspace = strings.TrimSpace(workspace) principal = strings.TrimSpace(principal) diff --git a/harness/cmd/mnemon-harness/multica_test.go b/harness/cmd/mnemon-harness/multica_test.go index 0e432922..65065357 100644 --- a/harness/cmd/mnemon-harness/multica_test.go +++ b/harness/cmd/mnemon-harness/multica_test.go @@ -284,6 +284,34 @@ esac } } +func TestMergeMulticaParticipantRuntimeEnvPrunesStaleManagedKeys(t *testing.T) { + merged := mergeMulticaParticipantRuntimeEnv(map[string]string{ + "MNEMON_CONTROL_TOKEN": "old-token", + "MNEMON_CONTROL_TOKEN_FILE": "/old/token", + "MNEMON_MANAGED_RUNTIME": "codex-appserver", + "MNEMON_HUB_BACKEND": "old", + "CUSTOM_USER_ENV": "keep", + }, map[string]string{ + "MNEMON_HUB_BACKEND": "multica", + "MNEMON_CONTROL_ADDR": "http://127.0.0.1:8791", + "MNEMON_CONTROL_PRINCIPAL": "planner@team", + "MNEMON_MANAGED_WORKSPACE": "/workspace", + "MNEMON_MULTICA_REGISTRY": "/registry.json", + "MNEMON_MULTICA_WORKSPACE_ID": "ws-1", + }) + for _, stale := range []string{"MNEMON_CONTROL_TOKEN", "MNEMON_CONTROL_TOKEN_FILE", "MNEMON_MANAGED_RUNTIME"} { + if _, ok := merged[stale]; ok { + t.Fatalf("stale managed key %s should be pruned: %+v", stale, merged) + } + } + if merged["CUSTOM_USER_ENV"] != "keep" { + t.Fatalf("unmanaged env should be preserved: %+v", merged) + } + if merged["MNEMON_HUB_BACKEND"] != "multica" || merged["MNEMON_CONTROL_PRINCIPAL"] != "planner@team" { + t.Fatalf("desired managed env not applied: %+v", merged) + } +} + func restoreMulticaFlags(t *testing.T) { t.Helper() oldBin := multicaBin diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 8aaa7917..980967d5 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -505,21 +505,7 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr } func runtimeAssignmentCorrelationProgress(result runtimeImportResult) string { - var b strings.Builder - b.WriteString("Mnemon assignment mailbox: correlated") - if result.AssignmentID != "" { - b.WriteString(" assignment=") - b.WriteString(result.AssignmentID) - } else if label := strings.TrimSpace(runtimeAssignmentLabel(result)); label != "" { - b.WriteString(" assignment=") - b.WriteString(label) - } - if result.SessionID != "" { - b.WriteString(" session=") - b.WriteString(result.SessionID) - } - b.WriteString(".") - return b.String() + return "Mnemon assignment mailbox: correlated." } func applyMulticaHubMetadata(result *runtimeImportResult, meta driver.MulticaHubMetadata) { @@ -935,29 +921,11 @@ func formatRuntimeFinalAnswer(result runtimeImportResult) string { b.WriteString(principal) b.WriteString(".") } - if taskID := strings.TrimSpace(result.TaskID); taskID != "" { - b.WriteString(" Multica task: ") - b.WriteString(taskID) - b.WriteString(".") - } switch result.Status { case "recorded": - b.WriteString(" Mnemon ingest: recorded") - if result.Receipt != nil { - b.WriteString(fmt.Sprintf(" seq=%d duplicate=%v ticked=%v", result.Receipt.Seq, result.Receipt.Dup, result.Receipt.Ticked)) - } - b.WriteString(".") + b.WriteString(" Mnemon ingest: recorded.") case "correlated": - b.WriteString(" Mnemon assignment mailbox: correlated") - if result.AssignmentID != "" { - b.WriteString(" assignment=") - b.WriteString(result.AssignmentID) - } - if result.SessionID != "" { - b.WriteString(" session=") - b.WriteString(result.SessionID) - } - b.WriteString(".") + b.WriteString(" Mnemon assignment mailbox: correlated.") case "skipped": b.WriteString(" Mnemon ingest: skipped") if result.Err != nil { @@ -985,12 +953,7 @@ func formatRuntimeFinalAnswer(result runtimeImportResult) string { } switch result.ProjectionStatus { case "commented": - b.WriteString(" Multica projection: comment") - if result.ProjectionCommentID != "" { - b.WriteString("=") - b.WriteString(result.ProjectionCommentID) - } - b.WriteString(".") + b.WriteString(" Multica projection: comment posted.") case "skipped": b.WriteString(" Multica projection: skipped.") case "failed": @@ -1004,12 +967,7 @@ func formatRuntimeFinalAnswer(result runtimeImportResult) string { } switch result.WakeStatus { case "completed": - b.WriteString(" Managed wake: completed") - if result.WakeTurnID != "" { - b.WriteString(" turn=") - b.WriteString(result.WakeTurnID) - } - b.WriteString(".") + b.WriteString(" Managed wake: completed.") case "skipped": b.WriteString(" Managed wake: skipped") if result.WakeErr != nil { @@ -1035,19 +993,12 @@ func formatRuntimeFinalAnswer(result runtimeImportResult) string { } switch result.HubWriteStatus { case "created", "commented", "updated", "noop": - b.WriteString(" Multica hub write: ") - b.WriteString(result.HubWriteStatus) - if result.HubChildIssues > 0 { - b.WriteString(fmt.Sprintf(" child_issues=%d", result.HubChildIssues)) - } - if result.HubFeedbackComments > 0 { - b.WriteString(fmt.Sprintf(" feedback_comments=%d", result.HubFeedbackComments)) - } - b.WriteString(".") + b.WriteString(" Multica updates: ") + b.WriteString(runtimeHubWriteSummary(result)) case "skipped": - b.WriteString(" Multica hub write: skipped.") + b.WriteString(" Multica updates: skipped.") case "failed": - b.WriteString(" Multica hub write: failed") + b.WriteString(" Multica updates: failed") if result.HubWriteErr != nil { b.WriteString(" (") b.WriteString(result.HubWriteErr.Error()) @@ -1217,14 +1168,46 @@ func runtimeHubWriteProgress(result runtimeImportResult) string { return "" case "failed": if result.HubWriteErr != nil { - return "Multica hub projection failed: " + result.HubWriteErr.Error() + return "Multica updates failed: " + result.HubWriteErr.Error() } - return "Multica hub projection failed." + return "Multica updates failed." case "skipped": - return "Multica hub projection skipped." + return "Multica updates skipped." + default: + return "Multica updates: " + runtimeHubWriteSummary(result) + } +} + +func runtimeHubWriteSummary(result runtimeImportResult) string { + status := strings.TrimSpace(result.HubWriteStatus) + var parts []string + if result.HubChildIssues > 0 { + parts = append(parts, fmt.Sprintf("%d %s", result.HubChildIssues, pluralize(result.HubChildIssues, "assignment mailbox", "assignment mailboxes"))) + } + if result.HubFeedbackComments > 0 { + parts = append(parts, fmt.Sprintf("%d %s", result.HubFeedbackComments, pluralize(result.HubFeedbackComments, "feedback comment", "feedback comments"))) + } + switch { + case len(parts) > 0 && status == "created": + return strings.Join(parts, " and ") + " created." + case len(parts) > 0 && status == "commented": + return strings.Join(parts, " and ") + " posted." + case len(parts) > 0: + return strings.Join(parts, " and ") + " synced." + case status == "noop": + return "no visible updates needed." + case status != "": + return strings.ReplaceAll(status, "_", " ") + "." default: - return fmt.Sprintf("Multica hub projection %s: child issues=%d, feedback comments=%d.", result.HubWriteStatus, result.HubChildIssues, result.HubFeedbackComments) + return "no visible updates needed." + } +} + +func pluralize(n int, singular, plural string) string { + if n == 1 { + return singular } + return plural } func runtimeProjectionProgress(result runtimeImportResult) string { diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index ee02fe26..9e845f36 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -373,7 +373,7 @@ esac if strings.Contains(text, "Loading Multica issue iss-7") && phase == "commentary" { sawCommentaryProgress = true } - if strings.Contains(text, "Mnemon ingest: recorded seq=17") && phase == "final_answer" { + if strings.Contains(text, "Mnemon ingest: recorded") && phase == "final_answer" { sawFinalPhaseAnswer = true } } @@ -392,7 +392,7 @@ esac progressItemIDs[itemID] = true } } - if msg["method"] == "item/agentMessage/delta" && strings.Contains(line, "Mnemon ingest: recorded seq=17") && strings.Contains(line, "Multica projection: comment=comment-1") && strings.Contains(line, "Managed wake: completed turn=noop-turn") { + if msg["method"] == "item/agentMessage/delta" && strings.Contains(line, "Mnemon ingest: recorded") && strings.Contains(line, "Multica projection: comment posted") && strings.Contains(line, "Managed wake: completed") { sawAnswer = true params, _ := msg["params"].(map[string]any) answerItemID, _ = params["itemId"].(string) @@ -584,7 +584,7 @@ esac t.Fatalf("runtime comment must not expose machine field %q:\n%s", blocked, comment) } } - if !strings.Contains(out.String(), "Mnemon ingest: recorded seq=21") { + if !strings.Contains(out.String(), "Mnemon ingest: recorded") { t.Fatalf("runtime output missing ingest evidence:\n%s", out.String()) } } @@ -660,14 +660,14 @@ esac output := out.String() for _, want := range []string{ "Root session metadata write failed; continuing with Mnemon ingest from issue context.", - "Mnemon ingest: recorded seq=29", - "Multica projection: comment=comment-9", + "Mnemon ingest: recorded", + "Multica projection: comment posted", } { if !strings.Contains(output, want) { t.Fatalf("runtime output missing %q:\n%s", want, output) } } - if strings.Contains(output, "Mnemon ingest: failed") || strings.Contains(output, "Multica hub write: failed") { + if strings.Contains(output, "Mnemon ingest: failed") || strings.Contains(output, "Multica updates: failed") { t.Fatalf("metadata projection failure polluted protocol result:\n%s", output) } comment := mustReadRuntimeTestFile(t, commentPath) @@ -854,7 +854,7 @@ esac if err != nil { t.Fatal(err) } - if !strings.Contains(out.String(), "Multica hub write: updated child_issues=1 feedback_comments=1") { + if !strings.Contains(out.String(), "Multica updates: 1 assignment mailbox and 1 feedback comment synced") { t.Fatalf("runtime output missing hub write evidence:\n%s", out.String()) } args := mustReadRuntimeTestFile(t, argsPath) @@ -1108,7 +1108,7 @@ esac if err != nil { t.Fatal(err) } - if !strings.Contains(out.String(), "Multica hub write: updated child_issues=2 feedback_comments=2") { + if !strings.Contains(out.String(), "Multica updates: 2 assignment mailboxes and 2 feedback comments synced") { t.Fatalf("runtime output missing duplicate-id hub write evidence:\n%s", out.String()) } args := mustReadRuntimeTestFile(t, argsPath) @@ -1251,7 +1251,7 @@ esac t.Fatal(err) } output := out.String() - if !strings.Contains(output, "Multica hub write: failed") { + if !strings.Contains(output, "Multica updates: failed") { t.Fatalf("runtime should surface the partial metadata failure:\n%s", output) } args := mustReadRuntimeTestFile(t, argsPath) @@ -1361,7 +1361,7 @@ esac if err != nil { t.Fatal(err) } - if !strings.Contains(out.String(), "Multica hub write: commented feedback_comments=1") { + if !strings.Contains(out.String(), "Multica updates: 1 feedback comment posted") { t.Fatalf("runtime output missing recovered feedback projection:\n%s", out.String()) } args := mustReadRuntimeTestFile(t, argsPath) @@ -1617,7 +1617,7 @@ esac if ingestCalled { t.Fatal("assignment mailbox dispatch must not ingest a new teamwork signal") } - if !strings.Contains(out.String(), "Mnemon assignment mailbox: correlated assignment=asg-1") || !strings.Contains(out.String(), "Managed wake: completed turn=noop-turn") { + if !strings.Contains(out.String(), "Mnemon assignment mailbox: correlated") || !strings.Contains(out.String(), "Managed wake: completed") { t.Fatalf("runtime output missing correlation/wake evidence:\n%s", out.String()) } args := mustReadRuntimeTestFile(t, argsPath) diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index d0d13303..2ff153f0 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -19,6 +19,14 @@ type AssignmentMailboxMaterial struct { Rationale string } +type RootSessionMaterial struct { + Request string + WorkMode string + Handoffs []string + Validation []string + Completion string +} + type ProgressFeedbackMaterial struct { AssignmentRef string FeedbackKind string @@ -64,10 +72,41 @@ func AssignmentMailboxTitle(item AssignmentMailboxMaterial) string { return trimTitle("Assignment: " + topic) } +func RootSessionDescription(item RootSessionMaterial) string { + var b strings.Builder + request := strings.TrimSpace(item.Request) + if request == "" { + request = "Coordinate this request through Mnemon teamwork in Multica." + } + b.WriteString("## Request\n\n") + b.WriteString(request) + b.WriteString("\n\n") + + b.WriteString("## Teamwork\n\n") + writeBullet(&b, "Work mode", firstNonEmptyString(item.WorkMode, "Mnemon teamwork with Multica issue visibility")) + writeBullet(&b, "Assignment path", "Accepted assignments appear as child issues assigned to target agents") + writeBullet(&b, "Feedback path", "Progress, results, and blockers appear as child issue comments and statuses") + + if handoffs := cleanStrings(item.Handoffs); len(handoffs) > 0 { + b.WriteString("\n## Handoffs\n\n") + writeList(&b, handoffs) + } + if validation := cleanStrings(item.Validation); len(validation) > 0 { + b.WriteString("\n## Validation\n\n") + writeList(&b, validation) + } + if completion := strings.TrimSpace(item.Completion); completion != "" { + b.WriteString("\n## Completion\n\n") + b.WriteString(completion) + b.WriteString("\n") + } + return strings.TrimSpace(b.String()) +} + func AssignmentMailboxDescription(item AssignmentMailboxMaterial) string { var b strings.Builder b.WriteString("## Assignment\n\n") - if work := strings.TrimSpace(item.ExpectedWork); work != "" { + if work := visibleProtocolText(item.ExpectedWork); work != "" { b.WriteString(work) b.WriteString("\n\n") } else if scope := strings.TrimSpace(item.Scope); scope != "" { @@ -78,7 +117,7 @@ func AssignmentMailboxDescription(item AssignmentMailboxMaterial) string { writeBullet(&b, "Root issue", rootIssueReference(item)) writeBullet(&b, "Assignee", assigneeReference(item)) writeBullet(&b, "Scope", item.Scope) - if rationale := strings.TrimSpace(item.Rationale); rationale != "" { + if rationale := visibleProtocolText(item.Rationale); rationale != "" { b.WriteString("\n## Rationale\n\n") b.WriteString(rationale) b.WriteString("\n") @@ -404,14 +443,10 @@ func assigneeReference(item AssignmentMailboxMaterial) string { } func visibleExpectedFeedback(value string) string { - value = strings.TrimSpace(value) + value = visibleProtocolText(value) if value == "" { return "" } - value = strings.ReplaceAll(value, "progress_digest", "runtime feedback") - value = strings.ReplaceAll(value, "progress digest", "runtime feedback") - value = strings.ReplaceAll(value, "feedback_kind=", "status=") - value = strings.ReplaceAll(value, "feedback_kind", "status") lower := strings.ToLower(value) for _, prefix := range []string{"runtime feedback with "} { if strings.HasPrefix(lower, prefix) { @@ -424,6 +459,15 @@ func visibleExpectedFeedback(value string) string { return value } +func visibleProtocolText(value string) string { + value = strings.TrimSpace(value) + value = strings.ReplaceAll(value, "progress_digest", "runtime feedback") + value = strings.ReplaceAll(value, "progress digest", "runtime feedback") + value = strings.ReplaceAll(value, "feedback_kind=", "status=") + value = strings.ReplaceAll(value, "feedback_kind", "status") + return strings.TrimSpace(value) +} + func visibleFeedbackStatus(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "": diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 1e75888c..1d9f3b41 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -5,6 +5,45 @@ import ( "testing" ) +func TestRootSessionDescriptionIsStructuredVisibleText(t *testing.T) { + body := RootSessionDescription(RootSessionMaterial{ + Request: "Run a Multica readiness drill.", + WorkMode: "Mnemon teamwork with Multica issue visibility.", + Handoffs: []string{ + "Route root visibility and child routing checks to separate teammates.", + "Route a final integration check after teammate feedback is visible.", + }, + Validation: []string{ + "Root issue carries session metadata and shows run activity.", + "Accepted assignments become child issue mailboxes assigned to target agents.", + "Final root status reflects completion.", + }, + Completion: "Finish when child feedback comments are visible and the root issue reaches a terminal status.", + }) + for _, want := range []string{ + "## Request", + "Run a Multica readiness drill.", + "## Teamwork", + "Work mode: Mnemon teamwork with Multica issue visibility.", + "Assignment path: Accepted assignments appear as child issues assigned to target agents", + "Feedback path: Progress, results, and blockers appear as child issue comments and statuses", + "## Handoffs", + "Route root visibility and child routing checks to separate teammates.", + "## Validation", + "Root issue carries session metadata and shows run activity.", + "## Completion", + } { + if !strings.Contains(body, want) { + t.Fatalf("root session description missing %q:\n%s", want, body) + } + } + for _, blocked := range []string{"mnemon.", "session_id", "assignment_id", "assignment_ref", "progress_digest", "hub_backend", "projection owner"} { + if strings.Contains(strings.ToLower(body), blocked) { + t.Fatalf("root session description must not expose machine field %q:\n%s", blocked, body) + } + } +} + func TestAssignmentMailboxMaterial(t *testing.T) { item := AssignmentMailboxMaterial{ ID: "asg-1", @@ -127,6 +166,28 @@ func TestAssignmentMailboxDescriptionNormalizesProtocolFeedback(t *testing.T) { } } +func TestAssignmentMailboxDescriptionNormalizesProtocolWorkAndRationale(t *testing.T) { + body := AssignmentMailboxDescription(AssignmentMailboxMaterial{ + ID: "asg-1", + Scope: "release validation", + ExpectedWork: "Report a progress_digest result with exact evidence.", + Rationale: "Use feedback_kind=result only after checking Multica evidence.", + }) + for _, blocked := range []string{"progress_digest", "feedback_kind"} { + if strings.Contains(body, blocked) { + t.Fatalf("description should keep protocol word %q out of visible text:\n%s", blocked, body) + } + } + for _, want := range []string{ + "Report a runtime feedback result with exact evidence.", + "Use status=result only after checking Multica evidence.", + } { + if !strings.Contains(body, want) { + t.Fatalf("description missing normalized text %q:\n%s", want, body) + } + } +} + func TestAssignmentMailboxDescriptionNormalizesFeedbackKindProtocolWording(t *testing.T) { body := AssignmentMailboxDescription(AssignmentMailboxMaterial{ ID: "asg-1", From e2f0e5355e993be8c4fc41e079debd5b6fab9b64 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:15:13 +0800 Subject: [PATCH 068/117] test: add multica parallel poc task case Add selectable Multica runtime task cases for production-like acceptance, including a parallel PoC overlap drill with isolated workstream directories, shared context reuse checks, role overlap, and stricter hub-flow expectations. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance -run 'TestMultica'; go test -count=1 ./harness/cmd/mnemon-acceptance ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/... --- .../r2/multica-parallel-poc-overlap-case.md | 234 +++++++ .../acceptance_multica_runtime.go | 62 +- .../acceptance_multica_task_cases.go | 624 ++++++++++++++++++ .../acceptance_multica_task_cases_test.go | 208 ++++++ 4 files changed, 1110 insertions(+), 18 deletions(-) create mode 100644 .mnemon-dev/architecture/r2/multica-parallel-poc-overlap-case.md create mode 100644 harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go create mode 100644 harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go diff --git a/.mnemon-dev/architecture/r2/multica-parallel-poc-overlap-case.md b/.mnemon-dev/architecture/r2/multica-parallel-poc-overlap-case.md new file mode 100644 index 00000000..8ddd4b4b --- /dev/null +++ b/.mnemon-dev/architecture/r2/multica-parallel-poc-overlap-case.md @@ -0,0 +1,234 @@ +# Multica Parallel PoC Overlap Case + +- Status: R2 acceptance scenario design +- Date: 2026-06-30 +- Related: + - `multica-hub-backend-architecture.md` + - `multica-runtime-adapter.md` + - `teamwork-event-driven-cue-model.md` + - `../teamwork-protocol-sense-select-work-feedback.md` + +## 0. Purpose + +这个 case 用来验证 Multica 作为 Mnemon hub backend 时,是否能承载接近真实业务协作的复杂度: + +- 一个 root issue 作为 session mailbox。 +- 三个相关 PoC 同时启动,每个 PoC 有独立目录和交付物。 +- PoC 之间共享上下文,并要求反馈显式引用共享上下文。 +- 角色有重叠,同一个 agent 会把一个 PoC 的局部上下文带到另一个 PoC。 +- 第一轮反馈之后必须产生至少一个 follow-up assignment,触发第二轮 teamwork。 +- 最终 integrator 需要把多轮反馈合成一个 operator-facing decision。 + +本 case 不专门测试 GitHub mesh。GitHub mesh 只需要在设计上保持 mirror-only / no second activation path 的逻辑合理性。当前验收重点是 `mnemonhub` 与 Multica hub path,其中 Multica 是可见产品面和 assignment activation path。 + +## 1. Scenario + +Case id: + +```text +parallel-poc-overlap +``` + +Root issue title: + +```text +Parallel PoC overlap drill +``` + +Recommended command: + +```text +mnemon-acceptance multica-runtime-prod-sim \ + --task-case parallel-poc-overlap \ + --require-hub-flow \ + --min-participants 5 \ + --min-active-agents 5 +``` + +Directory plan created under the acceptance run root: + +```text +/ + acceptance-report.json + taskcase/ + parallel-poc-overlap/ + evidence/ + shared-context/ + session-map/ + mailbox-contract/ + risk-register/ + evidence-ledger/ + workstreams/ + poc-runtime-routing/ + poc-operator-runbook/ + poc-release-risk/ + roles/ + planner/ + researcher/ + implementer/ + reviewer/ + integrator/ +``` + +The root issue body should include the same plan in human-readable Markdown. Deterministic protocol data still belongs in Multica metadata or stable comment markers, not visible prose. + +## 2. Collaboration Topology + +```text + +-------------------------------+ + | Multica root issue | + | session mailbox | + +---------------+---------------+ + | + v + planner@team creates + first-round assignments + | + +-----------------------------------+-----------------------------------+ + | | | + v v v ++-----------------------+ +-----------------------+ +-----------------------+ +| poc-runtime-routing | | poc-operator-runbook | | poc-release-risk | +| assignment mailbox A | | assignment mailbox B | | assignment mailbox C | +| researcher/implementer| | implementer/reviewer | | researcher/reviewer/ | +| | | | | integrator | ++-----------+-----------+ +-----------+-----------+ +-----------+-----------+ + | | | + +---------------+-----------------+-----------------+---------------+ + | shared contexts and feedback | + v | + +---------------------+ | + | follow-up mailbox |<-----------------------+ + | chosen from gap or | + | disagreement | + +----------+----------+ + | + v + +-------------------+ + | integrator@team | + | final decision | + +-------------------+ +``` + +## 3. Shared Contexts + +```text ++------------------+---------------------------+-----------------------------+ +| Shared context | Used by | Purpose | ++------------------+---------------------------+-----------------------------+ +| session-map | runtime-routing, release | Map root, child, run, agent | +| mailbox-contract | runtime-routing, runbook | Visible text and metadata | +| risk-register | runbook, release | Risks, owners, mitigations | +| evidence-ledger | all PoCs | Issue/run/comment evidence | ++------------------+---------------------------+-----------------------------+ +``` + +Expected reuse: + +- `researcher@team` carries `session-map` from runtime routing into release risk. +- `implementer@team` carries `mailbox-contract` from runtime routing into runbook review. +- `reviewer@team` carries rollback and status-projection concerns from runbook review into release risk. +- `integrator@team` consumes all contexts and must wait for follow-up feedback before closing. + +## 4. Role Matrix + +```text ++------------------+-----------------------+-------------------------------+ +| Principal | Primary PoC | Overlap | ++------------------+-----------------------+-------------------------------+ +| planner@team | poc-runtime-routing | poc-release-risk | +| researcher@team | poc-runtime-routing | poc-release-risk | +| implementer@team | poc-operator-runbook | poc-runtime-routing | +| reviewer@team | poc-release-risk | poc-operator-runbook | +| integrator@team | poc-release-risk | all PoCs for final synthesis | ++------------------+-----------------------+-------------------------------+ +``` + +Roles are cues, not permanent identities. The protocol-level facts remain accepted events and hub metadata. An agent may be PoC-like when emitting a signal or assignment, and IC-like when working an assignment and producing feedback. + +## 5. Expected ReAct Progression + +```text +Round 1: Observe + root issue -> planner runtime run + planner emits three assignments + Multica projects three child issue mailboxes + runtime-routing, runbook, and release PoCs run in parallel + each feedback comment names: + - shared context consumed + - evidence artifact produced + - issue/run/comment/status refs + +Round 2: Act + planner or integrator reads first-round feedback + highest disagreement or missing evidence is selected + a follow-up assignment is created + follow-up owner reuses at least two shared contexts + feedback cites prior child comments before adding new evidence + +Round 3: Reflect + integrator consumes all PoC outputs and follow-up feedback + final root comment records: + - observed facts + - actions taken + - context reused across PoCs + - residual risk + - ship/hold/follow-up decision +``` + +## 6. Protocol And Multica Boundaries + +```text ++----------------------+------------------------------+------------------------------+ +| Layer | Carries | Must not carry | ++----------------------+------------------------------+------------------------------+ +| Multica visible text | task, context, evidence | session ids, assignment ids | +| Multica metadata | routing, dedupe, correlation | LLM prompt instructions | +| Multica comments | human feedback, event refs | sole canonical truth | +| Multica runs | activation evidence | proof of task completion | +| Mnemon event store | canonical accepted events | product-only display state | +| mnemond render | LLM-facing cue | raw Multica issue as prompt | ++----------------------+------------------------------+------------------------------+ +``` + +Standard Multica-visible references should be used for humans only: issue mentions, assigned agents, and normal comments. Machine correlation should use `mnemon.*` metadata keys and stable event/comment markers. + +## 7. Expected Acceptance Signals + +Minimum successful run: + +```text +active_agents >= 5 +child_mailboxes >= 4 +feedback_comments >= 4 +teamwork_rounds >= observe + act + reflect +root final status terminal +child final statuses result or blocker +``` + +Report evidence should show: + +- root metadata has `hub_backend=multica` and `kind=session_mailbox`. +- child issues carry assignment mailbox metadata. +- child visible text uses structured sections: Assignment, Context, Feedback. +- child visible text does not expose session ids, assignment ids, fingerprints, or projection owner keys. +- run evidence covers planner plus at least four additional participants. +- comments include context reuse evidence, not only generic completion text. + +## 8. Failure Signals + +```text ++------------------------------------+---------------------------------------+ +| Failure | Meaning | ++------------------------------------+---------------------------------------+ +| only one child mailbox | planner did not split teamwork | +| no follow-up mailbox | second ReAct round did not happen | +| no context names in feedback | context reuse is not observable | +| duplicated child mailbox | hub dedupe or assignment identity risk | +| protocol fields in visible text | metadata/visible boundary regression | +| active agent count below expected | Multica activation did not fan out | +| final root lacks decision | integration loop did not close | ++------------------------------------+---------------------------------------+ +``` + +This case is intentionally stronger than a routing smoke test. It should make weak assignment routing, missing feedback projection, stale context selection, and accidental protocol leakage visible in one Multica session. diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 9b0b2488..d80661fb 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -24,6 +24,7 @@ var ( acceptanceMulticaWorkspaceID string acceptanceMulticaRegistry string acceptanceMulticaAssigneePrincipal string + acceptanceMulticaTaskCase string acceptanceMulticaIssueTitle string acceptanceMulticaIssueDescription string acceptanceMulticaWait time.Duration @@ -47,6 +48,7 @@ var acceptanceMulticaRuntimeCmd = &cobra.Command{ WorkspaceID: acceptanceMulticaWorkspaceID, RegistryPath: acceptanceMulticaRegistry, AssigneePrincipal: acceptanceMulticaAssigneePrincipal, + TaskCase: acceptanceMulticaTaskCase, IssueTitle: acceptanceMulticaIssueTitle, IssueDescription: acceptanceMulticaIssueDescription, Wait: acceptanceMulticaWait, @@ -80,6 +82,7 @@ func init() { acceptanceMulticaRuntimeCmd.Flags().StringVar(&acceptanceMulticaWorkspaceID, "multica-workspace-id", multicaAcceptanceEnvDefault("MNEMON_MULTICA_WORKSPACE_ID", ""), "Multica workspace ID") acceptanceMulticaRuntimeCmd.Flags().StringVar(&acceptanceMulticaRegistry, "registry", "", "Multica participant registry path") acceptanceMulticaRuntimeCmd.Flags().StringVar(&acceptanceMulticaAssigneePrincipal, "assignee-principal", "planner@team", "Mnemon principal whose Multica agent receives the issue") + acceptanceMulticaRuntimeCmd.Flags().StringVar(&acceptanceMulticaTaskCase, "task-case", multicaAcceptanceTaskCaseR2Readiness, "real Multica task case to create ("+strings.Join(multicaAcceptanceTaskCaseNames(), ", ")+")") acceptanceMulticaRuntimeCmd.Flags().StringVar(&acceptanceMulticaIssueTitle, "issue-title", "", "Multica issue title") acceptanceMulticaRuntimeCmd.Flags().StringVar(&acceptanceMulticaIssueDescription, "issue-description", "", "Multica issue description") acceptanceMulticaRuntimeCmd.Flags().DurationVar(&acceptanceMulticaWait, "wait", 10*time.Minute, "time to wait for Multica runtime evidence") @@ -100,6 +103,7 @@ type multicaRuntimeProdSimOptions struct { WorkspaceID string RegistryPath string AssigneePrincipal string + TaskCase string IssueTitle string IssueDescription string Wait time.Duration @@ -122,6 +126,9 @@ type multicaRuntimeProdSimReport struct { ReportPath string `json:"report_path"` WorkspaceID string `json:"workspace_id"` RegistryPath string `json:"registry_path"` + TaskCase string `json:"task_case,omitempty"` + TaskExpectations multicaAcceptanceTaskCaseExpectations `json:"task_expectations,omitempty"` + ExecutionPlan multicaAcceptanceExecutionPlan `json:"execution_plan,omitempty"` Assignee driver.MulticaParticipantRecord `json:"assignee"` Participants []driver.MulticaParticipantRecord `json:"participants,omitempty"` Issue driver.MulticaIssue `json:"issue"` @@ -169,9 +176,20 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime if opts.MinActiveAgents <= 0 { opts.MinActiveAgents = 3 } + requestedTaskCase := strings.TrimSpace(opts.TaskCase) + if requestedTaskCase == "" { + requestedTaskCase = multicaAcceptanceTaskCaseR2Readiness + } + taskCase, taskCaseErr := multicaAcceptanceTaskCase(requestedTaskCase, started) runRoot := strings.TrimSpace(opts.RunRoot) if runRoot == "" { - runRoot = filepath.Join(".testdata", "multica-runtime-prod-sim", started.Format("20060102T150405Z")) + caseID := requestedTaskCase + if taskCaseErr != nil { + caseID = "invalid-task-case" + } else { + caseID = taskCase.ID + } + runRoot = filepath.Join(".testdata", "multica-runtime-prod-sim", multicaAcceptancePathSegment(caseID, "task-case"), started.Format("20060102T150405Z")) } absRunRoot, err := filepath.Abs(runRoot) if err != nil { @@ -185,10 +203,20 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime RunRoot: runRoot, WorkspaceID: strings.TrimSpace(opts.WorkspaceID), RegistryPath: strings.TrimSpace(opts.RegistryPath), + TaskCase: requestedTaskCase, } if err := prepareR1AcceptanceRunRoot(runRoot); err != nil { return finishMulticaRuntimeProdSimReport(report, err) } + if taskCaseErr != nil { + return finishMulticaRuntimeProdSimReport(report, taskCaseErr) + } + report.TaskCase = taskCase.ID + executionPlan, err := materializeMulticaAcceptanceExecutionPlan(runRoot, taskCase) + if err != nil { + return finishMulticaRuntimeProdSimReport(report, err) + } + report.ExecutionPlan = executionPlan var prereqErrs []error if cliPath, err := resolveMulticaAcceptanceCLI(opts.MulticaBin); err != nil { addMulticaProdSimAssertion(&report, "Multica CLI available", false, err.Error()) @@ -228,6 +256,10 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime return finishMulticaRuntimeProdSimReport(report, err) } report.Assignee = assignee + report.TaskExpectations = taskCase.Expectations + if taskCase.Expectations.MinActiveAgents > opts.MinActiveAgents { + opts.MinActiveAgents = taskCase.Expectations.MinActiveAgents + } cli := driver.MulticaCLI{ Command: strings.TrimSpace(opts.MulticaBin), Profile: strings.TrimSpace(opts.Profile), @@ -238,26 +270,11 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime } title := strings.TrimSpace(opts.IssueTitle) if title == "" { - title = "Mnemon Multica runtime prod-sim " + started.Format("150405") + title = taskCase.Title } description := strings.TrimSpace(opts.IssueDescription) if description == "" { - description = multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ - Request: "Run a small Mnemon R2 Multica readiness drill.", - WorkMode: "Use Mnemon teamwork; Multica shows issues, runs, comments, and statuses.", - Handoffs: []string{ - "Route root session visibility and child issue routing checks to separate teammates.", - "After teammate feedback is visible, route a final integration check.", - }, - Validation: []string{ - "Root issue carries session metadata and shows run activity.", - "Accepted assignments become child issue mailboxes assigned to target agents.", - "Feedback comments and statuses are projected back to Multica.", - "Stale or cross-session assignment material is ignored.", - "Final root status reflects completion.", - }, - Completion: "Finish when child feedback comments are visible and the root issue reaches a terminal status.", - }) + description = strings.TrimSpace(taskCase.Description + "\n\n" + renderMulticaAcceptanceExecutionPlan(executionPlan)) } issue, err := cli.CreateIssue(ctx, driver.MulticaCreateIssueRequest{ Title: title, @@ -347,6 +364,9 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o if minAssignmentChildren < 1 { minAssignmentChildren = 1 } + if report.TaskExpectations.MinChildMailboxes > minAssignmentChildren { + minAssignmentChildren = report.TaskExpectations.MinChildMailboxes + } children, childMeta, err := waitMulticaAssignmentChildren(ctx, cli, report.Issue.ID, opts.Wait, opts.Poll, minAssignmentChildren) report.ChildIssues = children report.ChildMetadata = childMeta @@ -398,6 +418,12 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o return err } addMulticaProdSimAssertion(report, "hub-flow projects feedback comments and completion statuses", multicaHubProjectionComplete(finalRoot, finalChildren, childComments), fmt.Sprintf("root=%s children=%v comments=%d", finalRoot.Status, multicaIssueStatuses(finalChildren), multicaCommentCount(childComments))) + if report.TaskExpectations.MinChildMailboxes > 0 { + addMulticaProdSimAssertion(report, "task case child mailbox expectation met", len(finalChildren) >= report.TaskExpectations.MinChildMailboxes, fmt.Sprintf("children=%d min=%d", len(finalChildren), report.TaskExpectations.MinChildMailboxes)) + } + if report.TaskExpectations.MinFeedbackComments > 0 { + addMulticaProdSimAssertion(report, "task case feedback comment expectation met", multicaCommentCount(childComments) >= report.TaskExpectations.MinFeedbackComments, fmt.Sprintf("comments=%d min=%d", multicaCommentCount(childComments), report.TaskExpectations.MinFeedbackComments)) + } visibleOK, visibleDetail := multicaAssignmentChildrenUseStructuredVisibleText(finalChildren) addMulticaProdSimAssertion(report, "assignment child issue visible text is structured", visibleOK, visibleDetail) return nil diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go new file mode 100644 index 00000000..da2a0e04 --- /dev/null +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go @@ -0,0 +1,624 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" +) + +const ( + multicaAcceptanceTaskCaseR2Readiness = "r2-readiness" + multicaAcceptanceTaskCaseProtocolReAct = "protocol-react-drill" + multicaAcceptanceTaskCaseParallelPoc = "parallel-poc-overlap" + multicaAcceptanceTaskCaseReleaseReadiness = "release-readiness" + multicaAcceptanceTaskCaseIncidentTriage = "incident-triage" + multicaAcceptanceTaskCaseRunbookReview = "runbook-review" +) + +type multicaAcceptanceTaskCaseMaterial struct { + ID string + Title string + Description string + Expectations multicaAcceptanceTaskCaseExpectations + Workstreams []multicaAcceptanceWorkstream + Roles []multicaAcceptanceRolePlan + SharedContexts []multicaAcceptanceSharedContext + RoleOverlaps []string + ContextReuseChecks []string +} + +type multicaAcceptanceTaskCaseExpectations struct { + MinActiveAgents int `json:"min_active_agents,omitempty"` + MinChildMailboxes int `json:"min_child_mailboxes,omitempty"` + MinFeedbackComments int `json:"min_feedback_comments,omitempty"` + TeamworkRounds []string `json:"teamwork_rounds,omitempty"` +} + +type multicaAcceptanceExecutionPlan struct { + RunRoot string `json:"run_root,omitempty"` + CaseRoot string `json:"case_root,omitempty"` + SharedContextDir string `json:"shared_context_dir,omitempty"` + EvidenceDir string `json:"evidence_dir,omitempty"` + Workstreams []multicaAcceptanceWorkstream `json:"workstreams,omitempty"` + Roles []multicaAcceptanceRolePlan `json:"roles,omitempty"` + SharedContexts []multicaAcceptanceSharedContext `json:"shared_contexts,omitempty"` + RoleOverlaps []string `json:"role_overlaps,omitempty"` + ContextReuseChecks []string `json:"context_reuse_checks,omitempty"` +} + +type multicaAcceptanceWorkstream struct { + ID string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Directory string `json:"directory,omitempty"` + PrimaryRoles []string `json:"primary_roles,omitempty"` + SharedContextRefs []string `json:"shared_context_refs,omitempty"` + ExpectedArtifacts []string `json:"expected_artifacts,omitempty"` +} + +type multicaAcceptanceRolePlan struct { + Role string `json:"role,omitempty"` + Principal string `json:"principal,omitempty"` + Directory string `json:"directory,omitempty"` + Primary []string `json:"primary,omitempty"` + Overlaps []string `json:"overlaps,omitempty"` + Responsibilities []string `json:"responsibilities,omitempty"` +} + +type multicaAcceptanceSharedContext struct { + ID string `json:"id,omitempty"` + Directory string `json:"directory,omitempty"` + UsedBy []string `json:"used_by,omitempty"` + Purpose string `json:"purpose,omitempty"` +} + +func multicaAcceptanceTaskCaseNames() []string { + names := make([]string, 0, len(multicaAcceptanceTaskCases)) + for name := range multicaAcceptanceTaskCases { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func multicaAcceptanceTaskCase(id string, started time.Time) (multicaAcceptanceTaskCaseMaterial, error) { + id = strings.TrimSpace(id) + if id == "" { + id = multicaAcceptanceTaskCaseR2Readiness + } + build, ok := multicaAcceptanceTaskCases[id] + if !ok { + return multicaAcceptanceTaskCaseMaterial{}, fmt.Errorf("unknown Multica task case %q (available: %s)", id, strings.Join(multicaAcceptanceTaskCaseNames(), ", ")) + } + material := build(started) + material.ID = id + return material, nil +} + +func materializeMulticaAcceptanceExecutionPlan(runRoot string, taskCase multicaAcceptanceTaskCaseMaterial) (multicaAcceptanceExecutionPlan, error) { + runRoot = strings.TrimSpace(runRoot) + if runRoot == "" { + return multicaAcceptanceExecutionPlan{}, fmt.Errorf("Multica task case execution plan requires a run root") + } + caseID := multicaAcceptancePathSegment(taskCase.ID, "task-case") + caseRoot := filepath.Join(runRoot, "taskcase", caseID) + sharedDir := filepath.Join(caseRoot, "shared-context") + evidenceDir := filepath.Join(caseRoot, "evidence") + plan := multicaAcceptanceExecutionPlan{ + RunRoot: runRoot, + CaseRoot: caseRoot, + SharedContextDir: sharedDir, + EvidenceDir: evidenceDir, + RoleOverlaps: multicaAcceptanceCleanStrings(taskCase.RoleOverlaps), + ContextReuseChecks: multicaAcceptanceCleanStrings(taskCase.ContextReuseChecks), + } + dirs := []string{caseRoot, sharedDir, evidenceDir} + for _, stream := range taskCase.Workstreams { + stream.ID = strings.TrimSpace(stream.ID) + if stream.ID == "" { + continue + } + stream.Directory = filepath.Join(caseRoot, "workstreams", multicaAcceptancePathSegment(stream.ID, "workstream")) + stream.PrimaryRoles = multicaAcceptanceCleanStrings(stream.PrimaryRoles) + stream.SharedContextRefs = multicaAcceptanceCleanStrings(stream.SharedContextRefs) + stream.ExpectedArtifacts = multicaAcceptanceCleanStrings(stream.ExpectedArtifacts) + plan.Workstreams = append(plan.Workstreams, stream) + dirs = append(dirs, stream.Directory) + } + for _, role := range taskCase.Roles { + role.Role = strings.TrimSpace(role.Role) + role.Principal = strings.TrimSpace(role.Principal) + if role.Role == "" && role.Principal == "" { + continue + } + role.Directory = filepath.Join(caseRoot, "roles", multicaAcceptancePathSegment(multicaAcceptanceFirstNonEmpty(role.Role, role.Principal), "role")) + role.Primary = multicaAcceptanceCleanStrings(role.Primary) + role.Overlaps = multicaAcceptanceCleanStrings(role.Overlaps) + role.Responsibilities = multicaAcceptanceCleanStrings(role.Responsibilities) + plan.Roles = append(plan.Roles, role) + dirs = append(dirs, role.Directory) + } + for _, shared := range taskCase.SharedContexts { + shared.ID = strings.TrimSpace(shared.ID) + if shared.ID == "" { + continue + } + shared.Directory = filepath.Join(sharedDir, multicaAcceptancePathSegment(shared.ID, "context")) + shared.UsedBy = multicaAcceptanceCleanStrings(shared.UsedBy) + shared.Purpose = strings.TrimSpace(shared.Purpose) + plan.SharedContexts = append(plan.SharedContexts, shared) + dirs = append(dirs, shared.Directory) + } + for _, dir := range dirs { + if err := os.MkdirAll(dir, 0o755); err != nil { + return plan, err + } + } + return plan, nil +} + +func renderMulticaAcceptanceExecutionPlan(plan multicaAcceptanceExecutionPlan) string { + if strings.TrimSpace(plan.CaseRoot) == "" { + return "" + } + var b strings.Builder + b.WriteString("## Execution Plan\n\n") + writeMulticaAcceptancePlanBullet(&b, "Case root", plan.CaseRoot) + writeMulticaAcceptancePlanBullet(&b, "Shared context", plan.SharedContextDir) + writeMulticaAcceptancePlanBullet(&b, "Evidence", plan.EvidenceDir) + if len(plan.Workstreams) > 0 { + b.WriteString("\n## Parallel PoCs\n\n") + for _, stream := range plan.Workstreams { + line := "- " + multicaAcceptanceMarkdownCode(stream.ID) + if title := strings.TrimSpace(stream.Title); title != "" { + line += ": " + title + } + if len(stream.PrimaryRoles) > 0 { + line += "; roles " + multicaAcceptanceInlineCodes(stream.PrimaryRoles) + } + if len(stream.SharedContextRefs) > 0 { + line += "; shared context " + multicaAcceptanceInlineCodes(stream.SharedContextRefs) + } + if strings.TrimSpace(stream.Directory) != "" { + line += "; dir " + multicaAcceptanceMarkdownCode(stream.Directory) + } + b.WriteString(line) + b.WriteString("\n") + if len(stream.ExpectedArtifacts) > 0 { + b.WriteString(" - expected artifacts: ") + b.WriteString(strings.Join(stream.ExpectedArtifacts, ", ")) + b.WriteString("\n") + } + } + } + if len(plan.SharedContexts) > 0 { + b.WriteString("\n## Shared Contexts\n\n") + for _, shared := range plan.SharedContexts { + line := "- " + multicaAcceptanceMarkdownCode(shared.ID) + if purpose := strings.TrimSpace(shared.Purpose); purpose != "" { + line += ": " + purpose + } + if len(shared.UsedBy) > 0 { + line += "; used by " + multicaAcceptanceInlineCodes(shared.UsedBy) + } + if strings.TrimSpace(shared.Directory) != "" { + line += "; dir " + multicaAcceptanceMarkdownCode(shared.Directory) + } + b.WriteString(line) + b.WriteString("\n") + } + } + if len(plan.Roles) > 0 { + b.WriteString("\n## Role Matrix\n\n") + for _, role := range plan.Roles { + label := multicaAcceptanceFirstNonEmpty(role.Principal, role.Role) + line := "- " + multicaAcceptanceMarkdownCode(label) + if role.Role != "" && role.Principal != "" && role.Role != role.Principal { + line += " (" + role.Role + ")" + } + if len(role.Primary) > 0 { + line += "; primary " + multicaAcceptanceInlineCodes(role.Primary) + } + if len(role.Overlaps) > 0 { + line += "; overlap " + multicaAcceptanceInlineCodes(role.Overlaps) + } + b.WriteString(line) + b.WriteString("\n") + for _, responsibility := range role.Responsibilities { + b.WriteString(" - ") + b.WriteString(responsibility) + b.WriteString("\n") + } + } + } + if len(plan.RoleOverlaps) > 0 { + b.WriteString("\n## Role Overlap\n\n") + for _, overlap := range plan.RoleOverlaps { + b.WriteString("- ") + b.WriteString(overlap) + b.WriteString("\n") + } + } + if len(plan.ContextReuseChecks) > 0 { + b.WriteString("\n## Context Reuse Checks\n\n") + for _, check := range plan.ContextReuseChecks { + b.WriteString("- ") + b.WriteString(check) + b.WriteString("\n") + } + } + return strings.TrimSpace(b.String()) +} + +func writeMulticaAcceptancePlanBullet(b *strings.Builder, label, value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + b.WriteString("- ") + b.WriteString(label) + b.WriteString(": ") + b.WriteString(multicaAcceptanceMarkdownCode(value)) + b.WriteString("\n") +} + +func multicaAcceptanceInlineCodes(values []string) string { + values = multicaAcceptanceCleanStrings(values) + if len(values) == 0 { + return "" + } + out := make([]string, 0, len(values)) + for _, value := range values { + out = append(out, multicaAcceptanceMarkdownCode(value)) + } + return strings.Join(out, ", ") +} + +func multicaAcceptanceMarkdownCode(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "``" + } + return "`" + strings.ReplaceAll(value, "`", "'") + "`" +} + +func multicaAcceptanceCleanStrings(values []string) []string { + out := make([]string, 0, len(values)) + seen := map[string]bool{} + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + seen[value] = true + out = append(out, value) + } + return out +} + +func multicaAcceptancePathSegment(value, fallback string) string { + value = strings.TrimSpace(value) + if value == "" { + value = fallback + } + var b strings.Builder + lastDash := false + for _, r := range value { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' || r == '.' { + b.WriteRune(r) + lastDash = false + continue + } + if !lastDash { + b.WriteByte('-') + lastDash = true + } + } + out := strings.Trim(b.String(), ".-") + if out == "" { + return fallback + } + return out +} + +var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTaskCaseMaterial{ + multicaAcceptanceTaskCaseR2Readiness: func(started time.Time) multicaAcceptanceTaskCaseMaterial { + return multicaAcceptanceTaskCaseMaterial{ + Title: "Mnemon Multica runtime prod-sim " + started.Format("150405"), + Description: multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Request: "Run a small Mnemon R2 Multica readiness drill.", + WorkMode: "Use Mnemon teamwork; Multica shows issues, runs, comments, and statuses.", + Handoffs: []string{ + "Route root session visibility and child issue routing checks to separate teammates.", + "After teammate feedback is visible, route a final integration check.", + }, + Validation: []string{ + "Root issue carries session metadata and shows run activity.", + "Accepted assignments become child issue mailboxes assigned to target agents.", + "Feedback comments and statuses are projected back to Multica.", + "Stale or cross-session assignment material is ignored.", + "Final root status reflects completion.", + }, + Completion: "Finish when child feedback comments are visible and the root issue reaches a terminal status.", + }), + Expectations: multicaAcceptanceTaskCaseExpectations{ + MinActiveAgents: 3, + MinChildMailboxes: 2, + MinFeedbackComments: 2, + TeamworkRounds: []string{ + "Round 1: root issue intake and first assignment split", + "Round 2: child mailbox feedback", + "Round 3: integration and final status projection", + }, + }, + } + }, + multicaAcceptanceTaskCaseProtocolReAct: func(started time.Time) multicaAcceptanceTaskCaseMaterial { + return multicaAcceptanceTaskCaseMaterial{ + Title: "Protocol ReAct collaboration drill " + started.Format("150405"), + Description: multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Request: "Run a complex Mnemon-on-Multica collaboration drill that forces observe-act-reflect cycles across assignment routing, mailbox correlation, feedback projection, and integration. Treat the task as an operator-facing acceptance investigation, not a simple happy-path flow check.", + WorkMode: "Use Multica as the visible teamwork hub. Mnemon should create child assignment mailboxes, collect teammate feedback, integrate results, and create at least one follow-up slice when the first round leaves an unresolved risk.", + Handoffs: []string{ + "Round 1 - Observe: assign separate teammates to inspect root session metadata, child mailbox routing, and runtime run visibility. Each teammate must report concrete issue IDs, agent IDs, statuses, and evidence.", + "Round 2 - Act: the planner/integrator must read first-round feedback, identify the highest-risk gap, and create a follow-up assignment for a different teammate to verify or falsify that gap.", + "Round 3 - Reflect: after the follow-up result appears, the integrator must reconcile all feedback into a final decision that names residual risks, owner, and next validation step.", + }, + Validation: []string{ + "At least three child assignment mailboxes are created across the initial and follow-up rounds.", + "At least four distinct Multica agents participate through root or child runs.", + "Feedback comments include evidence from both the initial observation round and the follow-up action round.", + "The final root result distinguishes observed facts, actions taken, reflection, and any remaining risk.", + "Machine-only protocol fields remain in metadata or markers, not in visible child issue text.", + }, + Completion: "Finish only after the follow-up assignment has feedback and the root issue contains an integrated ReAct-style conclusion: observations, actions, reflection, decision, and next validation step.", + }), + Expectations: multicaAcceptanceTaskCaseExpectations{ + MinActiveAgents: 4, + MinChildMailboxes: 3, + MinFeedbackComments: 3, + TeamworkRounds: []string{ + "Round 1 - Observe: split root metadata, routing, and run-visibility checks", + "Round 2 - Act: create a follow-up assignment for the highest-risk gap", + "Round 3 - Reflect: integrate first-round and follow-up feedback into a decision", + }, + }, + } + }, + multicaAcceptanceTaskCaseParallelPoc: func(started time.Time) multicaAcceptanceTaskCaseMaterial { + return multicaAcceptanceTaskCaseMaterial{ + Title: "Parallel PoC overlap drill " + started.Format("150405"), + Description: multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Request: "Run a production-like Mnemon-on-Multica collaboration case with three overlapping PoCs running in parallel. The goal is to validate Multica as the primary hub backend for assignment activation, feedback projection, and context reuse across related workstreams.", + WorkMode: "Use Multica root and child issues as the visible teamwork hub. The planner should split the case into parallel child assignment mailboxes, require shared context references in every feedback item, and create at least one follow-up assignment after reading first-round results.", + Handoffs: []string{ + "Round 1 - Observe: launch three parallel PoCs for runtime routing, operator runbook readiness, and release risk. Each PoC must name the shared context it consumed and the evidence it produced.", + "Round 2 - Act: create a follow-up assignment for the highest disagreement or missing evidence across PoCs. The follow-up owner must reuse at least two shared contexts and cite prior child feedback.", + "Round 3 - Reflect: the integrator reconciles all PoC outputs into one operator-facing decision with residual risks, context reuse evidence, and the next validation signal.", + }, + Validation: []string{ + "At least three initial child assignment mailboxes are created, followed by at least one follow-up mailbox after first-round feedback is visible.", + "At least five distinct Multica agents participate through root or child runs when a five-agent registry is available.", + "Every PoC feedback comment references a shared context and an evidence artifact, not only a local conclusion.", + "At least one shared context is reused by two or more PoCs, and the final integration comment names that reuse explicitly.", + "Machine routing, dedupe, session, and assignment fields remain in Multica metadata or stable markers rather than visible issue prose.", + }, + Completion: "Finish only after the follow-up assignment feedback is visible, all three PoCs have result or blocker comments, and the root issue records an integrated decision that explains context reuse.", + }), + Expectations: multicaAcceptanceTaskCaseExpectations{ + MinActiveAgents: 5, + MinChildMailboxes: 4, + MinFeedbackComments: 4, + TeamworkRounds: []string{ + "Round 1 - Observe: three parallel PoCs split runtime, runbook, and release risk", + "Round 2 - Act: follow up on disagreement or missing evidence across PoCs", + "Round 3 - Reflect: integrate reused context, final decision, and residual risks", + }, + }, + Workstreams: []multicaAcceptanceWorkstream{ + { + ID: "poc-runtime-routing", + Title: "Runtime routing and assignment mailbox correlation", + PrimaryRoles: []string{"planner@team", "researcher@team", "implementer@team"}, + SharedContextRefs: []string{"session-map", "mailbox-contract", "evidence-ledger"}, + ExpectedArtifacts: []string{"routing-evidence.md", "assignment-mailbox-map.json", "runtime-run-summary.md"}, + }, + { + ID: "poc-operator-runbook", + Title: "Operator runbook and rollback readiness", + PrimaryRoles: []string{"implementer@team", "reviewer@team"}, + SharedContextRefs: []string{"mailbox-contract", "risk-register", "evidence-ledger"}, + ExpectedArtifacts: []string{"runbook-gap-list.md", "rollback-checklist.md", "operator-risk-notes.md"}, + }, + { + ID: "poc-release-risk", + Title: "Release decision and product status projection", + PrimaryRoles: []string{"researcher@team", "reviewer@team", "integrator@team"}, + SharedContextRefs: []string{"session-map", "risk-register", "evidence-ledger"}, + ExpectedArtifacts: []string{"release-risk-matrix.md", "status-projection-evidence.md", "ship-hold-decision.md"}, + }, + }, + Roles: []multicaAcceptanceRolePlan{ + { + Role: "planner", + Principal: "planner@team", + Primary: []string{"poc-runtime-routing"}, + Overlaps: []string{"poc-release-risk"}, + Responsibilities: []string{ + "Seed the root teamwork signal and create the three first-round assignment mailboxes.", + "Read first-round feedback before creating the follow-up assignment.", + }, + }, + { + Role: "researcher", + Principal: "researcher@team", + Primary: []string{"poc-runtime-routing"}, + Overlaps: []string{"poc-release-risk"}, + Responsibilities: []string{ + "Trace runtime routing evidence and reuse the session map when judging release risk.", + "Report concrete issue IDs, agent IDs, run status, and evidence refs.", + }, + }, + { + Role: "implementer", + Principal: "implementer@team", + Primary: []string{"poc-operator-runbook"}, + Overlaps: []string{"poc-runtime-routing"}, + Responsibilities: []string{ + "Verify the mailbox contract against the operator runbook and runtime behavior.", + "Name the smallest code or runbook change that would reduce operator ambiguity.", + }, + }, + { + Role: "reviewer", + Principal: "reviewer@team", + Primary: []string{"poc-release-risk"}, + Overlaps: []string{"poc-operator-runbook"}, + Responsibilities: []string{ + "Challenge release readiness with rollback and status-projection evidence.", + "Identify disagreement between PoC outputs before final integration.", + }, + }, + { + Role: "integrator", + Principal: "integrator@team", + Primary: []string{"poc-release-risk"}, + Overlaps: []string{"poc-runtime-routing", "poc-operator-runbook"}, + Responsibilities: []string{ + "Consume all PoC outputs and the follow-up result.", + "Write the final root decision with context reuse evidence and residual risk.", + }, + }, + }, + SharedContexts: []multicaAcceptanceSharedContext{ + { + ID: "session-map", + UsedBy: []string{"poc-runtime-routing", "poc-release-risk"}, + Purpose: "Root issue, session mailbox, child issue, and agent run map used to correlate visible Multica artifacts.", + }, + { + ID: "mailbox-contract", + UsedBy: []string{"poc-runtime-routing", "poc-operator-runbook"}, + Purpose: "Human-readable assignment mailbox contract plus hidden metadata boundary for routing, dedupe, and correlation.", + }, + { + ID: "risk-register", + UsedBy: []string{"poc-operator-runbook", "poc-release-risk"}, + Purpose: "Shared release and operator risks with owner, severity, mitigation, and validation signal.", + }, + { + ID: "evidence-ledger", + UsedBy: []string{"poc-runtime-routing", "poc-operator-runbook", "poc-release-risk"}, + Purpose: "Cross-PoC evidence index for issue IDs, run IDs, comments, statuses, and artifacts.", + }, + }, + RoleOverlaps: []string{ + "researcher@team carries runtime routing evidence into poc-release-risk through session-map.", + "implementer@team reuses mailbox-contract across poc-runtime-routing and poc-operator-runbook.", + "reviewer@team connects rollback concerns from poc-operator-runbook to poc-release-risk.", + "integrator@team consumes all PoCs but must wait for follow-up feedback before closing the root issue.", + }, + ContextReuseChecks: []string{ + "Every first-round feedback comment names at least one shared context and one evidence artifact.", + "The follow-up assignment cites two prior child comments or artifacts before adding new work.", + "The final root comment names which shared contexts were reused and where disagreement was resolved.", + "No visible issue text should expose session ids, assignment ids, assignment fingerprints, or projection-owner keys.", + }, + } + }, + multicaAcceptanceTaskCaseReleaseReadiness: func(started time.Time) multicaAcceptanceTaskCaseMaterial { + return multicaAcceptanceTaskCaseMaterial{ + Title: "Release readiness handoff " + started.Format("150405"), + Description: multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Request: "Prepare a release readiness decision for a Mnemon Multica runtime update that affects assignment routing, mailbox metadata, and visible status projection.", + WorkMode: "Use Multica as the visible coordination hub while Mnemon keeps the canonical event state.", + Handoffs: []string{ + "Ask one teammate to verify release risk: registry coverage, runtime activation evidence, and stale-session isolation.", + "Ask another teammate to verify the operator path: rollback notes, status transitions, and what should be checked before enabling the update.", + "Have the integrator decide ship, hold, or ship-with-follow-up based on teammate feedback.", + }, + Validation: []string{ + "Release decision references concrete root and child issue evidence.", + "Assignment mailboxes route to the intended Multica agents.", + "Feedback comments distinguish release blockers from ordinary progress.", + "Final status makes the release decision visible without exposing machine-only protocol fields.", + }, + Completion: "Finish when the root issue contains an explicit release decision and every child mailbox has result or blocker feedback.", + }), + Expectations: multicaAcceptanceTaskCaseExpectations{ + MinActiveAgents: 4, + MinChildMailboxes: 3, + MinFeedbackComments: 3, + TeamworkRounds: []string{ + "Round 1: risk and operator-path review", + "Round 2: release decision follow-up for any hold risk", + "Round 3: final ship/hold integration", + }, + }, + } + }, + multicaAcceptanceTaskCaseIncidentTriage: func(started time.Time) multicaAcceptanceTaskCaseMaterial { + return multicaAcceptanceTaskCaseMaterial{ + Title: "Runtime regression triage " + started.Format("150405"), + Description: multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Request: "Triage a production-style regression report where Multica child assignment mailboxes may be created but feedback comments or completion statuses appear late.", + WorkMode: "Use Mnemon teamwork to split diagnosis, mitigation, and verification while Multica remains the operator-facing hub.", + Handoffs: []string{ + "Assign one teammate to inspect routing evidence and identify whether assignment metadata is complete enough for correlation.", + "Assign one teammate to inspect feedback projection and status mapping for delayed or missing updates.", + "Ask the integrator to propose the smallest mitigation and the follow-up acceptance signal needed before closing the incident.", + }, + Validation: []string{ + "The triage identifies whether the problem is intake, routing, wake, feedback projection, or Multica run visibility.", + "Each child mailbox reports evidence with issue IDs, agent IDs, and observed status.", + "The root issue records a mitigation decision rather than only stating that the flow completed.", + }, + Completion: "Finish when the root issue has a triage decision, mitigation summary, and clear next verification step.", + }), + Expectations: multicaAcceptanceTaskCaseExpectations{ + MinActiveAgents: 4, + MinChildMailboxes: 3, + MinFeedbackComments: 3, + TeamworkRounds: []string{ + "Round 1: diagnose intake, routing, and feedback projection", + "Round 2: act on the leading failure mode with a mitigation check", + "Round 3: reflect into incident decision and next verification", + }, + }, + } + }, + multicaAcceptanceTaskCaseRunbookReview: func(started time.Time) multicaAcceptanceTaskCaseMaterial { + return multicaAcceptanceTaskCaseMaterial{ + Title: "Operator runbook review " + started.Format("150405"), + Description: multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Request: "Review the operator runbook for installing, provisioning, and validating the Mnemon Multica runtime in a workspace with multiple participant agents.", + WorkMode: "Use Multica child issues to split documentation review, command validation, and operator risk notes.", + Handoffs: []string{ + "Ask one teammate to verify the install/provisioning commands and identify missing prerequisites.", + "Ask another teammate to verify the validation section covers root metadata, child mailbox routing, feedback comments, and final statuses.", + "Have the integrator produce a concise runbook change list with priority and owner.", + }, + Validation: []string{ + "Runbook findings are tied to concrete operator actions, not generic flow checks.", + "The review covers both successful operation and recovery from missing metadata or delayed run messages.", + "Final feedback identifies documentation changes that can be applied without changing runtime semantics.", + }, + Completion: "Finish when each review slice has feedback and the root issue contains a prioritized runbook update list.", + }), + Expectations: multicaAcceptanceTaskCaseExpectations{ + MinActiveAgents: 4, + MinChildMailboxes: 3, + MinFeedbackComments: 3, + TeamworkRounds: []string{ + "Round 1: install, validation, and risk review", + "Round 2: follow-up on the most ambiguous runbook step", + "Round 3: integrate prioritized documentation changes", + }, + }, + } + }, +} diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go new file mode 100644 index 00000000..6be534cc --- /dev/null +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go @@ -0,0 +1,208 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/driver" +) + +func TestMulticaTaskCaseProtocolReActIsMultiRound(t *testing.T) { + started := time.Date(2026, 6, 30, 9, 10, 11, 0, time.UTC) + material, err := multicaAcceptanceTaskCase(multicaAcceptanceTaskCaseProtocolReAct, started) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "Protocol ReAct collaboration drill", + "Round 1 - Observe", + "Round 2 - Act", + "Round 3 - Reflect", + "follow-up assignment", + } { + if !strings.Contains(material.Title+"\n"+material.Description, want) { + t.Fatalf("task case missing %q:\n%s\n%s", want, material.Title, material.Description) + } + } + if material.Expectations.MinActiveAgents != 4 || + material.Expectations.MinChildMailboxes != 3 || + material.Expectations.MinFeedbackComments != 3 || + len(material.Expectations.TeamworkRounds) != 3 { + t.Fatalf("unexpected expectations: %+v", material.Expectations) + } +} + +func TestMulticaTaskCaseParallelPocMaterializesIsolatedOverlapPlan(t *testing.T) { + started := time.Date(2026, 6, 30, 9, 10, 11, 0, time.UTC) + material, err := multicaAcceptanceTaskCase(multicaAcceptanceTaskCaseParallelPoc, started) + if err != nil { + t.Fatal(err) + } + runRoot := t.TempDir() + plan, err := materializeMulticaAcceptanceExecutionPlan(runRoot, material) + if err != nil { + t.Fatal(err) + } + if len(plan.Workstreams) != 3 || len(plan.Roles) != 5 || len(plan.SharedContexts) < 4 { + t.Fatalf("unexpected plan shape: %+v", plan) + } + for _, dir := range append([]string{plan.CaseRoot, plan.SharedContextDir, plan.EvidenceDir}, multicaPlanDirs(plan)...) { + info, err := os.Stat(dir) + if err != nil { + t.Fatalf("missing plan dir %s: %v", dir, err) + } + if !info.IsDir() { + t.Fatalf("plan path is not a dir: %s", dir) + } + rel, err := filepath.Rel(runRoot, dir) + if err != nil { + t.Fatal(err) + } + if strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) { + t.Fatalf("plan dir escaped run root: root=%s dir=%s rel=%s", runRoot, dir, rel) + } + } + reused := false + for _, shared := range plan.SharedContexts { + if len(shared.UsedBy) > 1 { + reused = true + break + } + } + if !reused { + t.Fatalf("expected at least one shared context reused by multiple PoCs: %+v", plan.SharedContexts) + } + rendered := renderMulticaAcceptanceExecutionPlan(plan) + for _, want := range []string{ + "Parallel PoCs", + "Context Reuse Checks", + "poc-runtime-routing", + "poc-operator-runbook", + "poc-release-risk", + "evidence-ledger", + } { + if !strings.Contains(rendered, want) { + t.Fatalf("rendered plan missing %q:\n%s", want, rendered) + } + } +} + +func TestMulticaRuntimeProdSimTaskCaseWritesExecutionPlanToIssue(t *testing.T) { + tmp := t.TempDir() + registryPath := filepath.Join(tmp, "registry.json") + if err := driver.SaveMulticaRegistry(registryPath, driver.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-1", + RuntimeProfileID: "profile-1", + RuntimeID: "runtime-1", + Participants: []driver.MulticaParticipantRecord{{ + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-1", + Role: "planner", + }}, + }); err != nil { + t.Fatal(err) + } + argsPath := filepath.Join(tmp, "args.txt") + stdinPath := filepath.Join(tmp, "stdin.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +cat >> "$MULTICA_STDIN_PATH" +case "$*" in + *"issue create"*) printf '{"id":"iss-poc","identifier":"TEA-50","title":"Parallel PoC overlap drill","description":"Teamwork acceptance","status":"todo"}\n' ;; + *"issue runs iss-poc"*) printf '[{"id":"task-poc","issue_id":"iss-poc","agent_id":"agent-1","status":"completed","completed_at":"2026-06-30T09:00:00Z","workspace_id":"ws-1"}]\n' ;; + *"issue run-messages task-poc"*) printf '[{"task_id":"task-poc","issue_id":"iss-poc","seq":1,"type":"assistant","content":"Mnemon Multica runtime handled issue TEA-50. Mnemon ingest: recorded.","created_at":"2026-06-30T09:00:01Z"}]\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("MULTICA_ARGS_PATH", argsPath) + t.Setenv("MULTICA_STDIN_PATH", stdinPath) + + report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ + RunRoot: filepath.Join(tmp, ".testdata", "multica-parallel-poc"), + MulticaBin: bin, + WorkspaceID: "ws-1", + RegistryPath: registryPath, + AssigneePrincipal: "planner@team", + TaskCase: multicaAcceptanceTaskCaseParallelPoc, + Wait: time.Millisecond, + Poll: time.Millisecond, + RequireIngest: true, + }) + if err != nil { + t.Fatalf("acceptance: %v report=%+v", err, report) + } + if report.TaskCase != multicaAcceptanceTaskCaseParallelPoc || + report.TaskExpectations.MinActiveAgents != 5 || + len(report.ExecutionPlan.Workstreams) != 3 { + t.Fatalf("task case report mismatch: %+v", report) + } + args, err := os.ReadFile(argsPath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(args), "issue create --title Parallel PoC overlap drill") { + t.Fatalf("issue title did not come from task case:\n%s", args) + } + stdin, err := os.ReadFile(stdinPath) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "## Execution Plan", + "## Parallel PoCs", + "## Context Reuse Checks", + "poc-runtime-routing", + "poc-operator-runbook", + "poc-release-risk", + } { + if !strings.Contains(string(stdin), want) { + t.Fatalf("issue stdin missing %q:\n%s", want, stdin) + } + } +} + +func TestMulticaRuntimeProdSimRejectsUnknownTaskCase(t *testing.T) { + runRoot := filepath.Join(t.TempDir(), ".testdata", "unknown-task-case") + report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ + RunRoot: runRoot, + TaskCase: "missing-task-case", + Wait: time.Millisecond, + Poll: time.Millisecond, + }) + if err == nil { + t.Fatal("expected unknown task case error") + } + if !strings.Contains(err.Error(), "unknown Multica task case") { + t.Fatalf("unexpected error: %v", err) + } + if report.Status != "failed" || report.ReportPath == "" || !strings.HasPrefix(report.ReportPath, runRoot) { + t.Fatalf("report mismatch: %+v", report) + } + if _, err := os.Stat(report.ReportPath); err != nil { + t.Fatalf("report was not written: %v", err) + } +} + +func multicaPlanDirs(plan multicaAcceptanceExecutionPlan) []string { + var out []string + for _, stream := range plan.Workstreams { + out = append(out, stream.Directory) + } + for _, role := range plan.Roles { + out = append(out, role.Directory) + } + for _, shared := range plan.SharedContexts { + out = append(out, shared.Directory) + } + return out +} From 01505e77d54af006142d7ac2ae0ab4f274987985 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:17:03 +0800 Subject: [PATCH 069/117] test: guard root cli boundary Add a coreguard test that keeps the root mnemon CLI separate from the R2 harness command cluster. The guard scans root cmd sources for harness imports and reserved service command names without changing root CLI behavior. Validation: go test -count=1 ./harness/internal/coreguard; go test -count=1 ./cmd ./harness/internal/coreguard --- .../coreguard/root_cli_boundary_test.go | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 harness/internal/coreguard/root_cli_boundary_test.go diff --git a/harness/internal/coreguard/root_cli_boundary_test.go b/harness/internal/coreguard/root_cli_boundary_test.go new file mode 100644 index 00000000..82fad491 --- /dev/null +++ b/harness/internal/coreguard/root_cli_boundary_test.go @@ -0,0 +1,122 @@ +package coreguard + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +var rootCLIForbiddenCommands = map[string]bool{ + "acceptance": true, + "daemon": true, + "harness": true, + "hub": true, + "mnemond": true, + "mnemon-acceptance": true, + "mnemon-harness": true, + "mnemon-hub": true, + "mnemon-multica-runtime": true, + "mnemon-runtime-multica": true, + "mnemonhub": true, + "multica": true, + "multica-runtime-prod-sim": true, +} + +func TestRootMnemonCLIDoesNotImportHarnessCluster(t *testing.T) { + for _, file := range rootCLIGoFiles(t) { + fset := token.NewFileSet() + parsed, err := parser.ParseFile(fset, file, nil, parser.ImportsOnly) + if err != nil { + t.Fatalf("parse imports %s: %v", file, err) + } + for _, imp := range parsed.Imports { + importPath := strings.Trim(imp.Path.Value, `"`) + if strings.Contains(importPath, "github.com/mnemon-dev/mnemon/harness/") { + t.Errorf("root mnemon CLI imports R2 harness cluster package %q in %s", importPath, file) + } + } + } +} + +func TestRootMnemonCLIDoesNotExposeR2ClusterCommands(t *testing.T) { + for _, file := range rootCLIGoFiles(t) { + fset := token.NewFileSet() + parsed, err := parser.ParseFile(fset, file, nil, 0) + if err != nil { + t.Fatalf("parse file %s: %v", file, err) + } + ast.Inspect(parsed, func(node ast.Node) bool { + kv, ok := node.(*ast.KeyValueExpr) + if !ok { + return true + } + key, ok := kv.Key.(*ast.Ident) + if !ok || key.Name != "Use" { + return true + } + value, ok := kv.Value.(*ast.BasicLit) + if !ok || value.Kind != token.STRING { + return true + } + use, err := strconv.Unquote(value.Value) + if err != nil { + t.Errorf("unquote Use value in %s: %v", file, err) + return true + } + command := firstCommandToken(use) + if rootCLIForbiddenCommands[command] { + t.Errorf("root mnemon CLI exposes R2 cluster command %q in %s; keep it under harness/cmd", command, file) + } + return true + }) + } +} + +func TestRootCLIBoundaryGuardLogicIsNotVacuous(t *testing.T) { + for _, command := range []string{"mnemond", "mnemon-hub", "multica", "acceptance"} { + if !rootCLIForbiddenCommands[command] { + t.Fatalf("root CLI boundary guard must forbid %q", command) + } + } + for _, command := range []string{"remember", "recall", "search", "setup"} { + if rootCLIForbiddenCommands[command] { + t.Fatalf("root CLI boundary guard must allow existing memory command %q", command) + } + } +} + +func rootCLIGoFiles(t *testing.T) []string { + t.Helper() + root := filepath.Join("..", "..", "..", "cmd") + entries, err := os.ReadDir(root) + if err != nil { + t.Fatalf("read root cmd dir: %v", err) + } + var files []string + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") { + files = append(files, filepath.Join(root, name)) + } + } + if len(files) == 0 { + t.Fatalf("root cmd dir has no non-test Go files") + } + return files +} + +func firstCommandToken(use string) string { + use = strings.TrimSpace(use) + if use == "" { + return "" + } + return strings.Fields(use)[0] +} From e075f3aac89a0cbc4967a1928c42184ae580830e Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:24:22 +0800 Subject: [PATCH 070/117] test: guard multica projection writer role Pin the mnemon-multica-runtime hub writer as a projection role. The guard allows read-only presentation access but rejects local ingest, managed drive, product config, and mnemonhub exchange ownership from the projection writer file. Validation: go test -count=1 ./harness/internal/coreguard; go test -count=1 ./harness/internal/coreguard ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance --- .../multica_runtime_role_boundary_test.go | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 harness/internal/coreguard/multica_runtime_role_boundary_test.go diff --git a/harness/internal/coreguard/multica_runtime_role_boundary_test.go b/harness/internal/coreguard/multica_runtime_role_boundary_test.go new file mode 100644 index 00000000..cb198082 --- /dev/null +++ b/harness/internal/coreguard/multica_runtime_role_boundary_test.go @@ -0,0 +1,72 @@ +package coreguard + +import ( + "go/ast" + "go/parser" + "go/token" + "path/filepath" + "strings" + "testing" +) + +var multicaRuntimeProjectionForbiddenImports = []string{ + "harness/internal/app", + "harness/internal/hostagent", + "harness/internal/mnemond/admission", + "harness/internal/mnemond/state", + "harness/internal/mnemonhub", + "harness/internal/productconfig", + "harness/internal/runtime", +} + +func TestMulticaRuntimeProjectionWriterDoesNotOwnIngestOrDrive(t *testing.T) { + path := filepath.Join("..", "..", "cmd", "mnemon-multica-runtime", "hub_writer.go") + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + for _, imp := range file.Imports { + importPath := strings.Trim(imp.Path.Value, `"`) + if roleImportForbidden(importPath, multicaRuntimeProjectionForbiddenImports) { + t.Errorf("Multica hub projection writer imports forbidden package %q; projection may read views and write Multica artifacts, but must not own local ingest, drive, product config, or hub exchange", importPath) + } + } + ast.Inspect(file, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.SelectorExpr: + switch n.Sel.Name { + case "IngestObserve", "IngestObservedEnvelope", "Observe", "Wake": + t.Errorf("Multica hub projection writer calls %s at %s; projection must not ingest governed events or drive managed turns", n.Sel.Name, fset.Position(n.Pos())) + } + case *ast.CompositeLit: + if selectorName(n.Type) == "ManagedAgentDriver" { + t.Errorf("Multica hub projection writer constructs ManagedAgentDriver at %s; managed drive belongs outside projection", fset.Position(n.Pos())) + } + } + return true + }) +} + +func TestMulticaRuntimeRoleBoundaryGuardLogicIsNotVacuous(t *testing.T) { + if !roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemonhub", multicaRuntimeProjectionForbiddenImports) { + t.Fatal("Multica projection writer guard must flag mnemonhub imports") + } + if roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/access", multicaRuntimeProjectionForbiddenImports) { + t.Fatal("Multica projection writer guard must allow read-only mnemond access") + } + if selectorName(&ast.SelectorExpr{Sel: ast.NewIdent("ManagedAgentDriver")}) != "ManagedAgentDriver" { + t.Fatal("selectorName helper must identify selector names") + } +} + +func selectorName(expr ast.Expr) string { + switch e := expr.(type) { + case *ast.Ident: + return e.Name + case *ast.SelectorExpr: + return e.Sel.Name + default: + return "" + } +} From 7636d07f372181cf7f8289d6e1614685d17c7dee Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:28:55 +0800 Subject: [PATCH 071/117] test: gate multica provisioning behind acceptance Keep five-participant Multica provisioning as a hidden mnemon-acceptance bridge instead of a directly usable mnemon-harness workflow. The harness command now requires a hidden acceptance bridge flag, and acceptance passes that flag explicitly. Validation: go test -count=1 ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-acceptance ./harness/internal/coreguard; go test -count=1 ./... --- .../acceptance_multica_provision.go | 2 +- .../acceptance_multica_provision_test.go | 3 ++- harness/cmd/mnemon-harness/multica.go | 6 ++++++ harness/cmd/mnemon-harness/multica_test.go | 17 +++++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go index 880777f4..6693f015 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go @@ -91,7 +91,7 @@ func buildAcceptanceMulticaProvisionArgs() []string { args = appendFlag(args, "--multica-profile", acceptanceMulticaProvisionProfile) args = appendFlag(args, "--multica-server-url", acceptanceMulticaProvisionServerURL) args = appendFlag(args, "--multica-workspace-id", acceptanceMulticaProvisionWorkspaceID) - args = append(args, "--json", "provision") + args = append(args, "--json", "provision", "--acceptance-bridge") args = appendFlag(args, "--registry", acceptanceMulticaProvisionRegistry) args = appendFlag(args, "--project-root", acceptanceMulticaProvisionProjectRoot) args = appendFlag(args, "--runtime-profile-name", acceptanceMulticaProvisionProfileName) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_provision_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_provision_test.go index 1692fa44..171f3d96 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_provision_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_provision_test.go @@ -45,6 +45,7 @@ func TestMulticaProvisionAcceptanceBuildsHiddenHarnessBridge(t *testing.T) { "multica", "--json", "provision", + "--acceptance-bridge", "--multica-workspace-id", "ws-1", "--runtime-command", @@ -90,7 +91,7 @@ func TestMulticaProvisionAcceptanceRunsHarnessBridge(t *testing.T) { if gotCommand != "/tmp/mnemon-harness" { t.Fatalf("command = %q", gotCommand) } - if !slices.Contains(gotArgs, "provision") || !slices.Contains(gotArgs, "ws-acceptance") { + if !slices.Contains(gotArgs, "provision") || !slices.Contains(gotArgs, "--acceptance-bridge") || !slices.Contains(gotArgs, "ws-acceptance") { t.Fatalf("hidden harness provision args not propagated: %v", gotArgs) } } diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 900cf1e4..988b5880 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -62,6 +62,7 @@ var ( multicaProvisionManagedCommand string multicaProvisionManagedWorkspace string multicaProvisionManagedTimeout time.Duration + multicaProvisionAcceptanceBridge bool multicaParticipantRegistry string multicaParticipantProjectRoot string @@ -372,6 +373,9 @@ func runMulticaParticipantRegister(cmd *cobra.Command, args []string) error { } func runMulticaProvision(cmd *cobra.Command, args []string) error { + if !multicaProvisionAcceptanceBridge { + return fmt.Errorf("multica provision is test-only; use mnemon-acceptance multica-provision") + } ctx := multicaCommandContext(cmd) workspaceID := strings.TrimSpace(multicaWorkspaceID) if workspaceID == "" { @@ -1001,6 +1005,8 @@ func init() { multicaProvisionCmd.Flags().StringVar(&multicaProvisionManagedCommand, "managed-command", envDefault("MNEMON_MANAGED_COMMAND", ""), "managed runtime command injected into participant env") multicaProvisionCmd.Flags().StringVar(&multicaProvisionManagedWorkspace, "managed-workspace", envDefault("MNEMON_MANAGED_WORKSPACE", ""), "managed runtime workspace injected into participant env") multicaProvisionCmd.Flags().DurationVar(&multicaProvisionManagedTimeout, "managed-turn-timeout", 0, "managed runtime turn timeout injected into participant env") + multicaProvisionCmd.Flags().BoolVar(&multicaProvisionAcceptanceBridge, "acceptance-bridge", false, "allow mnemon-acceptance to invoke hidden Multica provisioning bridge") + _ = multicaProvisionCmd.Flags().MarkHidden("acceptance-bridge") multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantRegistry, "registry", "", "Multica registry path") multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantProjectRoot, "project-root", ".", "project root for the default registry path") diff --git a/harness/cmd/mnemon-harness/multica_test.go b/harness/cmd/mnemon-harness/multica_test.go index 65065357..07cf4453 100644 --- a/harness/cmd/mnemon-harness/multica_test.go +++ b/harness/cmd/mnemon-harness/multica_test.go @@ -226,6 +226,7 @@ esac multicaProvisionHarnessBin = "/abs/mnemon-harness" multicaProvisionManagedRuntime = "noop" multicaProvisionManagedWorkspace = tmp + multicaProvisionAcceptanceBridge = true multicaJSON = true t.Setenv("MULTICA_ENV_STDIN_PATH", envStdinPath) @@ -284,6 +285,19 @@ esac } } +func TestMulticaProvisionRejectsDirectHarnessUse(t *testing.T) { + restoreMulticaFlags(t) + + multicaProvisionAcceptanceBridge = false + err := runMulticaProvision(multicaProvisionCmd, nil) + if err == nil { + t.Fatal("direct hidden harness provision should be rejected") + } + if !strings.Contains(err.Error(), "mnemon-acceptance multica-provision") { + t.Fatalf("unexpected direct provision error: %v", err) + } +} + func TestMergeMulticaParticipantRuntimeEnvPrunesStaleManagedKeys(t *testing.T) { merged := mergeMulticaParticipantRuntimeEnv(map[string]string{ "MNEMON_CONTROL_TOKEN": "old-token", @@ -353,6 +367,7 @@ func restoreMulticaFlags(t *testing.T) { oldProvisionManagedCommand := multicaProvisionManagedCommand oldProvisionManagedWorkspace := multicaProvisionManagedWorkspace oldProvisionManagedTimeout := multicaProvisionManagedTimeout + oldProvisionAcceptanceBridge := multicaProvisionAcceptanceBridge oldParticipantRegistry := multicaParticipantRegistry oldParticipantProjectRoot := multicaParticipantProjectRoot oldParticipantAgentID := multicaParticipantAgentID @@ -410,6 +425,7 @@ func restoreMulticaFlags(t *testing.T) { multicaProvisionManagedCommand = oldProvisionManagedCommand multicaProvisionManagedWorkspace = oldProvisionManagedWorkspace multicaProvisionManagedTimeout = oldProvisionManagedTimeout + multicaProvisionAcceptanceBridge = oldProvisionAcceptanceBridge multicaParticipantRegistry = oldParticipantRegistry multicaParticipantProjectRoot = oldParticipantProjectRoot multicaParticipantAgentID = oldParticipantAgentID @@ -465,6 +481,7 @@ func restoreMulticaFlags(t *testing.T) { multicaProvisionManagedCommand = "" multicaProvisionManagedWorkspace = "" multicaProvisionManagedTimeout = 0 + multicaProvisionAcceptanceBridge = false multicaParticipantRegistry = "" multicaParticipantProjectRoot = "." multicaParticipantAgentID = "" From 9c287b8f47ba402e321cbb5cccb29fcfbeeb8b6d Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:32:23 +0800 Subject: [PATCH 072/117] fix: keep mnemonhub out of projection surfaces Treat mnemonhub as the remote exchange backend in product config. Connecting or bridging mnemonhub now records the connection and daemon watcher role, but validation rejects using mnemonhub as a projection surface. Validation: go test -count=1 ./harness/internal/productconfig ./harness/cmd/mnemon-harness; go test -count=1 ./harness/internal/coreguard ./harness/cmd/mnemon-acceptance ./harness/internal/daemon; go test -count=1 ./... --- .../cmd/mnemon-harness/config_daemon_test.go | 5 ++++ harness/cmd/mnemon-harness/connect.go | 1 - harness/internal/productconfig/config.go | 24 +++++++++++++++++-- harness/internal/productconfig/config_test.go | 14 +++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index 6066437d..c396964b 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -348,10 +348,15 @@ func TestConnectCommandsWriteProductConfig(t *testing.T) { if !containsString(cfg.Daemon.InteractionWatchers, want) { t.Fatalf("interaction watcher %q missing: %+v", want, cfg.Daemon.InteractionWatchers) } + } + for _, want := range []string{productconfig.ConnectionMultica, productconfig.ConnectionGitHub} { if !containsString(cfg.Daemon.ProjectionSurfaces, want) { t.Fatalf("projection surface %q missing: %+v", want, cfg.Daemon.ProjectionSurfaces) } } + if containsString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionMnemonhub) { + t.Fatalf("mnemonhub must remain an exchange backend, not projection surface: %+v", cfg.Daemon.ProjectionSurfaces) + } if got := cfg.Sessions.PrimaryActivationCarrier; got != productconfig.ConnectionMultica { t.Fatalf("unexpected primary activation carrier: %q", got) } diff --git a/harness/cmd/mnemon-harness/connect.go b/harness/cmd/mnemon-harness/connect.go index 83bb8732..68cd3541 100644 --- a/harness/cmd/mnemon-harness/connect.go +++ b/harness/cmd/mnemon-harness/connect.go @@ -120,7 +120,6 @@ func runConnectMnemonhub(cmd *cobra.Command, args []string) error { Endpoint: strings.TrimSpace(connectMnemonhubURL), } cfg.Daemon.InteractionWatchers = appendUniqueString(cfg.Daemon.InteractionWatchers, productconfig.ConnectionMnemonhub) - cfg.Daemon.ProjectionSurfaces = appendUniqueString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionMnemonhub) path, err := saveHarnessProductConfig(connectRoot, connectConfigPath, cfg) if err != nil { return err diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go index b444450f..98337c76 100644 --- a/harness/internal/productconfig/config.go +++ b/harness/internal/productconfig/config.go @@ -196,7 +196,7 @@ func (cfg Config) Validate() error { if err := validateDaemonCarriers("interaction watcher", cfg.Daemon.InteractionWatchers, cfg); err != nil { return err } - if err := validateDaemonCarriers("projection surface", cfg.Daemon.ProjectionSurfaces, cfg); err != nil { + if err := validateProjectionSurfaces(cfg.Daemon.ProjectionSurfaces, cfg); err != nil { return err } if err := validateDaemonDriveSources(cfg.Daemon.DriveSources); err != nil { @@ -223,6 +223,27 @@ func validateDaemonCarriers(label string, values []string, cfg Config) error { return nil } +func validateProjectionSurfaces(values []string, cfg Config) error { + seen := map[string]bool{} + for _, value := range values { + surface := strings.TrimSpace(value) + if surface == "" { + return fmt.Errorf("projection surface cannot be empty") + } + if surface == ConnectionMnemonhub { + return fmt.Errorf("projection surface %q is unsupported; mnemonhub is a remote exchange backend", surface) + } + if err := validateCarrier("projection surface", surface, cfg); err != nil { + return err + } + if seen[surface] { + return fmt.Errorf("duplicate projection surface %q", surface) + } + seen[surface] = true + } + return nil +} + func validateDaemonDriveSources(values []string) error { seen := map[string]bool{} for _, source := range values { @@ -342,7 +363,6 @@ func FromLegacy(root string) (Config, bool, error) { } if cfg.Connections.Mnemonhub.Enabled { cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionMnemonhub) - cfg.Daemon.ProjectionSurfaces = appendCarrier(cfg.Daemon.ProjectionSurfaces, ConnectionMnemonhub) } if cfg.Connections.GitHub.Enabled { cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionGitHub) diff --git a/harness/internal/productconfig/config_test.go b/harness/internal/productconfig/config_test.go index 1f295558..f147b041 100644 --- a/harness/internal/productconfig/config_test.go +++ b/harness/internal/productconfig/config_test.go @@ -56,6 +56,14 @@ func TestConfigValidateRejectsCrossLayerLeaks(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "not enabled") { t.Fatalf("expected disabled projection surface error, got %v", err) } + + cfg = Default() + cfg.Connections.Mnemonhub = MnemonhubConnection{Enabled: true, Endpoint: "https://hub.example"} + cfg.Daemon.ProjectionSurfaces = []string{ConnectionMnemonhub} + err = cfg.Validate() + if err == nil || !strings.Contains(err.Error(), "remote exchange backend") { + t.Fatalf("expected mnemonhub projection surface error, got %v", err) + } } func TestConfigValidateRejectsDuplicateParticipants(t *testing.T) { @@ -174,6 +182,12 @@ func TestFromLegacyBridgesLocalAndRemoteConfigs(t *testing.T) { if !cfg.Connections.Mnemonhub.Enabled || cfg.Connections.Mnemonhub.Endpoint != "https://hub.example" { t.Fatalf("mnemonhub bridge mismatch: %+v", cfg.Connections.Mnemonhub) } + if !stringSliceContains(cfg.Daemon.InteractionWatchers, ConnectionMnemonhub) { + t.Fatalf("mnemonhub watcher missing: %+v", cfg.Daemon.InteractionWatchers) + } + if stringSliceContains(cfg.Daemon.ProjectionSurfaces, ConnectionMnemonhub) { + t.Fatalf("mnemonhub must not be bridged as a projection surface: %+v", cfg.Daemon.ProjectionSurfaces) + } if len(cfg.Daemon.DriveSources) != 1 || cfg.Daemon.DriveSources[0] != DriveManagedLocal { t.Fatalf("drive sources mismatch: %+v", cfg.Daemon.DriveSources) } From d688cf2e791400b40605fd295c60bbc28e04e51b Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:36:19 +0800 Subject: [PATCH 073/117] fix: keep mnemonhub out of activation carriers Reject mnemonhub as primary_activation_carrier while preserving it as an interaction watcher for remote exchange sync. Add config and connect-command coverage so mnemonhub cannot drift into activation or projection roles. Validation: go test -count=1 ./harness/internal/productconfig ./harness/cmd/mnemon-harness; go test -count=1 ./... --- .../cmd/mnemon-harness/config_daemon_test.go | 32 +++++++++++++++++++ harness/internal/productconfig/config.go | 13 +++++++- harness/internal/productconfig/config_test.go | 8 +++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index c396964b..0f975580 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -362,6 +362,38 @@ func TestConnectCommandsWriteProductConfig(t *testing.T) { } } +func TestConnectMnemonhubKeepsExchangeOutOfActivationAndProjection(t *testing.T) { + root := t.TempDir() + oldRoot, oldPath := connectRoot, connectConfigPath + oldEndpoint := connectMnemonhubURL + connectRoot = root + connectConfigPath = "" + connectMnemonhubURL = "https://hub.example.invalid" + t.Cleanup(func() { + connectRoot, connectConfigPath = oldRoot, oldPath + connectMnemonhubURL = oldEndpoint + }) + + cmd, _ := testCommand() + if err := runConnectMnemonhub(cmd, nil); err != nil { + t.Fatal(err) + } + + cfg := loadProductConfigForTest(t, root) + if !cfg.Connections.Mnemonhub.Enabled || cfg.Connections.Mnemonhub.Endpoint != "https://hub.example.invalid" { + t.Fatalf("mnemonhub connection not written: %+v", cfg.Connections.Mnemonhub) + } + if !containsString(cfg.Daemon.InteractionWatchers, productconfig.ConnectionMnemonhub) { + t.Fatalf("mnemonhub watcher missing: %+v", cfg.Daemon.InteractionWatchers) + } + if containsString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionMnemonhub) { + t.Fatalf("mnemonhub must remain an exchange backend, not projection surface: %+v", cfg.Daemon.ProjectionSurfaces) + } + if got := cfg.Sessions.PrimaryActivationCarrier; got != "" { + t.Fatalf("mnemonhub must not become primary activation carrier, got %q", got) + } +} + func loadProductConfigForTest(t *testing.T, root string) productconfig.Config { t.Helper() cfg, err := productconfig.Load(filepath.Join(root, productconfig.DefaultRelPath)) diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go index 98337c76..600a3c1d 100644 --- a/harness/internal/productconfig/config.go +++ b/harness/internal/productconfig/config.go @@ -185,7 +185,7 @@ func (cfg Config) Validate() error { return fmt.Errorf("mnemonhub connection endpoint is required when enabled") } } - if err := validateCarrier("primary activation carrier", cfg.Sessions.PrimaryActivationCarrier, cfg); err != nil { + if err := validatePrimaryActivationCarrier(cfg.Sessions.PrimaryActivationCarrier, cfg); err != nil { return err } switch strings.TrimSpace(cfg.Sessions.DuplicateActivationPolicy) { @@ -223,6 +223,17 @@ func validateDaemonCarriers(label string, values []string, cfg Config) error { return nil } +func validatePrimaryActivationCarrier(carrier string, cfg Config) error { + carrier = strings.TrimSpace(carrier) + if carrier == "" { + return nil + } + if carrier == ConnectionMnemonhub { + return fmt.Errorf("primary activation carrier %q is unsupported; mnemonhub is a remote exchange backend", carrier) + } + return validateCarrier("primary activation carrier", carrier, cfg) +} + func validateProjectionSurfaces(values []string, cfg Config) error { seen := map[string]bool{} for _, value := range values { diff --git a/harness/internal/productconfig/config_test.go b/harness/internal/productconfig/config_test.go index f147b041..7a365918 100644 --- a/harness/internal/productconfig/config_test.go +++ b/harness/internal/productconfig/config_test.go @@ -64,6 +64,14 @@ func TestConfigValidateRejectsCrossLayerLeaks(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "remote exchange backend") { t.Fatalf("expected mnemonhub projection surface error, got %v", err) } + + cfg = Default() + cfg.Connections.Mnemonhub = MnemonhubConnection{Enabled: true, Endpoint: "https://hub.example"} + cfg.Sessions.PrimaryActivationCarrier = ConnectionMnemonhub + err = cfg.Validate() + if err == nil || !strings.Contains(err.Error(), "primary activation carrier") || !strings.Contains(err.Error(), "remote exchange backend") { + t.Fatalf("expected mnemonhub primary activation carrier error, got %v", err) + } } func TestConfigValidateRejectsDuplicateParticipants(t *testing.T) { From 140722d4a42d06e7d4c25c88d960b82e1b01a8ab Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:42:56 +0800 Subject: [PATCH 074/117] fix: parse structured multica issue input Preserve Multica turn input as visible text plus structured issue identity so runtime routing can use issue mentions and tags without copying machine metadata into the user-facing message. Keep MULTICA_ISSUE_ID first, then structured input, then visible @TEA tags as fallback. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./... --- harness/cmd/mnemon-multica-runtime/main.go | 10 +- .../cmd/mnemon-multica-runtime/main_test.go | 42 +++++++ .../internal/surface/multica/runtime_items.go | 106 +++++++++++++++++- .../surface/multica/runtime_items_test.go | 71 ++++++++++++ 4 files changed, 222 insertions(+), 7 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 980967d5..f712feb9 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -213,9 +213,9 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er return emitAll(rpcMessage{ID: msg.ID, Result: map[string]any{}}) case "turn/start": s.TurnID = runtimeID("turn", s.now()) - input := multicasurface.RuntimeTextInput(msg.Params) + input := multicasurface.RuntimeInputMaterial(msg.Params) nowMs := s.now().UnixMilli() - userItem := multicasurface.RuntimeUserMessage(input) + userItem := multicasurface.RuntimeUserMessage(input.Text) if err := emitAll( rpcMessage{ ID: msg.ID, @@ -299,14 +299,14 @@ func (s *runtimeRPCState) nextItemID(prefix string) string { return fmt.Sprintf("%s-%d-%d", prefix, s.now().UTC().UnixNano(), s.ItemSeq) } -func (s *runtimeRPCState) runTurn(input string, progress runtimeProgressSink) string { +func (s *runtimeRPCState) runTurn(input multicasurface.RuntimeInput, progress runtimeProgressSink) string { result := s.importIssue(input, progress) return formatRuntimeFinalAnswer(result) } -func (s *runtimeRPCState) importIssue(input string, progress runtimeProgressSink) runtimeImportResult { +func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progress runtimeProgressSink) runtimeImportResult { taskID := envValue(s.Env, "MULTICA_TASK_ID") - issueID := firstNonEmpty(envValue(s.Env, "MULTICA_ISSUE_ID"), multicasurface.ExtractIssueIdentity(input)) + issueID := firstNonEmpty(envValue(s.Env, "MULTICA_ISSUE_ID"), input.IssueIdentity, multicasurface.ExtractIssueIdentity(input.Text)) result := runtimeImportResult{ IssueID: issueID, Principal: resolveRuntimePrincipal(s.Env, s.CWD), diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 9e845f36..3d5092cc 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -148,6 +148,48 @@ func runtimeTestEnv(values ...string) []string { return append(env, values...) } +func TestRuntimeImportUsesStructuredIssueIdentity(t *testing.T) { + tmp := t.TempDir() + argsPath := filepath.Join(tmp, "multica.args") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue get iss-structured"*) printf '{"id":"iss-structured","identifier":"TEA-77","title":"Structured issue mention","description":"Use structured Multica input metadata instead of prompt text.","status":"todo","priority":"medium"}\n' ;; + *"issue metadata list iss-structured"*) printf '[]\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + state := runtimeRPCState{ + CWD: tmp, + Env: runtimeTestEnv( + "MNEMON_MULTICA_BIN="+bin, + "MULTICA_ARGS_PATH="+argsPath, + "MULTICA_TASK_ID=task-structured", + ), + Now: fixedRuntimeTime, + } + + result := state.importIssue(multicasurface.RuntimeInput{ + Text: "Please use the linked issue, not a copied issue id.", + IssueIdentity: "iss-structured", + }, nil) + + if result.IssueID != "iss-structured" || result.Identifier != "TEA-77" { + t.Fatalf("structured issue import mismatch: %+v", result) + } + if result.Status != "skipped" { + t.Fatalf("runtime without control addr should skip ingest after loading issue, got %+v", result) + } + args := mustReadRuntimeTestFile(t, argsPath) + if !strings.Contains(args, "issue get iss-structured --output json") { + t.Fatalf("structured issue identity was not used for issue get:\n%s", args) + } +} + func TestRuntimeImportsAssignedIssueIntoMnemon(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "multica.args") diff --git a/harness/internal/surface/multica/runtime_items.go b/harness/internal/surface/multica/runtime_items.go index a78959a3..115322eb 100644 --- a/harness/internal/surface/multica/runtime_items.go +++ b/harness/internal/surface/multica/runtime_items.go @@ -22,6 +22,11 @@ type RuntimeCommandExecutionMaterial struct { DurationMs int64 } +type RuntimeInput struct { + Text string + IssueIdentity string +} + func RuntimeManagedTraceMessages(threadID, turnID string, event activationtrace.Event, now time.Time) []RuntimeRPCMessage { switch event.Method { case "item/started": @@ -137,21 +142,36 @@ func RuntimeUserMessage(text string) map[string]any { } func RuntimeTextInput(params map[string]any) string { + return RuntimeInputMaterial(params).Text +} + +func RuntimeInputMaterial(params map[string]any) RuntimeInput { input, ok := params["input"].([]any) if !ok { - return "" + return RuntimeInput{} } var parts []string + structuredIssue := "" + textIssue := "" for _, item := range input { obj, ok := item.(map[string]any) if !ok { continue } + if structuredIssue == "" { + structuredIssue = runtimeStructuredIssueIdentity(obj) + } if text, _ := obj["text"].(string); strings.TrimSpace(text) != "" { parts = append(parts, text) + if textIssue == "" { + textIssue = ExtractIssueIdentity(text) + } } } - return strings.Join(parts, "\n") + return RuntimeInput{ + Text: strings.Join(parts, "\n"), + IssueIdentity: firstNonEmptyString(structuredIssue, textIssue), + } } func RuntimeRef(kind, id string) string { @@ -163,6 +183,88 @@ func RuntimeRef(kind, id string) string { return "multica:" + kind + ":" + id } +func runtimeStructuredIssueIdentity(raw any) string { + switch value := raw.(type) { + case nil: + return "" + case string: + return ExtractIssueIdentity(value) + case []any: + for _, item := range value { + if issue := runtimeStructuredIssueIdentity(item); issue != "" { + return issue + } + } + case map[string]any: + for _, key := range []string{"issue_id", "issueId", "issueID", "target_issue_id", "targetIssueId"} { + if issue := cleanIssueIdentity(anyString(value[key])); issue != "" { + return issue + } + } + for _, key := range []string{"url", "href", "uri", "ref", "reference"} { + if issue := ExtractIssueIdentity(anyString(value[key])); issue != "" { + return issue + } + } + if runtimeMapLooksLikeIssueRef(value) { + for _, key := range []string{"id", "target_id", "targetId", "resource_id", "resourceId"} { + if issue := cleanIssueIdentity(anyString(value[key])); issue != "" { + return issue + } + } + } + for _, key := range []string{"identifier", "issue_identifier", "issueIdentifier", "label", "tag"} { + if issue := runtimeIssueIdentifierTag(anyString(value[key])); issue != "" { + return issue + } + } + for _, key := range []string{"issue", "target", "resource", "mention", "mentions", "entities", "text_elements", "references", "tags"} { + if issue := runtimeStructuredIssueIdentity(value[key]); issue != "" { + return issue + } + } + for _, item := range value { + switch item.(type) { + case map[string]any, []any: + if issue := runtimeStructuredIssueIdentity(item); issue != "" { + return issue + } + } + } + } + return "" +} + +func runtimeMapLooksLikeIssueRef(value map[string]any) bool { + for _, key := range []string{"type", "kind", "resource", "resource_type", "resourceType", "entity", "entity_type", "entityType", "target_type", "targetType"} { + if strings.Contains(strings.ToLower(anyString(value[key])), "issue") { + return true + } + } + if _, ok := value["issue"]; ok { + return true + } + if issue := runtimeIssueIdentifierTag(anyString(value["identifier"])); issue != "" { + return true + } + return false +} + +func runtimeIssueIdentifierTag(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + if issue := ExtractIssueIdentity(value); issue != "" { + return issue + } + value = strings.TrimLeft(value, "@#") + if value == "" { + return "" + } + return ExtractIssueIdentity("@" + value) +} + func runtimeManagedTraceItem(event activationtrace.Event) map[string]any { if len(event.Item) == 0 { return nil diff --git a/harness/internal/surface/multica/runtime_items_test.go b/harness/internal/surface/multica/runtime_items_test.go index e141cdbb..e985e7cd 100644 --- a/harness/internal/surface/multica/runtime_items_test.go +++ b/harness/internal/surface/multica/runtime_items_test.go @@ -142,6 +142,77 @@ func TestRuntimeTextInputExtractsOnlyTextItems(t *testing.T) { } } +func TestRuntimeInputMaterialExtractsStructuredIssueIdentity(t *testing.T) { + got := RuntimeInputMaterial(map[string]any{ + "input": []any{ + map[string]any{ + "type": "text", + "id": "item-1", + "text": "Please review the linked issue.", + "text_elements": []any{ + map[string]any{ + "type": "mention", + "target_type": "issue", + "target_id": "iss-49", + "text": "@TEA-49", + }, + }, + }, + }, + }) + if got.Text != "Please review the linked issue." { + t.Fatalf("visible text changed: %+v", got) + } + if got.IssueIdentity != "iss-49" { + t.Fatalf("structured issue identity = %q, want iss-49", got.IssueIdentity) + } +} + +func TestRuntimeInputMaterialFallsBackToVisibleIssueTag(t *testing.T) { + got := RuntimeInputMaterial(map[string]any{ + "input": []any{ + map[string]any{"type": "text", "id": "item-1", "text": "Please handle @TEA-50 next."}, + }, + }) + if got.IssueIdentity != "TEA-50" { + t.Fatalf("visible issue tag identity = %q, want TEA-50", got.IssueIdentity) + } +} + +func TestRuntimeInputMaterialPrefersStructuredIssueOverVisibleTag(t *testing.T) { + got := RuntimeInputMaterial(map[string]any{ + "input": []any{ + map[string]any{"type": "text", "text": "Ignore the stale copied tag @TEA-1."}, + map[string]any{ + "type": "text", + "text": "Use the selected Multica issue tag.", + "text_elements": []any{ + map[string]any{ + "type": "mention", + "target_type": "issue", + "target_id": "iss-selected", + "text": "@TEA-99", + }, + }, + }, + }, + }) + if got.IssueIdentity != "iss-selected" { + t.Fatalf("structured issue identity should win over visible tag, got %q", got.IssueIdentity) + } +} + +func TestRuntimeInputMaterialIgnoresNonIssueItemID(t *testing.T) { + got := RuntimeInputMaterial(map[string]any{ + "input": []any{ + map[string]any{"type": "text", "id": "item-1", "text": "Coordinate with @team."}, + }, + }) + if got.IssueIdentity != "" { + t.Fatalf("non-issue item id should be ignored, got %q", got.IssueIdentity) + } +} + func TestRuntimeRefNormalizesMulticaRefs(t *testing.T) { if got := RuntimeRef(" issue ", " iss-1 "); got != "multica:issue:iss-1" { t.Fatalf("RuntimeRef() = %q", got) From 17ab57eb2b3b27f1d4f71b92407c4ef577d9bf69 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:52:52 +0800 Subject: [PATCH 075/117] refactor: move multica runtime config policy Move Multica runtime environment, timeout, ledger, projection, and hub-write switches into the Multica surface adapter package so the runtime command delegates adapter policy instead of owning it in main.go. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 23 +-- harness/cmd/mnemon-multica-runtime/main.go | 149 ++++-------------- .../cmd/mnemon-multica-runtime/main_test.go | 21 +-- harness/cmd/mnemon-multica-runtime/probe.go | 6 +- .../surface/multica/runtime_config.go | 89 +++++++++++ .../surface/multica/runtime_config_test.go | 57 +++++++ 6 files changed, 191 insertions(+), 154 deletions(-) create mode 100644 harness/internal/surface/multica/runtime_config.go create mode 100644 harness/internal/surface/multica/runtime_config_test.go diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index fd4d920e..becd6d38 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -23,7 +23,7 @@ func (s *runtimeRPCState) writeMulticaHubArtifacts(ctx context.Context, cli driv if result == nil { return } - if !runtimeMulticaHubWriteEnabled(s.Env) { + if !multicasurface.RuntimeHubWriteEnabled(s.Env) { result.HubWriteStatus = "skipped" return } @@ -607,25 +607,12 @@ func runtimeMulticaScopeRefMatches(ref string, result *runtimeImportResult, extr return true, false } -func runtimeMulticaHubWriteEnabled(env []string) bool { - value := strings.TrimSpace(envValue(env, "MNEMON_MULTICA_HUB_WRITE")) - if value == "" { - return true - } - switch strings.ToLower(value) { - case "0", "false", "off", "disabled", "no": - return false - default: - return true - } -} - func runtimeMulticaRegistry(env []string, cwd string) (driver.MulticaRegistry, bool, error) { paths := []string{} - if explicit := envValue(env, "MNEMON_MULTICA_REGISTRY"); explicit != "" { + if explicit := multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_REGISTRY"); explicit != "" { paths = append(paths, explicit) } - if workspace := envValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { + if workspace := multicasurface.RuntimeEnvValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { paths = append(paths, driver.MulticaRegistryPath(workspace, "")) } if strings.TrimSpace(cwd) != "" { @@ -641,10 +628,10 @@ func runtimeMulticaRegistry(env []string, cwd string) (driver.MulticaRegistry, b } func runtimeMulticaHubLedgerPath(env []string, cwd string) string { - if explicit := envValue(env, "MNEMON_MULTICA_HUB_LEDGER"); explicit != "" { + if explicit := multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_HUB_LEDGER"); explicit != "" { return driver.MulticaHubLedgerPath("", explicit) } - if workspace := envValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { + if workspace := multicasurface.RuntimeEnvValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { return driver.MulticaHubLedgerPath(workspace, "") } return driver.MulticaHubLedgerPath(cwd, "") diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index f712feb9..4855a656 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "os" - "path/filepath" "runtime" "strings" "time" @@ -180,7 +179,7 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er ID: msg.ID, Result: map[string]any{ "userAgent": "mnemon-multica-runtime/" + runtimeVersion, - "codexHome": envValue(s.Env, "CODEX_HOME"), + "codexHome": multicasurface.RuntimeEnvValue(s.Env, "CODEX_HOME"), "platformFamily": "unix", "platformOs": runtime.GOOS, }, @@ -305,8 +304,8 @@ func (s *runtimeRPCState) runTurn(input multicasurface.RuntimeInput, progress ru } func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progress runtimeProgressSink) runtimeImportResult { - taskID := envValue(s.Env, "MULTICA_TASK_ID") - issueID := firstNonEmpty(envValue(s.Env, "MULTICA_ISSUE_ID"), input.IssueIdentity, multicasurface.ExtractIssueIdentity(input.Text)) + taskID := multicasurface.RuntimeEnvValue(s.Env, "MULTICA_TASK_ID") + issueID := firstNonEmpty(multicasurface.RuntimeEnvValue(s.Env, "MULTICA_ISSUE_ID"), input.IssueIdentity, multicasurface.ExtractIssueIdentity(input.Text)) result := runtimeImportResult{ IssueID: issueID, Principal: resolveRuntimePrincipal(s.Env, s.CWD), @@ -338,7 +337,7 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres result.Statement = issue.Description result.HubMetadata = driver.MulticaIssueHubMetadata(issue) applyMulticaHubMetadata(&result, result.HubMetadata) - result.HubBackend = firstNonEmpty(result.HubBackend, envValue(s.Env, "MNEMON_HUB_BACKEND")) + result.HubBackend = firstNonEmpty(result.HubBackend, multicasurface.RuntimeEnvValue(s.Env, "MNEMON_HUB_BACKEND")) emitRuntimeProgress(progress, "Loaded "+runtimeIssueLabel(issue)+"; classifying Mnemon hub metadata.") markIssueInProgress(multicaCtx, cli, issue.ID) if result.HubMetadata.IsAssignmentMailbox() { @@ -354,17 +353,17 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres Title: issue.Title, Description: issue.Description, }, multicasurface.IssueSignalOptions{ - Scope: envDefault(s.Env, "MNEMON_MULTICA_SCOPE", "multica/teamwork"), - TTL: envDefault(s.Env, "MNEMON_MULTICA_TTL", "30m"), + Scope: multicasurface.RuntimeEnvDefault(s.Env, "MNEMON_MULTICA_SCOPE", "multica/teamwork"), + TTL: multicasurface.RuntimeEnvDefault(s.Env, "MNEMON_MULTICA_TTL", "30m"), WhyTeamwork: "Multica assigned this issue to a Mnemon participant, so Mnemon should admit it through the teamwork protocol.", - WorkspaceID: firstNonEmpty(envValue(s.Env, "MNEMON_MULTICA_WORKSPACE_ID"), envValue(s.Env, "MULTICA_WORKSPACE_ID")), + WorkspaceID: firstNonEmpty(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_MULTICA_WORKSPACE_ID"), multicasurface.RuntimeEnvValue(s.Env, "MULTICA_WORKSPACE_ID")), TaskID: taskID, - AgentID: envValue(s.Env, "MULTICA_AGENT_ID"), + AgentID: multicasurface.RuntimeEnvValue(s.Env, "MULTICA_AGENT_ID"), Principal: result.Principal, ContextRefs: []string{ multicasurface.RuntimeRef("issue", issue.ID), multicasurface.RuntimeRef("task", taskID), - multicasurface.RuntimeRef("agent", envValue(s.Env, "MULTICA_AGENT_ID")), + multicasurface.RuntimeRef("agent", multicasurface.RuntimeEnvValue(s.Env, "MULTICA_AGENT_ID")), }, EvidenceRefs: []string{multicasurface.RuntimeRef("issue", issue.ID)}, ExternalID: externalID, @@ -400,7 +399,7 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres emitRuntimeProgress(progress, "Root session metadata written for "+runtimeIssueLabel(issue)+".") } } - addr := strings.TrimSpace(envValue(s.Env, "MNEMON_CONTROL_ADDR")) + addr := strings.TrimSpace(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_CONTROL_ADDR")) if addr == "" { result.Status = "skipped" emitRuntimeProgress(progress, "Local Mnemon control address is not configured; skipping protocol ingest.") @@ -475,7 +474,7 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr correlationProgress := runtimeAssignmentCorrelationProgress(*result) emitRuntimeCommand(progress, "mnemon multica assignment correlate --issue "+issue.ID, correlationProgress, 0) emitRuntimeProgress(progress, correlationProgress) - addr := strings.TrimSpace(envValue(s.Env, "MNEMON_CONTROL_ADDR")) + addr := strings.TrimSpace(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_CONTROL_ADDR")) var client *access.Client if addr == "" { result.WakeStatus = "skipped" @@ -552,12 +551,12 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress if result == nil { return } - runtimeName := strings.TrimSpace(envValue(s.Env, "MNEMON_MANAGED_RUNTIME")) + runtimeName := strings.TrimSpace(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_MANAGED_RUNTIME")) if runtimeName == "" || strings.EqualFold(runtimeName, "off") || strings.EqualFold(runtimeName, "disabled") || strings.EqualFold(runtimeName, "none") { result.WakeStatus = "skipped" return } - addr := strings.TrimSpace(envValue(s.Env, "MNEMON_CONTROL_ADDR")) + addr := strings.TrimSpace(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_CONTROL_ADDR")) if addr == "" { result.WakeStatus = "skipped" result.WakeErr = fmt.Errorf("MNEMON_CONTROL_ADDR is not set") @@ -569,7 +568,7 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress result.WakeErr = err return } - renderCtx, cancel := context.WithTimeout(context.Background(), runtimeTimeout(s.Env)) + renderCtx, cancel := context.WithTimeout(context.Background(), multicasurface.RuntimeTimeout(s.Env)) defer cancel() resp, err := (driver.HTTPRenderClient{ BaseURL: addr, @@ -579,7 +578,7 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress SchemaVersion: 1, Principal: contract.ActorID(result.Principal), Host: "multica", - Lifecycle: envDefault(s.Env, "MNEMON_MANAGED_RENDER_LIFECYCLE", "remind"), + Lifecycle: multicasurface.RuntimeEnvDefault(s.Env, "MNEMON_MANAGED_RENDER_LIFECYCLE", "remind"), Surface: "runtime", RenderIntent: presentation.IntentTeamworkEvents, SessionID: result.SessionID, @@ -603,12 +602,12 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress result.WakeErr = err return } - wakeCtx, cancel := context.WithTimeout(context.Background(), runtimeManagedTurnTimeout(s.Env)) + wakeCtx, cancel := context.WithTimeout(context.Background(), multicasurface.RuntimeManagedTurnTimeout(s.Env)) defer cancel() record, err := (&driver.ManagedAgentDriver{ Principal: result.Principal, Client: client, - Ledger: driver.NewFileManagedWakeLedger(runtimeManagedLedgerPath(s.Env, workspace)), + Ledger: driver.NewFileManagedWakeLedger(multicasurface.RuntimeManagedLedgerPath(s.Env, workspace)), TraceSink: driver.ManagedTurnTraceSinkFunc(func(event driver.ManagedTurnTraceEvent) { if progress != nil { progress(runtimeProgressEvent{Trace: &event}) @@ -636,7 +635,7 @@ func runtimeManagedTurnEnv(env []string, result runtimeImportResult) []string { out := append([]string(nil), env...) add := func(key, value string) { value = strings.TrimSpace(value) - if value == "" || envValue(out, key) != "" { + if value == "" || multicasurface.RuntimeEnvValue(out, key) != "" { return } out = append(out, key+"="+value) @@ -662,7 +661,7 @@ func (s *runtimeRPCState) wakeManagedAgentWithHubProjection(ctx context.Context, if result == nil || client == nil || !strings.EqualFold(result.HubBackend, driver.MulticaHubBackend) || result.HubKind != driver.MulticaHubKindSession || - !runtimeMulticaHubWriteEnabled(s.Env) { + !multicasurface.RuntimeHubWriteEnabled(s.Env) { s.wakeManagedAgent(result, progress) return nil } @@ -670,7 +669,7 @@ func (s *runtimeRPCState) wakeManagedAgentWithHubProjection(ctx context.Context, deltas := make(chan runtimeHubProjectionDelta, 16) go func(snapshot runtimeImportResult) { defer close(deltas) - ticker := time.NewTicker(runtimeHubProjectionInterval(s.Env)) + ticker := time.NewTicker(multicasurface.RuntimeHubProjectionInterval(s.Env)) defer ticker.Stop() for { select { @@ -736,7 +735,7 @@ func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.M if result == nil { return } - if !runtimeProjectionEnabled(s.Env) { + if !multicasurface.RuntimeProjectionCommentsEnabled(s.Env) { result.ProjectionStatus = "skipped" return } @@ -792,12 +791,12 @@ func runtimeProjectionMaterial(issue driver.MulticaIssue, result runtimeImportRe func runtimeMulticaCLI(env []string) driver.MulticaCLI { return driver.MulticaCLI{ - Command: envValue(env, "MNEMON_MULTICA_BIN"), - Profile: envValue(env, "MNEMON_MULTICA_PROFILE"), - ServerURL: firstNonEmpty(envValue(env, "MNEMON_MULTICA_SERVER_URL"), envValue(env, "MULTICA_SERVER_URL")), - WorkspaceID: firstNonEmpty(envValue(env, "MNEMON_MULTICA_WORKSPACE_ID"), envValue(env, "MULTICA_WORKSPACE_ID")), + Command: multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_BIN"), + Profile: multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_PROFILE"), + ServerURL: firstNonEmpty(multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_SERVER_URL"), multicasurface.RuntimeEnvValue(env, "MULTICA_SERVER_URL")), + WorkspaceID: firstNonEmpty(multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_WORKSPACE_ID"), multicasurface.RuntimeEnvValue(env, "MULTICA_WORKSPACE_ID")), Env: append([]string(nil), env...), - Timeout: runtimeTimeout(env), + Timeout: multicasurface.RuntimeTimeout(env), } } @@ -816,8 +815,8 @@ func runtimeControlClient(env []string, addr, principal string) (*access.Client, } func runtimeControlToken(env []string) (string, error) { - token := envValue(env, "MNEMON_CONTROL_TOKEN") - if tokenFile := envValue(env, "MNEMON_CONTROL_TOKEN_FILE"); tokenFile != "" { + token := multicasurface.RuntimeEnvValue(env, "MNEMON_CONTROL_TOKEN") + if tokenFile := multicasurface.RuntimeEnvValue(env, "MNEMON_CONTROL_TOKEN_FILE"); tokenFile != "" { data, err := os.ReadFile(tokenFile) if err != nil { return "", fmt.Errorf("read MNEMON_CONTROL_TOKEN_FILE: %w", err) @@ -828,7 +827,7 @@ func runtimeControlToken(env []string) (string, error) { } func runtimeManagedTurnClient(env []string, cwd, runtimeName string) (driver.ManagedTurnClient, string, error) { - workspace := envDefault(env, "MNEMON_MANAGED_WORKSPACE", cwd) + workspace := multicasurface.RuntimeEnvDefault(env, "MNEMON_MANAGED_WORKSPACE", cwd) if strings.TrimSpace(workspace) == "" { workspace = "." } @@ -838,10 +837,10 @@ func runtimeManagedTurnClient(env []string, cwd, runtimeName string) (driver.Man case "codex-appserver": return driver.CodexAppServerTurnClient{ Principal: resolveRuntimePrincipal(env, cwd), - Command: envDefault(env, "MNEMON_MANAGED_COMMAND", "codex"), + Command: multicasurface.RuntimeEnvDefault(env, "MNEMON_MANAGED_COMMAND", "codex"), Workspace: workspace, Env: append([]string(nil), env...), - TurnTimeout: runtimeManagedTurnTimeout(env), + TurnTimeout: multicasurface.RuntimeManagedTurnTimeout(env), ClientName: "mnemon-multica-runtime", }, workspace, nil default: @@ -859,12 +858,12 @@ func (runtimeNoopTurnClient) StartTurn(_ context.Context, query string) (driver. } func resolveRuntimePrincipal(env []string, cwd string) string { - agentID := envValue(env, "MULTICA_AGENT_ID") - agentName := envValue(env, "MULTICA_AGENT_NAME") + agentID := multicasurface.RuntimeEnvValue(env, "MULTICA_AGENT_ID") + agentName := multicasurface.RuntimeEnvValue(env, "MULTICA_AGENT_NAME") if principal := principalFromRegistry(env, cwd, agentID, agentName); principal != "" { return principal } - if principal := envValue(env, "MNEMON_CONTROL_PRINCIPAL"); principal != "" { + if principal := multicasurface.RuntimeEnvValue(env, "MNEMON_CONTROL_PRINCIPAL"); principal != "" { return principal } if agentName != "" { @@ -875,7 +874,7 @@ func resolveRuntimePrincipal(env []string, cwd string) string { func principalFromRegistry(env []string, cwd, agentID, agentName string) string { paths := []string{} - if explicit := envValue(env, "MNEMON_MULTICA_REGISTRY"); explicit != "" { + if explicit := multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_REGISTRY"); explicit != "" { paths = append(paths, explicit) } if strings.TrimSpace(cwd) != "" { @@ -1268,24 +1267,6 @@ func stringParam(params map[string]any, key string) string { return strings.TrimSpace(value) } -func envValue(env []string, key string) string { - prefix := key + "=" - for i := len(env) - 1; i >= 0; i-- { - item := env[i] - if strings.HasPrefix(item, prefix) { - return strings.TrimSpace(strings.TrimPrefix(item, prefix)) - } - } - return "" -} - -func envDefault(env []string, key, fallback string) string { - if value := envValue(env, key); value != "" { - return value - } - return fallback -} - func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { @@ -1308,66 +1289,6 @@ func runtimeID(prefix string, now time.Time) string { return fmt.Sprintf("%s-%d", prefix, now.UTC().UnixNano()) } -func runtimeTimeout(env []string) time.Duration { - raw := envValue(env, "MNEMON_MULTICA_RUNTIME_TIMEOUT") - if raw == "" { - raw = envValue(env, "MULTICA_HTTP_TIMEOUT") - } - if raw == "" { - return 30 * time.Second - } - d, err := time.ParseDuration(raw) - if err != nil || d <= 0 { - return 30 * time.Second - } - return d -} - -func runtimeManagedTurnTimeout(env []string) time.Duration { - raw := envValue(env, "MNEMON_MANAGED_TURN_TIMEOUT") - if raw == "" { - return 5 * time.Minute - } - d, err := time.ParseDuration(raw) - if err != nil || d <= 0 { - return 5 * time.Minute - } - return d -} - -func runtimeHubProjectionInterval(env []string) time.Duration { - raw := envValue(env, "MNEMON_MULTICA_HUB_PROJECT_INTERVAL") - if raw == "" { - return 5 * time.Second - } - d, err := time.ParseDuration(raw) - if err != nil || d <= 0 { - return 5 * time.Second - } - return d -} - -func runtimeManagedLedgerPath(env []string, workspace string) string { - if explicit := envValue(env, "MNEMON_MANAGED_LEDGER"); explicit != "" { - return explicit - } - root := strings.TrimSpace(workspace) - if root == "" { - root = "." - } - return filepath.Join(root, ".mnemon", "harness", "local", "managed-agent", "wake-ledger.jsonl") -} - -func runtimeProjectionEnabled(env []string) bool { - value := strings.ToLower(envDefault(env, "MNEMON_MULTICA_PROJECT_COMMENTS", "true")) - switch value { - case "0", "false", "no", "off": - return false - default: - return true - } -} - func sanitizePrincipal(value string) string { value = strings.TrimSpace(strings.ToLower(value)) var b strings.Builder diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 3d5092cc..353f8c3c 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -21,25 +21,6 @@ import ( multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) -func TestRuntimeEnvValueUsesLastValue(t *testing.T) { - env := []string{ - "MNEMON_MANAGED_RUNTIME=codex-appserver", - "MNEMON_MANAGED_RUNTIME=off", - } - if got := envValue(env, "MNEMON_MANAGED_RUNTIME"); got != "off" { - t.Fatalf("envValue = %q, want off", got) - } -} - -func TestRuntimeTimeoutUsesMulticaHTTPFallback(t *testing.T) { - if got := runtimeTimeout([]string{"MULTICA_HTTP_TIMEOUT=2m"}); got != 2*time.Minute { - t.Fatalf("runtimeTimeout fallback = %s, want 2m", got) - } - if got := runtimeTimeout([]string{"MULTICA_HTTP_TIMEOUT=2m", "MNEMON_MULTICA_RUNTIME_TIMEOUT=15s"}); got != 15*time.Second { - t.Fatalf("runtimeTimeout override = %s, want 15s", got) - } -} - func TestRuntimeMulticaHubLedgerPathDefaultsToManagedWorkspace(t *testing.T) { tmp := t.TempDir() workspace := filepath.Join(tmp, "managed-workspace") @@ -128,7 +109,7 @@ func TestRuntimeManagedTurnEnvInjectsRenderScope(t *testing.T) { } preserved := runtimeManagedTurnEnv([]string{"MNEMON_RENDER_HOST=custom"}, runtimeImportResult{SessionID: "session-1", RootIssueID: "root-1"}) - if got := envValue(preserved, "MNEMON_RENDER_HOST"); got != "custom" { + if got := multicasurface.RuntimeEnvValue(preserved, "MNEMON_RENDER_HOST"); got != "custom" { t.Fatalf("managed env should preserve explicit host, got %q", got) } } diff --git a/harness/cmd/mnemon-multica-runtime/probe.go b/harness/cmd/mnemon-multica-runtime/probe.go index b526f55a..47d976d1 100644 --- a/harness/cmd/mnemon-multica-runtime/probe.go +++ b/harness/cmd/mnemon-multica-runtime/probe.go @@ -10,6 +10,8 @@ import ( "runtime" "strings" "time" + + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) type probeRecorder struct { @@ -306,9 +308,9 @@ func extractProbeInput(params map[string]any) string { } func newProbeRecorder(env []string, cwd string, now func() time.Time) (*probeRecorder, error) { - path := envValue(env, "MNEMON_MULTICA_PROBE_LOG") + path := multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_PROBE_LOG") if path == "" { - dir := envValue(env, "MNEMON_MULTICA_PROBE_DIR") + dir := multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_PROBE_DIR") if dir == "" { home, err := os.UserHomeDir() if err != nil || strings.TrimSpace(home) == "" { diff --git a/harness/internal/surface/multica/runtime_config.go b/harness/internal/surface/multica/runtime_config.go new file mode 100644 index 00000000..d8bf00ff --- /dev/null +++ b/harness/internal/surface/multica/runtime_config.go @@ -0,0 +1,89 @@ +package multica + +import ( + "path/filepath" + "strings" + "time" +) + +func RuntimeEnvValue(env []string, key string) string { + prefix := key + "=" + for i := len(env) - 1; i >= 0; i-- { + item := env[i] + if strings.HasPrefix(item, prefix) { + return strings.TrimSpace(strings.TrimPrefix(item, prefix)) + } + } + return "" +} + +func RuntimeEnvDefault(env []string, key, fallback string) string { + if value := RuntimeEnvValue(env, key); value != "" { + return value + } + return fallback +} + +func RuntimeTimeout(env []string) time.Duration { + return runtimeDuration(env, []string{"MNEMON_MULTICA_RUNTIME_TIMEOUT", "MULTICA_HTTP_TIMEOUT"}, 30*time.Second) +} + +func RuntimeManagedTurnTimeout(env []string) time.Duration { + return runtimeDuration(env, []string{"MNEMON_MANAGED_TURN_TIMEOUT"}, 5*time.Minute) +} + +func RuntimeHubProjectionInterval(env []string) time.Duration { + return runtimeDuration(env, []string{"MNEMON_MULTICA_HUB_PROJECT_INTERVAL"}, 5*time.Second) +} + +func RuntimeManagedLedgerPath(env []string, workspace string) string { + if explicit := RuntimeEnvValue(env, "MNEMON_MANAGED_LEDGER"); explicit != "" { + return explicit + } + root := strings.TrimSpace(workspace) + if root == "" { + root = "." + } + return filepath.Join(root, ".mnemon", "harness", "local", "managed-agent", "wake-ledger.jsonl") +} + +func RuntimeProjectionCommentsEnabled(env []string) bool { + value := strings.ToLower(RuntimeEnvDefault(env, "MNEMON_MULTICA_PROJECT_COMMENTS", "true")) + switch value { + case "0", "false", "no", "off": + return false + default: + return true + } +} + +func RuntimeHubWriteEnabled(env []string) bool { + value := strings.TrimSpace(RuntimeEnvValue(env, "MNEMON_MULTICA_HUB_WRITE")) + if value == "" { + return true + } + switch strings.ToLower(value) { + case "0", "false", "off", "disabled", "no": + return false + default: + return true + } +} + +func runtimeDuration(env []string, keys []string, fallback time.Duration) time.Duration { + raw := "" + for _, key := range keys { + raw = RuntimeEnvValue(env, key) + if raw != "" { + break + } + } + if raw == "" { + return fallback + } + d, err := time.ParseDuration(raw) + if err != nil || d <= 0 { + return fallback + } + return d +} diff --git a/harness/internal/surface/multica/runtime_config_test.go b/harness/internal/surface/multica/runtime_config_test.go new file mode 100644 index 00000000..d41237d6 --- /dev/null +++ b/harness/internal/surface/multica/runtime_config_test.go @@ -0,0 +1,57 @@ +package multica + +import ( + "path/filepath" + "testing" + "time" +) + +func TestRuntimeEnvValueUsesLastValue(t *testing.T) { + env := []string{ + "MNEMON_MANAGED_RUNTIME=codex-appserver", + "MNEMON_MANAGED_RUNTIME=off", + } + if got := RuntimeEnvValue(env, "MNEMON_MANAGED_RUNTIME"); got != "off" { + t.Fatalf("RuntimeEnvValue = %q, want off", got) + } + if got := RuntimeEnvDefault(env, "MISSING", "fallback"); got != "fallback" { + t.Fatalf("RuntimeEnvDefault = %q, want fallback", got) + } +} + +func TestRuntimeTimeoutUsesMulticaHTTPFallback(t *testing.T) { + if got := RuntimeTimeout([]string{"MULTICA_HTTP_TIMEOUT=2m"}); got != 2*time.Minute { + t.Fatalf("RuntimeTimeout fallback = %s, want 2m", got) + } + if got := RuntimeTimeout([]string{"MULTICA_HTTP_TIMEOUT=2m", "MNEMON_MULTICA_RUNTIME_TIMEOUT=15s"}); got != 15*time.Second { + t.Fatalf("RuntimeTimeout override = %s, want 15s", got) + } + if got := RuntimeTimeout([]string{"MNEMON_MULTICA_RUNTIME_TIMEOUT=bad"}); got != 30*time.Second { + t.Fatalf("RuntimeTimeout invalid = %s, want default 30s", got) + } +} + +func TestRuntimeAdapterSwitches(t *testing.T) { + if !RuntimeProjectionCommentsEnabled(nil) || !RuntimeHubWriteEnabled(nil) { + t.Fatal("runtime switches should default on") + } + if RuntimeProjectionCommentsEnabled([]string{"MNEMON_MULTICA_PROJECT_COMMENTS=off"}) { + t.Fatal("projection comments should honor off") + } + if RuntimeHubWriteEnabled([]string{"MNEMON_MULTICA_HUB_WRITE=disabled"}) { + t.Fatal("hub write should honor disabled") + } +} + +func TestRuntimeManagedLedgerPath(t *testing.T) { + tmp := t.TempDir() + workspace := filepath.Join(tmp, "managed-workspace") + want := filepath.Join(workspace, ".mnemon", "harness", "local", "managed-agent", "wake-ledger.jsonl") + if got := RuntimeManagedLedgerPath(nil, workspace); got != want { + t.Fatalf("RuntimeManagedLedgerPath = %q, want %q", got, want) + } + explicit := filepath.Join(tmp, "explicit.jsonl") + if got := RuntimeManagedLedgerPath([]string{"MNEMON_MANAGED_LEDGER=" + explicit}, workspace); got != explicit { + t.Fatalf("explicit RuntimeManagedLedgerPath = %q, want %q", got, explicit) + } +} From 5a7ad5614e137f82d6dca4733ac33f5dc487b63f Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 02:59:03 +0800 Subject: [PATCH 076/117] refactor: move multica runtime result text Move Multica runtime final-answer and progress wording into the surface adapter package behind a RuntimeResultSummary material. The runtime command now maps execution state to adapter presentation instead of owning visible result policy in main.go. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./... --- harness/cmd/mnemon-multica-runtime/main.go | 290 +++--------------- .../surface/multica/runtime_result.go | 255 +++++++++++++++ .../surface/multica/runtime_result_test.go | 84 +++++ 3 files changed, 381 insertions(+), 248 deletions(-) create mode 100644 harness/internal/surface/multica/runtime_result.go create mode 100644 harness/internal/surface/multica/runtime_result_test.go diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 4855a656..fe859b33 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -300,7 +300,7 @@ func (s *runtimeRPCState) nextItemID(prefix string) string { func (s *runtimeRPCState) runTurn(input multicasurface.RuntimeInput, progress runtimeProgressSink) string { result := s.importIssue(input, progress) - return formatRuntimeFinalAnswer(result) + return multicasurface.FormatRuntimeFinalAnswer(runtimeResultSummary(result)) } func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progress runtimeProgressSink) runtimeImportResult { @@ -311,7 +311,7 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres Principal: resolveRuntimePrincipal(s.Env, s.CWD), TaskID: taskID, } - emitRuntimeProgress(progress, "Mnemon runtime accepted the Multica task for "+displayRuntimePrincipal(result.Principal)+".") + emitRuntimeProgress(progress, "Mnemon runtime accepted the Multica task for "+multicasurface.RuntimePrincipalLabel(result.Principal)+".") if issueID == "" { result.Status = "skipped" result.Err = fmt.Errorf("no Multica issue id was available in task environment or runtime input") @@ -434,15 +434,15 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres emitRuntimeProgress(progress, fmt.Sprintf("Mnemon recorded the issue observation at seq=%d.", rec.Seq)) emitRuntimeProgress(progress, "Waking the managed local agent with [mnemon:wake].") earlyHubDeltas := s.wakeManagedAgentWithHubProjection(multicaCtx, cli, client, issue, &result, progress) - emitRuntimeCommand(progress, "mnemond managed wake --principal "+result.Principal+" [mnemon:wake]", runtimeWakeProgress(result), runtimeExitCode(result.WakeErr)) - emitRuntimeProgress(progress, runtimeWakeProgress(result)) + emitRuntimeCommand(progress, "mnemond managed wake --principal "+result.Principal+" [mnemon:wake]", multicasurface.RuntimeWakeProgress(runtimeResultSummary(result)), runtimeExitCode(result.WakeErr)) + emitRuntimeProgress(progress, multicasurface.RuntimeWakeProgress(runtimeResultSummary(result))) s.writeMulticaHubArtifacts(multicaCtx, cli, client, issue, &result) mergeRuntimeHubProjectionDeltas(&result, earlyHubDeltas) - emitRuntimeCommand(progress, "mnemon multica hub project --issue "+issue.ID, runtimeHubWriteProgress(result), runtimeExitCode(result.HubWriteErr)) - emitRuntimeProgress(progress, runtimeHubWriteProgress(result)) + emitRuntimeCommand(progress, "mnemon multica hub project --issue "+issue.ID, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(result)), runtimeExitCode(result.HubWriteErr)) + emitRuntimeProgress(progress, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(result))) s.projectImportComment(multicaCtx, cli, issue, draft.ExternalID, draft.EventType, &result) - emitRuntimeCommand(progress, "multica issue comment add "+issue.ID, runtimeProjectionProgress(result), runtimeExitCode(result.ProjectionErr)) - emitRuntimeProgress(progress, runtimeProjectionProgress(result)) + emitRuntimeCommand(progress, "multica issue comment add "+issue.ID, multicasurface.RuntimeProjectionProgress(runtimeResultSummary(result)), runtimeExitCode(result.ProjectionErr)) + emitRuntimeProgress(progress, multicasurface.RuntimeProjectionProgress(runtimeResultSummary(result))) return result } @@ -471,7 +471,7 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr result.HubMetadata.EventID, string(eventmodel.Subject("assignment", result.AssignmentID)), ) - correlationProgress := runtimeAssignmentCorrelationProgress(*result) + correlationProgress := multicasurface.RuntimeAssignmentCorrelationProgress() emitRuntimeCommand(progress, "mnemon multica assignment correlate --issue "+issue.ID, correlationProgress, 0) emitRuntimeProgress(progress, correlationProgress) addr := strings.TrimSpace(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_CONTROL_ADDR")) @@ -490,23 +490,19 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr } else { emitRuntimeProgress(progress, "Waking assigned local agent with [mnemon:wake].") s.wakeManagedAgent(result, progress) - emitRuntimeCommand(progress, "mnemond managed wake --principal "+result.Principal+" [mnemon:wake]", runtimeWakeProgress(*result), runtimeExitCode(result.WakeErr)) - emitRuntimeProgress(progress, runtimeWakeProgress(*result)) + emitRuntimeCommand(progress, "mnemond managed wake --principal "+result.Principal+" [mnemon:wake]", multicasurface.RuntimeWakeProgress(runtimeResultSummary(*result)), runtimeExitCode(result.WakeErr)) + emitRuntimeProgress(progress, multicasurface.RuntimeWakeProgress(runtimeResultSummary(*result))) s.writeMulticaHubArtifacts(ctx, cli, client, issue, result) - emitRuntimeCommand(progress, "mnemon multica hub project --issue "+issue.ID, runtimeHubWriteProgress(*result), runtimeExitCode(result.HubWriteErr)) - emitRuntimeProgress(progress, runtimeHubWriteProgress(*result)) + emitRuntimeCommand(progress, "mnemon multica hub project --issue "+issue.ID, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(*result)), runtimeExitCode(result.HubWriteErr)) + emitRuntimeProgress(progress, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(*result))) } } s.projectImportComment(ctx, cli, issue, multicasurface.AssignmentMailboxMarker(result.HubMetadata, result.IssueID), result.HubMetadata.EventType, result) - emitRuntimeCommand(progress, "multica issue comment add "+issue.ID, runtimeProjectionProgress(*result), runtimeExitCode(result.ProjectionErr)) - emitRuntimeProgress(progress, runtimeProjectionProgress(*result)) + emitRuntimeCommand(progress, "multica issue comment add "+issue.ID, multicasurface.RuntimeProjectionProgress(runtimeResultSummary(*result)), runtimeExitCode(result.ProjectionErr)) + emitRuntimeProgress(progress, multicasurface.RuntimeProjectionProgress(runtimeResultSummary(*result))) return *result } -func runtimeAssignmentCorrelationProgress(result runtimeImportResult) string { - return "Mnemon assignment mailbox: correlated." -} - func applyMulticaHubMetadata(result *runtimeImportResult, meta driver.MulticaHubMetadata) { if result == nil { return @@ -897,115 +893,34 @@ func principalFromRegistry(env []string, cwd, agentID, agentName string) string return "" } -func formatRuntimeFinalAnswer(result runtimeImportResult) string { - var b strings.Builder - if result.IssueID == "" { - b.WriteString("Mnemon Multica runtime did not receive a Multica issue id.") - } else { - label := strings.TrimSpace(result.Identifier) - if label == "" { - label = result.IssueID - } - b.WriteString("Mnemon Multica runtime handled issue ") - b.WriteString(label) - if title := strings.TrimSpace(result.Title); title != "" { - b.WriteString(" (") - b.WriteString(title) - b.WriteString(")") - } - b.WriteString(".") - } - if principal := strings.TrimSpace(result.Principal); principal != "" { - b.WriteString(" Principal: ") - b.WriteString(principal) - b.WriteString(".") - } - switch result.Status { - case "recorded": - b.WriteString(" Mnemon ingest: recorded.") - case "correlated": - b.WriteString(" Mnemon assignment mailbox: correlated.") - case "skipped": - b.WriteString(" Mnemon ingest: skipped") - if result.Err != nil { - b.WriteString(" (") - b.WriteString(result.Err.Error()) - b.WriteString(")") - } else { - b.WriteString(" because MNEMON_CONTROL_ADDR is not set") - } - b.WriteString(".") - case "failed": - b.WriteString(" Mnemon ingest: failed") - if result.Err != nil { - b.WriteString(" (") - b.WriteString(result.Err.Error()) - b.WriteString(")") - } - b.WriteString(".") - default: - if result.Err != nil { - b.WriteString(" Mnemon ingest: failed (") - b.WriteString(result.Err.Error()) - b.WriteString(").") - } - } - switch result.ProjectionStatus { - case "commented": - b.WriteString(" Multica projection: comment posted.") - case "skipped": - b.WriteString(" Multica projection: skipped.") - case "failed": - b.WriteString(" Multica projection: failed") - if result.ProjectionErr != nil { - b.WriteString(" (") - b.WriteString(result.ProjectionErr.Error()) - b.WriteString(")") - } - b.WriteString(".") - } - switch result.WakeStatus { - case "completed": - b.WriteString(" Managed wake: completed.") - case "skipped": - b.WriteString(" Managed wake: skipped") - if result.WakeErr != nil { - b.WriteString(" (") - b.WriteString(result.WakeErr.Error()) - b.WriteString(")") - } - b.WriteString(".") - case "failed": - b.WriteString(" Managed wake: failed") - if result.WakeErr != nil { - b.WriteString(" (") - b.WriteString(result.WakeErr.Error()) - b.WriteString(")") - } - b.WriteString(".") - default: - if result.WakeStatus != "" { - b.WriteString(" Managed wake: ") - b.WriteString(result.WakeStatus) - b.WriteString(".") - } +func runtimeResultSummary(result runtimeImportResult) multicasurface.RuntimeResultSummary { + summary := multicasurface.RuntimeResultSummary{ + IssueID: result.IssueID, + Identifier: result.Identifier, + Title: result.Title, + Principal: result.Principal, + Status: result.Status, + ProjectionStatus: result.ProjectionStatus, + ProjectionCommentID: result.ProjectionCommentID, + WakeStatus: result.WakeStatus, + WakeTurnID: result.WakeTurnID, + HubWriteStatus: result.HubWriteStatus, + HubChildIssues: result.HubChildIssues, + HubFeedbackComments: result.HubFeedbackComments, + } + if result.Err != nil { + summary.Err = result.Err.Error() + } + if result.ProjectionErr != nil { + summary.ProjectionErr = result.ProjectionErr.Error() + } + if result.WakeErr != nil { + summary.WakeErr = result.WakeErr.Error() } - switch result.HubWriteStatus { - case "created", "commented", "updated", "noop": - b.WriteString(" Multica updates: ") - b.WriteString(runtimeHubWriteSummary(result)) - case "skipped": - b.WriteString(" Multica updates: skipped.") - case "failed": - b.WriteString(" Multica updates: failed") - if result.HubWriteErr != nil { - b.WriteString(" (") - b.WriteString(result.HubWriteErr.Error()) - b.WriteString(")") - } - b.WriteString(".") + if result.HubWriteErr != nil { + summary.HubWriteErr = result.HubWriteErr.Error() } - return strings.TrimSpace(b.String()) + return summary } func (s *runtimeRPCState) threadStartResult(params map[string]any) map[string]any { @@ -1103,129 +1018,8 @@ func runtimeExitCode(err error) int { return 0 } -func displayRuntimePrincipal(principal string) string { - principal = strings.TrimSpace(principal) - if principal == "" { - return "the resolved principal" - } - return principal -} - func runtimeIssueLabel(issue driver.MulticaIssue) string { - if strings.TrimSpace(issue.Identifier) != "" && strings.TrimSpace(issue.Title) != "" { - return issue.Identifier + " (" + issue.Title + ")" - } - if strings.TrimSpace(issue.Identifier) != "" { - return issue.Identifier - } - if strings.TrimSpace(issue.ID) != "" { - return issue.ID - } - return "the Multica issue" -} - -func runtimeAssignmentLabel(result runtimeImportResult) string { - if strings.TrimSpace(result.AssignmentID) != "" { - return result.AssignmentID - } - if strings.TrimSpace(result.AssignmentFingerprint) != "" { - return result.AssignmentFingerprint - } - return "assignment mailbox" -} - -func runtimeWakeProgress(result runtimeImportResult) string { - switch strings.TrimSpace(result.WakeStatus) { - case "": - if result.WakeErr != nil { - return "Managed wake failed: " + result.WakeErr.Error() - } - return "Managed wake did not run." - case "completed": - if strings.TrimSpace(result.WakeTurnID) != "" { - return "Managed wake completed: turn=" + result.WakeTurnID + "." - } - return "Managed wake completed." - case "skipped": - if result.WakeErr != nil { - return "Managed wake skipped: " + result.WakeErr.Error() - } - return "Managed wake skipped." - case "failed": - if result.WakeErr != nil { - return "Managed wake failed: " + result.WakeErr.Error() - } - return "Managed wake failed." - default: - return "Managed wake status: " + result.WakeStatus + "." - } -} - -func runtimeHubWriteProgress(result runtimeImportResult) string { - switch strings.TrimSpace(result.HubWriteStatus) { - case "": - return "" - case "failed": - if result.HubWriteErr != nil { - return "Multica updates failed: " + result.HubWriteErr.Error() - } - return "Multica updates failed." - case "skipped": - return "Multica updates skipped." - default: - return "Multica updates: " + runtimeHubWriteSummary(result) - } -} - -func runtimeHubWriteSummary(result runtimeImportResult) string { - status := strings.TrimSpace(result.HubWriteStatus) - var parts []string - if result.HubChildIssues > 0 { - parts = append(parts, fmt.Sprintf("%d %s", result.HubChildIssues, pluralize(result.HubChildIssues, "assignment mailbox", "assignment mailboxes"))) - } - if result.HubFeedbackComments > 0 { - parts = append(parts, fmt.Sprintf("%d %s", result.HubFeedbackComments, pluralize(result.HubFeedbackComments, "feedback comment", "feedback comments"))) - } - switch { - case len(parts) > 0 && status == "created": - return strings.Join(parts, " and ") + " created." - case len(parts) > 0 && status == "commented": - return strings.Join(parts, " and ") + " posted." - case len(parts) > 0: - return strings.Join(parts, " and ") + " synced." - case status == "noop": - return "no visible updates needed." - case status != "": - return strings.ReplaceAll(status, "_", " ") + "." - default: - return "no visible updates needed." - } -} - -func pluralize(n int, singular, plural string) string { - if n == 1 { - return singular - } - return plural -} - -func runtimeProjectionProgress(result runtimeImportResult) string { - switch strings.TrimSpace(result.ProjectionStatus) { - case "": - return "" - case "failed": - if result.ProjectionErr != nil { - return "Multica comment projection failed: " + result.ProjectionErr.Error() - } - return "Multica comment projection failed." - case "skipped": - return "Multica comment projection skipped." - default: - if strings.TrimSpace(result.ProjectionCommentID) != "" { - return "Multica comment projection completed: comment=" + result.ProjectionCommentID + "." - } - return "Multica comment projection completed." - } + return multicasurface.RuntimeIssueLabel(issue.ID, issue.Identifier, issue.Title) } func markIssueInProgress(ctx context.Context, cli driver.MulticaCLI, issueID string) { diff --git a/harness/internal/surface/multica/runtime_result.go b/harness/internal/surface/multica/runtime_result.go new file mode 100644 index 00000000..cfd08660 --- /dev/null +++ b/harness/internal/surface/multica/runtime_result.go @@ -0,0 +1,255 @@ +package multica + +import ( + "fmt" + "strings" +) + +type RuntimeResultSummary struct { + IssueID string + Identifier string + Title string + Principal string + Status string + Err string + ProjectionStatus string + ProjectionCommentID string + ProjectionErr string + WakeStatus string + WakeTurnID string + WakeErr string + HubWriteStatus string + HubChildIssues int + HubFeedbackComments int + HubWriteErr string +} + +func FormatRuntimeFinalAnswer(result RuntimeResultSummary) string { + var b strings.Builder + if result.IssueID == "" { + b.WriteString("Mnemon Multica runtime did not receive a Multica issue id.") + } else { + label := strings.TrimSpace(result.Identifier) + if label == "" { + label = result.IssueID + } + b.WriteString("Mnemon Multica runtime handled issue ") + b.WriteString(label) + if title := strings.TrimSpace(result.Title); title != "" { + b.WriteString(" (") + b.WriteString(title) + b.WriteString(")") + } + b.WriteString(".") + } + if principal := strings.TrimSpace(result.Principal); principal != "" { + b.WriteString(" Principal: ") + b.WriteString(principal) + b.WriteString(".") + } + switch result.Status { + case "recorded": + b.WriteString(" Mnemon ingest: recorded.") + case "correlated": + b.WriteString(" Mnemon assignment mailbox: correlated.") + case "skipped": + b.WriteString(" Mnemon ingest: skipped") + if result.Err != "" { + b.WriteString(" (") + b.WriteString(result.Err) + b.WriteString(")") + } else { + b.WriteString(" because MNEMON_CONTROL_ADDR is not set") + } + b.WriteString(".") + case "failed": + b.WriteString(" Mnemon ingest: failed") + if result.Err != "" { + b.WriteString(" (") + b.WriteString(result.Err) + b.WriteString(")") + } + b.WriteString(".") + default: + if result.Err != "" { + b.WriteString(" Mnemon ingest: failed (") + b.WriteString(result.Err) + b.WriteString(").") + } + } + switch result.ProjectionStatus { + case "commented": + b.WriteString(" Multica projection: comment posted.") + case "skipped": + b.WriteString(" Multica projection: skipped.") + case "failed": + b.WriteString(" Multica projection: failed") + if result.ProjectionErr != "" { + b.WriteString(" (") + b.WriteString(result.ProjectionErr) + b.WriteString(")") + } + b.WriteString(".") + } + switch result.WakeStatus { + case "completed": + b.WriteString(" Managed wake: completed.") + case "skipped": + b.WriteString(" Managed wake: skipped") + if result.WakeErr != "" { + b.WriteString(" (") + b.WriteString(result.WakeErr) + b.WriteString(")") + } + b.WriteString(".") + case "failed": + b.WriteString(" Managed wake: failed") + if result.WakeErr != "" { + b.WriteString(" (") + b.WriteString(result.WakeErr) + b.WriteString(")") + } + b.WriteString(".") + default: + if result.WakeStatus != "" { + b.WriteString(" Managed wake: ") + b.WriteString(result.WakeStatus) + b.WriteString(".") + } + } + switch result.HubWriteStatus { + case "created", "commented", "updated", "noop": + b.WriteString(" Multica updates: ") + b.WriteString(RuntimeHubWriteSummary(result)) + case "skipped": + b.WriteString(" Multica updates: skipped.") + case "failed": + b.WriteString(" Multica updates: failed") + if result.HubWriteErr != "" { + b.WriteString(" (") + b.WriteString(result.HubWriteErr) + b.WriteString(")") + } + b.WriteString(".") + } + return strings.TrimSpace(b.String()) +} + +func RuntimePrincipalLabel(principal string) string { + principal = strings.TrimSpace(principal) + if principal == "" { + return "the resolved principal" + } + return principal +} + +func RuntimeIssueLabel(id, identifier, title string) string { + if strings.TrimSpace(identifier) != "" && strings.TrimSpace(title) != "" { + return strings.TrimSpace(identifier) + " (" + strings.TrimSpace(title) + ")" + } + if strings.TrimSpace(identifier) != "" { + return strings.TrimSpace(identifier) + } + if strings.TrimSpace(id) != "" { + return strings.TrimSpace(id) + } + return "the Multica issue" +} + +func RuntimeAssignmentCorrelationProgress() string { + return "Mnemon assignment mailbox: correlated." +} + +func RuntimeWakeProgress(result RuntimeResultSummary) string { + switch strings.TrimSpace(result.WakeStatus) { + case "": + if result.WakeErr != "" { + return "Managed wake failed: " + result.WakeErr + } + return "Managed wake did not run." + case "completed": + if strings.TrimSpace(result.WakeTurnID) != "" { + return "Managed wake completed: turn=" + result.WakeTurnID + "." + } + return "Managed wake completed." + case "skipped": + if result.WakeErr != "" { + return "Managed wake skipped: " + result.WakeErr + } + return "Managed wake skipped." + case "failed": + if result.WakeErr != "" { + return "Managed wake failed: " + result.WakeErr + } + return "Managed wake failed." + default: + return "Managed wake status: " + result.WakeStatus + "." + } +} + +func RuntimeHubWriteProgress(result RuntimeResultSummary) string { + switch strings.TrimSpace(result.HubWriteStatus) { + case "": + return "" + case "failed": + if result.HubWriteErr != "" { + return "Multica updates failed: " + result.HubWriteErr + } + return "Multica updates failed." + case "skipped": + return "Multica updates skipped." + default: + return "Multica updates: " + RuntimeHubWriteSummary(result) + } +} + +func RuntimeHubWriteSummary(result RuntimeResultSummary) string { + status := strings.TrimSpace(result.HubWriteStatus) + var parts []string + if result.HubChildIssues > 0 { + parts = append(parts, fmt.Sprintf("%d %s", result.HubChildIssues, runtimePluralize(result.HubChildIssues, "assignment mailbox", "assignment mailboxes"))) + } + if result.HubFeedbackComments > 0 { + parts = append(parts, fmt.Sprintf("%d %s", result.HubFeedbackComments, runtimePluralize(result.HubFeedbackComments, "feedback comment", "feedback comments"))) + } + switch { + case len(parts) > 0 && status == "created": + return strings.Join(parts, " and ") + " created." + case len(parts) > 0 && status == "commented": + return strings.Join(parts, " and ") + " posted." + case len(parts) > 0: + return strings.Join(parts, " and ") + " synced." + case status == "noop": + return "no visible updates needed." + case status != "": + return strings.ReplaceAll(status, "_", " ") + "." + default: + return "no visible updates needed." + } +} + +func RuntimeProjectionProgress(result RuntimeResultSummary) string { + switch strings.TrimSpace(result.ProjectionStatus) { + case "": + return "" + case "failed": + if result.ProjectionErr != "" { + return "Multica comment projection failed: " + result.ProjectionErr + } + return "Multica comment projection failed." + case "skipped": + return "Multica comment projection skipped." + default: + if strings.TrimSpace(result.ProjectionCommentID) != "" { + return "Multica comment projection completed: comment=" + result.ProjectionCommentID + "." + } + return "Multica comment projection completed." + } +} + +func runtimePluralize(n int, singular, plural string) string { + if n == 1 { + return singular + } + return plural +} diff --git a/harness/internal/surface/multica/runtime_result_test.go b/harness/internal/surface/multica/runtime_result_test.go new file mode 100644 index 00000000..a4ccbbb2 --- /dev/null +++ b/harness/internal/surface/multica/runtime_result_test.go @@ -0,0 +1,84 @@ +package multica + +import ( + "strings" + "testing" +) + +func TestFormatRuntimeFinalAnswerSummarizesRuntimeOutcome(t *testing.T) { + got := FormatRuntimeFinalAnswer(RuntimeResultSummary{ + IssueID: "iss-1", + Identifier: "TEA-1", + Title: "Runtime adapter cleanup", + Principal: "planner@team", + Status: "recorded", + ProjectionStatus: "commented", + WakeStatus: "completed", + HubWriteStatus: "updated", + HubChildIssues: 1, + HubFeedbackComments: 2, + }) + for _, want := range []string{ + "Mnemon Multica runtime handled issue TEA-1 (Runtime adapter cleanup).", + "Principal: planner@team.", + "Mnemon ingest: recorded.", + "Multica projection: comment posted.", + "Managed wake: completed.", + "Multica updates: 1 assignment mailbox and 2 feedback comments synced.", + } { + if !strings.Contains(got, want) { + t.Fatalf("final answer missing %q:\n%s", want, got) + } + } +} + +func TestRuntimeFinalAnswerCarriesFailures(t *testing.T) { + got := FormatRuntimeFinalAnswer(RuntimeResultSummary{ + IssueID: "iss-2", + Status: "failed", + Err: "ingest refused", + ProjectionStatus: "failed", + ProjectionErr: "comment rejected", + WakeStatus: "failed", + WakeErr: "render unavailable", + HubWriteStatus: "failed", + HubWriteErr: "metadata timeout", + }) + for _, want := range []string{ + "Mnemon ingest: failed (ingest refused).", + "Multica projection: failed (comment rejected).", + "Managed wake: failed (render unavailable).", + "Multica updates: failed (metadata timeout).", + } { + if !strings.Contains(got, want) { + t.Fatalf("failure answer missing %q:\n%s", want, got) + } + } +} + +func TestRuntimeProgressSummaries(t *testing.T) { + if got := RuntimeWakeProgress(RuntimeResultSummary{WakeStatus: "completed", WakeTurnID: "turn-1"}); got != "Managed wake completed: turn=turn-1." { + t.Fatalf("wake progress = %q", got) + } + if got := RuntimeHubWriteProgress(RuntimeResultSummary{HubWriteStatus: "commented", HubFeedbackComments: 1}); got != "Multica updates: 1 feedback comment posted." { + t.Fatalf("hub progress = %q", got) + } + if got := RuntimeProjectionProgress(RuntimeResultSummary{ProjectionStatus: "commented", ProjectionCommentID: "comment-1"}); got != "Multica comment projection completed: comment=comment-1." { + t.Fatalf("projection progress = %q", got) + } + if got := RuntimeAssignmentCorrelationProgress(); got != "Mnemon assignment mailbox: correlated." { + t.Fatalf("assignment correlation progress = %q", got) + } +} + +func TestRuntimeLabels(t *testing.T) { + if got := RuntimePrincipalLabel(""); got != "the resolved principal" { + t.Fatalf("empty principal label = %q", got) + } + if got := RuntimeIssueLabel("iss-1", "TEA-1", "Issue title"); got != "TEA-1 (Issue title)" { + t.Fatalf("issue label = %q", got) + } + if got := RuntimeIssueLabel("iss-1", "", ""); got != "iss-1" { + t.Fatalf("fallback issue label = %q", got) + } +} From 6b0e53285b5764def7c39621bbcc302a87b4b760 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:04:03 +0800 Subject: [PATCH 077/117] refactor: move multica managed wake policy Move managed wake render scope and environment injection into the Multica surface adapter behind RuntimeManagedWakeMaterial. The runtime command now maps execution state into adapter policy instead of owning the render env rules directly. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./... --- harness/cmd/mnemon-multica-runtime/main.go | 32 ++++++--------- .../cmd/mnemon-multica-runtime/main_test.go | 39 ------------------- .../surface/multica/runtime_config.go | 35 +++++++++++++++++ .../surface/multica/runtime_config_test.go | 38 ++++++++++++++++++ 4 files changed, 84 insertions(+), 60 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index fe859b33..d3414f9c 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -543,6 +543,15 @@ func runtimeManagedWakeMatchMaterial(result runtimeImportResult) drive.ManagedWa } } +func runtimeManagedWakeMaterial(result runtimeImportResult) multicasurface.RuntimeManagedWakeMaterial { + return multicasurface.RuntimeManagedWakeMaterial{ + IssueID: result.IssueID, + RootIssueID: result.RootIssueID, + AssignmentID: result.AssignmentID, + SessionID: result.SessionID, + } +} + func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress runtimeProgressSink) { if result == nil { return @@ -578,7 +587,7 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress Surface: "runtime", RenderIntent: presentation.IntentTeamworkEvents, SessionID: result.SessionID, - InputDigest: runtimeManagedWakeScopeID(*result), + InputDigest: multicasurface.RuntimeManagedWakeScopeID(runtimeManagedWakeMaterial(*result)), }) if err != nil { result.WakeStatus = "failed" @@ -591,7 +600,7 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress result.WakeErr = fmt.Errorf("no managed wake candidate in rendered context") return } - managedEnv := runtimeManagedTurnEnv(s.Env, *result) + managedEnv := multicasurface.RuntimeManagedTurnEnv(s.Env, runtimeManagedWakeMaterial(*result)) client, workspace, err := runtimeManagedTurnClient(managedEnv, s.CWD, runtimeName) if err != nil { result.WakeStatus = "failed" @@ -623,25 +632,6 @@ func (s *runtimeRPCState) wakeManagedAgent(result *runtimeImportResult, progress } } -func runtimeManagedWakeScopeID(result runtimeImportResult) string { - return firstNonEmpty(result.AssignmentID, result.RootIssueID, result.IssueID) -} - -func runtimeManagedTurnEnv(env []string, result runtimeImportResult) []string { - out := append([]string(nil), env...) - add := func(key, value string) { - value = strings.TrimSpace(value) - if value == "" || multicasurface.RuntimeEnvValue(out, key) != "" { - return - } - out = append(out, key+"="+value) - } - add("MNEMON_RENDER_HOST", "multica") - add("MNEMON_RENDER_SESSION_ID", result.SessionID) - add("MNEMON_RENDER_INPUT_ID", runtimeManagedWakeScopeID(result)) - return out -} - type runtimeHubProjectionDelta struct { ChildIssues int FeedbackComments int diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 353f8c3c..53a0ae62 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -75,45 +75,6 @@ func TestManagedWakeMatchTermsPreferStableIssueIdentity(t *testing.T) { } } -func TestRuntimeManagedWakeScopeIDPrefersAssignmentThenRoot(t *testing.T) { - if got := runtimeManagedWakeScopeID(runtimeImportResult{ - AssignmentID: "asg-1", - RootIssueID: "root-1", - IssueID: "child-1", - }); got != "asg-1" { - t.Fatalf("assignment mailbox scope = %q, want asg-1", got) - } - if got := runtimeManagedWakeScopeID(runtimeImportResult{ - RootIssueID: "root-1", - IssueID: "root-1", - }); got != "root-1" { - t.Fatalf("root session scope = %q, want root-1", got) - } -} - -func TestRuntimeManagedTurnEnvInjectsRenderScope(t *testing.T) { - env := runtimeManagedTurnEnv([]string{"EXISTING=1"}, runtimeImportResult{ - SessionID: "multica:session:root-1", - RootIssueID: "root-1", - AssignmentID: "asg-1", - }) - joined := strings.Join(env, "\n") - for _, want := range []string{ - "MNEMON_RENDER_HOST=multica", - "MNEMON_RENDER_SESSION_ID=multica:session:root-1", - "MNEMON_RENDER_INPUT_ID=asg-1", - } { - if !strings.Contains(joined, want) { - t.Fatalf("managed env missing %q: %v", want, env) - } - } - - preserved := runtimeManagedTurnEnv([]string{"MNEMON_RENDER_HOST=custom"}, runtimeImportResult{SessionID: "session-1", RootIssueID: "root-1"}) - if got := multicasurface.RuntimeEnvValue(preserved, "MNEMON_RENDER_HOST"); got != "custom" { - t.Fatalf("managed env should preserve explicit host, got %q", got) - } -} - func runtimeTestEnv(values ...string) []string { env := make([]string, 0, len(os.Environ())+len(values)) for _, item := range os.Environ() { diff --git a/harness/internal/surface/multica/runtime_config.go b/harness/internal/surface/multica/runtime_config.go index d8bf00ff..428c8084 100644 --- a/harness/internal/surface/multica/runtime_config.go +++ b/harness/internal/surface/multica/runtime_config.go @@ -6,6 +6,13 @@ import ( "time" ) +type RuntimeManagedWakeMaterial struct { + IssueID string + RootIssueID string + AssignmentID string + SessionID string +} + func RuntimeEnvValue(env []string, key string) string { prefix := key + "=" for i := len(env) - 1; i >= 0; i-- { @@ -47,6 +54,25 @@ func RuntimeManagedLedgerPath(env []string, workspace string) string { return filepath.Join(root, ".mnemon", "harness", "local", "managed-agent", "wake-ledger.jsonl") } +func RuntimeManagedWakeScopeID(material RuntimeManagedWakeMaterial) string { + return firstNonEmptyRuntimeString(material.AssignmentID, material.RootIssueID, material.IssueID) +} + +func RuntimeManagedTurnEnv(env []string, material RuntimeManagedWakeMaterial) []string { + out := append([]string(nil), env...) + add := func(key, value string) { + value = strings.TrimSpace(value) + if value == "" || RuntimeEnvValue(out, key) != "" { + return + } + out = append(out, key+"="+value) + } + add("MNEMON_RENDER_HOST", "multica") + add("MNEMON_RENDER_SESSION_ID", material.SessionID) + add("MNEMON_RENDER_INPUT_ID", RuntimeManagedWakeScopeID(material)) + return out +} + func RuntimeProjectionCommentsEnabled(env []string) bool { value := strings.ToLower(RuntimeEnvDefault(env, "MNEMON_MULTICA_PROJECT_COMMENTS", "true")) switch value { @@ -70,6 +96,15 @@ func RuntimeHubWriteEnabled(env []string) bool { } } +func firstNonEmptyRuntimeString(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} + func runtimeDuration(env []string, keys []string, fallback time.Duration) time.Duration { raw := "" for _, key := range keys { diff --git a/harness/internal/surface/multica/runtime_config_test.go b/harness/internal/surface/multica/runtime_config_test.go index d41237d6..b54a48bd 100644 --- a/harness/internal/surface/multica/runtime_config_test.go +++ b/harness/internal/surface/multica/runtime_config_test.go @@ -55,3 +55,41 @@ func TestRuntimeManagedLedgerPath(t *testing.T) { t.Fatalf("explicit RuntimeManagedLedgerPath = %q, want %q", got, explicit) } } + +func TestRuntimeManagedWakeScopeIDPrefersAssignmentThenRoot(t *testing.T) { + if got := RuntimeManagedWakeScopeID(RuntimeManagedWakeMaterial{ + AssignmentID: "asg-1", + RootIssueID: "root-1", + IssueID: "child-1", + }); got != "asg-1" { + t.Fatalf("assignment mailbox scope = %q, want asg-1", got) + } + if got := RuntimeManagedWakeScopeID(RuntimeManagedWakeMaterial{ + RootIssueID: "root-1", + IssueID: "root-1", + }); got != "root-1" { + t.Fatalf("root session scope = %q, want root-1", got) + } +} + +func TestRuntimeManagedTurnEnvInjectsRenderScope(t *testing.T) { + env := RuntimeManagedTurnEnv([]string{"EXISTING=1"}, RuntimeManagedWakeMaterial{ + SessionID: "multica:session:root-1", + RootIssueID: "root-1", + AssignmentID: "asg-1", + }) + if got := RuntimeEnvValue(env, "MNEMON_RENDER_HOST"); got != "multica" { + t.Fatalf("render host = %q", got) + } + if got := RuntimeEnvValue(env, "MNEMON_RENDER_SESSION_ID"); got != "multica:session:root-1" { + t.Fatalf("render session = %q", got) + } + if got := RuntimeEnvValue(env, "MNEMON_RENDER_INPUT_ID"); got != "asg-1" { + t.Fatalf("render input = %q", got) + } + + preserved := RuntimeManagedTurnEnv([]string{"MNEMON_RENDER_HOST=custom"}, RuntimeManagedWakeMaterial{SessionID: "session-1", RootIssueID: "root-1"}) + if got := RuntimeEnvValue(preserved, "MNEMON_RENDER_HOST"); got != "custom" { + t.Fatalf("managed env should preserve explicit host, got %q", got) + } +} From c4b2375b53f58987d22d83067cd2173b167d0a01 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:07:57 +0800 Subject: [PATCH 078/117] refactor: move multica scope matching policy Move Multica session, issue, task, and mention ref matching into the Multica surface adapter. The runtime hub writer now maps import state into RuntimeScopeMaterial and delegates scope decisions to the adapter layer. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 101 +++--------------- .../internal/surface/multica/runtime_scope.go | 93 ++++++++++++++++ .../surface/multica/runtime_scope_test.go | 58 ++++++++++ 3 files changed, 164 insertions(+), 88 deletions(-) create mode 100644 harness/internal/surface/multica/runtime_scope.go create mode 100644 harness/internal/surface/multica/runtime_scope_test.go diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index becd6d38..96a16eeb 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -500,111 +500,36 @@ func runtimeProgressItem(item map[string]any) runtimeProgress { } func runtimeAssignmentMatchesCurrentMulticaScope(item runtimeAssignment, result *runtimeImportResult) bool { - if !runtimeExplicitMulticaScopeMatches(item.SessionID, item.RootIssueID, result) { + scope := runtimeMulticaScopeMaterial(result) + if !multicasurface.RuntimeExplicitScopeMatches(item.SessionID, item.RootIssueID, scope) { return false } refs := append([]string{}, item.ContextRefs...) refs = append(refs, item.EvidenceRefs...) - return runtimeRefsMatchCurrentMulticaScope(refs, result) + return multicasurface.RuntimeRefsMatchScope(refs, scope) } func runtimeProgressMatchesCurrentMulticaScope(item runtimeProgress, result *runtimeImportResult, childIssueID string) bool { - if !runtimeExplicitMulticaScopeMatches(item.SessionID, item.RootIssueID, result) { + scope := runtimeMulticaScopeMaterial(result) + if !multicasurface.RuntimeExplicitScopeMatches(item.SessionID, item.RootIssueID, scope) { return false } refs := append([]string{}, item.ContextRefs...) refs = append(refs, item.EvidenceRefs...) refs = append(refs, item.ArtifactRefs...) - return runtimeRefsMatchCurrentMulticaScope(refs, result, childIssueID) + return multicasurface.RuntimeRefsMatchScope(refs, scope, childIssueID) } -func runtimeExplicitMulticaScopeMatches(sessionID, rootIssueID string, result *runtimeImportResult) bool { +func runtimeMulticaScopeMaterial(result *runtimeImportResult) multicasurface.RuntimeScopeMaterial { if result == nil { - return true - } - if sessionID = strings.TrimSpace(sessionID); sessionID != "" && strings.TrimSpace(result.SessionID) != "" && sessionID != strings.TrimSpace(result.SessionID) { - return false - } - if rootIssueID = strings.TrimSpace(rootIssueID); rootIssueID != "" && strings.TrimSpace(result.RootIssueID) != "" && rootIssueID != strings.TrimSpace(result.RootIssueID) { - return false - } - return true -} - -func runtimeRefsMatchCurrentMulticaScope(refs []string, result *runtimeImportResult, extraIssueIDs ...string) bool { - scoped := false - for _, ref := range refs { - isScoped, matches := runtimeMulticaScopeRefMatches(ref, result, extraIssueIDs...) - if !isScoped { - continue - } - scoped = true - if matches { - return true - } + return multicasurface.RuntimeScopeMaterial{} } - return !scoped -} - -func runtimeMulticaScopeRefMatches(ref string, result *runtimeImportResult, extraIssueIDs ...string) (bool, bool) { - ref = strings.TrimSpace(ref) - if ref == "" { - return false, false - } - lower := strings.ToLower(ref) - prefixes := []string{"multica:issue:", "multica:issue/", "mention://issue/", "multica:session:", "multica:session/", "multica:task:", "multica:task/"} - scoped := false - for _, prefix := range prefixes { - if strings.HasPrefix(lower, prefix) { - scoped = true - break - } - } - if !scoped { - return false, false - } - candidates := []string{} - if result != nil { - root := strings.TrimSpace(result.RootIssueID) - if root != "" { - candidates = append(candidates, - "multica:issue:"+root, - "multica:issue/"+root, - "mention://issue/"+root, - "multica:session:"+root, - "multica:session/"+root, - ) - } - if session := strings.TrimSpace(result.SessionID); session != "" { - candidates = append(candidates, session) - } - if correlation := strings.TrimSpace(result.CorrelationID); correlation != "" { - candidates = append(candidates, correlation) - } - if task := strings.TrimSpace(result.TaskID); task != "" { - candidates = append(candidates, - "multica:task:"+task, - "multica:task/"+task, - ) - } - } - for _, issueID := range extraIssueIDs { - issueID = strings.TrimSpace(issueID) - if issueID == "" { - continue - } - candidates = append(candidates, - "multica:issue:"+issueID, - "multica:issue/"+issueID, - "mention://issue/"+issueID, - ) - } - for _, candidate := range candidates { - if ref == candidate { - return true, true - } + return multicasurface.RuntimeScopeMaterial{ + SessionID: result.SessionID, + RootIssueID: result.RootIssueID, + CorrelationID: result.CorrelationID, + TaskID: result.TaskID, } - return true, false } func runtimeMulticaRegistry(env []string, cwd string) (driver.MulticaRegistry, bool, error) { diff --git a/harness/internal/surface/multica/runtime_scope.go b/harness/internal/surface/multica/runtime_scope.go new file mode 100644 index 00000000..0376b963 --- /dev/null +++ b/harness/internal/surface/multica/runtime_scope.go @@ -0,0 +1,93 @@ +package multica + +import "strings" + +type RuntimeScopeMaterial struct { + SessionID string + RootIssueID string + CorrelationID string + TaskID string +} + +func RuntimeExplicitScopeMatches(sessionID, rootIssueID string, material RuntimeScopeMaterial) bool { + if sessionID = strings.TrimSpace(sessionID); sessionID != "" && strings.TrimSpace(material.SessionID) != "" && sessionID != strings.TrimSpace(material.SessionID) { + return false + } + if rootIssueID = strings.TrimSpace(rootIssueID); rootIssueID != "" && strings.TrimSpace(material.RootIssueID) != "" && rootIssueID != strings.TrimSpace(material.RootIssueID) { + return false + } + return true +} + +func RuntimeRefsMatchScope(refs []string, material RuntimeScopeMaterial, extraIssueIDs ...string) bool { + scoped := false + for _, ref := range refs { + isScoped, matches := RuntimeScopeRefMatches(ref, material, extraIssueIDs...) + if !isScoped { + continue + } + scoped = true + if matches { + return true + } + } + return !scoped +} + +func RuntimeScopeRefMatches(ref string, material RuntimeScopeMaterial, extraIssueIDs ...string) (bool, bool) { + ref = strings.TrimSpace(ref) + if ref == "" { + return false, false + } + lower := strings.ToLower(ref) + prefixes := []string{"multica:issue:", "multica:issue/", "mention://issue/", "multica:session:", "multica:session/", "multica:task:", "multica:task/"} + scoped := false + for _, prefix := range prefixes { + if strings.HasPrefix(lower, prefix) { + scoped = true + break + } + } + if !scoped { + return false, false + } + candidates := []string{} + if root := strings.TrimSpace(material.RootIssueID); root != "" { + candidates = append(candidates, + "multica:issue:"+root, + "multica:issue/"+root, + "mention://issue/"+root, + "multica:session:"+root, + "multica:session/"+root, + ) + } + if session := strings.TrimSpace(material.SessionID); session != "" { + candidates = append(candidates, session) + } + if correlation := strings.TrimSpace(material.CorrelationID); correlation != "" { + candidates = append(candidates, correlation) + } + if task := strings.TrimSpace(material.TaskID); task != "" { + candidates = append(candidates, + "multica:task:"+task, + "multica:task/"+task, + ) + } + for _, issueID := range extraIssueIDs { + issueID = strings.TrimSpace(issueID) + if issueID == "" { + continue + } + candidates = append(candidates, + "multica:issue:"+issueID, + "multica:issue/"+issueID, + "mention://issue/"+issueID, + ) + } + for _, candidate := range candidates { + if ref == candidate { + return true, true + } + } + return true, false +} diff --git a/harness/internal/surface/multica/runtime_scope_test.go b/harness/internal/surface/multica/runtime_scope_test.go new file mode 100644 index 00000000..fb739c5c --- /dev/null +++ b/harness/internal/surface/multica/runtime_scope_test.go @@ -0,0 +1,58 @@ +package multica + +import "testing" + +func TestRuntimeExplicitScopeMatchesChecksKnownSessionAndRoot(t *testing.T) { + material := RuntimeScopeMaterial{ + SessionID: "multica:session:root-1", + RootIssueID: "root-1", + } + if !RuntimeExplicitScopeMatches("multica:session:root-1", "root-1", material) { + t.Fatal("matching explicit scope rejected") + } + if RuntimeExplicitScopeMatches("multica:session:other", "root-1", material) { + t.Fatal("mismatched session accepted") + } + if RuntimeExplicitScopeMatches("multica:session:root-1", "other-root", material) { + t.Fatal("mismatched root accepted") + } + if !RuntimeExplicitScopeMatches("", "", material) { + t.Fatal("unscoped item should remain eligible") + } +} + +func TestRuntimeRefsMatchScopeUsesMulticaStructuredRefs(t *testing.T) { + material := RuntimeScopeMaterial{ + SessionID: "multica:session:root-1", + RootIssueID: "root-1", + CorrelationID: "multica:issue:root-1", + TaskID: "task-1", + } + for _, refs := range [][]string{ + {"multica:issue:root-1"}, + {"multica:issue/root-1"}, + {"mention://issue/root-1"}, + {"multica:session:root-1"}, + {"multica:session/root-1"}, + {"multica:task:task-1"}, + {"multica:task/task-1"}, + {"unrelated", "mention://issue/child-1"}, + } { + if !RuntimeRefsMatchScope(refs, material, "child-1") { + t.Fatalf("refs should match current scope: %v", refs) + } + } +} + +func TestRuntimeRefsMatchScopeRejectsOtherMulticaScope(t *testing.T) { + material := RuntimeScopeMaterial{SessionID: "multica:session:root-1", RootIssueID: "root-1"} + if RuntimeRefsMatchScope([]string{"multica:issue:other-root"}, material) { + t.Fatal("other issue scope accepted") + } + if RuntimeRefsMatchScope([]string{"multica:task:other-task"}, material) { + t.Fatal("other task scope accepted") + } + if !RuntimeRefsMatchScope([]string{"local:event:1", "docs/example"}, material) { + t.Fatal("unscoped refs should not filter item out") + } +} From 1b057c667b0bd49f88c885baf4047f98a2328b2b Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:13:32 +0800 Subject: [PATCH 079/117] refactor: move multica view item parsing Move assignment and progress view-item parsing, sequence filtering, and scope matching entry points into the Multica surface adapter. The runtime hub writer now consumes structured surface items while keeping only view traversal and Multica CLI orchestration locally. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 224 +----------------- .../cmd/mnemon-multica-runtime/main_test.go | 20 +- .../surface/multica/runtime_view_item.go | 218 +++++++++++++++++ .../surface/multica/runtime_view_item_test.go | 104 ++++++++ 4 files changed, 345 insertions(+), 221 deletions(-) create mode 100644 harness/internal/surface/multica/runtime_view_item.go create mode 100644 harness/internal/surface/multica/runtime_view_item_test.go diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 96a16eeb..1fab5395 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -2,11 +2,9 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "sort" - "strconv" "strings" "time" @@ -92,11 +90,11 @@ func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driv } var projections []runtimeAssignmentProjection for _, assignment := range hubViewItems(proj, "assignment") { - item := runtimeAssignmentItem(assignment) + item := multicasurface.RuntimeAssignmentViewItem(assignment) if item.ID == "" || item.Assignee == "" { continue } - if !hubItemAfterRootIngest(item.IngestSeq, result) { + if !runtimeItemAfterRootIngest(item.IngestSeq, result) { continue } if !runtimeAssignmentMatchesCurrentMulticaScope(item, result) { @@ -183,7 +181,7 @@ func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driv } type runtimeAssignmentProjection struct { - Item runtimeAssignment + Item multicasurface.RuntimeAssignmentItem Participant driver.MulticaParticipantRecord Source driver.MulticaHubLedgerSource Metadata driver.MulticaHubMetadata @@ -298,11 +296,11 @@ func retryMulticaHubValue[T any](ctx context.Context, op func() (T, error)) (T, func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver.MulticaCLI, ledger *driver.FileMulticaHubLedger, proj pview.View, result *runtimeImportResult) error { for _, progress := range hubViewItems(proj, "progress_digest") { - item := runtimeProgressItem(progress) + item := multicasurface.RuntimeProgressViewItem(progress) if item.ID == "" || item.AssignmentRef == "" { continue } - if !hubItemAfterRootIngest(item.IngestSeq, result) { + if !runtimeItemAfterRootIngest(item.IngestSeq, result) { continue } source := driver.MulticaHubLedgerSource{ @@ -418,106 +416,12 @@ func (s *runtimeRPCState) ensureProgressIssueStatuses(ctx context.Context, cli d return nil } -type runtimeAssignment struct { - ID string - EventID string - IngestSeq int64 - SessionID string - RootIssueID string - Actor string - Assignee string - Scope string - TTL string - SignalRef string - ExpectedWork string - ExpectedFeedback string - Rationale string - ContextRefs []string - EvidenceRefs []string +func runtimeAssignmentMatchesCurrentMulticaScope(item multicasurface.RuntimeAssignmentItem, result *runtimeImportResult) bool { + return multicasurface.RuntimeAssignmentMatchesScope(item, runtimeMulticaScopeMaterial(result)) } -type runtimeProgress struct { - ID string - EventID string - IngestSeq int64 - SessionID string - RootIssueID string - Actor string - AssignmentRef string - Scope string - FeedbackKind string - Summary string - Result string - Blocker string - ContextRefs []string - ArtifactRefs []string - EvidenceRefs []string -} - -func runtimeAssignmentItem(item map[string]any) runtimeAssignment { - id := hubItemFirstString(item, "assignment_id", "id", "declaration_id") - if id == "" { - id = hubItemString(item, "event_id") - } - return runtimeAssignment{ - ID: id, - EventID: hubItemFirstString(item, "event_id", "id", "declaration_id", "assignment_id"), - IngestSeq: hubItemInt64(item, "ingest_seq"), - SessionID: hubItemString(item, "session_id"), - RootIssueID: hubItemString(item, "root_issue_id"), - Actor: hubItemString(item, "actor"), - Assignee: hubItemString(item, "assignee"), - Scope: hubItemString(item, "scope"), - TTL: hubItemString(item, "ttl"), - SignalRef: hubItemString(item, "signal_ref"), - ExpectedWork: hubItemString(item, "expected_work"), - ExpectedFeedback: hubItemString(item, "expected_feedback"), - Rationale: hubItemString(item, "rationale"), - ContextRefs: hubItemStringList(item, "context_refs"), - EvidenceRefs: hubItemStringList(item, "evidence_refs"), - } -} - -func runtimeProgressItem(item map[string]any) runtimeProgress { - id := hubItemFirstString(item, "id", "declaration_id", "event_id") - return runtimeProgress{ - ID: id, - EventID: hubItemFirstString(item, "event_id", "id", "declaration_id"), - IngestSeq: hubItemInt64(item, "ingest_seq"), - SessionID: hubItemString(item, "session_id"), - RootIssueID: hubItemString(item, "root_issue_id"), - Actor: hubItemString(item, "actor"), - AssignmentRef: hubItemString(item, "assignment_ref"), - Scope: hubItemString(item, "scope"), - FeedbackKind: hubItemString(item, "feedback_kind"), - Summary: hubItemString(item, "summary"), - Result: hubItemString(item, "result"), - Blocker: hubItemString(item, "blocker"), - ContextRefs: hubItemStringList(item, "context_refs"), - ArtifactRefs: hubItemStringList(item, "artifact_refs"), - EvidenceRefs: hubItemStringList(item, "evidence_refs"), - } -} - -func runtimeAssignmentMatchesCurrentMulticaScope(item runtimeAssignment, result *runtimeImportResult) bool { - scope := runtimeMulticaScopeMaterial(result) - if !multicasurface.RuntimeExplicitScopeMatches(item.SessionID, item.RootIssueID, scope) { - return false - } - refs := append([]string{}, item.ContextRefs...) - refs = append(refs, item.EvidenceRefs...) - return multicasurface.RuntimeRefsMatchScope(refs, scope) -} - -func runtimeProgressMatchesCurrentMulticaScope(item runtimeProgress, result *runtimeImportResult, childIssueID string) bool { - scope := runtimeMulticaScopeMaterial(result) - if !multicasurface.RuntimeExplicitScopeMatches(item.SessionID, item.RootIssueID, scope) { - return false - } - refs := append([]string{}, item.ContextRefs...) - refs = append(refs, item.EvidenceRefs...) - refs = append(refs, item.ArtifactRefs...) - return multicasurface.RuntimeRefsMatchScope(refs, scope, childIssueID) +func runtimeProgressMatchesCurrentMulticaScope(item multicasurface.RuntimeProgressItem, result *runtimeImportResult, childIssueID string) bool { + return multicasurface.RuntimeProgressMatchesScope(item, runtimeMulticaScopeMaterial(result), childIssueID) } func runtimeMulticaScopeMaterial(result *runtimeImportResult) multicasurface.RuntimeScopeMaterial { @@ -602,116 +506,14 @@ func hubAnyItems(raw any) []map[string]any { return out } -func hubItemFirstString(item map[string]any, keys ...string) string { - for _, key := range keys { - if value := hubItemString(item, key); value != "" { - return value - } - } - return "" -} - -func hubItemString(item map[string]any, key string) string { - if value, ok := item[key].(string); ok { - return strings.TrimSpace(value) - } - for _, section := range []string{eventmodel.PayloadRuleKey, eventmodel.PayloadNarrativeKey, eventmodel.PayloadRefsKey} { - if m, ok := item[section].(map[string]any); ok { - if value, ok := m[key].(string); ok { - return strings.TrimSpace(value) - } - } - } - return "" -} - -func hubItemInt64(item map[string]any, key string) int64 { - if value, ok := hubInt64(item[key]); ok { - return value - } - for _, section := range []string{eventmodel.PayloadRuleKey, eventmodel.PayloadNarrativeKey, eventmodel.PayloadRefsKey} { - if m, ok := item[section].(map[string]any); ok { - if value, ok := hubInt64(m[key]); ok { - return value - } - } - } - return 0 -} - -func hubInt64(raw any) (int64, bool) { - switch v := raw.(type) { - case int: - return int64(v), true - case int64: - return v, true - case int32: - return int64(v), true - case float64: - return int64(v), true - case float32: - return int64(v), true - case json.Number: - n, err := v.Int64() - return n, err == nil - case string: - n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) - return n, err == nil - default: - return 0, false - } -} - -func hubItemAfterRootIngest(ingestSeq int64, result *runtimeImportResult) bool { +func runtimeItemAfterRootIngest(ingestSeq int64, result *runtimeImportResult) bool { if result == nil || result.Receipt == nil || result.Receipt.Seq <= 0 || ingestSeq <= 0 { return true } - return ingestSeq > result.Receipt.Seq -} - -func hubItemStringList(item map[string]any, key string) []string { - if out := hubStringList(item[key]); len(out) > 0 { - return out - } - for _, section := range []string{eventmodel.PayloadRuleKey, eventmodel.PayloadNarrativeKey, eventmodel.PayloadRefsKey} { - if m, ok := item[section].(map[string]any); ok { - if out := hubStringList(m[key]); len(out) > 0 { - return out - } - } - } - return nil -} - -func hubStringList(raw any) []string { - seen := map[string]bool{} - var out []string - add := func(value string) { - value = strings.TrimSpace(value) - if value == "" || seen[value] { - return - } - seen[value] = true - out = append(out, value) - } - switch v := raw.(type) { - case []string: - for _, item := range v { - add(item) - } - case []any: - for _, item := range v { - if value, ok := item.(string); ok { - add(value) - } - } - case string: - add(v) - } - return out + return multicasurface.RuntimeItemAfterRootIngest(ingestSeq, result.Receipt.Seq) } -func assignmentMailboxMaterial(item runtimeAssignment, result *runtimeImportResult, rootIssue driver.MulticaIssue, participant driver.MulticaParticipantRecord) multicasurface.AssignmentMailboxMaterial { +func assignmentMailboxMaterial(item multicasurface.RuntimeAssignmentItem, result *runtimeImportResult, rootIssue driver.MulticaIssue, participant driver.MulticaParticipantRecord) multicasurface.AssignmentMailboxMaterial { material := multicasurface.AssignmentMailboxMaterial{ ID: item.ID, Scope: item.Scope, @@ -731,7 +533,7 @@ func assignmentMailboxMaterial(item runtimeAssignment, result *runtimeImportResu return material } -func progressFeedbackMaterial(item runtimeProgress) multicasurface.ProgressFeedbackMaterial { +func progressFeedbackMaterial(item multicasurface.RuntimeProgressItem) multicasurface.ProgressFeedbackMaterial { return multicasurface.ProgressFeedbackMaterial{ AssignmentRef: item.AssignmentRef, FeedbackKind: item.FeedbackKind, diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 53a0ae62..6d468d8a 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -1633,18 +1633,18 @@ esac func TestMulticaStatusForProgressIsRuleBased(t *testing.T) { for _, tc := range []struct { name string - item runtimeProgress + item multicasurface.RuntimeProgressItem want string }{ - {name: "progress", item: runtimeProgress{FeedbackKind: "progress"}, want: "in_progress"}, - {name: "waiting", item: runtimeProgress{FeedbackKind: "waiting"}, want: "todo"}, - {name: "review", item: runtimeProgress{FeedbackKind: "review"}, want: "in_review"}, - {name: "result", item: runtimeProgress{FeedbackKind: "result"}, want: "done"}, - {name: "blocker", item: runtimeProgress{FeedbackKind: "blocker"}, want: "blocked"}, - {name: "canceled", item: runtimeProgress{FeedbackKind: "canceled"}, want: "cancelled"}, - {name: "blocker narrative fallback", item: runtimeProgress{Blocker: "waiting on access"}, want: "blocked"}, - {name: "result narrative fallback", item: runtimeProgress{Result: "validated"}, want: "done"}, - {name: "unknown", item: runtimeProgress{Summary: "not enough signal"}, want: ""}, + {name: "progress", item: multicasurface.RuntimeProgressItem{FeedbackKind: "progress"}, want: "in_progress"}, + {name: "waiting", item: multicasurface.RuntimeProgressItem{FeedbackKind: "waiting"}, want: "todo"}, + {name: "review", item: multicasurface.RuntimeProgressItem{FeedbackKind: "review"}, want: "in_review"}, + {name: "result", item: multicasurface.RuntimeProgressItem{FeedbackKind: "result"}, want: "done"}, + {name: "blocker", item: multicasurface.RuntimeProgressItem{FeedbackKind: "blocker"}, want: "blocked"}, + {name: "canceled", item: multicasurface.RuntimeProgressItem{FeedbackKind: "canceled"}, want: "cancelled"}, + {name: "blocker narrative fallback", item: multicasurface.RuntimeProgressItem{Blocker: "waiting on access"}, want: "blocked"}, + {name: "result narrative fallback", item: multicasurface.RuntimeProgressItem{Result: "validated"}, want: "done"}, + {name: "unknown", item: multicasurface.RuntimeProgressItem{Summary: "not enough signal"}, want: ""}, } { if got := multicasurface.ProgressIssueStatus(progressFeedbackMaterial(tc.item)); got != tc.want { t.Fatalf("%s: status = %q, want %q", tc.name, got, tc.want) diff --git a/harness/internal/surface/multica/runtime_view_item.go b/harness/internal/surface/multica/runtime_view_item.go new file mode 100644 index 00000000..eb7d9f98 --- /dev/null +++ b/harness/internal/surface/multica/runtime_view_item.go @@ -0,0 +1,218 @@ +package multica + +import ( + "encoding/json" + "strconv" + "strings" + + eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" +) + +type RuntimeAssignmentItem struct { + ID string + EventID string + IngestSeq int64 + SessionID string + RootIssueID string + Actor string + Assignee string + Scope string + TTL string + SignalRef string + ExpectedWork string + ExpectedFeedback string + Rationale string + ContextRefs []string + EvidenceRefs []string +} + +type RuntimeProgressItem struct { + ID string + EventID string + IngestSeq int64 + SessionID string + RootIssueID string + Actor string + AssignmentRef string + Scope string + FeedbackKind string + Summary string + Result string + Blocker string + ContextRefs []string + ArtifactRefs []string + EvidenceRefs []string +} + +func RuntimeAssignmentViewItem(item map[string]any) RuntimeAssignmentItem { + id := runtimeViewItemFirstString(item, "assignment_id", "id", "declaration_id") + if id == "" { + id = runtimeViewItemString(item, "event_id") + } + return RuntimeAssignmentItem{ + ID: id, + EventID: runtimeViewItemFirstString(item, "event_id", "id", "declaration_id", "assignment_id"), + IngestSeq: runtimeViewItemInt64(item, "ingest_seq"), + SessionID: runtimeViewItemString(item, "session_id"), + RootIssueID: runtimeViewItemString(item, "root_issue_id"), + Actor: runtimeViewItemString(item, "actor"), + Assignee: runtimeViewItemString(item, "assignee"), + Scope: runtimeViewItemString(item, "scope"), + TTL: runtimeViewItemString(item, "ttl"), + SignalRef: runtimeViewItemString(item, "signal_ref"), + ExpectedWork: runtimeViewItemString(item, "expected_work"), + ExpectedFeedback: runtimeViewItemString(item, "expected_feedback"), + Rationale: runtimeViewItemString(item, "rationale"), + ContextRefs: runtimeViewItemStringList(item, "context_refs"), + EvidenceRefs: runtimeViewItemStringList(item, "evidence_refs"), + } +} + +func RuntimeProgressViewItem(item map[string]any) RuntimeProgressItem { + id := runtimeViewItemFirstString(item, "id", "declaration_id", "event_id") + return RuntimeProgressItem{ + ID: id, + EventID: runtimeViewItemFirstString(item, "event_id", "id", "declaration_id"), + IngestSeq: runtimeViewItemInt64(item, "ingest_seq"), + SessionID: runtimeViewItemString(item, "session_id"), + RootIssueID: runtimeViewItemString(item, "root_issue_id"), + Actor: runtimeViewItemString(item, "actor"), + AssignmentRef: runtimeViewItemString(item, "assignment_ref"), + Scope: runtimeViewItemString(item, "scope"), + FeedbackKind: runtimeViewItemString(item, "feedback_kind"), + Summary: runtimeViewItemString(item, "summary"), + Result: runtimeViewItemString(item, "result"), + Blocker: runtimeViewItemString(item, "blocker"), + ContextRefs: runtimeViewItemStringList(item, "context_refs"), + ArtifactRefs: runtimeViewItemStringList(item, "artifact_refs"), + EvidenceRefs: runtimeViewItemStringList(item, "evidence_refs"), + } +} + +func RuntimeAssignmentMatchesScope(item RuntimeAssignmentItem, scope RuntimeScopeMaterial) bool { + if !RuntimeExplicitScopeMatches(item.SessionID, item.RootIssueID, scope) { + return false + } + refs := append([]string{}, item.ContextRefs...) + refs = append(refs, item.EvidenceRefs...) + return RuntimeRefsMatchScope(refs, scope) +} + +func RuntimeProgressMatchesScope(item RuntimeProgressItem, scope RuntimeScopeMaterial, extraIssueIDs ...string) bool { + if !RuntimeExplicitScopeMatches(item.SessionID, item.RootIssueID, scope) { + return false + } + refs := append([]string{}, item.ContextRefs...) + refs = append(refs, item.EvidenceRefs...) + refs = append(refs, item.ArtifactRefs...) + return RuntimeRefsMatchScope(refs, scope, extraIssueIDs...) +} + +func RuntimeItemAfterRootIngest(ingestSeq, rootIngestSeq int64) bool { + if rootIngestSeq <= 0 || ingestSeq <= 0 { + return true + } + return ingestSeq > rootIngestSeq +} + +func runtimeViewItemFirstString(item map[string]any, keys ...string) string { + for _, key := range keys { + if value := runtimeViewItemString(item, key); value != "" { + return value + } + } + return "" +} + +func runtimeViewItemString(item map[string]any, key string) string { + if value, ok := item[key].(string); ok { + return strings.TrimSpace(value) + } + for _, section := range []string{eventmodel.PayloadRuleKey, eventmodel.PayloadNarrativeKey, eventmodel.PayloadRefsKey} { + if m, ok := item[section].(map[string]any); ok { + if value, ok := m[key].(string); ok { + return strings.TrimSpace(value) + } + } + } + return "" +} + +func runtimeViewItemInt64(item map[string]any, key string) int64 { + if value, ok := runtimeViewInt64(item[key]); ok { + return value + } + for _, section := range []string{eventmodel.PayloadRuleKey, eventmodel.PayloadNarrativeKey, eventmodel.PayloadRefsKey} { + if m, ok := item[section].(map[string]any); ok { + if value, ok := runtimeViewInt64(m[key]); ok { + return value + } + } + } + return 0 +} + +func runtimeViewInt64(raw any) (int64, bool) { + switch v := raw.(type) { + case int: + return int64(v), true + case int64: + return v, true + case int32: + return int64(v), true + case float64: + return int64(v), true + case float32: + return int64(v), true + case json.Number: + n, err := v.Int64() + return n, err == nil + case string: + n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + return n, err == nil + default: + return 0, false + } +} + +func runtimeViewItemStringList(item map[string]any, key string) []string { + if out := runtimeViewStringList(item[key]); len(out) > 0 { + return out + } + for _, section := range []string{eventmodel.PayloadRuleKey, eventmodel.PayloadNarrativeKey, eventmodel.PayloadRefsKey} { + if m, ok := item[section].(map[string]any); ok { + if out := runtimeViewStringList(m[key]); len(out) > 0 { + return out + } + } + } + return nil +} + +func runtimeViewStringList(raw any) []string { + seen := map[string]bool{} + var out []string + add := func(value string) { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + return + } + seen[value] = true + out = append(out, value) + } + switch v := raw.(type) { + case []string: + for _, item := range v { + add(item) + } + case []any: + for _, item := range v { + if value, ok := item.(string); ok { + add(value) + } + } + case string: + add(v) + } + return out +} diff --git a/harness/internal/surface/multica/runtime_view_item_test.go b/harness/internal/surface/multica/runtime_view_item_test.go new file mode 100644 index 00000000..f36e3b02 --- /dev/null +++ b/harness/internal/surface/multica/runtime_view_item_test.go @@ -0,0 +1,104 @@ +package multica + +import ( + "encoding/json" + "reflect" + "testing" + + eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" +) + +func TestRuntimeAssignmentViewItemReadsStructuredPayloadSections(t *testing.T) { + item := RuntimeAssignmentViewItem(map[string]any{ + "id": "event-1", + "ingest_seq": json.Number("42"), + "actor": "planner@team", + eventmodel.PayloadRuleKey: map[string]any{ + "assignment_id": "asg-1", + "assignee": "worker@team", + "scope": "release/readiness", + "ttl": "30m", + "signal_ref": "sig-1", + "session_id": "multica:session:root-1", + "root_issue_id": "root-1", + }, + eventmodel.PayloadNarrativeKey: map[string]any{ + "expected_work": "Check the release note flow.", + "expected_feedback": "result or blocker", + "rationale": "Validate routing.", + }, + eventmodel.PayloadRefsKey: map[string]any{ + "context_refs": []any{" multica:issue:root-1 ", "multica:issue:root-1"}, + "evidence_refs": "evidence-1", + }, + }) + if item.ID != "asg-1" || item.EventID != "event-1" || item.IngestSeq != 42 { + t.Fatalf("assignment identity mismatch: %+v", item) + } + if item.Assignee != "worker@team" || item.Scope != "release/readiness" || item.TTL != "30m" || item.SignalRef != "sig-1" { + t.Fatalf("assignment rule fields mismatch: %+v", item) + } + if item.ExpectedWork == "" || item.ExpectedFeedback == "" || item.Rationale == "" { + t.Fatalf("assignment narrative fields missing: %+v", item) + } + if !reflect.DeepEqual(item.ContextRefs, []string{"multica:issue:root-1"}) { + t.Fatalf("context refs = %#v", item.ContextRefs) + } + if !reflect.DeepEqual(item.EvidenceRefs, []string{"evidence-1"}) { + t.Fatalf("evidence refs = %#v", item.EvidenceRefs) + } +} + +func TestRuntimeProgressViewItemReadsRefsAndNarrative(t *testing.T) { + item := RuntimeProgressViewItem(map[string]any{ + "event_id": "pg-1", + "ingest_seq": "51", + eventmodel.PayloadRuleKey: map[string]any{ + "assignment_ref": "asg-1", + "feedback_kind": "result", + "scope": "release/readiness", + "session_id": "multica:session:root-1", + "root_issue_id": "root-1", + }, + eventmodel.PayloadNarrativeKey: map[string]any{ + "summary": "Done", + "result": "Validated", + }, + eventmodel.PayloadRefsKey: map[string]any{ + "context_refs": []string{"multica:issue:root-1"}, + "artifact_refs": []any{"artifact-1", "artifact-1"}, + "evidence_refs": []any{"evidence-1"}, + }, + }) + if item.ID != "pg-1" || item.EventID != "pg-1" || item.IngestSeq != 51 { + t.Fatalf("progress identity mismatch: %+v", item) + } + if item.AssignmentRef != "asg-1" || item.FeedbackKind != "result" || item.Summary != "Done" || item.Result != "Validated" { + t.Fatalf("progress fields mismatch: %+v", item) + } + if !reflect.DeepEqual(item.ArtifactRefs, []string{"artifact-1"}) { + t.Fatalf("artifact refs = %#v", item.ArtifactRefs) + } +} + +func TestRuntimeViewItemsMatchScopeAndRootIngest(t *testing.T) { + scope := RuntimeScopeMaterial{SessionID: "multica:session:root-1", RootIssueID: "root-1"} + if !RuntimeAssignmentMatchesScope(RuntimeAssignmentItem{ + SessionID: "multica:session:root-1", + RootIssueID: "root-1", + ContextRefs: []string{"multica:issue:root-1"}, + }, scope) { + t.Fatal("assignment should match current scope") + } + if RuntimeProgressMatchesScope(RuntimeProgressItem{ + ContextRefs: []string{"multica:issue:other-root"}, + }, scope) { + t.Fatal("progress from another scoped issue should not match") + } + if !RuntimeItemAfterRootIngest(11, 10) || RuntimeItemAfterRootIngest(9, 10) { + t.Fatal("root ingest sequence filter mismatch") + } + if !RuntimeItemAfterRootIngest(0, 10) || !RuntimeItemAfterRootIngest(9, 0) { + t.Fatal("unknown sequence should remain eligible") + } +} From 5ea9d5232f0d1f23ca25c267658c19e1b1d2b204 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:16:08 +0800 Subject: [PATCH 080/117] refactor: move multica view traversal Move presentation view item traversal into the Multica surface adapter so the runtime hub writer no longer owns item container compatibility rules. The command now delegates assignment and progress item extraction through RuntimeViewItems before running CLI orchestration. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 35 +---------------- .../surface/multica/runtime_view_item.go | 32 +++++++++++++++ .../surface/multica/runtime_view_item_test.go | 39 +++++++++++++++++++ 3 files changed, 73 insertions(+), 33 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 1fab5395..50c8fad0 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -89,7 +89,7 @@ func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driv return err } var projections []runtimeAssignmentProjection - for _, assignment := range hubViewItems(proj, "assignment") { + for _, assignment := range multicasurface.RuntimeViewItems(proj, "assignment") { item := multicasurface.RuntimeAssignmentViewItem(assignment) if item.ID == "" || item.Assignee == "" { continue @@ -295,7 +295,7 @@ func retryMulticaHubValue[T any](ctx context.Context, op func() (T, error)) (T, } func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver.MulticaCLI, ledger *driver.FileMulticaHubLedger, proj pview.View, result *runtimeImportResult) error { - for _, progress := range hubViewItems(proj, "progress_digest") { + for _, progress := range multicasurface.RuntimeViewItems(proj, "progress_digest") { item := multicasurface.RuntimeProgressViewItem(progress) if item.ID == "" || item.AssignmentRef == "" { continue @@ -475,37 +475,6 @@ func multicaParticipantForPrincipal(reg driver.MulticaRegistry, principal string return driver.MulticaParticipantRecord{}, false } -func hubViewItems(proj pview.View, kind string) []map[string]any { - var out []map[string]any - for _, content := range proj.Content { - if string(content.Ref.Kind) != kind { - continue - } - for _, field := range []string{"items", "entries", "declarations"} { - if raw, ok := content.Fields[field]; ok { - out = append(out, hubAnyItems(raw)...) - break - } - } - } - return out -} - -func hubAnyItems(raw any) []map[string]any { - var out []map[string]any - switch v := raw.(type) { - case []any: - for _, item := range v { - if m, ok := item.(map[string]any); ok { - out = append(out, m) - } - } - case []map[string]any: - out = append(out, v...) - } - return out -} - func runtimeItemAfterRootIngest(ingestSeq int64, result *runtimeImportResult) bool { if result == nil || result.Receipt == nil || result.Receipt.Seq <= 0 || ingestSeq <= 0 { return true diff --git a/harness/internal/surface/multica/runtime_view_item.go b/harness/internal/surface/multica/runtime_view_item.go index eb7d9f98..8338ca8f 100644 --- a/harness/internal/surface/multica/runtime_view_item.go +++ b/harness/internal/surface/multica/runtime_view_item.go @@ -6,6 +6,7 @@ import ( "strings" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" + pview "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view" ) type RuntimeAssignmentItem struct { @@ -89,6 +90,22 @@ func RuntimeProgressViewItem(item map[string]any) RuntimeProgressItem { } } +func RuntimeViewItems(proj pview.View, kind string) []map[string]any { + var out []map[string]any + for _, content := range proj.Content { + if string(content.Ref.Kind) != kind { + continue + } + for _, field := range []string{"items", "entries", "declarations"} { + if raw, ok := content.Fields[field]; ok { + out = append(out, runtimeViewAnyItems(raw)...) + break + } + } + } + return out +} + func RuntimeAssignmentMatchesScope(item RuntimeAssignmentItem, scope RuntimeScopeMaterial) bool { if !RuntimeExplicitScopeMatches(item.SessionID, item.RootIssueID, scope) { return false @@ -115,6 +132,21 @@ func RuntimeItemAfterRootIngest(ingestSeq, rootIngestSeq int64) bool { return ingestSeq > rootIngestSeq } +func runtimeViewAnyItems(raw any) []map[string]any { + var out []map[string]any + switch v := raw.(type) { + case []any: + for _, item := range v { + if m, ok := item.(map[string]any); ok { + out = append(out, m) + } + } + case []map[string]any: + out = append(out, v...) + } + return out +} + func runtimeViewItemFirstString(item map[string]any, keys ...string) string { for _, key := range keys { if value := runtimeViewItemString(item, key); value != "" { diff --git a/harness/internal/surface/multica/runtime_view_item_test.go b/harness/internal/surface/multica/runtime_view_item_test.go index f36e3b02..7d0b7388 100644 --- a/harness/internal/surface/multica/runtime_view_item_test.go +++ b/harness/internal/surface/multica/runtime_view_item_test.go @@ -5,7 +5,9 @@ import ( "reflect" "testing" + "github.com/mnemon-dev/mnemon/harness/internal/contract" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" + pview "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view" ) func TestRuntimeAssignmentViewItemReadsStructuredPayloadSections(t *testing.T) { @@ -81,6 +83,43 @@ func TestRuntimeProgressViewItemReadsRefsAndNarrative(t *testing.T) { } } +func TestRuntimeViewItemsSelectsKindAndCompatibleItemFields(t *testing.T) { + view := pview.View{Content: []pview.ResourceContent{ + { + Ref: contract.ResourceRef{Kind: "assignment", ID: "project"}, + Fields: map[string]any{ + "items": []any{ + map[string]any{"id": "asg-items"}, + "ignored", + }, + }, + }, + { + Ref: contract.ResourceRef{Kind: "assignment", ID: "team"}, + Fields: map[string]any{ + "entries": []map[string]any{{"id": "asg-entries"}}, + }, + }, + { + Ref: contract.ResourceRef{Kind: "progress_digest", ID: "project"}, + Fields: map[string]any{ + "declarations": []any{map[string]any{"id": "pg-1"}}, + }, + }, + }} + assignments := RuntimeViewItems(view, "assignment") + if len(assignments) != 2 { + t.Fatalf("assignment items = %d, want 2: %#v", len(assignments), assignments) + } + if assignments[0]["id"] != "asg-items" || assignments[1]["id"] != "asg-entries" { + t.Fatalf("assignment order/content mismatch: %#v", assignments) + } + progress := RuntimeViewItems(view, "progress_digest") + if len(progress) != 1 || progress[0]["id"] != "pg-1" { + t.Fatalf("progress items mismatch: %#v", progress) + } +} + func TestRuntimeViewItemsMatchScopeAndRootIngest(t *testing.T) { scope := RuntimeScopeMaterial{SessionID: "multica:session:root-1", RootIssueID: "root-1"} if !RuntimeAssignmentMatchesScope(RuntimeAssignmentItem{ From 2366bb222e4c886a12dd649b0407407973486517 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:19:24 +0800 Subject: [PATCH 081/117] refactor: move multica assignment projection metadata Move assignment mailbox fingerprint, ledger source, and Multica metadata construction into the Multica surface adapter. The runtime hub writer now passes runtime item and issue context into AssignmentMailboxProjectionForRuntimeItem instead of owning those adapter metadata rules. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 50 ++++---------- harness/internal/surface/multica/hub.go | 66 +++++++++++++++++++ harness/internal/surface/multica/hub_test.go | 55 ++++++++++++++++ 3 files changed, 132 insertions(+), 39 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 50c8fad0..2a07b1fd 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -10,7 +10,6 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/contract" "github.com/mnemon-dev/mnemon/harness/internal/driver" - eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" pview "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view" "github.com/mnemon-dev/mnemon/harness/internal/projection" @@ -104,26 +103,17 @@ func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driv if !ok || strings.TrimSpace(participant.AgentID) == "" { return fmt.Errorf("no Multica agent mapping for assignment assignee %q", item.Assignee) } - fingerprint := driver.MulticaAssignmentFingerprint(driver.MulticaAssignmentFingerprintInput{ - AssignmentID: item.ID, - Assignee: item.Assignee, - Scope: item.Scope, - ExpectedWork: item.ExpectedWork, - ExpectedFeedback: item.ExpectedFeedback, - SignalRef: item.SignalRef, - ContextRefs: item.ContextRefs, - EvidenceRefs: item.EvidenceRefs, - CorrelationID: result.CorrelationID, + projection := multicasurface.AssignmentMailboxProjectionForRuntimeItem(multicasurface.AssignmentMailboxProjectionMaterial{ + Item: item, + SessionID: result.SessionID, + CorrelationID: result.CorrelationID, + RootIssueID: result.RootIssueID, + SourceIssueID: rootIssue.ID, + ProjectionOwner: result.Principal, + MulticaAgentID: participant.AgentID, + ProjectedAt: s.now(), }) - source := driver.MulticaHubLedgerSource{ - SessionID: result.SessionID, - CorrelationID: result.CorrelationID, - EventID: item.EventID, - AssignmentID: item.ID, - AssignmentFingerprint: fingerprint, - Principal: item.Assignee, - ProjectionKind: "assignment", - } + source := projection.Source if _, ok, err := ledger.Find(driver.MulticaHubKindAssignmentMailbox, source); err != nil { return err } else if ok { @@ -145,29 +135,11 @@ func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driv } continue } - meta := driver.MulticaHubMetadata{ - SchemaVersion: "1", - HubBackend: driver.MulticaHubBackend, - Kind: driver.MulticaHubKindAssignmentMailbox, - SessionID: result.SessionID, - CorrelationID: result.CorrelationID, - EventID: item.EventID, - EventType: "assignment.accepted", - EventPhase: string(eventmodel.PhaseAccepted), - AssignmentID: item.ID, - AssignmentFingerprint: fingerprint, - Principal: item.Assignee, - SourceIssueID: rootIssue.ID, - RootIssueID: result.RootIssueID, - ProjectionOwner: result.Principal, - MulticaAgentID: participant.AgentID, - ProjectedAt: s.now().UTC().Format(time.RFC3339), - } projections = append(projections, runtimeAssignmentProjection{ Item: item, Participant: participant, Source: source, - Metadata: meta, + Metadata: projection.Metadata, RootIssue: rootIssue, Result: result, }) diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go index 26e63a81..646af76e 100644 --- a/harness/internal/surface/multica/hub.go +++ b/harness/internal/surface/multica/hub.go @@ -72,6 +72,23 @@ type RootSessionMetadataMaterial struct { ProjectedAt time.Time } +type AssignmentMailboxProjectionMaterial struct { + Item RuntimeAssignmentItem + SessionID string + CorrelationID string + RootIssueID string + SourceIssueID string + ProjectionOwner string + MulticaAgentID string + ProjectedAt time.Time +} + +type AssignmentMailboxProjection struct { + Fingerprint string + Source MulticaHubLedgerSource + Metadata MulticaHubMetadata +} + type MulticaAssignmentFingerprintInput struct { AssignmentID string `json:"assignment_id,omitempty"` Assignee string `json:"assignee,omitempty"` @@ -376,6 +393,55 @@ func RootSessionMetadataMap(material RootSessionMetadataMaterial) map[string]str return meta.Map() } +func AssignmentMailboxProjectionForRuntimeItem(material AssignmentMailboxProjectionMaterial) AssignmentMailboxProjection { + item := material.Item + fingerprint := MulticaAssignmentFingerprint(MulticaAssignmentFingerprintInput{ + AssignmentID: item.ID, + Assignee: item.Assignee, + Scope: item.Scope, + ExpectedWork: item.ExpectedWork, + ExpectedFeedback: item.ExpectedFeedback, + SignalRef: item.SignalRef, + ContextRefs: item.ContextRefs, + EvidenceRefs: item.EvidenceRefs, + CorrelationID: material.CorrelationID, + }) + source := MulticaHubLedgerSource{ + SessionID: material.SessionID, + CorrelationID: material.CorrelationID, + EventID: item.EventID, + AssignmentID: item.ID, + AssignmentFingerprint: fingerprint, + Principal: item.Assignee, + ProjectionKind: "assignment", + } + meta := MulticaHubMetadata{ + SchemaVersion: "1", + HubBackend: MulticaHubBackend, + Kind: MulticaHubKindAssignmentMailbox, + SessionID: material.SessionID, + CorrelationID: material.CorrelationID, + EventID: item.EventID, + EventType: "assignment.accepted", + EventPhase: "accepted", + AssignmentID: item.ID, + AssignmentFingerprint: fingerprint, + Principal: item.Assignee, + SourceIssueID: material.SourceIssueID, + RootIssueID: material.RootIssueID, + ProjectionOwner: material.ProjectionOwner, + MulticaAgentID: material.MulticaAgentID, + } + if !material.ProjectedAt.IsZero() { + meta.ProjectedAt = material.ProjectedAt.UTC().Format(time.RFC3339) + } + return AssignmentMailboxProjection{ + Fingerprint: fingerprint, + Source: source, + Metadata: meta, + } +} + func AssignmentMailboxDispatchMetadata(full map[string]string) map[string]string { keys := []string{ MulticaMetadataSchemaVersion, diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go index 6db60b7b..fd90d0ad 100644 --- a/harness/internal/surface/multica/hub_test.go +++ b/harness/internal/surface/multica/hub_test.go @@ -130,6 +130,61 @@ func TestRootSessionMetadataMap(t *testing.T) { } } +func TestAssignmentMailboxProjectionForRuntimeItem(t *testing.T) { + now := time.Unix(200, 0).UTC() + projection := AssignmentMailboxProjectionForRuntimeItem(AssignmentMailboxProjectionMaterial{ + Item: RuntimeAssignmentItem{ + ID: " assignment-1 ", + EventID: "event-1", + Assignee: "worker@team", + Scope: "release/readiness", + ExpectedWork: "Check release notes.", + ExpectedFeedback: "result or blocker", + SignalRef: "sig-1", + ContextRefs: []string{"ctx-b", "ctx-a", "ctx-a"}, + EvidenceRefs: []string{" ev-1 "}, + }, + SessionID: "multica:session:root-1", + CorrelationID: "multica:issue:root-1", + RootIssueID: "root-1", + SourceIssueID: "root-1", + ProjectionOwner: "planner@team", + MulticaAgentID: "agent-worker", + ProjectedAt: now, + }) + if projection.Fingerprint == "" || !strings.HasPrefix(projection.Fingerprint, "sha256:") { + t.Fatalf("fingerprint missing algorithm prefix: %+v", projection) + } + if projection.Source.SessionID != "multica:session:root-1" || + projection.Source.CorrelationID != "multica:issue:root-1" || + projection.Source.EventID != "event-1" || + projection.Source.AssignmentID != " assignment-1 " || + projection.Source.AssignmentFingerprint != projection.Fingerprint || + projection.Source.Principal != "worker@team" || + projection.Source.ProjectionKind != "assignment" { + t.Fatalf("source mismatch: %+v", projection.Source) + } + meta := projection.Metadata + if meta.SchemaVersion != "1" || + meta.HubBackend != MulticaHubBackend || + meta.Kind != MulticaHubKindAssignmentMailbox || + meta.SessionID != "multica:session:root-1" || + meta.CorrelationID != "multica:issue:root-1" || + meta.EventID != "event-1" || + meta.EventType != "assignment.accepted" || + meta.EventPhase != "accepted" || + meta.AssignmentID != " assignment-1 " || + meta.AssignmentFingerprint != projection.Fingerprint || + meta.Principal != "worker@team" || + meta.SourceIssueID != "root-1" || + meta.RootIssueID != "root-1" || + meta.ProjectionOwner != "planner@team" || + meta.MulticaAgentID != "agent-worker" || + meta.ProjectedAt != now.Format(time.RFC3339) { + t.Fatalf("metadata mismatch: %+v", meta) + } +} + func TestAssignmentMailboxMetadataGroupsDispatchBeforeSupplemental(t *testing.T) { full := map[string]string{ MulticaMetadataSchemaVersion: " 1 ", From bea2f42c0d8e411fca9c920fc3f061de8d6ec5da Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:22:33 +0800 Subject: [PATCH 082/117] refactor: move multica progress projection metadata Move progress feedback source, feedback material, and comment marker construction into the Multica surface adapter. The runtime hub writer now consumes ProgressFeedbackProjectionForRuntimeItem and only performs target lookup, CLI comment writes, status updates, and ledger recording. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 45 ++++------------- .../cmd/mnemon-multica-runtime/main_test.go | 2 +- .../internal/surface/multica/projection.go | 50 +++++++++++++++++++ .../surface/multica/projection_test.go | 42 ++++++++++++++++ 4 files changed, 103 insertions(+), 36 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 2a07b1fd..7b73e61f 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -12,7 +12,6 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/driver" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" pview "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view" - "github.com/mnemon-dev/mnemon/harness/internal/projection" multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) @@ -275,16 +274,12 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if !runtimeItemAfterRootIngest(item.IngestSeq, result) { continue } - source := driver.MulticaHubLedgerSource{ - SessionID: result.SessionID, - CorrelationID: result.CorrelationID, - EventID: item.EventID, - AssignmentID: item.AssignmentRef, - Principal: item.Actor, - ProjectionKind: "progress", - } - material := progressFeedbackMaterial(item) - if rec, ok, err := ledger.Find(driver.MulticaHubKindFeedbackCarrier, source); err != nil { + progressProjection := multicasurface.ProgressFeedbackProjectionForRuntimeItem(multicasurface.ProgressFeedbackProjectionMaterial{ + Item: item, + SessionID: result.SessionID, + CorrelationID: result.CorrelationID, + }) + if rec, ok, err := ledger.Find(driver.MulticaHubKindFeedbackCarrier, progressProjection.Source); err != nil { return err } else if ok { child := strings.TrimSpace(rec.Target.ChildIssueID) @@ -307,7 +302,7 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if !runtimeProgressMatchesCurrentMulticaScope(item, result, child) { continue } - if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, material); err != nil { + if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, progressProjection.Feedback); err != nil { return err } continue @@ -328,24 +323,16 @@ func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver. if !runtimeProgressMatchesCurrentMulticaScope(item, result, child) { continue } - commentBody := projection.FormatComment(projection.CommentMaterial{ - Title: "assignment feedback", - Body: multicasurface.ProgressCommentBody(material), - EventIDs: []string{item.EventID}, - EventType: "progress_digest.accepted", - SessionID: result.SessionID, - AssignmentID: item.AssignmentRef, - }) - comment, err := cli.AddIssueComment(ctx, child, commentBody) + comment, err := cli.AddIssueComment(ctx, child, progressProjection.CommentBody) if err != nil { return err } - if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, material); err != nil { + if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, progressProjection.Feedback); err != nil { return err } if err := ledger.Record(driver.MulticaHubLedgerRecord{ Kind: driver.MulticaHubKindFeedbackCarrier, - Source: source, + Source: progressProjection.Source, Target: driver.MulticaHubLedgerTarget{ RootIssueID: result.RootIssueID, ChildIssueID: child, @@ -474,18 +461,6 @@ func assignmentMailboxMaterial(item multicasurface.RuntimeAssignmentItem, result return material } -func progressFeedbackMaterial(item multicasurface.RuntimeProgressItem) multicasurface.ProgressFeedbackMaterial { - return multicasurface.ProgressFeedbackMaterial{ - AssignmentRef: item.AssignmentRef, - FeedbackKind: item.FeedbackKind, - Summary: item.Summary, - Result: item.Result, - Blocker: item.Blocker, - ArtifactRefs: item.ArtifactRefs, - EvidenceRefs: item.EvidenceRefs, - } -} - func findExistingMulticaAssignmentIssue(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, source driver.MulticaHubLedgerSource) (driver.MulticaIssue, bool, error) { children, err := cli.ListIssueChildren(ctx, rootIssueID) if err != nil { diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 6d468d8a..bf3cee73 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -1646,7 +1646,7 @@ func TestMulticaStatusForProgressIsRuleBased(t *testing.T) { {name: "result narrative fallback", item: multicasurface.RuntimeProgressItem{Result: "validated"}, want: "done"}, {name: "unknown", item: multicasurface.RuntimeProgressItem{Summary: "not enough signal"}, want: ""}, } { - if got := multicasurface.ProgressIssueStatus(progressFeedbackMaterial(tc.item)); got != tc.want { + if got := multicasurface.ProgressIssueStatus(multicasurface.ProgressFeedbackMaterialForRuntimeItem(tc.item)); got != tc.want { t.Fatalf("%s: status = %q, want %q", tc.name, got, tc.want) } } diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index 2ff153f0..08c1e633 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -3,6 +3,8 @@ package multica import ( "fmt" "strings" + + commentprojection "github.com/mnemon-dev/mnemon/harness/internal/projection" ) type AssignmentMailboxMaterial struct { @@ -60,6 +62,18 @@ type RuntimeProjectionMaterial struct { HubFeedbackComment int } +type ProgressFeedbackProjectionMaterial struct { + Item RuntimeProgressItem + SessionID string + CorrelationID string +} + +type ProgressFeedbackProjection struct { + Source MulticaHubLedgerSource + Feedback ProgressFeedbackMaterial + CommentBody string +} + func AssignmentMailboxTitle(item AssignmentMailboxMaterial) string { topic := assignmentTitleTopic(item) root := strings.TrimSpace(item.RootIssueLabel) @@ -134,6 +148,42 @@ func AssignmentMailboxDescription(item AssignmentMailboxMaterial) string { return strings.TrimSpace(b.String()) } +func ProgressFeedbackProjectionForRuntimeItem(material ProgressFeedbackProjectionMaterial) ProgressFeedbackProjection { + item := material.Item + feedback := ProgressFeedbackMaterialForRuntimeItem(item) + return ProgressFeedbackProjection{ + Source: MulticaHubLedgerSource{ + SessionID: material.SessionID, + CorrelationID: material.CorrelationID, + EventID: item.EventID, + AssignmentID: item.AssignmentRef, + Principal: item.Actor, + ProjectionKind: "progress", + }, + Feedback: feedback, + CommentBody: commentprojection.FormatComment(commentprojection.CommentMaterial{ + Title: "assignment feedback", + Body: ProgressCommentBody(feedback), + EventIDs: []string{item.EventID}, + EventType: "progress_digest.accepted", + SessionID: material.SessionID, + AssignmentID: item.AssignmentRef, + }), + } +} + +func ProgressFeedbackMaterialForRuntimeItem(item RuntimeProgressItem) ProgressFeedbackMaterial { + return ProgressFeedbackMaterial{ + AssignmentRef: item.AssignmentRef, + FeedbackKind: item.FeedbackKind, + Summary: item.Summary, + Result: item.Result, + Blocker: item.Blocker, + ArtifactRefs: item.ArtifactRefs, + EvidenceRefs: item.EvidenceRefs, + } +} + func ProgressCommentBody(item ProgressFeedbackMaterial) string { var b strings.Builder b.WriteString("## Feedback\n\n") diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 1d9f3b41..42ba35b7 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -246,6 +246,48 @@ func TestProgressFeedbackMaterial(t *testing.T) { } } +func TestProgressFeedbackProjectionForRuntimeItem(t *testing.T) { + projection := ProgressFeedbackProjectionForRuntimeItem(ProgressFeedbackProjectionMaterial{ + Item: RuntimeProgressItem{ + EventID: "pg-1", + Actor: "worker@team", + AssignmentRef: "asg-1", + FeedbackKind: "result", + Summary: "Validated the assignment mailbox.", + Result: "Comment projection is visible in Multica.", + ArtifactRefs: []string{"run-1"}, + EvidenceRefs: []string{"comment-1"}, + }, + SessionID: "multica:session:root-1", + CorrelationID: "multica:issue:root-1", + }) + if projection.Source.SessionID != "multica:session:root-1" || + projection.Source.CorrelationID != "multica:issue:root-1" || + projection.Source.EventID != "pg-1" || + projection.Source.AssignmentID != "asg-1" || + projection.Source.Principal != "worker@team" || + projection.Source.ProjectionKind != "progress" { + t.Fatalf("progress source mismatch: %+v", projection.Source) + } + if projection.Feedback.AssignmentRef != "asg-1" || projection.Feedback.FeedbackKind != "result" { + t.Fatalf("feedback material mismatch: %+v", projection.Feedback) + } + for _, want := range []string{ + "Mnemon update: assignment feedback", + "Status: result", + "Validated the assignment mailbox.", + "Comment projection is visible in Multica.", + "mnemon:event=pg-1", + "mnemon:type=progress_digest.accepted", + "mnemon:session=multica:session:root-1", + "mnemon:assignment=asg-1", + } { + if !strings.Contains(projection.CommentBody, want) { + t.Fatalf("comment body missing %q:\n%s", want, projection.CommentBody) + } + } +} + func TestRuntimeProjectionCommentBodyForIntake(t *testing.T) { body := RuntimeProjectionCommentBody(RuntimeProjectionMaterial{ Status: "recorded", From 3d50f611d98a6db681e5f8f2d20cf8e3dcf2b0ad Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:26:01 +0800 Subject: [PATCH 083/117] refactor: move multica assignment target selection Move assignment mailbox target candidate extraction and duplicate-assignment disambiguation into the Multica surface adapter. Runtime hub writing now maps ledger records and child metadata to surface candidates and delegates principal-aware target selection. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 51 ++++----------- harness/internal/surface/multica/hub.go | 63 +++++++++++++++++++ harness/internal/surface/multica/hub_test.go | 61 ++++++++++++++++++ 3 files changed, 136 insertions(+), 39 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 7b73e61f..b2829d99 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -497,29 +497,13 @@ func findAssignmentTargetFromLedger(ledger *driver.FileMulticaHubLedger, session if err != nil { return "", false, err } - var fallback string - matches := 0 - principal = strings.TrimSpace(principal) - for i := len(records) - 1; i >= 0; i-- { - record := records[i] - if record.Kind != driver.MulticaHubKindAssignmentMailbox { - continue - } - if record.Source.SessionID != sessionID || record.Source.AssignmentID != assignmentID || strings.TrimSpace(record.Target.ChildIssueID) == "" { - continue - } - matches++ - if principal != "" && record.Source.Principal == principal { - return record.Target.ChildIssueID, true, nil - } - if fallback == "" { - fallback = record.Target.ChildIssueID - } - } - if principal == "" || matches == 1 { - return fallback, fallback != "", nil - } - return "", false, nil + target, ok := multicasurface.SelectAssignmentTarget( + multicasurface.AssignmentTargetCandidatesFromLedgerRecords(records), + sessionID, + assignmentID, + principal, + ) + return target, ok, nil } func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaCLI, rootIssueID, sessionID, assignmentID, principal string) (string, bool, error) { @@ -527,9 +511,7 @@ func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaC if err != nil { return "", false, err } - var fallback string - matches := 0 - principal = strings.TrimSpace(principal) + candidates := []multicasurface.AssignmentTargetCandidate{} for _, child := range children { meta := driver.MulticaIssueHubMetadata(child) if !meta.IsAssignmentMailbox() { @@ -542,21 +524,12 @@ func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaC if !meta.IsAssignmentMailbox() { continue } - if meta.SessionID != sessionID || meta.AssignmentID != assignmentID || strings.TrimSpace(child.ID) == "" { - continue - } - matches++ - if principal != "" && meta.Principal == principal { - return child.ID, true, nil - } - if fallback == "" { - fallback = child.ID + if candidate, ok := multicasurface.AssignmentTargetCandidateFromMailboxMetadata(child.ID, meta); ok { + candidates = append(candidates, candidate) } } - if principal == "" || matches == 1 { - return fallback, fallback != "", nil - } - return "", false, nil + target, ok := multicasurface.SelectAssignmentTarget(candidates, sessionID, assignmentID, principal) + return target, ok, nil } func allMulticaAssignmentChildrenDone(ctx context.Context, cli driver.MulticaCLI, rootIssueID, sessionID, justCompletedChildID string) (bool, error) { diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go index 646af76e..537e5882 100644 --- a/harness/internal/surface/multica/hub.go +++ b/harness/internal/surface/multica/hub.go @@ -127,6 +127,13 @@ type MulticaHubLedgerTarget struct { Status string `json:"status,omitempty"` } +type AssignmentTargetCandidate struct { + SessionID string + AssignmentID string + Principal string + ChildIssueID string +} + type FileMulticaHubLedger struct { path string loaded bool @@ -265,6 +272,62 @@ func (l *FileMulticaHubLedger) upsertLoaded(record MulticaHubLedgerRecord) { l.records = append(l.records, record) } +func AssignmentTargetCandidatesFromLedgerRecords(records []MulticaHubLedgerRecord) []AssignmentTargetCandidate { + out := []AssignmentTargetCandidate{} + for i := len(records) - 1; i >= 0; i-- { + record := records[i] + if record.Kind != MulticaHubKindAssignmentMailbox { + continue + } + out = append(out, AssignmentTargetCandidate{ + SessionID: record.Source.SessionID, + AssignmentID: record.Source.AssignmentID, + Principal: record.Source.Principal, + ChildIssueID: record.Target.ChildIssueID, + }) + } + return out +} + +func AssignmentTargetCandidateFromMailboxMetadata(childIssueID string, meta MulticaHubMetadata) (AssignmentTargetCandidate, bool) { + if !meta.IsAssignmentMailbox() { + return AssignmentTargetCandidate{}, false + } + return AssignmentTargetCandidate{ + SessionID: meta.SessionID, + AssignmentID: meta.AssignmentID, + Principal: meta.Principal, + ChildIssueID: childIssueID, + }, true +} + +func SelectAssignmentTarget(candidates []AssignmentTargetCandidate, sessionID, assignmentID, principal string) (string, bool) { + sessionID = strings.TrimSpace(sessionID) + assignmentID = strings.TrimSpace(assignmentID) + principal = strings.TrimSpace(principal) + var fallback string + matches := 0 + for _, candidate := range candidates { + childIssueID := strings.TrimSpace(candidate.ChildIssueID) + if strings.TrimSpace(candidate.SessionID) != sessionID || + strings.TrimSpace(candidate.AssignmentID) != assignmentID || + childIssueID == "" { + continue + } + matches++ + if principal != "" && strings.TrimSpace(candidate.Principal) == principal { + return childIssueID, true + } + if fallback == "" { + fallback = childIssueID + } + } + if principal == "" || matches == 1 { + return fallback, fallback != "" + } + return "", false +} + func multicaHubLedgerKey(kind string, source MulticaHubLedgerSource) string { return strings.Join([]string{ strings.TrimSpace(kind), diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go index fd90d0ad..5048362f 100644 --- a/harness/internal/surface/multica/hub_test.go +++ b/harness/internal/surface/multica/hub_test.go @@ -283,3 +283,64 @@ func TestFileHubLedgerDedupesRecords(t *testing.T) { t.Fatalf("ledger find mismatch: ok=%v record=%+v", ok, found) } } + +func TestSelectAssignmentTargetDisambiguatesDuplicateAssignments(t *testing.T) { + candidates := []AssignmentTargetCandidate{ + {SessionID: "session-1", AssignmentID: "asg-1", Principal: "implementer@team", ChildIssueID: "child-impl"}, + {SessionID: "session-1", AssignmentID: "asg-1", Principal: "researcher@team", ChildIssueID: "child-research"}, + } + if got, ok := SelectAssignmentTarget(candidates, "session-1", "asg-1", "researcher@team"); !ok || got != "child-research" { + t.Fatalf("principal target = %q ok=%v", got, ok) + } + if got, ok := SelectAssignmentTarget(candidates, "session-1", "asg-1", "writer@team"); ok || got != "" { + t.Fatalf("ambiguous unmatched principal should not fallback, got %q ok=%v", got, ok) + } + if got, ok := SelectAssignmentTarget(candidates[:1], "session-1", "asg-1", "writer@team"); !ok || got != "child-impl" { + t.Fatalf("single candidate fallback = %q ok=%v", got, ok) + } + if got, ok := SelectAssignmentTarget(candidates, "session-1", "asg-1", ""); !ok || got != "child-impl" { + t.Fatalf("empty principal fallback = %q ok=%v", got, ok) + } +} + +func TestAssignmentTargetCandidatesFromLedgerRecordsPreferNewestRecordOrder(t *testing.T) { + records := []MulticaHubLedgerRecord{ + { + Kind: MulticaHubKindAssignmentMailbox, + Source: MulticaHubLedgerSource{ + SessionID: "session-1", + AssignmentID: "asg-1", + Principal: "worker@team", + }, + Target: MulticaHubLedgerTarget{ChildIssueID: "child-old"}, + }, + { + Kind: MulticaHubKindAssignmentMailbox, + Source: MulticaHubLedgerSource{ + SessionID: "session-1", + AssignmentID: "asg-1", + Principal: "worker@team", + }, + Target: MulticaHubLedgerTarget{ChildIssueID: "child-new"}, + }, + } + if got, ok := SelectAssignmentTarget(AssignmentTargetCandidatesFromLedgerRecords(records), "session-1", "asg-1", "worker@team"); !ok || got != "child-new" { + t.Fatalf("ledger target = %q ok=%v", got, ok) + } +} + +func TestAssignmentTargetCandidateFromMailboxMetadata(t *testing.T) { + candidate, ok := AssignmentTargetCandidateFromMailboxMetadata("child-1", MulticaHubMetadata{ + HubBackend: MulticaHubBackend, + Kind: MulticaHubKindAssignmentMailbox, + SessionID: "session-1", + AssignmentID: "asg-1", + Principal: "worker@team", + }) + if !ok || candidate.ChildIssueID != "child-1" || candidate.SessionID != "session-1" || candidate.AssignmentID != "asg-1" || candidate.Principal != "worker@team" { + t.Fatalf("candidate mismatch: ok=%v candidate=%+v", ok, candidate) + } + if _, ok := AssignmentTargetCandidateFromMailboxMetadata("child-2", MulticaHubMetadata{Kind: MulticaHubKindSession}); ok { + t.Fatal("non-assignment mailbox metadata should not produce a candidate") + } +} From 81db85a6627eb628b90b3fab5503ab7b6f7066af Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:28:40 +0800 Subject: [PATCH 084/117] refactor: move multica assignment mailbox matching Move assignment mailbox metadata/source matching into the Multica surface adapter. Runtime hub writing now delegates existing child issue reuse checks to AssignmentMailboxMatchesSource instead of comparing adapter metadata fields inline. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 5 +--- harness/internal/surface/multica/hub.go | 8 ++++++ harness/internal/surface/multica/hub_test.go | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index b2829d99..558548ad 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -482,10 +482,7 @@ func findExistingMulticaAssignmentIssueInChildren(ctx context.Context, cli drive if !meta.IsAssignmentMailbox() { continue } - if meta.SessionID == source.SessionID && - meta.AssignmentID == source.AssignmentID && - meta.AssignmentFingerprint == source.AssignmentFingerprint && - meta.Principal == source.Principal { + if multicasurface.AssignmentMailboxMatchesSource(meta, source) { return child, true, nil } } diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go index 537e5882..46052484 100644 --- a/harness/internal/surface/multica/hub.go +++ b/harness/internal/surface/multica/hub.go @@ -404,6 +404,14 @@ func (m MulticaHubMetadata) IsAssignmentMailbox() bool { } } +func AssignmentMailboxMatchesSource(meta MulticaHubMetadata, source MulticaHubLedgerSource) bool { + return meta.IsAssignmentMailbox() && + strings.TrimSpace(meta.SessionID) == strings.TrimSpace(source.SessionID) && + strings.TrimSpace(meta.AssignmentID) == strings.TrimSpace(source.AssignmentID) && + strings.TrimSpace(meta.AssignmentFingerprint) == strings.TrimSpace(source.AssignmentFingerprint) && + strings.TrimSpace(meta.Principal) == strings.TrimSpace(source.Principal) +} + func RootSessionHubMetadata(meta MulticaHubMetadata, issueID string) MulticaHubMetadata { issueID = strings.TrimSpace(issueID) meta.HubBackend = MulticaHubBackend diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go index 5048362f..d449dad9 100644 --- a/harness/internal/surface/multica/hub_test.go +++ b/harness/internal/surface/multica/hub_test.go @@ -87,6 +87,34 @@ func TestAssignmentMailboxHubMetadataDefaults(t *testing.T) { } } +func TestAssignmentMailboxMatchesSource(t *testing.T) { + source := MulticaHubLedgerSource{ + SessionID: "session-1", + AssignmentID: "asg-1", + AssignmentFingerprint: "sha256:abc", + Principal: "worker@team", + } + meta := MulticaHubMetadata{ + Kind: MulticaHubKindAssignmentMailbox, + SessionID: " session-1 ", + AssignmentID: " asg-1 ", + AssignmentFingerprint: " sha256:abc ", + Principal: " worker@team ", + } + if !AssignmentMailboxMatchesSource(meta, source) { + t.Fatalf("metadata should match source: meta=%+v source=%+v", meta, source) + } + meta.Principal = "other@team" + if AssignmentMailboxMatchesSource(meta, source) { + t.Fatalf("principal mismatch should not match: meta=%+v source=%+v", meta, source) + } + meta.Principal = "worker@team" + meta.Kind = MulticaHubKindSession + if AssignmentMailboxMatchesSource(meta, source) { + t.Fatal("non-assignment mailbox metadata should not match") + } +} + func TestAssignmentMailboxMarkerPrefersEventThenAssignmentThenIssue(t *testing.T) { if got := AssignmentMailboxMarker(MulticaHubMetadata{EventID: "event-1", AssignmentID: "asg-1"}, "child-1"); got != "event-1" { t.Fatalf("event marker = %q", got) From 42c3026a314520a5e070c9f363bf80e8992f49a6 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:31:41 +0800 Subject: [PATCH 085/117] refactor: move multica assignment mailbox material Move runtime assignment item to visible assignment mailbox material mapping into the Multica surface adapter. The runtime hub writer now passes root issue and participant fields into AssignmentMailboxMaterialForRuntimeItem before creating child issue projections. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 22 ++++++------- .../internal/surface/multica/projection.go | 29 +++++++++++++++++ .../surface/multica/projection_test.go | 32 +++++++++++++++++++ 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 558548ad..520ee8a4 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -442,23 +442,19 @@ func runtimeItemAfterRootIngest(ingestSeq int64, result *runtimeImportResult) bo } func assignmentMailboxMaterial(item multicasurface.RuntimeAssignmentItem, result *runtimeImportResult, rootIssue driver.MulticaIssue, participant driver.MulticaParticipantRecord) multicasurface.AssignmentMailboxMaterial { - material := multicasurface.AssignmentMailboxMaterial{ - ID: item.ID, - Scope: item.Scope, - Assignee: item.Assignee, - AssigneeDisplay: firstNonEmpty(participant.AgentName, participant.AgentID), - RootIssueID: rootIssue.ID, - RootIssueLabel: firstNonEmpty(rootIssue.Identifier, rootIssue.ID), - RootIssueTitle: rootIssue.Title, - ExpectedWork: item.ExpectedWork, - ExpectedFeedback: item.ExpectedFeedback, - Rationale: item.Rationale, + material := multicasurface.AssignmentMailboxRuntimeMaterial{ + Item: item, + RootIssueID: rootIssue.ID, + RootIssueIdentifier: rootIssue.Identifier, + RootIssueTitle: rootIssue.Title, + AssigneeAgentName: participant.AgentName, + AssigneeAgentID: participant.AgentID, } if result != nil { material.SessionID = result.SessionID - material.RootIssueID = firstNonEmpty(material.RootIssueID, result.RootIssueID) + material.FallbackRootIssueID = result.RootIssueID } - return material + return multicasurface.AssignmentMailboxMaterialForRuntimeItem(material) } func findExistingMulticaAssignmentIssue(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, source driver.MulticaHubLedgerSource) (driver.MulticaIssue, bool, error) { diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go index 08c1e633..b420c52e 100644 --- a/harness/internal/surface/multica/projection.go +++ b/harness/internal/surface/multica/projection.go @@ -21,6 +21,17 @@ type AssignmentMailboxMaterial struct { Rationale string } +type AssignmentMailboxRuntimeMaterial struct { + Item RuntimeAssignmentItem + SessionID string + RootIssueID string + FallbackRootIssueID string + RootIssueIdentifier string + RootIssueTitle string + AssigneeAgentName string + AssigneeAgentID string +} + type RootSessionMaterial struct { Request string WorkMode string @@ -86,6 +97,24 @@ func AssignmentMailboxTitle(item AssignmentMailboxMaterial) string { return trimTitle("Assignment: " + topic) } +func AssignmentMailboxMaterialForRuntimeItem(material AssignmentMailboxRuntimeMaterial) AssignmentMailboxMaterial { + item := material.Item + rootIssueID := firstNonEmptyString(material.RootIssueID, material.FallbackRootIssueID) + return AssignmentMailboxMaterial{ + ID: item.ID, + SessionID: material.SessionID, + Scope: item.Scope, + Assignee: item.Assignee, + AssigneeDisplay: firstNonEmptyString(material.AssigneeAgentName, material.AssigneeAgentID), + RootIssueID: rootIssueID, + RootIssueLabel: firstNonEmptyString(material.RootIssueIdentifier, rootIssueID), + RootIssueTitle: material.RootIssueTitle, + ExpectedWork: item.ExpectedWork, + ExpectedFeedback: item.ExpectedFeedback, + Rationale: item.Rationale, + } +} + func RootSessionDescription(item RootSessionMaterial) string { var b strings.Builder request := strings.TrimSpace(item.Request) diff --git a/harness/internal/surface/multica/projection_test.go b/harness/internal/surface/multica/projection_test.go index 42ba35b7..1d93549b 100644 --- a/harness/internal/surface/multica/projection_test.go +++ b/harness/internal/surface/multica/projection_test.go @@ -81,6 +81,38 @@ func TestAssignmentMailboxMaterial(t *testing.T) { } } +func TestAssignmentMailboxMaterialForRuntimeItem(t *testing.T) { + item := AssignmentMailboxMaterialForRuntimeItem(AssignmentMailboxRuntimeMaterial{ + Item: RuntimeAssignmentItem{ + ID: "asg-1", + Scope: "runtime/readiness", + Assignee: "researcher@team", + ExpectedWork: "Check runtime output.", + ExpectedFeedback: "Brief result.", + Rationale: "Route to the researcher mailbox.", + }, + SessionID: "multica:session:root-1", + RootIssueID: "root-1", + FallbackRootIssueID: "fallback-root", + RootIssueIdentifier: "TEA-1", + RootIssueTitle: "Validate runtime", + AssigneeAgentName: "mnemon-researcher", + AssigneeAgentID: "agent-researcher", + }) + if item.ID != "asg-1" || + item.SessionID != "multica:session:root-1" || + item.RootIssueID != "root-1" || + item.RootIssueLabel != "TEA-1" || + item.RootIssueTitle != "Validate runtime" || + item.Assignee != "researcher@team" || + item.AssigneeDisplay != "mnemon-researcher" || + item.ExpectedWork != "Check runtime output." || + item.ExpectedFeedback != "Brief result." || + item.Rationale != "Route to the researcher mailbox." { + t.Fatalf("assignment mailbox material mismatch: %+v", item) + } +} + func TestAssignmentMailboxTitleStripsRootLabelFromScope(t *testing.T) { item := AssignmentMailboxMaterial{ ID: "asg-1", From bb7c0a0d02485d84a6b7660562f01cf97b68e975 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:35:53 +0800 Subject: [PATCH 086/117] refactor: move multica listed metadata parsing Move parsing of Multica CLI listed metadata into the Multica surface adapter. Runtime hub writing now consumes ParseMulticaHubListedMetadata instead of carrying a local map conversion helper before hub metadata interpretation. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 14 +++----------- harness/internal/surface/multica/hub.go | 9 ++++++++- harness/internal/surface/multica/hub_test.go | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 520ee8a4..1c1b3238 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -473,7 +473,7 @@ func findExistingMulticaAssignmentIssueInChildren(ctx context.Context, cli drive if err != nil { return driver.MulticaIssue{}, false, err } - meta = driver.ParseMulticaHubMetadata(stringMapToAny(listed)) + meta = multicasurface.ParseMulticaHubListedMetadata(listed) } if !meta.IsAssignmentMailbox() { continue @@ -512,7 +512,7 @@ func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaC if err != nil { return "", false, err } - meta = driver.ParseMulticaHubMetadata(stringMapToAny(listed)) + meta = multicasurface.ParseMulticaHubListedMetadata(listed) } if !meta.IsAssignmentMailbox() { continue @@ -538,7 +538,7 @@ func allMulticaAssignmentChildrenDone(ctx context.Context, cli driver.MulticaCLI if err != nil { return false, err } - meta = driver.ParseMulticaHubMetadata(stringMapToAny(listed)) + meta = multicasurface.ParseMulticaHubListedMetadata(listed) } if !meta.IsAssignmentMailbox() || meta.SessionID != sessionID { continue @@ -554,11 +554,3 @@ func allMulticaAssignmentChildrenDone(ctx context.Context, cli driver.MulticaCLI } return seen, nil } - -func stringMapToAny(in map[string]string) map[string]any { - out := make(map[string]any, len(in)) - for key, value := range in { - out[key] = value - } - return out -} diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go index 46052484..b1dbd4fb 100644 --- a/harness/internal/surface/multica/hub.go +++ b/harness/internal/surface/multica/hub.go @@ -342,7 +342,14 @@ func multicaHubLedgerKey(kind string, source MulticaHubLedgerSource) string { } func ParseMulticaHubMetadata(raw map[string]any) MulticaHubMetadata { - meta := NormalizeMulticaMetadata(raw) + return multicaHubMetadataFromMap(NormalizeMulticaMetadata(raw)) +} + +func ParseMulticaHubListedMetadata(raw map[string]string) MulticaHubMetadata { + return multicaHubMetadataFromMap(NormalizeMulticaMetadata(raw)) +} + +func multicaHubMetadataFromMap(meta map[string]string) MulticaHubMetadata { return MulticaHubMetadata{ SchemaVersion: meta[MulticaMetadataSchemaVersion], HubBackend: meta[MulticaMetadataHubBackend], diff --git a/harness/internal/surface/multica/hub_test.go b/harness/internal/surface/multica/hub_test.go index d449dad9..8d7a322f 100644 --- a/harness/internal/surface/multica/hub_test.go +++ b/harness/internal/surface/multica/hub_test.go @@ -29,6 +29,24 @@ func TestHubMetadataDetectsAssignmentMailbox(t *testing.T) { } } +func TestParseMulticaHubListedMetadata(t *testing.T) { + meta := ParseMulticaHubListedMetadata(map[string]string{ + MulticaMetadataHubBackend: MulticaHubBackend, + MulticaMetadataKind: MulticaHubKindAssignmentMailbox, + MulticaMetadataSessionID: "session-1", + MulticaMetadataAssignmentID: "asg-1", + MulticaMetadataAssignmentFingerprint: "sha256:abc", + MulticaMetadataPrincipal: "worker@team", + }) + if !meta.IsAssignmentMailbox() || + meta.SessionID != "session-1" || + meta.AssignmentID != "asg-1" || + meta.AssignmentFingerprint != "sha256:abc" || + meta.Principal != "worker@team" { + t.Fatalf("listed metadata mismatch: %+v", meta) + } +} + func TestAssignmentFingerprintStable(t *testing.T) { left := MulticaAssignmentFingerprint(MulticaAssignmentFingerprintInput{ AssignmentID: " assignment-1 ", From 7591dc40d04a37d8c6b3d4d2458f0d493d9d5d82 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:40:27 +0800 Subject: [PATCH 087/117] refactor: move multica runtime path policy Move Multica runtime registry and hub-ledger path selection into the Multica surface package alongside registry participant lookup. The runtime adapter now delegates these metadata/config rules instead of carrying command-local copies. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 45 +------------ harness/cmd/mnemon-multica-runtime/main.go | 27 +------- .../cmd/mnemon-multica-runtime/main_test.go | 19 ------ harness/internal/surface/multica/registry.go | 41 ++++++++++++ .../internal/surface/multica/registry_test.go | 36 ++++++++++ .../surface/multica/runtime_config.go | 46 +++++++++++++ .../surface/multica/runtime_config_test.go | 66 +++++++++++++++++++ 7 files changed, 193 insertions(+), 87 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index 1c1b3238..bcf4adf5 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -45,9 +45,9 @@ func (s *runtimeRPCState) writeMulticaHubArtifacts(ctx context.Context, cli driv result.HubWriteErr = fmt.Errorf("pull teamwork view for Multica hub write: %w", err) return } - ledger := driver.NewFileMulticaHubLedger(runtimeMulticaHubLedgerPath(s.Env, s.CWD)) + ledger := driver.NewFileMulticaHubLedger(multicasurface.RuntimeMulticaHubLedgerPath(s.Env, s.CWD)) if result.HubKind == driver.MulticaHubKindSession { - reg, ok, err := runtimeMulticaRegistry(s.Env, s.CWD) + reg, ok, err := multicasurface.RuntimeMulticaRegistry(s.Env, s.CWD) if err != nil { result.HubWriteStatus = "failed" result.HubWriteErr = err @@ -98,7 +98,7 @@ func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driv if !runtimeAssignmentMatchesCurrentMulticaScope(item, result) { continue } - participant, ok := multicaParticipantForPrincipal(reg, item.Assignee) + participant, ok := multicasurface.MulticaParticipantForPrincipal(reg, item.Assignee) if !ok || strings.TrimSpace(participant.AgentID) == "" { return fmt.Errorf("no Multica agent mapping for assignment assignee %q", item.Assignee) } @@ -395,45 +395,6 @@ func runtimeMulticaScopeMaterial(result *runtimeImportResult) multicasurface.Run } } -func runtimeMulticaRegistry(env []string, cwd string) (driver.MulticaRegistry, bool, error) { - paths := []string{} - if explicit := multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_REGISTRY"); explicit != "" { - paths = append(paths, explicit) - } - if workspace := multicasurface.RuntimeEnvValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { - paths = append(paths, driver.MulticaRegistryPath(workspace, "")) - } - if strings.TrimSpace(cwd) != "" { - paths = append(paths, driver.MulticaRegistryPath(cwd, "")) - } - for _, path := range paths { - reg, ok, err := driver.LoadMulticaRegistry(path) - if err != nil || ok { - return reg, ok, err - } - } - return driver.MulticaRegistry{}, false, nil -} - -func runtimeMulticaHubLedgerPath(env []string, cwd string) string { - if explicit := multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_HUB_LEDGER"); explicit != "" { - return driver.MulticaHubLedgerPath("", explicit) - } - if workspace := multicasurface.RuntimeEnvValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { - return driver.MulticaHubLedgerPath(workspace, "") - } - return driver.MulticaHubLedgerPath(cwd, "") -} - -func multicaParticipantForPrincipal(reg driver.MulticaRegistry, principal string) (driver.MulticaParticipantRecord, bool) { - for _, participant := range reg.Participants { - if strings.TrimSpace(participant.Principal) == strings.TrimSpace(principal) { - return participant, true - } - } - return driver.MulticaParticipantRecord{}, false -} - func runtimeItemAfterRootIngest(ingestSeq int64, result *runtimeImportResult) bool { if result == nil || result.Receipt == nil || result.Receipt.Seq <= 0 || ingestSeq <= 0 { return true diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index d3414f9c..1f5b9029 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -846,7 +846,7 @@ func (runtimeNoopTurnClient) StartTurn(_ context.Context, query string) (driver. func resolveRuntimePrincipal(env []string, cwd string) string { agentID := multicasurface.RuntimeEnvValue(env, "MULTICA_AGENT_ID") agentName := multicasurface.RuntimeEnvValue(env, "MULTICA_AGENT_NAME") - if principal := principalFromRegistry(env, cwd, agentID, agentName); principal != "" { + if principal := multicasurface.RuntimeMulticaRegistryPrincipal(env, cwd, agentID, agentName); principal != "" { return principal } if principal := multicasurface.RuntimeEnvValue(env, "MNEMON_CONTROL_PRINCIPAL"); principal != "" { @@ -858,31 +858,6 @@ func resolveRuntimePrincipal(env []string, cwd string) string { return "multica@runtime" } -func principalFromRegistry(env []string, cwd, agentID, agentName string) string { - paths := []string{} - if explicit := multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_REGISTRY"); explicit != "" { - paths = append(paths, explicit) - } - if strings.TrimSpace(cwd) != "" { - paths = append(paths, driver.MulticaRegistryPath(cwd, "")) - } - for _, path := range paths { - reg, ok, err := driver.LoadMulticaRegistry(path) - if err != nil || !ok { - continue - } - for _, participant := range reg.Participants { - if agentID != "" && participant.AgentID == agentID && strings.TrimSpace(participant.Principal) != "" { - return strings.TrimSpace(participant.Principal) - } - if agentName != "" && participant.AgentName == agentName && strings.TrimSpace(participant.Principal) != "" { - return strings.TrimSpace(participant.Principal) - } - } - } - return "" -} - func runtimeResultSummary(result runtimeImportResult) multicasurface.RuntimeResultSummary { summary := multicasurface.RuntimeResultSummary{ IssueID: result.IssueID, diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index bf3cee73..ada60542 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -21,25 +21,6 @@ import ( multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) -func TestRuntimeMulticaHubLedgerPathDefaultsToManagedWorkspace(t *testing.T) { - tmp := t.TempDir() - workspace := filepath.Join(tmp, "managed-workspace") - cwd := filepath.Join(tmp, "task-workdir") - got := runtimeMulticaHubLedgerPath([]string{"MNEMON_MANAGED_WORKSPACE=" + workspace}, cwd) - want := filepath.Join(workspace, driver.MulticaDefaultHubLedgerRelPath) - if got != want { - t.Fatalf("hub ledger path = %q, want %q", got, want) - } - explicit := filepath.Join(tmp, "explicit.jsonl") - got = runtimeMulticaHubLedgerPath([]string{ - "MNEMON_MANAGED_WORKSPACE=" + workspace, - "MNEMON_MULTICA_HUB_LEDGER=" + explicit, - }, cwd) - if got != explicit { - t.Fatalf("explicit hub ledger path = %q, want %q", got, explicit) - } -} - func TestMergeRuntimeHubProjectionDeltasPreservesEarlyDispatchCounts(t *testing.T) { result := runtimeImportResult{HubWriteStatus: "noop"} mergeRuntimeHubProjectionDeltas(&result, []runtimeHubProjectionDelta{ diff --git a/harness/internal/surface/multica/registry.go b/harness/internal/surface/multica/registry.go index 881fefb3..ffaaedd6 100644 --- a/harness/internal/surface/multica/registry.go +++ b/harness/internal/surface/multica/registry.go @@ -92,3 +92,44 @@ func UpsertMulticaParticipantRecord(records []MulticaParticipantRecord, next Mul } return append(records, next) } + +func MulticaParticipantForPrincipal(reg MulticaRegistry, principal string) (MulticaParticipantRecord, bool) { + principal = strings.TrimSpace(principal) + for _, participant := range reg.Participants { + if strings.TrimSpace(participant.Principal) == principal { + return participant, true + } + } + return MulticaParticipantRecord{}, false +} + +func MulticaPrincipalForAgent(reg MulticaRegistry, agentID, agentName string) string { + agentID = strings.TrimSpace(agentID) + agentName = strings.TrimSpace(agentName) + for _, participant := range reg.Participants { + principal := strings.TrimSpace(participant.Principal) + if principal == "" { + continue + } + if agentID != "" && strings.TrimSpace(participant.AgentID) == agentID { + return principal + } + if agentName != "" && strings.TrimSpace(participant.AgentName) == agentName { + return principal + } + } + return "" +} + +func RuntimeMulticaRegistryPrincipal(env []string, cwd, agentID, agentName string) string { + for _, path := range RuntimeMulticaRegistryPaths(env, cwd) { + reg, ok, err := LoadMulticaRegistry(path) + if err != nil || !ok { + continue + } + if principal := MulticaPrincipalForAgent(reg, agentID, agentName); principal != "" { + return principal + } + } + return "" +} diff --git a/harness/internal/surface/multica/registry_test.go b/harness/internal/surface/multica/registry_test.go index 598b3c25..4e88f330 100644 --- a/harness/internal/surface/multica/registry_test.go +++ b/harness/internal/surface/multica/registry_test.go @@ -58,3 +58,39 @@ func TestRegistryRoundTrip(t *testing.T) { t.Fatalf("registry mismatch: %+v", got) } } + +func TestMulticaParticipantLookups(t *testing.T) { + reg := MulticaRegistry{Participants: []MulticaParticipantRecord{{ + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-planner", + }}} + participant, ok := MulticaParticipantForPrincipal(reg, " planner@team ") + if !ok || participant.AgentID != "agent-planner" { + t.Fatalf("participant lookup = ok:%v %+v", ok, participant) + } + if got := MulticaPrincipalForAgent(reg, " agent-planner ", ""); got != "planner@team" { + t.Fatalf("principal by agent id = %q", got) + } + if got := MulticaPrincipalForAgent(reg, "", " mnemon-planner "); got != "planner@team" { + t.Fatalf("principal by agent name = %q", got) + } +} + +func TestRuntimeMulticaRegistryPrincipalUsesManagedWorkspace(t *testing.T) { + tmp := t.TempDir() + workspace := filepath.Join(tmp, "managed-workspace") + if err := SaveMulticaRegistry(MulticaRegistryPath(workspace, ""), MulticaRegistry{ + Participants: []MulticaParticipantRecord{{ + Principal: "reviewer@team", + AgentName: "mnemon-reviewer", + AgentID: "agent-reviewer", + }}, + }); err != nil { + t.Fatal(err) + } + got := RuntimeMulticaRegistryPrincipal([]string{"MNEMON_MANAGED_WORKSPACE=" + workspace}, tmp, "agent-reviewer", "") + if got != "reviewer@team" { + t.Fatalf("runtime registry principal = %q", got) + } +} diff --git a/harness/internal/surface/multica/runtime_config.go b/harness/internal/surface/multica/runtime_config.go index 428c8084..0c4449a5 100644 --- a/harness/internal/surface/multica/runtime_config.go +++ b/harness/internal/surface/multica/runtime_config.go @@ -54,6 +54,52 @@ func RuntimeManagedLedgerPath(env []string, workspace string) string { return filepath.Join(root, ".mnemon", "harness", "local", "managed-agent", "wake-ledger.jsonl") } +func RuntimeMulticaHubLedgerPath(env []string, cwd string) string { + if explicit := RuntimeEnvValue(env, "MNEMON_MULTICA_HUB_LEDGER"); explicit != "" { + return MulticaHubLedgerPath("", explicit) + } + if workspace := RuntimeEnvValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { + return MulticaHubLedgerPath(workspace, "") + } + return MulticaHubLedgerPath(cwd, "") +} + +func RuntimeMulticaRegistryPaths(env []string, cwd string) []string { + paths := []string{} + add := func(path string) { + path = strings.TrimSpace(path) + if path == "" { + return + } + for _, existing := range paths { + if existing == path { + return + } + } + paths = append(paths, path) + } + if explicit := RuntimeEnvValue(env, "MNEMON_MULTICA_REGISTRY"); explicit != "" { + add(explicit) + } + if workspace := RuntimeEnvValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { + add(MulticaRegistryPath(workspace, "")) + } + if strings.TrimSpace(cwd) != "" { + add(MulticaRegistryPath(cwd, "")) + } + return paths +} + +func RuntimeMulticaRegistry(env []string, cwd string) (MulticaRegistry, bool, error) { + for _, path := range RuntimeMulticaRegistryPaths(env, cwd) { + reg, ok, err := LoadMulticaRegistry(path) + if err != nil || ok { + return reg, ok, err + } + } + return MulticaRegistry{}, false, nil +} + func RuntimeManagedWakeScopeID(material RuntimeManagedWakeMaterial) string { return firstNonEmptyRuntimeString(material.AssignmentID, material.RootIssueID, material.IssueID) } diff --git a/harness/internal/surface/multica/runtime_config_test.go b/harness/internal/surface/multica/runtime_config_test.go index b54a48bd..28f38b41 100644 --- a/harness/internal/surface/multica/runtime_config_test.go +++ b/harness/internal/surface/multica/runtime_config_test.go @@ -56,6 +56,72 @@ func TestRuntimeManagedLedgerPath(t *testing.T) { } } +func TestRuntimeMulticaHubLedgerPath(t *testing.T) { + tmp := t.TempDir() + workspace := filepath.Join(tmp, "managed-workspace") + cwd := filepath.Join(tmp, "task-workdir") + got := RuntimeMulticaHubLedgerPath([]string{"MNEMON_MANAGED_WORKSPACE=" + workspace}, cwd) + want := filepath.Join(workspace, MulticaDefaultHubLedgerRelPath) + if got != want { + t.Fatalf("hub ledger path = %q, want %q", got, want) + } + explicit := filepath.Join(tmp, "explicit.jsonl") + got = RuntimeMulticaHubLedgerPath([]string{ + "MNEMON_MANAGED_WORKSPACE=" + workspace, + "MNEMON_MULTICA_HUB_LEDGER=" + explicit, + }, cwd) + if got != explicit { + t.Fatalf("explicit hub ledger path = %q, want %q", got, explicit) + } +} + +func TestRuntimeMulticaRegistryPaths(t *testing.T) { + tmp := t.TempDir() + explicit := filepath.Join(tmp, "explicit-registry.json") + workspace := filepath.Join(tmp, "managed-workspace") + cwd := filepath.Join(tmp, "task-workdir") + got := RuntimeMulticaRegistryPaths([]string{ + "MNEMON_MULTICA_REGISTRY=" + explicit, + "MNEMON_MANAGED_WORKSPACE=" + workspace, + }, cwd) + want := []string{ + explicit, + MulticaRegistryPath(workspace, ""), + MulticaRegistryPath(cwd, ""), + } + if len(got) != len(want) { + t.Fatalf("registry paths len = %d, want %d: %+v", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("registry path %d = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestRuntimeMulticaRegistryLoadsManagedWorkspace(t *testing.T) { + tmp := t.TempDir() + workspace := filepath.Join(tmp, "managed-workspace") + path := MulticaRegistryPath(workspace, "") + if err := SaveMulticaRegistry(path, MulticaRegistry{ + WorkspaceID: "ws-1", + Participants: []MulticaParticipantRecord{{ + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-planner", + }}, + }); err != nil { + t.Fatal(err) + } + reg, ok, err := RuntimeMulticaRegistry([]string{"MNEMON_MANAGED_WORKSPACE=" + workspace}, tmp) + if err != nil { + t.Fatal(err) + } + if !ok || reg.WorkspaceID != "ws-1" { + t.Fatalf("registry = ok:%v %+v", ok, reg) + } +} + func TestRuntimeManagedWakeScopeIDPrefersAssignmentThenRoot(t *testing.T) { if got := RuntimeManagedWakeScopeID(RuntimeManagedWakeMaterial{ AssignmentID: "asg-1", From cd8451ca289920020bd861d485becbe873b59773 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:42:41 +0800 Subject: [PATCH 088/117] refactor: centralize multica issue hub metadata lookup Add a driver helper that resolves Multica hub metadata from embedded issue metadata or falls back to issue metadata list when needed. The runtime hub writer now consumes that helper instead of duplicating fallback lookup logic across child-issue scans. Validation: go test -count=1 ./harness/internal/driver ./harness/cmd/mnemon-multica-runtime ./harness/internal/surface/multica ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-multica-runtime/hub_writer.go | 30 ++++-------- harness/internal/driver/multica_hub.go | 14 ++++++ harness/internal/driver/multica_test.go | 48 +++++++++++++++++++ 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go index bcf4adf5..7713aeee 100644 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ b/harness/cmd/mnemon-multica-runtime/hub_writer.go @@ -428,13 +428,9 @@ func findExistingMulticaAssignmentIssue(ctx context.Context, cli driver.MulticaC func findExistingMulticaAssignmentIssueInChildren(ctx context.Context, cli driver.MulticaCLI, children []driver.MulticaIssue, source driver.MulticaHubLedgerSource) (driver.MulticaIssue, bool, error) { for _, child := range children { - meta := driver.MulticaIssueHubMetadata(child) - if !meta.IsAssignmentMailbox() { - listed, err := cli.ListIssueMetadata(ctx, child.ID) - if err != nil { - return driver.MulticaIssue{}, false, err - } - meta = multicasurface.ParseMulticaHubListedMetadata(listed) + meta, err := cli.ResolveIssueHubMetadata(ctx, child) + if err != nil { + return driver.MulticaIssue{}, false, err } if !meta.IsAssignmentMailbox() { continue @@ -467,13 +463,9 @@ func findAssignmentTargetFromMulticaHub(ctx context.Context, cli driver.MulticaC } candidates := []multicasurface.AssignmentTargetCandidate{} for _, child := range children { - meta := driver.MulticaIssueHubMetadata(child) - if !meta.IsAssignmentMailbox() { - listed, err := cli.ListIssueMetadata(ctx, child.ID) - if err != nil { - return "", false, err - } - meta = multicasurface.ParseMulticaHubListedMetadata(listed) + meta, err := cli.ResolveIssueHubMetadata(ctx, child) + if err != nil { + return "", false, err } if !meta.IsAssignmentMailbox() { continue @@ -493,13 +485,9 @@ func allMulticaAssignmentChildrenDone(ctx context.Context, cli driver.MulticaCLI } seen := false for _, child := range children { - meta := driver.MulticaIssueHubMetadata(child) - if !meta.IsAssignmentMailbox() { - listed, err := cli.ListIssueMetadata(ctx, child.ID) - if err != nil { - return false, err - } - meta = multicasurface.ParseMulticaHubListedMetadata(listed) + meta, err := cli.ResolveIssueHubMetadata(ctx, child) + if err != nil { + return false, err } if !meta.IsAssignmentMailbox() || meta.SessionID != sessionID { continue diff --git a/harness/internal/driver/multica_hub.go b/harness/internal/driver/multica_hub.go index fbab8089..08435f7c 100644 --- a/harness/internal/driver/multica_hub.go +++ b/harness/internal/driver/multica_hub.go @@ -1,6 +1,8 @@ package driver import ( + "context" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) @@ -55,6 +57,18 @@ func MulticaIssueHubMetadata(issue MulticaIssue) MulticaHubMetadata { return ParseMulticaHubMetadata(issue.Metadata) } +func (c MulticaCLI) ResolveIssueHubMetadata(ctx context.Context, issue MulticaIssue) (MulticaHubMetadata, error) { + meta := MulticaIssueHubMetadata(issue) + if meta.IsAssignmentMailbox() { + return meta, nil + } + listed, err := c.ListIssueMetadata(ctx, issue.ID) + if err != nil { + return MulticaHubMetadata{}, err + } + return multicasurface.ParseMulticaHubListedMetadata(listed), nil +} + func IsMulticaAssignmentMailboxIssue(issue MulticaIssue) bool { return MulticaIssueHubMetadata(issue).IsAssignmentMailbox() } diff --git a/harness/internal/driver/multica_test.go b/harness/internal/driver/multica_test.go index 6d466c5b..245732a5 100644 --- a/harness/internal/driver/multica_test.go +++ b/harness/internal/driver/multica_test.go @@ -286,6 +286,54 @@ func TestMulticaHubMetadataDetectsAssignmentMailbox(t *testing.T) { } } +func TestMulticaCLIResolveIssueHubMetadata(t *testing.T) { + tmp := t.TempDir() + argsPath := filepath.Join(tmp, "args.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue metadata list child-listed"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.assignment_id","value":"assignment-listed"},{"key":"mnemon.principal","value":"worker@team"}]\n' ;; + *"issue metadata list child-embedded"*) printf 'embedded metadata should not be listed\n' >&2; exit 42 ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + cli := MulticaCLI{ + Command: bin, + Env: append(os.Environ(), + "MULTICA_ARGS_PATH="+argsPath, + ), + } + listed, err := cli.ResolveIssueHubMetadata(context.Background(), MulticaIssue{ID: "child-listed"}) + if err != nil { + t.Fatal(err) + } + if !listed.IsAssignmentMailbox() || listed.AssignmentID != "assignment-listed" || listed.Principal != "worker@team" { + t.Fatalf("listed metadata = %+v", listed) + } + embedded, err := cli.ResolveIssueHubMetadata(context.Background(), MulticaIssue{ + ID: "child-embedded", + Metadata: map[string]any{ + MulticaMetadataHubBackend: MulticaHubBackend, + MulticaMetadataKind: MulticaHubKindAssignmentMailbox, + MulticaMetadataAssignmentID: "assignment-embedded", + }, + }) + if err != nil { + t.Fatal(err) + } + if embedded.AssignmentID != "assignment-embedded" { + t.Fatalf("embedded metadata = %+v", embedded) + } + args := mustReadDriverTestFile(t, argsPath) + if strings.Contains(args, "child-embedded") { + t.Fatalf("embedded assignment metadata should avoid metadata list fallback:\n%s", args) + } +} + func TestMulticaAssignmentFingerprintStable(t *testing.T) { left := MulticaAssignmentFingerprint(MulticaAssignmentFingerprintInput{ AssignmentID: " assignment-1 ", From d5d279b9e6d95c2ea9af5c57ae5a0817f68a13bc Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:44:19 +0800 Subject: [PATCH 089/117] refactor: centralize multica issue metadata loading Add a Multica CLI helper that lists and merges issue metadata into issue payloads before runtime classification. The runtime keeps progress reporting but delegates the metadata merge rule to the driver boundary. Validation: go test -count=1 ./harness/internal/driver ./harness/cmd/mnemon-multica-runtime ./harness/internal/surface/multica ./harness/internal/coreguard; go test -count=1 ./... --- harness/cmd/mnemon-multica-runtime/main.go | 7 ++-- harness/internal/driver/multica.go | 12 ++++++ harness/internal/driver/multica_test.go | 43 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 1f5b9029..460cea37 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -450,15 +450,14 @@ func loadRuntimeIssueMetadata(ctx context.Context, cli driver.MulticaCLI, issue if strings.TrimSpace(issue.ID) == "" { return issue } - listed, err := cli.ListIssueMetadata(ctx, issue.ID) + loaded, count, err := cli.LoadIssueMetadata(ctx, issue) if err != nil { emitRuntimeCommand(progress, "multica issue metadata list "+issue.ID, err.Error(), 1) emitRuntimeProgress(progress, "Multica issue metadata list failed; falling back to metadata returned by issue get.") return issue } - issue.Metadata = multicasurface.MergeIssueMetadata(issue.Metadata, listed) - emitRuntimeCommand(progress, "multica issue metadata list "+issue.ID, fmt.Sprintf("Loaded %d Multica issue metadata keys.", len(listed)), 0) - return issue + emitRuntimeCommand(progress, "multica issue metadata list "+issue.ID, fmt.Sprintf("Loaded %d Multica issue metadata keys.", count), 0) + return loaded } func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, result *runtimeImportResult, progress runtimeProgressSink) runtimeImportResult { diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index 1a258ad6..b5caf650 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -741,6 +741,18 @@ func (c MulticaCLI) ListIssueMetadata(ctx context.Context, issueID string) (map[ return decodeMulticaMetadataOutput([]byte(out.Stdout)) } +func (c MulticaCLI) LoadIssueMetadata(ctx context.Context, issue MulticaIssue) (MulticaIssue, int, error) { + if strings.TrimSpace(issue.ID) == "" { + return issue, 0, nil + } + listed, err := c.ListIssueMetadata(ctx, issue.ID) + if err != nil { + return issue, 0, err + } + issue.Metadata = multicasurface.MergeIssueMetadata(issue.Metadata, listed) + return issue, len(listed), nil +} + func (c MulticaCLI) ListIssueChildren(ctx context.Context, parentID string) ([]MulticaIssue, error) { parentID = strings.TrimSpace(parentID) if parentID == "" { diff --git a/harness/internal/driver/multica_test.go b/harness/internal/driver/multica_test.go index 245732a5..83d1fe2e 100644 --- a/harness/internal/driver/multica_test.go +++ b/harness/internal/driver/multica_test.go @@ -334,6 +334,49 @@ esac } } +func TestMulticaCLILoadIssueMetadataMergesListedMetadata(t *testing.T) { + tmp := t.TempDir() + argsPath := filepath.Join(tmp, "args.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue metadata list issue-1"*) printf '[{"key":"mnemon.kind","value":"session_mailbox"},{"key":"listed","value":"yes"}]\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + cli := MulticaCLI{ + Command: bin, + Env: append(os.Environ(), + "MULTICA_ARGS_PATH="+argsPath, + ), + } + loaded, count, err := cli.LoadIssueMetadata(context.Background(), MulticaIssue{ + ID: "issue-1", + Metadata: map[string]any{ + MulticaMetadataKind: "stale", + "base": "kept", + }, + }) + if err != nil { + t.Fatal(err) + } + if count != 2 || loaded.Metadata[MulticaMetadataKind] != MulticaHubKindSession || loaded.Metadata["base"] != "kept" || loaded.Metadata["listed"] != "yes" { + t.Fatalf("loaded metadata count=%d metadata=%+v", count, loaded.Metadata) + } + empty, count, err := cli.LoadIssueMetadata(context.Background(), MulticaIssue{Title: "missing id"}) + if err != nil || count != 0 || empty.Title != "missing id" { + t.Fatalf("empty metadata load = count:%d issue:%+v err:%v", count, empty, err) + } + args := mustReadDriverTestFile(t, argsPath) + if !strings.Contains(args, "issue metadata list issue-1 --output json") || strings.Contains(args, "missing id") { + t.Fatalf("metadata load args mismatch:\n%s", args) + } +} + func TestMulticaAssignmentFingerprintStable(t *testing.T) { left := MulticaAssignmentFingerprint(MulticaAssignmentFingerprintInput{ AssignmentID: " assignment-1 ", From 49709904d980ebe881120f76c19835577c9f87e4 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:47:43 +0800 Subject: [PATCH 090/117] refactor: reuse multica registry selection in acceptance Move the fallback participant-with-agent selection into the Multica surface registry helpers and have the real Multica acceptance flow reuse those semantics. This keeps acceptance orchestration aligned with the runtime adapter registry rules instead of carrying a local traversal copy. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/cmd/mnemon-acceptance ./harness/internal/coreguard; go test -count=1 ./... --- .../acceptance_multica_runtime.go | 19 ++++++-------- .../acceptance_multica_runtime_test.go | 25 ++++++++++++++++++ harness/internal/surface/multica/registry.go | 9 +++++++ .../internal/surface/multica/registry_test.go | 26 +++++++++++++------ 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index d80661fb..a9dc1b57 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -331,21 +331,18 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime func selectMulticaAcceptanceAssignee(reg driver.MulticaRegistry, principal string) (driver.MulticaParticipantRecord, error) { principal = strings.TrimSpace(principal) - for _, participant := range reg.Participants { - if principal != "" && participant.Principal == principal { - if strings.TrimSpace(participant.AgentID) == "" { - return driver.MulticaParticipantRecord{}, fmt.Errorf("participant %s has no Multica agent id", participant.Principal) - } - return participant, nil - } - } if principal != "" { - return driver.MulticaParticipantRecord{}, fmt.Errorf("participant principal %q not found in registry", principal) - } - for _, participant := range reg.Participants { + participant, ok := multicasurface.MulticaParticipantForPrincipal(reg, principal) + if !ok { + return driver.MulticaParticipantRecord{}, fmt.Errorf("participant principal %q not found in registry", principal) + } if strings.TrimSpace(participant.AgentID) != "" { return participant, nil } + return driver.MulticaParticipantRecord{}, fmt.Errorf("participant %s has no Multica agent id", participant.Principal) + } + if participant, ok := multicasurface.FirstMulticaParticipantWithAgentID(reg); ok { + return participant, nil } return driver.MulticaParticipantRecord{}, fmt.Errorf("registry has no participant with a Multica agent id") } diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 17ef49bd..1bcf4f80 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -12,6 +12,31 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/driver" ) +func TestSelectMulticaAcceptanceAssigneeUsesRegistryHelpers(t *testing.T) { + reg := driver.MulticaRegistry{Participants: []driver.MulticaParticipantRecord{ + {Principal: "planner@team", AgentName: "mnemon-planner"}, + {Principal: " reviewer@team ", AgentName: "mnemon-reviewer", AgentID: "agent-reviewer"}, + {Principal: "implementer@team", AgentName: "mnemon-implementer", AgentID: "agent-implementer"}, + }} + assignee, err := selectMulticaAcceptanceAssignee(reg, "reviewer@team") + if err != nil { + t.Fatal(err) + } + if assignee.AgentID != "agent-reviewer" { + t.Fatalf("principal assignee = %+v", assignee) + } + assignee, err = selectMulticaAcceptanceAssignee(reg, "") + if err != nil { + t.Fatal(err) + } + if assignee.AgentID != "agent-reviewer" { + t.Fatalf("fallback assignee = %+v", assignee) + } + if _, err := selectMulticaAcceptanceAssignee(reg, "planner@team"); err == nil || !strings.Contains(err.Error(), "no Multica agent id") { + t.Fatalf("missing agent id error = %v", err) + } +} + func TestMulticaRuntimeProdSimAcceptanceObservesRunMessages(t *testing.T) { tmp := t.TempDir() registryPath := filepath.Join(tmp, "registry.json") diff --git a/harness/internal/surface/multica/registry.go b/harness/internal/surface/multica/registry.go index ffaaedd6..e55432d6 100644 --- a/harness/internal/surface/multica/registry.go +++ b/harness/internal/surface/multica/registry.go @@ -103,6 +103,15 @@ func MulticaParticipantForPrincipal(reg MulticaRegistry, principal string) (Mult return MulticaParticipantRecord{}, false } +func FirstMulticaParticipantWithAgentID(reg MulticaRegistry) (MulticaParticipantRecord, bool) { + for _, participant := range reg.Participants { + if strings.TrimSpace(participant.AgentID) != "" { + return participant, true + } + } + return MulticaParticipantRecord{}, false +} + func MulticaPrincipalForAgent(reg MulticaRegistry, agentID, agentName string) string { agentID = strings.TrimSpace(agentID) agentName = strings.TrimSpace(agentName) diff --git a/harness/internal/surface/multica/registry_test.go b/harness/internal/surface/multica/registry_test.go index 4e88f330..82aed949 100644 --- a/harness/internal/surface/multica/registry_test.go +++ b/harness/internal/surface/multica/registry_test.go @@ -60,19 +60,29 @@ func TestRegistryRoundTrip(t *testing.T) { } func TestMulticaParticipantLookups(t *testing.T) { - reg := MulticaRegistry{Participants: []MulticaParticipantRecord{{ - Principal: "planner@team", - AgentName: "mnemon-planner", - AgentID: "agent-planner", - }}} + reg := MulticaRegistry{Participants: []MulticaParticipantRecord{ + { + Principal: "planner@team", + AgentName: "mnemon-planner", + }, + { + Principal: "reviewer@team", + AgentName: "mnemon-reviewer", + AgentID: "agent-reviewer", + }, + }} participant, ok := MulticaParticipantForPrincipal(reg, " planner@team ") - if !ok || participant.AgentID != "agent-planner" { + if !ok || participant.AgentName != "mnemon-planner" { t.Fatalf("participant lookup = ok:%v %+v", ok, participant) } - if got := MulticaPrincipalForAgent(reg, " agent-planner ", ""); got != "planner@team" { + participant, ok = FirstMulticaParticipantWithAgentID(reg) + if !ok || participant.Principal != "reviewer@team" { + t.Fatalf("first participant with agent id = ok:%v %+v", ok, participant) + } + if got := MulticaPrincipalForAgent(reg, " agent-reviewer ", ""); got != "reviewer@team" { t.Fatalf("principal by agent id = %q", got) } - if got := MulticaPrincipalForAgent(reg, "", " mnemon-planner "); got != "planner@team" { + if got := MulticaPrincipalForAgent(reg, "", " mnemon-reviewer "); got != "reviewer@team" { t.Fatalf("principal by agent name = %q", got) } } From 5e815a0e9180b462490e31721c9d4c13dcb33276 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:50:09 +0800 Subject: [PATCH 091/117] refactor: reuse multica metadata resolver in acceptance Have the real Multica runtime acceptance evidence path resolve child hub metadata through the driver helper used by the runtime adapter. The hub-flow fixture now covers partial child metadata from issue children with authoritative metadata list fallback. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance ./harness/internal/driver ./harness/internal/coreguard; go test -count=1 ./... --- .../mnemon-acceptance/acceptance_multica_runtime.go | 12 ++++-------- .../acceptance_multica_runtime_test.go | 4 +++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index a9dc1b57..87334555 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -505,15 +505,11 @@ func listMulticaChildrenWithMetadata(ctx context.Context, cli driver.MulticaCLI, } metaByIssue := map[string]map[string]string{} for _, child := range rawChildren { - meta := driver.NormalizeMulticaMetadata(child.Metadata) - if len(meta) == 0 || meta[driver.MulticaMetadataKind] == "" { - listed, err := cli.ListIssueMetadata(ctx, child.ID) - if err != nil { - return nil, nil, err - } - meta = listed + meta, err := cli.ResolveIssueHubMetadata(ctx, child) + if err != nil { + return nil, nil, err } - metaByIssue[child.ID] = meta + metaByIssue[child.ID] = meta.Map() } return rawChildren, metaByIssue, nil } diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 1bcf4f80..800210c8 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -152,7 +152,8 @@ case "$*" in *"issue get child-2"*) printf '%s\n' '{"id":"child-2","identifier":"TEA-10","title":"TEA-9: routing check","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-9](mention://issue/root-9) - Runtime hub flow\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing check\n\n## Feedback\n\n- Expected feedback: result or blocker\n- Progress path: Mnemon runtime progress, result, or blocker feedback","status":"done"}' ;; *"issue get child-3"*) printf '%s\n' '{"id":"child-3","identifier":"TEA-11","title":"TEA-9: runtime display","description":"## Assignment\n\nCheck runtime display.\n\n## Context\n\n- Root issue: [TEA-9](mention://issue/root-9) - Runtime hub flow\n- Assignee: implementer@team (mnemon-implementer)\n- Scope: runtime display\n\n## Feedback\n\n- Expected feedback: result or blocker\n- Progress path: Mnemon runtime progress, result, or blocker feedback","status":"done"}' ;; *"issue metadata list root-9"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"session_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-9"}]\n' ;; - *"issue children root-9"*) printf '{"children":[{"id":"child-2","identifier":"TEA-10","title":"Assignment 2","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-9","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-2","mnemon.principal":"researcher@team"}},{"id":"child-3","identifier":"TEA-11","title":"Assignment 3","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-9","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-3","mnemon.principal":"implementer@team"}}]}\n' ;; + *"issue children root-9"*) printf '{"children":[{"id":"child-2","identifier":"TEA-10","title":"Assignment 2","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox"}},{"id":"child-3","identifier":"TEA-11","title":"Assignment 3","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-9","mnemon.session_id":"multica:session:root-9","mnemon.assignment_id":"asg-3","mnemon.principal":"implementer@team"}}]}\n' ;; + *"issue metadata list child-2"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.root_issue_id","value":"root-9"},{"key":"mnemon.session_id","value":"multica:session:root-9"},{"key":"mnemon.assignment_id","value":"asg-2"},{"key":"mnemon.principal","value":"researcher@team"}]\n' ;; *"issue comment list root-9"*) printf '[{"id":"comment-root","issue_id":"root-9","content":"Mnemon update: issue admitted\\n\\nmnemon:event=multica-task-root"}]\n' ;; *"issue comment list child-2"*) printf '[{"id":"comment-child-2","issue_id":"child-2","content":"Mnemon update: assignment feedback\\n\\nSummary: checked routing.\\n\\nmnemon:event=pg-2"}]\n' ;; *"issue comment list child-3"*) printf '[{"id":"comment-child-3","issue_id":"child-3","content":"Mnemon update: assignment feedback\\n\\nSummary: checked runtime display.\\n\\nmnemon:event=pg-3"}]\n' ;; @@ -215,6 +216,7 @@ esac } for _, want := range []string{ "issue metadata list root-9 --output json", + "issue metadata list child-2 --output json", "issue children root-9 --output json", "issue runs child-2 --output json", "issue runs child-3 --output json", From d00e7361a806d7d908dc46420d8e5f9b4fa6e5a5 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:54:58 +0800 Subject: [PATCH 092/117] refactor: centralize multica runtime command name Pin the Multica runtime adapter command name in the Multica surface layer and reuse it through driver, product config, harness provisioning, acceptance provisioning, and runtime self-description. This keeps the existing mnemon-multica-runtime binary name explicit while preventing default drift across CLI cluster surfaces. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/internal/driver ./harness/internal/productconfig ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-acceptance ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../acceptance_multica_provision.go | 3 ++- harness/cmd/mnemon-harness/connect.go | 2 +- harness/cmd/mnemon-harness/multica.go | 4 ++-- harness/cmd/mnemon-multica-runtime/main.go | 14 +++++++------- harness/cmd/mnemon-multica-runtime/probe.go | 6 +++--- harness/internal/driver/multica.go | 5 +++-- harness/internal/productconfig/config.go | 4 +++- harness/internal/surface/multica/runtime_config.go | 2 ++ .../surface/multica/runtime_config_test.go | 6 ++++++ 9 files changed, 29 insertions(+), 17 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go index 6693f015..36f3847f 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/mnemon-dev/mnemon/harness/internal/driver" "github.com/spf13/cobra" ) @@ -69,7 +70,7 @@ func init() { acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRegistry, "registry", "", "Multica participant registry path") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProjectRoot, "project-root", ".", "project root for the default registry path") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProfileName, "runtime-profile-name", "mnemon-runtime", "Multica runtime profile display name") - acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRuntimeCommand, "runtime-command", "mnemon-multica-runtime", "runtime executable name registered with Multica") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRuntimeCommand, "runtime-command", driver.MulticaRuntimeCommandName, "runtime executable name registered with Multica") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRuntimePath, "runtime-path", "", "absolute local executable path for the runtime profile") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionAgentPrefix, "agent-prefix", "mnemon", "Multica participant agent name prefix") acceptanceMulticaProvisionCmd.Flags().BoolVar(&acceptanceMulticaProvisionRestartDaemon, "restart-daemon", false, "restart the local Multica daemon after setting the runtime path") diff --git a/harness/cmd/mnemon-harness/connect.go b/harness/cmd/mnemon-harness/connect.go index 68cd3541..50d357a8 100644 --- a/harness/cmd/mnemon-harness/connect.go +++ b/harness/cmd/mnemon-harness/connect.go @@ -46,7 +46,7 @@ func init() { connectCmd.PersistentFlags().StringVar(&connectConfigPath, "config", "", "harness product config path") _ = connectCmd.PersistentFlags().MarkHidden("config") connectMulticaCmd.Flags().StringVar(&connectMulticaWS, "workspace", "", "Multica workspace") - connectMulticaCmd.Flags().StringVar(&connectMulticaRuntime, "runtime-binary", "mnemon-multica-runtime", "Multica runtime binary") + connectMulticaCmd.Flags().StringVar(&connectMulticaRuntime, "runtime-binary", productconfig.DefaultMulticaRuntimeBinary, "Multica runtime binary") _ = connectMulticaCmd.Flags().MarkHidden("runtime-binary") connectGitHubCmd.Flags().StringVar(&connectGitHubRepo, "repo", "", "GitHub repository owner/name") connectGitHubCmd.Flags().StringVar(&connectGitHubBranch, "branch", "", "GitHub publication branch") diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 988b5880..5b86ec04 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -394,7 +394,7 @@ func runMulticaProvision(cmd *cobra.Command, args []string) error { } runtimeCommand := strings.TrimSpace(multicaProvisionRuntimeCommand) if runtimeCommand == "" { - runtimeCommand = "mnemon-multica-runtime" + runtimeCommand = driver.MulticaRuntimeCommandName } registryPath := driver.MulticaRegistryPath(multicaProvisionProjectRoot, multicaProvisionRegistry) report := multicaProvisionReport{WorkspaceID: workspaceID, RegistryPath: registryPath} @@ -992,7 +992,7 @@ func init() { multicaProvisionCmd.Flags().StringVar(&multicaProvisionRegistry, "registry", "", "Multica registry path") multicaProvisionCmd.Flags().StringVar(&multicaProvisionProjectRoot, "project-root", ".", "project root for the default registry path") multicaProvisionCmd.Flags().StringVar(&multicaProvisionProfileName, "runtime-profile-name", "mnemon-runtime", "Multica runtime profile display name") - multicaProvisionCmd.Flags().StringVar(&multicaProvisionRuntimeCommand, "runtime-command", "mnemon-multica-runtime", "runtime executable name registered with Multica") + multicaProvisionCmd.Flags().StringVar(&multicaProvisionRuntimeCommand, "runtime-command", driver.MulticaRuntimeCommandName, "runtime executable name registered with Multica") multicaProvisionCmd.Flags().StringVar(&multicaProvisionRuntimePath, "runtime-path", "", "absolute local executable path for the runtime profile") multicaProvisionCmd.Flags().StringVar(&multicaProvisionAgentPrefix, "agent-prefix", "mnemon", "Multica participant agent name prefix") multicaProvisionCmd.Flags().BoolVar(&multicaProvisionRestartDaemon, "restart-daemon", false, "restart the local Multica daemon after setting the runtime path") diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 460cea37..2143db2b 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -135,7 +135,7 @@ func runRuntime(cfg runtimeConfig) error { return runRuntimeProbe(cfg) } if wantsVersion(cfg.Args) { - fmt.Fprintf(cfg.Stdout, "mnemon-multica-runtime %s\n", runtimeVersion) + fmt.Fprintf(cfg.Stdout, "%s %s\n", multicasurface.MulticaRuntimeCommandName, runtimeVersion) return nil } return runRuntimeRPC(cfg, cwd) @@ -152,7 +152,7 @@ func runRuntimeRPC(cfg runtimeConfig, cwd string) error { } var msg rpcMessage if err := json.Unmarshal([]byte(line), &msg); err != nil { - fmt.Fprintf(cfg.Stderr, "mnemon-multica-runtime: ignoring invalid rpc line: %v\n", err) + fmt.Fprintf(cfg.Stderr, "%s: ignoring invalid rpc line: %v\n", multicasurface.MulticaRuntimeCommandName, err) continue } if err := state.handle(msg, func(response rpcMessage) error { @@ -178,7 +178,7 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er return emitAll(rpcMessage{ ID: msg.ID, Result: map[string]any{ - "userAgent": "mnemon-multica-runtime/" + runtimeVersion, + "userAgent": multicasurface.MulticaRuntimeCommandName + "/" + runtimeVersion, "codexHome": multicasurface.RuntimeEnvValue(s.Env, "CODEX_HOME"), "platformFamily": "unix", "platformOs": runtime.GOOS, @@ -193,8 +193,8 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er Method: "remoteControl/status/changed", Params: map[string]any{ "status": "disabled", - "serverName": "mnemon-multica-runtime", - "installationId": "mnemon-multica-runtime", + "serverName": multicasurface.MulticaRuntimeCommandName, + "installationId": multicasurface.MulticaRuntimeCommandName, }, }, rpcMessage{ @@ -826,7 +826,7 @@ func runtimeManagedTurnClient(env []string, cwd, runtimeName string) (driver.Man Workspace: workspace, Env: append([]string(nil), env...), TurnTimeout: multicasurface.RuntimeManagedTurnTimeout(env), - ClientName: "mnemon-multica-runtime", + ClientName: multicasurface.MulticaRuntimeCommandName, }, workspace, nil default: return nil, workspace, fmt.Errorf("unsupported MNEMON_MANAGED_RUNTIME %q", runtimeName) @@ -991,7 +991,7 @@ func markIssueInProgress(ctx context.Context, cli driver.MulticaCLI, issueID str } func wantsVersion(args []string) bool { - fs := flag.NewFlagSet("mnemon-multica-runtime", flag.ContinueOnError) + fs := flag.NewFlagSet(multicasurface.MulticaRuntimeCommandName, flag.ContinueOnError) fs.SetOutput(io.Discard) version := fs.Bool("version", false, "") _ = fs.Parse(args) diff --git a/harness/cmd/mnemon-multica-runtime/probe.go b/harness/cmd/mnemon-multica-runtime/probe.go index 47d976d1..e8321a44 100644 --- a/harness/cmd/mnemon-multica-runtime/probe.go +++ b/harness/cmd/mnemon-multica-runtime/probe.go @@ -66,7 +66,7 @@ func runRuntimeProbe(cfg runtimeConfig) error { return err } if wantsVersion(cfg.Args) { - fmt.Fprintf(cfg.Stdout, "mnemon-multica-runtime %s (probe mode)\n", runtimeVersion) + fmt.Fprintf(cfg.Stdout, "%s %s (probe mode)\n", multicasurface.MulticaRuntimeCommandName, runtimeVersion) return rec.record(probeEvent{Kind: "version"}) } return runProbeRPC(cfg, rec, cwd) @@ -122,7 +122,7 @@ func (s *probeRPCState) handle(msg rpcMessage) []rpcMessage { return []rpcMessage{{ ID: msg.ID, Result: map[string]any{ - "userAgent": "mnemon-multica-runtime/" + runtimeVersion + " probe", + "userAgent": multicasurface.MulticaRuntimeCommandName + "/" + runtimeVersion + " probe", "codexHome": os.Getenv("CODEX_HOME"), "platformFamily": "unix", "platformOs": runtime.GOOS, @@ -135,7 +135,7 @@ func (s *probeRPCState) handle(msg rpcMessage) []rpcMessage { Method: "remoteControl/status/changed", Params: map[string]any{ "status": "disabled", - "serverName": "mnemon-multica-runtime", + "serverName": multicasurface.MulticaRuntimeCommandName, "installationId": "mnemon-runtime-probe", }, }, diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index b5caf650..6914c50a 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -17,8 +17,9 @@ import ( ) const ( - MulticaDefaultCommand = "multica" - MulticaExternalSource = multicasurface.MulticaExternalSource + MulticaDefaultCommand = "multica" + MulticaRuntimeCommandName = multicasurface.MulticaRuntimeCommandName + MulticaExternalSource = multicasurface.MulticaExternalSource ) type MulticaCLI struct { diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go index 600a3c1d..b13fedef 100644 --- a/harness/internal/productconfig/config.go +++ b/harness/internal/productconfig/config.go @@ -27,6 +27,8 @@ const ( ConnectionGitHub = "github" ConnectionMnemonhub = "mnemonhub" + DefaultMulticaRuntimeBinary = multicasurface.MulticaRuntimeCommandName + DriveManagedLocal = "managed-local" DuplicateActivationSuppress = "suppress" @@ -394,7 +396,7 @@ func bridgeMulticaRegistry(cfg *Config, reg multicasurface.MulticaRegistry) erro cfg.Connections.Multica.Workspace = strings.TrimSpace(reg.WorkspaceID) } if strings.TrimSpace(cfg.Connections.Multica.RuntimeBinary) == "" { - cfg.Connections.Multica.RuntimeBinary = "mnemon-multica-runtime" + cfg.Connections.Multica.RuntimeBinary = DefaultMulticaRuntimeBinary } cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionMultica) cfg.Daemon.ProjectionSurfaces = appendCarrier(cfg.Daemon.ProjectionSurfaces, ConnectionMultica) diff --git a/harness/internal/surface/multica/runtime_config.go b/harness/internal/surface/multica/runtime_config.go index 0c4449a5..801a7efe 100644 --- a/harness/internal/surface/multica/runtime_config.go +++ b/harness/internal/surface/multica/runtime_config.go @@ -6,6 +6,8 @@ import ( "time" ) +const MulticaRuntimeCommandName = "mnemon-multica-runtime" + type RuntimeManagedWakeMaterial struct { IssueID string RootIssueID string diff --git a/harness/internal/surface/multica/runtime_config_test.go b/harness/internal/surface/multica/runtime_config_test.go index 28f38b41..b812758c 100644 --- a/harness/internal/surface/multica/runtime_config_test.go +++ b/harness/internal/surface/multica/runtime_config_test.go @@ -19,6 +19,12 @@ func TestRuntimeEnvValueUsesLastValue(t *testing.T) { } } +func TestMulticaRuntimeCommandNamePinned(t *testing.T) { + if MulticaRuntimeCommandName != "mnemon-multica-runtime" { + t.Fatalf("MulticaRuntimeCommandName = %q", MulticaRuntimeCommandName) + } +} + func TestRuntimeTimeoutUsesMulticaHTTPFallback(t *testing.T) { if got := RuntimeTimeout([]string{"MULTICA_HTTP_TIMEOUT=2m"}); got != 2*time.Minute { t.Fatalf("RuntimeTimeout fallback = %s, want 2m", got) From dff95cbad1cf3c954a2493863a219669e9403dd4 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 03:56:58 +0800 Subject: [PATCH 093/117] refactor: centralize multica runtime profile name Pin the default Multica runtime profile display name beside the adapter command name and reuse it from harness and acceptance provisioning. This removes another default naming copy from the CLI cluster without changing the registered profile value. Validation: go test -count=1 ./harness/internal/surface/multica ./harness/internal/driver ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-acceptance ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-acceptance/acceptance_multica_provision.go | 2 +- harness/cmd/mnemon-harness/multica.go | 4 ++-- harness/internal/driver/multica.go | 1 + harness/internal/surface/multica/runtime_config.go | 5 ++++- harness/internal/surface/multica/runtime_config_test.go | 3 +++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go index 36f3847f..7eda6f0f 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go @@ -69,7 +69,7 @@ func init() { acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionWorkspaceID, "multica-workspace-id", multicaAcceptanceEnvDefault("MNEMON_MULTICA_WORKSPACE_ID", ""), "Multica workspace ID") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRegistry, "registry", "", "Multica participant registry path") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProjectRoot, "project-root", ".", "project root for the default registry path") - acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProfileName, "runtime-profile-name", "mnemon-runtime", "Multica runtime profile display name") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProfileName, "runtime-profile-name", driver.MulticaRuntimeProfileName, "Multica runtime profile display name") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRuntimeCommand, "runtime-command", driver.MulticaRuntimeCommandName, "runtime executable name registered with Multica") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionRuntimePath, "runtime-path", "", "absolute local executable path for the runtime profile") acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionAgentPrefix, "agent-prefix", "mnemon", "Multica participant agent name prefix") diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 5b86ec04..0a8476a7 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -390,7 +390,7 @@ func runMulticaProvision(cmd *cobra.Command, args []string) error { } profileName := strings.TrimSpace(multicaProvisionProfileName) if profileName == "" { - profileName = "mnemon-runtime" + profileName = driver.MulticaRuntimeProfileName } runtimeCommand := strings.TrimSpace(multicaProvisionRuntimeCommand) if runtimeCommand == "" { @@ -991,7 +991,7 @@ func init() { multicaProvisionCmd.Flags().StringVar(&multicaProvisionRegistry, "registry", "", "Multica registry path") multicaProvisionCmd.Flags().StringVar(&multicaProvisionProjectRoot, "project-root", ".", "project root for the default registry path") - multicaProvisionCmd.Flags().StringVar(&multicaProvisionProfileName, "runtime-profile-name", "mnemon-runtime", "Multica runtime profile display name") + multicaProvisionCmd.Flags().StringVar(&multicaProvisionProfileName, "runtime-profile-name", driver.MulticaRuntimeProfileName, "Multica runtime profile display name") multicaProvisionCmd.Flags().StringVar(&multicaProvisionRuntimeCommand, "runtime-command", driver.MulticaRuntimeCommandName, "runtime executable name registered with Multica") multicaProvisionCmd.Flags().StringVar(&multicaProvisionRuntimePath, "runtime-path", "", "absolute local executable path for the runtime profile") multicaProvisionCmd.Flags().StringVar(&multicaProvisionAgentPrefix, "agent-prefix", "mnemon", "Multica participant agent name prefix") diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index 6914c50a..2172b6c4 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -19,6 +19,7 @@ import ( const ( MulticaDefaultCommand = "multica" MulticaRuntimeCommandName = multicasurface.MulticaRuntimeCommandName + MulticaRuntimeProfileName = multicasurface.MulticaRuntimeProfileName MulticaExternalSource = multicasurface.MulticaExternalSource ) diff --git a/harness/internal/surface/multica/runtime_config.go b/harness/internal/surface/multica/runtime_config.go index 801a7efe..fa7c7625 100644 --- a/harness/internal/surface/multica/runtime_config.go +++ b/harness/internal/surface/multica/runtime_config.go @@ -6,7 +6,10 @@ import ( "time" ) -const MulticaRuntimeCommandName = "mnemon-multica-runtime" +const ( + MulticaRuntimeCommandName = "mnemon-multica-runtime" + MulticaRuntimeProfileName = "mnemon-runtime" +) type RuntimeManagedWakeMaterial struct { IssueID string diff --git a/harness/internal/surface/multica/runtime_config_test.go b/harness/internal/surface/multica/runtime_config_test.go index b812758c..92eaced0 100644 --- a/harness/internal/surface/multica/runtime_config_test.go +++ b/harness/internal/surface/multica/runtime_config_test.go @@ -23,6 +23,9 @@ func TestMulticaRuntimeCommandNamePinned(t *testing.T) { if MulticaRuntimeCommandName != "mnemon-multica-runtime" { t.Fatalf("MulticaRuntimeCommandName = %q", MulticaRuntimeCommandName) } + if MulticaRuntimeProfileName != "mnemon-runtime" { + t.Fatalf("MulticaRuntimeProfileName = %q", MulticaRuntimeProfileName) + } } func TestRuntimeTimeoutUsesMulticaHTTPFallback(t *testing.T) { From d5f51401ea919e54cd68b99798467714493fad5a Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 04:00:28 +0800 Subject: [PATCH 094/117] fix: keep mnemonhub help on successful path Return nil for mnemon-hub --help so the remote exchange backend command exposes normal CLI help without an error exit. Tighten command and transitive boundary guards so mnemonhub does not drift into local mnemond access, admission, presentation, daemon, or driver behavior layers. Validation: go test -count=1 ./harness/cmd/mnemon-hub ./harness/internal/mnemonhub ./harness/internal/coreguard; go run ./harness/cmd/mnemon-hub --help; go run ./harness/cmd/mnemon-hub --dev-selfsigned ; go test -count=1 ./... --- harness/cmd/mnemon-hub/main.go | 3 +++ harness/cmd/mnemon-hub/main_test.go | 6 ++---- harness/internal/coreguard/command_boundary_test.go | 6 ++++++ harness/internal/mnemonhub/boundary_test.go | 12 ++++++++---- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/harness/cmd/mnemon-hub/main.go b/harness/cmd/mnemon-hub/main.go index b3864e5a..6f939059 100644 --- a/harness/cmd/mnemon-hub/main.go +++ b/harness/cmd/mnemon-hub/main.go @@ -50,6 +50,9 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { fs.PrintDefaults() } if err := fs.Parse(args); err != nil { + if err == flag.ErrHelp { + return nil + } return err } if *devSelfsigned != "" { diff --git a/harness/cmd/mnemon-hub/main_test.go b/harness/cmd/mnemon-hub/main_test.go index 409ca018..3ab64468 100644 --- a/harness/cmd/mnemon-hub/main_test.go +++ b/harness/cmd/mnemon-hub/main_test.go @@ -7,8 +7,6 @@ import ( "crypto/tls" "encoding/hex" "encoding/json" - "errors" - "flag" "io" "os" "path/filepath" @@ -45,8 +43,8 @@ func writeToken(t *testing.T, dir, name, token string) { func TestHelpDescribesRemoteExchangeBackend(t *testing.T) { var errw bytes.Buffer err := run(context.Background(), []string{"--help"}, io.Discard, &errw) - if !errors.Is(err, flag.ErrHelp) { - t.Fatalf("help error = %v, want flag.ErrHelp", err) + if err != nil { + t.Fatalf("help should exit successfully, got %v", err) } got := errw.String() for _, want := range []string{"remote event exchange backend", "replica push", "pull", "status", "cursors", "tenant boundaries"} { diff --git a/harness/internal/coreguard/command_boundary_test.go b/harness/internal/coreguard/command_boundary_test.go index 243613a0..35b6e4b8 100644 --- a/harness/internal/coreguard/command_boundary_test.go +++ b/harness/internal/coreguard/command_boundary_test.go @@ -37,6 +37,9 @@ var commandImportBoundaries = []commandImportBoundary{ "harness/internal/drive", "harness/internal/driver", "harness/internal/hostagent", + "harness/internal/mnemond/access", + "harness/internal/mnemond/admission", + "harness/internal/mnemond/presentation", "harness/internal/productconfig", "harness/internal/projection", "harness/internal/runtime", @@ -70,6 +73,9 @@ func TestCommandImportBoundaryGuardLogicIsNotVacuous(t *testing.T) { if !commandImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/app", commandImportBoundaries[1].forbids) { t.Fatal("mnemon-hub command boundary must flag app imports") } + if !commandImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/access", commandImportBoundaries[1].forbids) { + t.Fatal("mnemon-hub command boundary must flag local mnemond access imports") + } if commandImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/access", commandImportBoundaries[2].forbids) { t.Fatal("mnemon-multica-runtime must be allowed to call mnemond access APIs") } diff --git a/harness/internal/mnemonhub/boundary_test.go b/harness/internal/mnemonhub/boundary_test.go index 2c07aa68..3b31dc28 100644 --- a/harness/internal/mnemonhub/boundary_test.go +++ b/harness/internal/mnemonhub/boundary_test.go @@ -23,10 +23,14 @@ func TestHubImportBoundaryExcludesLocalTrustDomain(t *testing.T) { t.Fatalf("go list -deps: %v\n%s", err, out) } forbidden := map[string]bool{ - "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access": true, - "github.com/mnemon-dev/mnemon/harness/internal/runtime": true, - "github.com/mnemon-dev/mnemon/harness/internal/app": true, - "github.com/mnemon-dev/mnemon/harness/internal/hostagent": true, + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access": true, + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/admission": true, + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation": true, + "github.com/mnemon-dev/mnemon/harness/internal/runtime": true, + "github.com/mnemon-dev/mnemon/harness/internal/app": true, + "github.com/mnemon-dev/mnemon/harness/internal/daemon": true, + "github.com/mnemon-dev/mnemon/harness/internal/driver": true, + "github.com/mnemon-dev/mnemon/harness/internal/hostagent": true, } for _, dep := range strings.Split(strings.TrimSpace(string(out)), "\n") { if forbidden[strings.TrimSpace(dep)] { From fce8b55bc9dcfc4a9ad4ed256d9e61954b5784b6 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 04:05:17 +0800 Subject: [PATCH 095/117] test: cover manual sync through mnemonhub Add a CLI-level sync round trip that points mnemon-harness push and pull at an in-process mnemonhub HTTP handler. The case proves local progress is acked by the hub, foreign hub material imports through Local Mnemon, and the pull cursor remains idempotent. Validation: go test -count=1 ./harness/cmd/mnemon-harness ./harness/internal/mnemonhub ./harness/internal/app ./harness/cmd/mnemon-hub; go test -count=1 ./... --- harness/cmd/mnemon-harness/sync_test.go | 124 ++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/harness/cmd/mnemon-harness/sync_test.go b/harness/cmd/mnemon-harness/sync_test.go index 9b9d6624..c3290193 100644 --- a/harness/cmd/mnemon-harness/sync_test.go +++ b/harness/cmd/mnemon-harness/sync_test.go @@ -11,11 +11,14 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/mnemon-dev/mnemon/harness/internal/app" "github.com/mnemon-dev/mnemon/harness/internal/contract" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/access" + "github.com/mnemon-dev/mnemon/harness/internal/mnemond/state" + "github.com/mnemon-dev/mnemon/harness/internal/mnemonhub" "github.com/mnemon-dev/mnemon/harness/internal/mnemonhub/exchange" "github.com/mnemon-dev/mnemon/harness/internal/runtime" ) @@ -269,6 +272,127 @@ func TestSyncPullOnceImportsRemoteAssignmentThroughLocalMnemon(t *testing.T) { } } +func TestSyncPushPullOnceRoundTripsThroughMnemonHub(t *testing.T) { + restoreSyncFlags(t) + root := t.TempDir() + storePath := filepath.Join(root, runtime.DefaultStorePath) + ref := contract.ResourceRef{Kind: "progress_digest", ID: "project"} + + localBinding := access.HostAgentBinding("codex@project", "http://127.0.0.1:8787", []contract.ResourceRef{ref}) + local, err := app.OpenLocalRuntime(storePath, access.LoadedBindings{Bindings: []access.ChannelBinding{localBinding}}, nil, nil) + if err != nil { + t.Fatalf("open local runtime: %v", err) + } + if _, _, err := local.API().Ingest("codex@project", contract.ObservationEnvelope{ + ExternalID: "hub-cli-local-progress", + Event: contract.Event{Type: "progress_digest.write_candidate.observed", Payload: cmdR2Progress("manual sync push reaches mnemonhub")}, + }); err != nil { + t.Fatalf("local observe: %v", err) + } + if _, err := local.Tick(); err != nil { + t.Fatalf("local tick: %v", err) + } + if err := local.Close(); err != nil { + t.Fatalf("close local runtime: %v", err) + } + + hubStore, err := state.OpenStore(filepath.Join(t.TempDir(), "hub.db")) + if err != nil { + t.Fatalf("open hub store: %v", err) + } + defer hubStore.Close() + grants := mnemonhub.GrantMap{ + "replica-local@team": {Principal: "replica-local@team", Scopes: []contract.ResourceRef{ref}}, + "replica-other@team": {Principal: "replica-other@team", Scopes: []contract.ResourceRef{ref}}, + } + tokens := map[string]contract.ActorID{ + "tok-local": "replica-local@team", + "tok-other": "replica-other@team", + } + hub := mnemonhub.New(hubStore, grants, func() string { return time.Now().UTC().Format(time.RFC3339) }) + hubSrv := httptest.NewServer(mnemonhub.NewHTTPHandler(hub, mnemonhub.BearerAuthenticator{Tokens: tokens}, nil)) + defer hubSrv.Close() + + foreignFields := remoteProgressFields("hub-cli-remote-entry", "manual sync pull imports from mnemonhub") + foreignMaterial := contract.SyncedEventMaterial{ + OriginReplicaID: "other-replica", + LocalDecisionID: "dec-hub-cli-remote", + LocalIngestSeq: 9, + Actor: "codex@other", + ResourceRef: ref, + ResourceVersion: 1, + FieldsDigest: syncTestDigest(foreignFields), + Fields: foreignFields, + DecidedAt: "2026-06-30T00:00:00Z", + Status: "pending", + } + otherClient, err := access.NewSyncClient(hubSrv.URL, access.SyncClientConfig{Token: "tok-other"}) + if err != nil { + t.Fatalf("build other hub client: %v", err) + } + if resp, err := otherClient.SyncPush(contract.SyncPushRequest{ + ReplicaID: "other-replica", + BatchID: "seed-hub-cli-remote", + Events: syncTestEvents(t, foreignMaterial), + }); err != nil || len(resp.Accepted) != 1 { + t.Fatalf("seed hub remote material: resp=%+v err=%v", resp, err) + } + + syncRoot = root + syncStorePath = storePath + syncRemoteID = "hub" + syncRemoteURL = hubSrv.URL + syncRemoteToken = "tok-local" + var out bytes.Buffer + cmd := mustTestCommand(t) + cmd.SetOut(&out) + if err := runSyncPush(cmd, nil); err != nil { + t.Fatalf("sync push to mnemonhub: %v", err) + } + if !strings.Contains(out.String(), "Sync push: 1 accepted, 0 rejected, 0 conflicts") { + t.Fatalf("unexpected hub push output: %s", out.String()) + } + st, err := syncStatusForTest(storePath) + if err != nil { + t.Fatalf("status after hub push: %v", err) + } + if st.SyncPending != 0 || st.SyncSynced != 1 { + t.Fatalf("hub push must ack local synced event, got %+v", st) + } + hubStatus, err := hub.Status("replica-local@team") + if err != nil || hubStatus.HubEventsReceived != 2 { + t.Fatalf("hub must hold seeded and pushed events: %+v err=%v", hubStatus, err) + } + + out.Reset() + cmd = mustTestCommand(t) + cmd.SetOut(&out) + if err := runSyncPull(cmd, nil); err != nil { + t.Fatalf("sync pull from mnemonhub: %v", err) + } + if !strings.Contains(out.String(), "Sync pull: 1 events") { + t.Fatalf("unexpected hub pull output: %s", out.String()) + } + content := localResourceContentForTest(t, storePath, ref) + if !strings.Contains(content, "manual sync push reaches mnemonhub") || !strings.Contains(content, "manual sync pull imports from mnemonhub") { + t.Fatalf("manual hub round trip not visible through local presentation view:\n%s", content) + } + + out.Reset() + cmd = mustTestCommand(t) + cmd.SetOut(&out) + if err := runSyncPull(cmd, nil); err != nil { + t.Fatalf("second sync pull from mnemonhub: %v", err) + } + if !strings.Contains(out.String(), "Sync pull: 0 events") { + t.Fatalf("second hub pull must be cursor-idempotent, got %s", out.String()) + } + content = localResourceContentForTest(t, storePath, ref) + if strings.Count(content, "manual sync pull imports from mnemonhub") != 1 { + t.Fatalf("second hub pull duplicated imported progress:\n%s", content) + } +} + func TestSyncConnectWritesRemoteConfigWithoutLeakingToken(t *testing.T) { restoreSyncFlags(t) root := t.TempDir() From 2d17e0cc5e7cd9cdade16b9273e0aa31dca46e18 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 04:44:26 +0800 Subject: [PATCH 096/117] test: preserve partial multica hub evidence Capture child run and projection snapshots when Multica hub-flow acceptance fails before all expected assignment mailboxes or active agents arrive. This keeps failed complex teamwork runs useful for diagnosing which Multica artifacts already materialized. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance ./harness/cmd/mnemon-harness ./harness/internal/mnemonhub ./harness/internal/app ./harness/internal/coreguard; go test -count=1 ./... --- .../acceptance_multica_runtime.go | 54 ++++++++++ .../acceptance_multica_runtime_test.go | 99 +++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 87334555..116262a1 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -369,6 +369,7 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o report.ChildMetadata = childMeta if err != nil { addMulticaProdSimAssertion(report, "assignment child issue mailboxes created", false, err.Error()) + collectMulticaHubFlowPartialSnapshot(ctx, cli, opts, report, children, err) return err } addMulticaProdSimAssertion(report, "assignment child issue mailboxes created", len(children) > 0, fmt.Sprintf("children=%d", len(children))) @@ -388,6 +389,7 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o report.ActiveAgents = activeAgents if err != nil { addMulticaProdSimAssertion(report, "hub-flow activates multiple Multica agents", false, err.Error()) + collectMulticaHubFlowPartialSnapshot(ctx, cli, opts, report, children, err) return err } addMulticaProdSimAssertion(report, "hub-flow activates multiple Multica agents", len(activeAgents) >= opts.MinActiveAgents, fmt.Sprintf("active_agents=%v min=%d", activeAgents, opts.MinActiveAgents)) @@ -426,6 +428,58 @@ func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, o return nil } +func collectMulticaHubFlowPartialSnapshot(ctx context.Context, cli driver.MulticaCLI, opts multicaRuntimeProdSimOptions, report *multicaRuntimeProdSimReport, children []driver.MulticaIssue, reason error) { + if len(children) == 0 || strings.TrimSpace(report.Issue.ID) == "" { + return + } + snapshotCtx, cancel := multicaProdSimSnapshotContext(ctx) + defer cancel() + snapshotCLI := cli + if snapshotCLI.Timeout <= 0 || snapshotCLI.Timeout > 5*time.Second { + snapshotCLI.Timeout = 5 * time.Second + } + var runErr error + if len(report.ChildRuns) == 0 { + childRuns, childMessages, activeAgents, err := waitMulticaChildRunEvidence(snapshotCtx, snapshotCLI, report.Issue.ID, report.Runs, children, 0, opts.Poll, opts.MinActiveAgents) + report.ChildRuns = childRuns + report.ChildMessages = childMessages + report.ChildMessageTypes = multicaChildRunMessageTypeCounts(childMessages) + report.ActiveAgents = activeAgents + runErr = err + } + finalRoot, finalChildren, rootComments, childComments, projectionErr := waitMulticaHubProjectionCompletion(snapshotCtx, snapshotCLI, report.Issue.ID, children, 0, opts.Poll) + report.FinalRoot = finalRoot + report.FinalChildren = finalChildren + report.RootComments = rootComments + report.ChildComments = childComments + captured := len(report.ChildRuns) > 0 || len(report.FinalChildren) > 0 || multicaCommentCount(report.ChildComments) > 0 + addMulticaProdSimAssertion(report, "hub-flow partial evidence snapshot captured", captured, multicaProdSimPartialSnapshotDetail(reason, runErr, projectionErr, report)) +} + +func multicaProdSimSnapshotContext(ctx context.Context) (context.Context, context.CancelFunc) { + if ctx == nil || ctx.Err() != nil { + return context.WithTimeout(context.Background(), 15*time.Second) + } + return ctx, func() {} +} + +func multicaProdSimPartialSnapshotDetail(reason, runErr, projectionErr error, report *multicaRuntimeProdSimReport) string { + parts := []string{fmt.Sprintf("reason=%v", reason)} + if runErr != nil { + parts = append(parts, "child_runs="+runErr.Error()) + } + if projectionErr != nil { + parts = append(parts, "projection="+projectionErr.Error()) + } + parts = append(parts, + fmt.Sprintf("child_runs=%d", len(report.ChildRuns)), + fmt.Sprintf("active_agents=%v", report.ActiveAgents), + fmt.Sprintf("final_children=%d", len(report.FinalChildren)), + fmt.Sprintf("comments=%d", multicaCommentCount(report.ChildComments)), + ) + return strings.Join(parts, "; ") +} + func waitMulticaRuntimeEvidence(ctx context.Context, cli driver.MulticaCLI, issueID string, wait, poll time.Duration) ([]driver.MulticaIssueRun, []driver.MulticaRunMessage, error) { deadline := time.Now().Add(wait) var lastRuns []driver.MulticaIssueRun diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 800210c8..55ac2d90 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -227,6 +227,105 @@ esac } } +func TestMulticaRuntimeProdSimHubFlowWritesPartialSnapshotWhenMailboxExpectationMisses(t *testing.T) { + tmp := t.TempDir() + registryPath := filepath.Join(tmp, "registry.json") + var participants []driver.MulticaParticipantRecord + for _, role := range []string{"planner", "researcher", "implementer", "reviewer", "integrator"} { + participants = append(participants, driver.MulticaParticipantRecord{ + Principal: role + "@team", + AgentName: "mnemon-" + role, + AgentID: "agent-" + role, + Role: role, + }) + } + if err := driver.SaveMulticaRegistry(registryPath, driver.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-1", + RuntimeProfileID: "profile-1", + RuntimeID: "runtime-1", + Participants: participants, + }); err != nil { + t.Fatal(err) + } + argsPath := filepath.Join(tmp, "args.txt") + stdinPath := filepath.Join(tmp, "stdin.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +cat >> "$MULTICA_STDIN_PATH" +case "$*" in + *"issue create"*) printf '{"id":"root-partial","identifier":"TEA-30","title":"Partial hub evidence","description":"Teamwork acceptance","status":"todo"}\n' ;; + *"issue get root-partial"*) printf '{"id":"root-partial","identifier":"TEA-30","title":"Partial hub evidence","description":"Teamwork acceptance","status":"in_progress"}\n' ;; + *"issue get child-partial"*) printf '%s\n' '{"id":"child-partial","identifier":"TEA-31","title":"TEA-30: routing","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-30](mention://issue/root-partial) - Partial hub evidence\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing\n\n## Feedback\n\n- Expected feedback: result","status":"done"}' ;; + *"issue metadata list root-partial"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"session_mailbox"},{"key":"mnemon.session_id","value":"multica:session:root-partial"}]\n' ;; + *"issue children root-partial"*) printf '{"children":[{"id":"child-partial","identifier":"TEA-31","title":"TEA-30: routing","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-partial","mnemon.session_id":"multica:session:root-partial","mnemon.assignment_id":"asg-partial","mnemon.principal":"researcher@team"}}]}\n' ;; + *"issue comment list root-partial"*) printf '[{"id":"root-comment","issue_id":"root-partial","content":"Mnemon update: issue admitted\\n\\nmnemon:event=root"}]\n' ;; + *"issue comment list child-partial"*) printf '[{"id":"child-comment","issue_id":"child-partial","content":"Mnemon update: assignment feedback\\n\\nSummary: partial evidence exists.\\n\\nmnemon:event=pg-partial"}]\n' ;; + *"issue runs root-partial"*) printf '[{"id":"task-root","issue_id":"root-partial","agent_id":"agent-planner","status":"completed","completed_at":"2026-06-30T09:00:00Z","workspace_id":"ws-1"}]\n' ;; + *"issue run-messages task-root"*) printf '[{"task_id":"task-root","issue_id":"root-partial","seq":1,"type":"text","content":"Mnemon Multica runtime handled issue TEA-30. Mnemon ingest: recorded.","created_at":"2026-06-30T09:00:01Z"},{"task_id":"task-root","issue_id":"root-partial","seq":2,"type":"tool_use","content":"mnemond ingest observe","created_at":"2026-06-30T09:00:02Z"},{"task_id":"task-root","issue_id":"root-partial","seq":3,"type":"tool_result","content":"recorded","created_at":"2026-06-30T09:00:03Z"}]\n' ;; + *"issue runs child-partial"*) printf '[{"id":"task-child","issue_id":"child-partial","agent_id":"agent-researcher","status":"completed","completed_at":"2026-06-30T09:01:00Z","workspace_id":"ws-1"}]\n' ;; + *"issue run-messages task-child"*) printf '[{"task_id":"task-child","issue_id":"child-partial","seq":1,"type":"text","content":"Mnemon Multica runtime handled issue TEA-31. Mnemon assignment mailbox: correlated.","created_at":"2026-06-30T09:01:01Z"}]\n' ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("MULTICA_ARGS_PATH", argsPath) + t.Setenv("MULTICA_STDIN_PATH", stdinPath) + + report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ + RunRoot: filepath.Join(tmp, ".testdata", "multica-hub-partial"), + MulticaBin: bin, + WorkspaceID: "ws-1", + RegistryPath: registryPath, + AssigneePrincipal: "planner@team", + TaskCase: multicaAcceptanceTaskCaseParallelPoc, + IssueTitle: "Partial hub evidence", + IssueDescription: "Teamwork acceptance", + Wait: time.Millisecond, + Poll: time.Millisecond, + RequireIngest: true, + RequireHubFlow: true, + MinParticipants: 5, + MinActiveAgents: 5, + }) + if err == nil { + t.Fatalf("expected mailbox expectation failure: %+v", report) + } + if report.Status != "failed" || report.ReportPath == "" { + t.Fatalf("report mismatch: %+v", report) + } + if len(report.ChildIssues) != 1 || len(report.ChildRuns["child-partial"]) != 1 || len(report.ChildMessages["child-partial"]) != 1 { + t.Fatalf("partial child run evidence missing: %+v", report) + } + if len(report.FinalChildren) != 1 || multicaCommentCount(report.ChildComments) != 1 { + t.Fatalf("partial projection evidence missing: %+v", report) + } + var snapshotAssertion bool + for _, assertion := range report.Assertions { + if assertion.Name == "hub-flow partial evidence snapshot captured" { + snapshotAssertion = assertion.Passed + break + } + } + if !snapshotAssertion { + t.Fatalf("missing partial snapshot assertion: %+v", report.Assertions) + } + data, err := os.ReadFile(report.ReportPath) + if err != nil { + t.Fatal(err) + } + var written multicaRuntimeProdSimReport + if err := json.Unmarshal(data, &written); err != nil { + t.Fatalf("report JSON: %v\n%s", err, data) + } + if len(written.ChildRuns["child-partial"]) != 1 || multicaCommentCount(written.ChildComments) != 1 { + t.Fatalf("written report missing partial evidence: %+v", written) + } +} + func TestMulticaRuntimeProdSimHubFlowAllowsDeferredRunMessages(t *testing.T) { tmp := t.TempDir() registryPath := filepath.Join(tmp, "registry.json") From 213674db4b3f737cb49cabed785b8bb84b1eabc6 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 04:46:29 +0800 Subject: [PATCH 097/117] fix: let acceptance interrupts write reports Run mnemon-acceptance commands with a signal-aware Cobra context so Ctrl-C cancels waits through cmd.Context instead of bypassing report finalization paths. This pairs with Multica partial evidence snapshots for long hub-flow runs. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance; git diff --check; go test -count=1 ./... --- harness/cmd/mnemon-acceptance/root.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/harness/cmd/mnemon-acceptance/root.go b/harness/cmd/mnemon-acceptance/root.go index 9728863b..6e55494a 100644 --- a/harness/cmd/mnemon-acceptance/root.go +++ b/harness/cmd/mnemon-acceptance/root.go @@ -1,8 +1,10 @@ package main import ( + "context" "fmt" "os" + "os/signal" "github.com/spf13/cobra" ) @@ -19,7 +21,9 @@ var rootCmd = &cobra.Command{ } func main() { - if err := rootCmd.Execute(); err != nil { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + if err := rootCmd.ExecuteContext(ctx); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } From 7683fe6271cb1f41627d98881e4a836d0ecfd038 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 04:51:21 +0800 Subject: [PATCH 098/117] test: keep mnemonhub daemon role as exchange Add a daemon snapshot guard proving mnemonhub produces only an interaction watcher and never a projection worker. This reinforces the R2 boundary that mnemonhub is the remote exchange backend, not an activation carrier or projection surface. Validation: go test -count=1 ./harness/cmd/mnemon-harness; git diff --check; go test -count=1 ./harness/cmd/mnemon-harness ./harness/internal/productconfig ./harness/internal/daemon ./harness/internal/coreguard; go test -count=1 ./... --- .../cmd/mnemon-harness/config_daemon_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index 0f975580..0b7fab32 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -220,6 +220,25 @@ func TestConfiguredDaemonSnapshotIncludesConfiguredRoles(t *testing.T) { } } +func TestConfiguredDaemonSnapshotKeepsMnemonhubAsExchangeWatcher(t *testing.T) { + now := time.Date(2026, 6, 29, 10, 5, 0, 0, time.UTC) + cfg := productconfig.Default() + cfg.Connections.Mnemonhub = productconfig.MnemonhubConnection{Enabled: true, Endpoint: "https://hub.example.invalid"} + cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMnemonhub} + + snapshot := configuredDaemonSnapshot(cfg, now) + worker, ok := snapshot.Workers["mnemonhub-watch"] + if !ok { + t.Fatalf("snapshot missing mnemonhub watcher: %+v", snapshot.Workers) + } + if worker.Kind != daemon.WorkerInteraction || worker.Status != "configured" || worker.Message != "watcher=mnemonhub" { + t.Fatalf("mnemonhub watcher mismatch: %+v", worker) + } + if _, ok := snapshot.Workers["mnemonhub-project"]; ok { + t.Fatalf("mnemonhub must remain a remote exchange watcher, not projection worker: %+v", snapshot.Workers) + } +} + func TestLoadDaemonSnapshotConfigRejectsInvalidProductConfig(t *testing.T) { root := t.TempDir() reg := multicasurface.MulticaRegistry{ From 75db5fc326dd3c91706840d5558442b19eee403f Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:01:54 +0800 Subject: [PATCH 099/117] fix: absolutize multica participant env paths Normalize local registry, managed workspace, and inferred control token file paths before writing Multica participant env. Live protocol-react validation showed relative token paths are resolved from the Multica runtime cwd and can prevent Local Mnemon ingest. Validation: go test -count=1 ./harness/cmd/mnemon-harness; git diff --check; go test -count=1 ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-acceptance ./harness/cmd/mnemon-multica-runtime ./harness/internal/surface/multica; go test -count=1 ./... --- harness/cmd/mnemon-harness/multica.go | 22 +++++++++++++-- harness/cmd/mnemon-harness/multica_test.go | 33 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 0a8476a7..d87d9b65 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -709,10 +709,13 @@ func ensureMulticaParticipantEnv(ctx context.Context, cli driver.MulticaCLI, par func multicaParticipantRuntimeEnv(cli driver.MulticaCLI, participant driver.MulticaParticipantRecord, registryPath, workspaceID string, opts multicaParticipantEnvOptions) map[string]string { env := map[string]string{} + registryPath = multicaLocalEnvPath(registryPath) + managedWorkspace := multicaLocalEnvPath(opts.ManagedWorkspace) controlTokenFile := strings.TrimSpace(opts.ControlTokenFile) if controlTokenFile == "" && strings.TrimSpace(opts.ControlToken) == "" { - controlTokenFile = defaultMulticaParticipantControlTokenFile(opts.ManagedWorkspace, participant.Principal) + controlTokenFile = defaultMulticaParticipantControlTokenFile(managedWorkspace, participant.Principal) } + controlTokenFile = multicaLocalEnvPath(controlTokenFile) addStringEnv(env, "MNEMON_HUB_BACKEND", driver.MulticaHubBackend) addStringEnv(env, "MNEMON_MULTICA_REGISTRY", registryPath) addStringEnv(env, "MNEMON_MULTICA_WORKSPACE_ID", workspaceID) @@ -726,13 +729,28 @@ func multicaParticipantRuntimeEnv(cli driver.MulticaCLI, participant driver.Mult addStringEnv(env, "MNEMON_HARNESS_BIN", opts.HarnessBin) addStringEnv(env, "MNEMON_MANAGED_RUNTIME", opts.ManagedRuntime) addStringEnv(env, "MNEMON_MANAGED_COMMAND", opts.ManagedCommand) - addStringEnv(env, "MNEMON_MANAGED_WORKSPACE", opts.ManagedWorkspace) + addStringEnv(env, "MNEMON_MANAGED_WORKSPACE", managedWorkspace) if opts.ManagedTimeout > 0 { env["MNEMON_MANAGED_TURN_TIMEOUT"] = opts.ManagedTimeout.String() } return env } +func multicaLocalEnvPath(path string) string { + path = strings.TrimSpace(path) + if path == "" { + return "" + } + if filepath.IsAbs(path) { + return filepath.Clean(path) + } + abs, err := filepath.Abs(path) + if err != nil { + return filepath.Clean(path) + } + return abs +} + func mergeMulticaParticipantRuntimeEnv(existing, desired map[string]string) map[string]string { merged := cloneStringMap(existing) for _, key := range multicaParticipantRuntimeEnvKeys { diff --git a/harness/cmd/mnemon-harness/multica_test.go b/harness/cmd/mnemon-harness/multica_test.go index 07cf4453..f99af2e3 100644 --- a/harness/cmd/mnemon-harness/multica_test.go +++ b/harness/cmd/mnemon-harness/multica_test.go @@ -326,6 +326,39 @@ func TestMergeMulticaParticipantRuntimeEnvPrunesStaleManagedKeys(t *testing.T) { } } +func TestMulticaParticipantRuntimeEnvUsesAbsoluteLocalPaths(t *testing.T) { + restoreMulticaFlags(t) + + tmp := t.TempDir() + t.Chdir(tmp) + workspace := "managed-workspace" + tokenDir := filepath.Join(workspace, ".mnemon", "harness", "channel", "credentials") + if err := os.MkdirAll(tokenDir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tokenDir, "planner-team.token"), []byte("token\n"), 0o600); err != nil { + t.Fatal(err) + } + multicaProfile = "desktop-api.multica.ai" + env := multicaParticipantRuntimeEnv(driver.MulticaCLI{Command: "multica"}, driver.MulticaParticipantRecord{ + Principal: "planner@team", + AgentName: "mnemon-planner", + AgentID: "agent-planner", + }, filepath.Join("state", "registry.json"), "ws-1", multicaParticipantEnvOptions{ + ManagedWorkspace: workspace, + }) + + for _, key := range []string{"MNEMON_MULTICA_REGISTRY", "MNEMON_MANAGED_WORKSPACE", "MNEMON_CONTROL_TOKEN_FILE"} { + value := env[key] + if value == "" || !filepath.IsAbs(value) { + t.Fatalf("%s should be absolute, got %q in %+v", key, value, env) + } + } + if want := filepath.Join(tmp, workspace, ".mnemon", "harness", "channel", "credentials", "planner-team.token"); env["MNEMON_CONTROL_TOKEN_FILE"] != want { + t.Fatalf("token file = %q, want %q", env["MNEMON_CONTROL_TOKEN_FILE"], want) + } +} + func restoreMulticaFlags(t *testing.T) { t.Helper() oldBin := multicaBin From d438a159ce050af72f11477b0de8bcf54e12b4bf Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:13:41 +0800 Subject: [PATCH 100/117] refactor: move daemon role snapshots internal Move product-configured daemon worker snapshot construction out of the mnemon-harness command layer and into harness/internal/daemon, keeping the command focused on loading config and reporting status. Validation: go test -count=1 ./...; go test -count=1 ./harness/internal/daemon ./harness/cmd/mnemon-harness ./harness/internal/coreguard; git diff --check. --- .../cmd/mnemon-harness/config_daemon_test.go | 48 ------------ harness/cmd/mnemon-harness/daemon.go | 61 +-------------- harness/internal/daemon/configured.go | 77 +++++++++++++++++++ harness/internal/daemon/configured_test.go | 68 ++++++++++++++++ 4 files changed, 148 insertions(+), 106 deletions(-) create mode 100644 harness/internal/daemon/configured.go create mode 100644 harness/internal/daemon/configured_test.go diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index 0b7fab32..9de3666c 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -191,54 +191,6 @@ func TestDaemonStatusShowsWorkerSnapshot(t *testing.T) { } } -func TestConfiguredDaemonSnapshotIncludesConfiguredRoles(t *testing.T) { - now := time.Date(2026, 6, 29, 9, 50, 0, 0, time.UTC) - cfg := productconfig.Default() - cfg.Daemon.InteractionWatchers = []string{" ", productconfig.ConnectionMultica} - cfg.Daemon.DriveSources = []string{"", productconfig.DriveManagedLocal} - cfg.Daemon.ProjectionSurfaces = []string{"\t", productconfig.ConnectionMultica} - - snapshot := configuredDaemonSnapshot(cfg, now) - for name, want := range map[string]daemon.WorkerKind{ - "multica-watch": daemon.WorkerInteraction, - "managed-drive": daemon.WorkerDrive, - "multica-project": daemon.WorkerProjection, - "status-readiness": daemon.WorkerStatus, - } { - worker, ok := snapshot.Workers[name] - if !ok { - t.Fatalf("snapshot missing worker %q: %+v", name, snapshot.Workers) - } - if worker.Kind != want || worker.Status != "configured" || !worker.StartedAt.Equal(now) || !worker.UpdatedAt.Equal(now) { - t.Fatalf("worker %q mismatch: %+v", name, worker) - } - } - for _, name := range []string{"-watch", "-drive", "-project"} { - if _, ok := snapshot.Workers[name]; ok { - t.Fatalf("snapshot should skip empty worker name %q: %+v", name, snapshot.Workers) - } - } -} - -func TestConfiguredDaemonSnapshotKeepsMnemonhubAsExchangeWatcher(t *testing.T) { - now := time.Date(2026, 6, 29, 10, 5, 0, 0, time.UTC) - cfg := productconfig.Default() - cfg.Connections.Mnemonhub = productconfig.MnemonhubConnection{Enabled: true, Endpoint: "https://hub.example.invalid"} - cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMnemonhub} - - snapshot := configuredDaemonSnapshot(cfg, now) - worker, ok := snapshot.Workers["mnemonhub-watch"] - if !ok { - t.Fatalf("snapshot missing mnemonhub watcher: %+v", snapshot.Workers) - } - if worker.Kind != daemon.WorkerInteraction || worker.Status != "configured" || worker.Message != "watcher=mnemonhub" { - t.Fatalf("mnemonhub watcher mismatch: %+v", worker) - } - if _, ok := snapshot.Workers["mnemonhub-project"]; ok { - t.Fatalf("mnemonhub must remain a remote exchange watcher, not projection worker: %+v", snapshot.Workers) - } -} - func TestLoadDaemonSnapshotConfigRejectsInvalidProductConfig(t *testing.T) { root := t.TempDir() reg := multicasurface.MulticaRegistry{ diff --git a/harness/cmd/mnemon-harness/daemon.go b/harness/cmd/mnemon-harness/daemon.go index 2484d9d3..3fe7fa17 100644 --- a/harness/cmd/mnemon-harness/daemon.go +++ b/harness/cmd/mnemon-harness/daemon.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "sort" - "strings" "time" "github.com/mnemon-dev/mnemon/harness/internal/app" @@ -117,7 +116,8 @@ func runDaemonStatus(cmd *cobra.Command, args []string) error { } func writeDaemonRoleSummary(out io.Writer, cfg productconfig.Config) { - fmt.Fprintf(out, "Harness daemon roles: watchers=%d drive=%d surfaces=%d\n", len(cfg.Daemon.InteractionWatchers), len(cfg.Daemon.DriveSources), len(cfg.Daemon.ProjectionSurfaces)) + summary := daemon.RoleSummary(cfg) + fmt.Fprintf(out, "Harness daemon roles: watchers=%d drive=%d surfaces=%d\n", summary.InteractionWatchers, summary.DriveSources, summary.ProjectionSurfaces) } func writeConfiguredDaemonSnapshot(root string, now time.Time) error { @@ -125,7 +125,7 @@ func writeConfiguredDaemonSnapshot(root string, now time.Time) error { if err != nil || !ok { return err } - snapshot := configuredDaemonSnapshot(cfg, now) + snapshot := daemon.ConfiguredSnapshot(cfg, now) if len(snapshot.Workers) == 0 { return nil } @@ -146,61 +146,6 @@ func loadDaemonSnapshotConfig(root string) (productconfig.Config, bool, error) { return productconfig.Config{}, false, nil } -func configuredDaemonSnapshot(cfg productconfig.Config, now time.Time) daemon.Snapshot { - snapshot := daemon.Snapshot{StartedAt: now.UTC(), Workers: map[string]daemon.WorkerSnapshot{}} - add := func(name string, kind daemon.WorkerKind, message string) { - name = daemonWorkerName(name) - if name == "" { - return - } - snapshot.Workers[name] = daemon.WorkerSnapshot{ - Kind: kind, - Status: "configured", - Message: strings.TrimSpace(message), - StartedAt: snapshot.StartedAt, - UpdatedAt: snapshot.StartedAt, - } - } - for _, watcher := range cfg.Daemon.InteractionWatchers { - watcher = strings.TrimSpace(watcher) - if watcher == "" { - continue - } - add(watcher+"-watch", daemon.WorkerInteraction, "watcher="+watcher) - } - for _, source := range cfg.Daemon.DriveSources { - source = strings.TrimSpace(source) - if source == "" { - continue - } - name := source + "-drive" - if source == productconfig.DriveManagedLocal { - name = "managed-drive" - } - add(name, daemon.WorkerDrive, "drive="+source) - } - for _, surface := range cfg.Daemon.ProjectionSurfaces { - surface = strings.TrimSpace(surface) - if surface == "" { - continue - } - add(surface+"-project", daemon.WorkerProjection, "surface="+surface) - } - if len(snapshot.Workers) > 0 { - add("status-readiness", daemon.WorkerStatus, "daemon status snapshot") - } - return snapshot -} - -func daemonWorkerName(value string) string { - value = strings.TrimSpace(value) - if value == "" { - return "" - } - replacer := strings.NewReplacer(" ", "-", "/", "-", "\\", "-", ":", "-") - return replacer.Replace(value) -} - func writeDaemonSnapshotSummary(out io.Writer, root string) { snapshot, ok, err := daemon.NewFileSnapshotStore(daemon.StatusSnapshotPath(root, "")).Load() if err != nil { diff --git a/harness/internal/daemon/configured.go b/harness/internal/daemon/configured.go new file mode 100644 index 00000000..bef7b540 --- /dev/null +++ b/harness/internal/daemon/configured.go @@ -0,0 +1,77 @@ +package daemon + +import ( + "strings" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" +) + +type ConfiguredRoleSummary struct { + InteractionWatchers int + DriveSources int + ProjectionSurfaces int +} + +func RoleSummary(cfg productconfig.Config) ConfiguredRoleSummary { + return ConfiguredRoleSummary{ + InteractionWatchers: len(cfg.Daemon.InteractionWatchers), + DriveSources: len(cfg.Daemon.DriveSources), + ProjectionSurfaces: len(cfg.Daemon.ProjectionSurfaces), + } +} + +func ConfiguredSnapshot(cfg productconfig.Config, now time.Time) Snapshot { + snapshot := Snapshot{StartedAt: now.UTC(), Workers: map[string]WorkerSnapshot{}} + add := func(name string, kind WorkerKind, message string) { + name = workerName(name) + if name == "" { + return + } + snapshot.Workers[name] = WorkerSnapshot{ + Kind: kind, + Status: "configured", + Message: strings.TrimSpace(message), + StartedAt: snapshot.StartedAt, + UpdatedAt: snapshot.StartedAt, + } + } + for _, watcher := range cfg.Daemon.InteractionWatchers { + watcher = strings.TrimSpace(watcher) + if watcher == "" { + continue + } + add(watcher+"-watch", WorkerInteraction, "watcher="+watcher) + } + for _, source := range cfg.Daemon.DriveSources { + source = strings.TrimSpace(source) + if source == "" { + continue + } + name := source + "-drive" + if source == productconfig.DriveManagedLocal { + name = "managed-drive" + } + add(name, WorkerDrive, "drive="+source) + } + for _, surface := range cfg.Daemon.ProjectionSurfaces { + surface = strings.TrimSpace(surface) + if surface == "" { + continue + } + add(surface+"-project", WorkerProjection, "surface="+surface) + } + if len(snapshot.Workers) > 0 { + add("status-readiness", WorkerStatus, "daemon status snapshot") + } + return snapshot +} + +func workerName(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + replacer := strings.NewReplacer(" ", "-", "/", "-", "\\", "-", ":", "-") + return replacer.Replace(value) +} diff --git a/harness/internal/daemon/configured_test.go b/harness/internal/daemon/configured_test.go new file mode 100644 index 00000000..bd289508 --- /dev/null +++ b/harness/internal/daemon/configured_test.go @@ -0,0 +1,68 @@ +package daemon + +import ( + "testing" + "time" + + "github.com/mnemon-dev/mnemon/harness/internal/productconfig" +) + +func TestConfiguredSnapshotIncludesConfiguredRoles(t *testing.T) { + now := time.Date(2026, 6, 29, 9, 50, 0, 0, time.UTC) + cfg := productconfig.Default() + cfg.Daemon.InteractionWatchers = []string{" ", productconfig.ConnectionMultica} + cfg.Daemon.DriveSources = []string{"", productconfig.DriveManagedLocal} + cfg.Daemon.ProjectionSurfaces = []string{"\t", productconfig.ConnectionMultica} + + snapshot := ConfiguredSnapshot(cfg, now) + for name, want := range map[string]WorkerKind{ + "multica-watch": WorkerInteraction, + "managed-drive": WorkerDrive, + "multica-project": WorkerProjection, + "status-readiness": WorkerStatus, + } { + worker, ok := snapshot.Workers[name] + if !ok { + t.Fatalf("snapshot missing worker %q: %+v", name, snapshot.Workers) + } + if worker.Kind != want || worker.Status != "configured" || !worker.StartedAt.Equal(now) || !worker.UpdatedAt.Equal(now) { + t.Fatalf("worker %q mismatch: %+v", name, worker) + } + } + for _, name := range []string{"-watch", "-drive", "-project"} { + if _, ok := snapshot.Workers[name]; ok { + t.Fatalf("snapshot should skip empty worker name %q: %+v", name, snapshot.Workers) + } + } +} + +func TestConfiguredSnapshotKeepsMnemonhubAsExchangeWatcher(t *testing.T) { + now := time.Date(2026, 6, 29, 10, 5, 0, 0, time.UTC) + cfg := productconfig.Default() + cfg.Connections.Mnemonhub = productconfig.MnemonhubConnection{Enabled: true, Endpoint: "https://hub.example.invalid"} + cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMnemonhub} + + snapshot := ConfiguredSnapshot(cfg, now) + worker, ok := snapshot.Workers["mnemonhub-watch"] + if !ok { + t.Fatalf("snapshot missing mnemonhub watcher: %+v", snapshot.Workers) + } + if worker.Kind != WorkerInteraction || worker.Status != "configured" || worker.Message != "watcher=mnemonhub" { + t.Fatalf("mnemonhub watcher mismatch: %+v", worker) + } + if _, ok := snapshot.Workers["mnemonhub-project"]; ok { + t.Fatalf("mnemonhub must remain a remote exchange watcher, not projection worker: %+v", snapshot.Workers) + } +} + +func TestRoleSummaryReflectsConfiguredDaemonRoles(t *testing.T) { + cfg := productconfig.Default() + cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMultica, productconfig.ConnectionGitHub} + cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} + cfg.Daemon.ProjectionSurfaces = []string{productconfig.ConnectionMultica} + + got := RoleSummary(cfg) + if got.InteractionWatchers != 2 || got.DriveSources != 1 || got.ProjectionSurfaces != 1 { + t.Fatalf("role summary mismatch: %+v", got) + } +} From 12bc61947863c29318f2fb2d2763a42b743b1d3c Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:15:51 +0800 Subject: [PATCH 101/117] test: guard root cli cluster surface names Extend the root CLI boundary guard to scan root command string literals for R2 harness cluster surface names, so mnemond, mnemonhub, and Multica runtime naming stays out of the root mnemon CLI during this phase. Validation: go test -count=1 ./...; go test -count=1 ./harness/internal/coreguard; git diff --check. --- .../coreguard/root_cli_boundary_test.go | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/harness/internal/coreguard/root_cli_boundary_test.go b/harness/internal/coreguard/root_cli_boundary_test.go index 82fad491..db563645 100644 --- a/harness/internal/coreguard/root_cli_boundary_test.go +++ b/harness/internal/coreguard/root_cli_boundary_test.go @@ -27,6 +27,17 @@ var rootCLIForbiddenCommands = map[string]bool{ "multica-runtime-prod-sim": true, } +var rootCLIForbiddenSurfaceStrings = []string{ + "mnemond", + "mnemon-acceptance", + "mnemon-harness", + "mnemon-hub", + "mnemon-multica-runtime", + "mnemon-runtime-multica", + "mnemonhub", + "multica-runtime-prod-sim", +} + func TestRootMnemonCLIDoesNotImportHarnessCluster(t *testing.T) { for _, file := range rootCLIGoFiles(t) { fset := token.NewFileSet() @@ -43,6 +54,33 @@ func TestRootMnemonCLIDoesNotImportHarnessCluster(t *testing.T) { } } +func TestRootMnemonCLIDoesNotNameR2ClusterSurfaces(t *testing.T) { + for _, file := range rootCLIGoFiles(t) { + fset := token.NewFileSet() + parsed, err := parser.ParseFile(fset, file, nil, 0) + if err != nil { + t.Fatalf("parse file %s: %v", file, err) + } + ast.Inspect(parsed, func(node ast.Node) bool { + lit, ok := node.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return true + } + value, err := strconv.Unquote(lit.Value) + if err != nil { + t.Errorf("unquote string literal in %s: %v", file, err) + return true + } + for _, forbidden := range rootCLIForbiddenSurfaceStrings { + if strings.Contains(value, forbidden) { + t.Errorf("root mnemon CLI names R2 cluster surface %q in %s; keep this phase under harness/cmd", forbidden, file) + } + } + return true + }) + } +} + func TestRootMnemonCLIDoesNotExposeR2ClusterCommands(t *testing.T) { for _, file := range rootCLIGoFiles(t) { fset := token.NewFileSet() @@ -83,6 +121,11 @@ func TestRootCLIBoundaryGuardLogicIsNotVacuous(t *testing.T) { t.Fatalf("root CLI boundary guard must forbid %q", command) } } + for _, surface := range []string{"mnemond", "mnemon-harness", "mnemon-multica-runtime"} { + if !containsRootCLIForbiddenSurface(surface) { + t.Fatalf("root CLI surface guard must forbid %q", surface) + } + } for _, command := range []string{"remember", "recall", "search", "setup"} { if rootCLIForbiddenCommands[command] { t.Fatalf("root CLI boundary guard must allow existing memory command %q", command) @@ -90,6 +133,15 @@ func TestRootCLIBoundaryGuardLogicIsNotVacuous(t *testing.T) { } } +func containsRootCLIForbiddenSurface(surface string) bool { + for _, forbidden := range rootCLIForbiddenSurfaceStrings { + if forbidden == surface { + return true + } + } + return false +} + func rootCLIGoFiles(t *testing.T) []string { t.Helper() root := filepath.Join("..", "..", "..", "cmd") From 7abe936efd956e392fe24c4d75b8f750f21ff0ab Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:32:08 +0800 Subject: [PATCH 102/117] test: keep harness internals hidden Add a structured Cobra command-tree guard for mnemon-harness internal and debug commands so control, loop, Multica provisioning/import/projection shims, token, and tower remain hidden from the public product surface. Validation: go test -count=1 ./...; go test -count=1 ./harness/cmd/mnemon-harness ./harness/internal/coreguard; git diff --check. --- harness/cmd/mnemon-harness/root_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index b215b071..0501dae7 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -76,6 +76,31 @@ func TestProductHelpDoesNotExposeInternalVocabulary(t *testing.T) { } } +func TestInternalCommandsStayHidden(t *testing.T) { + for _, path := range [][]string{ + {"control"}, + {"loop"}, + {"multica"}, + {"multica", "import-issue"}, + {"multica", "participant"}, + {"multica", "project-comment"}, + {"multica", "provision"}, + {"token"}, + {"tower"}, + } { + cmd, _, err := rootCmd.Find(path) + if err != nil { + t.Fatalf("find command %q: %v", strings.Join(path, " "), err) + } + if cmd == nil || cmd.Name() != path[len(path)-1] { + t.Fatalf("command %q resolved to %+v", strings.Join(path, " "), cmd) + } + if !cmd.Hidden { + t.Fatalf("internal/debug command %q must remain hidden from public help", strings.Join(path, " ")) + } + } +} + func executeRootForHelp(t *testing.T, args ...string) string { t.Helper() var out bytes.Buffer From d74d6a4c0ccb7c27b0fdb40e9b6c814e715b63fb Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:34:31 +0800 Subject: [PATCH 103/117] fix: name multica runtime progress as adapter Replace stale runtime progress pseudo-commands that implied a root mnemon multica command surface with mnemon-multica-runtime adapter labels, and guard the runtime command tree against reintroducing those shapes. Validation: go test -count=1 ./...; go test -count=1 ./harness/cmd/mnemon-multica-runtime ./harness/internal/coreguard; git diff --check. --- harness/cmd/mnemon-multica-runtime/main.go | 6 +-- .../multica_runtime_role_boundary_test.go | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 2143db2b..82719f5d 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -438,7 +438,7 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres emitRuntimeProgress(progress, multicasurface.RuntimeWakeProgress(runtimeResultSummary(result))) s.writeMulticaHubArtifacts(multicaCtx, cli, client, issue, &result) mergeRuntimeHubProjectionDeltas(&result, earlyHubDeltas) - emitRuntimeCommand(progress, "mnemon multica hub project --issue "+issue.ID, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(result)), runtimeExitCode(result.HubWriteErr)) + emitRuntimeCommand(progress, "mnemon-multica-runtime hub-write --issue "+issue.ID, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(result)), runtimeExitCode(result.HubWriteErr)) emitRuntimeProgress(progress, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(result))) s.projectImportComment(multicaCtx, cli, issue, draft.ExternalID, draft.EventType, &result) emitRuntimeCommand(progress, "multica issue comment add "+issue.ID, multicasurface.RuntimeProjectionProgress(runtimeResultSummary(result)), runtimeExitCode(result.ProjectionErr)) @@ -471,7 +471,7 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr string(eventmodel.Subject("assignment", result.AssignmentID)), ) correlationProgress := multicasurface.RuntimeAssignmentCorrelationProgress() - emitRuntimeCommand(progress, "mnemon multica assignment correlate --issue "+issue.ID, correlationProgress, 0) + emitRuntimeCommand(progress, "mnemon-multica-runtime assignment-correlate --issue "+issue.ID, correlationProgress, 0) emitRuntimeProgress(progress, correlationProgress) addr := strings.TrimSpace(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_CONTROL_ADDR")) var client *access.Client @@ -492,7 +492,7 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr emitRuntimeCommand(progress, "mnemond managed wake --principal "+result.Principal+" [mnemon:wake]", multicasurface.RuntimeWakeProgress(runtimeResultSummary(*result)), runtimeExitCode(result.WakeErr)) emitRuntimeProgress(progress, multicasurface.RuntimeWakeProgress(runtimeResultSummary(*result))) s.writeMulticaHubArtifacts(ctx, cli, client, issue, result) - emitRuntimeCommand(progress, "mnemon multica hub project --issue "+issue.ID, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(*result)), runtimeExitCode(result.HubWriteErr)) + emitRuntimeCommand(progress, "mnemon-multica-runtime hub-write --issue "+issue.ID, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(*result)), runtimeExitCode(result.HubWriteErr)) emitRuntimeProgress(progress, multicasurface.RuntimeHubWriteProgress(runtimeResultSummary(*result))) } } diff --git a/harness/internal/coreguard/multica_runtime_role_boundary_test.go b/harness/internal/coreguard/multica_runtime_role_boundary_test.go index cb198082..1c66e908 100644 --- a/harness/internal/coreguard/multica_runtime_role_boundary_test.go +++ b/harness/internal/coreguard/multica_runtime_role_boundary_test.go @@ -4,7 +4,9 @@ import ( "go/ast" "go/parser" "go/token" + "os" "path/filepath" + "strconv" "strings" "testing" ) @@ -60,6 +62,44 @@ func TestMulticaRuntimeRoleBoundaryGuardLogicIsNotVacuous(t *testing.T) { } } +func TestMulticaRuntimeDoesNotAdvertiseRootMnemonMulticaCommands(t *testing.T) { + root := filepath.Join("..", "..", "cmd", "mnemon-multica-runtime") + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + return err + } + ast.Inspect(file, func(node ast.Node) bool { + lit, ok := node.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return true + } + value, err := strconv.Unquote(lit.Value) + if err != nil { + t.Errorf("unquote string literal in %s: %v", path, err) + return true + } + for _, forbidden := range []string{"mnemon multica", "mnemon-harness multica"} { + if strings.Contains(value, forbidden) { + t.Errorf("%s advertises forbidden root/harness Multica command shape %q at %s; runtime progress should name the adapter process", path, forbidden, fset.Position(lit.Pos())) + } + } + return true + }) + return nil + }) + if err != nil { + t.Fatalf("walk Multica runtime command: %v", err) + } +} + func selectorName(expr ast.Expr) string { switch e := expr.(type) { case *ast.Ident: From 352ee6961d4719778daaa9047a34ef8774a2e2a8 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:37:34 +0800 Subject: [PATCH 104/117] fix: hide legacy harness service verbs Move local and sync out of the mnemon-harness public product help while keeping the commands callable as hidden debug/service verbs. This keeps the visible harness surface aligned with setup, config, daemon, doctor, agent, connect, session, and status. Validation: go test -count=1 ./...; go test -count=1 ./harness/cmd/mnemon-harness ./harness/internal/coreguard; git diff --check. --- harness/cmd/mnemon-harness/local.go | 7 ++++--- harness/cmd/mnemon-harness/root_test.go | 6 ++++-- harness/cmd/mnemon-harness/sync.go | 7 ++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/harness/cmd/mnemon-harness/local.go b/harness/cmd/mnemon-harness/local.go index ceb25b73..6c2f00ed 100644 --- a/harness/cmd/mnemon-harness/local.go +++ b/harness/cmd/mnemon-harness/local.go @@ -22,8 +22,9 @@ var ( ) var localCmd = &cobra.Command{ - Use: "local", - Short: "Run and inspect Local Mnemon", + Use: "local", + Short: "Run and inspect Local Mnemon", + Hidden: true, } var localRunCmd = &cobra.Command{ @@ -81,7 +82,7 @@ func init() { localRunCmd.Flags().BoolVar(&localAllowInsecureRemote, "allow-insecure-remote", false, "let the background sync worker use a plaintext http:// Remote Workspace endpoint with a non-loopback host (T2: fail-closed by default)") _ = localRunCmd.Flags().MarkHidden("bindings") localCmd.AddCommand(localRunCmd, localStatusCmd, localStopCmd) - localCmd.GroupID = groupSpine + localCmd.GroupID = groupAdvanced rootCmd.AddCommand(localCmd) } diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index 0501dae7..af0ef23a 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -22,7 +22,7 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help returned error: %v", err) } got := out.String() - for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "local", "config", "daemon", "doctor", "session", "agent", "connect"} { + for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "config", "daemon", "doctor", "session", "agent", "connect"} { if !strings.Contains(got, want) { t.Fatalf("expected root help to contain %q:\n%s", want, got) } @@ -32,7 +32,7 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help leaked unsupported product term %q:\n%s", blocked, got) } } - for _, blocked := range []string{" multica", " tower", " token"} { + for _, blocked := range []string{" local", " multica", " sync", " tower", " token"} { if strings.Contains(got, blocked) { t.Fatalf("root help leaked debug command %q:\n%s", strings.TrimSpace(blocked), got) } @@ -80,11 +80,13 @@ func TestInternalCommandsStayHidden(t *testing.T) { for _, path := range [][]string{ {"control"}, {"loop"}, + {"local"}, {"multica"}, {"multica", "import-issue"}, {"multica", "participant"}, {"multica", "project-comment"}, {"multica", "provision"}, + {"sync"}, {"token"}, {"tower"}, } { diff --git a/harness/cmd/mnemon-harness/sync.go b/harness/cmd/mnemon-harness/sync.go index 58be586c..10084439 100644 --- a/harness/cmd/mnemon-harness/sync.go +++ b/harness/cmd/mnemon-harness/sync.go @@ -37,8 +37,9 @@ var ( ) var syncCmd = &cobra.Command{ - Use: "sync", - Short: "Sync Local Mnemon with Remote Workspace", + Use: "sync", + Short: "Sync Local Mnemon with Remote Workspace", + Hidden: true, } var syncConnectCmd = &cobra.Command{ @@ -88,7 +89,7 @@ func init() { syncRunCmd.Flags().BoolVar(&syncBackground, "background", false, "run until interrupted") syncRunCmd.Flags().DurationVar(&syncInterval, "interval", 30*time.Second, "background sync interval") syncCmd.AddCommand(syncConnectCmd, syncPushCmd, syncPullCmd, syncRunCmd) - syncCmd.GroupID = groupSpine + syncCmd.GroupID = groupAdvanced rootCmd.AddCommand(syncCmd) } From 8347506453a00d9c4225a5c4da1f6a79892735b7 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:40:26 +0800 Subject: [PATCH 105/117] test: pin harness public command surface Add an exact root command whitelist for the R2 product harness surface, keeping agent/config/connect/daemon/doctor/session/setup/status as the only visible commands while hidden internals remain callable by explicit path. Validation: go test -count=1 ./harness/cmd/mnemon-harness; go test -count=1 ./harness/cmd/mnemon-harness ./harness/internal/coreguard; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemon-harness/root_test.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index af0ef23a..0e5ded38 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "os" + "sort" "strings" "testing" ) @@ -22,7 +23,7 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { t.Fatalf("root help returned error: %v", err) } got := out.String() - for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "config", "daemon", "doctor", "session", "agent", "connect"} { + for _, want := range []string{"event-driven", "collaboration substrate", "Teamwork is a profile", "Agent Integration", "Local Mnemon", "setup", "config", "daemon", "doctor", "status", "session", "agent", "connect"} { if !strings.Contains(got, want) { t.Fatalf("expected root help to contain %q:\n%s", want, got) } @@ -39,6 +40,15 @@ func TestRootHelpUsesLocalFirstProductSurface(t *testing.T) { } } +func TestPublicRootCommandsMatchProductSurface(t *testing.T) { + got := publicRootCommandNames() + want := []string{"agent", "config", "connect", "daemon", "doctor", "session", "setup", "status"} + sort.Strings(want) + if strings.Join(got, "\x00") != strings.Join(want, "\x00") { + t.Fatalf("public root commands mismatch:\ngot: %v\nwant: %v", got, want) + } +} + func TestRootDoesNotExposeAcceptanceCommands(t *testing.T) { commands := map[string]bool{} for _, cmd := range rootCmd.Commands() { @@ -103,6 +113,18 @@ func TestInternalCommandsStayHidden(t *testing.T) { } } +func publicRootCommandNames() []string { + var names []string + for _, cmd := range rootCmd.Commands() { + if cmd.Hidden || cmd.Name() == "help" { + continue + } + names = append(names, cmd.Name()) + } + sort.Strings(names) + return names +} + func executeRootForHelp(t *testing.T, args ...string) string { t.Helper() var out bytes.Buffer From df99daf6332ef51e9800a066429380b6aae04b17 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:44:11 +0800 Subject: [PATCH 106/117] fix: let mnemond help exit cleanly Treat flag.ErrHelp as a successful service help path for the local event node and its local managed-drive command, matching the exchange hub behavior while keeping other flag errors fail-closed. Validation: go test -count=1 ./harness/cmd/mnemond; go run ./harness/cmd/mnemond --help; go run ./harness/cmd/mnemond agent run --help; go test -count=1 ./harness/cmd/mnemond ./harness/cmd/mnemon-hub ./harness/internal/coreguard; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemond/main.go | 12 +++++++++++- harness/cmd/mnemond/main_test.go | 10 ++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/harness/cmd/mnemond/main.go b/harness/cmd/mnemond/main.go index df1fa368..cc59b887 100644 --- a/harness/cmd/mnemond/main.go +++ b/harness/cmd/mnemond/main.go @@ -13,6 +13,7 @@ package main import ( "context" + "errors" "flag" "fmt" "io" @@ -51,13 +52,22 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { case "logs": return daemonLogs(args[1:], out, errw) case "agent": - return runAgent(ctx, args[1:], out, errw) + if err := runAgent(ctx, args[1:], out, errw); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + return nil case "serve": args = args[1:] } } cfg, err := parseServe(args, errw) if err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } return serveForeground(ctx, cfg, out) diff --git a/harness/cmd/mnemond/main_test.go b/harness/cmd/mnemond/main_test.go index 577e0b94..3f836cc2 100644 --- a/harness/cmd/mnemond/main_test.go +++ b/harness/cmd/mnemond/main_test.go @@ -3,8 +3,6 @@ package main import ( "bytes" "context" - "errors" - "flag" "io" "strings" "testing" @@ -48,8 +46,8 @@ func TestRunRefusesNonLoopbackAddr(t *testing.T) { func TestHelpDescribesLocalEventNode(t *testing.T) { var errw bytes.Buffer err := run(context.Background(), []string{"--help"}, io.Discard, &errw) - if !errors.Is(err, flag.ErrHelp) { - t.Fatalf("help error = %v, want flag.ErrHelp", err) + if err != nil { + t.Fatalf("help should exit successfully, got %v", err) } got := errw.String() for _, want := range []string{"Local Mnemon event node", "local event API", "admission", "state", "presentation", "drive candidates"} { @@ -62,8 +60,8 @@ func TestHelpDescribesLocalEventNode(t *testing.T) { func TestAgentRunHelpFramesLocalDriveSource(t *testing.T) { var errw bytes.Buffer err := run(context.Background(), []string{"agent", "run", "--help"}, io.Discard, &errw) - if !errors.Is(err, flag.ErrHelp) { - t.Fatalf("agent run help error = %v, want flag.ErrHelp", err) + if err != nil { + t.Fatalf("agent run help should exit successfully, got %v", err) } got := errw.String() for _, want := range []string{"local drive source", "[mnemon:wake]", "managed runtime"} { From f15dab2fd36f3e52f5f0f553bb63477de90e0209 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:47:07 +0800 Subject: [PATCH 107/117] fix: describe multica runtime adapter help Add a successful help path for mnemon-multica-runtime, including probe help, so the independent adapter advertises its stdio runtime boundary, mnemond ingest path, local [mnemon:wake] behavior, activation trace, and Multica projection role without naming root or harness Multica command shapes. Validation: go test -count=1 ./harness/cmd/mnemon-multica-runtime; go run ./harness/cmd/mnemon-multica-runtime --help; go run ./harness/cmd/mnemon-multica-runtime --probe --help; go test -count=1 ./harness/cmd/mnemon-multica-runtime ./harness/internal/surface/multica ./harness/internal/coreguard; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemon-multica-runtime/main.go | 29 ++++++++++++++++ .../cmd/mnemon-multica-runtime/main_test.go | 34 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 82719f5d..87354d2f 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -132,12 +132,20 @@ func runRuntime(cfg runtimeConfig) error { } } if runtimeProbeModeEnabled(cfg.Args) { + if wantsRuntimeHelp(cfg.Args) { + writeRuntimeHelp(cfg.Stdout) + return nil + } return runRuntimeProbe(cfg) } if wantsVersion(cfg.Args) { fmt.Fprintf(cfg.Stdout, "%s %s\n", multicasurface.MulticaRuntimeCommandName, runtimeVersion) return nil } + if wantsRuntimeHelp(cfg.Args) { + writeRuntimeHelp(cfg.Stdout) + return nil + } return runRuntimeRPC(cfg, cwd) } @@ -1007,6 +1015,27 @@ func wantsVersion(args []string) bool { return false } +func wantsRuntimeHelp(args []string) bool { + for _, arg := range args { + switch strings.TrimSpace(arg) { + case "help", "--help", "-help", "-h": + return true + } + } + return false +} + +func writeRuntimeHelp(w io.Writer) { + fmt.Fprintf(w, "%s is an external Multica runtime adapter for the Mnemon event system.\n", multicasurface.MulticaRuntimeCommandName) + fmt.Fprintln(w) + fmt.Fprintln(w, "Usage:") + fmt.Fprintf(w, " %s app-server --listen stdio://\n", multicasurface.MulticaRuntimeCommandName) + fmt.Fprintf(w, " %s --version\n", multicasurface.MulticaRuntimeCommandName) + fmt.Fprintf(w, " %s --probe app-server --listen stdio://\n", multicasurface.MulticaRuntimeCommandName) + fmt.Fprintln(w) + fmt.Fprintln(w, "The adapter receives Multica runtime RPC on stdin/stdout, loads issue context through the Multica CLI, submits structured EventMaterial to mnemond when configured, wakes only the locally owned managed agent with [mnemon:wake], streams activation trace, and projects accepted state back to Multica.") +} + func runtimeProbeModeEnabled(args []string) bool { for _, arg := range args { switch strings.TrimSpace(arg) { diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index ada60542..6894d681 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -71,6 +71,40 @@ func runtimeTestEnv(values ...string) []string { return append(env, values...) } +func TestRuntimeHelpDescribesIndependentAdapter(t *testing.T) { + for _, args := range [][]string{{"--help"}, {"--probe", "--help"}} { + var out bytes.Buffer + err := runRuntime(runtimeConfig{ + Args: args, + CWD: t.TempDir(), + Stdin: strings.NewReader(""), + Stdout: &out, + }) + if err != nil { + t.Fatalf("%v help should exit successfully, got %v", args, err) + } + got := out.String() + for _, want := range []string{ + "external Multica runtime adapter", + "Mnemon event system", + "stdin/stdout", + "structured EventMaterial", + "mnemond", + "[mnemon:wake]", + "projects accepted state back to Multica", + } { + if !strings.Contains(got, want) { + t.Fatalf("%v runtime help missing %q:\n%s", args, want, got) + } + } + for _, blocked := range []string{"mnemon multica", "mnemon-harness multica", "global planner"} { + if strings.Contains(got, blocked) { + t.Fatalf("%v runtime help leaked forbidden command/role wording %q:\n%s", args, blocked, got) + } + } + } +} + func TestRuntimeImportUsesStructuredIssueIdentity(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "multica.args") From 634baefa7d46133b8a72f2c265bbd518bdc1241e Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:51:29 +0800 Subject: [PATCH 108/117] fix: show mnemond service command help Expand mnemond help so the local event node exposes its service commands and local managed-agent drive-source subcommand, and align the command comment with the event-node boundary. Validation: go test -count=1 ./harness/cmd/mnemond; go run ./harness/cmd/mnemond --help; go run ./harness/cmd/mnemond agent --help; go test -count=1 ./harness/cmd/mnemond ./harness/cmd/mnemon-hub ./harness/internal/coreguard; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemond/agent.go | 13 +++++++++ harness/cmd/mnemond/main.go | 46 +++++++++++++++++++++++++------- harness/cmd/mnemond/main_test.go | 24 ++++++++++++++--- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/harness/cmd/mnemond/agent.go b/harness/cmd/mnemond/agent.go index 4f7dfc89..2b701832 100644 --- a/harness/cmd/mnemond/agent.go +++ b/harness/cmd/mnemond/agent.go @@ -21,6 +21,9 @@ func runAgent(ctx context.Context, args []string, out, errw io.Writer) error { return fmt.Errorf("agent requires a subcommand") } switch args[0] { + case "help", "--help", "-help", "-h": + writeAgentHelp(errw) + return flag.ErrHelp case "run": return runAgentRun(ctx, args[1:], out, errw) default: @@ -28,6 +31,16 @@ func runAgent(ctx context.Context, args []string, out, errw io.Writer) error { } } +func writeAgentHelp(errw io.Writer) { + fmt.Fprintln(errw, "mnemond agent commands operate local managed-agent drive sources for one local principal.") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Usage:") + fmt.Fprintln(errw, " mnemond agent run [flags]") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Commands:") + fmt.Fprintln(errw, " run Render local wake candidates and send only [mnemon:wake] to the selected managed runtime") +} + func runAgentRun(ctx context.Context, args []string, out, errw io.Writer) error { fs := flag.NewFlagSet("mnemond agent run", flag.ContinueOnError) fs.SetOutput(errw) diff --git a/harness/cmd/mnemond/main.go b/harness/cmd/mnemond/main.go index cc59b887..c3f52e05 100644 --- a/harness/cmd/mnemond/main.go +++ b/harness/cmd/mnemond/main.go @@ -1,9 +1,9 @@ -// mnemond is the LOCAL governance daemon: the standalone-daemon packaging of the exact -// `mnemon-harness local run` boot path (P1 D13 — the mnemond name now belongs to the local -// trust domain; the remote hub binary builds as mnemon-hub). It is the LOCAL trust domain -// main: it imports internal/app and shares the boot face in app/localboot.go with `local run`, -// so flags, banner, T1 loopback floor, and serve behavior stay alias-identical. One daemon per -// project store (the store's single-writer flock enforces it). +// mnemond is the Local Mnemon event node: the standalone packaging of the exact +// `mnemon-harness local run` boot path. The remote exchange binary builds as +// mnemon-hub. This main imports internal/app and shares the boot face in +// app/localboot.go with `local run`, so flags, banner, T1 loopback floor, and +// serve behavior stay alias-identical. One daemon per project store (the store's +// single-writer flock enforces it). // // mnemond is a real daemon (P2 / PD8): `up` starts the serve loop as a detached background // process (pidfile + log under .mnemon/harness/local/), `down` stops it, `status` reports it, @@ -59,6 +59,11 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { return err } return nil + case "help": + if _, err := parseServe([]string{"--help"}, errw); err != nil && !errors.Is(err, flag.ErrHelp) { + return err + } + return nil case "serve": args = args[1:] } @@ -97,10 +102,7 @@ func parseServe(args []string, errw io.Writer) (serveConfig, error) { ignoreExternal := fs.Bool("ignore-external", false, "boot the embedded-only capability catalog, ignoring external packages under .mnemon/loops (each ignored package is named on stderr)") allowInsecureRemote := fs.Bool("allow-insecure-remote", false, "let the background sync worker use a plaintext http:// Remote Workspace endpoint with a non-loopback host (T2: fail-closed by default)") fs.Usage = func() { - fmt.Fprintln(errw, "mnemond is the Local Mnemon event node: it serves local event API, admission, state, presentation, and drive candidates.") - fmt.Fprintln(errw) - fmt.Fprintln(errw, "Usage of mnemond:") - fs.PrintDefaults() + writeMnemondHelp(errw, fs) } if err := fs.Parse(args); err != nil { return serveConfig{}, err @@ -136,6 +138,30 @@ func parseServe(args []string, errw io.Writer) (serveConfig, error) { }, nil } +func writeMnemondHelp(errw io.Writer, fs *flag.FlagSet) { + fmt.Fprintln(errw, "mnemond is the Local Mnemon event node: it serves local event API, admission, state, presentation, and drive candidates.") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Usage:") + fmt.Fprintln(errw, " mnemond serve [flags]") + fmt.Fprintln(errw, " mnemond [flags]") + fmt.Fprintln(errw, " mnemond up|down|reload|status|logs [flags]") + fmt.Fprintln(errw, " mnemond agent run [flags]") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Commands:") + fmt.Fprintln(errw, " serve Run the local event node in the foreground") + fmt.Fprintln(errw, " up Start the local event node in the background") + fmt.Fprintln(errw, " down Stop the background local event node") + fmt.Fprintln(errw, " reload Restart the background local event node") + fmt.Fprintln(errw, " status Show background local event node status") + fmt.Fprintln(errw, " logs Show background local event node logs") + fmt.Fprintln(errw, " agent run Local managed-agent drive source using [mnemon:wake]") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Flags:") + if fs != nil { + fs.PrintDefaults() + } +} + // serveForeground runs the governed HTTP server in the foreground until ctx cancels — the body of // `mnemond serve` and the process the daemon child runs. func serveForeground(ctx context.Context, cfg serveConfig, out io.Writer) error { diff --git a/harness/cmd/mnemond/main_test.go b/harness/cmd/mnemond/main_test.go index 3f836cc2..986431af 100644 --- a/harness/cmd/mnemond/main_test.go +++ b/harness/cmd/mnemond/main_test.go @@ -44,15 +44,31 @@ func TestRunRefusesNonLoopbackAddr(t *testing.T) { } func TestHelpDescribesLocalEventNode(t *testing.T) { + for _, args := range [][]string{{"--help"}, {"help"}} { + var errw bytes.Buffer + err := run(context.Background(), args, io.Discard, &errw) + if err != nil { + t.Fatalf("%v help should exit successfully, got %v", args, err) + } + got := errw.String() + for _, want := range []string{"Local Mnemon event node", "local event API", "admission", "state", "presentation", "drive candidates", "Commands:", "serve", "status", "agent run"} { + if !strings.Contains(got, want) { + t.Fatalf("%v mnemond help missing %q:\n%s", args, want, got) + } + } + } +} + +func TestAgentHelpListsLocalDriveSourceCommand(t *testing.T) { var errw bytes.Buffer - err := run(context.Background(), []string{"--help"}, io.Discard, &errw) + err := run(context.Background(), []string{"agent", "--help"}, io.Discard, &errw) if err != nil { - t.Fatalf("help should exit successfully, got %v", err) + t.Fatalf("agent help should exit successfully, got %v", err) } got := errw.String() - for _, want := range []string{"Local Mnemon event node", "local event API", "admission", "state", "presentation", "drive candidates"} { + for _, want := range []string{"local managed-agent drive sources", "mnemond agent run", "[mnemon:wake]", "managed runtime"} { if !strings.Contains(got, want) { - t.Fatalf("mnemond help missing %q:\n%s", want, got) + t.Fatalf("agent help missing %q:\n%s", want, got) } } } From f8beb863d9c9c798aac73e6bd78f8b1c3da836e8 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:53:26 +0800 Subject: [PATCH 109/117] fix: show mnemon-hub serve help Add serve/help command-shape handling around the remote event exchange backend while preserving the existing flag parser and sync wire. The serve alias now parses the same store, replicas, and TLS flags as the previous direct invocation. Validation: go test -count=1 ./harness/cmd/mnemon-hub; go run ./harness/cmd/mnemon-hub help; go run ./harness/cmd/mnemon-hub serve --store x.db --replicas r.json --tls-cert c.pem; go test -count=1 ./harness/cmd/mnemon-hub ./harness/cmd/mnemond ./harness/internal/coreguard ./harness/internal/mnemonhub ./harness/internal/mnemonhub/exchange; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemon-hub/main.go | 37 +++++++++++++++++++++++++---- harness/cmd/mnemon-hub/main_test.go | 31 ++++++++++++++---------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/harness/cmd/mnemon-hub/main.go b/harness/cmd/mnemon-hub/main.go index 6f939059..032971e8 100644 --- a/harness/cmd/mnemon-hub/main.go +++ b/harness/cmd/mnemon-hub/main.go @@ -35,6 +35,7 @@ func main() { // generator exit, load replicas.json (fail-closed), take the hub store's single-writer lock, and // serve the three sync verbs (TLS when both cert+key are set) until ctx cancels. func run(ctx context.Context, args []string, out, errw io.Writer) error { + args = normalizeHubArgs(args) fs := flag.NewFlagSet("mnemon-hub", flag.ContinueOnError) fs.SetOutput(errw) addr := fs.String("addr", "127.0.0.1:9787", "listen address") @@ -44,10 +45,7 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { tlsKey := fs.String("tls-key", "", "TLS private key file") devSelfsigned := fs.String("dev-selfsigned", "", "generate a self-signed dev/e2e cert+key pair into this directory, print their paths, and exit") fs.Usage = func() { - fmt.Fprintln(errw, "mnemon-hub is the remote event exchange backend: it handles authenticated replica push, pull, status, cursors, and tenant boundaries.") - fmt.Fprintln(errw) - fmt.Fprintln(errw, "Usage of mnemon-hub:") - fs.PrintDefaults() + writeHubHelp(errw, fs) } if err := fs.Parse(args); err != nil { if err == flag.ErrHelp { @@ -91,6 +89,37 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { return serveHub(ctx, *addr, handler, *tlsCert, *tlsKey, *storePath, out) } +func normalizeHubArgs(args []string) []string { + if len(args) == 0 { + return args + } + switch args[0] { + case "serve": + return args[1:] + case "help": + return []string{"--help"} + default: + return args + } +} + +func writeHubHelp(errw io.Writer, fs *flag.FlagSet) { + fmt.Fprintln(errw, "mnemon-hub is the remote event exchange backend: it handles authenticated replica push, pull, status, cursors, and tenant boundaries.") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Usage:") + fmt.Fprintln(errw, " mnemon-hub serve --store PATH --replicas PATH [flags]") + fmt.Fprintln(errw, " mnemon-hub --dev-selfsigned DIR") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Commands:") + fmt.Fprintln(errw, " serve Run the authenticated remote event exchange backend") + fmt.Fprintln(errw, " help Show this help") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Flags:") + if fs != nil { + fs.PrintDefaults() + } +} + // serveHub listens (so the bound address is printable before any request) and serves until ctx // cancels, then shuts down cleanly. With cert+key it serves TLS natively. func serveHub(ctx context.Context, addr string, handler http.Handler, certFile, keyFile, storePath string, out io.Writer) error { diff --git a/harness/cmd/mnemon-hub/main_test.go b/harness/cmd/mnemon-hub/main_test.go index 3ab64468..92e13d6e 100644 --- a/harness/cmd/mnemon-hub/main_test.go +++ b/harness/cmd/mnemon-hub/main_test.go @@ -41,20 +41,22 @@ func writeToken(t *testing.T, dir, name, token string) { } func TestHelpDescribesRemoteExchangeBackend(t *testing.T) { - var errw bytes.Buffer - err := run(context.Background(), []string{"--help"}, io.Discard, &errw) - if err != nil { - t.Fatalf("help should exit successfully, got %v", err) - } - got := errw.String() - for _, want := range []string{"remote event exchange backend", "replica push", "pull", "status", "cursors", "tenant boundaries"} { - if !strings.Contains(got, want) { - t.Fatalf("mnemon-hub help missing %q:\n%s", want, got) + for _, args := range [][]string{{"--help"}, {"help"}, {"serve", "--help"}} { + var errw bytes.Buffer + err := run(context.Background(), args, io.Discard, &errw) + if err != nil { + t.Fatalf("%v help should exit successfully, got %v", args, err) } - } - for _, blocked := range []string{"managed runtime", "Multica projection", "local drive source"} { - if strings.Contains(got, blocked) { - t.Fatalf("mnemon-hub help leaked non-exchange wording %q:\n%s", blocked, got) + got := errw.String() + for _, want := range []string{"remote event exchange backend", "replica push", "pull", "status", "cursors", "tenant boundaries", "Commands:", "serve"} { + if !strings.Contains(got, want) { + t.Fatalf("%v mnemon-hub help missing %q:\n%s", args, want, got) + } + } + for _, blocked := range []string{"managed runtime", "Multica projection", "local drive source"} { + if strings.Contains(got, blocked) { + t.Fatalf("%v mnemon-hub help leaked non-exchange wording %q:\n%s", args, blocked, got) + } } } } @@ -160,6 +162,9 @@ func TestRunFlagValidation(t *testing.T) { if err := run(context.Background(), []string{"--store", "x.db", "--replicas", "r.json", "--tls-cert", "c.pem"}, &out, &out); err == nil || !strings.Contains(err.Error(), "set together") { t.Fatalf("lone --tls-cert must fail: %v", err) } + if err := run(context.Background(), []string{"serve", "--store", "x.db", "--replicas", "r.json", "--tls-cert", "c.pem"}, &out, &out); err == nil || !strings.Contains(err.Error(), "set together") { + t.Fatalf("serve alias must parse service flags: %v", err) + } } // Full hub integration over native TLS: mnemon-hub serves push/pull/status with the dev self-signed From f0224cc33f12bac10c9fa30ae8ec961c2a70039d Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 05:58:19 +0800 Subject: [PATCH 110/117] feat: add mnemond readiness doctor Add a read-only mnemond doctor command for the local event node. It reports local setup config, boot-chain readiness, background daemon pid state, and Remote Workspace status without crossing into product config or hub/projection ownership. Validation: go test -count=1 ./harness/cmd/mnemond; go run ./harness/cmd/mnemond doctor --root ; go run ./harness/cmd/mnemond help; go test -count=1 ./harness/cmd/mnemond ./harness/cmd/mnemon-hub ./harness/internal/coreguard ./harness/internal/app; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemond/doctor.go | 82 ++++++++++++++++++++++++++++ harness/cmd/mnemond/doctor_test.go | 85 ++++++++++++++++++++++++++++++ harness/cmd/mnemond/main.go | 11 +++- 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 harness/cmd/mnemond/doctor.go create mode 100644 harness/cmd/mnemond/doctor_test.go diff --git a/harness/cmd/mnemond/doctor.go b/harness/cmd/mnemond/doctor.go new file mode 100644 index 00000000..da8acd7b --- /dev/null +++ b/harness/cmd/mnemond/doctor.go @@ -0,0 +1,82 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/mnemon-dev/mnemon/harness/internal/app" +) + +func daemonDoctor(args []string, out, errw io.Writer) error { + fs := flag.NewFlagSet("mnemond doctor", flag.ContinueOnError) + fs.SetOutput(errw) + rootFlag := fs.String("root", ".", "project root") + fs.Usage = func() { + fmt.Fprintln(errw, "mnemond doctor checks the local event node setup, boot chain, and background process state.") + fmt.Fprintln(errw) + fmt.Fprintln(errw, "Usage of mnemond doctor:") + fs.PrintDefaults() + } + if err := fs.Parse(args); err != nil { + return err + } + root := strings.TrimSpace(*rootFlag) + if root == "" { + root = "." + } + root = filepath.Clean(root) + fmt.Fprintln(out, "mnemond doctor") + writeLocalConfigDoctor(out, root) + writeLocalBootDoctor(out, root) + writeBackgroundDoctor(out, root) + fmt.Fprintf(out, "- Remote Workspace: %s\n", app.RemoteWorkspaceStatus(root)) + return nil +} + +func writeLocalConfigDoctor(out io.Writer, root string) { + cfg, err := app.ReadLocalConfig(root) + if err != nil { + if os.IsNotExist(err) { + fmt.Fprintln(out, "- Local event node config: missing") + fmt.Fprintln(out, "- Setup remediation: mnemon-harness setup --host codex") + return + } + fmt.Fprintf(out, "- Local event node config: invalid (%v)\n", err) + return + } + fmt.Fprintln(out, "- Local event node config: configured") + fmt.Fprintf(out, "- Endpoint: %s\n", cfg.Endpoint) + fmt.Fprintf(out, "- Principal: %s\n", cfg.Principal) +} + +func writeLocalBootDoctor(out io.Writer, root string) { + boot, err := app.ResolveLocalBoot(root, "", "") + if err != nil { + if errors.Is(err, app.ErrLocalNotSetup) { + fmt.Fprintln(out, "- Boot chain: not ready (setup required)") + return + } + fmt.Fprintf(out, "- Boot chain: invalid (%v)\n", err) + return + } + fmt.Fprintf(out, "- Boot chain: ready (bindings=%d)\n", len(boot.Loaded.Bindings)) + fmt.Fprintf(out, "- Store: %s\n", boot.StorePath) +} + +func writeBackgroundDoctor(out io.Writer, root string) { + _, pidPath, _ := daemonPaths(root) + pid, alive := readLivePid(pidPath) + switch { + case alive: + fmt.Fprintf(out, "- Background daemon: running (pid %d)\n", pid) + case pid > 0: + fmt.Fprintf(out, "- Background daemon: stopped (stale pidfile pid %d)\n", pid) + default: + fmt.Fprintln(out, "- Background daemon: stopped") + } +} diff --git a/harness/cmd/mnemond/doctor_test.go b/harness/cmd/mnemond/doctor_test.go new file mode 100644 index 00000000..dad6ab82 --- /dev/null +++ b/harness/cmd/mnemond/doctor_test.go @@ -0,0 +1,85 @@ +package main + +import ( + "bytes" + "context" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/mnemon-dev/mnemon/harness/internal/app" +) + +func TestDoctorReportsMissingLocalConfigWithoutMutating(t *testing.T) { + root := t.TempDir() + var out bytes.Buffer + if err := daemonDoctor([]string{"--root", root}, &out, &out); err != nil { + t.Fatalf("doctor: %v", err) + } + got := out.String() + for _, want := range []string{ + "mnemond doctor", + "- Local event node config: missing", + "- Setup remediation: mnemon-harness setup --host codex", + "- Boot chain: not ready (setup required)", + "- Background daemon: stopped", + "- Remote Workspace: not connected", + } { + if !strings.Contains(got, want) { + t.Fatalf("doctor output missing %q:\n%s", want, got) + } + } + if _, err := os.Stat(filepath.Join(root, ".mnemon")); !os.IsNotExist(err) { + t.Fatalf("doctor must not create local state, stat err=%v", err) + } +} + +func TestDoctorReportsConfiguredLocalEventNode(t *testing.T) { + root := t.TempDir() + if _, err := app.New(root).Setup(context.Background(), io.Discard, io.Discard, app.SetupOptions{ + Host: "codex", + Principal: "codex@project", + ControlURL: "http://127.0.0.1:9007", + UseToken: true, + }); err != nil { + t.Fatalf("setup: %v", err) + } + var out bytes.Buffer + if err := daemonDoctor([]string{"--root", root}, &out, &out); err != nil { + t.Fatalf("doctor: %v", err) + } + got := out.String() + for _, want := range []string{ + "- Local event node config: configured", + "- Endpoint: http://127.0.0.1:9007", + "- Principal: codex@project", + "- Boot chain: ready (bindings=1)", + "- Store: " + filepath.Join(root, ".mnemon", "harness", "local", "governed.db"), + "- Background daemon: stopped", + } { + if !strings.Contains(got, want) { + t.Fatalf("doctor output missing %q:\n%s", want, got) + } + } +} + +func TestDoctorReportsBackgroundDaemonState(t *testing.T) { + root := t.TempDir() + dir, pidPath, _ := daemonPaths(root) + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(pidPath, []byte(strconv.Itoa(os.Getpid())+"\n"), 0o600); err != nil { + t.Fatal(err) + } + var out bytes.Buffer + if err := daemonDoctor([]string{"--root", root}, &out, &out); err != nil { + t.Fatalf("doctor: %v", err) + } + if got := out.String(); !strings.Contains(got, "- Background daemon: running (pid "+strconv.Itoa(os.Getpid())+")") { + t.Fatalf("doctor did not report live daemon pid:\n%s", got) + } +} diff --git a/harness/cmd/mnemond/main.go b/harness/cmd/mnemond/main.go index c3f52e05..3160417e 100644 --- a/harness/cmd/mnemond/main.go +++ b/harness/cmd/mnemond/main.go @@ -49,6 +49,14 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { return daemonReload(args[1:], out, errw) case "status": return daemonStatus(args[1:], out, errw) + case "doctor": + if err := daemonDoctor(args[1:], out, errw); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + return nil case "logs": return daemonLogs(args[1:], out, errw) case "agent": @@ -144,7 +152,7 @@ func writeMnemondHelp(errw io.Writer, fs *flag.FlagSet) { fmt.Fprintln(errw, "Usage:") fmt.Fprintln(errw, " mnemond serve [flags]") fmt.Fprintln(errw, " mnemond [flags]") - fmt.Fprintln(errw, " mnemond up|down|reload|status|logs [flags]") + fmt.Fprintln(errw, " mnemond up|down|reload|status|doctor|logs [flags]") fmt.Fprintln(errw, " mnemond agent run [flags]") fmt.Fprintln(errw) fmt.Fprintln(errw, "Commands:") @@ -153,6 +161,7 @@ func writeMnemondHelp(errw io.Writer, fs *flag.FlagSet) { fmt.Fprintln(errw, " down Stop the background local event node") fmt.Fprintln(errw, " reload Restart the background local event node") fmt.Fprintln(errw, " status Show background local event node status") + fmt.Fprintln(errw, " doctor Check local event node readiness") fmt.Fprintln(errw, " logs Show background local event node logs") fmt.Fprintln(errw, " agent run Local managed-agent drive source using [mnemon:wake]") fmt.Fprintln(errw) From 8e34b488204316d9a972caa728f52f10c6b4a6d0 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 06:02:02 +0800 Subject: [PATCH 111/117] fix: normalize mnemond lifecycle help Treat lifecycle subcommand help as a successful service path for mnemond and give root-only lifecycle verbs command-specific help names. This keeps serve/up/reload/down/status/logs/doctor consistent with the local event-node service surface without changing normal execution behavior. Validation: go test -count=1 ./harness/cmd/mnemond; go run ./harness/cmd/mnemond --help; go test -count=1 ./harness/cmd/mnemond ./harness/cmd/mnemon-hub ./harness/internal/coreguard ./harness/internal/app; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemond/daemon.go | 18 ++++++++++---- harness/cmd/mnemond/main.go | 40 ++++++++++++++++++++++++++++---- harness/cmd/mnemond/main_test.go | 24 +++++++++++++++++++ 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/harness/cmd/mnemond/daemon.go b/harness/cmd/mnemond/daemon.go index 97f0d4c6..59661bbb 100644 --- a/harness/cmd/mnemond/daemon.go +++ b/harness/cmd/mnemond/daemon.go @@ -24,10 +24,18 @@ func daemonPaths(root string) (dir, pidPath, logPath string) { } // rootFlag parses --root for the lifecycle verbs that take no serve flags (down/status/logs). -func rootFlag(args []string, errw io.Writer) (string, error) { - fs := flag.NewFlagSet("mnemond", flag.ContinueOnError) +func rootFlag(args []string, errw io.Writer, verb ...string) (string, error) { + name := "mnemond" + if len(verb) > 0 && strings.TrimSpace(verb[0]) != "" { + name = "mnemond " + strings.TrimSpace(verb[0]) + } + fs := flag.NewFlagSet(name, flag.ContinueOnError) fs.SetOutput(errw) root := fs.String("root", ".", "project root") + fs.Usage = func() { + fmt.Fprintf(errw, "Usage of %s:\n", name) + fs.PrintDefaults() + } if err := fs.Parse(args); err != nil { return "", err } @@ -129,7 +137,7 @@ func waitListening(pid int, addr string) error { // traps for graceful shutdown), waits for it to exit, and removes the pidfile. A stale or absent // pidfile is reported, not an error — `down` is idempotent. func daemonDown(args []string, out, errw io.Writer) error { - root, err := rootFlag(args, errw) + root, err := rootFlag(args, errw, "down") if err != nil { return err } @@ -193,7 +201,7 @@ func daemonReload(args []string, out, errw io.Writer) error { // daemonStatus reports whether the recorded daemon is alive. func daemonStatus(args []string, out, errw io.Writer) error { - root, err := rootFlag(args, errw) + root, err := rootFlag(args, errw, "status") if err != nil { return err } @@ -208,7 +216,7 @@ func daemonStatus(args []string, out, errw io.Writer) error { // daemonLogs prints the daemon's captured stdout/stderr. func daemonLogs(args []string, out, errw io.Writer) error { - root, err := rootFlag(args, errw) + root, err := rootFlag(args, errw, "logs") if err != nil { return err } diff --git a/harness/cmd/mnemond/main.go b/harness/cmd/mnemond/main.go index 3160417e..7286be89 100644 --- a/harness/cmd/mnemond/main.go +++ b/harness/cmd/mnemond/main.go @@ -42,13 +42,37 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { if len(args) > 0 { switch args[0] { case "up": - return daemonUp(args[1:], out, errw) + if err := daemonUp(args[1:], out, errw); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + return nil case "down": - return daemonDown(args[1:], out, errw) + if err := daemonDown(args[1:], out, errw); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + return nil case "reload": - return daemonReload(args[1:], out, errw) + if err := daemonReload(args[1:], out, errw); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + return nil case "status": - return daemonStatus(args[1:], out, errw) + if err := daemonStatus(args[1:], out, errw); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + return nil case "doctor": if err := daemonDoctor(args[1:], out, errw); err != nil { if errors.Is(err, flag.ErrHelp) { @@ -58,7 +82,13 @@ func run(ctx context.Context, args []string, out, errw io.Writer) error { } return nil case "logs": - return daemonLogs(args[1:], out, errw) + if err := daemonLogs(args[1:], out, errw); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + return nil case "agent": if err := runAgent(ctx, args[1:], out, errw); err != nil { if errors.Is(err, flag.ErrHelp) { diff --git a/harness/cmd/mnemond/main_test.go b/harness/cmd/mnemond/main_test.go index 986431af..702e813e 100644 --- a/harness/cmd/mnemond/main_test.go +++ b/harness/cmd/mnemond/main_test.go @@ -73,6 +73,30 @@ func TestAgentHelpListsLocalDriveSourceCommand(t *testing.T) { } } +func TestLifecycleHelpExitsSuccessfully(t *testing.T) { + for _, tc := range []struct { + args []string + want string + }{ + {[]string{"serve", "--help"}, "Local Mnemon event node"}, + {[]string{"up", "--help"}, "Local Mnemon event node"}, + {[]string{"reload", "--help"}, "Local Mnemon event node"}, + {[]string{"down", "--help"}, "Usage of mnemond down"}, + {[]string{"status", "--help"}, "Usage of mnemond status"}, + {[]string{"logs", "--help"}, "Usage of mnemond logs"}, + {[]string{"doctor", "--help"}, "Usage of mnemond doctor"}, + } { + var errw bytes.Buffer + err := run(context.Background(), tc.args, io.Discard, &errw) + if err != nil { + t.Fatalf("%v help should exit successfully, got %v", tc.args, err) + } + if got := errw.String(); !strings.Contains(got, tc.want) { + t.Fatalf("%v help missing %q:\n%s", tc.args, tc.want, got) + } + } +} + func TestAgentRunHelpFramesLocalDriveSource(t *testing.T) { var errw bytes.Buffer err := run(context.Background(), []string{"agent", "run", "--help"}, io.Discard, &errw) From e9a4f02f23e0e06a6bad451c4bb3e70015b25baf Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 06:09:00 +0800 Subject: [PATCH 112/117] refactor: structure multica runtime activation context Add a Multica runtime context model that normalizes daemon-provided environment fields, structured issue mentions/tags, workspace/server metadata, and Mnemon control routing before the adapter imports an issue. The runtime import path now consumes that context for issue lookup, principal resolution, rule refs, CLI workspace/server routing, and assignment-mailbox correlation. Validation: go test -count=1 ./harness/internal/surface/multica; go test -count=1 ./harness/cmd/mnemon-multica-runtime; go test -count=1 ./harness/internal/coreguard ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-harness ./harness/cmd/mnemond ./harness/cmd/mnemon-hub ./harness/internal/daemon ./harness/internal/productconfig; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemon-multica-runtime/main.go | 41 +++++++------ .../surface/multica/runtime_config_test.go | 50 ++++++++++++++++ .../surface/multica/runtime_context.go | 60 +++++++++++++++++++ .../internal/surface/multica/runtime_items.go | 26 ++++++-- .../surface/multica/runtime_items_test.go | 6 ++ 5 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 harness/internal/surface/multica/runtime_context.go diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 87354d2f..956d1da5 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -312,11 +312,12 @@ func (s *runtimeRPCState) runTurn(input multicasurface.RuntimeInput, progress ru } func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progress runtimeProgressSink) runtimeImportResult { - taskID := multicasurface.RuntimeEnvValue(s.Env, "MULTICA_TASK_ID") - issueID := firstNonEmpty(multicasurface.RuntimeEnvValue(s.Env, "MULTICA_ISSUE_ID"), input.IssueIdentity, multicasurface.ExtractIssueIdentity(input.Text)) + activation := multicasurface.RuntimeContextFromActivation(s.Env, s.CWD, input) + taskID := activation.TaskID + issueID := activation.IssueIdentity result := runtimeImportResult{ IssueID: issueID, - Principal: resolveRuntimePrincipal(s.Env, s.CWD), + Principal: resolveRuntimePrincipalFromContext(s.Env, s.CWD, activation), TaskID: taskID, } emitRuntimeProgress(progress, "Mnemon runtime accepted the Multica task for "+multicasurface.RuntimePrincipalLabel(result.Principal)+".") @@ -326,7 +327,7 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres emitRuntimeProgress(progress, "No assigned Multica issue id was available; Mnemon skipped this turn.") return result } - cli := runtimeMulticaCLI(s.Env) + cli := runtimeMulticaCLI(s.Env, activation) multicaCtx := context.Background() emitRuntimeProgress(progress, "Loading Multica issue "+issueID+".") issue, err := cli.GetIssue(multicaCtx, issueID) @@ -345,11 +346,11 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres result.Statement = issue.Description result.HubMetadata = driver.MulticaIssueHubMetadata(issue) applyMulticaHubMetadata(&result, result.HubMetadata) - result.HubBackend = firstNonEmpty(result.HubBackend, multicasurface.RuntimeEnvValue(s.Env, "MNEMON_HUB_BACKEND")) + result.HubBackend = firstNonEmpty(result.HubBackend, activation.HubBackend) emitRuntimeProgress(progress, "Loaded "+runtimeIssueLabel(issue)+"; classifying Mnemon hub metadata.") markIssueInProgress(multicaCtx, cli, issue.ID) if result.HubMetadata.IsAssignmentMailbox() { - return s.correlateAssignmentMailbox(multicaCtx, cli, issue, &result, progress) + return s.correlateAssignmentMailbox(multicaCtx, cli, issue, &result, activation, progress) } externalID := "" if taskID != "" { @@ -364,14 +365,14 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres Scope: multicasurface.RuntimeEnvDefault(s.Env, "MNEMON_MULTICA_SCOPE", "multica/teamwork"), TTL: multicasurface.RuntimeEnvDefault(s.Env, "MNEMON_MULTICA_TTL", "30m"), WhyTeamwork: "Multica assigned this issue to a Mnemon participant, so Mnemon should admit it through the teamwork protocol.", - WorkspaceID: firstNonEmpty(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_MULTICA_WORKSPACE_ID"), multicasurface.RuntimeEnvValue(s.Env, "MULTICA_WORKSPACE_ID")), + WorkspaceID: activation.WorkspaceID, TaskID: taskID, - AgentID: multicasurface.RuntimeEnvValue(s.Env, "MULTICA_AGENT_ID"), + AgentID: activation.AgentID, Principal: result.Principal, ContextRefs: []string{ multicasurface.RuntimeRef("issue", issue.ID), multicasurface.RuntimeRef("task", taskID), - multicasurface.RuntimeRef("agent", multicasurface.RuntimeEnvValue(s.Env, "MULTICA_AGENT_ID")), + multicasurface.RuntimeRef("agent", activation.AgentID), }, EvidenceRefs: []string{multicasurface.RuntimeRef("issue", issue.ID)}, ExternalID: externalID, @@ -407,7 +408,7 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres emitRuntimeProgress(progress, "Root session metadata written for "+runtimeIssueLabel(issue)+".") } } - addr := strings.TrimSpace(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_CONTROL_ADDR")) + addr := strings.TrimSpace(activation.ControlAddr) if addr == "" { result.Status = "skipped" emitRuntimeProgress(progress, "Local Mnemon control address is not configured; skipping protocol ingest.") @@ -468,7 +469,7 @@ func loadRuntimeIssueMetadata(ctx context.Context, cli driver.MulticaCLI, issue return loaded } -func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, result *runtimeImportResult, progress runtimeProgressSink) runtimeImportResult { +func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, result *runtimeImportResult, activation multicasurface.RuntimeContext, progress runtimeProgressSink) runtimeImportResult { result.HubMetadata = multicasurface.AssignmentMailboxHubMetadata(result.HubMetadata, issue.ID) applyMulticaHubMetadata(result, result.HubMetadata) result.Status = "correlated" @@ -481,7 +482,7 @@ func (s *runtimeRPCState) correlateAssignmentMailbox(ctx context.Context, cli dr correlationProgress := multicasurface.RuntimeAssignmentCorrelationProgress() emitRuntimeCommand(progress, "mnemon-multica-runtime assignment-correlate --issue "+issue.ID, correlationProgress, 0) emitRuntimeProgress(progress, correlationProgress) - addr := strings.TrimSpace(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_CONTROL_ADDR")) + addr := strings.TrimSpace(activation.ControlAddr) var client *access.Client if addr == "" { result.WakeStatus = "skipped" @@ -782,12 +783,12 @@ func runtimeProjectionMaterial(issue driver.MulticaIssue, result runtimeImportRe return material } -func runtimeMulticaCLI(env []string) driver.MulticaCLI { +func runtimeMulticaCLI(env []string, activation multicasurface.RuntimeContext) driver.MulticaCLI { return driver.MulticaCLI{ Command: multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_BIN"), Profile: multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_PROFILE"), - ServerURL: firstNonEmpty(multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_SERVER_URL"), multicasurface.RuntimeEnvValue(env, "MULTICA_SERVER_URL")), - WorkspaceID: firstNonEmpty(multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_WORKSPACE_ID"), multicasurface.RuntimeEnvValue(env, "MULTICA_WORKSPACE_ID")), + ServerURL: activation.ServerURL, + WorkspaceID: activation.WorkspaceID, Env: append([]string(nil), env...), Timeout: multicasurface.RuntimeTimeout(env), } @@ -851,12 +852,16 @@ func (runtimeNoopTurnClient) StartTurn(_ context.Context, query string) (driver. } func resolveRuntimePrincipal(env []string, cwd string) string { - agentID := multicasurface.RuntimeEnvValue(env, "MULTICA_AGENT_ID") - agentName := multicasurface.RuntimeEnvValue(env, "MULTICA_AGENT_NAME") + return resolveRuntimePrincipalFromContext(env, cwd, multicasurface.RuntimeContextFromActivation(env, cwd, multicasurface.RuntimeInput{})) +} + +func resolveRuntimePrincipalFromContext(env []string, cwd string, activation multicasurface.RuntimeContext) string { + agentID := activation.AgentID + agentName := activation.AgentName if principal := multicasurface.RuntimeMulticaRegistryPrincipal(env, cwd, agentID, agentName); principal != "" { return principal } - if principal := multicasurface.RuntimeEnvValue(env, "MNEMON_CONTROL_PRINCIPAL"); principal != "" { + if principal := activation.ControlPrincipal; principal != "" { return principal } if agentName != "" { diff --git a/harness/internal/surface/multica/runtime_config_test.go b/harness/internal/surface/multica/runtime_config_test.go index 92eaced0..c2f27724 100644 --- a/harness/internal/surface/multica/runtime_config_test.go +++ b/harness/internal/surface/multica/runtime_config_test.go @@ -19,6 +19,56 @@ func TestRuntimeEnvValueUsesLastValue(t *testing.T) { } } +func TestRuntimeContextFromActivationNormalizesDaemonAndRuntimeMetadata(t *testing.T) { + ctx := RuntimeContextFromActivation([]string{ + "MULTICA_TASK_ID=task-1", + "MULTICA_ISSUE_ID=iss-env", + "MULTICA_AGENT_ID=agent-1", + "MULTICA_AGENT_NAME=mnemon-planner", + "MULTICA_WORKSPACE_ID=ws-daemon", + "MNEMON_MULTICA_WORKSPACE_ID=ws-mnemon", + "MULTICA_SERVER_URL=https://api.multica.ai", + "MNEMON_MULTICA_SERVER_URL=https://desktop-api.multica.ai", + "MNEMON_HUB_BACKEND=multica", + "MNEMON_CONTROL_ADDR=http://127.0.0.1:8787", + "MNEMON_CONTROL_PRINCIPAL=planner@team", + }, "/repo", RuntimeInput{ + Text: "Ignore copied @TEA-1.", + IssueIdentity: "iss-input", + IssueIdentitySource: RuntimeIssueSourceInput, + }) + if ctx.IssueIdentity != "iss-env" || ctx.IssueIdentitySource != RuntimeIssueSourceEnv { + t.Fatalf("issue identity/source = %q/%q", ctx.IssueIdentity, ctx.IssueIdentitySource) + } + if ctx.TaskID != "task-1" || ctx.AgentID != "agent-1" || ctx.AgentName != "mnemon-planner" { + t.Fatalf("task/agent metadata mismatch: %+v", ctx) + } + if ctx.WorkspaceID != "ws-mnemon" || ctx.ServerURL != "https://desktop-api.multica.ai" { + t.Fatalf("workspace/server metadata mismatch: %+v", ctx) + } + if ctx.HubBackend != "multica" || ctx.ControlAddr != "http://127.0.0.1:8787" || ctx.ControlPrincipal != "planner@team" { + t.Fatalf("Mnemon control metadata mismatch: %+v", ctx) + } +} + +func TestRuntimeContextFromActivationFallsBackToStructuredInput(t *testing.T) { + ctx := RuntimeContextFromActivation(nil, "", RuntimeInput{ + Text: "Please review the linked issue.", + IssueIdentity: "iss-selected", + IssueIdentitySource: RuntimeIssueSourceInput, + }) + if ctx.IssueIdentity != "iss-selected" || ctx.IssueIdentitySource != RuntimeIssueSourceInput { + t.Fatalf("structured input issue mismatch: %+v", ctx) + } +} + +func TestRuntimeContextFromActivationFallsBackToVisibleIssueTag(t *testing.T) { + ctx := RuntimeContextFromActivation(nil, "", RuntimeInput{Text: "Please handle @TEA-50 next."}) + if ctx.IssueIdentity != "TEA-50" || ctx.IssueIdentitySource != RuntimeIssueSourceInputText { + t.Fatalf("visible tag issue mismatch: %+v", ctx) + } +} + func TestMulticaRuntimeCommandNamePinned(t *testing.T) { if MulticaRuntimeCommandName != "mnemon-multica-runtime" { t.Fatalf("MulticaRuntimeCommandName = %q", MulticaRuntimeCommandName) diff --git a/harness/internal/surface/multica/runtime_context.go b/harness/internal/surface/multica/runtime_context.go new file mode 100644 index 00000000..94047a37 --- /dev/null +++ b/harness/internal/surface/multica/runtime_context.go @@ -0,0 +1,60 @@ +package multica + +import "strings" + +const ( + RuntimeIssueSourceEnv = "env:MULTICA_ISSUE_ID" + RuntimeIssueSourceInput = "input:issue" + RuntimeIssueSourceInputText = "input:text" + RuntimeIssueSourceUnresolved = "" +) + +type RuntimeContext struct { + CWD string + Text string + IssueIdentity string + IssueIdentitySource string + TaskID string + AgentID string + AgentName string + WorkspaceID string + ServerURL string + HubBackend string + ControlAddr string + ControlPrincipal string +} + +func RuntimeContextFromActivation(env []string, cwd string, input RuntimeInput) RuntimeContext { + issueIdentity, issueSource := runtimeIssueIdentityFromActivation(env, input) + return RuntimeContext{ + CWD: strings.TrimSpace(cwd), + Text: strings.TrimSpace(input.Text), + IssueIdentity: issueIdentity, + IssueIdentitySource: issueSource, + TaskID: RuntimeEnvValue(env, "MULTICA_TASK_ID"), + AgentID: RuntimeEnvValue(env, "MULTICA_AGENT_ID"), + AgentName: RuntimeEnvValue(env, "MULTICA_AGENT_NAME"), + WorkspaceID: firstNonEmptyRuntimeString(RuntimeEnvValue(env, "MNEMON_MULTICA_WORKSPACE_ID"), RuntimeEnvValue(env, "MULTICA_WORKSPACE_ID")), + ServerURL: firstNonEmptyRuntimeString(RuntimeEnvValue(env, "MNEMON_MULTICA_SERVER_URL"), RuntimeEnvValue(env, "MULTICA_SERVER_URL")), + HubBackend: RuntimeEnvValue(env, "MNEMON_HUB_BACKEND"), + ControlAddr: RuntimeEnvValue(env, "MNEMON_CONTROL_ADDR"), + ControlPrincipal: RuntimeEnvValue(env, "MNEMON_CONTROL_PRINCIPAL"), + } +} + +func runtimeIssueIdentityFromActivation(env []string, input RuntimeInput) (string, string) { + if issue := cleanIssueIdentity(RuntimeEnvValue(env, "MULTICA_ISSUE_ID")); issue != "" { + return issue, RuntimeIssueSourceEnv + } + if issue := cleanIssueIdentity(input.IssueIdentity); issue != "" { + source := strings.TrimSpace(input.IssueIdentitySource) + if source == "" { + source = RuntimeIssueSourceInput + } + return issue, source + } + if issue := ExtractIssueIdentity(input.Text); issue != "" { + return issue, RuntimeIssueSourceInputText + } + return "", RuntimeIssueSourceUnresolved +} diff --git a/harness/internal/surface/multica/runtime_items.go b/harness/internal/surface/multica/runtime_items.go index 115322eb..59d4f59c 100644 --- a/harness/internal/surface/multica/runtime_items.go +++ b/harness/internal/surface/multica/runtime_items.go @@ -23,8 +23,9 @@ type RuntimeCommandExecutionMaterial struct { } type RuntimeInput struct { - Text string - IssueIdentity string + Text string + IssueIdentity string + IssueIdentitySource string } func RuntimeManagedTraceMessages(threadID, turnID string, event activationtrace.Event, now time.Time) []RuntimeRPCMessage { @@ -152,7 +153,9 @@ func RuntimeInputMaterial(params map[string]any) RuntimeInput { } var parts []string structuredIssue := "" + structuredSource := "" textIssue := "" + textSource := "" for _, item := range input { obj, ok := item.(map[string]any) if !ok { @@ -160,17 +163,32 @@ func RuntimeInputMaterial(params map[string]any) RuntimeInput { } if structuredIssue == "" { structuredIssue = runtimeStructuredIssueIdentity(obj) + if structuredIssue != "" { + structuredSource = RuntimeIssueSourceInput + } } if text, _ := obj["text"].(string); strings.TrimSpace(text) != "" { parts = append(parts, text) if textIssue == "" { textIssue = ExtractIssueIdentity(text) + if textIssue != "" { + textSource = RuntimeIssueSourceInputText + } } } } + issueIdentity := firstNonEmptyString(structuredIssue, textIssue) + issueSource := "" + switch issueIdentity { + case structuredIssue: + issueSource = structuredSource + case textIssue: + issueSource = textSource + } return RuntimeInput{ - Text: strings.Join(parts, "\n"), - IssueIdentity: firstNonEmptyString(structuredIssue, textIssue), + Text: strings.Join(parts, "\n"), + IssueIdentity: issueIdentity, + IssueIdentitySource: issueSource, } } diff --git a/harness/internal/surface/multica/runtime_items_test.go b/harness/internal/surface/multica/runtime_items_test.go index e985e7cd..cef6f2ed 100644 --- a/harness/internal/surface/multica/runtime_items_test.go +++ b/harness/internal/surface/multica/runtime_items_test.go @@ -166,6 +166,9 @@ func TestRuntimeInputMaterialExtractsStructuredIssueIdentity(t *testing.T) { if got.IssueIdentity != "iss-49" { t.Fatalf("structured issue identity = %q, want iss-49", got.IssueIdentity) } + if got.IssueIdentitySource != RuntimeIssueSourceInput { + t.Fatalf("structured issue source = %q, want %q", got.IssueIdentitySource, RuntimeIssueSourceInput) + } } func TestRuntimeInputMaterialFallsBackToVisibleIssueTag(t *testing.T) { @@ -177,6 +180,9 @@ func TestRuntimeInputMaterialFallsBackToVisibleIssueTag(t *testing.T) { if got.IssueIdentity != "TEA-50" { t.Fatalf("visible issue tag identity = %q, want TEA-50", got.IssueIdentity) } + if got.IssueIdentitySource != RuntimeIssueSourceInputText { + t.Fatalf("visible issue source = %q, want %q", got.IssueIdentitySource, RuntimeIssueSourceInputText) + } } func TestRuntimeInputMaterialPrefersStructuredIssueOverVisibleTag(t *testing.T) { From 518bd305874416187b50c5205714b398ac8efda5 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 06:18:06 +0800 Subject: [PATCH 113/117] fix: reject occupied mnemond listen addresses Preflight the mnemond background listen address before detaching the child process. This prevents mnemond up from treating an unrelated listener on the configured port as its own readiness signal and leaving a misleading pidfile path. Validation: go test -count=1 ./harness/cmd/mnemond; go test -count=1 ./harness/internal/coreguard ./harness/cmd/mnemond ./harness/cmd/mnemon-hub ./harness/internal/daemon; temporary setup/up/status/doctor/down lifecycle on a free loopback port; go test -count=1 ./...; git diff --check. --- harness/cmd/mnemond/daemon.go | 11 +++++++++++ harness/cmd/mnemond/daemon_test.go | 31 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/harness/cmd/mnemond/daemon.go b/harness/cmd/mnemond/daemon.go index 59661bbb..4ecb50e6 100644 --- a/harness/cmd/mnemond/daemon.go +++ b/harness/cmd/mnemond/daemon.go @@ -79,6 +79,9 @@ func daemonUp(args []string, out, errw io.Writer) error { if pid, alive := readLivePid(pidPath); alive { return fmt.Errorf("already running (pid %d); run `mnemond down` first", pid) } + if err := ensureListenAddrAvailable(cfg.listenAddr); err != nil { + return err + } if err := os.MkdirAll(dir, 0o700); err != nil { return err } @@ -115,6 +118,14 @@ func daemonUp(args []string, out, errw io.Writer) error { return nil } +func ensureListenAddrAvailable(addr string) error { + ln, err := net.Listen("tcp", addr) + if err != nil { + return fmt.Errorf("listen address %s is unavailable: %w", addr, err) + } + return ln.Close() +} + // waitListening confirms the detached child came up: it polls for the child to accept a TCP // connection on its listen address (a strong readiness signal that also catches a bind failure), // failing fast if the child exits during startup. diff --git a/harness/cmd/mnemond/daemon_test.go b/harness/cmd/mnemond/daemon_test.go index b2679041..1d2aa2cb 100644 --- a/harness/cmd/mnemond/daemon_test.go +++ b/harness/cmd/mnemond/daemon_test.go @@ -2,11 +2,16 @@ package main import ( "bytes" + "context" + "io" + "net" "os" "path/filepath" "strconv" "strings" "testing" + + "github.com/mnemon-dev/mnemon/harness/internal/app" ) // status/down/logs operate on the pidfile + logfile under .mnemon/harness/local without spawning a @@ -101,6 +106,32 @@ func TestDaemonLogsNoFileYet(t *testing.T) { } } +func TestDaemonUpRefusesOccupiedListenAddress(t *testing.T) { + root := t.TempDir() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + addr := ln.Addr().String() + if _, err := app.New(root).Setup(context.Background(), io.Discard, io.Discard, app.SetupOptions{ + Host: "codex", + Principal: "planner@team", + ControlURL: "http://" + addr, + }); err != nil { + t.Fatalf("setup: %v", err) + } + var out bytes.Buffer + err = daemonUp([]string{"--root", root, "--addr", addr}, &out, &out) + if err == nil || !strings.Contains(err.Error(), "listen address") || !strings.Contains(err.Error(), "unavailable") { + t.Fatalf("daemon up should refuse occupied address, got %v output=%q", err, out.String()) + } + _, pidPath, _ := daemonPaths(root) + if _, err := os.Stat(pidPath); !os.IsNotExist(err) { + t.Fatalf("daemon up must not write pidfile when address is occupied, stat err=%v", err) + } +} + func TestDaemonPathsUnderLocalStateDir(t *testing.T) { _, pidPath, logPath := daemonPaths("/proj") if pidPath != filepath.FromSlash("/proj/.mnemon/harness/local/mnemond.pid") { From ad6181a04413f054cd1ae66500ad2443fc7b2f9e Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 06:25:45 +0800 Subject: [PATCH 114/117] test: guard multica hub-flow prerequisites Require the live Multica hub-flow acceptance gate to prove participant agents have a managed runtime capable of driving teamwork before creating the root issue. This prevents noop-configured runtime agents from being mistaken for a valid multi-agent hub-flow environment and keeps failed prerequisite checks from polluting the workspace with impossible acceptance issues. Validation: go test -count=1 ./harness/cmd/mnemon-acceptance; go test -count=1 ./harness/internal/driver ./harness/cmd/mnemon-acceptance; live multica-runtime-prod-sim --require-hub-flow prerequisite check against the current workspace failed before issue creation because all five participants are configured with MNEMON_MANAGED_RUNTIME=noop; go test -count=1 ./...; git diff --check. --- .../acceptance_multica_runtime.go | 63 ++++++++++++++ .../acceptance_multica_runtime_test.go | 83 +++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 116262a1..6bbb9fb6 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -268,6 +268,16 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime Env: os.Environ(), Timeout: 30 * time.Second, } + if opts.RequireHubFlow { + ok, detail, err := multicaHubFlowManagedRuntimeReady(ctx, cli, registry.Participants, opts.MinActiveAgents, assignee.Principal) + addMulticaProdSimAssertion(&report, "hub-flow agents expose managed runtime", ok, detail) + if err != nil { + return finishMulticaRuntimeProdSimReport(report, err) + } + if !ok { + return finishMulticaRuntimeProdSimReport(report, fmt.Errorf("Multica hub-flow requires at least %d managed runtime participants and a managed root assignee", opts.MinActiveAgents)) + } + } title := strings.TrimSpace(opts.IssueTitle) if title == "" { title = taskCase.Title @@ -347,6 +357,59 @@ func selectMulticaAcceptanceAssignee(reg driver.MulticaRegistry, principal strin return driver.MulticaParticipantRecord{}, fmt.Errorf("registry has no participant with a Multica agent id") } +func multicaHubFlowManagedRuntimeReady(ctx context.Context, cli driver.MulticaCLI, participants []driver.MulticaParticipantRecord, minActive int, requiredPrincipal string) (bool, string, error) { + if minActive < 1 { + minActive = 1 + } + requiredPrincipal = strings.TrimSpace(requiredPrincipal) + active := 0 + requiredActive := requiredPrincipal == "" + var details []string + for _, participant := range participants { + principal := strings.TrimSpace(participant.Principal) + agentID := strings.TrimSpace(participant.AgentID) + if principal == "" || agentID == "" { + continue + } + env, err := cli.GetAgentEnv(ctx, agentID) + if err != nil { + return false, strings.Join(details, "; "), fmt.Errorf("read Multica agent env for %s: %w", principal, err) + } + runtimeName := strings.TrimSpace(env["MNEMON_MANAGED_RUNTIME"]) + ready := multicaManagedRuntimeCanDriveTeamwork(runtimeName) + if ready { + active++ + } + if principal == requiredPrincipal { + requiredActive = ready + } + details = append(details, fmt.Sprintf("%s=%s", principal, multicaManagedRuntimeReadinessLabel(runtimeName, ready))) + } + ok := active >= minActive && requiredActive + details = append(details, fmt.Sprintf("active=%d min=%d root_ready=%v", active, minActive, requiredActive)) + return ok, strings.Join(details, "; "), nil +} + +func multicaManagedRuntimeCanDriveTeamwork(runtimeName string) bool { + switch strings.ToLower(strings.TrimSpace(runtimeName)) { + case "codex-appserver": + return true + default: + return false + } +} + +func multicaManagedRuntimeReadinessLabel(runtimeName string, ready bool) string { + runtimeName = strings.TrimSpace(runtimeName) + if runtimeName == "" { + runtimeName = "missing" + } + if ready { + return runtimeName + } + return runtimeName + " (not hub-flow capable)" +} + func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, opts multicaRuntimeProdSimOptions, report *multicaRuntimeProdSimReport) error { rootMeta, err := cli.ListIssueMetadata(ctx, report.Issue.ID) if err != nil { diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 55ac2d90..444fe3a0 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -147,6 +147,7 @@ func TestMulticaRuntimeProdSimAcceptanceRequiresHubFlow(t *testing.T) { printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" cat >> "$MULTICA_STDIN_PATH" case "$*" in + *"agent env get agent-"*) agent="${4:-}"; printf '{"agent_id":"%s","custom_env":{"MNEMON_MANAGED_RUNTIME":"codex-appserver"}}\n' "$agent" ;; *"issue create"*) printf '{"id":"root-9","identifier":"TEA-9","title":"Runtime hub flow","description":"Teamwork acceptance","status":"todo"}\n' ;; *"issue get root-9"*) printf '{"id":"root-9","identifier":"TEA-9","title":"Runtime hub flow","description":"Teamwork acceptance","status":"done"}\n' ;; *"issue get child-2"*) printf '%s\n' '{"id":"child-2","identifier":"TEA-10","title":"TEA-9: routing check","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-9](mention://issue/root-9) - Runtime hub flow\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing check\n\n## Feedback\n\n- Expected feedback: result or blocker\n- Progress path: Mnemon runtime progress, result, or blocker feedback","status":"done"}' ;; @@ -227,6 +228,86 @@ esac } } +func TestMulticaRuntimeProdSimHubFlowRejectsNoopManagedRuntimeBeforeCreatingIssue(t *testing.T) { + tmp := t.TempDir() + registryPath := filepath.Join(tmp, "registry.json") + var participants []driver.MulticaParticipantRecord + for _, role := range []string{"planner", "researcher", "implementer", "reviewer", "integrator"} { + participants = append(participants, driver.MulticaParticipantRecord{ + Principal: role + "@team", + AgentName: "mnemon-" + role, + AgentID: "agent-" + role, + Role: role, + }) + } + if err := driver.SaveMulticaRegistry(registryPath, driver.MulticaRegistry{ + SchemaVersion: 1, + WorkspaceID: "ws-1", + RuntimeProfileID: "profile-1", + RuntimeID: "runtime-1", + Participants: participants, + }); err != nil { + t.Fatal(err) + } + argsPath := filepath.Join(tmp, "args.txt") + stdinPath := filepath.Join(tmp, "stdin.txt") + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +cat >> "$MULTICA_STDIN_PATH" +case "$*" in + *"agent env get agent-"*) agent="${4:-}"; printf '{"agent_id":"%s","custom_env":{"MNEMON_MANAGED_RUNTIME":"noop"}}\n' "$agent" ;; + *"issue create"*) printf 'issue create should not be reached\n' >&2; exit 99 ;; + *) printf '{}\n' ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("MULTICA_ARGS_PATH", argsPath) + t.Setenv("MULTICA_STDIN_PATH", stdinPath) + + report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ + RunRoot: filepath.Join(tmp, ".testdata", "multica-hub-noop"), + MulticaBin: bin, + WorkspaceID: "ws-1", + RegistryPath: registryPath, + AssigneePrincipal: "planner@team", + IssueTitle: "Noop hub flow", + IssueDescription: "Teamwork acceptance", + Wait: time.Millisecond, + Poll: time.Millisecond, + RequireIngest: true, + RequireHubFlow: true, + MinParticipants: 5, + MinActiveAgents: 3, + }) + if err == nil || !strings.Contains(err.Error(), "managed runtime participants") { + t.Fatalf("expected managed runtime prerequisite error, got err=%v report=%+v", err, report) + } + if report.Status != "failed" || report.Issue.ID != "" { + t.Fatalf("hub-flow prerequisite should fail before issue create: %+v", report) + } + var readinessAssertion *multicaRuntimeProdSimAssertion + for i := range report.Assertions { + if report.Assertions[i].Name == "hub-flow agents expose managed runtime" { + readinessAssertion = &report.Assertions[i] + break + } + } + if readinessAssertion == nil || readinessAssertion.Passed || !strings.Contains(readinessAssertion.Detail, "noop (not hub-flow capable)") { + t.Fatalf("unexpected readiness assertion: %+v all=%+v", readinessAssertion, report.Assertions) + } + rawArgs, err := os.ReadFile(argsPath) + if err != nil { + t.Fatal(err) + } + args := string(rawArgs) + if strings.Contains(args, "issue create") { + t.Fatalf("issue create must not run when hub-flow managed runtime is noop:\n%s", args) + } +} + func TestMulticaRuntimeProdSimHubFlowWritesPartialSnapshotWhenMailboxExpectationMisses(t *testing.T) { tmp := t.TempDir() registryPath := filepath.Join(tmp, "registry.json") @@ -255,6 +336,7 @@ func TestMulticaRuntimeProdSimHubFlowWritesPartialSnapshotWhenMailboxExpectation printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" cat >> "$MULTICA_STDIN_PATH" case "$*" in + *"agent env get agent-"*) agent="${4:-}"; printf '{"agent_id":"%s","custom_env":{"MNEMON_MANAGED_RUNTIME":"codex-appserver"}}\n' "$agent" ;; *"issue create"*) printf '{"id":"root-partial","identifier":"TEA-30","title":"Partial hub evidence","description":"Teamwork acceptance","status":"todo"}\n' ;; *"issue get root-partial"*) printf '{"id":"root-partial","identifier":"TEA-30","title":"Partial hub evidence","description":"Teamwork acceptance","status":"in_progress"}\n' ;; *"issue get child-partial"*) printf '%s\n' '{"id":"child-partial","identifier":"TEA-31","title":"TEA-30: routing","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-30](mention://issue/root-partial) - Partial hub evidence\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing\n\n## Feedback\n\n- Expected feedback: result","status":"done"}' ;; @@ -354,6 +436,7 @@ func TestMulticaRuntimeProdSimHubFlowAllowsDeferredRunMessages(t *testing.T) { printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" cat >> "$MULTICA_STDIN_PATH" case "$*" in + *"agent env get agent-"*) agent="${4:-}"; printf '{"agent_id":"%s","custom_env":{"MNEMON_MANAGED_RUNTIME":"codex-appserver"}}\n' "$agent" ;; *"issue create"*) printf '{"id":"root-deferred","identifier":"TEA-20","title":"Deferred run messages","description":"Teamwork acceptance","status":"todo"}\n' ;; *"issue get root-deferred"*) printf '{"id":"root-deferred","identifier":"TEA-20","title":"Deferred run messages","description":"Teamwork acceptance","status":"done"}\n' ;; *"issue get child-a"*) printf '%s\n' '{"id":"child-a","identifier":"TEA-21","title":"TEA-20: routing","description":"## Assignment\n\nCheck routing.\n\n## Context\n\n- Root issue: [TEA-20](mention://issue/root-deferred) - Deferred run messages\n- Assignee: researcher@team (mnemon-researcher)\n- Scope: routing\n\n## Feedback\n\n- Expected feedback: result","status":"done"}' ;; From 2f390929dd4251546153d1a7ed56c19050e15c05 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 07:13:00 +0800 Subject: [PATCH 115/117] fix: pass managed wake context to codex appserver Codex app-server no longer accepts turn/start additionalContext, so pass the rendered Mnemon hook context through thread developerInstructions while preserving the sentinel-only user input contract. Validation: go test ./harness/internal/drive ./harness/internal/driver ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-acceptance ./harness/cmd/mnemond; live Multica hub-flow TEA-133 passed with child mailboxes and multi-agent runs. --- harness/internal/drive/runtime.go | 61 +++++++++++++-- harness/internal/drive/runtime_test.go | 102 +++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 7 deletions(-) diff --git a/harness/internal/drive/runtime.go b/harness/internal/drive/runtime.go index 7e6847d8..9683926f 100644 --- a/harness/internal/drive/runtime.go +++ b/harness/internal/drive/runtime.go @@ -12,6 +12,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" "strings" "sync" "time" @@ -228,12 +229,19 @@ func (c CodexAppServerTurnClient) StartTurnWithTrace(ctx context.Context, query }, requestTimeout); err != nil { return ManagedTurnResult{}, fmt.Errorf("initialize: %w", err) } + developerInstructions, err := CodexAppServerDeveloperInstructions(ctx, workspace, c.Env, ManagedWakeQuery) + if err != nil { + return ManagedTurnResult{}, fmt.Errorf("load hook context: %w", err) + } threadParams := map[string]any{ "cwd": workspace, "approvalPolicy": "never", "sandbox": "danger-full-access", "ephemeral": true, } + if developerInstructions != "" { + threadParams["developerInstructions"] = developerInstructions + } thread, err := server.Request("thread/start", threadParams, requestTimeout) if err != nil { return ManagedTurnResult{}, fmt.Errorf("thread/start: %w", err) @@ -242,10 +250,6 @@ func (c CodexAppServerTurnClient) StartTurnWithTrace(ctx context.Context, query if threadID == "" { return ManagedTurnResult{}, fmt.Errorf("thread/start returned no thread id") } - additionalContext, err := CodexAppServerAdditionalContext(ctx, workspace, c.Env, ManagedWakeQuery) - if err != nil { - return ManagedTurnResult{}, fmt.Errorf("load hook context: %w", err) - } turnParams := map[string]any{ "threadId": threadID, "input": []map[string]any{{"type": "text", "text": ManagedWakeQuery}}, @@ -253,9 +257,6 @@ func (c CodexAppServerTurnClient) StartTurnWithTrace(ctx context.Context, query "approvalPolicy": "never", "sandboxPolicy": map[string]any{"type": "dangerFullAccess"}, } - if len(additionalContext) > 0 { - turnParams["additionalContext"] = additionalContext - } before := server.NotificationCount() if _, err := server.Request("turn/start", turnParams, requestTimeout); err != nil { return ManagedTurnResult{}, fmt.Errorf("turn/start: %w", err) @@ -280,6 +281,14 @@ func (c CodexAppServerTurnClient) StartTurnWithTrace(ctx context.Context, query return ManagedTurnResult{TurnID: threadID, Status: "completed", FinalAnswer: answer}, nil } +func CodexAppServerDeveloperInstructions(ctx context.Context, workspace string, env []string, query string) (string, error) { + additionalContext, err := CodexAppServerAdditionalContext(ctx, workspace, env, query) + if err != nil { + return "", err + } + return formatCodexAppServerDeveloperInstructions(additionalContext), nil +} + func CodexAppServerAdditionalContext(ctx context.Context, workspace string, env []string, query string) (map[string]any, error) { out := map[string]any{} for _, lifecycle := range []string{"prime", "remind"} { @@ -298,6 +307,44 @@ func CodexAppServerAdditionalContext(ctx context.Context, workspace string, env return out, nil } +func formatCodexAppServerDeveloperInstructions(entries map[string]any) string { + if len(entries) == 0 { + return "" + } + keys := make([]string, 0, len(entries)) + for key := range entries { + keys = append(keys, key) + } + sort.Strings(keys) + var b strings.Builder + b.WriteString("Mnemon managed wake context.\n") + b.WriteString("The user input for this turn is only the local wake sentinel `[mnemon:wake]`. Use the application context below as the governed Mnemon context for deciding whether to read or record state. If a teamwork signal says assignment or self-assignment may be useful, act through the Mnemon commands described in the guide instead of answering that no task was supplied.\n") + for _, key := range keys { + value := additionalContextEntryValue(entries[key]) + if value == "" { + continue + } + b.WriteString("\n## ") + b.WriteString(key) + b.WriteString("\n") + b.WriteString(value) + b.WriteString("\n") + } + return strings.TrimSpace(b.String()) +} + +func additionalContextEntryValue(entry any) string { + switch v := entry.(type) { + case string: + return strings.TrimSpace(v) + case map[string]any: + if value, ok := v["value"].(string); ok { + return strings.TrimSpace(value) + } + } + return "" +} + func runCodexLifecycleHook(ctx context.Context, workspace string, env []string, lifecycle string, query string) (string, error) { hook := filepath.Join(workspace, ".codex", "hooks", "mnemon-r1", lifecycle+".sh") if _, err := os.Stat(hook); errors.Is(err, os.ErrNotExist) { diff --git a/harness/internal/drive/runtime_test.go b/harness/internal/drive/runtime_test.go index 2eaa24ed..90b613bf 100644 --- a/harness/internal/drive/runtime_test.go +++ b/harness/internal/drive/runtime_test.go @@ -5,8 +5,11 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "os" "path/filepath" + "strings" "testing" + "time" eventmodel "github.com/mnemon-dev/mnemon/harness/internal/event" "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation" @@ -107,3 +110,102 @@ func TestCodexAppServerTurnClientRejectsContextQuery(t *testing.T) { t.Fatal("codex appserver client must reject non-sentinel queries before starting a process") } } + +func TestCodexAppServerTurnClientInjectsHookContextAsDeveloperInstructions(t *testing.T) { + workspace := t.TempDir() + requestLog := filepath.Join(workspace, "requests.jsonl") + fakeCodex := filepath.Join(workspace, "fake-codex") + if err := os.WriteFile(fakeCodex, []byte(`#!/usr/bin/env bash +while IFS= read -r line; do + printf '%s\n' "$line" >> "$CODEX_REQUEST_LOG" + case "$line" in + *'"method":"initialize"'*) + printf '{"jsonrpc":"2.0","id":1,"result":{"userAgent":"fake"}}\n' + ;; + *'"method":"thread/start"'*) + printf '{"jsonrpc":"2.0","id":2,"result":{"thread":{"id":"thread-1"}}}\n' + ;; + *'"method":"turn/start"'*) + printf '{"jsonrpc":"2.0","id":3,"result":{"turn":{"id":"turn-1","status":"inProgress"}}}\n' + printf '{"jsonrpc":"2.0","method":"item/completed","params":{"threadId":"thread-1","turnId":"turn-1","item":{"type":"agentMessage","id":"msg-1","text":"handled wake","phase":"final_answer"}}}\n' + printf '{"jsonrpc":"2.0","method":"turn/completed","params":{"threadId":"thread-1","turn":{"id":"turn-1","status":"completed"}}}\n' + ;; + esac +done +`), 0o755); err != nil { + t.Fatal(err) + } + hookDir := filepath.Join(workspace, ".codex", "hooks", "mnemon-r1") + if err := os.MkdirAll(hookDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(hookDir, "prime.sh"), []byte(`#!/usr/bin/env bash +printf '{"systemMessage":"prime guide loaded"}\n' +`), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(hookDir, "remind.sh"), []byte(`#!/usr/bin/env bash +INPUT="$(cat || true)" +case "${INPUT}" in + *"[mnemon:wake]"*) printf '{"systemMessage":"remind rendered governed context"}\n' ;; + *) printf '{"systemMessage":"remind generic"}\n' ;; +esac +`), 0o755); err != nil { + t.Fatal(err) + } + + client := CodexAppServerTurnClient{ + Command: fakeCodex, + Workspace: workspace, + Env: append(os.Environ(), "CODEX_REQUEST_LOG="+requestLog), + TurnTimeout: 5 * time.Second, + RequestTimeout: 5 * time.Second, + } + result, err := client.StartTurn(context.Background(), ManagedWakeQuery) + if err != nil { + t.Fatal(err) + } + if result.Status != "completed" || !strings.Contains(result.FinalAnswer, "handled wake") { + t.Fatalf("result = %+v", result) + } + + raw, err := os.ReadFile(requestLog) + if err != nil { + t.Fatal(err) + } + var threadStart, turnStart map[string]any + for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { + var msg map[string]any + if err := json.Unmarshal([]byte(line), &msg); err != nil { + t.Fatalf("decode request %q: %v", line, err) + } + switch msg["method"] { + case "thread/start": + threadStart = msg + case "turn/start": + turnStart = msg + } + } + if threadStart == nil || turnStart == nil { + t.Fatalf("missing thread/start or turn/start request:\n%s", raw) + } + threadParams := threadStart["params"].(map[string]any) + instructions, _ := threadParams["developerInstructions"].(string) + for _, want := range []string{"Mnemon managed wake context", "prime guide loaded", "remind rendered governed context", "assignment or self-assignment may be useful"} { + if !strings.Contains(instructions, want) { + t.Fatalf("developerInstructions missing %q:\n%s", want, instructions) + } + } + if _, ok := threadParams["additionalContext"]; ok { + t.Fatalf("thread/start must not use removed additionalContext field: %+v", threadParams) + } + turnParams := turnStart["params"].(map[string]any) + if _, ok := turnParams["additionalContext"]; ok { + t.Fatalf("turn/start must not use removed additionalContext field: %+v", turnParams) + } + input := turnParams["input"].([]any) + first := input[0].(map[string]any) + if first["text"] != ManagedWakeQuery { + t.Fatalf("turn input = %+v, want sentinel only", first) + } +} From 8ecac0183a5fbcac026f68c8af94bd71fb7d3673 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 07:19:13 +0800 Subject: [PATCH 116/117] feat: show structured daemon role details Expose normalized daemon role details in mnemon-harness daemon status so configured Multica, GitHub, mnemonhub, managed-drive, and projection roles are visible without relying on prose-only summaries. The detail view marks mnemonhub as a remote exchange boundary while Multica remains the activation carrier/projection surface. Validation: go test ./harness/internal/daemon ./harness/cmd/mnemon-harness; go test ./harness/internal/productconfig ./harness/internal/coreguard; git diff --check. --- .../cmd/mnemon-harness/config_daemon_test.go | 15 ++- harness/cmd/mnemon-harness/daemon.go | 8 ++ harness/internal/daemon/configured.go | 92 ++++++++++++++----- harness/internal/daemon/configured_test.go | 34 +++++++ 4 files changed, 125 insertions(+), 24 deletions(-) diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index 9de3666c..18ed17ff 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -88,7 +88,8 @@ func TestDaemonStatusShowsConfiguredRoleSummary(t *testing.T) { cfg := productconfig.Default() cfg.Connections.Multica = productconfig.MulticaConnection{Enabled: true, Workspace: "ws-multica", RuntimeBinary: "mnemon-multica-runtime"} cfg.Connections.GitHub = productconfig.GitHubConnection{Enabled: true, Repo: "mnemon-dev/mnemon-teamwork-example"} - cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMultica, productconfig.ConnectionGitHub} + cfg.Connections.Mnemonhub = productconfig.MnemonhubConnection{Enabled: true, Endpoint: "https://hub.example.invalid"} + cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMultica, productconfig.ConnectionGitHub, productconfig.ConnectionMnemonhub} cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} cfg.Daemon.ProjectionSurfaces = []string{productconfig.ConnectionMultica} if err := productconfig.Save(productconfig.DefaultPath(root, ""), cfg); err != nil { @@ -103,7 +104,17 @@ func TestDaemonStatusShowsConfiguredRoleSummary(t *testing.T) { t.Fatal(err) } got := out.String() - for _, want := range []string{"Harness config: configured", "Harness daemon roles: watchers=2 drive=1 surfaces=1", "Harness daemon: not running"} { + for _, want := range []string{ + "Harness config: configured", + "Harness daemon roles: watchers=3 drive=1 surfaces=1", + "Harness daemon role details:", + "multica-watch [interaction]: watcher=multica boundary=activation-carrier", + "github-watch [interaction]: watcher=github boundary=external-interaction", + "mnemonhub-watch [interaction]: watcher=mnemonhub boundary=remote-exchange", + "managed-drive [drive]: drive=managed-local boundary=managed-runtime", + "multica-project [projection]: surface=multica boundary=projection-surface", + "Harness daemon: not running", + } { if !strings.Contains(got, want) { t.Fatalf("daemon status missing %q:\n%s", want, got) } diff --git a/harness/cmd/mnemon-harness/daemon.go b/harness/cmd/mnemon-harness/daemon.go index 3fe7fa17..ea895ac1 100644 --- a/harness/cmd/mnemon-harness/daemon.go +++ b/harness/cmd/mnemon-harness/daemon.go @@ -118,6 +118,14 @@ func runDaemonStatus(cmd *cobra.Command, args []string) error { func writeDaemonRoleSummary(out io.Writer, cfg productconfig.Config) { summary := daemon.RoleSummary(cfg) fmt.Fprintf(out, "Harness daemon roles: watchers=%d drive=%d surfaces=%d\n", summary.InteractionWatchers, summary.DriveSources, summary.ProjectionSurfaces) + details := daemon.RoleDetails(cfg) + if len(details) == 0 { + return + } + fmt.Fprintln(out, "Harness daemon role details:") + for _, role := range details { + fmt.Fprintf(out, " - %s [%s]: %s=%s boundary=%s\n", role.WorkerName, role.Kind, role.Label, role.Value, role.Boundary) + } } func writeConfiguredDaemonSnapshot(root string, now time.Time) error { diff --git a/harness/internal/daemon/configured.go b/harness/internal/daemon/configured.go index bef7b540..6dbd79a1 100644 --- a/harness/internal/daemon/configured.go +++ b/harness/internal/daemon/configured.go @@ -13,35 +13,50 @@ type ConfiguredRoleSummary struct { ProjectionSurfaces int } -func RoleSummary(cfg productconfig.Config) ConfiguredRoleSummary { - return ConfiguredRoleSummary{ - InteractionWatchers: len(cfg.Daemon.InteractionWatchers), - DriveSources: len(cfg.Daemon.DriveSources), - ProjectionSurfaces: len(cfg.Daemon.ProjectionSurfaces), - } +type ConfiguredRoleDetail struct { + WorkerName string + Kind WorkerKind + Label string + Value string + Boundary string } -func ConfiguredSnapshot(cfg productconfig.Config, now time.Time) Snapshot { - snapshot := Snapshot{StartedAt: now.UTC(), Workers: map[string]WorkerSnapshot{}} - add := func(name string, kind WorkerKind, message string) { - name = workerName(name) - if name == "" { - return - } - snapshot.Workers[name] = WorkerSnapshot{ - Kind: kind, - Status: "configured", - Message: strings.TrimSpace(message), - StartedAt: snapshot.StartedAt, - UpdatedAt: snapshot.StartedAt, +func RoleSummary(cfg productconfig.Config) ConfiguredRoleSummary { + var summary ConfiguredRoleSummary + for _, role := range RoleDetails(cfg) { + switch role.Label { + case "watcher": + summary.InteractionWatchers++ + case "drive": + summary.DriveSources++ + case "surface": + summary.ProjectionSurfaces++ } } + return summary +} + +func RoleDetails(cfg productconfig.Config) []ConfiguredRoleDetail { + var details []ConfiguredRoleDetail for _, watcher := range cfg.Daemon.InteractionWatchers { watcher = strings.TrimSpace(watcher) if watcher == "" { continue } - add(watcher+"-watch", WorkerInteraction, "watcher="+watcher) + boundary := "external-interaction" + if watcher == productconfig.ConnectionMultica { + boundary = "activation-carrier" + } + if watcher == productconfig.ConnectionMnemonhub { + boundary = "remote-exchange" + } + details = append(details, ConfiguredRoleDetail{ + WorkerName: workerName(watcher + "-watch"), + Kind: WorkerInteraction, + Label: "watcher", + Value: watcher, + Boundary: boundary, + }) } for _, source := range cfg.Daemon.DriveSources { source = strings.TrimSpace(source) @@ -52,14 +67,47 @@ func ConfiguredSnapshot(cfg productconfig.Config, now time.Time) Snapshot { if source == productconfig.DriveManagedLocal { name = "managed-drive" } - add(name, WorkerDrive, "drive="+source) + details = append(details, ConfiguredRoleDetail{ + WorkerName: workerName(name), + Kind: WorkerDrive, + Label: "drive", + Value: source, + Boundary: "managed-runtime", + }) } for _, surface := range cfg.Daemon.ProjectionSurfaces { surface = strings.TrimSpace(surface) if surface == "" { continue } - add(surface+"-project", WorkerProjection, "surface="+surface) + details = append(details, ConfiguredRoleDetail{ + WorkerName: workerName(surface + "-project"), + Kind: WorkerProjection, + Label: "surface", + Value: surface, + Boundary: "projection-surface", + }) + } + return details +} + +func ConfiguredSnapshot(cfg productconfig.Config, now time.Time) Snapshot { + snapshot := Snapshot{StartedAt: now.UTC(), Workers: map[string]WorkerSnapshot{}} + add := func(name string, kind WorkerKind, message string) { + name = workerName(name) + if name == "" { + return + } + snapshot.Workers[name] = WorkerSnapshot{ + Kind: kind, + Status: "configured", + Message: strings.TrimSpace(message), + StartedAt: snapshot.StartedAt, + UpdatedAt: snapshot.StartedAt, + } + } + for _, role := range RoleDetails(cfg) { + add(role.WorkerName, role.Kind, role.Label+"="+role.Value) } if len(snapshot.Workers) > 0 { add("status-readiness", WorkerStatus, "daemon status snapshot") diff --git a/harness/internal/daemon/configured_test.go b/harness/internal/daemon/configured_test.go index bd289508..8abaaba9 100644 --- a/harness/internal/daemon/configured_test.go +++ b/harness/internal/daemon/configured_test.go @@ -55,6 +55,31 @@ func TestConfiguredSnapshotKeepsMnemonhubAsExchangeWatcher(t *testing.T) { } } +func TestRoleDetailsDescribeDaemonBoundaries(t *testing.T) { + cfg := productconfig.Default() + cfg.Daemon.InteractionWatchers = []string{ + productconfig.ConnectionMultica, + productconfig.ConnectionMnemonhub, + } + cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} + cfg.Daemon.ProjectionSurfaces = []string{productconfig.ConnectionMultica} + + details := RoleDetails(cfg) + if len(details) != 4 { + t.Fatalf("role details mismatch: %+v", details) + } + for _, want := range []ConfiguredRoleDetail{ + {WorkerName: "multica-watch", Kind: WorkerInteraction, Label: "watcher", Value: productconfig.ConnectionMultica, Boundary: "activation-carrier"}, + {WorkerName: "mnemonhub-watch", Kind: WorkerInteraction, Label: "watcher", Value: productconfig.ConnectionMnemonhub, Boundary: "remote-exchange"}, + {WorkerName: "managed-drive", Kind: WorkerDrive, Label: "drive", Value: productconfig.DriveManagedLocal, Boundary: "managed-runtime"}, + {WorkerName: "multica-project", Kind: WorkerProjection, Label: "surface", Value: productconfig.ConnectionMultica, Boundary: "projection-surface"}, + } { + if !hasRoleDetail(details, want) { + t.Fatalf("missing role detail %+v in %+v", want, details) + } + } +} + func TestRoleSummaryReflectsConfiguredDaemonRoles(t *testing.T) { cfg := productconfig.Default() cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMultica, productconfig.ConnectionGitHub} @@ -66,3 +91,12 @@ func TestRoleSummaryReflectsConfiguredDaemonRoles(t *testing.T) { t.Fatalf("role summary mismatch: %+v", got) } } + +func hasRoleDetail(details []ConfiguredRoleDetail, want ConfiguredRoleDetail) bool { + for _, detail := range details { + if detail == want { + return true + } + } + return false +} From 71eca533ad9c658e60c7e924d8183318c30cb664 Mon Sep 17 00:00:00 2001 From: Grivn Date: Tue, 30 Jun 2026 09:10:52 +0800 Subject: [PATCH 117/117] chore: keep mnemon dev notes untracked Remove the accidentally tracked .mnemon-dev note from the repository index while leaving the local ignored workspace notes intact. --- .../r2/multica-parallel-poc-overlap-case.md | 234 ------------------ 1 file changed, 234 deletions(-) delete mode 100644 .mnemon-dev/architecture/r2/multica-parallel-poc-overlap-case.md diff --git a/.mnemon-dev/architecture/r2/multica-parallel-poc-overlap-case.md b/.mnemon-dev/architecture/r2/multica-parallel-poc-overlap-case.md deleted file mode 100644 index 8ddd4b4b..00000000 --- a/.mnemon-dev/architecture/r2/multica-parallel-poc-overlap-case.md +++ /dev/null @@ -1,234 +0,0 @@ -# Multica Parallel PoC Overlap Case - -- Status: R2 acceptance scenario design -- Date: 2026-06-30 -- Related: - - `multica-hub-backend-architecture.md` - - `multica-runtime-adapter.md` - - `teamwork-event-driven-cue-model.md` - - `../teamwork-protocol-sense-select-work-feedback.md` - -## 0. Purpose - -这个 case 用来验证 Multica 作为 Mnemon hub backend 时,是否能承载接近真实业务协作的复杂度: - -- 一个 root issue 作为 session mailbox。 -- 三个相关 PoC 同时启动,每个 PoC 有独立目录和交付物。 -- PoC 之间共享上下文,并要求反馈显式引用共享上下文。 -- 角色有重叠,同一个 agent 会把一个 PoC 的局部上下文带到另一个 PoC。 -- 第一轮反馈之后必须产生至少一个 follow-up assignment,触发第二轮 teamwork。 -- 最终 integrator 需要把多轮反馈合成一个 operator-facing decision。 - -本 case 不专门测试 GitHub mesh。GitHub mesh 只需要在设计上保持 mirror-only / no second activation path 的逻辑合理性。当前验收重点是 `mnemonhub` 与 Multica hub path,其中 Multica 是可见产品面和 assignment activation path。 - -## 1. Scenario - -Case id: - -```text -parallel-poc-overlap -``` - -Root issue title: - -```text -Parallel PoC overlap drill -``` - -Recommended command: - -```text -mnemon-acceptance multica-runtime-prod-sim \ - --task-case parallel-poc-overlap \ - --require-hub-flow \ - --min-participants 5 \ - --min-active-agents 5 -``` - -Directory plan created under the acceptance run root: - -```text -/ - acceptance-report.json - taskcase/ - parallel-poc-overlap/ - evidence/ - shared-context/ - session-map/ - mailbox-contract/ - risk-register/ - evidence-ledger/ - workstreams/ - poc-runtime-routing/ - poc-operator-runbook/ - poc-release-risk/ - roles/ - planner/ - researcher/ - implementer/ - reviewer/ - integrator/ -``` - -The root issue body should include the same plan in human-readable Markdown. Deterministic protocol data still belongs in Multica metadata or stable comment markers, not visible prose. - -## 2. Collaboration Topology - -```text - +-------------------------------+ - | Multica root issue | - | session mailbox | - +---------------+---------------+ - | - v - planner@team creates - first-round assignments - | - +-----------------------------------+-----------------------------------+ - | | | - v v v -+-----------------------+ +-----------------------+ +-----------------------+ -| poc-runtime-routing | | poc-operator-runbook | | poc-release-risk | -| assignment mailbox A | | assignment mailbox B | | assignment mailbox C | -| researcher/implementer| | implementer/reviewer | | researcher/reviewer/ | -| | | | | integrator | -+-----------+-----------+ +-----------+-----------+ +-----------+-----------+ - | | | - +---------------+-----------------+-----------------+---------------+ - | shared contexts and feedback | - v | - +---------------------+ | - | follow-up mailbox |<-----------------------+ - | chosen from gap or | - | disagreement | - +----------+----------+ - | - v - +-------------------+ - | integrator@team | - | final decision | - +-------------------+ -``` - -## 3. Shared Contexts - -```text -+------------------+---------------------------+-----------------------------+ -| Shared context | Used by | Purpose | -+------------------+---------------------------+-----------------------------+ -| session-map | runtime-routing, release | Map root, child, run, agent | -| mailbox-contract | runtime-routing, runbook | Visible text and metadata | -| risk-register | runbook, release | Risks, owners, mitigations | -| evidence-ledger | all PoCs | Issue/run/comment evidence | -+------------------+---------------------------+-----------------------------+ -``` - -Expected reuse: - -- `researcher@team` carries `session-map` from runtime routing into release risk. -- `implementer@team` carries `mailbox-contract` from runtime routing into runbook review. -- `reviewer@team` carries rollback and status-projection concerns from runbook review into release risk. -- `integrator@team` consumes all contexts and must wait for follow-up feedback before closing. - -## 4. Role Matrix - -```text -+------------------+-----------------------+-------------------------------+ -| Principal | Primary PoC | Overlap | -+------------------+-----------------------+-------------------------------+ -| planner@team | poc-runtime-routing | poc-release-risk | -| researcher@team | poc-runtime-routing | poc-release-risk | -| implementer@team | poc-operator-runbook | poc-runtime-routing | -| reviewer@team | poc-release-risk | poc-operator-runbook | -| integrator@team | poc-release-risk | all PoCs for final synthesis | -+------------------+-----------------------+-------------------------------+ -``` - -Roles are cues, not permanent identities. The protocol-level facts remain accepted events and hub metadata. An agent may be PoC-like when emitting a signal or assignment, and IC-like when working an assignment and producing feedback. - -## 5. Expected ReAct Progression - -```text -Round 1: Observe - root issue -> planner runtime run - planner emits three assignments - Multica projects three child issue mailboxes - runtime-routing, runbook, and release PoCs run in parallel - each feedback comment names: - - shared context consumed - - evidence artifact produced - - issue/run/comment/status refs - -Round 2: Act - planner or integrator reads first-round feedback - highest disagreement or missing evidence is selected - a follow-up assignment is created - follow-up owner reuses at least two shared contexts - feedback cites prior child comments before adding new evidence - -Round 3: Reflect - integrator consumes all PoC outputs and follow-up feedback - final root comment records: - - observed facts - - actions taken - - context reused across PoCs - - residual risk - - ship/hold/follow-up decision -``` - -## 6. Protocol And Multica Boundaries - -```text -+----------------------+------------------------------+------------------------------+ -| Layer | Carries | Must not carry | -+----------------------+------------------------------+------------------------------+ -| Multica visible text | task, context, evidence | session ids, assignment ids | -| Multica metadata | routing, dedupe, correlation | LLM prompt instructions | -| Multica comments | human feedback, event refs | sole canonical truth | -| Multica runs | activation evidence | proof of task completion | -| Mnemon event store | canonical accepted events | product-only display state | -| mnemond render | LLM-facing cue | raw Multica issue as prompt | -+----------------------+------------------------------+------------------------------+ -``` - -Standard Multica-visible references should be used for humans only: issue mentions, assigned agents, and normal comments. Machine correlation should use `mnemon.*` metadata keys and stable event/comment markers. - -## 7. Expected Acceptance Signals - -Minimum successful run: - -```text -active_agents >= 5 -child_mailboxes >= 4 -feedback_comments >= 4 -teamwork_rounds >= observe + act + reflect -root final status terminal -child final statuses result or blocker -``` - -Report evidence should show: - -- root metadata has `hub_backend=multica` and `kind=session_mailbox`. -- child issues carry assignment mailbox metadata. -- child visible text uses structured sections: Assignment, Context, Feedback. -- child visible text does not expose session ids, assignment ids, fingerprints, or projection owner keys. -- run evidence covers planner plus at least four additional participants. -- comments include context reuse evidence, not only generic completion text. - -## 8. Failure Signals - -```text -+------------------------------------+---------------------------------------+ -| Failure | Meaning | -+------------------------------------+---------------------------------------+ -| only one child mailbox | planner did not split teamwork | -| no follow-up mailbox | second ReAct round did not happen | -| no context names in feedback | context reuse is not observable | -| duplicated child mailbox | hub dedupe or assignment identity risk | -| protocol fields in visible text | metadata/visible boundary regression | -| active agent count below expected | Multica activation did not fan out | -| final root lacks decision | integration loop did not close | -+------------------------------------+---------------------------------------+ -``` - -This case is intentionally stronger than a routing smoke test. It should make weak assignment routing, missing feedback projection, stale context selection, and accidental protocol leakage visible in one Multica session.