From 89050ad2ca620eeb4f8a3f6c9b8bfd37468604ed Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 02:53:30 +0800 Subject: [PATCH 01/11] feat: add r3 multica surface primitives Introduce explicit Multica surface roles, action classification, display-safe writeback planning, activation carrier metadata, human interaction classification, and projection ledger idempotency for the R3 architecture. Validation: go test ./harness/internal/surface/multica --- harness/internal/surface/multica/ledger.go | 257 +++++++++++++ harness/internal/surface/multica/surface.go | 357 ++++++++++++++++++ .../surface/multica/surface_r3_test.go | 187 +++++++++ 3 files changed, 801 insertions(+) create mode 100644 harness/internal/surface/multica/ledger.go create mode 100644 harness/internal/surface/multica/surface.go create mode 100644 harness/internal/surface/multica/surface_r3_test.go diff --git a/harness/internal/surface/multica/ledger.go b/harness/internal/surface/multica/ledger.go new file mode 100644 index 00000000..faffc6f9 --- /dev/null +++ b/harness/internal/surface/multica/ledger.go @@ -0,0 +1,257 @@ +package multica + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" +) + +const MulticaDefaultProjectionLedgerRelPath = ".mnemon/multica/projection-ledger.jsonl" + +type ProjectionLedgerRecord struct { + EventRef string `json:"event_ref"` + ResourceRef string `json:"resource_ref,omitempty"` + ProjectionRef string `json:"projection_ref,omitempty"` + SourceArtifactRef string `json:"source_artifact_ref,omitempty"` + SurfaceRole SurfaceRole `json:"surface_role"` + TargetKind string `json:"target_kind"` + TargetID string `json:"target_id"` + Status string `json:"status"` +} + +type ProjectionLedgerTarget struct { + Kind string + ID string +} + +type FileProjectionLedger struct { + mu sync.Mutex + path string + records []ProjectionLedgerRecord + loaded bool +} + +func ProjectionLedgerPath(root, explicit string) string { + if strings.TrimSpace(explicit) != "" { + return filepath.Clean(explicit) + } + root = strings.TrimSpace(root) + if root == "" { + root = "." + } + return filepath.Join(root, MulticaDefaultProjectionLedgerRelPath) +} + +func NewFileProjectionLedger(path string) *FileProjectionLedger { + return &FileProjectionLedger{path: strings.TrimSpace(path)} +} + +func (l *FileProjectionLedger) Records() ([]ProjectionLedgerRecord, error) { + if err := l.withLockedLoad(func() error { return nil }); err != nil { + return nil, err + } + out := append([]ProjectionLedgerRecord(nil), l.records...) + return out, nil +} + +func (l *FileProjectionLedger) Find(eventRef string, role SurfaceRole, target ProjectionLedgerTarget) (ProjectionLedgerRecord, bool, error) { + eventRef = strings.TrimSpace(eventRef) + target.Kind = strings.TrimSpace(target.Kind) + target.ID = strings.TrimSpace(target.ID) + if eventRef == "" || role == "" || target.Kind == "" || target.ID == "" { + return ProjectionLedgerRecord{}, false, nil + } + var found ProjectionLedgerRecord + err := l.withLockedLoad(func() error { + for _, record := range l.records { + if projectionLedgerRecordMatches(record, eventRef, role, target) { + found = record + } + } + return nil + }) + if err != nil { + return ProjectionLedgerRecord{}, false, err + } + return found, strings.TrimSpace(found.EventRef) != "", nil +} + +func (l *FileProjectionLedger) Reserve(record ProjectionLedgerRecord) (ProjectionLedgerRecord, bool, error) { + record = normalizeProjectionLedgerRecord(record) + if err := validateProjectionLedgerRecord(record); err != nil { + return ProjectionLedgerRecord{}, false, err + } + var reserved ProjectionLedgerRecord + ok := false + err := l.withLockedLoad(func() error { + for _, existing := range l.records { + if projectionLedgerRecordMatches(existing, record.EventRef, record.SurfaceRole, ProjectionLedgerTarget{Kind: record.TargetKind, ID: record.TargetID}) { + reserved = existing + return nil + } + } + if record.Status == "" { + record.Status = "reserved" + } + l.records = append(l.records, record) + reserved = record + ok = true + return l.appendRecord(record) + }) + return reserved, ok, err +} + +func (l *FileProjectionLedger) Record(record ProjectionLedgerRecord) error { + record = normalizeProjectionLedgerRecord(record) + if err := validateProjectionLedgerRecord(record); err != nil { + return err + } + return l.withLockedLoad(func() error { + replaced := false + for i, existing := range l.records { + if projectionLedgerRecordMatches(existing, record.EventRef, record.SurfaceRole, ProjectionLedgerTarget{Kind: record.TargetKind, ID: record.TargetID}) { + l.records[i] = record + replaced = true + } + } + if !replaced { + l.records = append(l.records, record) + } + return l.rewrite() + }) +} + +func (l *FileProjectionLedger) withLockedLoad(fn func() error) error { + l.mu.Lock() + defer l.mu.Unlock() + if !l.loaded { + if err := l.load(); err != nil { + return err + } + l.loaded = true + } + return fn() +} + +func (l *FileProjectionLedger) load() error { + if l.path == "" { + return nil + } + file, err := os.Open(l.path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + defer file.Close() + var records []ProjectionLedgerRecord + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + var record ProjectionLedgerRecord + if err := json.Unmarshal([]byte(line), &record); err != nil { + return fmt.Errorf("decode projection ledger %s: %w", l.path, err) + } + records = upsertProjectionLedgerRecord(records, normalizeProjectionLedgerRecord(record)) + } + if err := scanner.Err(); err != nil { + return err + } + l.records = records + return nil +} + +func (l *FileProjectionLedger) appendRecord(record ProjectionLedgerRecord) error { + if l.path == "" { + return nil + } + if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { + return err + } + file, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer file.Close() + encoded, err := json.Marshal(record) + if err != nil { + return err + } + if _, err := file.Write(append(encoded, '\n')); err != nil { + return err + } + return nil +} + +func (l *FileProjectionLedger) rewrite() error { + if l.path == "" { + return nil + } + if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { + return err + } + file, err := os.OpenFile(l.path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer file.Close() + for _, record := range l.records { + encoded, err := json.Marshal(record) + if err != nil { + return err + } + if _, err := file.Write(append(encoded, '\n')); err != nil { + return err + } + } + return nil +} + +func normalizeProjectionLedgerRecord(record ProjectionLedgerRecord) ProjectionLedgerRecord { + record.EventRef = strings.TrimSpace(record.EventRef) + record.ResourceRef = strings.TrimSpace(record.ResourceRef) + record.ProjectionRef = strings.TrimSpace(record.ProjectionRef) + record.SourceArtifactRef = strings.TrimSpace(record.SourceArtifactRef) + record.TargetKind = strings.TrimSpace(record.TargetKind) + record.TargetID = strings.TrimSpace(record.TargetID) + record.Status = strings.TrimSpace(record.Status) + return record +} + +func validateProjectionLedgerRecord(record ProjectionLedgerRecord) error { + if strings.TrimSpace(record.EventRef) == "" { + return fmt.Errorf("event_ref is required") + } + if record.SurfaceRole == "" { + return fmt.Errorf("surface_role is required") + } + if strings.TrimSpace(record.TargetKind) == "" || strings.TrimSpace(record.TargetID) == "" { + return fmt.Errorf("projection ledger target is required") + } + return nil +} + +func projectionLedgerRecordMatches(record ProjectionLedgerRecord, eventRef string, role SurfaceRole, target ProjectionLedgerTarget) bool { + return record.EventRef == strings.TrimSpace(eventRef) && + record.SurfaceRole == role && + record.TargetKind == strings.TrimSpace(target.Kind) && + record.TargetID == strings.TrimSpace(target.ID) +} + +func upsertProjectionLedgerRecord(records []ProjectionLedgerRecord, next ProjectionLedgerRecord) []ProjectionLedgerRecord { + for i, record := range records { + if projectionLedgerRecordMatches(record, next.EventRef, next.SurfaceRole, ProjectionLedgerTarget{Kind: next.TargetKind, ID: next.TargetID}) { + records[i] = next + return records + } + } + return append(records, next) +} diff --git a/harness/internal/surface/multica/surface.go b/harness/internal/surface/multica/surface.go new file mode 100644 index 00000000..b5fe64ef --- /dev/null +++ b/harness/internal/surface/multica/surface.go @@ -0,0 +1,357 @@ +package multica + +import ( + "fmt" + "strings" +) + +type SurfaceRole string + +const ( + SurfaceRoleDisplay SurfaceRole = "display" + SurfaceRoleActivate SurfaceRole = "activate" + SurfaceRoleInput SurfaceRole = "input" + SurfaceRoleDrift SurfaceRole = "drift" +) + +type SurfaceAction string + +const ( + SurfaceActionNone SurfaceAction = "no_action" + SurfaceActionDisplayOnly SurfaceAction = "display_only" + SurfaceActionActivationNeeded SurfaceAction = "activation_needed" +) + +const ( + MulticaMetadataEventRef = "mnemon.event_ref" + MulticaMetadataResourceRef = "mnemon.resource_ref" + MulticaMetadataProjectionRef = "mnemon.projection_ref" + MulticaMetadataSourceArtifactRef = "mnemon.source_artifact_ref" + MulticaMetadataSurfaceRole = "mnemon.surface_role" + MulticaMetadataNoAutoDispatch = "mnemon.no_auto_dispatch" + MulticaMetadataCaseID = "mnemon.case_id" + MulticaMetadataRound = "mnemon.round" + MulticaMetadataPoCID = "mnemon.poc_id" +) + +type SurfaceRefs struct { + EventRef string + ResourceRef string + ProjectionRef string + SourceArtifactRef string +} + +func (r SurfaceRefs) Metadata(role SurfaceRole) map[string]string { + meta := map[string]string{} + addSurfaceMetadata(meta, MulticaMetadataEventRef, r.EventRef) + addSurfaceMetadata(meta, MulticaMetadataResourceRef, r.ResourceRef) + addSurfaceMetadata(meta, MulticaMetadataProjectionRef, r.ProjectionRef) + addSurfaceMetadata(meta, MulticaMetadataSourceArtifactRef, r.SourceArtifactRef) + if role != "" { + meta[MulticaMetadataSurfaceRole] = string(role) + } + return meta +} + +func ParseSurfaceRefs(meta map[string]any) SurfaceRefs { + if meta == nil { + return SurfaceRefs{} + } + return SurfaceRefs{ + EventRef: surfaceAnyString(meta[MulticaMetadataEventRef]), + ResourceRef: surfaceAnyString(meta[MulticaMetadataResourceRef]), + ProjectionRef: surfaceAnyString(meta[MulticaMetadataProjectionRef]), + SourceArtifactRef: surfaceAnyString(meta[MulticaMetadataSourceArtifactRef]), + } +} + +func ParseListedSurfaceRefs(meta map[string]string) SurfaceRefs { + if meta == nil { + return SurfaceRefs{} + } + return SurfaceRefs{ + EventRef: strings.TrimSpace(meta[MulticaMetadataEventRef]), + ResourceRef: strings.TrimSpace(meta[MulticaMetadataResourceRef]), + ProjectionRef: strings.TrimSpace(meta[MulticaMetadataProjectionRef]), + SourceArtifactRef: strings.TrimSpace(meta[MulticaMetadataSourceArtifactRef]), + } +} + +type SurfaceClassificationInput struct { + EventRef string + ResourceRef string + VisibleSummary string + VisibleStatus string + DisplayRequested bool + ActivationRequested bool + LocalActionRequired bool + HasMulticaControlRef bool +} + +type SurfaceClassification struct { + Action SurfaceAction + Role SurfaceRole + Reason string +} + +func ClassifySurfaceAction(in SurfaceClassificationInput) SurfaceClassification { + if in.ActivationRequested || in.LocalActionRequired { + if strings.TrimSpace(in.EventRef) == "" { + return SurfaceClassification{Action: SurfaceActionNone, Reason: "activation requires event_ref"} + } + return SurfaceClassification{Action: SurfaceActionActivationNeeded, Role: SurfaceRoleActivate, Reason: "accepted event requires explicit activation"} + } + if in.DisplayRequested || + strings.TrimSpace(in.VisibleSummary) != "" || + strings.TrimSpace(in.VisibleStatus) != "" || + in.HasMulticaControlRef { + return SurfaceClassification{Action: SurfaceActionDisplayOnly, Role: SurfaceRoleDisplay, Reason: "accepted event has display surface value"} + } + return SurfaceClassification{Action: SurfaceActionNone, Reason: "no Multica surface action required"} +} + +type DisplayWritebackRequest struct { + IssueID string + Refs SurfaceRefs + Title string + StatusLabel string + Summary string + Result string + EvidenceRefs []string + ArtifactRefs []string + DesiredStatus string + AssigneeAgentID string + AssignedToProvider bool +} + +type DisplayWritebackPlan struct { + IssueID string + CommentBody string + Metadata map[string]string + Status string + SkippedStatusReason string + NoAutoDispatch bool + SourceArtifactLinked bool +} + +func BuildDisplayWritebackPlan(req DisplayWritebackRequest) (DisplayWritebackPlan, error) { + issueID := strings.TrimSpace(req.IssueID) + if issueID == "" { + return DisplayWritebackPlan{}, fmt.Errorf("Multica issue id is required") + } + if strings.TrimSpace(req.Refs.EventRef) == "" { + return DisplayWritebackPlan{}, fmt.Errorf("event_ref is required for display writeback") + } + meta := req.Refs.Metadata(SurfaceRoleDisplay) + meta[MulticaMetadataNoAutoDispatch] = "true" + plan := DisplayWritebackPlan{ + IssueID: issueID, + CommentBody: FormatDisplayWritebackComment(req), + Metadata: meta, + NoAutoDispatch: true, + SourceArtifactLinked: strings.TrimSpace(req.Refs.SourceArtifactRef) != "", + } + status := CanonicalIssueStatus(req.DesiredStatus) + if status == "" { + return plan, nil + } + if !DisplayStatusWriteAllowed(DisplayStatusWriteInput{ + Status: status, + AssigneeAgentID: req.AssigneeAgentID, + AssignedToProvider: req.AssignedToProvider, + }) { + plan.SkippedStatusReason = "display status would risk provider dispatch" + return plan, nil + } + plan.Status = status + return plan, nil +} + +type DisplayStatusWriteInput struct { + Status string + AssigneeAgentID string + AssignedToProvider bool +} + +func DisplayStatusWriteAllowed(in DisplayStatusWriteInput) bool { + status := CanonicalIssueStatus(in.Status) + if status == "" { + return false + } + if !in.AssignedToProvider && strings.TrimSpace(in.AssigneeAgentID) == "" { + return true + } + switch status { + case StatusBacklog, StatusDone, StatusCancelled: + return true + default: + return false + } +} + +func FormatDisplayWritebackComment(req DisplayWritebackRequest) string { + var b strings.Builder + title := strings.TrimSpace(req.Title) + if title == "" { + title = "进展" + } + b.WriteString("Mnemon 更新: ") + b.WriteString(title) + b.WriteString("\n\n") + writeDisplaySection(&b, "状态", req.StatusLabel) + writeDisplaySection(&b, "摘要", req.Summary) + writeDisplaySection(&b, "结果", req.Result) + writeDisplayList(&b, "证据", req.EvidenceRefs) + writeDisplayList(&b, "产物", req.ArtifactRefs) + writeDisplaySection(&b, "事件引用", req.Refs.EventRef) + return strings.TrimSpace(b.String()) +} + +type ActivationCarrierRequest struct { + IssueID string + EventRef string + ResourceRef string + CarrierTitle string + TargetAgentID string +} + +type ActivationCarrier struct { + IssueID string + Title string + Metadata map[string]string +} + +func BuildActivationCarrier(req ActivationCarrierRequest) (ActivationCarrier, error) { + issueID := strings.TrimSpace(req.IssueID) + eventRef := strings.TrimSpace(req.EventRef) + resourceRef := strings.TrimSpace(req.ResourceRef) + if issueID == "" { + return ActivationCarrier{}, fmt.Errorf("Multica issue id is required") + } + if eventRef == "" { + return ActivationCarrier{}, fmt.Errorf("event_ref is required for activation carrier") + } + if resourceRef == "" { + return ActivationCarrier{}, fmt.Errorf("resource_ref is required for activation carrier") + } + title := strings.TrimSpace(req.CarrierTitle) + if title == "" { + title = "Mnemon activation" + } + meta := SurfaceRefs{EventRef: eventRef, ResourceRef: resourceRef}.Metadata(SurfaceRoleActivate) + addSurfaceMetadata(meta, "mnemon.target_agent_id", req.TargetAgentID) + return ActivationCarrier{IssueID: issueID, Title: title, Metadata: meta}, nil +} + +type HumanInteractionKind string + +const ( + HumanInteractionComment HumanInteractionKind = "comment" + HumanInteractionStatusMove HumanInteractionKind = "status_move" + HumanInteractionManagedEdit HumanInteractionKind = "managed_edit" + HumanInteractionTag HumanInteractionKind = "tag" +) + +type HumanInteraction struct { + Kind HumanInteractionKind + IssueID string + CommentID string + EventRef string + Body string + Status string +} + +type InteractionDecision struct { + SurfaceRole SurfaceRole + ObservationType string + ExternalID string + RequestActivation bool + Drift bool + Reason string +} + +func ClassifyHumanInteraction(in HumanInteraction) InteractionDecision { + switch in.Kind { + case HumanInteractionComment: + return InteractionDecision{ + SurfaceRole: SurfaceRoleInput, + ObservationType: "multica.comment.observed", + ExternalID: "multica-comment-" + strings.TrimSpace(in.CommentID), + Reason: "human comment imports as observation", + } + case HumanInteractionStatusMove: + return InteractionDecision{ + SurfaceRole: SurfaceRoleInput, + ObservationType: "multica.status_request.observed", + ExternalID: "multica-status-" + strings.TrimSpace(in.IssueID) + "-" + CanonicalIssueStatus(in.Status), + Reason: "human status move is a request, not canonical state", + } + case HumanInteractionManagedEdit: + return InteractionDecision{SurfaceRole: SurfaceRoleDrift, Drift: true, Reason: "managed block was edited outside Mnemon writeback"} + case HumanInteractionTag: + if strings.TrimSpace(in.EventRef) != "" { + return InteractionDecision{SurfaceRole: SurfaceRoleActivate, RequestActivation: true, Reason: "tag carries event_ref"} + } + return InteractionDecision{SurfaceRole: SurfaceRoleInput, ObservationType: "multica.tag_input.observed", Reason: "tag without event_ref is external input"} + default: + return InteractionDecision{SurfaceRole: SurfaceRoleInput, ObservationType: "multica.input.observed", Reason: "unknown human interaction imports as observation"} + } +} + +func writeDisplaySection(b *strings.Builder, heading, value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + b.WriteString("## ") + b.WriteString(heading) + b.WriteString("\n\n") + b.WriteString(value) + b.WriteString("\n\n") +} + +func writeDisplayList(b *strings.Builder, heading string, values []string) { + cleaned := cleanSurfaceStrings(values) + if len(cleaned) == 0 { + return + } + b.WriteString("## ") + b.WriteString(heading) + b.WriteString("\n\n") + for _, value := range cleaned { + b.WriteString("- ") + b.WriteString(value) + b.WriteByte('\n') + } + b.WriteByte('\n') +} + +func cleanSurfaceStrings(values []string) []string { + var out []string + for _, value := range values { + value = strings.TrimSpace(value) + if value != "" { + out = append(out, value) + } + } + return out +} + +func addSurfaceMetadata(meta map[string]string, key, value string) { + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + if key != "" && value != "" { + meta[key] = value + } +} + +func surfaceAnyString(value any) string { + switch typed := value.(type) { + case string: + return strings.TrimSpace(typed) + case fmt.Stringer: + return strings.TrimSpace(typed.String()) + default: + return "" + } +} diff --git a/harness/internal/surface/multica/surface_r3_test.go b/harness/internal/surface/multica/surface_r3_test.go new file mode 100644 index 00000000..682697e2 --- /dev/null +++ b/harness/internal/surface/multica/surface_r3_test.go @@ -0,0 +1,187 @@ +package multica + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestClassifySurfaceActionSplitsNoDisplayAndActivation(t *testing.T) { + none := ClassifySurfaceAction(SurfaceClassificationInput{ + EventRef: "event-1", + ResourceRef: "assignment/asg-1", + }) + if none.Action != SurfaceActionNone { + t.Fatalf("no surface action = %s, want %s", none.Action, SurfaceActionNone) + } + + display := ClassifySurfaceAction(SurfaceClassificationInput{ + EventRef: "event-2", + ResourceRef: "progress_digest/prog-1", + VisibleSummary: "支付回调已恢复,等待复核。", + DisplayRequested: true, + }) + if display.Action != SurfaceActionDisplayOnly || display.Role != SurfaceRoleDisplay { + t.Fatalf("display classification = %+v", display) + } + + activation := ClassifySurfaceAction(SurfaceClassificationInput{ + EventRef: "event-3", + ResourceRef: "assignment/asg-2", + ActivationRequested: true, + }) + if activation.Action != SurfaceActionActivationNeeded || activation.Role != SurfaceRoleActivate { + t.Fatalf("activation classification = %+v", activation) + } + + invalidActivation := ClassifySurfaceAction(SurfaceClassificationInput{ActivationRequested: true}) + if invalidActivation.Action != SurfaceActionNone { + t.Fatalf("activation without event_ref must not create surface action: %+v", invalidActivation) + } +} + +func TestBuildDisplayWritebackPlanIsDisplaySafe(t *testing.T) { + plan, err := BuildDisplayWritebackPlan(DisplayWritebackRequest{ + IssueID: "issue-1", + Refs: SurfaceRefs{ + EventRef: "event-progress-1", + ResourceRef: "progress_digest/prog-1", + ProjectionRef: "projection-1", + SourceArtifactRef: "multica.comment:comment-1", + }, + Title: "进展", + StatusLabel: "result", + Summary: "已确认退款风控误杀来自昨日规则变更。", + EvidenceRefs: []string{"ctx:风险登记", "artifact:规则 diff"}, + DesiredStatus: StatusInProgress, + AssignedToProvider: true, + AssigneeAgentID: "agent-1", + }) + if err != nil { + t.Fatal(err) + } + if plan.Status != "" { + t.Fatalf("display lane must not move provider-assigned issue to active status: %+v", plan) + } + if plan.SkippedStatusReason == "" { + t.Fatalf("unsafe status skip reason missing: %+v", plan) + } + if plan.Metadata[MulticaMetadataSurfaceRole] != string(SurfaceRoleDisplay) || + plan.Metadata[MulticaMetadataNoAutoDispatch] != "true" || + plan.Metadata[MulticaMetadataEventRef] != "event-progress-1" { + t.Fatalf("display metadata mismatch: %+v", plan.Metadata) + } + if !strings.Contains(plan.CommentBody, "Mnemon 更新: 进展") || + !strings.Contains(plan.CommentBody, "已确认退款风控误杀") || + !strings.Contains(plan.CommentBody, "ctx:风险登记") { + t.Fatalf("comment body is not a Chinese structured OA update:\n%s", plan.CommentBody) + } +} + +func TestBuildDisplayWritebackPlanAllowsSafeDoneStatus(t *testing.T) { + plan, err := BuildDisplayWritebackPlan(DisplayWritebackRequest{ + IssueID: "issue-1", + Refs: SurfaceRefs{EventRef: "event-result-1", ResourceRef: "progress_digest/prog-2"}, + Title: "结果", + Summary: "支付回调失败已完成复核。", + DesiredStatus: StatusDone, + }) + if err != nil { + t.Fatal(err) + } + if plan.Status != StatusDone { + t.Fatalf("safe done status = %q, want %q", plan.Status, StatusDone) + } +} + +func TestBuildActivationCarrierRequiresEventRef(t *testing.T) { + if _, err := BuildActivationCarrier(ActivationCarrierRequest{ + IssueID: "issue-1", + ResourceRef: "assignment/asg-1", + }); err == nil { + t.Fatal("activation carrier without event_ref must fail") + } + + carrier, err := BuildActivationCarrier(ActivationCarrierRequest{ + IssueID: "issue-1", + EventRef: "event-assignment-1", + ResourceRef: "assignment/asg-1", + CarrierTitle: "继续处理支付回调排查", + }) + if err != nil { + t.Fatal(err) + } + if carrier.Metadata[MulticaMetadataSurfaceRole] != string(SurfaceRoleActivate) || + carrier.Metadata[MulticaMetadataEventRef] != "event-assignment-1" { + t.Fatalf("activation metadata mismatch: %+v", carrier.Metadata) + } +} + +func TestClassifyHumanInteractionKeepsCanonicalBoundary(t *testing.T) { + comment := ClassifyHumanInteraction(HumanInteraction{ + Kind: HumanInteractionComment, + IssueID: "issue-1", + CommentID: "comment-1", + Body: "客服侧已经承诺今晚 20:00 前给出补偿名单。", + }) + if comment.SurfaceRole != SurfaceRoleInput || + comment.ObservationType != "multica.comment.observed" || + comment.ExternalID != "multica-comment-comment-1" { + t.Fatalf("comment decision mismatch: %+v", comment) + } + + status := ClassifyHumanInteraction(HumanInteraction{ + Kind: HumanInteractionStatusMove, + IssueID: "issue-1", + Status: "审核中", + }) + if status.RequestActivation || status.Drift || status.ObservationType != "multica.status_request.observed" { + t.Fatalf("status move must be a request observation: %+v", status) + } + + edit := ClassifyHumanInteraction(HumanInteraction{Kind: HumanInteractionManagedEdit}) + if !edit.Drift || edit.SurfaceRole != SurfaceRoleDrift { + t.Fatalf("managed edit must become drift: %+v", edit) + } + + tag := ClassifyHumanInteraction(HumanInteraction{Kind: HumanInteractionTag, EventRef: "event-1"}) + if !tag.RequestActivation || tag.SurfaceRole != SurfaceRoleActivate { + t.Fatalf("tag with event_ref must request activation: %+v", tag) + } + + externalTag := ClassifyHumanInteraction(HumanInteraction{Kind: HumanInteractionTag}) + if externalTag.RequestActivation || externalTag.ObservationType != "multica.tag_input.observed" { + t.Fatalf("tag without event_ref must remain external input: %+v", externalTag) + } +} + +func TestProjectionLedgerDedupesByEventRoleAndTarget(t *testing.T) { + path := filepath.Join(t.TempDir(), "projection-ledger.jsonl") + ledger := NewFileProjectionLedger(path) + record := ProjectionLedgerRecord{ + EventRef: "event-1", + ResourceRef: "progress_digest/prog-1", + ProjectionRef: "projection-1", + SurfaceRole: SurfaceRoleDisplay, + TargetKind: "comment", + TargetID: "comment-1", + Status: "reserved", + } + if _, ok, err := ledger.Reserve(record); err != nil || !ok { + t.Fatalf("reserve failed ok=%v err=%v", ok, err) + } + if _, ok, err := NewFileProjectionLedger(path).Reserve(record); err != nil || ok { + t.Fatalf("duplicate reserve ok=%v err=%v", ok, err) + } + record.Status = "written" + if err := NewFileProjectionLedger(path).Record(record); err != nil { + t.Fatal(err) + } + found, ok, err := NewFileProjectionLedger(path).Find("event-1", SurfaceRoleDisplay, ProjectionLedgerTarget{Kind: "comment", ID: "comment-1"}) + if err != nil || !ok { + t.Fatalf("find failed ok=%v err=%v", ok, err) + } + if found.Status != "written" || found.ProjectionRef != "projection-1" { + t.Fatalf("ledger record mismatch: %+v", found) + } +} From a223034f20ee1916658e273f8facf525e5da489a Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:00:42 +0800 Subject: [PATCH 02/11] refactor: remove multica runtime hub mailbox path Drop the old runtime hub writer and mailbox/wake/projectImport path from mnemon-multica-runtime. Runtime issue handling now imports Multica surface input into mnemond without writing Multica metadata, comments, statuses, or managed wake side effects. Validation: go test ./harness/cmd/mnemon-multica-runtime ./harness/internal/surface/multica --- .../cmd/mnemon-multica-runtime/hub_writer.go | 572 ----- harness/cmd/mnemon-multica-runtime/main.go | 475 +---- .../cmd/mnemon-multica-runtime/main_test.go | 1863 ++--------------- 3 files changed, 139 insertions(+), 2771 deletions(-) delete mode 100644 harness/cmd/mnemon-multica-runtime/hub_writer.go diff --git a/harness/cmd/mnemon-multica-runtime/hub_writer.go b/harness/cmd/mnemon-multica-runtime/hub_writer.go deleted file mode 100644 index 8accffed..00000000 --- a/harness/cmd/mnemon-multica-runtime/hub_writer.go +++ /dev/null @@ -1,572 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "sort" - "strconv" - "strings" - "time" - - "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" - pview "github.com/mnemon-dev/mnemon/harness/internal/mnemond/presentation/view" - 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) { - if result == nil { - return - } - if !multicasurface.RuntimeHubWriteEnabled(s.Env) { - result.HubWriteStatus = "skipped" - return - } - if !strings.EqualFold(result.HubBackend, driver.MulticaHubBackend) || - (result.HubKind != driver.MulticaHubKindSession && result.HubKind != driver.MulticaHubKindAssignmentMailbox) { - result.HubWriteStatus = "skipped" - return - } - if client == nil { - result.HubWriteStatus = "failed" - result.HubWriteErr = fmt.Errorf("mnemond client is required") - return - } - proj, err := client.PullPresentationView(contract.ActorID(result.Principal), contract.Subscription{ - Actor: contract.ActorID(result.Principal), - Refs: []contract.ResourceRef{ - {Kind: "assignment", ID: "project"}, - {Kind: "progress_digest", ID: "project"}, - }, - }) - if err != nil { - result.HubWriteStatus = "failed" - result.HubWriteErr = fmt.Errorf("pull teamwork view for Multica hub write: %w", err) - return - } - ledger := driver.NewFileMulticaHubLedger(multicasurface.RuntimeMulticaHubLedgerPath(s.Env, s.CWD)) - if result.HubKind == driver.MulticaHubKindSession { - reg, ok, err := multicasurface.RuntimeMulticaRegistry(s.Env, s.CWD) - if err != nil { - result.HubWriteStatus = "failed" - result.HubWriteErr = err - return - } - if !ok { - result.HubWriteStatus = "failed" - result.HubWriteErr = fmt.Errorf("Multica registry is required for assignment routing") - return - } - if err := s.writeAssignmentMailboxes(ctx, cli, ledger, reg, proj, rootIssue, result); err != nil { - result.HubWriteStatus = "failed" - result.HubWriteErr = err - return - } - } - if err := s.writeProgressComments(ctx, cli, ledger, proj, result); err != nil { - result.HubWriteStatus = "failed" - result.HubWriteErr = err - return - } - switch { - case result.HubChildIssues > 0 && result.HubFeedbackComments > 0: - result.HubWriteStatus = "updated" - case result.HubChildIssues > 0: - result.HubWriteStatus = "created" - case result.HubFeedbackComments > 0: - result.HubWriteStatus = "commented" - default: - result.HubWriteStatus = "noop" - } -} - -func (s *runtimeRPCState) writeAssignmentMailboxes(ctx context.Context, cli driver.MulticaCLI, ledger *driver.FileMulticaHubLedger, reg driver.MulticaRegistry, proj pview.View, rootIssue driver.MulticaIssue, result *runtimeImportResult) error { - existingChildren, err := cli.ListIssueChildren(ctx, result.RootIssueID) - if err != nil { - return err - } - var projections []runtimeAssignmentProjection - for _, assignment := range multicasurface.RuntimeViewItems(proj, "assignment") { - item := multicasurface.RuntimeAssignmentViewItem(assignment) - if item.ID == "" || item.Assignee == "" { - continue - } - if !runtimeItemAfterRootIngest(item.IngestSeq, result) { - continue - } - if !runtimeAssignmentMatchesCurrentMulticaScope(item, result) { - continue - } - 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) - } - 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 := projection.Source - if _, ok, err := ledger.Find(driver.MulticaHubKindAssignmentMailbox, source); err != nil { - return err - } else if ok { - continue - } - if existing, ok, err := findExistingMulticaAssignmentIssueInChildren(ctx, cli, existingChildren, source); err != nil { - return err - } else if ok { - if err := ledger.Record(driver.MulticaHubLedgerRecord{ - Kind: driver.MulticaHubKindAssignmentMailbox, - Source: source, - Target: driver.MulticaHubLedgerTarget{ - RootIssueID: result.RootIssueID, - ChildIssueID: existing.ID, - Status: "existing", - }, - }); err != nil { - return err - } - continue - } - projections = append(projections, runtimeAssignmentProjection{ - Item: item, - Participant: participant, - Source: source, - Metadata: projection.Metadata, - RootIssue: rootIssue, - Result: result, - }) - } - created, err := s.projectAssignmentMailboxes(ctx, cli, ledger, projections) - result.HubChildIssues += created - if err != nil { - return err - } - return nil -} - -type runtimeAssignmentProjection struct { - Item multicasurface.RuntimeAssignmentItem - Participant driver.MulticaParticipantRecord - Source driver.MulticaHubLedgerSource - Metadata driver.MulticaHubMetadata - RootIssue driver.MulticaIssue - Result *runtimeImportResult -} - -func (s *runtimeRPCState) projectAssignmentMailboxes(ctx context.Context, cli driver.MulticaCLI, ledger *driver.FileMulticaHubLedger, projections []runtimeAssignmentProjection) (int, error) { - if len(projections) == 0 { - return 0, nil - } - created := 0 - var errs []error - for _, projection := range projections { - material := assignmentMailboxMaterial(projection.Item, projection.Result, projection.RootIssue, projection.Participant) - reservation := driver.MulticaHubLedgerRecord{ - Kind: driver.MulticaHubKindAssignmentMailbox, - Source: projection.Source, - Target: driver.MulticaHubLedgerTarget{ - RootIssueID: projection.Result.RootIssueID, - Status: "reserved", - }, - } - if _, reserved, err := ledger.Reserve(reservation); err != nil { - return created, err - } else if !reserved { - continue - } - 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 - } - 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++ - 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 := 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 - } - } - 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) - } - 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)) - } - } - 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 - } - } - return zero, err -} - -func (s *runtimeRPCState) writeProgressComments(ctx context.Context, cli driver.MulticaCLI, ledger *driver.FileMulticaHubLedger, proj pview.View, result *runtimeImportResult) error { - for _, progress := range multicasurface.RuntimeViewItems(proj, "progress_digest") { - item := multicasurface.RuntimeProgressViewItem(progress) - if item.ID == "" || item.AssignmentRef == "" { - continue - } - progressProjection := multicasurface.ProgressFeedbackProjectionForRuntimeItem(multicasurface.ProgressFeedbackProjectionMaterial{ - Item: item, - SessionID: result.SessionID, - CorrelationID: result.CorrelationID, - }) - rec, recorded, err := ledger.Find(driver.MulticaHubKindFeedbackCarrier, progressProjection.Source) - if err != nil { - return err - } - child := strings.TrimSpace(rec.Target.ChildIssueID) - if child == "" { - var found bool - 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, item.Actor) - if err != nil { - return err - } - } - if !found { - continue - } - } - if !runtimeProgressMatchesCurrentMulticaScope(item, result, child) { - continue - } - ok, err := runtimeProgressAfterAssignmentMailbox(ledger, result.SessionID, item.AssignmentRef, item.Actor, child, item.IngestSeq) - if err != nil { - return err - } - if !ok { - continue - } - if recorded { - if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, progressProjection.Feedback); err != nil { - return err - } - continue - } - reservation := driver.MulticaHubLedgerRecord{ - Kind: driver.MulticaHubKindFeedbackCarrier, - Source: progressProjection.Source, - Target: driver.MulticaHubLedgerTarget{ - RootIssueID: result.RootIssueID, - ChildIssueID: child, - Status: "reserved", - }, - } - if _, reserved, err := ledger.Reserve(reservation); err != nil { - return err - } else if !reserved { - if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, progressProjection.Feedback); err != nil { - return err - } - continue - } - comment, err := cli.AddIssueComment(ctx, child, progressProjection.CommentBody) - if err != nil { - return err - } - if err := s.ensureProgressIssueStatuses(ctx, cli, result, child, progressProjection.Feedback); err != nil { - return err - } - if err := ledger.Record(driver.MulticaHubLedgerRecord{ - Kind: driver.MulticaHubKindFeedbackCarrier, - Source: progressProjection.Source, - Target: driver.MulticaHubLedgerTarget{ - RootIssueID: result.RootIssueID, - ChildIssueID: child, - CommentID: comment.ID, - Status: "commented", - }, - }); err != nil { - return err - } - result.HubFeedbackComments++ - } - return nil -} - -func runtimeProgressAfterAssignmentMailbox(ledger *driver.FileMulticaHubLedger, sessionID, assignmentID, principal, childIssueID string, progressSeq int64) (bool, error) { - if progressSeq <= 0 { - return true, nil - } - records, err := ledger.Records() - if err != nil { - return false, err - } - sessionID = strings.TrimSpace(sessionID) - assignmentID = strings.TrimSpace(assignmentID) - principal = strings.TrimSpace(principal) - childIssueID = strings.TrimSpace(childIssueID) - for i := len(records) - 1; i >= 0; i-- { - record := records[i] - if record.Kind != driver.MulticaHubKindAssignmentMailbox || - strings.TrimSpace(record.Source.SessionID) != sessionID || - strings.TrimSpace(record.Source.AssignmentID) != assignmentID || - strings.TrimSpace(record.Source.Principal) != principal || - strings.TrimSpace(record.Target.ChildIssueID) != childIssueID { - continue - } - assignmentSeq := record.Source.IngestSeq - if assignmentSeq <= 0 { - assignmentSeq = multicaLocalEventSeq(record.Source.EventID) - } - if assignmentSeq <= 0 { - return true, nil - } - return progressSeq > assignmentSeq, nil - } - return true, nil -} - -func multicaLocalEventSeq(eventID string) int64 { - eventID = strings.TrimSpace(eventID) - if eventID == "" { - return 0 - } - parts := strings.Split(eventID, "/") - if len(parts) == 0 { - return 0 - } - seq, _ := strconv.ParseInt(parts[len(parts)-1], 10, 64) - return seq -} - -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 -} - -func runtimeAssignmentMatchesCurrentMulticaScope(item multicasurface.RuntimeAssignmentItem, result *runtimeImportResult) bool { - return multicasurface.RuntimeAssignmentMatchesScope(item, runtimeMulticaScopeMaterial(result)) -} - -func runtimeProgressMatchesCurrentMulticaScope(item multicasurface.RuntimeProgressItem, result *runtimeImportResult, childIssueID string) bool { - return multicasurface.RuntimeProgressMatchesScope(item, runtimeMulticaScopeMaterial(result), childIssueID) -} - -func runtimeMulticaScopeMaterial(result *runtimeImportResult) multicasurface.RuntimeScopeMaterial { - if result == nil { - return multicasurface.RuntimeScopeMaterial{} - } - return multicasurface.RuntimeScopeMaterial{ - SessionID: result.SessionID, - RootIssueID: result.RootIssueID, - CorrelationID: result.CorrelationID, - TaskID: result.TaskID, - } -} - -func runtimeItemAfterRootIngest(ingestSeq int64, result *runtimeImportResult) bool { - if result == nil || result.Receipt == nil || result.Receipt.Seq <= 0 || ingestSeq <= 0 { - return true - } - return multicasurface.RuntimeItemAfterRootIngest(ingestSeq, result.Receipt.Seq) -} - -func assignmentMailboxMaterial(item multicasurface.RuntimeAssignmentItem, result *runtimeImportResult, rootIssue driver.MulticaIssue, participant driver.MulticaParticipantRecord) multicasurface.AssignmentMailboxMaterial { - 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.FallbackRootIssueID = result.RootIssueID - } - return multicasurface.AssignmentMailboxMaterialForRuntimeItem(material) -} - -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 { - return driver.MulticaIssue{}, false, err - } - return findExistingMulticaAssignmentIssueInChildren(ctx, cli, children, source) -} - -func findExistingMulticaAssignmentIssueInChildren(ctx context.Context, cli driver.MulticaCLI, children []driver.MulticaIssue, source driver.MulticaHubLedgerSource) (driver.MulticaIssue, bool, error) { - for _, child := range children { - meta, err := cli.ResolveIssueHubMetadata(ctx, child) - if err != nil { - return driver.MulticaIssue{}, false, err - } - if !meta.IsAssignmentMailbox() { - continue - } - if multicasurface.AssignmentMailboxMatchesSource(meta, source) { - return child, true, nil - } - } - return driver.MulticaIssue{}, false, nil -} - -func findAssignmentTargetFromLedger(ledger *driver.FileMulticaHubLedger, sessionID, assignmentID, principal string) (string, bool, error) { - records, err := ledger.Records() - if err != nil { - return "", false, err - } - 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) { - children, err := cli.ListIssueChildren(ctx, rootIssueID) - if err != nil { - return "", false, err - } - candidates := []multicasurface.AssignmentTargetCandidate{} - for _, child := range children { - meta, err := cli.ResolveIssueHubMetadata(ctx, child) - if err != nil { - return "", false, err - } - if !meta.IsAssignmentMailbox() { - continue - } - if candidate, ok := multicasurface.AssignmentTargetCandidateFromMailboxMetadata(child.ID, meta); ok { - candidates = append(candidates, candidate) - } - } - 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) { - children, err := cli.ListIssueChildren(ctx, rootIssueID) - if err != nil { - return false, err - } - seen := false - for _, child := range children { - meta, err := cli.ResolveIssueHubMetadata(ctx, child) - if err != nil { - return false, err - } - if !meta.IsAssignmentMailbox() || meta.SessionID != sessionID { - continue - } - seen = true - if child.ID == justCompletedChildID { - continue - } - if multicasurface.IssueStatusDone(child.Status) { - continue - } - return false, nil - } - return seen, nil -} diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 3bb8f078..d356eb53 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -14,12 +14,8 @@ 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" - "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" ) @@ -62,38 +58,18 @@ type runtimeProgressEvent struct { Output string ExitCode int DurationMs int64 - Trace *driver.ManagedTurnTraceEvent } type runtimeImportResult struct { - IssueID string - Identifier string - Title string - Statement string - Principal string - TaskID string - HubMetadata driver.MulticaHubMetadata - HubBackend string - HubKind string - SessionID string - CorrelationID string - RootIssueID string - AssignmentID string - AssignmentFingerprint string - MatchTerms []string - Status string - Receipt *access.IngestReceipt - ProjectionStatus string - ProjectionCommentID string - ProjectionErr error - WakeStatus string - WakeTurnID string - WakeErr error - HubWriteStatus string - HubChildIssues int - HubFeedbackComments int - HubWriteErr error - Err error + IssueID string + Identifier string + Title string + Statement string + Principal string + TaskID string + Status string + Receipt *access.IngestReceipt + Err error } func main() { @@ -254,10 +230,6 @@ func (s *runtimeRPCState) handle(msg rpcMessage, emit func(rpcMessage) error) er if progressErr != nil { return } - if event.Trace != nil { - progressErr = emitRuntimeMessages(emit, multicasurface.RuntimeManagedTraceMessages(s.ThreadID, s.TurnID, *event.Trace, s.now())) - return - } if strings.TrimSpace(event.Command) != "" { progressErr = emitRuntimeMessages(emit, multicasurface.RuntimeCommandExecutionMessages(s.ThreadID, s.TurnID, s.nextItemID("call"), s.CWD, multicasurface.RuntimeCommandExecutionMaterial{ Command: event.Command, @@ -344,14 +316,7 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres result.Identifier = issue.Identifier result.Title = issue.Title result.Statement = issue.Description - result.HubMetadata = driver.MulticaIssueHubMetadata(issue) - applyMulticaHubMetadata(&result, result.HubMetadata) - 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, activation, progress) - } + emitRuntimeProgress(progress, "Loaded "+runtimeIssueLabel(issue)+"; importing Multica surface input.") externalID := "" if taskID != "" { externalID = "multica-task-" + taskID @@ -382,32 +347,6 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres result.Err = err return result } - if strings.EqualFold(result.HubBackend, driver.MulticaHubBackend) { - 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) - addPayloadRuleString(draft.Payload, "source_issue_id", issue.ID) - 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.") - } 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)+".") - } - } addr := strings.TrimSpace(activation.ControlAddr) if addr == "" { result.Status = "skipped" @@ -441,17 +380,7 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres result.Receipt = &rec emitRuntimeCommand(progress, "mnemond ingest observe --principal "+result.Principal, fmt.Sprintf("recorded seq=%d duplicate=%v ticked=%v", rec.Seq, rec.Dup, rec.Ticked), 0) 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]", 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-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)) - emitRuntimeProgress(progress, multicasurface.RuntimeProjectionProgress(runtimeResultSummary(result))) + emitRuntimeProgress(progress, "Multica surface input was imported; accepted-state writeback is handled only by explicit Mnemon report commands.") return result } @@ -469,329 +398,6 @@ 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, activation multicasurface.RuntimeContext, progress runtimeProgressSink) runtimeImportResult { - result.HubMetadata = multicasurface.AssignmentMailboxHubMetadata(result.HubMetadata, issue.ID) - applyMulticaHubMetadata(result, result.HubMetadata) - result.Status = "correlated" - result.MatchTerms = drive.CleanManagedWakeMatchTerms( - result.AssignmentID, - result.AssignmentFingerprint, - result.HubMetadata.EventID, - string(eventmodel.Subject("assignment", result.AssignmentID)), - ) - correlationProgress := multicasurface.RuntimeAssignmentCorrelationProgress() - emitRuntimeCommand(progress, "mnemon-multica-runtime assignment-correlate --issue "+issue.ID, correlationProgress, 0) - emitRuntimeProgress(progress, correlationProgress) - addr := strings.TrimSpace(activation.ControlAddr) - var client *access.Client - if addr == "" { - result.WakeStatus = "skipped" - result.WakeErr = fmt.Errorf("MNEMON_CONTROL_ADDR is not set") - emitRuntimeProgress(progress, "Local Mnemon control address is not configured; skipping managed wake.") - } else { - var err error - client, err = runtimeControlClient(s.Env, addr, result.Principal) - if err != nil { - result.WakeStatus = "failed" - result.WakeErr = err - emitRuntimeProgress(progress, "Failed to create Local Mnemon control client.") - } else { - emitRuntimeProgress(progress, "Waking assigned local agent with [mnemon:wake].") - s.wakeManagedAgent(result, progress) - 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-runtime hub-write --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, multicasurface.RuntimeProjectionProgress(runtimeResultSummary(*result)), runtimeExitCode(result.ProjectionErr)) - emitRuntimeProgress(progress, multicasurface.RuntimeProjectionProgress(runtimeResultSummary(*result))) - return *result -} - -func applyMulticaHubMetadata(result *runtimeImportResult, meta driver.MulticaHubMetadata) { - if result == nil { - return - } - result.HubBackend = firstNonEmpty(meta.HubBackend, result.HubBackend) - result.HubKind = firstNonEmpty(meta.Kind, result.HubKind) - result.SessionID = firstNonEmpty(meta.SessionID, result.SessionID) - result.CorrelationID = firstNonEmpty(meta.CorrelationID, result.CorrelationID) - result.RootIssueID = firstNonEmpty(meta.RootIssueID, result.RootIssueID) - result.AssignmentID = firstNonEmpty(meta.AssignmentID, result.AssignmentID) - result.AssignmentFingerprint = firstNonEmpty(meta.AssignmentFingerprint, result.AssignmentFingerprint) -} - -func addPayloadRuleString(payload map[string]any, key, value string) { - key = strings.TrimSpace(key) - value = strings.TrimSpace(value) - if payload == nil || key == "" || value == "" { - return - } - rule, _ := payload[eventmodel.PayloadRuleKey].(map[string]any) - if rule == nil { - rule = map[string]any{} - payload[eventmodel.PayloadRuleKey] = rule - } - rule[key] = value -} - -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 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 - } - 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(multicasurface.RuntimeEnvValue(s.Env, "MNEMON_CONTROL_ADDR")) - if addr == "" { - result.WakeStatus = "skipped" - result.WakeErr = fmt.Errorf("MNEMON_CONTROL_ADDR is not set") - return - } - token, err := runtimeControlToken(s.Env) - if err != nil { - result.WakeStatus = "failed" - result.WakeErr = err - return - } - renderCtx, cancel := context.WithTimeout(context.Background(), multicasurface.RuntimeTimeout(s.Env)) - defer cancel() - resp, err := (driver.HTTPRenderClient{ - BaseURL: addr, - Token: token, - Principal: contract.ActorID(result.Principal), - }).Render(renderCtx, presentation.Request{ - SchemaVersion: 1, - Principal: contract.ActorID(result.Principal), - Host: "multica", - Lifecycle: multicasurface.RuntimeEnvDefault(s.Env, "MNEMON_MANAGED_RENDER_LIFECYCLE", "remind"), - Surface: "runtime", - RenderIntent: presentation.IntentTeamworkEvents, - SessionID: result.SessionID, - InputDigest: multicasurface.RuntimeManagedWakeScopeID(runtimeManagedWakeMaterial(*result)), - }) - if err != nil { - result.WakeStatus = "failed" - result.WakeErr = fmt.Errorf("render managed wake candidates: %w", err) - return - } - 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") - return - } - managedEnv := multicasurface.RuntimeManagedTurnEnv(s.Env, runtimeManagedWakeMaterial(*result)) - client, workspace, err := runtimeManagedTurnClient(managedEnv, s.CWD, runtimeName) - if err != nil { - result.WakeStatus = "failed" - result.WakeErr = err - return - } - wakeCtx, cancel := context.WithTimeout(context.Background(), multicasurface.RuntimeManagedTurnTimeout(s.Env)) - defer cancel() - record, err := (&driver.ManagedAgentDriver{ - Principal: result.Principal, - Client: client, - Ledger: driver.NewFileManagedWakeLedger(multicasurface.RuntimeManagedLedgerPath(s.Env, workspace)), - TraceSink: driver.ManagedTurnTraceSinkFunc(func(event driver.ManagedTurnTraceEvent) { - if progress != nil { - progress(runtimeProgressEvent{Trace: &event}) - } - }), - Now: func() time.Time { return s.now() }, - }).Wake(wakeCtx, candidate) - result.WakeTurnID = record.TurnID - if err != nil { - result.WakeStatus = "failed" - result.WakeErr = err - return - } - result.WakeStatus = record.Status - if result.WakeStatus == "" { - result.WakeStatus = "completed" - } -} - -type runtimeHubProjectionDelta struct { - ChildIssues int - FeedbackComments int - Status string - Err error -} - -func (d runtimeHubProjectionDelta) active() bool { - 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 { - if result == nil || client == nil || - !strings.EqualFold(result.HubBackend, driver.MulticaHubBackend) || - !runtimeManagedWakeHubProjectionKind(result.HubKind) || - !multicasurface.RuntimeHubWriteEnabled(s.Env) { - s.wakeManagedAgent(result, progress) - return nil - } - done := make(chan struct{}) - deltas := make(chan runtimeHubProjectionDelta, 16) - go func(snapshot runtimeImportResult) { - defer close(deltas) - ticker := time.NewTicker(multicasurface.RuntimeHubProjectionInterval(s.Env)) - defer ticker.Stop() - for { - select { - case <-done: - return - case <-ticker.C: - delta := s.writeMulticaHubArtifactsDelta(ctx, cli, client, rootIssue, snapshot) - if delta.active() { - select { - case deltas <- delta: - default: - } - } - } - } - }(*result) - s.wakeManagedAgent(result, progress) - close(done) - var out []runtimeHubProjectionDelta - for delta := range deltas { - out = append(out, delta) - } - return out -} - -func runtimeManagedWakeHubProjectionKind(kind string) bool { - switch strings.TrimSpace(kind) { - case driver.MulticaHubKindSession, driver.MulticaHubKindAssignmentMailbox: - return true - default: - return false - } -} - -func (s *runtimeRPCState) writeMulticaHubArtifactsDelta(ctx context.Context, cli driver.MulticaCLI, client *access.Client, rootIssue driver.MulticaIssue, base runtimeImportResult) runtimeHubProjectionDelta { - snapshot := base - snapshot.HubChildIssues = 0 - snapshot.HubFeedbackComments = 0 - snapshot.HubWriteStatus = "" - snapshot.HubWriteErr = nil - s.writeMulticaHubArtifacts(ctx, cli, client, rootIssue, &snapshot) - return runtimeHubProjectionDelta{ - ChildIssues: snapshot.HubChildIssues, - FeedbackComments: snapshot.HubFeedbackComments, - Status: snapshot.HubWriteStatus, - Err: snapshot.HubWriteErr, - } -} - -func mergeRuntimeHubProjectionDeltas(result *runtimeImportResult, deltas []runtimeHubProjectionDelta) { - if result == nil || len(deltas) == 0 { - return - } - for _, delta := range deltas { - result.HubChildIssues += delta.ChildIssues - result.HubFeedbackComments += delta.FeedbackComments - } - if result.HubWriteErr != nil { - return - } - switch { - case result.HubChildIssues > 0 && result.HubFeedbackComments > 0: - result.HubWriteStatus = "updated" - case result.HubChildIssues > 0: - result.HubWriteStatus = "created" - case result.HubFeedbackComments > 0: - result.HubWriteStatus = "commented" - } -} - -func (s *runtimeRPCState) projectImportComment(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, externalID, eventType string, result *runtimeImportResult) { - if result == nil { - return - } - if !multicasurface.RuntimeProjectionCommentsEnabled(s.Env) { - result.ProjectionStatus = "skipped" - return - } - title := "issue admitted" - if result.HubKind == driver.MulticaHubKindAssignmentMailbox || result.Status == "correlated" { - title = "assignment mailbox correlated" - } - commentBody := projection.FormatComment(projection.CommentMaterial{ - 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 { - result.ProjectionStatus = "failed" - result.ProjectionErr = err - return - } - result.ProjectionStatus = "commented" - result.ProjectionCommentID = comment.ID -} - -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, activation multicasurface.RuntimeContext) driver.MulticaCLI { return driver.MulticaCLI{ Command: multicasurface.RuntimeEnvValue(env, "MNEMON_MULTICA_BIN"), @@ -829,37 +435,6 @@ func runtimeControlToken(env []string) (string, error) { return token, nil } -func runtimeManagedTurnClient(env []string, cwd, runtimeName string) (driver.ManagedTurnClient, string, error) { - workspace := multicasurface.RuntimeEnvDefault(env, "MNEMON_MANAGED_WORKSPACE", cwd) - if strings.TrimSpace(workspace) == "" { - workspace = "." - } - switch strings.TrimSpace(runtimeName) { - case "noop": - return runtimeNoopTurnClient{}, workspace, nil - case "codex-appserver": - return driver.CodexAppServerTurnClient{ - Principal: resolveRuntimePrincipal(env, cwd), - Command: multicasurface.RuntimeEnvDefault(env, "MNEMON_MANAGED_COMMAND", "codex"), - Workspace: workspace, - Env: append([]string(nil), env...), - TurnTimeout: multicasurface.RuntimeManagedTurnTimeout(env), - ClientName: multicasurface.MulticaRuntimeCommandName, - }, workspace, nil - default: - return nil, workspace, fmt.Errorf("unsupported MNEMON_MANAGED_RUNTIME %q", runtimeName) - } -} - -type runtimeNoopTurnClient struct{} - -func (runtimeNoopTurnClient) StartTurn(_ context.Context, query string) (driver.ManagedTurnResult, error) { - if strings.TrimSpace(query) != driver.ManagedWakeQuery { - return driver.ManagedTurnResult{}, fmt.Errorf("unexpected managed wake query %q", query) - } - return driver.ManagedTurnResult{TurnID: "noop-turn", Status: "completed"}, nil -} - func resolveRuntimePrincipal(env []string, cwd string) string { return resolveRuntimePrincipalFromContext(env, cwd, multicasurface.RuntimeContextFromActivation(env, cwd, multicasurface.RuntimeInput{})) } @@ -881,31 +456,15 @@ func resolveRuntimePrincipalFromContext(env []string, cwd string, activation mul 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, + IssueID: result.IssueID, + Identifier: result.Identifier, + Title: result.Title, + Principal: result.Principal, + Status: result.Status, } 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() - } - if result.HubWriteErr != nil { - summary.HubWriteErr = result.HubWriteErr.Error() - } return summary } @@ -1047,7 +606,7 @@ func writeRuntimeHelp(w io.Writer) { 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.") + fmt.Fprintln(w, "The adapter receives Multica runtime RPC on stdin/stdout, loads issue context through the Multica CLI, and imports Multica surface input into mnemond when configured. Accepted-state Multica writeback is handled only by explicit Mnemon report commands or activation carriers.") } func runtimeProbeModeEnabled(args []string) bool { diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index dbc9fef8..746269f0 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -1,8 +1,6 @@ package main import ( - "bytes" - "context" "encoding/json" "net/http" "net/http/httptest" @@ -13,1793 +11,176 @@ 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" - "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 TestMergeRuntimeHubProjectionDeltasPreservesEarlyDispatchCounts(t *testing.T) { - result := runtimeImportResult{HubWriteStatus: "noop"} - mergeRuntimeHubProjectionDeltas(&result, []runtimeHubProjectionDelta{ - {ChildIssues: 2}, - {FeedbackComments: 1}, - }) - if result.HubChildIssues != 2 || result.HubFeedbackComments != 1 || result.HubWriteStatus != "updated" { - t.Fatalf("merged projection result = %+v", result) - } - failed := runtimeImportResult{HubWriteStatus: "failed", HubWriteErr: os.ErrInvalid} - mergeRuntimeHubProjectionDeltas(&failed, []runtimeHubProjectionDelta{{ChildIssues: 1}}) - if failed.HubWriteStatus != "failed" || failed.HubChildIssues != 1 { - t.Fatalf("failed projection merge should preserve final failure while carrying counts: %+v", failed) - } -} - -func TestRuntimeItemAfterRootIngestFiltersHistoricalAssignments(t *testing.T) { - receipt := &access.IngestReceipt{Seq: 50} - if runtimeItemAfterRootIngest(49, &runtimeImportResult{Receipt: receipt, HubKind: driver.MulticaHubKindSession}) { - t.Fatal("session hub write should ignore assignments older than the root ingest receipt") - } - if !runtimeItemAfterRootIngest(51, &runtimeImportResult{Receipt: receipt, HubKind: driver.MulticaHubKindSession}) { - t.Fatal("session hub write should project assignments after the root ingest receipt") - } -} - -func TestRuntimeManagedWakeHubProjectionIncludesAssignmentMailboxes(t *testing.T) { - for _, kind := range []string{driver.MulticaHubKindSession, driver.MulticaHubKindAssignmentMailbox} { - if !runtimeManagedWakeHubProjectionKind(kind) { - t.Fatalf("hub projection should run during managed wake for %s", kind) - } - } - if runtimeManagedWakeHubProjectionKind(driver.MulticaHubKindFeedbackCarrier) { - t.Fatal("feedback carriers are projected artifacts, not managed wake issue kinds") - } -} - -func TestProjectAssignmentMailboxesSkipsExistingReservation(t *testing.T) { - ledger := driver.NewFileMulticaHubLedger(filepath.Join(t.TempDir(), "hub-ledger.jsonl")) - source := driver.MulticaHubLedgerSource{ - SessionID: "session-1", - CorrelationID: "corr-1", - EventID: "event-1", - AssignmentID: "assignment-1", - AssignmentFingerprint: "sha256:abc", - Principal: "worker@team", - ProjectionKind: "assignment", - } - if _, ok, err := ledger.Reserve(driver.MulticaHubLedgerRecord{ - Kind: driver.MulticaHubKindAssignmentMailbox, - Source: source, - Target: driver.MulticaHubLedgerTarget{ - RootIssueID: "root-1", - Status: "reserved", - }, - }); err != nil || !ok { - t.Fatalf("reserve: ok=%v err=%v", ok, err) - } - created, err := (&runtimeRPCState{}).projectAssignmentMailboxes(context.Background(), driver.MulticaCLI{ - Command: filepath.Join(t.TempDir(), "missing-multica"), - }, ledger, []runtimeAssignmentProjection{{ - Item: multicasurface.RuntimeAssignmentItem{ - ID: "assignment-1", - Assignee: "worker@team", - ExpectedWork: "check routing", - ExpectedFeedback: "progress digest", - }, - Participant: driver.MulticaParticipantRecord{ - Principal: "worker@team", - AgentID: "agent-1", - AgentName: "worker", - }, - Source: source, - Metadata: multicasurface.MulticaHubMetadata{ - Kind: driver.MulticaHubKindAssignmentMailbox, - }, - RootIssue: driver.MulticaIssue{ID: "root-1", Identifier: "TEA-1", Title: "Root"}, - Result: &runtimeImportResult{ - RootIssueID: "root-1", - SessionID: "session-1", - Principal: "planner@team", - }, - }}) - if err != nil { - t.Fatal(err) - } - if created != 0 { - t.Fatalf("created = %d, want 0", created) - } -} - -func TestManagedWakeMatchTermsPreferStableIssueIdentity(t *testing.T) { - 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) { - 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 runtimeTestEnv(values ...string) []string { - env := make([]string, 0, len(os.Environ())+len(values)) - for _, item := range os.Environ() { - key, _, ok := strings.Cut(item, "=") - if !ok { - continue - } - if strings.HasPrefix(key, "MNEMON_") || strings.HasPrefix(key, "MULTICA_") { - continue - } - env = append(env, item) - } - 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") - 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) - } +func TestRuntimeSkipsWithoutIssueID(t *testing.T) { + result := (&runtimeRPCState{Env: nil, CWD: t.TempDir(), Now: fixedRuntimeTime}).importIssue(multicasurface.RuntimeInput{}, nil) if result.Status != "skipped" { - t.Fatalf("runtime without control addr should skip ingest after loading issue, got %+v", result) + t.Fatalf("status = %q, want skipped", result.Status) } - 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) + if result.Err == nil || !strings.Contains(result.Err.Error(), "no Multica issue id") { + t.Fatalf("unexpected error: %v", result.Err) } } -func TestRuntimeImportsAssignedIssueIntoMnemon(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 iss-7"*) printf '{"id":"iss-7","identifier":"TEA-7","title":"Prepare teamwork acceptance","description":"Validate runtime issue import through real Multica task context.","status":"todo","priority":"medium"}\n' ;; - *"issue comment add iss-7"*) cat > "$MULTICA_COMMENT_PATH"; printf '{"id":"comment-1","issue_id":"iss-7","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", - 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) - } - - 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: 17, Dup: false, Ticked: true}) - case "/render": - if r.Header.Get(access.PrincipalHeader) != "planner@team" { - t.Fatalf("render principal header = %q", r.Header.Get(access.PrincipalHeader)) - } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(presentation.Response{ - SchemaVersion: 1, - Status: presentation.StatusOK, - AuditID: "render-audit-1", - BodyDigest: "sha256:render-body", - Events: []eventmodel.EventEnvelope{{ - SchemaVersion: eventmodel.SchemaVersion, - Phase: eventmodel.PhaseDerived, - Event: eventmodel.Event{ - SchemaVersion: eventmodel.SchemaVersion, - ID: "derived-stale", - Type: "assignment.brief.derived", - Subject: "assignment/asg-stale", - Actor: "mnemond", - Audience: "planner@team", - Payload: eventmodel.BuildPayload(nil, map[string]any{ - "body": "assignment for an older issue", - }, nil), - }, - Meta: map[string]any{"presentation_hint": "work"}, - }, { - SchemaVersion: eventmodel.SchemaVersion, - Phase: eventmodel.PhaseDerived, - Event: eventmodel.Event{ - SchemaVersion: eventmodel.SchemaVersion, - ID: "derived-1", - Type: "assignment.brief.derived", - Subject: "assignment/asg-1", - Actor: "mnemond", - Audience: "planner@team", - Payload: eventmodel.BuildPayload(nil, map[string]any{ - "body": "assignment for iss-7", - }, nil), - }, - Meta: map[string]any{"presentation_hint": "work"}, - }}, - }) - default: - t.Fatalf("unexpected path %s", r.URL.Path) - } - })) - defer srv.Close() - - input := strings.Join([]string{ - `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`, - `{"jsonrpc":"2.0","method":"initialized"}`, - `{"jsonrpc":"2.0","id":2,"method":"thread/start","params":{"cwd":"` + tmp + `","ephemeral":true}}`, - `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"threadId":"thread-1","input":[{"type":"text","text":"Your assigned issue ID is: iss-7\nPlease work on it."}]}}`, - }, "\n") + "\n" +func TestRuntimeImportsMulticaIssueWithoutHubWritebackOrWake(t *testing.T) { + logPath := filepath.Join(t.TempDir(), "multica-args.log") + multicaBin := writeFakeMulticaCLI(t, logPath, fakeMulticaScript{ + IssueJSON: `{"id":"issue-1","identifier":"TEA-1","title":"中文验收-退款规则澄清","description":"请澄清退款规则。","status":"todo","priority":"medium"}`, + MetadataJSON: `[]`, + }) + server, received := fakeIngestServer(t) + defer server.Close() - var out bytes.Buffer - err := runRuntime(runtimeConfig{ - Args: []string{"app-server", "--listen", "stdio://"}, - Env: runtimeTestEnv( - "MNEMON_MULTICA_BIN="+bin, - "MNEMON_MULTICA_REGISTRY="+registryPath, - "MNEMON_CONTROL_ADDR="+srv.URL, - "MNEMON_CONTROL_PRINCIPAL=wrong@team", - "MNEMON_MANAGED_RUNTIME=noop", - "MNEMON_MANAGED_LEDGER="+filepath.Join(tmp, "wake-ledger.jsonl"), - "MULTICA_ARGS_PATH="+argsPath, - "MULTICA_COMMENT_PATH="+commentPath, - "MULTICA_WORKSPACE_ID=ws-1", + result := (&runtimeRPCState{ + Env: []string{ + "MULTICA_ISSUE_ID=issue-1", "MULTICA_TASK_ID=task-1", "MULTICA_AGENT_ID=agent-1", - "MULTICA_AGENT_NAME=mnemon-planner", - "MULTICA_SERVER_URL=https://desktop-api.multica.ai", - ), - 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.ExternalID != "multica-task-task-1" { - t.Fatalf("external id = %q", got.ExternalID) - } - 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["external_issue_id"] != "iss-7" || rule["external_task_id"] != "task-1" || rule["external_agent_id"] != "agent-1" || rule["external_workspace_id"] != "ws-1" || rule["principal"] != "planner@team" { - t.Fatalf("rule mismatch: %+v", rule) - } - narrative := got.Event.Payload["narrative"].(map[string]any) - if narrative["statement"] != "Validate runtime issue import through real Multica task context." { - t.Fatalf("narrative mismatch: %+v", narrative) - } - if _, ok := narrative["external_task_id"]; ok { - t.Fatalf("narrative leaked rule ids: %+v", narrative) - } - if got.Event.CorrelationID != "multica:issue:iss-7" { - t.Fatalf("correlation id = %q", got.Event.CorrelationID) - } - - args, err := os.ReadFile(argsPath) - if err != nil { - t.Fatal(err) - } - if strings.Contains(string(args), "Validate runtime issue import") { - t.Fatalf("issue narrative leaked into CLI arguments: %q", string(args)) - } - if !strings.Contains(string(args), "issue comment add iss-7 --content-stdin --output json") { - t.Fatalf("unexpected multica args: %q", string(args)) - } - comment, err := os.ReadFile(commentPath) - if err != nil { - t.Fatal(err) - } - for _, want := range []string{ - "Mnemon update: issue admitted", - "## Mnemon Runtime", - "Issue: [TEA-7](mention://issue/iss-7)", - "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) - } - } - - lines := strings.Split(strings.TrimSpace(out.String()), "\n") - var sawComplete, sawProgress, sawAnswer, sawUserItem, sawAgentStart, sawAgentCompletedAt bool - var sawCommandStart, sawCommandComplete bool - var sawCommentaryProgress, sawFinalPhaseAnswer bool - agentCompleted := map[string]string{} - progressItemIDs := map[string]bool{} - answerItemID := "" - for _, line := range lines { - var msg map[string]any - if err := json.Unmarshal([]byte(line), &msg); err != nil { - t.Fatalf("invalid rpc line %q: %v", line, err) - } - if msg["method"] == "turn/completed" { - sawComplete = true - } - if msg["method"] == "item/started" { - params, _ := msg["params"].(map[string]any) - item, _ := params["item"].(map[string]any) - switch item["type"] { - case "userMessage": - _, sawUserItem = params["startedAtMs"] - case "agentMessage": - id, _ := item["id"].(string) - _, hasStarted := params["startedAtMs"] - if hasStarted && strings.HasPrefix(id, "msg-") { - sawAgentStart = true - } - case "commandExecution": - command, _ := item["command"].(string) - if strings.Contains(command, "multica issue get iss-7") && item["status"] == "inProgress" { - sawCommandStart = true - } - } - } - if msg["method"] == "item/completed" { - params, _ := msg["params"].(map[string]any) - item, _ := params["item"].(map[string]any) - if item["type"] == "agentMessage" { - _, sawAgentCompletedAt = params["completedAtMs"] - id, _ := item["id"].(string) - text, _ := item["text"].(string) - if id != "" { - agentCompleted[id] = text - } - phase, _ := item["phase"].(string) - if strings.Contains(text, "Loading Multica issue iss-7") && phase == "commentary" { - sawCommentaryProgress = true - } - if strings.Contains(text, "Mnemon ingest: recorded") && phase == "final_answer" { - sawFinalPhaseAnswer = true - } - } - if item["type"] == "commandExecution" { - command, _ := item["command"].(string) - output, _ := item["aggregatedOutput"].(string) - if strings.Contains(command, "multica issue get iss-7") && item["status"] == "completed" && strings.Contains(output, "Loaded TEA-7") { - sawCommandComplete = true - } - } - } - if msg["method"] == "item/agentMessage/delta" && strings.Contains(line, "Loading Multica issue iss-7") { - sawProgress = true - params, _ := msg["params"].(map[string]any) - if itemID, _ := params["itemId"].(string); itemID != "" { - progressItemIDs[itemID] = true - } - } - 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) - } - } - if !sawComplete || !sawProgress || !sawAnswer || !sawUserItem || !sawAgentStart || !sawAgentCompletedAt { - t.Fatalf("missing expected runtime response complete=%v progress=%v answer=%v user=%v agent_start=%v agent_completed_at=%v:\n%s", - sawComplete, sawProgress, sawAnswer, sawUserItem, sawAgentStart, sawAgentCompletedAt, out.String()) - } - if !sawCommandStart || !sawCommandComplete { - t.Fatalf("missing commandExecution projection start=%v complete=%v:\n%s", sawCommandStart, sawCommandComplete, out.String()) - } - if !sawCommentaryProgress || !sawFinalPhaseAnswer { - t.Fatalf("unexpected agent message phases commentary=%v final=%v:\n%s", sawCommentaryProgress, sawFinalPhaseAnswer, out.String()) - } - if len(agentCompleted) < 4 { - t.Fatalf("runtime progress should complete multiple agent messages, got %d:\n%s", len(agentCompleted), out.String()) - } - if answerItemID == "" || progressItemIDs[answerItemID] { - t.Fatalf("final answer should be a separate agent message item from progress, answer=%q progress=%v", answerItemID, progressItemIDs) - } -} - -func TestRuntimeForwardsManagedCodexTraceItemsOnOuterTurn(t *testing.T) { - var messages []rpcMessage - emit := func(message rpcMessage) error { - messages = append(messages, message) - return nil - } - traceStarted := driver.ManagedTurnTraceEvent{ - SourceRuntime: driver.ManagedTurnTraceSourceCodexAppServer, - Principal: "planner@team", - TurnID: "inner-turn", - ItemID: "inner-msg", - Method: "item/started", - ItemType: "agentMessage", - Item: map[string]any{ - "type": "agentMessage", - "id": "inner-msg", - "text": "", - "phase": "commentary", + "MULTICA_AGENT_NAME=Reviewer", + "MNEMON_MULTICA_BIN=" + multicaBin, + "MNEMON_CONTROL_ADDR=" + server.URL, + "MNEMON_CONTROL_PRINCIPAL=reviewer@team", + "FAKE_MULTICA_LOG=" + logPath, }, - } - traceDelta := driver.ManagedTurnTraceEvent{ - SourceRuntime: driver.ManagedTurnTraceSourceCodexAppServer, - Principal: "planner@team", - TurnID: "inner-turn", - ItemID: "inner-msg", - Method: "item/agentMessage/delta", - Text: "native Codex agent detail", - } - traceCompleted := traceStarted - traceCompleted.Method = "item/completed" - traceCompleted.Item = map[string]any{ - "type": "agentMessage", - "id": "inner-msg", - "text": "native Codex agent detail", - "phase": "commentary", - } - for _, event := range []driver.ManagedTurnTraceEvent{traceStarted, traceDelta, traceCompleted} { - if err := emitRuntimeMessages(emit, multicasurface.RuntimeManagedTraceMessages("outer-thread", "outer-turn", event, fixedRuntimeTime())); err != nil { - t.Fatal(err) - } - } - if len(messages) != 3 { - t.Fatalf("messages = %+v, want 3", messages) - } - var itemID string - for i, message := range messages { - params := message.Params - if params["threadId"] != "outer-thread" || params["turnId"] != "outer-turn" { - t.Fatalf("message %d not attached to outer turn: %+v", i, message) - } - switch message.Method { - case "item/started", "item/completed": - item, _ := params["item"].(map[string]any) - if item["id"] == "inner-msg" { - t.Fatalf("message %d leaked inner item id: %+v", i, message) - } - if item["mnemonManagedTurnId"] != "inner-turn" || item["mnemonPrincipal"] != "planner@team" { - t.Fatalf("message %d missing trace metadata: %+v", i, item) - } - if itemID == "" { - itemID, _ = item["id"].(string) - } else if item["id"] != itemID { - t.Fatalf("trace item id changed across lifecycle: first=%q got=%q", itemID, item["id"]) - } - case "item/agentMessage/delta": - if params["itemId"] != itemID { - t.Fatalf("delta item id = %q, want %q", params["itemId"], itemID) - } - if params["delta"] != "native Codex agent detail" { - t.Fatalf("delta = %+v", params) - } - default: - t.Fatalf("unexpected method %q", message.Method) - } - } -} - -func TestRuntimeInitializesMulticaRootSessionMetadata(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 root-1"*) printf '{"id":"root-1","identifier":"TEA-1","title":"Coordinate release validation","description":"Plan and split release validation across the team.","status":"todo","priority":"medium"}\n' ;; - *"issue metadata set root-1"*) printf '{}\n' ;; - *"issue comment add root-1"*) cat > "$MULTICA_COMMENT_PATH"; printf '{"id":"comment-root","issue_id":"root-1","content":"ok","type":"comment"}\n' ;; - *) printf '{}\n' ;; -esac -` - if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { - t.Fatal(err) - } - - var got contract.ObservationEnvelope - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/ingest": - 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: 21, Dup: false, Ticked: true}) - default: - t.Fatalf("unexpected path %s", r.URL.Path) - } - })) - defer srv.Close() + CWD: t.TempDir(), + Now: fixedRuntimeTime, + }).importIssue(multicasurface.RuntimeInput{}, nil) - input := `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"input":[{"type":"text","text":"Your assigned issue ID is: root-1"}]}}` + "\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-root", - "MULTICA_AGENT_ID=agent-planner", - ), - CWD: tmp, - Stdin: strings.NewReader(input), - Stdout: &out, - Now: fixedRuntimeTime, - }) - if err != nil { - t.Fatal(err) + if result.Status != "recorded" { + t.Fatalf("status = %q err=%v", result.Status, result.Err) } - rule := got.Event.Payload["rule"].(map[string]any) - if rule["hub_backend"] != driver.MulticaHubBackend || rule["root_issue_id"] != "root-1" || rule["source_issue_id"] != "root-1" { - t.Fatalf("root hub rule mismatch: %+v", rule) + if result.Receipt == nil || result.Receipt.Seq != 7 { + t.Fatalf("receipt mismatch: %+v", result.Receipt) } - if rule["session_id"] != driver.MulticaSessionID("root-1") { - t.Fatalf("session id = %q", rule["session_id"]) + env := <-received + if env.ExternalID != "multica-task-task-1" || + env.Event.Type != "teamwork_signal.write_candidate.observed" { + t.Fatalf("observation mismatch: %+v", env) } - args := mustReadRuntimeTestFile(t, argsPath) - for _, want := range []string{ - "issue metadata set root-1 --key mnemon.hub_backend --value multica --type string --output json", - "issue metadata set root-1 --key mnemon.kind --value session_mailbox --type string --output json", - "issue metadata set root-1 --key mnemon.root_issue_id --value root-1 --type string --output json", - "issue comment add root-1 --content-stdin --output json", - } { - if !strings.Contains(args, want) { - t.Fatalf("args missing %q:\n%s", want, args) - } + rule, _ := env.Event.Payload["rule"].(map[string]any) + if rule["external_issue_id"] != "issue-1" || rule["external_source"] != "multica" { + t.Fatalf("rule payload mismatch: %+v", rule) } - comment := mustReadRuntimeTestFile(t, commentPath) - for _, want := range []string{ - "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", + argsLog := readFile(t, logPath) + for _, forbidden := range []string{ + "issue comment add", + "issue metadata set", + "issue status set", + "issue assign", + "hub-write", + "[mnemon:wake]", } { - 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(argsLog, forbidden) { + t.Fatalf("runtime performed forbidden R2 side effect %q:\n%s", forbidden, argsLog) } } - if !strings.Contains(out.String(), "Mnemon ingest: recorded") { - t.Fatalf("runtime output missing ingest evidence:\n%s", out.String()) - } } -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, +func TestRuntimeTreatsLegacyMailboxMetadataAsSurfaceInputOnly(t *testing.T) { + logPath := filepath.Join(t.TempDir(), "multica-args.log") + multicaBin := writeFakeMulticaCLI(t, logPath, fakeMulticaScript{ + IssueJSON: `{"id":"issue-legacy","identifier":"TEA-2","title":"旧 assignment mailbox","description":"旧元数据不应触发 wake。","status":"todo","priority":"medium"}`, + MetadataJSON: `[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.assignment_id","value":"asg-1"}]`, }) - 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", - "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 updates: failed") { - t.Fatalf("metadata projection failure polluted protocol result:\n%s", output) - } - comment := mustReadRuntimeTestFile(t, commentPath) - for _, want := range []string{ - "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) - } - } - if strings.Contains(comment, "Hub backend:") { - t.Fatalf("runtime comment must not expose hub backend routing details:\n%s", comment) - } - args := mustReadRuntimeTestFile(t, argsPath) - metadataAttempt := strings.Index(args, "issue metadata set iss-9 --key mnemon.") - commentProjection := strings.Index(args, "issue comment add iss-9 --content-stdin --output json") - if metadataAttempt < 0 || commentProjection < 0 || metadataAttempt > commentProjection { - 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") - 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 -printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" -case "$*" in - *"issue get root-2"*) printf '{"id":"root-2","identifier":"TEA-10","title":"Coordinate docs release","description":"Split docs release validation across the team.","status":"todo","priority":"medium"}\n' ;; - *"issue metadata set root-2"*) printf '{}\n' ;; - *"issue children root-2"*) printf '{"children":[]}\n' ;; - *"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 -` - 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": - var sub contract.Subscription - if err := json.NewDecoder(r.Body).Decode(&sub); err != nil { - t.Fatal(err) - } - if sub.Actor != "planner@team" { - t.Fatalf("presentation actor = %q", sub.Actor) - } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(pview.View{ - Ref: "view-assignments", - Digest: "digest-assignments", - Content: []pview.ResourceContent{{ - Ref: contract.ResourceRef{Kind: "assignment", ID: "project"}, - Fields: map[string]any{"items": []any{map[string]any{ - "id": "asg-stale", - "ingest_seq": float64(30), - "actor": "planner@team", - "rule": map[string]any{ - "assignment_id": "asg-stale", - "assignee": "worker@team", - "scope": "old release notes", - }, - "narrative": map[string]any{ - "expected_work": "stale assignment from an older Multica session", - "expected_feedback": "progress_digest with result or blocker", - }, - }, map[string]any{ - "id": "asg-writer", - "ingest_seq": float64(32), - "actor": "planner@team", - "rule": map[string]any{ - "assignment_id": "asg-writer", - "assignee": "worker@team", - "scope": "check release notes", - "ttl": "30m", - "signal_ref": "teamwork_signal/root-2", - }, - "narrative": map[string]any{ - "expected_work": "check release notes against the public changelog", - "expected_feedback": "progress_digest with result or blocker", - }, - "refs": map[string]any{ - "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"}, - Fields: map[string]any{"items": []any{map[string]any{ - "id": "pg-old-writer", - "event_id": "pg-old-writer", - "ingest_seq": float64(31), - "actor": "worker@team", - "assignment_ref": "asg-writer", - "feedback_kind": "progress", - "summary": "Old writer progress must not attach to the new mailbox.", - "artifact_refs": []any{"multica:issue:root-2"}, - }, 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"}, - }, 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"}, - }, - }}}, - }}, - }) - 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-2"}]}}` + "\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_CHILD_COMMENT_PATH="+childCommentPath, - "MULTICA_CREATE_DESCRIPTION_PATH="+createDescriptionPath, - "MULTICA_TASK_ID=task-root-2", - "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 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) - for _, want := range []string{ - "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", - } { - if !strings.Contains(args, want) { - t.Fatalf("args missing %q:\n%s", want, args) - } - } - if strings.Contains(args, "check release notes against") { - t.Fatalf("assignment description leaked into argv:\n%s", args) - } - 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") - 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) - } - 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{ - "## Assignment", - "check release notes against the public changelog", - "Root issue: [TEA-10](mention://issue/root-2) - Coordinate docs release", - "Assignee: `worker@team` (mnemon-worker)", - "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:", "Assignment: `asg-writer`", "assignment_ref", "progress_digest"} { - if strings.Contains(description, blocked) { - t.Fatalf("description must not expose machine field %q:\n%s", blocked, description) - } - } - comment := mustReadRuntimeTestFile(t, commentPath) - for _, want := range []string{ - "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) - } - } - 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) - } - } - 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", - "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) - } - } - if strings.Contains(childComment, "Old writer progress") { - t.Fatalf("old progress attached to new assignment mailbox:\n%s", childComment) - } -} - -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"}, + server, received := fakeIngestServer(t) + defer server.Close() + + result := (&runtimeRPCState{ + Env: []string{ + "MULTICA_ISSUE_ID=issue-legacy", + "MNEMON_MULTICA_BIN=" + multicaBin, + "MNEMON_CONTROL_ADDR=" + server.URL, + "MNEMON_CONTROL_PRINCIPAL=reviewer@team", + "FAKE_MULTICA_LOG=" + logPath, }, - }); 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 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) - 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") - 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() + CWD: t.TempDir(), + Now: fixedRuntimeTime, + }).importIssue(multicasurface.RuntimeInput{}, nil) - 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) + if result.Status != "recorded" { + t.Fatalf("status = %q err=%v", result.Status, result.Err) } - output := out.String() - if !strings.Contains(output, "Multica updates: failed") { - t.Fatalf("runtime should surface the partial metadata failure:\n%s", output) + env := <-received + rule, _ := env.Event.Payload["rule"].(map[string]any) + if _, ok := rule["assignment_id"]; ok { + t.Fatalf("legacy mailbox metadata leaked into canonical rule payload: %+v", rule) } - 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) + argsLog := readFile(t, logPath) + if strings.Contains(argsLog, "issue comment add") || + strings.Contains(argsLog, "issue metadata set") || + strings.Contains(argsLog, "issue status set") { + t.Fatalf("legacy mailbox metadata triggered R2 writeback:\n%s", argsLog) } } -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 updates: 1 feedback comment posted") { - 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) - } - } +type fakeMulticaScript struct { + IssueJSON string + MetadataJSON string } -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" +func writeFakeMulticaCLI(t *testing.T, logPath string, script fakeMulticaScript) string { + t.Helper() + path := filepath.Join(t.TempDir(), "multica") + body := `#!/bin/sh +set -eu +printf '%s\n' "$*" >> "$FAKE_MULTICA_LOG" 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' ;; + *"issue get "*) + printf '%s\n' '` + script.IssueJSON + `' + ;; + *"issue metadata list "*) + printf '%s\n' '` + script.MetadataJSON + `' + ;; + *) + echo "unexpected multica args: $*" >&2 + exit 42 + ;; 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 { + if err := os.WriteFile(path, []byte(body), 0o755); 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 { + if err := os.WriteFile(logPath, nil, 0o644); 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) - } - } + return path } -func TestRuntimeCorrelatesAssignmentMailboxWithoutNewIngest(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 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.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 -` - if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { - t.Fatal(err) - } - - var ingestCalled bool - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/ingest": - ingestCalled = true - http.Error(w, "assignment mailbox must not ingest a new signal", http.StatusInternalServerError) - case "/render": - if r.Header.Get(access.PrincipalHeader) != "worker@team" { - t.Fatalf("render principal header = %q", r.Header.Get(access.PrincipalHeader)) - } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(presentation.Response{ - SchemaVersion: 1, - Status: presentation.StatusOK, - AuditID: "render-audit-child", - BodyDigest: "sha256:render-child", - Events: []eventmodel.EventEnvelope{{ - SchemaVersion: eventmodel.SchemaVersion, - Phase: eventmodel.PhaseDerived, - Event: eventmodel.Event{ - SchemaVersion: eventmodel.SchemaVersion, - ID: "derived-work-asg-1", - Type: "assignment.work_available", - Subject: "assignment/asg-1", - Actor: "mnemond", - Audience: "worker@team", - Payload: eventmodel.BuildPayload(nil, map[string]any{ - "body": "Assignment asg-1 is yours: release validation. Expected work: check the edge cases.", - }, nil), - }, - Meta: map[string]any{"presentation_hint": "work"}, - }}, - }) - case "/presentation-view": - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(pview.View{ - Ref: "view-empty", - Digest: "digest-empty", - }) - default: - t.Fatalf("unexpected path %s", r.URL.Path) - } +func fakeIngestServer(t *testing.T) (*httptest.Server, <-chan contract.ObservationEnvelope) { + t.Helper() + received := make(chan contract.ObservationEnvelope, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/ingest" { + http.NotFound(w, r) + return + } + if got := r.Header.Get("X-Mnemon-Principal"); got != "reviewer@team" { + http.Error(w, "unexpected principal "+got, http.StatusUnauthorized) + return + } + var env contract.ObservationEnvelope + if err := json.NewDecoder(r.Body).Decode(&env); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + received <- env + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"seq":7,"dup":false,"ticked":true}`)) })) - defer srv.Close() - - input := `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"input":[{"type":"text","text":"Your assigned issue ID is: child-1"}]}}` + "\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=worker@team", - "MNEMON_MANAGED_RUNTIME=noop", - "MNEMON_MANAGED_LEDGER="+filepath.Join(tmp, "wake-ledger.jsonl"), - "MULTICA_ARGS_PATH="+argsPath, - "MULTICA_COMMENT_PATH="+commentPath, - "MULTICA_TASK_ID=task-child", - "MULTICA_AGENT_ID=agent-worker", - ), - CWD: tmp, - Stdin: strings.NewReader(input), - Stdout: &out, - Now: fixedRuntimeTime, - }) - if err != nil { - t.Fatal(err) - } - if ingestCalled { - t.Fatal("assignment mailbox dispatch must not ingest a new teamwork signal") - } - 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) - 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", - "## 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) { - 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) { - for _, tc := range []struct { - name string - item multicasurface.RuntimeProgressItem - want string - }{ - {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(multicasurface.ProgressFeedbackMaterialForRuntimeItem(tc.item)); got != tc.want { - t.Fatalf("%s: status = %q, want %q", tc.name, got, tc.want) - } - } + return server, received } -func TestRuntimeSkipsIngestWhenControlAddressIsUnset(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" -printf '{"id":"iss-8","identifier":"TEA-8","title":"No local mnemond","description":"The runtime can still complete a Multica turn when local ingest is not configured."}\n' -` - if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { - t.Fatal(err) - } - input := `{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"input":[{"type":"text","text":"Your assigned issue ID is: iss-8"}]}}` + "\n" - var out bytes.Buffer - err := runRuntime(runtimeConfig{ - Args: []string{"app-server", "--listen", "stdio://"}, - Env: runtimeTestEnv( - "MNEMON_MULTICA_BIN="+bin, - "MULTICA_ARGS_PATH="+argsPath, - "MULTICA_TASK_ID=task-8", - "MULTICA_AGENT_NAME=mnemon-reviewer", - ), - CWD: tmp, - Stdin: strings.NewReader(input), - Stdout: &out, - Now: fixedRuntimeTime, - }) +func readFile(t *testing.T, path string) string { + t.Helper() + data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } - if !strings.Contains(out.String(), "Mnemon ingest: skipped because MNEMON_CONTROL_ADDR is not set") { - t.Fatalf("runtime output did not report skipped ingest:\n%s", out.String()) - } + return string(data) } func fixedRuntimeTime() time.Time { - return time.Date(2026, 6, 28, 9, 0, 0, 0, time.UTC) -} - -func mustReadRuntimeTestFile(t *testing.T, path string) string { - t.Helper() - data, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - return strings.TrimSpace(string(data)) + return time.Date(2026, 7, 1, 8, 0, 0, 0, time.UTC) } From c150e7e23d8333c3b19a598b87610259ba27ae52 Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:03:39 +0800 Subject: [PATCH 03/11] refactor: stop configuring multica as hub backend Remove MNEMON_HUB_BACKEND from Multica participant runtime env while still pruning stale values from existing agent env. Update Multica-hosted teamwork cues to keep canonical handoffs in Mnemon assignment events and treat Multica issues as OA and activation surfaces. Validation: go test ./harness/cmd/mnemon-harness -run Multica; go test ./harness/internal/mnemond/presentation --- harness/cmd/mnemon-harness/multica.go | 1 - harness/cmd/mnemon-harness/multica_test.go | 13 ++++++++----- .../internal/mnemond/presentation/render_test.go | 6 +++--- .../mnemond/presentation/teamwork_presenter.go | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index d87d9b65..837fe551 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -716,7 +716,6 @@ func multicaParticipantRuntimeEnv(cli driver.MulticaCLI, participant driver.Mult 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) addStringEnv(env, "MNEMON_MULTICA_BIN", multicaCLICommand(cli)) diff --git a/harness/cmd/mnemon-harness/multica_test.go b/harness/cmd/mnemon-harness/multica_test.go index f99af2e3..abcbc7c0 100644 --- a/harness/cmd/mnemon-harness/multica_test.go +++ b/harness/cmd/mnemon-harness/multica_test.go @@ -156,7 +156,6 @@ esac } for _, want := range []string{ `"MNEMON_CONTROL_PRINCIPAL":"reviewer@team"`, - `"MNEMON_HUB_BACKEND":"multica"`, `"MNEMON_MULTICA_REGISTRY":"` + registryPath + `"`, `"MNEMON_MANAGED_RUNTIME":"codex-appserver"`, `"MNEMON_MANAGED_COMMAND":"codex"`, @@ -165,6 +164,9 @@ esac t.Fatalf("agent env stdin missing %s:\n%s", want, envStdin) } } + if strings.Contains(string(envStdin), "MNEMON_HUB_BACKEND") { + t.Fatalf("R3 runtime env must not configure Multica as hub backend:\n%s", envStdin) + } argsLog, err := os.ReadFile(argsPath) if err != nil { t.Fatal(err) @@ -268,7 +270,6 @@ esac t.Fatal(err) } for _, want := range []string{ - `"MNEMON_HUB_BACKEND":"multica"`, `"MNEMON_MULTICA_REGISTRY":"` + registryPath + `"`, `"MNEMON_MULTICA_WORKSPACE_ID":"ws-1"`, `"MNEMON_CONTROL_ADDR":"http://127.0.0.1:8787"`, @@ -283,6 +284,9 @@ esac t.Fatalf("agent env stdin missing %s:\n%s", want, envStdin) } } + if strings.Contains(string(envStdin), "MNEMON_HUB_BACKEND") { + t.Fatalf("R3 provision env must not configure Multica as hub backend:\n%s", envStdin) + } } func TestMulticaProvisionRejectsDirectHarnessUse(t *testing.T) { @@ -306,14 +310,13 @@ func TestMergeMulticaParticipantRuntimeEnvPrunesStaleManagedKeys(t *testing.T) { "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"} { + for _, stale := range []string{"MNEMON_CONTROL_TOKEN", "MNEMON_CONTROL_TOKEN_FILE", "MNEMON_MANAGED_RUNTIME", "MNEMON_HUB_BACKEND"} { if _, ok := merged[stale]; ok { t.Fatalf("stale managed key %s should be pruned: %+v", stale, merged) } @@ -321,7 +324,7 @@ func TestMergeMulticaParticipantRuntimeEnvPrunesStaleManagedKeys(t *testing.T) { 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" { + if merged["MNEMON_CONTROL_PRINCIPAL"] != "planner@team" { t.Fatalf("desired managed env not applied: %+v", merged) } } diff --git a/harness/internal/mnemond/presentation/render_test.go b/harness/internal/mnemond/presentation/render_test.go index 54a6639d..38dc216c 100644 --- a/harness/internal/mnemond/presentation/render_test.go +++ b/harness/internal/mnemond/presentation/render_test.go @@ -100,9 +100,9 @@ func TestMulticaTeamworkSignalPresentationKeepsHandoffInMnemonProtocol(t *testin t.Fatal(err) } for _, want := range []string{ - "For Multica-backed teamwork", + "For Multica-hosted teamwork", "Mnemon assignment events", - "runtime projects Multica assignment mailboxes", + "not canonical assignment mailboxes", } { if !strings.Contains(resp.Body, want) { t.Fatalf("Multica teamwork cue missing %q:\n%s", want, resp.Body) @@ -114,7 +114,7 @@ func TestMulticaTeamworkSignalPresentationKeepsHandoffInMnemonProtocol(t *testin if err != nil { t.Fatal(err) } - if strings.Contains(plain.Body, "Multica-backed teamwork") { + if strings.Contains(plain.Body, "Multica-hosted teamwork") { t.Fatalf("non-Multica cue should stay host-neutral:\n%s", plain.Body) } } diff --git a/harness/internal/mnemond/presentation/teamwork_presenter.go b/harness/internal/mnemond/presentation/teamwork_presenter.go index ed5f8821..2194a499 100644 --- a/harness/internal/mnemond/presentation/teamwork_presenter.go +++ b/harness/internal/mnemond/presentation/teamwork_presenter.go @@ -80,7 +80,7 @@ func DeriveEventEnvelopes(req Request, proj view.View, now time.Time) []eventmod 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." + body += " For Multica-hosted teamwork, create handoffs as Mnemon assignment events; Multica issues are OA and activation surfaces, not canonical assignment mailboxes." } if refs := signalContextRefs(signal); len(refs) > 0 { body = fmt.Sprintf("%s Context refs: %s.", body, strings.Join(refs, ", ")) From c2f1cf9627f42273e01782ffc56f743e0b511f49 Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:05:57 +0800 Subject: [PATCH 04/11] test: guard r3 multica runtime boundaries Update coreguard to check the full mnemon-multica-runtime command now that the old hub writer is gone. The guard forbids managed wake and direct display writeback from the runtime input-import path, and pins mnemond/MnemonHub as Multica-surface agnostic. Validation: go test ./harness/internal/coreguard ./harness/cmd/mnemon-multica-runtime --- harness/cmd/mnemon-multica-runtime/main.go | 4 - .../multica_runtime_role_boundary_test.go | 76 +++++++++++++------ 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index d356eb53..2a758320 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -567,10 +567,6 @@ func runtimeIssueLabel(issue driver.MulticaIssue) string { return multicasurface.RuntimeIssueLabel(issue.ID, issue.Identifier, issue.Title) } -func markIssueInProgress(ctx context.Context, cli driver.MulticaCLI, issueID string) { - _, _ = cli.SetIssueStatus(ctx, issueID, "in_progress") -} - func wantsVersion(args []string) bool { fs := flag.NewFlagSet(multicasurface.MulticaRuntimeCommandName, flag.ContinueOnError) fs.SetOutput(io.Discard) diff --git a/harness/internal/coreguard/multica_runtime_role_boundary_test.go b/harness/internal/coreguard/multica_runtime_role_boundary_test.go index 1c66e908..d96b3957 100644 --- a/harness/internal/coreguard/multica_runtime_role_boundary_test.go +++ b/harness/internal/coreguard/multica_runtime_role_boundary_test.go @@ -21,33 +21,45 @@ var multicaRuntimeProjectionForbiddenImports = []string{ "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) +func TestMulticaRuntimeDoesNotOwnManagedWakeOrDisplayWriteback(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 } - } - 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())) + 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 + } + for _, imp := range file.Imports { + importPath := strings.Trim(imp.Path.Value, `"`) + if roleImportForbidden(importPath, multicaRuntimeProjectionForbiddenImports) { + t.Errorf("Multica runtime imports forbidden package %q; runtime may import surface input and call mnemond access, but must not own product config, state, runtime core, or hub exchange", importPath) } } - return true + ast.Inspect(file, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.SelectorExpr: + switch n.Sel.Name { + case "Wake", "SetIssueStatus", "AssignIssue", "AddIssueComment", "SetIssueMetadata", "SetIssueMetadataMap": + t.Errorf("Multica runtime calls %s at %s; R3 runtime input import must not own managed wake or display writeback", n.Sel.Name, fset.Position(n.Pos())) + } + case *ast.CompositeLit: + if selectorName(n.Type) == "ManagedAgentDriver" { + t.Errorf("Multica runtime constructs ManagedAgentDriver at %s; managed wake belongs to mnemond-managed source", fset.Position(n.Pos())) + } + } + return true + }) + return nil }) + if err != nil { + t.Fatalf("walk Multica runtime command: %v", err) + } } func TestMulticaRuntimeRoleBoundaryGuardLogicIsNotVacuous(t *testing.T) { @@ -55,13 +67,29 @@ func TestMulticaRuntimeRoleBoundaryGuardLogicIsNotVacuous(t *testing.T) { 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") + t.Fatal("Multica runtime guard must allow mnemond access") } if selectorName(&ast.SelectorExpr{Sel: ast.NewIdent("ManagedAgentDriver")}) != "ManagedAgentDriver" { t.Fatal("selectorName helper must identify selector names") } } +func TestMnemondAndMnemonHubDoNotDependOnMulticaSurface(t *testing.T) { + for _, pkg := range []string{"mnemond/access", "mnemond/admission", "mnemond/state", "mnemond/presentation", "mnemonhub"} { + _, files := packageFiles(t, pkg) + for _, file := range files { + for _, imp := range file.Imports { + importPath := strings.Trim(imp.Path.Value, `"`) + for _, forbidden := range []string{"harness/internal/surface/multica", "harness/internal/driver"} { + if strings.Contains(importPath, forbidden) { + t.Errorf("package %q imports Multica-facing package %q; mnemond/MnemonHub must stay product-surface agnostic", pkg, importPath) + } + } + } + } + } +} + 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 { From 139c2f2e8b5be25f0d657ec49dc031fb456829a7 Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:09:31 +0800 Subject: [PATCH 05/11] refactor: remove r2 multica runtime result wording Keep the runtime final answer focused on Multica surface input import results. Remove the old projection, managed wake, and assignment mailbox summary helpers so the product output cannot imply R2 hub-backend behavior. Validation: go test ./harness/internal/surface/multica ./harness/cmd/mnemon-multica-runtime --- .../surface/multica/runtime_result.go | 174 +----------------- .../surface/multica/runtime_result_test.go | 62 +++---- 2 files changed, 27 insertions(+), 209 deletions(-) diff --git a/harness/internal/surface/multica/runtime_result.go b/harness/internal/surface/multica/runtime_result.go index cfd08660..1c75ed66 100644 --- a/harness/internal/surface/multica/runtime_result.go +++ b/harness/internal/surface/multica/runtime_result.go @@ -1,7 +1,6 @@ package multica import ( - "fmt" "strings" ) @@ -12,16 +11,6 @@ type RuntimeResultSummary struct { 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 { @@ -49,11 +38,9 @@ func FormatRuntimeFinalAnswer(result RuntimeResultSummary) string { } switch result.Status { case "recorded": - b.WriteString(" Mnemon ingest: recorded.") - case "correlated": - b.WriteString(" Mnemon assignment mailbox: correlated.") + b.WriteString(" Multica surface input: observed.") case "skipped": - b.WriteString(" Mnemon ingest: skipped") + b.WriteString(" Multica surface input: skipped") if result.Err != "" { b.WriteString(" (") b.WriteString(result.Err) @@ -63,7 +50,7 @@ func FormatRuntimeFinalAnswer(result RuntimeResultSummary) string { } b.WriteString(".") case "failed": - b.WriteString(" Mnemon ingest: failed") + b.WriteString(" Multica surface input: failed") if result.Err != "" { b.WriteString(" (") b.WriteString(result.Err) @@ -72,66 +59,11 @@ func FormatRuntimeFinalAnswer(result RuntimeResultSummary) string { b.WriteString(".") default: if result.Err != "" { - b.WriteString(" Mnemon ingest: failed (") + b.WriteString(" Multica surface input: 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()) } @@ -155,101 +87,3 @@ func RuntimeIssueLabel(id, identifier, title string) string { } 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 index a4ccbbb2..cf143e05 100644 --- a/harness/internal/surface/multica/runtime_result_test.go +++ b/harness/internal/surface/multica/runtime_result_test.go @@ -7,48 +7,36 @@ import ( 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, + IssueID: "iss-1", + Identifier: "TEA-1", + Title: "Runtime adapter cleanup", + Principal: "planner@team", + Status: "recorded", }) 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.", + "Multica surface input: observed.", } { if !strings.Contains(got, want) { t.Fatalf("final answer missing %q:\n%s", want, got) } } + for _, old := range []string{"assignment mailbox", "Managed wake", "Multica projection", "Multica updates"} { + if strings.Contains(got, old) { + t.Fatalf("final answer contains old R2 wording %q:\n%s", old, 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", + IssueID: "iss-2", + Status: "failed", + Err: "ingest refused", }) for _, want := range []string{ - "Mnemon ingest: failed (ingest refused).", - "Multica projection: failed (comment rejected).", - "Managed wake: failed (render unavailable).", - "Multica updates: failed (metadata timeout).", + "Multica surface input: failed (ingest refused).", } { if !strings.Contains(got, want) { t.Fatalf("failure answer missing %q:\n%s", want, got) @@ -56,18 +44,14 @@ func TestRuntimeFinalAnswerCarriesFailures(t *testing.T) { } } -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 TestRuntimeFinalAnswerSummarizesSkippedSurfaceInput(t *testing.T) { + got := FormatRuntimeFinalAnswer(RuntimeResultSummary{ + IssueID: "iss-3", + Status: "skipped", + Err: "MNEMON_CONTROL_ADDR is not set", + }) + if !strings.Contains(got, "Multica surface input: skipped (MNEMON_CONTROL_ADDR is not set).") { + t.Fatalf("skipped answer mismatch:\n%s", got) } } From 51e159cd3e493e4f0e904064b290b2f2c77f417c Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:17:19 +0800 Subject: [PATCH 06/11] refactor: configure multica runtime as provider wrapper Replace managed-runtime participant env with MNEMON_MULTICA_PROVIDER_* settings so Multica provisioning describes the wrapped provider, not a Multica-owned wake scheduler. Keep stale MNEMON_MANAGED_* and MNEMON_HUB_BACKEND keys in the prune list for cleanup. Validation: go test ./harness/cmd/mnemon-harness ./harness/internal/surface/multica ./harness/cmd/mnemon-acceptance --- .../acceptance_multica_provision.go | 26 +-- harness/cmd/mnemon-harness/multica.go | 150 +++++++++--------- harness/cmd/mnemon-harness/multica_test.go | 97 +++++------ .../internal/surface/multica/registry_test.go | 6 +- .../surface/multica/runtime_config.go | 81 +--------- .../surface/multica/runtime_config_test.go | 101 ++---------- .../surface/multica/runtime_context.go | 2 - 7 files changed, 152 insertions(+), 311 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go index 7eda6f0f..7d6ff359 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_provision.go @@ -30,10 +30,10 @@ var ( acceptanceMulticaProvisionControlToken string acceptanceMulticaProvisionControlTokenFile string acceptanceMulticaProvisionInjectedHarnessBin string - acceptanceMulticaProvisionManagedRuntime string - acceptanceMulticaProvisionManagedCommand string - acceptanceMulticaProvisionManagedWorkspace string - acceptanceMulticaProvisionManagedTimeout time.Duration + acceptanceMulticaProvisionProviderRuntime string + acceptanceMulticaProvisionProviderCommand string + acceptanceMulticaProvisionProviderWorkspace string + acceptanceMulticaProvisionProviderTimeout time.Duration ) type acceptanceCommandRunner func(ctx context.Context, command string, args []string, stdout, stderr io.Writer) error @@ -79,10 +79,10 @@ func init() { 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") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProviderRuntime, "provider-runtime", multicaAcceptanceEnvDefault("MNEMON_MULTICA_PROVIDER_RUNTIME", ""), "provider runtime kind wrapped by mnemon-multica-runtime") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProviderCommand, "provider-command", multicaAcceptanceEnvDefault("MNEMON_MULTICA_PROVIDER_COMMAND", ""), "provider command wrapped by mnemon-multica-runtime") + acceptanceMulticaProvisionCmd.Flags().StringVar(&acceptanceMulticaProvisionProviderWorkspace, "provider-workspace", multicaAcceptanceEnvDefault("MNEMON_MULTICA_PROVIDER_WORKSPACE", ""), "provider workspace injected into participant runtime env") + acceptanceMulticaProvisionCmd.Flags().DurationVar(&acceptanceMulticaProvisionProviderTimeout, "provider-turn-timeout", 0, "provider turn timeout injected into participant runtime env") rootCmd.AddCommand(acceptanceMulticaProvisionCmd) } @@ -109,11 +109,11 @@ func buildAcceptanceMulticaProvisionArgs() []string { 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()) + args = appendFlag(args, "--provider-runtime", acceptanceMulticaProvisionProviderRuntime) + args = appendFlag(args, "--provider-command", acceptanceMulticaProvisionProviderCommand) + args = appendFlag(args, "--provider-workspace", acceptanceMulticaProvisionProviderWorkspace) + if acceptanceMulticaProvisionProviderTimeout > 0 { + args = appendFlag(args, "--provider-turn-timeout", acceptanceMulticaProvisionProviderTimeout.String()) } return args } diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 837fe551..4f267b62 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -46,41 +46,41 @@ var ( multicaCommentTitle string multicaCommentEvents []string - multicaProvisionRegistry string - multicaProvisionProjectRoot string - multicaProvisionProfileName string - multicaProvisionRuntimeCommand string - multicaProvisionRuntimePath string - multicaProvisionAgentPrefix string - multicaProvisionRestartDaemon bool - multicaProvisionWait time.Duration - multicaProvisionControlAddr string - multicaProvisionControlToken string - multicaProvisionControlTokenFile string - multicaProvisionHarnessBin string - multicaProvisionManagedRuntime string - multicaProvisionManagedCommand string - multicaProvisionManagedWorkspace string - multicaProvisionManagedTimeout time.Duration - multicaProvisionAcceptanceBridge bool + multicaProvisionRegistry string + multicaProvisionProjectRoot string + multicaProvisionProfileName string + multicaProvisionRuntimeCommand string + multicaProvisionRuntimePath string + multicaProvisionAgentPrefix string + multicaProvisionRestartDaemon bool + multicaProvisionWait time.Duration + multicaProvisionControlAddr string + multicaProvisionControlToken string + multicaProvisionControlTokenFile string + multicaProvisionHarnessBin string + multicaProvisionProviderRuntime string + multicaProvisionProviderCommand string + multicaProvisionProviderWorkspace string + multicaProvisionProviderTimeout time.Duration + multicaProvisionAcceptanceBridge bool - multicaParticipantRegistry string - multicaParticipantProjectRoot string - multicaParticipantAgentID string - multicaParticipantAgentName string - multicaParticipantPrincipal string - multicaParticipantRole string - multicaParticipantRuntimeID string - multicaParticipantCreateIfMissing bool - multicaParticipantSyncAgent bool - multicaParticipantControlAddr string - multicaParticipantControlToken string - multicaParticipantControlTokenFile string - multicaParticipantHarnessBin string - multicaParticipantManagedRuntime string - multicaParticipantManagedCommand string - multicaParticipantManagedWorkspace string - multicaParticipantManagedTimeout time.Duration + multicaParticipantRegistry string + multicaParticipantProjectRoot string + multicaParticipantAgentID string + multicaParticipantAgentName string + multicaParticipantPrincipal string + multicaParticipantRole string + multicaParticipantRuntimeID string + multicaParticipantCreateIfMissing bool + multicaParticipantSyncAgent bool + multicaParticipantControlAddr string + multicaParticipantControlToken string + multicaParticipantControlTokenFile string + multicaParticipantHarnessBin string + multicaParticipantProviderRuntime string + multicaParticipantProviderCommand string + multicaParticipantProviderWorkspace string + multicaParticipantProviderTimeout time.Duration ) var multicaCmd = &cobra.Command{ @@ -169,14 +169,14 @@ type multicaParticipantRegisterReport struct { } type multicaParticipantEnvOptions struct { - ControlAddr string - ControlToken string - ControlTokenFile string - HarnessBin string - ManagedRuntime string - ManagedCommand string - ManagedWorkspace string - ManagedTimeout time.Duration + ControlAddr string + ControlToken string + ControlTokenFile string + HarnessBin string + ProviderRuntime string + ProviderCommand string + ProviderWorkspace string + ProviderTimeout time.Duration } type multicaParticipantRegisterOptions struct { @@ -710,10 +710,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{} registryPath = multicaLocalEnvPath(registryPath) - managedWorkspace := multicaLocalEnvPath(opts.ManagedWorkspace) + providerWorkspace := multicaLocalEnvPath(opts.ProviderWorkspace) controlTokenFile := strings.TrimSpace(opts.ControlTokenFile) if controlTokenFile == "" && strings.TrimSpace(opts.ControlToken) == "" { - controlTokenFile = defaultMulticaParticipantControlTokenFile(managedWorkspace, participant.Principal) + controlTokenFile = defaultMulticaParticipantControlTokenFile(providerWorkspace, participant.Principal) } controlTokenFile = multicaLocalEnvPath(controlTokenFile) addStringEnv(env, "MNEMON_MULTICA_REGISTRY", registryPath) @@ -726,11 +726,11 @@ func multicaParticipantRuntimeEnv(cli driver.MulticaCLI, participant driver.Mult 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) - addStringEnv(env, "MNEMON_MANAGED_COMMAND", opts.ManagedCommand) - addStringEnv(env, "MNEMON_MANAGED_WORKSPACE", managedWorkspace) - if opts.ManagedTimeout > 0 { - env["MNEMON_MANAGED_TURN_TIMEOUT"] = opts.ManagedTimeout.String() + addStringEnv(env, "MNEMON_MULTICA_PROVIDER_RUNTIME", opts.ProviderRuntime) + addStringEnv(env, "MNEMON_MULTICA_PROVIDER_COMMAND", opts.ProviderCommand) + addStringEnv(env, "MNEMON_MULTICA_PROVIDER_WORKSPACE", providerWorkspace) + if opts.ProviderTimeout > 0 { + env["MNEMON_MULTICA_PROVIDER_TURN_TIMEOUT"] = opts.ProviderTimeout.String() } return env } @@ -775,6 +775,10 @@ var multicaParticipantRuntimeEnvKeys = []string{ "MNEMON_CONTROL_TOKEN_FILE", "MNEMON_CONTROL_PRINCIPAL", "MNEMON_HARNESS_BIN", + "MNEMON_MULTICA_PROVIDER_RUNTIME", + "MNEMON_MULTICA_PROVIDER_COMMAND", + "MNEMON_MULTICA_PROVIDER_WORKSPACE", + "MNEMON_MULTICA_PROVIDER_TURN_TIMEOUT", "MNEMON_MANAGED_RUNTIME", "MNEMON_MANAGED_COMMAND", "MNEMON_MANAGED_WORKSPACE", @@ -801,27 +805,27 @@ func sanitizeMulticaPrincipal(principal string) string { func multicaProvisionEnvOptionsFromFlags() multicaParticipantEnvOptions { return multicaParticipantEnvOptions{ - ControlAddr: multicaProvisionControlAddr, - ControlToken: multicaProvisionControlToken, - ControlTokenFile: multicaProvisionControlTokenFile, - HarnessBin: multicaProvisionHarnessBin, - ManagedRuntime: multicaProvisionManagedRuntime, - ManagedCommand: multicaProvisionManagedCommand, - ManagedWorkspace: multicaProvisionManagedWorkspace, - ManagedTimeout: multicaProvisionManagedTimeout, + ControlAddr: multicaProvisionControlAddr, + ControlToken: multicaProvisionControlToken, + ControlTokenFile: multicaProvisionControlTokenFile, + HarnessBin: multicaProvisionHarnessBin, + ProviderRuntime: multicaProvisionProviderRuntime, + ProviderCommand: multicaProvisionProviderCommand, + ProviderWorkspace: multicaProvisionProviderWorkspace, + ProviderTimeout: multicaProvisionProviderTimeout, } } func multicaParticipantEnvOptionsFromFlags() multicaParticipantEnvOptions { return multicaParticipantEnvOptions{ - ControlAddr: multicaParticipantControlAddr, - ControlToken: multicaParticipantControlToken, - ControlTokenFile: multicaParticipantControlTokenFile, - HarnessBin: multicaParticipantHarnessBin, - ManagedRuntime: multicaParticipantManagedRuntime, - ManagedCommand: multicaParticipantManagedCommand, - ManagedWorkspace: multicaParticipantManagedWorkspace, - ManagedTimeout: multicaParticipantManagedTimeout, + ControlAddr: multicaParticipantControlAddr, + ControlToken: multicaParticipantControlToken, + ControlTokenFile: multicaParticipantControlTokenFile, + HarnessBin: multicaParticipantHarnessBin, + ProviderRuntime: multicaParticipantProviderRuntime, + ProviderCommand: multicaParticipantProviderCommand, + ProviderWorkspace: multicaParticipantProviderWorkspace, + ProviderTimeout: multicaParticipantProviderTimeout, } } @@ -1018,10 +1022,10 @@ func init() { multicaProvisionCmd.Flags().StringVar(&multicaProvisionControlToken, "mnemon-control-token", envDefault("MNEMON_CONTROL_TOKEN", ""), "Local Mnemon bearer token injected into participant runtime env") multicaProvisionCmd.Flags().StringVar(&multicaProvisionControlTokenFile, "mnemon-control-token-file", envDefault("MNEMON_CONTROL_TOKEN_FILE", ""), "Local Mnemon bearer token file injected into participant runtime env") multicaProvisionCmd.Flags().StringVar(&multicaProvisionHarnessBin, "harness-bin", envDefault("MNEMON_HARNESS_BIN", ""), "mnemon-harness executable injected into participant runtime env") - multicaProvisionCmd.Flags().StringVar(&multicaProvisionManagedRuntime, "managed-runtime", envDefault("MNEMON_MANAGED_RUNTIME", ""), "managed agent runtime injected into participant env (noop or codex-appserver)") - 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().StringVar(&multicaProvisionProviderRuntime, "provider-runtime", envDefault("MNEMON_MULTICA_PROVIDER_RUNTIME", ""), "provider runtime kind wrapped by mnemon-multica-runtime") + multicaProvisionCmd.Flags().StringVar(&multicaProvisionProviderCommand, "provider-command", envDefault("MNEMON_MULTICA_PROVIDER_COMMAND", ""), "provider command wrapped by mnemon-multica-runtime") + multicaProvisionCmd.Flags().StringVar(&multicaProvisionProviderWorkspace, "provider-workspace", envDefault("MNEMON_MULTICA_PROVIDER_WORKSPACE", ""), "provider workspace injected into participant runtime env") + multicaProvisionCmd.Flags().DurationVar(&multicaProvisionProviderTimeout, "provider-turn-timeout", 0, "provider turn timeout injected into participant runtime env") multicaProvisionCmd.Flags().BoolVar(&multicaProvisionAcceptanceBridge, "acceptance-bridge", false, "allow mnemon-acceptance to invoke hidden Multica provisioning bridge") _ = multicaProvisionCmd.Flags().MarkHidden("acceptance-bridge") @@ -1038,10 +1042,10 @@ func init() { multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantControlToken, "mnemon-control-token", envDefault("MNEMON_CONTROL_TOKEN", ""), "Local Mnemon bearer token injected into participant runtime env") multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantControlTokenFile, "mnemon-control-token-file", envDefault("MNEMON_CONTROL_TOKEN_FILE", ""), "Local Mnemon bearer token file injected into participant runtime env") multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantHarnessBin, "harness-bin", envDefault("MNEMON_HARNESS_BIN", ""), "mnemon-harness executable injected into participant runtime env") - multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantManagedRuntime, "managed-runtime", envDefault("MNEMON_MANAGED_RUNTIME", ""), "managed agent runtime injected into participant env (noop or codex-appserver)") - multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantManagedCommand, "managed-command", envDefault("MNEMON_MANAGED_COMMAND", ""), "managed runtime command injected into participant env") - multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantManagedWorkspace, "managed-workspace", envDefault("MNEMON_MANAGED_WORKSPACE", ""), "managed runtime workspace injected into participant env") - multicaParticipantRegisterCmd.Flags().DurationVar(&multicaParticipantManagedTimeout, "managed-turn-timeout", 0, "managed runtime turn timeout injected into participant env") + multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantProviderRuntime, "provider-runtime", envDefault("MNEMON_MULTICA_PROVIDER_RUNTIME", ""), "provider runtime kind wrapped by mnemon-multica-runtime") + multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantProviderCommand, "provider-command", envDefault("MNEMON_MULTICA_PROVIDER_COMMAND", ""), "provider command wrapped by mnemon-multica-runtime") + multicaParticipantRegisterCmd.Flags().StringVar(&multicaParticipantProviderWorkspace, "provider-workspace", envDefault("MNEMON_MULTICA_PROVIDER_WORKSPACE", ""), "provider workspace injected into participant runtime env") + multicaParticipantRegisterCmd.Flags().DurationVar(&multicaParticipantProviderTimeout, "provider-turn-timeout", 0, "provider turn timeout injected into participant runtime env") multicaParticipantCmd.AddCommand(multicaParticipantRegisterCmd) multicaCmd.AddCommand(multicaProbeCmd, multicaParticipantCmd, multicaProvisionCmd, multicaImportIssueCmd, multicaProjectCommentCmd) diff --git a/harness/cmd/mnemon-harness/multica_test.go b/harness/cmd/mnemon-harness/multica_test.go index abcbc7c0..51da48aa 100644 --- a/harness/cmd/mnemon-harness/multica_test.go +++ b/harness/cmd/mnemon-harness/multica_test.go @@ -118,9 +118,9 @@ esac multicaParticipantRole = "reviewer" multicaParticipantControlAddr = "http://127.0.0.1:8791" multicaParticipantHarnessBin = "/abs/mnemon-harness" - multicaParticipantManagedRuntime = "codex-appserver" - multicaParticipantManagedCommand = "codex" - multicaParticipantManagedWorkspace = tmp + multicaParticipantProviderRuntime = "codex" + multicaParticipantProviderCommand = "codex" + multicaParticipantProviderWorkspace = tmp multicaJSON = true t.Setenv("MULTICA_ARGS_PATH", argsPath) t.Setenv("MULTICA_ENV_STDIN_PATH", envStdinPath) @@ -157,8 +157,8 @@ esac for _, want := range []string{ `"MNEMON_CONTROL_PRINCIPAL":"reviewer@team"`, `"MNEMON_MULTICA_REGISTRY":"` + registryPath + `"`, - `"MNEMON_MANAGED_RUNTIME":"codex-appserver"`, - `"MNEMON_MANAGED_COMMAND":"codex"`, + `"MNEMON_MULTICA_PROVIDER_RUNTIME":"codex"`, + `"MNEMON_MULTICA_PROVIDER_COMMAND":"codex"`, } { if !strings.Contains(string(envStdin), want) { t.Fatalf("agent env stdin missing %s:\n%s", want, envStdin) @@ -226,8 +226,8 @@ esac multicaProvisionWait = 0 multicaProvisionControlAddr = "http://127.0.0.1:8787" multicaProvisionHarnessBin = "/abs/mnemon-harness" - multicaProvisionManagedRuntime = "noop" - multicaProvisionManagedWorkspace = tmp + multicaProvisionProviderRuntime = "noop" + multicaProvisionProviderWorkspace = tmp multicaProvisionAcceptanceBridge = true multicaJSON = true t.Setenv("MULTICA_ENV_STDIN_PATH", envStdinPath) @@ -277,8 +277,8 @@ esac `"MNEMON_CONTROL_TOKEN_FILE":"` + plannerTokenFile + `"`, `"MNEMON_CONTROL_TOKEN_FILE":"` + implementerTokenFile + `"`, `"MNEMON_HARNESS_BIN":"/abs/mnemon-harness"`, - `"MNEMON_MANAGED_RUNTIME":"noop"`, - `"MNEMON_MANAGED_WORKSPACE":"` + tmp + `"`, + `"MNEMON_MULTICA_PROVIDER_RUNTIME":"noop"`, + `"MNEMON_MULTICA_PROVIDER_WORKSPACE":"` + tmp + `"`, } { if !strings.Contains(string(envStdin), want) { t.Fatalf("agent env stdin missing %s:\n%s", want, envStdin) @@ -304,21 +304,22 @@ func TestMulticaProvisionRejectsDirectHarnessUse(t *testing.T) { 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", + "MNEMON_CONTROL_TOKEN": "old-token", + "MNEMON_CONTROL_TOKEN_FILE": "/old/token", + "MNEMON_MANAGED_RUNTIME": "codex-appserver", + "MNEMON_MULTICA_PROVIDER_RUNTIME": "old-provider", + "MNEMON_HUB_BACKEND": "old", + "CUSTOM_USER_ENV": "keep", }, map[string]string{ - "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", + "MNEMON_CONTROL_ADDR": "http://127.0.0.1:8791", + "MNEMON_CONTROL_PRINCIPAL": "planner@team", + "MNEMON_MULTICA_PROVIDER_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", "MNEMON_HUB_BACKEND"} { + for _, stale := range []string{"MNEMON_CONTROL_TOKEN", "MNEMON_CONTROL_TOKEN_FILE", "MNEMON_MANAGED_RUNTIME", "MNEMON_HUB_BACKEND", "MNEMON_MULTICA_PROVIDER_RUNTIME"} { if _, ok := merged[stale]; ok { - t.Fatalf("stale managed key %s should be pruned: %+v", stale, merged) + t.Fatalf("stale runtime env key %s should be pruned: %+v", stale, merged) } } if merged["CUSTOM_USER_ENV"] != "keep" { @@ -334,7 +335,7 @@ func TestMulticaParticipantRuntimeEnvUsesAbsoluteLocalPaths(t *testing.T) { tmp := t.TempDir() t.Chdir(tmp) - workspace := "managed-workspace" + workspace := "provider-workspace" tokenDir := filepath.Join(workspace, ".mnemon", "harness", "channel", "credentials") if err := os.MkdirAll(tokenDir, 0o700); err != nil { t.Fatal(err) @@ -348,10 +349,10 @@ func TestMulticaParticipantRuntimeEnvUsesAbsoluteLocalPaths(t *testing.T) { AgentName: "mnemon-planner", AgentID: "agent-planner", }, filepath.Join("state", "registry.json"), "ws-1", multicaParticipantEnvOptions{ - ManagedWorkspace: workspace, + ProviderWorkspace: workspace, }) - for _, key := range []string{"MNEMON_MULTICA_REGISTRY", "MNEMON_MANAGED_WORKSPACE", "MNEMON_CONTROL_TOKEN_FILE"} { + for _, key := range []string{"MNEMON_MULTICA_REGISTRY", "MNEMON_MULTICA_PROVIDER_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) @@ -399,10 +400,10 @@ func restoreMulticaFlags(t *testing.T) { oldProvisionControlToken := multicaProvisionControlToken oldProvisionControlTokenFile := multicaProvisionControlTokenFile oldProvisionHarnessBin := multicaProvisionHarnessBin - oldProvisionManagedRuntime := multicaProvisionManagedRuntime - oldProvisionManagedCommand := multicaProvisionManagedCommand - oldProvisionManagedWorkspace := multicaProvisionManagedWorkspace - oldProvisionManagedTimeout := multicaProvisionManagedTimeout + oldProvisionProviderRuntime := multicaProvisionProviderRuntime + oldProvisionProviderCommand := multicaProvisionProviderCommand + oldProvisionProviderWorkspace := multicaProvisionProviderWorkspace + oldProvisionProviderTimeout := multicaProvisionProviderTimeout oldProvisionAcceptanceBridge := multicaProvisionAcceptanceBridge oldParticipantRegistry := multicaParticipantRegistry oldParticipantProjectRoot := multicaParticipantProjectRoot @@ -417,10 +418,10 @@ func restoreMulticaFlags(t *testing.T) { oldParticipantControlToken := multicaParticipantControlToken oldParticipantControlTokenFile := multicaParticipantControlTokenFile oldParticipantHarnessBin := multicaParticipantHarnessBin - oldParticipantManagedRuntime := multicaParticipantManagedRuntime - oldParticipantManagedCommand := multicaParticipantManagedCommand - oldParticipantManagedWorkspace := multicaParticipantManagedWorkspace - oldParticipantManagedTimeout := multicaParticipantManagedTimeout + oldParticipantProviderRuntime := multicaParticipantProviderRuntime + oldParticipantProviderCommand := multicaParticipantProviderCommand + oldParticipantProviderWorkspace := multicaParticipantProviderWorkspace + oldParticipantProviderTimeout := multicaParticipantProviderTimeout t.Cleanup(func() { multicaBin = oldBin multicaProfile = oldProfile @@ -457,10 +458,10 @@ func restoreMulticaFlags(t *testing.T) { multicaProvisionControlToken = oldProvisionControlToken multicaProvisionControlTokenFile = oldProvisionControlTokenFile multicaProvisionHarnessBin = oldProvisionHarnessBin - multicaProvisionManagedRuntime = oldProvisionManagedRuntime - multicaProvisionManagedCommand = oldProvisionManagedCommand - multicaProvisionManagedWorkspace = oldProvisionManagedWorkspace - multicaProvisionManagedTimeout = oldProvisionManagedTimeout + multicaProvisionProviderRuntime = oldProvisionProviderRuntime + multicaProvisionProviderCommand = oldProvisionProviderCommand + multicaProvisionProviderWorkspace = oldProvisionProviderWorkspace + multicaProvisionProviderTimeout = oldProvisionProviderTimeout multicaProvisionAcceptanceBridge = oldProvisionAcceptanceBridge multicaParticipantRegistry = oldParticipantRegistry multicaParticipantProjectRoot = oldParticipantProjectRoot @@ -475,10 +476,10 @@ func restoreMulticaFlags(t *testing.T) { multicaParticipantControlToken = oldParticipantControlToken multicaParticipantControlTokenFile = oldParticipantControlTokenFile multicaParticipantHarnessBin = oldParticipantHarnessBin - multicaParticipantManagedRuntime = oldParticipantManagedRuntime - multicaParticipantManagedCommand = oldParticipantManagedCommand - multicaParticipantManagedWorkspace = oldParticipantManagedWorkspace - multicaParticipantManagedTimeout = oldParticipantManagedTimeout + multicaParticipantProviderRuntime = oldParticipantProviderRuntime + multicaParticipantProviderCommand = oldParticipantProviderCommand + multicaParticipantProviderWorkspace = oldParticipantProviderWorkspace + multicaParticipantProviderTimeout = oldParticipantProviderTimeout }) multicaBin = "" multicaProfile = "" @@ -513,10 +514,10 @@ func restoreMulticaFlags(t *testing.T) { multicaProvisionControlAddr = "" multicaProvisionControlToken = "" multicaProvisionControlTokenFile = "" - multicaProvisionManagedRuntime = "" - multicaProvisionManagedCommand = "" - multicaProvisionManagedWorkspace = "" - multicaProvisionManagedTimeout = 0 + multicaProvisionProviderRuntime = "" + multicaProvisionProviderCommand = "" + multicaProvisionProviderWorkspace = "" + multicaProvisionProviderTimeout = 0 multicaProvisionAcceptanceBridge = false multicaParticipantRegistry = "" multicaParticipantProjectRoot = "." @@ -531,8 +532,8 @@ func restoreMulticaFlags(t *testing.T) { multicaParticipantControlToken = "" multicaParticipantControlTokenFile = "" multicaParticipantHarnessBin = "" - multicaParticipantManagedRuntime = "" - multicaParticipantManagedCommand = "" - multicaParticipantManagedWorkspace = "" - multicaParticipantManagedTimeout = 0 + multicaParticipantProviderRuntime = "" + multicaParticipantProviderCommand = "" + multicaParticipantProviderWorkspace = "" + multicaParticipantProviderTimeout = 0 } diff --git a/harness/internal/surface/multica/registry_test.go b/harness/internal/surface/multica/registry_test.go index 82aed949..4fa0aaa7 100644 --- a/harness/internal/surface/multica/registry_test.go +++ b/harness/internal/surface/multica/registry_test.go @@ -87,9 +87,9 @@ func TestMulticaParticipantLookups(t *testing.T) { } } -func TestRuntimeMulticaRegistryPrincipalUsesManagedWorkspace(t *testing.T) { +func TestRuntimeMulticaRegistryPrincipalUsesProviderWorkspace(t *testing.T) { tmp := t.TempDir() - workspace := filepath.Join(tmp, "managed-workspace") + workspace := filepath.Join(tmp, "provider-workspace") if err := SaveMulticaRegistry(MulticaRegistryPath(workspace, ""), MulticaRegistry{ Participants: []MulticaParticipantRecord{{ Principal: "reviewer@team", @@ -99,7 +99,7 @@ func TestRuntimeMulticaRegistryPrincipalUsesManagedWorkspace(t *testing.T) { }); err != nil { t.Fatal(err) } - got := RuntimeMulticaRegistryPrincipal([]string{"MNEMON_MANAGED_WORKSPACE=" + workspace}, tmp, "agent-reviewer", "") + got := RuntimeMulticaRegistryPrincipal([]string{"MNEMON_MULTICA_PROVIDER_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 fa7c7625..6f8f6b80 100644 --- a/harness/internal/surface/multica/runtime_config.go +++ b/harness/internal/surface/multica/runtime_config.go @@ -1,7 +1,6 @@ package multica import ( - "path/filepath" "strings" "time" ) @@ -11,13 +10,6 @@ const ( MulticaRuntimeProfileName = "mnemon-runtime" ) -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-- { @@ -40,35 +32,6 @@ 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 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) { @@ -86,7 +49,7 @@ func RuntimeMulticaRegistryPaths(env []string, cwd string) []string { if explicit := RuntimeEnvValue(env, "MNEMON_MULTICA_REGISTRY"); explicit != "" { add(explicit) } - if workspace := RuntimeEnvValue(env, "MNEMON_MANAGED_WORKSPACE"); workspace != "" { + if workspace := RuntimeEnvValue(env, "MNEMON_MULTICA_PROVIDER_WORKSPACE"); workspace != "" { add(MulticaRegistryPath(workspace, "")) } if strings.TrimSpace(cwd) != "" { @@ -105,48 +68,6 @@ func RuntimeMulticaRegistry(env []string, cwd string) (MulticaRegistry, bool, er return MulticaRegistry{}, false, nil } -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 { - 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 firstNonEmptyRuntimeString(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { diff --git a/harness/internal/surface/multica/runtime_config_test.go b/harness/internal/surface/multica/runtime_config_test.go index c2f27724..0f022b14 100644 --- a/harness/internal/surface/multica/runtime_config_test.go +++ b/harness/internal/surface/multica/runtime_config_test.go @@ -8,10 +8,10 @@ import ( func TestRuntimeEnvValueUsesLastValue(t *testing.T) { env := []string{ - "MNEMON_MANAGED_RUNTIME=codex-appserver", - "MNEMON_MANAGED_RUNTIME=off", + "MNEMON_MULTICA_PROVIDER_RUNTIME=codex", + "MNEMON_MULTICA_PROVIDER_RUNTIME=off", } - if got := RuntimeEnvValue(env, "MNEMON_MANAGED_RUNTIME"); got != "off" { + if got := RuntimeEnvValue(env, "MNEMON_MULTICA_PROVIDER_RUNTIME"); got != "off" { t.Fatalf("RuntimeEnvValue = %q, want off", got) } if got := RuntimeEnvDefault(env, "MISSING", "fallback"); got != "fallback" { @@ -29,7 +29,6 @@ func TestRuntimeContextFromActivationNormalizesDaemonAndRuntimeMetadata(t *testi "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{ @@ -46,7 +45,7 @@ func TestRuntimeContextFromActivationNormalizesDaemonAndRuntimeMetadata(t *testi 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" { + if ctx.ControlAddr != "http://127.0.0.1:8787" || ctx.ControlPrincipal != "planner@team" { t.Fatalf("Mnemon control metadata mismatch: %+v", ctx) } } @@ -90,58 +89,14 @@ func TestRuntimeTimeoutUsesMulticaHTTPFallback(t *testing.T) { } } -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) - } -} - -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") + workspace := filepath.Join(tmp, "provider-workspace") cwd := filepath.Join(tmp, "task-workdir") got := RuntimeMulticaRegistryPaths([]string{ "MNEMON_MULTICA_REGISTRY=" + explicit, - "MNEMON_MANAGED_WORKSPACE=" + workspace, + "MNEMON_MULTICA_PROVIDER_WORKSPACE=" + workspace, }, cwd) want := []string{ explicit, @@ -158,9 +113,9 @@ func TestRuntimeMulticaRegistryPaths(t *testing.T) { } } -func TestRuntimeMulticaRegistryLoadsManagedWorkspace(t *testing.T) { +func TestRuntimeMulticaRegistryLoadsProviderWorkspace(t *testing.T) { tmp := t.TempDir() - workspace := filepath.Join(tmp, "managed-workspace") + workspace := filepath.Join(tmp, "provider-workspace") path := MulticaRegistryPath(workspace, "") if err := SaveMulticaRegistry(path, MulticaRegistry{ WorkspaceID: "ws-1", @@ -172,7 +127,7 @@ func TestRuntimeMulticaRegistryLoadsManagedWorkspace(t *testing.T) { }); err != nil { t.Fatal(err) } - reg, ok, err := RuntimeMulticaRegistry([]string{"MNEMON_MANAGED_WORKSPACE=" + workspace}, tmp) + reg, ok, err := RuntimeMulticaRegistry([]string{"MNEMON_MULTICA_PROVIDER_WORKSPACE=" + workspace}, tmp) if err != nil { t.Fatal(err) } @@ -180,41 +135,3 @@ func TestRuntimeMulticaRegistryLoadsManagedWorkspace(t *testing.T) { t.Fatalf("registry = ok:%v %+v", ok, reg) } } - -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) - } -} diff --git a/harness/internal/surface/multica/runtime_context.go b/harness/internal/surface/multica/runtime_context.go index 94047a37..2bafe734 100644 --- a/harness/internal/surface/multica/runtime_context.go +++ b/harness/internal/surface/multica/runtime_context.go @@ -19,7 +19,6 @@ type RuntimeContext struct { AgentName string WorkspaceID string ServerURL string - HubBackend string ControlAddr string ControlPrincipal string } @@ -36,7 +35,6 @@ func RuntimeContextFromActivation(env []string, cwd string, input RuntimeInput) 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"), } From bcc02858a7e535a2d7a806bde4e2d67649638ca8 Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:33:51 +0800 Subject: [PATCH 07/11] refactor: remove multica hub mailbox surface Delete the old Multica hub backend, assignment mailbox, and projection helpers. Replace driver exposure with R3 surface metadata aliases and update Multica acceptance to validate provider-wrapper surface flow instead of child mailbox fanout. Validation: go test ./harness/internal/surface/multica ./harness/internal/driver ./harness/cmd/mnemon-acceptance ./harness/internal/mnemond/presentation ./harness/internal/drive ./harness/cmd/mnemon-harness --- .../acceptance_multica_runtime.go | 470 ++--------- .../acceptance_multica_runtime_test.go | 568 ++----------- .../acceptance_multica_task_cases.go | 68 +- .../acceptance_multica_task_cases_test.go | 6 +- harness/cmd/mnemon-harness/control_test.go | 4 +- harness/internal/drive/selection_test.go | 20 +- harness/internal/driver/multica_hub.go | 86 -- harness/internal/driver/multica_surface.go | 34 + harness/internal/driver/multica_test.go | 215 +---- .../mnemond/presentation/render_test.go | 16 +- .../presentation/teamwork_presenter.go | 2 +- harness/internal/surface/multica/hub.go | 759 ------------------ harness/internal/surface/multica/hub_test.go | 392 --------- .../surface/multica/issue_surface_text.go | 50 ++ harness/internal/surface/multica/metadata.go | 37 + .../internal/surface/multica/metadata_test.go | 20 +- .../internal/surface/multica/projection.go | 659 --------------- .../surface/multica/projection_test.go | 447 ----------- harness/internal/surface/multica/util.go | 44 + 19 files changed, 384 insertions(+), 3513 deletions(-) delete mode 100644 harness/internal/driver/multica_hub.go create mode 100644 harness/internal/driver/multica_surface.go delete mode 100644 harness/internal/surface/multica/hub.go delete mode 100644 harness/internal/surface/multica/hub_test.go create mode 100644 harness/internal/surface/multica/issue_surface_text.go delete mode 100644 harness/internal/surface/multica/projection.go delete mode 100644 harness/internal/surface/multica/projection_test.go create mode 100644 harness/internal/surface/multica/util.go diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 28340d52..300bfe99 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -31,7 +31,7 @@ var ( acceptanceMulticaPoll time.Duration acceptanceMulticaRequireIngest bool acceptanceMulticaRequireManagedWake bool - acceptanceMulticaRequireHubFlow bool + acceptanceMulticaRequireSurfaceFlow bool acceptanceMulticaMinParticipants int acceptanceMulticaMinActiveAgents int ) @@ -55,7 +55,7 @@ var acceptanceMulticaRuntimeCmd = &cobra.Command{ Poll: acceptanceMulticaPoll, RequireIngest: acceptanceMulticaRequireIngest, RequireManagedWake: acceptanceMulticaRequireManagedWake, - RequireHubFlow: acceptanceMulticaRequireHubFlow, + RequireSurfaceFlow: acceptanceMulticaRequireSurfaceFlow, MinParticipants: acceptanceMulticaMinParticipants, MinActiveAgents: acceptanceMulticaMinActiveAgents, Stdout: cmd.OutOrStdout(), @@ -89,9 +89,9 @@ func init() { acceptanceMulticaRuntimeCmd.Flags().DurationVar(&acceptanceMulticaPoll, "poll", 5*time.Second, "poll interval for Multica runs") acceptanceMulticaRuntimeCmd.Flags().BoolVar(&acceptanceMulticaRequireIngest, "require-mnemon-ingest", true, "require run output to show recorded Mnemon ingest") acceptanceMulticaRuntimeCmd.Flags().BoolVar(&acceptanceMulticaRequireManagedWake, "require-managed-wake", false, "require run output to show a completed managed wake") - acceptanceMulticaRuntimeCmd.Flags().BoolVar(&acceptanceMulticaRequireHubFlow, "require-hub-flow", false, "require Multica hub backend evidence: root metadata, child issue mailboxes, and multi-agent runs") - acceptanceMulticaRuntimeCmd.Flags().IntVar(&acceptanceMulticaMinParticipants, "min-participants", 5, "minimum Multica participant agents required for hub-flow acceptance") - acceptanceMulticaRuntimeCmd.Flags().IntVar(&acceptanceMulticaMinActiveAgents, "min-active-agents", 3, "minimum distinct Multica agents with run evidence for hub-flow acceptance") + acceptanceMulticaRuntimeCmd.Flags().BoolVar(&acceptanceMulticaRequireSurfaceFlow, "require-surface-flow", false, "require R3 Multica surface evidence: provider-wrapper env, root run, readable metadata, and visible OA state") + acceptanceMulticaRuntimeCmd.Flags().IntVar(&acceptanceMulticaMinParticipants, "min-participants", 5, "minimum Multica participant agents required for surface-flow acceptance") + acceptanceMulticaRuntimeCmd.Flags().IntVar(&acceptanceMulticaMinActiveAgents, "min-active-agents", 3, "minimum distinct Multica agents with provider-wrapper readiness for surface-flow acceptance") rootCmd.AddCommand(acceptanceMulticaRuntimeCmd) } @@ -110,7 +110,7 @@ type multicaRuntimeProdSimOptions struct { Poll time.Duration RequireIngest bool RequireManagedWake bool - RequireHubFlow bool + RequireSurfaceFlow bool MinParticipants int MinActiveAgents int Stdout io.Writer @@ -245,10 +245,10 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime } report.RegistryPath = registryPath report.Participants = registry.Participants - if opts.RequireHubFlow { - addMulticaProdSimAssertion(&report, "hub-flow registry has participant team", len(registry.Participants) >= opts.MinParticipants, fmt.Sprintf("participants=%d min=%d", len(registry.Participants), opts.MinParticipants)) + if opts.RequireSurfaceFlow { + addMulticaProdSimAssertion(&report, "surface-flow registry has participant team", len(registry.Participants) >= opts.MinParticipants, fmt.Sprintf("participants=%d min=%d", len(registry.Participants), opts.MinParticipants)) if len(registry.Participants) < opts.MinParticipants { - return finishMulticaRuntimeProdSimReport(report, fmt.Errorf("Multica hub-flow requires at least %d participants", opts.MinParticipants)) + return finishMulticaRuntimeProdSimReport(report, fmt.Errorf("Multica surface-flow requires at least %d participants", opts.MinParticipants)) } } assignee, err := selectMulticaAcceptanceAssignee(registry, opts.AssigneePrincipal) @@ -268,14 +268,14 @@ 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 opts.RequireSurfaceFlow { + ok, detail, err := multicaSurfaceFlowProviderReady(ctx, cli, registry.Participants, opts.MinActiveAgents, assignee.Principal) + addMulticaProdSimAssertion(&report, "surface-flow agents expose provider wrapper", 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)) + return finishMulticaRuntimeProdSimReport(report, fmt.Errorf("Multica surface-flow requires at least %d provider-wrapper participants and a ready root assignee", opts.MinActiveAgents)) } } title := strings.TrimSpace(opts.IssueTitle) @@ -299,7 +299,7 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime addMulticaProdSimAssertion(&report, "issue created through Multica", issue.ID != "", issue.ID) addMulticaProdSimAssertion(&report, "issue assigned to Mnemon participant", assignee.AgentID != "", assignee.Principal) runtimeEvidenceWait := opts.Wait - if opts.RequireHubFlow && runtimeEvidenceWait > 2*time.Minute { + if opts.RequireSurfaceFlow && runtimeEvidenceWait > 2*time.Minute { runtimeEvidenceWait = 2 * time.Minute } runs, messages, err := waitMulticaRuntimeEvidence(ctx, cli, issue.ID, runtimeEvidenceWait, opts.Poll) @@ -307,7 +307,7 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime report.RunMessages = messages report.MessageTypes = multicaRunMessageTypeCounts(messages) if err != nil { - if !opts.RequireHubFlow || len(runs) == 0 { + if !opts.RequireSurfaceFlow || len(runs) == 0 { addMulticaProdSimAssertion(&report, "Multica runtime produced run evidence", false, err.Error()) return finishMulticaRuntimeProdSimReport(report, err) } @@ -320,16 +320,16 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime 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) + addMulticaProdSimAssertion(&report, "runtime observed Multica surface input", strings.Contains(combined, "Multica surface input: observed"), combined) } if opts.RequireManagedWake { addMulticaProdSimAssertion(&report, "runtime completed managed wake", multicaMessagesContainManagedWakeCompleted(combined), combined) } } - if opts.RequireHubFlow { + if opts.RequireSurfaceFlow { 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 { + if err := collectMulticaSurfaceFlowEvidence(ctx, cli, opts, &report); err != nil { return finishMulticaRuntimeProdSimReport(report, err) } } @@ -357,7 +357,7 @@ 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) { +func multicaSurfaceFlowProviderReady(ctx context.Context, cli driver.MulticaCLI, participants []driver.MulticaParticipantRecord, minActive int, requiredPrincipal string) (bool, string, error) { if minActive < 1 { minActive = 1 } @@ -375,7 +375,7 @@ func multicaHubFlowManagedRuntimeReady(ctx context.Context, cli driver.MulticaCL if err != nil { return false, strings.Join(details, "; "), fmt.Errorf("read Multica agent env for %s: %w", principal, err) } - ready, label := multicaHubFlowParticipantReady(env) + ready, label := multicaSurfaceFlowParticipantReady(env) if ready { active++ } @@ -389,38 +389,41 @@ func multicaHubFlowManagedRuntimeReady(ctx context.Context, cli driver.MulticaCL return ok, strings.Join(details, "; "), nil } -func multicaHubFlowParticipantReady(env map[string]string) (bool, string) { - runtimeName := strings.TrimSpace(env["MNEMON_MANAGED_RUNTIME"]) - runtimeReady := multicaManagedRuntimeCanDriveTeamwork(runtimeName) +func multicaSurfaceFlowParticipantReady(env map[string]string) (bool, string) { + runtimeName := strings.TrimSpace(env["MNEMON_MULTICA_PROVIDER_RUNTIME"]) + runtimeReady := multicaProviderRuntimeCanDriveTeamwork(runtimeName) if !runtimeReady { - return false, multicaManagedRuntimeReadinessLabel(runtimeName, false) + return false, multicaProviderRuntimeReadinessLabel(runtimeName, false) } var missing []string if strings.TrimSpace(env["MNEMON_CONTROL_ADDR"]) == "" { missing = append(missing, "MNEMON_CONTROL_ADDR") } + if strings.TrimSpace(env["MNEMON_MULTICA_PROVIDER_COMMAND"]) == "" { + missing = append(missing, "MNEMON_MULTICA_PROVIDER_COMMAND") + } if tokenFile := strings.TrimSpace(env["MNEMON_CONTROL_TOKEN_FILE"]); tokenFile != "" { if info, err := os.Stat(tokenFile); err != nil || info.IsDir() { missing = append(missing, "MNEMON_CONTROL_TOKEN_FILE") } } - label := multicaManagedRuntimeReadinessLabel(runtimeName, true) + label := multicaProviderRuntimeReadinessLabel(runtimeName, true) if len(missing) > 0 { return false, label + " (missing " + strings.Join(missing, ", ") + ")" } return true, label } -func multicaManagedRuntimeCanDriveTeamwork(runtimeName string) bool { +func multicaProviderRuntimeCanDriveTeamwork(runtimeName string) bool { switch strings.ToLower(strings.TrimSpace(runtimeName)) { - case "codex-appserver": + case "codex", "codex-appserver": return true default: return false } } -func multicaManagedRuntimeReadinessLabel(runtimeName string, ready bool) string { +func multicaProviderRuntimeReadinessLabel(runtimeName string, ready bool) string { runtimeName = strings.TrimSpace(runtimeName) if runtimeName == "" { runtimeName = "missing" @@ -428,127 +431,33 @@ func multicaManagedRuntimeReadinessLabel(runtimeName string, ready bool) string if ready { return runtimeName } - return runtimeName + " (not hub-flow capable)" + return runtimeName + " (not surface-flow capable)" } -func collectMulticaHubFlowEvidence(ctx context.Context, cli driver.MulticaCLI, opts multicaRuntimeProdSimOptions, report *multicaRuntimeProdSimReport) error { +func collectMulticaSurfaceFlowEvidence(ctx context.Context, cli driver.MulticaCLI, opts multicaRuntimeProdSimOptions, report *multicaRuntimeProdSimReport) error { rootMeta, err := cli.ListIssueMetadata(ctx, report.Issue.ID) if err != nil { addMulticaProdSimAssertion(report, "root issue metadata readable", false, err.Error()) return err } report.RootMetadata = rootMeta - 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]) + addMulticaProdSimAssertion(report, "root issue metadata readable", true, fmt.Sprintf("%+v", rootMeta)) + addMulticaProdSimAssertion(report, "root issue does not use legacy hub metadata", !multicaMetadataContainsLegacyHubKeys(rootMeta), fmt.Sprintf("%+v", rootMeta)) - minAssignmentChildren := opts.MinActiveAgents - 1 - if minAssignmentChildren < 1 { - minAssignmentChildren = 1 - } - if report.TaskExpectations.InitialChildMailboxes > 0 { - minAssignmentChildren = report.TaskExpectations.InitialChildMailboxes - } else 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 - 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))) - rawChildren, rawChildMeta, rawErr := listMulticaChildrenWithMetadata(ctx, cli, report.Issue.ID) - if rawErr != nil { - addMulticaProdSimAssertion(report, "root children are session-scoped assignment mailboxes", false, rawErr.Error()) - 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 := 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) - report.ChildRuns = childRuns - report.ChildMessages = childMessages - report.ChildMessageTypes = multicaChildRunMessageTypeCounts(childMessages) - report.ActiveAgents = activeAgents - if refreshedChildren, refreshedMeta, refreshErr := listMulticaAssignmentChildren(ctx, cli, report.Issue.ID); refreshErr == nil && len(refreshedChildren) > len(children) { - children = refreshedChildren - report.ChildIssues = refreshedChildren - for issueID, meta := range refreshedMeta { - report.ChildMetadata[issueID] = meta - } - } - 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)) - 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) - 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", multicaMessagesContainManagedWakeCompleted(combinedChild), 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 - report.FinalChildren = finalChildren - report.RootComments = rootComments - report.ChildComments = childComments + latest, comments, err := waitMulticaSurfaceVisibility(ctx, cli, report.Issue.ID, opts.Wait, opts.Poll) + report.FinalRoot = latest + report.RootComments = comments if err != nil { - addMulticaProdSimAssertion(report, "hub-flow projects feedback comments and completion statuses", false, err.Error()) + addMulticaProdSimAssertion(report, "surface-flow root visibility readable", false, err.Error()) 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)) + addMulticaProdSimAssertion(report, "surface-flow root visibility readable", latest.ID != "", fmt.Sprintf("status=%s comments=%d", latest.Status, len(comments))) + if len(comments) > 0 { + addMulticaProdSimAssertion(report, "surface-flow comments carry OA output", multicaCommentsContainFeedbackMarker(comments) || multicaCommentsContainMnemonUpdate(comments), fmt.Sprintf("comments=%d", len(comments))) } - visibleOK, visibleDetail := multicaAssignmentChildrenUseStructuredVisibleText(finalChildren) - addMulticaProdSimAssertion(report, "assignment child issue visible text is structured", visibleOK, visibleDetail) 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) @@ -605,115 +514,47 @@ func waitMulticaRuntimeEvidence(ctx context.Context, cli driver.MulticaCLI, issu } } -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 - } +func waitMulticaSurfaceVisibility(ctx context.Context, cli driver.MulticaCLI, issueID string, wait, poll time.Duration) (driver.MulticaIssue, []driver.MulticaComment, error) { deadline := time.Now().Add(wait) + var lastRoot driver.MulticaIssue + var lastComments []driver.MulticaComment for { - children, meta, err := listMulticaAssignmentChildren(ctx, cli, rootIssueID) + root, err := cli.GetIssue(ctx, issueID) if err != nil { - return children, meta, err + return lastRoot, lastComments, err } - if len(children) >= minChildren && multicaAssignmentChildrenHaveCompleteMetadata(children, meta, "") { - return children, meta, nil + comments, err := cli.ListIssueComments(ctx, issueID) + if err != nil { + return root, lastComments, err + } + lastRoot = root + lastComments = comments + if root.ID != "" { + return root, comments, nil } if wait <= 0 || time.Now().After(deadline) { - return children, meta, fmt.Errorf("timed out waiting for %d assignment child issues on root %s (got %d)", minChildren, rootIssueID, len(children)) + return root, comments, fmt.Errorf("timed out waiting for Multica surface visibility on issue %s", issueID) } select { case <-ctx.Done(): - return children, meta, ctx.Err() + return root, comments, ctx.Err() case <-time.After(poll): } } } -func listMulticaAssignmentChildren(ctx context.Context, cli driver.MulticaCLI, rootIssueID string) ([]driver.MulticaIssue, map[string]map[string]string, error) { - rawChildren, metaByIssue, err := listMulticaChildrenWithMetadata(ctx, cli, rootIssueID) - if err != nil { - return nil, nil, err - } - var children []driver.MulticaIssue - for _, child := range rawChildren { - meta := metaByIssue[child.ID] - if meta[driver.MulticaMetadataKind] != driver.MulticaHubKindAssignmentMailbox { - continue - } - children = append(children, child) - } - return children, metaByIssue, nil -} - -func listMulticaChildrenWithMetadata(ctx context.Context, cli driver.MulticaCLI, rootIssueID string) ([]driver.MulticaIssue, map[string]map[string]string, error) { - rawChildren, err := cli.ListIssueChildren(ctx, rootIssueID) - if err != nil { - return nil, nil, err - } - metaByIssue := map[string]map[string]string{} - for _, child := range rawChildren { - meta, err := cli.ResolveIssueHubMetadata(ctx, child) - if err != nil { - return nil, nil, err - } - metaByIssue[child.ID] = meta.Map() - } - return rawChildren, metaByIssue, nil -} - -func waitMulticaChildRunEvidence(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, rootRuns []driver.MulticaIssueRun, children []driver.MulticaIssue, wait, poll time.Duration, minActive int) (map[string][]driver.MulticaIssueRun, map[string][]driver.MulticaRunMessage, []string, error) { - deadline := time.Now().Add(wait) - currentChildren := append([]driver.MulticaIssue(nil), children...) - var lastRuns map[string][]driver.MulticaIssueRun - var lastMessages map[string][]driver.MulticaRunMessage - for { - refreshedChildren, _, err := listMulticaAssignmentChildren(ctx, cli, rootIssueID) - if err != nil { - return lastRuns, lastMessages, nil, err - } - if len(refreshedChildren) > len(currentChildren) { - currentChildren = refreshedChildren - } - childRuns := map[string][]driver.MulticaIssueRun{} - childMessages := map[string][]driver.MulticaRunMessage{} - active := multicaActiveAgentIDs(rootRuns) - for _, child := range currentChildren { - runs, err := cli.ListIssueRuns(ctx, child.ID) - if err != nil { - return childRuns, childMessages, sortedMulticaActiveAgents(active), err - } - childRuns[child.ID] = runs - for _, run := range runs { - if strings.TrimSpace(run.AgentID) != "" { - active[run.AgentID] = true - } - if !multicaRunTerminal(run) { - continue - } - messages, err := cli.ListRunMessages(ctx, run.ID, child.ID) - if err != nil { - return childRuns, childMessages, sortedMulticaActiveAgents(active), err - } - if len(messages) > 0 { - childMessages[child.ID] = append(childMessages[child.ID], messages...) - } - } - } - lastRuns = childRuns - lastMessages = childMessages - activeList := sortedMulticaActiveAgents(active) - if len(activeList) >= minActive && multicaEveryChildHasRun(childRuns, currentChildren) { - return childRuns, childMessages, activeList, nil - } - if wait <= 0 || time.Now().After(deadline) { - return lastRuns, lastMessages, activeList, fmt.Errorf("timed out waiting for child run evidence on root %s children=%d active_agents=%v child_messages=%d", rootIssueID, len(currentChildren), activeList, len(childMessages)) +func multicaMetadataContainsLegacyHubKeys(meta map[string]string) bool { + for key, value := range meta { + lowerKey := strings.ToLower(strings.TrimSpace(key)) + lowerValue := strings.ToLower(strings.TrimSpace(value)) + if lowerKey == "mnemon.hub_backend" || lowerKey == "mnemon.kind" { + return true } - select { - case <-ctx.Done(): - return lastRuns, lastMessages, activeList, ctx.Err() - case <-time.After(poll): + if strings.Contains(lowerValue, "assignment_mailbox") || strings.Contains(lowerValue, "session_mailbox") { + return true } } + return false } func multicaRunTerminal(run driver.MulticaIssueRun) bool { @@ -728,18 +569,6 @@ 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 { @@ -750,18 +579,6 @@ func combinedMulticaRunMessages(messages []driver.MulticaRunMessage) string { return strings.Join(parts, "\n") } -func combinedMulticaChildMessages(messages map[string][]driver.MulticaRunMessage) string { - var parts []string - for _, items := range messages { - for _, message := range items { - if strings.TrimSpace(message.Content) != "" { - parts = append(parts, strings.TrimSpace(message.Content)) - } - } - } - return strings.Join(parts, "\n") -} - func multicaMessagesContainManagedWakeCompleted(text string) bool { lower := strings.ToLower(text) return strings.Contains(lower, "managed wake: completed") || @@ -780,20 +597,6 @@ func multicaRunMessageTypeCounts(messages []driver.MulticaRunMessage) map[string return out } -func multicaChildRunMessageTypeCounts(messages map[string][]driver.MulticaRunMessage) map[string]int { - out := map[string]int{} - for _, items := range messages { - for _, message := range items { - typ := strings.TrimSpace(message.Type) - if typ == "" { - typ = "unknown" - } - out[typ]++ - } - } - return out -} - func multicaMessagesExposeRuntimeActivity(types map[string]int) bool { if len(types) == 0 { return false @@ -804,143 +607,14 @@ func multicaMessagesExposeRuntimeActivity(types map[string]int) bool { return types["commandExecution"] > 0 || types["exec_command"] > 0 } -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 - } - for _, child := range children { - childMeta := meta[child.ID] - 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 - } - } - return true -} - -func waitMulticaHubProjectionCompletion(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, children []driver.MulticaIssue, wait, poll time.Duration) (driver.MulticaIssue, []driver.MulticaIssue, []driver.MulticaComment, map[string][]driver.MulticaComment, error) { - deadline := time.Now().Add(wait) - var lastRoot driver.MulticaIssue - var lastChildren []driver.MulticaIssue - var lastRootComments []driver.MulticaComment - var lastChildComments map[string][]driver.MulticaComment - for { - root, err := cli.GetIssue(ctx, rootIssueID) - if err != nil { - return lastRoot, lastChildren, lastRootComments, lastChildComments, err - } - rootComments, err := cli.ListIssueComments(ctx, rootIssueID) - 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 { - latest, err := cli.GetIssue(ctx, child.ID) - if err != nil { - return root, finalChildren, rootComments, childComments, err - } - finalChildren = append(finalChildren, latest) - comments, err := cli.ListIssueComments(ctx, child.ID) - if err != nil { - return root, finalChildren, rootComments, childComments, err - } - childComments[child.ID] = comments - } - lastRoot = root - lastChildren = finalChildren - lastRootComments = rootComments - lastChildComments = childComments - if multicaHubProjectionComplete(root, finalChildren, childComments) { - return root, finalChildren, rootComments, childComments, nil - } - if wait <= 0 || time.Now().After(deadline) { - return root, finalChildren, rootComments, childComments, fmt.Errorf("timed out waiting for Multica feedback projection completion root=%s children=%v comments=%d", root.Status, multicaIssueStatuses(finalChildren), multicaCommentCount(childComments)) - } - select { - case <-ctx.Done(): - return root, finalChildren, rootComments, childComments, ctx.Err() - case <-time.After(poll): - } - } -} - -func multicaHubProjectionComplete(root driver.MulticaIssue, children []driver.MulticaIssue, childComments map[string][]driver.MulticaComment) bool { - if !multicasurface.IssueStatusDone(root.Status) || len(children) == 0 { - return false - } - for _, child := range children { - if !multicasurface.IssueStatusDone(child.Status) { - return false - } - if !multicaCommentsContainFeedbackMarker(childComments[child.ID]) { - return false - } - } - 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)) - } +func multicaCommentsContainMnemonUpdate(comments []driver.MulticaComment) bool { + for _, comment := range comments { + content := strings.ToLower(comment.Content) + if strings.Contains(content, "mnemon 更新") || strings.Contains(content, "mnemon update") { + return true } } - if len(failures) > 0 { - return false, strings.Join(failures, "; ") - } - return true, fmt.Sprintf("children=%d", len(children)) + return false } func multicaCommentsContainFeedbackMarker(comments []driver.MulticaComment) bool { diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 314a90c2..6d721f73 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -39,21 +39,7 @@ func TestSelectMulticaAcceptanceAssigneeUsesRegistryHelpers(t *testing.T) { func TestMulticaRuntimeProdSimAcceptanceObservesRunMessages(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) - } + registryPath := writeMulticaRuntimeTestRegistry(t, tmp, []string{"planner"}) argsPath := filepath.Join(tmp, "args.txt") stdinPath := filepath.Join(tmp, "stdin.txt") bin := filepath.Join(tmp, "multica") @@ -62,29 +48,24 @@ printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" 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. Managed wake: completed.","created_at":"2026-06-28T09:00:01Z"}]\n' ;; + *"issue runs iss-9"*) printf '[{"id":"task-9","issue_id":"iss-9","agent_id":"agent-planner","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. Multica surface input: observed.","created_at":"2026-06-28T09: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) + writeFakeMultica(t, bin, argsPath, stdinPath, script) report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ - RunRoot: filepath.Join(tmp, ".testdata", "multica-runtime"), - MulticaBin: bin, - WorkspaceID: "ws-1", - RegistryPath: registryPath, - AssigneePrincipal: "planner@team", - IssueTitle: "Runtime prod sim", - IssueDescription: "Teamwork acceptance", - Wait: time.Millisecond, - Poll: time.Millisecond, - RequireIngest: true, - RequireManagedWake: true, + RunRoot: filepath.Join(tmp, ".testdata", "multica-runtime"), + MulticaBin: bin, + WorkspaceID: "ws-1", + RegistryPath: registryPath, + AssigneePrincipal: "planner@team", + IssueTitle: "Runtime prod sim", + IssueDescription: "Teamwork acceptance", + Wait: time.Millisecond, + Poll: time.Millisecond, + RequireIngest: true, }) if err != nil { t.Fatalf("acceptance: %v report=%+v", err, report) @@ -107,7 +88,7 @@ esac if err != nil { t.Fatal(err) } - if !strings.Contains(string(args), "issue create --title Runtime prod sim") || !strings.Contains(string(args), "--assignee-id agent-1") { + if !strings.Contains(string(args), "issue create --title Runtime prod sim") || !strings.Contains(string(args), "--assignee-id agent-planner") { t.Fatalf("issue was not created through assigned Multica path:\n%s", args) } stdin, err := os.ReadFile(stdinPath) @@ -119,27 +100,9 @@ esac } } -func TestMulticaRuntimeProdSimAcceptanceRequiresHubFlow(t *testing.T) { +func TestMulticaRuntimeProdSimAcceptanceRequiresSurfaceFlow(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) - } + registryPath := writeMulticaRuntimeTestRegistry(t, tmp, []string{"planner", "researcher", "implementer", "reviewer", "integrator"}) argsPath := filepath.Join(tmp, "args.txt") stdinPath := filepath.Join(tmp, "stdin.txt") bin := filepath.Join(tmp, "multica") @@ -147,307 +110,113 @@ 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","MNEMON_CONTROL_ADDR":"http://127.0.0.1:8787"}}\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"}' ;; - *"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"}},{"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' ;; + *"agent env get agent-"*) agent="${4:-}"; printf '{"agent_id":"%s","custom_env":{"MNEMON_MULTICA_PROVIDER_RUNTIME":"codex","MNEMON_MULTICA_PROVIDER_COMMAND":"codex","MNEMON_CONTROL_ADDR":"http://127.0.0.1:8787"}}\n' "$agent" ;; + *"issue create"*) printf '{"id":"root-9","identifier":"TEA-9","title":"Runtime surface flow","description":"Teamwork acceptance","status":"todo"}\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. 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. 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. 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' ;; + *"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. Multica surface input: observed.","created_at":"2026-06-28T09:00:01Z"},{"task_id":"task-root","issue_id":"root-9","seq":2,"type":"tool_use","content":"mnemon observe --principal planner@team","created_at":"2026-06-28T09:00:02Z"}]\n' ;; + *"issue metadata list root-9"*) printf '[{"key":"mnemon.surface_role","value":"display"},{"key":"mnemon.event_ref","value":"event-root-9"},{"key":"mnemon.no_auto_dispatch","value":"true"}]\n' ;; + *"issue get root-9"*) printf '{"id":"root-9","identifier":"TEA-9","title":"Runtime surface flow","description":"Teamwork acceptance","status":"in_progress"}\n' ;; + *"issue comment list root-9"*) printf '[{"id":"comment-root","issue_id":"root-9","content":"Mnemon 更新: 进展\\n\\n事件引用: event-root-9"}]\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) + writeFakeMultica(t, bin, argsPath, stdinPath, script) report, err := runMulticaRuntimeProdSimAcceptance(context.Background(), multicaRuntimeProdSimOptions{ - RunRoot: filepath.Join(tmp, ".testdata", "multica-hub-runtime"), + RunRoot: filepath.Join(tmp, ".testdata", "multica-surface-runtime"), MulticaBin: bin, WorkspaceID: "ws-1", RegistryPath: registryPath, AssigneePrincipal: "planner@team", - IssueTitle: "Runtime hub flow", + IssueTitle: "Runtime surface flow", IssueDescription: "Teamwork acceptance", Wait: time.Millisecond, Poll: time.Millisecond, RequireIngest: true, - RequireManagedWake: true, - RequireHubFlow: true, + RequireSurfaceFlow: true, MinParticipants: 5, MinActiveAgents: 3, }) if err != nil { t.Fatalf("acceptance: %v report=%+v", err, report) } - if report.Status != "ok" || len(report.Participants) != 5 || len(report.ChildIssues) != 2 || len(report.ActiveAgents) != 3 || report.FinalRoot.Status != "done" { - t.Fatalf("hub report mismatch: %+v", report) + if report.Status != "ok" || len(report.Participants) != 5 || report.FinalRoot.ID != "root-9" || len(report.RootComments) != 1 { + t.Fatalf("surface report mismatch: %+v", report) } - if report.RootMetadata[driver.MulticaMetadataKind] != driver.MulticaHubKindSession { + if report.RootMetadata[driver.MulticaMetadataSurfaceRole] != driver.MulticaSurfaceRoleDisplay { t.Fatalf("root metadata mismatch: %+v", report.RootMetadata) } - 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) - } - 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", - } { - if !strings.Contains(string(args), want) { - t.Fatalf("args missing %q:\n%s", want, args) + if assertion.Name == "root issue does not use legacy hub metadata" && !assertion.Passed { + t.Fatalf("legacy metadata assertion failed: %+v", assertion) } } } -func TestWaitMulticaChildRunEvidenceRefreshesFollowupChildren(t *testing.T) { +func TestMulticaRuntimeProdSimAcceptanceRejectsLegacySurfaceFlowPrereq(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 children root-1"*) printf '{"children":[{"id":"child-1","identifier":"TEA-2","title":"TEA-1: runtime","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-1","mnemon.session_id":"multica:session:root-1","mnemon.assignment_id":"asg-1","mnemon.principal":"researcher@team"}},{"id":"child-2","identifier":"TEA-3","title":"TEA-1: runbook","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-1","mnemon.session_id":"multica:session:root-1","mnemon.assignment_id":"asg-2","mnemon.principal":"implementer@team"}},{"id":"child-3","identifier":"TEA-4","title":"TEA-1: release","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-1","mnemon.session_id":"multica:session:root-1","mnemon.assignment_id":"asg-3","mnemon.principal":"reviewer@team"}},{"id":"child-4","identifier":"TEA-5","title":"TEA-1: followup","status":"done","metadata":{"mnemon.hub_backend":"multica","mnemon.kind":"assignment_mailbox","mnemon.root_issue_id":"root-1","mnemon.session_id":"multica:session:root-1","mnemon.assignment_id":"asg-4","mnemon.principal":"integrator@team"}}]}\n' ;; - *"issue metadata list child-1"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.root_issue_id","value":"root-1"},{"key":"mnemon.session_id","value":"multica:session:root-1"},{"key":"mnemon.assignment_id","value":"asg-1"},{"key":"mnemon.principal","value":"researcher@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-1"},{"key":"mnemon.session_id","value":"multica:session:root-1"},{"key":"mnemon.assignment_id","value":"asg-2"},{"key":"mnemon.principal","value":"implementer@team"}]\n' ;; - *"issue metadata list child-3"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.root_issue_id","value":"root-1"},{"key":"mnemon.session_id","value":"multica:session:root-1"},{"key":"mnemon.assignment_id","value":"asg-3"},{"key":"mnemon.principal","value":"reviewer@team"}]\n' ;; - *"issue metadata list child-4"*) printf '[{"key":"mnemon.hub_backend","value":"multica"},{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.root_issue_id","value":"root-1"},{"key":"mnemon.session_id","value":"multica:session:root-1"},{"key":"mnemon.assignment_id","value":"asg-4"},{"key":"mnemon.principal","value":"integrator@team"}]\n' ;; - *"issue runs child-1"*) printf '[{"id":"task-child-1","issue_id":"child-1","agent_id":"agent-researcher","status":"completed","completed_at":"2026-06-30T09:01:00Z"}]\n' ;; - *"issue runs child-2"*) printf '[{"id":"task-child-2","issue_id":"child-2","agent_id":"agent-implementer","status":"completed","completed_at":"2026-06-30T09:02:00Z"}]\n' ;; - *"issue runs child-3"*) printf '[{"id":"task-child-3","issue_id":"child-3","agent_id":"agent-reviewer","status":"completed","completed_at":"2026-06-30T09:03:00Z"}]\n' ;; - *"issue runs child-4"*) printf '[{"id":"task-child-4","issue_id":"child-4","agent_id":"agent-integrator","status":"completed","completed_at":"2026-06-30T09:04:00Z"}]\n' ;; - *"issue run-messages task-child-"*) printf '[{"seq":1,"type":"text","content":"Mnemon assignment mailbox: correlated. Managed wake: completed.","created_at":"2026-06-30T09:05:00Z"}]\n' ;; - *) printf '{}\n' ;; -esac -` - if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { - t.Fatal(err) - } - t.Setenv("MULTICA_ARGS_PATH", argsPath) - - initialChildren := []driver.MulticaIssue{ - {ID: "child-1"}, - {ID: "child-2"}, - {ID: "child-3"}, - } - childRuns, _, activeAgents, err := waitMulticaChildRunEvidence( - context.Background(), - driver.MulticaCLI{Command: bin, WorkspaceID: "ws-1"}, - "root-1", - []driver.MulticaIssueRun{{ID: "task-root", IssueID: "root-1", AgentID: "agent-planner", Status: "completed", CompletedAt: "2026-06-30T09:00:00Z"}}, - initialChildren, - time.Second, - time.Millisecond, - 5, - ) - if err != nil { - t.Fatalf("wait child run evidence: %v", err) - } - if len(activeAgents) != 5 || len(childRuns["child-4"]) != 1 { - t.Fatalf("expected refreshed follow-up child evidence, active=%v runs=%+v", activeAgents, childRuns) - } - args, err := os.ReadFile(argsPath) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(args), "issue children root-1 --output json") || !strings.Contains(string(args), "issue runs child-4 --output json") { - t.Fatalf("expected child refresh and follow-up run lookup:\n%s", args) - } -} - -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) - } + registryPath := writeMulticaRuntimeTestRegistry(t, tmp, []string{"planner", "researcher", "implementer"}) 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 ;; + *"agent env get agent-"*) agent="${4:-}"; printf '{"agent_id":"%s","custom_env":{"MNEMON_MANAGED_RUNTIME":"codex-appserver","MNEMON_CONTROL_ADDR":"http://127.0.0.1:8787"}}\n' "$agent" ;; + *"issue create"*) printf '{"id":"should-not-create"}\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) + writeFakeMultica(t, bin, argsPath, stdinPath, script) 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, + RunRoot: filepath.Join(tmp, ".testdata", "multica-surface-runtime"), + MulticaBin: bin, + WorkspaceID: "ws-1", + RegistryPath: registryPath, + AssigneePrincipal: "planner@team", + Wait: time.Millisecond, + Poll: time.Millisecond, + RequireSurfaceFlow: true, + MinParticipants: 3, + MinActiveAgents: 2, }) - 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) + if err == nil { + t.Fatalf("surface-flow prerequisite should fail: %+v", report) } - var readinessAssertion *multicaRuntimeProdSimAssertion + var readiness *multicaRuntimeProdSimAssertion for i := range report.Assertions { - if report.Assertions[i].Name == "hub-flow agents expose managed runtime" { - readinessAssertion = &report.Assertions[i] + if report.Assertions[i].Name == "surface-flow agents expose provider wrapper" { + readiness = &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) + if readiness == nil || readiness.Passed || !strings.Contains(readiness.Detail, "missing (not surface-flow capable)") { + t.Fatalf("readiness assertion mismatch: %+v", report.Assertions) } - rawArgs, err := os.ReadFile(argsPath) + args, 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) + if strings.Contains(string(args), "issue create") { + t.Fatalf("issue create must not run when provider wrapper env is missing:\n%s", args) } } -func TestMulticaRuntimeProdSimHubFlowRejectsMissingControlAddrBeforeCreatingIssue(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":"codex-appserver"}}\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-no-control"), - MulticaBin: bin, - WorkspaceID: "ws-1", - RegistryPath: registryPath, - AssigneePrincipal: "planner@team", - IssueTitle: "No control 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 control 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, "missing MNEMON_CONTROL_ADDR") { - t.Fatalf("unexpected readiness assertion: %+v all=%+v", readinessAssertion, report.Assertions) +func TestMulticaMetadataContainsLegacyHubKeys(t *testing.T) { + if !multicaMetadataContainsLegacyHubKeys(map[string]string{"mnemon.kind": "assignment_mailbox"}) { + t.Fatal("legacy mnemon.kind must be rejected") } - 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 control addr is missing:\n%s", args) + if multicaMetadataContainsLegacyHubKeys(map[string]string{driver.MulticaMetadataSurfaceRole: driver.MulticaSurfaceRoleDisplay}) { + t.Fatal("R3 surface metadata should be accepted") } } -func TestMulticaRuntimeProdSimHubFlowWritesPartialSnapshotWhenMailboxExpectationMisses(t *testing.T) { - tmp := t.TempDir() +func writeMulticaRuntimeTestRegistry(t *testing.T, tmp string, roles []string) string { + t.Helper() registryPath := filepath.Join(tmp, "registry.json") var participants []driver.MulticaParticipantRecord - for _, role := range []string{"planner", "researcher", "implementer", "reviewer", "integrator"} { + for _, role := range roles { participants = append(participants, driver.MulticaParticipantRecord{ Principal: role + "@team", AgentName: "mnemon-" + role, @@ -464,215 +233,14 @@ func TestMulticaRuntimeProdSimHubFlowWritesPartialSnapshotWhenMailboxExpectation }); 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":"codex-appserver","MNEMON_CONTROL_ADDR":"http://127.0.0.1:8787"}}\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"}' ;; - *"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) - } + return registryPath } -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 - *"agent env get agent-"*) agent="${4:-}"; printf '{"agent_id":"%s","custom_env":{"MNEMON_MANAGED_RUNTIME":"codex-appserver","MNEMON_CONTROL_ADDR":"http://127.0.0.1:8787"}}\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"}' ;; - *"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.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' ;; - *"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 -` +func writeFakeMultica(t *testing.T, bin, argsPath, stdinPath, script string) { + t.Helper() 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 TestMulticaMessagesContainManagedWakeCompleted(t *testing.T) { - cases := []string{ - "Managed wake: completed.", - "Managed wake completed: turn=turn-1.", - } - for _, text := range cases { - if !multicaMessagesContainManagedWakeCompleted(text) { - t.Fatalf("expected managed wake completion in %q", text) - } - } - if multicaMessagesContainManagedWakeCompleted("Managed wake failed: no candidate.") { - t.Fatal("failed wake should not be accepted as completed") - } -} - -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) - } } diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go index 7711a941..92d5fac2 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go @@ -330,7 +330,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas multicaAcceptanceTaskCaseR2Readiness: func(started time.Time) multicaAcceptanceTaskCaseMaterial { return multicaAcceptanceTaskCaseMaterial{ Title: "Mnemon Multica runtime prod-sim " + started.Format("150405"), - Description: multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Description: multicasurface.IssueSurfaceDescription(multicasurface.IssueSurfaceDescriptionMaterial{ Request: "Run a small Mnemon R2 Multica readiness drill.", WorkMode: "Use Mnemon teamwork; Multica shows issues, runs, comments, and statuses.", Handoffs: []string{ @@ -339,8 +339,8 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }, 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.", + "Accepted Mnemon events can be exposed as Multica surface state without becoming canonical state.", + "Agent-visible updates are written back through explicit commands and carry R3 surface metadata.", "Stale or cross-session assignment material is ignored.", "Final root status reflects completion.", }, @@ -352,7 +352,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas MinFeedbackComments: 2, TeamworkRounds: []string{ "Round 1: root issue intake and first assignment split", - "Round 2: child mailbox feedback", + "Round 2: teammate feedback through Mnemon events", "Round 3: integration and final status projection", }, }, @@ -361,16 +361,16 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas 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.", + Description: multicasurface.IssueSurfaceDescription(multicasurface.IssueSurfaceDescriptionMaterial{ + Request: "运行一个复杂的 Mnemon-on-Multica 协作演练,强制经历 observe-act-reflect 循环,覆盖事件接入、provider wrapper 调度、OA 状态回写和最终集成。把它当作面向运营人员的验收调查,而不是简单 happy path。", + WorkMode: "Multica 作为可见 OA 协作界面;Mnemon EventEnvelope 和 mnemond accepted state 保持 canonical。协作者通过 Mnemon 事件拆分工作,通过显式命令把必要状态写回 Multica,并在第一轮发现风险后创建至少一个 follow-up slice。", 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 1 - Observe: 分别让 teammate 检查根 issue 的 surface metadata、provider wrapper run visibility、以及 OA 状态可读性。每个 teammate 必须报告具体 issue ID、agent ID、状态和证据。", "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.", + "至少三个独立工作切片通过 Mnemon assignment event 表达,并在 Multica 中留下可追踪的 surface/评论/状态证据。", "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.", @@ -393,16 +393,16 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas 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, but create work only through Mnemon assignment events. Do not call Multica issue create, assign, rerun, or cancel commands to fan out work; the runtime projection creates child assignment mailboxes from accepted assignments. After recording Mnemon result or blocker feedback, workers should finish rather than waiting for their own Multica comment projection. 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.", + Description: multicasurface.IssueSurfaceDescription(multicasurface.IssueSurfaceDescriptionMaterial{ + Request: "运行一个接近真实业务的 Mnemon-on-Multica 协作 case:三个有重叠上下文的 PoC 并发推进。目标是验证 Multica 作为 OA/runtime 体验层时,provider wrapper 调度、显式状态回写、以及跨 workstream 上下文复用是否清晰可靠。", + WorkMode: "Multica 只承担可见 issue、run、comment、status 的 OA 体验;工作拆分必须先进入 Mnemon assignment/progress/integration 事件。不要把 Multica 当 canonical hub backend,也不要通过 Multica issue fan-out 替代 Mnemon 协议。planner 需要把 case 拆成三个并行 PoC,要求每个反馈引用 shared context,并在第一轮后创建至少一个 follow-up event。", 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.", + "至少三个初始 PoC 切片被表达为 Mnemon assignment event,并在第一轮反馈后追加至少一个 follow-up event。", "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.", @@ -424,16 +424,16 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas Workstreams: []multicaAcceptanceWorkstream{ { ID: "poc-runtime-routing", - Title: "Runtime routing and assignment mailbox correlation", + Title: "Provider wrapper routing and surface 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"}, + SharedContextRefs: []string{"surface-map", "provider-contract", "evidence-ledger"}, + ExpectedArtifacts: []string{"routing-evidence.md", "surface-correlation-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"}, + SharedContextRefs: []string{"provider-contract", "risk-register", "evidence-ledger"}, ExpectedArtifacts: []string{"runbook-gap-list.md", "rollback-checklist.md", "operator-risk-notes.md"}, }, { @@ -451,7 +451,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas 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.", + "Seed root teamwork signal,并创建三个第一轮 Mnemon assignment event。", "Read first-round feedback before creating the follow-up assignment.", }, }, @@ -471,7 +471,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas Primary: []string{"poc-operator-runbook"}, Overlaps: []string{"poc-runtime-routing"}, Responsibilities: []string{ - "Verify the mailbox contract against the operator runbook and runtime behavior.", + "Verify provider wrapper contract against the operator runbook and runtime behavior.", "Name the smallest code or runbook change that would reduce operator ambiguity.", }, }, @@ -500,12 +500,12 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas { 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.", + Purpose: "Root issue、surface metadata、runtime run 和 agent trace 的对应表,用于关联 Multica 可见产物。", }, { - ID: "mailbox-contract", + ID: "provider-contract", UsedBy: []string{"poc-runtime-routing", "poc-operator-runbook"}, - Purpose: "Human-readable assignment mailbox contract plus hidden metadata boundary for routing, dedupe, and correlation.", + Purpose: "Provider wrapper 输入输出、Mnemon event emission channel、以及 Multica surface metadata 的边界约定。", }, { ID: "risk-register", @@ -520,7 +520,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }, 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.", + "implementer@team reuses provider-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.", }, @@ -528,7 +528,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas "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.", - "Direct Multica issue fan-out is invalid; all child work must originate from Mnemon assignment events and runtime mailbox projection.", + "Direct Multica issue fan-out is invalid; all work must originate from Mnemon assignment/progress/integration events and explicit surface writeback commands.", "No visible issue text should expose session ids, assignment ids, assignment fingerprints, or projection-owner keys.", }, } @@ -536,8 +536,8 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas 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.", + Description: multicasurface.IssueSurfaceDescription(multicasurface.IssueSurfaceDescriptionMaterial{ + Request: "Prepare a release readiness decision for a Mnemon Multica runtime update that affects provider wrapper routing, R3 surface metadata, and visible status writeback.", 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.", @@ -546,11 +546,11 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }, Validation: []string{ "Release decision references concrete root and child issue evidence.", - "Assignment mailboxes route to the intended Multica agents.", + "Provider wrapper turns route to the intended Multica-visible 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.", + Completion: "Finish when the root issue contains an explicit release decision and every work slice has result or blocker feedback recorded through Mnemon events.", }), Expectations: multicaAcceptanceTaskCaseExpectations{ MinActiveAgents: 4, @@ -567,8 +567,8 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas 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.", + Description: multicasurface.IssueSurfaceDescription(multicasurface.IssueSurfaceDescriptionMaterial{ + Request: "Triage a production-style regression report where Multica run activity appears, but Mnemon event observation or explicit surface writeback appears 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.", @@ -577,7 +577,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }, 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.", + "Each work slice reports evidence with issue IDs, agent IDs, run 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.", @@ -597,12 +597,12 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas multicaAcceptanceTaskCaseRunbookReview: func(started time.Time) multicaAcceptanceTaskCaseMaterial { return multicaAcceptanceTaskCaseMaterial{ Title: "Operator runbook review " + started.Format("150405"), - Description: multicasurface.RootSessionDescription(multicasurface.RootSessionMaterial{ + Description: multicasurface.IssueSurfaceDescription(multicasurface.IssueSurfaceDescriptionMaterial{ 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.", + WorkMode: "Use Mnemon assignments to split documentation review, command validation, and operator risk notes; use Multica to show issue/run/comment/status evidence.", 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.", + "Ask another teammate to verify the validation section covers R3 surface metadata, provider wrapper routing, feedback comments, and final statuses.", "Have the integrator produce a concise runbook change list with priority and owner.", }, Validation: []string{ diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go index b3062a0b..ebc527f4 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go @@ -117,7 +117,7 @@ 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' ;; + *"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. Multica surface input: observed.","created_at":"2026-06-30T09:00:01Z"}]\n' ;; *) printf '{}\n' ;; esac ` @@ -163,8 +163,8 @@ esac "## Execution Plan", "## Parallel PoCs", "## Context Reuse Checks", - "Do not call Multica issue create", - "finish rather than waiting for their own Multica comment projection", + "不要把 Multica 当 canonical hub backend", + "explicit surface writeback commands", "Direct Multica issue fan-out is invalid", "poc-runtime-routing", "poc-operator-runbook", diff --git a/harness/cmd/mnemon-harness/control_test.go b/harness/cmd/mnemon-harness/control_test.go index f421210d..f200aa5b 100644 --- a/harness/cmd/mnemon-harness/control_test.go +++ b/harness/cmd/mnemon-harness/control_test.go @@ -534,12 +534,12 @@ func TestControlTeamworkAssignDefaultsStructuredAssignmentID(t *testing.T) { controlExtID = "assign-default-id" controlTeamworkAssignID = "" controlTeamworkAssignAssignee = "researcher@team" - controlTeamworkAssignScope = "TEA-74 Mnemon R2 hub-flow readiness drill" + controlTeamworkAssignScope = "TEA-74 Mnemon surface-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"} + controlTeamworkAssignEvidence = []string{"TEA-74 root issue is current Multica surface"} var buf bytes.Buffer controlTeamworkAssignCmd.SetOut(&buf) diff --git a/harness/internal/drive/selection_test.go b/harness/internal/drive/selection_test.go index ed4907a5..b74e7e23 100644 --- a/harness/internal/drive/selection_test.go +++ b/harness/internal/drive/selection_test.go @@ -14,13 +14,13 @@ func TestManagedWakeCandidateForRenderMatchesStableIssueIdentity(t *testing.T) { 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."), + renderedWakeEnvelope("derived:second", "assignment/asg-27", "Please handle TEA-27 for the Mnemon surface-flow completion drill."), }, } candidate, ok := ManagedWakeCandidateForRender("planner@team", resp, ManagedWakeMatchMaterial{ IssueID: "issue-123", Identifier: "TEA-27", - Title: "Mnemon R2 hub-flow completion drill", + Title: "Mnemon surface-flow completion drill", TaskID: "task-123", }) if !ok { @@ -39,7 +39,7 @@ func TestManagedWakeCandidateForRenderRequiresStrongRootIdentity(t *testing.T) { 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."), + renderedWakeEnvelope("derived:stale", "assignment/asg-stale", "Assignment asg-stale is yours: Mnemon Multica surface validation. Expected work: inspect an older surface task."), 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."), }, } @@ -75,29 +75,29 @@ 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.", + Title: "Mnemon surface-flow completion drill", + Statement: "Run a small surface-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"} { + for _, want := range []string{"issue-123", "TEA-27", "Mnemon surface-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.") { + if strings.Contains(joined, "Run a small surface-flow readiness drill.") { t.Fatalf("root issue matching must not prefer reusable statement text: %+v", got) } } -func TestManagedWakeMatchTermsPreferAssignmentMailboxIdentity(t *testing.T) { +func TestManagedWakeMatchTermsPreferAssignmentIdentity(t *testing.T) { got := ManagedWakeMatchTerms(ManagedWakeMatchMaterial{ - AssignmentID: "assignment-final-routing-mailbox", + AssignmentID: "assignment-final-routing-surface", AssignmentFingerprint: "fp-1", IssueID: "child-issue", Identifier: "TEA-16", }) - if strings.Join(got, "\n") != "assignment-final-routing-mailbox\nfp-1" { + if strings.Join(got, "\n") != "assignment-final-routing-surface\nfp-1" { t.Fatalf("assignment terms = %+v", got) } } diff --git a/harness/internal/driver/multica_hub.go b/harness/internal/driver/multica_hub.go deleted file mode 100644 index 08435f7c..00000000 --- a/harness/internal/driver/multica_hub.go +++ /dev/null @@ -1,86 +0,0 @@ -package driver - -import ( - "context" - - multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" -) - -const ( - 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 = 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 { - return multicasurface.MulticaHubLedgerPath(root, explicit) -} - -func NewFileMulticaHubLedger(path string) *FileMulticaHubLedger { - return multicasurface.NewFileMulticaHubLedger(path) -} - -func ParseMulticaHubMetadata(raw map[string]any) MulticaHubMetadata { - return multicasurface.ParseMulticaHubMetadata(raw) -} - -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() -} - -func NormalizeMulticaMetadata(raw any) map[string]string { - return multicasurface.NormalizeMulticaMetadata(raw) -} - -func MulticaAssignmentFingerprint(input MulticaAssignmentFingerprintInput) string { - return multicasurface.MulticaAssignmentFingerprint(input) -} - -func MulticaSessionID(rootIssueID string) string { - return multicasurface.MulticaSessionID(rootIssueID) -} diff --git a/harness/internal/driver/multica_surface.go b/harness/internal/driver/multica_surface.go new file mode 100644 index 00000000..dabc3be8 --- /dev/null +++ b/harness/internal/driver/multica_surface.go @@ -0,0 +1,34 @@ +package driver + +import multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" + +const ( + MulticaMetadataEventRef = multicasurface.MulticaMetadataEventRef + MulticaMetadataResourceRef = multicasurface.MulticaMetadataResourceRef + MulticaMetadataProjectionRef = multicasurface.MulticaMetadataProjectionRef + MulticaMetadataSourceArtifactRef = multicasurface.MulticaMetadataSourceArtifactRef + MulticaMetadataSurfaceRole = multicasurface.MulticaMetadataSurfaceRole + MulticaMetadataNoAutoDispatch = multicasurface.MulticaMetadataNoAutoDispatch + MulticaMetadataCaseID = multicasurface.MulticaMetadataCaseID + MulticaMetadataRound = multicasurface.MulticaMetadataRound + MulticaMetadataPoCID = multicasurface.MulticaMetadataPoCID + + MulticaSurfaceRoleDisplay = string(multicasurface.SurfaceRoleDisplay) + MulticaSurfaceRoleActivate = string(multicasurface.SurfaceRoleActivate) + MulticaSurfaceRoleInput = string(multicasurface.SurfaceRoleInput) + MulticaSurfaceRoleDrift = string(multicasurface.SurfaceRoleDrift) +) + +type MulticaSurfaceRefs = multicasurface.SurfaceRefs + +func ParseMulticaSurfaceRefs(raw map[string]any) MulticaSurfaceRefs { + return multicasurface.ParseSurfaceRefs(raw) +} + +func MulticaIssueSurfaceRefs(issue MulticaIssue) MulticaSurfaceRefs { + return ParseMulticaSurfaceRefs(issue.Metadata) +} + +func NormalizeMulticaMetadata(raw any) map[string]string { + return multicasurface.NormalizeMulticaMetadata(raw) +} diff --git a/harness/internal/driver/multica_test.go b/harness/internal/driver/multica_test.go index 0cc51d61..849f463a 100644 --- a/harness/internal/driver/multica_test.go +++ b/harness/internal/driver/multica_test.go @@ -5,8 +5,6 @@ import ( "os" "path/filepath" "strings" - "sync" - "sync/atomic" "testing" ) @@ -277,80 +275,6 @@ printf '{"id":"issue-1","identifier":"TEA-9","title":"Run teamwork","status":"to } } -func TestMulticaHubMetadataDetectsAssignmentMailbox(t *testing.T) { - issue := MulticaIssue{ - ID: "issue-2", - Metadata: 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 !IsMulticaAssignmentMailboxIssue(issue) { - t.Fatalf("issue was not detected as assignment mailbox: %+v", MulticaIssueHubMetadata(issue)) - } - meta := MulticaIssueHubMetadata(issue) - 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 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 TestMulticaCLILoadIssueMetadataMergesListedMetadata(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "args.txt") @@ -358,7 +282,7 @@ func TestMulticaCLILoadIssueMetadataMergesListedMetadata(t *testing.T) { 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' ;; + *"issue metadata list issue-1"*) printf '[{"key":"mnemon.surface_role","value":"display"},{"key":"listed","value":"yes"}]\n' ;; *) printf '{}\n' ;; esac ` @@ -374,14 +298,14 @@ esac loaded, count, err := cli.LoadIssueMetadata(context.Background(), MulticaIssue{ ID: "issue-1", Metadata: map[string]any{ - MulticaMetadataKind: "stale", - "base": "kept", + MulticaMetadataSurfaceRole: "input", + "base": "kept", }, }) if err != nil { t.Fatal(err) } - if count != 2 || loaded.Metadata[MulticaMetadataKind] != MulticaHubKindSession || loaded.Metadata["base"] != "kept" || loaded.Metadata["listed"] != "yes" { + if count != 2 || loaded.Metadata[MulticaMetadataSurfaceRole] != MulticaSurfaceRoleDisplay || 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"}) @@ -394,123 +318,6 @@ esac } } -func TestMulticaAssignmentFingerprintStable(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 TestFileMulticaHubLedgerDedupesRecords(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) - } -} - -func TestFileMulticaHubLedgerReserveIsAtomicAcrossInstances(t *testing.T) { - path := filepath.Join(t.TempDir(), "hub-ledger.jsonl") - source := MulticaHubLedgerSource{ - SessionID: "session-1", - EventID: "progress-1", - AssignmentID: "assignment-1", - Principal: "worker@team", - ProjectionKind: "progress", - } - record := MulticaHubLedgerRecord{ - Kind: MulticaHubKindFeedbackCarrier, - Source: source, - Target: MulticaHubLedgerTarget{ - RootIssueID: "root-1", - ChildIssueID: "child-1", - Status: "reserved", - }, - } - var created int64 - var wg sync.WaitGroup - for i := 0; i < 16; i++ { - wg.Add(1) - go func() { - defer wg.Done() - _, ok, err := NewFileMulticaHubLedger(path).Reserve(record) - if err != nil { - t.Errorf("reserve: %v", err) - return - } - if ok { - atomic.AddInt64(&created, 1) - } - }() - } - wg.Wait() - if created != 1 { - t.Fatalf("reserve winners = %d, want 1", created) - } - records, err := NewFileMulticaHubLedger(path).Records() - if err != nil { - t.Fatal(err) - } - if len(records) != 1 || records[0].Target.Status != "reserved" { - t.Fatalf("reservation records = %+v", records) - } -} - func TestMulticaCLIChildrenAndMetadataCommands(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "args.txt") @@ -518,9 +325,9 @@ func TestMulticaCLIChildrenAndMetadataCommands(t *testing.T) { script := `#!/usr/bin/env sh printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" case "$*" in - *"issue children root-1"*) printf '{"children":[{"id":"child-1","identifier":"TEA-2","title":"Assignment","metadata":{"mnemon.kind":"assignment_mailbox"}}]}\n' ;; - *"issue metadata list child-1"*) printf '[{"key":"mnemon.kind","value":"assignment_mailbox"},{"key":"mnemon.assignment_id","value":"assignment-1"}]\n' ;; - *"issue metadata get child-1 --key mnemon.kind"*) printf '{"key":"mnemon.kind","value":"assignment_mailbox"}\n' ;; + *"issue children root-1"*) printf '{"children":[{"id":"child-1","identifier":"TEA-2","title":"Surface","metadata":{"mnemon.surface_role":"display"}}]}\n' ;; + *"issue metadata list child-1"*) printf '[{"key":"mnemon.surface_role","value":"display"},{"key":"mnemon.event_ref","value":"event-1"}]\n' ;; + *"issue metadata get child-1 --key mnemon.surface_role"*) printf '{"key":"mnemon.surface_role","value":"display"}\n' ;; *"issue metadata set child-1"*) printf '{}\n' ;; *) printf '{}\n' ;; esac @@ -545,14 +352,14 @@ esac if err != nil { t.Fatal(err) } - if meta[MulticaMetadataKind] != MulticaHubKindAssignmentMailbox || meta[MulticaMetadataAssignmentID] != "assignment-1" { + if meta[MulticaMetadataSurfaceRole] != MulticaSurfaceRoleDisplay || meta[MulticaMetadataEventRef] != "event-1" { t.Fatalf("metadata = %+v", meta) } - value, ok, err := cli.GetIssueMetadata(context.Background(), "child-1", MulticaMetadataKind) + value, ok, err := cli.GetIssueMetadata(context.Background(), "child-1", MulticaMetadataSurfaceRole) if err != nil { t.Fatal(err) } - if !ok || value != MulticaHubKindAssignmentMailbox { + if !ok || value != MulticaSurfaceRoleDisplay { t.Fatalf("metadata get = %q ok=%v", value, ok) } if err := cli.SetIssueMetadataMap(context.Background(), "child-1", map[string]string{ @@ -565,7 +372,7 @@ esac for _, want := range []string{ "issue children root-1 --output json", "issue metadata list child-1 --output json", - "issue metadata get child-1 --key mnemon.kind --output json", + "issue metadata get child-1 --key mnemon.surface_role --output json", "issue metadata set child-1 --key a --value one --type string --output json", "issue metadata set child-1 --key b --value two --type string --output json", } { diff --git a/harness/internal/mnemond/presentation/render_test.go b/harness/internal/mnemond/presentation/render_test.go index 38dc216c..717aa856 100644 --- a/harness/internal/mnemond/presentation/render_test.go +++ b/harness/internal/mnemond/presentation/render_test.go @@ -102,7 +102,7 @@ func TestMulticaTeamworkSignalPresentationKeepsHandoffInMnemonProtocol(t *testin for _, want := range []string{ "For Multica-hosted teamwork", "Mnemon assignment events", - "not canonical assignment mailboxes", + "not canonical assignment state", } { if !strings.Contains(resp.Body, want) { t.Fatalf("Multica teamwork cue missing %q:\n%s", want, resp.Body) @@ -165,7 +165,7 @@ func TestMulticaTeamworkPresentationFiltersOtherSessions(t *testing.T) { } } -func TestMulticaAssignmentMailboxPresentationKeepsCurrentAssignment(t *testing.T) { +func TestMulticaSurfacePresentationKeepsCurrentAssignment(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{ @@ -173,8 +173,8 @@ func TestMulticaAssignmentMailboxPresentationKeepsCurrentAssignment(t *testing.T "id": "asg-current", "actor": "planner@team", "assignee": "worker@team", - "scope": "current mailbox work", - "expected_work": "inspect current mailbox", + "scope": "current surface work", + "expected_work": "inspect current surface", "ttl": "30m", "created_at": "2026-06-24T09:45:00Z", }, @@ -182,8 +182,8 @@ func TestMulticaAssignmentMailboxPresentationKeepsCurrentAssignment(t *testing.T "id": "asg-stale", "actor": "planner@team", "assignee": "worker@team", - "scope": "stale mailbox work", - "expected_work": "inspect stale mailbox", + "scope": "stale surface work", + "expected_work": "inspect stale surface", "ttl": "30m", "created_at": "2026-06-24T09:45:00Z", }, @@ -201,10 +201,10 @@ func TestMulticaAssignmentMailboxPresentationKeepsCurrentAssignment(t *testing.T if err != nil { t.Fatal(err) } - if !strings.Contains(resp.Body, "inspect current mailbox") { + if !strings.Contains(resp.Body, "inspect current surface") { 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") { + if strings.Contains(resp.Body, "inspect stale surface") || strings.Contains(resp.Body, "asg-stale") { t.Fatalf("Multica assignment render must not leak stale assignment:\n%s", resp.Body) } } diff --git a/harness/internal/mnemond/presentation/teamwork_presenter.go b/harness/internal/mnemond/presentation/teamwork_presenter.go index 2194a499..06c6de82 100644 --- a/harness/internal/mnemond/presentation/teamwork_presenter.go +++ b/harness/internal/mnemond/presentation/teamwork_presenter.go @@ -80,7 +80,7 @@ func DeriveEventEnvelopes(req Request, proj view.View, now time.Time) []eventmod 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-hosted teamwork, create handoffs as Mnemon assignment events; Multica issues are OA and activation surfaces, not canonical assignment mailboxes." + body += " For Multica-hosted teamwork, create handoffs as Mnemon assignment events; Multica issues are OA and activation surfaces, not canonical assignment state." } if refs := signalContextRefs(signal); len(refs) > 0 { body = fmt.Sprintf("%s Context refs: %s.", body, strings.Join(refs, ", ")) diff --git a/harness/internal/surface/multica/hub.go b/harness/internal/surface/multica/hub.go deleted file mode 100644 index 2579fc1d..00000000 --- a/harness/internal/surface/multica/hub.go +++ /dev/null @@ -1,759 +0,0 @@ -package multica - -import ( - "bufio" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "golang.org/x/sys/unix" -) - -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 RootSessionMetadataMaterial struct { - HubMetadata MulticaHubMetadata - EventID string - EventType string - EventPhase string - Principal string - SourceIssueID string - ProjectionOwner string - 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"` - 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"` - IngestSeq int64 `json:"ingest_seq,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 AssignmentTargetCandidate struct { - SessionID string - AssignmentID string - Principal string - ChildIssueID string -} - -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) { - var records []MulticaHubLedgerRecord - err := l.withLockedLoad(func() error { - records = append([]MulticaHubLedgerRecord(nil), l.records...) - return nil - }) - return records, err -} - -func (l *FileMulticaHubLedger) Find(kind string, source MulticaHubLedgerSource) (MulticaHubLedgerRecord, bool, error) { - var found MulticaHubLedgerRecord - var ok bool - err := l.withLockedLoad(func() error { - want := multicaHubLedgerKey(kind, source) - for i := len(l.records) - 1; i >= 0; i-- { - rec := l.records[i] - if multicaHubLedgerKey(rec.Kind, rec.Source) == want { - found = rec - ok = true - return nil - } - } - return nil - }) - return found, ok, err -} - -func (l *FileMulticaHubLedger) Record(record MulticaHubLedgerRecord) error { - return l.withLockedLoad(func() error { - return l.recordLoaded(record) - }) -} - -func (l *FileMulticaHubLedger) Reserve(record MulticaHubLedgerRecord) (MulticaHubLedgerRecord, bool, error) { - var reserved MulticaHubLedgerRecord - var created bool - err := l.withLockedLoad(func() error { - key := multicaHubLedgerKey(record.Kind, record.Source) - for i := len(l.records) - 1; i >= 0; i-- { - rec := l.records[i] - if multicaHubLedgerKey(rec.Kind, rec.Source) == key { - reserved = rec - return nil - } - } - if err := l.recordLoaded(record); err != nil { - return err - } - reserved = record - created = true - return nil - }) - return reserved, created, err -} - -func (l *FileMulticaHubLedger) recordLoaded(record MulticaHubLedgerRecord) error { - 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 := l.appendRecord(record); err != nil { - return err - } - l.upsertLoaded(record) - return nil -} - -func (l *FileMulticaHubLedger) appendRecord(record MulticaHubLedgerRecord) error { - 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 - } - return f.Close() -} - -func (l *FileMulticaHubLedger) withLockedLoad(fn func() error) error { - if strings.TrimSpace(l.path) == "" { - return fmt.Errorf("multica hub ledger path is required") - } - if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { - return err - } - lock, err := os.OpenFile(l.path+".lock", os.O_CREATE|os.O_RDWR, 0o600) - if err != nil { - return err - } - defer lock.Close() - if err := unix.Flock(int(lock.Fd()), unix.LOCK_EX); err != nil { - return err - } - defer unix.Flock(int(lock.Fd()), unix.LOCK_UN) - l.loaded = false - l.loadErr = nil - l.records = nil - if err := l.load(); err != nil { - return err - } - return fn() -} - -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 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), - 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 { - 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], - 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 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 - 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 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 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, - IngestSeq: item.IngestSeq, - 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, - MulticaMetadataHubBackend, - MulticaMetadataKind, - MulticaMetadataSessionID, - MulticaMetadataCorrelationID, - MulticaMetadataEventID, - MulticaMetadataEventType, - MulticaMetadataEventPhase, - 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) { - 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 deleted file mode 100644 index 8d7a322f..00000000 --- a/harness/internal/surface/multica/hub_test.go +++ /dev/null @@ -1,392 +0,0 @@ -package multica - -import ( - "path/filepath" - "strings" - "testing" - "time" -) - -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 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 ", - 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 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 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) - } - 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 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 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 ", - MulticaMetadataHubBackend: MulticaHubBackend, - MulticaMetadataKind: MulticaHubKindAssignmentMailbox, - MulticaMetadataSessionID: "session-1", - MulticaMetadataCorrelationID: "correlation-1", - MulticaMetadataEventID: "event-1", - MulticaMetadataEventType: "assignment.accepted", - MulticaMetadataEventPhase: "accepted", - 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, - MulticaMetadataEventType, - MulticaMetadataEventPhase, - 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) - 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) - } -} - -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") - } -} diff --git a/harness/internal/surface/multica/issue_surface_text.go b/harness/internal/surface/multica/issue_surface_text.go new file mode 100644 index 00000000..4b266e52 --- /dev/null +++ b/harness/internal/surface/multica/issue_surface_text.go @@ -0,0 +1,50 @@ +package multica + +import "strings" + +type IssueSurfaceDescriptionMaterial struct { + Request string + WorkMode string + Handoffs []string + Validation []string + Completion string +} + +func IssueSurfaceDescription(item IssueSurfaceDescriptionMaterial) string { + var b strings.Builder + writeDescriptionSection(&b, "Request", item.Request) + writeDescriptionSection(&b, "Work mode", item.WorkMode) + writeDescriptionList(&b, "Expected collaboration", item.Handoffs) + writeDescriptionList(&b, "Acceptance", item.Validation) + writeDescriptionSection(&b, "Completion", item.Completion) + return strings.TrimSpace(b.String()) +} + +func writeDescriptionSection(b *strings.Builder, title, body string) { + body = strings.TrimSpace(body) + if body == "" { + return + } + if b.Len() > 0 { + b.WriteString("\n\n") + } + b.WriteString(title) + b.WriteString(":\n") + b.WriteString(body) +} + +func writeDescriptionList(b *strings.Builder, title string, values []string) { + cleaned := cleanSurfaceStrings(values) + if len(cleaned) == 0 { + return + } + if b.Len() > 0 { + b.WriteString("\n\n") + } + b.WriteString(title) + b.WriteString(":") + for _, value := range cleaned { + b.WriteString("\n- ") + b.WriteString(value) + } +} diff --git a/harness/internal/surface/multica/metadata.go b/harness/internal/surface/multica/metadata.go index 4faa5edf..dffdcb7e 100644 --- a/harness/internal/surface/multica/metadata.go +++ b/harness/internal/surface/multica/metadata.go @@ -2,6 +2,43 @@ package multica import "strings" +func NormalizeMulticaMetadata(raw any) map[string]string { + out := map[string]string{} + var visit func(any) + visit = func(value any) { + switch typed := value.(type) { + case nil: + return + case map[string]string: + for key, value := range typed { + if strings.TrimSpace(key) != "" { + out[strings.TrimSpace(key)] = strings.TrimSpace(value) + } + } + case map[string]any: + if key := strings.TrimSpace(anyString(typed["key"])); key != "" { + out[key] = strings.TrimSpace(anyString(typed["value"])) + return + } + if items, ok := typed["metadata"]; ok { + visit(items) + return + } + for key, value := range typed { + if strings.TrimSpace(key) != "" { + out[strings.TrimSpace(key)] = strings.TrimSpace(anyString(value)) + } + } + case []any: + for _, item := range typed { + visit(item) + } + } + } + visit(raw) + return out +} + func MergeIssueMetadata(base map[string]any, listed map[string]string) map[string]any { merged := map[string]any{} for key, value := range base { diff --git a/harness/internal/surface/multica/metadata_test.go b/harness/internal/surface/multica/metadata_test.go index 49e24c21..f5194b34 100644 --- a/harness/internal/surface/multica/metadata_test.go +++ b/harness/internal/surface/multica/metadata_test.go @@ -4,29 +4,29 @@ import "testing" func TestMergeIssueMetadataUsesListedValuesAsAuthoritative(t *testing.T) { base := map[string]any{ - MulticaMetadataKind: "embedded", - MulticaMetadataSessionID: "embedded-session", - "local": 7, + MulticaMetadataSurfaceRole: "input", + MulticaMetadataEventRef: "embedded-event", + "local": 7, } listed := map[string]string{ - MulticaMetadataKind: MulticaHubKindAssignmentMailbox, - MulticaMetadataCorrelationID: "multica:issue:child-1", - "": "ignored", + MulticaMetadataSurfaceRole: string(SurfaceRoleDisplay), + MulticaMetadataResourceRef: "progress_digest/prog-1", + "": "ignored", } got := MergeIssueMetadata(base, listed) - if got[MulticaMetadataKind] != MulticaHubKindAssignmentMailbox { + if got[MulticaMetadataSurfaceRole] != string(SurfaceRoleDisplay) { t.Fatalf("listed metadata should override embedded value: %+v", got) } - if got[MulticaMetadataSessionID] != "embedded-session" || got["local"] != 7 { + if got[MulticaMetadataEventRef] != "embedded-event" || got["local"] != 7 { t.Fatalf("base metadata not preserved: %+v", got) } - if got[MulticaMetadataCorrelationID] != "multica:issue:child-1" { + if got[MulticaMetadataResourceRef] != "progress_digest/prog-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" { + if base[MulticaMetadataSurfaceRole] != "input" { t.Fatalf("base metadata mutated: %+v", base) } } diff --git a/harness/internal/surface/multica/projection.go b/harness/internal/surface/multica/projection.go deleted file mode 100644 index 179fb780..00000000 --- a/harness/internal/surface/multica/projection.go +++ /dev/null @@ -1,659 +0,0 @@ -package multica - -import ( - "fmt" - "strings" - - commentprojection "github.com/mnemon-dev/mnemon/harness/internal/projection" -) - -type AssignmentMailboxMaterial struct { - ID string - SessionID string - Scope string - Assignee string - AssigneeDisplay string - RootIssueID string - RootIssueLabel string - RootIssueTitle string - ExpectedWork string - ExpectedFeedback string - 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 - Handoffs []string - Validation []string - Completion string -} - -type ProgressFeedbackMaterial struct { - AssignmentRef string - FeedbackKind string - Summary string - Result string - Blocker string - ArtifactRefs []string - 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 -} - -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) - if root != "" && topic != "" { - return trimTitle(root + ": " + topic) - } - if topic == "" { - topic = "assignment" - } - 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) - 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 := visibleProtocolText(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) - if rationale := visibleProtocolText(item.Rationale); rationale != "" { - b.WriteString("\n## Rationale\n\n") - b.WriteString(rationale) - b.WriteString("\n") - } - 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") - } - 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, - IngestSeq: item.IngestSeq, - 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") - 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) - 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") - } - if len(item.ArtifactRefs) > 0 { - b.WriteString("\n## Artifacts\n\n") - writeList(&b, item.ArtifactRefs) - } - if len(item.EvidenceRefs) > 0 { - b.WriteString("\n## Evidence\n\n") - writeList(&b, item.EvidenceRefs) - } - return strings.TrimSpace(b.String()) -} - -func RuntimeProjectionCommentBody(item RuntimeProjectionMaterial) string { - var b strings.Builder - if item.AssignmentMailbox { - b.WriteString("## Mnemon Runtime\n\n") - writeBullet(&b, "Status", firstNonEmptyString(item.Status, "correlated")) - writeBullet(&b, "Root issue", IssueMention(firstNonEmptyString(item.RootIssueLabel, item.RootIssueID), item.RootIssueID)) - } else { - 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)) - 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## Effects\n\n") - writeBullet(&b, "Managed wake", codeSpan(item.WakeStatus)) - writeBullet(&b, "Managed turn", codeSpan(item.WakeTurnID)) - if item.HubWriteStatus != "" { - 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()) -} - -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 canonicalProgressStatus(item.FeedbackKind) { - case "blocker": - return StatusBlocked - case "result": - return StatusDone - case "progress": - return StatusInProgress - case "review": - return StatusInReview - case "waiting": - return StatusTodo - case "cancelled": - return StatusCancelled - } - if strings.TrimSpace(item.Blocker) != "" { - return StatusBlocked - } - if strings.TrimSpace(item.Result) != "" { - return StatusDone - } - return "" -} - -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 "" - } -} - -func ProgressCompletesAssignment(item ProgressFeedbackMaterial) bool { - return ProgressIssueStatus(item) == StatusDone -} - -func IssueStatusDone(status string) bool { - return CanonicalIssueStatus(status) == StatusDone -} - -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 - } - for _, candidate := range []string{scope, firstSentence(item.ExpectedWork), idTopic, item.ID} { - candidate = stripTitleRootLabel(candidate, item.RootIssueLabel) - if strings.TrimSpace(candidate) != "" { - return candidate - } - } - 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") || - strings.Contains(lower, "readiness") || - strings.Contains(lower, "hub-flow") -} - -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 == "" { - 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 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", "r1", "r2", "drill", "readiness", 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 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) - 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 visibleExpectedFeedback(value string) string { - value = visibleProtocolText(value) - if value == "" { - return "" - } - lower := strings.ToLower(value) - for _, prefix := range []string{"runtime feedback with "} { - if strings.HasPrefix(lower, prefix) { - return strings.TrimSpace(value[len(prefix):]) - } - } - if lower == "runtime feedback" { - return "progress, result, or blocker" - } - 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 "": - 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 == "" { - 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 deleted file mode 100644 index 1d93549b..00000000 --- a/harness/internal/surface/multica/projection_test.go +++ /dev/null @@ -1,447 +0,0 @@ -package multica - -import ( - "strings" - "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", - 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 != "TEA-1: runtime/readiness" { - t.Fatalf("title = %q", got) - } - body := AssignmentMailboxDescription(item) - for _, want := range []string{ - "## Assignment", - "Root issue: [TEA-1](mention://issue/root-1) - Validate runtime", - "Assignee: `researcher@team` (mnemon-researcher)", - "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) - } - } - 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) - } - } -} - -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", - 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 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 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", - 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", - 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 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", - 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", - FeedbackKind: "result", - Summary: "Done", - Result: "Validated", - ArtifactRefs: []string{"run-1"}, - EvidenceRefs: []string{"comment-1"}, - } - body := ProgressCommentBody(item) - for _, want := range []string{ - "## Feedback", - "Status: result", - "## Summary", - "Done", - "## Result", - "Validated", - "## Artifacts", - "- run-1", - "## Evidence", - "- comment-1", - } { - if !strings.Contains(body, want) { - 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) - } - if !ProgressCompletesAssignment(item) { - t.Fatal("result progress should complete assignment") - } - if !IssueStatusDone("completed") { - t.Fatal("completed should be terminal") - } -} - -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", - 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 Runtime", - "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`", - "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) { - 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{ - "## Mnemon Runtime", - "Status: correlated", - "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) - } - } - 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) { - 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/util.go b/harness/internal/surface/multica/util.go new file mode 100644 index 00000000..03165a08 --- /dev/null +++ b/harness/internal/surface/multica/util.go @@ -0,0 +1,44 @@ +package multica + +import ( + "fmt" + "strings" +) + +func firstNonEmptyString(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} + +func cleanMulticaRefs(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 anyString(value any) string { + switch typed := value.(type) { + case nil: + return "" + case string: + return typed + case fmt.Stringer: + return typed.String() + case []byte: + return string(typed) + default: + return fmt.Sprint(typed) + } +} From 945defef12d7610b714a99c109edb055324da97f Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:51:41 +0800 Subject: [PATCH 08/11] feat: add r3 multica provider surface flow Run Multica turns through an optional provider command after surface ingest, preserving provider stdout as the final answer. Add explicit surface-report and activation-carrier commands for accepted-state display and execution routing, with surface_ref metadata and a surface write ledger replacing projection terminology. Refresh Multica acceptance cases around Chinese R3 OA scenarios, provider wrapper readiness, display-only writeback, activation carriers, and child surface expectations. Validation: go test ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-acceptance ./harness/internal/surface/multica ./harness/internal/driver ./harness/internal/coreguard ./harness/internal/mnemond/presentation ./harness/internal/drive; go build -o mnemon . --- .../acceptance_multica_runtime.go | 23 +- .../acceptance_multica_task_cases.go | 82 +++---- .../acceptance_multica_task_cases_test.go | 12 +- harness/cmd/mnemon-harness/multica.go | 226 ++++++++++++++++-- harness/cmd/mnemon-harness/multica_test.go | 190 ++++++++++++++- harness/cmd/mnemon-harness/root_test.go | 5 +- harness/cmd/mnemon-multica-runtime/main.go | 116 +++++++++ .../cmd/mnemon-multica-runtime/main_test.go | 95 ++++++++ harness/internal/driver/multica_surface.go | 2 +- harness/internal/surface/multica/ledger.go | 84 +++---- .../surface/multica/runtime_config.go | 4 + harness/internal/surface/multica/surface.go | 10 +- .../surface/multica/surface_r3_test.go | 32 +-- 13 files changed, 722 insertions(+), 159 deletions(-) diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index 300bfe99..f7ab41a6 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -30,7 +30,6 @@ var ( acceptanceMulticaWait time.Duration acceptanceMulticaPoll time.Duration acceptanceMulticaRequireIngest bool - acceptanceMulticaRequireManagedWake bool acceptanceMulticaRequireSurfaceFlow bool acceptanceMulticaMinParticipants int acceptanceMulticaMinActiveAgents int @@ -54,7 +53,6 @@ var acceptanceMulticaRuntimeCmd = &cobra.Command{ Wait: acceptanceMulticaWait, Poll: acceptanceMulticaPoll, RequireIngest: acceptanceMulticaRequireIngest, - RequireManagedWake: acceptanceMulticaRequireManagedWake, RequireSurfaceFlow: acceptanceMulticaRequireSurfaceFlow, MinParticipants: acceptanceMulticaMinParticipants, MinActiveAgents: acceptanceMulticaMinActiveAgents, @@ -82,13 +80,12 @@ 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(&acceptanceMulticaTaskCase, "task-case", multicaAcceptanceTaskCaseR3Surface, "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") acceptanceMulticaRuntimeCmd.Flags().DurationVar(&acceptanceMulticaPoll, "poll", 5*time.Second, "poll interval for Multica runs") acceptanceMulticaRuntimeCmd.Flags().BoolVar(&acceptanceMulticaRequireIngest, "require-mnemon-ingest", true, "require run output to show recorded Mnemon ingest") - acceptanceMulticaRuntimeCmd.Flags().BoolVar(&acceptanceMulticaRequireManagedWake, "require-managed-wake", false, "require run output to show a completed managed wake") acceptanceMulticaRuntimeCmd.Flags().BoolVar(&acceptanceMulticaRequireSurfaceFlow, "require-surface-flow", false, "require R3 Multica surface evidence: provider-wrapper env, root run, readable metadata, and visible OA state") acceptanceMulticaRuntimeCmd.Flags().IntVar(&acceptanceMulticaMinParticipants, "min-participants", 5, "minimum Multica participant agents required for surface-flow acceptance") acceptanceMulticaRuntimeCmd.Flags().IntVar(&acceptanceMulticaMinActiveAgents, "min-active-agents", 3, "minimum distinct Multica agents with provider-wrapper readiness for surface-flow acceptance") @@ -109,7 +106,6 @@ type multicaRuntimeProdSimOptions struct { Wait time.Duration Poll time.Duration RequireIngest bool - RequireManagedWake bool RequireSurfaceFlow bool MinParticipants int MinActiveAgents int @@ -178,7 +174,7 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime } requestedTaskCase := strings.TrimSpace(opts.TaskCase) if requestedTaskCase == "" { - requestedTaskCase = multicaAcceptanceTaskCaseR2Readiness + requestedTaskCase = multicaAcceptanceTaskCaseR3Surface } taskCase, taskCaseErr := multicaAcceptanceTaskCase(requestedTaskCase, started) runRoot := strings.TrimSpace(opts.RunRoot) @@ -322,9 +318,6 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime if opts.RequireIngest { addMulticaProdSimAssertion(&report, "runtime observed Multica surface input", strings.Contains(combined, "Multica surface input: observed"), combined) } - if opts.RequireManagedWake { - addMulticaProdSimAssertion(&report, "runtime completed managed wake", multicaMessagesContainManagedWakeCompleted(combined), combined) - } } if opts.RequireSurfaceFlow { rootRuntimeActivity := multicaMessagesExposeRuntimeActivity(report.MessageTypes) || len(runs) > 0 @@ -465,13 +458,13 @@ func multicaProdSimSnapshotContext(ctx context.Context) (context.Context, contex return ctx, func() {} } -func multicaProdSimPartialSnapshotDetail(reason, runErr, projectionErr error, report *multicaRuntimeProdSimReport) string { +func multicaProdSimPartialSnapshotDetail(reason, runErr, surfaceErr 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()) + if surfaceErr != nil { + parts = append(parts, "surface="+surfaceErr.Error()) } parts = append(parts, fmt.Sprintf("child_runs=%d", len(report.ChildRuns)), @@ -579,12 +572,6 @@ func combinedMulticaRunMessages(messages []driver.MulticaRunMessage) string { return strings.Join(parts, "\n") } -func multicaMessagesContainManagedWakeCompleted(text string) bool { - lower := strings.ToLower(text) - return strings.Contains(lower, "managed wake: completed") || - strings.Contains(lower, "managed wake completed") -} - func multicaRunMessageTypeCounts(messages []driver.MulticaRunMessage) map[string]int { out := map[string]int{} for _, message := range messages { diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go index 92d5fac2..990d596b 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases.go @@ -12,7 +12,7 @@ import ( ) const ( - multicaAcceptanceTaskCaseR2Readiness = "r2-readiness" + multicaAcceptanceTaskCaseR3Surface = "r3-surface-readiness" multicaAcceptanceTaskCaseProtocolReAct = "protocol-react-drill" multicaAcceptanceTaskCaseParallelPoc = "parallel-poc-overlap" multicaAcceptanceTaskCaseReleaseReadiness = "release-readiness" @@ -33,11 +33,11 @@ type multicaAcceptanceTaskCaseMaterial struct { } type multicaAcceptanceTaskCaseExpectations struct { - MinActiveAgents int `json:"min_active_agents,omitempty"` - InitialChildMailboxes int `json:"initial_child_mailboxes,omitempty"` - MinChildMailboxes int `json:"min_child_mailboxes,omitempty"` - MinFeedbackComments int `json:"min_feedback_comments,omitempty"` - TeamworkRounds []string `json:"teamwork_rounds,omitempty"` + MinActiveAgents int `json:"min_active_agents,omitempty"` + InitialChildSurfaces int `json:"initial_child_surfaces,omitempty"` + MinChildSurfaces int `json:"min_child_surfaces,omitempty"` + MinFeedbackComments int `json:"min_feedback_comments,omitempty"` + TeamworkRounds []string `json:"teamwork_rounds,omitempty"` } type multicaAcceptanceExecutionPlan struct { @@ -89,7 +89,7 @@ func multicaAcceptanceTaskCaseNames() []string { func multicaAcceptanceTaskCase(id string, started time.Time) (multicaAcceptanceTaskCaseMaterial, error) { id = strings.TrimSpace(id) if id == "" { - id = multicaAcceptanceTaskCaseR2Readiness + id = multicaAcceptanceTaskCaseR3Surface } build, ok := multicaAcceptanceTaskCases[id] if !ok { @@ -327,33 +327,33 @@ func multicaAcceptancePathSegment(value, fallback string) string { } var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTaskCaseMaterial{ - multicaAcceptanceTaskCaseR2Readiness: func(started time.Time) multicaAcceptanceTaskCaseMaterial { + multicaAcceptanceTaskCaseR3Surface: func(started time.Time) multicaAcceptanceTaskCaseMaterial { return multicaAcceptanceTaskCaseMaterial{ - Title: "Mnemon Multica runtime prod-sim " + started.Format("150405"), + Title: "Mnemon R3 Multica surface readiness " + started.Format("150405"), Description: multicasurface.IssueSurfaceDescription(multicasurface.IssueSurfaceDescriptionMaterial{ - Request: "Run a small Mnemon R2 Multica readiness drill.", - WorkMode: "Use Mnemon teamwork; Multica shows issues, runs, comments, and statuses.", + Request: "运行一次小型 Mnemon R3 Multica surface readiness 演练,验证 Multica 作为 OA/runtime 体验层时,provider wrapper、surface ingest、显式 report 写回和 activation carrier 的边界是否清晰。", + WorkMode: "Mnemon EventEnvelope 和 mnemond accepted state 保持 canonical;Multica 只展示 issue、run、comment、status,并通过明确的 surface-report 或 activation-carrier 命令参与交互。", Handoffs: []string{ - "Route root session visibility and child issue routing checks to separate teammates.", - "After teammate feedback is visible, route a final integration check.", + "第一轮让两个 teammate 分别检查 provider wrapper run visibility 和 root surface metadata。", + "在第一轮反馈可见后,由 integrator 创建或要求一个 activation carrier 来验证下一轮调度入口。", }, Validation: []string{ - "Root issue carries session metadata and shows run activity.", - "Accepted Mnemon events can be exposed as Multica surface state without becoming canonical state.", - "Agent-visible updates are written back through explicit commands and carry R3 surface metadata.", - "Stale or cross-session assignment material is ignored.", - "Final root status reflects completion.", + "Root issue shows Multica run activity and readable R3 surface metadata, without legacy hub/mailbox keys.", + "Accepted Mnemon events are exposed through display-only report writeback without triggering provider work.", + "需要执行时使用 activation carrier,并携带 event_ref/resource_ref,而不是靠 display comment 触发。", + "显式写回的 comment/status 对运营人员可读,machine-only 字段只在 metadata 或 stable marker 中出现。", + "Final root status reflects the accepted integration decision rather than raw Multica task state.", }, - Completion: "Finish when child feedback comments are visible and the root issue reaches a terminal status.", + Completion: "当第一轮反馈、activation carrier 证据、最终 surface-report comment 和 root terminal status 都可见时完成。", }), Expectations: multicaAcceptanceTaskCaseExpectations{ MinActiveAgents: 3, - MinChildMailboxes: 2, + MinChildSurfaces: 2, MinFeedbackComments: 2, TeamworkRounds: []string{ - "Round 1: root issue intake and first assignment split", - "Round 2: teammate feedback through Mnemon events", - "Round 3: integration and final status projection", + "Round 1: root surface intake and first provider-wrapper visibility split", + "Round 2: activation carrier check for an accepted event", + "Round 3: integration and display-only surface-report status", }, }, } @@ -380,7 +380,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }), Expectations: multicaAcceptanceTaskCaseExpectations{ MinActiveAgents: 4, - MinChildMailboxes: 3, + MinChildSurfaces: 3, MinFeedbackComments: 3, TeamworkRounds: []string{ "Round 1 - Observe: split root metadata, routing, and run-visibility checks", @@ -395,7 +395,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas Title: "Parallel PoC overlap drill " + started.Format("150405"), Description: multicasurface.IssueSurfaceDescription(multicasurface.IssueSurfaceDescriptionMaterial{ Request: "运行一个接近真实业务的 Mnemon-on-Multica 协作 case:三个有重叠上下文的 PoC 并发推进。目标是验证 Multica 作为 OA/runtime 体验层时,provider wrapper 调度、显式状态回写、以及跨 workstream 上下文复用是否清晰可靠。", - WorkMode: "Multica 只承担可见 issue、run、comment、status 的 OA 体验;工作拆分必须先进入 Mnemon assignment/progress/integration 事件。不要把 Multica 当 canonical hub backend,也不要通过 Multica issue fan-out 替代 Mnemon 协议。planner 需要把 case 拆成三个并行 PoC,要求每个反馈引用 shared context,并在第一轮后创建至少一个 follow-up event。", + WorkMode: "Multica 只承担可见 issue、run、comment、status 的 OA 体验;工作拆分必须先进入 Mnemon assignment/progress/integration 事件。不要把 Multica 当 canonical state store,也不要用普通 display issue 替代 activation carrier。planner 需要把 case 拆成三个并行 PoC,要求每个反馈引用 shared context,并在第一轮后创建至少一个 follow-up event。", 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.", @@ -411,10 +411,10 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas 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, - InitialChildMailboxes: 3, - MinChildMailboxes: 4, - MinFeedbackComments: 4, + MinActiveAgents: 5, + InitialChildSurfaces: 3, + MinChildSurfaces: 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", @@ -438,10 +438,10 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }, { ID: "poc-release-risk", - Title: "Release decision and product status projection", + Title: "Release decision and product status writeback", 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"}, + ExpectedArtifacts: []string{"release-risk-matrix.md", "status-writeback-evidence.md", "ship-hold-decision.md"}, }, }, Roles: []multicaAcceptanceRolePlan{ @@ -481,7 +481,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas Primary: []string{"poc-release-risk"}, Overlaps: []string{"poc-operator-runbook"}, Responsibilities: []string{ - "Challenge release readiness with rollback and status-projection evidence.", + "Challenge release readiness with rollback and status-writeback evidence.", "Identify disagreement between PoC outputs before final integration.", }, }, @@ -528,8 +528,8 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas "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.", - "Direct Multica issue fan-out is invalid; all work must originate from Mnemon assignment/progress/integration events and explicit surface writeback commands.", - "No visible issue text should expose session ids, assignment ids, assignment fingerprints, or projection-owner keys.", + "Ordinary Multica display issues must not trigger execution; execution carriers must originate from accepted Mnemon events and explicit activation-carrier commands.", + "No visible issue text should expose session ids, assignment ids, assignment fingerprints, or surface owner keys.", }, } }, @@ -554,7 +554,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }), Expectations: multicaAcceptanceTaskCaseExpectations{ MinActiveAgents: 4, - MinChildMailboxes: 3, + MinChildSurfaces: 3, MinFeedbackComments: 3, TeamworkRounds: []string{ "Round 1: risk and operator-path review", @@ -571,12 +571,12 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas Request: "Triage a production-style regression report where Multica run activity appears, but Mnemon event observation or explicit surface writeback appears 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.", + "Assign one teammate to inspect routing evidence and identify whether surface metadata is complete enough for correlation.", + "Assign one teammate to inspect display report and status-writeback 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.", + "The triage identifies whether the problem is intake, provider-wrapper routing, activation carrier, display report, status writeback, or Multica run visibility.", "Each work slice reports evidence with issue IDs, agent IDs, run IDs, and observed status.", "The root issue records a mitigation decision rather than only stating that the flow completed.", }, @@ -584,10 +584,10 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }), Expectations: multicaAcceptanceTaskCaseExpectations{ MinActiveAgents: 4, - MinChildMailboxes: 3, + MinChildSurfaces: 3, MinFeedbackComments: 3, TeamworkRounds: []string{ - "Round 1: diagnose intake, routing, and feedback projection", + "Round 1: diagnose intake, provider routing, and display report writeback", "Round 2: act on the leading failure mode with a mitigation check", "Round 3: reflect into incident decision and next verification", }, @@ -614,7 +614,7 @@ var multicaAcceptanceTaskCases = map[string]func(time.Time) multicaAcceptanceTas }), Expectations: multicaAcceptanceTaskCaseExpectations{ MinActiveAgents: 4, - MinChildMailboxes: 3, + MinChildSurfaces: 3, MinFeedbackComments: 3, TeamworkRounds: []string{ "Round 1: install, validation, and risk review", diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go index ebc527f4..a217ae1e 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_task_cases_test.go @@ -29,7 +29,7 @@ func TestMulticaTaskCaseProtocolReActIsMultiRound(t *testing.T) { } } if material.Expectations.MinActiveAgents != 4 || - material.Expectations.MinChildMailboxes != 3 || + material.Expectations.MinChildSurfaces != 3 || material.Expectations.MinFeedbackComments != 3 || len(material.Expectations.TeamworkRounds) != 3 { t.Fatalf("unexpected expectations: %+v", material.Expectations) @@ -143,8 +143,8 @@ esac } if report.TaskCase != multicaAcceptanceTaskCaseParallelPoc || report.TaskExpectations.MinActiveAgents != 5 || - report.TaskExpectations.InitialChildMailboxes != 3 || - report.TaskExpectations.MinChildMailboxes != 4 || + report.TaskExpectations.InitialChildSurfaces != 3 || + report.TaskExpectations.MinChildSurfaces != 4 || len(report.ExecutionPlan.Workstreams) != 3 { t.Fatalf("task case report mismatch: %+v", report) } @@ -163,9 +163,9 @@ esac "## Execution Plan", "## Parallel PoCs", "## Context Reuse Checks", - "不要把 Multica 当 canonical hub backend", - "explicit surface writeback commands", - "Direct Multica issue fan-out is invalid", + "不要把 Multica 当 canonical state store", + "activation carrier", + "activation-carrier commands", "poc-runtime-routing", "poc-operator-runbook", "poc-release-risk", diff --git a/harness/cmd/mnemon-harness/multica.go b/harness/cmd/mnemon-harness/multica.go index 4f267b62..2aaa83d4 100644 --- a/harness/cmd/mnemon-harness/multica.go +++ b/harness/cmd/mnemon-harness/multica.go @@ -13,7 +13,6 @@ 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" multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" "github.com/spf13/cobra" ) @@ -40,11 +39,22 @@ var ( multicaLocalToken string multicaLocalTokenFile string - multicaCommentContent string - multicaCommentFile string - multicaCommentStdin bool - multicaCommentTitle string - multicaCommentEvents []string + multicaCommentContent string + multicaCommentFile string + multicaCommentStdin bool + multicaCommentTitle string + multicaSurfaceStatusLabel string + multicaSurfaceSummary string + multicaSurfaceDesiredStatus string + multicaSurfaceEventRef string + multicaSurfaceResourceRef string + multicaSurfaceRef string + multicaSurfaceSourceArtifactRef string + multicaSurfaceEvidenceRefs []string + multicaSurfaceArtifactRefs []string + multicaSurfaceAssigneeAgentID string + multicaSurfaceAssignedToProvider bool + multicaSurfaceTargetAgentID string multicaProvisionRegistry string multicaProvisionProjectRoot string @@ -102,11 +112,18 @@ var multicaImportIssueCmd = &cobra.Command{ RunE: runMulticaImportIssue, } -var multicaProjectCommentCmd = &cobra.Command{ - Use: "project-comment", - Short: "Write a Mnemon update as a Multica issue comment", +var multicaSurfaceReportCmd = &cobra.Command{ + Use: "surface-report", + Short: "Write accepted Mnemon state to a Multica display surface", Hidden: true, - RunE: runMulticaProjectComment, + RunE: runMulticaSurfaceReport, +} + +var multicaActivationCarrierCmd = &cobra.Command{ + Use: "activation-carrier", + Short: "Create a Multica activation carrier for an accepted Mnemon event", + Hidden: true, + RunE: runMulticaActivationCarrier, } var multicaProvisionCmd = &cobra.Command{ @@ -300,7 +317,25 @@ func runMulticaImportIssue(cmd *cobra.Command, args []string) error { return nil } -func runMulticaProjectComment(cmd *cobra.Command, args []string) error { +type multicaSurfaceReport struct { + IssueID string `json:"issue_id"` + Comment driver.MulticaComment `json:"comment"` + Metadata map[string]string `json:"metadata"` + Status string `json:"status,omitempty"` + SkippedStatusReason string `json:"skipped_status_reason,omitempty"` + StatusIssue *driver.MulticaIssue `json:"status_issue,omitempty"` + NoAutoDispatch bool `json:"no_auto_dispatch"` + SourceArtifact string `json:"source_artifact,omitempty"` +} + +type multicaActivationCarrierReport struct { + ParentIssueID string `json:"parent_issue_id"` + Issue driver.MulticaIssue `json:"issue"` + Metadata map[string]string `json:"metadata"` + TargetAgentID string `json:"target_agent_id,omitempty"` +} + +func runMulticaSurfaceReport(cmd *cobra.Command, args []string) error { if strings.TrimSpace(multicaIssueID) == "" { return fmt.Errorf("--issue-id is required") } @@ -308,24 +343,169 @@ func runMulticaProjectComment(cmd *cobra.Command, args []string) error { if err != nil { return err } - body := projection.FormatComment(projection.CommentMaterial{ - Title: multicaCommentTitle, - Body: content, - EventIDs: multicaCommentEvents, + plan, err := multicasurface.BuildDisplayWritebackPlan(multicasurface.DisplayWritebackRequest{ + IssueID: strings.TrimSpace(multicaIssueID), + Refs: multicasurface.SurfaceRefs{ + EventRef: multicaSurfaceEventRef, + ResourceRef: multicaSurfaceResourceRef, + SurfaceRef: multicaSurfaceRef, + SourceArtifactRef: multicaSurfaceSourceArtifactRef, + }, + Title: multicaCommentTitle, + StatusLabel: multicaSurfaceStatusLabel, + Summary: multicaSurfaceSummary, + Result: content, + EvidenceRefs: multicaSurfaceEvidenceRefs, + ArtifactRefs: multicaSurfaceArtifactRefs, + DesiredStatus: multicaSurfaceDesiredStatus, + AssigneeAgentID: multicaSurfaceAssigneeAgentID, + AssignedToProvider: multicaSurfaceAssignedToProvider, }) - comment, err := multicaCLI().AddIssueComment(multicaCommandContext(cmd), multicaIssueID, body) if err != nil { return err } + ctx := multicaCommandContext(cmd) + cli := multicaCLI() + comment, err := cli.AddIssueComment(ctx, plan.IssueID, plan.CommentBody) + if err != nil { + return err + } + if err := cli.SetIssueMetadataMap(ctx, plan.IssueID, plan.Metadata); err != nil { + return err + } + report := multicaSurfaceReport{ + IssueID: plan.IssueID, + Comment: comment, + Metadata: plan.Metadata, + Status: plan.Status, + SkippedStatusReason: plan.SkippedStatusReason, + NoAutoDispatch: plan.NoAutoDispatch, + SourceArtifact: plan.Metadata[multicasurface.MulticaMetadataSourceArtifactRef], + } + if plan.Status != "" { + issue, err := cli.SetIssueStatus(ctx, plan.IssueID, plan.Status) + if err != nil { + return err + } + report.StatusIssue = &issue + } if multicaJSON { enc := json.NewEncoder(cmd.OutOrStdout()) enc.SetIndent("", " ") - return enc.Encode(comment) + return enc.Encode(report) + } + fmt.Fprintf(cmd.OutOrStdout(), "reported issue %s comment=%s", plan.IssueID, comment.ID) + if plan.Status != "" { + fmt.Fprintf(cmd.OutOrStdout(), " status=%s", plan.Status) + } + if plan.SkippedStatusReason != "" { + fmt.Fprintf(cmd.OutOrStdout(), " status-skipped=%q", plan.SkippedStatusReason) } - fmt.Fprintf(cmd.OutOrStdout(), "commented issue %s comment=%s\n", multicaIssueID, comment.ID) + fmt.Fprintln(cmd.OutOrStdout()) return nil } +func runMulticaActivationCarrier(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(multicaIssueID) == "" { + return fmt.Errorf("--issue-id is required") + } + content, err := readMulticaTextInput(cmd.InOrStdin(), multicaCommentContent, multicaCommentFile, multicaCommentStdin) + if err != nil { + return err + } + carrier, err := multicasurface.BuildActivationCarrier(multicasurface.ActivationCarrierRequest{ + IssueID: multicaIssueID, + EventRef: multicaSurfaceEventRef, + ResourceRef: multicaSurfaceResourceRef, + CarrierTitle: multicaCommentTitle, + TargetAgentID: multicaSurfaceTargetAgentID, + }) + if err != nil { + return err + } + description := formatActivationCarrierDescription(content, carrier.Metadata) + ctx := multicaCommandContext(cmd) + cli := multicaCLI() + issue, err := cli.CreateIssue(ctx, driver.MulticaCreateIssueRequest{ + Title: carrier.Title, + Description: description, + ParentID: carrier.IssueID, + AssigneeID: strings.TrimSpace(multicaSurfaceTargetAgentID), + Status: multicasurface.StatusTodo, + Priority: "medium", + AllowDuplicate: true, + }) + if err != nil { + return err + } + if strings.TrimSpace(issue.ID) == "" { + return fmt.Errorf("created Multica activation carrier did not return an issue id") + } + if err := cli.SetIssueMetadataMap(ctx, issue.ID, carrier.Metadata); err != nil { + return err + } + report := multicaActivationCarrierReport{ + ParentIssueID: carrier.IssueID, + Issue: issue, + Metadata: carrier.Metadata, + TargetAgentID: strings.TrimSpace(multicaSurfaceTargetAgentID), + } + if multicaJSON { + enc := json.NewEncoder(cmd.OutOrStdout()) + enc.SetIndent("", " ") + return enc.Encode(report) + } + fmt.Fprintf(cmd.OutOrStdout(), "created activation carrier parent=%s issue=%s\n", carrier.IssueID, issue.ID) + return nil +} + +func formatActivationCarrierDescription(body string, metadata map[string]string) string { + var b strings.Builder + if text := strings.TrimSpace(body); text != "" { + b.WriteString(text) + b.WriteString("\n\n") + } + b.WriteString("Mnemon 激活载体\n\n") + b.WriteString("- 事件引用: ") + b.WriteString(metadata[multicasurface.MulticaMetadataEventRef]) + b.WriteString("\n") + b.WriteString("- 资源引用: ") + b.WriteString(metadata[multicasurface.MulticaMetadataResourceRef]) + b.WriteString("\n\n") + b.WriteString("该 issue 用于通过 Multica 原生调度触发 provider runtime;canonical state 仍由 mnemond 的 accepted event 决定。") + return strings.TrimSpace(b.String()) +} + +func addMulticaSurfaceReportFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&multicaIssueID, "issue-id", "", "Multica issue ID") + cmd.Flags().StringVar(&multicaCommentContent, "content", "", "comment result body") + cmd.Flags().StringVar(&multicaCommentFile, "content-file", "", "read result body from a file") + cmd.Flags().BoolVar(&multicaCommentStdin, "content-stdin", false, "read result body from stdin") + cmd.Flags().StringVar(&multicaCommentTitle, "title", "", "short Mnemon update title") + cmd.Flags().StringVar(&multicaSurfaceStatusLabel, "status-label", "", "human-readable status label") + cmd.Flags().StringVar(&multicaSurfaceSummary, "summary", "", "short display summary") + cmd.Flags().StringVar(&multicaSurfaceDesiredStatus, "desired-status", "", "optional Multica issue status") + cmd.Flags().StringVar(&multicaSurfaceEventRef, "event-ref", "", "accepted Mnemon event reference") + cmd.Flags().StringVar(&multicaSurfaceResourceRef, "resource-ref", "", "accepted Mnemon resource reference") + cmd.Flags().StringVar(&multicaSurfaceRef, "surface-ref", "", "surface writeback reference") + cmd.Flags().StringVar(&multicaSurfaceSourceArtifactRef, "source-artifact-ref", "", "source artifact reference") + cmd.Flags().StringArrayVar(&multicaSurfaceEvidenceRefs, "evidence", nil, "evidence reference; may be repeated") + cmd.Flags().StringArrayVar(&multicaSurfaceArtifactRefs, "artifact", nil, "artifact reference; may be repeated") + cmd.Flags().StringVar(&multicaSurfaceAssigneeAgentID, "assignee-agent-id", "", "current Multica assignee agent id") + cmd.Flags().BoolVar(&multicaSurfaceAssignedToProvider, "assigned-to-provider", false, "treat current assignee as provider-backed") +} + +func addMulticaActivationCarrierFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&multicaIssueID, "issue-id", "", "parent Multica issue ID") + cmd.Flags().StringVar(&multicaCommentContent, "content", "", "activation carrier description") + cmd.Flags().StringVar(&multicaCommentFile, "content-file", "", "read activation carrier description from a file") + cmd.Flags().BoolVar(&multicaCommentStdin, "content-stdin", false, "read activation carrier description from stdin") + cmd.Flags().StringVar(&multicaCommentTitle, "title", "", "activation carrier title") + cmd.Flags().StringVar(&multicaSurfaceEventRef, "event-ref", "", "accepted Mnemon event reference") + cmd.Flags().StringVar(&multicaSurfaceResourceRef, "resource-ref", "", "accepted Mnemon resource reference") + cmd.Flags().StringVar(&multicaSurfaceTargetAgentID, "target-agent-id", "", "Multica agent id to assign the activation carrier to") +} + func runMulticaParticipantRegister(cmd *cobra.Command, args []string) error { ctx := multicaCommandContext(cmd) if strings.TrimSpace(multicaWorkspaceID) == "" { @@ -1003,12 +1183,8 @@ func init() { multicaImportIssueCmd.Flags().StringVar(&multicaLocalTokenFile, "token-file", envDefault("MNEMON_CONTROL_TOKEN_FILE", ""), "read Local Mnemon bearer token from a file") multicaImportIssueCmd.Flags().BoolVar(&multicaDryRun, "dry-run", false, "print the Mnemon observed draft without writing") - multicaProjectCommentCmd.Flags().StringVar(&multicaIssueID, "issue-id", "", "Multica issue ID") - multicaProjectCommentCmd.Flags().StringVar(&multicaCommentContent, "content", "", "comment body") - multicaProjectCommentCmd.Flags().StringVar(&multicaCommentFile, "content-file", "", "read comment body from a file") - multicaProjectCommentCmd.Flags().BoolVar(&multicaCommentStdin, "content-stdin", false, "read comment body from stdin") - multicaProjectCommentCmd.Flags().StringVar(&multicaCommentTitle, "title", "", "short Mnemon update title") - multicaProjectCommentCmd.Flags().StringArrayVar(&multicaCommentEvents, "event", nil, "Mnemon event marker; may be repeated") + addMulticaSurfaceReportFlags(multicaSurfaceReportCmd) + addMulticaActivationCarrierFlags(multicaActivationCarrierCmd) multicaProvisionCmd.Flags().StringVar(&multicaProvisionRegistry, "registry", "", "Multica registry path") multicaProvisionCmd.Flags().StringVar(&multicaProvisionProjectRoot, "project-root", ".", "project root for the default registry path") @@ -1048,7 +1224,7 @@ func init() { multicaParticipantRegisterCmd.Flags().DurationVar(&multicaParticipantProviderTimeout, "provider-turn-timeout", 0, "provider turn timeout injected into participant runtime env") multicaParticipantCmd.AddCommand(multicaParticipantRegisterCmd) - multicaCmd.AddCommand(multicaProbeCmd, multicaParticipantCmd, multicaProvisionCmd, multicaImportIssueCmd, multicaProjectCommentCmd) + multicaCmd.AddCommand(multicaProbeCmd, multicaParticipantCmd, multicaProvisionCmd, multicaImportIssueCmd, multicaSurfaceReportCmd, multicaActivationCarrierCmd) multicaCmd.GroupID = groupAdvanced rootCmd.AddCommand(multicaCmd) } diff --git a/harness/cmd/mnemon-harness/multica_test.go b/harness/cmd/mnemon-harness/multica_test.go index 51da48aa..417e08a9 100644 --- a/harness/cmd/mnemon-harness/multica_test.go +++ b/harness/cmd/mnemon-harness/multica_test.go @@ -12,6 +12,7 @@ import ( "github.com/mnemon-dev/mnemon/harness/internal/contract" "github.com/mnemon-dev/mnemon/harness/internal/driver" + multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/multica" ) func TestMulticaImportIssueWritesObservedDraftToLocalIngest(t *testing.T) { @@ -85,6 +86,147 @@ func TestMulticaImportIssueWritesObservedDraftToLocalIngest(t *testing.T) { } } +func TestMulticaSurfaceReportWritesDisplayOnlyState(t *testing.T) { + restoreMulticaFlags(t) + + tmp := t.TempDir() + bin := filepath.Join(tmp, "multica") + argsPath := filepath.Join(tmp, "args.log") + commentPath := filepath.Join(tmp, "comment.md") + script := `#!/usr/bin/env sh +set -eu +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue comment add iss-report"*) cat > "$MULTICA_COMMENT_PATH"; printf '{"id":"comment-1","issue_id":"iss-report","content":"ok"}\n' ;; + *"issue metadata set iss-report"*) printf '{}\n' ;; + *"issue status iss-report done"*) printf '{"id":"iss-report","status":"done"}\n' ;; + *) printf 'unexpected multica args: %s\n' "$*" >&2; exit 42 ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + multicaBin = bin + multicaProfile = "desktop-api.multica.ai" + multicaIssueID = "iss-report" + multicaCommentTitle = "中文进展报告" + multicaSurfaceStatusLabel = "已完成第一轮" + multicaSurfaceSummary = "完成退款规则和客服话术复盘。" + multicaCommentContent = "结论:需要补充异常退款审批人。" + multicaSurfaceDesiredStatus = "done" + multicaSurfaceEventRef = "event:accepted-1" + multicaSurfaceResourceRef = "assignment:refund-review" + multicaSurfaceRef = "multica:iss-report:comment" + multicaSurfaceSourceArtifactRef = "artifact:refund.md" + multicaSurfaceEvidenceRefs = []string{"evidence:log-1"} + multicaSurfaceArtifactRefs = []string{"artifact:refund.md"} + multicaJSON = true + t.Setenv("MULTICA_ARGS_PATH", argsPath) + t.Setenv("MULTICA_COMMENT_PATH", commentPath) + + var out bytes.Buffer + multicaSurfaceReportCmd.SetOut(&out) + t.Cleanup(func() { + multicaSurfaceReportCmd.SetOut(os.Stdout) + }) + if err := runMulticaSurfaceReport(multicaSurfaceReportCmd, nil); err != nil { + t.Fatalf("surface report: %v", err) + } + comment := readTestFile(t, commentPath) + for _, want := range []string{"Mnemon 更新: 中文进展报告", "## 摘要", "完成退款规则", "## 事件引用", "event:accepted-1"} { + if !strings.Contains(comment, want) { + t.Fatalf("surface report comment missing %q:\n%s", want, comment) + } + } + argsLog := readTestFile(t, argsPath) + for _, want := range []string{ + "issue comment add iss-report --content-stdin --output json", + "issue metadata set iss-report --key " + multicasurface.MulticaMetadataEventRef + " --value event:accepted-1", + "issue metadata set iss-report --key " + multicasurface.MulticaMetadataNoAutoDispatch + " --value true", + "issue status iss-report done --output json", + } { + if !strings.Contains(argsLog, want) { + t.Fatalf("surface report args missing %q:\n%s", want, argsLog) + } + } + for _, forbidden := range []string{"issue assign", "[mnemon:wake]"} { + if strings.Contains(argsLog, forbidden) { + t.Fatalf("display report must not trigger execution via %q:\n%s", forbidden, argsLog) + } + } + var report multicaSurfaceReport + if err := json.Unmarshal(out.Bytes(), &report); err != nil { + t.Fatalf("surface report output must be JSON: %v\n%s", err, out.String()) + } + if report.Status != "done" || !report.NoAutoDispatch || report.Metadata[multicasurface.MulticaMetadataSurfaceRole] != string(multicasurface.SurfaceRoleDisplay) { + t.Fatalf("surface report mismatch: %+v", report) + } +} + +func TestMulticaActivationCarrierCreatesTriggerIssue(t *testing.T) { + restoreMulticaFlags(t) + + tmp := t.TempDir() + bin := filepath.Join(tmp, "multica") + argsPath := filepath.Join(tmp, "args.log") + descriptionPath := filepath.Join(tmp, "activation.md") + script := `#!/usr/bin/env sh +set -eu +printf '%s\n' "$*" >> "$MULTICA_ARGS_PATH" +case "$*" in + *"issue create"*) cat > "$MULTICA_DESCRIPTION_PATH"; printf '{"id":"iss-activation","identifier":"ACT-1","title":"激活下一轮协作","status":"todo"}\n' ;; + *"issue metadata set iss-activation"*) printf '{}\n' ;; + *) printf 'unexpected multica args: %s\n' "$*" >&2; exit 42 ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + multicaBin = bin + multicaProfile = "desktop-api.multica.ai" + multicaIssueID = "iss-parent" + multicaCommentTitle = "激活下一轮协作" + multicaCommentContent = "请根据已接受事件继续处理客户退款争议。" + multicaSurfaceEventRef = "event:accepted-2" + multicaSurfaceResourceRef = "assignment:refund-follow-up" + multicaSurfaceTargetAgentID = "agent-reviewer" + multicaJSON = true + t.Setenv("MULTICA_ARGS_PATH", argsPath) + t.Setenv("MULTICA_DESCRIPTION_PATH", descriptionPath) + + var out bytes.Buffer + multicaActivationCarrierCmd.SetOut(&out) + t.Cleanup(func() { + multicaActivationCarrierCmd.SetOut(os.Stdout) + }) + if err := runMulticaActivationCarrier(multicaActivationCarrierCmd, nil); err != nil { + t.Fatalf("activation carrier: %v", err) + } + argsLog := readTestFile(t, argsPath) + for _, want := range []string{ + "issue create --title 激活下一轮协作 --output json --description-stdin --assignee-id agent-reviewer --parent iss-parent --status todo --priority medium --allow-duplicate", + "issue metadata set iss-activation --key " + multicasurface.MulticaMetadataEventRef + " --value event:accepted-2", + "issue metadata set iss-activation --key " + multicasurface.MulticaMetadataSurfaceRole + " --value activate", + } { + if !strings.Contains(argsLog, want) { + t.Fatalf("activation carrier args missing %q:\n%s", want, argsLog) + } + } + description := readTestFile(t, descriptionPath) + for _, want := range []string{"请根据已接受事件继续处理客户退款争议", "Mnemon 激活载体", "event:accepted-2"} { + if !strings.Contains(description, want) { + t.Fatalf("activation description missing %q:\n%s", want, description) + } + } + var report multicaActivationCarrierReport + if err := json.Unmarshal(out.Bytes(), &report); err != nil { + t.Fatalf("activation carrier output must be JSON: %v\n%s", err, out.String()) + } + if report.ParentIssueID != "iss-parent" || report.Issue.ID != "iss-activation" || report.Metadata[multicasurface.MulticaMetadataSurfaceRole] != string(multicasurface.SurfaceRoleActivate) { + t.Fatalf("activation report mismatch: %+v", report) + } +} + func TestMulticaParticipantRegisterAdoptsExistingUIAgent(t *testing.T) { restoreMulticaFlags(t) @@ -387,7 +529,18 @@ func restoreMulticaFlags(t *testing.T) { oldFile := multicaCommentFile oldStdin := multicaCommentStdin oldTitle := multicaCommentTitle - oldEvents := multicaCommentEvents + oldStatusLabel := multicaSurfaceStatusLabel + oldSurfaceSummary := multicaSurfaceSummary + oldDesiredStatus := multicaSurfaceDesiredStatus + oldEventRef := multicaSurfaceEventRef + oldResourceRef := multicaSurfaceResourceRef + oldSurfaceRef := multicaSurfaceRef + oldSourceArtifactRef := multicaSurfaceSourceArtifactRef + oldSurfaceEvidence := multicaSurfaceEvidenceRefs + oldSurfaceArtifacts := multicaSurfaceArtifactRefs + oldAssigneeAgentID := multicaSurfaceAssigneeAgentID + oldAssignedToProvider := multicaSurfaceAssignedToProvider + oldTargetAgentID := multicaSurfaceTargetAgentID oldProvisionRegistry := multicaProvisionRegistry oldProvisionProjectRoot := multicaProvisionProjectRoot oldProvisionProfileName := multicaProvisionProfileName @@ -445,7 +598,18 @@ func restoreMulticaFlags(t *testing.T) { multicaCommentFile = oldFile multicaCommentStdin = oldStdin multicaCommentTitle = oldTitle - multicaCommentEvents = oldEvents + multicaSurfaceStatusLabel = oldStatusLabel + multicaSurfaceSummary = oldSurfaceSummary + multicaSurfaceDesiredStatus = oldDesiredStatus + multicaSurfaceEventRef = oldEventRef + multicaSurfaceResourceRef = oldResourceRef + multicaSurfaceRef = oldSurfaceRef + multicaSurfaceSourceArtifactRef = oldSourceArtifactRef + multicaSurfaceEvidenceRefs = oldSurfaceEvidence + multicaSurfaceArtifactRefs = oldSurfaceArtifacts + multicaSurfaceAssigneeAgentID = oldAssigneeAgentID + multicaSurfaceAssignedToProvider = oldAssignedToProvider + multicaSurfaceTargetAgentID = oldTargetAgentID multicaProvisionRegistry = oldProvisionRegistry multicaProvisionProjectRoot = oldProvisionProjectRoot multicaProvisionProfileName = oldProvisionProfileName @@ -502,7 +666,18 @@ func restoreMulticaFlags(t *testing.T) { multicaCommentFile = "" multicaCommentStdin = false multicaCommentTitle = "" - multicaCommentEvents = nil + multicaSurfaceStatusLabel = "" + multicaSurfaceSummary = "" + multicaSurfaceDesiredStatus = "" + multicaSurfaceEventRef = "" + multicaSurfaceResourceRef = "" + multicaSurfaceRef = "" + multicaSurfaceSourceArtifactRef = "" + multicaSurfaceEvidenceRefs = nil + multicaSurfaceArtifactRefs = nil + multicaSurfaceAssigneeAgentID = "" + multicaSurfaceAssignedToProvider = false + multicaSurfaceTargetAgentID = "" multicaProvisionRegistry = "" multicaProvisionProjectRoot = "." multicaProvisionProfileName = "mnemon-runtime" @@ -537,3 +712,12 @@ func restoreMulticaFlags(t *testing.T) { multicaParticipantProviderWorkspace = "" multicaParticipantProviderTimeout = 0 } + +func readTestFile(t *testing.T, path string) string { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(data) +} diff --git a/harness/cmd/mnemon-harness/root_test.go b/harness/cmd/mnemon-harness/root_test.go index 0e5ded38..cab7aa44 100644 --- a/harness/cmd/mnemon-harness/root_test.go +++ b/harness/cmd/mnemon-harness/root_test.go @@ -78,7 +78,7 @@ func TestProductHelpDoesNotExposeInternalVocabulary(t *testing.T) { {"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", "provision", "participant"} { + for _, blocked := range []string{"binding", "channel", "projection", "kernel", "runtime", "sync cursor", "token file", "control-agent", "import-issue", "project-comment", "surface-report", "activation-carrier", "provision", "participant"} { if strings.Contains(strings.ToLower(got), blocked) { t.Fatalf("%q help leaked internal term %q:\n%s", strings.Join(args, " "), blocked, got) } @@ -92,10 +92,11 @@ func TestInternalCommandsStayHidden(t *testing.T) { {"loop"}, {"local"}, {"multica"}, + {"multica", "activation-carrier"}, {"multica", "import-issue"}, {"multica", "participant"}, - {"multica", "project-comment"}, {"multica", "provision"}, + {"multica", "surface-report"}, {"sync"}, {"token"}, {"tower"}, diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 2a758320..257560b2 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "os/exec" "runtime" "strings" "time" @@ -72,6 +73,17 @@ type runtimeImportResult struct { Err error } +type providerTurnResult struct { + Configured bool + Runtime string + Command string + CWD string + Output string + ExitCode int + DurationMs int64 + Err error +} + func main() { if err := runRuntime(runtimeConfig{ Args: os.Args[1:], @@ -280,6 +292,13 @@ func (s *runtimeRPCState) nextItemID(prefix string) string { func (s *runtimeRPCState) runTurn(input multicasurface.RuntimeInput, progress runtimeProgressSink) string { result := s.importIssue(input, progress) + provider := s.runProviderTurn(input, result, progress) + if provider.Configured { + if strings.TrimSpace(provider.Output) != "" && provider.Err == nil { + return strings.TrimSpace(provider.Output) + } + return formatProviderFailureAnswer(provider, result) + } return multicasurface.FormatRuntimeFinalAnswer(runtimeResultSummary(result)) } @@ -384,6 +403,97 @@ func (s *runtimeRPCState) importIssue(input multicasurface.RuntimeInput, progres return result } +func (s *runtimeRPCState) runProviderTurn(input multicasurface.RuntimeInput, imported runtimeImportResult, progress runtimeProgressSink) providerTurnResult { + command := multicasurface.RuntimeEnvValue(s.Env, "MNEMON_MULTICA_PROVIDER_COMMAND") + if command == "" { + return providerTurnResult{} + } + provider := multicasurface.RuntimeEnvDefault(s.Env, "MNEMON_MULTICA_PROVIDER_RUNTIME", "provider") + cwd := multicasurface.RuntimeEnvDefault(s.Env, "MNEMON_MULTICA_PROVIDER_WORKSPACE", s.CWD) + prompt := providerPrompt(input, imported) + timeout := multicasurface.RuntimeProviderTurnTimeout(s.Env) + result := providerTurnResult{ + Configured: true, + Runtime: provider, + Command: command, + CWD: cwd, + } + emitRuntimeProgress(progress, "Starting Multica-hosted provider turn through "+provider+".") + started := s.now() + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command) + cmd.Dir = cwd + cmd.Env = append([]string(nil), s.Env...) + cmd.Stdin = strings.NewReader(prompt) + out, err := cmd.CombinedOutput() + result.DurationMs = s.now().Sub(started).Milliseconds() + result.Output = strings.TrimSpace(string(out)) + if err != nil { + result.Err = err + result.ExitCode = runtimeExitCode(err) + if exitErr, ok := err.(*exec.ExitError); ok { + result.ExitCode = exitErr.ExitCode() + } + if ctx.Err() == context.DeadlineExceeded { + result.Err = fmt.Errorf("provider turn timed out after %s", timeout) + } + emitRuntimeCommandCWD(progress, command, cwd, result.Output, result.ExitCode, result.DurationMs) + emitRuntimeProgress(progress, "Provider turn failed; Mnemon surface import remains separate from provider execution.") + return result + } + result.ExitCode = 0 + emitRuntimeCommandCWD(progress, command, cwd, result.Output, result.ExitCode, result.DurationMs) + emitRuntimeProgress(progress, "Provider turn completed; Mnemon did not rewrite provider output.") + return result +} + +func providerPrompt(input multicasurface.RuntimeInput, imported runtimeImportResult) string { + if text := strings.TrimSpace(input.Text); text != "" { + return text + "\n" + } + var b strings.Builder + label := firstNonEmpty(imported.Identifier, imported.IssueID) + if label != "" || strings.TrimSpace(imported.Title) != "" { + b.WriteString("Multica issue") + if label != "" { + b.WriteByte(' ') + b.WriteString(label) + } + if strings.TrimSpace(imported.Title) != "" { + b.WriteString(": ") + b.WriteString(strings.TrimSpace(imported.Title)) + } + b.WriteString("\n\n") + } + if strings.TrimSpace(imported.Statement) != "" { + b.WriteString(strings.TrimSpace(imported.Statement)) + b.WriteByte('\n') + } + return b.String() +} + +func formatProviderFailureAnswer(provider providerTurnResult, imported runtimeImportResult) string { + var b strings.Builder + b.WriteString("Provider turn failed") + if provider.Runtime != "" { + b.WriteString(" (") + b.WriteString(provider.Runtime) + b.WriteString(")") + } + if provider.Err != nil { + b.WriteString(": ") + b.WriteString(provider.Err.Error()) + } + if strings.TrimSpace(provider.Output) != "" { + b.WriteString("\n\n") + b.WriteString(strings.TrimSpace(provider.Output)) + } + b.WriteString("\n\n") + b.WriteString(multicasurface.FormatRuntimeFinalAnswer(runtimeResultSummary(imported))) + return strings.TrimSpace(b.String()) +} + func loadRuntimeIssueMetadata(ctx context.Context, cli driver.MulticaCLI, issue driver.MulticaIssue, progress runtimeProgressSink) driver.MulticaIssue { if strings.TrimSpace(issue.ID) == "" { return issue @@ -556,6 +666,12 @@ func emitRuntimeCommand(progress runtimeProgressSink, command, output string, ex } } +func emitRuntimeCommandCWD(progress runtimeProgressSink, command, cwd, output string, exitCode int, durationMs int64) { + if progress != nil { + progress(runtimeProgressEvent{Command: command, CWD: cwd, Output: output, ExitCode: exitCode, DurationMs: durationMs}) + } +} + func runtimeExitCode(err error) int { if err != nil { return 1 diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 746269f0..1539aebc 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -115,6 +115,87 @@ func TestRuntimeTreatsLegacyMailboxMetadataAsSurfaceInputOnly(t *testing.T) { } } +func TestRuntimeRunsProviderCommandWithOriginalTurnInput(t *testing.T) { + tmp := t.TempDir() + logPath := filepath.Join(tmp, "multica-args.log") + multicaBin := writeFakeMulticaCLI(t, logPath, fakeMulticaScript{ + IssueJSON: `{"id":"issue-provider","identifier":"TEA-9","title":"中文验收-多角色决策","description":"请协调多角色完成退款策略复盘。","status":"todo","priority":"high"}`, + MetadataJSON: `[]`, + }) + server, received := fakeIngestServer(t) + defer server.Close() + providerInputPath := filepath.Join(tmp, "provider.stdin") + providerOutput := "原生 provider 输出:已读取中文 turn" + providerBin := writeFakeProvider(t) + progress := []runtimeProgressEvent{} + + final := (&runtimeRPCState{ + Env: []string{ + "MULTICA_ISSUE_ID=issue-provider", + "MULTICA_TASK_ID=task-provider", + "MULTICA_AGENT_ID=agent-1", + "MULTICA_AGENT_NAME=Reviewer", + "MNEMON_MULTICA_BIN=" + multicaBin, + "MNEMON_CONTROL_ADDR=" + server.URL, + "MNEMON_CONTROL_PRINCIPAL=reviewer@team", + "MNEMON_MULTICA_PROVIDER_RUNTIME=codex", + "MNEMON_MULTICA_PROVIDER_COMMAND=" + providerBin, + "MNEMON_MULTICA_PROVIDER_WORKSPACE=" + tmp, + "MNEMON_MULTICA_PROVIDER_TURN_TIMEOUT=5s", + "FAKE_PROVIDER_INPUT=" + providerInputPath, + "FAKE_PROVIDER_OUTPUT=" + providerOutput, + "FAKE_MULTICA_LOG=" + logPath, + }, + CWD: tmp, + Now: fixedRuntimeTime, + }).runTurn(multicasurface.RuntimeInput{Text: "请基于当前 issue 继续推进中文 ReAct 协作。"}, func(event runtimeProgressEvent) { + progress = append(progress, event) + }) + + if final != providerOutput { + t.Fatalf("final answer = %q", final) + } + if got := readFile(t, providerInputPath); got != "请基于当前 issue 继续推进中文 ReAct 协作。\n" { + t.Fatalf("provider input = %q", got) + } + env := <-received + if env.ExternalID != "multica-task-task-provider" { + t.Fatalf("unexpected ingest external id: %+v", env) + } + foundProviderCommand := false + for _, event := range progress { + if event.Command == providerBin { + foundProviderCommand = true + if event.CWD != tmp { + t.Fatalf("provider cwd = %q, want %q", event.CWD, tmp) + } + } + } + if !foundProviderCommand { + t.Fatalf("provider command was not emitted in progress: %+v", progress) + } + argsLog := readFile(t, logPath) + if strings.Contains(argsLog, "issue comment add") || + strings.Contains(argsLog, "issue status set") || + strings.Contains(argsLog, "[mnemon:wake]") { + t.Fatalf("provider wrapper performed forbidden R2 side effect:\n%s", argsLog) + } +} + +func TestProviderPromptFallsBackToIssueWhenTurnInputIsEmpty(t *testing.T) { + got := providerPrompt(multicasurface.RuntimeInput{}, runtimeImportResult{ + IssueID: "issue-1", + Identifier: "TEA-1", + Title: "中文复用上下文评审", + Statement: "请复用上一轮风险结论,并给出下一步。", + }) + for _, want := range []string{"TEA-1", "中文复用上下文评审", "请复用上一轮风险结论"} { + if !strings.Contains(got, want) { + t.Fatalf("provider prompt missing %q:\n%s", want, got) + } + } +} + type fakeMulticaScript struct { IssueJSON string MetadataJSON string @@ -148,6 +229,20 @@ esac return path } +func writeFakeProvider(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "provider") + body := `#!/bin/sh +set -eu +cat > "$FAKE_PROVIDER_INPUT" +printf '%s\n' "$FAKE_PROVIDER_OUTPUT" +` + if err := os.WriteFile(path, []byte(body), 0o755); err != nil { + t.Fatal(err) + } + return path +} + func fakeIngestServer(t *testing.T) (*httptest.Server, <-chan contract.ObservationEnvelope) { t.Helper() received := make(chan contract.ObservationEnvelope, 1) diff --git a/harness/internal/driver/multica_surface.go b/harness/internal/driver/multica_surface.go index dabc3be8..ae5cb2cb 100644 --- a/harness/internal/driver/multica_surface.go +++ b/harness/internal/driver/multica_surface.go @@ -5,7 +5,7 @@ import multicasurface "github.com/mnemon-dev/mnemon/harness/internal/surface/mul const ( MulticaMetadataEventRef = multicasurface.MulticaMetadataEventRef MulticaMetadataResourceRef = multicasurface.MulticaMetadataResourceRef - MulticaMetadataProjectionRef = multicasurface.MulticaMetadataProjectionRef + MulticaMetadataSurfaceRef = multicasurface.MulticaMetadataSurfaceRef MulticaMetadataSourceArtifactRef = multicasurface.MulticaMetadataSourceArtifactRef MulticaMetadataSurfaceRole = multicasurface.MulticaMetadataSurfaceRole MulticaMetadataNoAutoDispatch = multicasurface.MulticaMetadataNoAutoDispatch diff --git a/harness/internal/surface/multica/ledger.go b/harness/internal/surface/multica/ledger.go index faffc6f9..6e4f964f 100644 --- a/harness/internal/surface/multica/ledger.go +++ b/harness/internal/surface/multica/ledger.go @@ -10,12 +10,12 @@ import ( "sync" ) -const MulticaDefaultProjectionLedgerRelPath = ".mnemon/multica/projection-ledger.jsonl" +const MulticaDefaultSurfaceWriteLedgerRelPath = ".mnemon/multica/surface-write-ledger.jsonl" -type ProjectionLedgerRecord struct { +type SurfaceWriteLedgerRecord struct { EventRef string `json:"event_ref"` ResourceRef string `json:"resource_ref,omitempty"` - ProjectionRef string `json:"projection_ref,omitempty"` + SurfaceRef string `json:"surface_ref,omitempty"` SourceArtifactRef string `json:"source_artifact_ref,omitempty"` SurfaceRole SurfaceRole `json:"surface_role"` TargetKind string `json:"target_kind"` @@ -23,19 +23,19 @@ type ProjectionLedgerRecord struct { Status string `json:"status"` } -type ProjectionLedgerTarget struct { +type SurfaceWriteLedgerTarget struct { Kind string ID string } -type FileProjectionLedger struct { +type FileSurfaceWriteLedger struct { mu sync.Mutex path string - records []ProjectionLedgerRecord + records []SurfaceWriteLedgerRecord loaded bool } -func ProjectionLedgerPath(root, explicit string) string { +func SurfaceWriteLedgerPath(root, explicit string) string { if strings.TrimSpace(explicit) != "" { return filepath.Clean(explicit) } @@ -43,53 +43,53 @@ func ProjectionLedgerPath(root, explicit string) string { if root == "" { root = "." } - return filepath.Join(root, MulticaDefaultProjectionLedgerRelPath) + return filepath.Join(root, MulticaDefaultSurfaceWriteLedgerRelPath) } -func NewFileProjectionLedger(path string) *FileProjectionLedger { - return &FileProjectionLedger{path: strings.TrimSpace(path)} +func NewFileSurfaceWriteLedger(path string) *FileSurfaceWriteLedger { + return &FileSurfaceWriteLedger{path: strings.TrimSpace(path)} } -func (l *FileProjectionLedger) Records() ([]ProjectionLedgerRecord, error) { +func (l *FileSurfaceWriteLedger) Records() ([]SurfaceWriteLedgerRecord, error) { if err := l.withLockedLoad(func() error { return nil }); err != nil { return nil, err } - out := append([]ProjectionLedgerRecord(nil), l.records...) + out := append([]SurfaceWriteLedgerRecord(nil), l.records...) return out, nil } -func (l *FileProjectionLedger) Find(eventRef string, role SurfaceRole, target ProjectionLedgerTarget) (ProjectionLedgerRecord, bool, error) { +func (l *FileSurfaceWriteLedger) Find(eventRef string, role SurfaceRole, target SurfaceWriteLedgerTarget) (SurfaceWriteLedgerRecord, bool, error) { eventRef = strings.TrimSpace(eventRef) target.Kind = strings.TrimSpace(target.Kind) target.ID = strings.TrimSpace(target.ID) if eventRef == "" || role == "" || target.Kind == "" || target.ID == "" { - return ProjectionLedgerRecord{}, false, nil + return SurfaceWriteLedgerRecord{}, false, nil } - var found ProjectionLedgerRecord + var found SurfaceWriteLedgerRecord err := l.withLockedLoad(func() error { for _, record := range l.records { - if projectionLedgerRecordMatches(record, eventRef, role, target) { + if surfaceWriteLedgerRecordMatches(record, eventRef, role, target) { found = record } } return nil }) if err != nil { - return ProjectionLedgerRecord{}, false, err + return SurfaceWriteLedgerRecord{}, false, err } return found, strings.TrimSpace(found.EventRef) != "", nil } -func (l *FileProjectionLedger) Reserve(record ProjectionLedgerRecord) (ProjectionLedgerRecord, bool, error) { - record = normalizeProjectionLedgerRecord(record) - if err := validateProjectionLedgerRecord(record); err != nil { - return ProjectionLedgerRecord{}, false, err +func (l *FileSurfaceWriteLedger) Reserve(record SurfaceWriteLedgerRecord) (SurfaceWriteLedgerRecord, bool, error) { + record = normalizeSurfaceWriteLedgerRecord(record) + if err := validateSurfaceWriteLedgerRecord(record); err != nil { + return SurfaceWriteLedgerRecord{}, false, err } - var reserved ProjectionLedgerRecord + var reserved SurfaceWriteLedgerRecord ok := false err := l.withLockedLoad(func() error { for _, existing := range l.records { - if projectionLedgerRecordMatches(existing, record.EventRef, record.SurfaceRole, ProjectionLedgerTarget{Kind: record.TargetKind, ID: record.TargetID}) { + if surfaceWriteLedgerRecordMatches(existing, record.EventRef, record.SurfaceRole, SurfaceWriteLedgerTarget{Kind: record.TargetKind, ID: record.TargetID}) { reserved = existing return nil } @@ -105,15 +105,15 @@ func (l *FileProjectionLedger) Reserve(record ProjectionLedgerRecord) (Projectio return reserved, ok, err } -func (l *FileProjectionLedger) Record(record ProjectionLedgerRecord) error { - record = normalizeProjectionLedgerRecord(record) - if err := validateProjectionLedgerRecord(record); err != nil { +func (l *FileSurfaceWriteLedger) Record(record SurfaceWriteLedgerRecord) error { + record = normalizeSurfaceWriteLedgerRecord(record) + if err := validateSurfaceWriteLedgerRecord(record); err != nil { return err } return l.withLockedLoad(func() error { replaced := false for i, existing := range l.records { - if projectionLedgerRecordMatches(existing, record.EventRef, record.SurfaceRole, ProjectionLedgerTarget{Kind: record.TargetKind, ID: record.TargetID}) { + if surfaceWriteLedgerRecordMatches(existing, record.EventRef, record.SurfaceRole, SurfaceWriteLedgerTarget{Kind: record.TargetKind, ID: record.TargetID}) { l.records[i] = record replaced = true } @@ -125,7 +125,7 @@ func (l *FileProjectionLedger) Record(record ProjectionLedgerRecord) error { }) } -func (l *FileProjectionLedger) withLockedLoad(fn func() error) error { +func (l *FileSurfaceWriteLedger) withLockedLoad(fn func() error) error { l.mu.Lock() defer l.mu.Unlock() if !l.loaded { @@ -137,7 +137,7 @@ func (l *FileProjectionLedger) withLockedLoad(fn func() error) error { return fn() } -func (l *FileProjectionLedger) load() error { +func (l *FileSurfaceWriteLedger) load() error { if l.path == "" { return nil } @@ -149,18 +149,18 @@ func (l *FileProjectionLedger) load() error { return err } defer file.Close() - var records []ProjectionLedgerRecord + var records []SurfaceWriteLedgerRecord scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } - var record ProjectionLedgerRecord + var record SurfaceWriteLedgerRecord if err := json.Unmarshal([]byte(line), &record); err != nil { - return fmt.Errorf("decode projection ledger %s: %w", l.path, err) + return fmt.Errorf("decode surface write ledger %s: %w", l.path, err) } - records = upsertProjectionLedgerRecord(records, normalizeProjectionLedgerRecord(record)) + records = upsertSurfaceWriteLedgerRecord(records, normalizeSurfaceWriteLedgerRecord(record)) } if err := scanner.Err(); err != nil { return err @@ -169,7 +169,7 @@ func (l *FileProjectionLedger) load() error { return nil } -func (l *FileProjectionLedger) appendRecord(record ProjectionLedgerRecord) error { +func (l *FileSurfaceWriteLedger) appendRecord(record SurfaceWriteLedgerRecord) error { if l.path == "" { return nil } @@ -191,7 +191,7 @@ func (l *FileProjectionLedger) appendRecord(record ProjectionLedgerRecord) error return nil } -func (l *FileProjectionLedger) rewrite() error { +func (l *FileSurfaceWriteLedger) rewrite() error { if l.path == "" { return nil } @@ -215,10 +215,10 @@ func (l *FileProjectionLedger) rewrite() error { return nil } -func normalizeProjectionLedgerRecord(record ProjectionLedgerRecord) ProjectionLedgerRecord { +func normalizeSurfaceWriteLedgerRecord(record SurfaceWriteLedgerRecord) SurfaceWriteLedgerRecord { record.EventRef = strings.TrimSpace(record.EventRef) record.ResourceRef = strings.TrimSpace(record.ResourceRef) - record.ProjectionRef = strings.TrimSpace(record.ProjectionRef) + record.SurfaceRef = strings.TrimSpace(record.SurfaceRef) record.SourceArtifactRef = strings.TrimSpace(record.SourceArtifactRef) record.TargetKind = strings.TrimSpace(record.TargetKind) record.TargetID = strings.TrimSpace(record.TargetID) @@ -226,7 +226,7 @@ func normalizeProjectionLedgerRecord(record ProjectionLedgerRecord) ProjectionLe return record } -func validateProjectionLedgerRecord(record ProjectionLedgerRecord) error { +func validateSurfaceWriteLedgerRecord(record SurfaceWriteLedgerRecord) error { if strings.TrimSpace(record.EventRef) == "" { return fmt.Errorf("event_ref is required") } @@ -234,21 +234,21 @@ func validateProjectionLedgerRecord(record ProjectionLedgerRecord) error { return fmt.Errorf("surface_role is required") } if strings.TrimSpace(record.TargetKind) == "" || strings.TrimSpace(record.TargetID) == "" { - return fmt.Errorf("projection ledger target is required") + return fmt.Errorf("surface write ledger target is required") } return nil } -func projectionLedgerRecordMatches(record ProjectionLedgerRecord, eventRef string, role SurfaceRole, target ProjectionLedgerTarget) bool { +func surfaceWriteLedgerRecordMatches(record SurfaceWriteLedgerRecord, eventRef string, role SurfaceRole, target SurfaceWriteLedgerTarget) bool { return record.EventRef == strings.TrimSpace(eventRef) && record.SurfaceRole == role && record.TargetKind == strings.TrimSpace(target.Kind) && record.TargetID == strings.TrimSpace(target.ID) } -func upsertProjectionLedgerRecord(records []ProjectionLedgerRecord, next ProjectionLedgerRecord) []ProjectionLedgerRecord { +func upsertSurfaceWriteLedgerRecord(records []SurfaceWriteLedgerRecord, next SurfaceWriteLedgerRecord) []SurfaceWriteLedgerRecord { for i, record := range records { - if projectionLedgerRecordMatches(record, next.EventRef, next.SurfaceRole, ProjectionLedgerTarget{Kind: next.TargetKind, ID: next.TargetID}) { + if surfaceWriteLedgerRecordMatches(record, next.EventRef, next.SurfaceRole, SurfaceWriteLedgerTarget{Kind: next.TargetKind, ID: next.TargetID}) { records[i] = next return records } diff --git a/harness/internal/surface/multica/runtime_config.go b/harness/internal/surface/multica/runtime_config.go index 6f8f6b80..2c69b09c 100644 --- a/harness/internal/surface/multica/runtime_config.go +++ b/harness/internal/surface/multica/runtime_config.go @@ -32,6 +32,10 @@ func RuntimeTimeout(env []string) time.Duration { return runtimeDuration(env, []string{"MNEMON_MULTICA_RUNTIME_TIMEOUT", "MULTICA_HTTP_TIMEOUT"}, 30*time.Second) } +func RuntimeProviderTurnTimeout(env []string) time.Duration { + return runtimeDuration(env, []string{"MNEMON_MULTICA_PROVIDER_TURN_TIMEOUT"}, 10*time.Minute) +} + func RuntimeMulticaRegistryPaths(env []string, cwd string) []string { paths := []string{} add := func(path string) { diff --git a/harness/internal/surface/multica/surface.go b/harness/internal/surface/multica/surface.go index b5fe64ef..619c32a3 100644 --- a/harness/internal/surface/multica/surface.go +++ b/harness/internal/surface/multica/surface.go @@ -25,7 +25,7 @@ const ( const ( MulticaMetadataEventRef = "mnemon.event_ref" MulticaMetadataResourceRef = "mnemon.resource_ref" - MulticaMetadataProjectionRef = "mnemon.projection_ref" + MulticaMetadataSurfaceRef = "mnemon.surface_ref" MulticaMetadataSourceArtifactRef = "mnemon.source_artifact_ref" MulticaMetadataSurfaceRole = "mnemon.surface_role" MulticaMetadataNoAutoDispatch = "mnemon.no_auto_dispatch" @@ -37,7 +37,7 @@ const ( type SurfaceRefs struct { EventRef string ResourceRef string - ProjectionRef string + SurfaceRef string SourceArtifactRef string } @@ -45,7 +45,7 @@ func (r SurfaceRefs) Metadata(role SurfaceRole) map[string]string { meta := map[string]string{} addSurfaceMetadata(meta, MulticaMetadataEventRef, r.EventRef) addSurfaceMetadata(meta, MulticaMetadataResourceRef, r.ResourceRef) - addSurfaceMetadata(meta, MulticaMetadataProjectionRef, r.ProjectionRef) + addSurfaceMetadata(meta, MulticaMetadataSurfaceRef, r.SurfaceRef) addSurfaceMetadata(meta, MulticaMetadataSourceArtifactRef, r.SourceArtifactRef) if role != "" { meta[MulticaMetadataSurfaceRole] = string(role) @@ -60,7 +60,7 @@ func ParseSurfaceRefs(meta map[string]any) SurfaceRefs { return SurfaceRefs{ EventRef: surfaceAnyString(meta[MulticaMetadataEventRef]), ResourceRef: surfaceAnyString(meta[MulticaMetadataResourceRef]), - ProjectionRef: surfaceAnyString(meta[MulticaMetadataProjectionRef]), + SurfaceRef: surfaceAnyString(meta[MulticaMetadataSurfaceRef]), SourceArtifactRef: surfaceAnyString(meta[MulticaMetadataSourceArtifactRef]), } } @@ -72,7 +72,7 @@ func ParseListedSurfaceRefs(meta map[string]string) SurfaceRefs { return SurfaceRefs{ EventRef: strings.TrimSpace(meta[MulticaMetadataEventRef]), ResourceRef: strings.TrimSpace(meta[MulticaMetadataResourceRef]), - ProjectionRef: strings.TrimSpace(meta[MulticaMetadataProjectionRef]), + SurfaceRef: strings.TrimSpace(meta[MulticaMetadataSurfaceRef]), SourceArtifactRef: strings.TrimSpace(meta[MulticaMetadataSourceArtifactRef]), } } diff --git a/harness/internal/surface/multica/surface_r3_test.go b/harness/internal/surface/multica/surface_r3_test.go index 682697e2..0b28e7a1 100644 --- a/harness/internal/surface/multica/surface_r3_test.go +++ b/harness/internal/surface/multica/surface_r3_test.go @@ -46,7 +46,7 @@ func TestBuildDisplayWritebackPlanIsDisplaySafe(t *testing.T) { Refs: SurfaceRefs{ EventRef: "event-progress-1", ResourceRef: "progress_digest/prog-1", - ProjectionRef: "projection-1", + SurfaceRef: "surface-1", SourceArtifactRef: "multica.comment:comment-1", }, Title: "进展", @@ -155,33 +155,33 @@ func TestClassifyHumanInteractionKeepsCanonicalBoundary(t *testing.T) { } } -func TestProjectionLedgerDedupesByEventRoleAndTarget(t *testing.T) { - path := filepath.Join(t.TempDir(), "projection-ledger.jsonl") - ledger := NewFileProjectionLedger(path) - record := ProjectionLedgerRecord{ - EventRef: "event-1", - ResourceRef: "progress_digest/prog-1", - ProjectionRef: "projection-1", - SurfaceRole: SurfaceRoleDisplay, - TargetKind: "comment", - TargetID: "comment-1", - Status: "reserved", +func TestSurfaceWriteLedgerDedupesByEventRoleAndTarget(t *testing.T) { + path := filepath.Join(t.TempDir(), "surface-write-ledger.jsonl") + ledger := NewFileSurfaceWriteLedger(path) + record := SurfaceWriteLedgerRecord{ + EventRef: "event-1", + ResourceRef: "progress_digest/prog-1", + SurfaceRef: "surface-1", + SurfaceRole: SurfaceRoleDisplay, + TargetKind: "comment", + TargetID: "comment-1", + Status: "reserved", } if _, ok, err := ledger.Reserve(record); err != nil || !ok { t.Fatalf("reserve failed ok=%v err=%v", ok, err) } - if _, ok, err := NewFileProjectionLedger(path).Reserve(record); err != nil || ok { + if _, ok, err := NewFileSurfaceWriteLedger(path).Reserve(record); err != nil || ok { t.Fatalf("duplicate reserve ok=%v err=%v", ok, err) } record.Status = "written" - if err := NewFileProjectionLedger(path).Record(record); err != nil { + if err := NewFileSurfaceWriteLedger(path).Record(record); err != nil { t.Fatal(err) } - found, ok, err := NewFileProjectionLedger(path).Find("event-1", SurfaceRoleDisplay, ProjectionLedgerTarget{Kind: "comment", ID: "comment-1"}) + found, ok, err := NewFileSurfaceWriteLedger(path).Find("event-1", SurfaceRoleDisplay, SurfaceWriteLedgerTarget{Kind: "comment", ID: "comment-1"}) if err != nil || !ok { t.Fatalf("find failed ok=%v err=%v", ok, err) } - if found.Status != "written" || found.ProjectionRef != "projection-1" { + if found.Status != "written" || found.SurfaceRef != "surface-1" { t.Fatalf("ledger record mismatch: %+v", found) } } From 9e4dfad0d77c38e8f6d7a6c88697b54755d20f86 Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:55:19 +0800 Subject: [PATCH 09/11] refactor: rename multica display surface roles Replace projection_surfaces and projection worker terminology with display_surfaces, surface workers, and display-surface boundaries in product config and daemon status. This keeps Multica writeback as an explicit OA display surface instead of a separate projection layer. Validation: go test ./harness/cmd/mnemon-multica-runtime ./harness/cmd/mnemon-harness ./harness/cmd/mnemon-acceptance ./harness/internal/surface/multica ./harness/internal/driver ./harness/internal/productconfig ./harness/internal/daemon ./harness/internal/coreguard ./harness/internal/mnemond/presentation ./harness/internal/drive; go build -o mnemon . --- .../cmd/mnemon-harness/config_daemon_test.go | 16 ++++++------ harness/cmd/mnemon-harness/connect.go | 4 +-- harness/cmd/mnemon-harness/daemon.go | 2 +- harness/cmd/mnemon-harness/doctor.go | 2 +- harness/cmd/mnemon-harness/doctor_test.go | 2 +- .../coreguard/command_boundary_test.go | 2 +- harness/internal/coreguard/mainaxis_test.go | 26 +++++++++---------- .../coreguard/r2_role_boundary_test.go | 18 ++++++------- harness/internal/daemon/configured.go | 12 ++++----- harness/internal/daemon/configured_test.go | 16 ++++++------ harness/internal/daemon/supervisor.go | 2 +- harness/internal/daemon/supervisor_test.go | 2 +- harness/internal/daemon/work_ledger.go | 4 +-- harness/internal/daemon/work_ledger_test.go | 12 ++++----- harness/internal/productconfig/config.go | 18 ++++++------- harness/internal/productconfig/config_test.go | 24 ++++++++--------- 16 files changed, 81 insertions(+), 81 deletions(-) diff --git a/harness/cmd/mnemon-harness/config_daemon_test.go b/harness/cmd/mnemon-harness/config_daemon_test.go index 18ed17ff..cc65d0f6 100644 --- a/harness/cmd/mnemon-harness/config_daemon_test.go +++ b/harness/cmd/mnemon-harness/config_daemon_test.go @@ -91,7 +91,7 @@ func TestDaemonStatusShowsConfiguredRoleSummary(t *testing.T) { 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} + cfg.Daemon.DisplaySurfaces = []string{productconfig.ConnectionMultica} if err := productconfig.Save(productconfig.DefaultPath(root, ""), cfg); err != nil { t.Fatal(err) } @@ -112,7 +112,7 @@ func TestDaemonStatusShowsConfiguredRoleSummary(t *testing.T) { "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", + "multica-display [surface]: surface=multica boundary=display-surface", "Harness daemon: not running", } { if !strings.Contains(got, want) { @@ -332,12 +332,12 @@ func TestConnectCommandsWriteProductConfig(t *testing.T) { } } 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.DisplaySurfaces, want) { + t.Fatalf("display surface %q missing: %+v", want, cfg.Daemon.DisplaySurfaces) } } - if containsString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionMnemonhub) { - t.Fatalf("mnemonhub must remain an exchange backend, not projection surface: %+v", cfg.Daemon.ProjectionSurfaces) + if containsString(cfg.Daemon.DisplaySurfaces, productconfig.ConnectionMnemonhub) { + t.Fatalf("mnemonhub must remain an exchange backend, not display surface: %+v", cfg.Daemon.DisplaySurfaces) } if got := cfg.Sessions.PrimaryActivationCarrier; got != productconfig.ConnectionMultica { t.Fatalf("unexpected primary activation carrier: %q", got) @@ -368,8 +368,8 @@ func TestConnectMnemonhubKeepsExchangeOutOfActivationAndProjection(t *testing.T) 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 containsString(cfg.Daemon.DisplaySurfaces, productconfig.ConnectionMnemonhub) { + t.Fatalf("mnemonhub must remain an exchange backend, not display surface: %+v", cfg.Daemon.DisplaySurfaces) } if got := cfg.Sessions.PrimaryActivationCarrier; got != "" { t.Fatalf("mnemonhub must not become primary activation carrier, got %q", got) diff --git a/harness/cmd/mnemon-harness/connect.go b/harness/cmd/mnemon-harness/connect.go index 50d357a8..760dc2b6 100644 --- a/harness/cmd/mnemon-harness/connect.go +++ b/harness/cmd/mnemon-harness/connect.go @@ -70,7 +70,7 @@ func runConnectMultica(cmd *cobra.Command, args []string) error { RuntimeBinary: strings.TrimSpace(connectMulticaRuntime), } cfg.Daemon.InteractionWatchers = appendUniqueString(cfg.Daemon.InteractionWatchers, productconfig.ConnectionMultica) - cfg.Daemon.ProjectionSurfaces = appendUniqueString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionMultica) + cfg.Daemon.DisplaySurfaces = appendUniqueString(cfg.Daemon.DisplaySurfaces, productconfig.ConnectionMultica) if cfg.Sessions.PrimaryActivationCarrier == "" { cfg.Sessions.PrimaryActivationCarrier = productconfig.ConnectionMultica } @@ -97,7 +97,7 @@ func runConnectGitHub(cmd *cobra.Command, args []string) error { Branch: strings.TrimSpace(connectGitHubBranch), } cfg.Daemon.InteractionWatchers = appendUniqueString(cfg.Daemon.InteractionWatchers, productconfig.ConnectionGitHub) - cfg.Daemon.ProjectionSurfaces = appendUniqueString(cfg.Daemon.ProjectionSurfaces, productconfig.ConnectionGitHub) + cfg.Daemon.DisplaySurfaces = appendUniqueString(cfg.Daemon.DisplaySurfaces, productconfig.ConnectionGitHub) path, err := saveHarnessProductConfig(connectRoot, connectConfigPath, cfg) if err != nil { return err diff --git a/harness/cmd/mnemon-harness/daemon.go b/harness/cmd/mnemon-harness/daemon.go index ea895ac1..c569f9f7 100644 --- a/harness/cmd/mnemon-harness/daemon.go +++ b/harness/cmd/mnemon-harness/daemon.go @@ -117,7 +117,7 @@ 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) + fmt.Fprintf(out, "Harness daemon roles: watchers=%d drive=%d surfaces=%d\n", summary.InteractionWatchers, summary.DriveSources, summary.DisplaySurfaces) details := daemon.RoleDetails(cfg) if len(details) == 0 { return diff --git a/harness/cmd/mnemon-harness/doctor.go b/harness/cmd/mnemon-harness/doctor.go index 84b47a4c..00ec894e 100644 --- a/harness/cmd/mnemon-harness/doctor.go +++ b/harness/cmd/mnemon-harness/doctor.go @@ -38,7 +38,7 @@ func runDoctor(cmd *cobra.Command, args []string) error { 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(), "- Daemon roles: watchers=%d drive=%d surfaces=%d\n", len(cfg.Daemon.InteractionWatchers), len(cfg.Daemon.DriveSources), len(cfg.Daemon.DisplaySurfaces)) fmt.Fprintf(cmd.OutOrStdout(), "- Connections: %s\n", doctorConnections(cfg)) } fmt.Fprintf(cmd.OutOrStdout(), "- Local Mnemon config: %s\n", doctorLocalConfig(root)) diff --git a/harness/cmd/mnemon-harness/doctor_test.go b/harness/cmd/mnemon-harness/doctor_test.go index a653f77d..97002a63 100644 --- a/harness/cmd/mnemon-harness/doctor_test.go +++ b/harness/cmd/mnemon-harness/doctor_test.go @@ -46,7 +46,7 @@ func TestDoctorReportsConfiguredProductSurface(t *testing.T) { }} cfg.Daemon.InteractionWatchers = []string{productconfig.ConnectionMultica} cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} - cfg.Daemon.ProjectionSurfaces = []string{productconfig.ConnectionMultica} + cfg.Daemon.DisplaySurfaces = []string{productconfig.ConnectionMultica} if err := productconfig.Save(productconfig.DefaultPath(root, ""), cfg); err != nil { t.Fatal(err) } diff --git a/harness/internal/coreguard/command_boundary_test.go b/harness/internal/coreguard/command_boundary_test.go index 35b6e4b8..0c5de389 100644 --- a/harness/internal/coreguard/command_boundary_test.go +++ b/harness/internal/coreguard/command_boundary_test.go @@ -25,7 +25,7 @@ var commandImportBoundaries = []commandImportBoundary{ "harness/internal/surface/", "harness/cmd/", }, - rationale: "mnemond is the local event node, not a remote exchange backend or external projection surface", + rationale: "mnemond is the local event node, not a remote exchange backend or external display surface", }, { dir: "mnemon-hub", diff --git a/harness/internal/coreguard/mainaxis_test.go b/harness/internal/coreguard/mainaxis_test.go index 234a6d24..31f838bf 100644 --- a/harness/internal/coreguard/mainaxis_test.go +++ b/harness/internal/coreguard/mainaxis_test.go @@ -10,16 +10,16 @@ import ( type mainAxisOwner string const ( - ownerEvent mainAxisOwner = "event" - ownerHostAgent mainAxisOwner = "hostagent" - ownerMnemond mainAxisOwner = "mnemond" - ownerMnemonhub mainAxisOwner = "mnemonhub" - ownerParticipant mainAxisOwner = "participant" - ownerInteractionPoint mainAxisOwner = "interaction-point" - ownerDriveSource mainAxisOwner = "drive-source" - ownerProjectionSurface mainAxisOwner = "projection-surface" - ownerProductConfig mainAxisOwner = "product-config" - ownerGuard mainAxisOwner = "guard" + ownerEvent mainAxisOwner = "event" + ownerHostAgent mainAxisOwner = "hostagent" + ownerMnemond mainAxisOwner = "mnemond" + ownerMnemonhub mainAxisOwner = "mnemonhub" + ownerParticipant mainAxisOwner = "participant" + ownerInteractionPoint mainAxisOwner = "interaction-point" + ownerDriveSource mainAxisOwner = "drive-source" + ownerDisplaySurface mainAxisOwner = "display-surface" + ownerProductConfig mainAxisOwner = "product-config" + ownerGuard mainAxisOwner = "guard" ) type packageMainAxis struct { @@ -32,7 +32,7 @@ type packageMainAxis struct { // not claim the current package names are final; it prevents new top-level implementation concepts // from appearing without an explicit owner under the R2 architecture layers. var packageMainAxisInventory = map[string]packageMainAxis{ - "activationtrace": {owner: ownerProjectionSurface, role: "runtime activation trace material for external run displays", target: "projection/activationtrace"}, + "activationtrace": {owner: ownerDisplaySurface, role: "runtime activation trace material for external run displays", target: "display/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"}, @@ -50,7 +50,7 @@ var packageMainAxisInventory = map[string]packageMainAxis{ "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"}, + "projection": {owner: ownerDisplaySurface, role: "accepted/derived state material prepared for external display 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"}, @@ -71,7 +71,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"}, + "surface/multica": {owner: ownerDisplaySurface, role: "Multica surface registry, metadata, and surface write ledger primitives", target: "display/surface/multica"}, } var retiredTopLevelImplementationPackages = []string{ diff --git a/harness/internal/coreguard/r2_role_boundary_test.go b/harness/internal/coreguard/r2_role_boundary_test.go index 7408ef6e..6630d040 100644 --- a/harness/internal/coreguard/r2_role_boundary_test.go +++ b/harness/internal/coreguard/r2_role_boundary_test.go @@ -11,7 +11,7 @@ type roleImportBoundary struct { rationale string } -var projectionSurfaceImportBoundaries = []roleImportBoundary{ +var displaySurfaceImportBoundaries = []roleImportBoundary{ { pkg: "projection", forbids: []string{ @@ -36,7 +36,7 @@ var projectionSurfaceImportBoundaries = []roleImportBoundary{ "harness/internal/app", "harness/internal/assembler", }, - rationale: "the Multica surface is a projection/adapter boundary, not a local mnemond write path", + rationale: "the Multica surface is a display/adapter boundary, not a local mnemond write path", }, } @@ -61,22 +61,22 @@ var driverProjectionImportBoundary = roleImportBoundary{ } 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/access", displaySurfaceImportBoundaries[0].forbids) { + t.Fatal("display 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/mnemond/presentation/view", displaySurfaceImportBoundaries[0].forbids) { + t.Fatal("display 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") } if !roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/projection", driverProjectionImportBoundary.forbids) { - t.Fatal("driver projection boundary guard must flag projection imports") + t.Fatal("driver display boundary guard must flag projection imports") } } -func TestProjectionSurfacesDoNotIngestGovernedEvents(t *testing.T) { - for _, boundary := range projectionSurfaceImportBoundaries { +func TestDisplaySurfacesDoNotIngestGovernedEvents(t *testing.T) { + for _, boundary := range displaySurfaceImportBoundaries { assertPackageAvoidsForbiddenImports(t, boundary) } } diff --git a/harness/internal/daemon/configured.go b/harness/internal/daemon/configured.go index 6dbd79a1..f3676aeb 100644 --- a/harness/internal/daemon/configured.go +++ b/harness/internal/daemon/configured.go @@ -10,7 +10,7 @@ import ( type ConfiguredRoleSummary struct { InteractionWatchers int DriveSources int - ProjectionSurfaces int + DisplaySurfaces int } type ConfiguredRoleDetail struct { @@ -30,7 +30,7 @@ func RoleSummary(cfg productconfig.Config) ConfiguredRoleSummary { case "drive": summary.DriveSources++ case "surface": - summary.ProjectionSurfaces++ + summary.DisplaySurfaces++ } } return summary @@ -75,17 +75,17 @@ func RoleDetails(cfg productconfig.Config) []ConfiguredRoleDetail { Boundary: "managed-runtime", }) } - for _, surface := range cfg.Daemon.ProjectionSurfaces { + for _, surface := range cfg.Daemon.DisplaySurfaces { surface = strings.TrimSpace(surface) if surface == "" { continue } details = append(details, ConfiguredRoleDetail{ - WorkerName: workerName(surface + "-project"), - Kind: WorkerProjection, + WorkerName: workerName(surface + "-display"), + Kind: WorkerSurface, Label: "surface", Value: surface, - Boundary: "projection-surface", + Boundary: "display-surface", }) } return details diff --git a/harness/internal/daemon/configured_test.go b/harness/internal/daemon/configured_test.go index 8abaaba9..d3fd5166 100644 --- a/harness/internal/daemon/configured_test.go +++ b/harness/internal/daemon/configured_test.go @@ -12,13 +12,13 @@ func TestConfiguredSnapshotIncludesConfiguredRoles(t *testing.T) { cfg := productconfig.Default() cfg.Daemon.InteractionWatchers = []string{" ", productconfig.ConnectionMultica} cfg.Daemon.DriveSources = []string{"", productconfig.DriveManagedLocal} - cfg.Daemon.ProjectionSurfaces = []string{"\t", productconfig.ConnectionMultica} + cfg.Daemon.DisplaySurfaces = []string{"\t", productconfig.ConnectionMultica} snapshot := ConfiguredSnapshot(cfg, now) for name, want := range map[string]WorkerKind{ "multica-watch": WorkerInteraction, "managed-drive": WorkerDrive, - "multica-project": WorkerProjection, + "multica-display": WorkerSurface, "status-readiness": WorkerStatus, } { worker, ok := snapshot.Workers[name] @@ -29,7 +29,7 @@ func TestConfiguredSnapshotIncludesConfiguredRoles(t *testing.T) { t.Fatalf("worker %q mismatch: %+v", name, worker) } } - for _, name := range []string{"-watch", "-drive", "-project"} { + for _, name := range []string{"-watch", "-drive", "-display"} { if _, ok := snapshot.Workers[name]; ok { t.Fatalf("snapshot should skip empty worker name %q: %+v", name, snapshot.Workers) } @@ -50,7 +50,7 @@ func TestConfiguredSnapshotKeepsMnemonhubAsExchangeWatcher(t *testing.T) { 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 { + if _, ok := snapshot.Workers["mnemonhub-display"]; ok { t.Fatalf("mnemonhub must remain a remote exchange watcher, not projection worker: %+v", snapshot.Workers) } } @@ -62,7 +62,7 @@ func TestRoleDetailsDescribeDaemonBoundaries(t *testing.T) { productconfig.ConnectionMnemonhub, } cfg.Daemon.DriveSources = []string{productconfig.DriveManagedLocal} - cfg.Daemon.ProjectionSurfaces = []string{productconfig.ConnectionMultica} + cfg.Daemon.DisplaySurfaces = []string{productconfig.ConnectionMultica} details := RoleDetails(cfg) if len(details) != 4 { @@ -72,7 +72,7 @@ func TestRoleDetailsDescribeDaemonBoundaries(t *testing.T) { {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"}, + {WorkerName: "multica-display", Kind: WorkerSurface, Label: "surface", Value: productconfig.ConnectionMultica, Boundary: "display-surface"}, } { if !hasRoleDetail(details, want) { t.Fatalf("missing role detail %+v in %+v", want, details) @@ -84,10 +84,10 @@ 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} + cfg.Daemon.DisplaySurfaces = []string{productconfig.ConnectionMultica} got := RoleSummary(cfg) - if got.InteractionWatchers != 2 || got.DriveSources != 1 || got.ProjectionSurfaces != 1 { + if got.InteractionWatchers != 2 || got.DriveSources != 1 || got.DisplaySurfaces != 1 { t.Fatalf("role summary mismatch: %+v", got) } } diff --git a/harness/internal/daemon/supervisor.go b/harness/internal/daemon/supervisor.go index 46c7b403..da33fb86 100644 --- a/harness/internal/daemon/supervisor.go +++ b/harness/internal/daemon/supervisor.go @@ -12,7 +12,7 @@ type WorkerKind string const ( WorkerInteraction WorkerKind = "interaction" WorkerDrive WorkerKind = "drive" - WorkerProjection WorkerKind = "projection" + WorkerSurface WorkerKind = "surface" WorkerStatus WorkerKind = "status" ) diff --git a/harness/internal/daemon/supervisor_test.go b/harness/internal/daemon/supervisor_test.go index 50d6e3a0..0b821a87 100644 --- a/harness/internal/daemon/supervisor_test.go +++ b/harness/internal/daemon/supervisor_test.go @@ -47,7 +47,7 @@ func TestSupervisorCapturesWorkerFailure(t *testing.T) { supervisor := NewSupervisor([]Worker{ workerFunc{ name: "project-multica", - kind: WorkerProjection, + kind: WorkerSurface, run: func(context.Context, Reporter) error { return errors.New("projection failed") }, diff --git a/harness/internal/daemon/work_ledger.go b/harness/internal/daemon/work_ledger.go index 7f7f2fc8..cdc75da1 100644 --- a/harness/internal/daemon/work_ledger.go +++ b/harness/internal/daemon/work_ledger.go @@ -12,8 +12,8 @@ import ( ) const ( - WorkKindProjection = "projection" - WorkKindWake = "wake" + WorkKindSurface = "surface" + WorkKindWake = "wake" ) type WorkLedgerRecord struct { diff --git a/harness/internal/daemon/work_ledger_test.go b/harness/internal/daemon/work_ledger_test.go index 593bc2e8..9df84cd5 100644 --- a/harness/internal/daemon/work_ledger_test.go +++ b/harness/internal/daemon/work_ledger_test.go @@ -10,11 +10,11 @@ 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 { + if seen, err := ledger.Seen(WorkKindSurface, "multica:comment:root-1"); err != nil || seen { t.Fatalf("initial seen = %v err=%v", seen, err) } if err := ledger.Record(WorkLedgerRecord{ - Kind: WorkKindProjection, + Kind: WorkKindSurface, Key: "multica:comment:root-1", Status: "completed", Message: "comment=comment-1", @@ -32,7 +32,7 @@ func TestFileWorkLedgerPersistsProjectionAndWakeRecords(t *testing.T) { } resumed := NewFileWorkLedger(path, func() time.Time { return now.Add(time.Hour) }) - projection, ok, err := resumed.Load(WorkKindProjection, "multica:comment:root-1") + projection, ok, err := resumed.Load(WorkKindSurface, "multica:comment:root-1") if err != nil { t.Fatal(err) } @@ -54,14 +54,14 @@ func TestFileWorkLedgerDedupesByKindAndKey(t *testing.T) { 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 { + if err := ledger.Record(WorkLedgerRecord{Kind: WorkKindSurface, 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 { + if err := ledger.Record(WorkLedgerRecord{Kind: WorkKindSurface, Key: "target-1", Status: "completed"}); err != nil { t.Fatal(err) } - records, err := NewFileWorkLedger(path, nil).Records(WorkKindProjection) + records, err := NewFileWorkLedger(path, nil).Records(WorkKindSurface) if err != nil { t.Fatal(err) } diff --git a/harness/internal/productconfig/config.go b/harness/internal/productconfig/config.go index b13fedef..16b97814 100644 --- a/harness/internal/productconfig/config.go +++ b/harness/internal/productconfig/config.go @@ -87,7 +87,7 @@ type MnemonhubConnection struct { type Daemon struct { InteractionWatchers []string `json:"interaction_watchers,omitempty"` DriveSources []string `json:"drive_sources,omitempty"` - ProjectionSurfaces []string `json:"projection_surfaces,omitempty"` + DisplaySurfaces []string `json:"display_surfaces,omitempty"` } type SessionDefaults struct { @@ -198,7 +198,7 @@ func (cfg Config) Validate() error { if err := validateDaemonCarriers("interaction watcher", cfg.Daemon.InteractionWatchers, cfg); err != nil { return err } - if err := validateProjectionSurfaces(cfg.Daemon.ProjectionSurfaces, cfg); err != nil { + if err := validateDisplaySurfaces(cfg.Daemon.DisplaySurfaces, cfg); err != nil { return err } if err := validateDaemonDriveSources(cfg.Daemon.DriveSources); err != nil { @@ -236,21 +236,21 @@ func validatePrimaryActivationCarrier(carrier string, cfg Config) error { return validateCarrier("primary activation carrier", carrier, cfg) } -func validateProjectionSurfaces(values []string, cfg Config) error { +func validateDisplaySurfaces(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") + return fmt.Errorf("display surface cannot be empty") } if surface == ConnectionMnemonhub { - return fmt.Errorf("projection surface %q is unsupported; mnemonhub is a remote exchange backend", surface) + return fmt.Errorf("display surface %q is unsupported; mnemonhub is a remote exchange backend", surface) } - if err := validateCarrier("projection surface", surface, cfg); err != nil { + if err := validateCarrier("display surface", surface, cfg); err != nil { return err } if seen[surface] { - return fmt.Errorf("duplicate projection surface %q", surface) + return fmt.Errorf("duplicate display surface %q", surface) } seen[surface] = true } @@ -379,7 +379,7 @@ func FromLegacy(root string) (Config, bool, error) { } if cfg.Connections.GitHub.Enabled { cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionGitHub) - cfg.Daemon.ProjectionSurfaces = appendCarrier(cfg.Daemon.ProjectionSurfaces, ConnectionGitHub) + cfg.Daemon.DisplaySurfaces = appendCarrier(cfg.Daemon.DisplaySurfaces, ConnectionGitHub) } if len(cfg.Participants) > 0 { cfg.Daemon.DriveSources = appendCarrier(cfg.Daemon.DriveSources, DriveManagedLocal) @@ -399,7 +399,7 @@ func bridgeMulticaRegistry(cfg *Config, reg multicasurface.MulticaRegistry) erro cfg.Connections.Multica.RuntimeBinary = DefaultMulticaRuntimeBinary } cfg.Daemon.InteractionWatchers = appendCarrier(cfg.Daemon.InteractionWatchers, ConnectionMultica) - cfg.Daemon.ProjectionSurfaces = appendCarrier(cfg.Daemon.ProjectionSurfaces, ConnectionMultica) + cfg.Daemon.DisplaySurfaces = appendCarrier(cfg.Daemon.DisplaySurfaces, ConnectionMultica) if strings.TrimSpace(cfg.Sessions.PrimaryActivationCarrier) == "" { cfg.Sessions.PrimaryActivationCarrier = ConnectionMultica } diff --git a/harness/internal/productconfig/config_test.go b/harness/internal/productconfig/config_test.go index 7a365918..e8c7baa3 100644 --- a/harness/internal/productconfig/config_test.go +++ b/harness/internal/productconfig/config_test.go @@ -25,7 +25,7 @@ func TestConfigRoundTrip(t *testing.T) { RuntimeBinary: "mnemon-multica-runtime", } cfg.Daemon.InteractionWatchers = []string{ConnectionMultica} - cfg.Daemon.ProjectionSurfaces = []string{ConnectionMultica} + cfg.Daemon.DisplaySurfaces = []string{ConnectionMultica} cfg.Daemon.DriveSources = []string{DriveManagedLocal} cfg.Sessions.PrimaryActivationCarrier = ConnectionMultica @@ -51,18 +51,18 @@ func TestConfigValidateRejectsCrossLayerLeaks(t *testing.T) { Mode: RuntimeModeManaged, }, }} - cfg.Daemon.ProjectionSurfaces = []string{ConnectionMultica} + cfg.Daemon.DisplaySurfaces = []string{ConnectionMultica} err := cfg.Validate() if err == nil || !strings.Contains(err.Error(), "not enabled") { - t.Fatalf("expected disabled projection surface error, got %v", err) + t.Fatalf("expected disabled display surface error, got %v", err) } cfg = Default() cfg.Connections.Mnemonhub = MnemonhubConnection{Enabled: true, Endpoint: "https://hub.example"} - cfg.Daemon.ProjectionSurfaces = []string{ConnectionMnemonhub} + cfg.Daemon.DisplaySurfaces = []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) + t.Fatalf("expected mnemonhub display surface error, got %v", err) } cfg = Default() @@ -111,16 +111,16 @@ func TestConfigValidateRejectsDuplicateDaemonRoles(t *testing.T) { { name: "surface", edit: func(cfg *Config) { - cfg.Daemon.ProjectionSurfaces = []string{ConnectionMultica, " " + ConnectionMultica} + cfg.Daemon.DisplaySurfaces = []string{ConnectionMultica, " " + ConnectionMultica} }, - want: "duplicate projection surface", + want: "duplicate display surface", }, { name: "empty surface", edit: func(cfg *Config) { - cfg.Daemon.ProjectionSurfaces = []string{""} + cfg.Daemon.DisplaySurfaces = []string{""} }, - want: "projection surface cannot be empty", + want: "display surface cannot be empty", }, { name: "drive", @@ -193,8 +193,8 @@ func TestFromLegacyBridgesLocalAndRemoteConfigs(t *testing.T) { 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 stringSliceContains(cfg.Daemon.DisplaySurfaces, ConnectionMnemonhub) { + t.Fatalf("mnemonhub must not be bridged as a display surface: %+v", cfg.Daemon.DisplaySurfaces) } if len(cfg.Daemon.DriveSources) != 1 || cfg.Daemon.DriveSources[0] != DriveManagedLocal { t.Fatalf("drive sources mismatch: %+v", cfg.Daemon.DriveSources) @@ -251,7 +251,7 @@ func TestFromLegacyBridgesMulticaRegistry(t *testing.T) { if cfg.Sessions.PrimaryActivationCarrier != ConnectionMultica { t.Fatalf("primary carrier = %q", cfg.Sessions.PrimaryActivationCarrier) } - if !stringSliceContains(cfg.Daemon.InteractionWatchers, ConnectionMultica) || !stringSliceContains(cfg.Daemon.ProjectionSurfaces, ConnectionMultica) { + if !stringSliceContains(cfg.Daemon.InteractionWatchers, ConnectionMultica) || !stringSliceContains(cfg.Daemon.DisplaySurfaces, ConnectionMultica) { t.Fatalf("daemon multica roles missing: %+v", cfg.Daemon) } planner := participantByPrincipal(cfg.Participants, "planner@team") From 897695043b7a88df7fe1f2b28273f22fe1150fb6 Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 03:58:04 +0800 Subject: [PATCH 10/11] test: rename multica runtime surface guard Keep the Multica runtime role guard terminology aligned with the R3 surface adapter boundary instead of projection writer language. Validation: go test ./harness/internal/coreguard --- .../coreguard/multica_runtime_role_boundary_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/harness/internal/coreguard/multica_runtime_role_boundary_test.go b/harness/internal/coreguard/multica_runtime_role_boundary_test.go index d96b3957..3b13f027 100644 --- a/harness/internal/coreguard/multica_runtime_role_boundary_test.go +++ b/harness/internal/coreguard/multica_runtime_role_boundary_test.go @@ -11,7 +11,7 @@ import ( "testing" ) -var multicaRuntimeProjectionForbiddenImports = []string{ +var multicaRuntimeSurfaceForbiddenImports = []string{ "harness/internal/app", "harness/internal/hostagent", "harness/internal/mnemond/admission", @@ -37,7 +37,7 @@ func TestMulticaRuntimeDoesNotOwnManagedWakeOrDisplayWriteback(t *testing.T) { } for _, imp := range file.Imports { importPath := strings.Trim(imp.Path.Value, `"`) - if roleImportForbidden(importPath, multicaRuntimeProjectionForbiddenImports) { + if roleImportForbidden(importPath, multicaRuntimeSurfaceForbiddenImports) { t.Errorf("Multica runtime imports forbidden package %q; runtime may import surface input and call mnemond access, but must not own product config, state, runtime core, or hub exchange", importPath) } } @@ -63,10 +63,10 @@ func TestMulticaRuntimeDoesNotOwnManagedWakeOrDisplayWriteback(t *testing.T) { } 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/mnemonhub", multicaRuntimeSurfaceForbiddenImports) { + t.Fatal("Multica surface adapter guard must flag mnemonhub imports") } - if roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/access", multicaRuntimeProjectionForbiddenImports) { + if roleImportForbidden("github.com/mnemon-dev/mnemon/harness/internal/mnemond/access", multicaRuntimeSurfaceForbiddenImports) { t.Fatal("Multica runtime guard must allow mnemond access") } if selectorName(&ast.SelectorExpr{Sel: ast.NewIdent("ManagedAgentDriver")}) != "ManagedAgentDriver" { From ee477f24cd8039fa58c6944a7a46aa693e5d0c86 Mon Sep 17 00:00:00 2001 From: Grivn Date: Wed, 1 Jul 2026 09:48:26 +0800 Subject: [PATCH 11/11] test: add Multica R3 live validation Add live Multica R3 acceptance scripts and Chinese validation docs grounded in the native Multica runtime flow. Tighten acceptance so root and child surfaces must expose run messages, comments, metadata, and active agent evidence. Update the Multica driver for current CLI/API shapes and pass imported issue context into provider commands without duplicate env keys. Validation: go test ./harness/internal/driver; go test ./harness/cmd/mnemon-acceptance; go test ./harness/cmd/mnemon-multica-runtime; go test ./harness/internal/surface/multica; go test ./harness/cmd/mnemon-harness; bash -n scripts/multica_r3_live_*.sh; go build -o /tmp/mnemon-r3-build-check .; live cases r3-surface-readiness TEA-199, protocol-react-drill TEA-203, parallel-poc-overlap TEA-208 all status=ok. --- docs/zh/harness/README.md | 4 + docs/zh/harness/multica-r3-live-validation.md | 238 ++++++++++++++ .../acceptance_multica_runtime.go | 141 ++++++++- .../acceptance_multica_runtime_test.go | 9 +- harness/cmd/mnemon-multica-runtime/main.go | 31 +- .../cmd/mnemon-multica-runtime/main_test.go | 38 +++ harness/internal/driver/multica.go | 299 +++++++++++++++++- harness/internal/driver/multica_test.go | 162 ++++++++++ scripts/multica_r3_live_acceptance.sh | 51 +++ scripts/multica_r3_live_prepare.sh | 97 ++++++ scripts/multica_r3_live_provider.sh | 199 ++++++++++++ 11 files changed, 1241 insertions(+), 28 deletions(-) create mode 100644 docs/zh/harness/multica-r3-live-validation.md create mode 100755 scripts/multica_r3_live_acceptance.sh create mode 100755 scripts/multica_r3_live_prepare.sh create mode 100755 scripts/multica_r3_live_provider.sh diff --git a/docs/zh/harness/README.md b/docs/zh/harness/README.md index ec3abe34..6980a43c 100644 --- a/docs/zh/harness/README.md +++ b/docs/zh/harness/README.md @@ -52,6 +52,10 @@ go build -o mnemon-harness ./harness/cmd/mnemon-harness 更多命令示例见 [USAGE.md](USAGE.md)。 +维护者验证 Multica R3 live 接入时,可以参考 +[multica-r3-live-validation.md](multica-r3-live-validation.md)。该文档记录 +源码校准、验收脚本和中文复杂 case,不属于普通用户产品契约。 + ## 5. 发布边界 这个 beta 只发布最小公开文档。内部计划、实验命令面、生成站点 HTML 和未来 diff --git a/docs/zh/harness/multica-r3-live-validation.md b/docs/zh/harness/multica-r3-live-validation.md new file mode 100644 index 00000000..9ec77d1f --- /dev/null +++ b/docs/zh/harness/multica-r3-live-validation.md @@ -0,0 +1,238 @@ +# Multica R3 Live Validation + +本文记录 R3 Multica 接入的维护者验证方法。它不是公开产品契约, +而是用于校准实现边界、复现实机验收和解释脚本行为的工程文档。 + +## 1. 源码校准结论 + +这次实现必须贴近 Multica 原生 runtime 模型,而不是把 Multica +重新解释成 MnemonHub、mailbox 或 projector。 + +关键源码约束来自 Multica 原仓库: + +- `server/pkg/agent/codex.go` + - Codex backend 启动的是 `codex app-server --listen stdio://`。 + - daemon 通过 JSON-RPC 保留 provider 原生的 thread、turn、tool message + 和 final answer 流。 + - 这说明 Mnemon wrapper 不应重新实现 Codex CLI 的每个输入输出结构; + 正确方向是透明保留 provider 流,再在流外接入 mnemond 能力。 +- `server/internal/daemon/execenv/runtime_config.go` + - Multica 通过 provider 原生发现机制注入运行时说明:Codex/Kimi 等写 + `AGENTS.md`,Claude 写 `CLAUDE.md`。 + - 注入内容把 issue/comment/status/mention 等 OA 操作暴露为 CLI 工作流。 + - 因此 Mnemon 也应该通过 skill/hook/command 让 agent 显式发出 + Mnemon event 或 surface report,而不是解析自由文本输出。 +- `server/internal/service/task.go` + - issue assignment 和 `@agent` mention 都会进入 `agent_task_queue`。 + - queue task 指向 agent runtime,daemon 再按 runtime claim task。 + - 这说明 Multica-hosted provider turn 与 mnemond-managed wake turn 都可以 + 通过 Multica task 进入,但必须在 Mnemon 层保留不同 source 语义。 +- `server/internal/handler/daemon.go` + - claim task 会携带 agent identity、custom env、custom args、workspace、 + prior session/workdir、trigger comment 等上下文。 + - run messages 由 daemon 持久化并推送给 UI。 + - issue status/comment 是 OA 表达层;daemon 不自动替 agent 完成所有业务状态。 + +由此得到的 R3 原则: + +- Mnemon 真相仍是 mnemond 接纳后的 EventEnvelope 和 governed resource。 +- Multica 是 activation surface、provider hosting surface 和 OA 表达 surface。 +- `mnemon-multica-runtime` 是 Multica-hosted surface adapter/provider wrapper, + 不是 hub backend、scheduler、mailbox 或 projector。 +- 写回 Multica 的 comment/status/metadata 必须来自明确命令或 accepted event, + 不能通过解析 provider transcript 推断。 +- display-only 写回不能触发 provider 执行;只有 assignment、mention、 + activation carrier 或 mnemond-managed wake 这类 activation lane 可以触发。 + +## 2. R3 数据流 + +``` ++-------------------------+ +| MnemonHub / local input | +| accepted EventEnvelope | ++------------+------------+ + | + v ++-------------------------+ +| mnemond | +| policy + admission | +| canonical resources | ++------------+------------+ + | + | accepted activation intent + v ++-------------------------------+ +| mnemon-multica-runtime | +| surface adapter/provider wrap | +| - import Multica issue input | +| - submit observed event | +| - preserve provider stream | +| - run explicit writeback cmd | ++------+----------------+-------+ + | | + | activation | display-only writeback + v v ++-------------+ +----------------------------+ +| Multica task| | Multica OA surface | +| assignment | | issue status/comment/meta | +| @agent | | run messages, child issues | +| wake carrier| | no provider trigger by self | ++------+------+ +-------------+--------------+ + | ^ + v | ++-------------+ | +| Provider | | +| Codex/Kimi | | +| native flow |-----------------+ ++-------------+ explicit commands: + mnemon observe / surface-report +``` + +### Activation lane + +Activation lane 决定是否拉起 provider: + +- 人在 Multica 中 assign issue 或 `@agent`。 +- mnemond-managed source 产生 `[mnemon:wake]`,再通过 Multica task 承载。 +- accepted event 被转换成 activation carrier,carrier issue/comment 再触发 + Multica 原生 task。 + +Activation lane 可以使用 Multica 原生 `@agent` 语义,但这个事实不改变 +Mnemon source 语义:`[mnemon:wake]` 仍属于 mnemond-managed source, +不是 adapter 自己定义的协议。 + +### Display lane + +Display lane 只补全 OA 表达: + +- 更新 issue status。 +- 写入 final/comment feedback。 +- 写入 Mnemon surface metadata,如 `mnemon.surface_role=display`、 + `mnemon.event_ref`、`mnemon.resource_ref`。 +- 记录 artifact/evidence refs。 + +Display lane 不创建 provider task,不追加 `@agent` mention,不改变 Mnemon +canonical state。它只是把已经接纳的状态投影到 Multica UI。 + +## 3. Live 验收脚本 + +三个脚本用于复现实机验证: + +- `scripts/multica_r3_live_prepare.sh` + - 构建 `mnemon-acceptance`、`mnemon-harness`、`mnemon-multica-runtime`、 + `mnemond`。 + - 检查 Multica daemon/runtime 在线。 + - 根据 `.mnemon/harness/multica/registry.json` 同步 5 个 agent。 + - 为 agent 设置 provider wrapper 命令和 mnemond token/env。 +- `scripts/multica_r3_live_provider.sh` + - 作为验收用 deterministic provider wrapper。 + - 使用真实 Multica task/run/comment/status/child issue 链路。 + - 通过 `mnemon-harness multica activation-carrier` 触发 child work。 + - 通过 `mnemon-harness multica surface-report` 写回 display-only OA 结果。 +- `scripts/multica_r3_live_acceptance.sh` + - 顺序运行中文 case。 + - 默认开启 `--require-surface-flow`,要求 root/child 都有 run messages、 + comments、status 和 active agent 证据。 + +准备环境: + +```sh +MNEMON_MULTICA_WORKSPACE_ID= \ +scripts/multica_r3_live_prepare.sh +``` + +运行默认 live case: + +```sh +MNEMON_MULTICA_WORKSPACE_ID= \ +scripts/multica_r3_live_acceptance.sh +``` + +单独运行 overlap case: + +```sh +MNEMON_MULTICA_WORKSPACE_ID= \ +scripts/multica_r3_live_acceptance.sh parallel-poc-overlap +``` + +## 4. 中文复杂 case + +### r3-surface-readiness + +目标是验证最小 R3 surface 链路: + +- root issue 进入 planner。 +- planner 创建 3 个 child carrier: + - `surface-metadata-check` + - `provider-run-visibility-check` + - `activation-carrier-follow-up` +- child agents 分别写回 comment/status/metadata。 +- 验收要求至少 3 个 active agents,root 和 child 都必须有 run messages。 + +### protocol-react-drill + +目标是验证多轮 ReAct 风格协作: + +- Observe:检查 root surface metadata 与 provider routing。 +- Act:检查 OA writeback 不触发 provider。 +- Reflect:integrator 整合残余风险。 +- 验收要求 4 个 child issues、4 个 child terminal runs、全 5 个角色活跃。 + +### parallel-poc-overlap + +目标是模拟多个 PoC 同时推进并复用上下文: + +- `poc-runtime-routing` +- `poc-operator-runbook` +- `poc-release-risk` +- `follow-up-context-reuse` + +前三个 PoC 共享 `ctx:evidence-ledger`、`ctx:provider-contract`、 +`ctx:risk-register` 等上下文。follow-up issue 必须复用共享上下文并整合第一轮反馈。 +验收要求 4 个 child issues、4 个 terminal child runs、全 5 个角色活跃。 + +## 5. 2026-07-01 验收记录 + +本轮在 Multica workspace `0925fd3e-ca35-4cb0-9bee-7d53070b7988` +和 profile `desktop-api.multica.ai` 上完成。 + +``` ++-----------------------+----------+---------+------------+---------------+ +| case | root | child | active | report | ++-----------------------+----------+---------+------------+---------------+ +| r3-surface-readiness | TEA-199 | 3 | 4 agents | status=ok | +| protocol-react-drill | TEA-203 | 4 | 5 agents | status=ok | +| parallel-poc-overlap | TEA-208 | 4 | 5 agents | status=ok | ++-----------------------+----------+---------+------------+---------------+ +``` + +报告路径: + +- `/tmp/mnemon-r3-multica-live/r3-surface-readiness-20260701T013810Z/acceptance-report.json` +- `/tmp/mnemon-r3-multica-live/protocol-react-drill-20260701T014050Z/acceptance-report.json` +- `/tmp/mnemon-r3-multica-live/parallel-poc-overlap-20260701T014209Z/acceptance-report.json` + +关键证据: + +- root runs 均包含 `text`、`tool_use`、`tool_result` message。 +- root comments 均可读,并且由 surface-report 写入。 +- child issues 均达到 `done`。 +- child runs 均有 terminal feedback comments。 +- metadata 使用 R3 keys:`mnemon.event_ref`、`mnemon.resource_ref`、 + `mnemon.surface_ref`、`mnemon.surface_role=display`。 +- 未使用 legacy hub/mailbox metadata。 + +## 6. 仍需保持的边界 + +live provider wrapper 是验收工具,不代表最终产品形态。最终产品实现应该继续向 +Multica 原生 runtime 靠拢: + +- 优先透明代理 provider 原生协议流。 +- 不复制 Codex/Kimi/Claude CLI 的完整输入输出结构。 +- 不把 Multica issue/comment 当 canonical Mnemon state。 +- 不通过 display-only projection 触发执行。 +- 不从自由文本 transcript 中反推 Mnemon event。 + +只要这些边界保持,Multica UI 可以充分承担 OA 体验:issue 看板、状态、child issue、 +run messages、comments、mentions、inbox 和 agent runtime 配置都可以被充分使用; +Mnemon 只保留事件治理和协作语义的权威位置。 diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go index f7ab41a6..b9e87c3c 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime.go @@ -152,6 +152,18 @@ type multicaRuntimeProdSimAssertion struct { Detail string `json:"detail,omitempty"` } +type multicaChildSurfaceSnapshot struct { + Children []driver.MulticaIssue + Comments map[string][]driver.MulticaComment + Runs map[string][]driver.MulticaIssueRun + Messages map[string][]driver.MulticaRunMessage + MessageTypes map[string]int + ActiveAgents []string + FeedbackCount int + TerminalRunCount int + MessageCount int +} + func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntimeProdSimOptions) (multicaRuntimeProdSimReport, error) { started := time.Now().UTC() if opts.Stdout == nil { @@ -295,24 +307,16 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime addMulticaProdSimAssertion(&report, "issue created through Multica", issue.ID != "", issue.ID) addMulticaProdSimAssertion(&report, "issue assigned to Mnemon participant", assignee.AgentID != "", assignee.Principal) runtimeEvidenceWait := opts.Wait - if opts.RequireSurfaceFlow && 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 { - if !opts.RequireSurfaceFlow || 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)) + addMulticaProdSimAssertion(&report, "Multica runtime produced run evidence", false, fmt.Sprintf("runs=%d messages=%d error=%v", len(runs), len(messages), err)) + return finishMulticaRuntimeProdSimReport(report, err) } combined := combinedMulticaRunMessages(messages) - 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))) - } + addMulticaProdSimAssertion(&report, "Multica runtime produced run evidence", len(runs) > 0 && len(messages) > 0, fmt.Sprintf("runs=%d messages=%d", len(runs), len(messages))) if strings.TrimSpace(combined) != "" { addMulticaProdSimAssertion(&report, "runtime output names Mnemon runtime", strings.Contains(combined, "Mnemon Multica runtime handled issue"), combined) if opts.RequireIngest { @@ -320,7 +324,7 @@ func runMulticaRuntimeProdSimAcceptance(ctx context.Context, opts multicaRuntime } } if opts.RequireSurfaceFlow { - rootRuntimeActivity := multicaMessagesExposeRuntimeActivity(report.MessageTypes) || len(runs) > 0 + rootRuntimeActivity := multicaMessagesExposeRuntimeActivity(report.MessageTypes) addMulticaProdSimAssertion(&report, "root run exposes rich Multica activity", rootRuntimeActivity, fmt.Sprintf("types=%+v runs=%d", report.MessageTypes, len(runs))) if err := collectMulticaSurfaceFlowEvidence(ctx, cli, opts, &report); err != nil { return finishMulticaRuntimeProdSimReport(report, err) @@ -445,8 +449,25 @@ func collectMulticaSurfaceFlowEvidence(ctx context.Context, cli driver.MulticaCL return err } addMulticaProdSimAssertion(report, "surface-flow root visibility readable", latest.ID != "", fmt.Sprintf("status=%s comments=%d", latest.Status, len(comments))) - if len(comments) > 0 { - addMulticaProdSimAssertion(report, "surface-flow comments carry OA output", multicaCommentsContainFeedbackMarker(comments) || multicaCommentsContainMnemonUpdate(comments), fmt.Sprintf("comments=%d", len(comments))) + visibleOA := multicaCommentsContainFeedbackMarker(comments) || + multicaCommentsContainMnemonUpdate(comments) || + (len(report.RunMessages) > 0 && multicaCommentsContainVisibleOAOutput(comments)) + addMulticaProdSimAssertion(report, "surface-flow comments carry OA output", visibleOA, fmt.Sprintf("comments=%d run_messages=%d", len(comments), len(report.RunMessages))) + if report.TaskExpectations.MinChildSurfaces > 0 || report.TaskExpectations.MinFeedbackComments > 0 || report.TaskExpectations.MinActiveAgents > 0 { + child, err := waitMulticaChildSurfaceEvidence(ctx, cli, report.Issue.ID, report.Runs, report.TaskExpectations, opts.Wait, opts.Poll) + report.ChildIssues = child.Children + report.ChildComments = child.Comments + report.ChildRuns = child.Runs + report.ChildMessages = child.Messages + report.ChildMessageTypes = child.MessageTypes + report.ActiveAgents = child.ActiveAgents + addMulticaProdSimAssertion(report, "surface-flow child issues match teamwork plan", len(child.Children) >= report.TaskExpectations.MinChildSurfaces, fmt.Sprintf("children=%d min=%d", len(child.Children), report.TaskExpectations.MinChildSurfaces)) + addMulticaProdSimAssertion(report, "surface-flow child runs reached terminal feedback", child.TerminalRunCount >= report.TaskExpectations.MinChildSurfaces && child.MessageCount >= report.TaskExpectations.MinChildSurfaces, fmt.Sprintf("terminal_runs=%d messages=%d min=%d", child.TerminalRunCount, child.MessageCount, report.TaskExpectations.MinChildSurfaces)) + addMulticaProdSimAssertion(report, "surface-flow feedback comments meet case expectation", child.FeedbackCount >= report.TaskExpectations.MinFeedbackComments, fmt.Sprintf("comments=%d min=%d", child.FeedbackCount, report.TaskExpectations.MinFeedbackComments)) + addMulticaProdSimAssertion(report, "surface-flow active agents meet case expectation", len(child.ActiveAgents) >= report.TaskExpectations.MinActiveAgents, fmt.Sprintf("active_agents=%v min=%d", child.ActiveAgents, report.TaskExpectations.MinActiveAgents)) + if err != nil { + return err + } } return nil } @@ -475,6 +496,89 @@ func multicaProdSimPartialSnapshotDetail(reason, runErr, surfaceErr error, repor return strings.Join(parts, "; ") } +func waitMulticaChildSurfaceEvidence(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, rootRuns []driver.MulticaIssueRun, expect multicaAcceptanceTaskCaseExpectations, wait, poll time.Duration) (multicaChildSurfaceSnapshot, error) { + deadline := time.Now().Add(wait) + var last multicaChildSurfaceSnapshot + for { + snapshot, err := collectMulticaChildSurfaceSnapshot(ctx, cli, rootIssueID, rootRuns) + if err != nil { + return snapshot, err + } + last = snapshot + childOK := len(snapshot.Children) >= expect.MinChildSurfaces + feedbackOK := snapshot.FeedbackCount >= expect.MinFeedbackComments + activeOK := len(snapshot.ActiveAgents) >= expect.MinActiveAgents + runOK := snapshot.TerminalRunCount >= expect.MinChildSurfaces && snapshot.MessageCount >= expect.MinChildSurfaces + if childOK && feedbackOK && activeOK && runOK { + return snapshot, nil + } + if wait <= 0 || time.Now().After(deadline) { + return last, fmt.Errorf("timed out waiting for Multica child surface evidence: children=%d/%d comments=%d/%d active=%d/%d terminal_runs=%d messages=%d", + len(snapshot.Children), expect.MinChildSurfaces, + snapshot.FeedbackCount, expect.MinFeedbackComments, + len(snapshot.ActiveAgents), expect.MinActiveAgents, + snapshot.TerminalRunCount, snapshot.MessageCount) + } + select { + case <-ctx.Done(): + return last, ctx.Err() + case <-time.After(poll): + } + } +} + +func collectMulticaChildSurfaceSnapshot(ctx context.Context, cli driver.MulticaCLI, rootIssueID string, rootRuns []driver.MulticaIssueRun) (multicaChildSurfaceSnapshot, error) { + children, err := cli.ListIssueChildren(ctx, rootIssueID) + if err != nil { + return multicaChildSurfaceSnapshot{}, err + } + snapshot := multicaChildSurfaceSnapshot{ + Children: children, + Comments: map[string][]driver.MulticaComment{}, + Runs: map[string][]driver.MulticaIssueRun{}, + Messages: map[string][]driver.MulticaRunMessage{}, + MessageTypes: map[string]int{}, + } + active := multicaActiveAgentIDs(rootRuns) + for _, child := range children { + childID := strings.TrimSpace(child.ID) + if childID == "" { + continue + } + comments, err := cli.ListIssueComments(ctx, childID) + if err != nil { + return snapshot, err + } + snapshot.Comments[childID] = comments + snapshot.FeedbackCount += len(comments) + runs, err := cli.ListIssueRuns(ctx, childID) + if err != nil { + return snapshot, err + } + snapshot.Runs[childID] = runs + for _, run := range runs { + if strings.TrimSpace(run.AgentID) != "" { + active[run.AgentID] = true + } + if !multicaRunTerminal(run) { + continue + } + snapshot.TerminalRunCount++ + messages, err := cli.ListRunMessages(ctx, run.ID, childID) + if err != nil { + return snapshot, err + } + snapshot.Messages[childID] = append(snapshot.Messages[childID], messages...) + snapshot.MessageCount += len(messages) + for typ, count := range multicaRunMessageTypeCounts(messages) { + snapshot.MessageTypes[typ] += count + } + } + } + snapshot.ActiveAgents = sortedMulticaActiveAgents(active) + return snapshot, nil +} + 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 @@ -614,6 +718,15 @@ func multicaCommentsContainFeedbackMarker(comments []driver.MulticaComment) bool return false } +func multicaCommentsContainVisibleOAOutput(comments []driver.MulticaComment) bool { + for _, comment := range comments { + if strings.TrimSpace(comment.Content) != "" { + return true + } + } + return false +} + func multicaIssueStatuses(issues []driver.MulticaIssue) map[string]string { out := map[string]string{} for _, issue := range issues { diff --git a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go index 6d721f73..514acf95 100644 --- a/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go +++ b/harness/cmd/mnemon-acceptance/acceptance_multica_runtime_test.go @@ -113,10 +113,17 @@ case "$*" in *"agent env get agent-"*) agent="${4:-}"; printf '{"agent_id":"%s","custom_env":{"MNEMON_MULTICA_PROVIDER_RUNTIME":"codex","MNEMON_MULTICA_PROVIDER_COMMAND":"codex","MNEMON_CONTROL_ADDR":"http://127.0.0.1:8787"}}\n' "$agent" ;; *"issue create"*) printf '{"id":"root-9","identifier":"TEA-9","title":"Runtime surface flow","description":"Teamwork acceptance","status":"todo"}\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. Multica surface input: observed.","created_at":"2026-06-28T09:00:01Z"},{"task_id":"task-root","issue_id":"root-9","seq":2,"type":"tool_use","content":"mnemon observe --principal planner@team","created_at":"2026-06-28T09:00:02Z"}]\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. Multica surface input: observed.","created_at":"2026-06-28T09:00:01Z"},{"task_id":"task-root","issue_id":"root-9","seq":2,"type":"tool_use","content":"mnemon 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=1","created_at":"2026-06-28T09:00:03Z"}]\n' ;; *"issue metadata list root-9"*) printf '[{"key":"mnemon.surface_role","value":"display"},{"key":"mnemon.event_ref","value":"event-root-9"},{"key":"mnemon.no_auto_dispatch","value":"true"}]\n' ;; + *"issue children root-9"*) printf '{"issues":[{"id":"child-a","identifier":"TEA-10","title":"子任务 A","status":"done"},{"id":"child-b","identifier":"TEA-11","title":"子任务 B","status":"done"}]}\n' ;; *"issue get root-9"*) printf '{"id":"root-9","identifier":"TEA-9","title":"Runtime surface flow","description":"Teamwork acceptance","status":"in_progress"}\n' ;; *"issue comment list root-9"*) printf '[{"id":"comment-root","issue_id":"root-9","content":"Mnemon 更新: 进展\\n\\n事件引用: event-root-9"}]\n' ;; + *"issue comment list child-a"*) printf '[{"id":"comment-a","issue_id":"child-a","content":"Mnemon update: assignment feedback\\nmnemon:event=event-a"}]\n' ;; + *"issue comment list child-b"*) printf '[{"id":"comment-b","issue_id":"child-b","content":"Mnemon update: assignment feedback\\nmnemon:event=event-b"}]\n' ;; + *"issue runs child-a"*) printf '[{"id":"task-a","issue_id":"child-a","agent_id":"agent-researcher","status":"completed","completed_at":"2026-06-28T09:01:00Z","workspace_id":"ws-1"}]\n' ;; + *"issue runs child-b"*) printf '[{"id":"task-b","issue_id":"child-b","agent_id":"agent-reviewer","status":"completed","completed_at":"2026-06-28T09:02:00Z","workspace_id":"ws-1"}]\n' ;; + *"issue run-messages task-a"*) printf '[{"task_id":"task-a","issue_id":"child-a","seq":1,"type":"tool_use","content":"surface-report","created_at":"2026-06-28T09:01:01Z"},{"task_id":"task-a","issue_id":"child-a","seq":2,"type":"tool_result","content":"reported","created_at":"2026-06-28T09:01:02Z"}]\n' ;; + *"issue run-messages task-b"*) printf '[{"task_id":"task-b","issue_id":"child-b","seq":1,"type":"tool_use","content":"surface-report","created_at":"2026-06-28T09:02:01Z"},{"task_id":"task-b","issue_id":"child-b","seq":2,"type":"tool_result","content":"reported","created_at":"2026-06-28T09:02:02Z"}]\n' ;; *) printf '{}\n' ;; esac ` diff --git a/harness/cmd/mnemon-multica-runtime/main.go b/harness/cmd/mnemon-multica-runtime/main.go index 257560b2..e47de007 100644 --- a/harness/cmd/mnemon-multica-runtime/main.go +++ b/harness/cmd/mnemon-multica-runtime/main.go @@ -424,7 +424,7 @@ func (s *runtimeRPCState) runProviderTurn(input multicasurface.RuntimeInput, imp defer cancel() cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command) cmd.Dir = cwd - cmd.Env = append([]string(nil), s.Env...) + cmd.Env = runtimeProviderEnv(s.Env, imported) cmd.Stdin = strings.NewReader(prompt) out, err := cmd.CombinedOutput() result.DurationMs = s.now().Sub(started).Milliseconds() @@ -448,6 +448,35 @@ func (s *runtimeRPCState) runProviderTurn(input multicasurface.RuntimeInput, imp return result } +func runtimeProviderEnv(env []string, imported runtimeImportResult) []string { + out := append([]string(nil), env...) + out = setRuntimeProviderEnv(out, "MULTICA_ISSUE_ID", imported.IssueID) + out = setRuntimeProviderEnv(out, "MULTICA_TASK_ID", imported.TaskID) + out = setRuntimeProviderEnv(out, "MNEMON_MULTICA_ISSUE_ID", imported.IssueID) + out = setRuntimeProviderEnv(out, "MNEMON_MULTICA_ISSUE_IDENTIFIER", imported.Identifier) + out = setRuntimeProviderEnv(out, "MNEMON_MULTICA_ISSUE_TITLE", imported.Title) + out = setRuntimeProviderEnv(out, "MNEMON_MULTICA_ISSUE_STATUS", imported.Status) + out = setRuntimeProviderEnv(out, "MNEMON_MULTICA_PRINCIPAL", imported.Principal) + return out +} + +func setRuntimeProviderEnv(env []string, key, value string) []string { + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + if key == "" || value == "" { + return env + } + next := make([]string, 0, len(env)+1) + prefix := key + "=" + for _, entry := range env { + if strings.HasPrefix(entry, prefix) { + continue + } + next = append(next, entry) + } + return append(next, key+"="+value) +} + func providerPrompt(input multicasurface.RuntimeInput, imported runtimeImportResult) string { if text := strings.TrimSpace(input.Text); text != "" { return text + "\n" diff --git a/harness/cmd/mnemon-multica-runtime/main_test.go b/harness/cmd/mnemon-multica-runtime/main_test.go index 1539aebc..a3e7650f 100644 --- a/harness/cmd/mnemon-multica-runtime/main_test.go +++ b/harness/cmd/mnemon-multica-runtime/main_test.go @@ -182,6 +182,44 @@ func TestRuntimeRunsProviderCommandWithOriginalTurnInput(t *testing.T) { } } +func TestRuntimeProviderEnvAddsImportedIssueContext(t *testing.T) { + env := runtimeProviderEnv([]string{ + "MULTICA_TASK_ID=daemon-task", + "MULTICA_AGENT_ID=agent-1", + }, runtimeImportResult{ + IssueID: "issue-imported", + Identifier: "TEA-42", + Title: "中文协作验收", + Principal: "planner@team", + TaskID: "task-imported", + Status: "recorded", + }) + got := map[string]string{} + counts := map[string]int{} + for _, entry := range env { + key, value, ok := strings.Cut(entry, "=") + if ok { + got[key] = value + counts[key]++ + } + } + for key, want := range map[string]string{ + "MULTICA_ISSUE_ID": "issue-imported", + "MULTICA_TASK_ID": "task-imported", + "MNEMON_MULTICA_ISSUE_IDENTIFIER": "TEA-42", + "MNEMON_MULTICA_ISSUE_TITLE": "中文协作验收", + "MNEMON_MULTICA_ISSUE_STATUS": "recorded", + "MNEMON_MULTICA_PRINCIPAL": "planner@team", + } { + if got[key] != want { + t.Fatalf("%s = %q, want %q (env=%v)", key, got[key], want, env) + } + if counts[key] != 1 { + t.Fatalf("%s appears %d times in env=%v", key, counts[key], env) + } + } +} + func TestProviderPromptFallsBackToIssueWhenTurnInputIsEmpty(t *testing.T) { got := providerPrompt(multicasurface.RuntimeInput{}, runtimeImportResult{ IssueID: "issue-1", diff --git a/harness/internal/driver/multica.go b/harness/internal/driver/multica.go index 2172b6c4..2eff4f1e 100644 --- a/harness/internal/driver/multica.go +++ b/harness/internal/driver/multica.go @@ -7,7 +7,11 @@ import ( "errors" "fmt" "io" + "net/http" + "net/url" + "os" "os/exec" + "path/filepath" "sort" "strings" "sync" @@ -288,9 +292,13 @@ func (c MulticaCLI) ListIssueComments(ctx context.Context, issueID string) ([]Mu if issueID == "" { return nil, fmt.Errorf("multica issue id is required") } - out, err := c.Run(ctx, []string{"issue", "comment", "list", issueID, "--full", "--output", "json"}, "") + out, err := c.Run(ctx, []string{"issue", "comment", "list", issueID, "--output", "json"}, "") if err != nil { - return nil, err + comments, apiErr := c.listIssueCommentsViaAPI(ctx, issueID) + if apiErr == nil { + return comments, nil + } + return nil, fmt.Errorf("%w; fallback Multica API issue comment list failed: %v", err, apiErr) } comments, err := decodeMulticaCommentListOutput([]byte(out.Stdout)) if err != nil { @@ -299,6 +307,18 @@ func (c MulticaCLI) ListIssueComments(ctx context.Context, issueID string) ([]Mu return comments, nil } +func (c MulticaCLI) listIssueCommentsViaAPI(ctx context.Context, issueID string) ([]MulticaComment, error) { + var raw any + if err := c.multicaAPIJSON(ctx, http.MethodGet, "/api/issues/"+issueID+"/comments", nil, &raw); err != nil { + return nil, err + } + data, err := json.Marshal(raw) + if err != nil { + return nil, err + } + return decodeMulticaCommentListOutput(data) +} + func (c MulticaCLI) ListRuntimeProfiles(ctx context.Context) ([]MulticaRuntimeProfile, error) { out, err := c.Run(ctx, []string{"runtime", "profile", "list", "--output", "json"}, "") if err != nil { @@ -526,7 +546,11 @@ func (c MulticaCLI) GetAgentEnv(ctx context.Context, agentID string) (map[string } out, err := c.Run(ctx, []string{"agent", "env", "get", agentID, "--output", "json"}, "") if err != nil { - return nil, err + env, apiErr := c.getAgentEnvViaAPI(ctx, agentID) + if apiErr == nil { + return env, nil + } + return nil, fmt.Errorf("%w; fallback Multica API agent env get failed: %v", err, apiErr) } env, err := decodeMulticaAgentEnv([]byte(out.Stdout)) if err != nil { @@ -549,7 +573,11 @@ func (c MulticaCLI) SetAgentEnv(ctx context.Context, agentID string, env map[str } out, err := c.Run(ctx, []string{"agent", "env", "set", agentID, "--custom-env-stdin", "--output", "json"}, string(data)) if err != nil { - return nil, err + updated, apiErr := c.setAgentEnvViaAPI(ctx, agentID, env) + if apiErr == nil { + return updated, nil + } + return nil, fmt.Errorf("%w; fallback Multica API agent env set failed: %v", err, apiErr) } if strings.TrimSpace(out.Stdout) == "" { return env, nil @@ -561,6 +589,156 @@ func (c MulticaCLI) SetAgentEnv(ctx context.Context, agentID string, env map[str return updated, nil } +type multicaCLIProfileConfig struct { + ServerURL string `json:"server_url"` + WorkspaceID string `json:"workspace_id"` + Token string `json:"token"` +} + +type multicaAgentEnvAPIResponse struct { + CustomEnv map[string]string `json:"custom_env"` + CustomEnvRedacted bool `json:"custom_env_redacted"` +} + +func (c MulticaCLI) getAgentEnvViaAPI(ctx context.Context, agentID string) (map[string]string, error) { + var agent multicaAgentEnvAPIResponse + if err := c.multicaAPIJSON(ctx, http.MethodGet, "/api/agents/"+agentID+"/env", nil, &agent); err == nil { + if agent.CustomEnv == nil { + return map[string]string{}, nil + } + return agent.CustomEnv, nil + } + if err := c.multicaAPIJSON(ctx, http.MethodGet, "/api/agents/"+agentID, nil, &agent); err != nil { + return nil, err + } + if agent.CustomEnv == nil { + return map[string]string{}, nil + } + return agent.CustomEnv, nil +} + +func (c MulticaCLI) setAgentEnvViaAPI(ctx context.Context, agentID string, env map[string]string) (map[string]string, error) { + var agent multicaAgentEnvAPIResponse + body := map[string]any{"custom_env": env} + if err := c.multicaAPIJSON(ctx, http.MethodPut, "/api/agents/"+agentID+"/env", body, &agent); err != nil { + if rawErr := c.multicaAPIJSON(ctx, http.MethodPut, "/api/agents/"+agentID+"/env", env, &agent); rawErr != nil { + return nil, err + } + } + if agent.CustomEnv == nil { + return map[string]string{}, nil + } + return agent.CustomEnv, nil +} + +func (c MulticaCLI) multicaAPIJSON(ctx context.Context, method, path string, body any, out any) error { + cfg, err := c.multicaAPIConfig() + if err != nil { + return err + } + if cfg.ServerURL == "" { + return fmt.Errorf("Multica server URL not configured") + } + if cfg.Token == "" { + return fmt.Errorf("Multica token not configured") + } + var reader io.Reader + if body != nil { + data, err := json.Marshal(body) + if err != nil { + return err + } + reader = bytes.NewReader(data) + } + req, err := http.NewRequestWithContext(ctx, method, strings.TrimRight(cfg.ServerURL, "/")+path, reader) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+cfg.Token) + if cfg.WorkspaceID != "" { + req.Header.Set("X-Workspace-ID", cfg.WorkspaceID) + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + timeout := c.Timeout + if timeout <= 0 { + timeout = 30 * time.Second + } + client := &http.Client{Timeout: timeout} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return fmt.Errorf("%s %s returned %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(data))) + } + if out == nil { + return nil + } + return json.NewDecoder(resp.Body).Decode(out) +} + +func (c MulticaCLI) multicaAPIConfig() (multicaCLIProfileConfig, error) { + cfg, err := loadMulticaCLIProfileConfig(strings.TrimSpace(c.Profile)) + if err != nil { + return multicaCLIProfileConfig{}, err + } + if value := strings.TrimSpace(c.ServerURL); value != "" { + cfg.ServerURL = value + } else if value := multicaEnvValue(c.Env, "MULTICA_SERVER_URL"); value != "" { + cfg.ServerURL = value + } + if value := strings.TrimSpace(c.WorkspaceID); value != "" { + cfg.WorkspaceID = value + } else if value := multicaEnvValue(c.Env, "MULTICA_WORKSPACE_ID"); value != "" { + cfg.WorkspaceID = value + } + if value := multicaEnvValue(c.Env, "MULTICA_TOKEN"); value != "" { + cfg.Token = value + } + return cfg, nil +} + +func loadMulticaCLIProfileConfig(profile string) (multicaCLIProfileConfig, error) { + home, err := os.UserHomeDir() + if err != nil { + return multicaCLIProfileConfig{}, err + } + path := filepath.Join(home, ".multica", "config.json") + if profile != "" { + path = filepath.Join(home, ".multica", "profiles", profile, "config.json") + } + data, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return multicaCLIProfileConfig{}, nil + } + return multicaCLIProfileConfig{}, fmt.Errorf("read Multica profile config: %w", err) + } + var cfg multicaCLIProfileConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return multicaCLIProfileConfig{}, fmt.Errorf("parse Multica profile config: %w", err) + } + return cfg, nil +} + +func multicaEnvValue(env []string, key string) string { + if len(env) == 0 { + return strings.TrimSpace(os.Getenv(key)) + } + prefix := key + "=" + var value string + for _, entry := range env { + if strings.HasPrefix(entry, prefix) { + value = strings.TrimPrefix(entry, prefix) + } + } + return strings.TrimSpace(value) +} + func (c MulticaCLI) CreateIssue(ctx context.Context, req MulticaCreateIssueRequest) (MulticaIssue, error) { if strings.TrimSpace(req.Title) == "" { return MulticaIssue{}, fmt.Errorf("multica issue title is required") @@ -591,7 +769,11 @@ func (c MulticaCLI) CreateIssue(ctx context.Context, req MulticaCreateIssueReque } out, err := c.Run(ctx, args, stdin) if err != nil { - return MulticaIssue{}, err + issue, apiErr := c.createIssueViaAPI(ctx, req) + if apiErr == nil { + return issue, nil + } + return MulticaIssue{}, fmt.Errorf("%w; fallback Multica API issue create failed: %v", err, apiErr) } var issue MulticaIssue if err := json.Unmarshal([]byte(out.Stdout), &issue); err != nil { @@ -600,6 +782,36 @@ func (c MulticaCLI) CreateIssue(ctx context.Context, req MulticaCreateIssueReque return issue, nil } +func (c MulticaCLI) createIssueViaAPI(ctx context.Context, req MulticaCreateIssueRequest) (MulticaIssue, error) { + body := map[string]any{ + "title": strings.TrimSpace(req.Title), + } + if strings.TrimSpace(req.Description) != "" { + description := req.Description + body["description"] = description + } + if strings.TrimSpace(req.AssigneeID) != "" { + assigneeType := "agent" + assigneeID := strings.TrimSpace(req.AssigneeID) + body["assignee_type"] = assigneeType + body["assignee_id"] = assigneeID + } + if strings.TrimSpace(req.ParentID) != "" { + body["parent_issue_id"] = strings.TrimSpace(req.ParentID) + } + if strings.TrimSpace(req.Status) != "" { + body["status"] = strings.TrimSpace(req.Status) + } + if strings.TrimSpace(req.Priority) != "" { + body["priority"] = strings.TrimSpace(req.Priority) + } + var issue MulticaIssue + if err := c.multicaAPIJSON(ctx, http.MethodPost, "/api/issues", body, &issue); err != nil { + return MulticaIssue{}, err + } + return issue, nil +} + func (c MulticaCLI) AssignIssue(ctx context.Context, issueID, assigneeID string) (MulticaIssue, error) { issueID = strings.TrimSpace(issueID) assigneeID = strings.TrimSpace(assigneeID) @@ -656,7 +868,14 @@ func (c MulticaCLI) SetIssueMetadata(ctx context.Context, issueID, key, value, v } args = append(args, "--output", "json") _, err := c.Run(ctx, args, "") - return err + if err != nil { + if apiErr := c.setIssueMetadataViaAPI(ctx, issueID, key, value, valueType); apiErr == nil { + return nil + } else { + return fmt.Errorf("%w; fallback Multica API issue metadata set failed: %v", err, apiErr) + } + } + return nil } func (c MulticaCLI) SetIssueMetadataMap(ctx context.Context, issueID string, values map[string]string) error { @@ -721,7 +940,12 @@ func (c MulticaCLI) GetIssueMetadata(ctx context.Context, issueID, key string) ( } out, err := c.Run(ctx, []string{"issue", "metadata", "get", issueID, "--key", key, "--output", "json"}, "") if err != nil { - return "", false, err + meta, apiErr := c.ListIssueMetadata(ctx, issueID) + if apiErr != nil { + return "", false, fmt.Errorf("%w; fallback Multica API issue metadata get failed: %v", err, apiErr) + } + value, ok := meta[key] + return value, ok, nil } meta, err := decodeMulticaMetadataOutput([]byte(out.Stdout)) if err != nil { @@ -738,11 +962,49 @@ func (c MulticaCLI) ListIssueMetadata(ctx context.Context, issueID string) (map[ } out, err := c.Run(ctx, []string{"issue", "metadata", "list", issueID, "--output", "json"}, "") if err != nil { - return nil, err + meta, apiErr := c.listIssueMetadataViaAPI(ctx, issueID) + if apiErr == nil { + return meta, nil + } + return nil, fmt.Errorf("%w; fallback Multica API issue metadata list failed: %v", err, apiErr) } return decodeMulticaMetadataOutput([]byte(out.Stdout)) } +func (c MulticaCLI) listIssueMetadataViaAPI(ctx context.Context, issueID string) (map[string]string, error) { + var raw any + if err := c.multicaAPIJSON(ctx, http.MethodGet, "/api/issues/"+issueID+"/metadata", nil, &raw); err == nil { + return NormalizeMulticaMetadata(raw), nil + } + issue, err := c.GetIssue(ctx, issueID) + if err != nil { + return nil, err + } + return NormalizeMulticaMetadata(issue.Metadata), nil +} + +func (c MulticaCLI) setIssueMetadataViaAPI(ctx context.Context, issueID, key, value, valueType string) error { + body := map[string]any{ + "key": key, + "value": value, + } + if strings.TrimSpace(valueType) != "" { + body["type"] = strings.TrimSpace(valueType) + } + if err := c.multicaAPIJSON(ctx, http.MethodPut, "/api/issues/"+issueID+"/metadata", body, nil); err == nil { + return nil + } + if err := c.multicaAPIJSON(ctx, http.MethodPost, "/api/issues/"+issueID+"/metadata", body, nil); err == nil { + return nil + } + keyPath := url.PathEscape(key) + valueBody := map[string]any{"value": value} + if strings.TrimSpace(valueType) != "" { + valueBody["type"] = strings.TrimSpace(valueType) + } + return c.multicaAPIJSON(ctx, http.MethodPut, "/api/issues/"+issueID+"/metadata/"+keyPath, valueBody, nil) +} + func (c MulticaCLI) LoadIssueMetadata(ctx context.Context, issue MulticaIssue) (MulticaIssue, int, error) { if strings.TrimSpace(issue.ID) == "" { return issue, 0, nil @@ -762,7 +1024,11 @@ func (c MulticaCLI) ListIssueChildren(ctx context.Context, parentID string) ([]M } out, err := c.Run(ctx, []string{"issue", "children", parentID, "--output", "json"}, "") if err != nil { - return nil, err + issues, apiErr := c.listIssueChildrenViaAPI(ctx, parentID) + if apiErr == nil { + return issues, nil + } + return nil, fmt.Errorf("%w; fallback Multica API issue children failed: %v", err, apiErr) } issues, err := decodeMulticaIssueListOutput([]byte(out.Stdout)) if err != nil { @@ -771,6 +1037,18 @@ func (c MulticaCLI) ListIssueChildren(ctx context.Context, parentID string) ([]M return issues, nil } +func (c MulticaCLI) listIssueChildrenViaAPI(ctx context.Context, parentID string) ([]MulticaIssue, error) { + var raw any + if err := c.multicaAPIJSON(ctx, http.MethodGet, "/api/issues/"+parentID+"/children", nil, &raw); err != nil { + return nil, err + } + data, err := json.Marshal(raw) + if err != nil { + return nil, err + } + return decodeMulticaIssueListOutput(data) +} + func (c MulticaCLI) ListIssueRuns(ctx context.Context, issueID string) ([]MulticaIssueRun, error) { issueID = strings.TrimSpace(issueID) if issueID == "" { @@ -793,9 +1071,6 @@ func (c MulticaCLI) ListRunMessages(ctx context.Context, taskID, issueID string) return nil, fmt.Errorf("multica task id is required") } args := []string{"issue", "run-messages", taskID, "--output", "json"} - if strings.TrimSpace(issueID) != "" { - args = append(args, "--issue", strings.TrimSpace(issueID)) - } out, err := c.Run(ctx, args, "") if err != nil { return nil, err diff --git a/harness/internal/driver/multica_test.go b/harness/internal/driver/multica_test.go index 849f463a..df5a3270 100644 --- a/harness/internal/driver/multica_test.go +++ b/harness/internal/driver/multica_test.go @@ -2,6 +2,10 @@ package driver import ( "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -146,6 +150,164 @@ esac } } +func TestMulticaCLIAgentEnvFallsBackToAPI(t *testing.T) { + tmp := t.TempDir() + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf 'Error: unknown command "env" for "multica agent"\n' >&2 +exit 1 +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + var sawPut bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer token-1" { + t.Fatalf("Authorization header = %q", r.Header.Get("Authorization")) + } + if r.Header.Get("X-Workspace-ID") != "ws-1" { + t.Fatalf("X-Workspace-ID header = %q", r.Header.Get("X-Workspace-ID")) + } + if r.URL.Path != "/api/agents/agent-1/env" { + t.Fatalf("path = %s", r.URL.Path) + } + switch r.Method { + case http.MethodGet: + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "agent-1", + "custom_env": map[string]string{"EXISTING": "yes"}, + }) + case http.MethodPut: + sawPut = true + var body struct { + CustomEnv map[string]string `json:"custom_env"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if body.CustomEnv["MNEMON_CONTROL_ADDR"] != "http://127.0.0.1:8787" { + t.Fatalf("custom_env body = %+v", body.CustomEnv) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "agent-1", + "custom_env": body.CustomEnv, + }) + default: + t.Fatalf("method = %s", r.Method) + } + })) + defer server.Close() + cli := MulticaCLI{ + Command: bin, + ServerURL: server.URL, + WorkspaceID: "ws-1", + Env: append(os.Environ(), + "MULTICA_TOKEN=token-1", + ), + } + env, err := cli.GetAgentEnv(context.Background(), "agent-1") + if err != nil { + t.Fatal(err) + } + if env["EXISTING"] != "yes" { + t.Fatalf("env = %+v", env) + } + updated, err := cli.SetAgentEnv(context.Background(), "agent-1", map[string]string{ + "MNEMON_CONTROL_ADDR": "http://127.0.0.1:8787", + }) + if err != nil { + t.Fatal(err) + } + if !sawPut || updated["MNEMON_CONTROL_ADDR"] != "http://127.0.0.1:8787" { + t.Fatalf("updated=%+v sawPut=%v", updated, sawPut) + } +} + +func TestMulticaCLICreateIssueFallsBackToAPI(t *testing.T) { + tmp := t.TempDir() + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf 'Error: unknown flag: --description-stdin\n' >&2 +exit 1 +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/api/issues" { + t.Fatalf("%s %s", r.Method, r.URL.Path) + } + var body map[string]any + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if body["title"] != "R3 中文 case" || body["description"] != "详细说明" || body["assignee_type"] != "agent" || body["assignee_id"] != "agent-1" { + t.Fatalf("body = %+v", body) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "issue-1", + "identifier": "TEA-1", + "title": body["title"], + "description": body["description"], + "status": "todo", + }) + })) + defer server.Close() + cli := MulticaCLI{ + Command: bin, + ServerURL: server.URL, + WorkspaceID: "ws-1", + Env: append(os.Environ(), + "MULTICA_TOKEN=token-1", + ), + } + issue, err := cli.CreateIssue(context.Background(), MulticaCreateIssueRequest{ + Title: "R3 中文 case", + Description: "详细说明", + AssigneeID: "agent-1", + }) + if err != nil { + t.Fatal(err) + } + if issue.ID != "issue-1" || issue.Identifier != "TEA-1" { + t.Fatalf("issue = %+v", issue) + } +} + +func TestMulticaCLIListIssueChildrenFallsBackToAPI(t *testing.T) { + tmp := t.TempDir() + bin := filepath.Join(tmp, "multica") + script := `#!/usr/bin/env sh +printf 'Error: unknown command "children" for "multica issue"\n' >&2 +exit 1 +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/api/issues/root-1/children" { + t.Fatalf("%s %s", r.Method, r.URL.Path) + } + fmt.Fprint(w, `{"issues":[{"id":"child-1","identifier":"TEA-2","title":"子任务","status":"done"}]}`) + })) + defer server.Close() + cli := MulticaCLI{ + Command: bin, + ServerURL: server.URL, + WorkspaceID: "ws-1", + Env: append(os.Environ(), + "MULTICA_TOKEN=token-1", + ), + } + children, err := cli.ListIssueChildren(context.Background(), "root-1") + if err != nil { + t.Fatal(err) + } + if len(children) != 1 || children[0].ID != "child-1" || children[0].Status != "done" { + t.Fatalf("children = %+v", children) + } +} + func TestMulticaCLIProvisioningCommandsUseExpectedShapes(t *testing.T) { tmp := t.TempDir() argsPath := filepath.Join(tmp, "args.txt") diff --git a/scripts/multica_r3_live_acceptance.sh b/scripts/multica_r3_live_acceptance.sh new file mode 100755 index 00000000..da78de9e --- /dev/null +++ b/scripts/multica_r3_live_acceptance.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_ROOT="${MNEMON_PROJECT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +GOBIN="${GOBIN:-$(go env GOPATH)/bin}" +MULTICA_BIN="${MNEMON_MULTICA_BIN:-$GOBIN/multica}" +PROFILE="${MNEMON_MULTICA_PROFILE:-desktop-api.multica.ai}" +REGISTRY="${MNEMON_MULTICA_REGISTRY:-$PROJECT_ROOT/.mnemon/harness/multica/registry.json}" +WORKSPACE_ID="${MNEMON_MULTICA_WORKSPACE_ID:-}" +RUN_ROOT_BASE="${MNEMON_MULTICA_LIVE_RUN_ROOT:-/tmp/mnemon-r3-multica-live}" +WAIT="${MNEMON_MULTICA_ACCEPTANCE_WAIT:-12m}" +POLL="${MNEMON_MULTICA_ACCEPTANCE_POLL:-10s}" +PREPARE="${MNEMON_MULTICA_ACCEPTANCE_PREPARE:-1}" + +if [[ -z "$WORKSPACE_ID" && -f "$REGISTRY" ]]; then + WORKSPACE_ID="$(jq -r '.workspace_id // ""' "$REGISTRY")" +fi +if [[ -z "$WORKSPACE_ID" ]]; then + echo "workspace id is required; set MNEMON_MULTICA_WORKSPACE_ID or update $REGISTRY" >&2 + exit 1 +fi + +if [[ "$PREPARE" == "1" ]]; then + "$PROJECT_ROOT/scripts/multica_r3_live_prepare.sh" +fi + +cases=("$@") +if [[ ${#cases[@]} -eq 0 ]]; then + cases=(r3-surface-readiness protocol-react-drill parallel-poc-overlap) +fi + +mkdir -p "$RUN_ROOT_BASE" +for case_id in "${cases[@]}"; do + stamp="$(date -u +%Y%m%dT%H%M%SZ)" + run_root="$RUN_ROOT_BASE/${case_id}-${stamp}" + echo "running $case_id -> $run_root" + "$GOBIN/mnemon-acceptance" multica-runtime-prod-sim \ + --multica-bin "$MULTICA_BIN" \ + --multica-profile "$PROFILE" \ + --multica-workspace-id "$WORKSPACE_ID" \ + --registry "$REGISTRY" \ + --task-case "$case_id" \ + --require-surface-flow \ + --min-participants 5 \ + --min-active-agents 3 \ + --wait "$WAIT" \ + --poll "$POLL" \ + --run-root "$run_root" +done + +echo "Multica R3 live acceptance complete" diff --git a/scripts/multica_r3_live_prepare.sh b/scripts/multica_r3_live_prepare.sh new file mode 100755 index 00000000..752e74c3 --- /dev/null +++ b/scripts/multica_r3_live_prepare.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_ROOT="${MNEMON_PROJECT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +GOBIN="${GOBIN:-$(go env GOPATH)/bin}" +MULTICA_SOURCE="${MULTICA_SOURCE:-/Users/grivn/go/src/github.com/multica-ai/multica}" +MULTICA_BIN="${MNEMON_MULTICA_BIN:-$GOBIN/multica}" +PROFILE="${MNEMON_MULTICA_PROFILE:-desktop-api.multica.ai}" +REGISTRY="${MNEMON_MULTICA_REGISTRY:-$PROJECT_ROOT/.mnemon/harness/multica/registry.json}" +WORKSPACE_ID="${MNEMON_MULTICA_WORKSPACE_ID:-}" +CONTROL_ADDR="${MNEMON_CONTROL_ADDR:-http://127.0.0.1:8787}" +PROVIDER_RUNTIME="${MNEMON_MULTICA_PROVIDER_RUNTIME:-codex}" +PROVIDER_COMMAND="${MNEMON_MULTICA_PROVIDER_COMMAND:-$PROJECT_ROOT/scripts/multica_r3_live_provider.sh}" +PROVIDER_TIMEOUT="${MNEMON_MULTICA_PROVIDER_TURN_TIMEOUT:-5m}" + +if [[ ! -f "$REGISTRY" ]]; then + echo "missing Multica registry: $REGISTRY" >&2 + exit 1 +fi + +if [[ -z "$WORKSPACE_ID" ]]; then + WORKSPACE_ID="$(jq -r '.workspace_id // ""' "$REGISTRY")" +fi +if [[ -z "$WORKSPACE_ID" ]]; then + echo "workspace id is required; set MNEMON_MULTICA_WORKSPACE_ID or update $REGISTRY" >&2 + exit 1 +fi + +if [[ ! -x "$MULTICA_BIN" ]]; then + if [[ ! -d "$MULTICA_SOURCE/server" ]]; then + echo "Multica CLI not found at $MULTICA_BIN and source not found at $MULTICA_SOURCE" >&2 + exit 1 + fi + (cd "$MULTICA_SOURCE/server" && go build -o "$MULTICA_BIN" ./cmd/multica) +fi + +mkdir -p "$GOBIN" +(cd "$PROJECT_ROOT" && go build -o "$GOBIN/mnemon-acceptance" ./harness/cmd/mnemon-acceptance) +(cd "$PROJECT_ROOT" && go build -o "$GOBIN/mnemon-harness" ./harness/cmd/mnemon-harness) +(cd "$PROJECT_ROOT" && go build -o "$GOBIN/mnemon-multica-runtime" ./harness/cmd/mnemon-multica-runtime) +(cd "$PROJECT_ROOT" && go build -o "$GOBIN/mnemond" ./harness/cmd/mnemond) + +HARNESS_BIN="${MNEMON_HARNESS_BIN:-$GOBIN/mnemon-harness}" + +"$MULTICA_BIN" --profile "$PROFILE" --workspace-id "$WORKSPACE_ID" daemon status --output json >/dev/null +"$MULTICA_BIN" --profile "$PROFILE" --workspace-id "$WORKSPACE_ID" runtime list --output json | jq -e --arg rid "$(jq -r '.runtime_id // ""' "$REGISTRY")" 'map(select(.id == $rid and .status == "online")) | length > 0' >/dev/null + +runtime_id="$(jq -r '.runtime_id // ""' "$REGISTRY")" +if [[ -z "$runtime_id" ]]; then + echo "registry missing runtime_id: $REGISTRY" >&2 + exit 1 +fi + +while IFS= read -r participant; do + principal="$(jq -r '.principal // ""' <<<"$participant")" + agent_id="$(jq -r '.multica_agent_id // ""' <<<"$participant")" + agent_name="$(jq -r '.multica_agent_name // ""' <<<"$participant")" + role="$(jq -r '.role // ""' <<<"$participant")" + if [[ -z "$principal" || -z "$agent_id" ]]; then + echo "skipping incomplete participant: $participant" >&2 + continue + fi + token_name="${principal//@/-}" + token_file="$PROJECT_ROOT/.mnemon/harness/channel/credentials/${token_name}.token" + if [[ ! -f "$token_file" ]]; then + echo "missing mnemond token file for $principal: $token_file" >&2 + exit 1 + fi + "$HARNESS_BIN" multica \ + --multica-bin "$MULTICA_BIN" \ + --multica-profile "$PROFILE" \ + --multica-workspace-id "$WORKSPACE_ID" \ + --json \ + participant register \ + --registry "$REGISTRY" \ + --project-root "$PROJECT_ROOT" \ + --agent-id "$agent_id" \ + --agent-name "$agent_name" \ + --principal "$principal" \ + --role "$role" \ + --runtime-id "$runtime_id" \ + --sync-agent \ + --mnemon-control-addr "$CONTROL_ADDR" \ + --mnemon-control-token-file "$token_file" \ + --harness-bin "$HARNESS_BIN" \ + --provider-runtime "$PROVIDER_RUNTIME" \ + --provider-command "$PROVIDER_COMMAND" \ + --provider-workspace "$PROJECT_ROOT" \ + --provider-turn-timeout "$PROVIDER_TIMEOUT" >/dev/null + echo "prepared $principal -> $agent_name" +done < <(jq -c '.participants[]' "$REGISTRY") + +echo "Multica R3 live environment ready" +echo " workspace: $WORKSPACE_ID" +echo " registry: $REGISTRY" +echo " runtime: $runtime_id" +echo " provider: $PROVIDER_COMMAND" diff --git a/scripts/multica_r3_live_provider.sh b/scripts/multica_r3_live_provider.sh new file mode 100755 index 00000000..af3ba654 --- /dev/null +++ b/scripts/multica_r3_live_provider.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROMPT="$(cat)" +ISSUE_ID="${MULTICA_ISSUE_ID:-}" +if [[ -z "$ISSUE_ID" ]]; then + echo "Mnemon Multica runtime handled issue unknown. Multica surface input: observed. Missing MULTICA_ISSUE_ID." >&2 + exit 1 +fi + +MULTICA_BIN="${MNEMON_MULTICA_BIN:-multica}" +MULTICA_PROFILE="${MNEMON_MULTICA_PROFILE:-}" +WORKSPACE_ID="${MNEMON_MULTICA_WORKSPACE_ID:-${MULTICA_WORKSPACE_ID:-}}" +HARNESS_BIN="${MNEMON_HARNESS_BIN:-mnemon-harness}" +REGISTRY="${MNEMON_MULTICA_REGISTRY:-.mnemon/harness/multica/registry.json}" +PRINCIPAL="${MNEMON_CONTROL_PRINCIPAL:-${MULTICA_AGENT_NAME:-multica-agent}}" +AGENT_ID="${MULTICA_AGENT_ID:-}" + +mcli() { + local args=() + if [[ -n "$MULTICA_PROFILE" ]]; then + args+=(--profile "$MULTICA_PROFILE") + fi + if [[ -n "$WORKSPACE_ID" ]]; then + args+=(--workspace-id "$WORKSPACE_ID") + fi + "$MULTICA_BIN" "${args[@]}" "$@" +} + +mh_multica() { + local args=(multica) + if [[ -n "$MULTICA_BIN" ]]; then + args+=(--multica-bin "$MULTICA_BIN") + fi + if [[ -n "$MULTICA_PROFILE" ]]; then + args+=(--multica-profile "$MULTICA_PROFILE") + fi + if [[ -n "$WORKSPACE_ID" ]]; then + args+=(--multica-workspace-id "$WORKSPACE_ID") + fi + args+=(--json) + "$HARNESS_BIN" "${args[@]}" "$@" +} + +agent_id_for() { + local principal="$1" + if [[ ! -f "$REGISTRY" ]]; then + return 0 + fi + jq -r --arg principal "$principal" '.participants[]? | select(.principal == $principal) | .multica_agent_id // empty' "$REGISTRY" | head -1 +} + +safe_segment() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9._-' '-' | sed 's/^-//; s/-$//' +} + +issue_json="$(mcli issue get "$ISSUE_ID" --output json)" +IDENTIFIER="$(jq -r '.identifier // .id // ""' <<<"$issue_json")" +TITLE="$(jq -r '.title // ""' <<<"$issue_json")" +DESCRIPTION="$(jq -r '.description // ""' <<<"$issue_json")" +PARENT_ID="$(jq -r '.parent_issue_id // ""' <<<"$issue_json")" +CASE_ROOT="$(printf '%s\n' "$DESCRIPTION" | sed -n 's/^- Case root: `\(.*\)`/\1/p' | head -1)" +if [[ -z "$CASE_ROOT" ]]; then + CASE_ROOT="${MNEMON_MULTICA_LIVE_RUN_ROOT:-/tmp/mnemon-r3-multica-live-provider}/${ISSUE_ID}" +fi +mkdir -p "$CASE_ROOT/evidence" "$CASE_ROOT/shared-context" + +write_artifact() { + local slug="$1" + local body="$2" + local path="$CASE_ROOT/evidence/${slug}.md" + printf '%s\n' "$body" > "$path" + printf '%s' "$path" +} + +surface_report() { + local issue_id="$1" + local slug="$2" + local summary="$3" + local status="$4" + local body="$5" + local artifact + artifact="$(write_artifact "$slug" "$body")" + printf '%s' "$body" | mh_multica surface-report \ + --issue-id "$issue_id" \ + --title "assignment feedback" \ + --status-label "$status" \ + --summary "$summary" \ + --desired-status "$status" \ + --event-ref "accepted:multica-live/${issue_id}/${slug}" \ + --resource-ref "resource:multica-live/${issue_id}" \ + --surface-ref "surface:multica/${issue_id}" \ + --source-artifact-ref "$artifact" \ + --evidence "ctx:evidence-ledger" \ + --evidence "ctx:provider-contract" \ + --artifact "$artifact" \ + --assignee-agent-id "$AGENT_ID" \ + --assigned-to-provider \ + --content-stdin >/dev/null +} + +create_carrier() { + local title="$1" + local target_principal="$2" + local slug="$3" + local body="$4" + local target_agent + target_agent="$(agent_id_for "$target_principal")" + if [[ -z "$target_agent" ]]; then + echo "missing target agent for ${target_principal}" >&2 + return 1 + fi + printf '%s' "$body" | mh_multica activation-carrier \ + --issue-id "$ISSUE_ID" \ + --title "$title" \ + --event-ref "accepted:multica-live/${ISSUE_ID}/${slug}" \ + --resource-ref "assignment:${slug}" \ + --target-agent-id "$target_agent" \ + --content-stdin >/dev/null +} + +root_case() { + local text="$TITLE"$'\n'"$DESCRIPTION" + if grep -qi "parallel poc" <<<"$text"; then + echo "parallel" + elif grep -qi "react" <<<"$text"; then + echo "react" + elif grep -qi "surface readiness" <<<"$text"; then + echo "surface" + else + echo "generic" + fi +} + +run_child_issue() { + local slug + slug="$(safe_segment "$TITLE")" + if [[ -z "$slug" ]]; then + slug="child-feedback" + fi + local body + body="中文反馈: + +- 执行者: ${PRINCIPAL} +- Issue: ${IDENTIFIER} ${ISSUE_ID} +- 复用上下文: ctx:evidence-ledger, ctx:provider-contract +- 证据产物: ${CASE_ROOT}/evidence/${slug}.md +- 结论: 该子任务已经通过 Multica 原生 runtime 调度进入 mnemon-multica-runtime,并通过显式 surface-report 写回 OA comment/status。 +- 边界: 本 comment 是 display-only 写回,不触发新的 provider 执行;执行需要 activation carrier。 + +mnemon:event=accepted:multica-live/${ISSUE_ID}/${slug} +" + surface_report "$ISSUE_ID" "$slug" "子任务 ${slug} 完成,已留下共享上下文和证据。" "done" "$body" +} + +run_root_issue() { + local kind="$1" + case "$kind" in + parallel) + create_carrier "poc-runtime-routing" "researcher@team" "poc-runtime-routing" "验证 provider wrapper routing 与 surface correlation。必须引用 ctx:surface-map、ctx:provider-contract、ctx:evidence-ledger。" + create_carrier "poc-operator-runbook" "implementer@team" "poc-operator-runbook" "验证 operator runbook 与 rollback readiness。必须引用 ctx:provider-contract、ctx:risk-register、ctx:evidence-ledger。" + create_carrier "poc-release-risk" "reviewer@team" "poc-release-risk" "验证 release risk 与 status writeback。必须引用 ctx:session-map、ctx:risk-register、ctx:evidence-ledger。" + create_carrier "follow-up-context-reuse" "integrator@team" "follow-up-context-reuse" "第二轮 follow-up:复用至少两个 shared contexts,综合第一轮 PoC 反馈并给出最终风险判断。" + ;; + react) + create_carrier "observe-surface-metadata" "researcher@team" "observe-surface-metadata" "Round 1 Observe:检查 root surface metadata 与 run correlation。" + create_carrier "observe-provider-routing" "implementer@team" "observe-provider-routing" "Round 1 Observe:检查 provider wrapper run visibility。" + create_carrier "act-oa-writeback" "reviewer@team" "act-oa-writeback" "Round 2 Act:验证 OA status/comment writeback 是否不触发 provider。" + create_carrier "reflect-decision" "integrator@team" "reflect-decision" "Round 3 Reflect:整合 observe/act 证据并给出 residual risk。" + ;; + surface|generic) + create_carrier "surface-metadata-check" "researcher@team" "surface-metadata-check" "检查 root surface metadata,不允许 legacy hub/mailbox keys。" + create_carrier "provider-run-visibility-check" "reviewer@team" "provider-run-visibility-check" "检查 provider wrapper run messages、comments、status 可见性。" + create_carrier "activation-carrier-follow-up" "integrator@team" "activation-carrier-follow-up" "验证 activation carrier 携带 event_ref/resource_ref 后才触发执行。" + ;; + esac + + local root_body + root_body="根任务中文汇总: + +- Root issue: ${IDENTIFIER} ${ISSUE_ID} +- Case: ${kind} +- 已创建 activation carriers,carrier 才负责触发后续 provider 执行。 +- 已通过 surface-report 写回 display-only OA 状态,不把 Multica 当 canonical state。 +- 复用上下文: ctx:evidence-ledger, ctx:provider-contract, ctx:risk-register +- Provider wrapper: live deterministic wrapper,用于验证真实 Multica/mnemond/OA 链路;产品路径应继续贴近 Multica 原生 provider backend/app-server。 + +mnemon:event=accepted:multica-live/${ISSUE_ID}/root +" + surface_report "$ISSUE_ID" "root-${kind}-summary" "根任务已创建多角色 carrier 并写回 R3 surface 证据。" "done" "$root_body" +} + +if [[ -n "$PARENT_ID" && "$PARENT_ID" != "null" ]]; then + run_child_issue +else + run_root_issue "$(root_case)" +fi + +echo "Mnemon Multica runtime handled issue ${IDENTIFIER:-$ISSUE_ID}. Multica surface input: observed. Live provider wrapper completed ${TITLE} for ${PRINCIPAL}."